Home 电脑技术 编程技术 PE 文件格式启发式学习2 -- 导出表,重定位表
PE 文件格式启发式学习2 -- 导出表,重定位表 E-mail
作者:洋葱圈   
周日, 22 6月 2008 04:44
文章索引
PE 文件格式启发式学习2 -- 导出表,重定位表
1
全部页面
问5.1:dll 为什么叫动态连接库,与平常的静态连接有什么不同。
答5.1:静态连接库在编译连接时由link 程序把库文件直接添加到运行程序中。
    动态连接库在编译连接时只是把插桩加到代码里。运行时由加载器载入
    内存,修改插桩代码使指向正确的地址。这个过程在上一讲中已经说过了。
问5.2:既然是库函数,就会有一堆函数构成,那么是否每个函数都可以被外边调用呢?
答5.2:库函数可分为三类,一个是库入口函数。
  一类为可被外部调用,叫导出库函数。
  一类不能被外部调用,我们叫它私有函数吧。因为它没有向外提供接口。
  
问5.3:导出函数是怎样向外提供接口的呢? 或者说我们怎样才能使用导出函数呢?
答5.3: 这个问题是我们的重点,我们结合实例来吧,慢慢把它讲清楚。
  用ultraedit 打开这个dll 文件。
  ultraedit看到的是最原始的文件,其它众多的pe 分析软件都是从原始文件分析得到的。哦,当然,这话说的多余了。
  大概浏览一下这个文件。
  区分一下dos头, PE 头, 节表, 有几个块组成。这些都是很明显的。上一讲中已经说过了。
  哦,上一讲讲的是exe, 这里是dll, 不过它们都是PE 文件, 格式是一样的。
  
顺便复习一下上次内容,这可以说是上次4讲的精华了,却是以count.dll 为例,同样通俗易懂:
  dos 头: 标记 "MZ"
00000000   4D 5A 90 00 03 00 00 00  04 00 00 00 FF FF 00 00   MZ?..........
00000010   B8 00 00 00 00 00 00 00  40 00 00 00 00 00 00 00   ?......@.......
00000020   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ................
00000030   00 00 00 00 00 00 00 00  00 00 00 00 C0 00 00 00   ............?..
  PE 头: 标记 "PE"
000000C0   50 45 00 00 4C 01 04 00  F6 34 EB 3C 00 00 00 00   PE..L...??....
000000D0   00 00 00 00 E0 00 0E 21  0B 01 05 0C 00 02 00 00   ....?.!........  
从dos头 0x3c 处也能看出PE 头位置。
  节表: 有明显的字符串标记,此处是".text"
000001B0                            2E 74 65 78 74 00 00 00           .text...
000001C0   70 00 00 00 00 10 00 00  00 02 00 00 00 04 00 00   p...............
000001D0   00 00 00 00 00 00 00 00  00 00 00 00 20 00 00 60   ............ ..`
000001E0   2E 72 64 61 74 61 00 00  BC 00 00 00 00 20 00 00   .rdata..?... ..
000001F0   00 02 00 00 00 06 00 00  00 00 00 00 00 00 00 00   ................
00000200   00 00 00 00 40 00 00 40  2E 64 61 74 61 00 00 00   ....@..@.data...
00000210   04 00 00 00 00 30 00 00  00 00 00 00 00 00 00 00   .....0..........
00000220   00 00 00 00 00 00 00 00  00 00 00 00 40 00 00 C0   ............@..?
00000230   2E 72 65 6C 6F 63 00 00  2C 00 00 00 00 40 00 00   .reloc..,....@..
00000240   00 02 00 00 00 08 00 00  00 00 00 00 00 00 00 00   ................
00000250   00 00 00 00 40 00 00 42                            ....@..B........    
  数一数节表有4个
  从PE 头 0xc7处也能看出来。
  大致浏览一下后面的数据块划分,块与块之间很容易识别,因为每一块之间都有很多0,
  它们是以512字节对齐填充的。
  咦! 怎么只看到了3块, 节表头中不是说4块吗?
  再仔细对照一下节表头:跟我一块找找。
00000400   55 8B EC B8 01 00 00 00  C9 C2 0C 00 55 8B EC 6A   U嬱?...陕..U嬱j
00000410   01 FF 75 10 FF 75 0C FF  75 08 E8 4B 00 00 00 C9   .u.u.u.鐺...?
00000420   C2 0C 00 55 8B EC FF 05  00 30 00 10 FF 35 00 30   ?.U嬱..0..5.0
00000430   00 10 FF 75 0C FF 75 08  E8 CF FF FF FF A1 00 30   ..u.u.柘?0
00000440   00 10 C9 C2 08 00 55 8B  EC FF 0D 00 30 00 10 FF   ..陕..U嬱..0..
00000450   35 00 30 00 10 FF 75 0C  FF 75 08 E8 AC FF FF FF   5.0..u.u.璎
00000460   A1 00 30 00 10 C9 C2 08  00 CC FF 25 00 20 00 10   ?0..陕..?%. ..
00000470   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ................  
这段是 text 段,因为节表已经说了,text 段内存地址0x1000,大小0x70
在文件中处于偏移0x400, 占用文件大小0x200 字节。

00000600   38 20 00 00 00 00 00 00  30 20 00 00 00 00 00 00   8 ......0 ......
00000610   00 00 00 00 48 20 00 00  00 20 00 00 00 00 00 00   ....H ... ......
00000620   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ................
00000630   38 20 00 00 00 00 00 00  27 02 53 65 74 44 6C 67   8 ......'.SetDlg
00000640   49 74 65 6D 49 6E 74 00  55 53 45 52 33 32 2E 64   ItemInt.USER32.d
00000650   6C 6C 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ll..............
00000660   00 00 00 00 F6 34 EB 3C  00 00 00 00 9C 20 00 00   ....??....?..
00000670   01 00 00 00 02 00 00 00  02 00 00 00 88 20 00 00   ............?..
00000680   90 20 00 00 98 20 00 00  46 10 00 00 23 10 00 00   ?..?..F...#...
00000690   A8 20 00 00 B2 20 00 00  00 00 01 00 43 6F 75 6E   ?..?......Coun
000006A0   74 65 72 2E 64 6C 6C 00  5F 44 65 63 43 6F 75 6E   ter.dll._DecCoun
000006B0   74 00 5F 49 6E 63 43 6F  75 6E 74 00 00 00 00 00   t._IncCount.....
000006C0   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ................  
这段是 rdata 段,因为节表已经说了,rdata 段内存地址0x2000,大小0xbc
在文件中处于偏移0x600, 占用文件大小0x200 字节。


00000800   00 10 00 00 18 00 00 00  28 30 2E 30 3E 30 4B 30   ........(0.0>0K0
00000810   51 30 61 30 6C 30 00 00  00 00 00 00 00 00 00 00   Q0a0l0..........
00000820   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ................
这段是 reloc 段,因为节表已经说了,reloc 段内存地址0x4000,大小0x20
在文件中处于偏移0x800, 占用文件大小0x200 字节。

哦!也!文件分析完了。
呦,不是说看看丢了哪一块吗? 是data 段丢了。看看节表怎么说:
节表已经说了,data 段内存地址0x4000,大小0x4
在文件中处于偏移0x0, 占用文件大小0x0 字节。
怪不得文件中找不到它的踪影,原来它不存在。但内存中还是给它留了位置。
不过这里是个特例,一般文件都会有data 段的存在。

(小声说)别高兴太早了,这只是划分了各个块,把每块的具体内容分析完才算完
哦,也.
下面我们再详细分析一下各个段功能。
text 段为核心,其它段都是为它服务的。
text 段
即代码段,由指令集构成。你可以反汇编出这部分内容,就知道它们的功能了。
为了本帖的完整性,我把它贴过来,并不长。
 10001000                           EntryPoint:
 10001000  55                            push  ebp
 10001001  8BEC                          mov  ebp,esp
 10001003  B801000000                    mov  eax,00000001h
 10001008  C9                            leave
 10001009  C20C00                        retn  000Ch
;----------------------------------------------------------------------------------------------------
 1000100C                           SUB_L1000100C:
 1000100C  55                            push  ebp
 1000100D  8BEC                          mov  ebp,esp
 1000100F  6A01                          push  00000001h
 10001011  FF7510                        push  [ebp+10h]
 10001014  FF750C                        push  [ebp+0Ch]
 10001017  FF7508                        push  [ebp+08h]
 1000101A  E84B000000                    call  jmp_USER32.dll!SetDlgItemInt
 1000101F  C9                            leave
 10001020  C20C00                        retn  000Ch
;----------------------------------------------------------------------------------------------------
 10001023                           _IncCount:
 10001023  55                            push  ebp
 10001024  8BEC                          mov  ebp,esp
 10001026  FF0500300010                  inc  [L10003000]
 1000102C  FF3500300010                  push  [L10003000]
 10001032  FF750C                        push  [ebp+0Ch]
 10001035  FF7508                        push  [ebp+08h]
 10001038  E8CFFFFFFF                    call  SUB_L1000100C
 1000103D  A100300010                    mov  eax,[L10003000]
 10001042  C9                            leave
 10001043  C20800                        retn  0008h
;----------------------------------------------------------------------------------------------------
 10001046                           _DecCount:
 10001046  55                            push  ebp
 10001047  8BEC                          mov  ebp,esp
 10001049  FF0D00300010                  dec  [L10003000]
 1000104F  FF3500300010                  push  [L10003000]
 10001055  FF750C                        push  [ebp+0Ch]
 10001058  FF7508                        push  [ebp+08h]
 1000105B  E8ACFFFFFF                    call  SUB_L1000100C
 10001060  A100300010                    mov  eax,[L10003000]
 10001065  C9                            leave
 10001066  C20800                        retn  0008h
;----------------------------------------------------------------------------------------------------
 10001069  CC                            Align  2
 1000106A                           jmp_USER32.dll!SetDlgItemInt:
 1000106A  FF2500200010                  jmp  [********]  //故意把名字隐含了
;----------------------------------------------------------------------------------------------------
代码分三类:
  第一类与地址无关,它们二进制代码已经定下来了
  第二类与地址有关,它们二进制代码也定下来了,如果dll 加载到它的默认地址,代码不用修改
  第三类是代码还没有确定,用插桩来表示。如:
  jmp  [********], 它的插桩是 10002000 地址

先来解决插桩问题吧,这就是hello.exe 讲座中的导入表问题。
1. 内存地址 10002000-10000000(image_base) = 2000(RVA)
   说实话,image_base 我记不清位置,也没有明显标记,每次要查结构偏移。
   好在dll 通常是10000000,exe 通常是400000,不查也没有问题。
   我又查了一边,偏移是PE 标识后(不包括PE00标识)第13 个DWORD 偏移处。加深点印象,跟IAT在目录项偏移一样
   
2. RVA -> offset: 从节表知 RVA 0x2000== offset 0x600:
3. [0x600] == 2038,  RVA 2038==offset 638, [0x638]== "0207 setDlgItemInt", 前面是导出序号,后面是导出名称
; ---------------------------------------------------------------------------
loader 解决插桩的问题是从导入表开始的。导入表是目录项的第二项:
00000140   08 20 00 00 28 00 00 00                            . ..(...

导入表指向导入函数库数组。
RVA 2008 == offset 608, length=0x28 (一个导入表结构为5个DWORD-0x14, 故0x28为两项,一个有效项,一个全0尾标识)
第一项:
originFirstThunk == 2030, RVA 2030==offset 630 (IAT 的备份,供你看的)
Name = 2048,  RVA 2048 = offset 648 == "USER32.dll"
FirstThunk == 2000, RVA 2000 == offset 600 (IAT loader 加载时会更改这一部分,以完成插桩)
导入表给出了"USER32.dll",插桩处2038 给出了函数名"setDlgItemInt", loader 根据这些信息完成插桩。
; ---------------------------------------------------------------------------
至此我们已经复习了hello.exe 中讲过的东西了。

问5.4:其实话说的越多越不清楚,越少越容易扼要,把导入表部分再概括一下把。
答5.4:导入表在rdata 域。
  第一部分为导入地址表,这部分loader 在加载时会修改其数值完成插桩。
  第二部分为导入表。导入表是为IAT 服务的,loader 要修改IAT, 必须要知道导入函数的名称。
  这由导入表提供。导入表同时还提供,该函数名负责IAT 表中哪一个区间。
  所以,导入表是导入函数库数组。每个结构有5个DWORD 变量,2个没用。
  3个变量全是RVA, 一个指向函数名,一个指向IAT, 另一个也指向IAT,在结构最前面,但这个IAT loader 不会更改它。
  第三部分按地理位置划分是IAT 的备份表,就是导入表中Origin_FirstThunk指向的部分。
  第四部分是导入函数名表,前面是序号导出,后面紧跟名称导出。修改插桩当然要用这些信息
  第五部分为函数库名称区域。因为导入表只提供RVA, RVA指向的是这个区域。
    
问5.5:好,这样一看rdata 的上半部分意义就明白了,那下面还有一部分内容呢?
答5.5:这就是还没开始讲的导出表了。导出表比导入表简单。因为导入表可能从好几个库中导入,
  而导出表只是将自己的相关函数导出。
  
问5.6:为什么要把导出表和导入表放到一起呢。
答5.6:因为它们的属性相同。看节表rdata. 0x40000040,两个属性,我查了一下文档,前面那个1代表可读,
后面那个1代表代码包含初始化数据。

问5.7: 具体导出表要包含那些内容才算把函数导出了呢?
答:呦,拖课了! 今天主要是复习了一下前边讲的内容,下一课我们再讲导出表!