由于基于探针的动态插桩,通常只能在函数边界插入代码,难以对程序的指令流进行很好的分析,所以平时用的比较少。以前使用微软研究院的detour的API觉得它很神奇,最近看了下它的原理还是很简单:基于简单动态重写函数的开始几个字节,然后跳转到特定函数。呵呵,但是要做好还是不容易的。闲来无事写了一个很粗糙的实现。
基本原理就是:(1)保存函数的入口的几个字节,并插入一天跳回函数的jmp指令(这一块代码称为trampaline)。这里的前几个字节不是个定数是有原因的,实际上我们只需要前5字节来保存一条JMP指令,但入口的5个字节可能并不是几条完整的指令,因此若只保存5个字节就会截断指令。如下面的代码所示,test函数入口的第5个字节包含于sub,保存前6个字节就可以避免截断sub指令。
- 080483e4 <test>:
- 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 <stdio.h>
- 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