PE文件解析(PE File Parsing)
PE(Portable Executable) File Parsing
WIN32 PE文件解析
链接:https://pan.baidu.com/s/1SR6IgJ645IBTPWy3PXNQgQ
提取码:4veh
- RVA:Relative Virtual Address(相对虚拟地址)。RVA 是指相对于映像基址的地址偏移量,在内存中定位特定数据的地址。
- RAW:Raw Data(原始数据)。RAW 是指 PE 文件中未经任何处理或压缩的原始二进制数据,它直接从文件中读取,例如节的原始数据。有的也叫FOA(file offset address)
PE头
DOS头
typedef struct _IMAGE_DOS_HEADER { // DOS .EXE header |
64字节,主要是为了兼容当时比较盛行的DOS系统,我们只关心下面两个
- e_magic:dos签名 4D5A(MZ)
- e_lfanew:NT/PE头偏移(IMAGE_NT_HEADERS)
- 如果该值为0,则该文件是一个DOS“MZ“可执行文件,Windows会启动DOS子系统来执行它,否则为Windows的PE可执行文件
DOS存根(stub)
e_lfanew偏移和_IMAGE_DOS_HEADER之间位置
前0xd字节是16位的汇编,输出This program cannot be run in DOS mode.后退出
dos头的e_cs e_ip一般指向这里
NT/PE头
typedef struct _IMAGE_NT_HEADERS { |
Signature
PE签名 0x50450000(“PE00”)
标准PE头 IMAGE_FILE_HEADER FileHeader
typedef struct _IMAGE_FILE_HEADER { |
- Machine
- PE文件的目标架构,执行文件所针对的CPU或处理器架构类型
- NumberOfSections
- 节表数量
- TimeDateStamp
- PE文件的创建或修改时间戳
- PointerToSymbolTable
- 符号表在文件中的偏移量
- SizeOfOptionalHeader
- IMAGE_OPTIONAL_HEADER32结构体长度
- Characteristics
- 标示文件的属性 是否为dll,是否可执行等,bitOR形式
扩展PE头 IMAGE_OPTIONAL_HEADER32 OptionalHeader
|
Magic
- 32位为0x10B,64位为0x20B
AddressOfEntryPoint
- RVA值,程序入口点
ImageBase
- PE文件被加载到内存地址,加载完后,EIP指向ImageBase+AddressOfEntryPoint
SizeOfImage
- PE文件装载到虚拟内存后的大小
SizeOfHeaders
- PE头按照FileAlignment对齐后的大小,包括section head
Subsystem
- 区分系统文件和普通可执行文件
- 系统驱动:drive 窗口:GUI 控制台:CUI 等
NumberOfRvaAndSizes
- DataDirectory数目
DataDirectory
数据目录项 索引位置 导出表(Export Table) IMAGE_DIRECTORY_ENTRY_EXPORT
(0)导入表(Import Table) IMAGE_DIRECTORY_ENTRY_IMPORT
(1)资源表(Resource Table) IMAGE_DIRECTORY_ENTRY_RESOURCE
(2)异常表(Exception Table) IMAGE_DIRECTORY_ENTRY_EXCEPTION
(3)安全表(Security Table) IMAGE_DIRECTORY_ENTRY_SECURITY
(4)重定位表(Base Relocation Table) IMAGE_DIRECTORY_ENTRY_BASERELOC
(5)调试表(Debug Table) IMAGE_DIRECTORY_ENTRY_DEBUG
(6)版本信息(Version Information) IMAGE_DIRECTORY_ENTRY_ARCHITECTURE
(7)全局指针(Global Pointer) IMAGE_DIRECTORY_ENTRY_GLOBALPTR
(8)TLS表(Thread Local Storage Table) IMAGE_DIRECTORY_ENTRY_TLS
(9)载入配置表(Load Configuration Table) IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG
(10)绑定导入表(Bound Import Table) IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT
(11)IAT表(Import Address Table) IMAGE_DIRECTORY_ENTRY_IAT
(12)延迟导入描述符(Delay Import Descriptor) IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT
(13)COM运行时描述符(COM Runtime Descriptor) IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR
(14)保留(Reserved) IMAGE_DIRECTORY_ENTRY_RESERVED
(15)
节区头
|
RAW(文件偏移)=RVA-VirtualAddress+PointerToRawData
这个换算有时候是不对的,比如说有未初始化初始的数据节,为了节约磁盘空间,VirtualSize比SizeOfRawData要大,换算后节区会不一样、
PE头内的话 RAW=RVA
导入表
_IMAGE_OPTIONAL_HEADER里DataDirectory的第二项,导入多少个库就有多少个_IMAGE_IMPORT_DESCRIPTOR结构,最后以一个空结构体结尾
typedef struct _IMAGE_IMPORT_DESCRIPTOR { |
以notepad为例
>>>Name
就是当前模块依赖模块名字
0x7604是RVA
可以看出0x7604他在第一个section(PE文件的section相比于ELF很少)
RAW=RVA-VirtualAddress+PointerToRawData=0x7604-0x1000+0x400=0x6A04
他的name字段就是0x7AAC,同样位于第一个节区RAW=0x7AAC-0x1000+0x400=0x6EAC
他的OriginalFirstThunk和FirstThunk分别指向INT(Import Name Table ,导入名称表) 和IAT(Import Address Table ,导入地址表)
>>>OriginalFirstThunk:INT表(导入名称表)
指向_IMAGE_THUNK_DATA32数组的RVA
typedef struct _IMAGE_THUNK_DATA32 { |
u1的最高位为1,去除这个1,其余就是导入函数的序号Ordinal,否则就是AddressOfData
RAW=0x6D90 0x6D90处就是_IMAGE_THUNK_DATA32数组,同样以NULL结构体结尾
RVA=0x7A7A RAW=0x6E7A,指向_IMAGE_IMPORT_BY_NAME结构体
0xF为库中函数序号,后面是导入函数名称
后面0x7a5e RAW为0x6e50
>>>FirstThunk:IAT表(导入地址表)
同样是指向_IMAGE_THUNK_DATA32数组的RVA
RAW为0x6c4
一堆不明所以的数据
丢到x32dbg看看
由于可执行文件PE是是进程第一个加载的模块,所以他几乎百分百都能抢到他原来的imagebase(除了开了aslr的情况),不需要去做重定位,所以这个IAT地址直接就是RVA+ImageBase
RVA+ImageBase=0x10012c4
0x77243e60这个地址就是PageSetupDlgw导入函数地址
类似于ELF里的got表,调用这种导入函数时是一个间接调用call *(iat)
由于这个notepad太老了,属于xp时代(和绑定导入表Bound Import Table有关),现在的操作系统上pe中IAT和INT表里的内容是一样的
导入表加载的大致流程就是(不同版本操作系统会有差别)
- 检查IMAGE_IMPORT_DESCRIPTOR.Name和FirstTunk,二者有一为空就停止加载导入表
- 检查OriginalFirstThunk是否为空,为空则从FirstTunk指向的IAT拿名称,否则用OriginalFirstThunk执行的INT拿名称
大致伪代码
PIMAGE_IMPORT_DESCRIPTOR pImportDes;//导入表位置
IMAGE_IMPORT_DESCRIPTOR EmptyImport = { 0 };
while (memcmp(pImportDes, &EmptyImport, sizeof(IMAGE_IMPORT_DESCRIPTOR))) {
//检查导入表名称
if (NULL == pImportDes->Name) {
break;
}
HMODULE hDll = LoadLibrary((LPCSTR)((DWORD)ImageBase + pImportDes->Name));
if (NULL == hDll) {
break;
}
//检查并获取IAT地址
if (NULL == pImportDes->FirstThunk) {
break;
}
DWORD* dwIAT = (DWORD *)((DWORD)pImportDes->FirstThunk+(DWORD)ImageBase);
//确定INT表位置
PIMAGE_THUNK_DATA pImportNameTable = (PIMAGE_THUNK_DATA)(pImportDes->OriginalFirstThunk + (DWORD)ImageBase);
if (NULL == pImportDes->OriginalFirstThunk) {//初始时IAT和INT内容一样
pImportNameTable = (PIMAGE_THUNK_DATA)(pImportDes->FirstThunk+(DWORD)ImageBase);
}
//判断高位是否为1,获得函数地址,并给IAT表复制
while (NULL != pImportNameTable->u1.Ordinal) {
DWORD dwPFNAddr;
if ((pImportNameTable->u1.Ordinal & 0x80000000)) {
dwPFNAddr = (DWORD)GetProcAddress(hDll, (LPCSTR)(pImportNameTable->u1.Ordinal&0x7fffffff));
}
else {
dwPFNAddr = (DWORD)GetProcAddress(hDll, (LPCSTR)
((PIMAGE_IMPORT_BY_NAME)(pImportNameTable->u1.AddressOfData))->Name+(DWORD)ImageBase);
}
*dwIAT = dwPFNAddr;
++dwIAT;
pImportNameTable++;
}
++pImportDes;
}
导出表
_IMAGE_OPTIONAL_HEADER里DataDirectory的第一项,导出表只有一个
typedef struct _IMAGE_EXPORT_DIRECTORY { |
导出函数名称表和导出函数序号表是一一对应的,可以说是一张表
NumberOfFunctions函数,导出时不按序号顺序导出,空余位也计入,比如1,5,2,3 但是NumberOfFunctions=5
所以就需要导出函数序号表的将函数地址和名称联系起来
导出函数可以没有名字但是一定要有序号
看一下GetProcAddress是如何解析的
FARPROC GetProcAddress(HMODULE hModule ,LPCWSTR lpProcName) |
lpProcName参数是name时(大于 0x10000)
- 根据AddressOfNames的RVA找到导出函数名称表(里面时字符串指针的RVA),利用strcmp比较字符串,找到名称,并记录下该名称在导出函数名称表的name_index,名称表里的指针指向的名字都是A-Za-z排序好的,按照朴素的二分法效率蛮高的 毕竟最多log2(n) 次嘛:)
- AddressOfNameOrdinals找到导出函数序号表,根据named_index找到对应的orinal
- AddressOfFunctions找到导出函数地址表的(EAT Export Address Table),根据orinal找到函数的RVA
lpProcName参数是序号时(小于 0x10000)
- 序号减去Base(起始序号)得到index
- 根据index去函数地址表去找
重定位表
typedef struct _IMAGE_BASE_RELOCATION { |
VirtualAddress是一个页对齐地址,TypeOffset的低12位记录了需要修正位置的页内偏移 TypeOffset高4位记录了重定位信息的类型,如下所示
// |
TLS表
显示tls动态使用__teb里TlsSlots的位置
BOOL TlsSetValue( |
索引小于0x40会根据索引存到teb的0xE10处
0:004> dt _teb |
隐式tls会用到tls表
typedef struct _IMAGE_TLS_DIRECTORY32 { |
程序开始时会把StartAddressOfRawData~EndAddressOfRawData(.tls section)处的数据拷到teb.ThreadLocalStoragePointer指向的位置
0:004> dt _teb |
__declspec(thread) int tls_i = 0x12345678; |
4: tls_i = 0x0; |
资源表
typedef struct _IMAGE_RESOURCE_DIRECTORY { |
但是现在是固定三层,所以结构固定,导致DUMMYSTRUCTNAME解析和上面不太一样
**第一层: **
DUMMYUNIONNAME 资源类型
/* |
**第二层: **
DUMMYUNIONNAME 对应的资源ID
第三层:资源数据
DUMMYUNIONNAME为代码页
DUMMYSTRUCTNAME2处rva处指向的数据按照下面结构解析
typedef struct _IMAGE_RESOURCE_DATA_ENTRY { |