Frida学习笔记(二十四):Stalker指令级追踪"

admin 2026-10-18 21:52:09 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

cover_image

Frida学习笔记(二十四):Stalker%20指令级追踪

原创

泡泡以安
泡泡以安

泡泡以安

2026年6月29日%2008:34
浙江

在小说阅读器读本章

去阅读

本篇目标:掌握%20Frida%20内置的代码追踪引擎%20Stalker,实现指令级别的执行跟踪。前面几篇教的%20Interceptor%20是「方法级」Hook——它能告诉你函数被调用了、参数是什么、返回值是什么,但看不到函数内部的执行细节。当你面对%20OLLVM%20混淆的加密函数、VMP%20保护的关键逻辑、或者需要分析代码覆盖率时,Stalker%20才是真正的\"杀手锏\"——它能像放慢镜头一样逐条回放每一条汇编指令、每一次%20CALL/RET、每一个寄存器的值变化。本篇从原理到实战,全面讲解%20Stalker%20的使用方法和优化技巧。

一、Stalker%20概述 1.1%20什么是%20Stalker

Stalker%20是%20Frida%20提供的线程级代码追踪引擎,它能够:

  • 跟踪线程执行:捕获目标线程执行的每一条指令
  • 记录函数调用:追踪所有%20CALL/RET%20指令
  • 实时代码插桩:在指令执行前后插入自定义逻辑
  • 修改执行流程:通过%20transform%20回调动态修改代码行为

简单来说,如果%20Interceptor%20是在高速公路入口/出口设置的检查站,那么%20Stalker%20就是一架全程跟拍的无人机——它能看到车辆在高速上的每一个动作:变道、加速、转弯,一览无余。

1.2%20底层原理

Stalker%20基于%20动态重编译(Dynamic%20Recompilation)%20机制工作,这种机制也被称为%20copy-on-exec:

Stalker%20动态重编译工作原理

具体流程如下:

  1. 拦截执行:当目标线程即将执行一个基本块(Basic%20Block,即一段没有分支的连续指令序列)时,Stalker%20拦截执行
  2. 分配新内存:Stalker%20分配一块新的内存区域
  3. 复制并重定位:将原始基本块的指令复制到新位置,在复制过程中进行指令重定位(Relocation),处理%20PC%20相关寻址(如%20adr、adrp、PC-relative%20load%20等)
  4. 插入插桩代码:在指令副本的适当位置插入事件记录代码、回调调用代码等
  5. 修改跳转:修改基本块末尾的分支指令,使其跳回%20Stalker%20引擎,以便对下一个基本块进行同样的处理
  6. 执行副本:线程转而执行插桩后的代码副本

什么是基本块(Basic%20Block)?%20基本块是编译原理中的一个基础概念,指一段连续的指令序列,其中只有第一条指令是跳转目标(入口),只有最后一条指令是分支/跳转指令(出口)。换句话说,进入基本块后,程序一定会顺序执行到最后一条指令,中间不会有跳转。

这种%20copy-on-exec%20机制的巧妙之处在于:它不修改原始代码,因此不会被简单的代码完整性校验检测到。已编译的基本块会被缓存在内存中,避免重复编译带来的开销——第二次执行同一个基本块时,直接执行缓存的副本即可。

类比一下:Stalker%20像是一家代工厂——原版代码不动,工厂照着原版复制一份「代工货」,在复制过程中给每条指令贴上小标签(插桩代码),把代工货装回去让%20CPU%20跑。CPU%20跑的全是代工货,完全不知道工厂的存在——所以代码完整性校验(对原版做%20hash)抓不到它。后面要讲的%20Stalker.invalidate(让代工货作废重新生产)、SMC%20兼容(原版自己改了,要重新代工)、反检测(原版没动%20=%20代码%20hash%20校验过得了),三处都是这条「代工厂」机制的直接推论。

1.3%20支持架构
架构 支持状态 典型场景
AArch64 完整支持 Android/iOS%20手机和平板
ARM%20(32-bit) 支持 旧版%20Android%20设备
Intel%20x64 完整支持 桌面应用、模拟器
IA-32%20(x86) 支持 32位桌面应用

提示:在实际逆向工作中,AArch64%20是最常用的架构。现代%20Android%20设备(Android%207.0+)和所有%20iOS%20设备基本都是%2064%20位%20ARM。如果你的目标%20App%20仍然只提供%2032%20位%20SO,可以考虑使用%2064%20位设备的%2032%20位兼容模式运行。

1.4%20Stalker%20vs%20Interceptor

Interceptor%20vs%20Stalker%20对比

特性 Interceptor Stalker
粒度 函数级 指令级
性能开销 低 较高(exec%20全开%20~80-100x、call/ret%20~10-15x、稳态%20~3-5x,详见第%205%20章)
使用场景 Hook%20特定函数 追踪执行流程
修改能力 修改参数/返回值 修改任意指令
数据量 少量精确数据 大量追踪数据
适用阶段 已知目标函数 探索未知逻辑

最佳实践:先用%20Interceptor%20定位关键函数,再用%20Stalker%20深入分析函数内部的指令执行流程。这种\"先粗后细\"的策略能在效率和精度之间取得最佳平衡。

二、核心%20API%20详解

下面这%209%20个%20API%20不必现在全记。按使用频率分三档:

  • 4%20件套(任何%20Stalker%20脚本都会用到):follow%20/%20unfollow%20/%20flush%20/%20exclude
  • 按场景使用(知道有就行,等遇到问题再回头查):invalidate(处理自修改代码)、addCallProbe(函数调用探针)、parse(解析事件二进制)
  • 配置项(写完一次基本不动):queueCapacity%20/%20queueDrainInterval%20/%20trustThreshold

读本章时不要逐%20API%20死记;混个脸熟,等%20第%203%20章%20看到完整脚本时再回头查表。下图先给个%20API%20关系全景图:

Stalker%20核心%20API%20全景图

2.1%20Stalker.follow(threadId,%20options)

这是%20Stalker%20最核心的%20API,用于开始跟踪指定线程。它接受线程%20ID%20和一个配置对象,配置对象中可以指定要捕获的事件类型和回调函数。

//%20Stalker.follow%20-%20开始跟踪指定线程
//%20threadId:%20目标线程的%20ID(可通过%20Process.enumerateThreads()%20或%20this.threadId%20获取)
Stalker.follow(threadId, {
    //%20事件配置:指定要捕获哪些类型的事件
    events: {
        call: true,    //%20捕获%20CALL%20指令(函数调用,如%20BL/BLR)
        ret: false,    //%20捕获%20RET%20指令(函数返回)
        exec: false,   //%20捕获所有执行的指令(慎用!数据量极大,可能导致卡死)
        block: false,  //%20捕获基本块执行(每次进入一个基本块时触发)
        compile: false //%20捕获基本块编译(仅在首次编译时触发,适用于代码覆盖率收集)
    },

    //%20方式一:接收原始事件数据(二进制格式,数据更完整)
    onReceive(events) {
        //%20events%20是%20ArrayBuffer,包含%20GumEvent%20结构体数组
        //%20需要使用%20Stalker.parse()%20解析为可读格式
        const parsed = Stalker.parse(events, {
            annotate: true,  //%20在结果中标注事件类型(call/ret/exec/block/compile)
            stringify: true  //%20将%20NativePointer%20格式化为十六进制字符串(减少%20send()%20的序列化开销)
        });
        console.log(JSON.stringify(parsed));
    },

    //%20方式二:接收调用统计摘要(更高效,适合快速了解调用分布)
    //%20注意:onReceive%20和%20onCallSummary%20是互斥的,只能选择其中一个!
    //%20onCallSummary(summary)%20{
    //%20%20%20%20%20//%20summary%20是一个对象:{%20\"0x7fff12345\":%2042,%20\"0x7fff12346\":%2010,%20...%20}
    //%20%20%20%20%20//%20key%20为调用目标地址,value%20为在统计周期内的调用次数
    //%20},

    //%20高级功能:transform%20回调(后续第四节详细介绍)
    //%20transform(iterator)%20{%20...%20}
});

重要提示:onReceive%20和%20onCallSummary%20只能二选一。如果你不需要某种回调,不要传空函数(如%20onReceive()%20{}),因为即使回调是空的,Stalker%20仍然会为其生成事件数据,造成不必要的性能开销。

2.2%20Stalker.unfollow(threadId)

停止跟踪指定线程。如果省略参数,则停止跟踪当前线程:

//%20停止跟踪指定线程
Stalker.unfollow(targetThreadId);

//%20停止跟踪当前线程(省略参数时默认当前线程)
Stalker.unfollow();
2.3%20Stalker.flush()

立即刷新事件缓冲区,将已缓冲的事件数据发送到%20onReceive%20/%20onCallSummary%20回调:

//%20强制刷新缓冲区,不等待下一个%20queueDrainInterval%20周期
Stalker.flush();