feat: apk-reverse Agent Skill

Progressive-disclosure skill for Android APK reverse engineering:
recon, ad removal, membership limits, dex surgical patching, repack/sign,
runtime data, Frida, environment setup, and a hard-won failure catalogue.
This commit is contained in:
inliver233
2026-09-19 21:25:35 +08:00
commit c66658b6b3
33 changed files with 3713 additions and 0 deletions
+104
View File
@@ -0,0 +1,104 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Capture a crash stack when the app's crash-reporter SDK is hiding it.
Problem this solves
-------------------
Many apps install a global uncaught-exception handler (友盟/UCrash, Bugly, Firebase
Crashlytics in some configs). The Java stack then never reaches `logcat` -- you only
see something like:
W/UCrash.Java: uncaughtException time: <ts>
...and the process dies. Without a stack you are guessing.
Three ways to get it, in order of reliability:
1. Frida: hook Thread.setDefaultUncaughtExceptionHandler (use references/dynamic-frida.md)
2. Race the reporter's own log file (this script): it writes a file under the app's
data dir, then zips, uploads, and deletes it. Poll fast and copy on sight.
3. Use a build with the reporter disabled.
Usage
-----
python grab_crash.py --pkg com.example.app --activity .MainActivity
python grab_crash.py --pkg com.example.app --activity .MainActivity \
--scan-dir /data/data/com.example.app --wait 25
Notes
-----
* `--scan-dir` defaults to /data/data/<pkg>; the script searches a few levels deep for
newly appeared *.log / *.txt / *.stacktrace files, so you do not have to know which
SDK is installed.
* The watcher loop self-terminates after ~80 s, so nothing is left running on device.
* Requires root (the app's data dir is private).
"""
import argparse
import subprocess
import sys
import time
CAPTURE = '/data/local/tmp/_skill_crash_capture.log'
def adb(args, serial=None, timeout=180):
cmd = ['adb']
if serial:
cmd += ['-s', serial]
cmd += args
r = subprocess.run(cmd, capture_output=True, text=True, errors='replace', timeout=timeout)
return (r.stdout or '') + (('\n[stderr]\n' + r.stderr) if (r.stderr or '').strip() else '')
def su(cmd, serial=None):
return adb(['shell', 'su -c "%s"' % cmd.replace('"', '\\"')], serial)
def main():
ap = argparse.ArgumentParser(add_help=True)
ap.add_argument('--pkg', required=True)
ap.add_argument('--activity', required=True)
ap.add_argument('--serial')
ap.add_argument('--scan-dir')
ap.add_argument('--wait', type=int, default=25)
a = ap.parse_args()
scan = a.scan_dir or ('/data/data/%s' % a.pkg)
s = a.serial
# On-device watcher: copy any *.log/*.txt that appears under the app dir.
watch = (
"i=0; while [ $i -lt 400 ]; do "
" find {d} -maxdepth 5 -type f \\( -name '*.log' -o -name '*.txt' -o -name '*.stacktrace' "
" -o -name '*.crash' \\) -newermt '-3 minutes' -exec cp -f {{}} {c} \\; 2>/dev/null; "
" i=$((i+1)); sleep 0.2; "
"done"
).format(d=scan, c=CAPTURE)
su('am force-stop %s' % a.pkg, s)
su('rm -f %s' % CAPTURE, s)
print('[grab] arming on-device watcher over %s' % scan)
subprocess.Popen(['adb'] + (['-s', s] if s else []) +
['shell', "su -c '%s'" % watch],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
time.sleep(1.5)
print('[grab] starting app')
print(adb(['shell', 'am start -n %s/%s' % (a.pkg, a.activity)], s))
time.sleep(a.wait)
pid = adb(['shell', 'pidof %s' % a.pkg], s).strip()
print('[grab] pid: %s' % (pid or '(dead -> likely crashed)'))
print('[grab] ==== captured file ====')
out = su('head -c 6000 %s' % CAPTURE, s)
if 'No such file' in out or not out.strip():
print('(nothing captured)')
print('[hint] the reporter may have deleted it faster than 0.2 s, or it writes '
'outside --scan-dir. Use the Frida approach instead.')
else:
print(out)
return 0
if __name__ == '__main__':
sys.exit(main())