关于跨进程模块注入的一次尝试,以及为何尽量不要LoadLibrary一个exe
通过直接把自身当作一个模块注入到目标进程,实现代码注入
这是我偶然之间的一个想法,既然通过CreateRemoteThread函数,可以实现让其他进程去LoadLibrary一个我们指定的dll,从而实现将该dll的代码注入到目标进程。
那是否可以把这个dll省略掉,直接把自身(exe)的路径作为LoadLibrary的参数,让目标进程去加载,并在我们的exe内,像dll那样定义出一些导出函数,后续再通过CreateRemoteThread随时在目标进程内启动呢?
好消息是,这种操作在理论上完全可行。但坏消息是,这并没有我们想象中方便和实用,接下来我会简述自己的实验过程,遇到的问题,然后进行简单的总结。
这里默认读者知道如何利用CreateRemoteThread进行Dll注入,并对此过程不再赘述。
在exe内定义一个导出函数
为了方便,我定义了几个宏。
#define DLLAPI extern "C" __declspec(dllexport)
#define ROUTINE DLLAPI DWORD WINAPI
typedef DWORD(WINAPI* Routine)(LPVOID);
为exe定义一个导出函数的过程,与在dll中是完全一样的,例如,我们定义一个Test函数。
ROUTINE Test(LPVOID lParam)
{
MessageBoxA(0, "(LPCSTR)lParam", "123", 0);
return 20;
}
编译后,我们用IDA检查生成的exe的导出表,可以发现Test函数正在其列。
这也就是为什么,我们说exe没有导出表,要加上“通常”二字。exe并不是不可以有导出表,只不过通常情况下,我们不需要它有。
注入到远程进程
随后,我们用注入dll相似的方法,在这个exe中,让id为9236的进程用LoadLibrary加载自己。
int PID = 9236;
HMODULE hPatch = GetModuleHandle(NULL);
RemoteCode rc(PID, hPatch); //RemoteCode 是我写的一个库,这行的操作就是获取自身的全路径,然后让目标进程加载
在执行完注入的操作后,我们用x64dbg附加到被注入的进程,然后查看它的模块列表。
我们发现,目标进程确实把我们自己的exe当作一个dll去加载了,我们的exe作为一个模块出现在了目标进程的内存空间中,Test函数也作为一个导出函数出现在了它的导出表中。
启动我们的导出函数
一切看起来非常顺利,现在只剩下最后一步,启动我们事先定义的导出函数Test。
rc.invoke("Test", NULL);
具体过程也就是,先获取Test函数在自己进程内的RVA(相对虚拟地址),然后通过枚举目标进程的模块,找到注入的模块的地址,最后把这两个地址相加,就得到了Test函数在目标进程中的地址。
但接下来,奇怪的事情就发生了,在使用CreateRemoteThread在Test的地址上启动一个线程之后,被注入的进程崩溃了。
问题的发现
我首先检查,是不是我在上述的操作中,获取到了一个无效的Test地址?于是我在CreateRemoteThread前打上了一个断电,然后查看获取到的Test函数地址,并将其与x64dbg中显示的Test函数地址对比。
结果说明,我们获取到的Test函数地址是正确的(目标进程中的地址见上上个图,虽然Test在两个进程中的地址相同,但这是个巧合)。
那问题出现在哪里呢?我们用x64dbg在目标进程的Test函数上打上断点,跟进,然后就发现了问题。
在图中画红框的call命令处,call的是一个无效的地址,这里在源程序中对应的是那句MessageBoxA,也就是说,exe在被当作dll加载后,原本导入表中的函数,就变成了无效的地址。
问题分析
我们在调用一个导入表中的函数(例如MessageBoxA)时,大致的过程是:
- 通过MessageBoxA符号,定位到user32.dll中的MessageBoxA地址
- 执行user32.dll中的MessageBoxA代码。
虽说通常情况下,MessageBoxA的地址对于所有进程都是一致的,但这里的“一致”,指的是user32.dll被加载的位置一致,而MessageBoxA的相对地址是一定的,所以我们说MessageBoxA的地址一定。
然而我们想获取这个地址,需要先通过导入表,也就是我们在程序中写的MessageBoxA所指向的地址。我们现在面临的情况是,当exe被LoadLibrary后,MessageBoxA符号所指向的是一个无效的地址。显然,在这种情况下,exe的导入表并没有被正确地配置。
引起该问题的原因是,在对一个exe文件使用LoadLibrary时,系统并不会像对dll做的那样,为我们配置正确的导入表,那么这部分工作就需要我们手动进行,大致过程是遍历目标模块的导入表,然后一个个把他们设置为正确的地址,并且因为操作目标不是本进程,我们就需要不停地VirtualProtectEx,ReadProcessMemory,WriteProcessMemory,非常非常麻烦。对本进程LoadLibrary的exe进行导入表修复的代码可以在网络上找到,这里就不贴出了,如果你有耐心的话,可以尝试把它改成对其他进程有效的。
所以如果你问可行吗,答案是可行。但我都已经写这么多复杂又耗费性能的代码了,为什么不直接写一个dll呢?
总结
这种模块注入方式,在理论上是完全可行的,但实际中遇到的问题不止上述一种,在LoadLibrary一个exe时,不仅导入表会变为无效,这个exe的CRT也没有被正确地加载,也就是说,所有的全局变量都没有被正确地初始化。另外,在生成一个exe文件时,/DYNAMICBASE和/FIXED选项默认是关闭的。虽然在这次实验中,exe的基址在本进程和目标进程中是相同的,但别忘了,这只是一个巧合。如果自己的exe的基址被抢占,它就无法被正确地重定位。
简而言之,除非你有必须LoadLibrary一个exe的理由,否则Don’t do that!