criu中实现了一个代码注入工具 -> compel
这篇blog将对compel工具的实现原理,进行一个梳理。
代码注入
wiki对code injection的介绍如下:
1 | Code injection is the exploitation of a computer bug that is caused by processing invalid data. The injection is used by an attacker to introduce (or "inject") code into a vulnerable computer program and change the course of execution. |
简单的来说,代码注入,就是在某个进程上下文中执行一段代码。
实现要点
仅讨论广泛的实现,代码注入通常分为以下过程:
- 编写注入代码,生成relocate file
- 将该relocate file映射到进程上下文中
- 对注入的代码进行relocation处理(这一步不是必须的,取决于注入代码的复杂程度)
- 修改执行上下文的寄存器值,让进程跳转到区域执行
compel实现,在以上步骤的基础上,还加入了一个限制:注入程序用于获取上下文信息,其执行不能影响原有上下文,注入代码执行结束后,原有上下文可以继续执行。
下面,我将按个讨论以上步骤的具体实现。
生成用于注入的relocate file
实际上,注入代码并不一定是relocate file。只要求是可执行的汇编代码即可。这里我用relocate file来讨论,是因为注入代码可能很复杂,这种情况下,全部手写汇编是不现实的,因此势必要让编译器来生成最终的注入代码。这种情况下,不可避免的就会出现重定向过程。
由于我们是在运行时,加载该注入代码,因此重定向过程,需要我们自行编写代码完成。
为了简化实现,该relocate file生成过程存在以下的限制:
- 不能链接任何外部库,包括libc
- 内存模型需要高度定制 –> 需要自己编写链接脚本
compel的编译过程和linker script
相关的编译参数包括:
- ffreestanding -> 告诉编译器,该程序不使用任何标准库
- fno-stack-protector –> 不需要在栈中插入额外空间,方便我们简化设计
- nostdlib –> 不链接标准库
- fpie –> 生成位置无关的二进制代码
- -r -z noexecstack –> linker参数
链接脚本源码在这里
.compel.exit和.compel.init两个section是为了实现插件的初始化,不多赘述。
compel的syscall封装过程
源码涉及以下两个文件:
这里有意思的细节很多:
- 为了对齐加入0x90,0x90在X86下是NOP指令,见参考1
- “movq %rcx, %r10”是因为用户态函数调用的ABI和syscall的ABI存在一个细微差异,见参考2
将该relocate file映射到进程上下文中
这一步通过ptrace实现,ptrace可以实现在进程上下文进程syscall,具体原理此处不赘述。
对注入的代码进行relocation处理
源码实现在这里
这一步compel分成两部分进行
- 将第一步生成的relocate file进行预处理,生成一个头文件
- 头文件里记录了简化后的relocation information
第一步预处理,生成头文件,我个人猜测是为了执行速度和简化内存处理。这一步会为所有的section都分配内存,包含NOBITS的BSS,塞到一个unsigned char数组中。其他符号和重定向信息会生成特定的数据结构。
第二步,根据描述信息,对blob进行重定向处理。
最终执行时,还需要分配栈空间,信号处理函数的栈空间等等,其内存模型参考这里
这里内存分配中,在多线程环境下,有两个栈,一个用于主线程,一个是其他线程。其他线程共用一个栈,即其他线程之间不存在并发。
修改执行上下文的寄存器值,让进程跳转到区域执行
通过ptrace实现