Add deliverable-form discipline, execution identity, and four reusable scripts
Generalized from a long hardened-target engagement. Additions target the failure
modes that cost the most time and are not yet covered; nothing here is sample-specific.
New discipline
- SKILL.md: a third override rule -- deliver in the environment the request requires.
A root-only or live-instrumentation result can look like success while failing the
actual requirement (unrooted, installable, self-contained). Also adds two classify
questions: what form the deliverable must take, and which architecture/library is
actually executing.
- long-task-discipline.md: "the most expensive drift" -- a constraint table (privilege,
modification form, ABI, network, persistence, distribution), the checkpoint question,
and how to report a privileged workaround as a fallback rather than a deliverable.
Plus "bound every wait": timeouts on every command, budgets calibrated from
measurement so slow and hung are distinguishable, and sampling an observable you can
inspect instead of sleeping blind.
- environment.md: look at the screen instead of driving blind; which of the two screen
evidence kinds is trustworthy (an empty accessibility tree is common for
runtime-rendered UI, where the image is primary rather than a fallback); emulator
console control and adb recovery; preflight before every experiment block.
- native-and-so.md: what the device claims vs what the package manager chose vs what is
actually mapped; translation layers and their consequences for hooks and native
checks; fat-APK ABI selection.
- runtime-data.md: when the app rewrites your edit (server refresh vs local
regeneration), why chmod/chown cannot work because the app deletes and recreates the
file, and the immutable-attribute fix; decoding a value that only looks encrypted.
- framework-runtimes.md: string encoding traps (a UTF-8 scan of a UTF-16 snapshot
returns nothing and reads as "the strings were stripped"), and non-ASCII obfuscated
identifiers -- resolve by shape, not by name.
- pitfalls.md: P23 the fix lives in a file the app rewrites; P24 the artifact changed
but the wrong one is executing; P25 "search found nothing" treated as "not there".
New scripts (parameterized, path-agnostic, all with explicit timeouts)
- preflight.py environment check: device, root, ABI/translation, clock skew, leftover
proxy/forwards, dead device server, chosen ABI, free space
- lib_map.py what is actually mapped into a live process: path, base, architecture
from the ELF header, and whether it came from the APK or was
materialized at runtime
- blob_decode.py search (don't guess) the framing of a stored value, then re-encode the
edited payload with the same parameters
- snap.py bounded burst screenshots + control-tree capture, with a stall detector
and an explicit verdict on whether the tree is usable
All four were exercised against a live device while writing: preflight correctly
flags multi-device ambiguity and detected the ARM translation layer; lib_map read real
ELF headers and identified the translation shim; blob_decode round-tripped a
rotation+deflate+base64 blob at realistic size in ~0.2s; snap detected a stalled screen
across three hashes.
This commit is contained in:
@@ -493,3 +493,113 @@ an impression of work being done.
|
||||
- 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.
|
||||
|
||||
---
|
||||
|
||||
## P23. The fix lives in a file the app rewrites
|
||||
|
||||
**Symptom**
|
||||
You change a value in the app's data, the write succeeds, the file reads back correctly — and after
|
||||
the next launch the value is back to what it was. Often byte for byte identical, which makes it look
|
||||
like nothing happened at all.
|
||||
|
||||
**Root cause**
|
||||
The stored value is a **cache, not a source of truth**. Either the app re-fetches it and re-persists
|
||||
it, or it rewrites the file from its own defaults on every start. Your edit was never authoritative.
|
||||
|
||||
Compounding it: the intuitive way to protect the file — a restrictive mode or a changed owner — **does
|
||||
not work**, because the app does not open-and-write the existing file. It **deletes the file and
|
||||
creates a new one**, and a new file is created with the app's own mode and owner. Nothing is inherited,
|
||||
so `chmod`/`chown` are silently ineffective.
|
||||
|
||||
**Why it is hard to see**
|
||||
Verification is usually done immediately after writing, while the file is still correct. The rewrite
|
||||
only happens on the next start, which is one step further along than you looked. And the "fix" that
|
||||
seems obviously right (tighten permissions) fails without any error.
|
||||
|
||||
**Do instead**
|
||||
- **Verify after a relaunch, not after the write.** The write succeeding is not the finding; surviving
|
||||
a restart is.
|
||||
- **Distinguish the two causes with one offline launch.** If the value survives with the network down,
|
||||
it came from the server. If it does not, the app is regenerating it locally — and then a data edit is
|
||||
the wrong layer entirely; patch the read path instead.
|
||||
- To make a data edit stick, use the **immutable attribute** and confirm it took effect:
|
||||
```bash
|
||||
su -c "chattr +i <file>"; su -c "lsattr <file>" # expect the 'i' flag
|
||||
```
|
||||
It is enforced by the filesystem against the delete itself, which is why it holds where permissions
|
||||
do not. Undo with `chattr -i`.
|
||||
- Then **exercise the feature**, not just the value. A blocked write the app depends on can make it
|
||||
misbehave; "the file still has my value" is not "the app still works".
|
||||
- Remember a lock is **device state, not artifact state** — it does not travel with an APK. Record it
|
||||
as an environment requirement. The only form that ships is a code patch (`runtime-data.md`).
|
||||
|
||||
---
|
||||
|
||||
## P24. The artifact changed, but the wrong one is executing
|
||||
|
||||
**Symptom**
|
||||
The build differs from the original, the pipeline reports success, the file on disk is genuinely
|
||||
modified — and the app behaves exactly as before. Or a native hook reports nothing while the feature
|
||||
plainly runs.
|
||||
|
||||
**Root cause**
|
||||
Something other than your edit is being used at runtime:
|
||||
|
||||
- **Wrong ABI.** A fat APK ships several `lib/<abi>/` directories; the package manager extracts and
|
||||
loads **one**. Editing `arm64-v8a` while the device loads `armeabi-v7a` produces a byte-different,
|
||||
behaviorally identical build.
|
||||
- **The library is not from the APK at all.** Some libraries are written into the app's data directory
|
||||
at runtime rather than extracted from the package. Patching the APK copy changes a file nobody loads.
|
||||
- **Multiple processes.** The work was done in, or the check lives in, a different process than the one
|
||||
you are observing.
|
||||
- **A stale install.** The package manager reported success for a request that did not replace what is
|
||||
on disk (`P18`).
|
||||
|
||||
**Why it is hard to see**
|
||||
Every local indicator agrees: the diff is non-empty, the build is signed, the install returned success.
|
||||
Nothing in the *build* pipeline can detect this, because the build is fine. The contradiction only
|
||||
exists at runtime.
|
||||
|
||||
**Do instead**
|
||||
- **Ask the running process what it loaded**, before editing: `scripts/lib_map.py --pkg <pkg>`.
|
||||
Libraries whose path is not under the installed APK's lib directory were materialized at runtime
|
||||
and belong to whatever produced them.
|
||||
- Confirm the ABI the package manager actually chose (`dumpsys package <pkg> | grep primaryCpuAbi`)
|
||||
rather than the one you assumed from the manifest.
|
||||
- **If the library you patched is not in the live mapping, stop.** No amount of re-patching helps; the
|
||||
plan is wrong.
|
||||
- Treat a behaviorally identical rebuild as **positive evidence that your edit is not being loaded**,
|
||||
not as "the change had no effect". Those are different conclusions and only one of them is actionable.
|
||||
|
||||
---
|
||||
|
||||
## P25. "The search found nothing" treated as "the data is not there"
|
||||
|
||||
**Symptom**
|
||||
You scan an artifact for a known-present value — a UI label, a marker string, an endpoint — get zero
|
||||
hits, and conclude the content is stripped, encrypted, or otherwise unavailable. A route gets written
|
||||
off on that basis.
|
||||
|
||||
**Root cause**
|
||||
The search used the wrong representation. A byte scan for UTF-8 text returns nothing against content
|
||||
that is stored as UTF-16, or compressed, or framed inside a container, or split across fragments. The
|
||||
data is present; the needle was encoded differently from the haystack.
|
||||
|
||||
**Why it is hard to see**
|
||||
"Zero results" is a clean, confident-looking output. It feels like a measurement, so it gets recorded
|
||||
as a finding, and findings propagate into the plan.
|
||||
|
||||
**Do instead**
|
||||
- **Before concluding absence, search more than one encoding.** UTF-8 and UTF-16LE will between them
|
||||
cover most text storage:
|
||||
```python
|
||||
blob.find(needle.encode('utf-8')), blob.find(needle.encode('utf-16-le'))
|
||||
```
|
||||
- **Search the shortest distinctive fragment.** Text is often assembled from pieces or templates, so a
|
||||
full sentence can be absent while its parts are present.
|
||||
- **Try the value without its framing.** A hit rate of zero is also the expected result for content
|
||||
that is inside a compressed or encoded container — decode the container first (`runtime-data.md`,
|
||||
`scripts/blob_decode.py`).
|
||||
- **State a negative result with its scope**: "no UTF-8 or UTF-16LE literal match in this artifact"
|
||||
is a finding. "The string does not exist" is a guess wearing a finding's clothes.
|
||||
|
||||
Reference in New Issue
Block a user