Linux系統調試之return probe原理和示例

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

嗯,這纔是正確的方法:
在這裏插入圖片描述

後記
雖然我一直都在頑強得抗爭着,但我感覺我的精神已經達到了頂點,很難再次突破,所以,我決定開始學習編程,順便考個中級職稱!基礎差,底子薄並不可怕,過不了幾個月,我應該就不會再說自己不會編程了,也算一件幸事!


浙江溫州皮鞋溼,下雨進水不會胖。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章