本文详细的介绍了在Visual Studio(以下简称VS)下实现API钩子的编程方法,阅读本文需要基础:有操作系统的基本知识(进程管理,内存管理),会在VS下编写和调试Win32应用程序和动态链接库(以下简称DLL)。
API钩子是一种高级编程技巧,常常用来完成一些特别的功能,比如词典软件的屏幕取词,游戏修改软件的数据修改等。当然,此技术更多的是被黑客或是病毒用来攻击其它程序,截获需要的数据或改变目标程序的行为。本文不探讨此技术的应用,只讲实现。同时希望掌握此技术的人都能够合法的应用它,不要去做危险或违法的事情,害人害己。
一、原理
每一个程序在操作系统中运行,都必须调用操作系统提供的函数——也就是API(应用程序编程接口)——来实现程序的各种功能。在Windows操作系统下,API就是那几千个系统函数。在有些程序中并没直接调用API的代码,比如下面的程序:
1 2 3 4 5 6 7 | #include <iostream> using namespace std; int main( void ) { cout << "Hello World!" << endl; return 0; } |
事实上,cout对象的内部处理函数已经替你调用API。就算你的main函数是空的,里面什么代码都不写,只要程序被操作系统启动,也会调用一些基本的API,比如LoadLibrary。这个函数是用来加载DLL的,也就是在进程运行的过程中,把DLL中的程序指令和数据读入当前进程并执行启动代码,我们后面会用到这个函数。
如果能够设法用自定义函数替换宿主进程调用的目标API函数,那么就可以截获宿主进程传入目标API的参数,并可以改变宿主进程的行为。但要想修改目标API函数必须先查找并打开宿主进程,并让自定义代码能在宿主进程中运行。因此挂API钩子分为四步:1. 查找并打开宿主进程,2. 将注入体装入宿主进程中运行,3. 用伪装函数替换目标API,4. 执行伪装函数。整个程序也分为两部分,一部分是负责查找并打开宿主进程和注入代码的应用程序,另一部分是包含修改代码和伪装函数的注入体。
二、查找指定的进程
查找指定的进程有很多方法,下面简单的介绍三种:
1. 找到鼠标所指窗体的进程句柄
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | DWORD GetProcIDFromCursor( void ) { //Get current mouse cursor position POINT ptCursor; if (!GetCursorPos(&ptCursor)) { cout << "GetCursorPos Error: " << GetLastError() << endl; return 0; } //Get window handle from cursor postion HWND hWnd = WindowFromPoint(&ptCursor); if (NULL == hWnd) { cout << "No window exists at the given point!" << endl; return 0; } //Get the process ID belong to the window. DWORD dwProcId; GetWindowThreadProcessId(hWnd, &dwProcId); return dwProcId; } |
2. 查找指定文件名的进程
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 | #include <Psapi.h> #pragma comment(lib, "Psapi.lib") DWORD GetProcIDFromName( LPCTSTR lpName) { DWORD aProcId[1024], dwProcCnt, dwModCnt; HMODULE hMod; TCHAR szPath[MAX_PATH]; //枚举出所有进程ID if (!EnumProcesses(aProcId, sizeof (aProcId), &dwProcCnt)) { cout << "EnumProcesses error: " << GetLastError() << endl; return 0; } //遍例所有进程 for ( DWORD i = 0; i < dwProcCnt; ++i) { //打开进程,如果没有权限打开则跳过 HANDLE hProc = OpenProcess(PROCESS_ALL_ACCESS, FALSE, aProcId[i]); if (NULL != hProc) { //打开进程的第1个Module,并检查其名称是否与目标相符 if (EnumProcessModules(hProc, &hMod, sizeof (hMod), &dwModCnt)) { GetModuleBaseName(hProc, hMod, szPath, MAX_PATH); if (0 == lstrcmpi(szPath, lpName)) { CloseHandle(hProc); return aProcId[i]; } } CloseHandle(hProc); } } return 0; } |
3. 查找其它指定信息的进程
通过CreateToolhelp32Snapshot枚举系统中正在运行的所有进程,并通过相关数据结构得到进程的信息,具体用法可以参见:
三、代码注入
上面提到过LoadLibrary可以将指定的DLL代码注入当前进程,如果能让宿主进程来执行这个函数,并把我们自己的DLL的文件名传入,那么我们的代码就可以在宿主进程中运行了。
1 2 3 | HMODULE WINAPI LoadLibrary( __in LPCTSTR lpFileName ); |
再看另一个函数:CreateRemoteThread,它可以让宿主进程新开一个线程,但是新线程的处理函数(LPTHREAD_START_ROUTINE)必须是宿主进程中的函数地址或系统API。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | HANDLE WINAPI CreateRemoteThread( __in HANDLE hProcess, __in LPSECURITY_ATTRIBUTES lpThreadAttributes, __in SIZE_T dwStackSize, __in LPTHREAD_START_ROUTINE lpStartAddress, __in LPVOID lpParameter, __in DWORD dwCreationFlags, __out LPDWORD lpThreadId ); //其中LPTHREAD_START_ROUTINE的定义如下 typedef DWORD (WINAPI *PTHREAD_START_ROUTINE)( LPVOID lpThreadParameter ); |
如果可以用让宿主进程新开一个线程,执行LoadLibrary函数,而参数是注入体DLL的文件名,就大功告成了。不过要完成这些操作,我们先来分析一下可行性。
我们知道,所有系统API函数的调用方式都是__stdcall,即参数采用从右到左的压栈方式,自己在退出时清空堆栈。这一类函数的具体调用过程如下:在调用前先由调用者将所有参数以地址或数值的形式从右向左压入栈中,然后用call指令调用该函数;进入函数后,先从栈中取出这些参数再进行运算,并在函数返回前将之前压入的栈数据全部弹出以维持栈平衡,最后用eax寄存储传递返回值(地址或数值)给调用者。这也就是说在指令层面上讲,API函数的基本调用方式都相同,然而调用者必须在栈中压入确定数量的参数,若压入的参数数量不匹配,函数内的取栈和弹栈操作将会使得栈数据错乱,最终导致程序崩溃。
通过观察发现LoadLibrary的参数数量刚好与LPTHREAD_START_ROUTINE都只有一个参数,那么如果能够获取LoadLibrary函数在宿主进程中的地址,作为lpStartAddress传入CreateRemoteThread,并将我们的注入体DLL的文件名作为lpParameter传入,那么就可以让宿主进程执行注入体代码了。为了将DLL的文件名传入宿主进程,我们还需要以下四个API:VirtualAllocEx和VirtualFreeEx可以在宿主进程中分配和释放一段内存空间;ReadProcessMemory和WriteProcessMemory可以在宿主进程中的指定内存地址读出或写入数据。
在注入的代码执行完毕后,还要完成清理工作。首先是卸载刚刚载入的DLL,需要使用另一个系统API:FreeLibrary。过程与上面的代码注入一样,使用CreateRemoteThread,将FreeLibrary的地址作为lpStartAddress参数传入。注意到FreeLibrary的参数是一个HMODULE,该句柄其实是一个Module的全局ID,一般由LoadLibrary的返回值给出。因此可以调用GetExitCodeThread获取前面执行的LoadLibrary线程的返回值,再作为CreateRemoteThread的lpParameter参数传入,这样就完成了DLL的卸载。还要记得用VirtualFreeEx释放VirtualAllocEx申请到的内存,并关闭打开的所有句柄,完成最后的清理工作。
现在注入代码的步骤就比较清晰了:
- 调用OpenProcess获取宿主进程句柄;
- 调用GetProcAddress查找LoadLibrary函数在宿主进程中的地址;
- 调用VirtualAllocEx和WriteProcessMemory将DLL文件名字符串写入宿主进程的内存;
- 调用CreateRemoteThread执行LoadLibrary在宿主进程中运行DLL;
- 调用VirtualFreeEx释放刚申请的内存;
- 调用WaitForSingleObject等待注入线程结束;
- 调用GetExitCodeThread获取前面加载的DLL的句柄;
- 调用CreateRemoveThead执行FreeLibrary卸载DLL;
- 调用CloseHandle关闭打开的所有句柄。
代码注入的所有代码整理如下。(注意:这个程序需要在win32控制台模式下编译生成一个exe文件。在控制台下运行时需要两个参数:第1个参数为宿主进程的映象名称,可以在任务管理器中查看;第2个参数为注入体DLL的完整路径文件名。程序运行后就会将指定的DLL装入指定名称的宿主进程)
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 158 159 | #include <tchar.h> #include <Windows.h> #include <atlstr.h> #include <Psapi.h> #pragma comment(lib, "Psapi.lib") #include <iostream> #include <string> using namespace std; DWORD FindProc( LPCSTR lpName) { DWORD aProcId[1024], dwProcCnt, dwModCnt; char szPath[MAX_PATH]; HMODULE hMod; //枚举出所有进程ID if (!EnumProcesses(aProcId, sizeof (aProcId), &dwProcCnt)) { //cout << "EnumProcesses error: " << GetLastError() << endl; return 0; } //遍例所有进程 for ( DWORD i = 0; i < dwProcCnt; ++i) { //打开进程,如果没有权限打开则跳过 HANDLE hProc = OpenProcess(PROCESS_ALL_ACCESS, FALSE, aProcId[i]); if (NULL != hProc) { //打开进程的第1个Module,并检查其名称是否与目标相符 if (EnumProcessModules(hProc, &hMod, sizeof (hMod), &dwModCnt)) { GetModuleBaseNameA(hProc, hMod, szPath, MAX_PATH); if (0 == _stricmp(szPath, lpName)) { CloseHandle(hProc); return aProcId[i]; } } CloseHandle(hProc); } } return 0; } //第一个参数为宿主进程的映象名称,可以任务管理器中查看 //第二个参数为需要注入的DLL的完整文件名 int main( int argc, char *argv[]) { if (argc != 3) { cout << "Invalid parameters!" << endl; return -1; } //查找目标进程,并打开句柄 DWORD dwProcID = FindProc(argv[1]); if (dwProcID == 0) { cout << "Target process not found!" << endl; return -1; } HANDLE hTarget = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcID); if (NULL == hTarget) { cout << "Can't Open target process!" << endl; return -1; } //获取LoadLibraryW和FreeLibrary在宿主进程中的入口点地址 HMODULE hKernel32 = GetModuleHandle(_T( "Kernel32" )); LPTHREAD_START_ROUTINE pLoadLib = (LPTHREAD_START_ROUTINE) GetProcAddress(hKernel32, "LoadLibraryW" ); LPTHREAD_START_ROUTINE pFreeLib = (LPTHREAD_START_ROUTINE) GetProcAddress(hKernel32, "FreeLibrary" ); if (NULL == pLoadLib || NULL == pFreeLib) { cout << "Library procedure not found: " << GetLastError() << endl; CloseHandle(hTarget); return -1; } WCHAR szPath[MAX_PATH]; MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED, argv[2], -1, szPath, sizeof (szPath) / sizeof (szPath[0])); //在宿主进程中为LoadLibraryW的参数分配空间,并将参数值写入 LPVOID lpMem = VirtualAllocEx(hTarget, NULL, sizeof (szPath), MEM_COMMIT, PAGE_READWRITE); if (NULL == lpMem) { cout << "Can't alloc memory block: " << GetLastError() << endl; CloseHandle(hTarget); return -1; } // 参数即为要注入的DLL的文件路径 if (!WriteProcessMemory(hTarget, lpMem, ( void *)szPath, sizeof (szPath), NULL)) { cout << "Can't write parameter to memory: " << GetLastError() << endl; VirtualFreeEx(hTarget, lpMem, sizeof (szPath), MEM_RELEASE); CloseHandle(hTarget); return -1; } //创建信号量,DLL代码可以通过ReleaseSemaphore来通知主程序清理 HANDLE hSema = CreateSemaphore(NULL, 0, 1, _T( "Global\\InjHack" )); //将DLL注入宿主进程 HANDLE hThread = CreateRemoteThread(hTarget, NULL, 0, pLoadLib, lpMem, 0, NULL); //释放宿主进程内的参数内存 VirtualFreeEx(hTarget, lpMem, sizeof (szPath), MEM_RELEASE); if (NULL == hThread) { cout << "Can't create remote thread: " << GetLastError() << endl; CloseHandle(hTarget); return -1; } //等待DLL信号量或宿主进程退出 WaitForSingleObject(hThread, INFINITE); HANDLE hObj[2] = {hTarget, hSema}; if (WAIT_OBJECT_0 == WaitForMultipleObjects(2, hObj, FALSE, INFINITE)) { cout << "Target process exit." << endl; CloseHandle(hTarget); return 0; } CloseHandle(hSema); //根据线程退出代码获取DLL的Module ID DWORD dwLibMod; if (!GetExitCodeThread(hThread, &dwLibMod)) { cout << "Can't get return code of LoadLibrary: " << GetLastError() << endl; CloseHandle(hThread); CloseHandle(hTarget); return -1; } //关闭线程句柄 CloseHandle(hThread); //再次注入FreeLibrary代码以释放宿主进程加载的注入体DLL hThread = CreateRemoteThread(hTarget, NULL, 0, pFreeLib, ( void *)dwLibMod, 0, NULL); if (NULL == hThread) { cout << "Can't call FreeLibrary: " << GetLastError() << endl; CloseHandle(hTarget); return -1; } WaitForSingleObject(hThread, INFINITE); CloseHandle(hThread); CloseHandle(hTarget); return 0; } |
四、挂钩
上面的程序已经可以将自编代码注入到宿主进程中了,下面就要进一步讨论如何来编写注入体(动态链接库)以实现对目标API进行拦截。这一部分的内容比上面要深一些,需要一点汇编基础知识。
1. 在VS中进行汇编级调试
VS为用户提供了非常强大的调试功能,可以方便的查看注入代码与宿主代码的运行情况。现在需要另创建一个项目作为宿主进程,MFC简单对话框程序是一个不错的选择。下面就以GetTickCount作为目标API进行讲解。先响应对话框的鼠标左键按下事件,并添加GetTickCount代码:
1 2 3 4 5 | void CMyTargetDlg::OnLButtonDown( UINT nFlags, CPoint point) { GetTickCount(); CDialog::OnLButtonDown(nFlags, point); } |
在GetTickCount前设置断点,运行程序后点左键让程序停在这里,然后打开反汇编(调试菜单->窗口),会看到下面的反汇编代码:
上图中有4行汇编指令,第1列是指令所在的内存地址,第2列是汇编指令,第3列是操作数。在不同的机器上编译结果也不同,所以内存地址会不一样,但后面的指令和操作数都大同小异。按一下F10(逐过程),运行到0063E615这一行,再按下F11(逐语句)就会进入到GetTickCount的代码中去,见下图:
接下来要执行的指令是:
mov edx, 7FFE0000h |
注意这一句代码所在的地址是7C80934A,下一句代码是7C80934F,说明这一行mov指令的长度为5。现在打开内存查看窗口(调试->窗口->内存),并在地址里输入0x7C80934A,显示如下:
可知这条mov指令对应的机器码即是:ba 00 00 fe 7f。此时打开寄存器窗口(调试->窗口->寄存器),可以看到当前各寄存器的值。按下F10执行单步,还可以看到各寄存器的变化(变化的值用红色标出),如下图:
2. 指令的格式
为了继续要了解x86架构下汇编码和机器码的对应关系,需要参考一部非常重要的文献“Intel® 64 and IA-32 Architectures Software Developer's Manual”(以下简称IA32SDM),这是Intel公司免费提供给开发者的,可以在下面的网址找到3卷合订本:
在IA32SDM的Vol. 2B - 4.2(总第1257页)可以找到各种mov指令的说明:
上表中每一行表示了一种mov指令,第一列是这种指令的操作码格式,第二列是指令格式,最后一列是描述信息。格令格式一列中r*是指位长为*的寄存 器,r/m*是指位长为*的内存地址,Imm*是指位长为*的立即数。上文中的mov指令“mov edx, 7FFE0000h”的操作数有两个:第一个是32位寄存器(r32)edx,第二个是一个32位立即数(Imm32)7FFE0000h。查上表可知该 mov指令就是用红框划出的那一种:“MOV r32, Imm32”,它对应的Opcode(操作码)是“B8+rd”,机器码编码格式为OI。在1258页可以看到各种mov指令的编码格式,第一列与上表中的第三列对应,后面四列是四个操作数。
表中编码格式OI包含两个操作数,先是Opcode加寄存器代码,后面紧跟了一个立即数。而Opcode的编写格式参见IA32SDM的Vol. 2A - 3.1.1.1(总第606页),摘录如下:
The “Opcode” column in the table above shows the object code produced for each form of the instruction. When possible, codes are given as hexadecimal bytes in the same order in which they appear in memory. Definitions of entries other than hexadecimal bytes are as follows:
- REX.W — Indicates the use of a REX prefix that affects operand size or instruction semantics. The ordering of the REX prefix and other optional/mandatory instruction prefixes are discussed Chapter 2. Note that REX prefixes that promote legacy instructions to 64-bit behavior are not listed explicitly in the opcode column.
- /digit — A digit between 0 and 7 indicates that the ModR/M byte of the instruction uses only the r/m (register or memory) operand. The reg field contains the digit that provides an extension to the instruction's opcode.(翻译:这是一个0到7的数字,表示指令的ModR/M字节只使用r/m操作数。ModR/M的reg位就是该数,作为操作码的一个附加码)
- /r — Indicates that the ModR/M byte of the instruction contains a register operand and an r/m operand.
- cb, cw, cd, cp, co, ct — A 1-byte (cb), 2-byte (cw), 4-byte (cd), 6-byte (cp), 8-byte (co) or 10-byte (ct) value following the opcode. This value is used to specify a code offset and possibly a new value for the code segment register.
- ib, iw, id, io — A 1-byte (ib), 2-byte (iw), 4-byte (id) or 8-byte (io) immediate operand to the instruction that follows the opcode, ModR/M bytes or scaleindexing bytes. The opcode determines if the operand is a signed value. All words, doublewords and quadwords are given with the low-order byte first.
- +rb, +rw, +rd, +ro — A register code, from 0 through 7, added to the hexadecimal byte given at the left of the plus sign to form a single opcode byte. See Table 3-1 for the codes(翻译:这是一个寄存器代码,范围由0到7。与+号左边的16进制数代数相加构成一个完整的操作码字节。具体代码参见Table 3-1). The +ro columns in the table are applicable only in 64-bit mode.
- +i — A number used in floating-point instructions when one of the operands is ST(i) from the FPU register stack. The number i (which can range from 0 to 7) is added to the hexadecimal byte given at the left of the plus sign to form a single opcode byte.
按照标记为红色的描述可知Opcode“B8+rd”中的B8是基础码值0xB8,rd表示32位寄存器EDX的代号。寄存器的代码表可参见IA32SDM的Vol. 2A - Table 3-1(总第607页),如下图:
从上表中红色线框标出的部分中可以看出,EDX对应的附加码为2,因此这条mov指令的Opcode就是0xB8 + 0x02 = 0xBA。跟据编码格式OI,后面紧跟一个32位的立即数0x7FFE0000,由于Intel的CPU体系是Little Ending,所以字节序为逆序,故在内存查看器中立即数显示为“00 00 fe 7f”。综上所述,该mov指令的完整机器码为:“ba 00 00 fe 7f”,与内存查看器的结果吻合。
2. 准备JMP
上面简单介绍了在VS进行汇编级调试的基本方法,并以mov指令为范例讲解了如何分析机器码。掌握了这些工具和资料,就可以清晰地了解我们下面要完成的代码在系统内部执行的细节。从上节可知,GetTickCount这个API执行的第一条指令是mov,如果能把mov的Opcode改为jmp,那就可以跳转到自定义的函数地址执行任意代码了。从IA32SDM(Vol. 2A - 3.2)中查出jmp指令的机器码:
由于自定义的函数位置随机,且在win32操作系统的保护下,每个进程的段地址都是固定的,程序可以通过CS寄存器访问,但不能够改变。因此我们有两种选择,一是用JMP r/m32指令执行段内绝对跳转,二是用JMP rel32指令执行段内相对跳转。先讲解如何利用JMP r/m32执行绝对跳转。机器码的格式参见IA32SDM的Vol. 2A - 2.1,如下图:
从上图可知,机器码由6大部分组成,而JMP r/m32指令对应的机器码为“FF /4”(其中/4的含义参见上文中Opcode说明里用蓝色标记的文字),用到了其中3个部分:1个字节的Opcode(即0xFF)、1个字节的ModR/M和4个字节的Displacement操作数。其中的ModR/M指定了CPU的寻址方式以及Opcode的附加码,它又分为三段:Mod、Reg/Opcode和R/M,具体构成可参见IA32SDM的Vol. 2A - 2.1.3和后面的Table 2-2,如下图:
先看一下表头最左边一格,第6行“/digit (Opcode)”就是机器码“FF /4”中的4,所以看红框标记的那一列(4的二进制为100)就可以了。“Effective Address”指定了寻指方式,为了避免对寄存器进行操作,用1条指令就完成跳转,我们选择最简单的“disp32”这一行,它表示仅用指令机器码中的第3部分Displacement表示跳转的目标地址。这样就确定了使用的Mod位为00,Reg/Opcode位为100,R/M位为101。计算可得ModR/M字节为00 100 101(二进制) = 0x25。
Displacement指向一段4字节的内存,这段内存里存放的是最终的目标地址。因此需要先用VirtualAllocEx申请4个字节的空间,将自定义函数的地址存入,然后再将申请的地址填入Displacement。综上所述,完整的机器码应该是FF 25 XX XX XX XX,最后面的4个字节是一个存有目标函数入口地址的内存地址。
用JMP r/m32指令完成跳转是比较复杂的,不仅需要申请和释放内存,且整个机器指令有6个字节。更简单的方法就是利用JMP rel32指令执行相对跳转,而机器码只有5个字节。JMP rel32对应的机器码是E9 cd,其中cd就是相对地址,计算方法为:目标地址 - 当前指令地址 - 5。在准备好JMP指令的机器码后,就可以将其替换到目标API的入口地址处,欺骗宿主进程执行伪装函数。
3. 修改入口点
看完上面的介绍,相信您已经迫不及待的想要尝试如何对目标API挂钩了。虽然还有很多问题没有解决,比如怎样返回,怎样执行原API功能,怎样全身而退等等,但这些问题可以先放一放,先来看看能否利用上面的方法成功挂钩。
首先需要建立一个DLL项目以生成注入体,自定义一个DllMain函数,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | #include <windows.h> BOOL WINAPI DllMain( HINSTANCE hInstDll, DWORD fdwReason, LPVOID lpvReserved) { switch (fdwReason) { case DLL_PROCESS_ATTACH: DisableThreadLibraryCalls(hInstDll); InstallMonitor(); break ; case DLL_PROCESS_DETACH: break ; } return TRUE; } |
然后编写挂钩函数InstallMonitor:
1 2 3 4 5 6 7 8 | void InstallMonitor( void ) { HANDLE hProc = GetCurrentProcess(); BYTE aOpcode[5] = {0xE9}; //JMP Procudure *( DWORD *)(&g_aOpcode[1]) = DWORD (MonFunc) - DWORD (GetTickCount) - 5; WriteProcessMemory(hProc, LPVOID (GetTickCount), LPVOID (aOpcode), 5, NULL); CloseHandle(hProc); } |
上面的代码很好理解,aOpcode就是根据前文介绍的方法构造的jmp指令,指定跳转到自定义的伪装函数MonFunc,然后用WriteProcessMemory将jmp指令填写入GetTickCount的代码处。伪装函数MonFunc函数很好写:
1 2 3 4 | void WINAPI MonFunc( void ) { MessageBox(NULL, _T( "注入代码" ), _T( "示例" ), 0); } |
至此,您就可以按上面的代码编译一个注入体DLL了,然后利用本文第三部分的注入程序就可以将此DLL注入到宿主进程执行。
五、完美欺骗
如果您按上文所述的方法执行出成功的结果,那么你很可能会发现在对话框确定后宿主进程崩溃了。原因有下面几条:
- 伪装函数没有正确的保持栈的平衡,导致返回时宿主清栈出错;
- 伪装函数没有按API执行方式执行出结果,宿主不能正常的调用系统API导致错误;
- 伪装函数不是线程安全的,导致宿主在并发调用时出错;
- 宿主有安全防护措施,检查到攻击后自动恢复或自我毁灭。
本文只讨论前3条原因的解决方案,不考虑第4条原因。下面逐条解释。
1. 保持栈的平衡
大部分API都是有参数的,而参数是由宿主在call指令执行前压入堆栈。Win32API的调用约定是__stdcall,表示由API负责栈的清理,那么如果伪装函数在返回时没有适当的清栈必将导致出错。因此,伪装函数的参数表一定要与原API相同,才能保证编译器生成的代码能够正确返回到宿主代码。
2. 执行原API的功能
为了能够执行原API的功能,必须在调用它之前恢复它原来的代码,否则就会陷入死循环。当然,应该在改写机器码时保留原先的机器码,这样就可以利用WriteProcessMemory将其恢复原状。ReadProcessMemory这个API函数与WriteProcessMemory的功能相反,可以读取指定位置的机器码。还要记得,在原API调用结束后还要修改它的入口点,否则下次就无法欺骗了。整个伪装函数的结构如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | //Monitor Function DWORD WINAPI MonFunc() { //Restore the original API before calling it ReleaseBase(); //Calling the original API DWORD dw = GetTickCount(); //Monitor the original API again MonitorBase(); //You can do anything here return dw; } |
3. 线程安全
用EnterCriticalSection和LeaveCriticalSection是保证线程安全的最佳选择,将伪装函数用这对函数包起来就可以解决并发访问的问题。现在的代码应该看起来是这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | //Monitor Function DWORD WINAPI MonFunc() { //Thread safety EnterCriticalSection(&g_cs); //Restore the original API before calling it ReleaseBase(); DWORD dw = GetTickCount(); MonitorBase(); //You can do anything here //Thread safety LeaveCriticalSection(&g_cs); return dw; } |
4. 完整示例
下面贴出注入体DLL的完整代码,供您参考。这个DLL对GetTickCount挂了钩子,您可以在伪装函数MonFunc中添加任意的自定义代码,并在退出的时候调用UninstallMonitor结束钩子程序。
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 | #include <tchar.h> #include <Windows.h> //Handle of current process HANDLE g_hProc; //Backup of orignal code of target api BYTE g_aBackup[6]; BYTE g_aOpcode[6]; //Critical section, prevent concurrency of calling the monitor CRITICAL_SECTION g_cs; //Base address of target API in DWORD DWORD g_dwApiFunc = ( DWORD )GetTickCount; //Hook the target API __inline BOOL MonitorBase( void ) { // Modify the heading 6 bytes opcode in target API to jmp instruction, // the jmp instruction will lead the EIP to our fake function ReadProcessMemory(g_hProc, LPVOID (g_dwApiFunc), LPVOID (g_aBackup),<br> sizeof (g_aBackup)/ sizeof (g_aBackup[0]), NULL);<br> return WriteProcessMemory(g_hProc, LPVOID (g_dwApiFunc), LPVOID (g_aOpcode), sizeof (g_aOpcode) / sizeof (g_aOpcode[0]), NULL); } //Unhook the target API __inline BOOL ReleaseBase( void ) { // Restore the heading 6 bytes opcode of target API. return WriteProcessMemory(g_hProc, LPVOID (g_dwApiFunc), LPVOID (g_aBackup), sizeof (g_aOpcode) / sizeof (g_aOpcode[0]), NULL); } //Pre-declare BOOL UninstallMonitor( void ); //Monitor Function DWORD WINAPI MonFunc() { //Thread safety EnterCriticalSection(&g_cs); //Restore the original API before calling it ReleaseBase(); DWORD dw = GetTickCount(); MonitorBase(); //You can do anything here, and you can call the UninstallMonitor //when you want to leave. //Thread safety LeaveCriticalSection(&g_cs); return dw; } //Install Monitor BOOL InstallMonitor( void ) { //Get handle of current process g_hProc = GetCurrentProcess(); g_aOpcode[0] = 0xE9; //JMP Procudure *( DWORD *)(&g_aOpcode[1]) = ( DWORD )MonFunc - g_dwApiFunc - 5; InitializeCriticalSection(&g_cs); //Start monitor return MonitorBase(); } BOOL UninstallMonitor( void ) { //Release monitor if (!ReleaseBase()) return FALSE; DeleteCriticalSection(&g_cs); CloseHandle(g_hProc); //Synchronize to main application, release semaphore to free injector HANDLE hSema = OpenSemaphore(EVENT_ALL_ACCESS, FALSE, _T( "Global\\InjHack" )); if (hSema == NULL) return FALSE; return ReleaseSemaphore(hSema, 1, ( LPLONG )g_hProc); } BOOL WINAPI DllMain( HINSTANCE hInstDll, DWORD fdwReason, LPVOID lpvReserved) { switch (fdwReason) { case DLL_PROCESS_ATTACH: DisableThreadLibraryCalls(hInstDll); InstallMonitor(); break ; case DLL_PROCESS_DETACH: break ; } return TRUE; } |