第4章 Python 绑定
本章基于 Frida 17.9.11 Python 绑定编写。安装:
pip install frida==17.9.11
4.1 基本 Attach 流程
import frida
import sys
def on_message(message, data):
if message['type'] == 'send':
print(f"[*] {message['payload']}")
else:
print(f"[!] {message}")
# 获取 USB 设备
device = frida.get_usb_device()
# 附加到运行中的进程(按名称或 PID)
session = device.attach('com.example.app')
# 创建并加载脚本
script = session.create_script("""
Java.perform(function() {
var Activity = Java.use('android.app.Activity');
Activity.onResume.implementation = function() {
send("onResume called");
this.onResume();
};
});
""")
script.on('message', on_message)
script.load()
# 保持运行
sys.stdin.read()
session.detach()4.2 Spawn 流程(从头 Hook)
import frida
import sys
def on_message(message, data):
if message['type'] == 'send':
print(f"[*] {message['payload']}")
else:
print(f"[!] {message}")
device = frida.get_usb_device()
# Spawn 进程(挂起状态)
pid = device.spawn(['com.example.app'])
# 附加到挂起的进程
session = device.attach(pid)
# 加载 instrumentation
script = session.create_script("""
Java.perform(function() {
send("App 启动前就已完成 Hook!");
});
""")
script.on('message', on_message)
script.load()
# 恢复执行
device.resume(pid)
sys.stdin.read()spawn() 完整签名
pid = device.spawn(
program, # str 或 list — 程序路径/包名,或 [程序, 参数...]
argv=None, # list — 命令行参数
envp=None, # dict — 完整环境变量(替换)
env=None, # dict — 额外环境变量(追加)
cwd=None, # str — 工作目录
stdio=None, # str — "inherit" 或 "pipe"
**kwargs # 其他平台特定选项
)spawn vs attach 选择:
spawn(-f): 需要 hook 应用启动阶段代码时使用attach: hook 已运行的进程
4.3 设备发现与管理
import frida
# USB 设备(移动设备最常用)
device = frida.get_usb_device()
device = frida.get_usb_device(timeout=10) # 带超时
# 本地设备
device = frida.get_local_device()
# 远程设备
device = frida.get_remote_device()
# 按 ID 获取设备
manager = frida.get_device_manager()
device = manager.get_device('0216027d1d6d3a03')
# 枚举所有设备
devices = frida.enumerate_devices()
for d in devices:
print(f"{d.id} - {d.name} ({d.type})")
# 查询设备系统参数(17.x)
params = device.query_system_parameters()
print(f"OS: {params.get('os', {}).get('id')}")
print(f"Arch: {params.get('arch')}")
# 覆盖后端选项(17.9+ 新增)
# Droidy 后端:自定义 ADB 端点
device.override_option("control-endpoint", "localabstract:/my-frida-server")
# Fruity 后端:自定义 TCP 端点
device.override_option("control-endpoint", "tcp:27042")
# 检查设备是否已断开(17.x 属性)
if device.is_lost:
print("设备连接已丢失")Device 类完整方法列表
| 方法 | 说明 |
|---|---|
spawn(program, ...) | 创建新进程(挂起状态) |
attach(target) | 附加到进程(名称或 PID) |
resume(target) | 恢复挂起的进程 |
kill(target) | 终止进程 |
enumerate_processes(pids=None, scope=None) | 枚举进程 |
get_process(name) | 按名称获取进程 |
get_frontmost_application(scope=None) | 获取前台应用 |
enumerate_applications(identifiers=None, scope=None) | 枚举应用 |
enable_spawn_gating() | 启用 spawn 拦截 |
disable_spawn_gating() | 禁用 spawn 拦截 |
enumerate_pending_spawn() | 枚举待处理的 spawn |
enumerate_pending_children() | 枚举待处理的子进程 |
query_system_parameters() | 查询系统参数 |
override_option(name, value) | 覆盖后端选项(17.9+) |
4.4 从文件加载脚本
import frida
import sys
device = frida.get_usb_device()
pid = device.spawn(['com.example.app'])
session = device.attach(pid)
# 从文件加载 JS 脚本
with open('hook.js', 'r') as f:
source = f.read()
script = session.create_script(source)
script.on('message', on_message)
script.load()
device.resume(pid)
sys.stdin.read()Session 类完整方法列表
| 方法 | 说明 |
|---|---|
create_script(source, name=None, snapshot=None, runtime=None) | 创建脚本 |
create_script_from_bytes(data, name=None, snapshot=None, runtime=None) | 从字节码创建脚本 |
compile_script(source, name=None, runtime=None) | 编译脚本为字节码 |
snapshot_script(embed_script, warmup_script=None, runtime=None) | 创建脚本快照 |
detach() | 断开连接 |
resume() | 恢复 session |
enable_child_gating() | 启用子进程拦截 |
disable_child_gating() | 禁用子进程拦截 |
setup_peer_connection(stun_server=None, relays=None) | 配置 P2P 连接 |
join_portal(address, certificate=None, token=None, acl=None) | 加入 Portal |
is_detached | 属性:是否已断开 |
Script 类完整方法列表
| 方法 | 说明 |
|---|---|
load() | 加载脚本 |
unload() | 卸载脚本 |
eternalize() | 永久化脚本(detach 后继续运行) |
post(message, data=None) | 向脚本发送消息 |
enable_debugger(port=None) | 启用调试器 |
disable_debugger() | 禁用调试器 |
exports_sync | 属性:同步调用 RPC 导出 |
exports_async | 属性:异步调用 RPC 导出 |
list_exports_sync() | 列出 RPC 导出(同步) |
list_exports_async() | 列出 RPC 导出(异步) |
is_destroyed | 属性:脚本是否已销毁 |
4.5 错误处理
import frida
import sys
def on_message(message, data):
if message['type'] == 'send':
print(f"[*] {message['payload']}")
elif message['type'] == 'error':
print(f"[!] Error: {message['stack']}")
def on_detached(reason, crash):
print(f"[!] Detached: {reason}")
if crash:
print(f" Crash: {crash}")
try:
device = frida.get_usb_device(timeout=10)
session = device.attach('com.example.app')
session.on('detached', on_detached)
script = session.create_script(source)
script.on('message', on_message)
script.load()
sys.stdin.read()
except frida.ProcessNotFoundError:
print("进程未找到,确认应用是否在运行")
except frida.TransportError:
print("与设备的连接断开")
except frida.InvalidOperationError as e:
print(f"无效操作: {e}")
except frida.ServerNotRunningError:
print("frida-server 未在设备上运行")
finally:
if 'session' in dir():
session.detach()4.6 Spawn-Gating(监控所有新进程)
拦截设备上的进程创建,在执行前进行 instrumentation:
import frida
import threading
class SpawnGatingApp:
def __init__(self):
self._device = frida.get_usb_device()
self._device.on("spawn-added", self._on_spawn_added)
self._device.on("spawn-removed", self._on_spawn_removed)
def start(self):
self._device.enable_spawn_gating()
print("[*] Spawn gating enabled, waiting for spawns...")
def _on_spawn_added(self, spawn):
print(f"[*] Spawn: {spawn.identifier} (PID: {spawn.pid})")
if spawn.identifier == "com.target.app":
self._instrument(spawn.pid)
else:
self._device.resume(spawn.pid)
def _on_spawn_removed(self, spawn):
print(f"[-] Spawn removed: {spawn.identifier}")
def _instrument(self, pid):
session = self._device.attach(pid)
script = session.create_script("/* instrumentation */")
script.load()
self._device.resume(pid)
app = SpawnGatingApp()
app.start()4.7 Child-Gating(追踪子进程)
检测 instrumented 进程创建的子进程 (fork, execve):
import frida
class ChildGatingApp:
def __init__(self):
self._device = frida.get_local_device()
self._device.on("child-added", self._on_child_added)
def start(self, argv):
pid = self._device.spawn(argv, env={}, stdio="pipe")
session = self._device.attach(pid)
session.enable_child_gating()
script = session.create_script("/* parent instrumentation */")
script.load()
self._device.resume(pid)
def _on_child_added(self, child):
print(f"[*] Child spawned: PID={child.pid}, "
f"Parent={child.parent_pid}, Path={child.path}")
session = self._device.attach(child.pid)
script = session.create_script("/* child instrumentation */")
script.load()
self._device.resume(child.pid)4.8 崩溃报告
import frida
import sys
def on_process_crashed(crash):
print(f"[!] CRASH DETECTED: {crash}")
def on_detached(reason, crash):
print(f"[!] Detached: {reason}")
if crash:
print(f" Crash info: {crash}")
device = frida.get_usb_device()
session = device.attach("com.example.app")
# 设备级崩溃监控
device.on("process-crashed", on_process_crashed)
# Session 级 detach 监控
session.on("detached", on_detached)
script = session.create_script("/* instrumentation */")
script.load()
sys.stdin.read()Detach 原因:
"application-requested"— 应用主动断开"process-replaced"— 进程被替换"server-terminated"— frida-server 终止"device-lost"— 设备断开
4.9 PackageManager API(17.2+ 新增)
通过 Python 绑定直接搜索和安装 Frida 包(无需 Node.js):
import frida
# 创建包管理器
pm = frida.PackageManager()
# 查看默认 registry
print(f"Registry: {pm.registry}")
# 搜索包
results = pm.search("objc")
for pkg in results:
print(f" {pkg['name']} - {pkg['description']}")
# 安装包
pm.install(["frida-objc-bridge"])
# 安装指定版本
pm.install(["frida-il2cpp-bridge@^2.0.0"])
# 安装时指定角色(类似 npm --save-dev)
pm.install(["frida-compile"], role="dev")
# 跳过特定依赖类型
pm.install([], omits=["dev"])
# 监听安装进度
def on_progress(phase, fraction, detail):
print(f" [{phase}] {fraction*100:.0f}% - {detail}")
pm.on("install-progress", on_progress)
pm.install(["frida-objc-bridge"])4.10 RPC 导出调用
利用 exports_sync 属性直接调用脚本中的 RPC 导出函数:
import frida
import sys
device = frida.get_usb_device()
session = device.attach('com.example.app')
script = session.create_script("""
rpc.exports = {
getPackageName() {
return Java.performNow(() => {
const app = Java.use('android.app.ActivityThread')
.currentApplication();
return app.getPackageName();
});
},
readFile(path) {
const File = Java.use('java.io.File');
const f = File.$new(path);
return f.exists() ? f.length() : -1;
}
};
""")
script.load()
# 同步调用 RPC 导出
api = script.exports_sync
print(f"Package: {api.get_package_name()}")
print(f"File size: {api.read_file('/data/local/tmp/test')}")
session.detach()注意: RPC 导出名在 Python 侧自动转换为 snake_case(getPackageName → get_package_name)。
4.11 最佳实践
- 先注册事件再 load:
script.on('message', ...)必须在script.load()之前 - spawn 后必须 resume: 否则进程会一直挂起
- 使用 timeout:
frida.get_usb_device(timeout=10)避免无限等待 - 处理 detach: 总是注册
on_detached处理意外断开 - 脚本分离: JS 代码放文件中,Python 只负责设备管理和消息处理
- 17.0+ 迁移注意: 旧的回调风格枚举 API 已移除,使用返回数组的新 API
- RPC 优先: 复杂交互优先使用
rpc.exports+exports_sync,比send/on_message更可靠 - override_option: 连接非标准端口的 frida-server 时使用
device.override_option()