Broaden skill with unpacking, TLS triage, third-party build auditing, runtime probe
references: recon gains packer identification + unpacking workflow (payload/plaintext size mapping, dump validation, hash de-duplication, business-dex heuristic); repack-and-sign gains the de-shelled repack pipeline, why apktool over binary AXML, and the apksigner scheme-selection caveat; new tls-and-cert documents feature-scoped network failures, dual trust chains and the fix template; new third-party-builds covers auditing a cracked/modded APK before trusting it; dynamic-frida gains version pinning, remote-device path, the four-layer probe and hook-never-fires triage; environment gains UI automation pitfalls; ad-removal gains the single convergence point, dynamic-plugin SDKs and the global timestamp gate; pitfalls gains P13-P17; verification gains rung 7 and the three-signal ad check. scripts: new frida_probe.js (four-layer runtime probe), run_probe.py and tls_check.py; repack.py fixes two crash-on-default bugs and a misleading META-INF note; datastore_inject.py, PatchMethodExample.java and build_scripts.py lose hardcoded values - the example class name did not match its file and could not compile.
This commit is contained in:
+49
-4
@@ -23,13 +23,57 @@ Read `AndroidManifest.xml` → `application android:name`.
|
||||
|---|---|
|
||||
| The app's own class (e.g. `com.example.app.App`) | **No packer.** Dex is directly editable. |
|
||||
| A vendor class (`com.stub.StubApp`, `com.secneo...`, `com.tencent.StubShell`, `s.h.e.l.l.*`, `com.nagain.*`, …) | **Packed.** Unpack first. |
|
||||
| `android:appComponentFactory` overridden to a vendor class | **Packed or wrapped** — often a runtime shell that loads a second APK. |
|
||||
| `assets/` contains a second `.apk`, `.jar`, or an encrypted blob | Wrapped/loader build. |
|
||||
| A short meaningless name that does not match the app's package (e.g. `com.bc41`), plus `android:appComponentFactory` pointing at another short class | **Dex-level whole-APK packer.** The shell registers its own Application/proxy under names unrelated to the app. |
|
||||
| `classes.dex` is only tens of KB and holds nothing but shell classes | The real code is not on disk. |
|
||||
| A shell library in `lib/<abi>/` (`libDexHelper.so`, `libjiagu*.so`, `libshell*.so`, …) | Native half of the shell: decrypts and loads the payload. |
|
||||
| One or more large high-entropy `assets/` blobs, possibly **disguised as JPEG** (valid magic bytes, no decodable image) | Encrypted payload. |
|
||||
| `assets/` contains a second `.apk` or `.jar` | Wrapped/loader build. |
|
||||
|
||||
Signs in `lib/`: lone `.so` files with names unlike normal libraries (a custom-named shell lib), plus unusual `assets/` artifacts.
|
||||
Decide from sizes and structure, not from names:
|
||||
|
||||
```bash
|
||||
unzip -l app.apk | sort -k1 -n | tail -20 # biggest entries: assets? libs? dex?
|
||||
```
|
||||
|
||||
**Loaded-shell check (highest fidelity):** if the app runs and its own classes are reachable, it is unpacked *at runtime* regardless of what the manifest says. If the app's own code is not among the dex classes, it is packed on disk.
|
||||
|
||||
### Unpacking a dex-level packer
|
||||
|
||||
**1. Map payload → dumped dex by length.** If the shell uses **length-preserving** encryption, each encrypted `assets/` payload has **exactly the same byte count as the plaintext dex** it decrypts to. That equality is the strongest single discriminator for "which dump is a real original dex" — far more reliable than sorting dumps by size. Measure it per file before trusting any dump.
|
||||
|
||||
**2. Check how the real dex is loaded.** A log line like `InMemoryDexFile[cookie=[0, ...]]` means the dex is built in memory from the payload and never written to disk, so there is no dropped file to find. Use a **memory-scanning** dumper (`frida-dexdump`, or your own script walking the class loaders) instead of hunting for artifacts under the app's data directory. See `references/dynamic-frida.md`.
|
||||
|
||||
**3. Two dumper caveats that cost real time.**
|
||||
- `-D <device-id>` does **not** accept a remote device (`Device '<id>' not found`). Register a remote device with `-H <host>:<port>`; keep `-D` for local/USB ids.
|
||||
- The `-o <dir>` output directory **must already exist** — the tool does not create it. Otherwise every dex write fails and the run still ends with `All done`, which reads exactly like a successful dump of nothing. `mkdir -p out` first, and judge the result by the file count, never by the final message.
|
||||
|
||||
**4. Filter the dumps before using any of them.** A memory dump contains three kinds of garbage:
|
||||
- **Duplicate copies of one dex.** Identical byte count does **not** imply identical content — files of the same size have been measured to differ in ~73% of their bytes, carry a different dex version, and fail to parse. Dedupe by **content hash**, never by size.
|
||||
- **SDK plugin dexes downloaded at runtime**, which were never part of the APK.
|
||||
- **Structurally broken dexes** whose header and body disagree.
|
||||
|
||||
Cheap header check that catches the third class, plus the hash used for the first:
|
||||
|
||||
```python
|
||||
import struct, hashlib
|
||||
d = open(p, 'rb').read()
|
||||
assert d[:4] == b'dex\n' # magic
|
||||
assert struct.unpack_from('<I', d, 32)[0] == len(d) # file_size == actual size
|
||||
assert struct.unpack_from('<I', d, 36)[0] == 112 # header_size
|
||||
sha = hashlib.sha256(d).hexdigest()
|
||||
```
|
||||
|
||||
Then confirm survivors actually parse (`baksmali`/dexlib2) or load on device. Header consistency alone is not proof of a usable dex.
|
||||
|
||||
**5. Which dump is the app's own business dex.** Count string-feature hits per dump instead of trusting file order:
|
||||
|
||||
```bash
|
||||
python scripts/dex_strings.py <dump_dir> --find 'Lcom/example/app/' --per-file
|
||||
python scripts/dex_strings.py <dump_dir> --find 'Lcom/example/adsdk/' --per-file
|
||||
```
|
||||
|
||||
Heuristic: the business dex mixes the app's package prefix (hundreds of hits) with an ad-SDK prefix (thousands of hits). A pure SDK plugin dex has one vendor prefix at its maximum count and **zero** app-package hits. Zero app-package hits means it is not the app.
|
||||
|
||||
## 3. Where does the app's own code live?
|
||||
|
||||
| Indicator | Code location |
|
||||
@@ -48,7 +92,7 @@ For a **Kotlin/Java app**, note the module layout: app code often lives in a sma
|
||||
python scripts/dex_strings.py <dex_dir> --find 'Lcom/example/app/' --per-file
|
||||
```
|
||||
|
||||
Consequence for editing: you usually only need to replace **one or two dex files**, which keeps the repack minimal.
|
||||
Consequence for editing: you usually only need to replace **one or two dex files**, which keeps the repack minimal. When the app is packed, run this check against the **dumped** dexes, not the ones on disk (§2).
|
||||
|
||||
## 4. SDK and library inventory
|
||||
|
||||
@@ -102,6 +146,7 @@ Write a short profile before patching. Minimum:
|
||||
```
|
||||
package / version / ABI
|
||||
packed? (evidence)
|
||||
if packed: packer family, unpack method, kept dumps + sha256
|
||||
code location (dex / flutter / native)
|
||||
app code in which dex files
|
||||
ad SDK(s) and the app's wrapper class
|
||||
|
||||
Reference in New Issue
Block a user