逆向之远程线程注入
Method |
|
CreateRemoteThread() |
Windows中的函数,用于在其他进程中创建一个新的线程,让该线程在其他进程中运行。 |
NtCreateThreadEx() |
使用系统调用在目标进程直接创建线程,提供更高级的线程创建功能 |
QueueUserAPC() |
向目标进程内核对象队列中加入需要运行的用户空间异步过程调用(APC),被线程从APC队列中取出并执行 |
SetWindowsHookEx() |
Windows中的函数,运行应用程序注入到其它应用程序的消息循环中,监控或处理这些应用程序收到的消息 |
RtlCreateUserThread() |
来自NTDLL的函数,主要用户底层或者驱动中,支持更多安全参数,可以运行在更高版本和CreateRemoteThread类似 |
SetThreadContext() |
Windows中的函数,允许设置一个线程的cpu上下文 |
Reflective DLL |
反射式注入,不需要建立远程线程来加载dll,直接复制dll到目标进程空闲内存区域中。 |
本文主要讨论第一个函数,简单入门,让我们先看看这个函数的定义吧!
CreateRemoteThread()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| #include<Windows.h>
HANDLE CreateRemoteThread( Handle hProcess, LPSECURITY_ATTRIBUTES lpThreadAttributes, SIZE_T dwStackSize, LPTHREAD_START_ROUTINE lpStartAddress, LPVOID lpParameter, DWORD dwCreationFlags, LPDWORD lpThreadId);
|
目标程序
我们来个简单的例子,我们自己写一个程序,然后注入线程,修改函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| #include <Windows.h> #include <iostream>
void function() { std::cout << "normal working state on:" << *function << std::endl; }
int main() {
SetConsoleTitle(TEXT("TESTAPP"));
std::cout << "program start:" << std::endl; while (true) { Sleep(1000); function(); } getchar(); return 0; }
|
这个就是我们需要注入的应用,很简单对吧,就是循环调用一个函数,打印函数地址,并且还设置了项目的控制台窗口的标题文本,方便我们等会进行查找,那我们注入的目的,就是修改这个unction函数,替换为我们自己的函数。
这个地址是每次运行都会变的,我们还需要找到这个函数的基地址(简单来说就是编译后,每次运行这个地址都不会变),相信大家都用过什么xx修改器吧,我记得我小时候玩游戏用过,好像是烧饼修改器,大伙应该都很熟悉,例如游戏金币为30,然后打开修改器搜索这个数字,肯定会有一大串出来,然后去改变这个值,增加或者减少,来个几次,就会发现只剩下一个(ps:那会反逆向还不是很厉害,基本都能通过这个检测,稍微复杂点的也就是修改完后,还会有个小小的对比检测,这里不深入讨论),然后我们通过修改这个值就可以修改金币的数字了,但是这个有个缺点,就是每次运行,这个地址都会改变,只有使用基地址,那么每次运行都可以使用,相信大家已经有了一个大概的印象了吧,非常简单的一个概念。
那我们这里就不使用那么复杂的去找了,我们直接使用我们的vs工具进行查看(ps:也可以使用ollydbg,IDA,或者X64dbg…),在function()函数前面打一个断点,然后点击调试,然后程序就会停在断点处,这个时候我们右键,转到反汇编。
就会看到这样的画面,我们找到function,然后看到那个call,后面的:function(),括号里面的就是function的基地址,记住这个值,等会我们需要用到。
注入内容
下面我们需要编写的就是,根据地址,我们去修改内容,让function去执行我们自己的函数。这一部分可能比较复杂,请耐心阅读。
这次我们要编写的是动态库,需要vs右键项目属性,配置类型改为动态库.dll
这次我们就不全部放在一起,我们进行拆开,每一部分单独解释
1 2 3
| DWORD functionAddress = 0x411320;
|
1 2 3 4
| void hook() { std::cout << "Hooked" << std::endl; }
|
! ! ! 下面是重点,也是难点
1 2 3 4 5 6 7 8
| std::cout<<"[+] hookAddress : "<<*hook<<std::endl;
const int instructionSize = 5;
DWORD relativeAddress = ((DWORD)hook - (DWORD)functionAddress) - instructionSize;
std::cout<<"[+] relativeAddress : "<<relativeAddress<<std::endl;
|
首先我们先看看我们注入函数的地址,然后定义一个instructionSize,可能有同学就要好奇了,为啥要是5,不能是10吗,那下面我就来仔细解释下。
我们使用的是x86编译器,也就是32位的程序,所以我们的指令也要匹配32位,大家可能对汇编不是很熟悉,没关系,在这里只需要了解就行,就像刚才我们反汇编第一个程序那样,都基本可以看到这些指令
1 2 3 4 5 6 7 8 9
| //基本汇编指令 00xxxxxx push ebp 00xxxxxx move ebp,esp 00xxxxxx sub esp,8 00xxxxxx move eax,1 00xxxxxx add eax, [ebp-4] 00xxxxxx call sub_401090 00xxxxxx jmp short loc_401056 00xxxxxx retn
|
在这里,我们只需要关注jmp指令就行。
这就是在x86下,jmp指令使用的相对地址,后面为地址,在32位应用程序中,寻址为32位,就是后面那4个字节,再加上前面那个指令,刚好就为5个字节,符合指令格式,多或者少,都不行,这就是为啥要5个字节了。
1
| DWORD relativeAddress = ((DWORD)hook - (DWORD)functionAddress) - instructionSize;
|
这句代码就是计算hook函数相对于要HOOK的函数的相对偏移地址
因为这个时候代码已经注入,hook函数和functionAddress在同一片地址空间,就需要计算相对偏移地址,然后再减去需要的指令格式大小,上一步已经具体说明。
下面需要修改内存页的保护属性,将要写入HOOK代码的内存页权限改为可执行,可读写。
因为默认内存页可能只是只读或只执行,我们需要改成可写才能修改代码,并且在写入后,要恢复内存页原始的保护属性,这可以避免留下可执行且可写的内存,减少安全风险。还有一些安全防护需要内存属性修改才能绕过。如果不修改权限,直接写入执行代码会失败并造成崩溃。
1 2 3 4 5 6 7 8 9 10 11
| BOOL VirtualProtect( LPVOID lpAddress, SIZE_T dwSize, DWORD flNewProtect, PDWORD lpflOldProtect );
|
1
| BOOL pageChange = VirtualProtect((LPVOID)functionAddress, instructionSize, PAGE_EXECUTE_READWRITE, &defaultProtection);
|
所以上面这段代码应该就能看懂了吧,函数的起始地址,区域为内存jmp指令,把这段内存改成可执行,可读写,然后保存原版的内存权限到defaultProtection.
1 2 3 4 5 6 7
| byte jmpInstruction[instructionSize];
jmpInstruction[0] = 0xE9; jmpInstruction[1] = relativeAddress; jmpInstruction[2] = (relativeAddress >> 8); jmpInstruction[3] = (relativeAddress >> 16); jmpInstruction[4] = (relativeAddress >> 24);
|
我们可以查询到,jmp的操作码就为0xE9,所以我们构造跳转指令,然后就要构造偏移地址,首先就是,必须把32位的地址拆分存储到这4个字节中了,如果我们只设置那个jmpInstruction[1],那么32位就会丢失后面的24位,所以我们要把地址拆分,逐个放在这个指令字节中。
好了,这一步,我们已经构造好了jmp指令,要跳转的地址也写入了,下面就是写入到内存中。
1 2 3
| SIZE_T bytesWritten;
int lpWrite = WriteProcessMemory(GetCurrentProcess(), (LPVOID)functionAddress, jmpInstruction, instructionSize, &bytesWritten);
|
1 2 3 4 5 6 7 8 9 10 11 12
| BOOL WriteProcessMemory( HANDLE hProcess, LPVOID lpBaseAddress, LPCVOID lpBuffer, SIZE_T nSize, SIZE_T *lpNumberOfByteWriteen );
|
如同你看到的那样,把jmp指令写入到functionaddress函数地址后,每次call这个函数的时候,执行到后面jmp指令,就会直接跳转到我们自己的hook函数地址,中间这段也就是属于原本函数的指令不执行。这样我们就完成了内存写入!
!!!但是,别着急,还有一步,记得我们上一步修改了一段内存的属性吧,现在我们要将这段内存的属性修改回去
1 2
| DWORD oldProtect; pageChange = VirtualProtect((LPVOID)functionAddress, instructionSize, defaultProtection, &oldProtect);
|
原理和上面类似,这里就不多赘述了。
step6
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| bool WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpReserved) { switch (fdwReason) { case DLL_PROCESS_ATTACH: CreateThread(0, 0, thread, hinstDLL, 0, 0); break; case DLL_THREAD_ATTACH: case DLL_THREAD_DETACH: case DLL_PROCESS_DETACH: break; } return true; }
|
DllMain函数就是DLL被加载和卸载时会被调用的函数,用来做初始化和清洁的工作。
1 2 3 4 5 6 7 8 9 10 11 12
| BOOL WINAPI DLLMain( HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved );
|
使用这个函数,我们就能在dll被调用的时候进行注入代码了
1 2 3 4 5 6 7 8 9 10
| DWORD WINAPI thread(LPVOID param) {
std::cout << "[+] Thread started" << std::endl; writeTheHook(); while (true) { Sleep(100); } FreeLibraryAndExitThread((HMODULE)param, 0); return 0; }
|
这段就是上面那个createThread调用的线程代码,就是执行我们的注入比较简单,这里就提一下,只要注意结束的时候进行线程清理。
好了,到这里,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
| #include <Windows.h> #include <iostream>
DWORD functionAddress = 0x411320;
void hook() {
std::cout << "Hooked" << std::endl; }
void writeTheHook() {
std::cout<<"[+] hookAddress : "<<*hook<<std::endl;
const int instructionSize = 5;
DWORD relativeAddress = ((DWORD)hook - (DWORD)functionAddress) - instructionSize;
std::cout<<"[+] relativeAddress : "<<relativeAddress<<std::endl;
DWORD defaultProtection; BOOL pageChange = VirtualProtect((LPVOID)functionAddress, instructionSize, PAGE_EXECUTE_READWRITE, &defaultProtection);
if (!pageChange) { std::cout<<"[-] Fail to VirtualProtect"<<std::endl; return; }
byte jmpInstruction[instructionSize];
jmpInstruction[0] = 0xE9; jmpInstruction[1] = relativeAddress; jmpInstruction[2] = (relativeAddress >> 8); jmpInstruction[3] = (relativeAddress >> 16); jmpInstruction[4] = (relativeAddress >> 24);
SIZE_T bytesWritten;
int lpWrite = WriteProcessMemory(GetCurrentProcess(), (LPVOID)functionAddress, jmpInstruction, instructionSize, &bytesWritten);
if (!lpWrite) { std::cout<<"[-] Fail to WriteProcessMemory"<<std::endl; return; }
std::cout<<"[+] Jump writed at 0x%x"<<functionAddress<<std::endl;
DWORD oldProtect; pageChange = VirtualProtect((LPVOID)functionAddress, instructionSize, defaultProtection, &oldProtect);
if (!pageChange) { std::cout << "[-] Fail to VirtualProtect" << std::endl; return; } }
DWORD WINAPI thread(LPVOID param) { std::cout << "[+] Thread started" << std::endl; writeTheHook(); while (true) { Sleep(100); } FreeLibraryAndExitThread((HMODULE)param, 0); return 0; }
bool WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpReserved) { switch (fdwReason) { case DLL_PROCESS_ATTACH: CreateThread(0, 0, thread, hinstDLL, 0, 0); break; case DLL_THREAD_ATTACH: case DLL_THREAD_DETACH: case DLL_PROCESS_DETACH: break;
} return true; }
|
然后进行编译生成
注入器
我们只是编写好了DLL的代码,但是我们想要注入的那个程序是不可能调用我们这个DLL的,所以我们还需要一个注入器
step1
加载一些库函数和找到目标进程
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| DWORD pid;
HMODULE kernel32 = LoadLibrary("kernel32.dll");
LPVOID loadLibraryA = GetProcAddress(kernel32, "LoadLibraryA");
HWND hwnd = FindWindow(0, TEXT("TESTAPP"));
GetWindowThreadProcessId(hwnd, &pid);
HANDLE hProc = OpenProcess(PROCESS_ALL_ACCESS, 1, pid);
|
首先加载win函数库,我们需要loadLibraryA去加载库文件,所以我们要先引入kernel32.dll
然后用GetProcAddress得到函数地址
现在知道为啥在第一步中设置那个title了吧,我们就可以直接使用TESTAPP来得到窗口,
然后再调用函数得到PID
step2
加载我们的DLL
1 2 3 4 5
| const char* path = "D:\\lib.dll";
LPVOID pathAddress = VirtualAllocEx(hProc, NULL, strlen(path), (MEM_COMMIT | MEM_RESERVE), 0x40); SIZE_T bytesWritten = 0; int lpWrite = WriteProcessMemory(hProc, pathAddress, path, strlen(path), &bytesWritten);
|
我们要先传入dll的路径,并且分配内存空间
1 2 3 4 5 6 7 8 9 10 11 12 13
| LPVOID VirtualAllocEx( HANDLE hProcess, LPVOID lpAddress, SIZE_T dwSize, DWORD flAllocationType, DWORD flProtect ); //hProcess --- 要在其地址空间分配内存的进程句柄 //lpAddress --- 分配的内存大小 //dwSize --- 分配的内存大小 //flProtect --- 内存保护属性 //flAllocation --- 内存类型,如MEM_COMMIT //此函数可以向打开的目标进程地址安全地分配可控内存块,是DLL注入中非常重要的函数
|
下面那个writeProcessMemory我们上面分析过,这里略过
创建远程线程注入
终于到了最后了,也是我们开头提到的远程线程注入
1
| HANDLE remoteProc = CreateRemoteThread(hProc, NULL, 0, (LPTHREAD_START_ROUTINE)loadLibraryA, pathAddress, 0, NULL);
|
简单解释下,就是在hProc进程中创建了一个线程,去执行LoadLibraryA,也就是加载dll库的win函数,然后参数就是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
| #include <Windows.h> #include <iostream>
int main() { DWORD pid;
HMODULE kernel32 = LoadLibrary("kernel32.dll");
LPVOID loadLibraryA = GetProcAddress(kernel32, "LoadLibraryA");
HWND hwnd = FindWindow(0, TEXT("TESTAPP"));
if (!hwnd) { std::cout<<"[-] Fail to FindWindow"<<std::endl; return 1; } GetWindowThreadProcessId(hwnd, &pid);
std::cout<<"[+] PID of target : "<<pid<<std::endl;
HANDLE hProc = OpenProcess(PROCESS_ALL_ACCESS, 1, pid);
if (hProc == INVALID_HANDLE_VALUE) { std::cout<<"[-] Fail to OpenProcess"<<std::endl; return 1; }
const char* path = "D:lib.dll";
LPVOID pathAddress = VirtualAllocEx(hProc, NULL, strlen(path), (MEM_COMMIT | MEM_RESERVE), 0x40);
if (!pathAddress) { std::cout<<"[-] Fail to VirtualAllocEx"<<std::endl; return 1; }
std::cout<<"[+] pathAddress in target : "<<pathAddress<<std::endl;
SIZE_T bytesWritten = 0;
int lpWrite = WriteProcessMemory(hProc, pathAddress, path, strlen(path), &bytesWritten);
if (!lpWrite) { std::cout<<"[-] Fail to WriteProcessMemory"<<std::endl; return 1; }
std::cout<<"[+] Bytes written : "<<bytesWritten<<std::endl;
HANDLE remoteProc = CreateRemoteThread(hProc, NULL, 0, (LPTHREAD_START_ROUTINE)loadLibraryA, pathAddress, 0, NULL); if (!remoteProc) { std::cout<<"[-] Fail to CreateRemoteThread"<<std::endl; return 1; }
std::cout<<"[+] Injected"<<std::endl; CloseHandle(hProc); getchar();
return 0; }
|
演示结果
我们先运行我们要注入的那个程序,走起!
正常打印,这个函数的地址
下面见证奇迹的时刻,我们来运行注入器,一定要确保dll的路径设置正确。
看到了吧,这就是逆向中的远程线程注入,好了,这篇博客也就到这了,希望大家都能有所收获,我会将代码上传到GitHub上,有需要的可以下载学习。
GitHub代码链接