正文
这个变量配合着
_GumExecBlock中的
每次执行到某个基本块,stalker就会先去对比原始代码和快照,上文也有提到,如果没变化给recycle_count加一,一直加到trust_threshold为止,到了
trust_threshold,就说明这个基本块已经稳定了,不会发生变化,此后如果再次执行到这个基本块,就啥也不做了直接跳转到插好桩后的基本块,这个信任阈值是用户指定的。
之后是_GumSlab,这个是stalker内存分配用的,为了避免每次都使用malloc或者mmap,比较影响性能,所以自己实现了内存分配机制,先预先申请一大块内存,之后需要用内存,就从这个块里获取,看一眼就知道大概意思和作用了,stalker里保存着很多。
他们是以链表的形式保存的,里面保存着stalker插装好的基本块,slob还有快慢两种不同用处的内存块,把执行频率高的代码集中放入快路径,以提供cache命中率,不过这是琐碎的细节优化问题,借用著名外交官耿爽大使的一句话,不必理会。
struct _GumSlab
{
guint8 * data;
guint offset;
guint size;
guint memory_size;
GumSlab * next;
接下来是_GumStalkerIterator,主要负责配合transform函数进行具体的翻译的,后文会提到。
struct _GumStalkerIterator
{
GumExecCtx * exec_context;
GumExecBlock * exec_block;
GumGeneratorContext * generator_context;
GumInstruction instruction;
GumVirtualizationRequirements requirements;
};
最后是_GumGeneratorContext,他是翻译过程中用于生成指令的主要数据结构,而更加具体的功能是利用frida gum提供的能力,比如GumArm64Relocator用于将指令复制到新内存,会自动处理pc相关指令,GumArm64Writer用于生成指令。
struct _GumGeneratorContext
{
GumInstruction * instruction;
GumArm64Relocator * relocator;
GumArm64Writer * code_writer;
GumArm64Writer * slow_writer;
gpointer continuation_real_address;
GumPrologType opened_prolog;
};
stalker的入口有两个api,gum_stalker_follow_me和
gum_stalker_follow,以
gum_stalker_follow为例说明,后文给出的代码只给出重要的部分,省略一部分平凡的。
1.gum_stalker_follow (GumStalker * self,
GumThreadId thread_id,
GumStalkerTransformer* transformer,
GumEventSink* sink)
gumInfectContext ctx;
ctx.stalker = self;
ctx.transformer = transformer;
ctx.sink = sink;
gum_process_modify_thread (thread_id, gum_stalker_infect, &ctx,
GUM_MODIFY_THREAD_FLAGS_NONE);
2.gum_process_modify_thread (GumThreadId thread_id,
GumModifyThreadFunc func,
gpointer user_data,
GumModifyThreadFlags flags)
GumModifyThreadContext ctx;
ctx.func = func;
ctx.user_data = user_data;
return gum_linux_modify_thread (thread_id, GUM_REGS_GENERAL_PURPOSE,
gum_do_modify_thread, &ctx, NULL);
3. gum_linux_modify_thread (GumThreadId thread_id,
GumLinuxRegsType regs_type,
GumLinuxModifyThreadFunc func,
gpointer user_data,
GError** error)
ctx.thread_id = thread_id;
ctx.regs_type = regs_type;
ctx.func = func;
ctx.user_data = user_data;
child = gum_libc_clone (
gum_linux_do_modify_thread,
stack + gum_query_page_size (),
CLONE_VM|CLONE_SETTLS,
&ctx,
NULL,
desc,
NULL);
prctl (PR_SET_PTRACER, child);
if (thread_id == gum_process_get_current_thread_id ())
{
success =GPOINTER_TO_UINT (g_thread_join (g_thread_new (
"gum-modify-thread-worker",
gum_linux_handle_modify_thread_comms,
&ctx)));
}
else
{
success =GPOINTER_TO_UINT (gum_linux_handle_modify_thread_comms (&ctx));
}
4.gum_linux_handle_modify_thread_comms (gpointer data)
GumLinuxModifyThreadContext* ctx = data;
ctx->func (ctx->thread_id, &ctx->regs_data, ctx->user_data);
5.gum_do_modify_thread (GumThreadId thread_id,
GumRegs* regs,
gpointer user_data)
GumModifyThreadContext* ctx = user_data;
ctx->func (thread_id, &cpu_context, ctx->user_data);
6.gum_stalker_infect (GumThreadId thread_id,
GumCpuContext* cpu_context,
gpointer user_data)
其中最后一个参数是事件相关的,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。