return handler 所谓return handler,是指在目标函数执行结束离开该函数时触发的handler。一般的断点是对指令进行替换,触发系统的异常处理。return handler的问题在于,函数有多个return出口,不可能全部一一识别并替换。
但是函数总是要返回的,返回的地址必然被记录在某处地方。在ARM体系结构下,有专门的LR寄存器。在X86下,return address则是被压入栈中。
注意,我们拿到的是记录返回地址的地址。这句话有点绕,寄存器或者栈本身不是返回的执行地址,而是记录了返回地址的地方。有两种办法,去修改返回地址:
修改寄存器或者栈,直接指向某一处handler地址,函数返回后,就会跳转到该handler执行。需要注意的是,这种方案下,这个handler必须跟函数在同一地址空间里。也就是,这个handler是在用户态执行的。(这里只讨论用户态程序)
修改寄存器或者栈指向的地址,修改成break指令。这样函数在执行完毕,跳转回去后,执行该指令后,将会触发系统的异常处理,从而进入了系统的handler处理。需要注意的是,系统的handler处理是在内核态触发的。
然而,仍然有两个问题需要解决:
上面两种方案的前提,都是要识别已经进入函数中,再对return address进行修改。因此return handler断点处理首先需要在函数的入口处打上一般断点,在入口的断点处理流程中,修改返回值。
修改之后如何还原,第一种方案需要记录原来的地址,在handler里面通过jmp还原正常的处理流程。第二种方案使用一般断点的处理思路即可。
X64 return handler的原理示例程序 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 #include <stdio.h> #include <string.h> #include <unistd.h> unsigned long original_ret; static void ret_handler(void) { // doing something necessary printf("return handler executed here 0x%lx \n", original_ret); asm volatile("jmp *%0" : : "r" (original_ret)); } unsigned long hijack_handler = 0x40116a; #define hijack_return_address() { \ asm volatile( \ "mov %%rbp, %%rdi \n\t" \ "addq $0x8, %%rdi \n\t" \ "mov (%%rdi), %0 \n\t" \ "movq %1, (%%rdi) \n\t" \ : "=&r" (original_ret) \ : "r" (hijack_handler) \ : "%rdi" \ ); \ } static unsigned long test_print(void) { int i; char name[] = "test for hijack handler \n"; unsigned long ret = 0; for (i = 0; i < strlen(name); i++) { printf("%c", name[i]); ret += name[i]; } // we can execute it in any place within this func. hijack_return_address(); return ret; } int main(void) { while (1) { test_print(); sleep(10); } return 0; }
一些说明:
内联汇编中,由于输入和输出会操作同一个寄存器,一个是写,一个是读。因此,需要限制输入和输出动作不会使用同一个额外的寄存器进行跳转,因此要加上”&”。GNU的说明,参考此处 。
此处汇编写成宏,是因为call指令会影响rbp/rsp寄存器,从而影响return address的寻找,实际应用过程中,这里对return address的修改一般是通过代码注入完成的。