发布于 

使用 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 过滤、合理设置超时。本文的完整案例展示了从输入到输出的全流程追踪方法,为算法还原提供了坚实的数据基础。