通过钩取记事本的kernel32!WriteFile() API,使其执行不同动作。C26ZWSTULTZL7HMW@~3H24X.png
由于该技术借助“调试”钩取,所以能够进行与用户更具交互性(interctive)的钩取操作。也就是说,这种技术会向用户提供简单的接口,使用户能够控制目标进程的运行,并且可以自由使用进程内存。

关于调试器的说明

术语

调试器(Debugger):进行调试的程序
被调试者(Debuggee):被调试的程序

调试器工作原理

调试进程经过注册后,每当被调试者发生调试事件(Debug Event)时,OS就会暂停其运行,并向调试器报告相应事件。调试器对应事件做适当处理后,使被调试者继续运行。
#一般的异常(Exception)也属于调试事件。
#若相应进程处于非调试,调试事件会在其自身的异常处理或OS的异常处理机制中被处理掉。
#调试器无法处理或不关心的调试事件最终由OS处理。
AN6IXLXYL4KJKN0P)U(ZBW0.png

调试事件

各种调试事件整理如下:
#EXCEPTIONDEBUG_EVENT
#CREATE_THREAD_DEBUG_EVENT
#CREATE
PROCESSDEBUG_EVENT
#EXIT
THREAD DEBUG EVENT
#EXIT PROCESS DEBUG EVENT
#LOAD DLL
DEBUG EVENT
#UNLOAD
DLL DEBUG EVENT
#OUTPUT DEBUG_STRING EVENT
#RIP EVENT
上面列出的调试事件中,与调试相关的事件为EXCEPTION
DEBUG EVENT,下面是与其相关的异常列表。
□ EXCEPTION_ACCESS_VI〇LATION
□ EXCEPTION_ARRAY_BOUNDS_EXCEEDED
□ EXCEPTION_BREAICPOINT
□ EXCEPTION_DATATYPE_MISALIGNMENT
□ EXCEPTION_FLT_DENORMAL_OPERAND
□ EXCEPTION_FLT_DIVIDE_BY_ZHRO
□ EXCEPTION_FLT_INEXACT_RESULT
□ EXCEPTION_FLT_INVALID_OPERATION
□ EXCEPTION_FLT_OVERFLOW
□ EXCEPTION_FLT_STACK_CHECK
□ EXCEPTION_FLT_UNDERFLOW
□ EXCEPTION_ILLHGAL_INSTRUCTION
□ EXCEPTION_IN_PAGE_ERROR
□ EXCEPTION_INT_DIVIDE_BY_ZERO
□ EXCEPTIONJNT_OVERFLOW
□ EXCEPTION_INVALID_DISPOSITION
□ EXCEPTION_NONCONTINUABLE_EXCEPTION
□ EXCEPTION_PRIV_INSTRUCTION
□ EXCEPTION_SINGLE_STEP
□ EXCEPTION_STACK_OVERFLOW
上面各种异常中,调试器
必须处理的是EXCEPTION_BREAKPOINT异常D断点对应的汇编指令为INT3,IA-32指令为0xCC。代码调试遇到INT3指令即中断运行,EXCEPTION_BREAKPOINT 异常亊件被传送到调试器,此时调试器可做多种处理。
调试器实现断点的方法非常简单,找到要设置断点的代码在内存中的起始地址,只要把1个 字节修改为0xCC就可以了。想继续调试时,再将它恢复原值即可。通过调试钩取API的技术就是 利用了断点的这种特性。

调试技术流程

借助调试技术钩取API的方法的基本思路是,在“调试器-被调试者”的状态下,将被调试者的API起始部分修改为0xCC,控制权转移到调试器后执行特定操作,最后使被调试者重新进入运行状态。
具体调试流程如下:
□对想钩取的进程进行附加操作,使之成为被调试者;
□“钩子”:将API起始地址的第一个字节修改为0xCC;
□调用相应API时,控制权转移到调试器;
□执行需要的操作(操作参数、返回值等);
□脱钩:将0xCC恢复原值(为了正常运行API);
□运行相应API(无0xCC的正常状态);
□“钩子”:再次修改为0xCC(为了继续钩取);
□控制权返还被调试者。
再次基础上可以有多种变化。既可以不调用原始API,也可以调用用户提供的客户API;可以只钩取一次,也可以钩取多次。实际应用时,根据需要适当调整即可。

记事本小写转大写

假设notepad要保存文件中的某些内容是会调用kernel32!WriteFile() API。

WriteFile()定义如下:
BOOL WriteFile(
HANDLE hFile,
LPCVOID lpBuffer,
DWORD nNumberOfBytesWritten,
LPOVERLAPPEN lpOverlapped
);
第二个参数(lpBuffer)为数据缓冲区指针,第三个参数(nNumberOfBytesToWrite)为要写的字节数。函数参数被以逆序形式存储到栈。使用OD调试查看程序栈。
使用OD打开notepad后,在Kernel32!WriteFile() API处设置断点。
![C7]BREG(_OE(5X}X1I4~8C.png
OD代码窗口中可以看到,调试器在kernel32!WriteFile()处(设有断点)暂停,然后查看进程栈。
![}38(P4M~}G4B8(JN4Q@I~A.png
当前栈(ESP:7FA7C)中存在一个返回值(01004C3),ESP+8(7FA84)中存在数据缓冲区的地址(0E7310)。直接转到数据缓冲区地址处,可以看到要保存到notepad的字符串。钩取WriteFile() API后,用指定字符串覆盖数据缓冲区中的字符串即可。

执行流

我们现在已经知道应该修改被调试进程内存的哪一部分了。接下来,只要正常运行 WriteFile(),将修改后的字符串保存到文件就可以了。
下面我们使用调试方法来钩取API。利用前面介绍的hookdbg.exe,在WriteFile() API起始地址 处设置断点(INT3)后,向被调试进程(notepad.exe)保存文件时,EXCEPTION_BREAKPOINT事件就会传给调试器(hookdbg.exe)。那么,此时被调试者(notepad.exe)的EIP值是多少呢?
乍一想很容易认为是WriteFile() API的起始地址(7C7E0E27)。但其实EIP的值应该为WriteFile() API的起始地址(7C7E0E27) +1=7C7E0E28。
原因在于,我们在WriteFile() API的起始地址处设置了断点,被调试者(notpad.exe)内部调 用WriteFile()时,会在起始地址7C7E0E27处遇到INT3 (0xCC)指令。执行该指令( Breakpoint-INT3 )时, EIP的值会增加1个字节(INT3指令的长度)。然后控制权会转移给调试器(hookdbg.exe ) (因为在“调试器-被调试者”关系中,被调试者中发生的EXCEPTION_BREAKPINT异常需要由调试器处理)。修改覆写了数据缓冲区的内容后,EIP值被重新更改为WriteFile() API的起始地址,继续运行。

“脱钩”&“钩子”

另一个问题是,若只将执行流返回到WriteFile()起始地址,再遇到相同的INT3指令时,就会陷人无限循环(发生EXCEPTION_BREAKPOINT)。为了不致于陷人这种境地,应该去除设置在 WriteFile() API起始地址处的断点,即将0xCC更改为original byte ( 0x6A )( original byte在钩取API前已保存)。这一操作称为“脱钩”,就是取消对API的钩取。
覆写好数据缓冲区并正常返回WriteFile() API代码后,EIP值恢复为WriteFile() API的地址,修改后的字符串最终保存到文件。这就是hookdbg.cpp的工作原理。
若只需要钩取1次,那到这儿就结束了。但如果需要不断钩取,就要再次设置断点。只靠说明是理解不了的,下面结合源代码(hookdbg.cpp)详细讲解。

源码分析

main()

  1. #include "windows.h"
  2. #include "stdio.h"
  3. LPVOID g_pfWriteFile = NULL;
  4. CREATEA_PROCESS_DEBUG_INFO g_cpdi;
  5. BYTE g_chINT3 = 0xCC g_chOrgByte = 0;
  6. int main(int argc, char* argv[])
  7. {
  8. DWORD dwPID;
  9. if( argc != 2 )
  10. {
  11. printf("\nUSAGE : hookdbg.exe <pid>\n");
  12. return 1;
  13. }
  14. // Attach Process
  15. dwPID = atoi(argv[1]);
  16. if( !DebugActiveProcess(dwPID) )
  17. {
  18. printf("DebugActiveProcess(%d) failed!!!\n"
  19. "Error Code = %d\n", dwPID, GetLastError());
  20. return 1;
  21. }
  22. DebugLoop();
  23. return 0;
  24. }

以程序运行参数的形式接收要钩取API进程的PID。然后通过DebugActiveProcess() API将调试器附加到该运行的进程上,开始调试(上面输入的PID作为参数传入函数)。

  1. BOOL WINAPI DebugActiveProcess(
  2. DWORD dwProcessId
  3. );

然后进入DebugLoop()函数处理来自调试者的调试事件。
提示:另一种启动调试的方法是使用CreateProcess() API,从一开始就直接以调试器模式运行相关进程。

DebugLoop()

  1. void DebugLoop()
  2. {
  3. DEBUG_EVENT de;
  4. DWORD dwContinueStatus;
  5. // 等待被调试者发生事件
  6. while( WaitForDebugEvent(&de, INFINITE) )
  7. {
  8. dwContinueStatus = DBG_CONTINUE;
  9. //被调试进程生成或者附加事件
  10. if( CREATE_PROCESS_DEBUG_EVENT == de.dwDebugEventCode )
  11. {
  12. OnCreateProcessDebugEvent(&de);
  13. }
  14. //异常事件
  15. else if( EXCEPTION_DEBUG_EVENT == de.dwDebugEventCode )
  16. {
  17. if( OnExceptionDebugEvent(&de) )
  18. continue;
  19. }
  20. //被调试进程终止事件
  21. else if( EXIT_PROCESS_DEBUG_EVENT == de.dwDebugEventCode )
  22. {
  23. //被调试者终止-调试器终止
  24. break;
  25. }
  26. //再次运行被调试者
  27. ContinueDebugEvent(de.dwProcessId, de.dwThreadId, dwContinueStatus);
  28. }
  29. }

该函数的工作原理类似于窗口过程函数(WndProc),它从被调试者处接收事件并处理,然后使被调试者继续运行。DebugLoop()函数代码比较简单,结合代码中的注释就能理解。比较重要的两个API。
WaitForDebugEvent() API是一个等待被调试者发生调试事件的函数(行为动作类似于WaitForSingleObject() API)。

  1. BOOL WINAPI WaitForDebugEvent(
  2. LPDEBUG_EVENT lpDebugEvent,
  3. DWORD dwMilliseconds
  4. );

DebugLoopz()函数代码中,若发生调试事件,WaitForDebugEvent() API就会将相关事件信息设置到其第一个参数的变量(DEBUG_EVENT结构体对象),然后立刻返回。DEBUG_EVENT结构体定义如下:
![[(DVWJHPQ3]8Z(})CQ1@KP.png
共有9种调试事件。DEBUG_EVENT.dwDebugEventCode成员会被设置为9种事件中的一种,根据相关事件的
种类,也会设置适当的DEBUG_EVENT.u(union)成员(DEBUG_EVENT.u共用体成员内部也由9个结构体组成,它们对应于事件种类的个数)。
提示:例如,发生异常事件时,dwDebugEventCode成员会被设置为EXCEPTION_DEBUG_EVENT,u.Exception结构体也会得到设置。
ContinueDebugEvent() API是一个使被调试者继续运行的函数。

  1. BOOL WINAPIContinuDebugEvent(
  2. DWORD dwProcessId,
  3. DWORD dwThreadId,
  4. DWORD dwContinueStatus
  5. );

ContinueDebugEvent() API的最后一参数dwContinueStatus的值为DBG_CONTINUE或DBG_EXCEPTION_NOT_HANDLED。
若处理正常则其值设置为DBG_CONTINUE;若无法处理,或希望在应用程序的SEH中处理,则其值设置为DBG_EXCEPTION_NOT_HANDLED。
DebugLoop()函数处理3种调试事件。
EXIT_PROCESS_DEBUG_EVENT
CREATE_PROCESS_DEBUG_EVENT
EXCEPTION_DEBUG_EVENT

EXIT_PROCESS_DEBUG_EVENT

被调试进程终止时会触发该事件。示例代码中发生该事件时,调试器与被调试者将一起终止。

CREATE_PROCESS_DEBUG_EVENT-OnCreateProcessDebugEvent()

OnCreateProcessDebugEvent()是CREATE_PROCESS_DEBUG_EVENT事件句柄,被调试进程启动或附加时即调用执行该函数。

  1. BOOL OnCreateProcessDebugEvent(LPDEBUG_EVENT pde)
  2. {
  3. //获取WriteFile() API地址
  4. g_pfWriteFile = GetProcAddress(GetModuleHandle(L"kernel32.dll"), "GetCommandLineA");
  5. //API“钩子”-WriteFile()
  6. //更改第一个字节为0xCC(INT3)
  7. //originalbyte是g_ch0rgByte备份
  8. memcpy(&g_cpdi, &pde->u.CreateProcessInfo, sizeof(CREATE_PROCESS_DEBUG_INFO));
  9. ReadProcessMemory(g_cpdi.hProcess, g_pfWriteFile, &g_chOrgByte, sizeof(BYTE), NULL);
  10. WriteProcessMemory(g_cpdi.hProcess, g_pfWriteFile, &g_chINT3, sizeof(BYTE), NULL);
  11. return TRUE;
  12. }

首先获取WriteFile() API的起始地址,需要注意,它获取的不是被调试进程的内存地址,而是调试进程的内存地址。对于Windows OS的系统DLL而言,它们所有进程中都会加载到相同地址(虚拟内存),所以上面的做法没有任何问题。
g_cpdi是CREATE_PROCESS_DEBUG_INFO结构体变量。

  1. typedef struct _CREATE_PROCESS_DEBUG_INFO {
  2. HANDLE hFile;
  3. HANDLE hProcess;
  4. HANDLE hThread;
  5. LPVOID lpBaseOfImage;
  6. DWORD dwDebugInfoFileOffset;
  7. DWORD nDebugInfoSize;
  8. LPVOID lpThreadLocalBase;
  9. LPTHREAD_START_ROUTINE lpStartAddress;
  10. LPVOID lpImageName;
  11. WORD fUnicode;
  12. } CREATE_PROCESS_DEBUG_INFO;

通过CREATEPROCESS_DEBUG_INFO结构体的hProcess成员(被调试进程的句柄),可以钩取WriteFile() API(不使用调试方法时,可以使用OpenProcess() API 获取相应进程的句柄)。
只要在API的起始位置设好断点即可,由于调试器拥有被调试进程的句柄(带有调试权限),所以可以使用ReadProcessMemory()、WriteProcssMemory() API对被调试进程的内存空间自由进行读写操作。使用上面函数可以向被调试者设置断点(INT3 0xCC)。通过ReadProcessMemory()读取WriteFile() API的第一个字节,并将其存储到g_chOrgByte变量。WriteFile() API的第一个字节为0x6A(Windows XP操作系统)。
![R]`MXTEP4L)KJI
)H8I]6B3.png](https://cdn.nlark.com/yuque/0/2020/png/554486/1585409679610-5b1e44e7-75c8-4312-8b6b-369b790a15d7.png#align=left&display=inline&height=110&name=R%5D%60MXTEP4L%29KJI_%29H8I%5D6B3.png&originHeight=110&originWidth=548&size=40668&status=done&style=none&width=548)
g_chOrgByte变量中存储的是WriteFile() API的第一个字节,后面“脱钩”时会用到。然后使用WriteProcessMemory() API将WriteFile() API的第一个字节更改为0xCC。
Z4UEY~ZWR9MRF7@BFMC%BAX.png
0xCC是IA-32指令,对应于INT3指令,也就是断点。CPU遇到INT3指令时会暂停执行程序,并触发异常。若相应程序正处于调试中,则将控制权转移到调试器,由调试器处理。这样一来,被调试进程调用WriteFile() API时,控制权都会转移给调试器。

EXCEPTION_DEBUG_EVENT-OnExceptionDebugEvent()

OnExceptionDebugEvent()是EXCEPTION_DEBUG_EVENT事件句柄,它处理被调试者的INT3指令。核心部分。

  1. BOOL OnExceptionDebugEvent(LPDEBUG_EVENT pde)
  2. {
  3. CONTEXT ctx;
  4. PBYTE lpBuffer = NULL;
  5. DWORD dwNumOfBytesToWrite, dwAddrOfBuffer, i;
  6. PEXCEPTION_RECORD per = &pde->u.Exception.ExceptionRecord;
  7. //是断点异常(INT3)时
  8. if (EXCEPTION_BREAKPOINT == per->ExceptionCode)
  9. {
  10. //断点地址为WriteFile() API地址时
  11. if (g_pfWriteFile == per->ExceptionAddress)
  12. {
  13. //#1.Unhook
  14. // 将0xCC恢复为original byte
  15. WriteProcessMemory(g_cpdi.hProcess, g_pfWriteFile, &g_chOrgByte, sizeof(BYTE), NULL);
  16. //#2.获取线程上下文
  17. ctx.ContextFlags = CONTEXT_CONTROL;
  18. GetThreadContext(g_cpdi.hThread,&ctx);
  19. //#3.获取WriteFile()的param 2、3值
  20. // 函数参数存在于相应进程的栈
  21. // param 2:ESP+0x8
  22. // param 3:ESP+0xC
  23. ReadProcessMemory(g_cpdi.hProcess, (LPVOID)(ctx.Esp + 0x8),
  24. &dwAddrOfBuffer, sizeof(DWORD), NULL);
  25. ReadProcessMemory(g_cpdi.hProcess, (LPVOID)(ctx.Esp + 0xC),
  26. &dwNumOfBytesToWrite, sizeof(DWORD), NULL);
  27. //#4.分配临时缓冲区
  28. lpBuffer = (PBYTE)malloc(dwNumOfBytesToWrite + 1);
  29. memset(lpBuffer, 0, dwNumOfBytesToWrite + 1);
  30. //#5.复制WriteFile()缓冲区到临时缓冲区
  31. ReadProcessMemory(g_cpdi.hProcess, (LPVOID)dwAddrOfBuffer,
  32. lpBuffer, dwNumOfBytesToWrite, NULL);
  33. printf("\n### original string : %s\n", lpBuffer);
  34. //#6.将小写字母转换为大写字母
  35. for (i = 0; i < dwNumOfBytesToWrite; i++)
  36. {
  37. if (0x61 <= lpBuffer[i] && lpBuffer[i] <= 0x7A)
  38. lpBuffer[i] -= 0x20;
  39. }
  40. printf("\n### original string : %s\n", lpBuffer);
  41. //#7.将变换后的缓冲区复制到WriteFile()缓冲区
  42. WriteProcessMemory(g_cpdi.hProcess, (LPVOID)dwAddrOfBuffer,
  43. lpBuffer, dwNumOfBytesToWrite, NULL);
  44. //#8.释放缓冲区
  45. free(lpBuffer);
  46. //#9.将线程上下文的EIP变改为WriteFile()首地址
  47. // (当前为WriteFile()+1位置,INT3命令之后)
  48. ctx.Eip = (DWORD)g_pfWriteFile;
  49. SetThreadContext(g_cpdi.hThread, &ctx);
  50. //#10.运行被调试进程
  51. ContinueDebugEvent(pde->dwProcessId, pde->dwThreadId, DBG_CONTINUE);
  52. Sleep(0);
  53. //#11.API“钩子”
  54. WriteProcessMemory(g_cpdi.hProcess, g_pfWriteFile,&g_chINT3, sizeof(BYTE), NULL);
  55. return TRUE;
  56. }
  57. }
  58. return FALSE;
  59. }

首先使用if语句用于检测异常是否为EXCEPTION_BREAKPOINT异常,然后用if语句检测发生断点的地址是否与kernel32!WriteFile()的起始地址一致。若满足条件则继续执行以下代码。