前面谈了kprobe的原理,其实uprobe也差不多:
https://blog.csdn.net/dog250/article/details/106520658
那么return probe如何实现呢?
我们知道,hook一个函数的起始位置非常容易,拿函数名当指针,直接修改成0xcc或者别的什么call/jmp即可,而hook一个函数的结束就没有这么简单了:
- 函数大小不容易计算。
- 函数可以在任意位置调用return。
怎么办呢?
很简单,只要执行流到了函数里面,直接取RSP寄存器指示的地址即可,它就是函数返回的地址,hook这个地址,就OK了。
于是,方法也就有了:
- 在函数开头打int3断点(也可以ftrace,但这里仅谈int3)。
- 在函数调用时的int3处理函数中获取stack上的return address。
- 将return adress替换成int3的address(也可以用单独的函数)。
- 在return address的int3处理函数中调用return probe函数。
- 恢复正常流程。
如下图所示:
下面是一个示例程序:
#include <stdio.h>
#include <sys/mman.h>
#include <signal.h>
// sigframe的RIP偏移
#define PC_OFFSET 192
// sigframe的RSP偏移
#define SP_OFFSET 184
// sigframe的RAX偏移,用于获取返回值
#define AX_OFFSET 168
#define I_BRK 0xcc
unsigned long *orig;
void trap(int unused);
void fbrk()
{
asm ("nop;");
}
unsigned char *pbrk;
void breakpoint(unsigned long addr)
{
unsigned char *page;
signal(SIGTRAP, trap);
page = (unsigned char *)((unsigned long)addr & 0xffffffffffff1000);
mprotect((void *)page, 4096, PROT_WRITE|PROT_READ|PROT_EXEC);
page = (unsigned char *)((unsigned long)fbrk & 0xffffffffffff1000);
mprotect((void *)page, 4096, PROT_WRITE|PROT_READ|PROT_EXEC);
// 配置int3 buffer,用于替换return address。
pbrk = page;
*pbrk = I_BRK;
// 保存函数头的原始指令,用于restore。
orig = (unsigned long *)*(unsigned long *)addr;
// 函数开头打断点。
*(unsigned char *)(addr) = I_BRK;
}
void trap(int unused)
{
unsigned long *p;
static int ret = 0;
if (ret == 0) { // 函数开头的int3处理
p = (unsigned long *)((unsigned char *)&p + PC_OFFSET);
// 恢复原始指令。
*p = *p - 1;
*(unsigned long *)*p = (unsigned long)orig;
// 保存原始的返回地址。
orig = (unsigned long *)*(unsigned long *)*(unsigned long *)((unsigned char *)&p + SP_OFFSET);
// 替换返回地址为int3 buffer。
*(unsigned long *)*(unsigned long *)((unsigned char *)&p + SP_OFFSET) = (unsigned long)pbrk;
ret = 1;
} else if (ret == 1) { // 函数返回时的int3处理
p = (unsigned long*)((unsigned char *)&p + PC_OFFSET);
printf("浙江温州皮鞋湿,下雨进水不会胖。[%d]\n", *(int *)((unsigned char *)&p + AX_OFFSET));
// 更改函数的返回值,仅做测试...
*(int *)((unsigned char *)&p + AX_OFFSET) = 1222;
// 恢复原始流程。
*p = (unsigned long)orig;
ret = 0;
}
}
// 测试函数,返回值为参数。
int test_function(int ret)
{
printf("[test function]\n");
return ret;
}
int main(int argc, char **argv)
{
int ret = 0;
ret = atoi(argv[1]);
breakpoint((unsigned long)&test_function);
printf("before call: %d\n", ret);
ret = test_function(ret);
printf("after call: %d\n", ret);
}
OK,编译,运行,看效果:
[root@localhost probe]# gcc retdebug.c -O0 -o retdebug
[root@localhost probe]# ./retdebug 12345
before call: 12345
[test function]
浙江温州皮鞋湿,下雨进水不会胖。[12345]
after call: 1222
成功打印了一句话并修改了返回值。
其实,内核中的kretprobe差不多也就是这个意思。
哦,不,你看我把return handler实现在trap信号处理函数里了,这并不好。不过在我的例子里,仅仅是打印一句话,所以也就无所谓了,真正正确的做法是,单独写一个stub,来call return handler,而不是用int3来中转:
; 汇编实现的stub
asm_stub:
SAVE_ALL;
call ret_handler;
RESTORE_ALL;
push _orig_;
ret;
// 更加简洁的trap函数
void trap(int unused)
{
unsigned long *p;
p = (unsigned long *)((unsigned char *)&p + PC_OFFSET);
// 恢复原始指令。
*p = *p - 1;
*(unsigned long *)*p = (unsigned long)orig;
// 保存原始的返回地址。
_orig_ = (unsigned long *)*(unsigned long *)*(unsigned long *)((unsigned char *)&p + SP_OFFSET);
// 替换返回地址为int3 buffer。
*(unsigned long *)*(unsigned long *)((unsigned char *)&p + SP_OFFSET) = (unsigned long)asm_stub;
}
嗯,这才是正确的方法:
后记
虽然我一直都在顽强得抗争着,但我感觉我的精神已经达到了顶点,很难再次突破,所以,我决定开始学习编程,顺便考个中级职称!基础差,底子薄并不可怕,过不了几个月,我应该就不会再说自己不会编程了,也算一件幸事!
浙江温州皮鞋湿,下雨进水不会胖。
来源:oschina
链接:https://my.oschina.net/u/4415254/blog/4300586