安全矩阵

 找回密码
 立即注册
搜索
查看: 2531|回复: 0

浅谈hook攻防

[复制链接]

65

主题

65

帖子

241

积分

中级会员

Rank: 3Rank: 3

积分
241
发表于 2022-5-4 21:49:17 | 显示全部楼层 |阅读模式
本帖最后由 PEnticE 于 2022-5-4 21:52 编辑

浅谈hook攻防 (qq.com)

首发于跳跳糖社区:https://tttang.com/archive/1558/
前言
攻与防都是相对的,只有掌握细节才能更好的对抗。
基础知识
对于Windows系统,它是建立在事件驱动机制上的,说白了就是整个系统都是通过消息传递实现的。hook(钩子)是一种特殊的消息处理机制,它可以监视系统或者进程中的各种事件消息,截获发往目标窗口的消息并进行处理。所以说,我们可以在系统中自定义钩子,用来监视系统中特定事件的发生,完成特定功能,如屏幕取词,监视日志,截获键盘、鼠标输入等等。
钩子的种类很多,每种钩子可以截获相应的消息,如键盘钩子可以截获键盘消息,外壳钩子可以截取、启动和关闭应用程序的消息等。钩子可以分为线程钩子和系统钩子,线程钩子可以监视指定线程的事件消息,系统钩子监视系统中的所有线程的事件消息。因为系统钩子会影响系统中所有的应用程序,所以钩子函数必须放在独立的动态链接库(DLL) 中。
所以说,hook(钩子)就是一个Windows消息的拦截机制,可以拦截单个进程的消息(线程钩子),也可以拦截所有进程的消息(系统钩子),也可以对拦截的消息进行自定义的处理。Windows消息带了一些程序有用的信息,比如Mouse类信息,就带有鼠标所在窗体句柄、鼠标位置等信息,拦截了这些消息,就可以做出例如金山词霸一类的屏幕取词功能。
hook原理
在正确使用钩子函数前,我们先讲解钩子函数的工作原理。当创建一个钩子时,WINDOWS会先在内存中创建一个数据结构,该数据结构包含了钩子的相关信息,然后把该结构体加到已经存在的钩子链表中去。新的钩子将加到老的前面。当一个事件发生时,如果您安装的是一个线程钩子,您进程中的钩子函数将被调用。如果是一个系统钩子,系统就必须把钩子函数插入到其它进程的地址空间,要做到这一点要求钩子函数必须在一个动态链接库中,所以如果您想要使用系统钩子,就必须把该钩子函数放到动态链接库中去。
当然有两个例外:工作日志钩子和工作日志回放钩子。这两个钩子的钩子函数必须在安装钩子的线程中。原因是:这两个钩子是用来监控比较底层的硬件事件的,既然是记录和回放,所有的事件就当然都是有先后次序的。所以如果把回调函数放在DLL中,输入的事件被放在几个线程中记录,所以我们无法保证得到正确的次序。故解决的办法是:把钩子函数放到单个的线程中,譬如安装钩子的线程。
几点需要说明的地方:
(1) 如果对于同一事件(如鼠标消息)既安装了线程钩子又安装了系统钩子,那么系统会自动先调用线程钩子,然后调用系统钩子。  (2) 对同一事件消息可安装多个钩子处理过程,这些钩子处理过程形成了钩子链。当前钩子处理结束后应把钩子信息传递给下一个钩子函数。而且最近安装的钩子放在链的开始,而最早安装的钩子放在最后,也就是后加入的先获得控制权。  (3) 钩子特别是系统钩子会消耗消息处理时间,降低系统性能。只有在必要的时候才安装钩子,在使用完毕后要及时卸载。
hook分类hook从总体来说可以分成两类:

  •         修改函数代码:Inline hook
  •         修改函数地址:IAT hook、SSDT hook、IRP hook、IDT hook等
局限性如果修改函数地址,其实是很容易被检测到的,例如IAT hook可以通过自己的代码去找到在真正内存中的IAT表,再去对比函数的真正地址,SSDT hook可以通过内核重载比对函数地址来检测到,另外一个局限性就是只能hook对应表里面的函数,IAT表只能hook IAT表导出的函数
所以在实际的应用中一般Inline hook会使用得比较多,虽然相比于修改函数地址更不易检测到,但是还是有技术手段能够进行修改函数代码的检测,本文就基于Inline hook来从防守方制定hook检测的策略和攻击方如何绕过hook检测两方面来浅谈hook技术的攻防
Inline hookAPI函数都保存在操作系统提供的DLL文件中,当在程序中使用某个API函数时,在运行程序后,程序会隐式地将API所在的DLL加载入进程中。这样,程序就会像调用自己的函数一样调用API。
在进程中当EXE模块调用CreateFile()函数的时候,会去调用kernel32.dll模块中的CreateFile()函数,因为真正的CreateFile()函数的实现在kernel32.dll模块中。
CreateFile()是API函数,API函数也是由人编写的代码再编译而成的,也有其对应的二进制代码。既然是代码,那么就可以被修改。通过一种“野蛮”的方法来直接修改API函数在内存中的映像,从而对API函数进行HOOK。使用的方法是,直接使用汇编指令的jmp指令将其代码执行流程改变,进而执行我们的代码,这样就使原来的函数的流程改变了。执行完我们的流程以后,可以选择性地执行原来的函数,也可以不继续执行原来的函数。
假设要对某进程的kernel32.dll的CreateFile()函数进行HOOK,首先需要在指定进程中的内存中找到CreateFile()函数的地址,然后修改CreateFile()函数的首地址的代码为jmp MyProc的指令。这样,当指定的进程调用CreateFile()函数时,就会首先跳转到我们的函数当中去执行流程,这样就完成了我们的HOOK了。
hook攻防这里我选择使用MessageBoxA函数来进行hook的检测,因为MessageBoxA在hook之后能够比较清晰的看到结果
这里我首先使用win32资源文件来创建一个图形窗口,功能是点击开始就会弹窗(不要问我为啥不用MFC写窗口,问就是不会 /狗头),代码如下
  1. // MessageBoxA.cpp : Defines the entry point for the application.
  2. //

  3. #include "stdafx.h"
  4. #include <windows.h>
  5. #include "resource.h"

  6. BOOL CALLBACK DialogProc(         
  7.        HWND hwndDlg,  // handle to dialog box   
  8.        UINT uMsg,     // message   
  9.        WPARAM wParam, // first message parameter   
  10.        LPARAM lParam  // second message parameter   
  11.        )   
  12. {         
  13.          
  14. switch(uMsg)        
  15. {        
  16.   case  WM_INITDIALOG :                       
  17.   
  18.    return TRUE ;      
  19.          
  20.   case  WM_COMMAND :        
  21.             
  22.    switch (LOWORD (wParam))      
  23.    {      
  24.    case   IDC_BUTTON_BEGIN :      
  25.          
  26.     MessageBox(NULL,TEXT("使用hook来更改此界面"),TEXT("文本框"),MB_OK);      
  27.          
  28.     return TRUE;      
  29.                
  30.    }
  31.    
  32.    break ;

  33.   case WM_CLOSE:
  34.    {
  35.     EndDialog(hwndDlg,0);
  36.     break;
  37.    }

  38.     }         
  39.          
  40. return FALSE ;        
  41. }

  42. int APIENTRY WinMain(HINSTANCE hInstance,
  43.                      HINSTANCE hPrevInstance,
  44.                      LPSTR     lpCmdLine,
  45.                      int       nCmdShow)
  46. {
  47.   // TODO: Place code here.

  48. DialogBox(hInstance, MAKEINTRESOURCE (IDD_DIALOG_MAIN), NULL, DialogProc);

  49. return 0;
  50. }
复制代码


这里生成的是一个没有任何检测代码的MessageBox测试程序


点击开始就会弹出文本框,我们hook要达到的目的就是修改文本框里面显示的文字

第一层这里就不细说Inline hook的细节了,跟到关键代码
定义我们要修改文本框内的值存放到szNewText里面


看一下MessageBox的函数结构
  1. int MessageBox(
  2.   [in, optional] HWND    hWnd,
  3.   [in, optional] LPCTSTR lpText,
  4.   [in, optional] LPCTSTR lpCaption,
  5.   [in]           UINT    uType
  6. );
复制代码


这里我们直接选择在函数的起始位置进行hook


将szNewText赋给eax,再压到ESP + 0x24 + 0x8的位置,0x24是pushad和pushfd压入堆栈寄存器占用的内存,因为我们要修改MessageBox的第二个值,位于0x8偏移,修改值之后将寄存器还原并执行之前被覆盖的代码,然后通过jmp跳转回原函数



然后通过HookMessageBox进行Inline hook

  1. BOOL HookMessageBox(BOOL bOpen)
  2. {
  3. BOOL bRet = FALSE;
  4. BYTE byJmpCode[PATCH_LENGTH] = {0xE9};
  5. DWORD dwOldProtect = 0;

  6. static BYTE byOriginalCode[PATCH_LENGTH] = {0};
  7. static BOOL bHookFlag = FALSE;

  8. memset(&byJmpCode[1],0x90,PATCH_LENGTH-1);

  9. *(DWORD*)&byJmpCode[1] = (DWORD)NewMessageBox - (DWORD)dwHookAddress - 5;

  10. memcpy(byOriginalCode, (LPVOID)dwHookAddress, PATCH_LENGTH);

  11. if (bOpen)
  12. {
  13.   if (!bHookFlag)
  14.   {
  15.    VirtualProtect((LPVOID)dwHookAddress, PATCH_LENGTH, PAGE_EXECUTE_READWRITE, &dwOldProtect);
  16.    memcpy((LPVOID)dwHookAddress, byJmpCode, PATCH_LENGTH);
  17.    ::VirtualProtect((LPVOID)dwHookAddress, PATCH_LENGTH, dwOldProtect, 0);
  18.    bHookFlag = TRUE;
  19.    bRet = TRUE;
  20.   }
  21.   else
  22.   {
  23.    if (bHookFlag)
  24.    {
  25.     VirtualProtect((LPVOID)dwHookAddress, PATCH_LENGTH, PAGE_EXECUTE_READWRITE, &dwOldProtect);
  26.     memcpy((LPVOID)dwHookAddress, byOriginalCode, PATCH_LENGTH);
  27.     ::VirtualProtect((LPVOID)dwHookAddress, PATCH_LENGTH, dwOldProtect, 0);
  28.     bHookFlag = FALSE;
  29.     bRet = TRUE;
  30.    }
  31.   }
  32.   return bRet;
  33. }
  34. }
复制代码


然后打包成dll,通过远程线程注入来hook


首先看一下没有hook的效果


通过远程线程注入,可以看到已经注入成功


然后这里再点击开始,可以看到文本框的内容已经是我们自己的内容


这里到汇编层面去看一下hook前后的变化,首先在MessageBoxA断点




在77D507EA的位置的汇编语句是mov edi,edi


注入dll之后再断MessageBoxA


在77D507EA的位置已经变成了jmp Hook.1000100F,即我们自己写的jmp语句


这里F7跟进去看看


可以发现这里正是我们通过_asm传入的语句


我们通过阅读MessageBox的源码可以发现,汇编语句里面是没有E9即jmp指令的,也就是说我们如果在MessageBox这一块内存里面检测到了有E9即jmp指令就可以认为MessageBox被hook


这里创建一个新线程检测在MessageBox的内存空间里是否有E9,这里因为MessageBox这里只有0x48字节的空间,这里就只循环判断80字节的空间里面有没有E9即可,如果发现E9则弹窗并直接退出进程
  1. DWORD WINAPI Hook_E9_Thread(LPVOID lpParameter)
  2. {
  3. char MessageBoxAddrString[0x10] = {0};

  4. while(1)
  5. {
  6.    Sleep(500);
  7.    sprintf(MessageBoxAddrString,"%x",*(char*)MessageBox);
  8.    //MessageBox(0,0,MessageBoxAddrString,0);

  9.    for (int i = 0; i < 80 ; i++)
  10.    {
  11.     if (*(unsigned char*)MessageBox + i == 0xE9)
  12.     {
  13.      MessageBoxA(NULL,TEXT("MessageBoxA is been hooked"),TEXT("MessageBoxA is been hooked"),MB_OK);
  14.      ::ExitProcess(0);
  15.     }
  16.    }

  17. }


  18. return 0;

  19. }


  20. DWORD Hook_E9()
  21. {
  22. HANDLE hThread;
  23. DWORD  threadId;

  24. hThread = CreateThread(NULL, 0, Hook_E9_Thread, 0, 0, &threadId);


  25. return 0;
  26. }
复制代码


这里首先启动程序


可以看到有两个线程


然后通过远程线程注入dll,这里显示dll已经注入成功,但是程序会退出,证明反调试成功


我们知道在汇编里面进行跳转一般有两个硬编码,分别为E8和E9,E8即为call,E9即为jmp

  •         短跳转:机器码为2个字节E8 XX,E8是call的硬编码,XX是跳转范围-128~127
  •         长跳转:机器码为5个字节E9 XX XX XX XX,E9是jmp的硬编码,剩下4个字节表示转移偏移量
我们知道如果要对E9进行监控肯定只会在MessageBoxA这块内存空间进行监控,那么我们就可以通过E8短跳到其他地址,再通过E9长跳到我们自己的函数
第二层在第一层的hook攻防中,我们首先用常规方式被hook,然后使用E8指令代替E9来实现了绕过hook的效果,作为蓝队人员也对检测机制进行了升级,在第二层hook攻防中,蓝方成员选择的是CRC/全代码校验。通俗来说,就是把某个API的硬编码给抠出来,单独起一个线程一直循环比对,无论是使用E8还是E9都会修改内存中的硬编码,这时候就会被检测到。CRC校验的定义如下:


因为CRC校验的实现难度较大,这里我就使用抠出MessageBox硬编码的方式,这里MessageBox的内存范围就是77D507EA到77D50832这一块


这里有一个坑,如果对着上面的硬编码抠的话有些硬编码是没有显示出来的,这里最好是对着下面的内存窗口抠


然后将抠出来的硬编码存放到szAPICode里面,使用memcmp函数进行比较,并用VirtualProtect来改变内存的读写情况,如果不相等则使用ExitProcess()结束进程
  1. if (memcmp((LPVOID)MessageBoxAddr, szAPICode, 0x30) != 0)
  2.    {
  3.     BOOL bRet = VirtualProtect((LPVOID)MessageBoxAddr, 0x10, PAGE_READWRITE, &dwOldProtect);

  4.     if (bRet)
  5.     {
  6.      ::memcpy((LPVOID)MessageBoxAddr, szAPICode, 0x30);
  7.      ::VirtualProtect((LPVOID)MessageBoxAddr,0x10, dwOldProtect, &dwOldProtect);
  8.     }

  9.     MessageBoxA(NULL,TEXT("MessageBoxA is been hooked"),TEXT("MessageBoxA is been hooked by CRC"),MB_OK);
  10.     ::ExitProcess(0);
  11.    }
复制代码


然后检验一下,首先启动exe


然后还是使用之前的dll进行远程线程注入,这里可以看到被检测到hook


当检测程序使用了全代码校验的情况下,就不能使用常规的方式去进行hook,这里我们去逆向分析一下代码
硬件断点
首先定位到MessageBox,尝试直接软件断点,这里软件断点的原理就是将8B这个硬编码改为CC,也就是说在全代码校验的情况下肯定是会被拦截的


果然不出所料拦截了


程序直接退出


我们在内存里面找到77D507EA


右键下一个硬件访问断点,我们知道硬件断点的原理是通过控制dr0-dr3寄存器的值来实现异常,也就是说我们不会去修改内存里面的值


可以看到断到了全代码的检测函数


这里单步往下跟,到call MessageB.memcmp是比较的关键


可以看到memcmp这个函数就是将edi和esi所在地址里面存的值存入cl和dl,如果相等继续往下走,取下一个地址里面存的值放入,不相等则直接跳转到0040114A


到00401118这个地址,如果上面都没有跳转,则又继续回到004010FD这个地址,可以发现这就是一个循环比较的过程,这里当eax的值减为0的时候结束循环


那么我们就可以将比较的jcc语句直接置为nop,然后让最后跳转会函数起始地址的jcc语句直接改为jmp,即恒执行循环


修改完成后我们删除硬件断点


再次执行hook,注入成功

线程挂起
这里因为我们的程序比较简单,通过线程很容易看出来哪个线程是检测线程,这里我们直接将检测线程挂起


然后进行注入也可以注入成功

第三层我们从第二层的hook攻防可以得出两种思路,因为我们程序的逻辑比较简单,那么我们在进行逆向分析的过程中很容易找到程序运行的逻辑点,对于相对复杂的程序,这种方法是行不通的,第二种思路当然也有一定的局限性,但是总体来说实现也还是比较容易
那么作为防守方,又想出了相应的对策:
对于硬件断点修改逻辑,防守方选择写多个call嵌套,在其中的一个call里面retn,但是不是retn到初始call的下一行,而是跳转到其他地址。那么当调试者看到这个call的时候,因为逻辑很复杂,会选择直接步过,如果直接步过,因为retn的地址并不是下一行,这样就会跟丢
对于挂起线程,同时起多个线程互相检测是否都处于活动状态,例如A检测B,B检测C,C检测D,D再检测防止hook的线程,这样就会使攻击方的工作量大大提升
对于防守方的策略可以概括为:首先对API进行全代码校验,然后通过多个线程互相监控线程状态,对VirtualProtect和ExitProcess进行挂钩处理。那么针对防守方的策略我们可以想到两种方法进行绕过,一种是通过逆向分析检测线程的代码,通过寻找代码的漏洞进行绕过,另一种方法就是通过CPU层面的DR0-DR7寄存器实现硬件断点来触发异常。
瞬时钩子
假设这里我们已经通过逆向分析找到了主监测线程的代码,这里可以发现是先监测E9,再监测ExitProcess有没有被hook,然后再检测MessageBoxA有没有被hook,这里我们的一个想法是直接hook ExitProcess函数,但是这里因为有另外一个线程监控着主线程,如果直接hook程序也会直接退出,那么我们可以发现主检测线程的一个漏洞点,就是先检测ExitProcess有没有被hook,再继续往下走,那么我们就可以在1这个地方给ExitProcess挂钩,这里要注意VirtualProtect这个函数并不是只有我们一个线程在使用,所以这里需要写一个判断确认当前的地址,然后在2这个地方ExitProcess因为被hook的关系不会正常退出,在即将进入下一个循环的时候将钩子摘除,即可达到瞬时钩子的效果


那么我们首先hookVirtualProtect,这里我就直接用E9来hook,如果有E9的检测就可以用E8来跳
  1. BOOL bRet = FALSE;
  2. BYTE byJmpCode[PATCH_LENGTH_VP] = { 0xE9 };
  3. DWORD dwOldProtect = 0;

  4. static BYTE byOriginalCode[PATCH_LENGTH_VP] = { 0 };
  5. static BOOL bHookFlag = FALSE;

  6. // 初始化 byJmpCode
  7. memset(&byJmpCode[1], 0x90, PATCH_LENGTH_VP - 1);
  8. // 存储跳转地址
  9. *(DWORD*)&byJmpCode[1] = (DWORD)NewVirtualProtect - (DWORD)dwHookAddressVP - 5;
  10. // 备份被覆盖的 Code
  11. memcpy(byOriginalCode, (LPVOID)dwHookAddressVP, PATCH_LENGTH_VP);
复制代码


然后把要hook的地址存到数组里面
  1. VirtualProtect((LPVOID)dwHookAddressVP, PATCH_LENGTH_VP, PAGE_EXECUTE_READWRITE, &dwOldProtect);
  2. memcpy((LPVOID)dwHookAddressVP, byJmpCode, PATCH_LENGTH_VP);
  3. VirtualProtect((LPVOID)dwHookAddressVP, PATCH_LENGTH_VP, dwOldProtect, 0);
复制代码


这里VirtualProtect在内存中的地址可以通过GetProcAddress配合LoadLibrary获取
  1. dwHookAddressVP = (DWORD)GetProcAddress(LoadLibrary("kernel32.dll"), "VirtualProtect");
复制代码


在NewVirtualProtect使用asm调用VirtualProtectProc为ExitProcess挂钩
  1. void VirtualProtectProc() {
  2. dwHookAddressVP = (DWORD)GetProcAddress(LoadLibrary("kernel32.dll"), "ExitProcess");
  3. OutputDebugString("VirtualProtect 开始挂钩 , hook ExitProcess successfully");
  4. HookExitProcess(TRUE);
  5. }
复制代码


再就是hook ExitProcess,跟hook VirtualProtect的方法相同
  1. BOOL bRet = FALSE;
  2. BYTE byJmpCode[PATCH_LENGTH_EP] = { 0xE9 };
  3. DWORD dwOldProtect = 0;

  4. static BYTE byOriginalCode[PATCH_LENGTH_EP] = { 0 };
  5. static BOOL bHookFlag = FALSE;

  6. // 初始化 byJmpCode
  7. memset(&byJmpCode[1], 0x90, PATCH_LENGTH_EP - 1);
  8. // 存储跳转地址
  9. *(DWORD*)&byJmpCode[1] = (DWORD)NewExitProcess - (DWORD)dwHookAddressEP - 5;
  10. // 备份被覆盖的 Code
  11. memcpy(byOriginalCode, (LPVOID)dwHookAddressEP, PATCH_LENGTH_EP);
复制代码


同样将地址存入数组
  1. VirtualProtect((LPVOID)dwHookAddressEP, PATCH_LENGTH_EP, PAGE_EXECUTE_READWRITE, &dwOldProtect);
  2. memcpy((LPVOID)dwHookAddressEP, byJmpCode, PATCH_LENGTH_EP);
  3. VirtualProtect((LPVOID)dwHookAddressEP, PATCH_LENGTH_EP, dwOldProtect, 0);
复制代码


然后同样在NewExitProcess里面将钩子摘除,避免在进入下一个循环的时候被检测到
  1. void ExitProcessProc()
  2. {
  3. // 卸载瞬时 HOOK 避免被检测到
  4. OutputDebugString("ExitProcess 开始卸载 , unhook ExitProcess successfully");
  5. HookExitProcess(FALSE);
  6. }
复制代码


再就是hook MessageBox的函数,这里直接用之前写好的Inline hook挂钩到NewMessageBox即可
  1. void __declspec(naked) NewMessageBox() {
  2. __asm {
  3.   pushad
  4.   pushfd
  5.   lea eax, dword ptr ds : [szNewText]
  6.   mov dword ptr ss : [esp + 0x24 + 8] , eax
  7.   popfd
  8.   popad
  9.   mov edi, edi
  10.   push ebp
  11.   mov ebp, esp
  12.   jmp dwRetAddress
  13. }
  14. }
复制代码


然后加载dll


首先在VirtualProtect挂钩ExitProcess,再对MessageBox进行hook,再将ExitProcess的钩子摘除,如此循环往复




看一下实现的效果,这里还是执行一下hook之前的程序


然后远程线程注入瞬时钩子的dll,修改文本框内容成功,可以看到windbg里面挂钩函数跟卸载函数是不断的交替执行的,hook成功


硬件钩子
我们知道硬件断点是基于线程的,因为每个线程的CONTEXT结构是不同的,这里首先就需要找到我们要修改dr寄存器的线程,也就是我们要hook的检测线程,找到线程之后我们通过OpenThread去获得线程的句柄,然后通过SetUnhandledExceptionFilter去注册一个异常处理函数,注册完成之后就可以更改dr寄存器的值来触发访问/写入/执行断点,然后再通过SetThreadContext放到CONTEXT结构里面即可
那么这里先找到OpenThread和MessageBoxA在内存中的地址
  1. g_fnOpenThread = (FNOPENTHREAD)::GetProcAddress(LoadLibrary("kernel32.dll"), "OpenThread");
  2. g_dwHookAddr = (DWORD)GetProcAddress(GetModuleHandle("user32.dll"),"MessageBoxA");
复制代码



然后拍摄快照遍历线程
  1. HANDLE hTool32 = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
复制代码



定位到我们要hook的线程
  1. <pre class="cke_widget_element" data-cke-widget-data="%7B%22code%22%3A%22if%C2%A0(Thread32First(hTool32%2C%C2%A0%26thread_entry32))%5Cn%C2%A0%C2%A0%7B%5Cn%C2%A0%C2%A0%C2%A0do%5Cn%C2%A0%C2%A0%C2%A0%7B%5Cn%C2%A0%C2%A0%C2%A0%C2%A0if%C2%A0(thread_entry32.th32OwnerProcessID%C2%A0%3D%3D%C2%A0GetCurrentProcessId())%5Cn%C2%A0%C2%A0%C2%A0%C2%A0%7B%5Cn%C2%A0%C2%A0%C2%A0%C2%A0%C2%A0dwCount%2B%2B%3B%C2%A0%5Cn%C2%A0%C2%A0%C2%A0%C2%A0%C2%A0%C2%A0%C2%A0%C2%A0%C2%A0%C2%A0%C2%A0%C2%A0%C2%A0%C2%A0%C2%A0%C2%A0%C2%A0%C2%A0%C2%A0%C2%A0if%C2%A0(dwCount%C2%A0%3D%3D%C2%A01)%C2%A0%5Cn%22%2C%22classes%22%3Anull%7D" data-cke-widget-keep-attr="0" data-cke-widget-upcasted="1" data-widget="codeSnippet"><code class="hljs">if (Thread32First(hTool32, &thread_entry32))
  2.   {
  3.    do
  4.    {
  5.     if (thread_entry32.th32OwnerProcessID == GetCurrentProcessId())
  6.     {
  7.      dwCount++;
  8.                     if (dwCount == 1)
  9. </code></pre>
  10. <span class="cke_reset cke_widget_drag_handler_container" style="background: url(" https:="" csdnimg.cn="" release="" blog_editor_html="" release2.1.0="" ckeditor="" plugins="" widget="" images="" handle.png")="" rgba(220,="" 220,="" 0.5);="" top:="" 0px;="" left:="" 0px;"=""><img class="cke_reset cke_widget_drag_handler" data-cke-widget-drag-handler="1" height="15" role="presentation" src="data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==" title="点击并拖拽以移动" width="15"></span>
复制代码


这里定位到线程之后我们把THREADENTRY32里面的进程ID和线程ID打印出来
  1. char szBuffer[0x100];
  2. ZeroMemory(szBuffer,0x100);
  3. sprintf(szBuffer, "PID:%x - TID:%x\n", thread_entry32.th32OwnerProcessID, thread_entry32.th32ThreadID);
  4. OutputDebugString(szBuffer);
复制代码



然后通过内存中定位的OpenThread得到线程的句柄
  1. hHookThread = g_fnOpenThread(THREAD_SET_CONTEXT | THREAD_GET_CONTEXT | THREAD_QUERY_INFORMATION, FALSE, thread_entry32.th32ThreadID);
复制代码



拿到线程句柄之后我们通过SetUnhandledExceptionFilter注册一个异常处理函数MyExceptionFilter
  1. SetUnhandledExceptionFilter(MyExceptionFilter);
复制代码



通过ExceptionRecord里面的ExceptionCode判断错误码是否为EXCEPTION_SINGLE_STEP即单步异常以及ExceptionAddress判断是否到我们设置hook的地址,然后通过ChangeContext修改CONTEXT,再修改EIP
  1. LONG WINAPI MyExceptionFilter(PEXCEPTION_POINTERS pExceptionInfo)
  2. {
  3. if (pExceptionInfo->ExceptionRecord->ExceptionCode == EXCEPTION_SINGLE_STEP)
  4. {
  5.   if((DWORD)pExceptionInfo->ExceptionRecord->ExceptionAddress == g_dwHookAddr)
  6.   {
  7.    PCONTEXT pContext = pExceptionInfo->ContextRecord;
  8.    ChangeContext(pContext);
  9.    pContext->Eip = (DWORD)&OriginalFunc;
  10.    return EXCEPTION_CONTINUE_EXECUTION;
  11.   }
  12. }

  13. return EXCEPTION_CONTINUE_SEARCH;
  14. }
复制代码


这里ChangeContext要实现的功能就是修改文本框中的内容,esp指向的是MessageBox,那么esp+8即为MessageBox的第二个参数
  1. <pre class="cke_widget_element" data-cke-widget-data="%7B%22code%22%3A%22void%C2%A0ChangeContext(PCONTEXT%C2%A0pContext)%5Cn%7B%5Cn%C2%A0char%C2%A0szBuffer%5B0x100%5D%3B%5Cn%C2%A0DWORD%C2%A0dwOldProtect%C2%A0%3D%C2%A00%3B%5Cn%C2%A0DWORD%C2%A0dwLength%C2%A0%3D%C2%A00%3B%5Cn%C2%A0LPSTR%C2%A0lpOldText%C2%A0%3D%C2%A0NULL%3B%5Cn%5Cn%C2%A0char%C2%A0szNewText%5B%5D%C2%A0%3D%C2%A0%5C%22SEH%C2%A0Hook%C2%A0successfully%5C%22%3B%5Cn%C2%A0%5Cn%C2%A0lpOldText%C2%A0%3D%C2%A0(LPSTR)(*(DWORD*)(pContext-%3EEsp%C2%A0%2B%C2%A00x8))%3B%5Cn%C2%A0dwLength%C2%A0%3D%C2%A0strlen(lpOldText)%3B%5Cn%5Cn%C2%A0VirtualProtect(lpOldText%2C%C2%A0dwLength%2C%C2%A0PAGE_EXECUTE_READWRITE%2C%C2%A0%26dwOldProtect)%3B%5Cn%C2%A0memcpy(lpOldText%2C%C2%A0szNewText%2C%C2%A0dwLength)%3B%5Cn%C2%A0VirtualProtect(lpOldText%2C%C2%A0dwLength%2C%C2%A0dwOldProtect%2C%C2%A00)%3B%5Cn%7D%5Cn%22%2C%22classes%22%3Anull%7D" data-cke-widget-keep-attr="0" data-cke-widget-upcasted="1" data-widget="codeSnippet"><code class="hljs">void ChangeContext(PCONTEXT pContext)
  2. {
  3. char szBuffer[0x100];
  4. DWORD dwOldProtect = 0;
  5. DWORD dwLength = 0;
  6. LPSTR lpOldText = NULL;

  7. char szNewText[] = "SEH Hook successfully";

  8. lpOldText = (LPSTR)(*(DWORD*)(pContext->Esp + 0x8));
  9. dwLength = strlen(lpOldText);

  10. VirtualProtect(lpOldText, dwLength, PAGE_EXECUTE_READWRITE, &dwOldProtect);
  11. memcpy(lpOldText, szNewText, dwLength);
  12. VirtualProtect(lpOldText, dwLength, dwOldProtect, 0);
  13. }
  14. </code></pre>
  15. <span class="cke_reset cke_widget_drag_handler_container" style="background: url(" https:="" csdnimg.cn="" release="" blog_editor_html="" release2.1.0="" ckeditor="" plugins="" widget="" images="" handle.png")="" rgba(220,="" 220,="" 0.5);="" top:="" 0px;="" left:="" 0px;"=""><img class="cke_reset cke_widget_drag_handler" data-cke-widget-drag-handler="1" height="15" role="presentation" src="data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==" title="点击并拖拽以移动" width="15"></span>
复制代码


然后就是Eip修改到hook+2的位置,我们知道一般API起始的位置都是mov edi,edi,不能从这个起始位置执行,否则会死循环
g_dwHookAddrOffset = g_dwHookAddr + 2;
  1. void __declspec(naked) OriginalFunc(void)
  2. {
  3. __asm
  4. {
  5.   mov edi,edi
  6.   jmp [g_dwHookAddrOffset]
  7. }
  8. }
复制代码


然后将hook的地址放到dr0寄存器里面,设置dr7的L0位为1即局部有效,断点长度设置为1即18、19位设置为0即可,断点类型设置为访问断点对应的值为0(20、21位设置为0),这样dr7寄存器的1-31位都为0,32位为1,所以将dr7寄存器的值设置为1。然后通过SetThreadContext存入CONTEXT结构
  1. <pre class="cke_widget_element" data-cke-widget-data="%7B%22code%22%3A%22%C2%A0%C2%A0%C2%A0threadContext.Dr0%C2%A0%3D%C2%A0g_dwHookAddr%3B%5Cn%C2%A0%C2%A0%C2%A0threadContext.Dr7%C2%A0%3D%C2%A01%3B%5Cn%5Cn%C2%A0%C2%A0%C2%A0SetThreadContext(hHookThread%2C%C2%A0%26threadContext)%3B%5Cn%C2%A0%C2%A0%C2%A0CloseHandle(hHookThread)%3B%5Cn%22%2C%22classes%22%3Anull%7D" data-cke-widget-keep-attr="0" data-cke-widget-upcasted="1" data-widget="codeSnippet"><code class="hljs">  threadContext.Dr0 = g_dwHookAddr;
  2.    threadContext.Dr7 = 1;

  3.    SetThreadContext(hHookThread, &threadContext);
  4.    CloseHandle(hHookThread);
  5. </code></pre>
  6. <span class="cke_reset cke_widget_drag_handler_container" style="background: url(" https:="" csdnimg.cn="" release="" blog_editor_html="" release2.1.0="" ckeditor="" plugins="" widget="" images="" handle.png")="" rgba(220,="" 220,="" 0.5);="" top:="" 0px;="" left:="" 0px;"=""><img class="cke_reset cke_widget_drag_handler" data-cke-widget-drag-handler="1" height="15" role="presentation" src="data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==" title="点击并拖拽以移动" width="15"></span>
复制代码


首先看一下没有hook之前


这里在ChangeContext里面将CONTEXT结构里面的EAX和ECX寄存器打印出来,修改文本框的内容为SEH Hook successfully


然后注入dll,hook成功


打印出了EAX和ECX


这里再打印出PID跟TID,证明hook成功


回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|安全矩阵

GMT+8, 2024-11-30 09:51 , Processed in 0.015668 second(s), 18 queries .

Powered by Discuz! X4.0

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表