PE文件的全称是Portable Executable ,意为可移植的可执行文件,常见的有EXE,DLL,SYS,COM,OCX,PE文件是微软Windows操作系统上的程序文件。
简单来理解,就是一种数据结构。我们知道知道CPU只认识二进制数,所以可执行文件保存在磁盘中都是以二进制数保存的,不过为了查看,所以市面上的编辑器都是用16进制显示的,比如下面这样,使用010Editer打开一个PE文件:
PE文件结构如下:
有的数据结构是用来执行的代码,有的数据结构是用来保存数据。
它们之间的关系:虚拟地址(VA) = 基地址(Image Base)+相对虚拟地址(RVA)
首先是DOS头和DOS存根,它们的存在主要是用来兼容DOS系统。当我们的程序运行在DOS系统的时候,就会运行DOS存根中的代码,代码内容就是输出一段字符串告诉用户,这个程序不能在16位系统运行。
而DOS头保存了程序的信息,DOS头的定义如下:
typedef struct _IMAGE_DOS_HEADER { // DOS .EXE header
WORD e_magic; // Magic number
WORD e_cblp; // Bytes on last page of file
WORD e_cp; // Pages in file
WORD e_crlc; // Relocations
WORD e_cparhdr; // Size of header in paragraphs
WORD e_minalloc; // Minimum extra paragraphs needed
WORD e_maxalloc; // Maximum extra paragraphs needed
WORD e_ss; // Initial (relative) SS value
WORD e_sp; // Initial SP value
WORD e_csum; // Checksum
WORD e_ip; // Initial IP value
WORD e_cs; // Initial (relative) CS value
WORD e_lfarlc; // File address of relocation table
WORD e_ovno; // Overlay number
WORD e_res[4]; // Reserved words
WORD e_oemid; // OEM identifier (for e_oeminfo)
WORD e_oeminfo; // OEM information; e_oemid specific
WORD e_res2[10]; // Reserved words
LONG e_lfanew; // File address of new exe header
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
这里面有用的信息就两个:
PE头结构如下:
注意:e_magic是word类型,占两个字节;Signature是dword类型,占四个字节。
Signature表示是否是PE,vs中有一个宏来定义它:
PE头其实由两部分组成,一个是标准PE文件头,一个是可选头。
先说标准PE头:
在VS中继续跟进查看PE文件头结构体:
typedef struct _IMAGE_FILE_HEADER {
WORD Machine; //运行平台,在不同平台上,其值也不一样
WORD NumberOfSections; //区段数据数量
DWORD TimeDateStamp; //文件什么时候被创建的
DWORD PointerToSymbolTable; //指向符号表的偏移指针
DWORD NumberOfSymbols; //符号表中的符号数量
WORD SizeOfOptionalHeader; //文件头扩展头的大小
WORD Characteristics; //PE文件的属性
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
下面做一些重点介绍:
Machine:
微软也给出了相应平台的宏定义:
NumberOfSections:
用loadPE打开一个PE文件,可以看到其区段:
常用区段:.text: 代码段,里面的数据都是代码,有的编译器也叫做code段,其实都是宏定义的;.data:数据段(可读写),存放全局变量和静态变量;.rdata:数据段(只读);.idata:导入数据区段,存放导入表数据信息;.edata:导出数据区段,存放导出表数据信息;.rsrc:资源段;.bss:存放未初始化数据;.crt:c++ 运行时库 runtime;.reloc:重定位;.tls:线程局部存储。
Characteristics:
PE文件的属性,相应宏定义如下:
其结构体成员如下:
typedef struct _IMAGE_ROM_OPTIONAL_HEADER {
WORD Magic; //文件类型的标识 32位的PE还是64位PE
BYTE MajorLinkerVersion; //链接器主版本号
BYTE MinorLinkerVersion;//链接器子版本号
DWORD SizeOfCode; //区段的总大小
DWORD SizeOfInitializedData; //已初始化数据段的大小
DWORD SizeOfUninitializedData;//未初始化数据段的大小
DWORD AddressOfEntryPoint; // 程序入口RVA
DWORD BaseOfCode; //代码段机址RVA
DWORD BaseOfData;//数据段基址RVA
DWORD BaseOfBss;//入口点,文件在内存中的首选装入地址
DWORD GprMask;
DWORD CprMask[4];
DWORD GpValue;
} IMAGE_ROM_OPTIONAL_HEADER, *PIMAGE_ROM_OPTIONAL_HEADER;
typedef struct _IMAGE_OPTIONAL_HEADER64 {
WORD Magic;
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode;
DWORD SizeOfInitializedData;
DWORD SizeOfUninitializedData;
DWORD AddressOfEntryPoint;
DWORD BaseOfCode;
ULONGLONG ImageBase;
DWORD SectionAlignment;//映像文件在装入内存中区段的对齐大小 ,通常是0x1000
DWORD FileAlignment;//磁盘中节区对齐大小
WORD MajorOperatingSystemVersion;//操作系统最低版本的主版本号
WORD MinorOperatingSystemVersion;//操作系统最低版本的子版本号
WORD MajorImageVersion;
WORD MinorImageVersion;
WORD MajorSubsystemVersion;
WORD MinorSubsystemVersion;
DWORD Win32VersionValue; //保留值 最低为00000000
DWORD SizeOfImage;
DWORD SizeOfHeaders;//ms—dos头,pe头,区段表总和
DWORD CheckSum;//映像文件静态和
WORD Subsystem;//可执行文件期望的子系统的值
WORD DllCharacteristics;//dllMain函数何时被调用
ULONGLONG SizeOfStackReserve;//exe中线程被保存的堆栈大小
ULONGLONG SizeOfStackCommit;//栈初始化的内存大小 默认大小4kb
ULONGLONG SizeOfHeapReserve;//堆初始化的内存大小默认大小1mb
ULONGLONG SizeOfHeapCommit;//每次指派给堆的大小,默认4kb
DWORD LoaderFlags;// 和调试有关 模式0
DWORD NumberOfRvaAndSizes;//数据目录成员的数量 一般16个
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER64, *PIMAGE_OPTIONAL_HEADER64;
Magic
宏定义PE是32位还是64位
#define IMAGE_NT_OPTIONAL_HDR32_MAGIC 0x10b //32位PE
#define IMAGE_NT_OPTIONAL_HDR64_MAGIC 0x20b //64位PE
AddressOfEntryPoint:
Subsystem:
可执行文件期望的子系统的值,宏定义如下:
通过C++打印相关成员,在监视中,我们也能看到相关成员的属性值:
#include <Windows.h>
#include <stdio.h>
#include<iostream>
#include <iomanip>
#define FilePath "C:\\Users\\Administrator\\Desktop\\CreateProcess.exe"
int main(int argc,char *argv[]) {
FILE* pFile = NULL;
char* buffer;
int nFileLength = 0;
pFile = fopen(FilePath, "rb");
fseek(pFile, 0, SEEK_END);
nFileLength = ftell(pFile);
rewind(pFile);
int imageLength = nFileLength * sizeof(char) + 1;
buffer = (char*)malloc(imageLength);
memset(buffer, 0, nFileLength * sizeof(char) + 1);
fread(buffer, 1, imageLength, pFile);
PIMAGE_DOS_HEADER ReadDosHeader;
ReadDosHeader = (PIMAGE_DOS_HEADER)buffer;
std::cout << "MS-DOS info:" << std::endl;
std::cout << "MZ标志位:" <<std::hex<< ReadDosHeader->e_magic << std::endl;
std::cout << "PE偏移头:" << std::hex<< ReadDosHeader->e_lfanew << std::endl;
std::cout << "PE info:" << std::endl;
PIMAGE_NT_HEADERS ReadNTheaders;
ReadNTheaders = (PIMAGE_NT_HEADERS)(buffer + ReadDosHeader->e_lfanew);
std::cout << "PE标志位:" << std::hex << ReadNTheaders->Signature << std::endl;
std::cout << "运行平台:" << std::hex << ReadNTheaders->FileHeader.Machine << std::endl;
std::cout << "ImageBase入口点:" << std::hex << ReadNTheaders->OptionalHeader.ImageBase << std::endl;
free(buffer);
return 0;
}
区段也是一个结构:
前面我们在loadPE中打开,已经能看到区段表相关信息:
typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; //区段名称,比如.data .code等
union {
DWORD PhysicalAddress;
DWORD VirtualSize; //实际使用的区段大小(没做对齐前的大小)
} Misc;
DWORD VirtualAddress; //区段载入内存之后的RVA,按照内存页对齐
DWORD SizeOfRawData;//在磁盘中的大小
DWORD PointerToRawData;//区段在文件中偏移
DWORD PointerToRelocations;//区段在重定位表中的偏移
DWORD PointerToLinenumbers;//行号表在文件中的偏移
WORD NumberOfRelocations;
WORD NumberOfLinenumbers;
DWORD Characteristics;//区段属性
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
现在通过已经了解到的,模仿loadPE,用控制台程序写一个PE 区段表解析器。
Microsoft Visual Studio提供了一个宏(IMAGE_FIRST_SECTION )来定义定位区段表:
以此来定义一个区段头:
PIMAGE_SECTION_HEADER ReadSectionHeader = IMAGE_FIRST_SECTION(ReadNTheaders);
然后去PE文件标准头的地址:
PIMAGE_FILE_HEADER pFileHeader = &ReadNTheaders->FileHeader;
IMAGE_FILE_HEADER 有个值表示了区段数量,可以根据这个数量来进行遍历:
代码如下:
for (int i = 0; i < pFileHeader->NumberOfSections; i++)
{
std::cout << "Name[区段名称]:" << std::hex << ReadSectionHeader[i].Name << std::endl;
std::cout << "VOffset[起始相对虚拟地址]:" << std::hex << ReadSectionHeader[i].VirtualAddress << std::endl;
std::cout << "VSzie[区段大小(内存中)]:" << std::hex << ReadSectionHeader[i].SizeOfRawData << std::endl;
std::cout << "ROffset[文件偏移]:" << std::hex << ReadSectionHeader[i].PointerToRawData << std::endl;
std::cout << "RSize[区段大小(文件中)]:" << std::hex << ReadSectionHeader[i].Misc.VirtualSize << std::endl;
std::cout << "标记[区段属性]:" << std::hex << ReadSectionHeader[i].Characteristics << std::endl;
std::cout << "----------------------------------"<< std::endl;
}
数据目录表是PE中比较重要的一个部分,其也是一个结构。微软在Microsoft Virtual Studio在对其结构又定义。
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress; //虚拟地址,就是数据目录表的起始位置
DWORD Size;//尺寸, 起始地址+尺寸 = 结束的位置
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
结构如下:
用loadPE打开一个PE文件,点击目录,即可看到数据目录表,再点击每个表对应可以展开的按钮,即可看到相应参数对应的值,
比如点开输入表,可看到调用了哪些动态链接库,又在动态链接库里调用了哪些API:
为了加深理解,下面对一些重要的表进行学习解析。
解析这些表之前,先写一个地址转换函数,就是将相对虚拟地址(RVA)转换为文件偏移地址(Offset)。
那么为什么要写这样一个函数呢?因为一些PE文件为了减小体积,磁盘对齐值不是一个内存页1000h,而是200h。当这类文件被映射到内存中后,同一数据相对于文件头的偏移量在内存中和磁盘文件是不同的,这样就出现了文件偏移地址和虚拟地址的转换问题。当然,那些磁盘对齐值与内存对齐值相同的区块,同一数据在磁盘文件中的偏移与在内存中的偏移相同,因此不需要转换。
如图,当文件被映射到内存中时,MS-DOS头,PE头和块表的偏移位置都没有改变,但是当区块被映射到内存中后,其偏移地址就发生了改变。
文件偏移地址(Offset)为add1 ,相对虚拟地址(RVA)为add2。它们直接相差了一个以0填充的空白区域,假设这个值为🔺H,那么:
Offset =RVA -🔺H
Offset =VA -ImageBase -🔺H
下面开始写代码
首先声明一个函数:
DWORD RvaToOffset(DWORD dwRa, char* buffer);
//dwRva 是某个数据目录表的起始位置
//buffer PE文件载入内存中的缓冲
//返回地址,所有用DWORD存储
获取文件的缓存区:
#define FilePath "C:\\Users\\Administrator\\Desktop\\CreateProcess.exe"
FILE* pFile = NULL;
char* buffer;
int nFileLength = 0;
pFile = fopen(FilePath, "rb");
fseek(pFile, 0, SEEK_END);
nFileLength = ftell(pFile);
rewind(pFile);
int imageLength = nFileLength * sizeof(char) + 1;
buffer = (char*)malloc(imageLength);
memset(buffer, 0, nFileLength * sizeof(char) + 1);
fread(buffer, 1, imageLength, pFile);
原理比较简单:首先判断这个地址是否在PE头中,如果在,文件偏移和内存偏移相等,如果存在于文件的区段中,则利用以下公式:内存偏移 - 该段起始的RVA(VirtualAddress) = 文件偏移 - 该段的PointerToRawData内存偏移 = 该段起始的RVA(VirtualAddress) + (文件偏移 - 该段的PointerToRawData)文件偏移 = 该段的PointerToRawData + (内存偏移 - 该段起始的RVA(VirtualAddress))
代码逻辑如下:
DWORD RvaToOffset(DWORD dwRva, char* buffer)
{
//DOS头
PIMAGE_DOS_HEADER pDOS = (PIMAGE_DOS_HEADER)buffer;
//PE头
PIMAGE_NT_HEADERS pNT = (PIMAGE_NT_HEADERS)(pDOS->e_lfanew + buffer);
//区段表
PIMAGE_SECTION_HEADER pSection = IMAGE_FIRST_SECTION(pNT);
//判断是否落在头部当中
if (dwRva < pSection[0].VirtualAddress) {
return dwRva;
}
//_IMAGE_NT_HEADERS
for (int i = 0; i <pNT->FileHeader.NumberOfSections; i++)
{
//VirtualAddress 起始地址
//Size 长度
// VirtualAddress + Size 结束地址
//判断是否落在某个区段内
if (dwRva >= pSection[i].VirtualAddress && dwRva <= pSection[i].VirtualAddress + pSection[i].Misc.VirtualSize)
{
//dwRva - pSection[i].VirtualAddress 数据目录表到区段起始地址的偏移(OFFSET)
//pSection[i].PointerToRawData 区段到文件头的偏移(OFFSET)
//返回的是数据目录起始表地址到文件头的偏移(OFFSET)
return dwRva - pSection[i].VirtualAddress + pSection[i].PointerToRawData;
}
}
//VirtualAddress 起始地址
//Size 长度
//VirtualAddress +Size 结束地址
return 0;
}
可执行文件使用来自其他DLL的代码或数据的动作称为输入。当PE文件在被载入时,Windows加载器的工作之一就是定位所有被数据的函数和数据,并让正在载入的文件可以使用那些地址。
导入函数就是被程序调用但其执行代码不在程序中的函数,这些函数在DLL文件中,当应用程序调用一个DLL的代码和数据时,它正被隐式地链接到DLL,这个过程由Windows加载器完成。另一种链接是显示链接,它是已经约定目标DLL已经被加载,然后寻找API的地址,一般是通过Loadlibrary 和GetprocAddress完成。
简而言之,导入表主要是PE文件从其他第三方库中导入API,以供本程序调用,结构如下:
OriginalFirstThunk和FirstThunk分别指向两个不同的IMAGE_THUNK_DATA结构的数组。这两个数组都以一个空的IMAGE_THUNK_DATA结构结尾。 一般情况下,导入表只需要关注OriginalFirstThunk和FirstThunk这两个字段。
IMAGE_THUNK_DATA结构:
那么要解析导入表,首先要定位到导入表:
通过PE扩展头里数据目录字段 + 导入表的宏定义,即可定位到导入表,
PIMAGE_DATA_DIRECTORY pImportDir = (PIMAGE_DATA_DIRECTORY)(pNt->OptionalHeader.DataDirectory + IMAGE_DIRECTORY_ENTRY_IMPORT);
//pNt->OptionalHeader.DataDirectory PE扩展头里数据目录字段 + 导入表的宏定义
在Microsoft Virtual Studio中,在IMAGE_DIRECTORY_ENTRY_IMPORT处 ,ctrl +鼠标左键 即可跳转到该宏定义:
然后就是填充结构,前面写了一个找数据表到文件头偏移的函数RvaToOffset,现在就用这个函数来找导入表到文件头的偏移:
PIMAGE_IMPORT_DESCRIPTOR pImport = (PIMAGE_IMPORT_DESCRIPTOR)(RvaToOffset(pImportDir->VirtualAddress, buffer)+buffer);
//填充结构
//pImportDir->VirtualAddress 导入表头的位置 也就是RVA
//RvaToOffset(pImportDir->VirtualAddress, buffer) 导入表到文件头的偏移 再加上buffer就找到了地址
然后就是遍历数据:
while (pImport->Name!=NULL)
{
char* szDllName = (char*)(RvaToOffset(pImport->Name, buffer)+buffer);
std::cout << "DLL名称[DllName]:" << szDllName << std::endl;
std::cout << "日期时间标志[TimeDateStamp]:" <<std::hex<< pImport->TimeDateStamp << std::endl;
std::cout << "转发链[ForWarderChain]:" << std::hex<<pImport->ForwarderChain << std::endl;
std::cout << "名称OFFSET[Name]:" << std::hex << pImport->Name << std::endl;
std::cout << "FirstThunk:" << std::hex << pImport->FirstThunk << std::endl;
std::cout << "OriginalFirstThunk:" << std::hex << pImport->OriginalFirstThunk << std::endl;
std::cout << "[******************************************************]" << std::endl;
//指向地址表中的RVA
PIMAGE_THUNK_DATA pIat = (PIMAGE_THUNK_DATA)(RvaToOffset(pImport->OriginalFirstThunk, buffer) + buffer);
DWORD Index = 0;
DWORD ImportOffset = 0;
//被导入函数的序号
while (pIat->u1.Ordinal !=0)
{
std::cout << "ThunkRva:" << std::hex << pImport->OriginalFirstThunk+Index << std::endl;
ImportOffset = RvaToOffset(pImport->OriginalFirstThunk, buffer);
std::cout << "ThunkOffset:" << std::hex << ImportOffset+Index << std::endl;
Index += 4;
if ((pIat->u1.Ordinal & 0x80000000) != 1)
{
PIMAGE_IMPORT_BY_NAME pName = (PIMAGE_IMPORT_BY_NAME)(RvaToOffset(pIat->u1.AddressOfData, buffer) + buffer);
std::cout << "API名称:" << pName->Name << std::endl;
std::cout << "API序号:" << pName->Hint << std::endl;
//被导入函数地址
std::cout << "ThunkValue:" << pIat->u1.Function << std::endl;
std::cout << "---------------------" << std::endl;
}
pIat++;
}
pImport++;
}
结果如下,可以看到是没有任何问题的:
导出表是PE文件为其他应用程序提供自身的一些变量、函数以及类,将其导出给第三方程序使用的一张清单,里面包含了可以导出的元素。
结构如下:
从逻辑上来说,导出表由名称表、函数表与序号表组成。函数表和序号表必不可少,名称表则是可选的。序号表与名称表的作用是索引,找到真正需要的函数表,函数表中保存着被导出的函数的地址信息。
导出地址表(EAT) | 序号 | 导出名称表(ENT) |
---|---|---|
0x00010000(某函数地址) | 0x0001 | FunName_A |
0x00025000(某函数地址) | 0x0002 | FunName_B |
0x00050000(某函数地址) | 0x0003 | FunName_C |
这里写一个有导出函数的测试dll:
在loadPE中打开,打开导出表,可以看到有导出函数:
现在就是和导入表一样,找导出表到文件头的偏移,然后手写代码获取相关信息:
逻辑和导入表类似,就不赘述了,直接给出代码:
void AnalysisExportTable(char* buffer)
{
//Dos
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)buffer;
//PE
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(pDos->e_lfanew + buffer);
//定位数据目录中的导出表
PIMAGE_DATA_DIRECTORY pExportDir = pNt->OptionalHeader.DataDirectory + IMAGE_DIRECTORY_ENTRY_EXPORT;
//填充导出表结构
PIMAGE_EXPORT_DIRECTORY pExport = (PIMAGE_EXPORT_DIRECTORY)(RvaToOffset(pExportDir->VirtualAddress, buffer)+buffer);
char* szName = (char*)(RvaToOffset(pExport->Name, buffer) + buffer);
if (pExport->AddressOfFunctions ==0)
{
printf("当前没有导出表");
return ;
}
printf("导出表OFFSET:%08X\n", RvaToOffset(pExportDir->VirtualAddress, buffer));
printf("特征值:%08X\n", pExport->Characteristics);
printf("基:%08X\n", pExport->Base);
printf("名称OFFSET:%08X\n", pExport->Name);
printf("名称: %s\n", szName);
printf("函数数量:%08X\n", pExport->NumberOfFunctions);
printf("函数名数量:%08X\n", pExport->NumberOfNames);
printf("函数地址:%08X\n", pExport->AddressOfFunctions);
printf("函数名称地址:%08X\n", pExport->AddressOfNames);
printf("函数名称序号地址:%08X\n", pExport->AddressOfNameOrdinals);
//函数地址数量
DWORD dwNumOfFun = pExport->NumberOfFunctions;
//函数名数量
DWORD dwNumOfNames = pExport->NumberOfNames;
//基
DWORD dwBase = pExport->Base;
//导出地址表
PDWORD pEAt32 = (PDWORD)(RvaToOffset(pExport->AddressOfFunctions, buffer) + buffer);
//导出名称表
PDWORD pENt32 = (PDWORD)(RvaToOffset(pExport->AddressOfNames, buffer) + buffer);
//导出序号表
PWORD pId = (PWORD)(RvaToOffset(pExport->AddressOfNameOrdinals, buffer) + buffer);
for (DWORD i = 0; i < dwNumOfFun; i++)
{
if (pEAt32[i] == 0)
{
continue;
}
DWORD Id = 0;
for (; Id < dwNumOfNames; Id++)
{
if (pId[Id] == i)
{
break;
}
}
if (Id == dwNumOfNames)
{
printf("Id:%x Address:0x%08X Name[NULL]\n", i + dwBase,pEAt32[i]);
}
else
{
char* szFunName = (char *)(RvaToOffset(pENt32[Id], buffer) + buffer);
printf("Id:%x Address:0x%08X Name[%s]\n", i + dwBase, pEAt32[i], szFunName);
}
}
}
结果如下,可以看到除了不美观以外,结果是没有任何问题的:
当向程序的虚拟内存加载PE文件时,文件会被加载到ImageBase所指向的地址。
ImageBase就是前面讲到的PE拓展头中的一个成员:
对EXE文件来说,EXE文件会首先加载到内存,每个文件总是使用独立的虚拟地址空间,这就意味着EXE文件不用考虑基址重定位问题;
对于DLL文件来说,多个DLL文件使用调用其本身的EXE文件的地址空间,不能保证ImageBase所指向的地址没有被其他DLL文件占用,所以DLL文件当中必须包含重定位信息,也就是说,本来A.DLL被加载到test.exe进程的00100000地址处,但是此处加载了B.DLL文件,PE装载器将A.DLL文件加载到其他还未被占用的地址处(00850000)处。
对于系统的DLL来说实际上不会发生重定位,因为同一系统的kernel32.dll、user32.dll等会被加载到自身固有的ImageBase。
重定位结构:
typedef struct _IMAGE_BASE_RELOCATION {
DWORD VirtualAddress; //指向需要重地位的地址的RVA,每个IMAGE_BASE_RELOCATION只负责4KB大小分页内的重定位信息。因此结构中的VirtualAddress值为0x1000的倍数。
DWORD SizeOfBlock; //imagebase 结构体 和TypeOffset 的总和 重定位块的大小
// WORD TypeOffset[1]; # 自定义的一个字段 表示这个结构体下面会出现WORD类型的数组,该数组元素的值就是硬编码在程序当中的偏移
} IMAGE_BASE_RELOCATION;
typedef IMAGE_BASE_RELOCATION UNALIGNED * PIMAGE_BASE_RELOCATION;
自定义Typeoffset结构:
typedef struct _TYPE {
WORD Offset :12; //大小 2bit 重定位的偏移
WORD Tyoe : 4;
}TYPE, * PTYPE;
Windows的PE装载器进行PE重定位处理的操作原理流程如下:
其中最关键的就是找到硬编码的位置,而要找到硬编码的位置,首先要找到基址重定位表,该表位于.reloc区段,找到基址重定位表的的正确打开方式是通过数据目录表的IMAGE_DIRECTORY_ENTRY_BASERELOC条目查找
实现如下:
void AnalysisRelocTable(char* buffer)
{
typedef struct _TYPE {
WORD Offset :12; //大小 2bit 重定位的偏移
WORD Tyoe : 4;
}TYPE, * PTYPE;
//Dos
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)buffer;
//PE
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(pDos->e_lfanew + buffer);
//定位重定位表
PIMAGE_DATA_DIRECTORY pRelocDir = (pNt->OptionalHeader.DataDirectory + IMAGE_DIRECTORY_ENTRY_BASERELOC);
//填充结构
PIMAGE_BASE_RELOCATION pReloc = (PIMAGE_BASE_RELOCATION)(RvaToOffset(pRelocDir->VirtualAddress, buffer) + buffer);
//定位区段
PIMAGE_SECTION_HEADER pSection = IMAGE_FIRST_SECTION(pNt);
while (pReloc->SizeOfBlock !=0)
{
//找到本0x1000个字节的起始位置
//重定位个数 = (SizeOfBlock - 8(IMAGE_BASE_RELOCATION的大小)) / 2(每个TypeOffset是2个字节)
DWORD dwCount = (pReloc->SizeOfBlock - 8) / 2; //需要重定位的个数
DWORD dwRva = pReloc->VirtualAddress;
PTYPE pRelocArr = (PTYPE)(pReloc + 1);
printf("区段:%s\n", pSection->Name);
printf("RVA:%08x\n", dwRva);
printf("项目:%X h / %d D\n", pReloc->SizeOfBlock,pReloc->SizeOfBlock);
std::cout << "[******************************************************]" << std::endl;
//找到下一个0x1000的结构体
pReloc = (PIMAGE_BASE_RELOCATION)((char*)pReloc + pReloc->SizeOfBlock);
for (int i = 0; i < dwCount; i++)
{
PDWORD pData = (PDWORD)(RvaToOffset(pRelocArr[i].Offset + dwRva, buffer) + buffer);
DWORD pDataOffset = RvaToOffset(pRelocArr[i].Offset + dwRva, buffer);
printf("RVA:%08X\n", pRelocArr[i].Offset + dwRva);
printf("区段:%08X\n", *pData);
printf("偏移:%08X\n", pDataOffset);
std::cout << "[-----------------------------------------------]" << std::endl;
}
}
}
TLS:线程本地存储器,可以将数据与执行的特定线程联系起来。怎么理解呢?
如果一个变量是全局的,那么所有线程访问的是同一份,某一个线程对其修改会影响其他所有线程。如果我们需要一个变量在每个线程中都能访问,并且值在每个线程中互不影响,这就是TLS。
线程局部存储在不同平台有不同的实现,可移植性不好。线程局部存储不难实现,最简单的办法是建立一个全局表,通过当前线程ID去查询相应的数据,因为各个线程ID去查询相应的数据,因为各个线程的ID不同,查到的数据自然也不同。
这里就不详细介绍了,我们简单解析一些PE中的TLS表即可:
先看一下它的结构,分为32和64位的:
解析起来也比较简单:
void AnalysisTLSTable(char* buffer)
{
//Dos
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)buffer;
//PE
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(pDos->e_lfanew+buffer);
//定位数据目录表中的TLS表
PIMAGE_DATA_DIRECTORY pTLSDir = (pNt->OptionalHeader.DataDirectory + IMAGE_DIRECTORY_ENTRY_TLS);
//填充结构
PIMAGE_TLS_DIRECTORY pTLS = (PIMAGE_TLS_DIRECTORY)(RvaToOffset(pTLSDir->VirtualAddress, buffer) + buffer);
printf("数据块开始VA: % 08X\n", pTLS->StartAddressOfRawData);
printf("数据块结束VA: % 08X\n",pTLS->EndAddressOfRawData);
printf("索引变量VA: % 08X\n",pTLS->AddressOfIndex);
printf("回调表VA: % 08X\n",pTLS->AddressOfCallBacks);
printf("填充大小: % 08X\n",pTLS->SizeOfZeroFill);
printf("特征值: % 08X\n",pTLS->Characteristics);
}
延迟载入是一种混合方式,通过LoadLibrary和GetProcAddress获取延迟加载函数额地址,然后直接加载转向对延迟加载函数的调用。
结构如下:
解析延迟导入表:
void AnalysisDelayTable(char* buffer)
{
//Dos
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)buffer;
//PE
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(pDos->e_lfanew + buffer);
//定位数据吧、目录表中的延迟导入表
PIMAGE_DATA_DIRECTORY pDelayLoadDir =(PIMAGE_DATA_DIRECTORY)(pNt->OptionalHeader.DataDirectory + IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT);
//填充延迟导入表数据结构
PIMAGE_DELAYLOAD_DESCRIPTOR pDelayLoad = (PIMAGE_DELAYLOAD_DESCRIPTOR)(RvaToOffset(pDelayLoadDir->VirtualAddress, buffer) + buffer);
while (pDelayLoad->DllNameRVA != NULL)
{
char* szDllName = (char*)(RvaToOffset(pDelayLoad->DllNameRVA, buffer) + buffer);
printf("DllName:%s", szDllName);
printf("Attributes:%08X\n", pDelayLoad->Attributes);
printf("ModuleHandleRVA:%08X\n", pDelayLoad->ModuleHandleRVA);
printf("ImportAddressTableRVA:%08X\n", pDelayLoad->ImportAddressTableRVA);
printf("ImportNameTableRVA:%08X\n", pDelayLoad->ImportNameTableRVA);
printf("BoundImportAddressTableRVA:%08X\n", pDelayLoad->BoundImportAddressTableRVA);
printf("UnloadInformationTableRVA:%08X\n", pDelayLoad->UnloadInformationTableRVA);
printf("TimeDateStamp:%08X\n", pDelayLoad->TimeDateStamp);
std::cout << "[******************************************************]" << std::endl;
pDelayLoad++;
}
}
文章参考: 百度,google