长亭百川云 - 文章详情

inlinehook心得分享

看雪学苑

58

2024-07-25

inlinehook的核心就是:跳转、跳板。

1、什么是hook?

hook的中文含义是钩子,介绍hook含义之前,先放一个“现实世界里的hook”:

2021年9月,曾报道“水门事件”的华盛顿邮报记者鲍勃·伍德沃德,披露出了一件大事,直接引发了世界震动。  
  
马克·米利,美国的4星上将,美参联主席,目前美国军方的4号人物,绝对的美国高层。  
  
在2021年1月8日,美国国会山骚乱事件的两天后,米利再次给中国打了个电话:  
  
“美国的情况“很稳定”,一切都很好,国会山骚乱仅仅只是“偶然事件””。  
  
“你知道的,民主有时候就是有点乱糟糟的”。  
  
同一天,米利在五角大楼里秘密召开了一次会议,告知其他高级将领:  
  
“自今天之后,军队执行任何重大行动之前必须首先与他磋商。”  
  
“不管谁给你们下达命令,你们都要按照“程序”来执行,而我就是这个“程序”!”

如果把作战行动当作一个函数,下达作战指令当作一次函数调用,川建国同志当作函数调用者,那么米利的行为就相当于hook了作战函数。这样,米利会监控所有作战行动,并且可以中止作战行动。

通过上面的例子,大家应该对hook有了一个初步的认识了,hook后调用原函数时,直接跳到指定的函数。

2、hook技术有什么用?

hook技术的应用很广泛,例如笔记本上的杀毒软件、网吧系统里的监控软件、pc游戏的反外挂防护系统、屏幕录制软件fraps都会使用hook来实现一些高级功能。

安全软件:360等安全软件会hook一些入口函数(例如驱动加载)获得系统最高权限。

监控软件:你在网吧上网时,监控软件hook了connect、send、recv等函数,这样访问的所有网站都会被记录下来(不要做坏事)。

游戏攻防:例如fps游戏外挂会hook d3d的DrawIndexedPrimitive实现透视功能。

#3、什么是inlinehook?

hook的实现有很多种,inlinehook是hook的一种方式。通过修改原函数开头的汇编指令,直接跳转到指定函数。

开源的inlinehook库有很多,例如subhook和微软的Detours,本文会带大家从0开始,一步一步实现一个基础的inlinehook库。

#4、inlinehook库代码结构

代码已经上传gitee,不用github的主要原因是国内gitee网速好一些。文章先介绍一些hook库代码结构,再介绍典型的使用场景。

4.1、hook库代码

xx_mem.hpp:修改代码段属性,改为可读写、可执行。

xx_asm.hpp:工具函数,向目标地址写入汇编指令,例如jmp,ret,push等。

xx_inline_hook.hpp:封装hook的c接口。包括hook、制作跳板、偏移重定位等操作。

4.2、经典场景

test_hook_jmp32:inlinehook入门示例,基础0xe9的jmp跳转。

test_hook_jmp64:64bit操作系统跳转函数,hook系统函数必备。

test_trampoline:跳板函数,hook以后如何再调用原函数?

test_trampoline_relocation:跳板函数中包含相对偏移,如何重定位?

5、inlinehook代码实现

咱们通过这几个经典场景,来说一说。

场景一:初级、经典inlinehook

inlinehook本质是一种汇编代码修改技术,修改原函数,开头第一条汇编指令改为jmp跳转到我们的函数。先看一下demo:

//test\_hook\_jmp32  
///////////////////////////////////////////////////////////  
void hello\_world()  
{  
  printf("\[call %s\]\\n",\_\_FUNCTION\_\_);  
}  
void my\_hello\_world()  
{  
    printf("\[call %s\]\\n", \_\_FUNCTION\_\_);  
}  
  
void test\_hook\_jmp32()  
{  
    // 修改被hook函数内存属性为可写  
  xx\_mem\_unprotect(hello\_world, 4096);  
    // 在函数开头插入jmp语句,跳转到my\_hello\_world  
  xx\_setjmp32(&hello\_world, &my\_hello\_world);  
    // 测试一下  
  hello\_world();  
}  
int main()  
{  
    printf("\\n\\n======test\_hook\_jmp32=================\\n");  
    test\_hook\_jmp32();  
 }

执行结果屏幕输出

\======test\_hook\_jmp32=================  
\[call my\_hello\_world\]  
可见,调用hello\_world时,实际执行的是my\_hello\_world。

下面说说实现,主要有两步:修改代码段内存属性;插入jmp汇编指令。

修改代码段内存属性

// 修改被hook函数内存属性为可写
xx_mem_unprotect(hello_world, 4096);
一段内存有是否可读、是否可写、是否可执行等属性。代码段默认是不可写的(不可修改),所以需要先设置为可写才能修改。xx_mem_unprotect的windows实现,调用VirtualProtect系统api来实现(linux下的api是mmap)。

static bool xx_mem_unprotect(void* address, size_t size) {
DWORD old_flags;
BOOL result = VirtualProtect(address,
size,
PAGE_EXECUTE_READWRITE,
&old_flags);
return result == TRUE;
}

插入jmp汇编指令

// 在函数开头插入jmp语句,跳转到my_hello_world
xx_setjmp32(&hello_world, &my_hello_world);

hook以后,hello_world的汇编指令如下:

void hello\_world()  
{  
00007FF6642410B0 E9 8B 00 00 00       jmp         my\_hello\_world (07FF664241140h)    
00007FF6642410B5

jmp指令共占用5个字节,指令opcode 0xe9占用1字节,偏移量占用4字节。偏移量是目标地址相对本地址的偏移。

上面的07FF664241140(下一条指令地址)-00007FF6642410B5(目标地址)=8b,刚好是e9后面的值。

代码实现部分

首先制作一个辅助类,用于向目标地址写入jmp汇编指令。

汇编指令

//JMP rel32  
class jmp\_rel32  
{  
public:  
  struct asm\_cmd {  
    uint8\_t opcode\_;  
    int32\_t rel32\_;  
  };  
  static void write(void\* cmd\_addr, int32\_t rel32) {  
    auto\* cmd = (asm\_cmd\*)cmd\_addr;  
    cmd->opcode\_ = 0xe9;  
    cmd->rel32\_ = rel32;  
  }  
  static uint8\_t size() { return sizeof(asm\_cmd); }  
};

其中write是写入指令,size返回本指令长度,后续会不断扩充汇编指令类,都有这两个接口。

下面再实现xx_setjmp32 就简单多了,计算一下偏移,然后写入指令。

// ret:两个地址的偏移  
static int64\_t xx\_get\_offset(void\* src, void\* dst) {  
  return (char\*)dst - (char\*)src;  
}  
  
// ret:返回从src jmp 到dst的偏移  
static int64\_t xx\_get\_jmp32\_offset(void\* src, void\* dst) {  
  return xx\_get\_offset((char\*)src + jmp\_rel32::size(), dst);  
}  
// 写入汇编,32bit位移跳转,jmp到dst  
// ret 5(修改5 byte)  
static uint32\_t xx\_setjmp32(void\* src, void\* dst) {  
  int32\_t offset = (int32\_t)xx\_get\_jmp32\_offset(src, dst);  
  jmp\_rel32::write(src, offset);  
  return jmp\_rel32::size();  
}

现在,我们的hook库支持功能如下:

◆32位跳转

场景二:64位系统的hook

上一个场景使用的jmp指令跳转,jmp指令有一个限制是,跳转地址与原地址的偏移不能超过int32的范围,在64bit操作系统下可能无法跳过去。我们的hook库需要进化一下,支持在64位地址空间下任意跳转。

偏移足够小时,尽量使用32位jmp跳转。因为32位跳转只修改5字节,而64位大跳需要修改14字节,hook时尽量减少对原函数修改。

先看一下demo,为了展示“大跳”,hook一个系统函数。

//test\_hook\_jmp64  
///////////////////////////////////////////////////////////  
int \_\_cdecl my\_fclose(  
    \_Inout\_ FILE\* \_Stream  
) {  
    printf("\[call %s\]\\n", \_\_FUNCTION\_\_);  
    return 0;  
}  
void test\_hook\_jmp64()  
{  
    // 判断偏移是否满足32位  
    int64\_t offset = xx\_get\_offset(&fclose, &my\_fclose);  
    bool need\_far\_jmp = xx\_int32\_overflow(offset);  
    printf("need\_far\_jmp=%u \\n", need\_far\_jmp);  
      
    // 修改被hook函数内存属性为可写  
    xx\_mem\_unprotect(&fclose, 1024);  
    // 借助ret汇编指令实现64位跳转  
    xx\_setjmp64(&fclose, &my\_fclose);  
    // 测试一下  
    fclose(nullptr);  
}  
int main()  
{  
    printf("\\n\\n======test\_hook\_jmp64=================\\n");  
    test\_hook\_jmp64();  
}

执行结果屏幕输出

\======test\_hook\_jmp64=================  
need\_far\_jmp=1  
\[call my\_fclose\]  
need\_far\_jmp=1代表需要“大跳”。

如何判断是否需要大跳

判断时,计算原函数和跳转函数的偏移,是否在int32的范围即可。

// ret:是否超过int32范围  
static bool xx\_int32\_overflow(int64\_t val) {  
  return val < INT32\_MIN || val > INT32\_MAX;  
}

64位大跳实现

实现时借助ret汇编指令。先说说ret汇编指令,在正常函数调用时,会先将返回地址入栈,等函数执行完后再调用ret指令完成返回地址出栈+跳转。

实现64位跳转时,先把要跳转的地址入栈,然后再调用ret指令实现跳转。

先看一下修改后,原函数的汇编代码:

00007FF97A2596A0 68 D0 10 A0 BE       push        0FFFFFFFFBEA010D0h    
00007FF97A2596A5 C7 44 24 04 F7 7F 00 00 mov         dword ptr \[rsp+4\],7FF7h    
00007FF97A2596AD C3                   ret  
push+mov指令负责把地址压栈,ret指令负责跳转。

代码实现部分

首先也是汇编指令辅助类,这里需要3个push、mov、ret。每个辅助类还是有write和size两个接口。

// PUSH imm32  
class push\_imm32  
{  
public:  
  struct asm\_cmd {  
    uint8\_t  opcode\_;  
    uint32\_t imm32\_;  
  };  
  
  static void write(void\* cmd\_addr, uint32\_t imm32) {  
    auto\* cmd = (asm\_cmd\*)cmd\_addr;  
    cmd->opcode\_ = 0x68;//jmp  
    cmd->imm32\_ = imm32;  
  }  
  
  
  static uint8\_t size() { return sizeof(asm\_cmd); }  
};  
// mov dword ptr\[rsp + offset\],imm32  
class mov\_rsp\_ptr\_imm32  
{  
public:  
  struct asm\_cmd {  
    uint8\_t  opcode\_;  
    uint8\_t  para1\_;  
    uint8\_t  reg\_type\_;  
    int8\_t  offset\_;  
    uint32\_t imm32\_;  
  };  
  static void write(void\* cmd\_addr, int8\_t off, uint32\_t imm32) {  
    auto\* cmd = (asm\_cmd\*)cmd\_addr;  
    cmd->opcode\_ = 0xc7;//mov  
    cmd->para1\_ = 0x44;// to reg ptr  
    cmd->reg\_type\_ = 0x24;//rsp  
    cmd->offset\_ = off;  
    cmd->imm32\_ = imm32;  
  }  
  static uint8\_t size() { return sizeof(asm\_cmd); }  
};  
  
  
// ret  
class ret  
{  
public:  
  struct asm\_cmd {  
    uint8\_t  opcode\_;  
  };  
  
  static void write(void\* cmd\_addr) {  
    auto\* cmd = (asm\_cmd\*)cmd\_addr;  
    cmd->opcode\_ = 0xc3;  
  }  
  
  static uint8\_t size() { return sizeof(asm\_cmd); }  
};

写入这3条汇编,代码如下:

// 写入汇编,64bit位移跳转利用ret来跳转  
// ret 14(修改14 byte)  
static uint32\_t xx\_setjmp64(void\* src, void\* dst) {  
  char\* cmd\_addr = (char\*)src;  
  
  push\_imm32::write(cmd\_addr, (uint32\_t)(uintptr\_t)dst);  
  cmd\_addr += push\_imm32::size();  
  
  mov\_rsp\_ptr\_imm32::write(cmd\_addr, 4, (uint32\_t)(((uintptr\_t)dst) >> 32));  
  cmd\_addr += mov\_rsp\_ptr\_imm32::size();  
  
  ret::write(cmd\_addr);  
  return push\_imm32::size() + mov\_rsp\_ptr\_imm32::size() + ret::size();  
}

现在,我们的hook库支持功能如下:

◆32位跳转

◆64位跳转

为了方便使用,封装了一个xx_setjmp函数,自动判断偏移量,选择合适的跳转方式,代码如下:

// 写入汇编,自动判断偏移选择适合汇编指令  
// ret 修改字节数  
static uint32\_t xx\_setjmp(void\* src, void\* dst) {  
  // 64bit system,check jmp32 ok.  
  int64\_t dis = xx\_get\_jmp32\_offset(src, dst);  
  if (xx\_int32\_overflow(dis))  
    return xx\_setjmp64(src, dst);  
  else  
    return xx\_setjmp32(src, dst);  
}

场景三:使用跳板,跳回原函数

前两个场景都没有调用原函数,如果想调用原函数怎么做呢?例如实现监控程序,跳转函数中记录函数参数,然后再调用原函数。直接调用原函数是不行的,会再次跳转到跳转函数。

我们先看一下demo,模拟监控功能,记录函数调用参数后,借助“跳板”执行原逻辑。

//test\_trampoline  
///////////////////////////////////////////////////////////  
char xx\_trampoline\[1024\];  
int sum(int a,int b)  
{  
    return a + b;  
}  
int mysum(int a, int b)  
{  
    printf("\[call %s\]!a=%d,b=%d\\n", \_\_FUNCTION\_\_,a,b);  
    //typedef int(sum\_func\_t)(int, int);  
    //sum\_func\_t \*f = (sum\_func\_t\*)((char\*)xx\_trampoline);  
    // 执行原逻辑  
    auto ori\_func = xx\_trampoline\_to\_func(&sum, xx\_trampoline);  
    return (\*ori\_func)(a, b);;  
}  
  
void test\_trampoline()   
{  
    // 制作跳板  
    xx\_mem\_unprotect(xx\_trampoline,1024);  
    xx\_make\_trampoline(&sum, xx\_trampoline, 4+4);  
    // hook  
    xx\_mem\_unprotect(&sum, 1024);  
    xx\_setjmp(&sum, &mysum);  
    // test  
    int a = sum(1, 2);  
    printf("a=%u\\n",a);  
}  
int main()  
{  
    printf("\\n\\n======test\_trampoline=================\\n");  
    test\_trampoline();  
}

执行结果屏幕输出。

\======test\_trampoline=================  
\[call mysum\]!a=1,b=2  
a=3

如何制作跳板?例如hook时破坏了原函数的前3条汇编指令,那么hook前需要申请一块内存把前3条指令复制过去,然后再跳转到原函数第四条指令,这块内存就是所谓的跳板。

跳板需要复制多少代码

复制汇编代码时,是以汇编指令为单位来复制的,不能修改了5字节,就复制5字节,那样有可能复制到半条指令。究竟复制多少字节呢?好多hook库都内嵌了一个反汇编引擎,自动判断需要拷贝条数,本文为了让大家更深入理解,决定人工计算,通过参数指定拷贝字节数。

首先先判断原函数需要修改多少字节(使用32位跳转还是64位跳转),xx_setjmp 的返回值是修改字节数,就是为了这里使用。我们的demo是修改5字节。

// 写入汇编,自动判断偏移选择适合汇编指令  
// ret 修改字节数  
static uint32\_t xx\_setjmp(void\* src, void\* dst)  
  
  
然后观察原函数汇编代码  
  
int sum(int a,int b)  
{  
00007FF728C411F0 89 54 24 10          mov         dword ptr \[b\],edx    
00007FF728C411F4 89 4C 24 08          mov         dword ptr \[a\],ecx    
00007FF728C411F8 8B 44 24 10          mov         eax,dword ptr \[b\]    
00007FF728C411FC 8B 4C 24 08          mov         ecx,dword ptr \[a\]

最少修改5字节 ,完整指令,所以需要复制2条指令,4+4=8字节。

复制代码+跳回去

制作跳板比较简单,复制代码后面跟着跳转语句跳回原函数。

// 制作跳板,hook以后再跳回去  
// ret:跳板长度  
static uint32\_t xx\_make\_trampoline(void\* src, void\* trampoline, uint32\_t copy\_len) {  
  memcpy(trampoline, src, copy\_len);  
  return copy\_len + xx\_setjmp((char\*)trampoline + copy\_len,(char\*)src + copy\_len);  
}

调用前,别忘了给跳板内存增加可执行属性!

// 制作跳板  
    xx\_mem\_unprotect(xx\_trampoline,1024);  
    xx\_make\_trampoline(&sum, xx\_trampoline, 4+4);

现在,我们的hook库支持功能如下:

◆32位跳转

◆64位跳转

◆跳板功能

场景四:跳板偏移修复

上个场景中,跳板直接复制原函数代码就行了,不过有时复制来的代码确是错的,例如下面这个demo,我们看一下。

bool g\_ready = true;  
char xx\_trampoline2\[1024\];  
  
  
void flag\_print() {  
    if (g\_ready) {  
        printf("\[call %s\]!ready\\n",\_\_FUNCTION\_\_);  
    }  
    else {  
        printf("\[call %s\]!not ready\\n", \_\_FUNCTION\_\_);  
    }  
}  
  
  
void my\_flag\_print() {  
    printf("\[call %s\]!\\n", \_\_FUNCTION\_\_);  
    auto ori = xx\_trampoline\_to\_func(&flag\_print, xx\_trampoline2);  
    (\*ori)();  
}  
void test\_trampoline\_relocation() {  
    // 制作跳板  
    xx\_mem\_unprotect(xx\_trampoline2, 1024);  
    xx\_make\_trampoline(&flag\_print, xx\_trampoline2, 4 + 7);  
    // 跳板偏移重定位  
    xx\_reloc\_offset(&flag\_print, xx\_trampoline2, 4 + 3 );  
  
    xx\_setjmp(&flag\_print, &my\_flag\_print);  
    flag\_print();  
}

什么时候跳板需要重定位?

我们在第25行执行了偏移重定位,我们看一下重定位之前的原函数和跳板函数的汇编指令。

原函数

void flag\_print() {  
00007FF635091070 48 83 EC 28          sub         rsp,28h    
00007FF635091074 0F B6 05 85 3F 00 00 movzx       eax,byte ptr \[g\_ready (07FF635095000h)\]

跳板

00007FF635095450 48 83 EC 28          sub         rsp,28h    
00007FF635095454 0F B6 05 85 3F 00 00 movzx       eax,byte ptr \[7FF6350993E0h\]    
00007FF63509545B E9 1B BC FF FF       jmp         flag\_print+0Bh (07FF63509107Bh)

大家发现没有,第二条指令,字节是一样的,但是指令不同!

原函数

00007FF635091074 0F B6 05 85 3F 00 00 movzx       eax,byte ptr \[g\_ready (07FF635095000h)\]    
跳板  
00007FF635095454 0F B6 05 85 3F 00 00 movzx       eax,byte ptr \[7FF6350993E0h\]

因为这条mov汇编指令使用的是相对偏移,指令所在位置不同,绝对地址就不同,大家可以回想一下前面的jmp指令。这种参数是偏移量的指令,就需要重定位。

以后我们会内嵌一个反汇编引擎,查表可以判断是否需要重定位,不用肉眼看了。

重定位概念很有用,以后手动加载dll或内存完整性校验时也会提到。

重定位过程

首先通过原函数计算绝对地址,然后根据跳板地址计算偏移。

// 偏移重定位,从src+val\_offset获取绝对addr,写入trampoline+val\_offset  
// val\_offset:偏移的位移  
static bool xx\_reloc\_offset(void\* src, void\* trampoline, uint32\_t val\_offset)  
{  
  // 获取绝对地址  
  int32\_t\* src\_offset = (int32\_t\*)((char\*)src + val\_offset);  
  char\* src\_next\_cmd = (char\*)src + val\_offset + sizeof(int32\_t);  
  char\* real\_addr = src\_next\_cmd + \*src\_offset;  
  
  
  // 判断trampoline相对于绝对地址的偏移是否超过int32  
  char\* tra\_next\_cmd = (char\*)trampoline + val\_offset + sizeof(int32\_t);  
  int64\_t tra\_offset\_val = xx\_get\_offset(tra\_next\_cmd,real\_addr);  
  if (xx\_int32\_overflow(tra\_offset\_val)) {  
    return false;  
  }  
  
  
  // trampoline重定位  
  int32\_t\* tra\_offset = (int32\_t\*)((char\*)trampoline + val\_offset);  
  \*tra\_offset = (int32\_t)tra\_offset\_val;  
  return true;  
}

至此,我们的hook库功能基本齐全,可以用了。支持功能如下:

◆32位跳转

◆64位跳转

◆跳板功能

◆跳板偏移重定位

接下来会介绍远程线程注入、然后配合inlinehook去观察其它软件如何工作。

看雪ID:assqqq

https://bbs.kanxue.com/user-home-405858.htm

*本文为看雪论坛优秀文章,由 assqqq 原创,转载请注明来自看雪社区



# 往期推荐

1、Alt-Tab Terminator注册算法逆向

2、恶意木马历险记

3、VMP源码分析:反调试与绕过方法

4、Chrome V8 issue 1486342浅析

5、Cython逆向-语言特性分析

球分享

球点赞

球在看

点击阅读原文查看更多

相关推荐
关注或联系我们
添加百川云公众号,移动管理云安全产品
咨询热线:
4000-327-707
百川公众号
百川公众号
百川云客服
百川云客服

Copyright ©2024 北京长亭科技有限公司
icon
京ICP备 2024055124号-2