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.
9.3 KiB
Recon — build the picture before touching anything
Ten minutes here prevents hours of wrong work. Answer the five questions from SKILL.md with concrete evidence.
1. Identity
# Without a full decompiler (fast, works everywhere):
aapt dump badging app.apk | head -20
# or, from the extracted manifest:
# package name, versionName, versionCode, sdkVersion, targetSdkVersion
Record: package name, version name/code, min/target SDK, all requested permissions, all declared components.
Note: some aapt builds choke on non-ASCII paths. Normalize sample paths to ASCII before running tooling.
2. Is it packed?
Read AndroidManifest.xml → application android:name.
| What you see | Meaning |
|---|---|
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. |
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. |
Decide from sizes and structure, not from names:
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-Dfor 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 withAll done, which reads exactly like a successful dump of nothing.mkdir -p outfirst, 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:
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:
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 |
|---|---|
Plain classes*.dex, app classes visible in them |
Dex — patchable with dexlib2/smali |
libflutter.so + libapp.so |
Flutter (Dart AOT) — dex contains only a thin shell |
libil2cpp.so + assets/bin/Data/ |
Unity (IL2CPP) — native |
libmono*.so + assets/bin/Data/Managed/ |
Unity (Mono) — managed DLLs under assets/ |
libreactnativejni.so + assets/index.android.bundle |
React Native — JS bundle under assets/ |
libhermes.so |
Hermes bytecode |
Almost everything in .so |
Native — different toolchain entirely |
For a Kotlin/Java app, note the module layout: app code often lives in a small subset of dex files while large SDKs occupy the rest. Finding which dex holds the app's package is a huge time-saver:
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. When the app is packed, run this check against the dumped dexes, not the ones on disk (§2).
4. SDK and library inventory
Extract strings across all dex and group by vendor markers. This reveals ads, analytics, crash reporting, attribution, and any anti-tamper SDK, in one pass.
python scripts/dex_strings.py <dex_dir> --urls
python scripts/dex_strings.py <dex_dir> --find 'anythink|openadsdk|com.qq.e|umeng|bugly|crashsdk'
What to look for:
- Ad networks / aggregators:
openadsdkTTAdSdkPanglecom.qq.eGDTAdanythinkATSDKksadmobadssigmobbdxadsdk - Analytics / crash / attribution:
umengbuglycrashsdkappsflyeradjustUMCrash - Identity / device id:
oaidmsaSupplementaryDIDfreemme - Anti-tamper / root / hook detection:
libsgcorelibInnolibqmcheat, strings likefridaxposedmagisksubstrate,/system/xbin/su - Media stack:
libmpvlibavcodeclibplayerlibgdx
Important distinction: a vendor marker in the string table does not mean the app uses that feature. Presence of xposed/frida strings is often just an SDK's own detection list. Decide based on behavior, not on strings (references/verification.md).
5. Signature and tamper checks
Search the app's own packages only (ignore third-party SDKs) for:
getPackageInfo,PackageInfo,signatures,GET_SIGNATURES,Signature→ app-side signature verification- hardcoded SHA-1/SHA-256 hex constants, base64 license blobs → certificate pinning to the original signer
checkSignature,verifySignature,signCheck→ a named check
Beware false positives. SignatureCheck and verifySignature exist inside okhttp3 (SuppressSignatureCheck, BasicCertificateChainCleaner.verifySignature) — TLS plumbing, not app tamper checks. Likewise a native lib*.so named like a security library may be an ad SDK's payload decryptor, not a shell.
Confirm by checking whether app code references it. If the only callers are library-internal, it is not your problem.
6. API surface
python scripts/dex_strings.py <dex_dir> --urls
Collect: base URLs, DoH/DoT lookups, path constants, CDN hosts, custom headers.
Treat unique strings as navigation aids — they can often be searched byte-wise in the dex to find the owning class (see references/dex-patching.md §finding-the-call-site).
Two patterns worth recognizing early:
- Dynamic gateway: the real API host is fetched at runtime (e.g. via a DNS TXT record over DoH). The hardcoded host in dex may be only a fallback. This affects server-side analysis, not patching.
- Minimal headers / no request signing: if auth is just
Authorization: Bearerplus a static app-name header, then a repackaged client is not distinguishable to the server by request signature — which matters forreferences/server-api.md.
Recon output
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
analytics/crash SDKs
anti-tamper: present? where? app-side or library-internal?
API base + notable endpoints
device plan (rooted real device / emulator / static only)