现在的位置: 首页 > 综合 > 正文

国庆PE总复习(六)

2018年04月06日 ⁄ 综合 ⁄ 共 11177字 ⁄ 字号 评论关闭

前面有一个朋友给我留言说要搞个链接,好的,等我把第七篇文章写完之后,会在最后一篇文章的最后把前面的六篇文章的地址都链接上去,也样第一方便了大家,也同时方便了自己阅读学习,给人方便,就是给自己方便!!
参考文献《Win32汇编语言程序设计》第二版

前面的五篇文章,所讲的都是对PE文件的结构进行了分析,以及对PE工具的编写进行了介绍,但在实际的应用中还未有涉及到很多方面的内容,其实学好PE在很多方面都有很重要的应用,比如文件的加密,压缩,编写病毒等都涉及到修改及重组PE文件,另外,像API HOOK,PE文件的内存映像DUMP等应用则涉及分析内存中的PE映像。在接下来的两篇文章中我将用几个例子来进一步说明这方面的应用,可以说这是编写病毒的基础吧!!

首先讲在病毒中用到的一种很常用的技术,就是如何从内存中动态获取某个API的地址。

动态获取API的入口地址
在Win32环境下编程,不便用API几乎是不可能的事情,一般情况下,在代码中使用API不外乎两种办法:第一是编译链接的时候使用导入库,那么生成的PE文件中就会包含导入表,这样程序执行时会由Windows装载器根据导入表中的信息来修正API调用语句中的地址;第二种方法是使用LoadLibrary函数动态装入某个DLL模块,并使用GetProcAddress函数从被装载入的模块中获取API函数的地址。

先讲解一下原理吧!
在DOS环境下,一个可执行文件既可以用INT 21h/4ch来结束程序,也可以用一个Ret指令来结束程序,实际上,在Win32下也可以用这种方法来结束程序,虽然大部分的Win32程序都使用ExitProcess函数来终止执行,但是使用Ret指令确实也是有效的。

如下图所示,当父进程要创建一个子进程的时候,它会调用Kernel32.dll中的CreateProcess函数,CreateProcess函数在完成装载应用程序后,会将一个返回地址压入堆栈并转而执行应用程序,如果应用程序用ExitProcess函数来终止,那么这个返回地址没有什么用途,但如果应用程序使用Ret指令的话,程序就会返回CreateProcess函数设定的地址。也就是说,应用程序的主程序可以看作是被Windows调用的一个子程序。
名称:  图1.jpg查看次数: 409文件大小:  15.4 KB
图中表示Win32可执行文件退出的示意图

那么Ret指令返回到的地址上究竟有什么指令呢?用Soft-ICE看看就会发现,它包含一句push eax指令和一句call ExitThread,也就是说,假如用Ret指令返回的话,Windows会替程序去调用ExitThread函数,如果这是进程的最后一个线程的话,ExitThread函数又会自动去调用ExitProcess,这样程序就会被终止执行。

从这个过程可以得到一个很重要的数据,那就是堆栈中的返回地址,这个地址只要在程序入口的地方用[esp]就可以将它读出,说它重要是因为它位于Kernel32.dll模块中,而LoadLibrary和GetProcAddress函数正是处于Kernel32.dll模块中,换句话说就是,我们得到的地址和这两个函数近在咫尺,完全可以从这个地址经过某种算法来找到这两个函数的入口地址,得到这两个函数的入口地址以后,什么问题都解决了。

结合本章前面内容中提到过的两个事实,可以确定这种想法是可行的。

首先,PE文件被装入内存后(包括Kernel32.dll文件),除了一些可丢弃的节如重定位节以外,其他的内容都会被装入内存,这样获取导出函数地址所需的PE文件头、导出表等数据都存在于内存中;第二,PE文件被装入内存时是按内存页对齐的,只要从Ret指令返回的地址按照页对齐的边界一页页地向低地址搜寻,就必然可以找到Kernel32.dll文件的文件头位置。
下面请看具体的源代码,我会在其中作出注释,以方便大家理解!
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; Sample code for < Win32ASM Programming 2nd Edition>
; by 罗云彬
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; NoImport.asm
; 以从内存中动态获取的办法使用 API
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
    .386
    .model flat,stdcall
    option casemap:none
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
include    windows.inc

_ProtoGetProcAddress  typedef  proto  :dword,:dword
_ProtoLoadLibrary  typedef  proto  :dword
_ProtoMessageBox  typedef  proto  :dword,:dword,:dword,:dword
_ApiGetProcAddress  typedef  ptr  _ProtoGetProcAddress
_ApiLoadLibrary    typedef  ptr  _ProtoLoadLibrary
_ApiMessageBox    typedef  ptr  _ProtoMessageBox
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; 数据段
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
    .data?
hDllKernel32  dd  ?
hDllUser32  dd  ?
_GetProcAddress  _ApiGetProcAddress  ?
_LoadLibrary  _ApiLoadLibrary    ?
_MessageBox  _ApiMessageBox    ?

    .const
szLoadLibrary  db  'LoadLibraryA',0
szGetProcAddress db  'GetProcAddress',0
szUser32  db  'user32',0
szMessageBox  db  'MessageBoxA',0

szCaption  db  'A MessageBox !',0
szText    db  'Hello, World !',0
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; 代码段
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
    .code
include    _GetKernel.asm                                             ;包含获取Kernel32.dll的基址的源代码
start:
;********************************************************************
; 从堆栈中的 Ret 地址转换 Kernel32.dll 的基址,并在 Kernel32.dll
; 的导出表中查找 GetProcAddress 函数的入口地址
;********************************************************************
    invoke  _GetKernelBase,[esp]                               ;获取Kernel32.dll的基址
    .if  eax
      mov  hDllKernel32,eax                           ;保存Kernel32.dll的基址
      invoke  _GetApi,hDllKernel32,addr szGetProcAddress ;在Kernel32.dll的导出表中查找GetProcAddress函数的入口地址
      mov  _GetProcAddress,eax                        ;保存GetProcAddress函数的入口地址
    .endif
;********************************************************************
; 用得到的 GetProcAddress 函数得到 LoadLibrary 函数地址并装入其他 Dll
;********************************************************************
    .if  _GetProcAddress                                            ;如果LoadLibrary函数地址不为NULL   
      invoke  _GetProcAddress,hDllKernel32,addr szLoadLibrary    ;通过GetProcAddress得到LoadLibrary函数的地址
      mov  _LoadLibrary,eax                                   ;保存LoadLibrary函数的地址
      .if  eax
        invoke  _LoadLibrary,addr szUser32                 ;使用LoadLibrary函数装载User32.dll
        mov  hDllUser32,eax                             ;保存User32.dll
        invoke  _GetProcAddress,hDllUser32,addr szMessageBox   ;获取User32.dll中MessageBox函数的地址
        mov  _MessageBox,eax                                ;保存MessageBox函数的地址
      .endif
    .endif
;********************************************************************
    .if  _MessageBox                                                ;如果MessageBox函数的地址不为NULL
      invoke  _MessageBox,NULL,offset szText,offset szCaption,MB_OK
    .endif
    ret
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
    end  start
上面这个函数很简单也没什么好说的,我已经注释的很详细了,这里就不多说了,我们来看看它所包含的一个源文件_GetKernel.asm是怎么样获取Kernel32.dll基址的
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; Sample code for < Win32ASM Programming 2nd Edition>
; by 罗云彬
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; 公用模块:_GetKernel.asm
; 根据程序被调用的时候堆栈中有个用于 Ret 的地址指向 Kernel32.dll
; 而从内存中扫描并获取 Kernel32.dll 的基址
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; 错误 Handler
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
_SEHHandler  proc  C _lpExceptionRecord,_lpSEH,_lpContext,_lpDispatcherContext    ;这里我不讲了前面已经说过SEH异常处理

    pushad
    mov  esi,_lpExceptionRecord
    mov  edi,_lpContext
    assume  esi:ptr EXCEPTION_RECORD,edi:ptr CONTEXT
    mov  eax,_lpSEH
    push  [eax + 0ch]
    pop  [edi].regEbp
    push  [eax + 8]
    pop  [edi].regEip
    push  eax
    pop  [edi].regEsp
    assume  esi:nothing,edi:nothing
    popad
    mov  eax,ExceptionContinueExecution
    ret

_SEHHandler  endp
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; 在内存中扫描 Kernel32.dll 的基址
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
_GetKernelBase  proc  _dwKernelRet                                  ;传入一个参数         
    local  @dwReturn

    pushad
    mov  @dwReturn,0
;********************************************************************
; 重定位
;********************************************************************
    call  @F                                           ;这里我就不说了,在病毒中经常用到,很多病毒开始就这经典的四句
    @@:                                                  ;主要就于重定位
    pop  ebx
    sub  ebx,offset @B
;********************************************************************
; 创建用于错误处理的 SEH 结构
;********************************************************************
    assume  fs:nothing
    push  ebp
    lea  eax,[ebx + offset _PageError]
    push  eax
    lea  eax,[ebx + offset _SEHHandler]
    push  eax
    push  fs:[0]
    mov  fs:[0],esp
;********************************************************************
; 查找 Kernel32.dll 的基地址
;********************************************************************
    mov  edi,_dwKernelRet                             ;将参数中传递过来的目标地址按64K对齐
    and  edi,0ffff0000h                               ;与0ffff0000h进行AND操作
    .while  TRUE
      .if  word ptr [edi] == IMAGE_DOS_SIGNATURE     ;在内存中寻找DOS MZ文件头标识和PE文件头标识
        mov  esi,edi
        add  esi,[esi+003ch]
        .if word ptr [esi] == IMAGE_NT_SIGNATURE
          mov  @dwReturn,edi
          .break
        .endif
      .endif
      _PageError:                                 ;页面异常处理
      sub  edi,010000h                         ;以一个页面作为间隔
      .break  .if edi < 070000000h
    .endw
    pop  fs:[0]
    add  esp,0ch
    popad
    mov  eax,@dwReturn
    ret

_GetKernelBase  endp
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; 从内存中模块的导出表中获取某个 API 的入口地址
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
_GetApi    proc  _hModule,_lpszApi                           ;传入两个参数,模块名和函数名,用于获取传入的函数的入口地址
    local  @dwReturn,@dwStringLength

    pushad
    mov  @dwReturn,0
;********************************************************************
; 重定位
;********************************************************************
    call  @F
    @@:
    pop  ebx
    sub  ebx,offset @B
;********************************************************************
; 创建用于错误处理的 SEH 结构
;********************************************************************
    assume  fs:nothing
    push  ebp
    lea  eax,[ebx + offset _Error]
    push  eax
    lea  eax,[ebx + offset _SEHHandler]
    push  eax
    push  fs:[0]
    mov  fs:[0],esp
;********************************************************************
; 计算 API 字符串的长度(带尾部的0)
;********************************************************************
    mov  edi,_lpszApi                                 
    mov  ecx,-1
    xor  al,al
    cld                                                 ;设置方向位,使EDI增1
    repnz  scasb                                       ;循环比较EDI中存放的函数名
    mov  ecx,edi
    sub  ecx,_lpszApi                                ;EDI的值减去函数名首地址的值为函数名的长度
    mov  @dwStringLength,ecx                         ;保存函数的长度
;********************************************************************
; 从 PE 文件头的数据目录获取导出表地址
;********************************************************************
    mov  esi,_hModule
    add  esi,[esi + 3ch]
    assume  esi:ptr IMAGE_NT_HEADERS
    mov  esi,[esi].OptionalHeader.DataDirectory.VirtualAddress
    add  esi,_hModule
    assume  esi:ptr IMAGE_EXPORT_DIRECTORY
;********************************************************************
; 查找符合名称的导出函数名
;********************************************************************
    mov  ebx,[esi].AddressOfNames                   ;从AddressOfNames字段指向的函数名称地址表的第一项开始         
    add  ebx,_hModule
    xor  edx,edx
    .repeat                                            
      push  esi
      mov  edi,[ebx]
      add  edi,_hModule
      mov  esi,_lpszApi
      mov  ecx,@dwStringLength                ;以函数的长度作为循环,查找相符合的函数名
      repz  cmpsb                              ;字符串比较,比较EDI与ESI中的字符串
      .if  ZERO?
        pop  esi
        jmp  @F
      .endif
      pop  esi
      add  ebx,4
      inc  edx
    .until  edx >=  [esi].NumberOfNames
    jmp  _Error
@@:
;********************************************************************
; API名称索引 --> 序号索引 --> 地址索引
;********************************************************************
    sub  ebx,[esi].AddressOfNames                   ;记下这个函数名在字符串地址表中的索引值
    sub  ebx,_hModule
    shr  ebx,1  
    add  ebx,[esi].AddressOfNameOrdinals            ;然后在AddressOfNameOrdinals指向的数组中以同样的索引值取出数组项的值
    add  ebx,_hModule
    movzx  eax,word ptr [ebx]
    shl  eax,2
    add  eax,[esi].AddressOfFunctions               ;以上面得到了值在AddressOfFunctions字段指向的函数入口地址表中获取函数RVA
    add  eax,_hModule
;********************************************************************
; 从地址表得到导出函数地址
;********************************************************************
    mov  eax,[eax]
    add  eax,_hModule
    mov  @dwReturn,eax
_Error:
    pop  fs:[0]
    add  esp,0ch
    assume  esi:nothing
    popad
    mov  eax,@dwReturn
    ret

_GetApi    endp
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

上面的代码中GetApi有前面讲的得到导出表的代码差不多,大家可以对照的看,我这里也讲了一些简单的注释,如果大家对前面的已经掌握,在来看这段代码,应该不会有什么问题,好了,今天就先讲到了这!!明天是国庆的最后一天,希望大家在这个国庆长假中都能玩好,吃好,学好! 

抱歉!评论已关闭.