references/native-tamper-and-suicide.md - how a hardened library terminates its own process: the mechanisms and their signatures, how to tell which one actually fires, arranged faults that call nothing (defeating every PLT-level fix), resolving a stub to its symbol, forged section headers, function boundaries from PT_GNU_EH_FRAME, scanner traps, and neutralising by returning rather than spinning. references/toolchain.md - what to install, non-interactive invocation, GUI-only tools (ask for a human instead of silently substituting a weaker method), version-alignment traps, and working offline. scripts/elf_plt.py - PLT stub to imported symbol on x86_64 and aarch64 from the relocation table; caller listing; library byte-diff naming each changed stub's symbol. scripts/apk_diff.py - entry-level diff by content hash, so same-size replacements are caught. scripts/native_crash.py - locate a native death from a log or tombstone and flag an arranged fault. pitfalls P31-P36; SKILL.md gains a decision question, a hard constraint on neutralising terminate paths, and cross-references from packers/native-and-so.
196 lines
11 KiB
Markdown
196 lines
11 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.
|
|
|
|
If instead the problem is that the app **dies on its own** — hangs, restarts, or crashes natively at a
|
|
roughly constant time after launch — that is a terminate mechanism, not a hooking problem, and the
|
|
methods differ: read `native-tamper-and-suicide.md` before this file.
|
|
|
|
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
|
|
|
|
Architecture is not background information here — it decides **which artifact you must edit** and
|
|
**whether your hook can observe anything at all**. Get it from the runtime, not from the manifest.
|
|
|
|
### What the device claims vs what is executing
|
|
|
|
Three different answers, and they disagree more often than people expect:
|
|
|
|
| Source | Answers | Command |
|
|
|---|---|---|
|
|
| The device | which ABIs the system supports | `getprop ro.product.cpu.abi` and `ro.product.cpu.abilist` |
|
|
| The package manager | **which ABI it chose for this app on this device** | `dumpsys package <pkg> \| grep primaryCpuAbi` |
|
|
| The live process | **what is actually mapped right now** | `scripts/lib_map.py --pkg <pkg>` |
|
|
|
|
Only the third is ground truth. The first two are predictions, and they are wrong exactly when it
|
|
matters: an emulator whose primary ABI is `x86_64` can be running an `arm64-v8a`-only app through a
|
|
translator, and a fat APK can have the package manager pick an ABI you did not assume.
|
|
|
|
**Consequence: if the library you patched does not appear in the live mapping, your change cannot
|
|
matter.** That is a plan problem, and no amount of re-patching will fix it.
|
|
|
|
### Translation layers change what you are observing
|
|
|
|
When an ARM-only app runs on an x86 host, a translator (Intel Houdini, `libndk_translation`, a
|
|
`native_bridge` in general) is executing the guest code. Detect it from the maps — look for
|
|
translator marker libraries and for app libraries whose architecture differs from the host's
|
|
primary ABI (`lib_map.py` reports both).
|
|
|
|
What it changes:
|
|
|
|
- **Timing.** Translation is slower and less predictable. Anything you measured about latency is
|
|
about the translator, not the code.
|
|
- **Native integrity and anti-tamper checks can behave differently** under translation, in either
|
|
direction — a check that fails on hardware may pass here, and vice versa. A pass under translation
|
|
is not evidence of a pass on hardware.
|
|
- **Hook behaviour.** Intercepting translated code is not the same as intercepting native code:
|
|
address spaces, calling conventions, and what the host sees at a syscall boundary all differ. If a
|
|
native hook reports nothing while the feature plainly runs, suspect translation before suspecting
|
|
your script.
|
|
- **Syscall-level observation shows the host's view.** A file or network call made from the guest
|
|
may look different on the host side than it would natively.
|
|
|
|
**Rule: verify the artifact on the ABI the user will actually run.** An emulator-only result is a
|
|
mid-task checkpoint, never the final claim (`verification.md`).
|
|
|
|
### Fat APKs: patching the ABI that never loads
|
|
|
|
An APK can ship `lib/arm64-v8a/`, `lib/armeabi-v7a/`, `lib/x86_64/`… The package manager extracts
|
|
**one** of them into the install-time native library directory, and only that one is loaded.
|
|
|
|
- Check `primaryCpuAbi` and the live maps before editing a `.so`. Editing `arm64-v8a` while the
|
|
device loads `armeabi-v7a` produces a build that is byte-different and behaviorally identical.
|
|
- If the deliverable must work for an unknown user, remember their device may select a different
|
|
ABI than yours. Either patch every ABI present, or state which ABI your artifact targets.
|
|
- If the app's own libraries are written at *runtime* rather than extracted from the APK, the live
|
|
paths will not be inside the APK at all (`lib_map.py` marks these `materialized`). Those belong to
|
|
whatever produced them, and a repacked APK will not carry them in that location.
|
|
|
|
### Payloads and tooling are per-ABI too
|
|
|
|
- A native payload, an injected library, or a shellcode blob built for one ABI **will not load** on
|
|
another. The failure usually surfaces as a *missing library* or a generic linker error rather than
|
|
a clean "wrong architecture" message — do not read it as "my payload is broken".
|
|
- **Pointer size differs** between 32- and 64-bit targets. A script or struct layout that assumes
|
|
8-byte words misbehaves silently on a 32-bit target.
|
|
- Keep build artifacts per-ABI and label them with the ABI in the filename. Mixing them up is a
|
|
reliable source of "it crashed for no reason" when the same APK behaves differently on an emulator
|
|
and on a device.
|
|
|
|
### Quick decision list
|
|
|
|
1. `scripts/lib_map.py --pkg <pkg> --app-only` → which app libraries are loaded, from where, and at
|
|
what architecture.
|
|
2. If a translator is present and the task needs reliable native behaviour, move to a matching
|
|
device.
|
|
3. If the library you meant to patch is absent, stop — pick the right library first.
|
|
4. Only then start editing.
|
|
|
|
## 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.
|