DLL注入之远程线程注入(一)

前言

DLL注入的目的是使另一个进程加载一个DLL文件来改变其正常流程,通常用来实现该程序的作者本未设计或预期的结果。

常应用于拓展程序功能、当然这也非常受外挂、病毒、木马的青睐。

本文代码来自《windows黑客编程技术详解》。

将其打包放在了这里:https://github.com/1ye-p/Practice-source-code/blob/main/InjectDLL/CreateRemoteThread/Source

远程线程注入

在一个进程中创建远程线程并将其注入到其他进程中的技术。

在其中会遇到两个需要解决的问题,将在下文中依次介绍。

通常使用以下几个API函数配合使用来实现此功能。

LoadLibrary 函数。

1
2
3
HMODULE WINAPI LoadLibraryA(
LPCSTR lpLibFileName
);

其实并不存在 LoadLibrary 函数,跟进WinBase.h头文件中会发现他被定义为 LoadLibraryALoadLibraryW 。二者的区别是如果DLL文件名是以ANSI字符串形式保存的,就用 LoadLibraryA ,如果文件名是以Unicode字符串的形式保存的,那就需要用 LoadLibraryW

使用此函数作为 CreateRemoteThread 的参数进行传递是为了解决ASLR安全机制带来的问题——每次开机DLL的加载基址不固定,导致导出函数的地址发生变化。

因为此函数存在于Kernel32.dll中,有些系统DLL被要求系统启动后必须固定,即各个进程中kernel32.dll的加载基址相同,故获取自身 LoadLibrary 函数的地址后也可以在其他进程中使用。

GetProcAddress 函数

1
2
3
4
FARPROC GetProcAddress(
HMODULE hModule,
LPCSTR lpProcName
);

从指定的动态链接库(DLL)中检索导出函数的地址。

使用此函数去获取 LoadLibrary 函数的地址,是因为直接传地址会出现一些意外情况,具体情况稍后说明。

OpenProcess 函数

1
2
3
4
5
HANDLE OpenProcess(
DWORD dwDesiredAccess,
BOOL bInheritHandle,
DWORD dwProcessId
);

打开现有的本地进程对象。

VirtualAllocEx 函数

1
2
3
4
5
6
7
LPVOID VirtualAllocEx(
HANDLE hProcess,
LPVOID lpAddress,
SIZE_T dwSize,
DWORD flAllocationType,
DWORD flProtect
);

在指定进程的虚拟地址空间内保留、提交或更改内存的状态。

WriteProcessMemory 函数

1
2
3
4
5
6
7
BOOL WriteProcessMemory(
HANDLE hProcess,
LPVOID lpBaseAddress,
LPCVOID lpBuffer,
SIZE_T nSize,
SIZE_T *lpNumberOfBytesWritten
);

在指定的进程中将数据写入内存区域,要写入的整个区域必须可访问,否则操作失败。

使用 OpenProcess 函数、VirtualAllocEx 函数和 WriteProcessMemory 函数是为了解决第二个问题 —— DLL路径字符串位于调用进程的地址空间中,远程线程的内存空间中并不存在该字符串,我们将它和 LoadLibrary 函数作为参数进行传递时,远程进程的线程会在访问该内存空间时因为找不到该字符串而造成访问违规,发生异常。

对应的解决方案即为使用 OpenProcess 函数打开目标进程后,再使用 VirtualAllocEx 在该进程的地址空间中申请内存,最后使用 Write ProcessMemory 函数将DLL路径字符串写入远程进程的地址空间中。

CreateRemoteThread 函数

1
2
3
4
5
6
7
8
9
HANDLE CreateRemoteThread(
HANDLE hProcess,
LPSECURITY_ATTRIBUTES lpThreadAttributes,
SIZE_T dwStackSize,
LPTHREAD_START_ROUTINE lpStartAddress,
LPVOID lpParameter,
DWORD dwCreationFlags,
LPDWORD lpThreadId
);

通过此函数来实现在其他进程中创建线程的功能。除了有个额外的参数hProcess外,它与 CreateThread 完全相同。hProcess 参数用来表示新创建的线程归那个进程所有,lpStartAddress 参数是线程函数的内存地址,通常加载 LoadLibrary 函数的内存地址,至于为什么,上文中已说明。

新线程在远程进程中被创建的时候,会立即调用 LoadLibraryA 函数,并传入DLL路径名的地址。 但这样出现了我们的第一个问题: 我们不能直接将 Loadlibrary 作为第4个参数传给 CreateRemoteThread 。因为二进制文件中包含的导入段是由一系列转换函数(thunk)构成的,由这些转换函数跳转到导入的函数。如果直接作为参数进行传递的话,该引用会被解析为导入段中的 LoadLibrary 转换函数的地址,其结果是:我们不知道远程线程会执行什么代码,也可能会发生访问违规等。

解决的方案是:通过 GetProcAddress 函数来得到 LoadLibrary 函数的确切地址。

CreateRemoteThread 函数更底层调用的是 RtlCreateUserThreadZwCreateThreadEx 函数,该函数可以用来突破Session 0 隔离机制的限制,这个后续文章再讨论。

实现原理

根据 CreateRemoteThread 函数和 LoadLibrary 函数的声明,若我们能够得到 LoadLibrary 函数的地址,并且还能够获取目标进程空间中某个DLL路径字符串的地址,可将 Loadlibrary 函数的地址作为多线程函数的地址,某个DLL路径字符串作为多线程函数的参数,其返回值作为参数传递给 CreateRemoteThread 函数,这样就可以实现在目标进程中创建一个多线程,而这个多线程就是 Load’Library 函数加载的DLL。

大致包括以下几个步骤

  1. 使用 OpenProcess 函数打开目标进程(待注入进程)

  2. 通过调用 VirtualAllocEx 函数在目标/远程进程地址空间中为DLL文件路径开辟内存空间

  3. 调用 WriteProcessMemory 函数在之前所分配的内存空间中写入DLL文件路径

  4. 通过调用 GetProAddress 函数找到 LoadLibrary 函数的地址

  5. 调用 CreateRemoteThread 函数创建一个新的线程,新线程以DLL文件路径名称作为参数来调用 LoadLibrary 函数,DLLMain执行完成返回后会调用 ExitThread 来使远程线程终止。

  6. VirtualFreeEx 函数来释放第2步分配的内存

  7. GetProcAddress 来得到 FreeLibrary 函数在Kernel32.dll中的实际地址。

  8. CreateRemoteThread 函数在远程进程中创建一个线程,让该线程调用 FreeLibrary 函数并在参数中传入远程DLL的句柄(HMODULE)

测试

具体实践一下:

先运行待注入的测试进程。

查看当前进程已加载的dll

运行创建远程线程的进程,提示已经注入成功。

返回查看待注入的进程,可以看到已经加载了自制的DLL模块,并弹出了消息窗口。

小结

我们为什么要选择创建新线程呢?因为我们无法轻易控制其他进程中的线程,然而对于我们自己创建的线程,我们对它执行的代码可以做到很好的控制。

在创建远程线程的时候会遇到两个问题:

一个是我们不能直接传 LoadLibrary 函数的地址,因为它的地址刚开始执行转换函数(thunk),可能会造成访问违规。

另一个是我们在将 LoadLibrary 函数和DLL路径字符串作为参数传递时,DLL路径只存在于调用进程的地址空间,目标进程地址空间中并不存在,线程创建后 LoadLibrary 函数获取要加载的DLL路径时可能会造成访问违规。

在创建远程线程后会立即调用作为参数传递的 lpStartAddress 该地址指向的函数,作为参数的 LoadLibrary 不仅是为了解决ASLR问题,更是为了完成我们最重要的目的——加载我们需要的DLL文件。

运行时也可能会出现这样情况。

这是因为 OpenProcess 函数在打开高权限进程时,程序因为权限不足而无法打开进程,获取进程句柄。以管理员身份运行即可。

参考:

​ 维基百科

《加密与解密》

《Windows黑客编程技术详解》

《Windows核心编程》

《逆向工程核心原理》