第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 最佳实践

  1. 先注册事件再 load: script.on('message', ...) 必须在 script.load() 之前
  2. spawn 后必须 resume: 否则进程会一直挂起
  3. 使用 timeout: frida.get_usb_device(timeout=10) 避免无限等待
  4. 处理 detach: 总是注册 on_detached 处理意外断开
  5. 脚本分离: JS 代码放文件中,Python 只负责设备管理和消息处理
  6. 17.0+ 迁移注意: 旧的回调风格枚举 API 已移除,使用返回数组的新 API
  7. RPC 优先: 复杂交互优先使用 rpc.exports + exports_sync,比 send/on_message 更可靠
  8. override_option: 连接非标准端口的 frida-server 时使用 device.override_option()