基于探针的动态插桩
由于基于探针的动态插桩,通常只能在函数边界插入代码,难以对程序的指令流进行很好的分析,所以平时用的比较少。以前使用微软研究院的detour的API觉得它很神奇,最近看了下它的原理还是很简单:基于简单动态重写函数的开始几个字节,然后跳转到特定函数。呵呵,但是要做好还是不容易的。闲来无事写了一个很粗糙的实现。
基本原理就是:(1)保存函数的入口的几个字节,并插入一天跳回函数的jmp指令(这一块代码称为trampaline)。这里的前几个字节不是个定数是有原因的,实际上我们只需要前5字节来保存一条JMP指令,但入口的5个字节可能并不是几条完整的指令,因此若只保存5个字节就会截断指令。如下面的代码所示,test函数入口的第5个字节包含于sub,保存前6个字节就可以避免截断sub指令。
(2)修改函数入口的5个字节为jmp xxx指令,其中的xxx就是探针函数到当前函数的偏移量。跳往探针函数并执行它。
(3)执行trampaline代码,执行原函数。
(3)恢复原函数的入口。
基本数据结构:
/*探针函数描述符*/
typedef struct probe {
int id; /*探针id*/
int ref; /*引用计数*/
void *probe_fuc; /*函数地址*/
arg_type_t arg[MAX_ARG]; /*参数类型*/
char name[MAX_FUC_NAME_LEN]; /*函数名*/
struct probe *next; /*下一探针*/
}probe_t;
/*trampline描述符*/
typedef struct trampline {
char code[MAX_CODE_CACHE]; /*保存函数入口代码*/
struct trampline *next;
}trampline_t;
/*函数描述符*/
typedef struct fuc_info {
int id; /*id*/
char fuc_name[MAX_FUC_NAME_LEN]; /*函数名*/
void *fuc_addr; /*函数地址*/
arg_type_t arg[MAX_ARG]; /*参数类型*/
trampline_t *tp;
probe_t *probe_list;
}fuc_info_t;
初始化
利用Linux LD_PRELOAD的特性,初始化整个库。其实在linux下利用LD_PRELAOD可以直接拦截库函数的执行,这里使用这个简化实现。
printf(“init lib/n”);
//分配trampaline,将其权限设为可执行等
tp_table = mmap(NULL, MAX_TABLE_SIZE * sizeof(trampline_t), PROT_EXEC | PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
if (tp_table == -1) {
printf(“lib_init fail to map/n”);
exit(1);
}
//可以提供一个接口,让用户来指导探针和原函数信息
//这里简化这一步
probe = &probe_table[0];
probe->probe_fuc = (void *)hello;
strcpy(probe->name,”hello”);
probe->next = NULL;
fuc = &info_table[0];
strcpy(fuc->fuc_name,”test”);
fuc->fuc_addr = 0x080483e4;
fuc->probe_list = NULL;
fuc->tp = &tp_table[0];
//插入探针
insert_probe(fuc, probe);
}
插入探针
偏移量计算一般是目标地址 – jmp的下一条指令的地址。在计算跳回员函数偏移量时,多加6个字节。因为6个字节的代码已经被执行了,同时这样做也避免了在探针和原函数间跳来跳去的死循环。这里作为了简单保存的6字节,因为测试的是本文开始的test函数。
info = fuc;
tp = info->tp;
p = PAGE_ALIGN(info->fuc_addr);
//修改代码段权限为可写
if (mprotect(p, PAGESIZE, PROT_EXEC | PROT_READ | PROT_WRITE) == -1)
printf(“error change prot/n”);
//复制入口代码
memcpy(tp->code, info->fuc_addr, 6);
//jmp info->fuc_addr + 6,跳往保存代码的下一条指令
tp->code[6] = 0xE9; //jmp的机器码
//偏移量,(info->fuc_addr + 6) – (&tp->code[6] + 5)(jmp的下一条指令)
*((int *)(tp->code + 7)) = (char *)info->fuc_addr – tp->code – 5;
code = (char*)info->fuc_addr;
//jmp dispatch,跳到我们的分派函数
code[0] = 0xE9;
*((int *)(code + 1)) = (int)dispatch – (int)code – 5;
//重置权限
if (mprotect(p, PAGESIZE, PROT_EXEC | PROT_READ) == -1)
printf(“error rechange prot/n”);
probe->next = info->probe_list;
info->probe_list = probe;
}
删除探针
为了简单,只回复函数入口代码
info = fuc;
tp = info->tp;
p = PAGE_ALIGN(info->fuc_addr);
if (mprotect(p, PAGESIZE, PROT_EXEC | PROT_READ | PROT_WRITE) == -1)
printf(“error change prot/n”);
//恢复函数入口
memcpy(info->fuc_addr, tp->code, 6);
if (mprotect(p, PAGESIZE, PROT_EXEC | PROT_READ) == -1)
printf(“error rechange prot/n”);
}
dispatch函数
用来管理整个跳转和探针函数的执行。原函数有参数的话,需要进一步处理,这里简化了这一步。
tp = info->tp;
probe = info->probe_list;
//处理参数,未实现
//执行探针列表的探针函数
while (probe) {
((enter)probe->probe_fuc)();
probe = probe->next;
}
//执行原函数
((enter)tp->code)();
//作为测试删除函数的插桩信息
remove_probe(info);
}
探针的代码
fini函数:释放相应资源
printf(“fini lib/n”);
retval = -1;
if (tp_table) {
retval = munmap(tp_table, MAX_TABLE_SIZE * sizeof(trampline_t));
if (retval == -1) {
printf(“lib_fini fail to free map/n”);
}
}
}
测试程序:test.c
void test() {
printf(“test /n”);
}
int main()
{
int a,b;
test();
printf(“after remove probe/n”);
test();
return 0;
}
执行结果:
LD_PRELOAD=./libprobe.so ./test。可以看到hello函数在test之前执行了,删除探针后函数恢复正常执行。