逆向工程——PE
Posted re-and-er
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了逆向工程——PE相关的知识,希望对你有一定的参考价值。
在(一)中我们主要讨论了什么是PE以及PE头的组成结构,由此我们继续深入学习PE头的核心内容——IAT(IMPORT Address Table)和EAT(EXPORT Address Table)。
在进行Windows程序编程时,我们会使用到windows的各种API,那么在C语言里有类似的include语句,而在实现方面,操作系统是并不认识include语句的,其次,Windows使用了较为庞大的库函数来支持环境,因此运行多个进程,每个程序运行时都包含相同的库,会造成严重的内存浪费。这个时候,操作系统就使用一种手段,称为动态链接库(Dynamic Link Library,简称DLL),DLL的功能很强大,它是用来连接程序与库之间的联系,像桥梁的关系。在PE头(程序的PE头)里有一个IAT表,里面记录了程序正在使用哪些库中的哪些函数,而在DLL的PE头中则存在一张EAT表,它可以准确地求得从相应库中导出函数的起始地址。
使用DLL的好处主要有以下三点:
- 不需要把整个库加载进内存,需要什么用什么就行了。
- 进行内存映射以后,DLL文件可以被多个进程共享,不会造成资源的浪费。
- 更新库函数,只需要修改DLL里对应的地址即可。
- IAT
IAT的结构体为IMAGE_IMPORT_DESCRIPTOR,是PE头中可选项IMAGE_OPTIONAL_HEADER32里的DataDirectory[1],起始地址为IMAGE_OPTIONAL_HEADER32.DataDirectory[1].VirtualAddress(是RVA值,图中为1F4AC),Size为230。
1 typedef struct _IMAGE_IMPORT_DESCRIPTOR { 2 union { 3 DWORD Characteristics; // 0 for terminating null import descriptor 4 DWORD OriginalFirstThunk; // RVA to original unbound IAT (PIMAGE_THUNK_DATA) 5 }; 6 DWORD TimeDateStamp; // 0 if not bound, 7 // -1 if bound, and real date ime stamp 8 // in IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (new BIND) 9 // O.W. date/time stamp of DLL bound to (Old BIND) 10 11 DWORD ForwarderChain; // -1 if no forwarders 12 DWORD Name; 13 DWORD FirstThunk; // RVA to IAT (if bound this IAT has actual addresses) 14 } IMAGE_IMPORT_DESCRIPTOR; 15 typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR; 16 17 typedef struct _IMAGE_IMPORT_BY_NAME { 18 WORD Hint; 19 BYTE Name[1]; 20 } IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;
执行一个文件通常需要导入多个库,导入多少库就有多少个IMAGE_IMPORT_DESCRIPTOR结构体,这些结构体组成数组,结构体数组最后以NULL结束。
由上图的RVA地址(1F4AC)----->文件偏移地址为1C0AC(1F4AC - 所在节区的VirtualAddress(1F000) + PointerToRawData(1BC00) ),下图灰色选中区域即为第一个结构体。
Address values tips
1 01C0AC 0001F6DC OriginalFirstThunk(INT) 2 01C0B0 00000000 TimeDateStamp 3 01C0B4 00000000 ForwarderChain 4 01C0B8 0001FC7E Name 5 01C0BC 0001F000 FirstThunk(IAT)
-
- Name
Name是一个字符串指针,指向导入函数所属的库文件名称。(RVA 1FC7E ---> FOA 1C87E)
-
- OriginalFirstThunk(INT)
INT的各个元素的值为IMAGE_IMPORT_BY_NAME结构体指针,是由IMAGE_IMPORT_BY_NAME数组构成的表,每4个字节存放着对应的函数名称和Hint。
01C2DC 0001FB84 IMAGE_IMPORT_BY_NAME
...... ...... ...... ......
我们查看IMAGE_IMPORT_BY_NAME(数组中第一个元素)(RVA 1FB84 ---> FOA 1C784)。下图就是存放函数名字的空间。前2个字节为Hint编号,也称为Ordinal,是库中函数的编号,后面一长串是函数的名字。
01C784 0215 Hint 01C786 4F70656E50...... Name
-
- FirstThunk(IAT)
IAT数组中存放的每一个元素中的值是函数的地址(RVA 1F000 ---> FOA 1BC00)。有时IAT也拥有和INT一样的值,如下图。(1FB84转换为FOA与IAT的值一样)
IAT的输入(装载)顺序:
- 读取IMAGE_IMPORT_DESCRIPTOR结构体中的name成员,获取库名称字符串。
- 装载相对应的库(LoadLibrary("库名称字符串"))
- 读取IMAGE_IMPORT_DESCRIPTOR的OriginalFirstThunk成员,获取INT入口地址。
- 再逐一读取INT中数组的值,获取相对应IMAGE_IMPORT_BY_NAME地址(RVA地址)。
- 使用IMAGE_IMPORT_BY_NAME的Hint或者是Name成员,获取相应函数的起始地址。eg: GetProcAddress("GetCurrentThreadld")
- 读取IMAGE_IMPORT_DESCRIPTOR的FirstThunk(IAT)成员,获取IAT的地址。
- 最后将第5步求得的函数地址输入相应的IAT数组中。
- 重复以上4—7步骤,直到最后遇到NULL(NULL为结束)时结束。
- EAT
也有细心的读者发现了,在notepad.exe文件里的IMAGE_OPTIONAL_HEADER32.DataDirectory[0]的地址和大小都为0,也就是说,可执行文件只需要“保管好”自己的IAT表就可以了,而DLL文件则“保管好”自己的EAT值。以下是Kernel32.dll文件中的EAT值。
1 000168 00091020 RVA of EXPORT DIRECTORY 2 00016C 0000D850 size of EXPORT DIRECTORY
-
- IMAGE_EXPORT_DIRECTORY结构体
1 typedef struct _IMAGE_EXPORT_DIRECTORY { 2 DWORD Characteristics; 3 DWORD TimeDateStamp; 4 WORD MajorVersion; 5 WORD MinorVersion; 6 DWORD Name; 7 DWORD Base; 8 DWORD NumberOfFunctions; 9 DWORD NumberOfNames; 10 DWORD AddressOfFunctions; // RVA from base of image 11 DWORD AddressOfNames; // RVA from base of image 12 DWORD AddressOfNameOrdinals; // RVA from base of image 13 } IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
NumberOfFunctions 实际Export函数的个数 NumberOfNames Export函数中具名的函数个数 AddressOfFunctions Export函数地址数组(数组元素个数 = NumberOfFunctions) AddressOfNames 函数名称地址数组(数组元素个数 = NumberOfNames) AddressOfNameOrdinals Ordinal地址数组(数组元素个数 = NumberOfNames)
由上图可知,结构体位置在73020(RVA 91020 ---> FOA 73020)
1 073020 00000000 Characteristics 2 073024 AE0A74BF TimeDateStamp 3 073028 0000 MajorVersion 4 07302A 0000 MinorVersion 5 07302C 00094E96 Name 6 073030 00000001 Base 7 073034 0000063B NumerOfFuctions 8 073038 0000063B NumberOfNames 9 07303C 00091048 AddressOfFunctions 10 073040 00092934 AddressOfNames 11 073044 00094220 AddressOfNameOrdinals
-
- 函数名称数组(AddressOfNames) RVA 92934 ---> FOA 74934,以下图即为数组。每4个字节代表函数名称的RVA地址。
我们以94F02为例,(RVA 94F02 ---> FOA 76F02),以下深色选中区域即为函数名称。
我们再查找Ordinal数组。(RVA 94220 ---> FOA 76220),每2个字节代表索引号(Ordinal)。94F02处的函数名的索引号为0003。
最后我们再根据索引号,查出此函数的实际函数地址。通过AddressOfFunctions成员(RVA 91048 ---> FOA 73048 )的入口地址,再加上索引号产生的偏移量来确定函数地址。
AddressOfFunctions[Ordinal] = RVA (ordinal = 3 , RVA = 94F1A)
Kernel32.dll的ImageBase = 6B800000,因此,此函数的实际地址(VA)为6B894F1A(ImageBase + RVA)。
以上演示的过程的原理其实就是模拟了GetProcAdress()API函数的操作原理:
- 利用AddressOfNames成员转到“函数名称数组”。
- “函数名称数组”中存储着字符串地址。通过比较函数(strcmp)字符串,来查找函数名称。找到以后产生一个索引值(name_index)
- 利用AddressOfNameOrdinals成员,转到ordinal数组。
- 在ordinal数组中通过name_index查找与之相对应的ordinal值:AddressOfNameOrdinals[index] = ordinal
- 再利用AddressOfFunctions成员转到“函数地址数组”(EAT)
- 最后在“函数地址数组”中将求得的ordinal值用作数组索引,获得指定数组的起始地址。
以上是关于逆向工程——PE的主要内容,如果未能解决你的问题,请参考以下文章