文章总结: 本文详细介绍了Frida内置的Stalker代码追踪引擎,这是一种线程级指令跟踪工具,基于动态重编译机制实现逐条汇编指令的追踪。文章对比了Stalker与Interceptor的差异,系统讲解了9个核心API的使用方法,包括follow/unfollow/flush/exclude等高频操作,并提供了性能优化建议如exclude排除系统库减少噪音。文档强调Stalker在分析OLLVM混淆、VMP保护和代码覆盖率场景中的关键作用,建议采用Interceptor定位后Stalker深入分析的策略。 综合评分: 85 文章分类: 逆向分析,安全工具,移动安全,WEB安全,二进制安全
8.2 Stalker 被检测的风险
性能解决了,第二个最容易踩的坑就是被 App 反检测干掉。Stalker 的动态重编译机制可能被高级的反分析保护检测到:
- 代码完整性校验:App 可能计算代码段(.text section)的 hash。虽然 Stalker 不修改原始代码,但某些更激进的检测方式可能检查代码执行路径的完整性
- 时间检测:Stalker 显著增加执行时间(典型 3-5x,稳态可降到 1.5-2x),可被精确的计时器(如
clock_gettime)检测到。某些 App 会在关键操作前后设置时间窗口 - 内存特征:Stalker 分配的代码块有特定的内存布局特征(大量可执行的匿名 mmap 区域),可通过读取
/proc/self/maps来检测
商业反检测 App(银行 / 直播 / 加固后的国民级 App)往往同时上多种检测。具体绕过手法见第 17/18 篇反调试章节。
8.3 自修改代码(SMC)
如果目标代码会在运行时修改自身(常见于加壳/VMP/代码混淆保护),需要调整信任阈值:
// 处理自修改代码
Stalker.trustThreshold = -1; // 永不信任,每次执行基本块都重新编译
// 注意:这会显著降低性能,仅在确认存在 SMC 时使用
// 或者设为 0:立即信任(忽略自修改)
// 适用于代码虽然有解密过程,但解密后不会再变化的场景
Stalker.trustThreshold = 0;
何时怀疑存在 SMC? 如果你发现 Stalker 追踪的指令与 IDA 中看到的反汇编不一致,或者 Stalker 在某些基本块崩溃,可能是代码在运行时被解密/修改了。
8.4 多线程竞态
Stalker.follow()跟踪的是特定线程,不会自动跟踪该线程创建的子线程- 多个线程同时被 follow 时,各自的 transform 回调是独立的,不会相互干扰
onReceive/onCallSummary回调是在 Frida 的 JS 线程中串行执行的,不存在并发问题
8.5 ARM Thumb/ARM 模式切换
在 32 位 ARM 上,代码可能在 ARM(32 位指令)和 Thumb(16 位指令)模式之间切换。确保传给 Stalker 的地址考虑了 Thumb 位——地址的最低位为 1 表示 Thumb 模式:
// 在 32 位 ARM 上,注意 Thumb 位
const addr = Module.findExportByName("libtarget.so", "target_func");
// addr 可能包含 Thumb 位(最低位为 1),Stalker 会自动处理
// 不需要手动清除 Thumb 位
为什么要关注这个? 在 64 位 AArch64 上不存在 Thumb 模式,因此只有在分析 32 位 ARM 代码时需要注意。如果你的目标设备和 App 都是 64 位的,可以跳过此节。
九、进阶:与其他工具联动
Stalker 自己就能完成数据采集,但真要做大型项目逆向,3 个搭子缺一不可:IDA Lighthouse 把覆盖率可视化(看路径)、CModule 把热点回调从 JS 拉到 C(看性能)、Python 把 trace 数据自动化分析(看结论)。下面三段独立可读,但实战通常三者都会用上——大项目 = 数据采集(Stalker)+ 可视化(Lighthouse)+ 性能(CModule)+ 分析(Python),四件套一起转才转得顺。
9.1 Stalker + IDA Lighthouse(覆盖率可视化)
将 Stalker 收集的覆盖率数据导出为 drcov 格式,然后在 IDA 中通过 Lighthouse 插件加载,实现代码覆盖率的可视化着色:
# ida_coverage.py — 生成 Lighthouse 兼容的 drcov 二进制文件
import struct
def write_drcov(module_name, module_base, module_size, module_path, blocks, output_path):
"""
生成 drcov 格式的覆盖率文件
blocks: [(offset, size), ...] 列表,每个元素是一个基本块的偏移和大小
"""
with open(output_path, "wb") as f:
# 写入文本格式的头部
f.write(b"DRCOV VERSION: 2\n")
f.write(b"DRCOV FLAVOR: frida-stalker\n")
f.write(f"Module Table: version 2, count 1\n".encode())
f.write(b"Columns: id, base, end, entry, checksum, timestamp, path\n")
f.write(f"0, {module_base:#x}, {module_base + module_size:#x}, 0, 0, 0, {module_path}\n".encode())
f.write(f"BB Table: {len(blocks)} bbs\n".encode())
# 写入二进制格式的基本块数据
for offset, size in blocks:
# drcov BB entry 结构:uint32 start_offset, uint16 size, uint16 module_id
f.write(struct.pack("<IHH", offset, size, 0))
print(f"[*] 覆盖率文件已保存: {output_path}")
print(f"[*] 共 {len(blocks)} 个基本块")
print(f"[*] 在 IDA 中加载: File -> Load File -> Code Coverage File")
9.2 Stalker + CModule(高性能 C 回调)
当 JavaScript 回调的性能开销无法接受时,可以使用 Frida 的 CModule 将 transform 和事件处理逻辑用 C 语言实现。C 回调直接在目标进程的上下文中执行,没有 JS ↔ Native 的跨边界调用开销:
// cmodule_stalker.js — 使用 CModule 实现高性能 Stalker 回调
const cm = new CModule(`
#include <gum/gumstalker.h>
#include <stdio.h>
// 这些变量由 JS 端设置(通过 CModule 构造函数的第二个参数)
extern guint64 module_start;
extern guint64 module_end;
extern guint64 call_count;
// C 语言版本的 transform 回调
// 直接操作 Stalker 的 C API,没有 JS 桥接开销
void
transform (GumStalkerIterator * iterator,
GumStalkerOutput * output,
gpointer user_data)
{
cs_insn * insn;
while (gum_stalker_iterator_next (iterator, &insn))
{
// 只对目标模块内的代码进行插桩
if (insn->address >= module_start && insn->address < module_end)
{
// 在 RET 指令前插入 C 回调
if (insn->id == ARM64_INS_RET)
{
gum_stalker_iterator_put_callout (iterator,
on_ret, NULL, NULL);
}
}
gum_stalker_iterator_keep (iterator);
}
}
// C 语言版本的回调函数 — 直接处理,无 JS 调用开销
static void
on_ret (GumCpuContext * cpu_context,
gpointer user_data)
{
call_count++;
// 在 C 层直接处理数据,避免频繁的 JS 回调
if (cpu_context->x[0] != 0)
{
// 只记录非零返回值(减少输出量)
printf ("RET x0=0x%llx pc=0x%llx\\n",
(unsigned long long) cpu_context->x[0],
(unsigned long long) cpu_context->pc);
}
}
`, {
// 将 JS 端的变量传递给 CModule
module_start: ptr(targetModule.base),
module_end: ptr(targetModule.base.add(targetModule.size)),
call_count: Memory.alloc(8) // 分配 8 字节作为计数器
});
// 使用 CModule 导出的 transform 函数
Stalker.follow(threadId, {
transform: cm.transform,
data: ptr(0)
});
CModule 跨边界类型对照:JS 端传
ptr(...)(NativePointer),C 端extern guint64收 ── Frida 会把 NativePointer 当成 64 位无符号整数原样写入符号地址,C 代码里直接当地址用即可。Memory.alloc(N)同理传指针。详见 frida-gum 文档CModule symbol values。性能对比:使用 CModule 的 transform 回调比纯 JavaScript 版本通常快 5-10 倍,因为省去了每次 putCallout 时从 Native 调用 JS 引擎的开销。在需要对大量指令进行插桩时,CModule 是必须的。
9.3 Stalker + Python 数据分析
对大规模 trace 数据进行自动化分析。当追踪数据量达到数万甚至数十万条记录时,人工查看不现实,需要自动化分析框架:
# trace_analyzer.py — 自动化 trace 数据分析框架
import json
from collections import Counter, defaultdict
class StalkerTraceAnalyzer:
def __init__(self, trace_file):
"""从 JSONL 文件加载追踪数据"""
self.events = []
with open(trace_file) as f:
for line in f:
self.events.append(json.loads(line))
def call_frequency(self, top_n=20):
"""分析函数调用频率 — 找出最常被调用的函数(可能是循环体内的热点)"""
targets = Counter()
for ev in self.events:
if ev.get("type") == "call":
targets[ev["target"]] += 1
print(f"\n=== Top {top_n} 调用目标 ===")
for target, count in targets.most_common(top_n):
print(f"{count:6d}x {target}")
def call_sequence(self):
"""提取函数调用的时间序列 — 还原执行的先后顺序"""
calls = [ev for ev in self.events if ev.get("type") == "call"]
print(f"\n=== 调用序列(共 {len(calls)} 次调用)===")
for i, call in enumerate(calls[:50]): # 只显示前 50 个
print(f" [{i:3d}] {call['target']}")
def detect_loops(self):
"""检测循环模式 — 通过 PC 重复出现来识别循环"""
pc_sequence = [ev["pc"] for ev in self.events if "pc" in ev]
print("\n=== 热点地址(可能的循环)===")
pc_counts = Counter(pc_sequence)
for pc, count in pc_counts.most_common(10):
if count > 5: # 执行超过 5 次的地址很可能在循环内
print(f" 0x{pc}: {count} 次执行")
def data_flow(self, register="x0"):
"""追踪特定寄存器值的变化 — 观察数据如何在算法中流动"""
values = []
for ev in self.events:
if register in ev:
values.append((ev.get("pc", "?"), ev[register]))
print(f"\n=== {register} 值变化追踪 ===")
for pc, val in values[:30]:
print(f" [{pc}] {register} = {val}")
# 使用示例
analyzer = StalkerTraceAnalyzer("stalker_trace.jsonl")
analyzer.call_frequency() # 哪些函数被调用最多?
analyzer.call_sequence() # 调用的先后顺序是什么?
analyzer.detect_loops() # 有哪些循环?
analyzer.data_flow("x0") # x0 寄存器的值如何变化?
十、总结
Stalker 是 Frida 的指令级代码追踪引擎,填补了 Interceptor(方法级 Hook)的盲区——能看到函数内部每一条指令的执行。核心能力分五类:指令追踪(exec 事件)、调用追踪(call/ret 事件)、基本块追踪(block 事件)、编译时注入(transform + putCallout)、覆盖率收集(compile 事件配合 Lighthouse 可视化)。
Stalker 使用流程总结
1. 确定目标 → 用 Interceptor/Java Hook 定位关键 Native 函数
↓
2. 配置环境 → Stalker.exclude 排除无关模块
↓
3. 初步追踪 → call/block 事件,了解整体调用结构
↓
4. 深入分析 → transform + putCallout,在关键位置记录状态
↓
5. 数据分析 → Python 端自动化分析 trace 数据
↓
6. 算法还原 → 根据 trace 数据还原逻辑,编写验证代码
核心要点
- 先粗后细:从宏观的调用统计开始,逐步深入到指令级追踪。不要一上来就开
exec: true - 精确限定范围:善用
Stalker.exclude()和地址过滤,避免信息过载和性能崩溃 - 合理选择事件:按需开启事件类型——
compile适合覆盖率,call适合调用链,exec仅作为最后手段 - transform 是核心:掌握
putCallout和指令过滤,可以实现几乎任何分析需求 - 注意性能:始终在精度和性能之间找到平衡,必要时使用 CModule 将热点逻辑用 C 实现
- 配合使用:Stalker 不是孤立使用的,与 Interceptor、Java Hook、IDA、Python 分析工具配合效果最佳
下次再遇到一段 OLLVM 混淆 / 未知签名算法,推荐工作流
- 入口定位 — 抓包看异常字段(
X-Sign/sign头),Java 层 hook 反查到 Native 函数
- JNI 静态注册 → 直接
Module.findExportByName - JNI 动态注册 → 走 第 22 篇 hook
RegisterNatives拿真实地址 - 符号被 strip → 走 第 21 篇 多策略兜底(特征码 / 字符串交叉引用 / 调用点反查)
- 粗追踪 — Interceptor.attach + Stalker.follow +
events: { call: true }+onCallSummary
- 拿调用统计,看「我的 Native 函数内部都调了谁」
- 必开
Stalker.exclude把 libc / linker / frida-agent 全排除,只追目标 SO
- 精细 dump — 用
transform + putCallout在关键 mnemonic(eor / lsl / bl / blr 等)处插桩
- 把 PC / 寄存器值 / 栈值打成结构化 record,push 到数组
- onLeave 时一次性
send()给 Python 端
- 算法还原 — Python 端拿 trace 数据反推
-
看 call 序列 → 哪些是 libc 函数(memcpy / sha256)、哪些是自定义
看 ALU 序列 → 推算 XOR 密钥 / 偏移变换
- 独立验证 — 用 Python 复现整套算法,跟 Frida 抓到的真实输入/输出比对
- 完全一致 → 还原成功,可用于黑盒批量签名 / fuzz / 自动化
按这条流程走,大多数「Native 层签名/加密算法」逆向都能拿下。exec 事件永远是最后手段——除非你需要看每一条指令的执行,否则 call/block 已经够用。
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:泡泡以安 泡泡以安 泡泡以安《Frida学习笔记(二十四):Stalker 指令级追踪》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。









评论