Expand into packer/native/framework/long-task coverage; harden verification discipline
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.
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
# Hardened / Packed Targets
|
||||
|
||||
Load this when recon says the app's `Application` class is not the app's own, or when edits make the
|
||||
app die before your code ever runs. Packing is the single most common reason a correct patch appears to
|
||||
"do nothing", so treat it as its own phase with its own evidence standard.
|
||||
|
||||
## What a packer actually does
|
||||
|
||||
A shell (jiagu / weapon / leggu / 360 / ijiami / bangcle class) replaces the manifest's
|
||||
`application android:name` with its own stub. At process start the stub:
|
||||
|
||||
1. loads its own native library from a directory it controls (often extracted at first launch),
|
||||
2. decrypts and loads the real dex, usually from `assets/` or a same-named container,
|
||||
3. optionally verifies that nothing about the APK, its own components, or the environment changed,
|
||||
4. only then forwards to the app's real `Application`.
|
||||
|
||||
Consequences that shape every later decision:
|
||||
|
||||
- **Your code in the app's own dex may never run** if the shell aborts first.
|
||||
- **The shell's native library is the gatekeeper**, and it is usually the only thing that decides
|
||||
"tampered vs clean".
|
||||
- **The shell's own entry is native**, so Java-level reflection into it is usually a dead end.
|
||||
|
||||
## Reading the rejection signal
|
||||
|
||||
The most informative failure looks like this:
|
||||
|
||||
```
|
||||
UnsatisfiedLinkError: JNI_ERR returned from JNI_OnLoad in "<app>/.jiagu/lib<shell>.so"
|
||||
```
|
||||
|
||||
Read it precisely: the shell's **own** `JNI_OnLoad` returned `JNI_ERR`. That means the shell
|
||||
**decided** to refuse — it is not a crash, not a missing library, not an ABI problem. Distinguish it from
|
||||
the look-alikes:
|
||||
|
||||
| Message | Meaning |
|
||||
|---|---|
|
||||
| `JNI_ERR returned from JNI_OnLoad in <shell>.so` | shell actively refused (integrity or environment) |
|
||||
| `dlopen failed: library "X" not found` | a dependency you added does not exist — **your** bug |
|
||||
| `CANNOT LINK EXECUTABLE` / relocation errors | ELF you produced is malformed |
|
||||
| `ClassNotFoundException` during `Application` init | a manifest component you renamed no longer resolves |
|
||||
|
||||
This distinction matters more than it looks: several classes of "the shell rejected me" are actually
|
||||
self-inflicted. Always grep the crash for `linker`, `CANNOT LINK`, `dlopen` and `not found` **before**
|
||||
concluding that the shell detected anything.
|
||||
|
||||
## Map the validation boundary with single-variable tests
|
||||
|
||||
Do not guess what the shell checks. Measure it. The only reliable method is one change at a time, each
|
||||
with its own install-and-launch run:
|
||||
|
||||
1. **Baseline control** — repack with no changes at all, re-sign, install, launch. If this fails, stop:
|
||||
your pipeline is the problem, not the shell.
|
||||
2. **Existing file, semantically inert change** — flip one byte in a section that nothing reads
|
||||
(a `.comment` string, a duplicated `DT_NEEDED` entry). Survives ⇒ edits to existing files of that
|
||||
class are allowed.
|
||||
3. **New file added** — add one benign ELF under the native library directory. Very commonly this alone
|
||||
is enough to trip a check, which is a distinct policy from (2).
|
||||
4. **Component rename in the manifest** — equal-length rename of one lazy component
|
||||
(an `activity`). Survives ⇒ you have a cheap way to neutralise features.
|
||||
5. **Structural change** — move a program header table, add a segment, add a relocation.
|
||||
|
||||
Record every result. The boundary is usually **not uniform**: typical real outcomes are
|
||||
"existing native libraries editable, new ones not", "activities renamable, providers not",
|
||||
"this dex fingerprinted, that container ignored".
|
||||
|
||||
### Why one variable at a time is not optional
|
||||
|
||||
The most expensive mistake in this phase is testing two hypotheses in one build. If you both edit a
|
||||
shell component **and** add a library, and the app dies, you have learned nothing — and you will likely
|
||||
blame the wrong one, discard a viable route, and lose hours (or in this skill's history, dozens of
|
||||
rounds) re-deriving what a cleaner experiment would have shown in one run.
|
||||
|
||||
### Component-type rules worth knowing up front
|
||||
|
||||
- **Providers are instantiated by the system during process start.** Renaming one so its class no
|
||||
longer resolves fails inside `Application.onCreate` and kills the process before anything else runs.
|
||||
Providers are therefore a poor rename target; prefer disabling the feature that consumes them, or
|
||||
hooking it.
|
||||
- **Activities and services are resolved lazily**, only when actually used. Renaming them is a cheap,
|
||||
low-risk way to make a feature unable to display, and it survives most integrity checks because it
|
||||
changes nothing about the shell.
|
||||
- **Equal-length renames** keep binary XML chunk structure valid, so no manifest re-encoding is needed:
|
||||
replace the name bytes in `AndroidManifest.xml` in place, size unchanged.
|
||||
|
||||
## Native libraries as a place to live
|
||||
|
||||
When the shell blocks dex-level changes, native libraries often remain open — but only some of them are
|
||||
useful. A library is a valid host for your code **only if all three hold**:
|
||||
|
||||
1. **It is actually loaded** in the scenario you care about.
|
||||
2. **It is editable** (per your boundary map above).
|
||||
3. **Something in it is called early enough** to matter.
|
||||
|
||||
Condition 1 is the one people skip, and it is the one that wastes the most time. A library shipped in
|
||||
the APK is not necessarily loaded. **Check `/proc/<pid>/maps`** after a real launch rather than assuming:
|
||||
|
||||
```
|
||||
grep -o '/[^ ]*\.so' /proc/<pid>/maps | sort -u
|
||||
```
|
||||
|
||||
Libraries belonging to optional SDKs are frequently lazy: they exist, they may even be unpacked to disk,
|
||||
and they are never mapped during the startup path you are trying to influence. Unpacked-but-unmapped is
|
||||
the signature of a lazy SDK, not evidence that your file was rejected.
|
||||
|
||||
Pick a host that is (a) present in `maps`, (b) exports a JNI entry point the runtime must call, and
|
||||
(c) editable. A library that exports `JNI_OnLoad` and appears in `maps` is the strongest candidate,
|
||||
because the runtime calls `JNI_OnLoad` on its own, without any trigger from you.
|
||||
|
||||
## Where integrity evidence actually comes from
|
||||
|
||||
Shells commonly check a mix of: APK signature, `AndroidManifest.xml`, their own native libraries, the
|
||||
dex they load, and environment signals (root, emulator, hooking frameworks, debugging).
|
||||
|
||||
Practical consequences:
|
||||
|
||||
- **Your own re-signing is itself a detected change** in most signature-based schemes. Assume any
|
||||
signature check will see your build as tampered and plan around it rather than hoping otherwise.
|
||||
- **The environment matters.** Hooking frameworks, debugging, and non-standard filesystems are commonly
|
||||
probed. A patch that works on a clean device may be refused on a rooted one, and vice versa.
|
||||
- **Absence of a log line is not absence of a check.** A shell that fails silently looks identical to a
|
||||
shell that never checked.
|
||||
|
||||
## Locating what the dialog/detection is attached to
|
||||
|
||||
Blocking dialogs ("tampered", "third-party environment", "abnormal") are the usual user-visible symptom.
|
||||
Establish **which layer draws it** before trying to suppress it:
|
||||
|
||||
- If it is a Java dialog, hooking the dialog class shows a caller stack that names the detection.
|
||||
- If hooking the dialog classes produces **no** hits, the UI is likely drawn by a cross-platform runtime
|
||||
(see `framework-runtimes.md`) or by native code, and Java-level hunting is the wrong axis entirely.
|
||||
|
||||
The cheapest high-value experiment, and one that is routinely skipped: run the **unmodified official
|
||||
build** and the **repackaged build** through the *same* probe and diff what differs. The difference is
|
||||
your detection signal. Doing this early is far cheaper than enumerating candidate checks one by one.
|
||||
|
||||
## Reporting
|
||||
|
||||
State the boundary you measured, the evidence for each entry, and what remains untested. "The shell
|
||||
rejects X" must always carry the run that proves it, including the exact failure string. An unverified
|
||||
claim about integrity behaviour is worse than no claim, because it silently removes routes from
|
||||
consideration.
|
||||
Reference in New Issue
Block a user