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_details.aspx?id=3310


浏览次数 430 发布时间 2008/1/20 4:18:10 从属分类 ASM/ASM32 【评论】【 】【打印】【关闭
 
| www.asm32.net | 2006版 | 资料中心 | linux | asm/asm32 | C/C++ | VC++ | java | Python | 书签 | ASP.Net书签 | 京ICP备09029108号-1