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,88 @@
|
||||
# Cross-Platform Runtimes
|
||||
|
||||
Load this when the app's UI is not native, or when Java-layer hooks produce no hits at all.
|
||||
|
||||
Many apps ship a native shell plus a cross-platform runtime. Their business logic, their UI, and their
|
||||
feature gates live **inside the runtime**, not in the dex. Patching the dex then does nothing — the Java
|
||||
code you can see is mostly glue. Determine the runtime before choosing where to patch.
|
||||
|
||||
## Identify the runtime early
|
||||
|
||||
Signals in the APK:
|
||||
|
||||
| Runtime | Giveaways |
|
||||
|---|---|
|
||||
| Flutter | `libflutter.so`, `libapp.so`, `assets/flutter_assets/`, Dart snapshot |
|
||||
| React Native | `libhermes.so` or `libjsc.so`, `index.android.bundle`, `assets/index.android.bundle` |
|
||||
| Unity | `libunity.so`, `libil2cpp.so`, `assets/bin/Data/`, `globalgamemanagers` |
|
||||
| Cordova / Ionic | `assets/www/`, `cordova.js` |
|
||||
| Xamarin / .NET | `libmonodroid.so`, `assemblies/` in the APK |
|
||||
|
||||
A `classes.dex` that is small relative to the total payload is another strong hint that the real code
|
||||
lives elsewhere.
|
||||
|
||||
## The layer trap
|
||||
|
||||
The expensive mistake: seeing a UI element (dialog, paywall, gate) and assuming it is a native control,
|
||||
then spending a long time looking for it in the dex.
|
||||
|
||||
If it is drawn by the runtime, the dex contains only the bridge. Symptoms of being on the wrong layer:
|
||||
|
||||
- hooking the obvious Java dialog/Activity classes produces **zero** hits while the UI clearly appears;
|
||||
- the classes that do appear in stacks are the runtime's own, with obfuscated names;
|
||||
- hooking a Java method changes nothing about the visible behaviour.
|
||||
|
||||
Establish layer ownership **before** investing in a patch direction.
|
||||
|
||||
### How to tell which layer drew a given UI
|
||||
|
||||
1. Enumerate the Java dialog and presentation classes and hook their show/creation paths.
|
||||
2. Reproduce the UI. If any hook fires, the caller stack names the detection and you are on the Java
|
||||
layer.
|
||||
3. If nothing fires while the UI is on screen, the UI is runtime-drawn or native-drawn. Switch axis.
|
||||
4. Expect runtime-owned windows to appear as ordinary system `Dialog`/`Presentation` objects used as
|
||||
**containers** for the runtime's surface. A hit on such a container is not evidence that the visible
|
||||
element is a native dialog — read the class name carefully before drawing conclusions.
|
||||
|
||||
## Flutter specifics
|
||||
|
||||
- Business logic is compiled ahead-of-time into **`libapp.so`**; UI is rendered by `libflutter.so` with
|
||||
no per-widget Java objects.
|
||||
- **Widgets, dialogs, and paywalls are not Java views.** They will never appear in Java stack traces,
|
||||
and no Java hook can intercept them.
|
||||
- The Java side is thin: one Activity, plus plugin classes. Calls cross the boundary through the
|
||||
platform-channel mechanism, whose class names are frequently obfuscated.
|
||||
- Because channel classes may be renamed, locate them **by method signature and call shape**, not by
|
||||
class name.
|
||||
|
||||
### Where to intervene in a Flutter app
|
||||
|
||||
| Goal | Layer | Notes |
|
||||
|---|---|---|
|
||||
| Stop the app being killed / refused at startup | host native library | earliest point, see `native-and-so.md` |
|
||||
| Observe what the runtime is told | platform channel | shows the messages that drive the UI |
|
||||
| Change a decision permanently | `libapp.so` | most direct, hardest to locate |
|
||||
| Change startup behaviour only | host native library constructor | survives runtime restarts |
|
||||
|
||||
Editing `libapp.so` is the most durable but locating Dart AOT code is genuinely hard: strings are the
|
||||
most reliable entry, because Dart string objects and the code referencing them can be found by scanning
|
||||
the snapshot rather than by pattern-matching machine code.
|
||||
|
||||
## Locating logic without symbols
|
||||
|
||||
- **Strings first.** User-visible text (dialog bodies, feature labels) is the cheapest anchor in any
|
||||
runtime. Find it, then find what references it.
|
||||
- **Compare two builds.** The same feature in a slightly different version often reveals the code path.
|
||||
- **Watch the boundary, not the interior.** For cross-platform apps it is usually far cheaper to observe
|
||||
what crosses between layers than to reverse the interior of the runtime.
|
||||
- **Do not assume the obfuscated names are stable or meaningful.** They are not, and treating them as
|
||||
identities leads to conclusions that break on the next build.
|
||||
|
||||
## What this changes about your plan
|
||||
|
||||
- Budget for the runtime layer **up front**. If the app is Flutter/RN/Unity, the dex is not the main
|
||||
battlefield and a dex-only plan will stall.
|
||||
- Any behavioural claim must be validated **through the runtime's own UI**, because that is what the
|
||||
user sees. A patch that changes internal state without changing the UI is not a fix.
|
||||
- Keep the layer you are working in explicit in your notes. "Patched the paywall" is meaningless without
|
||||
saying which layer owned it.
|
||||
@@ -0,0 +1,110 @@
|
||||
# Long-Task Discipline
|
||||
|
||||
Load this when a task is likely to run long: many rounds, many experiments, or a conversation that will
|
||||
exceed what can be held in context. Also load it before resuming a task someone else (or a past you)
|
||||
started.
|
||||
|
||||
The failure mode this file prevents is not tooling — it is **judgement drift**. Over a long task, three
|
||||
things reliably go wrong, and each is expensive in a way that is invisible while it happens:
|
||||
|
||||
1. **You re-derive what you already proved.** Rounds get spent rediscovering a boundary or a mechanism
|
||||
that was established earlier and then lost.
|
||||
2. **You re-walk a route that was already excluded.** The exclusion is real, but the reason has been
|
||||
forgotten, so the route looks promising again.
|
||||
3. **A wrong conclusion keeps steering.** An early mis-attribution is written into your mental model and
|
||||
silently removes good options, or props up bad ones, for a long time afterwards.
|
||||
|
||||
All three come from the same cause: **conclusions living in context instead of on disk**.
|
||||
|
||||
## Keep a live record, not a log
|
||||
|
||||
Maintain one artifact — a single file — that is the task's authoritative state. Update it as things are
|
||||
learned, not at the end. What matters is not size; it is that every entry is **actionable later**.
|
||||
|
||||
Keep these sections:
|
||||
|
||||
| Section | Contents | Why |
|
||||
|---|---|---|
|
||||
| **Operating rules** | things that must be true every run (how to install, how to launch, how to capture) | prevents repeating a mechanical mistake |
|
||||
| **Confirmed facts** | each with the run that proved it | the basis everything else builds on |
|
||||
| **Refuted conclusions** | what you believed, why it was wrong, what replaced it | stops a dead idea from coming back |
|
||||
| **Dead routes** | ruled out, with the evidence | the single biggest time saver |
|
||||
| **Open questions** | what is still unknown | keeps the next step honest |
|
||||
| **Environment** | device, ports, tool paths, credentials locations | avoids re-discovery |
|
||||
|
||||
Two rules make this file worth having:
|
||||
|
||||
- **Every claim carries its evidence.** "The shell rejects this" must include the run that showed it,
|
||||
including the exact failure text. An unattributed claim is how a false conclusion gets adopted later.
|
||||
- **Refutations are first-class entries.** When you find that an earlier conclusion was wrong, record
|
||||
*both* the wrong conclusion and why it was wrong. Deleting the mistake loses the most valuable thing
|
||||
you learned.
|
||||
|
||||
## Grade your own conclusions
|
||||
|
||||
Label every non-obvious statement with its strength, and never let a weaker label inherit the authority
|
||||
of a stronger one:
|
||||
|
||||
| Grade | Meaning | May it justify a decision? |
|
||||
|---|---|---|
|
||||
| **Observed** | reproduced it, with the exact command and output | yes |
|
||||
| **Inferred** | follows from an observation, but the step is reasoned | provisionally |
|
||||
| **Hypothesis** | plausible, untested | only as something to test |
|
||||
| **Refuted** | tested and found false | never — keep it only as a warning |
|
||||
|
||||
Most long-task damage comes from hypotheses drifting upward into "facts" simply by being repeated. If
|
||||
you catch yourself referring to something as established, check the grade in the record. If it is not
|
||||
observed, go observe it or label it again.
|
||||
|
||||
## Single-variable discipline across the whole task
|
||||
|
||||
The most common source of a wrong *and durable* conclusion is a compound experiment: two changes, one
|
||||
failure, one invented explanation. Because the failure is real, the explanation feels earned.
|
||||
|
||||
Consequences to enforce:
|
||||
|
||||
- When a route is about to be abandoned, **re-read why it was abandoned**. If the evidence was compound,
|
||||
the abandonment is not yet justified — re-run it single-variable before writing it off.
|
||||
- Keep a semantic control in your pipeline: a build with **no** changes, run through the same
|
||||
install-and-launch path. If the control fails, nothing else you measure is meaningful.
|
||||
|
||||
## Guard against drift at natural checkpoints
|
||||
|
||||
Do these cheaply, and only when they can change a decision — not as ceremony.
|
||||
|
||||
- **Before starting a new experiment**: read the refuted-conclusions and dead-routes sections. If your
|
||||
plan appears there, stop and read why.
|
||||
- **Before declaring progress**: ask what the *user-visible* outcome is right now. An internal signal
|
||||
improving is not progress (see P19). Re-state the actual target.
|
||||
- **After any surprise**: write it down immediately with evidence, before you have a theory. Theories
|
||||
written after the fact are hard to distinguish from observations.
|
||||
- **When context feels long**: prefer writing to the record over re-reading conversation. The record is
|
||||
what survives; the conversation is what gets truncated.
|
||||
- **When resuming**: read the record first, and treat anything not in it as unknown, even if it feels
|
||||
familiar.
|
||||
|
||||
## Handover
|
||||
|
||||
A handover is the record plus three things, written for someone with **no** memory of the task:
|
||||
|
||||
1. **Operating rules first** — anything mechanical that silently wastes time if unknown.
|
||||
2. **Honest current state** — including what is *not* achieved. An optimistic summary costs the next
|
||||
person far more than an accurate one, because they will build on it.
|
||||
3. **The next best step, and why** — plus the dead routes, so they do not start there.
|
||||
|
||||
Two things belong in every handover, because they are the hardest to recover:
|
||||
|
||||
- **What you got wrong**, with the evidence that corrected it.
|
||||
- **What you never actually verified**, stated plainly. "Never confirmed" is more useful than silence.
|
||||
|
||||
## Avoiding the opposite failure
|
||||
|
||||
Discipline can itself become a burden. Guard against that too:
|
||||
|
||||
- **Do not log everything.** Record only what would change a future decision or prevent a repeated
|
||||
mistake. A record nobody reads is worse than no record, because it looks like coverage.
|
||||
- **Do not treat the record as authority.** It is a summary; the environment is ground truth. If the
|
||||
record and a fresh observation disagree, re-test — do not defend the record.
|
||||
- **Do not let grades calcify.** A hypothesis that becomes testable should be tested, not archived.
|
||||
- **Do not let the record's structure dictate the work.** If a section is empty because it is not
|
||||
relevant to this task, leave it empty.
|
||||
@@ -0,0 +1,121 @@
|
||||
# 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.
|
||||
@@ -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.
|
||||
@@ -359,3 +359,137 @@ All of these produce the same observable: nothing happens. A zero-byte screensho
|
||||
- Compare field values, not just presence — one case that burned an hour was two password fields of different length, causing local validation to `return` before any network call.
|
||||
- Treat "no visible change" as unproven, not as a negative result: confirm with an independent signal (logcat, a runtime probe, or a server-side request appearing in the capture).
|
||||
- If a tap does not register, fall back to launching the Activity directly or invoking the handler, rather than retrying coordinates.
|
||||
|
||||
---
|
||||
|
||||
## P18. The package manager reported success, but the build was never installed
|
||||
|
||||
**Symptom**
|
||||
Every install logs `Success`, so you run the next experiment and read its result — but the app being
|
||||
tested is still the previous build. Screenshots show stale UI or the launcher, and results look
|
||||
"unchanged".
|
||||
|
||||
**Root cause**
|
||||
On many ROMs a package installer interposes its own confirmation. The install command can return success
|
||||
for the *request*, while the actual install waits on a prompt — sometimes a password or account
|
||||
confirmation — that nobody fills in. The app stays at its old version indefinitely.
|
||||
|
||||
This is the most expensive failure in this skill's history: because the command "succeeded", the stale
|
||||
behaviour was measured across many rounds, and each measurement looked like a genuine negative result.
|
||||
|
||||
**Why it is hard to see**
|
||||
The success signal is real; it just answers a different question than the one you asked. Nothing in a
|
||||
normal install/launch script distinguishes "installed" from "install requested".
|
||||
|
||||
**Do instead**
|
||||
- After installing, **prove the artifact changed**: compare `dumpsys package <pkg> | grep -E 'versionName|lastUpdateTime'` before and after, or hash the on-device APK and compare it to what you built.
|
||||
- If a confirmation UI exists, drive it explicitly (type the credential, press the confirm control) and then re-verify.
|
||||
- Make the check a precondition of the run, not an afterthought: if the version did not change, **abort** rather than measuring.
|
||||
- Keep build artifacts named after the change they contain so a stale install is obvious from a screenshot.
|
||||
|
||||
---
|
||||
|
||||
## P19. Substituting an internal signal for the user-visible outcome
|
||||
|
||||
**Symptom**
|
||||
An error disappears from the log, no crash is recorded, and you report the problem solved. The user
|
||||
immediately shows you the same problem still on screen.
|
||||
|
||||
**Root cause**
|
||||
The internal signal and the user-visible outcome are different claims. Suppressing one error path does
|
||||
not remove the symptom if the symptom is produced by a **different** path — and a blocking dialog often
|
||||
is. The log going quiet proves that one code path was affected; it says nothing about whether the user's
|
||||
problem is gone.
|
||||
|
||||
**Why it is hard to see**
|
||||
The signal is specific, measurable, and genuinely changed. It is a true statement being used to support a
|
||||
false one.
|
||||
|
||||
**Do instead**
|
||||
- Define "done" as the **user-visible behaviour**: the blocking UI is gone, the app reaches its normal
|
||||
screen, the feature works. Nothing else counts.
|
||||
- Treat the absence of a log line as absence of evidence, never as evidence of success.
|
||||
- When a symptom persists after an internal signal improves, assume there is **another** producer of the
|
||||
symptom and go find it, rather than assuming your fix is merely incomplete.
|
||||
- Also verify the opposite direction: confirm the original symptom is reproducible **before** you patch,
|
||||
so you know what disappearing would even look like.
|
||||
|
||||
---
|
||||
|
||||
## P20. "I did not capture it" treated as "it is not there"
|
||||
|
||||
**Symptom**
|
||||
Screenshots taken every few seconds after launch show no blocking dialog, so the dialog is declared gone —
|
||||
then it turns out to be present the whole time.
|
||||
|
||||
**Root cause**
|
||||
Sampling is not observation. A transient state that appears and is then covered (a second window, a
|
||||
navigation, a system prompt) can fall entirely between samples. The modal appears, gets occluded, and
|
||||
every frame you happened to take shows something else.
|
||||
|
||||
**Why it is hard to see**
|
||||
The captures are real and consistently show the same thing, which feels like corroboration. Conviction
|
||||
grows with the number of frames, even though all of them share the same blind spot.
|
||||
|
||||
**Do instead**
|
||||
- For anything time-sensitive, capture **continuously** (recording) or in a dense burst immediately after
|
||||
launch, not on a fixed slow interval.
|
||||
- **Look at every frame**, not only at file sizes. A byte-size cluster that "looks familiar" is not a
|
||||
reading.
|
||||
- State conclusions with their sampling: "not observed in N consecutive seconds of recording" is honest;
|
||||
"does not occur" is not.
|
||||
- When something is reported present by a human who is looking at the screen, believe the screen. Your
|
||||
capture gap is the more likely explanation.
|
||||
|
||||
---
|
||||
|
||||
## P21. Changing two things at once, then attributing the result
|
||||
|
||||
**Symptom**
|
||||
A build fails, and you conclude that the mechanism you were most curious about is the culprit — then
|
||||
exclude it from consideration for a long time. Later, a clean experiment shows it was the other change
|
||||
all along.
|
||||
|
||||
**Root cause**
|
||||
Two edits, one observation, no attribution. The failure is real; the explanation is invented. Worse, the
|
||||
invented explanation survives because it sounds plausible and no one re-tests it.
|
||||
|
||||
**Why it is hard to see**
|
||||
The experiment "worked" in the sense that it produced a result. Acting on a wrong attribution feels
|
||||
exactly like acting on a right one until much later.
|
||||
|
||||
**Do instead**
|
||||
- One variable per install-and-launch cycle. Where a combination is unavoidable, add a third run that
|
||||
isolates each half.
|
||||
- Write the attribution into your notes **with the run that proves it**. An unproven cause is a
|
||||
hypothesis; keep it labelled as one.
|
||||
- When a route is about to be discarded, re-check whether the evidence was actually single-variable. A
|
||||
discarded route with compound evidence should be re-opened before being abandoned.
|
||||
- Prefer semantically inert controls (a change nothing reads) to prove "edits of this class are allowed"
|
||||
separately from "this specific edit is allowed".
|
||||
|
||||
---
|
||||
|
||||
## P22. Waiting for something that requires a human to advance
|
||||
|
||||
**Symptom**
|
||||
An automation loop polls for minutes or longer, waiting for a state that never arrives on its own. Time
|
||||
is consumed while nothing at all can change.
|
||||
|
||||
**Root cause**
|
||||
The awaited state is gated on a human action — a consent prompt, a permission dialog, an installer
|
||||
confirmation, a captcha. No amount of waiting resolves it. Automated polling is the wrong instrument for
|
||||
a state whose transition is external.
|
||||
|
||||
**Why it is hard to see**
|
||||
Polling is cheap-looking and the loop reports progress (timestamps, unchanged screenshots), which creates
|
||||
an impression of work being done.
|
||||
|
||||
**Do instead**
|
||||
- Before waiting on a state, ask what would cause it to change. If the answer is "a person", stop waiting
|
||||
and either perform the action programmatically or hand it back.
|
||||
- Bound every wait with a deadline and an explicit failure branch that **does something different**, not
|
||||
just a longer timeout.
|
||||
- Detect stalls by change, not by elapsed time: if N consecutive samples are identical, break out.
|
||||
- Prefer driving the prompt to completion over waiting it out — the same prompt usually recurs, so
|
||||
automating it once pays back immediately.
|
||||
|
||||
Reference in New Issue
Block a user