首页
论坛
课程
招聘
[分享][分享]API 钩取:逆向分析之“花”
2021-8-17 18:22 8893

[分享][分享]API 钩取:逆向分析之“花”

2021-8-17 18:22
8893

API 钩取:逆向分析之“花”

  1. 跟着李承远的逆向工程核心原理边学边做的。
  2. 发这个贴子的目的是为了鼓励自己坚持下去,毕竟才刚刚起步。
  3. 希望大家共同进步。
    目录

    钩取

代码逆向分析中,钩取(Hooking)是一种截取信息、更改程序执行流向、添加新功能的技术。钩取的整个流程如下:

  • 使用反汇编器/调试器把握程序的结构与工作原理。
  • 开发需要的“钩子”代码,用于修改 bug 、改善程序功能
  • 灵活操作可执行文件与进程内存,设置“钩子”代码。

上述这一系列的工作就是代码逆行分析工程的核心(Core)内容,所有“钩取”被称为“逆向分析之花”。

 

钩取多种多样,其中钩取 Win32 API 的技术被称为 API 钩取。它与消息钩取共同广泛应用于用户模式。 API 钩取是一种应用范围非常广泛的技术。。

API

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 是必须库。

 

1.jpg

 

假设 notepad.exe 要打开 c:\abc.txt 文件,首先在程序代码中调用 msvcrt!fopen() API ,然后引发一系列的 API 调用,如下:

1
2
3
4
5
6
- msvcrt ! fopen()
    kernel32 ! CreateFileW()
        ntdll ! ZwCreateFile()
            ntdll ! KiFastSystemCall()
                SYSENTRY        // IA-32 Instruction
                    ——> 进入内核模式

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

API 钩取

通过 API 钩取技术可以实现对某些 Win32 API 调用过程的拦截,并获得相应的控制权限。使用 API 钩取技术的优势如下:

  • 在 API 调用前/后运行用户的“钩子”代码。
  • 查看或操作传递给 API 的参数或 API 函数的返回时。
  • 取消对 API 的调用,或者更改执行流程,运行用户代码。

正常调用 API

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

 

2.jpg

钩取API调用

下图描述的是 kernel32!CreateFile()调用情形。用户先使用 DLL 注入技术将 hook.dll 注入目标进程的内存空间,然后用 hook!MyCreateFile() 钩取对 kernel32.dll!CreateFile()的调用(有多种方法可以设置钩取函数)。这样,每当目标进程要调用kernel32!CreateFile() API 时都会先调用 hook!MyCreateFile()。

 

3.jpg

 

钩取某函数的目的有很多,如调用它之前或之后运行用户代码,或者干脆阻止它调用执行等。实际操作中只要根据自身需要灵活运用该技术即可。这也是 API 钩取的基本理念。

 

实现 API 钩取的方法多种多样,但钩取的基本概念是不变的。只要掌握了上面的概念,就能很容易得理解后面得具体实现方法。

技术图标

下图是一张技术图标 (Tech Map),涵盖了 API 钩取得所有技术内容。

 

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

 

4.jpg

对象

首先时关于 API 钩取方法(Method)的分类,根据针对的对象(Object)不同,API 钩取方法大致可以分为静态方法与动态方法。

 

静态方法针对的时“文件”,而动态方法针对的是进程内存,一般 API 钩取技术指动态方法,当然在某些非常特殊的情况下也可以使用静态方法。如下

 

5.jpg

 

:静态方法在 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等)

记事本WriteFile() API 钩取

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

技术图表 - 调试技术

下面是调试方式的 API 钩取技术

 

6.jpg

 

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

关于调试器

术语

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

调试器功能

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

调试器的工作原理

调试进程经过注册后,每当被挑事者发生调试事件(Debug Event)时,OS 就会暂停其运行,并向调试器报告相应事件。调试器对相应事件做适当处理后,时被调试者继续运行。

  • 一般的异常(Exception)也属于调试事件。
  • 若相应进程处于非调试,调试事件会在其自身的异常处理或 OS 的异常处理机制中被处理掉。
  • 调试器无法处理或不关心的调试事件最终由 OS 处理

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

 

7.jpg

调试事件

各种调试事件整理如下:

1
2
3
4
5
6
7
8
9
EXCEPTION_DEBUG_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,下面是与相关的对应异常列表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
EXCEPTION_ACCESS_VIOLATION
EXCEPTION_ARRAY_BOUNDS_EXCEEDED
EXCEPTION_BREAKPOINT
EXCEPTION_DATATYPE_MISALIGNMENT
EXCEPTION_FLT_DENORMAL_OPERAND
EXCEPTION_ELT_DIVIDE_BY_ZERO
EXCEPTION_ELT_INEXACT_RESULT
EXCEPTION_ELT_INVALID_OPERATION
EXCEPTION_ELT_OVERFLOW
EXCEPTION_ELT_STACK_CHECK
EXCEPTION_ELT_UNDERFLOW
EXCEPTION_ILLEGAL_INSTRUCTION
EXCEPTION_IN_PAGE_ERROR
EXCEPTION_INT_DIVIDE_BY_ZERO
EXCEPTION_INT_OVERFLOW
EXCEPTION_INVALID_DISPOSITION
EXCEPTION_NONCONTINUABLE_EXCEPTION
EXCEPTION_PRIV_INSTRUCTION
EXCEPTION_SINGLE_STEP
EXCEPTION_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 ;可以只钩取一次,也可以钩取多次。实际应用时,根据需要适当调整即可。

练习

实验目标

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

工作原理

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

WriteFile()定义如下:

1
2
3
4
5
6
7
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 处设置断点,如下所示

 

8.jpg

 

按 F9 键运行程序。在记事本中输入文本后,以合适的文件名保存,如下所示

 

9.jpg

 

在OllyDbg代码窗口可以看见,调式器在 kernel32!WriteFile() 处(设有断点)暂停,然后查看进程栈,如下所示:

 

10.jpg

 

当前栈(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 的源代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
#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()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#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;  
}
1
2
3
4
5
6
7
8
9
10
BOOL DebugActiveProcess(
  DWORD dwProcessId
);//使调试器能够附加到活动进程并对其进行调试。
 
 
Parameters
    dwProcessId
    //要调试的进程的标识符。
    //调试器被授予对进程的调试访问权限,
    //就像它使用DEBUG_ONLY_THIS_PROCESS标志创建进程一样。

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

 

然后进入DebugLoop()函数,处理来自被调试者的调试信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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);
    }
}
1
2
3
4
5
6
7
8
9
10
11
BOOL WaitForDebugEvent(
  LPDEBUG_EVENT lpDebugEvent,
  DWORD         dwMilliseconds
);//等待正在调试的进程中发生调试事件。
 
Parameters
    lpDebugEvent
    指向接收调试事件信息的DEBUG_EVENT结构的指针 。
 
    dwMilliseconds
    等待调试事件的毫秒数。如果此参数为零,则该函数测试调试事件并立即返回。如果参数为 INFINITE,则函数在调试事件发生之前不会返回。

DesbugLoop()函数的工作原理类似窗口过程函数(WndProc),它从被调试者处接收事件并处理,然后使被调试者继续运行。DebugLoop()函数代码比较简单,结合代码中的注释就能理解。

 

DEBUG_EVENT 结构体定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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 是一个被调试者继续运行的函数。

1
2
3
4
5
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 事件句柄,被调试进程启动(或者附加)时即调用执行该函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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 结构体变量。

1
2
3
4
5
6
7
8
9
10
11
12
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 指令,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
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 钩子)
1
2
//0xCC 恢复为original byte
WriteProcessMemory( 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结构体),它的定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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;

下面是获取线程上下文的代码

1
2
3
//获取线程上下文
ctx.ContextFlags = CONTEXT_CONTROL;
GetThreadContext( g_cpdi,hThread, &ctx);

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

1
2
3
4
BOOL WINAPI GetThreadContext(
    HANDLE      hTread,
    LPCONTEXT   lpContext
);
获取 WriteFile() 的 param 2、3 值

调用 WriteFile() 函数时,我们要在传递过来的参数中知道 param2(数据缓冲区地址)与 param3(缓冲区大小)这2个参数。函数参数存储在栈中,通过线程上下文获取的 CONTEXT.Esp 成员可以分别获得它们的值。

1
2
3
4
5
6
7
8
9
//函数参数存在与相应进程栈
//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() 缓冲区

获取数据缓冲区的地址与大小后,将其内容读到调试器的内存空间,把小写字母转换为大写字母。然后将修改后的大写字母覆写到原来的位置(被调试者的虚拟内存)。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//分配临时缓冲区
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

1
2
3
//当前 WriteFie() + 1 位置,INT3命令之后。
ctx.Eip = (DWORD)g_pfWriteFile;
SetThreadContext(g_cpdi.hThread, &ctx);

SetThreadContext() API

1
2
3
4
SetThreadContext(
    HANDLE hThread,
    const CONTEXT *lpContext
);
运行调试进程

调用 ContinueDebugEvent() API 可以重启被调试的进程,使之继续运行。 由于 之前已经将CONTEXT.Eip 修改为 WriteFile() 的起始地址,所以会调用执行 WriteFile().

1
2
ContinueDebugEvent(pde -> dwProcessId, pde->dwThreadId, DBG_CONTINUE);
sleep(0);
设置 API “钩子”

最后设置 API “钩子”,方便下次钩取操作(若略去该操作,WritteFile() API 钩取将完全处于“脱钩”状态)。

1
2
WriteProcessMemory(g_cpdi.hProcess, g_pfWriteFile, &g_chINT3,
                    sizeof(BYTE), NULL);

建议

 

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

效果图

  1. 启动调试程序并打开记事本输入相关内容

11.jpg

  1. 保存文件,并查看新文件。

12.jpg


[注意] 欢迎加入看雪团队!base上海,招聘CTF安全工程师,将兴趣和工作融合在一起!看雪20年安全圈的口碑,助你快速成长!

最后于 2021-8-18 14:14 被Tray_PG编辑 ,原因: 原帖中由错别字,更改一下
收藏
点赞3
打赏
分享
最新回复 (2)
雪    币: 5308
活跃值: 活跃值 (801)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
Willarcap 活跃值 2021-8-17 20:23
2
1
感谢分享
雪    币: 682
活跃值: 活跃值 (732)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
Oday小斯 活跃值 2021-8-18 22:06
3
0
感谢分享
游客
登录 | 注册 方可回帖
返回