Second audit pass: fill gaps and correct drift, with verification where possible

Five read-only re-audits of the source material under a different lens -- not "is
it covered" but gap-filling, drift correction, and a user-assisted framing in
which the AI names the hook point or command and the human builds, runs, and
pastes back. The lens change earned its keep: the audits found drift that the
coverage lens could not, including two errors in what this repository had itself
absorbed.

Drift corrected, worst first:

- native-and-so.md: the absorbed syscall example loaded #0x47 (71, sendfile) --
  the source article's "real libc fragment" mislabels its own bytes, and the
  absorption inherited it. Now #0x40 with the mislabel named, the bionic wrapper
  epilogue (cmn x0,#0x1,lsl#12; b.cs) added as the discriminator between libc's
  own svc sites and a protector's inline ones, and the "no X8 load means data"
  claim softened to match svc_scan.py's nr=? semantics.
- system-ipc-audit.md: the generated-proxy example named installPackage, dead on
  modern IPackageManager -- swapped for getPackageInfo (already this skill's own
  hook) and IPackageInstallerSession.commit. Transaction-code guidance now says
  read the TRANSACTION_* constants, do not count the .aidl. getCallingUid gains
  its app-process reading semantics (own uid; uid 1000 on relayed callbacks --
  the "the system is calling my app" misread). service call gains the full type
  tags, the automatic interface token, and the default-binder-context boundary.
  The binderfs path variant and binder_calls_stats enable are recorded.
- packers.md: "dlopen not found = your bug" was one cause wearing three faces --
  now the namespace/DT_NEEDED/ABI triage with the classloader-namespace logcat
  discriminator and the 7.0+/8.0 gates; plus the extractNativeLibs="false"
  caveat on add-a-file tests, matching what so_constpatch.py already refuses.
- lsposed-and-modules.md: LSPatch paragraph re-labeled per evidence -- the
  delivery-class reasoning stays repo reasoning, the absorbed facts now carry a
  network-verified marker (upstream README: dex+so insertion, rootless, min
  Android 9; LSPosed active as of 2026-10-14). The api:100 parenthetical no
  longer implies a rewrite requirement.
- dynamic-frida.md: the LD_PRELOAD row gains its version gates (7.0+, Treble 8.0).

Gaps filled: signed-vs-unsigned branch suffixes and PAC/BTI recognition with the
SIGILL-on-patch consequence (native-and-so); the on-disk oat/vdex artifacts
check with the InMemory and quickened-opcodes gates, cross-linked from the
"decoder you did not write" rule (advanced-unpacking); the hook-point map --
funnel vs hop, ATMS/install/session gates stated (system-ipc-audit); the
interactive gdbserver escape hatch in detection Step 2, with the
frida-specific-vs-tracer-generic discrimination, and the user-session command
core in the toolchain row; the Hook vocabulary table translating instructions
into MethodHookParam calls, javadoc-verified, closing the one point where the
user-assisted loop forced a repo exit (lsposed-and-modules); cross-links from
the neutralization and TLS hook-body tables to their delivery vehicles.

Gates re-run: routing coherent, section references zero dangling and zero
warnings, repo clean, pytest 751 passed / 194 skipped / 25 xfailed.
This commit is contained in:
dabao1955
2026-10-19 21:32:41 +08:00
parent 8c00c88035
commit 2bb54a72fb
10 changed files with 138 additions and 35 deletions
@@ -201,6 +201,19 @@ failure surfaces later as a dex that half-parses. Walk the handler list with
**inferred** from repair-tool post-mortems, and it is the first place to look when a
spliced dex parses worse than the skeleton did.
## What is already on disk, before you spend a process window
Before any dump session, check what the installer's optimizer already left on disk — the
artifacts differ by Android version (`oat`/`odex` alongside the dex historically, `vdex` carrying
the dex copies on later releases, `.art` for boot images), and a `vdex` next to the installed
package sometimes holds the app's dex in plain form, extractable with a version-matched tool and
no live process at all. Two gates on that shortcut: `InMemoryDexClassLoader` (API 26+) loads dex
that never existed as a file, so a shell or hot-update path leaves nothing on disk to find; and
quickened-era artifacts carry `invoke-virtual-quick`-class opcodes, which a plain decoder reads
as foreign instructions — decode with a tool matching that artifact generation before concluding
anything. *(unverified — absorbed from milksu.org notes, retrieved 2026-10-19; verify the on-disk
layout against the device in hand, it moved more than once across releases)*
## Why the classic hooks die on Android 12-16
The original FART-era hooks assumed method entry flows through
@@ -535,7 +548,11 @@ five clean instructions under `dexdump`.
So: **before any VMP verdict, decode the same file with `dexdump`,
`baksmali`/`smali`, or `jadx`, and quote the failure count that came from *it*.** A
rare-opcode histogram is a lead, not evidence. Never let a private table's desync
inform a classification — "my decoder stopped" is a fact about the decoder.
inform a classification — "my decoder stopped" is a fact about the decoder. And one
non-VMP source of genuinely foreign opcodes is version drift, not obfuscation: a dex
recovered from a quickened-era `odex`/`vdex` carries `invoke-virtual-quick`-class
opcodes — decode it with a version-matched tool (see §What is already on disk) before
reaching for the VMP verdict.
### What a static dex probe can and cannot decide
@@ -62,6 +62,12 @@ example, the app verifies something the user's device will not have — then the
statement of what is blocked, with the evidence. Do not ship a build whose only purpose is to defeat a
detector you were not asked about.
**One interactive escape hatch exists before B:** a human-driven gdbserver session (`references/toolchain.md`,
the GDB form in the Tier-2 row) — the AI names the commands, the user runs them and pastes the output.
It survives *frida-specific* detection (the Stage-1 measured mappings) while still dying to *tracer-generic*
detection (`TracerPid`) — so it discriminates the two families before you decide the check is unbeatable.
Observer-only: the session must not also modify state (Step 2B).
**Signal to stop escalating:** you have spent more effort on the analysis environment than on the
change the user asked for. That is the drift this skill treats as most expensive, in its runtime form
(`long-task-discipline.md` §the most expensive drift).
@@ -83,7 +89,10 @@ apply after the detector is named — never as a blind stack.)*
Scope the neutralization to the *named* check's class and method. A blanket hook on `Runtime.exec`
or `File.exists` fires on legitimate app behaviour too, and an observer that also intercepts is no
longer an observer (Step 2B's rule — run the watch and the block as separate loads).
longer an observer (Step 2B's rule — run the watch and the block as separate loads). Delivery
vehicle for these hook bodies: a Frida script (`references/dynamic-frida.md`) or a persistent
module (`references/lsposed-and-modules.md` §Hook vocabulary — its table translates each
row above into `MethodHookParam` calls).
## Step 3: locating the check — the order of search
@@ -488,7 +488,7 @@ runtime-grade information, and they are often enough to finish the job:
| **Static instrumentation** — load a gadget/small agent by modifying the APK (e.g. an injected `Application` wrapper, a gadget `.so` added to `lib/<abi>/` and wired from the manifest) | Hooks run from process start with no external attach, no `ptrace`, no server process on the device to be found | Must repack and re-sign; the build you observe is no longer byte-identical to the original, so **it cannot serve as the unmodified control** (`verification.md` §the control build rule) |
| **Static read of the answer** — the check's inputs are usually visible: the field it reads, the string it compares, the response field it trusts | Often enough to plan a patch without ever observing | You do not learn the live values; combine with `runtime-data.md` for local state |
| **Self-recorded evidence** — the app's own cached responses, logs, or on-disk state | Real runtime data with no injection at all (a HTTP cache, a pref, a JSON snapshot left behind by a normal launch) | Only what the app happened to persist; needs one clean launch and then a filesystem read |
| **`LD_PRELOAD`** — the classic libc-interposition route | function-level interception with no on-device agent | effectively closed on Android: zygote-forked apps do not inherit your environment, restricted processes drop it, and linker namespaces limit which `.so` paths load at all; the debuggable-build `wrap.sh` escape hatch is the one place it still works. *(inferred — absorbed from milksu.org notes, retrieved 2026-10-19)* |
| **`LD_PRELOAD`** — the classic libc-interposition route | function-level interception with no on-device agent | effectively closed on Android: zygote-forked apps do not inherit your environment, restricted processes drop it, and linker namespaces limit which `.so` paths load at all (namespaces: Android 7.0+, fully in force from 8.0/Treble — older targets are less constrained); the debuggable-build `wrap.sh` escape hatch is the one place it still works. *(inferred — absorbed from milksu.org notes, retrieved 2026-10-19)* |
The last one is worth remembering because it is the cheapest and it is easy to
forget: a normally-launched app often leaves the exact runtime data you wanted —
@@ -25,12 +25,13 @@ you enjoy more.
| **Repackaged APK** | a modified, re-signed APK | whenever the app runs | no — every update needs rework | highest: integrity, signature, packing, repack |
A fourth route exists and breaks the table's implicit "module ⇒ rooted device" line: **LSPatch**
patches the module *into the target APK*, producing a standalone build that runs the hook on an
unrooted phone (its API line is `io.github.libxposed:api:100`, the modern libxposed surface). It
is repack-class in delivery — the output is a modified, re-signed APK, with everything that means
for signature checks (`references/signature-derived-keys.md`) and control builds — but module-class
in authoring. *(unverified — absorbed from milksu.org notes, retrieved 2026-10-19; this repository
has not built one.)*
patches the module *into the target APK* — by its own README, "integrating Xposed API by inserting
dex and so into the target APK", rootless, minimum Android 9 *(network-verified against the
upstream READMEs, 2026-10-19; this repository has still not built one)*. It is repack-class in
delivery — the output is a modified, re-signed APK, with everything that means for signature checks
(`references/signature-derived-keys.md`) and control builds — but module-class in authoring.
Upstream maintenance status of both projects: LSPosed's repository was active as of 2026-10-14
(last push, repository not archived) — re-check before betting a deliverable on either.
Two API-surface notes from the same source, same label: the runtime **resource hook** exists —
`IXposedHookInitPackageResources` with `res.setReplacement(...)` / `hookLayout(...)` replaces
@@ -95,11 +96,33 @@ Four files. Nothing else is required — no `res/`, no launcher Activity, no per
| File | Why it is required |
|---|---|
| `AndroidManifest.xml` with `<meta-data android:name="xposedmodule" android:value="true"/>` | without this key the APK installs as an ordinary app and never appears in the module list |
| same manifest: `xposeddescription`, `xposedminversion` (`82`) | shown in the manager UI; `82` is the classic XposedBridge API level and is accepted by LSPosed |
| same manifest: `xposeddescription`, `xposedminversion` (`82`) | shown in the manager UI; `82` is the classic XposedBridge API level and is accepted by LSPosed (newer templates declare `93`; `82` is the conservative level this kit measured) |
| `assets/xposed_init` | one line: the fully qualified entry class. A typo here produces a module that loads and silently does nothing |
| the entry class implementing `IXposedHookLoadPackage` | the only class LSPosed instantiates for you |
`scripts/lsposed_scaffold.py` writes exactly this set, plus a README containing the build chain below,
## Hook vocabulary — translating an instruction into `MethodHookParam` calls
The AI designs the hook and names the class/method; the user writes the body. This is the table for
that hand-off — the scaffold demonstrates observation only, and every *modification* request maps to
one line below. *(API surface verified against api.xposed.info javadoc, 2026-10-19; the table's
shapes absorbed from milksu.org notes same date.)*
| The instruction | What the user writes (legacy API this kit builds with) |
|---|---|
| "log the arguments" | in `beforeHookedMethod`: `param.args[i]`, `param.thisObject` (null for static) |
| "change what it is called with" | assign `param.args[i] = …` in **before** — the original then runs with the new argument |
| "make it return X" | `param.setResult(X)` in **before** skips the original entirely; in **after** it overrides whatever ran |
| "make it fail instead" | `param.setThrowable(new IOException(…))` in before; `param.getThrowable()/hasThrowable()` in after reads what the original threw |
| "replace it entirely" | `XC_MethodReplacement.replaceHookedMethod` returns a value and the original never runs |
| "the class may not exist on this build" | `XposedHelpers.findClassIfExists(...)` returns null instead of throwing |
| "hook construction / read fields / hook everything" | `findAndHookConstructor`, `get/setObjectField`, `hookMethod` over `getDeclaredMethods()` |
Decision rule in one line: **`XC_MethodHook` = observe or modify around the original;
`XC_MethodReplacement` = the original never runs.** The modern `io.github.libxposed:api:100` surface
exists alongside the legacy `api:82` this kit builds with; whether a legacy-API module runs unchanged
under LSPatch is unverified — record it the first time that route is used.
so the project is reproducible without Gradle:
```sh
+28 -4
View File
@@ -176,20 +176,44 @@ tombstone frames, GDB `bt`, Frida `Thread.backtrace` — is walking.
constants arrive as `movz` + `movk` pairs — a 64-bit constant is several instructions long in the
listing, which is why byte-patching "the constant" garbles it unless every piece is edited.
`cbz`/`cbnz` (compare-with-zero branch) and `tbz`/`tbnz` (test-bit branch) are the compact
conditionals; `blr` is the indirect call the vtable shape ends with.
conditionals; `blr` is the indirect call the vtable shape ends with. The other half of the branch
population is `cmp` + a flag-tested suffix — `cmp x0, x1` is really `subs xzr, x0, x1`, and which
suffix follows decides the comparison kind: `b.eq`/`b.ne` (equal/not), `b.gt`/`b.lt`/`b.ge`/`b.le`
(**signed**, N/V flag logic), `b.hi`/`b.lo`/`b.hs`/`b.ls` (**unsigned**). Signed-vs-unsigned is the
reading error that flips which side of a licence/VIP check you think you are on: a length compared
with `b.hi` is unsigned, so the same branch with `b.gt` means the author expected negatives.
**PAC/BTI**, on armv8.3+/armv9 targets: a function bracketed by `paciasp`/`autiasp` (sign/verify
the return address in LR) or starting with a `bti` landing pad is normal modern codegen — but a
byte patch that **drops** the `paciasp`, or a redirect that lands **past** the `bti`, dies with
`SIGILL` when the branch arrives. Recognition only; presence is probeable from `/proc/cpuinfo`
features. *(inferred — standard ARM semantics, absorbed from milksu.org notes 2026-10-19; see
also `references/native-dbi-and-deobfuscation.md` on translator interactions)*
**Syscall sites**, for reading what `scripts/svc_scan.py` (see
`references/detection-and-anti-analysis.md`) finds: the number is loaded into `X8` and the call
is `svc #0` —
```asm
mov x8, #0x47 // 64-bit table: write 64, ioctl 29, exit 93, clone 220, execve 221
mov x8, #0x40 // 64-bit table: write 64, ioctl 29, exit 93, clone 220, execve 221
svc #0
cmn x0, #0x1, lsl #12 // bionic wrapper epilogue: is x0 in [-4096, -1]?
b.cs done // yes → it is an errno, not a return value
```
so a termination site reads as an immediate into `X8` followed by `svc`, with the argument
registers holding fd/status — and a byte scan that reports an `svc` with no `X8` load nearby is
reading data, not code.
registers holding fd/status — and bionic's own wrappers wear that `cmn x0, #0x1, lsl #12` +
`b.cs` epilogue (the negative-errno range check), which separates **libc's own** svc sites from a
protector's **inline** ones in a `svc_scan` histogram. A byte scan that reports an `svc` with no
nearby instruction setting `X8` to a resolvable immediate is most likely reading data — the
measured false-positive case — with the exception of a runtime-computed or table-loaded number,
which `svc_scan` reports as `nr=?`: an unreadable site, not a non-site. *(Correction note: the
source article's "real libc fragment" labels `mov x8, #0x47` as `<write>` — 0x47 is 71, sendfile;
the real write wrapper loads 0x40. Its own GDB trace and decimal table were right.)* And the same
immediate-reading applies to a protector's mapping stub: `x2 = #3` is
`PROT_READ|PROT_WRITE`, `x3 = #0x22` is `MAP_PRIVATE|MAP_ANONYMOUS` — an anonymous-private mapping
is where runtime-materialized libraries come from, so the payload never exists in the APK file
and only a live dump can reach it (`references/advanced-unpacking.md`).
## Cross-architecture notes
@@ -60,7 +60,7 @@ Enumerate these before you patch anything. Each has a different signature and a
|---|---|---|---|
| `kill(getpid(), SIGKILL)` | `Process N exited due to signal 9 (Killed)`, **no** exit code, **no** tombstone | no | patch the call site or make the `kill` stub return 0 |
| `exit()` / `_exit()` | process ends with an exit code, no signal | no | find the caller; usually one branch of a check |
| `abort()` | `signal 6 (SIGABRT)` + tombstone | yes | usually a genuine assertion — find what tripped it |
| `abort()` | `signal 6 (SIGABRT)` + tombstone | yes | usually a genuine assertion — find what tripped it. **Read the abort message first**: `stack corruption detected` + a `__stack_chk_fail` frame is the canary tripping — memory corruption, and when your own hook/patch is in the process, suspect your instrumentation before the shell; `FORTIFY:` and `Scudo` prefixes are libc/allocator hardening aborts of the same family |
| **Deliberate crash** (see §Deliberate-crash stubs) | `signal 11 (SIGSEGV)`, tiny `fault addr` such as `0x4`, `Cause: null pointer dereference` | yes | nop the faulting store |
| `tgkill` / `raise` | same as kill/abort | varies | often not imported at all — check the import table before assuming |
+5 -1
View File
@@ -107,7 +107,11 @@ grep -o '/[^ ]*\.so' /proc/<pid>/maps | sort -u
Libraries belonging to optional SDKs are frequently lazy: they exist, they may even be unpacked to disk,
and they are never mapped during the startup path you are trying to influence. Unpacked-but-unmapped is
the signature of a lazy SDK, not evidence that your file was rejected.
the signature of a lazy SDK, not evidence that your file was rejected. And with `extractNativeLibs="false"`
(uncompressed, mmap-from-APK installs) an added library is never extracted at all, so "drop a benign ELF
next to it and see if the shell reacts" can silently no-op — visible in the APK, never mapped.
`scripts/so_constpatch.py` refuses to run in that configuration for the same reason; treat the flag as
part of the boundary map before designing an add-a-file test.
Pick a host that is (a) present in `maps`, (b) exports a JNI entry point the runtime must call, and
(c) editable. A library that exports `JNI_OnLoad` and appears in `maps` is the strongest candidate,
@@ -27,14 +27,17 @@ The app's privileged actions (install, start components, clipboard, location) cr
boundary, and the *server* side re-checks identity there: `Binder.getCallingUid()/getCallingPid()`
is filled in by the binder driver from the calling task, **not** copied from the parcel, so it is
not spoofable from the client side (inferred from the driver's documented behaviour). Two practical
consequences: you cannot fake another app's UID by crafting a parcel, and when you hook a system
service's entry the UID you see is the truth about who called.
consequences: you cannot fake another app's UID by crafting a parcel — and read the value with the
right semantics for *where* you hooked. In the **app's own process** `getCallingUid()` is the app's
own uid except while serving an inbound transaction; a callback relayed by system_server after its
own `clearCallingIdentity()` arrives as **uid 1000** — the classic "the system is calling my app!"
misread, when it is a relayed callback carrying the server's identity.
**Transaction codes.** AIDL generates `TRANSACTION_<method> = FIRST_CALL_TRANSACTION + n` with
`FIRST_CALL_TRANSACTION = 1` and `n` the method's 0-based ordinal in the `.aidl` file (inferred
from the codegen contract). So "the 5th method in `IFoo.aidl`" is code `5`. Version drift changes
ordinals — recompute against the `.aidl` or the generated Stub of the Android release you are on
before trusting a numeric code.
`FIRST_CALL_TRANSACTION = 1` (inferred from the codegen contract) — but **read the constants, do not count the `.aidl`**: classic codegen ordering and the on-device generated Stub are the ground
truth, and they only coincide with the AOSP-tag `.aidl` you happen to be reading (OEMs patch
them; stable AIDL freezes ordinals append-only, framework interfaces move every release). The
one-line runtime read: `Java.use("<iface>$Stub").TRANSACTION_<method>.value`.
**Observing outbound traffic (client side, in the target app's process):**
@@ -53,27 +56,46 @@ Java.perform(function() {
"which privileged call, with which arguments" record that static analysis of the app alone cannot
give you when the call site is behind a SDK facade. For inbound traffic (something calling *into*
the app — an exported service, a callback interface), hook `android.os.Binder.execTransact` the
same way. When the interface is known, hooking the **generated proxy method instead of the raw
transact** gives you decoded Java arguments — `Java.use("android.content.pm.IPackageManager$Stub$Proxy").installPackage`
and friends — no parcel parsing. The raw hook tells you *that* a call crossed; the proxy hook tells
you *what it meant*.
same way (signature `(int, long, long, int)` on O+). When the interface is known, hooking the
**generated proxy method instead of the raw transact** gives you decoded Java arguments — a live
example is `Java.use("android.content.pm.IPackageManager$Stub$Proxy").getPackageInfo`, already
this skill's own hook target in `references/detection-and-anti-analysis.md`; installs transit
`IPackageInstallerSession$Stub$Proxy.commit`. No parcel parsing. The raw hook tells you *that* a
call crossed; the proxy hook tells you *what it meant*.
*(Provenance note: the source article's version of the outbound script hooks
`android.os.Binder.transact` and calls `getCallingUid()` inside it — server-side semantics in a
client-side hook. The form here is corrected accordingly; only its provenance is external.)*
Root-level state, for the "why does my hook never fire" question: `adb shell su -c 'cat
/sys/kernel/debug/binder/proc/<pid>'` lists the process's binder threads, the nodes (objects it
serves) and the refs (objects it holds) — zero nodes means the process never registered the service
you are trying to reach, and no client-side hook will ever fire. *(unverified — absorbed from
milksu.org notes, retrieved 2026-10-19)*
/sys/kernel/debug/binder/proc/<pid>'` (on binderfs/GKI kernels, ~Android 12+, the same log lives
under `/sys/kernel/debug/binderfs/binder_logs/`) lists the process's binder threads, the nodes
(objects it serves) and the refs (objects it holds) — zero nodes means the process never
registered the service you are trying to reach, and no client-side hook will ever fire. *(unverified
— absorbed from milksu.org notes, retrieved 2026-10-19)*
**Hook-point map** — hook the funnel, not the hop; funnel names outlive every hop rename below.
*(unverified — absorbed from milksu.org notes, retrieved 2026-10-19)*
| Reversing question | The funnel (app-side, stable) | The hop it actually transits | Verify with |
|---|---|---|---|
| app starts a component | `Activity#startActivity` / `ContextImpl#startService` | `IActivityManager`, then **ATMS** — `ActivityTaskManager` facade since Android 10, `IActivityTaskManager` interface since 11 | `dumpsys activity` |
| app installs/updates itself | `PackageInstaller#Session#commit` | `IPackageInstallerSession` binder from the `createSession` **reply** — not from ServiceManager, so `service call` cannot reach it | hook-or-nothing by construction |
| package info / signature query | `PackageManager#getPackageInfo` | `IPackageManager$Stub$Proxy#getPackageInfo` (`GET_SIGNATURES` 64 deprecated P+ → `GET_SIGNING_CERTIFICATES`) | `dumpsys package <pkg>` |
| permission state | `ContextImpl#checkSelfPermission` | server side split into `PermissionManagerService` (11+) | `dumpsys package <pkg>` |
| clipboard | `ClipboardManager#set/getPrimaryClip` | `IClipboard$Stub$Proxy` (`clipboard`) | descriptor |
| unknown outbound call | `BinderProxy#transact` (script above) | — | `getInterfaceDescriptor()` |
| inbound into the app | the app's own Stub impls / `Binder#execTransact` | — | `getCallingUid()` inside (uid-1000 trap above) |
**Driving a service by hand:** `adb shell service list` enumerates services;
`adb shell service call <service> <code> i32 ... s16 ...` invokes transaction `<code>` directly
(`i32`/`i64`/`f`/`d`/`s16` are the argument type tags). Use it to confirm a code number against a
known method before you build a hook on it. `adb shell dumpsys binder_calls_stats` (Android 9+)
shows which binder calls a process actually made — a zero-friction first look before you hook
anything.
(argument type tags: `i32`/`i64`/`f`/`d`/`s16`, plus `null` and `fd`). The interface token is
written for you — never prefix an `s16` descriptor yourself. And it only reaches services in the
default binder context (`/dev/binder`): HAL interfaces live in other contexts and are not
`service call`-able. Use it to confirm a code number against a known method before you build a
hook on it. `adb shell dumpsys binder_calls_stats` (Android 9+; `dumpsys binder_calls_stats enable`
first if empty) shows which binder calls a process actually made — a zero-friction first look
before you hook anything.
## 2. Intent audit — the input channels a repack cannot remove
@@ -169,6 +169,10 @@ notes, retrieved 2026-10-19; standard shapes, not run here — and note what eac
analysis-environment move: it weakens a control the app deliberately added, so it belongs in
your instrumentation, never in a shipped patch (the same line the system-path template draws).
Both are hook bodies, not patches — the delivery vehicles and the
`MethodHookParam`/implementation vocabulary to write them are in `references/dynamic-frida.md`
and `references/lsposed-and-modules.md` §Hook vocabulary.
## Method notes
+1 -1
View File
@@ -309,7 +309,7 @@ deadline reported nothing about the target, only about your deadline.
| `pyelftools` (python) | library | **section-based — unreliable on hardened targets.** Prefer hand-walking `PT_LOAD`/`PT_DYNAMIC`, as `scripts/elf_plt.py` does |
| IDA Pro | GUI, **or headless via `idalib-mcp`** | the strongest decompiler output. Do not assume it needs a human in front of it — a headless MCP server exists (see *MCP tool servers* below). Ask for a human only when that setup is genuinely unavailable |
| `x64dbg` | GUI, Windows | human-driven live debugging of a native Windows target |
| `gdb` / `lldb` | CLI | live debugging where a device or emulator allows it. The LLDB remote form: `adb shell /data/local/tmp/lldb-server platform --listen "*:1234" --server &`, `adb forward tcp:1234 tcp:1234`, then `platform select remote-android` → `platform connect connect://localhost:1234` → `process attach --pid <pid>` *(unverified — absorbed from milksu.org notes, retrieved 2026-10-19)*. Attach itself can be the detected thing — check `references/detection-and-anti-analysis.md` Step 3 Stage 0 first |
| `gdb` / `lldb` | CLI | live debugging where a device or emulator allows it. The LLDB remote form: `adb shell /data/local/tmp/lldb-server platform --listen "*:1234" --server &`, `adb forward tcp:1234 tcp:1234`, then `platform select remote-android` → `platform connect connect://localhost:1234` → `process attach --pid <pid>` *(unverified — absorbed from milksu.org notes, retrieved 2026-10-19)*. The GDB form, same provenance: `adb shell "su -c 'gdbserver64 :1234 --attach <pid>'"` + `target remote :1234`. Attach itself can be the detected thing — check `references/detection-and-anti-analysis.md` Step 3 Stage 0 first. **User-assisted session core** — this is the route the AI names commands for and the human runs when Frida is refused: stop and inspect with `x/10i $pc` (instructions), `x/4gx $sp` (stack), `x/s $x0` (a string argument), `info registers`, `info frame`; conditional stop `break *<addr> if $x0 == 42`; session-address rule below. A gdbserver attach discriminates the two detection families: it survives frida-specific detection but not tracer-generic (`TracerPid`) — see the stealth ledger in `references/hardware-watchpoints.md` |
**capstone can stop silently.** Decoding a buffer that does not begin at an instruction boundary may
return a few instructions and then nothing, with no error. A scan built on that reports "no matches"