New references (loaded on demand, one topic each): - packers.md: reading the shell's rejection signal, distinguishing self-inflicted failures from genuine refusals, mapping the validation boundary via single-variable tests, component-type rules (providers vs activities), picking a native host - framework-runtimes.md: identifying Flutter/RN/Unity/Cordova, the wrong-layer trap, how to tell which layer draws a given UI before committing to a patch direction - native-and-so.md: host-library selection criteria, DT_NEEDED does-not-call-JNI_OnLoad, relocation vs page-permission constraints and relocation-free bootstrapping, replacing Java methods from native code with trampolines and fail-safe guards - long-task-discipline.md: live record structure, conclusion grading, drift checkpoints, handover contents, and the opposite failure of over-documenting Pitfalls: +P18 install reported success but the build was never installed, +P19 internal signal substituted for the user-visible outcome, +P20 not-captured treated as not-present, +P21 two changes then attributing the result, +P22 waiting on something that needs a human to advance. SKILL.md: index the new references; add a runtime check for layer ownership to Q3; add long-task rule; strengthen the verify step; add three hard constraints (user-visible definition of done, no route discarded on compound evidence, prove the device changed before measuring). No app names, package names, or sample-specific identifiers anywhere.
122 lines
6.7 KiB
Markdown
122 lines
6.7 KiB
Markdown
# Native & SO Layer
|
|
|
|
Load this when the Java/dex layer is blocked or unsuitable, when you need code to run **before** the app's
|
|
own code, or when a native library is the only editable place left.
|
|
|
|
The Java layer is usually the right choice. Go native when you need one of these:
|
|
|
|
- execution **earlier than any app code** (before `Application.onCreate`, before static initialisers),
|
|
- a change that survives the app re-loading or re-initialising its Java state,
|
|
- a host the integrity checks do not cover while the dex is covered.
|
|
|
|
## Picking a host library
|
|
|
|
A library is usable only if all three hold. Verify each; do not assume.
|
|
|
|
1. **Loaded in your scenario** — confirm in `/proc/<pid>/maps`, not by inspecting the APK.
|
|
2. **Editable** — per the boundary map you built in `packers.md`.
|
|
3. **Has a call the runtime makes for you** — an exported `JNI_OnLoad` is ideal, because the runtime
|
|
invokes it automatically when the library is loaded by the app, with no trigger of your own.
|
|
|
|
Prefer a host whose exported JNI entry point exists *and* which appears in `maps`.
|
|
|
|
## The auto-load trap: `DT_NEEDED` does not call `JNI_OnLoad`
|
|
|
|
Adding a `DT_NEEDED` entry so one library drags in another is a natural idea. It maps the second library,
|
|
but **`JNI_OnLoad` is not invoked for it** — that callback only fires when the runtime loads a library
|
|
through the normal library-load API. A dependency loaded purely by the dynamic linker is mapped, not
|
|
initialised.
|
|
|
|
If you rely on `DT_NEEDED`, your entry point must be an **ELF constructor** or a `DT_INIT` entry, not
|
|
`JNI_OnLoad`. Conversely, if your host exports `JNI_OnLoad` and you control when the app loads it, that is
|
|
the simplest reliable trigger.
|
|
|
|
Related: a constructor/`DT_INIT` runs so early that the VM may not exist yet. Retry, and treat failure as
|
|
non-fatal — a library that logs nothing must never take the process down.
|
|
|
|
## Relocations and page permissions
|
|
|
|
This is where most hand-built native payloads fail, and the failure looks like a crash **inside the
|
|
dynamic linker**, not inside your code.
|
|
|
|
Rules that follow from how Android maps libraries:
|
|
|
|
- **A relocation target must be writable at load time.** The linker writes the computed address into the
|
|
target slot. If the slot lives in a section that is mapped read-only, the write faults
|
|
(`SEGV_ACCERR` in `plain_relocate_impl`).
|
|
- **Modern Android refuses W+X segments.** Adding a read/write/execute segment to get both properties is
|
|
rejected; the loader will not give you one.
|
|
- Together these mean: relative relocations cannot heal pointer slots that sit in an executable-only page.
|
|
|
|
### Bootstrapping when relocations cannot work
|
|
|
|
If your payload's internal pointers all live in the same page as the code that reads them, and that page
|
|
must be executable, you cannot use relocations. Do the fixup at runtime instead:
|
|
|
|
1. derive the runtime page base (a PC-relative instruction gives you this without any relocation),
|
|
2. make the page writable/executable via a direct `mprotect` **syscall** (do not depend on the libc
|
|
symbol being imported; a syscall needs no linkage),
|
|
3. write the pointer slots yourself, computed from the runtime base,
|
|
4. restore the page to read+execute,
|
|
5. then branch into your entry point.
|
|
|
|
Every pointer you need is `runtime_base + fixed_offset`. No relocation is added, so the segment
|
|
properties never have to change.
|
|
|
|
Do **not** try to make a page writable by re-mapping it over itself with an anonymous fixed mapping:
|
|
that severs the file mapping, and any other thread executing code on that page faults immediately. Use a
|
|
permission change, not a re-map.
|
|
|
|
## Replacing a Java method from native code
|
|
|
|
You can redirect a Java method's implementation without touching dex:
|
|
|
|
- Look up the method id, walk to the `ArtMethod` structure, and rewrite its **quick entry point**.
|
|
- Emit a short stub: load a target address from a literal pool and branch to it; return values follow the
|
|
normal calling convention (arguments are already in place, return in the usual register).
|
|
- **Always preserve the original code you overwrote.** If the method can be entered through its own
|
|
entry point, a naive redirect makes it jump into your stub, which jumps back into the same entry —
|
|
infinite recursion. Copy the overwritten instructions into a trampoline and branch back after them.
|
|
- **Do not patch a method whose entry still points at a shared interpreter bridge.** Methods that have
|
|
not been compiled by JIT/AOT share an entry; rewriting it breaks every method that shares it.
|
|
A practical guard: if two unrelated target methods report the identical entry address, both are still
|
|
on the bridge — refuse to patch rather than corrupting the process.
|
|
- Also refuse when the first instruction at the entry is PC-relative (address computed from the current
|
|
program counter): copying it elsewhere silently changes what it points to.
|
|
|
|
Make installation **fail-safe**: if any precondition is not met, skip the patch and let the app run
|
|
unmodified. A hook that silently does not apply is survivable; a hook that corrupts a shared entry is not.
|
|
|
|
## Finding call sites without symbols
|
|
|
|
Stripped and obfuscated libraries still expose structure:
|
|
|
|
- Scan the code section for the pattern that reaches your target (a PC-relative address computation
|
|
followed by an indirect branch through a table slot). A method reached through a **cached table slot**
|
|
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.
|
|
- 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).
|
|
|
|
## Cross-architecture notes
|
|
|
|
- **arm64** is the common case on modern devices; prefer it and make sure the host library you pick
|
|
actually exists for that ABI.
|
|
- **x86_64** appears on emulators. Shells and payloads are ABI-specific: a file built for one ABI will
|
|
not be loaded on the other, and the failure mode looks like a missing library rather than a mismatch.
|
|
- Keep your build artifacts per-ABI and label them. Mixing them is a common source of "it crashed for no
|
|
reason" when the same APK behaves differently on an emulator versus a device.
|
|
|
|
## Verification
|
|
|
|
A native patch earns no credit until the behaviour changes **and** the app stays healthy:
|
|
|
|
- log a marker from your entry point so you know it ran at all;
|
|
- confirm the library is in `maps`;
|
|
- confirm the app reaches its normal UI afterwards;
|
|
- keep the original library so you can produce an unmodified control build.
|
|
|
|
Never conclude "the hook works" from the absence of a crash. Absence of effect is the normal outcome of a
|
|
hook that never installed.
|