首页
社区
课程
招聘
[原创]stalker源码浅入浅出
发表于: 2025-5-15 08:19 11661

[原创]stalker源码浅入浅出

2025-5-15 08:19
11661

记录一下之前的学习研究,最初的想法是熟悉一下stalker的大体轮廓,以便将来使用的时候万一需要修改或者定制,能够比较快速的进入状态.

stalker是以基本块为单位,翻译执行的,它会把原始的二进制代码以基本块为单位,拷贝到新内存,然后对这个拷贝过来的基本块执行翻译,也就是api中transform函数干的事情,这个图片很形象了,另外,stalker是线程级别的,只对某个线程trace



_GumExecCtx,这个是stalker的执行上下文,这个指针本身是存储在tls中的,线程独有,指针指向的内容是放在正常堆内存中的,以此来实现各个线程的执行上下文独立开来

之后是GumExecBlock,这玩意儿就是stalker复制并且插装好的基本块,它的内存布局如下

可以看到,除了保存了插装后的基本块,他还保存了原始代码的快照,为什么要保存这个快照呢,是因为后续的优化,stalker对基本块执行一次transform,之后再次执行这个基本块的时候,它会用原始代码和这个快照对比,如果没有发生变化,就不会重新翻译,变化了,就会重新翻译;下面是_GumExecBlock和stalker本身的定义

stalker本身是全局性的对象,虽然执行的时候以线程独立的方式各自运行的,但是各个线程都会用到一些通用的东西,所以stalker本身设计成全局性的,其中有一个变量叫做

这个变量配合着_GumExecBlock中的

每次执行到某个基本块,stalker就会先去对比原始代码和快照,上文也有提到,如果没变化给recycle_count加一,一直加到trust_threshold为止,到了trust_threshold,就说明这个基本块已经稳定了,不会发生变化,此后如果再次执行到这个基本块,就啥也不做了直接跳转到插好桩后的基本块,这个信任阈值是用户指定的


之后是_GumSlab,这个是stalker内存分配用的,为了避免每次都使用malloc或者mmap,比较影响性能,所以自己实现了内存分配机制,先预先申请一大块内存,之后需要用内存,就从这个块里获取,看一眼就知道大概意思和作用了,stalker里保存着很多

他们是以链表的形式保存的,里面保存着stalker插装好的基本块,slob还有快慢两种不同用处的内存块,把执行频率高的代码集中放入快路径,以提供cache命中率,不过这是琐碎的细节优化问题,借用著名外交官耿爽大使的一句话,不必理会

接下来是_GumStalkerIterator,主要负责配合transform函数进行具体的翻译的,后文会提到

最后是_GumGeneratorContext,他是翻译过程中用于生成指令的主要数据结构,而更加具体的功能是利用frida gum提供的能力,比如GumArm64Relocator用于将指令复制到新内存,会自动处理pc相关指令,GumArm64Writer用于生成指令

stalker的入口有两个api,gum_stalker_follow_me和gum_stalker_follow,以gum_stalker_follow为例说明,后文给出的代码只给出重要的部分,省略一部分平凡的

其中最后一个参数是事件相关的,stalker支持一些事件以及回调,比如执行基本块事件,执行指令事件,函数调用事件等等,通过事件机制让用户在这些重要的时间点执行一些操作

gum_stalker_follow做了很多事情,可分为两个阶段,第一阶段是为第二阶段调用gum_stalker_infect做好各种准备,也就是上面贴出来的代码;太细的就不说了,只说一些最重要的事情和大概的轮廓,stalker是通过不断地拉取基本块,翻译基本块,执行基本块,这样子工作的,那么如何拉取基本块呢,特别首次如何拉取基本块,就是gum_stalker_follow做的事情了,如果是gum_stalker_follow_me进入的,那么拉取基本块就比较简单,因为此时正在调用gum_stalker_follow_me函数,只要获取的lr寄存器,就可可以作为首个基本块的入口了;gum_stalker_follow跟踪的并不是本线程,而是跨线程,所以有一些技术在里面,stalker主要是通过clone系统调用,生成一个新线程,然后这个新线程通过ptrace系统调用,去attach要跟踪的目标线程,由此就可以拿到目标线程的寄存器上下文,通过pc寄存器即可拿到首个基本块的入口地址了,之所以采用clone而不是fork,是有两个原因的,一是因为stalker期望与主进程共享内存,fork出来的线程,是一个新进程,其内存是独立的,第二个原因是ptrace系统调用不允许attach同一个线程组的线程,使用clone可以比较精细的配置新线程,让它属于不同线程组,以满足这个要求;

注意,此时出现了三个线程,执行gum_stalker_follow的线程,成为主线程,clone出来的线程,称为clone线程,还有将要跟踪的目标线程,主线程和clone线程通过socket通信,主线程通过clone线程去ptrace目标线程,拿到目标线程的寄存器上下文,然后执行一定的逻辑,这些逻辑会修改寄存器上下文,最后让clone线程把被修改后的寄存器上下文写回目标线程,主要的修改就是获取了目标线程的pc,通过pc拉取基本块,翻译基本块,之后把pc改为指向翻译好的基本块,这样就完成了跟踪

上面说到gum_stalker_follow分为两阶段,一阶段主要是配置和clone新线程,二阶段就是在clone的线程中执行gum_stalker_infect

首先通过gum_stalker_create_exec_ctx创建线程独立的执行上下文GumExecCtx,之后通过gum_exec_ctx_obtain_block_for和拿到基本块入口(目标线程的pc寄存器值)拉拉取基本块GumExecBlock,拉取基本块的逻辑是,首先用入口地址查hash表,看看是否翻译过,如果翻译过就对比一下这个基本块recycle_count了多少次,是否达到信任阈值,如果达到了,则直接跳转到翻译好的基本块,如果没达到,就对比是否有变化,没变化的话就recycle_count加一,然后跳转到翻译好的基本块,有变化的话就触发gum_exec_ctx_recompile_block,重新翻译基本块,最后跳转到翻译好的基本块;如果是第一次执行这个基本块,就会触发transform去翻译,主要是通过gum_exec_ctx_compile_block函数完成,可以看到,在这个函数中调用了ctx->transform_block_impl (ctx->transformer, &iterator, &output);使用用户提高的transform回调去执行具体的翻译;这个函数另一个值得注意的点是,每次函数一进来会执行gum_arm64_writer_put_ldp_reg_reg_reg_offset,去恢复x16和x17寄存器,这两个寄存器是arm64的约定中,调用方和被调用方都不需要保存的,frida选择了这两个寄存器来完成全内存间接跳转,也就是br/blr 寄存器,之所以要在每次执行gum_exec_ctx_compile_block的时候要恢复这两个寄存器,原因是,stalker执行的时候是一个基本块一个基本块执行的,假如此时正在执行的是翻译后的基本块a,执行完a以后会去执行下一个翻译好的基本块,假若下一个基本块尚未翻译好,就要执行gum_exec_ctx_compile_block来翻译,而stalker跳转是通过x16,x17跳转的,污染了这两个寄存器,所以跳转前会先保存,跳转后再恢复,以保证寄存器上下文一致,特别的,如果不是通过br跳转到gum_exec_ctx_compile_block函数的,比如通过bl,那么stalker并不会跳到gum_exec_ctx_compile_block首地址,而是跳过了第一条指令,也就是不用恢复x16和x17了

gum_exec_ctx_compile_block函数中有一些涉及gc.continuation_real_address的逻辑,这些逻辑主要是处理内存不够的情况,比如翻译某个基本块的时候,翻译到原始基本块某条指令,发现存放翻译后基本块的内存不够了,就会使用gc.continuation_real_address记录下原始基本块这条指令的下一条指令,待stalker分配了新内存块后从这里继续翻译,并且处理从内存不够的基本块跳转到新分配基本块的那些逻辑,

最后看看gum_stalker_iterator_next函数,这个函数式transform函数会用到的,用于迭代原始基本块的所有指令,以完成用户自定义的插装逻辑

首先判断slob内存,如不足,通过

记录好断点位置的下一条指令,作为继续点

第二个事情是GumGeneratorContext的instruction存放着正在翻译的基本块正在迭代的那条指令,初始的时候设为空,这样根据是否为空就可以判断当前翻译的是不是第一条指令了;往下看,通过gum_arm64_relocator_read_one函数从原始基本块中读取一条指令,赋给self->generator_context->instruction.  gum_arm64_relocator_read_one是GumArm64Relocator的成员函数,因为我们翻译基本块的时候,很多时候都是会保留原始指令的,这就相当于把一条指令从原始基本块移到到新基本块,就需要用到GumArm64Relocator,而翻译的过程,其实也就是逐条迭代GumArm64Relocator中所有指令(某个基本块的所有指令)的过程,迭代的过程中,用户可以选择复制过来,或者不复制,同时也可以进行一些别的操作,比如插入回调,插入指令等等

对于stalker,了解原理之后我就有两个疑问,一个是首次的时候他是怎么劫持到控制流,然后开始他的翻译执行过程的,这个问题上文已经回答了,第二个问题是,它执行完某个翻译好的基本块之后,是怎么拿回控制权的,现在开始回答这个问题,首先是gum_stalker_iterator_keep函数

这个函数主要是把原始基本块中的指令放到新基本块,主要利用GumArm64Relocator完成;核心需要关注的地方是,stalker回去判断指令,是否是会分割基本块,比如各种跳转,函数调用指令等等,对于会分割基本块的指令,stalker会通过gum_exec_block_virtualize_*系列函数来对指令进行特殊处理,这里叫做虚拟化,以gum_exec_block_virtualize_branch_insn为例

这个函数主要是检查是哪种跳转指令,执行不同的逻辑,但是大体是相同的,所以随便挑了一个来看,首先调用GUM_ENTRYGATE宏生成一个函数名,不同的跳转指令会调用不同的函数,之后调用gum_exec_block_write_jmp_transfer_code把这个函数写入新基本块中,代替那条会分割基本块的指令,


[培训]Windows内核深度攻防:从Hook技术到Rootkit实战!

收藏
免费 11
支持
分享
最新回复 (14)
雪    币: 4338
活跃值: (3665)
能力值: ( LV12,RANK:220 )
在线值:
发帖
回帖
粉丝
2
好文,帮顶一下,看看你提的问题后面社区可以支持下,给大胡子pr
2025-5-15 08:26
1
雪    币: 4338
活跃值: (3665)
能力值: ( LV12,RANK:220 )
在线值:
发帖
回帖
粉丝
3

没好好看,作者已经写线程了,修改下评论

原来内容

线程级别可以补充下,他会读取当前线程的tls寄存器,然后存储下来内部做维护,保证线程的单一性转换

最后于 2025-5-15 08:30 被棕熊编辑 ,原因:
2025-5-15 08:27
1
雪    币: 1507
活跃值: (3893)
能力值: ( LV4,RANK:40 )
在线值:
发帖
回帖
粉丝
4
占位 前排 太好了这篇文章!
2025-5-15 11:26
0
雪    币: 7
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
5
感谢分享
2025-5-15 12:41
0
雪    币: 3480
活跃值: (8013)
能力值: ( LV9,RANK:235 )
在线值:
发帖
回帖
粉丝
6
d54K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8W2k6g2)9J5k6h3y4G2L8g2)9J5c8Y4W2#2L8Y4W2G2L8#2)9J5c8W2g2J5j5h3&6A6N6h3#2h3b7#2m8g2
插一个unicorn到目标进程,然后重写unicorn的MMU机制,直接操作进程内存。这种trace方法是不是会更好一些
2025-5-15 19:27
1
雪    币: 4149
活跃值: (7099)
能力值: ( LV5,RANK:60 )
在线值:
发帖
回帖
粉丝
7
天水姜伯约 2d7K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8W2k6g2)9J5k6h3y4G2L8g2)9J5c8Y4W2#2L8Y4W2G2L8#2)9J5c8W2g2J5j5h3&6A6N6h3#2h3b7#2m8g2 插一个unicorn到目标进程,然后重写unicorn的MMU机制,直接操作进程内存。这种trace方法是不是会更好一些
这个项目我很早就有留意,但是太小众,没有经过时间的检验,而且也没有开源,所以就没去尝试,后续我应该会尝试用frida的gum来自己写一个trace,
2025-5-15 20:30
0
雪    币: 3795
活跃值: (5972)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
8
感谢分享
2025-5-16 13:22
0
雪    币: 5436
活跃值: (2390)
能力值: ( LV12,RANK:230 )
在线值:
发帖
回帖
粉丝
9
学习了
2025-5-16 15:24
0
雪    币: 7
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
10
KerryS 这个项目我很早就有留意,但是太小众,没有经过时间的检验,而且也没有开源,所以就没去尝试,后续我应该会尝试用frida的gum来自己写一个trace,
可以支持unidbg的tracewrite吗
2025-5-18 11:46
0
雪    币: 244
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
11
我觉得unicorn还是蛮好用的 但是线程还得自己搞了
2025-5-21 09:49
0
雪    币: 402
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
12
我感觉是这个21亿对于arm64不太够用
Unable to allocate code slab near 0x6fd644d000 with max_distance=2138779647
我试了下把 spec.max_distance = 0xffffffffffffffff; 距离调大 试了50次就闪退一次
2025-5-23 03:19
1
雪    币: 4149
活跃值: (7099)
能力值: ( LV5,RANK:60 )
在线值:
发帖
回帖
粉丝
13
我爱学习ing 我感觉是这个21亿对于arm64不太够用 Unable to allocate code slab near 0x6fd644d000 with max_distance=2138779647 我 ...
非常好的做法,再把初始分配 gsize code_slab_size_initial;调大,然后动态分配的slow_slab_size_dynamic调小,应该还能进一步增加稳定性,这样的话stalker就会足够稳定了,就没必要再自己写trace工具了,不过性能可能会下降比较多,
2025-5-23 17:00
0
雪    币: 211
活跃值: (800)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
14
佬,求帮助,
在编译gum的时候有没有遇到gum的有些api会崩吗
    auto module = gum_process_find_module_by_name(so_name_);
    gum_module_ensure_initialized(module);
就比如这两个,module不是空,而且可以正确获取到地址和size,但是执行下面gum_module_ensure_initialized就会崩溃,stalker的api也会直接崩溃

我这边编译的是官方frida下的gum,16.7.1,ndk是r25c,gcc是9.4.0,
这个是构建的命令:./configure --host=android-arm64 -- -Dfrida-gum:devkits=gum
2025-6-9 16:18
0
雪    币: 57
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
15
多线程trace有什么方案吗
2025-12-31 20:54
0
游客
登录 | 注册 方可回帖
返回