PE结构详解(二)

(六)区段与区段表

前面花了很大的力气终于把复杂的PE header讲完了,在PE header紧接着的就是区段表(Secton Table),所谓的紧接着,就是一个字节也不差的意思,^_^
         还记得么?我们在很前面讲File header时,里面一个成员就是NumberOfSections,就是区段的个数,是7个,那么对应的区段表中的表项也应当有7项。
         每一个区段表项的结构都如下所示:
typedef struct _IMAGE_SECTION_HEADER {
    BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; // 名字,没的说吧?
    union {
        DWORD PhysicalAddress;  // 物理地址,保护模式下根本用不到
        DWORD VirtualSize;  //  区段的大小(内存中)
    } Misc;
    DWORD VirtualAddress; // 区段的虚拟内存地址
    DWORD SizeOfRawData; // 在硬盘上该区段的大小,是FileAlignment的整数倍
    DWORD PointerToRawData; // 文件中该区段的偏移
    DWORD PointerToRelocations; // 属于该区段的重定位表项的偏移,没有就是0
    DWORD PointerToLinenumbers; // 文件中的行号表项,没有就是0
    WORD NumberOfRelocations; // 重定位的个数,EXE是0个
    WORD NumberOfLinenumbers; // 行号表项的个数,没有是0
    DWORD Characteristics; // 下详
} IMAGE_SECTION_HEADER,*PIMAGE_SECTION_HEADER;
    其中IMAGE_SIZEOF_SHORT_NAME在winnt.h中定义等于8,即区段名Name最多8个字符。
    第二个成员Misc是一个共用体。我们只需要关注VirtualSize成员即可,也就是加载器分配给该Section的内存大小,是SectionAlignment的倍数。
    VirtualAddress和PointerToRawData分别表示内存和磁盘中的该段的起始地址,所以非常重要!
    最后一个Characteristics是该段的一个重要属性,其值的含义及相关常数定义也非常复杂,具体的可以到MSDN上搜索IMAGE_SECTION_HEADER,然后点Characteristics成员,就会有详解,这里就不列出了,在分析下面的实例时,会提到一些其常见的含义。
    好,下面我打算结合真实的区段表,逐步把每个常用区段都过一遍,其实内容很少,别紧张哈~
    下图是我截取的testPE.exe的区段表
                        PICTURE MISSING
    计算下IMAGE_SECTION_HEADER的长度=40字节,也就是图中的两行半长度,比较整齐好认,下文就不再标出它们的位置了。
    .bss是指存放未初始化数据的区段,包括所有的static变量,尽管没有出现在截图中,但比较重要,不得不提。首先请一定注意这里的未初始化与C语言中的未初始化概念!C语言的未初始化指不为堆栈变量提供初值,而变量又会在内存中随机位置出现,产生随机的无意义的初值。这里的未初始化,即指全部填0。已初始化的概念一致。
    .textbss是MS的Linker往PE中写入的区段。在正常的PE程序里,可以看到,它在文件中长度(RawData,原始数据)总是0,地址也是0,但在进程的虚拟地址空间中却占了1000h个字节。Characteristics为E0 00 00 A0 = IMAGE_SCN_MEM_EXECUTE|IMAGE_SCN_MEM_WRITE|IMAGE_SCN_CNT_CODE|IMAGE_SCN_CNT_UNINITIALIZED_DATA,意为可写可执行的代码段,不进行初始化。其实这个区段的出现是因为重定位映像基址比较困难,所以想通过这个空段来起到等同于重定位的作用。我分析下来,.textbss应该属于一种特殊的.text区段,代码段基地址BaseOfCode得到的是10000h,而.text起始地址是11000h,其差值1000h正好是.textbss的virtualsize。
    .text。不用说的重要,放程序的代码。图中内存占用大小为00 00 34 A4,内存中地址为00 01 10 00,区段的文件大小为00 00 36 00,区段在文件的起始偏移为00 00 04 00,当中几个不管了,特征值Characteristics为60 00 00 20,查阅MSDN,得到IMAGE_SCN_MEM_EXECUTE | IMAGE_SCN_MEM_READ | IMAGE_SCN_CNT_CODE,意为该段是代码段,可以被执行,可以被读取。
    .rdata,存放只读数据,比如字符串字面值,常量以及调试目录信息等。图中该区段在内存中,大小为00 00 1C 39,地址为00 01 50 00;在文件中,大小为00 00 1E 00,地址为00 00 3A 00;Characteristics是40 00 00 40,对应IMAGE_SCN_CNT_INITIALIZED_DATA | IMAGE_SCN_MEM_READ,已初始化且可读,但因没有IMAGE_SCN_MEM_WRITE故不可写。
    .data,数据区段,除了上面的static变量,还有存储在堆栈段的自动变量,其他所有变量都在.data里,典型的有各类全局变量。图中该区段在内存中,大小为00 00 05 AC,地址为00 01 70 00;在文件中,大小为00 00 02 00,地址为00 00 58 00。Characteristics是C0 00 00 40,由MSDN,IMAGE_SCN_MEM_WRITE | IMAGE_SCN_MEM_READ | IMAGE_SCN_CNT_INITIALIZED_DATA,可读可写且被初始化过,C语言默认全局变量初始化为0。
    .idata,所有输入的DLL函数信息。图中内存中,大小 00 00 08 4D,起始地址00 01 80 00,文件中,文件中,大小 00 00 0A 00,偏移00 00 5A 00。Characteristics同.data。我们会在稍后详细解析import table。
    .edata这里没有,是输出函数用的。
    .rsrc。PE的资源区段,包括程序所有资源信息,像什么图标、图片、AVI、字符串表、热键表、对话框、版本信息等等,你想放什么就有什么。图中内存中大小是00 00 0C 09,地址为00 01 90 00,文件中,大小是00 00 0E 00,地址为00 00 64 00。Characteristics同.rdata,意味着内存中资源区段是只读的。顺带提一下,编辑PE的资源,我个人比较喜欢eXescope,尽管原文中作者推荐resource hacker,各有所好吧!
    .reloc。重定位区段,又称基址重定位表,每一个表项仅仅描述了一个需要被加上基址差(期待的基址与实际的基址,EXE是0)的地址(一个4kb页面的地址)。就拿原文的例子好了,EXE假设它的映像会被载入到0x10000地址处,然后它有个字符串起始偏移是0x14002。但由于某种原因,EXE被PE加载器赶到0x60000处去了,那么显然如果再以0x14002访问字符串就是大大的错误。解决办法是,在重定位区段里告诉系统,0x14002这个地址需要被加上一个基址的差值,这里明显等于0x60000-0x10000=0x50000,而字符串相对偏移是
0x14002-0x10000=0x4002,所以新的字符串地址是0x64002,这样就天下太平了。
图中.reloc区段在内存中大小是00 00 04 51,地址为00 01 A0 00,文件中,大小是00 00 06 00,地址为00 00 72 00。Characteristics为42 00 00 40,IMAGE_SCN_CNT_INITIALIZED_DATA | IMAGE_SCN_MEM_READ | IMAGE_SCN_MEM_DISCARDABLE,最后一个参数的意思是,如果需要的话,重定位区段可以被PE加载器无视而不被加载。通常EXE的这个区段是被无视的,但DLL由于无法确定会被系统映射到地址空间的何处,所以这个区段是很关键的。
    区段这个部分就讲到这里,下一篇将讲述下输出区段(Export Section)。

(七)输出区段(Export Section)

首先介绍一下DLL相关内容。根据Win32 Programmer's Reference所述(自己翻译的):
“在Windows中,动态链接库(DLL)是含有函数与数据的模块。一个DLL在运行时期由其调用模块加载,当DLL被加载时,它会被映射到主叫进程的地址空间中去。
         DLL内有两类函数:输出(exported)与内部(internal)。输出函数是要被其他模块调用的函数,内部函数仅供DLL内部使用。尽管DLL可以输出数据,但通常数据也仅作内部使用。
         DLL可以使应用程序模块化,并且多个应用程序可以共享同一份内存中的DLL,因此也节省系统资源。Win32 API是被实现为一组DLL的,所以任何使用Win32 API的应用程序一定使用DLL。”
         DLL中函数(也常被叫做符号Symbols)可以通过两种方式输出:通过名字与通过序号。一个序号通常是一个字大小的一个数,它在一个DLL中唯一标识一个函数,注意,这个序号仅在同一个DLL中唯一,不同DLL间序号不不唯一!
         如果说函数通过名字输出,那么当其他模块要调用它时,可以在GetProcAddress中或使用它的名字或使用它的序号来指定,GetProcAddress函数则会返回被调用函数的地址。注意:使用名字的话,它的拼写与大小写必须与源DLL的模块定义文件(.DEF)中的一模一样,并且序号可以不从1开始,如果GetProcAddress找不到对应函数,就会返回NULL。具体GetProcAddress信息请参考Win32 Programmer’s Reference,或者MSDN。
         之所以GetProcAddress可以得到DLL的输出信息,是因为DLL中定义了输出目录表(Export Directory),还记得我们在(五)中讲的IMAGE_DATA_DIRECTORY数组吗?它的第0个元素就描述了Export Directory的虚拟地址与大小,这样我们就可以通过这些信息找到PE中对应的输出目录表的全部信息。
         输出目录表是用IMAGE_EXPORT_DIRECTORY这个结构定义的。
typedef struct _IMAGE_EXPORT_DIRECTORY {
    DWORD Characteristics;
    DWORD TimeDateStamp;
    WORD MajorVersion;
    WORD MinorVersion;
    DWORD Name;
    DWORD Base;
    DWORD NumberOfFunctions;
    DWORD NumberOfNames;
    DWORD AddressOfFunctions;
    DWORD AddressOfNames;
    DWORD AddressOfNameOrdinals;
} IMAGE_EXPORT_DIRECTORY,*PIMAGE_EXPORT_DIRECTORY;
    我们只关心其中几个成员:
    Name。模块的内部名称,如果DLL文件的名字被用户改了,那么PE加载器会使用这个内部名称。
    Base。前面说的序号的起始编号。比如一个函数的序号是4,起始序号Base是2,就表示该函数位于输出地址列表第三个元素,输出地址列表中默认首元素对应序号1,而不是序号0。序号的起始数字可以在.DEF文件中定义。
    NumberOfFunctions。顾名思义,就是该DLL输出函数的个数。
    NumberOfNames。就是通过名字输出的函数个数。
    AddressOfFunctions。输出地址列表(Export Address Table,EAT,其实就是一个地址数组)的首地址。
    AddressOfNames。输出名字列表(Export Name Table, ENT)的首地址。
    AddressOfNameOrdinals。输出序号列表(Export Ordinal Table, EOT)的首地址。该数组的元素都是16-bit(一个字)长度的整数。
    我们看到IMAGE_EXPORT_DIRECTORY其实主要就是指向了三个数组。现在我们再看下怎么通过函数的名字来找到对应的函数地址。
    假设Base = 3, 且有以下的表格:
AddressOfNames  - ENT
AddressOfNameOrdinals  - EOT
Name1
3
Name2
4
Name3
5


AddressOfFunctions - EAT
索引
地址
1
0x400042
2
0x400156
3
0x401256
4
0x400520
5
0x401452


 
    比如我们传给GetProcAddress的名字是”Name3”,之后系统会先查找ENT这张表格,发现找到了,然后平行地看过去,发现在EOT中对应的序数是5,那么说明该函数的地址在EAT的第5个位置(从1开始数),取出地址0x401452,任务就完成了。
    如果只通过序号来查找函数地址那就很方便,只要读取EOT,然后直接在EAT里索引就可以了。但这样的做法一不利于记忆,二不利于维护与扩展(因为序号一变就得改许多用户源代码)。
    下面看一种新的情况,如果一个输出函数尽管出现在了EAT中,但没有出现在ENT与EOT中,那该怎么办呢?那就只能通过排除法了,也就是要满足在EAT中而不在ENT与EOT中。
    好了,最后一个事儿了,就是输出转送(Export Forwarding)。就比如我调用了kernal32.dll里的HeapAlloc函数,它本身并没有实现这个函数,而是把我的调用请求转送到了ntdll.dll的RtlAllocHeap这个函数,这就是DLL输出转送,同样可以通过修改.DEF文件在链接时期进行。输出转送的引入,一来可以隔离通用的Win32 API与内核支持函数,达到屏蔽底层差异的目的,二来就是支持了操作系统的模式转换,划清用户模式与内核模式的界限。
    具体反应在输出目录表中是这样的,那个AddressOfNames指向的表格中,本来存的都是函数的名字,现在就换成“模块名.函数名”,比如上面的例子就是”NTDLL. RtlAllocHeap”,因此如果你看到了类似这样的名字,就说明这个函数调用被转送了。
    这次的文章没有图片实例,是因为咱们的testPE.exe没有输出表,而输出表本身也比较简单,我就懒得再去编一个DLL再提取16进制数据了,所以就抱歉啦!其实研究方法和前面几篇中的一模一样,就是先通过data directory找到输出表,然后读入IMAGE_EXPORT_DIRECTORY结构,得到三张表的地址,然后按上面的查表方法就可以了。
    OK!本文就到这。(八)将讲的是输入区段(The Import Section)。

(八)输入区段(The Import Section)

上次讲了输出区段,还是比较简单的,但输入区段内容就稍稍多了点,保持耐心阿,这是本系列关于PE格式部分的最后一讲啦!
         输入区段(Import Section),包含了所有从DLL中引用的函数的信息。与输出区段类似,这些信息是由几个数据结构描述的,其中最重要的是输入目录表(Import Directory)与输入地址表(Import Address Table),在一些应用程序中,还会有Bound_Import和Delay_Import,Delay_Import不那么重要,就忽略了,Bound_Import后面介绍。
         PE加载器的任务就是把DLL映射到进程的地址空间,并且找到每个函数的地址。
         DLL中函数的地址并不是总是固定不变的,而随着DLL版本更新变化,为了解决这个问题,PE引入了输入地址表(IAT),这样地址只要在DLL的IAT里更新一次,其他地方(尤其客户方)由于是间接访问DLL函数代码,就不用发生改变。IAT其实就是一个指针表,每一个指针指向一个函数的地址。
         我们先来看一下输入目录表的结构:
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
    _ANONYMOUS_UNION union {
        DWORD Characteristics;
        DWORD OriginalFirstThunk;
    } DUMMYUNIONNAME;
    DWORD TimeDateStamp;
    DWORD ForwarderChain;
    DWORD Name;
    DWORD FirstThunk;
} IMAGE_IMPORT_DESCRIPTOR,*PIMAGE_IMPORT_DESCRIPTOR;
    一个输入目录表项对应一个DLL,如果我们的EXE引用了100个DLL,就会有100个IMAGE_IMPORT_DESCRIPTOR结构。PE文件中没有字段可以指明输入目录表一共有多少个项,但默认最后一个项的字段是全0。
    如同输出目录表一样,你可以在data directory第1个元素中得到输入表的地址与大小,我们testPE.exe的输入表起始虚拟地址是00 01 80 00,大小3C,单个输入表项大小是10字节,所以表示testPE.exe引用了3个DLL。
    下面逐个讲下每一个成员:
    Characteristics。原先是存放一系列标志用的,现在已经废弃。
    OriginalFirstThunk。由于是共用体,所以这个匿名共用体仅表示OriginalFirstThunk,用来指向IMAGE_THUNK_DATA结构数组,稍后会描述。
    TimeDateStamp。除了应用程序被绑定(下详),值为-1,否则总是0。
    ForwarderChain。用于旧式风格的绑定,现在已经废弃。
    Name。指向ASCII字符串,是DLL的名字。
    FirstThunk也指向一个IMAGE_THUNK_DATA结构数组,其实OriginalFirstThunk那个数组的拷贝,当函数被绑定输入(Bound Import)后,它就被替换成函数的实际地址,这也就是为什么前面要有一个OriginalFirstThunk(原FirstThunk)的原因。
    自然,下一步我们就要关心下IMAGE_THUNK_DATA到底是个怎样的东西。
typedef struct _IMAGE_THUNK_DATA32 {
    union {
        DWORD ForwarderString;
        DWORD Function;
        DWORD Ordinal;
        DWORD AddressOfData;
    } u1;
} IMAGE_THUNK_DATA32,*PIMAGE_THUNK_DATA32;
    可以看到这个结构里只有一个是共用体成员,说明u1仅表示四个DWORD成员之一。
    ForwarderString没有任何意义,无视。
    在文件中,它要么表示函数的序号(Ordinal),且一般从8开始,要么表示IMAGE_IMPORT_BY_NAME(下详)结构的指针(AddressOfData)。当被载入到内存中后,它会被替换成输入函数的实际地址(Function),就是说由FirstThunk指向的IMAGE_THUNK_DATA32数组实际上就变成了输入地址表(IAT)。
typedef struct _IMAGE_IMPORT_BY_NAME {
    WORD Hint;
    BYTE Name[1];
} IMAGE_IMPORT_BY_NAME,*PIMAGE_IMPORT_BY_NAME;
    Hint。就是该函数在DLL中输出地址表(EAT)中的索引号,目的是方便PE加载器查阅目标DLL的EAT,所以名字就叫“暗示”(Hint)。此值可选,一些linker设置它为0。
    Name。这个字段比较奇怪,尽管从C定义中可以看到,结构里实际只包含一个字节而已,但是Name这样的定义,却预示着这是一个字符串,也就是从字符串第二个字节开始就出现在了结构外面,直到遇到\0结束符。它既没有采用DWORD类型来指向一个字符串的首地址,也没有开辟固定的长度来存储字符串,这是比较不符合常识的地方。
    好,稍稍总结一下。一个DLL对应一个IMAGE_IMPORT_DESCRIPTOR,然后创建两个一模一样的IMAGE_THUNK_DATA32数组,第一个由OriginalFirstThunk指向,第二个由FirstThunk指向。由于一个函数对应一个IMAGE_THUNK_DATA32,所以该DLL有几个函数,两个IMAGE_THUNK_DATA32数组就都有几个元素,刚开始都分别指向同一个IMAGE_IMPORT_BY_NAME结构,且两个IMAGE_THUNK_DATA32结构数组都以一个null DWORD表示结束。
    我们常称FirstThunk指向的IMAGE_THUNK_DATA32结构数组为输入地址表(IAT),而把OriginalFirstThunk指向的数组称为输入名称表(Import Name Table)或输入查询表(Import Lookup Table)。
    生成两个IMAGE_THUNK_DATA32结构数组的目的是比较显然的。因为FirstThunk指向的那个,在被加载时,PE加载器会以实际函数地址替代其中的元素,即FirstThunk指向的数组中的元素不再保存IMAGE_IMPORT_BY_NAME结构的地址,而是转而保存函数地址。但IMAGE_IMPORT_BY_NAME结构数组本身并没有被删除(并且始终只有一个该数组!),仍然由OriginalFirstThunk指向。这样万一要查找DLL的函数名称等信息,就可以去OriginalFIrstThunk指向的数组找到。
    看到这,你会发现输入表,或者说输入区段并不简单等同于输出地址表(IAT),查看Data Directory,你会发现第12个元素就是IAT,而并没有EAT。当然其实PE加载器并没有把它当作IAT指针,而只是把存储IAT的虚拟内存页面标记为可读可写而已,因为IAT是被载入到只读区域的,在载入时,先临时把页面设置为可读写,等输入表初始化完成后,再改回原先的受保护属性。
在调用DLL函数时,还涉及到一个性能优化问题。假设你要调用的函数所在的Thunk Data 的地址是00405030h,当前程序运行到0040100Ch。
那么比较高效的方式是:
    0040 100C CALL DWORD PTR [00405030]
    比较弱智的方式的:
    0040 100C CALL [00402200]
    …
    0040 2200 JMP DWORD PTR [00405030]
(注:这里我解释下加了DWORD PTR的区别,表示00405030指向的内存区块是DWORD类型,并且会“返回”区块的代表的值,这其实是一种间接寻址的方式)
    产生后一种的原因是,编译器本身是无法区别模块内的普通函数调用和位于别的模块的外部函数调用,因而产生同一类型代码 CALL [XXXXXXXX]。
    对于普通函数,XXXXXXXX就是函数的执行地址,而对于DLL函数,XXXXXXXX处实际保存的是一个指针,这个指针指向的地方才是真正的函数执行地址。但CALL指令里的XXXXXXXX只能是实际代码的地址,因此用在DLL函数上就会出错,于是链接器就想到了这么个笨拙的办法,用一段中间跳转代码来替换,造成了空间和时间资源的双重浪费。
    解决办法是,通过加上_declspec(dllimport)修饰符,告诉编译器这个函数是在DLL中,那么编译器就会直接产生CALL DWORD PTR [XXXXXXXX]之类的代码。不加这个修饰符,你的程序就会充满着特定的JMP语句而变得臃肿。
    第(七)讲提到过,一些函数可能只能通过EAT索引号来查找,就是那些出现在EAT但不出现在ENT与EOT中的函数。大家可能也注意到了,IMAGE_THUNK_DATA32中的Ordinal字段是DWORD类型的,但Ordinal属性本身只需要一个WORD即可,于是M$这样做,定义了一个常量IMAGE_ORDINAL_FLAG32 = 80000000h,如果一个函数的索引号是1234h,且是那种只能查EAT表调用的函数,那么他的Ordinal字段值就是80001234h,这样PE加载器就会很容易识别出来。
    绑定输入(Bound Imports)。当PE文件被加载到内存中时,加载器会先检查输入表然后把需要的DLL载入到地址空间中去。接着它会遍历FirstThunk指向的数组,并用输入函数的实际地址去替换每一个元素的内容,这个步骤可能会花一部分时间。但如果程序员(或者链接器)可以完全得知函数的地址,就可以直接把数组中的元素替换为地址,节省相当多的时间。这种方法就称为绑定(Binding)。
    M$采用一个bind.exe的程序(VS2008的在Windows SDK 6.0A里面),可以把IMAGE_THUNK_DATA32结构的内容都静态替换成地址。这样在载入DLL时,加载器会先检查这些地址是否正确合法,比如DLL版本是否符合(很早前就提过了DLL版本更新导致IAT的产生),如不符合或者DLL需要被重定位,加载器就会去遍历OriginalFirstThunk指向的数组(也就是INT),去计算新的地址。所以尽管INT对EXE是可选的,但是没有INT,就无法进行绑定。
    最后要提到的,也是最开始提到的,一个或许不是很重要的数据结构:绑定输入目录表(The Bound Import Directory)。它包含了可以让加载器判断绑定的地址是否合法的信息。描述它的数据结构是IMAGE_BOUND_IMPORT_DESCRIPTOR,目录表就是这种结构的数组,每一项都对应一个被绑定过的DLL。先看下这个结构:
typedef struct _IMAGE_BOUND_IMPORT_DESCRIPTOR {
    DWORD TimeDateStamp;
    WORD OffsetModuleName;
    WORD NumberOfModuleForwarderRefs;
} IMAGE_BOUND_IMPORT_DESCRIPTOR,*PIMAGE_BOUND_IMPORT_DESCRIPTOR;
    TimeDateStamp。这个成员必须和要引用的DLL的文件头信息相吻合,否则就会加载器去手动计算新IAT,这种情况一般发生在DLL版本不同时或者DLL映像被重定位时。
    OffsetModuleName。包含了以第一个IMAGE_BOUND_IMPORT_DESCRIPTOR为基址,DLL名称字符串(ASCII且以null结束)的偏移(非RVA)。
    NumberOfModuleForwarderRefs。是紧接着本结构后的另一个IMAGE_BOUND_FORWARDER_REF结构数组的元素个数。
typedef struct _IMAGE_BOUND_FORWARDER_REF {
    DWORD TimeDateStamp;
    WORD OffsetModuleName;
    WORD Reserved;
} IMAGE_BOUND_FORWARDER_REF,*PIMAGE_BOUND_FORWARDER_REF;
    这个结构与IMAGE_BOUND_IMPORT_DESCRIPTOR好像一样嘛。。。噢就最后一个字是保留的。这个结构数组干什么用的?你一定已经注意到ModuleForwarder这个词了,还记得我们在输出表中讲到的,函数的输出转送么?就是一个函数自己不实现而是把调用请求转发给另一个DLL中的函数。这里的IMAGE_BOUND_FORWARDER_REF结构就是用来记录接受转发的另一个DLL的校验信息,如果这个DLL还有输出转送,那么在该DLL中也有IMAGE_BOUND_FORWARDER_REF结构描述第三个DLL的校验信息。
    本文完。

(九)总结

好了,如果你确实按进程看到了这里,那么我首先要钦佩你的毅力,感谢你的支持,同时也恭喜你已经对PE格式有了比较全面的了解。回想起来,其实PE格式的理解并不很困难,只是比较复杂,也容易搞混结构关系,所以我写了这第九篇,把PE格式的总体结构按研究的顺序给出,让这个轮廓清晰地出现在脑中。
         好,如果我们拿到一个EXE,想要去分析它的格式,我们可以按下面的步骤进行:
         一。首先是DOS  MZ Header,对应C结构IMAGE_DOS_HEADER,其中第一个成员e_magic用于判断是否为合法的EXE,而通过最后一个成员e_lfanew可以找到PE Header的位置。
    二。找到PE Header,对应C结构为IMAGE_NT_HEADERS32。第一个成员Signature可以验证是否为合法的PE文件。第二个成员结构IMAGE_FILE_HEADER FileHeader可以得到PE的类型和区段的个数。通过IMAGE_OPTIONAL_HEADER OptionalHeader可以访问附加头。
    三。继续查看OptionalHeader,可以得到一些代码或数据的长度及虚拟地址,还有一个比较有用的Data Directory目录,里面可以查到输出表和输入表等数据结构的地址和长度。
    四。紧接着就是区段表(Section Table),可以找到EXE中所有段的信息。
    五。按照段表分别找到对应段的所有数据。
六。访问.edata或者输出表可以得到函数输出信息,表项结构为IMAGE_EXPORT_DIRECTORY
七。访问.idata段,可以得到DLL引用的所有信息。子步骤为:1。遍历所有IMAGE_IMPORT_DESCRIPTOR数组,每个元素对应一个DLL;2。找到每个元素中的成员,访问两个FirstThunk指向的IMAGE_THUNK_DATA32结构数组;3。其中每个元素要么按IMAGE_IMPORT_BY_NAME结构访问,要么当作Ordinal访问;4。如果EXE还有绑定输入目录表(Bound Import Table),则可以接着去访问它,元素结构为IMAGE_BOUND_IMPORT_DESCRIPTOR,对于有输出转送的DLL,还可以进而访问IMAGE_BOUND_FORWARDER_REF结构数组
八。访问数据段(?)、代码段(反汇编器)、资源段(资源编辑器)可用及其他的所有区段,这个就不属于PE格式范围了。
九。如果这个EXE被加过壳,则在正常访问前,还需要将它先脱壳。
好了,我的水平就只能把PE格式为你介绍到这,进一步的研究就靠你自己啦,最后说一声,感谢大家的支持!

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值