From 2bb54a72fb0b1610aa3789bc51c18800b37f9ffd Mon Sep 17 00:00:00 2001 From: dabao1955 Date: Mon, 19 Oct 2026 21:32:41 +0800 Subject: [PATCH] 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. --- .../references/advanced-unpacking.md | 19 +++++- .../references/detection-and-anti-analysis.md | 11 +++- .../apk-reverse/references/dynamic-frida.md | 2 +- .../references/lsposed-and-modules.md | 37 +++++++++--- .../apk-reverse/references/native-and-so.md | 32 ++++++++-- .../references/native-tamper-and-suicide.md | 2 +- skills/apk-reverse/references/packers.md | 6 +- .../references/system-ipc-audit.md | 58 +++++++++++++------ skills/apk-reverse/references/tls-and-cert.md | 4 ++ skills/apk-reverse/references/toolchain.md | 2 +- 10 files changed, 138 insertions(+), 35 deletions(-) diff --git a/skills/apk-reverse/references/advanced-unpacking.md b/skills/apk-reverse/references/advanced-unpacking.md index 8b4b713..683ea3f 100644 --- a/skills/apk-reverse/references/advanced-unpacking.md +++ b/skills/apk-reverse/references/advanced-unpacking.md @@ -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 diff --git a/skills/apk-reverse/references/detection-and-anti-analysis.md b/skills/apk-reverse/references/detection-and-anti-analysis.md index 5874cea..bdad00d 100644 --- a/skills/apk-reverse/references/detection-and-anti-analysis.md +++ b/skills/apk-reverse/references/detection-and-anti-analysis.md @@ -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 diff --git a/skills/apk-reverse/references/dynamic-frida.md b/skills/apk-reverse/references/dynamic-frida.md index 22e6fb0..459f28e 100644 --- a/skills/apk-reverse/references/dynamic-frida.md +++ b/skills/apk-reverse/references/dynamic-frida.md @@ -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//` 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 — diff --git a/skills/apk-reverse/references/lsposed-and-modules.md b/skills/apk-reverse/references/lsposed-and-modules.md index 011c15c..2ccae5d 100644 --- a/skills/apk-reverse/references/lsposed-and-modules.md +++ b/skills/apk-reverse/references/lsposed-and-modules.md @@ -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 `` | 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 diff --git a/skills/apk-reverse/references/native-and-so.md b/skills/apk-reverse/references/native-and-so.md index 0212a43..86f2c71 100644 --- a/skills/apk-reverse/references/native-and-so.md +++ b/skills/apk-reverse/references/native-and-so.md @@ -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 `` — 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 diff --git a/skills/apk-reverse/references/native-tamper-and-suicide.md b/skills/apk-reverse/references/native-tamper-and-suicide.md index 9013470..48df345 100644 --- a/skills/apk-reverse/references/native-tamper-and-suicide.md +++ b/skills/apk-reverse/references/native-tamper-and-suicide.md @@ -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 | diff --git a/skills/apk-reverse/references/packers.md b/skills/apk-reverse/references/packers.md index 5fc5362..dcf558b 100644 --- a/skills/apk-reverse/references/packers.md +++ b/skills/apk-reverse/references/packers.md @@ -107,7 +107,11 @@ grep -o '/[^ ]*\.so' /proc//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, diff --git a/skills/apk-reverse/references/system-ipc-audit.md b/skills/apk-reverse/references/system-ipc-audit.md index 83b43a0..c8592b0 100644 --- a/skills/apk-reverse/references/system-ipc-audit.md +++ b/skills/apk-reverse/references/system-ipc-audit.md @@ -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_ = 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("$Stub").TRANSACTION_.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/'` 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/'` (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 ` | +| permission state | `ContextImpl#checkSelfPermission` | server side split into `PermissionManagerService` (11+) | `dumpsys package ` | +| 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 i32 ... s16 ...` invokes transaction `` 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 diff --git a/skills/apk-reverse/references/tls-and-cert.md b/skills/apk-reverse/references/tls-and-cert.md index ca34113..98fe75b 100644 --- a/skills/apk-reverse/references/tls-and-cert.md +++ b/skills/apk-reverse/references/tls-and-cert.md @@ -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 diff --git a/skills/apk-reverse/references/toolchain.md b/skills/apk-reverse/references/toolchain.md index d67fa23..01f9ca9 100644 --- a/skills/apk-reverse/references/toolchain.md +++ b/skills/apk-reverse/references/toolchain.md @@ -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 ` *(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 ` *(unverified — absorbed from milksu.org notes, retrieved 2026-10-19)*. The GDB form, same provenance: `adb shell "su -c 'gdbserver64 :1234 --attach '"` + `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 * 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"