这不一定是自修改代码完全可以 - 可以动态创建的代码相反,即运行时生成的“蹦床”。
这意味着您保留一个(全局)函数指针,它将重定向到内存的可写/可执行映射部分 - 然后您可以在其中主动插入您想要进行的函数调用。
这样做的主要困难是call
是与 IP 相关的(大多数jmp
),这样您就必须计算蹦床的内存位置和“目标函数”之间的偏移量。这本身就很简单 - 但是将其与 64 位代码结合起来,您就会遇到相对位移call
只能处理+-2GB范围内的位移,它变得更加复杂 - 你需要通过链接表进行调用。
所以你本质上会创建这样的代码(/me 严重 UN*X 偏见,因此 AT&T 汇编,以及一些对 ELF-isms 的引用):
.Lstart_of_modifyable_section:
callq 0f
callq 1f
callq 2f
callq 3f
callq 4f
....
ret
.align 32
0: jmpq tgt0
.align 32
1: jmpq tgt1
.align 32
2: jmpq tgt2
.align 32
3: jmpq tgt3
.align 32
4: jmpq tgt4
.align 32
...
这可以在编译时创建(只需创建一个可写文本部分),也可以在运行时动态创建。
然后,您在运行时修补跳跃目标。这类似于.plt
ELF 部分(PLT = 过程链接表)有效 - 只是在那里,它是修补 jmp 插槽的动态链接器,而在您的情况下,您自己执行此操作。
如果您选择所有运行时,那么甚至可以通过 C/C++ 轻松创建上面这样的表;从数据结构开始,例如:
typedef struct call_tbl_entry __attribute__(("packed")) {
uint8_t call_opcode;
int32_t call_displacement;
};
typedef union jmp_tbl_entry_t {
uint8_t cacheline[32];
struct {
uint8_t jmp_opcode[2]; // 64bit absolute jump
uint64_t jmp_tgtaddress;
} tbl __attribute__(("packed"));
}
struct mytbl {
struct call_tbl_entry calltbl[NUM_CALL_SLOTS];
uint8_t ret_opcode;
union jmp_tbl_entry jmptbl[NUM_CALL_SLOTS];
}
这里唯一关键且有点依赖于系统的事情是它的“打包”性质,需要告诉编译器(即不要填充call
array out),并且应该对跳转表进行缓存行对齐。
你需要做calltbl[i].call_displacement = (int32_t)(&jmptbl[i]-&calltbl[i+1])
,初始化空/未使用的跳转表memset(&jmptbl, 0xC3 /* RET */, sizeof(jmptbl))
然后根据需要填写跳转操作码和目标地址的字段。