a wonderful way to implement HOOK

    技术2022-05-19  25

    一、Hook定义

          改变函数执行流程,将原函数的执行流引导入自定义的代码中,再处理参数或结果后在返回原函数继续执行的处理方式。常见的Hook方式绝大多数是在函数的开头,无论通过改变一些函数指针,如导入表,SSDT,甚至C++类的虚函数表,还是inline hook中在函数开头写入跳转指令,还是采用其他一些奇技淫巧,比如在函数头设断点,在异常处理中改变EIP实现(用硬件执行断点来玩的话,会令hook比较难以检测,但是很可惜 只能玩4个。用内存访问断点来实现的话更淫荡,完全没有数量限制,但是会有强烈的性能劣势) 。这些HOOK的特点是在函数还没有执行任何指令时获取控制权的,比较好处理。也有在函数开头执行过几条无关痛痒的指令后跳转的,这种情况稍微复杂点,要个现场保护。还有在函数执行完返回前hook的,这个显然只能处理结果,而且比较麻烦,因为稍微复杂的函数都不会只一条返回路径。当然,还有一些直接修改指令的patch操作,比如把第一条指令改成 ret; 这里由于没有将流程引入我们的函数进行处理,只能叫patch。

     

    二、一般实现

          Hook的目的一般两种:过滤参数和过滤函数执行结果。而且大多数是需要调用原函数的。修改各种函数表的hook实现着比较简单,只需保存原函数指针,然后调用即可。

          Inline Hook 稍微复杂一点点,因为改了原函数的指令(暂不描述奇技淫巧的HOOK),所以需要跳来跳去的。首先是在函数头的一个跳转,跳到我们的函数中。我们在执行原函数时,必须先执行那条(那几条)被我们写的跳转指令破坏掉的指令,然后再跳转到之后的指令。如果只是这样跳,那很可惜,只能改参数了,还得纠结栈平衡。为了能稍微方便点,我们可以这样认为。将函数执行流程引入一个包装函数中,函数的参数,调用个数完全一致。在包装函数中,我们调用原函数。这样可以之前处理参数,之后处理结果。啊,多么惬意。这样一来,我们就需要实现个原函数,好在我们只破坏了开头,我们帮忙实现一个开头,然后跳转即可。示例代码如下:

    __declspec(naked)int WINAPI DetourMessageBoxA(HWND hWnd,LPCSTR lpText,LPCSTR lpCaption,UINT uType) { __asm { mov edi,edi; push ebp; mov eax,[MessageBoxA]; add eax; jmp eax; } } int WINAPI MyMessageBoxA(HWND hWnd,LPCSTR lpText,LPCSTR lpCaption,UINT uType) { DetourMessageBoxA(hWnd,"Let's Roll!",lpCaption,uType); return 1; }

    __declspec(naked) 是告诉编译器不要动我函数的栈。DetourMessageBoxA的作用就是执行两条MessageBoxA原来的两条指令

    mov edi,edi;   push ebp; (当然,第一条可以不要)。因为这两条指令正好是5字节(jmp 指令也是5字节),然后后面3条指令计算出随后的指令,完成跳转。到包装函数那,就简单多了,各种改。在实现hook时,只需要将MessageBoxA的头5字节改了即可:

    *(BYTE*)(MessageBoxA) = 0xE9;*(DWORD*)((BYTE*)MessageBoxA+1) = (DWORD)MessageBoxA - (DWORD)MyMessageBoxA - 5;

    第一条指令的0xE9是跳转指令的机器码,第二条指令则是计算便宜的。0xE9的跳转是将EIP的值加上后面的偏移得出目的地址的,因为EIP总指向下一条指令,故执行该跳转时,EIP为该跳转指令所在地址加5。 从这里我们也大致感受到了类型强制的烦人地方,上面两句还不如用汇编用着简单那。

    __asm{ mov eax,MessageBoxA; mov byte ptr[eax],0xe9; mov ebx,eax; lea ecx,MyMessageBoxA; sub ebx,ecx; sub ebx,5; mov dword ptr[eax+1],ebx;}

    好吧,一样简单。

    这里看起来比较简化了,但其实,当你hook的函数多到时候,就开始纠结了。每个函数都得定义俩函数。每个Detour还得查原来代码是啥。太不爽了。就算用的HOOK的各种函数表,在用调原函数时,还是得没个函数用个typedef DWORD (__stdcall* FUNC1)(int,int);

    之类的定义,然后调用时也得玩个((FUNC1)0x77777777)(1,2); 写多了也不爽。

    幸好C有个可变参数可以玩,不如弄个这吧:

    typedef int ( *FUNCCALL)(...); #define CALLFUNC(address,_x_) ((FUNCCALL)(address)) _x_ #define AdjStack(ParaNum) __asm sub esp,ParaNum*4

    至少在调用时,不用一直typedef了,比如可以CALLFUNC(0x77777777,(1,2,3));当然,可变参数必须是__cdecl的,API都是 __stdcall的,所以需要修正一下堆栈在上面的调用后,应该加个AdjStack(3)。注意这样玩是要关闭什么基本运行时检查了,缓冲区安全检查了什么的,不然编译器在每个函数结束都要先荡一把。

     

          啊,其实这时候已经比较简化了。后来发现个叫LDE的神奇东西,可以检测指令长度,这时,已经可以不用再为每个函数写一个Detour了。比如我们可以定义一个这样的数据结构:

    #pragma pack(1) typedef struct _FuncTrunk { BYTE instruction[11]; BYTE jmpcode; int Offset; }FUNCTRUNK,*PFUNCTRUNK; #pragma pack()

    BYTE instruction[11]; 用来储存原函数的头几条指令,考虑到最差情况下第一条指令4字节,第二条指令离奇的7字节,所以需11字节。如果一般情况,只需要全部设置成0x90,即nop指令即可BYTE jmpcode;  设置为0xE9int Offset;   计算出便宜填入即可。

    这样一来,我们基本实现了一堆函数表,实现HOOK时,基本只需:

    extern "C" BOOLEAN __stdcall HookFuncWithInlineJmp(PVOID OriFunc,PVOID NewFunc ,DWORD detourNum) { DWORD step; BOOLEAN bRet = FALSE; DWORD dwSize; BYTE bInsBuffer[16]; memset(&g_FuncTrunks[detourNum],0x90,sizeof(FUNCTRUNK)); if(!ReadMem(OriFunc,bInsBuffer,16)) { step=0; goto HFJLEAVE;} dwSize = RoundToInstruction(bInsBuffer,5); if(dwSize == 0 ){ step=0; goto HFJLEAVE;} WriteMem(g_FuncTrunks[detourNum].instruction,bInsBuffer,dwSize); //copy ori instructions to FuncTrunk g_FuncTrunks[detourNum].jmpcode = 0xE9; g_FuncTrunks[detourNum].Offset = (ULONG)OriFunc + dwSize - (ULONG)(&g_FuncTrunks[detourNum]) -16; if(!PatchMemAccessMode(OriFunc,5)) { step=1; goto HFJLEAVE;} if(!ActiveSynchronization()) { step=2; goto HFJLEAVE;} *((BYTE*)OriFunc) = 0xE9; *(ULONG*)((BYTE*)OriFunc + 1) = (ULONG)NewFunc - (ULONG)OriFunc - 5; InactiveSynchronization(); RestoreMemAccessMode(OriFunc,5); bRet = TRUE; HFJLEAVE: switch(step) { case 0: default: ; } return bRet; }

    参数PVOID OriFunc,PVOID NewFunc ,DWORD detourNum中,前两不用说,detourNum为FUNCTRUNK数组下标。

    这样实现后,只需写包装函数即可:

    #define CALLFUNCFROMTRUNKS(num,_x_) CALLFUNC(&g_FuncTrunks[num],_x_) int WINAPI MyMessageBoxA(HWND hWnd,LPCSTR lpText,LPCSTR lpCaption,UINT uType) { int a; CALLFUNCFROMTRUNKS(1,(hWnd,lpText,lpCaption,uType)); AdjStack(4); __asm mov a,eax; return a; }

     

    啊,比较简单了!!

    啊,又3点半多了。昨晚给个小朋友发短信,早上她说恰好3月3号3点33分33秒。一下子7个3,应该是好运气的标志吧,希望考试的成绩能不错吧!!


    最新回复(0)