diff --git a/skills/apk-reverse/references/advanced-unpacking.md b/skills/apk-reverse/references/advanced-unpacking.md index b042cf7..8b4b713 100644 --- a/skills/apk-reverse/references/advanced-unpacking.md +++ b/skills/apk-reverse/references/advanced-unpacking.md @@ -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), diff --git a/skills/apk-reverse/references/dynamic-frida.md b/skills/apk-reverse/references/dynamic-frida.md index 08ecb64..22e6fb0 100644 --- a/skills/apk-reverse/references/dynamic-frida.md +++ b/skills/apk-reverse/references/dynamic-frida.md @@ -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//` 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 — diff --git a/skills/apk-reverse/references/java2c-and-jni-sinking.md b/skills/apk-reverse/references/java2c-and-jni-sinking.md index 979c4cd..d3f9068 100644 --- a/skills/apk-reverse/references/java2c-and-jni-sinking.md +++ b/skills/apk-reverse/references/java2c-and-jni-sinking.md @@ -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_____` - 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 `__` 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` / diff --git a/skills/apk-reverse/references/lsposed-and-modules.md b/skills/apk-reverse/references/lsposed-and-modules.md index 20e127c..011c15c 100644 --- a/skills/apk-reverse/references/lsposed-and-modules.md +++ b/skills/apk-reverse/references/lsposed-and-modules.md @@ -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 diff --git a/skills/apk-reverse/references/native-and-so.md b/skills/apk-reverse/references/native-and-so.md index 2c8c75c..f8dfd6d 100644 --- a/skills/apk-reverse/references/native-and-so.md +++ b/skills/apk-reverse/references/native-and-so.md @@ -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). diff --git a/skills/apk-reverse/references/recon.md b/skills/apk-reverse/references/recon.md index 45e732f..cb67a06 100644 --- a/skills/apk-reverse/references/recon.md +++ b/skills/apk-reverse/references/recon.md @@ -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 diff --git a/skills/apk-reverse/references/system-ipc-audit.md b/skills/apk-reverse/references/system-ipc-audit.md index b86c5b7..83b43a0 100644 --- a/skills/apk-reverse/references/system-ipc-audit.md +++ b/skills/apk-reverse/references/system-ipc-audit.md @@ -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/'` 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 i32 ... s16 ...` invokes transaction `` 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 -d + --es k v` and `adb shell am broadcast -a --es k v` (action/data/extras forms; +`cmd package resolve-activity -a -d ` 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//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_` 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. diff --git a/skills/apk-reverse/references/toolchain.md b/skills/apk-reverse/references/toolchain.md index 0d3d0e3..d67fa23 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 | +| `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 | **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"