PE文件详解02
1.内容概要
PE文件详解01中我们已经解析了dos头,nt头,区块头表中的数据,接下来本片文章主要讲数据目录表中索引的几种关键数据。这些关键数据包括导入表,导出表,资源表,重定位表,线程本地存储,延迟加载表。
2.数据目录表内容在nt头的扩展头中 ,数据目录表是对上面各种表的索引,方便我们从区块中快速检索到数据。
3.导出表
3.1:导出表的作用
导出表是PE文件自己给其他PE文件提供函数,变量的描述表,表中描述了自身这个PE文件给外界提供的函数名,函数地址,函数序号,变量名等符号。一般是dll文件有导出表,但并不是dll中的函数都全部要导出,只有在dll中申明了导出的函数,在导出表中才有记录。
3.2 dll导出函数的声明方式与调用方式:
- dll导出函数的几种方式
声明导出: _declspec(dllexport)
def文件导出 - dll函数调用
隐式链接 :包含头文件,载入lib库
显示连接 :LoadLibray,GetProcAddress
3.3 导出表解析中的注意事项
windows下存在导出表的可执行文件,可以提供其它第三方程序使用
导出表支持符号导出,序号导出,这两种方式可以共存
导出表位于数据目录表中第0选项(默认我们以数组下标0开始数)
3.4导出表的结构体描述
typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics; // (1) 保留,恒为0x00000000
DWORD TimeDateStamp; // (2) 时间戳
WORD MajorVersion; // (3) 主版本号,一般不赋值
WORD MinorVersion; // (4) 子版本号,一般不赋值
DWORD Name; // (5) 模块名称*
DWORD Base; // (6) 序号表中序号的基数*
DWORD NumberOfFunctions; // (7) 导出地址表中成员个数,也是导出的函数总数*
DWORD NumberOfNames; // (8) 导出名称表中成员个数,和序号表总数量一致*
DWORD AddressOfFunctions; // (9) 导出地址表(EAT)RVA*
DWORD AddressOfNames; // (10) 导出名称表(ENT)RVA*
DWORD AddressOfNameOrdinals; // (11) 指向导出函数的序号表RVA* }IMAGE_EXPORT_DIRECTORY,*PIMAGE_EXPORT_DIRECTORY;
3.5 导出表中的导出函数地址表(EAT),序号表(EOT),函数名称表(ENT)的关系如下图:
导出函数地址表中记录了所有导出函数的地址,下标就是导出函数的序号,导出函数地址表中,地址为0说明这是无效的函数地址,将导出函数地址表中的下标与序号表中的序号进行对比,能找到对应序号说明此函数是名称导出的,否则就是序号导出的,此时函数地址表中的下标就是这个函数的虚序号,序号导出的函数没有函数名,名称导出的可以根据序号表中的下标在名称表中找到对应下标,就可以找到函数名称。序号表和名称表下标一一对应关系。

手动解析导出表:
下图中就是导出表对应的IMAGE_EXPORT_DIRECTORY结构体,描述了一个dll中导出的函数。
注意下图中的base值,序号表中的序号需要加上Base值才是真正的序号。

3.6 使用代码解析导出表:
char * ReadPe(const TCHAR * szPath)
{
//1.打开一个文件
HANDLE hFile = CreateFile(
szPath, //打开的文件名
GENERIC_READ, //读方式打开
FILE_SHARE_READ,//共享方式
NULL, //安全属性
OPEN_EXISTING, //创建标志
FILE_ATTRIBUTE_NORMAL, //属性
NULL);
//2. 读取到内存中
//2.1获取文件大小
DWORD dwSize;
dwSize = GetFileSize(hFile, 0);
//2.2 开辟空间
m_pBuff= new char[dwSize];
//2.3 读取文件
DWORD dwReadSize;
ReadFile(hFile, m_pBuff, dwSize, &dwReadSize, 0);
//3.关闭文件
CloseHandle(hFile);
return m_pBuff;
}
//RVA 转 FOA 函数
DWORD RvaToOffset(char * pbuff, DWORD RVA) {
//1. 遍历判断RVA落在哪个区段
//2. 计算FOA = RVA - VOffset + ROffset;
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)pbuff;
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(pDos->e_lfanew + (DWORD)pbuff);
// 区段头位=pNT+4+sizeof(IMAGE_FILE_HEADERS)+sizeof(IMAGE_OPTIONAL_HEADERS)
PIMAGE_SECTION_HEADER pSection = IMAGE_FIRST_SECTION(pNt);
for (int i = 0; i < pNt->FileHeader.NumberOfSections; i++) {
//判断落在什么区段
if (RVA >= pSection[i].VirtualAddress && RVA < (pSection[i].VirtualAddress + pSection[i].SizeOfRawData))
{
//返回计算后的FOA
return RVA - pSection[i].VirtualAddress + pSection[i].PointerToRawData;
}
}return -1;
}
// GetExportDirectory,获取导出表地址
PIMAGE_EXPORT_DIRECTORY GetExprotDirectory()
{
//获取导出表
//扩展头中的数据目录表第0项
DWORD dwExprotRva =
GetNtHeader()->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress;
//转换FOA
DWORD dwExprotFoa = RvaToFoa(dwExprotRva)+ (DWORD)m_pBuff;
//返回导出表
return (PIMAGE_EXPORT_DIRECTORY)(dwExprotFoa);
}
// 前提,假设PE文件已经读取到内存中,且文件在内存中的起始地址为m_pbuff,
// 解析PE文件中导出表的数据
void ShowExportTable() {
PIMAGE_EXPORT_DIRECTORY p = GetExportDirectory();
//输出导出表信息
char * dllName = (char*)(RVAToFoa(p->Name) + (DWORD)m_pbuff);
printf("dll名 %s\n", dllName);
printf("导出函数地址表个数 %2d\n", p->NumberOfFunctions);
printf("导出函数名称表个数 %2d\n", p->NumberOfFunctions);
//遍历导出函数
//导出序号表
WORD *EOT = (WORD*)(RVAToFoa(p->AddressOfNameOrdinals) + (DWORD)m_pbuff);
//导出名称表
DWORD *pENT = (DWORD*)(RVAToFoa(p->AddressOfNames) + (DWORD)m_pbuff);
//导出序号表
DWORD *pEAT = (DWORD*)(RVAToFoa(p->AddressOfFunctions) + (DWORD)m_pbuff);
//遍历地址表所有函数
for (int i = 0; i < p->NumberOfFunctions; i++) {
printf("序号%d ", i + p->Base);
int j = 0;
//遍历所有序号表,找到有名字的函数
for (; j < p->NumberOfNames; j++) {
if (i == EOT[j]) {
// 导出名称表[ 序号表[j] ]
int RvaName = pENT[j];
char *szName = (char*)(RVAToFoa(RvaName) + (DWORD)m_pbuff);
printf("函数名 %s ", szName);
break;
}
}
//没有找到函数名
if (j > p->NumberOfNames) {
printf("函数名 NULL ");
}
//打印导出函数地址
printf("%08X\n", *(DWORD*)(RVAToFoa(pEAT[i]) + (DWORD)m_pbuff));
}
}
4.导入表
4.1 导入表的作用:
导入表是PE文件从其它第三方程序中导入API,以供本程序调用的机制(与导出表对应)
在exe运行起来的时候, 加载器会遍历导入表, 将导入表中所有dll 都加载到进程中,被加载的DLL的DllMain就会被调用
通过导入表可以知道程序使用了哪些函数
当然有写程序可以没有导入表,自己实现动态加载功能
导入表结构, 这个导入表是一个数组,以全为零结尾
注意:一个PE文件可能需要多个PE文件的支持,所以导入表结构体也对应多个,构成一个数组。数组中的每一个结构体描述需要载入的一个dll中的导入函数。
4.2 导入表的结构体描述
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics;
DWORD OriginalFirstThunk; //(1) 指向导入名称表(INT)的RAV*
};
DWORD TimeDateStamp; // (2) 时间标识
DWORD ForwarderChain; // (3) 转发链,如果不转发则此值为0
DWORD Name; // (4) 指向导入映像文件的名字*
DWORD FirstThunk; // (5) 指向导入地址表(IAT)的RAV*
} IMAGE_IMPORT_DESCRIPTOR;
注意:
1.结构体中的OriginalFirstThunk与FirstThunk使用的是同一个结构IMAGE_THUNK_DATA32,但是在内存中是不同的两块区域。
2.在磁盘文件中,OriginalFirstThunk与FirstThunk中的数据是相同的,可以将输入名称表(INT)看做是输入地址表(IAT)的一个备份,其中内容都是函数的名字或者是序号。在加载到内存中后,加载器会把相应的PE文件中的函数地址写入到IAT中。
IAT与INT的结构体IMAGE_THUNK_DATA32
typedef struct _IMAGE_THUNK_DATA32 {
union {
PBYTE ForwarderString; // (1) 转发字符串的RAV
PDWORD Function; // (2) 被导入函数的地址,PE载入内存后IAT中有用
DWORD Ordinal; // (3) 被导入函数的序号
PIMAGE_IMPORT_BY_NAME AddressOfData; // (4) 指向输入名称表
} u1;
} IMAGE_THUNK_DATA32;
IMAGE_THUNK_DATA32中值的最高位为1,表示序号导入,Ordinal
IMAGE_THUNK_DATA32中值的最高位为0,表示函数名导入,AddressOfData字段有效
PIMAGE_IMPORT_BY_NAME 指向导入函数序号与函数名 。
注意:
1.在磁盘文件中,只有后面两个字段有用。
2.这个结构占4个字节,最高位为1,表示序号导入,序号 Ordinal起作用,如果最高位为0,表示函数名导入,AddressOfData起作用,指向一个结构体IAMGE_IMPORT_BY_NAME,结构体中描述了导入函数的名字和序号。
typedef struct _IMAGE_IMPORT_BY_NAME {
WORD Hint; // (1) 需导入的函数序号
BYTE Name[1]; // (2) 需导入的函数名称
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;
IAT与INT的关系:

4.3 代码解析导入表
假设PE文件已经读取到内存中,并且PE在内存中的地址为m_pbuff.
void ShowImportTable()
{
//显示导入表信息
//1.获取到表信息
PIMAGE_IMPORT_DESCRIPTOR pImprot = GetImportDirectory();
//2.遍历导入表信息,导入表是一个全为零为结尾
while (pImprot->FirstThunk != NULL)
{
//3.输出导入表信息
char *Dllname = (char*)(RVAToFoa(pImprot->Name) + m_pbuff); printf("%s\n", Dllname);
//3.1 显示导入地址表
PIMAGE_THUNK_DATA pIat = (PIMAGE_THUNK_DATA)(RVAToFoa(pImprot->FirstThunk) + m_pbuff);
//3.2 遍历导出地址表 ,也是一个全为0结尾
while (pIat->u1.Ordinal!=0)
{
//函数导出方式: 名称导出,序号导出
//最高位为1,表示序号导入
//最高位为0,表示名称导入
// 使用宏IMAGE_SNAP_BY_ORDINAL判断最高位是否为1,序号导出
if (IMAGE_SNAP_BY_ORDINAL(pIat->u1.Ordinal)) {
// 取出序号
printf("序号 :%d \n", pIat->u1.Ordinal & 0x0000FFFF);
}else
{
PIMAGE_IMPORT_BY_NAME pName = (PIMAGE_IMPORT_BY_NAME)(RVAToFoa(pIat- >u1.Function)+ m_pbuff);
printf("序号 :%d %s\n", pName->Hint, pName->Name);
}
//找到下一个导入名称表
pIat++;
}
//遍历下一个导入表
pImprot++;
}
}
- 资源表数据
5.1 概述:
-
windows的资源有菜单、图标、快捷键、版本信息以及其它未格式化的二进制资源比如菜单、图标、快捷键、版本信息以及其它未格式化的二进制资源。
-
资源由三层一样的结构体组成
- 第一层:资源的类型是什么(图标,位图,菜单…)
- 第二次:资源的叫什么名字 (1.png, 2.png)
- 第三层:资源的语言,资源的信息(大小,文件中位置)
5.2 资源层次关系的图形表示:

5.2 资源表的结构体描述
资源目录结构体
typedef struct _IMAGE_RESOURCE_DIRECTORY {
DWORD Characteristics; // (1) 资源属性标识
DWORD TimeDateStamp; // (2) 资源建立的时间
WORD MajorVersion; // (3) 资源主版本
WORD MinorVersion; // (4) 资源子版本
WORD NumberOfNamedEntries; // (5) 资源名称条目个数 *
WORD NumberOfIdEntries; // (6) 资源ID条目个数 *
// IMAGE_RESOURCE_DIRECTORY_ENTRY DirectoryEntries[] 资源实体数组
} IMAGE_RESOURCE_DIRECTORY, *PIMAGE_RESOURCE_DIRECTORY;
// 字段5和字段6决定了总共有多少种(个)资源
资源实体结构体
typedef struct _IMAGE_RESOURCE_DIRECTORY_ENTRY {
union {
struct {
DWORD NameOffset :31; // (1) 资源名偏移
DWORD NameIsString:1; // (2) 资源名为字符串,值为1,资源名为字符串,为0说明资源是已知类型,使用Id作为资源标识符
};
DWORD Name; // (3) 资源/语言类型
WORD Id; // (4) 资源数字ID
};
union {
DWORD OffsetToData; // (5) 数据偏移地址,第三层或第二层中指向最终的资源数据,指向IMAGE_RESOURCE_DATA_ENTRY,此时DataIsDirectory等于0
struct {
DWORD OffsetToDirectory:31; // (6) 子目录偏移地址
DWORD DataIsDirectory :1; // (7) 数据为目录,值为1,说明还有下层内容,据需解析,直到值为0
};
};
}IMAGE_RESOURCE_DIRECTORY_ENTRY,*PIMAGE_RESOURCE_DIRECTORY_ENTRY;
当资源的名字为字符时(通常是自定义的资源),即NameIsString值为1,NameOffset指向这样一个结构体,IMAGE_RESOURCE_DIR_STRING_U.里边存储资源的名字字符串。
typedef struct _IMAGE_RESOURCE_DIR_STRING_U {
WORD Length; // (1) 字符串长度
WCHAR NameString[ 1 ]; // (2) 字符串数组
} IMAGE_RESOURCE_DIR_STRING_U,*PIMAGE_RESOURCE_DIR_STRING_U;
当资源以ID作为标识符的时候,通常是一些已知的资源类型:
如下表所示:
| 值 | 资源类型 | 值 | 资源类型 |
|---|---|---|---|
| 0x00000001 | 鼠标指针 | 0x00000008 | 字体 |
| 0x00000002 | 位图 | 0x00000009 | 快捷键 |
| 0x00000003 | 图标 | 0x0000000A | 非格式化资源 |
| 0x00000004 | 菜单 | 0x0000000B | 消息列表 |
| 0x00000005 | 对话框 | 0x0000000C | 鼠标指针 |
| 0x00000006 | 字符串列表 | 0x0000000E | 图标组 |
| 0x00000007 | 字体目录 | 0x00000010 | 版本信息 |
当IMAGE_RESOURCE_DIRECTORY_ENTRY结构体中的DataIsDirectory为0的时候,说明了此时OffsetToData指向了一个具体的资源结构体IMAGE_RESOURCE_DATA_ENTRY。
typedef struct _IMAGE_RESOURCE_DATA_ENTRY {
DWORD OffsetToData; // (1) 资源数据的RVA ,指向真正的资源数据
DWORD Size; // (2) 资源数据的长度
DWORD CodePage; // (3) 代码页
DWORD Reserved; // (4) 保留字段
} IMAGE_RESOURCE_DATA_ENTRY, *PIMAGE_RESOURCE_DATA_ENTRY;
注意:
1.一般情况下,资源会有三层,但有时候也存在只有两层的情况,在第二层中DataIsDirectory就等于0.
2.NameOffset,OffsetToDirectory与OffsetToData这三个RVA是相对于资源表最开始的位置的偏移,不是imageBase,使用OffsetToData找文件中具体资源数据时要转换成FOA。
5.3 代码解析资源表
void ShowResourceInfo() {
//1. 获取资源目录表
PIMAGE_RESOURCE_DIRECTORY pResourceOne = GetResourceDirectory();
//获取资源数组项
PIMAGE_RESOURCE_DIRECTORY_ENTRY pResouceOneEntry = (PIMAGE_RESOURCE_DIRECTORY_ENTRY)(pResourceOne + 1);
//1.2 遍历所有资源类型
//资源总个数
DWORD dwResourceNumber = pResourceOne->NumberOfIdEntries + pResourceOne- >NumberOfNamedEntries;
for (int i = 0; i < dwResourceNumber; i++)
{
//1.3 判断资源类型,是数字还是字符串
if (pResouceOneEntry[i].NameIsString)
{
//资源ID是字符串
//那么NameOffset 有效(基于资源表的偏移)
PIMAGE_RESOURCE_DIR_STRING_U szName = (PIMAGE_RESOURCE_DIR_STRING_U)(pResouceOneEntry[i].NameOffset + (DWORD)pResourceOne);
TCHAR szBuff[100];
wcsncpy_s(szBuff, szName->NameString, szName->Length); printf("%S\n", szBuff);
}else {
//资源ID是数字
// 系统定义的 0 - 16
if (pResouceOneEntry[i].Id < 16)
{
wprintf(L"%s\n", RES[pResouceOneEntry[i].Id]);
}
// 自定义的
else {
printf("%02d\n", pResouceOneEntry[i].Id);
}
}
//2 解析第二层数据
//2.1 是否有第二层数据
if (pResouceOneEntry[i].DataIsDirectory)
{
//2.2 获取第二层资源表
PIMAGE_RESOURCE_DIRECTORY pResourceTwo = (PIMAGE_RESOURCE_DIRECTORY)(pResouceOneEntry[i].OffsetToDirectory + (DWORD)pResourceOne);
//获取资源数组项
PIMAGE_RESOURCE_DIRECTORY_ENTRY pResouceTwoEntry = (PIMAGE_RESOURCE_DIRECTORY_ENTRY)(pResourceTwo + 1);
//这种资源有多少个
DWORD dwNumber2 = pResourceTwo->NumberOfIdEntries + pResourceTwo- >NumberOfNamedEntries;
//2.3 遍历资源
for (int i = 0; i < dwNumber2; i++)
{
//资源名字是 数字,还是字符串
if (pResouceTwoEntry[i].NameIsString)
{
PIMAGE_RESOURCE_DIR_STRING_U szName = (PIMAGE_RESOURCE_DIR_STRING_U)(pResouceTwoEntry[i].NameOffset + (DWORD)pResourceOne);
TCHAR szBuff[100];
wcsncpy_s(szBuff, szName->NameString, szName->Length); printf(" %S\n", szBuff);
}else
{
printf(" %02d\n", pResouceTwoEntry[i].Id);
}
//3. 解析第3层
//3.1 是否有第三层数据
if (pResouceTwoEntry[i].DataIsDirectory)
{
//3.2 获取第三层资源表
PIMAGE_RESOURCE_DIRECTORY pResourceThree = (PIMAGE_RESOURCE_DIRECTORY)(pResouceTwoEntry[i].OffsetToDirectory + (DWORD)pResourceOne);PIMAGE_RESOURCE_DIRECTORY_ENTRY pResourceThreadEntry = (PIMAGE_RESOURCE_DIRECTORY_ENTRY)(pResourceThree + 1);
//第三层 执行真正数据
PIMAGE_RESOURCE_DATA_ENTRY pResourceData = (PIMAGE_RESOURCE_DATA_ENTRY)(pResourceThreadEntry->OffsetToData + (DWORD)pResourceOne);
printf(" 资源位置 %08X,资源大小 %02d\n", pResourceData- >OffsetToData, pResourceData->Size);
}
}
}
}
}
6.重定位表
在PE文件中,有数据段和代码段,代码段存储代码,而数据段中存放全局变量和静态变量,代码段中读写这些变量使用的是VA,VA是链接器提前根据默认的加载基址ImageBase算好的,exe的默认加载基址是0x400000,dll的默认加载基址是0x10000000,但是dll或者exe在开启随机基址的情况下,并不一定加载到默认的加载基址上,所以使用VA来索引数据就会出错,需要重新定位数据,PE中的重定位表就记录了代码段中需要重新定位数据的代码位置。在PE没有加载到默认的加载基址候,操作系统PE加载器使用此表对数据重定位。
重定位公式:实际加载基址 - 默认加载基址 + 目标数据VA
重定位表在数据目录表下标为5的位置。
TypeOffffset的元素个数 = (SizeOfBlock - 8 )/ 2
6.1 重定位表的结构体描述
一个结构体描述内存中一个分页(4KB)访问内所需要重定位的条目,一个区段中会有很多分页,所以有多个结构体。
typedef struct _IMAGE_BASE_RELOCATION {
DWORD VirtualAddress; // (1) 需重定位数据的起始RVA *
DWORD SizeOfBlock; // (2) 本结构与TypeOffset总大小 *
WORD TypeOffset[1]; // (3) 原则上不属于本结构 *
} IMAGE_BASE_RELOCATION; typedef IMAGE_BASE_RELOCATION UNALIGNED IMAGE_BASE_RELOCATION;
定义一个描述一个分页中需要重定位条目的结构体
struct {
WORD Offset:12; // (1) 大小为12Bit的重定位偏移
WORD Type :4; // (2) 大小为4Bit的重定位信息类型值 ,通常属性为0x3是需要重定位,4个字节需要修改
}TypeOffset;
TypeOffset中Offset与IMAGE_BASE_RELOCATION可以计算出需要重定位的代码位置:

6.2 使用代码解析重定位表中的数据:
假设PE文件已经读取到内存中,并且PE文件在内存中的地址是m_pbuff。
void ShowBaseRelocation() {
//每一个重定位数据都是一个结构体,记录类型,与偏移
typedef struct TYPEOFFSET {
WORD Offset : 12; //一页种的偏移
WORD Type : 4; //重定位数据类型,3表示这个数据需要重定位
}*PTYPEOFFSET;
//1. 获取重定位表
PIMAGE_BASE_RELOCATION pRelocation = GetBaseRelocaltion();
//2. 遍历重定位 //重定位表是以一项全为零结尾的数
while (pRelocation->SizeOfBlock != 0)
{
//3.遍历重定位项
//重定位项是以0x1000页为一块,每一块负责一页
//每一页有有多少块 (sizeblock - 8) /2
DWORD dwCount = (pRelocation->SizeOfBlock - 8) / 2;
for (int i = 0; i < dwCount; i++)
{
PTYPEOFFSET pBlock = (PTYPEOFFSET)(pRelocation + 1);
//这个数据需要重定位吗?
if (pBlock->Type == 3)
{
//需要重定位数据的位置 (RVA)
DWORD RvaOffset = pRelocation->VirtualAddress + pBlock[i].Offset;
//文件种的位置 (FOA)
DWORD FoaOffset = RVAToFoa(RvaOffset);
//需要重定位的数据据是
DWORD Data = *(DWORD*)(FoaOffset + m_pbuff);
//显示重定位数据在内存中位置
//VA = Rva+ imagebase
printf("%08x:[%08x]\n", RvaOffset + GetNtHeader()->OptionalHeader.ImageBase, Data );
}
}
//找到下一个重定位表
pRelocation = (PIMAGE_BASE_RELOCATION)((DWORD)pRelocation + pRelocation->SizeOfBlock);
}
}
LordPE 中,PE文件的重定位表:要从重定位表中现如下结构的数据:

可以定义下面的结构体:
由于区段中所需要重定位的区域和区域中需要虫定位的条目是不确定的,所以使用vector来存储区域信息和条目信息,条目信息又放入了区段信息结构体中。
结构体关系如下:
定义如下结构,然后从重定位表中获取对应的数据填入到结构体中,然后将结构体中的数据展示,就完成了LordPE中的重定位解析的功能:
// 条目结构体
typdef struct _RELOCINFO
{
DWORD dwRelocRVA; // 需要重定位的相对虚拟地址RVA
DWORD dwOffect; // 根据RVA计算的文件偏移FOA
BYTE bType; // 重定位方式(也叫做属性)
DWORD dwRelocValue; // 从算出的文件偏移取出的数据,即需要重定位的VA
BYTE bData[10] // 使用刚才的地址的VA,减去基址得到相对虚拟地址(RVA),在计算出FOA,取出的数据(即静态变量或者全局变量中的数据)放到这个数组中
} RELOCINFO, *PRELOCINFO; // 一个需要重定位条目的信息
// 一个区域中的重定位信息
typedef struct _RELOCAREAINFO
{
CHAR szSectionName[8]; // 区域所在的节名
DWORD dwAreaRVA; // 区域的基址(是一个RVA)
DWORD NumberOfReloc; // 这个区域需要重定位的歌声
std::vector<RECLOCINFO> vecRelocInfo;// 这个区域冲抵等国内外的具体信息
} RELOCAREAINFO, *PRELOCAREAINFO; // 这个结构体描述的是一个区域的重定位信息
// 再定位一个针对第三个成员的TypeOffset的数据结构
struct _TYPEOFFSET
{
WORD offset:12; // 偏移值
WORD Type:4; // 重定位属性
} TYPEOFFECT, *PTYEOFFECT;
7.TLS表:
7.1 TLS概要:
TLS(Thread Local Storage),叫做线程局部存储,是windows为了解决一个进程中多个线程同时访问全局变量而提供的一种机制,这种机制使得全局变量不需要我们自己加锁控制。
TLS的工作原理:线程中的局部变量不用考虑线程同步的问题,因为局部变量保存在栈中,每个线程都有自己独立的栈空间,而对于全局变量来说,要实现线程同步需要同步控制,这样做开销昂贵,但是使用TLS,也可以实现线程同步,TLS在操作系统的支持下,将全局变量打包到一个特殊的节,tls节中,每次创建线程的时候,从这个节中拷贝全局变量到自己线程空间中,从而像访问局部变量一样,不会影响原始的全局变量。
TLS的用途:
TLS用于抢占式
TLS正常用法:定义一个在本线程内的全局变量,在TLS回调函数中,可以对应全局变量做初始化操作。
TLS用于反调试:将反调试代码写在TLS回调函数中,使得更难被调试器发现。
动态TLS线程局部存储:
TlsGetVallue
TlsAlloc
TlsFree
TlsSetValue
7.2 TLS使用实例:
#pragma comment(linker, "/INCLUDE:__tls_used")
// TLS变量
__declspec (thread) int g_nNum = 0x11111111;
__declspec (thread) char g_szStr[] = "TLS g_nNum = 0x%p ...\r\n";
//当有线程访问tls变量时,该线程会复制一份tls变量到自己tls空间
//线程只能修改自己的空间tls变量,不会修改到全局变量
// TLS回调函数A v
void NTAPI t_TlsCallBack_A(PVOID DllHandle, DWORD Reason, PVOID Red) {
if (DLL_THREAD_DETACH == Reason) // 如果线程退出则打印信息 printf("t_TlsCallBack_A -> ThreadDetach!\r\n");
return;
}
// TLS回调函数B
void NTAPI t_TlsCallBack_B(PVOID DllHandle, DWORD Reason, PVOID Red) {
if (DLL_THREAD_DETACH == Reason) // 如果线程退出则打印信息 printf("t_TlsCallBack_B -> ThreadDetach!\r\n");
/* Reason 什么事件触发的
DLL_PROCESS_ATTACH 1
DLL_THREAD_ATTACH 2
DLL_THREAD_DETACH 3
DLL_PROCESS_DETACH 0 */
return;
}
/** 注册TLS回调函数,".CRT$XLB"的含义是:
* CRT表明使用C RunTime机制
* X表示标识名随机
* L表示TLS callback section
* B其实也可以为B-Y的任意一个字母 */
#pragma data_seg(".CRT$XLB")
PIMAGE_TLS_CALLBACK p_thread_callback[] = {
t_TlsCallBack_A,
t_TlsCallBack_B, NULL
};
#pragma data_seg()
DWORD WINAPI t_ThreadFun(PVOID pParam) {
printf("t_Thread -> first printf:");
printf(g_szStr, g_nNum); g_nNum = 0x22222222; // 注意这里
printf("t_Thread -> second printf:");
printf(g_szStr, g_nNum);
return 0;
}
int _tmain(int argc, _TCHAR* argv[]) {
printf("_tmain -> TlsDemo.exe is runing...\r\n\r\n");
CreateThread(NULL, 0, t_ThreadFun, NULL, 0, 0);
Sleep(100); // 睡眠100毫秒用于确保第一个线程执行完毕
printf("\r\n");
CreateThread(NULL, 0, t_ThreadFun, NULL, 0, 0); system("pause");
return 0;
}
7.3 TLS 表的结构体描述
TLS表位于数据目录表下标为9的元素
typedef struct _IMAGE_TLS_DIRECTORY32 {
DWORD StartAddressOfRawData; //源数据的起始地址 * (VA)在.tls节
DWORD EndAddressOfRawData; //源数据的结束地址 * (VA)在.tls节
DWORD AddressOfIndex; // TLS数据索引
DWORD AddressOfCallBacks; // TLS回调函数地址表位置(VA)在.rdata中 *
DWORD SizeOfZeroFill; //0填充区域的字节数
union {
DWORD Characteristics; //保留
struct {
DWORD Reserved0 : 20;
DWORD Alignment : 4;
DWORD Reserved1 : 8;
} DUMMYSTRUCTNAME;
} DUMMYUNIONNAME;
} IMAGE_TLS_DIRECTORY32;
8.延迟加载表与DLL延迟加载
8.1 概述
- 延迟加载机制为了提高进程加载效率的技术
- 延迟加载机制没有对dll任何特殊要求,也就是说任意的一个DLL都可以被延迟加载
8.2 DLL延迟加载的使用
- 包含必要的头文件及静态库
#include <windows.h>
#include <delayimp.h>
#pragma comment(lib, "Delayimp.lib")
-
设置“连接器”>“输入”>“延迟加载的DLL”选项中的值为我们需要延迟加载的DLL名称(大小写必须完全一致
-
如果我们需要用到卸载功能,需要设置“连接器”>“高级”>“卸载延迟加载的DLL”
-
可以直接使用延迟加载的函数,当调用这个函数才会加载这个dll
-
DLL延迟加载机制的核心就是delayLoadHelper()函数,对延迟加载函数的调用实际上是对delayLoadHelper()函 数的调用,该函数引用特殊的Delay Import节,并且会在其内部自动调用LoadLibrary()之后再调用 GetProcAddress()以获取函数地址
8.4 注意事项:
- 手动卸载dll时,需要通过__FUnloadDelayLoadedDLL2(“xxx.dll”)卸载,函数参数中不能写路径名,只允许写单纯的DLL文件名。而且延迟加载的DLL不允许使用FreeLibray函数卸载,只能_用_FUnloadDelayLoadedDLL2函数释放,用否则发生异常。
8.5 延迟加载表的结构
-
延迟加载表位于数据目录表下标为13的元素中
typedef struct _IMAGE_DELAYLOAD_DESCRIPTOR { DWORD AllAttributes; //1(没用)为0 DWORD DllNameRVA; //2(有用)延迟加载的 Dll名字 DWORD ModuleHandleRVA; //3(有用) DWORD ImportAddressTableRVA; //4(重要)延迟载入IAT的RVA (函数地址(VA)) DWORD ImportNameTableRVA; //5(重要) 延迟载入INT的RVA(PIMAGE_THUNK_DAT DWORD BoundImportAddressTableRVA;//6(有用)绑定IAT的RVA DWORD UnloadInformationTableRVA; //7 卸载函数 DWORD TimeDateStamp; //8() if not bound }IMAGE_DELAYLOAD_DESCRIPTOR, *PIMAGE_DELAYLOAD_DESCRIPTOR;延迟载入表的代码解析思路和其他表一样,找nt头,扩展头,数据目录表13项,根据RVA找到这个结构体,解析数据就行,这里省略代码。
到此PE结构的详细解析就完成了,其中的重要数据结构和含义也都有了清晰的认识。

2555

被折叠的 条评论
为什么被折叠?



