WIN32汇编语言教程:第13章 进程控制 · 13.4 进程的隐藏(2)
该函数是CreateThread函数的扩充,与CreateThread相比,CreateRemoteThread函数多了一个hProcess参数,其他所有参数的定义和用法都与CreateThread的参数相同。hProcess用来指定要创建线程的目标进程句柄。注意:lpStartAddress指向的线程函数的地址是位于目标进程的地址空间内的。如果需要在目标进程中使用CreateRemoteThread函数,那么必须对进程拥有PROCESS_CREATE_THREAD权限。
使用VirtualAllocEx和CreateRemoteThread函数,再配合WriteProcessMemory函数,就能够让一段代码在其他进程中运行,由于远程线程是属于目标进程的,所以在任务管理器中不会产生新的进程,事实上,谁也不会发现列出的某个进程中会多了一个不属于它自己控制的线程。整个实现的过程归纳如下:
(1)使用VirtualAllocEx函数在目标进程中申请一块内存,内存块的长度必须能够容纳线程使用的代码和数据,内存块的属性应该是PAGE_EXECUTE_READWRITE,这样拷贝到内存块中的代码就可以被执行。
(2)使用WriteProcessMemory函数将需要在远程线程中执行的代码(包括它使用的数据)拷贝到第(1)步申请到的内存块中。
(3)使用CreateRemoteThread函数创建远程线程。
2. 远程线程存在的技术问题
实现远程线程的框架结构已经搭好了,但是在具体的实现中还有一些技术问题需要解决,归纳起来主要有两点:代码的重定位问题和函数的导入问题。
代码的重定位问题可以用下面的例子来说明:
dwVar dd ?
;####################################################################
Proc1 proc _dwParam
local @dwLocal
mov eax,dwVar
mov eax,@dwLocal
mov eax,_dwParam
ret
Proc1 endp
;####################################################################
invoke Proc1,1234
代码中包括了调用子程序,存取全局变量、局部变量和参数的情况,经过编译链接以后再反汇编,就成了下面的样子:
:00400FFC 0000 ;dwVar变量
:00401000 55 push ebp
:00401001 8BEC mov ebp, esp
:00401003 83C4FC add esp, FFFFFFFC
:00401006 A1FC0F4000 mov eax, dword ptr [00400FFC] ;mov eax,dwVar
:0040100B 8B45FC mov eax, dword ptr [ebp-04] ;mov eax,@dwLocal
:0040100E 8B4508 mov eax, dword ptr [ebp+08] ;mov eax,_dwParam
:00401011 C9 leave
:00401012 C20400 ret 0004
:00401015 68D2040000 push 000004D2
:0040101A E8E1FFFFFF call 00401000 ;invoke Proc1,1234
分析一下机器码就可以发现,存取全局变量的指令mov eax,dwVar中,全局变量的地址是包含在机器码中的(指令的机器码是A1FC0F4000,第一个字节A1h是mov eax,xxx的机器码,后面的FC0F4000按照高字节在后的顺序读就是变量的地址00400FFCh);存取局部变量和参数的指令中并不包含绝对地址;call指令中的地址数据也是相对的,所以,当这段机器码从00401000h地址被搬到00801000h处的时候,就成了下面的样子:
:00800FFC 0000
:00801000 55 push ebp
:00801001 8BEC mov ebp, esp
:00801003 83C4FC add esp, FFFFFFFC
:00801006 A1FC0F4000 mov eax, dword ptr [00400FFC] ;mov eax,dwVar
:0080100B 8B45FC mov eax, dword ptr [ebp-04] ;mov eax,@dwLocal
:0080100E 8B4508 mov eax, dword ptr [ebp+08] ;mov eax,_dwParam
:00801011 C9 leave
:00801012 C20400 ret 0004
:00801015 68D2040000 push 000004D2
:0080101A E8E1FFFFFF call 00801000 ;invoke Proc1,1234
这时候,A1FC0F4000机器码还是被解释为存取00400FFCh地址,而实际的变量地址已经被搬到00800FFCh处了,这就是说,指令存取的是错误的地址,所以这段指令要想正常执行,就必须放在00401000h地址开始的地方,如果想搬到别的地方去执行,就必须对访问全局变量的指令进行修正,这就是重定位的问题
由此可见,如果想把这段指令放到远程线程中去执行,由于无法保证将代码放到00401000h处,所以几乎可以肯定它是不能正常工作的,但是根据代码最后执行的实际位置来修正某些指令的话,在远程线程中执行它还是可行的。
对于高级语言来说,重定位问题是个致命的问题,是根本不可能解决的,因为高级语言无法在机器码级别上进行细微的操作,所以,即使在相对比较低级的C语言中也无法将一段代码拷贝到远程线程中去执行,大部分的教科书和资料在介绍远程线程的时候,都采用了变通的方法,就是将DLL嵌入到目标进程中去执行。
如Jeffrey Richer的《Windows高级编程指南》中就介绍了使用远程线程将DLL注入目标进程的方法,其实实现步骤是将需要远程执行的代码写到一个DLL中,然后在目标进程中申请一块内存并将DLL文件名写入,最后将目标进程地址空间中的LoadLibrary函数当做线程函数来执行,输入的参数就是前面的DLL文件名,这样LoadLibrary函数执行到ret的时候,远程线程结束,但是DLL也被装入了目标进程中,只要在DLL的入口函数中创建一个新的线程,就可以执行我们的代码了,在所附光盘的Chapter13\RemoteThreadDll中的例子演示了这种方法的汇编版本,程序将一个DLL文件插入到文件管理器Explorer.exe中运行,有兴趣的读者可以自己查看一下。
虽然DLL文件在目标进程中运行的时候,任务管理器中不会列出DLL文件名,看到的只是目标进程的文件名,但是有一些工具可以查看一个进程究竟装入了哪些DLL文件,通过这些工具还是可以发现进程中的可疑DLL。
要彻底解决这个问题,就必须脱离DLL文件,让远程运行的代码只存在于内存中,这样就不会有任何的蛛丝马迹显示有某个文件被非法装入,这个问题的关键也就是这个重定位问题。但现在Win32汇编程序员可以很骄傲地说“我可以实现它”,因为自定位的代码正是汇编语言的拿手好戏,在快成为历史的DOS病毒中,十个病毒中就有九个用到了自定位技术,这些技术完全可以用在这个地方。
自定位技术其实很简单,观察下面这段代码:
dwVar dd ?
call @F
@@:
pop ebx
sub ebx,offset @B
mov eax,[ebx + offset dwVar]
翻译成机器码就是:
:00401000 00000000 BYTE 4 DUP(0)
:00401004 E800000000 call 00401009
:00401009 5B pop ebx
:0040100A 81EB09104000 sub ebx, 00401009
:00401010 8B8300104000 mov eax, dword ptr [ebx+00401000]
这段代码不存在重定位的问题,分析如下。
call指令会将返回地址压入到堆栈中,当整段代码在没有移动的情况下执行的时候时,call @F指令执行后堆栈中的返回地址就是@@标号的地址00401009h,下一句pop指令将返回地址弹出到ebx中,再接下来ebx减去00401009h,现在ebx等于0,所以mov eax,[ebx + offset dwVar]指令就等于mov eax,dwVar指令。
当整段代码被移动到其他地方时(假设被移到00801000处执行),@@标号现在对应的地址是00801009h,变量dwVar的地址对应00801000h,当call指令执行后,压入到堆栈的地址是00801009h,pop到ebx中的就是这个数值,经过sub ebx,00401009指令以后,ebx等于00400000h,现在mov eax,dword ptr [ebx+00401000]指令就相当于mov eax,[00801000],而00801000这个地址刚好等于dwVar现在所处的位置,所以,虽然代码被移动了位置,mov eax,dwVar指令还是访问了正确的地方。
call/pop/sub这3个指令组合的用途就是计算出代码当前的位置和设计时位置的偏移值之差,只要用这个差值去修正包含绝对地址的指令,如访问全局变量的指令,就能够保证修正后的地址是正确的,这就解决了重定位的问题。
另一个问题就是函数的导入问题,由于Win32编程不可避免地要用到API函数,而API函数又存在于DLL中,当远程代码要用到一个API函数时,就必须保证目标进程中已经装入了相应的DLL,还必须知道API函数的地址,否则对函数的调用就无从谈起。
所以在设计远程代码的时候,不能直接使用API函数,因为函数的地址在不同的进程中会随着DLL装入位置的不同而不同,如果在代码中直接调用API函数,那么系统会按照当前进程的DLL装入位置填入函数地址,但这个地址搬到远程线程中可能是错误的。
要在远程代码中使用API函数,就必须手动完成本来由系统完成的工作,那就是自己装入每个要使用的DLL,并使用GetProcAddress函数获取全部要使用的API函数的入口地址。由于这个过程要用到DLL文件的名称和函数名称,这些字符串必须放在全局变量中,这就又遇到了重定位的问题(所以在高级语言中实现函数的手动导入也是个很大的麻烦),当然现在这个问题是很容易解决的。