API 钩取:逆向分析之“花”
2021-08-29 18:59:00 Author: mp.weixin.qq.com(查看原文) 阅读量:41 收藏


本文为看雪论坛优秀文章
看雪论坛作者ID:Tray_PG
跟着李承远的逆向工程核心原理边学边做的,发这个贴子的目的是为了鼓励自己坚持下去,毕竟才刚刚起步。希望大家共同进步。

1

代码逆向分析中,钩取(Hooking)是一种截取信息、更改程序执行流向、添加新功能的技术。钩取的整个流程如下:
  • 使用反汇编器/调试器把握程序的结构与工作原理。
  • 开发需要的“钩子”代码,用于修改 bug 、改善程序功能。
  • 灵活操作可执行文件与进程内存,设置“钩子”代码。
上述这一系列的工作就是代码逆行分析工程的核心(Core)内容,所以“钩取”被称为“逆向分析之花”。
 
钩取多种多样,其中钩取 Win32 API 的技术被称为 API 钩取。它与消息钩取共同广泛应用于用户模式。API 钩取是一种应用范围非常广泛的技术。

2

API(Application Programming Interface,应用程序编程接口)。
Windows OS 中,用户程序要使用系统资源(内存、文件、网络、视频、 音频等)时无法直接访问。这些资源都是由windows OS 直接管理的,出于多种考虑(稳定性、安全、效率等),Windows OS 禁止用户程序直接访问他们。用户需要使用这些资源时,必须向系统内核(Kernel)申请,申请方法就是使用微软提供的 Win32 API(或是其他OS开发公司提供的API)。
也就是说,若没有API函数,则不能创建出任何有意义的应用程序(因为它不能访问进程、线程、内存、文件、网络、注册表、图片、音频以及其他系统资源)。
 
为了运行实际的应用程序代码,需要加载许多系统库(DLL)。所有进程都会默认加载 kernel32.dll库,kernel32.dll又会加载 ntdll.dll库。
 
注:某些特定的系统进程(如:smss.exe)不会加载 kernel32.dll库。此外, GUI 应用程序中,user32.dll 与 gdi32.dll 是必须库。
 
 
假设 notepad.exe 要打开 c:\abc.txt 文件,首先在程序代码中调用 msvcrt!fopen() API ,然后引发一系列的 API 调用,如下:
- msvcrt ! fopen()    kernel32 ! CreateFileW()        ntdll ! ZwCreateFile()            ntdll ! KiFastSystemCall()                SYSENTRY        // IA-32 Instruction                    ——> 进入内核模式

如上所示,使用常规系统资源的 API 会经由 kernel32.dll 与 ntdll.dll 不断向下调用,通过 SYSRNTRY 命令进入内核模式。

3

通过 API 钩取技术可以实现对某些 Win32 API 调用过程的拦截,并获得相应的控制权限。使用 API 钩取技术的优势如下:
  • 在 API 调用前/后运行用户的“钩子”代码。
  • 查看或操作传递给 API 的参数或 API 函数的返回时。
  • 取消对 API 的调用,或者更改执行流程,运行用户代码。

正常调用 API

下图描述了正常 API 调用的情形,首先在应用程序代码区域中调用 CreateFile() API ,由于 CreateFile() API 是 kernel32.dll 的导出函数,所以,kernel32.dll 区域中的 CreateFile() API 会被调用执行并正常返回。
 

钩取API调用

下图描述的是 kernel32!CreateFile()调用情形。用户先使用 DLL 注入技术将 hook.dll 注入目标进程的内存空间,然后用 hook!MyCreateFile() 钩取对 kernel32.dll!CreateFile()的调用(有多种方法可以设置钩取函数)。这样,每当目标进程要调用kernel32!CreateFile() API 时都会先调用 hook!MyCreateFile()。
 
钩取某函数的目的有很多,如调用它之前或之后运行用户代码,或者干脆阻止它调用执行等。实际操作中只要根据自身需要灵活运用该技术即可。这也是 API 钩取的基本理念。
 
实现 API 钩取的方法多种多样,但钩取的基本概念是不变的。只要掌握了上面的概念,就能很容易得理解后面得具体实现方法。

4

下图是一张技术图标 (Tech Map),涵盖了 API 钩取得所有技术内容。
 
借助这张图表,就能(从技术层面)轻松理解前面学过的有关 API 钩取的内容。钩取 API 时,只要根据具体情况从图标中选择合适的技术即可。
 

对象

首先时关于 API 钩取方法(Method)的分类,根据针对的对象(Object)不同,API 钩取方法大致可以分为静态方法与动态方法。
 
静态方法针对的时“文件”,而动态方法针对的是进程内存,一般 API 钩取技术指动态方法,当然在某些非常特殊的情况下也可以使用静态方法。如下
 
 
注:静态方法在 API 勾取中并不常用。

位置

技术图表中这一栏用来指出实施 API 钩取时应该操作哪部分(通常有三个部分)。
 
IAT
 
IAT 将其内部的 API 地址更改为钩取函数的地址。该方法的优点是实现起来非常简单,缺点是无法勾取不在 IAT 而在程序用使用的 API (如:动态加载并使用 DLL 时)。
 
代码
 
系统库(.dll)映射到进程内存时,从中查找 API 的实际地址,并直接修改代码。该方法应用非常广泛,具体实现中有如下几种选择:
  • 使用 JMP 指令修改起始代码;
  • 覆写函数内部;
  • 仅修改必需部分的局部
EAT
 
将记录在 DLL 的 EAT 中的 API 的起始地址更改为钩取函数地址,也可以实现 API 钩取。这种方法从概念上看非常简单,但在具体实现上不如前面的 Code 方法简单、强大,所以修改 EAT 的这种方法并不常用。

技术

技术图表中的这一栏是向目标进程内存设置钩取函数的具体技术,大致分为调试法与注入法两类:注入法又细分为代码注入与DLL注入两种。
 
调试
 
调试法通过调试目标进程钩取 API 。调试器拥有被挑事者(被调试进程)的所有权限(执行控制、内存访问等),所以可以向被调试进程的内存任意设置钩取函数。
 
这里说的调试器并不是 Olludbg、WinDbg、IDAPro等,而是用户直接编写的、用来钩取的程序。也就是说,在用户编写的程序中使用调试 API 附加到目标进程,然后(执行处于暂停状态)设置钩取函数。这样,重启运行是就能完全实现 API 钩取了。
 
注入
 
注入技术是一种向目标进程内存区域进行渗透的技术,根据注入对象的不同,可以细分为 DLL 注入与代码注入两种,其中 DLL 注入技术应用最为广泛。
DLL 注入
使用 DLL 注入技术可以驱使目标进程强制加载用户指定的 DLL 文件。使用该技术时,先在要注入的 DLL 中创建钩取代码与设置代码,然后在 DllMain()中调用设置代码,注入的同时即可完成 API 钩取。

代码注入

代码注入技术比 DLL 注入技术更发达(更复杂),广泛应用于恶意代码(病毒、Shellcode等)

5

通过钩取记事本的 kernel32.dll!WriteFile() API,使其执行不同动作。

技术图表 - 调试技术

下面是调试方式的 API 钩取技术:
 
 
由于该技术借助“调试”钩取,所以能够进行与用户更具交互性(interactive)的钩取才做。也就是说,这种技术会向用户提供简单的接口,使用户能够控制目标进程的运行,并且可以自由使用金正内存。使用调试钩取技术前,我们先来了解一下调试器的构造。

关于调试器

术语

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


调试器功能

调试器用来确认被调试者是否正确运行,发现(未能预料到的)程序错误。调试器能够逐一执行被调试者的命令,拥有对寄存器与内存的所有访问权限。

调试器的工作原理

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

下图用来说明调试器工作原理

 

调试事件

各种调试事件整理如下:
EXCEPTION_DEBUG_EVENTCREATE_THREAD_DEBUG_EVENTCREATE_PROCESSDEBUG_EVENTEXIT_THREAD_DEBUG_EVENTEXIT_PROCESS_DEBUG_EVENTLOAD_DLL_DEBUG_EVENTUNLOAD_DLL_DEBUG_EVENTOUTPUT_DEBUG_STRING_EVENTRIP_EVENT

上面列出的调试事件中,与调试相关的时间为EXCEPTION_DEBUG_EVENT,下面是与相关的对应异常列表:
EXCEPTION_ACCESS_VIOLATIONEXCEPTION_ARRAY_BOUNDS_EXCEEDEDEXCEPTION_BREAKPOINTEXCEPTION_DATATYPE_MISALIGNMENTEXCEPTION_FLT_DENORMAL_OPERANDEXCEPTION_ELT_DIVIDE_BY_ZEROEXCEPTION_ELT_INEXACT_RESULTEXCEPTION_ELT_INVALID_OPERATIONEXCEPTION_ELT_OVERFLOWEXCEPTION_ELT_STACK_CHECKEXCEPTION_ELT_UNDERFLOWEXCEPTION_ILLEGAL_INSTRUCTIONEXCEPTION_IN_PAGE_ERROREXCEPTION_INT_DIVIDE_BY_ZEROEXCEPTION_INT_OVERFLOWEXCEPTION_INVALID_DISPOSITIONEXCEPTION_NONCONTINUABLE_EXCEPTIONEXCEPTION_PRIV_INSTRUCTIONEXCEPTION_SINGLE_STEPEXCEPTION_STACK_OOVERFLOW

上面的各种异常中,调试器必须处理的是EXCEPTION_BREAKPOINTER异常。断点对应的汇编指令为 INT3,IA-32 指令为 0xCC 。代码调试遇到 INT3 指令即中断运行,EXCEPTION_BREAKPOINTER异常事件被传送到调试器,测试调试器可做多种处理。
 
调试器实现断点的方法很简单,找到要设置断点的代码,在内存中的起始地址,只要把一个字节修改为 0xCC 就可以了。想继续调试时,再将它恢复原值即可。通过调试钩取 API 的技术就是利用了断点的这种特性。

调试技术的流程

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

6

实验目标

钩取 notepad.exe 的 WriteFile() API ,保存文件是操作输入参数,将小写字谜全部转换为大写字母。也就是说,在 Notepad 中保存文件内存时,其中输入的所有小写字母都会先被转换为大写字母,然后再保存。

工作原理

介绍下原理,方便实验的进行。

WriteFile()定义如下:
BOOL WriteFile(    HANDLE          hFile,    LPCVOID         lpBuffer,    DWORD           nNumberofBytesToWrite,    LPDWORD         lpNumberofBytesWritten,    LPOVERLAPPED    lpOverlapped );

  • 第一个参数(hFile)

文件或者I/O设备的句柄。
  • 第二个参数(lpBuffer)

为数据缓冲指针,指向包含要写入文件或设备的数据缓冲区的指针。
  • 第三个参数(nNumberOfBytesToWrite)

要写入文件或设备的字节数。
  • 第四个参数(lpNumberofBytesWritten)

一个指向接收使用同步hFile参数时写入的字节数的变量的指针。
  • 第五个参数(lpOverlapped)

如果hFile参数是用FILE_FLAG_OVERLAPPED打开的, 则需要指向OVERLAPPED结构的指针,否则该参数可以为 NULL。
 
顺便提醒一下:函数参数被以逆向形式存储到栈。
 
如下图所示,使用OllyDbg打开 notepad 后,在 Kernel32!WriteFile() API 处设置断点,如下所示:
 
 
按 F9 键运行程序。在记事本中输入文本后,以合适的文件名保存,如下所示:
 
 
在OllyDbg代码窗口可以看见,调式器在 kernel32!WriteFile() 处(设有断点)暂停,然后查看进程栈,如下所示:
 
 
当前栈(ESP:DF97C)中存在一个返回值(01004C30),ESP + 8 (DF984)中存在数据缓冲区的地址(0070DFC8)(如上图),直接跳转到数据缓冲区,可以看到要保存的 notepad 的字符串(“This is a test”),钩取 WriteFile() API 后,用指定的字符串覆盖数据缓冲区中的字符串即可达成所愿。

执行流

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

“脱钩”&“钩子”

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

源代码分析

这一节分析 hook.exe 的源代码。
#include "windows.h"#include "stdio.h" LPVOID g_pfWriteFile = NULL;CREATE_PROCESS_DEBUG_INFO g_cpdi;BYTE g_chINT3 = 0xCC, g_chOrgByte = 0; BOOL OnCreateProcessDebugEvent(LPDEBUG_EVENT pde){    // 获取WriteFile() API 地址    g_pfWriteFile = GetProcAddress(GetModuleHandleA("kernel32.dll"), "WriteFile");     // API Hook - WriteFile()    //   更改第一个字节为 0xCC(INT3)    //   originalbyte 是 g_ch0rgByte 备份    memcpy(&g_cpdi, &pde->u.CreateProcessInfo, sizeof(CREATE_PROCESS_DEBUG_INFO));    ReadProcessMemory(g_cpdi.hProcess, g_pfWriteFile,        &g_chOrgByte, sizeof(BYTE), NULL);    WriteProcessMemory(g_cpdi.hProcess, g_pfWriteFile,        &g_chINT3, sizeof(BYTE), NULL);     return TRUE;} BOOL OnExceptionDebugEvent(LPDEBUG_EVENT pde){    CONTEXT ctx;    PBYTE lpBuffer = NULL;    DWORD dwNumOfBytesToWrite, dwAddrOfBuffer, i;    PEXCEPTION_RECORD per = &pde->u.Exception.ExceptionRecord;     // 是断点异常(INT3)时    if (EXCEPTION_BREAKPOINT == per->ExceptionCode)    {        // 断点地址为 WriteFile() API 地址时        if (g_pfWriteFile == per->ExceptionAddress)        {            // #1. Unhook            //   将0xCC 恢复为 original byte            WriteProcessMemory(g_cpdi.hProcess, g_pfWriteFile,                &g_chOrgByte, sizeof(BYTE), NULL);             // #2. 获取线程上下文            ctx.ContextFlags = CONTEXT_CONTROL;            GetThreadContext(g_cpdi.hThread, &ctx);             // #3. 获取WriteFil() 的 param 2、3 值            //   函数参数存在于相应进程的栈            //   param 2 : ESP + 0x8            //   param 3 : ESP + 0xC            ReadProcessMemory(g_cpdi.hProcess, (LPVOID)(ctx.Esp + 0x8),                &dwAddrOfBuffer, sizeof(DWORD), NULL);            ReadProcessMemory(g_cpdi.hProcess, (LPVOID)(ctx.Esp + 0xC),                &dwNumOfBytesToWrite, sizeof(DWORD), NULL);             // #4. 分配领事缓冲区            lpBuffer = (PBYTE)malloc(dwNumOfBytesToWrite + 1);            memset(lpBuffer, 0, dwNumOfBytesToWrite + 1);             // #5. 复制 WriteFile() 缓冲区到临时缓冲区            ReadProcessMemory(g_cpdi.hProcess, (LPVOID)dwAddrOfBuffer,                lpBuffer, dwNumOfBytesToWrite, NULL);            printf("\n### original string ###\n%s\n", lpBuffer);             // #6. 将小写字母转换为大写字母            for (i = 0; i < dwNumOfBytesToWrite; i++)            {                if (0x61 <= lpBuffer[i] && lpBuffer[i] <= 0x7A)                    lpBuffer[i] -= 0x20;            }             printf("\n### converted string ###\n%s\n", lpBuffer);             // #7. 将变换后的缓冲区复制到WriteFile()缓冲区            WriteProcessMemory(g_cpdi.hProcess, (LPVOID)dwAddrOfBuffer,                lpBuffer, dwNumOfBytesToWrite, NULL);             // #8.释放临时缓冲区            free(lpBuffer);             // #9. 将线程上下文的 EIP 更改为 WriteFile()首地址            //   当前为 WriteFile()+1 位置,INT3命令之后            ctx.Eip = (DWORD)g_pfWriteFile;            SetThreadContext(g_cpdi.hThread, &ctx);             // #10. 运行被调试进程            ContinueDebugEvent(pde->dwProcessId, pde->dwThreadId, DBG_CONTINUE);            Sleep(0);             // #11. API Hook            WriteProcessMemory(g_cpdi.hProcess, g_pfWriteFile,                &g_chINT3, sizeof(BYTE), NULL);             return TRUE;        }    }     return FALSE;} void DebugLoop(){    DEBUG_EVENT de; //描述调试事件    DWORD dwContinueStatus;     // 等待被调试者发生事件    while (WaitForDebugEvent(&de, INFINITE))//等待正在调试的进程中发生调试事件。    {        dwContinueStatus = DBG_CONTINUE;         // 被调试进程生成或者附加事件        if (CREATE_PROCESS_DEBUG_EVENT == de.dwDebugEventCode)        {            OnCreateProcessDebugEvent(&de);        }        // 异常事件        else if (EXCEPTION_DEBUG_EVENT == de.dwDebugEventCode)        {            if (OnExceptionDebugEvent(&de))                continue;        }        // 被调试进程终止事件        else if (EXIT_PROCESS_DEBUG_EVENT == de.dwDebugEventCode)        {            // 被调试者终止---调试器终止            break;        }         // 再次运行被调试者        ContinueDebugEvent(de.dwProcessId, de.dwThreadId, dwContinueStatus);    }} int main(int argc, char* argv[]){    DWORD dwPID;     if (argc != 2)    {        printf("\nUSAGE : hookdbg.exe <pid>\n");        return 1;    }     // Attach Process    dwPID = atoi(argv[1]);    if (!DebugActiveProcess(dwPID))    {        printf("DebugActiveProcess(%d) failed!!!\n"            "Error Code = %d\n", dwPID, GetLastError());        return 1;    }     // 调试器循环    DebugLoop();     return 0;}


main()

#include "windows.h"#include "stdio.h" LPVOID g_pfWriteFile = NULL;CREATE_PROCESS_DEBUG_INFO g_cpdi;BYTE g_chINT3 = 0xCC, g_ch0rgByte = 0; int main(int agc, char* argv[]){    DWORD dwPID;     if( argc !=2 )    {        printf("\n USEAGE : %s  <PID>\n",argv[0],argv[1]);        return 1;    }     //Attach Process    dwPID = atoi(argv[1]);    if( !DebugActiveProcess(dwPID))    {        printf("DebugActiveProcess(%d) failed !!!\n""Error Code = %d\n",dwPID,GetLastError());        return 1;    }     //调试器    DebugLoop();    return 0;  }
BOOL DebugActiveProcess(  DWORD dwProcessId);//使调试器能够附加到活动进程并对其进行调试。  Parameters    dwProcessId    //要调试的进程的标识符。    //调试器被授予对进程的调试访问权限,    //就像它使用DEBUG_ONLY_THIS_PROCESS标志创建进程一样。
main() 函数的代码非常简单,以程序运行参数的形式接受要钩取 API 的进程 PID。然后通过 DebugActiveProcess() API 将调试器附加到该运行的进程上,开始调试(上面输入的 PID 作为参数传入函数)。
 
然后进入DebugLoop()函数,处理来自被调试者的调试信息。
void DebugLoop(){    DEBUG_EVENT de; //描述调试事件    DWORD dwContinueStatus;     // 等待被调试者发生事件    while (WaitForDebugEvent(&de, INFINITE))//等待正在调试的进程中发生调试事件。    {        dwContinueStatus = DBG_CONTINUE;         // 被调试进程生成或者附加事件        if (CREATE_PROCESS_DEBUG_EVENT == de.dwDebugEventCode)        {            OnCreateProcessDebugEvent(&de);        }        // 异常事件        else if (EXCEPTION_DEBUG_EVENT == de.dwDebugEventCode)        {            if (OnExceptionDebugEvent(&de))                continue;        }        // 被调试进程终止事件        else if (EXIT_PROCESS_DEBUG_EVENT == de.dwDebugEventCode)        {            // 被调试者终止---调试器终止            break;        }         // 再次运行被调试者        ContinueDebugEvent(de.dwProcessId, de.dwThreadId, dwContinueStatus);    }}
BOOL WaitForDebugEvent(  LPDEBUG_EVENT lpDebugEvent,  DWORD         dwMilliseconds);//等待正在调试的进程中发生调试事件。 Parameters    lpDebugEvent    指向接收调试事件信息的DEBUG_EVENT结构的指针      dwMilliseconds    等待调试事件的毫秒数。如果此参数为零,则该函数测试调试事件并立即返回。如果参数为 INFINITE,则函数在调试事件发生之前不会返回。
DesbugLoop()函数的工作原理类似窗口过程函数(WndProc),它从被调试者处接收事件并处理,然后使被调试者继续运行。DebugLoop()函数代码比较简单,结合代码中的注释就能理解。
 
DEBUG_EVENT 结构体定义
typedef struct _DEBUG_EVENT{    DWORD dwDebugEventCode;    DWORD dwProcessId;    DWORD dwThreadId;    union{        EXCEPTION_DEBUG_INFO        Exception;        CREATE_THREAD_DEBUG_INFO    CreateThread;        CREATE_PROCESS_DEBUG_INFO   CreateProcess;        EXIT_THREAD_DEBUG_INGO      ExitThread;        EXIT_PROCESS_DEBUG_INFO     ExitProcess;        LOAD_DLL_DDEBUG_INFO        LoadDll;        UNLOAD_DLL_DEBUG_INFO       UnloadDll;        OUTPUT_DEBUG_STRING_INFO    DebugString;        RIP_INFO                    Ripinfo    } u;} DEBUG_EVENT,*LPDEBUG_EVENT;

前面提到了共有9种调试事件。DEBUG_EVENT.dwDebugEventCode成员会被设置为九种事件中的一种,根据相关事件的种类,也会设置适当的`DEBUG_EVENT.u(union)成员(DEBUG_EVENT.u共用体成员内部也有九个结构体组成,它们对应事件种类的个数)。
 
提示
 
例如:如果发生异常事件时,dwDebugEventCode 成员会被设置为 EXCEPTION_DEBUG_EVENT , u.Exception 结构体也会得到设置。
 
ContinueDebugEvent() API 是一个被调试者继续运行的函数。
BOOL WINAPI ContinueDebugEvent(    DWORD dwProcessId,    DWORD dwThreadId,    DWORD dwContinueStatus);

ContinueDebugEvent() API 的最后一个参数 dwContinueStatus 的值为 DGBG_CONTINUE 或 DBG_EXCEPTION_NOT_HANDLED 。
 
若处理正常,则其值设置为 DBG_CONTINUE ;若无法处理,或希望在应用程序的 SEH 中 处理,则其值为 DBG_EXCEPTION_NOT_HANDLED 。
 
提示
 
SEH 时 Windows提供的异常处理机制。
 
DebugLoop()函数中处理3中调试事件,如下所示:
  • EXIT_PROCESS_DEBUG_EVENT
  • CREATE_PROCESS_DEBUG_EVENT
  • EXCEPTION_DEBUG_EVENT

EXIT_PPPROCESS_DEBUG_EVENT

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

CREATE_PROCESS_DEBUG_EVENT-OnCreateProcessDebugEvent()

OnCreateProcessDebugEvent()是 CREATE_PROCESS_DEBUG_EVENT 事件句柄,被调试进程启动(或者附加)时即调用执行该函数。
BOOL OnCreateProcessDebugEvent(LPDEBUG_EVENT pde){    // 获取WriteFile() API 地址    g_pfWriteFile = GetProcAddress(GetModuleHandleA("kernel32.dll"), "WriteFile");     // API Hook - WriteFile()    //   更改第一个字节为 0xCC(INT3)    //   originalbyte 是 g_ch0rgByte 备份    memcpy(&g_cpdi, &pde->u.CreateProcessInfo, sizeof(CREATE_PROCESS_DEBUG_INFO));    ReadProcessMemory(g_cpdi.hProcess, g_pfWriteFile,        &g_chOrgByte, sizeof(BYTE), NULL);    WriteProcessMemory(g_cpdi.hProcess, g_pfWriteFile,        &g_chINT3, sizeof(BYTE), NULL);     return TRUE;}

首先获取 WriteFile() API 的起始地址,需要注意,它获取的并不是被调试进程的内存地址,而是调试进程的内存地址。对于 windows OS 的系统 DLL 而言,它们在所有进程中都会加载到相同地址(虚拟内存),所以上面这样做是没有任何问题的。
 
g_cpdi 是 CREATE_PROCESS_DEBUG_INFO 结构体变量。
typedef struct _CREATE_PROCESS_DEBUG_INFO{    HANDLE                  hFile;    HANDLE                  hProcess;    HANDLE                  hThread;    LPVOID                  lpBaseOfImage;    DWORD                   dwDebugInfoFileOffset;    DWORD                   nDebugInfoSize;    LPVOID                  lpThreadLocalBase;    LPTHREAD_START_ROUTINE  lpStartAddress;    LPVOID                  lpImageName;    WORD                    fUnicode;} CREATE_PROCESS_DEBUG_INFO, *LPCREATE_PROCESS_DEBUG_INFO;

通过 CREATE_PROCESS_DEBUG_INFO 结构体的 hProcess 成员(被调试进程的句柄),可以钩取 WriteFile() API (不适用调试方法时,可以使用 OpenProcess() API 获取相应进程的句柄)。调试方法中,钩取的方法非常简单。
 
只要在 API 的起始位置好断点即可。由于调试器拥有被调试进程的句柄(带有调式权限),所以可以使用 ReadProcessMemory()、WriteProcessMemry() API 对调试进程的内存空间自由进行读写操作。用上面的函数可以向被调试者设置断点(INT3 0xCC)。通过 ReadProcessMemory() 读取 WriteFile() API 的第一个字节,并将其存储到 g_chOrgByte 变量。
 
g_chOrgByte 变量中存储的是 WriteFile() API 的第一个字节,后面“脱钩”时会用到。然后使用 WriteProcessMemory() API 将 WritFile() API 的第一个字节更改为 0xCC。
 
0xCC 时 IA-32 指令,对应于 INT3 指令,也就是断点。CPU 遇到 INT3 指令时会暂停执行程序,并触发异常 。若相应程序正处于调试中,则控制权转移到调试器,由调试器处理。这也是一般调试器设置断点的原理。
 
这样一来,被调试进程调用 WriteFile() API 时,控制权都会转移给调试器。

EXCEPTION_DEBUG_EVENT-OnExceptionDebugEvent()

OnExceptionDebugEvent()时EXCEPTION_DEBUG_EVENT事件句柄,它处理被调试者的 INT3 指令。
BOOL OnExceptionDebugEvent(LPDEBUG_EVENT pde){    CONTEXT ctx;    PBYTE lpBuffer = NULL;    DWORD dwNumOfBytesToWrite, dwAddrOfBuffer, i;    PEXCEPTION_RECORD per = &pde->u.Exception.ExceptionRecord;     // 是断点异常(INT3)时    if (EXCEPTION_BREAKPOINT == per->ExceptionCode)    {        // 断点地址为 WriteFile() API 地址时        if (g_pfWriteFile == per->ExceptionAddress)        {            // #1. Unhook            //   将0xCC 恢复为 original byte            WriteProcessMemory(g_cpdi.hProcess, g_pfWriteFile,                &g_chOrgByte, sizeof(BYTE), NULL);             // #2. 获取线程上下文            ctx.ContextFlags = CONTEXT_CONTROL;            GetThreadContext(g_cpdi.hThread, &ctx);             // #3. 获取WriteFil() 的 param 2、3 值            //   函数参数存在于相应进程的栈            //   param 2 : ESP + 0x8            //   param 3 : ESP + 0xC            ReadProcessMemory(g_cpdi.hProcess, (LPVOID)(ctx.Esp + 0x8),                &dwAddrOfBuffer, sizeof(DWORD), NULL);            ReadProcessMemory(g_cpdi.hProcess, (LPVOID)(ctx.Esp + 0xC),                &dwNumOfBytesToWrite, sizeof(DWORD), NULL);             // #4. 分配领事缓冲区            lpBuffer = (PBYTE)malloc(dwNumOfBytesToWrite + 1);            memset(lpBuffer, 0, dwNumOfBytesToWrite + 1);             // #5. 复制 WriteFile() 缓冲区到临时缓冲区            ReadProcessMemory(g_cpdi.hProcess, (LPVOID)dwAddrOfBuffer,                lpBuffer, dwNumOfBytesToWrite, NULL);            printf("\n### original string ###\n%s\n", lpBuffer);             // #6. 将小写字母转换为大写字母            for (i = 0; i < dwNumOfBytesToWrite; i++)            {                if (0x61 <= lpBuffer[i] && lpBuffer[i] <= 0x7A)                    lpBuffer[i] -= 0x20;            }             printf("\n### converted string ###\n%s\n", lpBuffer);             // #7. 将变换后的缓冲区复制到WriteFile()缓冲区            WriteProcessMemory(g_cpdi.hProcess, (LPVOID)dwAddrOfBuffer,                lpBuffer, dwNumOfBytesToWrite, NULL);             // #8.释放临时缓冲区            free(lpBuffer);             // #9. 将线程上下文的 EIP 更改为 WriteFile()首地址            //   当前为 WriteFile()+1 位置,INT3命令之后            ctx.Eip = (DWORD)g_pfWriteFile;            SetThreadContext(g_cpdi.hThread, &ctx);             // #10. 运行被调试进程            ContinueDebugEvent(pde->dwProcessId, pde->dwThreadId, DBG_CONTINUE);            Sleep(0);             // #11. API Hook            WriteProcessMemory(g_cpdi.hProcess, g_pfWriteFile,                &g_chINT3, sizeof(BYTE), NULL);             return TRUE;        }    }     return FALSE;}

首先,if 语句用于检测异常是否为 EXCEPTION_BREAKPOINT 异常(除此之外,还有大约19中异常)。然后用 if 语句检测发生断点的地址是否与 kernel32.dll!WriteFile() 的起始地址一致(OnCreateProcessDebugEvent()已经实现获取了 WriteFile()的起始地址)。若满足条件,则继续执行以下代码。
“脱钩”(删除 API 钩子)
//将 0xCC 恢复为original byteWriteProcessMemory( g_cpdi, hProcess, g_pfWriteFile,&g_chOrgByte, sizeof(BYTE), NULL)

首先需要“脱钩”(删除 API 钩子),因为在将小写字母转换为大写字母后需要正常调用 WriteFile() 函数。类似“钩子”、“脱钩”的方法也非常简单,只要将0xCC 恢复原值(g_chOrgByte)即可。
 
提示
 
可以根据实际需要取消对相关 API 的调用,也可以调用用户自定义的 MyWriteFile() 函数,所以“脱钩”不是必须的,要根据具体情况灵活选择处理方法。
获取上下文(Thread Context)
这是一次提到“线程上下文”,所有程序在内存中都以进程为单位运行,而进程的实际指令代码以线程为单位运行。Windows OS 是一个多线程(multi-thread)操作系统,统一进程中可以同时运行多个线程。
多任务(multi-tasking)是将 CPU 资源划分为多个时间片(time-slice),然后平等地逐一运行所有线程(考虑线程优先级)。CPU 运行完一个线程的时间片而切换到其他线程时间片时,它必须将先前线程处理的内容准确备份下来,这样再次运行它时才能正常无误。
 
再次运行先前线程时,必须有运行所需信息,这些重要信息指的就是 CPU 中各寄存器的值。通过这些值,才能保证 CPU 能够再次准确运行它(内存信息栈&堆存在于相应进程的虚拟空间,不需要另外保护)。负责保存线程 CPU 寄存器信息的就是 CONTEXT 结构体(每个线程都对应一个 CONTEXT结构体),它的定义如下:
typedef struct _CONTEXT{    DWORD ContextFlags;     DWORD Dr0;    DWORD Dr1;    DWORD Dr2;    DWORD Dr3;    DWORD Dr6;    DWORD Dr7;     FLOADTING_SAVE_AREA  FloatSave;     DWORD SegGs;    DWORD SegFs;    DWORD SegEs;    DWORD SegDs;     DWORD Edi;    DWORD Esi;    DWORD Ebx;    DWORD Edx;    DWORD Ecx;    DWORD Eax;     DWORD Ebp;    DWORD Eip;    DWORD SegCs;    DWORD EFlags;    DWORD Esp;    DWORD SegSs;     byte ExtendedRegisters[MAXIMUM_SUPPORTED_EXTENSION];} CONTEXT;

下面是获取线程上下文的代码:
//获取线程上下文ctx.ContextFlags = CONTEXT_CONTROL;GetThreadContext( g_cpdi,hThread, &ctx);

像这样调用 GetThreadContext() API ,即可将指定现线程(g_cpdi.hThread)的 CONTEXT 存储到 ctx 结构体变量(g_cpdi.hThread 是被调试者的注线程句柄):
BOOL WINAPI GetThreadContext(    HANDLE      hTread,    LPCONTEXT   lpContext);

获取 WriteFile() 的 param 2、3 值
调用 WriteFile() 函数时,我们要在传递过来的参数中知道 param2(数据缓冲区地址)与 param3(缓冲区大小)这2个参数。函数参数存储在栈中,通过线程上下文获取的 CONTEXT.Esp 成员可以分别获得它们的值。
//函数参数存在与相应进程栈//param 2 : ESP + 0x8//param 3 : ESP + 0xC ReadProcessMemory(g_cpdi.Process,(LPVOID)(ctx.Esp + 0x8),            &dwAddrOfBuffer, sizeof(DWORD), NULL); RradProcessMemory(g_cpdi.hProcess,(LPVOID)(ctx.Esp + 0xC),            &dwNumOfBytesToWrite, sizeof(DWORD), NULL);

提示
  • 存储在 dwAddrOfBuffer 中的数据缓冲区地址是被调试者(notepad.exe)虚拟内存空间中的地址。
  • param 2 与param 3 分别为 ESP + 0x8、ESP + 0xC。
把小写字母转换为大写字母后覆写 WriteFile() 缓冲区
获取数据缓冲区的地址与大小后,将其内容读到调试器的内存空间,把小写字母转换为大写字母。然后将修改后的大写字母覆写到原来的位置(被调试者的虚拟内存)。代码如下:
//分配临时缓冲区lpBuffer = (PBYTE)malloc(dwNumOfBytestoWrite + 1);memset(lpBuffer, 0, dwNumOfBytesToWrite +1 ); //复制 WriteFile() 缓冲区到临时缓冲区ReadProcessMemory( g_cpdi.hProcess, (LPVOID)dwAddrOfBuffer, lpBuffer.                    dwNumberOfBytesToWrite, NULL);printf("\n### oriiginal string : %s\n", lpBuffer);  //将小写字母转换为大写字母for(i = 0; i < dwNumberOfBytesToWrite; i++){    if( 0x61 <= lpBuffer[i] && lpBuffer[i] <= 0x7A)        lpBuffer[i] -= 0x20;}printf("\n### converted string : %s\n", lpBuffer); //将变换后的缓冲区复制到 WriteFile()缓冲区WriteProcessMemory(g_cpdi.hProcess, (LPVOID)dwAddrOfBuffer,                    lpBuffer, dwNumberOfBytesToWrite, NULL); //释放缓冲区free(lpBuffer);

把线程上下文的 EIP 修改为 WriteFile() 起始地址
将获取的 CONTEXT 结构体的 Eip 成员修改为 WriteFile() 的起始地址。EIP 的当前地址为 WriteFile()+1。
 
修改好 CONTEXT.Eip成员之后,调用 SetThreadContext() API
//当前 WriteFie() + 1 位置,INT3命令之后。ctx.Eip = (DWORD)g_pfWriteFile;SetThreadContext(g_cpdi.hThread, &ctx);

SetThreadContext() API
SetThreadContext(    HANDLE hThread,    const CONTEXT *lpContext);

运行调试进程
调用 ContinueDebugEvent() API 可以重启被调试的进程,使之继续运行。由于之前已经将CONTEXT.Eip 修改为 WriteFile() 的起始地址,所以会调用执行 WriteFile()。
ContinueDebugEvent(pde -> dwProcessId, pde->dwThreadId, DBG_CONTINUE);sleep(0);
设置 API “钩子”
最后设置 API “钩子”,方便下次钩取操作(若略去该操作,WritteFile() API 钩取将完全处于“脱钩”状态)。
WriteProcessMemory(g_cpdi.hProcess, g_pfWriteFile, &g_chINT3,                    sizeof(BYTE), NULL);

建议
 
建议在实际的代码调试过程中分别查看各种结构体的值,经过几次调试之后,相信大家都能掌握程序的执行流程。

效果图

启动调试程序并打开记事本输入相关内容:
保存文件,并查看新文件。

 

看雪ID:Tray_PG

https://bbs.pediy.com/user-home-879928.htm

*本文由看雪论坛 Tray_PG 原创,转载请注明来自看雪社区

# 往期推荐

1. 极为详细:双重释放漏洞调试分析

2. 新人PWN入坑总结

3.OD插件 - 支持chm帮助文档

4. Galgame汉化中的逆向:ArmArm64_ELF中汉化字符串超长修改方法

5. FartExt之优化更深主动调用的FART10

6. V8利用初探 2019 StarCTF oob 复现分析

公众号ID:ikanxue
官方微博:看雪安全
商务合作:[email protected]

球分享

球点赞

球在看

点击“阅读原文”,了解更多!


文章来源: http://mp.weixin.qq.com/s?__biz=MjM5NTc2MDYxMw==&mid=2458391973&idx=2&sn=f3266747e27f8513ec19dff9541cd8ec&chksm=b18f252f86f8ac39dfdc1fcaa4e732c43429e469a77cb4f8aa8fa93b2580ca5142804f35607d#rd
如有侵权请联系:admin#unsafe.sh