浅谈Windows下的inline hook

inline hook在我个人看来是Windows API编程里面一个比较重要的东西,尽管按照某些方面来说这个是初级的,不过多多少少还是要点内容才能学上手的。所以这篇文章的目的也只是简单的谈一谈其实现原理等东西。这里所说到的原本也是准备在CFC上学期的分享会上说的,然而由于一些奇奇怪怪的原因我那天并没有说,所以在这里补上,等下一次有机会再说了。至于其他的嘛,我只能按照我学的深度来说了。:)

所谓的inline hook是Windows系统中“挂钩”方式的一种,大家知道Windows系统是消息驱动的工作模式,在代码段的实现部分则是一个个函数之前的调用关系。inline hook就是对函数的一种挂钩方式,通过这种挂钩,使得在调用被挂钩函数之前执行自己所期望的操作,比如跳转到自己的函数,继续执行原函数功能,或者返回特定内容,等等。通俗的来说,比如A和B两个函数存在调用关系(A调用B),而inline hook就是在两个“恩爱的”函数A和B的调用关系之间加一个“小三”C函数,这个“小三”可以在A调用B的时候随意的执行自己想要的事情,比如不执行B,或者伪装成B返回假的结果,等等,而B函数本身对此却无可奈何。

所以在此之前呢我先从C语言的一小段代码讲起。在大家写C语言程序的时候一般会声明函数,或者调用。例如:

#include "stdio.h"

int add(int a,int b){
    return a+b;
}

int main(int argc,char* argv[]){
    int x = add(1 , 2);
    printf("result of 1+2 is %d\n" , x);
    return 0;
}

如上述内容所示,这段C语言代码包含了一个main函数和一个add函数。main函数中调用了自定义的函数add和库函数printf。你通过调用add函数完成1和2的加法,然后调用printf打印这段输出。

上面这段代码在正常执行的情况下,应该的输出是

result of 1+2 is 3

然而某一天,写这段代码的程序猿突然脑子抽了一下,他想让add返回错误的值,但又不想修改add函数本身,于是他构造了一个fake_add函数,想让他作为那个破坏main和add之间的小三函数。

int fake_add(int a,int b){
    return 1+a+b;
}

但是,函数写好了,程序猿又该怎么实现这个目的呢?

然后这里我们来跳一下,看下代码在内存里面是个什么样子。

程序员们应该知道的是C语言这种编译性语言是直接生成成二进制指令的。实际上,上面这段代码在内存里面给系统所看到的就是一大段二进制内容。如果你知道一点点汇编的话,你可能会知道实际上在内存里面的代码在“反编译”之后都是类似于下面这个样子的。你可能通过网上的文档,或者其他的途径,知道在Windows的二进制中,0x8B表示汇编的MOV指令,0xe9表示汇编的jmp指令,等等。

QQ截图20160215015523

一堆汇编指令真要看起来的话会比较头疼。那么我说这个的目的是想说明,其实你写的代码、函数,到最后都会是这种二进制的形式。某一个字节代表什么指令,后面跟的代表什么含义。就是这个样子。包括函数本身的调用也只是一条指令(汇编的call指令,二进制表示为0xE8)。在计算机组成原理课上说过,指令是一条一条按照顺序执行的,也就是说上面这些汇编指令都是按照顺序一条一条执行的,当我执行到call的时候,表示我要调用某个函数,执行到jmp的时候表示跳转到哪一条指令。

那么回到之前的问题,既然指令按照顺序执行,那么我人为的改变他的执行过程,修改内存里面指令的内容,使得他在调用目标函数之前先调用我自己的函数,这样不就可以达到挂钩的目的了吗?

这也就是inline hook的由来。因为这个钩子是通过修改函数执行指令来达到挂钩的。

那么,程序猿又应该如何去修改这段指令,使得add函数调用之前先调用fake函数呢?这里要提到两个函数。VirtualProtect和WriteProcessMemory.

这里我要补充一点点关于PE(可执行程序)本身的事情。在Windows将程序载入内存的时候,会在内存中分配多个“页”(对于不理解PE内容的,将其认为是一块内存区域就可以了),将程序的内容载入进去,并且按照数据类型分配“页”的属性,比如,数据段不能执行,所以这段内存属性是“可读可写”,代码段应该只能被执行,因此这段属性为“可执行,只读”。

了解了上面这点东西以后也就知道,如果我们要修改二进制代码内容,需要有权限修改这段区域的内容才可以,因此调用VirtualProtect修改add函数附近的内存属性,使得其可以被修改。

DWORD dwOldProtect , dwTemp;

//VirtualProtect调用约束:
//    基地址:内存片段中的起始位置
//    长度  :要修改内存中多少个字节的属性
//    保护  :将从基地址开始的这些字节修改为什么样的内存保护的属性。
//    旧保护:此参数用于保存该内存片段原始的内存保护属性
if(VirtualProtect(add , 5 , 64 , &dwOldProtect)){
    //do hook here
}
//恢复内存段为可执行属性
VirtualProtect(add , 5 , dwOldProtect , &dwTemp);

如上述代码片段所示,调用VirtualProtect函数修改add函数附近的5个字节码为可读可写(第三个参数64表示修改该内存段保护为可读可写),并将该段内存原来的保护存储到dwOldProtect中。在完成do hook部分(也就是修改代码执行逻辑)的内容后,将内存保护修改回原来的值。

既然do hook部分是要修改函数的执行逻辑,让执行add函数的时候执行到fake_add函数,我们要想办法执行一条指令,使得执行跳转到fake_add函数去。在汇编语言中使用jmp进行跳转,而jmp指令对应的二进制是0xe9,那么我们构造下面这种代码

jmp <fake_add的函数地址>

并将这段代码写入add函数的开始部分,就可以达到执行add前跳转执行fake_add的目的了。涉及到内存写入,这才有了WriteProcessMemory函数

//获取当前的进程句柄。类似于一种“对象”
HANDLE currentProcessHandle = GetCurrentProcess();
//构造字节码
BYTE shellCode[5] = {0};
shellCode[0] = 0xe9;//jmp指令

//获取HOOK函数和原始函数地址
LPVOID originAdd = add , fakedAdd = fake_add;
//jmp跳转的目的地址计算:HOOK函数地址 - 原始函数地址 - 5
*((DWORD*)(&shellCode[1])) = (DWORD)fake_add - (DWORD)add - 5;

DWORD dwWritten = 0;

//写入该段内存
if(WriteProcessMemory(currentProcessHandle , originAdd , shellCode , 5 , &dwWritten) && dwWritten == 5){
     MessageBoxA(NULL , "Write Hook Success !"  , "Hook add" , 0);
}

//进程句柄使用完毕后务必关闭。
CloseHandle(currentProcessHandle);

在执行过程中,当Hook写入成功时,会弹出一个消息框,提示Write Hook Success.如下图

hook成功

然后我们输出下新的结果呢?

x = add(1, 2);

printf("result of 1+2 is %d\n", x);

QQ截图20160215023616

看到了吗?现在add函数返回的结果成了4,证明fake_add函数挂钩成功。

所以经过上述简单的讲解以后,我们可以知道进行inline hook的基本操作是:

  1. 获取到原函数和挂钩函数的地址
  2. 修改原函数所在内存区段的保护
  3. 计算并写入跳转代码
  4. 修改回内存区段保护
  5. enjoy and check !

对于获取原函数地址,可以通过函数名(例如 LPVOID originFunction = MessageBoxA),或者如果该函数是存在在一个DLL并且已声明公开,则可以通过GetProcAddress函数来获取函数的地址。

比如要获取kernel32.dll里面的WriteProcessMemory函数:

LPVOID lpvWriteProcessMemory = GetProcAddress(GetModuleHandle("kernel32.dll") , "WriteProcessMemory");

其中GetModuleHandle函数是获取已加载到内存中的,DLL的基地址句柄。如果该DLL未被加载到内存中,你可以使用LoadLibrary函数将其加载,并且函数会返回DLL的基地址句柄。

至此,程序内的inline hook简单讲解完毕。

附:文章中的测试源代码(VS2013编译通过并正常运行):

#include "stdio.h"
#include "windows.h"

#include "ras.h"

#pragma warning(disable:4996)

int fake_add(int a, int b){
	return 1 + a + b;
}

int add(int a, int b){
	return a + b;
}

int main(int argc, char* argv[]){
	
	int x = add(1, 2);

	printf("result of 1+2 is %d\n" , x);
	
	printf("Now HOOKING add using fake_add\n");

	DWORD dwOldProtect, dwTempProtect;
	//获取HOOK函数和原始函数地址
	LPVOID originAdd = add, fakeAdd = fake_add;
	printf("\tModifying Memory Protection to Read/Write\n");
	if (VirtualProtect(originAdd, 5, 64, &dwOldProtect)){
		//获取当前的进程句柄。类似于一种“对象”
		HANDLE currentProcessHandle = GetCurrentProcess();
		//构造字节码
		BYTE shellCode[5] = { 0 };
		shellCode[0] = 0xe9;//jmp指令

		//jmp跳转的目的地址计算:HOOK函数地址 - 原始函数地址 - 5
		*((DWORD*)(&shellCode[1])) = (DWORD)fake_add - (DWORD)add - 5;

		DWORD dwWritten = 0;
		printf("\tWriting Shell Code to add funcion\n");
		//写入该段内存
		if (WriteProcessMemory(currentProcessHandle, originAdd, shellCode, 5, &dwWritten) && dwWritten == 5){
			MessageBoxA(NULL, "Write Hook Success !", "Hook add", 0);
		}

		//进程句柄使用完毕后务必关闭。
		CloseHandle(currentProcessHandle);
	}
	printf("\tModifying Memory Protection Back to original\n");
	VirtualProtect(originAdd, 5, dwOldProtect, &dwTempProtect);

	printf("Now Testing our fake functions\n");

	x = add(1, 2);

	printf("result of 1+2 is %d\n", x);
	system("pause");
	return 0;
}

By CrazyChen @ 2016/2/15

One thought on “浅谈Windows下的inline hook

  1. jmp指令占5字节,执行该指令后,IP + 5,然后在加上jmp指令的位移量
    所以 JMP的地址(fakeAdd) – 代码地址(originAdd) – 5(字节) = 机器码跳转地址(E9 位移量)

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注