实现DLL注入的方法已经不少了,也都比较成熟,用的最多的方法是经过远程线程进行线程注入,而后导入Dll文件。远程线程里有两个关键技术须要解决,一个是全局变量和字符串的存取问题,另外一个是地址重定位问题。若是用汇编来实现这两个问题是很简单的,但在高级语言中就显得有点笨拙了。之前有一篇用C++来实现这个技术的文章,但那篇文章用了一个变通的方法,即用局部变量来代替全局变量,在创建远程线程的时候把变量传给远程线程。这个方法能够说达到了一举两得的目的,由于局部变量是存在于堆栈中的,不存在绝对地址的调用问题,对局部变量的访问也就不存在地址重定位问题。但回头想一想,那个方法仍是没有可以突破C++的这些限制。笔者在参考了罗云彬的《windows环境下32位汇编语言程序设计》中线程隐藏那一章节后,提出了在C++中实现远程线程的方法。由于本文涉及到不少汇编方面的知识,因此我相信本文的意义不只仅是提出一种DLL注入的方法,更重要的是对编程能力的训练和对加深对操做系统底层的理解。
高级语言的编译过程
C++中的变量分为如下几个状况:全局变量,字符串和局部变量。对于变量咱们在用C++编程中定义了直接调用就能够,历来不用考虑这些变量在生成的二进制文件装入内存后是怎么样存储的,但在进行远程线程的操做时这些都是必须考虑的。实际上在编译时,编译器对变量和代码有着不一样的处理,全局变量和字符串被放在了数据段中,局部变量存放于堆栈中,代码位于代码段中,而数据段,堆栈区,代码段在内存中是独立存放的,它们的地址并不连续。在编译时对全局变量和字符串的存取被修改成对绝对地址的调用,这个绝对地址是编译时肯定的,在程序运行时由操做系统负责在该地址处分配内存并进行初始化。而编译器对局部变量的处理是将其修改成对堆栈的操做,这样局部变量的地址实际上依赖于栈寄存器,栈寄存器不一样,其绝对地址也不一样。
另外,高级语言中调用函数时,编译器做的处理以下:
1.将须要传递的参数反向压入堆栈;
2.用call语句调用函数地址,执行该函数;
3.函数执行完成后恢复堆栈的平衡;
在上面三步的第二步中又能够分为几个小步:
1.把本句call指令的下一指令地址压入堆栈;
2.转到函数地址执行;
3.当遇到ret之类的返指令时从栈中弹出返回地址;
4. 转到弹出的地址(也就是call语句的下句指令)继续执行。
如今在拿LoadLibrary来举个例子,该函数是windows中的一个API,位于Kernel32.dll中,其默认装载地址为7C801D7B。由于Kernel32.dll是常驻内存的,因此在通常状况下内存中的这个地址就是LoadLibrary的入口地址,但这不是绝对的。在C++中要调用这个函数咱们能够这样写: 编程
LoadLibrary(”Dll.dll”);
在汇编中就得这样写了:windows
Push 字符串”DLL.dll”在内存中的地址,一般这个地址位于数据区 Call LoadLibrary的入口地址,如7C801D7B
由于Windows中的API皆为标准调用约定,因此恢复堆栈的工做就由被调用的函数完成了。固然在C++中自定义的函数通常为C调用约定,若是调用本身写的函数你就要考虑恢复堆栈的问题了。最后说明一点,在汇编中调用API的返回值通常会放在寄存器eax中,因此要检测函数是否调用成功只要检测eax的值就能够了。
高级语言中远程线程遇到的问题
首先咱们来回想一下远程线程的实现过程:
1.在程序中的某个地方写出远程线程代码;
2.用WriteProcessMemory将上步写的远程线程代码复制到目标进程;
3.用CreateRemoteThread创建远程线程,并使其运行。
如今仔细分析一下,不妨假设本进程为进程A,要注入的宿主进程为进程B。第一步中所写的远程线程代码位于A中,此时用到的全局变量和字符串在编译时生成的绝对地址是按进程A肯定的,这些地址位于进程A的数据区,第二步复制代码时,咱们把代码区的数据完整的复制到了进程B中,而数据区并无被复制。被复制过来的代码要运行时一样要访问那些全局变量和字符串的绝对地址,在进程B中那个地址可能已经被其它进程占用,也多是一些随机的数据,这样会形成访问出错。在汇编语言中能够在代码区中申请变量空间,在复制代码时这些变量也相应被复制过去,这样就不存在数据区绝对地址访问的问题。但C++中是不容许在代码区中申请变量空间的,因此如何让变量随代码一块儿被复制是第一个须要解决的问题。
其二,假如咱们己经在代码区中成功的存放了变量。但编译器在编译时把对这个变量的存取修改成绝对地址的存取,这个地址一样是位于进程A中的。在进程B中申请内存空间时,这个空间的地址是不肯定的,这样代码被复制到进程B中后对这个地址的存取仍是会出错,其原理和上面是同样的。因此这就须要咱们对这些变量进行地址重定位。
远程线程代码的实现
由于咱们的目的就是要把本身的DLL文件导入到目标进程中,因此远程线程的代码相对来讲比较简单,就是调用LoadLibrary,也就是上面举的例子中的代码。咱们要在代码中保存的变量有两个,一个是LoadLibrary的地址(虽然说这个地址基本上是固定的,但为了保险起见咱们仍是动态获取,并将其保存,不然就不须要这个变量了),另外一个是保存DLL文件名的字符串。为了在写远程线程代码时,在代码区中为这两个变量分配内存,咱们能够用空指令给这两个变量占位,复制到进程B中后再把它修改成真正的值。如:API的地址占四个字节,汇编中一个空指令nop占一个字节,因此咱们就用四个nop来为其“申请”空间;咱们的DLL文件名为”Dll.dll”,占七个字节,考虑到字符要以0做为结尾标记,共占8个字节,因此咱们用8个nop来“申请”空间。
下一个问题是解决地址重定位问题,这个技术在病毒,木马等诸多方面有着普遍的应用,固然这个技术并非由笔者实现,笔者也是经过学习获得的,在这里也简要介绍一下实现的原理。
先看下面这段代码:函数
1 call relocal 2 relocal: 3 pop ebx 4 sub ebx , offset relocal
如今细细分析一下。第一句话执行时首先会把第三句运行时的地址(注意是运行时的地址,不是绝对地址,这个地址在进程A中与在进程B中是不同的)压入堆栈,而后执行第三句,而第三句又把该地址弹出到寄存器ebx。第四句的offset relocal,它在编译时被编译器修改成进程A中第三句的绝对地址,若是如今该段代码运行于进程B中,第四句相减执行完后ebx并非0,而是这段代码在进程A中的地址偏移与在进程B中地址偏移之差!获得这个差值后,在进程B中每当访问含有绝对地址的变量时只要加上这个差值就能够获得正确地址。
好了,关键技术实现后远程线程的代码以下:学习
REMOTE_THREAD_BEGIN: //远程线程代码开始标记 _asm { //*******给LoadLibrary函数地址占位******* LoadLibraryAddr: nop nop nop nop //*******给FreeLibrary函数地址占位******* FreeLibraryAddr: nop nop nop nop //*******给动态连接库名占位******* LibraryName: nop nop nop nop nop nop nop nop //*******代码开始的真正位置******* REMOTE_THREAD_CODE: //*******实现地址重定位,ebx保存差值******* call relocal relocal: pop ebx sub ebx , offset relocal //*******1.调用LoadLibrary******* //*******1.1.压入LoadLibrary参数(动态连接库名)******* mov eax , ebx add eax , offset LibraryName //变量地址加上ebx,实现地址重定位 push eax //*******1.2.调用LoadLibrary******* mov eax , ebx add eax , offset LoadLibraryAddr //一样实现地址重定位 mov eax , [eax] //从变量中取出LoadLibrary的地址 call eax //*******1.3.检测是否成功,若是失败了就直接返回,防止程序异常******* or eax , eax jnz NEXT1 //执行成功,跳转到位NEXT1继续执行 ret NEXT1: // *******2.释放动态连接库******* // *******2.1.压入FreeLibrary参数******* push eax // *******2.2.调用FreeLibrary******* mov eax , ebx add eax , offset FreeLibraryAddr //地址重定位 mov eax , [eax] //从变量中取出FreeLibrary的地址 call eax ret } REMOTE_THREAD_END:
由于DLL文件在第一次被导入时会自动执行DllMain中的代码,因此咱们把DLL中本身写的代码放在这个函数中,这样只要DLL文件被导入就能够执行代码了;若是不这样,咱们还必须去获取DLL文件中的函数地址,那样会加大工做量,固然若是你不怕麻烦也能够去试试。
主程序的实现
其实远程线程代码实现后,本节的技术含量就相对低得多了。本段代码主要实现如下几个功能:
1.在宿主进程中申请代码空间;
2.把远程线程的代码复制到宿主进程中;
3.修正远程线程变量的值;
4.建立远程线程,使远程代码执行。
关键代码以下:测试
//*******1. 在宿主进程中申请代码空间******* //*******1.1. 经过进程ID打开进程句柄,并得到进程句柄******* HANDLE hSelectedProcHandle; //保存宿主进程句柄 hSelectedProcHandle = OpenProcess(PROCESS_ALL_ACCESS , FALSE , nSelectedThreadId); //进程ID的获取方法,完整的源代码中有介绍,这里就不介绍了 //*******1.2.获得远程线程代码长度,目的是获得要申请的空间的大小****** int nRemoteThreadCodeLength; //保存代码长度 _asm { mov eax , offset REMOTE_THREAD_END mov ebx , offset REMOTE_THREAD_BEGIN sub eax , ebx //用代码结尾偏移减去开始的偏移,获得代码长度 mov nRemoteThreadCodeLength , eax } //*******1.3.在宿主进程中申请空间******* LPVOID pRemoteThreadAddr; //保存申请空间的基址 pRemoteThreadAddr = VirtualAllocEx(hSelectedProcHandle , NULL , nRemoteThreadCodeLength , MEM_COMMIT,PAGE_EXECUTE_READWRITE); //*******2.把远程线程的代码复制到宿主进程******* //*******2.1.获得本进程中远程线程代码的起始地址******* LPVOID pRemoteThreadCodeBuf; //指向本进程中远程线程代码的起始位置 DWORD nWritenNum , nSuccess; //临时变量 _asm mov eax , offset REMOTE_THREAD_BEGIN _asm mov pRemoteThreadCodeBuf , eax //*******2.2.向宿主进程中复制代码******* nSuccess = WriteProcessMemory(hSelectedProcHandle , pRemoteThreadAddr , pRemoteThreadCodeBuf , nRemoteThreadCodeLength , &nWritenNum); // *******3.修正远程线程中变量的值******* // *******3.1.首先获取两个关键函数的地址******* HMODULE hKernel32; hKernel32 = LoadLibrary("Kernel32.dll"); LPVOID pLoadLibrary , pFreeLibrary; pLoadLibrary = (LPVOID)GetProcAddress(hKernel32 , "LoadLibraryA"); pFreeLibrary = (LPVOID)GetProcAddress(hKernel32 , "FreeLibrary"); // *******3.2.修正代码******* PBYTE pRemoteAddrMove; //在远程线程地址上移动的指针 pRemoteAddrMove = (PBYTE)pRemoteThreadAddr; // *******3.2.1.修正LoadLibrary地址******* nSuccess = WriteProcessMemory(hSelectedProcHandle , pRemoteAddrMove , &pLoadLibrary , 4 , &nWritenNum); //*******3.2.2.修正FreeLibrary地址******* pRemoteAddrMove +=4; //定位到保存FreeLibrary的变量 nSuccess = WriteProcessMemory(hSelectedProcHandle , pRemoteAddrMove , &pFreeLibrary , 4 , &nWritenNum); //*******3.2.3.修正动态连接库名******* char szDllName[8] = {"Dll.dll"}; //注意这里必须是8个字符, //而且必须与你的DLL文件名相同 pRemoteAddrMove +=4; nSuccess = WriteProcessMemory(hSelectedProcHandle , pRemoteAddrMove , szDllName , 8 , &nWritenNum); //*******4.建立远程线程,使远程代码执行******* //*******4.1.把指针移动到远程线程代码开始处******* pRemoteAddrMove +=8; HANDLE hRemoteThreadHandle; //远程线程句柄 // *******4.2.定义远程线程函数类型******* typedef unsigned long (WINAPI *stRemoteThreadProc)(LPVOID); stRemoteThreadProc pRemoteThreadProc; // *******4.3.把入口地址赋给声明的函数******* pRemoteThreadProc = (stRemoteThreadProc)pRemoteAddrMove; //*******4.4.建立远程线程******* hRemoteThreadHandle = CreateRemoteThread(hSelectedProcHandle , NULL , 0 , pRemoteThreadProc , 0 , 0 , NULL);
由于本模块主要是调用一些API,这些API去查下资料就能够知道其用法,因此这里就不作详细介绍了。所附源代码为一个基于对话框的MFC程序,里面还有一个获取当前系统进程的模块,这里就不介绍其实现过程了。另外还附带一个简单的DLL工程做为测试。在运行程序时必定要把DLL文件放到系统搜索路径中,不然会因找不到DLL文件而失败。
至此全部功能模块已经介绍完毕,整体来讲,这个方法实现了咱们的预期功能,它的不足之处是实现起来比较繁琐,但从学习的角度来讲不失为一个好方法。若文中有说的不到之处,还请各位高手们批评指正。操作系统