问:1.1
我知道程序中最重要的段是text段,请告诉我text 段在哪?
答:1.1
text 段在文件偏移0x400处,大小0x200字节,该区可运行,可读取,包含代码。
该区在内存中RVA 0x1000处,大小0x1000.

问:1.2
我用urtraedit 打开hello.exe 看了,在0x400处-0x600处,大部分都是0,为什么这样呢。
答:1.2
pe 格式大部分文件都是这样,这是对齐所要求的,文件对齐为0x200, 内存对齐为0x1000
你可以在NT_Option_Header 的Section_Alignment, File_Alignment 域中看到这两个数据。

//
// Optional header format.
//

typedef struct _IMAGE_OPTIONAL_HEADER {
//
// Standard fields.
//

WORD Magic;
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode;
DWORD SizeOfInitializedData;
DWORD SizeOfUninitializedData;
DWORD AddressOfEntryPoint;
DWORD BaseOfCode;
DWORD BaseOfData;

//
// NT additional fields.
//

DWORD ImageBase;
DWORD SectionAlignment;
DWORD FileAlignment;
WORD MajorOperatingSystemVersion;
WORD MinorOperatingSystemVersion;
WORD MajorImageVersion;
WORD MinorImageVersion;
WORD MajorSubsystemVersion;
WORD MinorSubsystemVersion;
DWORD Win32VersionValue;
DWORD SizeOfImage;
DWORD SizeOfHeaders;
DWORD CheckSum;
WORD Subsystem;
WORD DllCharacteristics;
DWORD SizeOfStackReserve;
DWORD SizeOfStackCommit;
DWORD SizeOfHeapReserve;
DWORD SizeOfHeapCommit;
DWORD LoaderFlags;z
DWORD NumberOfRvaAndSizes;
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;

问:1.3
慢点,别一下子贴那么多东西,我还没有找到 _IMAGE_OPTIONAL_HEADER 的位置呢,
告诉我怎样找
答:1.3
贴上那个_IMAGE_OPTIONAL_HEADER结构好说话,它的位置紧跟在 MAGE_FILE_HEADER 之后
告诉你个小技巧,那个Magic对NT x86来讲总是010B,在头文件找到那个010b,就是IMAGE_OPTIONAL_HEADER32 结构的地址。

问:1.4
问题越来越多了。
_IMAGE_OPTIONAL_HEADER 还没有说清呢,又出来一个IMAGE_FILE_HEADER。先不管IMAGE_FILE_HEADER
先按你的小技巧,在头部找到010b, 因为是little endial, 在ultraedit 中要找0b 01.
好,找到了,离那个 50 45 00 00 (ascii PE)相距不远,在偏移D8处,按你所说SectionAlignment和FileAlignment 应该在结构第9个,第10个DWORD 处。
好,找到了,在f8处有00001000, FC处为00 00 02 00 (我已经考虑了endian,以后不用提醒了)。
答:1.4
呀,进步不小吗?这样一下子你就把IMAGE_OPTIONAL_HEADER32 中所有的东西都找出来了。

问:1.5
是的,我可以把Optional header中所有东西都找出来,但我现在除了刚才介绍的第9个DWORD为内存对齐大小,第10个DWORD为文件对齐大小,其它我都不知道是干什么的?
答:1.5
别着急,其实还是很容易理解的,从字面意义就能猜大概。不过我们现在还不是通读Optional header的时候,还是拣我们最关心的问题插手吧。

问:1.6
还是回到text 段上来吧,刚才你对text段大小,位置,属性分析的头头是到
你是从那看出来的?
答:1.6
是从section header 中看出来的,每一个section, 都有一个section header 描述其位置,大小,属性。
section header 的结构是这样定义的
#define IMAGE_SIZEOF_SHORT_NAME 8

typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME];
union {
DWORD PhysicalAddress;
DWORD VirtualSize;
} Misc;
DWORD VirtualAddress;
DWORD SizeOfRawData;
DWORD PointerToRawData;
DWORD PointerToRelocations;
DWORD PointerToLinenumbers;
WORD NumberOfRelocations;
WORD NumberOfLinenumbers;
DWORD Characteristics;
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;

问:1.7
呦,慢点,怎么又往外甩结构,我很菜! 哦,不太多,还行吧。
不过你还是告诉我具体位置在哪吧,我好拿结构和数据对对号。
答:1.7
好,正是这种学习方法。你一定能学会的。
节表头是一个数组,它把所有节的位置,长度,属性放在了一起
紧跟在option header 之后,所以你从文件头部往下找就可以了。
看到IMAGE_SECTION_HEADER结构的第一个成员了吗,它是
BYTE Name【8】
这是节名称,你要找的text 段名字就是 .text, 你看ultraedit
ascii 码区离文件开始不远的地方,有一个.text, 对应的二进制
数据是2E 74 65 78 74, 这就是text 端IMAGE_SECTION_HEADER处

问:1.8
原来玄机在这里呀。我试试看。哦,看见了,在1B8处。 前8个
字节是节名称。后面的00 00 00 28 到底是物理地址还是虚拟大小,
(偷偷的,虚拟大小,表示内存中只有0x28个字节有效,其它全是0),在后面00 00 10 00 是虚拟相对地址 俗称RVA, 就是在内存中相对与起始地址的偏移。再后面00 00 02 00 为SizeOfRawData, 就是文件中大小,再后面 00 00 04 00 是
PointerToRawData,是文件的偏移 后面有三个DWORD 全是0,他们
是重定位信息和行号,很好,EXE文件可以不用管这些。最后一个
60 00 00 20 代表属性可读,可写,是代码。好,我终于理解你的第一句话了。
不解释一下,我怎么能一下子听的懂呢! 谢谢你。
那么我又有问题了。那程序针真是搜索这个.text字符串找到Text 节表头吗?
答:1.8
不是。前面说过,节表头紧随Optional header 之后。

问:1.9
Optional header 结构变量太多,我数了一下都没数清,到底占多少个字节呢?
答:1.9
正等着你这一问呢?是啊,数都数不清,纵是现在记住了将来也容易忘。
估计微软也想到了这一点,他把OPTION header 的大小放到了 _IMAGE_FILE_HEADER 的一个变量中,
下面是_IMAGE_FILE_HEADER 的定义
typedef struct _IMAGE_FILE_HEADER {
WORD Machine;
WORD NumberOfSections;
DWORD TimeDateStamp;
DWORD PointerToSymbolTable;
DWORD NumberOfSymbols;
WORD SizeOfOptionalHeader;
WORD Characteristics;
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
SizeOfOptionalHeader 一般总是0xE0

问:1.10
我今天已经学了不少东西了,看样子后面还很多的样子。再问最后一个问题。
FILE_HEADER 在文件什么位置呢。
答:1.10
这个简单,就在PE标识符后面。看到了吗,在C0处,ascii 是PE. 二进制是50 45 00 00

代学生:
哦,看到了,今天10个问题已经满了,我还想学,可是有点累了。。。
代老师:
今天就到这里吧,好好休息一下。


引言: 上一次以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 . . . .鐺...?
00000420 C2 0C 00 55 8B EC FF 05 00 30 00 10 FF 35 00 30 ?.U嬱 .0.. .0
00000430 00 10 FF 75 0C FF 75 08 E8 CF FF FF FF A1 00 30 .. . .柘 0
00000440 00 10 C9 C2 08 00 55 8B EC FF 0D 00 30 00 10 FF ..陕..U嬱 .0.. br /> 00000450 35 00 30 00 10 FF 75 0C FF 75 08 E8 AC FF FF FF 5.0.. . .璎 br /> 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: 具体导出表要包含那些内容才算把函数导出了呢?
答:呦,拖课了! 今天主要是复习了一下前边讲的内容,下一课我们再讲导出表!
问5.7: dll 和 exe 文件都是PE 文件, 在PE 文件中是怎样区分的呢?
答5.7:有一些小差别。
例如, exe 通常被加载到0x400000, 而dll 默认加载是0x10000000
exe 通常不含reloc 段,而dll 包含。
exe 通常不含export 段, 而dll 包含。
exe 属性010f, 最后1bit 说它没有重定位信息
dll 属性210e. 最高位的2 说它是dll, 这个好像是最关键差别了吧。

问5.8:exe 的 entrypoint 是程序执行的起始点,dll 的 entrypoint 是什么呢?
答5.8:这个地方是dll 的入口函数地址,它是不可以省略的。
dll 在加载,卸载以及线程加载,卸载时都会
到这里执行程序。 它的通用结构是这样的。
BOOL WINAPI DllMain(HINSTANCE hinstDLL,DWORD fdwReason,LPVOID lpReserved)
{
switch(fswReason)
case DLL_PROCESS_ATTACH
......
case DLL_THREAD_ATTACH
.....
case DLL_THREAD_DETACH
.....
case DLL_PROCESS_DETACH
....
return (TRUE or False) // true , 成功,false 失败, loader 会把它从内存卸掉。
}
count.dll 中没有按这种结构,它只是简单的返回一个TRUE,因为它不需要申请内存和释放内存等初始化操作。
count.dll entrypoint 是10001000, PE 标识后第10 DWORD 地址,记不住用工具查。

问5.9:前面说过dll 有入口函数, 导出函数和非导出函数。又回到上次未讲的问题
导出表是怎样把函数导出的。
答5.9:我们先猜一猜导出函数的关键要素吧。
1. 导出库名称
2. 函数导出序号。(提供序号导出)
3. 导出函数名 (提供函数名导出)
4. 序号或函数名对应的地址。
它向系统报告这些信息已经足够了。

问5.10: 结合例子和结构定义具体说一下吧。
答5.10: 看count.dll 目录项第一项
00000130 60 20 00 00 5C 00 00 00 ` ..\...
位置 RVA 2060 == offset 660
大小 0x5c
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 t._IncCount.

那么这是一个什么样的数据结构呢?我们先猜猜看,这里也叫逆向学习方法吧。
首先函数名称:count.dll(offset 69c), 函数名称_DecCount(offset 6a8), _IncCount(6b2) 都已经看到了。
转换为RVA. offset 69c =RVA 209c
offset 6a8 =RVA 20a8
offset 6b2 =RVA 20b2
很高兴在数据中找到了9c 20, a8 20, b2 20. 关注一下这些地址。
从660 在滤一下,感觉后面的00000002 可能是个数吧。
后边的2088, 2090,2098
RVA 2088 == offset 688 [688] == 46 10 00 00 23 10 00 00 // 像函数地址,赶紧确认一下(在上一篇),正是。
RVA 2090 == offset 690 [690] == A8 20 00 00 B2 20 00 00 // 函数名称地址
RVA 2098 == offset 690 [690] == 00 00 01 00 // 像hint
这样划分后,看不懂的就不多了,学习数据结构是时候了。
typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics;
DWORD TimeDateStamp;
WORD MajorVersion;
WORD MinorVersion;
DWORD Name;
DWORD Base;
DWORD NumberOfFunctions;
DWORD NumberOfNames;
DWORD AddressOfFunctions; // RVA from base of image
DWORD AddressOfNames; // RVA from base of image
DWORD AddressOfNameOrdinals; // RVA from base of image
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
我们分析的是对的,那个base 是什么东西 ?是导出函数的基序号。所以导出函数序号不是0,1,而是1,2 了
很明显,1指的是1046地址,2指的是1023地址
问5.11 再总结一下导出表吧。
答5.11 导出表有两种导出方法,一种是按名称导出,一种是按序号导出。其中序号导出的个数总是大于等于名称导出的个数。
导出的函数地址按4字节一字排开。每一个函数索引号就是+Base 值就是导出序号。
序号不直观,所以有些函数用名称导出,名称导出最终还是要找到函数序号。所以把名称所在的名称数组的位置为索引
去从名称序号数组中拿到序号(此为索引号),由索引号取到函数地址。

问5.12 假如loader 要插桩本函数 _IncCount 地址,它是怎样操作的。
答5.12
1. 它首先要加载我们的dll. 用loadlibrary
2. 找到我们的导出表。
3. 再找到导出表中AddressOfNames。
4. 遍历该表找到_IncCount 函数,记下它的索引
5. 从AddressOfNameOrdinals数组中,取到该索引对应的函数地址序号
6. 从导出表中找到AddressOfFunctions, 用得到的序号去取到它的地址。
7. 将该地址去填充到IAT 的对应位置上。

这样插桩就完成了。哇!这个小插桩要经过这么多步骤哇,有没有办法简化一下啦... 等着你去实现呢!

问5.13 count.dll 中还有一个reloc 节,讲讲它是怎样构成的。
答5.13 reloc 也是为text 段服务的,前面说过,若dll 加载到它默认位置,可以不用reloc 段。
当不能加载到默认地址时, 某些于地址有关的指令需要重新定位。就是说要修改指令中地址
使其指向正确的地址。
看目录项中reloc 表,第6个表(索引号为5):
00000160 00 40 00 00 18 00 00 00 .@......
RVA 4000 = offset 800, size=0x18
哦,纵使不用reloc 目录项,直接目视也看见它了。这个程序很小,是这样的。
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..........
我们也像export 表一样,先猜猜reloc 结构应该有那些重要元素。
1. 内存地址, 4byte, 我们要知道对哪的指令进行重定位。即where 问题
2. 替换方法。 是替换一字节,2字节还是4字节, 是how 的问题,估计有几个bit 就够了。
3. 用什么替换。 是一个what 的问题。 这个问题就不用考虑了,这个what,就是加载地址与默认地址偏移。
这样看起来一个reloc 项至少也要 5 bytes 了。如果有很多项,那这很多项就构成一个数组。
我这里介绍的方法是一种逆向的学习方法,或者是原始的思考方法。因为我想最初设计这个PE 结构的人也会
这么想。
现在使用的PE 结构在这个想法的基础上进行了优化,使得reloc表占用较少的字节,
具体说是它让一个reloc 项占用2byte,下面看它的方法:

1.
typedef struct _IMAGE_BASE_RELOCATION {
DWORD VirtualAddress;
DWORD SizeOfBlock;
} IMAGE_BASE_RELOCATION;

它的意思是说,它要重定位VirtualAddress=1000这块区域, 该区域所占的重定位信息大小为SizeOfBlock=0x18
后面紧跟的每2 个bytes 构成一个重定位项, 其中低12 bit为重定位地址, 高4bits 为重定位类型。
我这里把重定位类型copy 过来,其中有的在x86 上是用不到的。
//
// Based relocation types.
//

#define IMAGE_REL_BASED_ABSOLUTE 0
#define IMAGE_REL_BASED_HIGH 1
#define IMAGE_REL_BASED_LOW 2
#define IMAGE_REL_BASED_HIGHLOW 3
#define IMAGE_REL_BASED_HIGHADJ 4
#define IMAGE_REL_BASED_MIPS_JMPADDR 5
#define IMAGE_REL_BASED_SECTION 6
#define IMAGE_REL_BASED_REL32 7

#define IMAGE_REL_BASED_MIPS_JMPADDR16 9
#define IMAGE_REL_BASED_IA64_IMM64 9
#define IMAGE_REL_BASED_DIR64 10
#define IMAGE_REL_BASED_HIGH3ADJ 11

地址只有12为意味着你只能管理4K 范围,是的,如果超过了4K范围,我们重新定义一个IMAGE_BASE_RELOCATION
变量就可以了,它又能管理下一个4K的范围。后续没16bit 为一个reloc 项, 最尾部以0000 结尾。当然,由于
重定位块大小有定义,纵使不用0000标识结尾也没有问题,把这里的冗余姑且叫双保险吧,就是浪费了2bytes
count.dll 中,我们先算算有几个重定位项。哦,不用算,有7个,一查就查出来了。
但当程序大的时候还是要计算的。(0x18-8)/2-1=7 个。
计算公式:(SizeOfBlock-sizeof(IMAGE_BASE_RELOCATION))/2-1

问:5.14 那这7个重定位项到底是怎样重定位的呢?还是给出具体结果更彻底。
答:5.14 好,做事做到底,现在我们就开始吧。
第一项:28 30 其内容为 0x3028 重定位类型为3, 地址为0x28
其它重定位类型都是3, 地址不同而已。
#define IMAGE_REL_BASED_HIGHLOW 3
那么这个BASE_HIGHLOW 是什么意思呢?我们先看看0x28处的指令。
10001026 FF0500300010 inc [L10003000]
假如我们的dll 不是被加载到0x10000000,而是加载到0x30000000, 即向上提高了0x20000000
我们只要把0x28地址处也加上0x20000000,就可以了。这样这条指令就变成了
10001026 FF0500300030 inc [L30003000]
看明白了吧! 窗户纸就是这样被捅破的


好了,本帖就到这里结束吧!