远程代码/进程注入和重定位
一种将整个可执行文件注入到另一个进程的方法(从而避免了对 DLL 的需求)

引言
网上已经有很多关于远程代码或 DLL 注入的指南。远程代码注入通常对注入的代码有严格的限制,而 DLL 注入自然需要一个 DLL。
在这里,我将介绍一种方法,可以说它在功能上等同于 DLL 注入,但它不需要 DLL。简而言之,它获取当前进程的一个映像,在远程进程中为其分配内存,进行重定位,复制映像,然后使用众所周知的 CreateRemoteThread
来转移控制权。这有点像在一个进程中运行两个进程。
这主要是一个概念验证,因为我之前没有在别处见过。因此,我不能保证它在所有情况下都能正常工作,但在我的有限测试中,它一直工作得很好。在示例中,我使用 CreateRemoteThread
将控制权转移到注入映像的入口点,因为这是最简单的方法(而且,你还能免费获得一个线程)。还有其他方法,但这超出了本文的范围。
Using the Code
代码假定该进程是一个 PE 可执行文件,并且可以在模块基址(通过 GetModuleHandle(0)
找到)找到其头。它还假定从模块基址到该模块最后一个节末尾的内存是连续的。这应该是一个可以安全地假定的假设,除非你正在做一些非常奇怪的事情。
该示例仅适用于 x86 32 位可执行文件,并且我仅使用 Visual C++ 9.0 进行过测试(这取决于 _mainCRTStartup
是否可用)。
入口点 & 映像
代码获取当前进程(即主模块)的一个映像。这个映像从模块的基地址一直到其最后一个节的末尾。虽然理论上可以在注入之前的任何时间获取映像,但最好在 *任何* 其他操作之前完成,主要是因为映像中存储的任何指向已分配内存的指针在注入的进程中都将无效。因此,如果我们希望在注入的进程中使用 CRT(C 运行时库)(我们当然希望如此),我们必须在 CRT 初始化之前,即在调用 main()
之前获取映像。
所以我们需要创建自己的入口点并将其指定给链接器。入口点必须在调用 CRT 的入口点(最终会调用 main()
)之前获取映像。VC++9.0 中 CRT 的入口点称为 _mainCRTStartup
,因此示例的入口点如下所示:
extern "C" void mainCRTStartup();
void start() {
hmodule = GetModuleHandle(0);
// take image before CRT is initialized
take_image();
mainCRTStartup();
}
由于映像获取得如此之早,有两个简单的函数 image_set
和 image_copy
,它们稍后用于更改映像中的变量。
注入
inject()
例程将映像注入到进程中。它首先在目标进程中分配内存,将映像重定位到该地址,将其复制过去,然后使用 CreateRemoteThread
将控制权转移给它。
最重要的一步是重定位。这是必需的,因为映像包含大量对内存地址的引用(在指令和数据中)。为此,代码使用了重定位节,Windows 在加载模块时(除非它们被加载到其期望的基地址)会使用该节来重定位模块。某些链接器会从可执行文件中剥离重定位节,原因可能是它认为该节总是首先被加载(因此总是会获得其期望的地址),或者因为它被指定为不支持重定位(VC++ 中的固定基地址)。VC++9.0 默认不会剥离重定位节。
如果示例模块没有重定位节,它就会崩溃。
注入完成后,实际上还有一件事需要注意。IAT(导入地址表),其中包含指向所有导入函数的指针,位于映像中,但在注入后无法保证这些指针的有效性。因此,如果可执行文件从外部模块导入任何函数,则这些指针在注入的进程中很可能无效。主要的例外是来自 kernel32.dll 的导入。kernel32 保证在每个进程中都已加载,并且在相同的地址空间中,因此 IAT 中指向 kernel32 的任何地址仍然是有效且可用的。user32.dll 也在每个进程中以相同的地址加载,但它不保证实际在每个进程中都已加载,因此只有在调用它中的任何函数之前执行 LoadLibrary("user32.dll")
后,使用它中的函数才是安全的。
解决此问题的一种方法是遍历 IAT,并在注入后加载每个模块和导入,但这有风险:模块可能不在当前搜索路径中,或者在注入时根本不可用。
尽管如此,我在示例中包含了一种尝试在注入后加载 IAT 的方法。它会静默忽略错误,因此如果无法加载某些内容,它很有可能会崩溃。它仅包含用于完整性。
最好只使用 kernel32 中的函数,然后使用 LoadLibrary
/GetProcAddress
手动加载任何其他导入。或者,如果您的链接器支持,也可以使用延迟加载的 DLL。
哦,而且似乎最好总是选择 VC++ 下的“在静态库中使用 MFC”选项;否则它总是会崩溃。:)
结论
虽然将整个可执行文件重定位到另一个进程空间乍一看可能很复杂,但实际上相当简单,并且不需要太多代码。避免需要单独的 DLL,同时保留注入整个模块而不是一小段代码的优势,对我来说非常方便。
历史
- 2009 年 9 月 18 日:初始帖子