mirror of
https://codeberg.org/reverse-skill-lab/apk-reverse
synced 2026-11-15 20:04:22 +08:00
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:
@@ -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
|
||||
|
||||
@@ -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 |
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user