基于探针的动态插桩

由于基于探针的动态插桩,通常只能在函数边界插入代码,难以对程序的指令流进行很好的分析,所以平时用的比较少。以前使用微软研究院的detour的API觉得它很神奇,最近看了下它的原理还是很简单:基于简单动态重写函数的开始几个字节,然后跳转到特定函数。呵呵,但是要做好还是不容易的。闲来无事写了一个很粗糙的实现。

 

基本原理就是:(1)保存函数的入口的几个字节,并插入一天跳回函数的jmp指令(这一块代码称为trampaline)。这里的前几个字节不是个定数是有原因的,实际上我们只需要前5字节来保存一条JMP指令,但入口的5个字节可能并不是几条完整的指令,因此若只保存5个字节就会截断指令。如下面的代码所示,test函数入口的第5个字节包含于sub,保存前6个字节就可以避免截断sub指令。

080483e4 : 80483e4: 55 push %ebp 80483e5: 89 e5 mov %esp,%ebp 80483e7: 83 ec 18 sub $0x18,%esp 80483ea: c7 04 24 e0 84 04 08 movl $0x80484e0,(%esp)

(2)修改函数入口的5个字节为jmp xxx指令,其中的xxx就是探针函数到当前函数的偏移量。跳往探针函数并执行它。

(3)执行trampaline代码,执行原函数。

(3)恢复原函数的入口。

 

基本数据结构:

/*参数类型,包含的字节数,例如INT8表示数据大小为一个字节*/ typedef enum arg_type { NULL_TYPE, INT8, INT16, INT32, INT64 } arg_type_t; /*探针函数描述符*/ 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可以直接拦截库函数的执行,这里使用这个简化实现。

void __attribute__((constructor)) lib_init() { probe_t *probe; fuc_info_t *fuc; 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函数。

 

 static void insert_probe(fuc_info_t *fuc, probe_t *probe) { void *p; char *code; trampline_t *tp; fuc_info_t *info; 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; }

 

删除探针

为了简单,只回复函数入口代码

static void remove_probe(fuc_info_t *fuc) { trampline_t *tp; fuc_info_t *info; void *p; 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函数

用来管理整个跳转和探针函数的执行。原函数有参数的话,需要进一步处理,这里简化了这一步。

static void dispatch() { probe_t *probe; trampline_t *tp; //根据地址进行查找,这里简化了这一步 fuc_info_t *info = &info_table[0]; tp = info->tp; probe = info->probe_list; //处理参数,未实现 //执行探针列表的探针函数 while (probe) { ((enter)probe->probe_fuc)(); probe = probe->next; } //执行原函数 ((enter)tp->code)(); //作为测试删除函数的插桩信息 remove_probe(info); }

 

探针的代码

static void hello() { printf("Hello world, I am a probe fuc!/n"); }  

 

fini函数:释放相应资源

void __attribute__ ((destructor)) lib_fini() { int retval; 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

#include 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之前执行了,删除探针后函数恢复正常执行。

init lib Hello world, I am a probe fuc! test after remove probe test fini lib

你可能感兴趣的:(虚拟化/编译)