mirror of
https://codeberg.org/reverse-skill-lab/apk-reverse
synced 2026-11-15 20:04:22 +08:00
Close the gaps an independent audit of the source articles found
The milksu integration was audited a second time, independently: four read-only passes, each taking a group of the source articles and verifying the coverage claims against the whole repository (references and scripts), with instructions to contradict rather than confirm. Their findings corrected the prior pass in both directions and drove every change here. The audit's most useful correction was against the integration itself: the ActivityThread.currentApplication() Context idiom had been reported missing while it ships working in scripts/rpc_template.js and scripts/sig_probe.py -- the pass had audited only references/. The fix is a cross-link, not absorption. Two more claims were narrowed (advanced-unpacking covers the classloader lab, not the dynamic-proxy lab; readelf coverage lives in toolchain.md, not the files first proposed), one attribution was repaired, and one provenance note was added: the absorbed Binder hook is not the article's script -- the source hooks Binder.transact with server-side getCallingUid() semantics inside; the form kept here is corrected and now says so. What the audit found genuinely absent, now integrated (all labelled unverified or inferred with the retrieval date at the point of use): - system-ipc-audit.md: generated-proxy hooking (decoded arguments beside the raw transact), the debugfs binder node/ref view for "why does my hook never fire", two new Intent signature rows (intent-scheme handoff, ClipData URI grants), am start/broadcast action-data-extras driving forms, the Intent monitor with live nested-Intent detection, the deep-link getData triage, three config rows the earlier table silently truncated (JavaScript, DOM storage, mixed content), the loadUrl dynamic hook, the logcat Console fallback channel, the four-surface static WebView pass, and the attestation KeyDescription/OID reading with the teeEnforced extraction discriminator. - lsposed-and-modules.md: LSPatch as the fourth route -- module-class authoring, repack-class delivery, runs on unrooted phones; plus existence proofs for the resource-hook and constructor/field/hook-all API surface. - dynamic-frida.md: the scripts' own Context idiom cross-linked with the Android 9+ hidden-API reflection boundary; dynamic proxy as an interception primitive; the LD_PRELOAD-closed-on-Android row in the non-live table. - toolchain.md: the lldb-server remote recipe, with the attach-is-detection pointer. - java2c-and-jni-sinking.md: the __<proto> mangling decode route step 2 silently depended on. - advanced-unpacking.md: the local-reference cap hazard on active-invocation loops. - native-and-so.md: the JNI width/type map beside the vtable shape. - recon.md: the MODE_WORLD_READABLE/WRITABLE row the earlier table dropped. Gates re-run: routing coherent, 409 section references with zero dangling, repo clean, pytest 751 passed / 194 skipped / 25 xfailed.
This commit is contained in:
@@ -167,7 +167,11 @@ live extraction shell):
|
||||
ART at the interpreter's entry so that, as each method begins executing, the
|
||||
method's `code_item` — now holding the decrypted body — is written to disk,
|
||||
tagged with its `method_idx`. These per-method records are the "bin" files of a
|
||||
FART dump.
|
||||
FART dump. One VM-side hazard scales with exactly this loop: every reflective
|
||||
call creates local references, and the local-reference table has a cap — an
|
||||
invocation loop that never releases them (no `DeleteLocalRef`) dies mid-run with
|
||||
a resource error that looks like the shell fighting back. *(inferred — absorbed
|
||||
from milksu.org notes, retrieved 2026-10-19)*
|
||||
3. **Splice the bodies back into the skeleton.** For each record, overwrite the
|
||||
stub body in the whole-dex image at the offset the skeleton gives you, fix the
|
||||
header (`scripts/dexutil.py` `fix_dex_header` — signature first, checksum last),
|
||||
|
||||
@@ -196,6 +196,16 @@ Java.perform(() => {
|
||||
});
|
||||
```
|
||||
- **Call stacks are the highest-value signal.** `Log.getStackTraceString(new Throwable())` or `Java.use('android.util.Log').getStackTraceString(...)` gives you the real chain from the framework down to the method — far more reliable than reading smali.
|
||||
- **Need a `Context` and have none?** This repository's own scripts already carry the idiom:
|
||||
`Java.use('android.app.ActivityThread').currentApplication()` — see `scripts/rpc_template.js` and
|
||||
`scripts/sig_probe.py`, both with a null-application retry the naive version lacks. Note the
|
||||
boundary: on Android 9+ reflection into hidden/greylist APIs is restricted, which is why a
|
||||
reflective route into `ServiceManager` or hidden AIDL sometimes fails where the bridge's own
|
||||
handles still work. *(inferred — platform restriction, absorbed from milksu.org notes, 2026-10-19)*
|
||||
- **Dynamic proxy as a hook alternative.** When the target exposes an interface with many or
|
||||
obfuscated implementations, `java.lang.Proxy.newProxyInstance` with an `InvocationHandler`
|
||||
registers one interceptor for all of them — the Java-native counterpart of wrapping every
|
||||
overload by hand. *(unverified — absorbed from milksu.org notes, retrieved 2026-10-19)*
|
||||
|
||||
### Native layer
|
||||
```javascript
|
||||
@@ -478,6 +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)* |
|
||||
|
||||
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 —
|
||||
|
||||
@@ -87,7 +87,11 @@ For Java2C the analyst's instinct — dump memory and find the dex — is not me
|
||||
still shows the same `native` declarations you started with, and the natural (wrong) reading is
|
||||
"the dump failed".
|
||||
2. **List the translated functions.** With static linkage, `Java_<PKG>_<Class>_<method>__<proto>`
|
||||
names map the `.so` back onto the dex one for one. Use the dex as the index and the `.so` as
|
||||
names map the `.so` back onto the dex one for one. The `__<proto>` suffix is the mangled
|
||||
descriptor — `;`→`_2`, `_`→`_1`, `[`→`_3` — so `__Landroid_os_Bundle_2` reads back as a
|
||||
`(Landroid/os/Bundle;)…` signature, resolving overloads without the dex open.
|
||||
*(inferred — JNI name-mangling rules, absorbed from milksu.org notes, 2026-10-19)*
|
||||
Use the dex as the index and the `.so` as
|
||||
the library.
|
||||
3. **Rebuild the chain backwards.** A translated function calls *out* to Java constantly. The
|
||||
strings `FindClass` / `GetMethodID` / `GetStaticMethodID` / `Call*Method` / `NewObject` /
|
||||
|
||||
@@ -24,6 +24,21 @@ you enjoy more.
|
||||
| **LSPosed module** | an installable APK the user keeps | every process start, automatically, after the module is enabled and scoped | usually yes — the hook targets classes/methods, not offsets | mid: one project, no repackaging of the target |
|
||||
| **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.)*
|
||||
|
||||
Two API-surface notes from the same source, same label: the runtime **resource hook** exists —
|
||||
`IXposedHookInitPackageResources` with `res.setReplacement(...)` / `hookLayout(...)` replaces
|
||||
strings and layouts without a repack, the runtime alternative to a resource edit; and beyond
|
||||
`findAndHookMethod` the shapes that matter are `findAndHookConstructor`,
|
||||
`XposedHelpers.get/setObjectField`, and `XposedBridge.hookMethod` over `getDeclaredMethods()` for
|
||||
hook-everything. Official API docs remain the reference; these are existence proofs, not tutorials.
|
||||
|
||||
Reach for a module when **any** of these is true:
|
||||
|
||||
- the target verifies its own signature or APK digest, so a repackaged build dies on arrival
|
||||
|
||||
@@ -108,6 +108,10 @@ Stripped and obfuscated libraries still expose structure:
|
||||
per call, so read the offset from a *known* JNI call in an unstripped library of the same
|
||||
target ABI before trusting a number. *(The shape is `inferred` from the JNIEnv table layout;
|
||||
absorbed from milksu.org notes, retrieved 2026-10-19.)*
|
||||
- The width/type map for reading those calls: `jboolean` u8, `jbyte` i8, `jchar` u16,
|
||||
`jshort`/`jint` i32, `jlong` i64, `jfloat`/`jdouble` 32/64-bit float;
|
||||
`jobject`/`jstring`/`j*Array` are pointers, not values. *(inferred from `jni.h`; same
|
||||
source, same date.)*
|
||||
- When a payload was produced by a just-in-time compiler on the target, the displacement between its
|
||||
pages is often zero, which is exactly why relocation-free bootstrapping is required (above).
|
||||
|
||||
|
||||
@@ -184,6 +184,7 @@ API names, `inferred`; their hit-rate as triage shortcuts is not measured here.)
|
||||
| `loadUrl` / `setAllowFileAccess` / `setAllowUniversalAccess` | WebView width — how far web content can reach (`references/system-ipc-audit.md`) |
|
||||
| `DexClassLoader` / `InMemoryDexClassLoader` / `PathClassLoader` on a download path | dynamic dex loading — plugin/hot-update surface (`references/packers.md`, `references/updates-and-forced-upgrade.md`) |
|
||||
| `getParcelableExtra` on an exported component, result passed to `startActivity`/`sendBroadcast` | intent redirection (`references/system-ipc-audit.md`) |
|
||||
| `MODE_WORLD_READABLE` / `MODE_WORLD_WRITABLE` | world-open file storage — legacy API, still tripped by old code |
|
||||
|
||||
## 6. API surface
|
||||
|
||||
|
||||
@@ -53,7 +53,20 @@ 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.
|
||||
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*.
|
||||
|
||||
*(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)*
|
||||
|
||||
**Driving a service by hand:** `adb shell service list` enumerates services;
|
||||
`adb shell service call <service> <code> i32 ... s16 ...` invokes transaction `<code>` directly
|
||||
@@ -73,11 +86,24 @@ prevalence but mechanically cheap to check:
|
||||
| `getParcelableExtra("intent"\|"next"\|"redirect"\|"forward")` fed straight to `startActivity`/`sendBroadcast` | Intent redirection: your crafted nested Intent runs with this app's identity and privileges |
|
||||
| implicit `Intent` sent with sensitive extras and no `setPackage()` | any higher-priority receiver can register for the action and harvest the extras |
|
||||
| deep link / Intent extra flowing into `loadUrl` or a file path without scheme/host whitelist | injection into the WebView or path traversal surface (next section) |
|
||||
| `Intent.parseUri(url, URI_INTENT_SCHEME)` fed straight to `startActivity` in `shouldOverrideUrlLoading` | intent-scheme handoff: an unvalidated nested Intent arriving from web content |
|
||||
| `ClipData` plus `FLAG_GRANT_READ/WRITE_URI_PERMISSION` forwarded to another component | temporary URI grants travelling further than the app intended |
|
||||
|
||||
`PendingIntent` without `FLAG_IMMUTABLE` (mandatory to *declare* either flag since Android 12 —
|
||||
inferred from the platform change) is the same class of bug with a capability token: a mutable
|
||||
PendingIntent handed to an untrusted party lets them rewrite the inner Intent.
|
||||
|
||||
**Driving an exported component mid-task** — no hook needed: `adb shell am start -a <action> -d
|
||||
<uri> --es k v` and `adb shell am broadcast -a <action> --es k v` (action/data/extras forms;
|
||||
`cmd package resolve-activity -a <action> -d <uri>` resolves the receiver first).
|
||||
|
||||
**The Intent monitor** *(both unverified — absorbed from milksu.org notes, retrieved 2026-10-19)*:
|
||||
wrap `Activity.startActivity`, `ContextImpl.sendBroadcast` and `startService`; print action, data,
|
||||
component and every extra, and flag any extra whose value is itself an `Intent` — the redirection
|
||||
signature caught live, with the caller stack one `Log.getStackTraceString` away. Its deep-link
|
||||
counterpart hooks `Intent.getData` and flags `..`, `file://`, `intent://` in the returned URI —
|
||||
traversal and scheme-abuse inputs without a full monitor.
|
||||
|
||||
## 3. WebView and the JS bridge — audit surface, not attack surface
|
||||
|
||||
For reverse engineering the interesting facts are configuration and bridge inventory, both readable
|
||||
@@ -94,13 +120,29 @@ WV.addJavascriptInterface.implementation = function(obj, name) {
|
||||
};
|
||||
```
|
||||
|
||||
The same one-liner shape on `WebView.loadUrl.overload('java.lang.String')` records every URL the
|
||||
WebView is asked to open — the dynamic counterpart of the `loadUrl` grep row in
|
||||
`references/recon.md` §5. When the app leaves debugging disabled, `adb logcat | grep Console`
|
||||
still carries the web side's `console.log` output (`I/chromium ... [INFO:CONSOLE(n)]`) — the
|
||||
fallback observation channel.
|
||||
|
||||
Settings that widen what web content can reach, in descending order of risk:
|
||||
|
||||
Once the bridge is found, the static pass over a jadx export covers four more surfaces the config
|
||||
table does not: `shouldOverrideUrlLoading` (does it hand `intent://`/`file://` straight to
|
||||
`startActivity`), `onPageFinished` (what JS gets injected into every page),
|
||||
`shouldInterceptRequest` (can a responder swap content), and the cookie/storage pair —
|
||||
`CookieManager` tokens and `localStorage` contents are the exfiltration targets the config rows
|
||||
measure.
|
||||
|
||||
| Setting | Effect if `true` |
|
||||
|---|---|
|
||||
| `setAllowUniversalAccessFromFileURLs` | a `file://` page can read any origin — the app's private files included |
|
||||
| `setAllowFileAccessFromFileURLs` | `file://` pages can read other `file://` URLs |
|
||||
| `setAllowFileAccess` / `setAllowContentAccess` | `file://` / `content://` loadable at all |
|
||||
| `setJavaScriptEnabled` | the base widening — required by most bridges, so rarely the finding itself |
|
||||
| `setDomStorageEnabled` | web-side state under `/data/data/<pkg>/app_webview/` — where tokens often live |
|
||||
| `setMixedContentMode(MIXED_CONTENT_ALWAYS_ALLOW)` | plain-HTTP subresources readable without any TLS fight (`references/tls-and-cert.md`) |
|
||||
| `setWebContentsDebuggingEnabled` | **in production: a free remote-debug entry for you** — `adb forward tcp:9222 localabstract:webview_devtools_remote_<pid>` then `chrome://inspect`, no root needed |
|
||||
|
||||
A `file://` deep link plus the first two settings is the classic private-data-exfiltration chain:
|
||||
@@ -130,7 +172,14 @@ or finding the server-side decision and what it actually gates. What you *can* o
|
||||
hook `android.security.keystore.KeyGenParameterSpec$Builder.build` and
|
||||
`java.security.KeyStore.getEntry` to see aliases, purposes, and whether user-auth binding is
|
||||
requested — that tells you whether the app leans on hardware keys at all before you spend time on
|
||||
the harder question. This is the boundary case for
|
||||
the harder question.
|
||||
|
||||
Reading a dumped chain: the leaf certificate's attestation extension (OID 1.3.6.1.4.1.11129.2.1.17,
|
||||
a `KeyDescription` sequence) splits every claim into `softwareEnforced` and `teeEnforced` halves —
|
||||
the same claim in both halves is the tell for a software-only implementation, and a
|
||||
`teeEnforced`-only key property means the key material never exists in the app's process, which
|
||||
decides the extraction question before you spend a process window on it. *(unverified — absorbed
|
||||
from milksu.org notes, retrieved 2026-10-19)* This is the boundary case for
|
||||
`references/detection-and-anti-analysis.md` §Step 3: locating the check — the order of search: the
|
||||
check runs outside the process, so in-process stages cannot reach it.
|
||||
|
||||
|
||||
@@ -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 |
|
||||
| `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 |
|
||||
|
||||
**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