[Win32]关于跨进程模块注入的一次尝试,以及为何尽量不要LoadLibrary一个exe

文章介绍了通过直接将exe作为模块注入目标进程的技术尝试,包括在exe中定义导出函数并注入远程进程。然而,这种方式存在导入表无效、CRT未正确加载、全局变量未初始化以及重定位问题,使得这种方法并不实用。作者建议除非必要,否则不应使用LoadLibrary加载exe进行代码注入。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

通过直接把自身当作一个模块注入到目标进程,实现代码注入

  这是我偶然之间的一个想法,既然通过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函数正在其列。
用IDA获取exe导出表
  这也就是为什么,我们说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函数地址是正确的(目标进程中的地址见上上个图,虽然Test在两个进程中的地址相同,但这是个巧合)。
  那问题出现在哪里呢?我们用x64dbg在目标进程的Test函数上打上断点,跟进,然后就发现了问题。
MessageBoxA是一个无效的地址
  在图中画红框的call命令处,call的是一个无效的地址,这里在源程序中对应的是那句MessageBoxA,也就是说,exe在被当作dll加载后,原本导入表中的函数,就变成了无效的地址

问题分析

  我们在调用一个导入表中的函数(例如MessageBoxA)时,大致的过程是:

  1. 通过MessageBoxA符号,定位到user32.dll中的MessageBoxA地址
  2. 执行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!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值