引言: 上一次以hello.exe 为例,介绍了pe 文件头,节表和导入表。
这一次我们以 count.dll 为例,介绍导出表和重定位表
count.dll 是罗云斌win32汇编编程中的例子程序,因其短小,故被选中。
问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: 具体导出表要包含那些内容才算把函数导出了呢?
答:呦,拖课了! 今天主要是复习了一下前边讲的内容,下一课我们再讲导出表!
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课