文本结构解析,PE结构解析
分类:澳门新萄京

typedef struct _IMAGE_EXPORT_DIRECTORY {
    DWORD   Characteristics;    // 未使用,总为0 

    DWORD   TimeDateStamp;      // 文件创建时间戳
    WORD    MajorVersion;       // 未使用,总为0 

    WORD    MinorVersion;       // 未使用,总为0
    DWORD   Name;               // 指向一个代表此 DLL名字的 ASCII字符串的 RVA
    DWORD   Base;               // 函数的起始序号
    DWORD   NumberOfFunctions;  // 导出函数的总数

    DWORD   NumberOfNames;      // 以名称方式导出的函数的总数

    DWORD   AddressOfFunctions;     // 指向输出函数地址的RVA
    DWORD   AddressOfNames;         // 指向输出函数名字的RVA
    DWORD   AddressOfNameOrdinals;  // 指向输出函数序号的RVA

} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;

ELF&PE 文件结构分析

说简单点,ELF 对应于UNIX 下的文件,而PE 则是Windows 的可执行文件,分析ELF 和 PE 的文件结构,是逆向工程,或者是做调试,甚至是开发所应具备的基本能力。在进行逆向工程的开端,我们拿到ELF 文件,或者是PE 文件,首先要做的就是分析文件头,了解信息,进而逆向文件。不说废话,开始分析:

ELF和PE 文件都是基于Unix 的 COFF(Common Object File Format) 改造而来,更加具体的来说,他是来源于当时著名的 DEC(Digital Equipment Corporation) 的VAX/VMS 上的COFF文件格式。我们从ELF 说起。

【pker / CVC.GB】 
5、关于FASM 
----------- 
下面我们用FASM来编写我们的第一个程序。我们可以编写如下代码: 
format  PE GUI 4.0 
entry   __start 
section '.text' code    readable executable 
    __start: 
            ret 
我们把这个文件存为test.asm并编译它: 
fasm test.asm test.exe 
没有任何烦人的参数,很方便,不是么? :P 
我们先来看一下这个程序的结构。第一句是format指示字,它指定了程序的类型,PE表示我 
们编写的是一个PE文件,后面的GUI指示编译器我们将使用Windows图形界面。如果要编写一 
个控制台应用程序则可以指定为CONSOLE。如果要写一个内核驱动,可以指定为NATIVE,表示 
不需要子系统支持。最后的4.0指定了子系统的版本号(还记得前面的MajorSubsystemVersion 
和MinorSubsystemVersion么?)。 
下面一行指定了程序的入口为__start。 
section指示字表示我们要开始一个新节。我们的程序只有一个节,即代码节,我们将其命名 
为.text,并指定节属性为只读(readable)和可执行(executable)。 
之后就是我们的代码了,我们仅仅用一条ret指令返回系统,这时堆栈里的返回地址为Exit- 
Thread,所以程序直接退出。 
下面运行它,程序只是简单地退出了,我们成功地用FASM编写了一个程序!我们已经迈出了 
第一步,下面要让我们的程序可以做点什么。我们想要调用一个API,我们要怎么做呢?让 
我们再来充充电吧 :D 

AddressOfFunctions 所指向内容是以 4 字节为一个单位的数组元素,每个元素代表函数入口

ELF

ELF 文件标准里把系统中采用ELF 格式的文件归类为四种:

  • 可重定位文件,Relocatable File ,这类文件包含代码和数据,可用来连接成可执行文件或共享目标文件,静态链接库归为此类,对应于Linux 中的.o ,Windows 的 .obj.
  • 可执行文件,Executable File ,这类文件包含了可以直接执行的程序,它的代表就是ELF 可执行文件,他们一般没有扩展名。比如/bin/bash ,Windows 下的 .exe
  • 共享目标文件,Shared Object File ,这种文件包含代码和数据,链接器可以使用这种文件跟其他可重定位文件的共享目标文件链接,产生新的目标文件。另外是动态链接器可以将几个这种共享目标文件与可执行文件结合,作为进程映像来运行。对应于Linux 中的 .so,Windows 中的 DLL
  • 核心转储文件,Core Dump File,当进程意外终止,系统可以将该进程地址空间的内容及终止时的一些信息转存到核心转储文件。 对应 Linux 下的core dump。

ELF 文件的总体结构大概是这样的:

ELF Header
.text
.data
.bss
... other section
Section header table
String Tables, Symbol Tables,..
  • ELF 文件头位于最前端,它包含了整个文件的基本属性,如文件版本,目标机器型号,程序入口等等。
  • .text 为代码段,也是反汇编处理的部分,他们是以机器码的形式存储,没有反汇编的过程基本不会有人读懂这些二进制代码的。
  • .data 数据段,保存的那些已经初始化了的全局静态变量局部静态变量
  • .bss 段,存放的是未初始化的全局变量局部静态变量,这个很容易理解,因为在未初始化的情况下,我们单独用一个段来保存,可以不在一开始就分配空间,而是在最终连接成可执行文件的时候,再在.bss 段分配空间。
  • 其他段,还有一些可选的段,比如.rodata 表示这里存储只读数据, .debug 表示调试信息等等,具体遇到可以查看相关文档。
  • 自定义段,这一块是为了实现用户特殊功能而存在的段,方便扩展,比如我们使用全局变量或者函数之前加上 **attribute(section('name'))** 就可以吧变量或者函数放到以name 作为段名的段中。
  • 段表,Section Header Table ,是一个重要的部分,它描述了ELF 文件包含的所有段的信息,比如每个段的段名,段长度,在文件中的偏移,读写权限和一些段的其他属性。

5.1、导入表 
----------- 
我们编写如下代码并用TASM编译: 

; tasm32 /ml /m5 test.asm 
; tlink32 -Tpe -aa test.obj ,,, import32.lib 

        ideal 
        p586 
        model   use32 flat 
extrn   MessageBoxA:near 
        dataseg 
str_hello       db      'Hello',0 
        codeseg 
__start: 
        push    0 
        push    offset str_hello 
        push    offset str_hello 
        push    0 
        call    MessageBoxA 
        ret 
        end     __start 
下面我们用w32dasm反汇编,得到: 
:00401000   6A00                    push    00000000 
:00401002   6800204000              push    00402000 
:00401007   6800204000              push    00402000 
:0040100C   6A00                    push    00000000 
:0040100E   E801000000              call    00401014 
:00401013   C3                      ret 
:00401014   FF2530304000            jmp     dword ptr [00403030] 
可以看到代码中的call MessageBoxA被翻译成了call 00401014,在这个地址处是一个跳转 
指令jmp dword ptr [00403030],我们可以确定在地址00403030处存放的是MessageBoxA的 
真正地址。 
其实这个地址是位于PE文件的导入表中的。下面我们继续我们的PE文件的学习。我们先来看 
一下导入表的结构。导入表是由一系列的IMAGE_IMPORT_DESCRIPTOR结构组成的。结构的个 
数由文件引用的DLL个数决定,文件引用了多少个DLL就有多少个IMAGE_IMPORT_DESCRIPTOR 
结构,最后还有一个全为零的IMAGE_IMPORT_DESCRIPTOR作为结束。 
typedef struct _IMAGE_IMPORT_DESCRIPTOR { 
    union { 
        DWORD   Characteristics; 
        DWORD   OriginalFirstThunk; 
    }; 
    DWORD   TimeDateStamp; 
    DWORD   ForwarderChain; 
    DWORD   Name; 
    DWORD   FirstThunk; 
} IMAGE_IMPORT_DESCRIPTOR; 
typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR; 
Name字段是一个RVA,指定了引入的DLL的名字。 
OriginalFirstThunk和FirstThunk在一个PE没有加载到内存中的时候是一样的,都是指向一 
个IMAGE_THUNK_DATA结构数组。最后以一个内容为0的结构结束。其实这个结构就是一个双 
字。这个结构很有意思,因为在不同的时候这个结构代表着不同的含义。当这个双字的最高 
位为1时,表示函数是以序号的方式导入的;当最高位为0时,表示函数是以名称方式导入的, 
这是这个双字是一个RVA,指向一个IMAGE_IMPORT_BY_NAME结构,这个结构用来指定导入函数 
名称。 
typedef struct _IMAGE_IMPORT_BY_NAME { 
    WORD    Hint; 
    BYTE    Name[1]; 
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME; 
Hint字段表示一个序号,不过因为是按名称导入,所以这个序号一般为零。 
Name字段是函数的名称。 
下面我们用一张图来说明这个复杂的过程。假设一个PE引用了kernel32.dll中的LoadLibraryA 
和GetProcAddress,还有一个按序号导入的函数80010002h。 
IMAGE_IMPORT_DESCRIPTOR                                  IMAGE_IMPORT_BY_NAME 
--------------------     -->  ------------------       -----------------------  
| OriginalFirstThunk | --     | IMAGE_THUNK_DATA | --> | 023B |  ExitProcess   | <--  
--------------------          ------------------       -----------------------     | 
|   TimeDataStamp    |        | IMAGE_THUNK_DATA | --> | 0191 | GetProcAddress | <-- --  
--------------------          ------------------       -----------------------     |  | 
|   ForwarderChain   |        |     80010002h    |                                  |  | 
--------------------          ------------------      --->  ------------------     |  | 
|        Name        | --     |         0        |    |     | IMAGE_THUNK_DATA | ---   | 
--------------------    |     ------------------     |      ------------------        | 
|     FirstThunk     |-  |                            |     | IMAGE_THUNK_DATA | ------  
--------------------  | |     ------------------     |      ------------------  
                       |  --> |   kernel32.dll   |    |     |     80010002h    | 
                       |       ------------------     |      ------------------  
                       |                              |     |         0        | 
                        ------------------------------       ------------------  
还记得前面我们说过在一个PE没有被加载到内存中的时候IMAGE_IMPORT_DESCRIPTOR中的 
OriginalFirstThunk和FirstThunk是相同的,那么为什么Windows要占用两个字段呢?其实 
是这样的,在PE文件被PE加载器加载到内存中的时候这个加载器会自动把FirstThunk的值替 
换为API函数的真正入口,也就是那个前面jmp的真正地址,而OriginalFirstThunk只不过是 
用来反向查找函数名而已。 
好了,又讲了这么多是要做什么呢?你马上就会看到。下面我们就来构造我们的导入表。 
我们用以下代码来开始我们的引入节: 
section '.idata' import data    readable 
section指示字表示我们要开始一个新节。.idata是这个新节的名称。import data表示这是 
一个引入节。readable表示这个节的节属性是只读的。 
假设我们的程序只需要引入user32.dll中的MessageBoxA函数,那么我们的引入节只有一个 
描述这个dll的IMAGE_IMPORT_DESCRIPTOR和一个全0的结构。考虑如下代码: 
    dd      0                   ; 我们并不需要OriginalFirstThunk 
    dd      0                   ; 我们也不需要管这个时间戳 
    dd      0                   ; 我们也不关心这个链 
    dd      RVA usr_dll         ; 指向我们的DLL名称的RVA 
    dd      RVA usr_thunk       ; 指向我们的IMAGE_IMPORT_BY_NAME数组的RVA 
                                ; 注意这个数组也是以0结尾的 
    dd      0,0,0,0,0           ; 结束标志 
上面用到了一个RVA伪指令,它指定的地址在编译时被自动写为对应的RVA值。下面定义我们 
要引入的动态链接库的名字,这是一个以0结尾的字符串: 
    usr_dll     db      'user32.dll',0 
还有我们的IMAGE_THUNK_DATA: 
    usr_thunk: 
        MessageBox      dd      RVA __imp_MessageBox 
                        dd      0                   ; 结束标志 
上面的__imp_MessageBox在编译时由于前面有RVA指示,所以表示是IMAGE_IMPORT_BY_NAME的 
RVA。下面我们定义这个结构: 
    __imp_MessageBox    dw      0                   ; 我们不按序号导入,所以可以 
                                                    ; 简单地置0 
                        db      'MessageBoxA',0     ; 导入的函数名 
好了,我们完成了导入表的建立。下面我们来看一个完整的程序,看看一个完整的FASM程序 
是多么的漂亮 :P 
format  PE GUI 4.0 
entry   __start 

AddressOfNames 所指向内容是以 4 字节为一个单位的数组元素,每个元素代表一个指向字符串的 RVA

ELF Header

ELF 文件信息的查看利器在Linux 下是是objdump, readelf, 相关命令较多,可查。下面我们从ELF 文件头说起。

文件头包含的内容很多,我们在Ubuntu 系统下使用 readelf 命令来查看ELF 文件头:

图片 1

我们以bash 这个可执行文件为例,我们可以看到ELF 文件头定义了ELF 魔数,文件机器字节长度,数据存储方式,版本,运行平台,ABI版本,ELF 重定位类型,硬件平台,硬件平台版本,入口地址,程序头入口和长度,段表的位置和长度,段的数量。

ELF 文件头的结构和相关常数一般定义在了 /usr/include/elf.h 中,我们可以进去查看一下:

图片 2

除了第一个,其他都是一一对应的,第一个是一个对应了Magic number, Class, Data, Version, OS/ABI, ABI version.

出现在最开始的ELF Magic number, 16字节是用来标识ELF 文件的平台属性,比如字长,字节序,ELF 文件版本。在加载的时候,首先会确认魔数的正确性,不正确的话就拒绝加载。

另一个重要的东西是段表(Section Header Table) ,保存了各种各样段的基本属性,比如段名,段长度,文件中的偏移,读写权限,段的其他属性。而段表自己在ELF 文件中的位置是在ELF 头文件 e_shoff 决定的。

我们可以使用 objdump -h 的指令来查看ELF 文件中包含哪些段,以bash 这个可执行为例,其实除了我们之前说的哪些基本结构,他包含很多其他的结构:

图片 3

同样的,我们使用readelf -S 的指令也可以进行查看。

下面我们来看一下结构,还是到elf.h 中去查看,他的结构体名字叫 Elf32_Shdr,64位对应Elf64_Shdr,结构如下:

图片 4

以上结构中,分别对应于:

  • 段名
  • 段类型
  • 段标志位
  • 段虚拟地址
  • 段偏移
  • 段长度
  • 段链接
  • 段对齐
  • 项,一些大小固定的项,如符号表等。

这些项目,在使用readelf -S 指令时一一对应。

另外还有一个重要的表,叫重定位表,一般段名叫.rel.text, 在上边没有出现,链接器在处理目标文件时,需要对目标文件中的某些部位进行重定位,就是代码段和数据段中那些对绝对地址引用的位置,这个时候就需要使用重定位表了。


; data section... 

section '.data' data    readable 
    pszText         db      'Hello, FASM world!',0 
    pszCaption      db      'Flat Assembler',0 

AddressOfNamesOrdinals 所指向内容是以 2 字节为一个单位的数组元素,每个元素代表对应名字在 AddressOfFunctions 中的序号数。

字符串表

为什么会有字符串表呢?其实这个也是在不断发展改进中找到的解决办法,在ELF 文件中,会用到很多的字符串,段名,变量名等等,但是字符串其本身又长度不固定,如果使用固定结构来表示,就会带来空间上的麻烦。所以,构造一个字符串表,将使用的字符串统一放在那里,然后通过偏移量来引用字符串,岂不美哉。

需要使用的时候,只需要给一个偏移量,然后就到字符串该位置找字符串,遇到 就停止。

字符串在ELF 文件中,也是以段的形式保存的,常见的段名 .strtab, .shstrtab 两个字符串分别为字符串表和段表字符串,前者用来保存普通的字符串,后者保存段名。

在我们使用readelf -h 的时候,我们看到最后一个成员,section header string table index ,实际上他指的就是字符串表的下标,bash 对应的字符串表下标为27,在使用objdump 的时候,实际上忽略了字符串表,我们使用readelf ,就可以看到第27位即字符串表:

图片 5


下面我们回顾一下,这个ELF 构造的精妙之处,当一个ELF 文件到来的时候,系统自然的找到他的开头,拿到文件头,首先看魔数,识别基本信息,看是不是正确的,或者是可识别的文件,然后加载他的基本信息,包括CPU 平台,版本号,段表的位置在哪,还可以拿到字符串表在哪,以及整个程序的入口地址。这一系列初始化信息拿到之后,程序可以通过字符串表定位,找到段名的字符串,通过段表的初始位置,确认每个段的位置,段名,长度等等信息,进而到达入口地址,准备执行。

当然,这只是最初始的内容,其后还要考虑链接,Import,Export 等等内容,留待以后完善。


; code section... 

section '.text' code    readable executable 
    __start: 
            push    0 
            push    pszCaption 
            push    pszText 
            push    0 
            call    [MessageBox] 
            push    0 
            call    [ExitProcess] 

AddressOfNames 和 AddressOfNamesOrdinals 的数目肯定是一样的,不是一样那么就出错了。

PE 文件

下面我们去看看更为常见的PE 文件格式,实际上PE 与 ELF 文件基本相同,也是采用了基于段的格式,同时PE 也允许程序员将变量或者函数放在自定义的段中, GCC 中**attribute(section('name'))** 扩展属性。

PE 文件的前身是COFF,所以分析PE 文件,先来看看COFF 的文件格式,他保存在WinNT.h 文件中。

COFF 的文件格式和ELF 几乎一毛一样:

Image Header
SectionTable Image_SECTION_HEADER
.text
data
.drectve
.debug$S
... other sections
Symbol Table

文件头定义在WinNT.h 中,我们打开来看一下:

图片 6

我们可以看到,它这个文件头和ELF 实际上是一样的,也在文件头中定义了段数,符号表的位置,Optional Header 的大小,这个Optional Header 后边就看到了,他就是PE 可执行文件的文件头的部分,以及段的属性等。

跟在文件头后边的是COFF 文件的段表,结构体名叫 IMAGE_SECTION_HEADER :

图片 7

属性包括这些,和ELF 没差:

  • 段名
  • 物理地址 PhysicalAddress
  • 虚拟地址 VirtualAddress
  • 原始数据大小 Sizeof raw data
  • 段在文件中的位置 File pointer to raw data
  • 该段的重定位表在文件中的位置 File pointer to relocation table
  • 该段的行号表在文件中的位置 File pointer to line number
  • 标志位,包括段的类型,对齐方式,读取权限等标志。


; import section... 

section '.idata' import data    readable 
    ; image import descriptor 
文本结构解析,PE结构解析。    dd      0,0,0,RVA usr_dll,RVA usr_thunk 
    dd      0,0,0,RVA krnl_dll,RVA krnl_thunk 
    dd      0,0,0,0,0 
    ; dll name 
    usr_dll     db      'user32.dll',0 
    krnl_dll    db      'kernel32.dll',0 
    ; image thunk data 
    usr_thunk: 
        MessageBox      dd      RVA __imp_MessageBox 
                        dd      0 
    krnl_thunk: 
        ExitProcess     dd      RVA __imp_ExitProcess 
                        dd      0 
    ; image import by name 
    __imp_MessageBox    dw      0 
                        db      'MessageBoxA',0 
    __imp_ExitProcess   dw      0 
                        db      'ExitProcess',0 
看到这里我相信大家都对FASM这个编译器有了一个初步的认识,也一定有很多读者会说:“ 
这么麻烦啊,干吗要用这个编译器呢?”。是的,也许上面的代码看起来很复杂,编写起来 
也很麻烦,但FASM的一个好处在于我们可以更主动地控制我们生成的PE文件结构,同时能对 
PE文件有更理性的认识。不过每个人的口味不同,嘿嘿,也许上面的理由还不够说服各位读 
者,没关系,选择一款适合你的编译器吧,它们都同样出色 :P 

主要要掌握两种寻找函数入口地址的方法:

DOS 头

在我们分析PE 的之前,还有另外一个头要了解一下,DOS 头,不得不说,微软事儿还是挺多的。

微软在创建PE 文件格式时,人们正在广泛使用DOS 文件,所以微软为了考虑兼容性的问题,所以在PE 头的最前边还添加了一个 IMAGE_DOS_HEADER 结构体,用来扩展已有的DOS EXE。在WinNTFS.h 里可以看到他的身影。

图片 8

DOS 头结构体的大小是40字节,这里边有两个重要的成员,需要知道,一个是e_magic 又见魔数,一个是e_lfanew,它只是了NT 头的偏移。

对于PE 文件来说,这个e_magic,也就是DOS 签名都是MZ,据说是一个叫 Mark Zbikowski 的开发人员在微软设计了这种ODS 可执行文件,所以...

我们以Windows 下的notepad 的可执行文件为例,在二进制编辑软件中打开,此类软件比较多,Heditor 打开:

图片 9

开始的两个字节是4D5A,e_lfanew 为00000108 注意存储顺序,小端。

你以为开头加上了DOS 头就完事了么,就可以跟着接PE 头了么。为了兼容DOS 当然不是这么简单了,紧接着DOS 头,跟的是DOS 存根,DOS stub。这一块就是为DOS 而准备的,对于PE 文件,即使没有它也可以正常运行。

图片 10

旁边的ASCII 是读不懂的,因为他是机器码,是汇编,为了在DOS 下执行,对于notepad 来说,这里是执行了一句,this program cannot be run in DOS mode 然后退出。逗我= =,有新的人,可以在DOS 中创造一个程序,做一些小动作。

5.2、导出表 
----------- 
通过导入表的学习,我想各位读者已经对PE文件的学习过程有了自己认识和方法,所以下面 
关于导出表的一节我将加快一些速度。“朋友们注意啦!!! @#$%$%&#^”  :D 
在导出表的起始位置是一个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;     // 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; 
Characteristics、MajorVersion和MinorVersion不使用,一般为0。 
TimeDataStamp是时间戳。 
Name字段是一个RVA值,它指向了这个模块的原始名称。这个名称与编译后的文件名无关。 
Base字段指定了导出函数序号的起始序号。假如Base的值为n,那么导出函数入口地址表中 
的第一个函数的序号就是n,第二个就是n 1... 
NumberOfFunctions指定了导出函数的总数。 
NumberOfNames指定了按名称导出的函数的总数。按序号导出的函数总数就是这个值与到处 
总数NumberOfFunctions的差。 
AddressOfFunctions字段是一个RVA值,指向一个RVA数组,数组中的每个RVA均指向一个导 
出函数的入口地址。数组的项数等于NumberOfFuntions。 
AddressOfNames字段是一个RVA值,同样指向一个RVA数组,数组中的每个双字是一个指向函 
数名字符串的RVA。数组的项数等于NumberOfNames。 
AddressOfNameOrdinals字段是一个RVA值,它指向一个字数组,注意这里不再是双字了!! 
这个数组起着很重要的作用,它的项数等于NumberOfNames,并与AddressOfNames指向的数组 
一一对应。其每个项目的值代表了这个函数在入口地址表中索引。现在我们来看一个例子, 
假如一个导出函数Foo在导出入口地址表中处于第m个位置,我们查找Ordinal数组的第m项, 
假设这个值为x,我们把这个值与导出序号的起始值Base的值n相加得到的值就是函数在入口 
地址表中索引。 
下图表示了导出表的结构和上述过程: 
-----------------------           -----------------  
|    Characteristics    |   ----> | 'dlltest.dll',0 | 
-----------------------   |       -----------------  
|     TimeDataStamp     |  | 
-----------------------   |   ->  -----------------  
|      MajorVersion     |  |  | 0 | 函数入口地址RVA | ==> 函数Foo,序号n 0    <--  
-----------------------   |  |    -----------------                             | 
|      MinorVersion     |  |  |   |       ...       |                            | 
-----------------------   |  |    -----------------                             | 
|         Name          | -   | x | 函数入口地址RVA | ==> 按序号导出,序号为n x  | 
-----------------------      |    -----------------                             | 
|    Base(假设值为n)  |     |   |       ...       |                            | 
-----------------------      |    -----------------                             | 
|   NumberOfFunctions   |     |                                                  | 
-----------------------      |   ->  -----       ----------        -----  <-    | 
|     NumberOfNames     |     |  |   | RVA | --> | '_foo',0 | <==> |  0  | -- ---  
-----------------------      |  |    -----       ----------        -----    | 
|   AddressOfFunctions  | ----   |   | ... |                       | ... |   | 
-----------------------         |    -----                         -----    | 
|     AddressOfNames    | -------                                            | 
-----------------------                                                     | 
| AddressOfNameOrdinals | ---------------------------------------------------  
-----------------------  
好了,下面我们来看构键我们的导出表。假设我们按名称导出一个函数_foo。我们以如下代 
码开始: 
section '.edata' export data    readable 
接着是IMAGE_EXPORT_DIRECTORY结构: 
    dd      0                   ; Characteristics 
    dd      0                   ; TimeDataStamp 
    dw      0                   ; MajorVersion 
    dw      0                   ; MinorVersion 
    dd      RVA dll_name        ; RVA,指向DLL名称 
    dd      0                   ; 起始序号为0 
    dd      1                   ; 只导出一个函数 
    dd      1                   ; 这个函数是按名称方式导出的 
    dd      RVA addr_tab        ; RVA,指向导出函数入口地址表 
    dd      RVA name_tab        ; RVA,指向函数名称地址表 
    dd      RVA ordinal_tab     ; RVA,指向函数索引表 
下面我们定义DLL名称: 
    dll_name    db      'foo.dll',0     ; DLL名称,编译的文件名可以与它不同 
接下来是导出函数入口地址表和函数名称地址表,我们要导出一个叫_foo的函数: 
    addr_tab    dd      RVA _foo        ; 函数入口地址 
    name_tab    dd      RVA func_name 
    func_name   db      '_foo',0        ; 函数名称 
最后是函数索引表: 
    ordinal_tab     dw      0           ; 只有一个按名称导出函数,序号为0 
下面我们看一个完整的程序: 
format  PE GUI 4.0 DLL at 76000000h 
entry   _dll_entry 

A. 从序号查找函数入口地址

  1. 定位到PE 文件头
  2. 从PE 文件头中的 IMAGE_OPTIONAL_HEADER32 结构中取出数据目录表,并从第一个数据目录中得到导出表的RVA
  3. 从导出表的 Base 字段得到起始序号
    4. 将需要查找的导出序号减去起始序号Base,得到函数在入口地址表中的索引,检测索引值是否大于导出表的 NumberOfFunctions 字段的值,如果大于后者的话,说明输入的序号是无效的
  4. 用这个索引值在 AddressOfFunctions 字段指向的导出函数入口地址表中取出相应的项目,这就是函数入口地址的RVA 值,当函数被装入内存的时候,这个RVA 值加上模块实际装入的基地址,就得到了函数真正的入口地址

NT头

下面进入正题,在HEditor 上也看到了PE,这一块就是正式的步入PE 的范畴。

图片 11

这是32位的PE 文件头定义,64位对应改。第一个成员就是签名,如我们所说,就是我们看到的「PE」,对应为50450000h。

这里边有两个东西,第一个就是我们之前看到的COFF 文件头,这里直接放进来了,我们不再分析。

看第二个,IMAGE_OPTIONAL_HEADER 不是说这个头可选,而是里边有些变量是可选的,而且有一些变量是必须的,否则会导致文件无法运行:

图片 12

有这么几个需要重点关注的成员,这些都是文件运行所必需的:

  1. Magic 魔数,对于32结构体来说是10B,对于64结构体来说是20B.
  2. AddressOfEntryPoint 持有EP 的RVA 值,之处程序最先执行的代码起始位置,也就是程序入口。
  3. ImageBase 进程虚拟内存的范围是0-FFFFFFFF (32位)。PE 文件被加载到这样的内存中,ImageBase 指出文件的优先装入位置。
  4. SectionAlignment, FileAlignment PE 文件的Body 部分划分为若干段,FileAlignment 之处段在磁盘文件中的最小单位,SectionAlignment指定了段在内存中的最小单位。
  5. SizeOfImage 指定 PE Image 在虚拟内存中所占的空间大小。
  6. SizeOfHeader PE 头的大小
  7. Subsystem 用来区分系统驱动文件与普通可执行文件。
  8. NumberOfRvaAndSizes 指定DataDirectory 数组的个数,虽然最后一个值,指出个数是16,但实际上PE 装载还是通过识别这个值来确定大小的。至于DataDirectory 是什么看下边
  9. DataDirectory 它是一个由IMAGE_DATA_DIRECTORY 结构体组成的数组,数组每一项都有定义的值,里边有一些重要的值,EXPORT/IMPORT/RESOURCE, TLS direction 是重点关注的。


; data section... 

section '.data' data    readable 
    pszText         db      'Hello, FASM world!',0 
    pszCaption      db      'Flat Assembler',0 

B. 从函数名称查找入口地址

我想通的地方,记录下来:用函数名来查找的话,Base 的值现在没有任何意义

  1. 首先得到导出表的地址
  2. 从导出表的 NumberOfNames 字段得到已命名函数的总数,并以这个数字作为循环的次数来构造一个循环,从 AddressOfNames 字段指向得到的函数名称地址表的第一项开始,在循环中将每一项定义的函数名与要查找的函数名相比较,如果没有任何一个函数名是符合的,表示文件中没有指定名称的函数。
    3. 如果某一项定义的函数名与要查找的函数名符合,那么记下这个函数名在字符串地址表中的索引值,然后在AddressOfNamesOrdinals 指向的数组中以同样的索引值取出数组项的值,我们这里假设这个值是 x
  3. 最后,以 x 的值作为索引值在 AddressOfFunctions  字段指向的函数入口地址表中获取 RVA 。此 RVA 就是函数的入口地址。

附上图片:

图片 13

段头

PE 的段头直接沿用的COFF 的段头结构,上边也说过了,我们查看notepad 的段头,可以获得各个段名,以及其信息,这里,我们可以使用一些软件查看,更加方便:

图片 14


; code section... 

section '.text' code    readable executable 
    _foo: 
文本结构解析,PE结构解析。            push    0 
            push    pszCaption 
            push    pszText 
            push    0 
            call    [MessageBox] 
            ret 
    _dll_entry: 
            xor     eax,eax 
            inc     eax 
            ret     0ch 

RVA to RAW

理解PE 最重要的一个部分就是理解文件从磁盘到内存地址的映射过程,做逆向的人员,只有熟练地掌握才能跟踪到程序的调用过程和位置,才能分析和寻找漏洞。

对于文件和内存的映射关系,其实很简单,他们通过一个简单的公式计算而来:

图片 15

换算公式是这样的:

RAW -PointToRawData = RVA - VirtualAddress

寻找过程就是先找到RVA 所在的段,然后根据公式计算出文件偏移。因为我们通过逆向工具,可以在内存中查找到所在的RVA,进而我们就可以计算出在文件中所在的位置,这样,就可以手动进行修改。

看回我们刚才载入的nodepad ,其中的V Addr, 实际上就是VirtualAddress,R offset 就是PointerToRawData。

图片 16

假如我们的RVA 地址是5000,那么计算方法就是,查看区段,发现在.text 中,5000-1000 400 = 4400,这就是RAW 00004400,而实际上,因为我们的ImageBase 是00400000,所以,我们在反编译时候内存中的地址是00405000.

接下来,使我们的PE头中的核心内容,IAT 和 EAT,也就是 Import address table, export address table.


; import section... 

section '.idata' import data    readable 
    ; image import descriptor 
    dd      0,0,0,RVA usr_dll,RVA usr_thunk 
    dd      0,0,0,RVA krnl_dll,RVA krnl_thunk 
    dd      0,0,0,0,0 
    ; dll name 
    usr_dll     db      'user32.dll',0 
    krnl_dll    db      'kernel32.dll',0 
    ; image thunk data 
    usr_thunk: 
        MessageBox      dd      RVA __imp_MessageBox 
                        dd      0 
    krnl_thunk: 
        ExitProcess     dd      RVA __imp_ExitProcess 
                        dd      0 
    ; image import by name 
    __imp_MessageBox    dw      0 
                        db      'MessageBoxA',0 
    __imp_ExitProcess   dw      0 
                        db      'ExitProcess',0 

IAT

导入地址表的内容与Windows 操作系统的核心进程,内存,DLL 结构有关。他是一种表格,记录了程序使用哪些库中的哪些函数。

下面,让我们把目光转到DLL 上,Dynamic Linked Library 支撑了整个 OS。DLL 的好处在于,不需要把库包含在程序中,单独组成DLL 文件,需要时调用即可,内存映射技术使加载后的DLL 代码,资源在多个进程中实现共享,更新库时候只要替换相关DLL 文件即可。

加载DLL 的方式有两种,一种是显式链接,使用DLL 时候加载,使用完释放内存。另一种是隐式链接,程序开始就一同加载DLL,程序终止的时候才释放掉内存。而IAT 提供的机制与隐式链接相关,最典型的Kernel32.dll。

我们来看看notepad 调用kernel32.dll 中的CreateFileW, 使用PE 调试工具Ollydbg

图片 17

我们看到填入参数之后,call 了35d7ffff 地址的内容,然后我们去dump 窗口,找一下kernel.CreateFileW:

图片 18

我们双击汇编窗口,启动编辑,发现确实是call 的这个数值:

图片 19

可是问题来了,上边是E8 35D7FFFF,下边地址却是 00C62178。其实这是Win Visita, Win 7的ASLR 技术,主要就是针对缓冲溢出攻击的一种保护技术,通过随机化布局,让逆向跟踪者,难以查找地址,就难以简单的进行溢出攻击。不过还是可以通过跳板的方式,找到溢出的办法,这就是后话了。

现在可以确定的是,35D7FFFF 可以认为保存的数值就是 CreateFileW 的地址。而为什么不直接使用CALL 7509168B 这种方式直接调用呢? Kernel32.dll 版本各不相同,对应的CreateFileW 函数也各不相同,为了兼容各种环境,编译器准备了CreateFileW 函数的实际地址,然后记下DWORD PTR DS:[xxxxxx] 这样的指令,执行文件时候,PE 装载器将CreateFileW 函数地址写到这个位置。

同时,由于重定位的原因存在,所以也不能直接使用CALL 7509168B 的方式,比如两个DLL 文件有相同的 ImageBase,装载的时候,一个装载到该位置之后,另一个就不能装载该位置了,需要换位置。所以我们不能对实际地址进行硬编码。

IMAGE_IMPORT_DESCRIPTOR

图片 20

图片 21

对于一个普通程序来说,需要导入多少个库,就会存在多少个这样的结构体,这些结构体组成数组,然后数组最后是以NULL 结构体结束。其中有几个重要的成员:

  • OriginalFirstThunk INT Import Name Table 地址,RVA
  • Name 库名称字符串地址,RVA,就是说该地址保存库名称
  • First Thunk IAT 地址 RVA
  • INT 中个元素的值是上边那个IMAGE_IMPORT_BY_NAME 结构体指针。
  • INT 与 IAT 大小应相同。

那么PE 是如何导入函数输出到IAT 的:

  1. 读取NAME 成员,获取扩名称字符串
  2. 装载相应库: LoadLibrary("kernel32.dll")
  3. 读取OriginalFirstThunk成员,获取INT 地址
  4. 读取INT 数组中的值,获取相应的 IMAGE_IMPORT_BY_NAME地址,是RVA地址
  5. 使用IMAGE_IMPORT_BY_NAME 的Hint 或者是name 项,获取相应函数的起始位置 GetProcAddress("GetCurrentThreadId")
  6. 读取FistrThunk 成员,获得IAT 地址。
  7. 将上面获得的函数地址输入相应IAT 数组值。
  8. 重复4-7 到INT 结束。

这里就产生了一个疑惑,OriginalFirstThunk 和 First Thunk 都指向的是函数,为什么多此一举呢?

首先,从直观上说,两个都指向了库中引入函数的数组,鱼C 画的这张图挺直观:

图片 22

OriginalFirstThunk 和 FirstThunk 他们都是两个类型为IMAGE_THUNK_DATA 的数组,它是一个指针大小的联合(union)类型。
每一个IMAGE_THUNK_DATA 结构定义一个导入函数信息(即指向结构为IMAGE_IMPORT_BY_NAME 的家伙,这家伙稍后再议)。
然后数组最后以一个内容为0 的 IMAGE_THUNK_DATA 结构作为结束标志。
IMAGE_THUNK_DATA32 结构体如下:

图片 23

因为是Union 结构,IMAGE_THUNK_DATA 事实上是一个双字大小。
规定如下:

当 IMAGE_THUNK_DATA 值的最高位为 1时,表示函数以序号方式输入,这时候低 31位被看作一个函数序号。

当 IMAGE_THUNK_DATA 值的最高位为 0时,表示函数以字符串类型的函数名方式输入,这时双字的值是一个 RVA,指向一个 IMAGE_IMPORT_BY_NAME 结构。

我们再看IMAGE_IMPORT_BY_NAME 结构:

图片 24

结构中的 Hint 字段也表示函数的序号,不过这个字段是可选的,有些编译器总是将它设置为 0。

Name 字段定义了导入函数的名称字符串,这是一个以 0 为结尾的字符串。

现在重点来了:

第一个数组(由 OriginalFirstThunk 所指向)是单独的一项,而且不能被改写,我们前边称为 INT。第二个数组(由 FirstThunk 所指向)事实上是由 PE 装载器重写的。

PE 装载器装载顺序正如上边所讲的那样,我们再将它讲详细一点:

PE 装载器首先搜索 OriginalFirstThunk ,找到之后加载程序迭代搜索数组中的每个指针,找到每个 IMAGE_IMPORT_BY_NAME 结构所指向的输入函数的地址,然后加载器用函数真正入口地址来替代由 FirstThunk 数组中的一个入口,因此我们称为输入地址表(IAT).

继续套用鱼C 的图,就能直观的感受到了:

图片 25

所以,在读取一次OriginalFirstThunk 之后,程序就是依靠IAT 提供的函数地址来运行了。


; export section... 

section '.edata' export data    readable 
    ; image export directory 
    dd      0,0,0,RVA dll_name,0,1,1 
    dd      RVA addr_tab 
    dd      RVA name_tab 
    dd      RVA ordinal_tab 
    ; dll name 
    dll_name        db      'foo.dll',0 
    ; function address table 
    addr_tab        dd      RVA _foo 
    ; function name table 
    name_tab        dd      RVA ex_foo 
    ; export name table 
    ex_foo          db      '_foo',0 
    ; ordinal table 
    ordinal_tab     dw      0 

EAT

搞清楚了IAT 的原理,EAT 就好理解了,目前这篇总结的有点长了,我长话短说。IAT 是导入的库和函数的表,那么EAT 就对应于导出,它使不同的应用程序可以调用库文件中提供的函数,为了方便导出函数,就需要保存这些导出信息。

回头看PE 文件中的PE头我们可以看到IMAGE_EXPORT_DIRECTORY 结构体以的位置,他在IMAGE_OPTIONAL_HEADER32.DataDirectory[0].VirtualAddress 的值就是 IMAGE_EXPORT_DIREDCTORY 的起始位置。

图片 26

IMAGE_EXPORT_DIRECTORY结构体如下:

图片 27

这里边同样是这么几个重要的成员:

  • NumberOfFunctions 实际Export 函数的个数
  • NumberOfNames Export 函数中具名的函数个数
  • AddressOfFunctins Export 函数地址数组,数组个数是上边的NOF
  • AddressOfNames 函数名称地址数组,个数是上边的NON
  • AddressOfNameOrdinals Ordinal 地址数组,个数等于上边NON
  • Name 一个RVA 值,指向一个定义了模块名称的字符串。如即使Kernel32.dll 文件被改名为”Ker.dll”。仍然可以从这个字符串中的值得知其在编译时的文件名是”Kernel32.dll”。
  • Base:导出函数序号的起始值,将AddressOfFunctions 字段指向的入口地址表的索引号加上这个起始值就是对应函数的导出 序号。
    以kernel32.dll 为例,我们看一下:
![](https://upload-images.jianshu.io/upload_images/30117-6bb373c33a5b9995.jpg)

从上边这些成员,我们实际上可以看出,是有两种方式提供给那些想调用该库中函数的,一种是直接从序号查找函数入口地址导入,一种是通过函数名来查找函数入口地址导入。

先上一个鱼C 的图,方便理解:

图片 28

上边图,注意一点,因为AddressOfNameOrdinals 的序号应当是从0开始的,不过图中映射的是第二个函数指向的序号1。

我们分别说一下两种方式:

当已知导出序号的时候

  1. Windows 装载器定位到PE 文件头,
  2. 从PE 文件头中的 IMAGE_OPTIONAL_HEADER32 结构中取出数据目录表,并从第一个数据目录中得到导出表的RVA ,
  3. 从导出表的 Base 字段得到起始序号,
  4. 将需要查找的导出序号减去起始序号,得到函数在入口地址表中的索引,
  5. 检测索引值是否大于导出表的 NumberOfFunctions 字段的值,如果大于后者的话,说明输入的序号是无效的用这个索引值在 AddressOfFunctions 字段指向的导出函数入口地址表中取出相应的项目,这就是函数入口地址的RVA 值,当函数被装入内存的时候,这个RVA 值加上模块实际装入的基地址,就得到了函数真正的入口地址

当已知函数名称查找入口地址时

  1. 从导出表的 NumberOfNames 字段得到已命名函数的总数,并以这个数字作为循环的次数来构造一个循环
  2. 从 AddressOfNames 字段指向得到的函数名称地址表的第一项开始,在循环中将每一项定义的函数名与要查找的函数名相比较,如果没有任何一个函数名是符合的,表示文件中没有指定名称的函数,如果某一项定义的函数名与要查找的函数名符合,那么记下这个函数名在字符串地址表中的索引值,然后在 AddressOfNamesOrdinals 指向的数组中以同样的索引值取出数组项的值,我们这里假设这个值是x
  3. 最后,以 x 值作为索引值,在 AddressOfFunctions 字段指向的函数入口地址表中获取的 RVA 就是函数的入口地址

一般来说,做逆向或者是写代码都是第二种方法,我们以kernel32.dll 中的GetProcAddress 函数为例,其操作原理如下:

  1. 利用 AddressOfNames 成员转到 『函数名称数组』
  2. 『函数名称数组』中存储着字符串地址,通过比较字符串,查找指定的函数名称,此时数组所以为成为name_index
  3. 利用 AddressOfNameOrdinals 成员,转到这个序号数组
  4. 在ordinal 数组中通过name_index 查找到相应的序号
  5. 利用AddressOfFunctions 成员,转到『函数地址数组』EAT
  6. 在EAT 中将刚刚得到的ordinal 作为索引,获得指定函数的入口地址

写了这么多,实际上算是对文件结构有了一个入门的认识,至少知道在程序运行过程中,系统是如何进行操作和链接的,而更加详细的内容注入运行时压缩,DLL 注入,API 钩取等技术,就需要在这个基础之上继续挖掘,所以PE ,ELF 文件结构的分析是相当重要的。

PS. 参考:
鱼C 讲解PE 文件格式之INT
《Windows PE 权威指南》
《逆向工程核心原理》
《程序员的自我修养-链接,装载与库》


; relocation section... 

section '.reloc' fixups data     discardable 
程序的一开始用format指定了PE和GUI,在子系统版本号的后面我们使用了DLL指示字,表示 
这是一个DLL文件。最后还有一个at关键字,指示了文件的image base。 
程序的最后一个节是重定位节,对于重定位表我不做过多解释,有兴趣的读者可以参考其他 
书籍或文章。我们可以把刚才的程序编译成一个DLL: 
fasm foo.asm foo.dll 
下面我们编写一个测试程序检验程序的正确性: 
#include <windows.h> 
int __stdcall WinMain (HINSTANCE,HINSTANCE,LPTSTR,int) 

    HMODULE hFoo=LoadLibrary ("foo.dll"); 
    FARPROC _foo=GetProcAddress (hFoo,"_foo"); 
    _foo (); 
    FreeLibrary (hFoo); 
    return 0; 

我们把编译后的exe和刚才的dll放在同一个目录下并运行,看看程序运行是否正确 :P 

5.3、强大的宏 
------------- 
关于FASM,还有一个强大的功能就是宏。大家对宏一定都不陌生,下面我们来看看在FASM中 
如何定义宏。假设我们要编写一个复制字符串的宏,其中源、目的串由ESI和EDI指定,我们 
可以: 
macro @copysz 

        local   next_char 
    next_char: 
        lodsb 
        stosb 
        or      al,al 
        jnz     next_char 

下面我们再来看一个带参数的宏定义: 
macro @stosd _dword 

    mov     eax,_dword 
    stosd 

如果我们要多次存入几个不同的双字我们可以简单地在定义宏时把参数用中括号括起来,比 
如: 
macro @stosd [_dword] 

    mov     eax,_dword 
    stosd 

这样当我们调用@stosd 1,2,3的时候,我们的代码被编译成: 
mov     eax,1 
stosd 
mov     eax,2 
stosd 
mov     eax,3 
stosd 
对于这种多参数的宏,FASM提供了三个伪指令common、forward和reverse。他们把宏代码分 
成块并分别处理。下面我分别来介绍: 
forward限定的块表示指令块对参数进行顺序处理,比如上面的宏,如果把上面的代码定义在 
forward块中,我们可以得到相同的结果。对于forward块我们可以这样定义 
macro @stosd [_dword] 

    forward 
        mov     eax,_dword 
        stosd 

reverse和forward正好相反,表示指令块对参数进行反向处理。对于上面的指令块如果用 
reverse限定,那么我们的参数将被按照相反的顺序存入内存。 
macro @stosd [_dword] 

    reverse 
        mov     eax,_dword 
        stosd 

这时当我们调用@stosd 1,2,3的时候,我们的代码被编译成: 
mov     eax,3 
stosd 
mov     eax,2 
stosd 
mov     eax,1 
stosd 
common限定的块将仅被处理处理一次。我们现在编写一个调用API的宏@invoke: 
macro @invoke _api,[_argv] 

    reverse 
        push    _argv 
    common 
        call    [_api] 

现在我们可以使用这个宏来调用API了,比如: 
@invoke     MessageBox,0,pszText,pszCaption,0 
对于宏的使用我们就介绍这些,更多的代码可以参看我的useful.inc(其中有很多29A的宏, 
tnx 29a :P)

本文由澳门新萄京发布于澳门新萄京,转载请注明出处:文本结构解析,PE结构解析

上一篇:澳门新萄京绘制基础,中自定义View 下一篇:没有了
猜你喜欢
热门排行
精彩图文