Files
apk-reverse/scripts/run_probe.py
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

361 lines
15 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Inject a Frida probe script into a running Android app and stay resident.
WHY THIS EXISTS
---------------
`frida -U -f <pkg> -l script.js` has two failure modes that cost real debugging time:
1. With a real device AND an emulator both attached, the USB auto-selection picks
the emulator, so you attach to the wrong process and see nothing. This tool
prefers an explicit remote device over `adb forward`, which cannot be hijacked.
2. Probe output that only goes to a terminal is lost the moment the terminal
scrolls, the session detaches, or the app crashes. Here every message is
written to a log file *and* to stdout, with both the device timestamp and the
host arrival timestamp -- that pair is itself evidence when you suspect a
clock skew problem.
It also answers the three questions that otherwise produce a bare traceback:
* is the `frida` python module importable, and which version?
* is `adb` reachable, and is the target process actually running?
* is an on-device frida-server answering on the forwarded port?
USAGE
-----
python run_probe.py frida_probe.js 10 --pkg com.example.app
python run_probe.py frida_probe.js 5 --pkg com.example.app --device <serial>
python run_probe.py frida_probe.js 5 --pkg com.example.app --via usb
python run_probe.py frida_probe.js 2 --pkg com.example.app --spawn
python run_probe.py frida_probe.js 10 --pkg com.example.app --log my.log --port 27043
<script.js> the probe to inject (e.g. frida_probe.js, see its header to adapt)
<minutes> how long to stay attached (default 10)
NOTES
-----
* The host `frida` package and the on-device `frida-server` must be the SAME
version, and 16.x is the version to align on: 17.x can fail to locate the
Android dynamic linker and removes the built-in Java bridge.
* `--spawn` is the right choice when the problem happens at startup: attach-only
misses init and the first network calls. Hooks are installed before resume.
* Log file: a timestamped name is generated unless --log is given.
"""
import argparse
import os
import shutil
import subprocess
import sys
import time
from datetime import datetime
DEFAULT_PORT = 27042
DEFAULT_MINUTES = 10
DEFAULT_SERVER_PORT = 27042 # the port frida-server listens on, on the device
FRIDA_INSTALL_HINT = (
'install the matching host package and the matching device server, e.g.\n'
' pip install frida==16.7.19\n'
' # then push frida-server-16.7.19-android-<abi> to the device and run it as root'
)
def host_ts():
return datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')[:-3]
class Logger(object):
"""Write every line to stdout AND to a file, flushing both immediately."""
def __init__(self, path):
self.path = path
self.fh = open(path, 'a', encoding='utf-8', errors='replace')
def line(self, text):
print(text, flush=True)
self.fh.write(text + '\n')
self.fh.flush()
def close(self):
try:
self.fh.close()
except Exception:
pass
def adb_run(adb, serial, args, timeout=30):
cmd = [adb]
if serial:
cmd += ['-s', serial]
cmd += args
return subprocess.run(cmd, capture_output=True, text=True,
errors='replace', timeout=timeout)
def find_adb(explicit=None):
if explicit:
return explicit
return shutil.which('adb') or shutil.which('adb.exe')
def get_pid(adb, serial, pkg):
"""Return (pid, raw_output). pid is None when the process is not running."""
try:
r = adb_run(adb, serial, ['shell', 'pidof', pkg])
except subprocess.TimeoutExpired:
return None, 'adb timed out'
raw = ((r.stdout or '') + (r.stderr or '')).strip()
if r.returncode != 0 or not raw:
return None, raw
first = raw.split()[0]
if not first.isdigit():
return None, raw
return int(first), raw
def attach_remote(frida, port, logger):
mgr = frida.get_device_manager()
logger.line('[host %s] registering remote device 127.0.0.1:%d' % (host_ts(), port))
return mgr.add_remote_device('127.0.0.1:%d' % port)
def attach_usb(frida, device_id, logger):
if device_id:
logger.line('[host %s] selecting usb device %s' % (host_ts(), device_id))
return frida.get_device(device_id, timeout=5)
logger.line('[host %s] selecting usb device (auto; an emulator can win this race)'
% host_ts())
return frida.get_usb_device(timeout=5)
def format_message(message):
"""Turn a frida message into one log line. Never raises."""
kind = message.get('type')
if kind == 'send':
payload = message.get('payload')
if isinstance(payload, dict):
dev = payload.get('t') or '-'
tag = payload.get('tag') or 'SEND'
msg = payload.get('msg')
if msg is None:
msg = payload
return dev, '%s %s' % (tag, msg)
return '-', 'SEND %r' % (payload,)
if kind == 'error':
desc = message.get('description') or ''
where = '%s:%s' % (message.get('fileName') or '?', message.get('lineNumber') or '?')
stack = message.get('stack') or ''
out = 'FRIDA-ERROR %s (%s)' % (desc, where)
if stack:
out += '\n ' + stack.replace('\n', '\n ')
return '-', out
return '-', '%s %r' % (kind, message)
def main():
ap = argparse.ArgumentParser(
description='Inject a Frida probe into a running Android app, log everything '
'to stdout and a file, and stay attached for N minutes.')
ap.add_argument('script', help='path to the frida JS probe, e.g. frida_probe.js')
ap.add_argument('minutes', nargs='?', type=float, default=DEFAULT_MINUTES,
help='how long to stay attached (default %d)' % DEFAULT_MINUTES)
ap.add_argument('--pkg', default=None, help='target package name, e.g. com.example.app')
ap.add_argument('--device', default=None,
help='adb serial (remote mode) or frida device id (usb mode)')
ap.add_argument('--via', choices=['remote', 'usb'], default='remote',
help='remote = adb forward + explicit remote device (default, '
'cannot be hijacked by an emulator); usb = frida.get_usb_device()')
ap.add_argument('--port', type=int, default=DEFAULT_PORT,
help='host-side forwarded port (default %d)' % DEFAULT_PORT)
ap.add_argument('--server-port', type=int, default=DEFAULT_SERVER_PORT,
help='port frida-server listens on, on the device (default %d)'
% DEFAULT_SERVER_PORT)
ap.add_argument('--spawn', action='store_true',
help='spawn the app instead of attaching, so hooks are live before '
'the first network call')
ap.add_argument('--log', default=None, help='log file path (default: auto-named)')
ap.add_argument('--adb', default=None, help='path to adb (default: from PATH)')
ap.add_argument('--script-timeout', type=float, default=30.0,
help='seconds to wait for the script to load')
ap.add_argument('--keep-forward', action='store_true',
help='leave the adb forward in place after exit')
args = ap.parse_args()
if not os.path.isfile(args.script):
print('probe script not found: %s' % args.script)
return 2
if args.spawn and not args.pkg:
print('--spawn needs --pkg (there is no pid to attach to)')
return 2
log_name = args.log or ('probe-%s-%s.log'
% (args.pkg or 'target', datetime.now().strftime('%Y%m%d-%H%M%S')))
logger = Logger(log_name)
logger.line('# probe log %s' % os.path.abspath(log_name))
logger.line('# script=%s minutes=%s via=%s pkg=%s'
% (os.path.abspath(args.script), args.minutes, args.via, args.pkg))
# 1) frida python package
try:
import frida
except ImportError:
logger.line(host_ts() + ' FATAL cannot import the frida python module.\n' +
FRIDA_INSTALL_HINT)
logger.close()
return 3
logger.line('[host %s] frida python %s' % (host_ts(), getattr(frida, '__version__', '?')))
# 2) adb
adb = find_adb(args.adb)
if not adb:
logger.line(host_ts() + ' FATAL adb not found.\n'
' put platform-tools on PATH, or pass --adb <path>')
logger.close()
return 3
logger.line('[host %s] adb %s' % (host_ts(), adb))
pid = None
forward_added = False
device = None
session = None
script = None
counts = {}
deadline = time.monotonic() + max(args.minutes, 0) * 60.0
try:
# 3) remote device over adb forward (default), or plain USB selection
if args.via == 'remote':
try:
r = adb_run(adb, args.device,
['forward', 'tcp:%d' % args.port, 'tcp:%d' % args.server_port])
except subprocess.TimeoutExpired:
logger.line(host_ts() + ' FATAL adb forward timed out; is the device responsive?')
return 3
if r.returncode != 0:
msg = ((r.stdout or '') + (r.stderr or '')).strip()
logger.line(host_ts() + ' FATAL adb forward failed.\n'
' adb said: %s\n'
' if it mentions more than one device, pass --device <serial>;\n'
' check the device is visible with: adb devices' % msg)
return 3
forward_added = True
logger.line('[host %s] forwarded tcp:%d -> device tcp:%d'
% (host_ts(), args.port, args.server_port))
device = attach_remote(frida, args.port, logger)
else:
device = attach_usb(frida, args.device, logger)
# 4) pid: spawn when asked, otherwise attach to the running process
if args.spawn:
try:
pid = device.spawn([args.pkg])
except Exception as exc:
logger.line(host_ts() + ' FATAL spawn failed for %s: %s\n'
' check the package is installed: adb shell pm list packages | grep %s'
% (args.pkg, exc, args.pkg))
return 3
logger.line('[device] spawned %s pid=%s' % (args.pkg, pid))
else:
if not args.pkg:
logger.line(host_ts() + ' FATAL --pkg is required when attaching '
'(or use --spawn with --pkg)')
return 3
pid, raw = get_pid(adb, args.device, args.pkg)
if pid is None:
logger.line(host_ts() + ' FATAL process not found: %s\n'
' adb output: %s\n'
' start the app first, or use --spawn to launch it under the probe;\n'
' if pidof is unavailable on this ROM, try: adb shell ps -A'
% (args.pkg, raw or '<empty>'))
return 3
logger.line('[device] pid of %s = %d' % (args.pkg, pid))
# 5) attach + load
try:
session = device.attach(pid)
except Exception as exc:
logger.line(host_ts() + ' FATAL attach failed: %s\n'
' usual causes: frida-server not running on the device, a\n'
' host/server version mismatch, or an anti-instrumentation check.\n'
' %s' % (exc, FRIDA_INSTALL_HINT))
return 3
with open(args.script, encoding='utf-8') as fh:
source = fh.read()
def on_message(message, data):
dev, text = format_message(message)
tag = 'OTHER'
if isinstance(message.get('payload'), dict):
tag = message['payload'].get('tag') or 'SEND'
elif message.get('type') == 'error':
tag = 'FRIDA-ERROR'
counts[tag] = counts.get(tag, 0) + 1
logger.line('[host %s] [dev %s] %s' % (host_ts(), dev, text))
if message.get('type') == 'error':
desc = message.get('description') or ''
if 'Java API not available' in desc or 'Java is not defined' in desc:
logger.line('[hint] the script loaded but this runtime has no Java bridge. '
'That is a version problem, not a hook problem: align the host '
'frida package and the on-device frida-server to the same 16.x '
'version, then retry. Do not debug the hooks until READY appears.')
script = session.create_script(source)
script.on('message', on_message)
try:
script.load()
except Exception as exc:
logger.line(host_ts() + ' FATAL script load failed: %s\n'
' if this says "Java is not defined", the runtime has no Java\n'
' bridge: align host frida and on-device frida-server to 16.x.'
% exc)
return 3
if args.spawn:
device.resume(pid)
logger.line('[device] resumed pid=%d (hooks were installed before resume)' % pid)
logger.line('[host %s] attached; staying resident for %s minute(s). '
'Now drive the app UI.' % (host_ts(), args.minutes))
while time.monotonic() < deadline:
time.sleep(0.5)
except KeyboardInterrupt:
logger.line('[host %s] interrupted; detaching' % host_ts())
finally:
if script is not None:
try:
script.unload()
except Exception:
pass
if session is not None:
try:
session.detach()
except Exception:
pass
if forward_added and not args.keep_forward:
try:
adb_run(adb, args.device, ['forward', '--remove', 'tcp:%d' % args.port])
logger.line('[host %s] removed adb forward tcp:%d' % (host_ts(), args.port))
except Exception:
pass
logger.line('')
logger.line('== summary: %d message(s)' % sum(counts.values()))
for tag in sorted(counts):
logger.line(' %-16s %d' % (tag, counts[tag]))
if not counts:
logger.line(' no probe output at all. Ordered checklist:')
logger.line(' 1. did READY appear? if not, the script never loaded')
logger.line(' 2. is the class/overload name you configured really present?')
logger.line(' 3. ClassLoader: the hooks bind against the app classloader')
logger.line(' 4. did the UI action actually reach the business code?')
logger.line(' 5. check THROW lines: a local validation may have returned early')
logger.line('# log written to %s' % os.path.abspath(log_name))
logger.close()
return 0
if __name__ == '__main__':
sys.exit(main())