使用 Frida Stalker 追踪算法实现
Frida Stalker 工作原理
Frida Stalker 是 Frida 框架中最强大的代码追踪工具,它能够在运行时对任意线程的代码执行进行实时的、指令级别的追踪。与 Interceptor 只能在函数入口和出口设置钩子不同,Stalker 可以追踪目标线程执行的每一条指令,记录完整的执行路径。
核心机制
Stalker 的工作原理可以概括为以下几个步骤:
1. 线程劫持
Stalker 接管目标线程的执行控制权
2. 代码重写 (Code Rewrite)
将原始代码复制到新的内存区域
在每条指令前后插入追踪探针 (instrumentation)
3. 实时记录
每次执行一条指令时,调用预定义的回调函数
回调可以记录地址、修改寄存器、改变执行流
4. 执行恢复
执行完毕后,将控制权归还给原始线程
Stalker vs Interceptor
| 特性 | Interceptor | Stalker |
|---|---|---|
| 粒度 | 函数级 | 指令级 |
| 覆盖范围 | 只能 Hook 指定函数 | 追踪线程的所有代码 |
| 性能开销 | 较低 | 较高 |
| 适用场景 | 参数监控、返回值修改 | 控制流分析、算法追踪 |
| 代码修改 | 不可修改执行流 | 可以替换指令 |
Stalker.follow() API 详解
Stalker.follow() 是启动 Stalker 追踪的核心 API。
基本语法
Stalker.follow(threadId, {
events: { ... }, // 要捕获的事件类型
onReceive: function(events) { ... }, // 事件接收回调
onCallSummary: function(summary) { ... } // 调用统计回调
});
参数说明
- threadId:要追踪的线程 ID,可以使用
Process.getCurrentThreadId()获取当前线程 - events:指定要追踪的事件类型
- onReceive:每积累一定数量事件后触发的批量回调
- onCallSummary:Stalker 停止追踪时输出的函数调用统计
事件类型
events: {
compile: true, // 代码编译事件(代码块被首次追踪时)
block: false, // 基本块执行事件
compile: false, // 编译事件
call: true, // 函数调用事件 (BL 指令)
ret: false, // 函数返回事件
exec: false, // 指令执行事件(最详细,性能开销最大)
exception: false // 异常事件
}
追踪基本块执行记录
基本块(Basic Block)是程序执行的最小连续单元——从入口到第一个分支指令之间的代码序列。追踪基本块可以得到函数的执行路径概览。
// 追踪基本块执行
function traceBasicBlocks(moduleName, funcOffset) {
var mod = Process.findModuleByName(moduleName);
var funcAddr = mod.base.add(funcOffset);
var blockLog = [];
var tid = Process.getCurrentThreadId();
// 在目标函数入口处启动 Stalker
Interceptor.attach(funcAddr, {
onEnter: function (args) {
blockLog = [];
Stalker.follow(tid, {
events: { block: true, compile: true },
onReceive: function (events) {
var parsed = Stalker.parse(events,
{ annotate: true, stringify: false });
parsed.forEach(function (ev) {
if (ev[0] === 'block') {
var blockAddr = ev[1];
blockLog.push(blockAddr);
}
});
}
});
// 保存输入参数
this._args = {
arg0: args[0],
arg1: args[1],
arg2: args[2] ? args[2].toInt32() : 0
};
},
onLeave: function (retval) {
// 停止追踪
Stalker.unfollow(tid);
Stalker.flush();
// 输出基本块执行序列
console.log("\n[基本块执行路径]");
console.log("共经过 " + blockLog.length + " 个基本块");
// 去除连续重复
var uniqueBlocks = [];
var prev = null;
for (var i = 0; i < blockLog.length; i++) {
if (blockLog[i] !== prev) {
uniqueBlocks.push(blockLog[i]);
prev = blockLog[i];
}
}
console.log("去重后 " + uniqueBlocks.length + " 个");
for (var i = 0; i < uniqueBlocks.length; i++) {
var offset = uniqueBlocks[i].sub(mod.base);
console.log(" " + (i + 1) + ". 0x" +
offset.toString(16));
}
}
});
}
traceBasicBlocks("libnative.so", 0x1A2B);
记录所有函数调用和参数
追踪函数调用可以理解算法的执行逻辑和调用关系。
// 追踪函数调用和参数
function traceFunctionCalls(moduleName, funcOffset) {
var mod = Process.findModuleByName(moduleName);
var funcAddr = mod.base.add(funcOffset);
var callLog = [];
var tid = Process.getCurrentThreadId();
Interceptor.attach(funcAddr, {
onEnter: function (args) {
callLog = [];
Stalker.follow(tid, {
events: { call: true },
onReceive: function (events) {
var parsed = Stalker.parse(events,
{ annotate: true, stringify: false });
parsed.forEach(function (ev) {
if (ev[0] === 'call') {
var target = ev[1];
var depth = ev[2]; // 调用深度
var caller = ev[3]; // 调用者地址
// 检查是否在目标模块内
if (target >= mod.base &&
target < mod.base.add(mod.size)) {
var offset = target.sub(mod.base);
callLog.push({
target: offset,
depth: depth,
caller: caller.sub(mod.base)
});
}
}
});
}
});
this._inputLen = args[2] ? args[2].toInt32() : 0;
},
onLeave: function (retval) {
Stalker.unfollow(tid);
Stalker.flush();
console.log("\n[函数调用追踪]");
console.log("共记录 " + callLog.length + " 次调用");
// 输出调用树
callLog.forEach(function (call, i) {
var indent = " ".repeat(call.depth);
console.log(indent + (i + 1) + ". " +
"→ 0x" + call.target.toString(16) +
" (from 0x" + call.caller.toString(16) + ")");
});
// 统计被调用最多的函数
var callCount = {};
callLog.forEach(function (call) {
var key = "0x" + call.target.toString(16);
callCount[key] = (callCount[key] || 0) + 1;
});
console.log("\n[调用频率 TOP 10]");
var sorted = Object.keys(callCount).sort(
function (a, b) { return callCount[b] - callCount[a]; }
);
sorted.slice(0, 10).forEach(function (key) {
console.log(" " + key + ": " + callCount[key] + " 次");
});
}
});
}
traceFunctionCalls("libcrypto.so", 0x4A20);
追踪 Native 代码执行流
使用 exec 事件进行指令级追踪
exec 事件记录每条指令的执行,是最详细的追踪模式,但性能开销也最大。
// 指令级执行追踪(谨慎使用,性能开销大)
function traceInstructions(moduleName, funcOffset, maxInstructions) {
var mod = Process.findModuleByName(moduleName);
var funcAddr = mod.base.add(funcOffset);
var instructionCount = 0;
var tid = Process.getCurrentThreadId();
Interceptor.attach(funcAddr, {
onEnter: function (args) {
instructionCount = 0;
var self = this;
self._startTime = Date.now();
Stalker.follow(tid, {
events: { exec: true },
transform: function (iterator) {
var instruction = iterator.next();
do {
// 只追踪目标模块内的指令
if (instruction.address >= mod.base &&
instruction.address < mod.base.add(mod.size)) {
var offset = instruction.address.sub(mod.base);
var mnemonic = instruction.mnemonic;
// 记录关键指令
if (['BL', 'BLR', 'B', 'CBZ', 'CBNZ',
'EOR', 'AND', 'ORR', 'LSL', 'LSR',
'LDR', 'LDRB', 'STR', 'STRB'].indexOf(
mnemonic) !== -1) {
iterator.putCallout(function (context) {
if (instructionCount < maxInstructions) {
instructionCount++;
console.log("[I" +
instructionCount + "] 0x" +
offset.toString(16) + ": " +
mnemonic + " " +
instruction.opStr);
}
});
}
}
iterator.keep();
} while ((instruction = iterator.next()) !== null);
}
});
},
onLeave: function (retval) {
Stalker.unfollow(tid);
Stalker.flush();
var elapsed = Date.now() - this._startTime;
console.log("\n[指令追踪完成]");
console.log("总指令数: " + instructionCount);
console.log("耗时: " + elapsed + "ms");
}
});
}
// 使用:限制最大追踪指令数以控制性能
traceInstructions("libcrypto.so", 0x4A20, 5000);
使用 Stalker 分析 OLLVM 混淆代码
Stalker 特别适合分析 OLLVM 混淆代码,因为它能够记录完整的执行路径,包括哪些代码块被执行了、哪些被跳过了。
追踪 FLA(控制流平坦化)的真实路径
// 使用 Stalker 分析 FLA 混淆
function analyzeFLAwithStalker(moduleName, funcOffset) {
var mod = Process.findModuleByName(moduleName);
var funcAddr = mod.base.add(funcOffset);
var tid = Process.getCurrentThreadId();
var executionPath = [];
Interceptor.attach(funcAddr, {
onEnter: function (args) {
executionPath = [];
Stalker.follow(tid, {
events: { block: true, call: true },
onReceive: function (events) {
var parsed = Stalker.parse(events);
parsed.forEach(function (ev) {
if (ev[0] === 'block') {
var addr = ev[1];
// 过滤只保留目标函数内的基本块
if (addr >= funcAddr &&
addr < funcAddr.add(0x500)) {
var offset = addr.sub(mod.base);
executionPath.push(offset);
}
}
});
}
});
},
onLeave: function (retval) {
Stalker.unfollow(tid);
Stalker.flush();
console.log("\n[FLA 分析 - Stalker 追踪结果]");
console.log("执行路径 (" + executionPath.length + " 个基本块):");
// 去除连续重复
var unique = [];
var prev = -1;
for (var i = 0; i < executionPath.length; i++) {
if (executionPath[i] !== prev) {
unique.push(executionPath[i]);
prev = executionPath[i];
}
}
// 输出执行路径
unique.forEach(function (offset, i) {
console.log(" Step " + i + ": 0x" +
offset.toString(16));
});
// 识别 dispatcher 循环(重复出现的地址)
var addrCount = {};
unique.forEach(function (offset) {
addrCount[offset] = (addrCount[offset] || 0) + 1;
});
var dispatcher = null;
var maxCount = 0;
Object.keys(addrCount).forEach(function (offset) {
if (addrCount[offset] > maxCount) {
maxCount = addrCount[offset];
dispatcher = parseInt(offset);
}
});
if (maxCount > unique.length * 0.3) {
console.log("\n[检测] Dispatcher 可能在 0x" +
dispatcher.toString(16) +
" (出现 " + maxCount + " 次)");
console.log("去除 dispatcher 后的真实路径:");
unique.forEach(function (offset) {
if (offset !== dispatcher) {
console.log(" → 0x" + offset.toString(16));
}
});
}
}
});
}
analyzeFLAwithStalker("libnative.so", 0x3A20);
分析虚假控制流(BCF)
// 多次执行,对比找出从不执行的块
function detectBogusWithStalker(moduleName, funcOffset, runCount) {
var mod = Process.findModuleByName(moduleName);
var funcAddr = mod.base.add(funcOffset);
var allBlocks = {}; // 所有观察到的块
var perRun = []; // 每次执行的块集合
var currentRun = 0;
// 获取函数内所有可能的代码块地址
// 可以通过 IDA 导出或手动指定
var knownOffsets = [
0x3A20, 0x3A30, 0x3A40, 0x3A50, 0x3A60,
0x3A70, 0x3A80, 0x3A90, 0x3AA0, 0x3AB0,
0x3AC0, 0x3AD0, 0x3AE0, 0x3AF0, 0x3B00
];
function doTrace() {
var tid = Process.getCurrentThreadId();
var runBlocks = new Set();
Interceptor.attach(funcAddr, {
onEnter: function (args) {
Stalker.follow(tid, {
events: { block: true },
onReceive: function (events) {
var parsed = Stalker.parse(events);
parsed.forEach(function (ev) {
if (ev[0] === 'block') {
var offset = ev[1].sub(mod.base).toInt32();
runBlocks.add(offset);
allBlocks[offset] =
(allBlocks[offset] || 0) + 1;
}
});
}
});
},
onLeave: function (retval) {
Stalker.unfollow(tid);
Stalker.flush();
perRun.push(runBlocks);
currentRun++;
}
});
}
// 执行多次追踪
doTrace();
// 延迟输出分析结果
setTimeout(function () {
console.log("\n[BCF 检测 - Stalker 多次执行对比]");
console.log("执行次数: " + currentRun);
knownOffsets.forEach(function (offset) {
var hits = allBlocks[offset] || 0;
var alwaysHit = perRun.every(function (run) {
return run.has(offset);
});
var status;
if (hits === 0) {
status = "❌ 从不执行 (BCF 虚假块)";
} else if (!alwaysHit) {
status = "⚠️ 部分执行 (" + hits + "/" +
currentRun + ")";
} else {
status = "✓ 每次都执行";
}
console.log(" 0x" + offset.toString(16) + ": " + status);
});
}, 5000);
}
detectBogusWithStalker("libnative.so", 0x3A20, 5);
性能注意事项和优化技巧
Stalker 的指令级追踪性能开销很大,使用时需要注意以下优化技巧:
1. 限制追踪范围
// 只追踪特定函数,不要追踪整个线程的生命周期
// 错误:在脚本加载时就 Stalker.follow()
// 正确:在目标函数 onEnter 时 follow,onLeave 时 unfollow
2. 使用 transform 过滤指令
// transform 回调中只对关心的指令插入 callout
transform: function (iterator) {
var instruction;
while ((instruction = iterator.next()) !== null) {
// 只对特定指令类型插入追踪
if (instruction.mnemonic === 'BL' ||
instruction.mnemonic === 'BLR') {
iterator.putCallout(function (ctx) {
// 只记录函数调用
});
}
iterator.keep();
}
}
3. 减少 onReceive 中的处理
// onReceive 中尽量做轻量操作
// 将数据收集后批量处理,避免在回调中做复杂计算
onReceive: function (events) {
// 只做简单的数据收集
eventBuffer.push(events);
}
4. 设置合理的超时
// 防止 Stalker 运行时间过长导致崩溃
var stalkerTimeout = setTimeout(function () {
Stalker.unfollow(tid);
Stalker.flush();
console.log("[!] Stalker 超时,已停止");
}, 10000); // 10 秒超时
完整案例:追踪加密算法从输入到输出的全过程
以一个 AES 变种加密函数为例,展示从输入到输出的完整追踪过程:
// 完整的加密算法追踪案例
function fullAlgorithmTrace(moduleName, funcOffset) {
var mod = Process.findModuleByName(moduleName);
var funcAddr = mod.base.add(funcOffset);
var tid = Process.getCurrentThreadId();
// 收集的数据
var traceData = {
blocks: [],
calls: [],
input: null,
output: null
};
Interceptor.attach(funcAddr, {
onEnter: function (args) {
traceData = { blocks: [], calls: [], input: null, output: null };
// 记录输入
var inputLen = args[2].toInt32();
traceData.input = {
ptr: args[0],
len: inputLen,
data: args[0].readByteArray(inputLen)
};
console.log("\n=== 加密函数追踪开始 ===");
console.log("[输入] " + inputLen + " 字节");
console.log(hexdump(args[0], { length: inputLen }));
this._startTime = Date.now();
Stalker.follow(tid, {
events: { block: true, call: true },
onReceive: function (events) {
var parsed = Stalker.parse(events,
{ annotate: true });
parsed.forEach(function (ev) {
if (ev[0] === 'block') {
traceData.blocks.push(
ev[1].sub(mod.base).toInt32());
} else if (ev[0] === 'call') {
var target = ev[1];
if (target >= mod.base &&
target < mod.base.add(mod.size)) {
traceData.calls.push(
target.sub(mod.base).toInt32());
}
}
});
}
});
},
onLeave: function (retval) {
Stalker.unfollow(tid);
Stalker.flush();
var elapsed = Date.now() - this._startTime;
// 记录输出
traceData.output = {
ptr: retval,
data: retval.readByteArray(traceData.input.len)
};
console.log("\n[输出] " + traceData.input.len + " 字节");
console.log(hexdump(retval, { length: traceData.input.len }));
// 分析结果
console.log("\n=== 追踪统计 ===");
console.log("执行时间: " + elapsed + "ms");
console.log("经过基本块: " + traceData.blocks.length);
console.log("函数调用次数: " + traceData.calls.length);
// 去重后的执行路径
var uniqueBlocks = [];
var prev = -1;
traceData.blocks.forEach(function (b) {
if (b !== prev) {
uniqueBlocks.push(b);
prev = b;
}
});
console.log("\n唯一基本块路径 (" +
uniqueBlocks.length + "):");
uniqueBlocks.forEach(function (offset, i) {
console.log(" " + (i + 1) + ". 0x" +
offset.toString(16));
});
// 调用频率分析
var callFreq = {};
traceData.calls.forEach(function (c) {
callFreq[c] = (callFreq[c] || 0) + 1;
});
console.log("\n被调用函数 (按频率排序):");
Object.keys(callFreq).sort(function (a, b) {
return callFreq[b] - callFreq[a];
}).slice(0, 10).forEach(function (offset) {
console.log(" 0x" + parseInt(offset).toString(16) +
": " + callFreq[offset] + " 次");
});
console.log("\n=== 追踪完成 ===");
}
});
}
fullAlgorithmTrace("libcrypto.so", 0x4A20);
总结
Frida Stalker 是分析复杂 Native 代码的终极武器。它通过代码重写技术实现了指令级的实时追踪,能够完整记录程序的执行路径。在使用 Stalker 分析 OLLVM 混淆代码时,可以通过追踪基本块执行路径来还原真实的控制流,识别虚假分支。在使用 Stalker 时要注意性能优化:限制追踪范围、使用 transform 过滤、合理设置超时。本文的完整案例展示了从输入到输出的全流程追踪方法,为算法还原提供了坚实的数据基础。