Close the byte-patching gap, add the server-config path, fix two script crashes
Worked end-to-end on a real unobfuscated commercial app (single-dex, no packer,
server-driven launch screen) and folded back everything that cost time. Net +17
files, no new dependencies, no sample-specific identifiers.
New references
byte-level-patching.md
The technique that was missing entirely. Equal-length in-place edits beat
method rebuilding (measured on a real sample: method rewrite grew debug_info
924 B -> 22.8 KB and the dex 4.32 MB -> 7.73 MB), but four failure modes are
silent: same-length is not enough, branch polarity, verifier legality, and
stale dex header fields. Covers locating an instruction's exact offset (why
listings and byte-pattern guesses both fail), the instruction width table and
the two traps that desynchronise a decode, neutralise-a-branch vs redirect-it,
and the header's signature-then-checksum order.
server-config-and-updates.md
The most common shape of "ad" in a modern app, and the one usually
mis-diagnosed: the server supplies UI the client renders. The two-layer fetch
(local fallback then remote override) that proves it, finding the config DTOs
through the field names data classes keep, why you patch the decision and not
the data, deciding the scope of "remove", and remote re-enable / cached config
durability.
New scripts
dexutil.py dependency-free dex reader: structural walk, exact
instruction decode, dex header recompute/verify in the
correct order, branch-target and operand helpers
dex_find_insn.py locate an instruction by decoded semantics, print its exact
byte offset with context and BOTH sides of any branch
dex_patch_bytes.py equal-length byte patches from a JSON spec: semantic match,
polarity pin via expect_next, equal-length enforcement,
verifier check, header recompute, re-decode to prove it landed
dex_check_verifier.py does any conditional branch target a move-result (bypassing
its producer)? Compares builds and separates pre-existing
findings from regressions the patch introduced
coldstart.py cold-launch capture: timed screenshot burst + logcat signals
+ installed-build facts + launch timing, and a warning when
the foreground activity is not the app
check_repo.py maintenance check: every script parses and answers --help,
every referenced path resolves, every file is documented
Fixes found while running the above
repack.py wrote archives with a plain zipfile writer, which cannot satisfy
the Android R+ install gate: resources.arsc must be STORED *and*
4-byte aligned, and a zip extra record cannot express a 1-3 byte
pad (a stored filler entry is inserted instead). Also fixes the
central-directory pointer and adds an alignment gate to the output.
usb_net_proxy.py, dex_strpatch.py
crashed on --help with a traceback instead of printing usage.
Updated
toolchain.md ddc added (single-binary dex->Java with query subcommands,
with measured timings and its real limits); "not on PATH is
not not-installed" with the search commands; which signer
to use and why jarsigner breaks alignment
repack-and-sign.md alignment requirement and its arithmetic; dex header
recompute; jarsigner hazard; vendor install interception
([N] with no symbolic constant -> root pm install); the
installer can keep owning the screen after an install
recon.md resolve package identity from a tool, not from hand-read
AXML strings (a wrong package sends all later device queries
nowhere); snapshot the config surface during recon
long-task-discipline.md captures you never looked at are not evidence (incl. the
consistent-but-wrong-window variant); long-context decay and
the record shape that survives it
dynamic-frida.md when live attach cannot work at all: static instrumentation,
static read, and self-recorded evidence, with the control
build caveat each one carries
ad-removal.md server-driven config is usually the EASIEST case, not the
hardest; a three-way zero result is a positive finding;
scope note on local brand splash vs promotional content
pitfalls.md P37 stale dex header checksum, P38 [-124] alignment,
P39 bare-numeric install failure, P40 wrong-window evidence,
P41 inverted branch polarity, P42 decode desynchronisation
SKILL.md, README.md symptom index, reference/script indexes and requirements
updated for all of the above
This commit is contained in:
+97
-1
@@ -30,12 +30,48 @@ cross-check with the program headers.
|
||||
| Tool | Invoke | Why this one |
|
||||
|---|---|---|
|
||||
| `baksmali` / `smali` (jars) | `java -cp <jars> org.jf.baksmali.Main d <dex> -o <dir>` | round-tripping, reading a method precisely |
|
||||
| `dexlib2` | small Java program | **the preferred patcher** — method-level rewrite, leaves everything else untouched (`dex-patching.md`) |
|
||||
| `dexlib2` | small Java program | method-level rewrite, leaves everything else untouched (`dex-patching.md`) |
|
||||
| **`ddc`** | `ddc app.apk -c <Class>` | **fastest read of dex as Java, plus query subcommands** — see below |
|
||||
| `apktool` | `java -jar apktool.jar d/b` | whole-app decode including resources |
|
||||
| `jadx` | `jadx --no-res -d <out> <apk>` | readable Java for orientation; **not** a source of truth (see below) |
|
||||
| `aapt2` | `aapt2 dump badging <apk>` | manifest facts, package name, versions |
|
||||
| `zipalign`, `apksigner` | from build-tools | alignment and signing |
|
||||
|
||||
### ddc — dex-to-Java with query subcommands (worth adopting)
|
||||
|
||||
A single-file Rust binary, no install, no JVM. Two things make it more useful than
|
||||
"a decompiler":
|
||||
|
||||
```bash
|
||||
ddc info app.apk # label, package, version, launcher, sdk, per-dex counts
|
||||
ddc findrefs app.apk string "SomeToken" # every reference site, class#method
|
||||
ddc strings app.apk -f "splash" --with-locations
|
||||
ddc app.apk -c com.example.Foo # one class as Java
|
||||
ddc app.apk -o out/ # full decompile, a few seconds for a normal APK
|
||||
```
|
||||
|
||||
Measured on a 4.7 MB APK (4,070 classes / 30k methods): `info` ~0.13 s, `findrefs`
|
||||
~0.11 s, one class ~0.25 s, full decompile ~2.8 s producing ~4,000 files.
|
||||
|
||||
Why the query subcommands matter more than the speed: **string cross-referencing
|
||||
becomes a lookup instead of a crawl.** Asking "which class mentions this config
|
||||
field name" is a sub-second command here and easily an hour of smali grepping by
|
||||
hand. That single property is what turns "find the convergence point" from
|
||||
exploration into a query.
|
||||
|
||||
`info` also **prevents a specific, costly mistake**: reading the package identity
|
||||
out of the binary manifest by hand. Hand-extracted AXML strings are ambiguous —
|
||||
class-name fragments look exactly like package names, and picking the wrong one
|
||||
sends every later `pm`/`dumpsys`/data-dir query to a package that does not exist.
|
||||
Let the tool report `package`, `label` and `launcher`, and cross-check with
|
||||
`aapt2 dump badging` when available.
|
||||
|
||||
Limits to plan around: types are erased (no generics); R8 short names stay short,
|
||||
so you still need string cross-references to infer meaning; output is a **reading
|
||||
aid, not compilable source** (occasional declaration/use ordering is inverted, and
|
||||
numeric resource ids appear as decimal integers); very large methods produce
|
||||
thousands of lines and are better approached via `findrefs`/`getmethod` first.
|
||||
|
||||
**Classpath gotcha.** `baksmali`/`smali` need their dependency jars on the classpath together
|
||||
(`smali`, `antlr-runtime`, `stringtemplate`, `baksmali`, `dexlib2`, `util`, `jcommander`, `guava`).
|
||||
Missing one produces `ClassNotFoundException` that reads like a broken target. `scripts/smtool.py`
|
||||
@@ -101,6 +137,66 @@ script, and check the device architecture — the server binary is per-ABI.
|
||||
for a different engine version produces output that is subtly wrong rather than obviously broken.
|
||||
Pin the version first (`dart-aot.md` §1) and do not "try it and see".
|
||||
|
||||
## "Not on PATH" is not "not installed"
|
||||
|
||||
`doctor.py` and `preflight.py` report whether each tool resolves on `PATH`. That
|
||||
is a statement about `PATH`, not about the machine, and the difference has cost
|
||||
real time: a full custom zip writer was built to work around a missing
|
||||
`apksigner` that was already installed in an SDK directory nothing had added to
|
||||
`PATH`.
|
||||
|
||||
**Before treating a tool as absent, search the filesystem once:**
|
||||
|
||||
```bash
|
||||
# POSIX
|
||||
find / -name apksigner -o -name zipalign -o -name baksmali.jar 2>/dev/null | head
|
||||
|
||||
# Windows (PowerShell) -- list every drive letter first, then search each
|
||||
(Get-PSDrive -PSProvider FileSystem).Root |
|
||||
ForEach-Object { Get-ChildItem -Path $_ -Recurse -Depth 5 -ErrorAction SilentlyContinue `
|
||||
-Include apksigner.bat,zipalign.exe,keytool.exe }
|
||||
```
|
||||
|
||||
Common places that are *not* on `PATH`:
|
||||
|
||||
| Artefact | Typical location |
|
||||
|---|---|
|
||||
| `apksigner`, `zipalign`, `aapt2` | any `build-tools/<ver>/` under an Android SDK, and portable tool bundles |
|
||||
| `apksigner.jar` | inside those same directories (runnable as `java -jar`) |
|
||||
| `keytool`, `jarsigner`, `javac` | a JDK install whose `bin` was never added to `PATH`; `java -XshowSettings:properties -version 2>&1 \| grep java.home` reveals the JDK root even when `keytool` does not resolve |
|
||||
| `uber-apk-signer.jar`, `baksmali*.jar` | bundled with portable APK toolkits |
|
||||
|
||||
Record the resolved absolute paths once, in your running notes, and pass them to
|
||||
the scripts that accept `--apksigner` / `--zipalign` / `--ks` style flags. Every
|
||||
script in this repository takes an explicit path for this reason.
|
||||
|
||||
**A useful asymmetry:** missing `apksigner` is survivable (a v2-only signature
|
||||
block can be appended without reordering the archive), but missing `zipalign` is
|
||||
not a reason to hand-roll alignment either -- write the archive aligned in the
|
||||
first place, as `scripts/repack.py` does. Prefer producing a correct archive over
|
||||
repairing one afterwards: post-hoc alignment tools rewrite the file, which
|
||||
changes every offset and can break a target that fingerprints its own layout.
|
||||
|
||||
## Signing: pick the signer deliberately
|
||||
|
||||
Two signers, and the choice changes the output bytes:
|
||||
|
||||
- **`apksigner`** (build-tools) appends the v2/v3 signature block after the zip
|
||||
content. It does **not** reorder entries, so a carefully aligned archive stays
|
||||
aligned. This is the default choice.
|
||||
- **`jarsigner`** (JDK) rewrites the archive as a side effect of adding the v1 JAR
|
||||
signature, which recompresses entries and **destroys the alignment** that
|
||||
Android R+ requires. A build signed this way can fail to install with
|
||||
`Failure [-124] ... resources.arsc ... aligned on a 4-byte boundary` even though
|
||||
the same archive installed fine moments earlier, and `jarsigner -verify` reports
|
||||
success. If you have run `jarsigner` on an archive and a previously-working
|
||||
install starts failing, re-pack and sign with `apksigner` before investigating
|
||||
anything else.
|
||||
|
||||
Enable **v1 + v2 + v3**. v1 keeps very old devices working; v2/v3 are what modern
|
||||
platforms actually verify, and v1-only builds are rejected in more configurations
|
||||
than people expect.
|
||||
|
||||
## The general trap: a tool's failure is not a finding about the target
|
||||
|
||||
A missing jar, a version skew, a forged section header, and a genuinely absent symbol all produce
|
||||
|
||||
Reference in New Issue
Block a user