Stalker.follow()
内部实现原理
当用户调用Stalker.follow()
时,内部调用:
- 要么是:
gum_stalker_follow_me()
:去跟踪当前的线程thread- 函数原型
GUM_API void gum_stalker_follow_me (GumStalker * self, GumStalkerTransformer * transformer, GumEventSink * sink);
- 底层
JS
引擎:是QuickJS
或V8
- 函数原型
- 要么是:
gum_stalker_follow(thread_id)
:去跟踪当前process进程中的其他某个线程thread
gum_stalker_follow_me
的内部原理
#ifdef __APPLE__
.globl _gum_stalker_follow_me
_gum_stalker_follow_me:
#else
.globl gum_stalker_follow_me
.type gum_stalker_follow_me, %function
gum_stalker_follow_me:
#endif
stp x29, x30, [sp, -16]!
mov x29, sp
mov x3, x30
#ifdef __APPLE__
bl __gum_stalker_do_follow_me
#else
bl _gum_stalker_do_follow_me
#endif
ldp x29, x30, [sp], 16
br x0
-》
- 内部原理
- LR=Link Register=X30=链接寄存器
- AArch64架构中,根据LR去决定从哪里开始跟踪
- 当遇到BR,BLR等跳转指令时,会去设置LR
- LR被设置为,当前函数返回后,继续运行的地址
- 由于只有一个LR,如果被调用函数调用了其他函数,此时LR的值就会被保存起来,比如保存到Stack栈上,后续当RET指令执行之前,会重新把LR从Stack栈中加载到寄存器中,最终返回到调用者
- FP=Frame Pointer=X29=帧指针
- FP始终指向Stack top栈的顶部,表示当前函数被调用时的栈的位置
- 所以就可以通过固定的偏移量去访问到,所有通过Stack栈传入的参数和基于栈的局部变量
- 且每个函数都有自己的FP,所以需要调用新函数时,保存之前的FP,返回之前函数时,恢复FP。
- 在刚进入新函数后,备份FP后,就可以去设置:mov x29, sp,把SP给X29=FP了。
- LR=Link Register=X30=链接寄存器
保持了原先传入x0-x2
的3个参数,额外加上x3
=x30
=LR
,所以再去调用函数,就对应上参数了:
gpointer_gum_stalker_do_follow_me (GumStalker * self, GumStalkerTransformer * transformer, GumEventSink * sink, gpointer ret_addr)
gum_stalker_follow
的内部原理
和gum_stalker_follow_me()
类似,但有额外参数:thread_id
void
gum_stalker_follow (GumStalker * self,
GumThreadId thread_id,
GumStalkerTransformer * transformer,
GumEventSink * sink)
{
if (thread_id == gum_process_get_current_thread_id ())
{
gum_stalker_follow_me (self, transformer, sink);
}
else
{
GumInfectContext ctx;
ctx.stalker = self;
ctx.transformer = transformer;
ctx.sink = sink;
gum_process_modify_thread (thread_id, gum_stalker_infect, &ctx);
}
}
其中:gum_process_modify_thread()
,不属于Stalker
,但属于Gum
回调callback会去修改:GumCpuContext
GumCpuContext
GumCpuContext的定义:
typedef GumArm64CpuContext GumCpuContext;
struct _GumArm64CpuContext
{
guint64 pc;
guint64 sp;
guint64 x[29];
guint64 fp;
guint64 lr;
guint8 q[128];
};
相关:
static void
gum_stalker_infect (GumThreadId thread_id,
GumCpuContext * cpu_context,
gpointer user_data)
gum_process_modify_thread()
内部实现- Linux/Android:ptrace
- GDB也用的这个:挂载到进程上,读写寄存器
- Linux/Android:ptrace
Stalker每次只处理一个代码块block
内部机制:
新申请一块内存,写入给原始代码中加了调试代码后的代码
加的指令,用于生成事件、提供其他Stalker所支持的功能。
以及根据情况去relocate重定位指令代码。
比如对于下面代码,要重定位:
- ADR Address of label at a PC-relative offset.
ADR Xd, label
Xd
Is the 64-bit name of the general-purpose destination register, in the range 0 to 31.label
Is the program label whose address is to be calculated. It is an offset from the address of this instruction, in the range ±1MB.
底层通过Gum的Relocator
frida-gum/gumarm64relocator.c at main · frida/frida-gum · GitHub
现在,回想一下我们说过潜行者一次工作一个块。那么我们如何检测下一个块呢?我们还记得每个块也以分支指令结尾,如果我们修改这个分支以分支回 Stalker 引擎,但确保我们存储分支打算结束的目的地,我们可以检测下一个块并在那里重定向执行。这个相同的简单过程可以一个接一个地继续。
Stalker=潜行者
现在,这个过程可能有点慢,因此我们可以应用一些优化。首先,如果我们多次执行相同的代码块(例如循环,或者可能只是一个多次调用的函数),我们不必重新检测它。我们可以重新执行相同的检测代码。出于这个原因,我们保留了一个哈希表,其中包含我们之前遇到的所有块以及我们放置块的检测副本的位置。
其次,当遇到呼叫指令时,在发出检测的呼叫后,我们随后会发出一个着陆板,我们可以返回该着陆板而无需重新进入 Stalker。Stalker使用记录真实返回地址(real_address)和此着陆垫(code_address)的GumExecFrame结构构建了一个侧堆栈。当一个函数返回时,我们发出代码,该代码将根据real_address检查侧堆栈中的返回地址,如果匹配,它可以简单地返回到code_address,而无需重新进入运行时。这个着陆板最初将包含进入 Stalker 引擎以检测下一个块的代码,但稍后可以将其反向修补以直接分支到该块。这意味着可以处理整个返回序列,而无需输入和离开 Stalker。
如果返回地址与存储的GumExecFrame real_address不匹配,或者我们在侧堆栈中的空间不足,我们只需从头开始重新构建一个新的。我们需要在应用程序代码执行时保留 LR 的值,以便应用程序不能使用它来检测 Stalker 的存在(反调试),或者如果它将其用于除简单返回之外的任何其他目的(例如,在代码部分中引用内联数据)。此外,我们希望 Stalker 能够随时取消关注,所以我们不想不得不返回我们的堆栈来更正我们在此过程中修改的 LR 值。
最后,虽然我们总是用对 Stalker 的调用来替换分支以检测下一个块,但根据 Stalker.trustThreshold 的配置,我们可能会对此类检测代码进行反向修补,以将调用替换为下一个检测块的直接分支。确定性分支(例如,目的地是固定的,分支不是有条件的)很简单,我们可以将分支替换为下一个块。但是我们也可以处理条件分支,如果我们检测两个代码块(一个是分支,一个是不是)。然后,我们可以将原始条件分支替换为一个条件分支,该条件分支将控制流定向到获取分支时遇到的块的检测版本,然后是另一个检测块的无条件分支。我们还可以部分处理目标不是静态的分支。假设我们的分支是这样的:
BR X0
这种指令在调用函数指针或类方法时很常见。虽然 X0 的值可以更改,但通常它实际上总是相同的。在这种情况下,我们可以将最终的分支指令替换为代码,该代码将 X0 的值与我们的已知函数进行比较,如果它将分支与代码的检测副本的地址匹配。然后,如果不匹配,则可以将无条件分支返回到 Stalker 引擎。因此,如果函数指针 say 的值发生了变化,那么代码仍然有效,无论我们最终到达哪里,我们都将重新输入 Stalker 和乐器。但是,如果如我们预期的那样它保持不变,那么我们可以完全绕过 Stalker 引擎并直接进入仪器化功能。