关于这个系列其实有很多内容,并且在给出的实例都是自己编写的源码,在Open security和Infosec Institute中也有相关文章介绍。当然在这里有很多大牛讲的更加深入。我并不是想在这些人的基础上做补充,而是希望花时间去理解这些东西,这样才会更好的帮助我们提升自己。
在 上一篇博客 中,我们介绍了 SetWindowsHookEx
方法注入。这部分的工作已经做完了,但是看起来还不是很棒。为了向一个进程中注入一个想要运行的DLL,我们需要获得这个进程的线程ID,这样就可以注入到任何接收到hook信息的进程中。幸运的是,还有一种方法可以做到这点。在这篇文章中,我们将会尝试 CreareRemoteThread
方法。在 SetWindowsHookEx
中,我们使用 LoadLibrary
将 DLL
加载至当前进程(不是目标进程)的地址空间中。接着使用 GetProcAddress
获得所需函数的地址。下面的事情就交给 SetWindowsHookEX
了,我们构造一个钩子,钩取一个会自动加载我们DLL的进程,这样就搞定整个 DLL
注入过程了。而 CreateRemoteThread
则跟上述过程不一样,下面是其步骤:
1. 使用 VirtualAllocEx
在目标进程的地址空间中创建一块我们 DLL
所在路径长度的内存空间。
2. 使用 WriteProcessMemory
将 DLL
路径写入分配的内存。
3. 一旦DLL路径写入内存中,再使用 CreateRemoteThread
(或者其他无正式说明的功能),它再调用 LoadLibrary
函数将 DLL
注入目标进程中。
在Windows中无正式说明的功能是指在微软当中没有详细文档介绍其信息的这些功能。这样在使用这些功能就会存在一些问题。最明显的问题就是没有一个特定功能的文档。不过在 ReactOS项目中对许多这种功能做了文档说明,而这篇文章则给了我们最直观的理解。此外在微软没有正式通过的情况先,这些功能可能最终会被“越界”使用。最后呢,他们都需要更多的代码,了解并需要正确使用。
试想一下这些问题,为何要使用这些无正式说明的功能?基本的原因就是自从Vista之后,如果目标进程不在当前会话中而是在一个不同的会话中,那么 CreateRemoteThread
将会失效。而这些无正式说明功能就不会。当然这个从逆向工程的角度也不会立即就能理解。最后这个只过是在Windows中一些不知名的功能中捣点小乱而已。
在我们的代码中,我们使用 CreateRemote
线程和两个无正式说明的函数 NtCreateThreadEx
和 RtlCreateUserThread
。也许你听说过Mimikatz 和 Metasploit。这两个都是使用 RtlCreateUserThread
来实现DLL注入的。如果你想看这些代码,Mimikatz可以在这里找到,Meterpreter则在这里。需要说明的是Mimikatz的博客是法语,如果有语言障碍则可以看这里。
那么这两个函数该挑选哪一个呢? NtCreateThreadEx
是一个系统调用,是用户空间应用和内核打交道的方法。快速在IDA中查看一下 RtlCreateUserThread
。将 ntdll.dll
拖进IDA中,通过名字标签找到 RtlCreateUserThread
,进入其中,可以看到如下信息:
后面的代码看这里:
当你跟踪这段代码你会发现, RtlCreateUserThread
调用 NtCreateThreadEx
。因此 RtlCreateUserThread
应该是 NtCreateThreadEx
的封装。我们想调用 RtlCreateUserThread
是因为 NtCreateThreadEx
的系统调用选项可以在Windows版本间改变。因此, RtlCreateUserThread
更好用一些。 Mimikatz 和Meterpreter使用 RtlCreateUserThread
是由于这个选项更加安全。
对下面的代码进行一些改进,下面使用CreateRemoteThread方法一步步实现上述步骤:
1. 使用VirtualAllocEx在目标进程的地址空间中创建一块我们DLL所在路径长度的内存空间。 //This dll path should be relative to the target process or an absolute path char* dll = "inject.dll"; //We need a handle to the process we will be injecting into HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid); //Create the space needed for the dll we are going to be injecting LPVOID lpSpace = (LPVOID)VirtualAllocEx(hProcess, NULL, strlen(dll), MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
VirtualAlloc.c
2.使用 WriteProcessMemory
将 DLL
路径写入分配的内存
//Write inject.dll to memory of process int n = WriteProcessMemory(hProcess, lpSpace, dll, strlen(dll), NULL);
WriteProcessMem.c
3. 一旦DLL路径写入内存中,再使用 CreateRemoteThread
(或者其他无正式说明的功能),它再调用LoadLibrary函数将DLL注入目标进程中。
HMODULE hModule = GetModuleHandle("kernel32.dll"); LPVOID lpBaseAddress = (LPVOID)GetProcAddress(hModule,"LoadLibraryA"); //Create Remote Thread using the address to LoadLibraryA and the space for the DLL hThread = CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)lpBaseAddress, lpSpace, NULL, NULL);
CreateRemThread.c
远程线程DLL注入
下一步是使用这些无正式说明的功能代替 CreateRemoteThread
。首先需要调用这些函数。在 CreateRemoteThread
中,我们可以直接调用因为它是 Windows API
的一部分。而这些函数是不具备这个特点的,因此需要创建一个模板。那就从RtlCreateUserThread开始。首先使用相同的名字(没必要一定是相同的名字,但是这样会更加清楚一些)来创建这个方法。这个方法来声明这个函数的原型。这个原型需要和通过NtInternals给出的模板相匹配。接着创建线程的句柄,作为输入,RtlCreateUserThread的指针指向这个线程的句柄,并且将RtlCreateUserThread 设置成创建的线程的句柄。接着我们获取到ntdll.dll的句柄,正是RtlCreateUserThread保存的地方。在不同的DLL中的函数都可以被输出了,因此可以直接使用。在DLL中我们可以使同样的方法“__declspec(dllexport)”来输出函数。由于无正式说明的函数没有被输出,因此我们必须要获得其句柄并且得到地址。下一步则是使用GetProcAddress获得进程中的地址。最后调用,并且返回线程的句柄。模板如下:
幸运的是,这个进程对于NtCreateThreadEx 本质上来说是相同的,如下:
HANDLE NtCreateThreadEx( HANDLE hProcess, LPVOID lpBaseAddress, LPVOID lpSpace ) { //The prototype NtCreateThreadEx from undocumented.ntinternals.com typedef DWORD (WINAPI * functypeNtCreateThreadEx)( PHANDLE ThreadHandle, ACCESS_MASK DesiredAccess, LPVOID ObjectAttributes, HANDLE ProcessHandle, LPTHREAD_START_ROUTINE lpStartAddress, LPVOID lpParameter, BOOL CreateSuspended, DWORD dwStackSize, DWORD Unknown1, DWORD Unknown2, LPVOID Unknown3 ); HANDLE hRemoteThread = NULL; HMODULE hNtDllModule = NULL; functypeNtCreateThreadEx funcNtCreateThreadEx = NULL; //Get handle for ntdll which contains NtCreateThreadEx hNtDllModule = GetModuleHandle( "ntdll.dll" ); if ( hNtDllModule == NULL ) { return NULL; } funcNtCreateThreadEx = (functypeNtCreateThreadEx)GetProcAddress( hNtDllModule, "NtCreateThreadEx" ); if ( !funcNtCreateThreadEx ) { return NULL; } funcNtCreateThreadEx( &hRemoteThread, GENERIC_ALL, NULL, hProcess, (LPTHREAD_START_ROUTINE)lpBaseAddress, lpSpace, FALSE, NULL, NULL, NULL, NULL ); return hRemoteThread; }
NtCreateThreadEx.c
#include #include HANDLE RtlCreateUserThread( HANDLE hProcess, LPVOID lpBaseAddress, LPVOID lpSpace ) { //The prototype of RtlCreateUserThread from undocumented.ntinternals.com typedef DWORD (WINAPI * functypeRtlCreateUserThread)( HANDLE ProcessHandle, PSECURITY_DESCRIPTOR SecurityDescriptor, BOOL CreateSuspended, ULONG StackZeroBits, PULONG StackReserved, PULONG StackCommit, LPVOID StartAddress, LPVOID StartParameter, HANDLE ThreadHandle, LPVOID ClientID ); //Get handle for ntdll which contains RtlCreateUserThread HANDLE hRemoteThread = NULL; HMODULE hNtDllModule = GetModuleHandle( "ntdll.dll"); if( hNtDllModule == NULL ) { return NULL; } functypeRtlCreateUserThread funcRtlCreateUserThread = (functypeRtlCreateUserThread)GetProcAddress(hNtDllModule, "RtlCreateUserThread"); if( !funcRtlCreateUserThread ) { return NULL; } funcRtlCreateUserThread(hProcess, NULL, 0, 0, 0, 0, lpBaseAddress,lpSpace, &hRemoteThread, NULL); DWORD lastError = GetLastError(); return hRemoteThread; } HANDLE NtCreateThreadEx( HANDLE hProcess, LPVOID lpBaseAddress, LPVOID lpSpace ) { //The prototype of NtCreateThreadEx from undocumented.ntinternals.com typedef DWORD (WINAPI * functypeNtCreateThreadEx)( PHANDLE ThreadHandle, ACCESS_MASK DesiredAccess, LPVOID ObjectAttributes, HANDLE ProcessHandle, LPTHREAD_START_ROUTINE lpStartAddress, LPVOID lpParameter, BOOL CreateSuspended, DWORD dwStackSize, DWORD Unknown1, DWORD Unknown2, LPVOID Unknown3 ); HANDLE hRemoteThread = NULL; HMODULE hNtDllModule = NULL; functypeNtCreateThreadEx funcNtCreateThreadEx = NULL; //Get handle for ntdll which contains NtCreateThreadEx hNtDllModule = GetModuleHandle( "ntdll.dll" ); if ( hNtDllModule == NULL ) { return NULL; } funcNtCreateThreadEx = (functypeNtCreateThreadEx)GetProcAddress( hNtDllModule, "NtCreateThreadEx" ); if ( !funcNtCreateThreadEx ) { return NULL; } funcNtCreateThreadEx( &hRemoteThread, GENERIC_ALL, NULL, hProcess, (LPTHREAD_START_ROUTINE)lpBaseAddress, lpSpace, FALSE, NULL, NULL, NULL, NULL ); return hRemoteThread; } int injectIntoPID(int process, int method) { DWORD pid = (DWORD) process; char* dll = "inject.dll"; //Gets the process handle for the target process HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid); if(OpenProcess == NULL) { puts( "Could not find process"); } //Retrieves kernel32.dll module handle for getting loadlibrary base address HMODULE hModule = GetModuleHandle( "kernel32.dll"); //Gets address for LoadLibraryA in kernel32.dll LPVOID lpBaseAddress = (LPVOID)GetProcAddress(hModule, "LoadLibraryA"); if(lpBaseAddress == NULL) { puts( "Unable to locate LoadLibraryA"); return -1; } //Allocates space inside for inject.dll to our target process LPVOID lpSpace = (LPVOID)VirtualAllocEx(hProcess, NULL, strlen(dll), MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); if(lpSpace == NULL) { printf( "Could not allocate memory in process %u", ( int)process); return -1; } //Write inject.dll to memory of process int n = WriteProcessMemory(hProcess, lpSpace, dll, strlen(dll), NULL); if(n == 0) { puts( "Could not write to process's address space"); return -1; } HANDLE hThread; switch( method ) { case 1: hThread = NtCreateThreadEx(hProcess, lpBaseAddress, lpSpace); break; case 2: hThread = RtlCreateUserThread(hProcess, lpBaseAddress, lpSpace); break; default: hThread = CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)lpBaseAddress, lpSpace, NULL, NULL); } if(hThread == NULL) { return -1; } else { DWORD threadId = GetThreadId(hThread); DWORD processId = GetProcessIdOfThread(hThread); printf( "Injected thread id: %u for pid: %u", threadId, processId); getchar(); getchar(); CloseHandle(hProcess); return 0; } } int main(int argc, char* argv) { int pid; int method; puts( "Inject into which PID?"); scanf( "%u", &pid); puts( "Which method? (0 (default): CRT, 1: NtCreateThread, 2: RtlCreateUserThread)"); scanf( "%u", &method); int result = injectIntoPID(pid,method); if(result == -1) { puts( "Could not inject into PID"); } }
crt_inject.c
运行上述代码,结果如下:
现在看一下我们的payload。编译完之后,拖进IDA查看一下。我不是IDA高手,但是好在不是很复杂(特别是下面的代码)。查看一下名字窗口,如下:
这里有一件事需要注意一下,在CreateRemoteThread 前有个“I”,在其他两个前面有个“A”。“I”表示输入名,“A”表示ASCII。ASCII是编译时的函数名。因此当我们将这两个函数名适当命名一下,就能做到混淆那些恶意的人。双击其中的一个,如下:
CreateRemoteThread
被输入了,因此才会是idata的一部分。 rdata
表示只读。右击任意一个,可以通过交叉引用选项可以查看交叉引用信息:
这样一来可以看到实际的函数代码。不过我更偏向于看代码调用图。如下所示,注意到这两个函数看起来很相似(这个并不奇怪)。按照顺序分别是 CreateRemoteThread, NtCreateThreadEx, RtlCreateUserThread。
CreateRemoteThread
由于被标记了,所以在汇编代码中比较好认,但是大多数的函数彼此看起来很接近。