Linux系统调试之return probe原理和示例

允我心安 提交于 2020-08-10 12:06:58

前面谈了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;
}

嗯,这才是正确的方法:
在这里插入图片描述

后记
虽然我一直都在顽强得抗争着,但我感觉我的精神已经达到了顶点,很难再次突破,所以,我决定开始学习编程,顺便考个中级职称!基础差,底子薄并不可怕,过不了几个月,我应该就不会再说自己不会编程了,也算一件幸事!


浙江温州皮鞋湿,下雨进水不会胖。

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!