mirror of
https://codeberg.org/reverse-skill-lab/apk-reverse
synced 2026-11-15 20:04:22 +08:00
Fill the reading-layer gaps the IPC reference exposed
The first milksu integration only took the system-boundary material and dismissed the workflow content as overlapping deeper files. That dismissal was asserted, not audited; auditing it found five places where the repository owns the procedure but not the reference material the procedure leans on: - dex-patching.md: a reading-level smali cheat sheet (type descriptors, register conventions and the .registers coupling, the five instruction families that carry control flow, obfuscation signatures). The file taught how to edit a method you can already read; nothing taught the reading. - recon.md: the sensitive-behavior grep table (shell-outs, JS bridges, mutable PendingIntents, WebView width, dynamic dex loading, intent redirection) -- the signature-check section had one pattern, the behaviour sweep had none. - detection-and-anti-analysis.md: the named Java-level root checks and their one-line neutralizations, as the concrete form of Step 2A's "simple, localised check" -- scoped to the named detector, never a blind stack. - tls-and-cert.md: the two OkHttp-path client sites (CertificatePinner.check, SSLContext.init) that the OkHttp section previously pointed at without naming, each annotated with what it honestly buys. - native-and-so.md: the JNIEnv vtable call shape for reading JNI calls in a stripped library, with the offset-calibration caveat. All absorbed material is marked unverified with the retrieval date (milksu.org, 2026-10-19) at the point of use; syntax and layout facts that follow from documented platform behaviour are marked inferred. 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:
@@ -66,6 +66,25 @@ detector you were not asked about.
|
||||
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).
|
||||
|
||||
### The named Java-level root checks, and their one-line neutralizations
|
||||
|
||||
Step 2A's "simple, localised check" has a known population. Once Stage 2 has *named* the detector,
|
||||
these are the standard neutralizations — each is one gate, in your environment, reversible.
|
||||
*(Unverified: absorbed from milksu.org notes, retrieved 2026-10-19; not one of them was run by
|
||||
this repository. They are starting points to verify against your named detector, and they only
|
||||
apply after the detector is named — never as a blind stack.)*
|
||||
|
||||
| Named detector | Neutralization |
|
||||
|---|---|
|
||||
| a root-check library class (`RootBeer`-style, `isRooted`/`isRooted`-shaped method) | replace the method to return the negative constant |
|
||||
| `Runtime.exec("su")` / `exec("which su")` probing for binaries | throw `IOException("Command not found")` from the hook instead of running it |
|
||||
| file-existence checks on `/system/xbin/su`, `/system/bin/su`, Magisk paths | hook the file API to report absent for those paths only |
|
||||
| package checks for `com.topjohnwu.magisk`, Xposed manager packages | hook `getPackageInfo` to throw `NameNotFoundException` for those names only |
|
||||
|
||||
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).
|
||||
|
||||
## Step 3: locating the check — the order of search
|
||||
|
||||
Step 2 decides *whether* to fight. This step is the part the two steps around it do not provide:
|
||||
|
||||
@@ -202,3 +202,40 @@ Search strategies that work:
|
||||
5. `dex_classdiff` every patched dex against the original → expect zero structural drift.
|
||||
6. Repack, sign, install, launch, exercise, read logcat.
|
||||
7. If anything is off, revert that dex to the original and re-verify the control build.
|
||||
|
||||
## Reading smali — the cheat sheet
|
||||
|
||||
The techniques above assume you can already read what you are editing. For orientation in a large
|
||||
smali tree — before any patch exists — these are the shapes that matter. *(Unverified: absorbed
|
||||
from milksu.org notes, retrieved 2026-10-19; the syntax facts are `inferred` from the dalvik
|
||||
bytecode spec, the reading shortcuts are someone else's workflow, not measured here.)*
|
||||
|
||||
**Type descriptors:** `V` void, `Z` boolean, `B` byte, `C` char, `S` short, `I` int, `J` long,
|
||||
`F` float, `D` double; objects `Ljava/lang/String;`; arrays `[I`, `[[Ljava/lang/Object;`. Method
|
||||
descriptors read `(...)R` with no separator between parameters — `add(II)I` is `(II)I`.
|
||||
|
||||
**Registers:** `.registers N` is the total; `p0` is `this`, `p1..` are parameters, `v0..` are
|
||||
locals. Parameter registers sit at the **top** of the range, so adding locals means renumbering
|
||||
nothing but growing `N` — while adding code that uses a register beyond `N` fails verification.
|
||||
Any edit that introduces a new local must update `.registers` in the same edit; this is the first
|
||||
thing to check when a hand-edited method fails to verify with no obvious cause.
|
||||
|
||||
**The five instruction families that carry most control flow:**
|
||||
|
||||
| Family | Shapes | What you read off them |
|
||||
|---|---|---|
|
||||
| invoke | `invoke-virtual/-static/-direct/-interface {regs}, Lcls;->m(Args)R` | every non-local call, with exact target |
|
||||
| result | `move-result` / `move-result-object` immediately after an `invoke` | the value a call returned (adjacency is verifier-enforced) |
|
||||
| fields | `iget/iput` instance, `sget/sput` static | field reads/writes with owner |
|
||||
| branches | `if-eqz/if-nez v0, :label`, `if-eq/ne/lt/ge`, `goto` | every decision point; a patched condition lives here |
|
||||
| constants | `const/4`, `const/16`, `const-string`, `const-wide` | the values compared against |
|
||||
|
||||
A method's logic is usually recoverable by reading only these — ignore the register shuffling
|
||||
between them on a first pass.
|
||||
|
||||
**Obfuscation signatures:** ProGuard renames to `La/b/c;->a(...)` — navigate by string constants
|
||||
and call targets instead of names. A wrapper like `decrypt("base64...")` feeding every string
|
||||
constant is string encryption — the decrypted values are recoverable at runtime by hooking the
|
||||
wrapper (`references/dynamic-frida.md` §Hook strategy) rather than by reversing it. An `if-nez`
|
||||
guarding a `goto` into dead code that never executes is an opaque predicate — identify the live
|
||||
branch and read only that.
|
||||
|
||||
@@ -100,6 +100,14 @@ Stripped and obfuscated libraries still expose structure:
|
||||
will ignore a rewrite of its own entry point, because callers never read that field — another reason
|
||||
the entry-point rewrite must be validated per call site.
|
||||
- Dump the executable range and disassemble around candidate offsets instead of trusting symbol names.
|
||||
- The JNI calls themselves have a fixed reading shape — in a stripped library this is often the
|
||||
fastest way to find where Java objects are touched. The first argument of an exported or
|
||||
registered JNI function is the `JNIEnv*`; every JNI call through it compiles to
|
||||
`ldr x8, [x0]` (load the vtable) → `ldr x8, [x8, #imm]` (slot) → `blr x8` (call), with `x0`
|
||||
holding the env and later slots taking the jobject/jstring. Offsets differ per JNI version and
|
||||
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.)*
|
||||
- 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).
|
||||
|
||||
|
||||
@@ -172,6 +172,19 @@ Search the app's **own** packages only (ignore third-party SDKs) for:
|
||||
|
||||
Confirm by checking whether **app code** references it. If the only callers are library-internal, it is not your problem.
|
||||
|
||||
The same grep-the-app's-own-packages pass answers behaviour questions beyond tamper checks.
|
||||
*(Unverified: absorbed from milksu.org notes, retrieved 2026-10-19 — the patterns are standard
|
||||
API names, `inferred`; their hit-rate as triage shortcuts is not measured here.)*
|
||||
|
||||
| Grep a jadx export for | Points at |
|
||||
|---|---|
|
||||
| `Runtime.exec` / `ProcessBuilder` | shell-out behaviour, often config flips or su probes |
|
||||
| `addJavascriptInterface` | a JS bridge — bridge method inventory next (`references/system-ipc-audit.md`) |
|
||||
| `PendingIntent.get` without `FLAG_IMMUTABLE` nearby | a mutable capability token (Android 12+ forces a flag) |
|
||||
| `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`) |
|
||||
|
||||
## 6. API surface
|
||||
|
||||
```bash
|
||||
|
||||
@@ -158,6 +158,18 @@ Do not: change the device clock, install your own CA into the device trust store
|
||||
|
||||
Then the failure is about the app's own client configuration, not the platform defaults. Typical causes: a pin that no longer matches, a hostname verifier rejecting a changed host, or a custom `SSLSocketFactory` built from a bundled keystore. Locate it the same way (Step 4, item 2) and patch at the client construction site instead of applying the template above. `dynamic-frida.md` covers finding the real construction site; `dex-patching.md` covers the patch layer.
|
||||
|
||||
The two client-side sites, for when the attribution is done. *(Unverified: absorbed from milksu.org
|
||||
notes, retrieved 2026-10-19; standard shapes, not run here — and note what each one honestly buys:*
|
||||
|
||||
- **`okhttp3.CertificatePinner.check`** — replace the method body to return immediately. This
|
||||
neutralizes OkHttp's own pin table only; it does not touch a verifier or factory the app
|
||||
installed separately, which is why the Step 4 attribution runs first.
|
||||
- **`javax.net.ssl.SSLContext.init`** — hook it and swap `args[1]` for a trust-all
|
||||
`TrustManager[]`. This covers an app-built factory on that path, and is strictly an
|
||||
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).
|
||||
|
||||
|
||||
## Method notes
|
||||
|
||||
- **Order of investigation is the lesson.** Every piece of evidence above comes from the server, the network, or the platform before anything is attributed to your patch: full exception chain, real host, out-of-band validation, second-host comparison, control build. Only when all of them point at a client trust decision is a patch justified. `pitfalls.md` P16 is this mistake written up as a failure.
|
||||
|
||||
Reference in New Issue
Block a user