Files
apk-reverse/references/environment.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

Environment — device, emulator, tooling, networking

Pick your target

Option Pros Cons
Rooted physical device, native ABI Truest behavior; native libs load natively; Frida works well One at a time; USB flakiness
Emulator with root Disposable; snapshots; easy reset Different ABI; some apps detect it or refuse to run; native ARM libs may need translation
Static only No device needed Cannot verify anything. Never claim success from static analysis alone.

Rule: the artifact must be verified where the user will run it. An emulator that runs the app is not evidence about a physical ARM device, and vice versa.

Important: if the app ships only arm64-v8a native libraries and your emulator is x86_64, it may still run via ARM translation — but translation changes timing, and some native checks misbehave. Prefer a real ARM device for the final verification pass.

ADB basics worth pinning down

adb devices -l
adb -s <serial> shell getprop ro.product.cpu.abi
adb -s <serial> shell getprop ro.build.version.release
adb -s <serial> shell getprop ro.build.version.sdk

Root:

adb -s <serial> shell "su -c id"

pm grant and pm install frequently require root on OEM builds; the shell user gets SecurityException.

Always specify -s <serial> when more than one device is attached — otherwise adb errors out or picks the wrong one.

Shell quoting (a real time sink)

The host shell expands $, |, >, and quotes before adb sees them. Windows PowerShell additionally mangles $var:, [^"..., and $(...).

Symptom: "Could not find a part of the path", "Missing type name after '['", "no closing quote".

Fix: never inline device commands in the host shell. Use a small helper that calls adb from a script file and passes the command as a single argument:

subprocess.run([ADB, '-s', SERIAL, 'shell', 'su -c "%s"' % cmd])

See scripts/devsh.py.

Same class of bug: javac reads sources using the platform default encoding. Pass -encoding UTF-8 or non-ASCII comments break the build.

Giving an offline device network over USB

Use when the device has no usable network (broken Wi-Fi, no SIM, restricted network) but your host does. adb reverse runs on the device's loopback, so it needs no device-side network interface at all.

# 1) host HTTP/HTTPS proxy (CONNECT-capable)
python scripts/usb_net_proxy.py 8080 proxy.log

# 2) forward device-localhost:8080 to host:8080
adb -s <serial> reverse tcp:8080 tcp:8080

# 3) point the device at it
adb -s <serial> shell "su -c 'settings put global http_proxy 127.0.0.1:8080'"

Notes:

  • adb reverse does not survive a device reboot — recreate it after any restart.
  • Keep the host proxy process alive; if it dies, the device goes offline again.
  • Some SDKs bypass the system proxy entirely (many ad SDKs do). Proxy logs therefore undercount traffic — do not conclude "no ad traffic" from proxy logs alone; confirm with a runtime DNS hook.
  • Clean up when done: settings put global http_proxy :0.

Emulator notes

  • Verify the app can install at all. INSTALL failures on emulators are common (ABI, min SDK, vendor checks) and are usually not related to your patch.
  • Snapshot/rollback is the main advantage — use it to A/B two builds quickly.
  • Never quote emulator behavior as proof for a device-only question (and vice versa).

Install / reinstall

adb -s <serial> shell "su -c 'pm uninstall <pkg>'"
adb push out.apk /data/local/tmp/x.apk
adb -s <serial> shell "su -c 'pm install -r -t -d /data/local/tmp/x.apk'"
  • -r reinstall (keep data, same signing key) · -t allow test-only · -d allow downgrade
  • Different signing key than the installed app → must uninstall first (data is lost)
  • Vendor installers may reject adb install; pushing + su -c pm install usually works

After reinstalling: if you restored a data directory, fix ownership, or the app crashes in a DB-init path:

su -c "chown -R <uid>:<uid> /data/user/0/<pkg>"
su -c "restorecon -R /data/user/0/<pkg>"

<uid> from dumpsys package <pkg> | grep userId=.

Driving the UI from adb

Automated UI interaction is where verification loops usually break, and most of the breakage looks like a bug in your script. Most of it is not.

input tap may simply not work on a given control

On some OEM ROMs input tap <x> <y> is silently ignored for certain controls while working fine for others at neighboring coordinates. Do not conclude that your automation is wrong. Before doubting the script:

adb -s <serial> shell "su -c 'uiautomator dump /sdcard/ui.xml'"
adb -s <serial> pull /sdcard/ui.xml
# bounds="[x1,y1][x2,y2]" -> tap the center: ((x1+x2)/2, (y1+y2)/2)
adb -s <serial> shell "su -c 'input tap <cx> <cy>'"

If the control still does not react, bypass the UI path entirely — am start -n <app.package>/<activity>, or invoke the logic from Frida. A finished task needs the code path, not the tap.

input requires INJECT_EVENTS, which the plain shell user does not have. An unprivileged input fails silently — no error, no effect. Always wrap it: su -c 'input tap ...'.

input text additionally mangles or drops non-ASCII input. For CJK text, either install an ADB-driven helper IME or set the field from a runtime call rather than typing.

Screen evidence without pixels

screencap returns a 0-byte file on some ROMs. First try writing on-device and pulling:

adb -s <serial> shell "su -c 'screencap -p /sdcard/x.png'" && adb -s <serial> pull /sdcard/x.png

If that is also empty, do not fight it: uiautomator dump's XML is better evidence — text, diffable, and it records the real control tree and its contents. (If the dump fails with could not get idle state, retry once; a paused animation is usually the cause.)

Verify form input by reading it back

Filling a form and tapping submit is not a verified interaction. Read the actual field contents and lengths back first:

adb -s <serial> shell "su -c 'input text <value>'"       # no literal spaces; %s encodes one
adb -s <serial> shell "su -c 'uiautomator dump /sdcard/ui.xml'"
adb -s <serial> pull /sdcard/ui.xml
grep -o 'text="[^"]*"' ui.xml                            # every populated field

A whole class of "the button does nothing" is really a local validation rejecting the input — two password fields of different length, a required field empty, a format check. The app returns before issuing any request, so a network probe stays silent and the tap looks broken. Compare the lengths you read back, fix the input, retry.

ROM background freezing kills your hooks

Aggressive ROMs freeze backgrounded apps; the logcat signature is a process state transition state: R -> F (running to frozen). Once frozen, hooks stop firing and network calls stop, which looks exactly like a broken probe.

adb -s <serial> shell "su -c 'dumpsys deviceidle whitelist +<app.package>'"
adb -s <serial> shell "su -c 'cmd appops set <app.package> RUN_IN_BACKGROUND allow'"

While debugging, do not press HOME — backgrounding the app is what triggers the freeze. Return to it with am start -n <app.package>/<activity> and keep it in the foreground for the whole session.

Signal extraction (what to actually read)

adb -s <serial> logcat -c                                  # clear before the run
adb -s <serial> shell "am start -n <pkg>/<activity>"
adb -s <serial> logcat -d -v brief | grep -E '<pkg>|FATAL|VerifyError|IncompatibleClassChange|uncaughtException'

Failure signatures worth memorizing:

Log line Likely cause
FATAL EXCEPTION + Java stack App-level crash — read the stack
VerifyError / IncompatibleClassChangeError Damaged dex (round-trip or bad rewrite) → pitfalls.md P3
Failure starting process (no stack, by ActivityManager) ART refused the dex, or device state is broken → pitfalls.md P4, P9
ClassNotFoundException: <App.Application> Dex rejected wholesale (ordering / structure) → pitfalls.md P2
uncaughtException with no stack, right after launch A crash-reporter SDK swallowed it. Check whether the process is gone, and look for the SDK's own log file under the app's data dir
App alive but nothing rendered A swallowed exception in the UI path; hunt the crash-reporter's log file

Crash-reporter SDKs hide your stack traces. When an app installs a global uncaught-exception handler (友盟/UCrash/Bugly etc.), the Java stack never reaches logcat — only a line like uncaughtException time: .... Three ways to get the stack:

  1. Frida, hooking Thread.setDefaultUncaughtExceptionHandler or the handler class (most reliable).
  2. Race the reporter's log file: it writes a file under <app data>/<sdk>/... then uploads and deletes it. Poll it every ~0.2 s from the device shell and copy on sight:
    while [ $i -lt 400 ]; do
      for f in <dir>/*.log; do [ -f "$f" ] && cp -f "$f" /data/local/tmp/capture.log; done
      i=$((i+1)); sleep 0.2
    done
    
  3. Run a build with the reporter disabled (not always possible).

Determinism

Before concluding anything from a failure: reboot the device and retry, and run a control build with zero patches. A surprising share of "my patch broke it" turns out to be device state (pitfalls.md P9).