《Windows PE权威指南》学习之第5章 导出表(2)

本文介绍了libcurl库的安装方法及使用流程,并通过一个简单的单线程爬虫程序展示了如何利用libcurl抓取网页内容和解析URL。

5.4 导出表编程

本节讨论与导出表有关的编程,包括获取导出表中导出函数地址的编程,以及遍历导出表信息的编程两部分。

如前所述,通过导出表可以获取相关函数的地址。函数可以通过索引值定位,也可以通过函数名定位。通过编程查找函数地址有两个不同方法,分别是:

❑ 根据编号查找函数地址

❑ 根据名字查找函数地址

下面分别介绍这两种方法。

5.4.1 根据编号查找函数地址

要通过编号查找函数地址,其步骤如下:

步骤1 定位到PE头。

步骤2 从PE文件头中找到数据目录表,表项的第一个双字值是导出表的起始RVA。

步骤3 从导出表的nBase字段得到起始序号。

步骤4 函数编号减去起始序号得到的是函数在AddressOfFunctions中的索引号。

步骤5 通过查询AddressOfFunctions指定索引位置的值,找到虚拟地址。

步骤6 将虚拟地址加上该动态链接库在被导入到地址空间后的基地址,即为函数的真实入口地址。

不建议使用编号查找函数地址。因为有很多的动态链接库中标识的编号与对应的函数并不一致,通过这种方法找到的函数地址往往是错误的。

5.4.2 根据名字查找函数地址

要根据函数名字从导出表结构中查找函数的地址,步骤如下:

步骤1 定位到PE头。

步骤2 从PE文件头中找到数据目录表,表项的第一个双字值是导出表的起始RVA。

步骤3 从导出表中获取NumberOfNames字段的值,以便构造一个循环,根据此值确定循环的次数。

步骤4 从AddressOfNames字段指向的函数名称数组的第一项开始,与给定的函数名字进行匹配;如果匹配成功,则记录从AddressOfNames开始的索引号。

步骤5 通过索引号再去检索AddressOfNameOrdinals数组,从同样索引的位置找到函数的地址索引。

步骤6 通过查询AddressOfFunctions指定函数地址索引位置的值,找到虚拟地址。

步骤7 将虚拟地址加上该动态链接库在被导入到地址空间的基地址,即为函数的真实入口地址。

其中通过函数名获取函数调用地址的编码见代码清单5-3。

代码清单5-3 获取指定字符串的API函数的调用地址的函数_getApi(chapter5\peinfo.asm)

   ;-------------------------------
   ; 获取指定字符串的API函数的调用地址
   ; 入口参数:_hModule为动态链接库的基址,_lpApi指向函数名
   ; 出口参数:eax为函数在虚拟地址空间中的真实地址
   ;-------------------------------
   _getApi proc _hModule,_lpApi
       local @ret
       local @dwLen

        pushad
        mov @ret,0
       ;计算API字符串的长度,含最后的0
        mov edi,_lpApi
        mov ecx,-1
        xor al,al
        cld
        repnz scasb
        mov ecx,edi
        sub ecx,_lpApi
        mov @dwLen,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
        add ebx,_hModule
        xor edx,edx
        .repeat
          push esi
          mov edi,[ebx]
          add edi,_hModule
          mov esi,_lpApi
          mov ecx,@dwLen
          repz cmpsb
          .if ZERO?
             pop esi
             jmp @F
          .endif
          pop esi
          add ebx,4
          inc edx
        .until edx>=[esi].NumberOfNames
        jmp _ret
    @@:
       ;通过API名称索引获取序号索引,再获取地址索引
        sub ebx,[esi].AddressOfNames
        sub ebx,_hModule
        shr ebx,1    ;除以2
        add ebx,[esi].AddressOfNameOrdinals
        add ebx,_hModule
        movzx eax,word ptr [ebx]
        shl eax,2    ;乘以4
        add eax,[esi].AddressOfFunctions
        add eax,_hModule

        ;从地址表得到导出函数的地址
        mov eax,[eax]
        add eax,_hModule   ;加上模块的基地址
        mov @ret,eax

    _ret:
        assume esi:nothing
        popad
        mov eax,@ret
        ret
    _getApi endp

23~24行对应步骤当中的步骤1,定位PE头。

34~49行是一个循环,结束条件为找到对应的函数地址或者函数个数已经达到NumberOfNames字段所标识的值。如何判断函数已经找到了呢?方法是通过与AddressOfNames所列的每个函数名进行比对,如果字符串相等,则表示找到,否则继续下一次循环。

如果找到,则跳出循环,转到行50处继续执行。如果函数名匹配成功,表示找到了对应的函数。则记录AddressOfNames的索引,即步骤4,对应代码中的51~54行。

代码中的55~61行实现了步骤5的操作。步骤6、7则对应63~64行。

5.4.3 遍历导出表

遍历导出表的编程是以第4章的PEInfo.asm程序为模板开始的。在函数_openFile中加入以下代码(加黑部分):

;到此为止,该文件的验证已经完成。为PE结构文件
;接下来分析文件映射到内存中的数据,并显示主要参数
invoke _getMainInfo,@lpMemory,esi,@dwFileSize
;显示导入表
invoke _getImportInfo,@lpMemory,esi,@dwFileSize
;显示导出表
invoke _getExportInfo,@lpMemory,esi,@dwFileSize

然后编写函数_getExportInfo,如代码清单5-4所示。

代码清单5-4 遍历导出表的函数_getExportInfo(chapter5\peinfo.asm)

   ;-------------------------------
   ; 获取PE文件的导出表
   ;-------------------------------
   _getExportInfo proc _lpFile,_lpPeHead,_dwSize
     local @szBuffer[1024]:byte
     local @szSectionName[16]:byte
     local @lpAddressOfNames,@dwIndex,@lpAddressOfNameOrdinals

     pushad
      mov esi,_lpPeHead
      assume esi:ptr IMAGE_NT_HEADERS
      mov eax,[esi].OptionalHeader.DataDirectory[0].VirtualAddress
      .if !eax
         invoke _appendInfo,addr szErrNoExport
         jmp _Ret
      .endif
      invoke _RVAToOffset,_lpFile,eax
      add eax,_lpFile
      mov edi,eax      ;计算导出表所在文件偏移位置
      assume edi:ptr IMAGE_EXPORT_DIRECTORY
      invoke _RVAToOffset,_lpFile,[edi].nName
      add eax,_lpFile
      mov ecx,eax
      invoke _getRVASectionName,_lpFile,[edi].nName
      invoke wsprintf,addr @szBuffer,addr szMsgExport,\
               eax,ecx,[edi].nBase,[edi].NumberOfFunctions,\
               [edi].NumberOfNames,[edi].AddressOfFunctions,\
               [edi].AddressOfNames,[edi].AddressOfNameOrdinals
      invoke _appendInfo,addr @szBuffer

      invoke _RVAToOffset,_lpFile,[edi].AddressOfNames
      add eax,_lpFile
      mov @lpAddressOfNames,eax
      invoke _RVAToOffset,_lpFile,[edi].AddressOfNameOrdinals
      add eax,_lpFile
      mov @lpAddressOfNameOrdinals,eax
      invoke _RVAToOffset,_lpFile,[edi].AddressOfFunctions
      add eax,_lpFile
      mov esi,eax    ;函数的地址表

      mov ecx,[edi].NumberOfFunctions
      mov @dwIndex,0
    @@:
      pushad
      mov eax,@dwIndex
      push edi
      mov ecx,[edi].NumberOfNames
      cld
      mov edi,@lpAddressOfNameOrdinals
      repnz scasw
      .if ZERO?   ;找到函数名称
         sub edi,@lpAddressOfNameOrdinals
         sub edi,2
         shl edi,1
         add edi,@lpAddressOfNames
         invoke _RVAToOffset,_lpFile,dword ptr [edi]
         add eax,_lpFile
      .else
         mov eax,offset szExportByOrd
      .endif
      pop edi
      ;序号在ecx中
      mov ecx,@dwIndex
      add ecx,[edi].nBase
      invoke wsprintf,addr @szBuffer,addr szMsg4,\
               ecx,dword ptr [esi],eax
      invoke _appendInfo,addr @szBuffer
      popad
      add esi,4
      inc @dwIndex
      loop @B
    _Ret:
      assume esi:nothing
      assume edi:nothing
      popad
      ret
    _getExportInfo endp

行12~16通过检索数据目录表的第1项(用[esi].OptionalHeader.DataDirectory[0]来表示),获取导出表的VirtualAddress。如果该值为0,意味着该PE文件没有导出表,则显示没有导出表的提示信息并退出,否则继续。

行17~30部分,首先将获取的导出表的VirtualAddress的RVA值转换为FOA,然后显示该地址所处的节的名称,并显示结构IMAGE_EXPORT_DIRECTORY的部分字段的值。

行43~71为一个循环,该循环完成了显示该PE文件中所有导出函数相关信息的功能。这些信息包括导出序号、函数的虚拟地址和导出函数的名称。变量@dwIndex跟随循环次数加1递增,同时该变量也是所有函数的索引值。通过查找AddressOfNameOrdinals数组获得该索引是否在数组中存在,以确定对应索引的函数是否是基于名称访问的。如果是,则执行第52~57行的代码;如果不是,表示该函数是基于索引值访问的,则执行第59行的代码。

以下内容是使用PEInfo小工具分析chapter5\winResult.dll文件输出的与导出表有关的信息:

--------------------------------------------------------------
导出表所处的节:.rdata
--------------------------------------------------------------
原始文件名:winresult.dll
nBase                   00000001
NumberOfFunctions    		00000004
NuberOfNames          	00000004
AddressOfFunctions  	 	00002168
AddressOfNames        	00002178
AddressOfNameOrd     		00002188
-------------------------------------

导出序号         虚拟地址        导出函数名称
-------------------------------------
00000001        00001183        AnimateClose
00000002        00001022        AnimateOpen
00000003        00001282        FadeInOpen
00000004        00001323        FadeOutClose

显示信息分三部分:导出表所处的节、导出表结构IMAGE_EXPORT_DIRECTORY的主要字段的值和导出函数的相关信息(含导出序号、虚拟地址和导出函数名称)。导出表的结构就分析到这里,下面来看导出表的常见应用。

5.5 导出表的应用

导出表常见的应用主要包括对导出表函数的覆盖,以及对动态链接库内部私有函数的导出等。通过对导出表函数进行覆盖,可以更改代码流程或代码功能,为应用程序实施补丁;通过对动态链接库内部私有函数的导出,可以更充分地利用已有的代码,减轻二次开发的工作量。

5.5.1 导出函数覆盖

导出表编程中常见的技术是,不需要修改用户程序,便能将用户程序中调用的动态链接库函数转向或者实施代码覆盖,实现用户程序的调用转移。这种技术通常用在病毒程序的开发中,因为用户程序没有发生改变,所以杀毒软件在对用户程序的防护过程中,针对这种渗透是无效的。下面介绍两种常见的导出函数覆盖技术:

❑ 修改导出结构中的函数地址

❑ 覆盖函数地址部分的指令代码

1.修改导出结构中的函数地址

以winResult.dll为例,使用FlexHex将AddressOfFunctions索引1和2的地址(分别对应函数AnimateOpen和FadeInOpen)交换位置。更改以后的字节码如下:

原始值:

修改后:

00000960  78 21 00 00 88 21 00 00 83 11 00 00 82 12 00 00  x!..!........
00000970  22 10 00 00 23 13 00 00 9E 21 00 00 AB 21 00 00  "...#...!..!..
00000980  B7 21 00 00 C2 21 00 00 00 00 01 00 02 00 03 00   !..!..........

如上所示,无需修改应用程序FirstWindow.exe,仅通过将函数调用RVA地址0x00001282和0x00001022交换位置,即可实现导出函数的覆盖。直接测试,发现显示窗口的动画效果发生了变化。

需要注意的是,在使用导出函数地址覆盖技术的时候,首先要保证所涉及的两个函数参数入口要一致,否则调用完成后栈不平衡,这会导致应用程序调用失败;其次,要求用户对两个函数的内部实现要有充分的了解,使得地址转向后,能够保证应用程序在功能上可以全面兼容并运行良好。

注意 该部分测试文件在随书文件的目录chapter5\a中,winResult.dll是被修改了AddressOfFunctions地址后的动态链接库。在实际的操作中并不赞成大家使用该技术。

2.覆盖函数地址部分的指令代码

第二种常见的覆盖技术,是将AddressOfFunctions指向的地址空间指令字节码实施覆盖。这种技术又衍生出两种:

❑ 暴力覆盖,即将所有的代码全部替换为新代码。新代码可能含有原来代码的全部功能,也可能不包含原有代码功能。

❑ 完美覆盖,通过构造指令,实施新代码与原代码的共存和无遗漏运行。

因为完美覆盖涉及代码的重定位,相对复杂一些,这里以暴力覆盖为例。相关文件在随书文件的chapter5\b目录中,winResult.dll是被覆盖了函数FadeInOpen后的动态链接库。

打开winResult.dll文件查看字节码,可以发现函数FadeInOpen定义部分(文件中起始偏移0x0682)被修改成如下指令序列:

00000680        55 8B EC 6A 00 6A 00 68 28 30 00 10 6A 00  ..Uj.j.h(0..j.
00000690  E8 08 00 00 00 90 90 90 90 C9 C2 04 00 FF 25 A3  ......  %
000006A0  12 00 10 EA 07 D5 77                                     ....w

为方便阅读,以上按照指令功能对字节码作了加黑处理。第一部分黑体保存原始栈基地址,第二部分黑体代码是维持栈平衡的返回指令。所有字节码对应的反汇编指令为:

10001282 >   55                PUSH EBP
10001283     8BEC               MOV EBP,ESP
10001285     6A 00              PUSH 0
10001287     6A 00              PUSH 0
10001289     68 28300010      	PUSH winResul.10003028           ; ASCII "user32.dll"
1000128E     6A 00              PUSH 0
10001290     E8 08000000      CALL winResul.1000129D
                  ; JMP 到 user32.MessageBoxA
10001295     90                  NOP
10001296     90                  NOP
10001297     90                  NOP
10001298     90                  NOP
10001299     C9                  LEAVE
1000129A     C2 0400           	 RETN 4
1000129D   - FF25 A3120010    	 JMP DWORD PTR DS:[100012A3] ; user32.MessageBoxA
100012A3    EA 07D57700 006>JMP FAR 6800:0077D507  ; 远跳转

因为函数FadeInOpen的代码被全部覆盖,所以运行FirstWindow只会弹出提示对话框,内容显示“user32.dll”。显示的字符串是借用了winResult.asm的数据段中定义的函数SetLayeredWindowAttributes所在动态链接库的名称。

从反汇编指令可以看出,调用函数user32.MessageBoxA时使用了硬编码,即将该函数在虚拟地址空间分配的VA直接写入了代码段,不通过导入表直接跳转到函数代码处执行,上面反汇编代码加黑部分即为地址字节码(在这里OD错误地将它识别成了指令序列)。使用硬编码最大的好处是引入动态链接库的函数时不需要修改导入表。因为FirstWindow引入的动态链接库就这一个,不存在基地址被占用的问题,所以,与重定位有关的信息在此例中不需要进行修改。

5.5.2 导出私有函数

在某些场合下,DLL中的私有函数还是很有用的。也许是出于保密考虑,或者其他原因, DLL的开发者将一些比较重要的函数设置为内部私有函数,并不在导出表中声明。当程序被二次开发时,开发者却需要这些函数,这时候就需要开发者自己将这些被定义为私有的函数添加到导出表中。

在本章的实例中,程序winResult.dll一共导出了4个公有函数;源代码中的TopXY函数被声明为私有函数,并未导出,所以在使用PEInfo分析时看不到该函数。下面就以这个函数为例,介绍一下导出私有函数需要做哪些工作。

首先,将最原始的导出表整体搬迁到一个空闲空间中,这里选择从文件偏移0x0940处搬到0x0a50处。以下是添加私有函数到导出表后的两处地址对应的字节码:

原始winResult.dll数据:

从《WindowsPE权威指南》附书源代码\chapter5\c\winResult.dll代码中导出要修改的数据到dll.zim

选中要导出的数据00000A50~00000AE0==》【编辑】菜单=》【读取/写入 数据】=》【写入到文件】

用FlexHEX打开要修改的winResult.dll,选择要导入的起始地址位置=》【编辑】菜单=》【读取/写入 数据】=》【从文件插入】导入上面导出的dll.zim文件

修改后:

00000940   00 00 00 00 EE 0E 51 4D 00 00 00 00 90 21 00 00   .....QM....!..
00000950   01 00 00 00 04 00 00 00 04 00 00 00 68 21 00 00   ............h!..
00000960   78 21 00 00 88 21 00 00 83 11 00 00 22 10 00 00   x!..!....."...
00000970   82 12 00 00 23 13 00 00 9E 21 00 00 AB 21 00 00   ...#...!..!..
00000980   B7 21 00 00 C2 21 00 00 00 00 01 00 02 00 03 00   !..!..........
00000990   77 69 6E 72 65 73 75 6C 74 2E 64 6C 6C 00 41 6E   winresult.dll.An
000009A0   69 6D 61 74 65 43 6C 6F 73 65 00 41 6E 69 6D 61   imateClose.Anima
000009B0   74 65 4F 70 65 6E 00 46 61 64 65 49 6E 4F 70 65   teOpen.FadeInOpe
000009C0   6E 00 46 61 64 65 4F 75 74 43 6C 6F 73 65 00 00   n.FadeOutClose..
000009D0   00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00   ................
000009E0   00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00   ................
000009F0   00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00   ................
00000A00   00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00   ................
00000A10   00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00   ................
00000A20   00 00 00 00 00 00 00 00 75 73 65 72 33 32 2E 64   ........user32.d
00000A30   6C 6C 00 53 65 74 4C 61 79 65 72 65 64 57 69 6E   ll.SetLayeredWin
00000A40   64 6F 77 41 74 74 72 69 62 75 74 65 73 00 00 00   dowAttributes...

00000A50   00 00 00 00 EE 0E 51 4D 00 00 00 00 AA 30 00 00   .....QM....0..
00000A60   01 00 00 00 05 00 00 00 05 00 00 00 78 30 00 00   ............x0..
00000A70   8C 30 00 00 A0 30 00 00 83 11 00 00 22 10 00 00   0..0....."...
00000A80   82 12 00 00 23 13 00 00 0C 10 00 00 B8 30 00 00  ...#.......0..
00000A90   C5 30 00 00 D1 30 00 00 DC 30 00 00 E9 30 00 00  0..0..0..0..
00000AA0   00 00 01 00 02 00 03 00 04 00 77 69 6E 52 65 73   ..........winRes
00000AB0   75 6C 74 2E 64 6C 6C 00 41 6E 69 6D 61 74 65 43   ult.dll.AnimateC
00000AC0   6C 6F 73 65 00 41 6E 69 6D 61 74 65 4F 70 65 6E   lose.AnimateOpen
00000AD0   00 46 61 64 65 49 6E 4F 70 65 6E 00 46 61 64 65   .FadeInOpen.Fade
00000AE0   4F 75 74 43 6C 6F 73 65 00 54 6F 70 58 59 00 00   OutClose.TopXY..

现在从几个方面来分析添加了私有函数的导出表与原有导出表的区别:

1)长度从8Fh变成了9Fh。增加的部分包含:

❑ 函数名:‘TopXY\0’共6个字节。

❑ 函数的RVA:0x0000100c,共4个字节。

❑ 函数名称所在地址:0x000032e9,共4个字节。

2)函数个数由原来的4个变成5个(见字节码的下划线部分)。

3)修正其他因搬迁和增加而变动的地址。因为上面已经同时列出了前后的字节码,在这里就不再详细描述,大家可以比照两者不同和导出表结构自己进行分析。

4)数据目录项。因为导出表位置和数据大小发生了变化,所以PE文件头部的数据目录项中需要进行如下修正:

❑ 位置由原来的0x2140变成0x3250。

❑ 大小由原来的8Fh变成9Fh。

修改后:(修改原理3050h - 3000h + a00h = A50h位置)

这样就为导出表增加了函数TopXY的导出信息。使用小工具PEInfo查看导出表,输出信息如下:

使用导出工具查看

-------------------------------------------------------------
导出表所处的节:.data
-------------------------------------------------------------
原始文件名:winResult.dll
nBase                   00000001
NumberOfFunctions       00000005
NuberOfNames            00000005
AddressOfFunctions      00003078
AddressOfNames          0000308c
AddressOfNameOrd        000030a0
-------------------------------------

导出序号          虚拟地址         导出函数名称
-----------------------------------------
00000001        00001183        AnimateClose
00000002        00001022        AnimateOpen
00000003        00001282        FadeInOpen
00000004        00001323        FadeOutClose
00000005        0000100c        TopXY

如上所示,加黑部分明确提示我们,导出函数已经由最初的4个变成了5个;在导出函数的描述部分也显示了新函数的导出序号、函数所在的RVA,以及新导出函数的名称TopXY,这意味着将私有函数转换为导出函数是成功的。你也可以新建一个测试程序对刚导出的函数进行测试,测试代码在随书文件列表chapter5\c\priFun.asm中。

;priFun.asm   导出表导出私有函数测试
;在XP系统环境下运行,使用ml命令进行编译和链接:
;ml /c /coff priFun.asm
;link /subsystem:windows priFun.obj
.386
.model flat, stdcall 
option casemap:none 	;区分大小写
;include文件定义
include		C:/masm32/include/windows.inc 
include 	C:/masm32/include/user32.inc 
includelib 	C:/masm32/lib/user32.lib 
include 	C:/masm32/include/kernel32.inc 
includelib 	C:/masm32/lib/kernel32.lib 

;数据段
.data 
szBuffer	db 200 dup(0)
szBuffer1	db 200 dup(0)
szOut 		db '(400:600) eax=%d', 0

winResu 	db 'winResult.dll', 0
SLWA 		db 'TopXY', 0
pSLWA 		dd ?

;代码段
.code 
;-------------------------------
;  私有函数模拟
;-------------------------------
NTopXY proc wDim:DWORD, sDim:DWORD 
	shr sDim, 1 
	shr wDim, 1 
	mov eax, wDim 
	sub sDim, eax 
	mov eax, sDim 
	ret 
NTopXY endp 

start:
	invoke NTopXY, 400, 600 
	invoke wsprintf, addr szBuffer, addr szOut, eax 
	invoke MessageBox, NULL, offset szBuffer, NULL, MB_OK 
	;指定的动态链接库(DLL)加载到当前进程的地址空间中。
	invoke LoadLibrary, addr winResu 
	;从指定的动态链接库(DLL)中获取一个导出函数或变量的地址
	invoke GetProcAddress, eax, addr SLWA 
	mov pSLWA, eax 
	
	push 600
	push 400 
	call pSLWA 
	invoke wsprintf, addr szBuffer1, addr szOut, eax 
	invoke MessageBox, NULL, offset szBuffer1, NULL, MB_OK 
	
	invoke ExitProcess, NULL 
end start 

测试结果:

5.6 小结

本章重点介绍了PE结构中的导出数据部分。通过对导出表的学习,读者能够了解导出表在PE中的作用,并从底层了解Windows加载程序修正导入表IAT的过程;同时,本章还对导出表的导出函数覆盖技术及私有函数导出做了简单的介绍。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值