导语:PE是Windows下的可执行文件的格式。这是微软基于UNIX平台的COFF(Common Object File Format,通用文件格式)制成的。微软原本的意思是提高程序的移植型。但是想法是好的,但是实际上只用于Windows系列的操作系统下。

什么是PE文件?

PE是Windows下的可执行文件的格式。这是微软基于UNIX平台的COFF(Common Object File Format,通用文件格式)制成的。微软原本的意思是提高程序的移植型。但是想法是好的,但是实际上只用于Windows系列的操作系统下。

PE文件是指32位的可执行文件,也称PE32。注意:64位的可执行文件称为PE+或PE32+,是PE32的一种扩展,不叫PE64。

PE文件的格式

使用notepad.exe为例。从DOS头到(DOS header)到节区头(Section header)是PE头的部分,剩下的部分都叫PE体。文件中实用偏移offset,内存中实用VA(Virtual Address, 虚拟地址)来表示位置。文件加载到内存中时,情况就会发生变数(节区的大小,位置等等)。文件一般可分为代码.text,数据.data,资源.rsrc,分别保存。不同的编译工具节区的名称大小,个数,内容都是不同的。采用不同的编译选项所编译出来的可执行文件也是不同的。

1.png

RV&RVA的转换

· VA是指进程虚拟内存的绝对相对地址.

· RVA(相对虚拟地址),是指从某个基准位置(Image Base)开始的相对地址。

转换关系如下:

<center>RVA+Image Base = VA</center>

PE头内部信息大多以RVA的形式存在。因为PE文件加载到进程虚拟内存的特定位置,但是,这个位置可能已加载了其他的PE文件(DLL)。因此必须重定向到其他的空白位置。若PE头信息使用的是VA,则无法正常访问。所以,使用RVA来定位信息,即使发生了重定位,只要基准地址没有发生变化,就可以正常访问到指定的信息

PE头

PE头中邮许多许多结构体组成的。而且基本都是结构体中嵌套结构体,嵌套好多层。很复杂。要想学好逆向就必须了解每一个结构体中的内容,所代表的含义。

1.DOS头

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

2.jpg

Dos结构体的大小为40个字节,这里面有两个重要的成员变量

· e_magic:DOS签名,这个属性的值基本都是4D5A=>ASCII值"MZ"

· e_lfanew:指示NT头的偏移量(根据不同的文件拥有的值就是不一样的)使用16进制编辑器打开windows自带的笔记本。来查看他的结构体。

3.png

2.DOS存根

DOS头后并不是PE头,他后面跟的是DOS存根,这是个可选项,而且大小不固定,他的大小为NT头的偏移量减去DOS头的40个字节,这一个区域中东西就是DOS存根。即使没有DOS存根,程序也可以正常执行。这里存的数据是由数据混合而成的。

4.png

可以再xp下实用debug查看这DOS存根中的代码。在命令行中输入:

debug C:Windowsnotepad.exe之后在按u,就会出现16位的汇编指令。

3.NT头

紧接在后面的就是NT头,这个头的地址也就是刚刚在上面提到的e_lfanew的值,来看下这个文件的结构体

5.jpg

IMAGE_NT_HEADERS结构体由3个成员组成,第一个成员为签名(Signature)结构体,其值为50450000h,另外两个成员为文件头(File Header)和可选头(Optional Header)结构体。用16进制编辑器打开记事本,查看IMAGE_NT_HEADERS的内同

6.png

(所选区域为结构体内容)IMAGE_NT_HEADERS结构体的大小位F8,很大。

3.1 NT头:文件头

文件头是表现的是IMAGE_FILE_HEADER结构体

typedef struct _IMAGE_FILE_HEADER {   
        WORD      Machine;                 //运行平台
        WORD      NumberOfSections;        //块(section)数目      
        DWORD     TimeDateStamp;           //时间日期标记     
        DWORD     PointerToSymbolTable;    //COFF符号指针,这是程序调试信息    
        DWORD     NumberOfSymbols;         //符号数  
        WORD      SizeOfOptionalHeader;    //可选部首长度,是IMAGE_OPTIONAL_HEADER的长度    
        WORD      Characteristics;         //文件属性
}

来看看几个成员变量中所代表的意思是什么首先是Machine,这个值表示运行程序需要的平台

7.png

可以看到这个值是014c,下面是一个列表,代表各种平台对应的值

8.png

第二个成员变量是NumberOfSections,表示节区数量,而且,当定义的节区与实际节区不同时,就会发生运行错误

9.png

可以看到是3个字节。

第三个成员变量是TimeDateStamp,表示编辑器创建文件的时间。该成员的值不影响文件运行。

10.png

之后看第6个成员变量SizeOfOptionalHeader,IMAGE_NT_HEADERS结构体的最后一个成员为IMAGE_OPTIONAL_HEADER32的结构体。SizeOfOptionalHeader成员用来指出IMAGE_OPTIONAL_HEADER32结构体的长度。IMAGE_OPTIONAL_HEADER32是由c语言编写的,所以,大小已经确定。WINDOWS的PE装载器需要查看IMAGE_FILE_HEADER中的SizeOfOptionalHeader来确定IMAGE_OPTIONAL_HEADER32的大小。00E0的十进制为224,也就是OptionalHeader的大小。PE32+格式的文件中实用的是IMAGE_OPTIONAL_HEADER64结构体,而不是IMAGE_OPTIONAL_HEADER32结构体。2个结构体的尺寸是不用的,所以需要SizeOfOptionalHeader成员明确的指出结构体的大小来看他的值

11.png

第7个成员变量Characteristics表示文件属性,他的每一个bit都代表了某种含义

Bit 0 :置1表示文件中没有重定向信息。每个段都有它们自己的重定向信息。
       这个标志在可执行文件中没有使用,在可执行文件中是用一个叫做基址重定向目录表来表示重定向信息的,这将在下面介绍。
Bit 1 :置1表示该文件是可执行文件(也就是说不是一个目标文件或库文件)。
Bit 2 :置1表示没有行数信息;在可执行文件中没有使用。
Bit 3 :置1表示没有局部符号信息;在可执行文件中没有使用。
Bit 4 :
Bit 7
Bit 8 :表示希望机器为32位机。这个值永远为1。
Bit 9 :表示没有调试信息,在可执行文件中没有使用。
Bit 10:置1表示该程序不能运行于可移动介质中(如软驱或CD-ROM)。在这    种情况下,OS必须把文件拷贝到交换文件中执行。
Bit 11:置1表示程序不能在网上运行。在这种情况下,OS必须把文件拷贝到交换文件中执行。
Bit 12:置1表示文件是一个系统文件例如驱动程序。在可执行文件中没有使用。
Bit 13:置1表示文件是一个动态链接库(DLL)。
Bit 14:表示文件被设计成不能运行于多处理器系统中。
Bit 15:表示文件的字节顺序如果不是机器所期望的,那么在读出之前要进行
       交换。在可执行文件中它们是不可信的(操作系统期望按正确的字节顺序执行程序)。

他的值如下

12.png

把010F这个值变为二进制就是0000000100001111再来看一个更详细的图片

13.png

需要记住的两个值为0002h(EXE文件)和2000h(DLL文件)

3.2 NT头:可选头

紧接着IMAGE_FILE_HEADER的结构体就是IMAGE_OPTIONAL_HEADER32,这个结构体是整个PE头结构中最大的一个结构体。

14.png

打蓝标的区域为可选头的内容,找不到内同的可以计算一下。找到NT头开头的地方,一个DWORD类型的Signature,4个字节,之后跟了一个20字节大小的文件头之后就是可选头所开始的地址了。

接着来看看这个结构体的结构:

typedef struct _IMAGE_OPTIONAL_HEADER {
  WORD                 Magic;
  BYTE                 MajorLinkerVersion;
  BYTE                 MinorLinkerVersion;
  DWORD                SizeOfCode;
  DWORD                SizeOfInitializedData;
  DWORD                SizeOfUninitializedData;
  DWORD                AddressOfEntryPoint;
  DWORD                BaseOfCode;
  DWORD                BaseOfData;
  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;
  DWORD                NumberOfRvaAndSizes;
  IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER, *PIMAGE_OPTIONAL_HEADER;

继续看每一个成员变量的值:

第一个成员变量:Magic 这个值代表的是 文件的格式

15.png

看看微软官方的说法

1506048296158096.png

IMAGE_OPTIONAL_HEADER32和IMAGE_OPTIONAL_HEADER64分别表示32位和64位的应用程序。这里的值010B可以判断出实用的是32位应用程序。看官方文档还有一个值是0x107这个值代表的是系统的ROM文件。

紧接着的几个成员变量都无关紧要。

直接看第7个值:AddressOfEntryPoint,这个成员变量保存着EP的RVA。也就是最先执行代码起始地址。

17.png

第8个成员变量为:BaseOfCode,表示代码段起始RVA先看他的值,是1000,剩下的等说到ImgaeBase再说。??

18.png

第9个成员变量为:BaseOfData,表示数据段的起始RVA,值位9000,跟上面一样,等下再多说。??

19.png

第10个成员变量:ImageBase,进程虚拟内存的的范围是0~FFFFFFFF(32位)。PE文件被加入内存中,ImageBase指出文件的优先装入地址。EXE,DLL文件被装载到用户内存的0~7FFFFFFF中,SYS文件被加载到80000000~FFFFFFFF中。DLL文件的ImageBase值位10000000(可以修改)。执行PE文件的时候。PE装载器先创建进程,再将文件载入内存,然后把EIP寄存器的值设置为Image+AddressOfEntryPoint,他的值如下。

20.png

ImageBase+AddressOfEmtryPoint=10000000+739D=0100739D

计算得出最先执行的代码起始地址,之后使用OD打开,看地址,就是刚刚计算得出的地址

21.png

接着来说上面的BaseOfCode,一个道理BaseofCode + ImageBase可以得到代码段的起始地址。

BaseofCode + ImageBase = 1000+10000000 = 1001000

22.png

同理BaseOfData + ImageBase可以得到段的起始地址

BaseOfData + ImageBase = 9000 + 10000000 =1009000

23.png

这是一些题外话,我们继续来说_IMAGE_OPTIONAL_HEADER的成员变量

第11个成员变量SectionAlignment,表示节区在内存中的最下单位,这些节存储着不同类型的数据。

24.png

第12个成员变量FileAlignment,他指定了节区在磁盘中的最小单位,对于同一个文件FileAlignment和SectionAlignment的值可能相同,也可能不同,但内存的节区大小或磁盘文件中的最小单位必定为SectionAlignment和FileAlignment。在网上看资料的时候说,SectionAlignment一定要大于或等于FileAlignment,这句话是不对的。。别问我为什么,看图

25.png

第20个成员变量SizeOflmage,这个值表示加载PE文件到内存中时,SizeOflmag指定了PE Image在内存中所占的大小。一般来说,文件的大小与加载到内存中的大小是不同的(街区中定义了各字节装载的位置与所占有内存的大小。后面说)

26.png

第21个成员变量SizeOfHeaders,这个变量用来指出整个PE头的大小。这个值必须是FileAlingment的整数倍。第一节区所在位置和SizeOfHeaders距文件开始偏移的量相同。(不明白往上看,上面的那个PE文件加载到内存中的图)

27.png

第23个成员变量Subsystem,这个值用来区分系统驱动文件(*.sys)与普通文件(*.exe,*.dll)。Subsystem成员可拥有的值。看表格(官方给的太多了。)官网传送门

28.png

2.png

第30个成员变量NumberOfRvaAndSizes,该成员变量记录着DataDirectory(第31个成员变量)数组的个数。

29.png

第31个成员变量DataDirectory.

30.png

DataDirectory是由IMAGE_DATA_DIRECTORY结构体,

结构体类型:

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

每一项都有被定义的成员变量

DataDirectory[0] = EXPORT Directory
DataDirectory[1] = IMPORT Directory
DataDirectory[2] = RESOURCE Directory
DataDirectory[3] = EXCEPTION Directory
DataDirectory[4] = SECURITY Directory
DataDirectory[5] = BASERELOC Directory
DataDirectory[6] = DEBUG Directory
DataDirectory[7] = COPYRIGHT Directory
DataDirectory[8] = GLOBALPTR Directory
DataDirectory[9] = TLS Directory
DataDirectory[A] = LOAD_CONFIG Directory
DataDirectory[B] = BOUND_IMPORT Directory
DataDirectory[C] = IAT Directory
DataDirectory[D] = DELAY_IMPORT Directory
DataDirectory[E] = COM_DESCRIPTOR Directory
DataDirectory[F] = Reserved Directory

看第一个EXPORT Directory导出表

22.png

因为没有导出表,所以正常,全部为0

第二个为导入表IMPORT Directory导入表

222.png

这里就说这两个,因为后面要用到

4 节区头

节区头中定义了各节区的属性。PE文件中的code(代码),data(数据),resource(资源)等都按照分类存储在不同的节区中。把PE文件创建成多个节区结构的好处是,这样可以保证安全性。起始完全可以把code和resource放在一个字节中纠缠,但是容易引发安全问题,比如,向data段中写数据,写的数据如果超过缓冲区大小的时候,那么下边的code就被覆盖了,应用程序崩溃。所以,PE文件格式的设计者就把相似的数据统一保存在一个被称为节区地方,之后把需要的属性记录在节区头中(内存大小啊,访问权限等等的)。也就是说各个节区分别设置了特性,权限等。

2222.png

接下来看存放节区头的结构体IMAGE_SECTION_HEADER。每个结构体对应一个节区

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;

3.png

·VirtualAddress,SizeOfRawData是没有值的,他们的值都是由IMAGE_OPTIONAL_HEADER32中的SectionAlignment和FileAgnment

·VirtualSize,SizeOfRawData的值一般是不同的,程序一般在内存中跟在磁盘中的大小是不同的

因为是相同的结构体,所以只找一个作为演示

31.png

33.png

ps:其实看到他的那个NAME的值的时候我是蒙蔽的,不是说好的小端续排列么。(问了个大牛,他只对数有意义,对字符没有这种说法的)

实用PEid来看下工具找到的值跟手动找到的一样不一样

32.png

完美,一毛一样。

5. RVA to RAW

PE文件加载到内存时,每个节区都要能准确完成内存地址与文件转移的映射。这种映射就叫 RVA to RAW,

方法如下:

· 查找RVA所在节区

· 实用简单的公式计算文件偏移RAW

RAW - PointerToRawData = RVA - VirtualAddress
 RAW= RVA - VirtualAddress + PointerToRawData

演示一个计算:

RVA = 8888, FILE Offset=?
 看上面的图,首先查找RVA的值所在区域在第一节区`.text`,刚刚的`ImageBase`是1000000
 计算就是:
 RAW = 8888(RVA)-1000(VitualAddress)+400(PointerToRawData) = 7C88

6. IAT

最最最恶心的部分IAT(import Address Table),导入地址表,IAT保存的内容与Windows操作系统核心的进程,内存,DLL结构等有关的,也就是说,IAT就是Windows的根基。在说简单点,IAT就是一张表,记录了程序正在使用那些库中的那些函数

不过在学之前,我们得先看DLL文件,我不知到英文怎么写的,但是中文就叫动态链接库。

DLL文件有两种加载方式:

· 显式加载:程序使用DLL文件的时候加载,使用完成释放

· 隐式加载:程序在启动的时候加载DLL文件,程序结束的时候释放DLL

IAT提供的机制与隐式加载有关

看一个例子:

33.png

这是notepad.exe被导入od,调用CreateFileW()函数的代码,这个函数位于kernel32.dll中,

调用CreateFilew()函数并不是直接调用,而是通过01001104处的地址处的值来实现的(所有API地址都是通过这种方法调用的),地址01001104是notepad.exe中.text节区的内存区域(更确切的说是 IAT的地址)01001104地址的值位7C8107F0,而7C8107F0地址即是加载到notepad.exe进程内存中的CreateFileW()。

之后那么问题来了。为什么不直接调用Call 7C8107F0?微软在想什么。。

Kernel32.dll版本在不同的操作系统肯定是不同的,对应的CreateFileW 函数也不同,为了兼容各种环境,编译器准备了CreateFileW函数的实际地址,然后记下DWORD PTR DS:[xxxxxx]这样的指令,执行文件时候,PE 装载器将CreateFileW函数地址写到这个位置。

而且,存在重定向的问题,如果两个DLL文件有想用的ImageBase,那装载的时候,一个装上去了,另一个肯定不能继续放在这个位置上了。

6.1 IMAGE_IMPORT_DESCRIPTOR

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,
                                            // -1 if bound, and real datetime stamp
                                            //     in IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (new BIND)
                                            // O.W. date/time stamp of DLL bound to (Old BIND)
 
    DWORD   ForwarderChain;                 // -1 if no forwarders
    DWORD   Name;
    DWORD   FirstThunk;                     // RVA to IAT (if bound this IAT has actual addresses)
} IMAGE_IMPORT_DESCRIPTOR;
#如果你看到了这段代码这里,暂时不要理会,下面会说的
typedef struct _IMAGE_IMPORT_BY_NAME {
    WORD    Hint;
    BYTE    Name[1];
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;

对一般人来说,导入多少个库,就会存在多少的这样的结构体,这样的结构体组成了数组,而且结构体是以NULL结束的。

333.png

INT元素的各个值就是上面的_IMAGE_IMPORT_BY_NAME结构体的指针

INT和IAT的大小应该是相同的

下面这张图描述了一个exe程序加载dll的IMAGE_IMPORT_DESCRIPTOR(感觉这是最直观的一张图)

34.jpg

接着来看下完整的加载过程

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 都指向的是函数,为什么要这么做?

OriginalFirstThunk 和 First Thunk 他们两个兄弟都是类型为IMAGE_THUNK_DATA的数组,而这个IMAGE_THUNK_DATA又是一个指针大小联合类型。

每一个IMAGE_THUNK_DATA的结构体没有能导入一个名为IMAGE_IMPORT_BY_NAME的东西(就是上面那个让你忽略的,一样,先放着,等会再说这个结构体)

然后,数组最后一个内同0的IMAGE_THUNK_DATA作为结束标识符。赶紧补上结构体,看蒙蔽了估计都

typedef struct _IMAGE_THUNK_DATA32 {
    union {
        DWORD ForwarderString;      // PBYTE 
        DWORD Function;             // PDWORD
        DWORD Ordinal;
        DWORD AddressOfData;        // PIMAGE_IMPORT_BY_NAME
    } u1;
} IMAGE_THUNK_DATA32;

对于IMAGE_THUNK_DATA,如果最高位为1,则表示函数以序号的方式从DLL中导出引用,低31位表示序号;如果最高位为0,则表示名字导出,此时32位表示一个RVA,这个RVA指向一个结构为IMAGE_IMPORT_BY_NAME的元素。

现在,再说我们一直提到的那个结构体IMAGE_IMPORT_BY_NAME

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

Hint表示字段也表示函数的序号,不过,这个值可有可无,有些编辑器一直把这个字段设置成0

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

–x.重中之重

第一个数组(OriginalFirstThunk所指向)是单独的一项,而且不能被改下,就是前面被称为INT的家伙。

第二个数组(FirstThunk所指向)事实上是由PE装载器从重写的

更详细的说一下

PE装载器首先搜索OriginalFirstThunk,找到之后加载程序迭代搜索数组中的每个指针,找到每个IMAGE_IMPORT_BY_NAME所指向的输入函数入口地址,然后加载器用真正的入口函数代替FirstThunk数组中的入口,所以叫做输入地址表,在偷一张图

35.jpg

来打开notepad.exe看一下

这个值值究竟在哪里了,他算PE文件的那个部分?他是PE体重的内容,但是想要知道他的位置信息,可以网上看,看IMAGE_OPTIONAL_HEADER32.DATADirectory[1].VirtualAddress中的值就能找到他的地址信息。

36.png

不相信手工找的也可以用工具来查看一下

37.png

后面的C8为他的大小,既然拿到了RVA地址,我们来计算一下(忘记公式的,网上看)

RAW = 7604 - 1000 + 400 = 6A04
6404 + C8 = 6acc  这也是结构体的结束

3333.png

红框框中的就是结构体第一个元素:

4.png

换算出地址之后,看一下他的NAME字符串的指针,他导入了函数所属库文件名称

41.png

可以看到字符串的名称为comdlg32.dll

之后我们来看OriginalFirstThunk这个东西。INT是一个包含导入函数信息的结构体数组。只有获取了这些信息,才能准确请求相应函数的起始地址。等会再扯(EAT)

查看6D90

42.png

跟踪第一个数组(7A7A),就能看到导入函数API的名称

7A7A(RVA) - 1000 + 400 = 6E7A(RAW)

43.png

已经看到了PageSetupDlgW这个字符串,这是一个_IMAGE_IMPORT_BY_NAME的结构体,000F表示函数的序号,不知道有什么用没。后面的字符串以结尾,跟c语言一毛毛一样

这时候来看IAT这个东西,RAW的值为6C4

44.png

6C4~6EB区域存放的就是IAT的数组区域,对应的comdlg32.dll,他跟INT类似,由结构体指针组成,也是以NULL结尾。第一个被硬编码位76344906,没有什么实际意义,因为在程序加载的时候,这个地址会被准确的地址覆盖掉

用od打开看一下notepad.exe的IAT地址

45.png

可以看到这块地址就是PageSetupDlgw方法的入口地址

7. EAT

Windows中库是为了更方便调用其他程序调用耳机中包含的相关文件的函数的文件

EAT是一种核心机制,它使不同的应用程序可以调用库文件中提供的函数,也就是说,只有通过EAT才能从相应库中导出函数的起始地址。也就是说IMAGE_EXPORT_DIRECTORY中保存着导出信息,而且PE文件仅有一个用来说明库EA的IMAGE_EXPORT_DIRECTORY结构体(有木有头觉得比IAT友好许多,恩,肯定了,导入可以导入多个文件啊,兄弟)

使用Kernel32.dll这个文件来看看

46.png

RVA为0000262C,大小为00006D19.我们先计算出文件的偏移,虽然现在不用,但是等下会用

262C-1000+400 = 1A2C(RAW)

在看他的文件之前,先来分析下他的结构体

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;     // Export函数地址数组(数组元素的个数=NumberOfFunctions)
    DWORD   AddressOfNames;         // 指向输出函数名字的RVA  函数名称地址数组(数组元素的个数=NumberOfNames)
    DWORD   AddressOfNameOrdinals;  // 指向输出函数序号的RVA  Ordinal地址数组(数组元素个数=NumberOfNames)
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;

附一张说明的图片

47.jpg

加载器加载过程:

1. 根据名称查找函数地址

(1)定位导出模块的IMAGE_EXPORT_DESCRIPTOR
(2)以NumberOfNames(已知的明明函数总数)为循环次数,循环比较AddressOfNames指向的地址项所对应的函数名字符串,如果没找到匹配的名称字符串,则找不到所对应的函数。
(3)如果找到匹配的函数名字符串,则以此为索引号,取出AddressOfNameOrdinal所指向的序号。
(4)取出的序号-nBase=函数地址索引号
(5)以此为索引号,取出AddressOfFunctions指向的函数地址。

2. 根据序号查找函数地址

(1)定位导出模块的IMAGE_EXPORT_DESCRIPTOR
(2)序号-nBase=函数地址索引号
(3)以此为索引号,取出AddressOfFunctions指向的函数地址。

最后,使用Kernel32.dll掌握下他的结构体

48.png

只看重要的值

5.png

先看函数名称数组RAW=293C

49.png

可以发现,这一串都是有规律的字符串,不一定是我标记的地方,再往下也是,我们随便找一个,比如说选择数组中的第3个元素00004BBD,那他的RAW就是3FBD我们查看此处的值

50.png

就能查看到函数的名称,接下来,就用这个函数查找该函数Ordinal的值

51.png

可以看到该区域由多个2字节的Ordinal组成的数组(Ordinal数组中的个元素大小位2字节),将2中求得的index(2)应用到Ordinal数组就能求得Ordinal(2).

AddressOfNameOrdinals[index] = ordinal(index=2,ordinal=2)

最后查看他的实际函数地址,进入到1A54地址处

52.png

找到了326F1

打开od导入文件进去,用它的ImageBase+326f1  你就可以在od中找到他了。我就不找了,写的头疼。。

如果你看完了,辛苦你了= =!

源链接

Hacking more

...