Files
apk-reverse/references/recon.md
T
inliver233 1a5cb6245f 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.
2026-09-19 21:44:00 +08:00

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 -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:

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: openadsdk TTAdSdk Pangle com.qq.e GDTAd anythink ATSDK ksad mobads sigmob bdxadsdk
  • Analytics / crash / attribution: umeng bugly crashsdk appsflyer adjust UMCrash
  • Identity / device id: oaid msa SupplementaryDID freemme
  • Anti-tamper / root / hook detection: libsgcore libInno libqmcheat, strings like frida xposed magisk substrate, /system/xbin/su
  • Media stack: libmpv libavcodec libplayer libgdx

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: Bearer plus a static app-name header, then a repackaged client is not distinguishable to the server by request signature — which matters for references/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)