前言
很多情况下的效率优化,基本上都是时间换空间,空间换时间。本方案就是通过多花一点点内存空间,在传统的inline-hook的基础上实现通用的,高效的hook。其核心原理就是可执行模块的重载。
传统的inline-hook
传统的内联hook是先备份目标函数的前n个字节,通过修改目标函数的前n个字节,替换为几条指令,指令中实现跳转到代理的函数。调用原函数则是有两种方式,一是调用前恢复被修改的字节,返回后再次进行hook。二是将原本的字节备份到一段可执行的内存,内存末尾跳转到原函数被修改的代码的下一条正常的指令,直接调用此处内存的代码。方式一的缺点是效率低,而且在并发上要做特别处理。方式二的缺点是,当前n个字节包含有局部跳转指令时,若不对偏移量进行修正,则会导致执行错误。对偏移量的修正比较麻烦且困难,对不同的处理器架构需要单独做处理。
新的hook方案
新的hook方案有以下优点:高效,调用原函数时,不需要做额外的处理;简单通用,不需要修正指令,不需要单独对处理器架构做处理。而缺点就是多占用一些内存。多占用的内存大小取决于被hook的模块的大小,若进行一些优化处理,这个内存占用大小仍有减少空间。新hook方案对环境要求如下:支持mmap,mprotect、支持共享内存。对共享内存的要求,不是绝对必要,只要能实现同一段内存映射到不同的内存地址即可。
实现细节
可执行文件通常是map到内存中的。可执行文件一般有若干段节,比如text代码段,data数据段。其中代码段是可读可执行的,数据段是可读可写的。text段中的代码对data段中的数据的寻址是相对寻址。本方案进行hook前,会克隆整个可执行文件的内存镜像到另一块内存。新代码段是普通的可写可执行的代码段,新数据段则有些特别。新数据段是旧数据段的remap。其关系是,新旧数据段虽然处于不同的内存地址,但是通过某些手段使其指向同一段物理内存。直观地说就是,对新旧数据段的读写,实际上呈现的是同样的内容。目的是为了同步全局变量。
hook的时候,只要对旧代码插入内联hook需要的代码即可。当需要调用原函数的时候,只要调用新代码段中的原函数即可。在这个过程中,基本上没有执行速度上的损耗。
代码
本例子通过使用共享内存的方式实现将一段内存映射到两个不同的地址
#define SET_PROXY(c,f) *((uint64_t*)(((uint8_t*)(c))+2)) = (uint64_t)f
void *ih_hook(void *oldFunc, void *newFunc, void **save) {
unsigned char hook_stub_code_rax[] = {
0x48, 0xB8, 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11, //movabsq 0x1122334455667788, %rax 10
0xff, 0xe0, //jmp *%rax 2
0x90,
};
unsigned char hook_stub_code_r11[] = {
0x49, 0xBB, 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11, //movabsq 0x1122334455667788, %r11 10
0x41, 0xff, 0xe3, //jmp *%11 3
};
void *image_ptr = NULL;
size_t image_size = 0;
void *data_ptr = NULL;
size_t data_size = 0;
// 获取可执行文件的信息
if (get_addr_info(oldFunc, &image_ptr, &image_size, &data_ptr, &data_size) != 0){
return NULL;
}
// 创建共享内存对象
const char *shm_file = "/tmp/hook.shm";
int fd = shm_open(shm_file, O_RDWR | O_CREAT, S_IRUSR | S_IWUSR);
if (fd < 0){
printf("Error: shm_open\n");
return NULL;
}
ftruncate(fd, image_size);
// 创建新镜像内存
void *new_image_ptr = mmap(NULL, image_size, PROT_READ | PROT_WRITE, MAP_ANON | MAP_PRIVATE, -1, 0);
if (new_image_ptr == MAP_FAILED){
shm_unlink(shm_file);
printf("Error: mmap for new_image_ptr: %s\n", strerror(errno));
return NULL;
}
// 拷贝数据到新内存
memcpy(new_image_ptr, image_ptr, image_size);
if (mprotect(new_image_ptr, image_size, PROT_EXEC|PROT_READ) != 0){
munmap(new_image_ptr, image_size);
printf("Error: mprotect: %s\n", strerror(errno));
return NULL;
}
// 将旧数据段覆盖为共享内存
void *shm1 = mmap(data_ptr, data_size, PROT_READ | PROT_WRITE, MAP_SHARED|MAP_FILE|MAP_FIXED, fd, 0);
my_memcpy(shm1, (uint8_t*)new_image_ptr + (data_ptr - image_ptr), data_size);
// 将新数据段覆盖为共享内存
void *shm2 = mmap((uint8_t*)new_image_ptr + (data_ptr - image_ptr), data_size, PROT_READ | PROT_WRITE, MAP_SHARED|MAP_FILE|MAP_FIXED, fd, 0);
my_memcpy(shm2, data_ptr, data_size);
// 做内联hook
SET_PROXY(hook_stub_code_r11, newFunc);
write_memory(oldFunc, hook_stub_code_r11, sizeof(hook_stub_code_r11));
// 计算原函数地址,原函数位于新代码段
*save = (uint8_t*)new_image_ptr + (oldFunc - image_ptr);;
//
// munmap(new_image_ptr, image_size);
// // munmap(shm1, data_size);
// munmap(shm2, data_size);
//
// shm_unlink(shm_file);
return NULL;
}
int (*org_puts)(const char *s);
int proxy_puts(const char *s) {
printf("Hook successful\n");
org_puts(s);
return 0;
}
int main(int argc, const char * argv[]) {
ih_hook(puts, proxy_puts, (void**)&org_puts);
puts("Normal");
printf("Hello, World!\n");
return 0;
}
结果:
Hook successful
Normal
Hello, World!
完整源码见附件,目前只做了osx上的版本,linux和win的日后再研究。
[课程]FART 脱壳王!加量不加价!FART作者讲授!