逆向分析之“花”:
钩取:
钩取是一种截取信息、更改程序执行流向、添加新功能的技术。钩取的整个流程:
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)作用:释放当前线程剩余时间片。