标 题: 【原创】PE 文件格式启发式学习(以hello.exe 为例)
作 者: hjjdebug
时 间: 2008-5-11 21:07

问: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个问题已经满了,我还想学,可是有点累了。。。
代老师:
今天就到这里吧,好好休息一下。


更新记录

第二部分:  9楼
第三部分:   13楼
第四部分:   20楼

  • 标 题:答复
  • 作 者:hjjdebug
  • 时 间:2008-05-12 13:19

接问题1.10后的第11个问题
问:2.11
干脆就把位置问题先问到底吧。PE标识符 54 45 00 00 总是在文件偏移00c0 处吗。
答:2.11
基本上可以这么说,主要是因为前面的部分是dos 头部和dos 体
dos 头部 IMAGE_DOS_HEADER 的结构我就不贴了,因为dos 已经离我们远去,它
已经失掉了意义,dos 体也几乎是固定不变的了。这部分的作用是当你拿这个
PE程序到dos 系统上运行时,dos 执行会在控制台上打印一行提示信息,
"this program cannot be run in DOS mode" 然后停在哪。总比你一运行,DOS
就hang 机强多了。如果拿PE代码在DOS 下直接执行,不用说那肯定hang 机。
微软就是怕这个事情发生才用了这么一个措施。
现在你只需要记住一件事,文件头两个字母是MZ标记,在3c偏移地址,00 00 00 c0
指的是NT header 的文件偏移,如果这个偏移处的标识正好是PE. 可以肯定,这个文件
就是PE 文件了。 如果在3c偏移地址处存其它DWORD 地址,那就到所指定的地址去找
如果该处正好ASCII "PE",此处就是NT的header 。

问:2.12
怎么有这么多header, 能否概要总结一下:
答:2.12
好的。在文件开头部分是 _IMAGE_DOS_HEADER ,小名MZ header, 我们已经不用关心它了。
只要关心地址偏移0x3c 处,该处存有 _IMAGE_NT_HEADER 的偏移。在dos header 和
NT header 之间是dos 体,我们也不用关心它了。
_IMAGE_NT_HEADER 到底是什么样呢?它实际是PE00标识+ NT_FILE_HEADER+NT_OPTION_HEADER
以下是它的结构声明。
typedef struct _IMAGE_NT_HEADERS {
    DWORD Signature;        //这里的标记是 PE00
    IMAGE_FILE_HEADER FileHeader;    //NT header 包含FILE header 和Option header
    IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;


问: 2.13
这样对header有了一个总体认识,它占据着文件开始部分。反正它是死的,而且每个文件只有
一个,有上面各个header 的结构定义,无非是存储这一些数据,指针。估计详细分析一下,
它也跑不了了。我们还是抓主要的,主要的分析清了,可能顺便就把头中的相关结构变量分析了。
还是回到节表上来。上次已经找到了节表头,前面说是在OptionalHeader下面,现在也可以
说是在NT header下面。其中以.text 居首,根据节表头结构,从.text 偏移一个节表结构,
我们看到了第二个ascii 字符 “.rdata", 不远的地方还要一个”.data", 正好也偏移一个节表头结构“,
还有一个".rsrc",再往后就是全0了,那么这是否是说,这个结构数组含有4个结构元素呢?
答: 2.13
正是如此,在_IMAGE_FILE_HEADER 中有一项定义了该数值  
    WORD    Machine;      //x86 的machine代码是 01 4c (hello.exe 中00c4处)
    WORD    NumberOfSections;    // hello.exe 中 是 00 04
与你数的完全一致。对照一下问题1.9的_IMAGE_FILE_HEADER 和 hello.exe 的,你会很容易辨别的。

问: 2.14
我对照过了,知道了它在文件中的位置。干脆把NT FILE Header结构中的其他数据也分析一下吧。反正也不多。
答: 2.14
好,我再把该结构抄过来:
 typedef struct _IMAGE_FILE_HEADER {
    WORD    Machine;      //答2.12已经说了,x86 总是01 4c
    WORD    NumberOfSections;    //hello.exe 是00 04
    DWORD   TimeDateStamp;    //时戳。表示你的文件是何时生成的。不过这个DWORD是用秒数表示的。
    DWORD   PointerToSymbolTable;  //调试信息,hello 中为全0
    DWORD   NumberOfSymbols;    //调试信息,hello 中为全0
    WORD    SizeOfOptionalHeader;  //答1.9已经说了,OptionalHeader 大小总是0xe0
    WORD    Characteristics;    // hello.exe 是010F, 看标志有5个bit 是1,那就是说5个属性为真了。
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
NT FileHeader其它项都好理解,没有什么关键的东西,只有这个属性稍微麻烦一点,最多也不超过16个属性。
顺便问一下,你在ultraedit 中看着这个FILE_HEADER 吗?
代学生:哦哦,看着呢,我的光标就停在这个010F 标志处呢。
代老师:
  好,继续。16个属性一下都说出来也太多,先学习hello.exe 的这5个吧。
  bit0: 文件不包含重定位信息。
  bit1: 文件可以运行。
  bit2: 文件不包含行号信息
  bit3: 文件不包含符号信息。
  bit8: 32bit 机器上运行。
  怎样,这你个属性很好理解吧,可执行文件不需要重定位,行号及符号信息。在32bit机器上运行。
代学生:
  说了半天原来没有一个关键性东西,我还以为有多神秘呢!
代老师: 是的,搞懂了它有时也觉得失望,其实,懂了也就这么简单。    

问:2.15
再问一个关键性问题,看起来有点菜。我以为,有text段,有data段就可以了,
那么.rdata, .rsrc 是干什么用的呢
答:2.15
这个问题确实很关键,这正是PE 文件与以往文件的差别所在。
其实只有text段,data段,pe文件是不可以运行的,因为PE文件的运行总是要调用
系统文件,而系统文件都是以DLL 文件格式存在的,所以你必须要在文件中有动态链接
信息。

问:2.16
太复杂了,什么是动态连接信息,什么是动态连接库,听说过但没有真正理解。
答:2.16
动态连接是多进程操作系统引进的一个概念。在DOS时代,单任务是没有动态连接的。
在DOS 时代,连接器总是把库文件直接连接到可执行文件中。叫静态连接。这种做法
在单任务时是可以接受的,无非是每个连接的文件都包含一个库文件,造成磁盘空间
的一点浪费。 但静态连接在多任务时代不可以接受。例如每个进程都会调用 kernel32.Dll
如果采用静态连接,造成磁盘空间浪费不说,假若系统有20个进程,系统中就将会
有20份kernel32.dll, 内存的浪费将是不可容忍的,动态连接的概念就是保证系统中
只有一份DLL,同时各个进程又都能够很好的运行。好在动态连接是加载器的功能,
我们程序不用刻意去做什么,所以用起来也不是太复杂。

问:2.17
哦!是这样,我原来以为链接程序都把事情处理好了,原来还没有,多进程中还要由
加载器进行动态连接。那我们怎样使用动态连接呢?
答:2.17
当我们用汇编语言或C C++或者其它语言开发是,生成的PE文件对系统库的调用都是
动态连接。我们并没有做什么。
当你想调用自己生成的DLL(第三方DLL),可以采用隐含动态连接或者显示动态连接来
加载DLL. 听起来很炫用起来很简单,隐含链接就跟使用系统dll 一样,你只要在文件
中包含第三方头文件(好引用它的函数啊。)在连接选项里设置第三方的lib,dll位置
链接程序就帮你搞定了。
显示动态连接是在你的程序里用loadlibrary 加载DLL, 用getprocess 获取DLL中函数
地址,然后用函数指针调用第三方函数。

问:2.18
哦. 听你的意思看来使用DLL 也是很简单的。系统DLL使用我们不用管,怎样使用第三方
DLL以后再说吧,我现在的重点是想搞明白PE 的文件格式。 那么既然它调用了
kernel32.Dll 中的函数。而这个函数的地址连接程序不知道,只能由加载器在运行时动态加载。
那么,加载器是怎么知道要加载那个DLL, 要执行DLL中的那个程序呢?
答: 2.18
这个问题问到点子上去了。搞清了这个问题,PE 格式就可以说入门了。
我们还是结合hello.exe 实例说吧。
有HIEW 软件吗,准备一下。
好: 
1. 用hiew 打开hello.exe
2. 按F4, 选Decode.
3. 按F5, 敲入偏移400(我们上面分析过,text 段在400)
干脆我把代码贴过来吧, 双// 是我注释的。
00000400: 6A00                         push        000
00000402: 6800304000                   push        000403000 ;" @0 "
00000407: 680C304000                   push        00040300C ;" @0♀"
0000040C: 6A00                         push        000
0000040E: E809000000                   call        00000041C  //hiew 分析出,这是MessageBoxA
00000413: 90                           nop
00000414: 90                           nop
00000415: 6A00                         push        000
00000417: E806000000                   call        000000422  //hiew 分析出,这是ExitProcess
0000041C: FF2508204000                 jmp         d,[00402008]
00000422: FF2500204000                 jmp         d,[00402000]
; ---------------------------------------------------------------------------
我们分析 call Messagebox 吧。
40e 处: call 41c, 
41c 处: jmp ds:[00402008]
00402008.
这是虚拟内存地址。我们用hiew 找到它。具体操作如下:
1.  按F4, 选hex. 要看数据,选hex 合适
2.  按F5, 敲入偏移600.
在hiew 中看到了下一行。
.00402000:  76 20 00 00-00 00 00 00-5C 20 00 00-00 00 00 00

问:2.19
喂,慢点,打扰一下,我这个人就喜欢刨根问底。你怎么知道要敲入600呢?
答:2.19
是这样,调用DLL采用动态连接,动态连接信息是放在.rdata 段,叫只读数据段。
你刚才不是问.rdata, .rsrc 是干什么的吗? 我现在才讲到了.rdata 段
要想知道.rdata 在哪,你的问.rdata 的节表头。我们已经讲过所有节表头组成一个
数组,紧跟在NT Header(总header)之后。.rdata 是第二项节表头,其名字是ascii码
很明显的。 我把它copy 到这啦。
00001e0: 2e72 6461 7461 0000 9200 0000 0020 0000  .rdata....... ..
00001f0: 0002 0000 0006 0000 0000 0000 0000 0000  ................
0000200: 0000 0000 4000 0040 2e64 6174 6100 0000  ....@..@.data...

这里根据 答1.6中 section header 的结构,可以知道.rdata 的如下信息
名字:.rdata, 虚拟大小:0x92, RVA:0x2000, 文件中大小0x200, 文件偏移0x600
属性,40000040,看来有两个属性有效。bit6 -- 包含初始化数据。bit30 -- 可读属性。

问:2.20
再回到刚才的问题吧。本来想call Messagebox, 由于Messagebox 是user32.dll 中的函数。
汇编器,链接器不知道Messagebox 地址在哪里,PE 文件里翻译的是代码要call 41c,
而41c 处向 402008 地址单元处存储的地址跳跃。由于402008 从文件看来存的是0x205c
看样子要跳到 0x205c 处执行了。
答:2.20
你前面的分析都是对的,但最后一句结论却错了,如果程序真要跳到0x205c 处,肯定会被系统
判你一个操作地址非法,强制你关闭程序。其实这个地方的205c,要被替换成另外的一个数据
这就是Messagebox 在内存的真正地址。谁替换的? 当然就是loader 了,加载器知道你想要
call MessageBox, 就帮你偷偷的把这个链条给拧上了。

代学生: 呀!讲的正精彩呢,怎么又下课了...
代老师: 好了,我们下一课再接着讲。

  • 标 题:答复
  • 作 者:hjjdebug
  • 时 间:2008-05-13 16:32

 
接问题2.20后的第21个问题
问:3.21
上回说到loader 为了使程序正常运行,偷偷的把.rdata 节中的某些数据给改了,能再讲清楚一些吗?
答:3.21
加载器未改之前,我们用hiew 看文件有如下数据
.00402000:  76 20 00 00-00 00 00 00-5C 20 00 00-00 00 00 00
加载器改动之后,我们可以用ollydbg 看一下,这是加载器完成修改后的结果
具体操作是用ollydbg 加载hello.exe,点击数据窗口,按ctrl-G,输入地址402000
然后看到如下数据
00402000 >DA CD 81 7C 00 00 00 00 8A 05 D5 77 00 00 00 00  谕....?....
这就说明,加载器把2076 改成了7c81cdda, 把205c 改成了 77d5d58a

问:3.22
呦,还藏着这等玄机呢! 不过,加载器也是一段程序,它总不能乱改吧?咱就以后面
的205c 为例,它凭什么要把205c 改成77D5058A呢?这个可是我们要call 的MessageBox的地址呢。
答:3.22
连接器知道你要CALL MessageBox, 但MessageBox 是USer32.dll 的函数,连接器不知道
MessageBox 在哪里,只有操作系统才知道USer32.dll 在哪里。loader 也是通过调用系统
函数才知道的。由于link 的时候文件还没有运行,所以它不知道MessagBox 的具体地址。
好,这个问题讲清了,连接器不知道DLL及其函数的具体地址。
但连接器也会尽其所能,告诉加载器一些信息,他对加载器说,运行前你要帮我把USer32.dll
的MessageBox 地址给填好!拜托了!
用计算机来描述是这样的。
他往402008处填了一个205c, 这个205c 是什么,是一个RVA, 它指向一个数据结构,该结构
实现linker 向 loader 的信息传递, loader 来完成linker 未完成的使命。
如果你要想看看linker 向 loader 说了什么,咱还得先看看这个数据结构的地址。

问:3.23
就已hello.exe 为例吧,看看205c 怎么找到那个数据结构地址的。
答:3.23
loader 拿到了数据205c, 知道这是一个RVA, 加上影像基地址0x400000,或者说叫module 地址吧。
得到了一个虚地址0x40205c,你从ollydbg 的数据窗口中看0x40205c 地址。是如下内容:
0040205C  9D 01 4D 65 73 73 61 67 65 42 6F 78 41 00 75 73  ?MessageBoxA.us
0040206C  65 72 33 32 2E 64 6C 6C 00 00 80 00 45 78 69 74  er32.dll...Exit
0040207C  50 72 6F 63 65 73 73 00 6B 65 72 6E 65 6C 33 32  Process.kernel32
0040208C  2E 64 6C 6C 00 00 00 00 00 00 00 00 00 00 00 00  .dll............


它指向一个WORD 数据019D, 后跟MessageBoxA 0字终结字符串,其后再跟USER32.dll。
这个结构有一个学名,叫 IMPORT_BY_NAME,如下定义:
//
// Import Format
//

typedef struct _IMAGE_IMPORT_BY_NAME {
    WORD    Hint;
    BYTE    Name[1];
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;

Hint, 就是那个019d 了,BYTE Name[1], 这个变量定义看起来很奇怪,是吗?
代学生: 是的,我很少见到这种定义。
  通常我们会定义 char buffer[256], BYTE data[8]; 等类型。
  BYTE Name[1], 只包含一个元素的数组,它也装不下后面的"MessageBoxA"字符串啊。
代老师:这种定义是一种指针的变通用法。
  如果你真要定义成数组来包含后面的字符串,你定义成多大呢?定义成100,短字符串浪费,
  长字符串可能就真能碰到一个101个字符的名字,你定义的还是占不下。
  所以说,这个Name[1], 不是要你往里面装东西的,C 语言里,你可以借助这个Name 变量访问到它对应的地址。
  这种用法通常是很少用的。因为它毛病很多,例如结构后面不能再定义其它变量了,必须是最后一个,定义了
  数组又不用它装东西,也不符合数组的初衷. 所以你只有明白这个道理就可以了。
  
代学生:既然它那么不好用,为什么还那样定义呢。
代老师:还是那句话,是变通。
  你看,它简洁,它完成了使命。否则你就要把结构变一变,例如按常规估计应该是这样子。
  WORD Hint; BYTE *pName; 然后你要求微软说,Hint 后面不要跟字符串,要跟一个地址。这样C语言好写。
  好比说大部分人沿着盘山路往山上走,也有人愿意盘着荆棘往山上爬,后者绕了近路,但风险也大。
  
代学生:讲了这么多,其实我看一个word 后面跟着一个0字终结符字符串,还是很好理解的吗。
代老师:C 语言以其简洁,高效,使我们受益良多。但在某些特殊的情况下,它也会力不从心。有时刻当你看着一堆堆
  结构套结构,一堆堆宏套宏令你头晕时,而看看它最终的list 表或二进制输出反而能令你豁然开朗。
  哦,有点扯远了。 我还是最喜欢C的。
  
问: 3.24
找到了这个 IMPORT BY Name 结构, loader 是怎样根据这些信息修改地址的呢?
答:3.24
loader 在加载时,是要先收集信息的,不过这部分还没有讲,收集好后,以MessageBoxA为例
loader询问系统,USER32.dll 加载了吗?没有我要先加载它啦。哦,加载了,告诉我MessageBoxA 
函数地址是多少?系统返回一个地址 77d5d58a, loader 就把这个地址覆盖了原来存储的 0x205c 。
这样就把MessageBoxA 的内存地址给拧上了。
注意了,这个 77d5d58a 是我机器上的MessageBoxA 内存地址, 到了你的机器上,它就变成别的啦。
这正是DLL 存在的妙处。
意思表达很明确,不是吗?

问:3.25
对,它肯定能表达明确。不过目前我还有很多问题要问,别嫌麻烦呦。我问得可是很细致的。
答:3.25
难得你精神可嘉,咱们也是互相促进的。能走到这里,也可以说是渐入佳境了。

问:3.26
弱弱得问一下,3.23 提到的那个module 地址0x400000, 是固定死的吗?
答:3.26
module 加载地址,是loader 将应用程序加载到内存时的起始地址,loader 是从Option header
结构中的Image Base 项得到这个地址的,由于exe 文件不存在地址冲突问题,所以loader 总能
把程序加载到Option header 中Image Base 指定的位置,这个位置通常都是0x400000.

问:3.27
刚才没顾上问,MessageBoxA 前面那个WORD 019d, 就是hint, 是干什么的。
答:3.27
那个东东是loader 向系统询问函数地址的另外一种方式,它可以向系统询问user32.dll 第019d 个
导出函数是多少 ? 系统回答 77d5d58a 。 这种导入方式叫ordinal. DLL 中有名称的导出函数都可以
用名称访问,也可以用ordinal 访问,而有的导出函数没有名,只能用ordinal方式导入。

问:3.28
刚才你是用ollydbg 来讲解的,我们不是一直用ultraedit 打开这个文件,直接分析它的二进制
结构的吗。能用ultraedit 再讲一讲linker 向 loader 说了什么吗?
答:3.28
这主要是因为动态链接的数据是一个RVA, 所以用ollydbg 讲的方便。同时由于在ollydbg中已经完
成了动态连接,跟utraedit 静态分析正好有个对照。现在我们再来看从ultraedit 中怎样找到 ???结构
loader 拿到了数据205c,  哦, 不对,是我们拿到了205A,知道这是一个RVA.需要把它转成文件偏移
好看看它到底对我们说了什么。
从RVA 到 OFFSET, 我们好像还没有讲呢,就在这里补上吧。
从RVA 到 OFFSET 没有一个简单的公式,唯一的办法就是查表。
查什么表,查section header 表。
已hello.exe 为例,我们查表,看到205c 落入.rdata 节,该节的虚拟地址(就是内存地址)0x2000
对应文件偏移0600,那么205c, 则对应文件偏移的065c. 用ultraedit 观看,如下图示。跟ollydbg看到的内容一样。
0000650: 0000 0000 5c20 0000 0000 0000 9d01 4d65  ....\ ........Me
0000660: 7373 6167 6542 6f78 4100 7573 6572 3332  ssageBoxA.user32
0000670: 2e64 6c6c 0000 8000 4578 6974 5072 6f63  .dll....ExitProc
0000680: 6573 7300 6b65 726e 656c 3332 2e64 6c6c  ess.kernel32.dll

代学生:顺便问一下,那些查虚拟地址到文件偏移转换的工具是这样查的吗?
代老师:是的。

问3.29
.rdata 节中大部分数据都明白了,但从610-65d 那一段数据是干什么的?
答3.29
这段数据,当然也是动态加载用的。
前面讲的link 与 loader 对话,确实如上所说,但那只是问题的一半,还有一半就是,当loader 把程序
加载到内存,它要修改数据,它怎样找到修改数据的地址,也就是说,那个存储着RVA 205c 的地址
00402008 loader 是怎样得到的?还有,它怎么知道MessageBoxA在user32.dll 里面?

问3.30 
平时都是我问,现在忽然被反问。翻翻前面讲的 。。。
啊! 是2.18 时提出来的,程序要call 41c, 41c 要向402008 地址所装的内容处跳。
哦,这是我们的读法。loader 是不会这么读的,loader 是死的,loader 是一段程序,它只会干机械的事。
它怎么找到402008的,它怎么知道MessageBoxA在user32.dll 里面? 还是听你讲吧。
答3.30
讲清这个问题,我们还要再看option header. 坚持住,动态加载导入部分也就差这一点点了。
在1.2中提到option header 的数据结构,在他的底部有一个成员。
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
看着眼晕,还不如写简单点,它的数组就是16个元素
IMAGE_DATA_DIRECTORY DataDirectory[16];

前面那个结构叫数据目录,如下定义
//
// Directory format.
//

typedef struct _IMAGE_DATA_DIRECTORY {
    DWORD   RelativeVirtualAddress;
    DWORD   Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

挺简单的,其实就是8个字节。 16个目录吗,就是16*8 = 108 字节。占8整行
别担心,大部分没有用,微软在这里搞捉迷藏。我们只关心几个就行了。
这里首先介绍的一个叫导入表(import table)。
0000130:         0000 0000 0000 0000  ................
0000140: 1020 0000 3c00 0000 0040 0000 a003 0000  . ..<....@......
0000150: 0000 0000 0000 0000 0000 0000 0000 0000  ................
0000160: 0000 0000 0000 0000 0000 0000 0000 0000  ................
0000170: 0000 0000 0000 0000 0000 0000 0000 0000  ................
0000180: 0000 0000 0000 0000 0000 0000 0000 0000  ................
0000190: 0000 0000 0000 0000 0020 0000 1000 0000  ......... ......
00001a0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00001b0: 0000 0000 0000 0000 2e74 6578 7400 0000  .........text...

为什么还留下那个.text, 一看就知道,.text 代表的是section header table 的开始地址
其上的8个整行就是16 个目录了。真要让你像计算机一样从上到下数偏移,我们还真不行,
找ascii 字,我们还在行。
第一个目录叫导出表,这里为空
第二个目录叫导入表,这里虚拟地址是0x2010, 大小是3c.
2010 RVA 对应的文件偏移是610 (算法我想你已经掌握了,看3.28)
第三个目录叫资源表。RVA=0x4000, size=0x3a0 (暂时先不讨论)
第十三个目录叫IAT 表,英文全称为:Import Address Table. RVA=0x2000,size=0x10
其它的目录都是空的,没用,不理它们。

代老师:哦,是不是讲的有点多了?
代学生: 还行,反正讲得挺清楚的。想不到这个小小的hello.exe 还有这么多事情。
不过再看看hello.exe文件,大部分内容都讲了,我想也不会有太多东西了。
代老师:正是,坚持一下就是胜利。

  • 标 题:答复
  • 作 者:hjjdebug
  • 时 间:2008-05-15 14:49

接问题3.30后的第31个问题
问4.31:
上回谈到目录项,有两个目录项要关心。
1. 导入表:RVA=0x2010, size=0x3c
2. 导入地址表(俗称IAT): RVA=0x2000,size=0x10
具体什么用途呢?
答4.31:
先看IAT表,loader要做的工作是把RVA=0x2000,size=0x10这么大范围的DWORD
都要重新定址。如果是00 00 00 00 就不用了。
对hello.exe 而言文件偏移是:(今后RVA 和 OFFSET 的转换我就不再提了)
0000600: 7620 0000 0000 0000 5c20 0000 0000 0000  v ......\ ......

再看导入表RVA=0x2010, offset=610. size=0x3c,内容如下:
0000610: 5420 0000 0000 0000 0000 0000 6a20 0000  T ..........j ..
0000620: 0820 0000 4c20 0000 0000 0000 0000 0000  . ..L ..........
0000630: 8420 0000 0020 0000 0000 0000 0000 0000  . ... ..........
0000640: 0000 0000 0000 0000 0000 0000 
这里是一个结构数组。该结构叫导入表的描述符
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
    union {
        DWORD   Characteristics;            // 0 for terminating null import descriptor
        DWORD   OriginalFirstThunk;         // RVA to original unbound IAT (PIMAGE_THUNK_DATA)
    };
    DWORD   TimeDateStamp;                  // 0 if not bound,
    DWORD   ForwarderChain;                
    DWORD   Name;
    DWORD   FirstThunk;                     // RVA to IAT (if bound this IAT has actual addresses)
} IMAGE_IMPORT_DESCRIPTOR;

每个结构包含5个DWORD 成员,最后一个结构,成员全是0
结构中第二个TimeDateStamp,第三个成员ForwarderChain不用关心,它们总是0
name 变量:
  第一个结构中为206A, 对应文件偏移66A, "user32.dll"
  第二个结构中为2084, 对应文件偏移684  "kernel32.dll"
FirstThunk 地址:
  第一个结构中为2008, 对应文件偏移608, //我们看到这个就是IAT 表的内容了
  第二个结构中为2000, 对应文件偏移600,//我们看到这个就是IAT 表的内容了
OriginalFirstThunk 地址:
  第一个结构中为2054, 对应文件偏移654, //654与608 存储的内容是一样的
  第二个结构中为204c, 对应文件偏移64c,//64c与600 存储的内容是一样的
怪不得一个叫first thunk, 一个叫OriginalFirstThunk,原来所指地址处包含的内容是一样的.
  
0000640: 0000 0000 0000 0000 0000 0000 7620 0000  ............v ..
0000650: 0000 0000 5c20 0000 0000 0000 9d01 4d65  ....\ ........Me    
0000660: 7373 6167 6542 6f78 4100 7573 6572 3332  ssageBoxA.user32
0000670: 2e64 6c6c 0000 8000 4578 6974 5072 6f63  .dll....ExitProc
0000680: 6573 7300 6b65 726e 656c 3332 2e64 6c6c  ess.kernel32.dll

导入描述符有2个,说明它有两个DLL 要导入,从Name 能看出来,
第一个叫"user32.dll", 第二个叫"Kernel32.dll"
描述符的 FirstThunk, 指向一个导入函数地址数组,就叫thunk data数组吧,该数组最后一项为00000000
描述符的 OriginalFirstThunk ,虽然与FirstThunk所指地址不同,但该地址所包含的内容却一样。
以第一个结构为例:
这个DWORD 数组为5c20 0000 0000 0000 ,这个数组只有两项,去掉末尾标志项,只有一项,说明
只有一个函数要导入。怎么导入,在这个地方,我们以0x205c 为例, 把3.23中叙述过的导入过程补充完整。
1. loader 从目录第二项得到 Import Table(导入表)
2. 导入表是一个导入描述符结构数组,以第一个结构为例
   从名字项中它知道,这个动态库的名字叫 "user32.dll"
   。   
3. first thunk 指向thunnk data数组,是一个以全0结尾的DWORD 数组,
   非全0 的元素或者是一个RVA(最高位为0),指向一个Import_BY_NAME结构,
   或者是一个Ordinal(序号,(其最高位为1,使用时把最高位去掉就成序号啦)。
   first thunk 指针属于IAT 表的地址范围。
   hello.exe 中import第一个描述符 first thunk表偏移是608,608处存205c(RVA) 
   对应Import_BY_NAME MessageBoxA, 下一个DWORD=0数组就结束了
   说明user32.dll 只导入了一个函数。

从上面分析可以看出。
目录项中的导入地址表:指向一个大的IAT。
目录项中的导入表:    指向一个导入描述符数组。每一个描述符,描述一个DLL。 而FirstThunk
    指向那个大的IAT中的一个部分。我们称它为thunk data数组,数组最后一个元素DWORD为全0.
这样的一个设计结构,导入表和导入地址表的信息是有冗余的。我们不管它,知道就行了。

问:4.32
既然有了 FirstThunk, 还要 OriginalFirstThunk 干什么? 它们指向的IAT 内容都是一样的。
答:4.32
FirstThunk 所指向的IAT, 会在加载时被loader 修改为真实的函数地址。而OriginalFirstThunk 所指向的
IAT 是不会被修改的 。其实我也认为,留着这个 OriginalFirstThunk 没有用途,可能是微软认为那个IAT
已经被修改了, 万一你要再用你到拿找哇。 其实我看这是多虑了, 第一 ,IAT用完了我不会再用它。 第二,
万一被我们改了回头又要用,我们可以先把没改之前的备份一下呀。
不过,这都是个人意见,人家这么定义了,我们知道就行了。

问:4.33
从运行的角度来看,好像没有问题了,.text 段依靠.rdata段的帮助,由loader 修改为可执行代码。代码
执行时读取或存入数据段数据。hello 就可以运行了。
那.rsrc 段是什么呢 ? 是资源段吗?
答:4.33
是的。 本来我是没想加这个资源段的。可是不小心给加上了。这个其实你可以不用关心它的。
解开附件hello.rar, 图标是一个笑脸,这就是那个资源段的功能。如果没有那个资源段。默认的图标是一个WINDOWS ICON.

问:4.34
要想显示需要的图标,需要我们做什么呢?
答:4.34
你只要把ICON资源文件连进去就行了,代码并不需要做任何事情。在创建MessageBox 窗口时,系统发现你文件里有一个ICON
资源项,就替你把它画出来,如果你没有包含ICON 资源,它就把自己手边的那个叫默认ICON,画到MessagBox 左上角了。

问:4.35
hello.exe 文件都分析完了,只是option header 中还有一些项没有提到,它们重要吗?
大:4.35
有些项还是很重要的,例如程序入口点,但它们都已经很好理解了。
这里,我就把option 做一个标注,此时再浏览option header 的各个项,已经是时候了。
//
// Optional header format.
//

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

    WORD    Magic;      // NT HDR32 定义为010b
    BYTE    MajorLinkerVersion;    //link version 不重要
    BYTE    MinorLinkerVersion;
    DWORD   SizeOfCode;      //代码段大小
    DWORD   SizeOfInitializedData;  //初始化数据大小
    DWORD   SizeOfUninitializedData;  //未初始化数据大小
    DWORD   AddressOfEntryPoint;  //程序入口点:  重要
    DWORD   BaseOfCode;      //代码基址(RVA)
    DWORD   BaseOfData;      //数据基址(RVA)

    //
    // 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;    //header的大小
    DWORD   CheckSum;      //未使用
    WORD    Subsystem;      //02 为gui, 03 是console
    WORD    DllCharacteristics;    //dll 用
    DWORD   SizeOfStackReserve;    //系统加载堆和栈初始化信息
    DWORD   SizeOfStackCommit;
    DWORD   SizeOfHeapReserve;
    DWORD   SizeOfHeapCommit;
    DWORD   LoaderFlags;z    // 不重要
    DWORD   NumberOfRvaAndSizes;  // 总是16
    IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];  //16个目录,重要
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;



optionheader 重要的项是程序入口点,目录项。
optionheader 中标注不重要的就不用关心了。
下面的项重要程度中等,了解一下就可以了。
1.SizeOfHeaders 是文件头和节表大小之和。所以其后面紧跟段地址,该数值应与节表中第一节数值匹配。
2.BaseOfCode,SizeOfCode,  该数值与节表中代码节的文件大小也是一致的,有多余之嫌。

3.SizeOfInitializedData 已初始化的数据组成的块的大小.但我不知道它有什么用。

4. ”.bss" 段:给loader 参考的。
加载器在虚拟内存中申请空间,但在磁盘上的文件中并不占用空间的块的尺寸。
这些块在程序启动时不需要指定初值,因此术语名就是"未初始化的数据"。未初始化的数据通常在一个名叫 .bss 的块中。 
BaseOfData,已载入映像的未初始化数据(“.bss”段)的相对偏移量 
SizeOfUninitializedData。 申请的".bss" 空间的大小。当为0时,表示没有使用".bss"段

问:4.36
能总结一下PE 文件的重要项吗?
答:4.36
从运行的角度看,PE文件中重要的是程序入口点,节表,目录项。
代学生:感谢你为我们写了这么多东西。
代老师:大家共同提高。由于时间仓促,水平有限,有些观点未必正确,错误之处在所难免,希望海涵并欢迎指正。

全文完。

引言: 上一次以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?...陕..Uj
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: 具体导出表要包含那些内容才算把函数导出了呢?
答:呦,拖课了! 今天主要是复习了一下前边讲的内容,下一课我们再讲导出表!

  • 标 题:答复
  • 作 者:hjjdebug
  • 时 间:2008-05-28 21:22

问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]
 看明白了吧! 窗户纸就是这样被捅破的


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