木马和病毒的好坏很大程度上取决于它的隐蔽性,木马和病毒本质上也是在执行程序代码,如果采用独立进程的方式需要考虑隐藏进程否则很容易被发现,在编写这类程序的时候可以考虑将代码注入到其他进程中,借用其他进程的环境和资源来执行代码。远程注入技术不仅被木马和病毒广泛使用,防病毒软件和软件调试中也有很大的用途,最近也简单的研究过这些东西,在这里将它发布出来。
想要将代码注入到其他进程并能成功执行需要解决两个问题:
- 第一个问题是如何让远程进程执行注入的代码。原始进程有它自己的执行逻辑,想要破坏原来的执行流程,使EIP寄存器跳转到注入的代码位置基本是不可能的
- 第二个问题是每个进程中地址空间是独立的,比如在调用某个句柄时,即使是同一个内核对象,在不同进程中对应的句柄也是不同的,这就需要进行地址转化。
要进行远程代码注入的要点和难点主要就是这两个问题,下面给出两种不同的注入方式来说明如何解决这两个问题
DLL注入
DLL注入很好的解决了第二个问题,DLL被加载到目标进程之后,它里面的代码中的地址就会自动被转化为对应进程中的地址,这个特性是由于DLL加载的过程决定的,它会自己使用它所在进程中的资源和地址空间,所以只要DLL中不存在硬编码的地址,基本不用担心里面会出现函数或者句柄需要进行地址转化的问题。
那么第一个问题改怎么解决呢?
要执行用户代码,在Windows中最常见的就是使用回调的方式,Windows采用的是事件驱动的方式,只要发生了某些事件就会调用回调,在众多使用回调的场景中,线程的回调是最简单的,它不会干扰到目标进程的正常执行,也就不用考虑最后还原EIP的问题,因此DLL注入采用的最常见的就是创建一个远程线程,让线程加载DLL代码。
DLL注入中一般的思路是:使用CreateRemoteThread来在目标进程中创建一个远程的线程,这个线程主要是加载DLL到目标进程中,由于DLL在入口函数(DLLMain)中会处理进程加载Dll的事件,所以将注入代码写到这个事件中,这样就能执行注入的代码了。那么如何在远程进程中执行DLL的加载操作呢?我们知道加载DLL主要使用的是函数LoadLibrary,仔细分析线程的回调函数和LoadLibrary函数的原型,会发现,它们同样都是传入一个参数,而CreateRemoteThread函数正好需要一个函数的地址作为回调,并且传入一个参数作为回调函数的参数。这样就有思路了,我们让LoadLibrary作为线程的回调函数,将对应dll的文件名和路径作为参数传入,这样就可以在对应进程中加载dll了,进一步也就可以执行dllmain中的对应代码了。
还有一个很重要的问题,我们知道不同进程中,地址空间是隔离的,那么我在注入的进程中传入LoadLibrary函数的地址,这算是一个硬编码的地址,它在目标进程中是否是一样的呢?答案是,二者的地址是一样的,这是由于kernel32.dll在32位程序中加载的基地址是一样的,而LoadLibrary在kernel32.dll中的偏移是一定的(只要不同的进程加载的是同一份kernel32.dll)那么不同进程中的LoadLibrary函数的地址是一样的。其实不光是LoadLibrary函数,只要是kernel32.dll中导出的函数,在不同进程中的地址都是一样的。注意这里只是32位,如果想要使用32位程序往64位目标程序中注入,可能需要考虑地址转换的问题,只要知道kernel32.dll在64位中的偏移,就可以计算出对应函数的地址了。
LoabLibrary函数传入的代表路径的字符串的首地址在不同进程中同样是不同的,而且也没办法利用偏移来计算,这个时候解决的办法就是在远程进程中申请一块虚拟地址空间,并将目标字符串写入对应的地址中,然后将对应的首地址作为参数传入即可。
最后总结一下DLL注入的步骤:
- 获取LoadLibrary函数的地址
- 调用VirtualAllocEx 函数在远程进程中申请一段虚拟内存
- 调用WriteProcessMemory 函数将参数写入对应的虚拟内存
- 调用CreateRemoteThread 函数创建远程线程,线程的回调函数为LoadLibrary,参数为对应的字符串的地址
按照这个思路可以编写如下的代码:
卸载远程DLL
上面进行了代码的注入,作为一个文明的程序,自然得考虑卸载dll,毕竟现在提倡环保,谁使用,谁治理。这里既然注入了,自然得考虑卸载。
卸载的思路与注入的类似,只是函数变为了FreeLibrary,传入的参数变成了对应的dll的句柄了。
如何获取这个模块的句柄呢?我们可以枚举进程中的模块,根据模块的名称来找到对应的模块并获取它的句柄。枚举的方式一般是使用toolhelp32中对应的函数,下面是卸载的例子代码
无DLL的注入
注入不一定需要使用DLL,虽然使用DLL比较简单一点,无DLL注入在解决上述两个问题的第一个思路是一样的,也是使用CreateRemoteThread来创建一个远程线程来执行目标代码。
无dll的注入主要麻烦是在进行地址转化上,在调用API的时候,如果无法保证对应的dll的基地址不变的话,就得在目标进程中自行调用LoadLibrary来动态获取函数地址,并调用。
在动态获取API函数的地址的时候,主要使用的函数是LoadLibrary、GetModuleHandle、GetProcAddress这三个函数,而线程的回调函数只能传入一个参数,所以我们需要将对应的需要传入的参数组成一个结构体,并将结构体对应的数据写入到目标进程的内存中,特别要注意的是,里面不要使用指针或者句柄这种与地址有关的东西。
例如我们想在目标进程中注入一段代码,让它弹出一个对话框,以便测试是否注入成功。这种情况除了要传入上述三个函数的地址外,还需要MesageBox,而MessageBox是在user32.dll中,user32.dll在每个进程中的基地址并不相同,因此在注入的代码中需要动态加载,因此可以定义下面一个结构
不使用DLL注入与使用DLL注入的另一个区别是,不使用DLL注入的时候需要自己加载目标代码到对应的进程中,这个操作可以借由WriteProcessMemory 将函数代码写到对应的虚拟内存中。
最后注入的代码主要如下: