win32asm编程:进程的隐藏
来源(null)
来源:电子工业出版社
作者:罗云彬
进程隐藏技术用得最多的地方就是在病毒和木马中,因为这些不适合出现在阳光下的程序,越隐蔽生存率就越高。在当今Windows环境下,病毒和木马流传得越来越广泛,让读者适当了解这方面的技术可以在防治方面起到积极的作用,技术这种东西就是这样,大家都知道的“秘技”也就不再是“秘技”了,所以,大家都知道了进程隐藏是怎么一回事,进程隐藏起来也就不那么隐蔽了。
13.4.1
在Windows 9x中隐藏进程
在Windows
9x系列操作系统中,可以通过Kernel32.dll中的一个未公开函数来完成隐藏功能,这个函数就是RegisterServiceProcess,该函数的功能是将一个进程注册为系统服务进程,由于Windows的任务管理器并不列出系统服务进程,所以可以用它来隐藏进程,不过该函数在Windows
NT系列中并不存在。
RegisterServiceProcess函数的使用方法是:
invoke RegisterServiceProcess,dwProcessID,dwFlag
|
dwProcessID指明目标进程的进程ID,参数dwFlag指定是注册还是撤销,指定TRUE的话,进程被注册为系统服务进程,如果指定为FALSE,则进程的属性恢复为普通进程属性。
Kernel32.lib导入库中并没有这个函数的导入信息,如果要使用这个函数,程序需要自己装入库文件并使用GetProcAddress函数获取入口地址后使用(方法请复习第11章)。所附光盘的Chapter13\HideProcess9x目录中的例子程序演示了该函数的使用方法。汇编源代码HideProcess9x.asm的内容如下:
.386 .model flat,stdcall option casemap:none ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> ; Include 文件定义 ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> include windows.inc include user32.inc includelib user32.lib include kernel32.inc includelib kernel32.lib ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> ; 数据段 ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> .const szFunction db 'RegisterServiceProcess',0 szDllKernel db 'Kernel32.dll',0 szText db '现在请按下Ctrl+Alt+Del调出任务管理器查看是否存在本进程',0 szCaption db '在Windows 9x中隐藏进程',0 szErr db '本功能只在Windows 9x中提供',0 ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> ; 代码段 ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> .code start: invoke GetModuleHandle,offset szDllKernel .if eax invoke GetProcAddress,eax,offset szFunction .if eax mov ebx,eax push TRUE invoke GetCurrentProcessId push eax call ebx invoke MessageBox,NULL,offset szText,\ offset szCaption,MB_OK jmp @F .endif .endif invoke MessageBox,NULL,offset szErr,NULL,\ MB_OK or MB_ICONWARNING @@: invoke ExitProcess,NULL ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> end start
|
由于可以确认Kernel32.dll库已经被装载到进程的地址空间中(GetProcAddress等函数就包括在这个库中,因此这个库肯定已经被装入),所以例子中使用GetModuleHandle函数而不是使用LoadLibrary函数来获取库句柄,这样就可以省去一个FreeLibrary的调用。接下来,程序使用GetProcAddress函数获取RegisterServiceProcess函数的入口地址,如果获取成功,则使用GetCurrentProcessId函数获取当前进程的ID并将这个ID注册为系统服务进程。
RegisterServiceProcess的缺点就是只能“欺骗”Windows的任务管理器,使用快照函数还是可以将全部进程枚举出来,即使是13.3.1节中的ProcessList.exe例子,也可以发现用RegisterServiceProcess隐藏的进程。所以使用这个函数只能实现简单的进程隐藏功能。相比之下,Windows
NT下远程线程的功能就要强大得多了。
13.4.2 Windows
NT中的远程线程
在Windows
9x中将进程注册为系统服务进程就能够从任务管理器中隐形,但在NT下就不同了。首先,NT下不存在RegisterServiceProcess函数;其次,NT的任务管理器会列出所有的进程(包括系统进程),即使一个进程将自己的可执行文件放在很隐蔽的目录中,文件名还是会被任务管理器列出来,所以想让别人看不见进程是不可能的。
当然,如果不用进程也能运行程序的话,那是最好不过的办法了,但是不用进程是无法执行文件的。
再从另一个角度考虑,如果进程显示的不是正确的名称呢,这也可以起到掩护作用,如果在DLL中执行我们的代码,系统报告的进程名称是装入DLL的进程的名称,而不是DLL本身的名称。
在Windows
NT中还有另一种办法,那就是使用远程线程,使用它可以在其他进程中创建一个线程,由于线程是被所属进程拥有的,所以任务管理器中列出来的还是所属进程的名称。
1.
Windows
NT的远程操作函数
有两个函数可以用来实现上述功能:VirtualAllocEx和CreateRemoteThread。这两个函数都只能在Windows
NT下使用。
VirtualAllocEx函数可以用来在其他进程的地址空间内申请内存,当然申请到的内存也是位于目标进程的地址空间内的,将这个函数和WriteProcessMemory函数配合就可以在目标进程的地址空间中“造”出任何东西来。
VirtualAllocEx函数的用法是:
invoke VirtualAllocEx,hProcess,lpAddress,dwSize,\ flAllocationType,flProtect .if eax mov lpMemory,eax .endif
|
在10.1.5节中已经介绍过虚拟内存管理函数VirtualAlloc,VirtualAllocEx函数就是这个函数的扩充,相比之下,VirtualAllocEx函数多了一个参数hProcess,其他参数定义和使用的方法都和VirtualAlloc函数相同,读者可以回过头去查看这些参数的用法。新增的hProcess参数用来指定要申请内存的进程句柄,如果需要在目标进程中使用VirtualAllocEx函数,那么必须对进程拥有PROCESS_VM_OPERATION权限。
如果内存申请成功,函数返回一个指针,指向申请到的内存块,当然这个指针是针对目标进程的地址空间的。如果内存申请失败,函数返回NULL。
CreateRemoteThread函数用来在其他进程内创建一个线程,当然创建的线程是运行于目标进程的地址空间内的,它和目标进程自己创建的线程并没有什么区别。函数的用法是:
invoke CreateRemoteThread,hProcess,lpThreadAttributes,dwStackSize,\ lpStartAddress,lpParameter,dwCreationFlags,lpThreadId
|
该函数是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文件的名称和函数名称,这些字符串必须放在全局变量中,这就又遇到了重定位的问题(所以在高级语言中实现函数的手动导入也是个很大的麻烦),当然现在这个问题是很容易解决的。
3.
远程线程的具体实现
好了,经过这么长时间的纸上谈兵,现在动真格的(本节中的所有源程序都可以在所附光盘的Chapter13\RemoteThread目录中找到),还记得第4章中的窗口例子吗?我们的目标就是将这个创建窗口的程序整个搬到Windows的文件管理器Explorer.exe进程中去执行。选定文件管理器开刀的原因是,它是Windows的“常任理事”,这个进程任何时刻都在运行,所以不必担心找不到它。
读者可能会问,难道连包含消息循环、窗口过程的代码都可以放到远程线程中去执行吗?当然可以,因为第12章中已经介绍过,每个线程的消息队列是独立的,远程线程的消息循环并不会和Explorer.exe程序原来的消息循环互相混淆。
工作的第一步就是设计远程线程使用的代码,代码中必须包括一个线程函数用做远程线程执行的入口,在线程函数中必须完成所有所需DLL的装入工作和API函数地址的获取工作,然后调用创建窗口的主程序;第二步就是修改所有访问全局变量的代码,以解决重定位问题。
完工后的远程代码存放在RemoteCode.asm文件中:
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> REMOTE_CODE_START equ this byte
_lpLoadLibrary dd ? ;导入函数地址表 _lpGetProcAddress dd ? _lpGetModuleHandle dd ?
_lpDestroyWindow dd ? _lpPostQuitMessage dd ? _lpDefWindowProc dd ? _lpLoadCursor dd ? _lpRegisterClassEx dd ? _lpCreateWindowEx dd ? _lpShowWindow dd ? _lpUpdateWindow dd ? _lpGetMessage dd ? _lpTranslateMessage dd ? _lpDispatchMessage dd ?
_hInstance dd ? _hWinMain dd ? _szClassName db 'RemoteClass',0 _szCaptionMain db 'RemoteWindow',0 _szDestroyWindow db 'DestroyWindow',0 _szPostQuitMessage db 'PostQuitMessage',0 _szDefWindowProc db 'DefWindowProcA',0 _szLoadCursor db 'LoadCursorA',0 _szRegisterClassEx db 'RegisterClassExA',0 _szCreateWindowEx db 'CreateWindowExA',0 _szShowWindow db 'ShowWindow',0 _szUpdateWindow db 'UpdateWindow',0 _szGetMessage db 'GetMessageA',0 _szTranslateMessage db 'TranslateMessage',0 _szDispatchMessage db 'DispatchMessageA',0 _szDllUser db 'User32.dll',0 ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> _RemoteThread proc uses ebx edi esi lParam local @hModule
call @F @@: pop ebx sub ebx,offset @B ;******************************************************************** _invoke [ebx + _lpGetModuleHandle],NULL mov [ebx + _hInstance],eax lea eax,[ebx + offset _szDllUser] _invoke [ebx + _lpGetModuleHandle],eax mov @hModule,eax lea esi,[ebx + offset _szDestroyWindow] lea edi,[ebx + offset _lpDestroyWindow] .while TRUE _invoke [ebx + _lpGetProcAddress],@hModule,esi mov [edi],eax add edi,4 @@: lodsb or al,al jnz @B .break .if ! byte ptr [esi+1] .endw ;******************************************************************** call _WinMain ret
_RemoteThread endp ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> _ProcWinMain proc uses ebx edi esi,hWnd,uMsg,wParam,lParam
call @F @@: pop ebx sub ebx,offset @B ;******************************************************************** mov eax,uMsg .if eax == WM_CLOSE _invoke [ebx + _lpDestroyWindow],hWnd _invoke [ebx + _lpPostQuitMessage],NULL ;******************************************************************** .else _invoke [ebx + _lpDefWindowProc],hWnd,\ uMsg,wParam,lParam ret .endif ;******************************************************************** xor eax,eax ret
_ProcWinMain endp ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> _ZeroMemory proc _lpDest,_dwSize
push edi mov edi,_lpDest mov ecx,_dwSize xor eax,eax cld rep stosb pop edi ret
_ZeroMemory endp ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> _WinMain proc uses ebx esi edi _lParam local @stWndClass:WNDCLASSEX local @stMsg:MSG call @F @@: pop ebx sub ebx,offset @B ;******************************************************************** invoke _ZeroMemory,addr @stWndClass,sizeof @stWndClass _invoke [ebx + _lpLoadCursor],0,IDC_ARROW mov @stWndClass.hCursor,eax push [ebx + _hInstance] pop @stWndClass.hInstance mov @stWndClass.cbSize,sizeof WNDCLASSEX mov @stWndClass.style,CS_HREDRAW or CS_VREDRAW lea eax,[ebx + offset _ProcWinMain] mov @stWndClass.lpfnWndProc,eax mov @stWndClass.hbrBackground,COLOR_WINDOW + 1 lea eax,[ebx + offset _szClassName] mov @stWndClass.lpszClassName,eax lea eax,@stWndClass _invoke [ebx + _lpRegisterClassEx],eax ;******************************************************************** ; 建立并显示窗口 ;******************************************************************** lea eax,[ebx + offset _szClassName] lea ecx,[ebx + offset _szCaptionMain] _invoke [ebx + _lpCreateWindowEx],WS_EX_CLIENTEDGE,eax,ecx,\ WS_OVERLAPPEDWINDOW,\ 100,100,600,400,\ NULL,NULL,[ebx + _hInstance],NULL mov [ebx + _hWinMain],eax _invoke [ebx + _lpShowWindow],[ebx + _hWinMain],SW_SHOWNORMAL _invoke [ebx + _lpUpdateWindow],[ebx + _hWinMain] ;******************************************************************** ; 消息循环 ;******************************************************************** .while TRUE lea eax,@stMsg _invoke [ebx + _lpGetMessage],eax,NULL,0,0 .break .if eax == 0 lea eax,@stMsg _invoke [ebx + _lpTranslateMessage],eax lea eax,@stMsg _invoke [ebx + _lpDispatchMessage],eax .endw ret
_WinMain endp REMOTE_CODE_END equ this byte REMOTE_CODE_LENGTH equ offset REMOTE_CODE_END - offset REMOTE_CODE_START ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
|
在上面的代码中,所有对API函数的调用被换成了对函数入口地址的调用,因为入口地址被存放在全局变量中,所以要用call
[ebx +
XXXX]的格式调用以解决重定位问题,但是这样的话就无法使用invoke伪指令了。
因为用一大堆的push指令来压入参数太麻烦,笔者写了一个宏来自动压入参数,宏的名称定为_invoke,宏定义存放在Macro.inc文件中。文件的内容是:
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> ; 将参数列表的顺序翻转 ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> reverseArgs macro arglist:VARARG local txt,count
txt TEXTEQU <> count = 0 for i,<arglist> count = count + 1 txt TEXTEQU @CatStr(i,<!,>,<%txt>) endm if count GT 0 txt SUBSTR txt,1,@SizeStr(%txt)-1 endif exitm txt endm ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> ; 建立一个类似于 invoke 的 Macro ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> _invoke macro _Proc,args:VARARG local count
count = 0 % for i,< reverseArgs( args ) > count = count + 1 push i endm call dword ptr _Proc endm ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
|
远程代码中的线程函数是_RemoteThread,在这里程序首先获取要用到的API函数的地址,所有API函数的名称被放到了一系列相连的字符串中,最后以一个附加的0结束,这样可以很方便地通过循环来处理它们。
要获取函数地址必须使用LoadLibrary,GetProcAddress和GetModuleHandle函数,但这些函数地址又从哪里得到呢(这就好像一个“先有鸡,还是先有蛋”的问题),幸亏这些函数都存在于Kernel32库中,Kernel32.dll库文件和User32.dll,Gdi32.dll一样,都是最常用的库,在不同的进程中,系统会将它们装入到同样的地址中,所以对于它们来说,在本地进程中获取的地址可以用在远程线程中。
完成获取API函数地址的工作后,就可以调用_WinMain函数来创建窗口了,注意在_WinMain函数的后面不能使用ExitProcess函数来结束进程,这样会将整个Explorer.exe结束掉,必须使用ret指令来结束线程。
_WinMain函数和其他的相关代码改编自第4章中的FirstWindow.asm程序,只不过是将程序中所有涉及全局变量的指令全部改成了以ebx为基址的指令而已。另外,在所有的子程序的开始处,都加上了call/pop/sub这3句用来计算偏移差的指令。
完成远程线程的代码后,现在来看如何将这段代码装载到目标进程中,装载代码存放在RemoteThread.asm中:
.386 .model flat, stdcall option casemap :none ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> ; Include 文件定义 ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> include windows.inc include user32.inc includelib user32.lib include kernel32.inc includelib kernel32.lib include Macro.inc ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> ; 数据段 ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> .data ? 1pLoadLibrary dd ? 1pGetProcAddress dd ? 1pGetModu1eHandle dd ? dwProcessID dd ? dwThreadID dd ? hProcess dd ? lpRemoteCode dd ? dwTemp dd ? .const szErrOpen db '无法打开远程线程!',0 szDesktopClass db 'Progman',0 szDesktopWindow db 'Program Manager',0 szDllKernel db 'Kernel32.dll',0 szLoadLibrary db 'LoadLibraryA',0 szGetProcAddress db 'GetProcAddress',0 szGetModuleHandle db 'GetModuleHandleA',0 ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> .code ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> include RemoteCode.asm start: invoke GetModuleHandle,addr szDllKernel mov ebx,eax invoke GetProcAddress,ebx,offset szLoadLibrary mov lpLoadLibrary,eax invoke GetProcAddress,ebx,offset szGetProcAddress mov lpGetProcAddress,eax invoke GetProcAddress,ebx,offset szGetModuleHandle mov lpGetModuleHandle,eax ;******************************************************************** ; 查找文件管理器窗口并获取进程ID,然后打开进程 ;******************************************************************** invoke FindWindow,addr szDesktopClass,addr szDesktopWindow invoke GetWindowThreadProcessId,eax,offset dwProcessID mov dwThreadID,eax invoke OpenProcess,PROCESS_CREATE_THREAD or PROCESS_VM_WRITE\ PROCESS_VM_OPERATION,FALSE,dwProcessID .if eax mov hProcess,eax ;******************************************************************** ; 在进程中分配空间并将执行代码拷贝过去,然后创建一个远程线程 ;******************************************************************** invoke VirtualAllocEx,hProcess,NULL,REMOTE_CODE_LENGTH,MEM_COMMIT,\ PAGE_EXECUTE_READWRITE .if eax mov lpRemoteCode,eax invoke WriteProcessMemory,hProcess,lpRemoteCode,\ offset REMOTE_CODE_START,REMOTE_CODE_LENGTH,\ offset dwTemp invoke WriteProcessMemory,hProcess,lpRemoteCode,\ offset lpLoadLibrary,sizeof dword * 3,offset dwTemp mov eax,lpRemoteCode add eax,offset _RemoteThread - offset REMOTE_CODE_START invoke CreateRemoteThread,hProcess,NULL,0,eax,0,0,NULL invoke CloseHandle,eax .endif invoke CloseHandle,hProcess .else invoke MessageBox,NULL,addr szErrOpen,NULL,MB_OK or MB_ICONWARNING .endif invoke ExitProcess,NULL ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> end start
|
在程序开始,首先获取LoadLibrary,GetProcAddress和GetModuleHandle函数的入口地址,这些地址将在远程线程中被用来获取其他API的入口地址。
接下来就是打开Explorer.exe进程的操作,程序通过GetWindowThreadProcessId和OpenProcess函数来完成,函数中使用的窗口句柄是桌面的窗口句柄,因为桌面就是由文件管理器进程创建的,桌面的窗口类是“Progman”,窗口名称是“Program
Manager”,使用FindWindow函数就可以很方便地找到它。在打开进程的时候必须包括对应的权限,PROCESS_CREATE_THREAD权限将允许创建远程线程,PROCESS_VM_OPERATION权限将允许在目标进程中分配内存并将远程代码写到里面。
程序使用VirtualAllocEx函数在目标进程中分配内存,在分配内存的时候,内存属性必须指定为PAGE_EXECUTE_READWRITE,这样分配到的内存可以有执行和读写的权限,分配方式必须指定为MEM_COMMIT,这样内存才会被提交到物理内存中去。
在分配到内存以后,程序使用WriteProcessMemory将远程代码写入,然后再一次将LoadLibrary,GetProcAddress和GetModuleHandle函数的地址写入到远程代码的数据段中。并不将这3个函数的地址存放到远程代码中一次性写入的原因在于:远程代码(包括远程代码使用的数据)是定义在本地的代码段中的,而本地的代码段是只读的,我们无法在本地对它们进行写入初始化数据的操作,所以只好采用远程写入的方式。
最后,用CreateRemoteThread函数创建远程线程后就万事大吉了。编译链接以后运行可执行文件可看到,窗口正常出现了,一眼看上去,这个窗口和别的窗口没有任何不同!但是在任务管理器中却没有多出任何新的进程。
假如远程线程不是这样“招摇过市”地创建了一个窗口,而是在后台偷偷地运行的话,大家能不能从各种蛛丝马迹来发现它的存在呢?反正笔者是找不到它的,因为它仅存在于目标进程的内存中,并不对应任何磁盘文件,当远程线程被执行的时候,惟一可以发现的就是Explorer.exe进程中的活动线程多了一个,使用的内存多了一点而已,但是活动线程用工具软件查看也只能看到一个线程ID,又怎么知道这个线程不是Explorer.exe进程自己的呢?
以上代码用在不合适的地方可能产生危害,笔者第一次公开这段代码,其目的就是希望能对有害代码的防治起到积极的作用,请读者负责任地使用这段代码。
Link: http://www.asm32.net/article_disp.asp?ID=3310
|