0%

API钩取

逆向分析之“花”:

钩取:

钩取是一种截取信息、更改程序执行流向、添加新功能的技术。钩取的整个流程:

1、使用反汇编器/调试器把握程序的结构与工作原理;

2、开发需要的“钩子”代码,用于修复BUG、改善程序功能;

3、灵活操作可执行文件与进程内存,设置“钩子”代码。

API:

应用程序编程接口。对于系统资源(内存、文件、网络、视频、音频等),处于稳定性,windows os禁止用户直接访问。用户使用他们时,必须向系统申请,申请的方法是使用微软提供的win32API。

用户模式的应用程序访问系统资源,由ntdll.dll向内核模式提出访问申请。

示例:向notepad.exe打开c:\abc.txt:

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

API钩取:

使用API钩取技术的优势:

1、在API调用前/后运行用户的“钩子”代码

2、查看或者操作传递给API的参数或API函数的返回值

3、取消对API的调用,或更改执行流,运行用户代码

钩取API调用:

先将dll文件注入目标内存空间,然后用hook!MyCreateFile()钩取对kernel32!CreateFile()的调用。这样,每当进程调用CreateFile()API时就会先调用MyCreateFile()

技术图表:

方法对象:

根据针对对象不同,API勾取方法大致分为静态方法和动态方法。

一般API钩取指动态。

静态:文件对象 程序运行前钩取 只需要最初钩取一次 用于特殊情况 不可脱勾

动态:内存对象 程序运行后钩取 每次运行时都要钩取 常规的钩取方法 程序运行时可以脱钩

位置:

技术图标用于指出钩取那一部分呢

IAT

IAT将内部的API地址更改为钩取函数地址。

优点:实现简单

缺点:无法钩取不在IAT而在程序中使用的API(如:动态加载并使用DLL时)

代码:

系统库映射到进程内存时,从中查找API的实际地址,并直接修改代码,具体的几种选择如下:

1、使用JMP指令修改起始代码;

2、覆写函数局部;

3、仅更改必须部分的局部

EAT:

记录在DLL的EAT中的API起始地址更改为钩取函数地址

技术:

调试:

优点:调试器拥有被调试者的所有权限,在钩取API过程中,用户可以暂停程序运行,进行添加、修改。删除API钩取等操作

不足:需大量测试以保证行为的稳定性

注入:

DLL注入:

驱使目标进程强制加载用户指定的DLL文件。使用该技术,要先注入DLL中创建钩取代码与设置代码,然后再DllMain()中调用设置代码,注入的同时即可完成API钩取

代码注入:

在执行代码与数据被注入的状态下直接获取自身所需API地址来使用。

除了上面列出的API,访问其他进程内存时也常常使用OpenProcess()、WriteProcessMemory()、ReadProcessMemory()等API。

记事本WriteFile()API钩取:

调试技术:

能够进行与用户更具交互性的勾取操作。这种技术会向用户提供简单的接口,使用户能够控制目标进程的运行,并且可以自由使用进程内存。

关于调试器的说明:

工作原理:

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

1.一般的异常也属于调试事件。

2、若相应进程处于非调试,调试事件会在其自身的异常处理或OS的异常处理机制中被处理掉。

3、调试器无法处理或不关心的调试事件最终由OS处理

调试事件:

关于调试事件与异常列表这里就不列出来了

调试器必须处理的是EXCEPTION_BREAKPOINT异常。断点对应的指令INT3,IA-32指令为0xCC。

调试器实现断点的方法很简单,找到要设置断点的代码在内存中的起始地址,只要把一个字节修改为0xCC。

调试技术流程:

1、对想钩取的进程进行附加操作,使之成为被调试者

2、“钩子”:将API起始地址的第一个字节修改为0xCC

3、调试相应API时,控制权转移到调试器

4、执行需要的操作(操作参数、返回值等)

5、脱钩:将0xCC恢复原值(为了正常运行API)

6、运行相应API(无0xCC的正常状态)

7、“钩子”:再次修改为0xCC(为了继续钩取)

8、控制权返还被调试者

工作原理:

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

栈:

WriteFile()定义:

1
2
3
4
5
6
BOOL WriteFile(
HANDLE hFile,
LPCVOID lpBuffer, //数据缓冲区指针
DWORD nNUmberofBytesWritten, //要写的字节数
LPOVERLAPPED lpOverlapped
);

在kernel32!WriteFile()API设置断点,当在记事本输入信息时,调试器会在断点处暂停,直接转到数据缓冲区地址处,可以看到记事本保存的字符串,钩取WriteFile()API,用指定字符串覆盖数据缓存区中的字符串即可。

执行流:

在WriteFile()API的起始地址设置了断点,被调试者内部调用WriteFile()时,会在起始地址处遇到INT3指令。执行该指令时,EIP会增加一个字节。然后控制权转移给调试器(因为在“调试器 - 被调试器”关系中,被调试者中发生的 EXCEPTION_BREAKPOINT异常需要由调试器处理)。修改覆写了数据缓存区的内容后,EIP值被重新更改为WriteFile()API的起始地址,继续运行。

“脱钩”与“钩子”:

若只将执行流返回到WriteFile()起始地址,再遇到相同的INT3指令时,就会陷入无限循环,。所以需要除去设置再WriteFile()API起始地址的断点,将0xCC改为original byte。这一操作称为脱钩。

源代码分析:

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
#include "windows.h"
#include "stdio.h"

LPVOID g_pfWriteFile = NULL;
CREATE_PROCESS_DEBUG_INFO g_cpdi;
BYTE g_chINT3 = 0xCC, g_chOrgByte = 0;
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;
}

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
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);
}

WaitForDebugEvent()API是等待被调试者发送调试事件的函数

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

发生调试事件,WaitForDebugEvent()API就会将相关事件信息设置到第一个参数的变量(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 CreateProcessInfo;
EXIT_THREAD_DEBUG_INFO ExitThread;
EXIT_PROCESS_DEBUG_INFO ExitProcess;
LOAD_DLL_DEBUG_INFO LoadDll;
UNLOAD_DLL_DEBUG_INFO UnloadDll;
OUTPUT_DEBUG_STRING_INFO DebugString;
RIP_INFO RipInfo;
} u;
} DEBUG_EVENT, *LPDEBUG_EVENT;

前面讲过调试事件。共有9种。DEBUG_EVENT.dwDebugEventCode成员会被设为其中的一种。

ContinueDebugEvent()API是一个被调试者继续运行的函数。

1
2
3
4
5
BOOL WINAPI ContinueDebugEvent(
DWORD dwProcessId,
DWORD dwThreadId,
DWORD dwContinueStatus
);

最后一个参数,如果处理正常,其值设为DBG_CONTINUE;若无法处理,或希望在应用程序SEH中处理,其值为DBG_EXCEPTION_NOT_HANDLED。

DebugLoop()函数的三种调试事件:

EXIT_PROCESS_DEBUG_EVENT:

被调试进程终止时会触发该事件。

CREATE_PROCESS_DEBUG_EVENT-OnCreateProcessDebugEvent():

OnCreateProcessDebugEvent():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
BOOL OnCreateProcessDebugEvent(LPDEBUG_EVENT pde)
{
// 获取WriteFile() API
g_pfWriteFile = GetProcAddress(GetModuleHandleA("kernel32.dll"), "WriteFile");
// API Hook - WriteFile()
// 更改第一个字节为0xCC
// orginalbyte是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;
}

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;

通过hProcess,可以钩取WriteFile()API(不使用调试,也可以用OpenProcess获取目标进程句柄)。

EXCEPTION_DEBUG_EVENT-OnExceptionDebugEvent():

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;

// BreakPoint exception
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. 获取Thread Context
ctx.ContextFlags = CONTEXT_CONTROL;
GetThreadContext(g_cpdi.hThread, &ctx);

// #3. WriteFile()的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. Thread Context的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;
}

把代码看完,应该就能明白了。

sleep(0)作用:释放当前线程剩余时间片。