打造自己的PE解释器
本文为看雪论坛精华文章
看雪论坛作者ID:QiuJYu
贯穿全文的概念
第一章:打造自己的PE解释器——PE文件头结构
本章目的:通过简单的讲解PE文件头结构及其基本概念,让刚开始学习PE的同学基本了解PE文件头的结构和相关理论知识,并通过学到的知识获取PE文件头的结构信息。
重点掌握:
(1) IMAGE_DOS_HEADER
(2) IMAGE_NT_HEADERS32
(3) IMAGE_FILE_HEADER
(4) IMAGE_OPTIONAL_HEADER32
(5) IMAGE_SECTION_HEADER
第一节:PE文件结构
PE文件头保存着整个PE文件的索引信息,可以帮助PE装载器定位资源,而节则保存着整个PE文件的所有资源。
第二节:IMAGE_DOS_HEADER
WORD e_magic; **重要成员 相对该结构的偏移0x00**
WORD e_cblp;
WORD e_cp;
WORD e_crlc;
WORD e_cparhdr;
WORD e_minalloc;
WORD e_maxalloc;
WORD e_ss;
WORD e_sp;
WORD e_csum;
WORD e_ip;
WORD e_cs;
WORD e_lfarlc;
WORD e_ovno;
WORD e_res[4];
WORD e_oemid;
WORD e_oeminfo;
WORD e_res2[10];
LONG e_lfanew; **重要成员 相对该结构的偏移0x3C**
} IMAGE_DOS-HEADER, *PIMAGE_DOS_HEADER;
第三节:IMAGE_DOS_STUB (了解)
第四节:IMAGE_NT_HEADERS32
DWORD Signature; **重要成员 PE签名 相对该结构的偏移0x00**
IMAGE_FILE_HEADER FileHeader; **重要成员 结构体 相对该结构的偏移0x04**
IMAGE_OPTIONAL_HEADER32 OptionalHeader; **重要成员 结构体 相对该结构的偏移0x18**
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
Signature也称作PE签名,这个成员和DOS头的MZ标记一样都是一个PE文件的标准特征,只不过这个成员是DWORD类型大小为4字节。
第五节:IMAGE_FILE_HEADER
WORD Machine; ** 机器号 相对该结构的偏移0x00**
WORD NumberOfSections; **重要成员 节区数量 相对该结构的偏移0x02**
DWORD TimeDateStamp; ** 时间戳 相对该结构的偏移0x04**
DWORD PointerToSymbolTable; ** 符号表偏移 相对该结构的偏移0x08**
DWORD NumberOfSymbols; ** 符号表数量 相对该结构的偏移0x0C**
WORD SizeOfOptionalHeader; **重要成员 可选头大小 相对该结构的偏移0x10**
WORD Characteristics; **重要成员 PE文件属性 相对该结构的偏移0x12**
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
秒:0x5CFBB225 % 60 = 33
分:0x5CFBB225 / 60 % 60 = 3
时:0x5CFBB225 / 3600 % 24 = 13
日:0x5CFBB225 / 3600 / 24 - (365 * 49 + 12) - 151 + 1 = 8
月:(0x5CFBB225 / 3600 / 24 - (365 * 49 + 12)) / 30 + 1 = 6
年:0x5CFBB225 / 3600 / 24 / 365 + 1970 = 2019
结果为:2019年6月8日 13:03:33
Characteristics,它描述了PE文件的一些属性信息,比如是否可执行,是否是一个动态连接库等。
第六节:IMAGE_OPTIONAL_HEADER32
WORD Magic; **魔术字 偏移0x00
BYTE MajorLinkerVersion; **链接器主版本 偏移0x02
BYTE MinorLinkerVersion; **链接器副版本 偏移0x03
DWORD SizeOfCode; **所有含代码的节的总大小 偏移0x04
DWORD SizeOfInitializedData; **所有含初始数据的节的总大小 偏移0x08
DWORD SizeOfUninitializedData; **所有含未初始数据的节的总大小 偏移0x0C
DWORD AddressOfEntryPoint; **程序执行入口地址 偏移0x10 重要
DWORD BaseOfCode; **代码节的起始地址 偏移0x14
DWORD BaseOfData; **数据节的起始地址 偏移0x18
DWORD ImageBase; **程序首选装载地址 偏移0x1C 重要
DWORD SectionAlignment; **内存中节区对齐大小 偏移0x20 重要
DWORD FileAlignment; **文件中节区对齐大小 偏移0x24 重要
WORD MajorOperatingSystemVersion; **操作系统的主版本号 偏移0x28
WORD MinorOperatingSystemVersion; **操作系统的副版本号 偏移0x2A
WORD MajorImageVersion; **镜像的主版本号 偏移0x2C
WORD MinorImageVersion; **镜像的副版本号 偏移0x2E
WORD MajorSubsystemVersion; **子系统的主版本号 偏移0x30
WORD MinorSubsystemVersion; **子系统的副版本号 偏移0x32
DWORD Win32VersionValue; **保留,必须为0 偏移0x34
DWORD SizeOfImage; **镜像大小 偏移0x38 重要
DWORD SizeOfHeaders; **PE头大小 偏移0x3C 重要
DWORD CheckSum; **校验和 偏移0x40
WORD Subsystem; **子系统类型 偏移0x44
WORD DllCharacteristics; **DLL文件特征 偏移0x46
DWORD SizeOfStackReserve; **栈的保留大小 偏移0x48
DWORD SizeOfStackCommit; **栈的提交大小 偏移0x4C
DWORD SizeOfHeapReserve; **堆的保留大小 偏移0x50
DWORD SizeOfHeapCommit; **堆的提交大小 偏移0x54
DWORD LoaderFlags; **保留,必须为0 偏移0x58
DWORD NumberOfRvaAndSizes; **数据目录的项数 偏移0x5C
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
Magic
这个无符号整数指出了镜像文件的状态,此成员可以是以下的值:
该成员保存着文件被执行时的入口地址,它是一个RVA。如果想要在一个可执行文件中附加了一段代码并且要让这段代码首先被执行,就可以通过更改入口地址到目标代码上,然后再跳转回原有的入口地址。
该成员指定了文件被执行时优先被装入的地址,如果这个地址已经被占用,那么程序装载器就会将它载入其他地址。
对于EXE文件来说,由于每个文件总是使用独立的虚拟地址空间,优先装入地址不可能被其他模块占据,所以EXE总是能够按照这个地址装入,这也意味着EXE文件不再需要重定位信息。
该成员指定了文件被装入内存时,节区的对齐单位。节区被装入内存的虚拟地址必须是该成员的整数倍,以字节为单位,并且该成员的值必须大于等于FileAlignment的值。该成员的默认大小为系统的页面大小。
FileAlignment
该成员指定了文件在硬盘上时,节区的对齐单位。节区在硬盘上的地址必须是该成员的整数倍,以字节为单位,并且该成员的值必须大于等于FileAlignment的值。
该成员指定了文件载入内存后的总体大小,包含所有的头部信息。并且它的值必须是SectionAlignment的整数倍。
SizeOfHeaders
该成员指定了PE文件头的大小,并且向上舍入为FileAlignment的倍数,值的计算方式为:
sizeof(IMAGE_FILE_HEADER) +
SizeOfOptionalHeader + /*NT头*/
sizeof(IMAGE_SECTION_HEADER) * NumberOfSections) / /*节表*/
FileAlignment *
FileAlignment +
FileAlignment; /*向上舍入 一般该结果不可能是FileAlignment的整数倍,所以直接加上FileAlignment还是没问题的 */
该成员指定了可选头中目录项的具体数目,由于以前发行的Windows NT的原因,它只能为10h。
第七节:IMAGE_SECTION_HEADER
这是一个8字节的ASCII字符串,长度不足8字节时用0x00填充,该名称并不遵守必须以"\0"结尾的规律,如果不是以"\0"结尾,系统会截取8个字节的长度进行处理。
Misc.VirtualSize
这个成员在一个共用体中,这个共用体中还有另外一个成员,由于用处不大我们就不讲解了,主要讲解VirtualSize的含义。
VirtualAddress
指定了该节区装入内存虚拟空间后的地址,这个地址是一个相对虚拟地址(RVA),它的值一般是SectionAlignment的整数倍。它加上ImageBase后才是真正的虚拟地址。
SizeOfRawData
指定了该节区在硬盘上初始化数据的大小,以字节为单位。它的值必须是FileAlignment的整数倍,如果小于Misc.VirtualSize,那么该部分的其余部分将用0x00填充。如果该部分仅包含未初始化的数据,那么这个值将会为零。
PointerToRawData
指出零该节区在硬盘文件中的地址,这个数值是从文件头开始算起的偏移量,也就是说这个地址是一个文件偏移地址(FOA)。它的值必须是FileAlignment的整数倍。如果这个部分仅包含未初始化的数据,则将此成员设置为零。
Characteristics
该成员指出了该节区的属性特征。其中的不同数据位代表了不同的属性,这些数据位组合起来就是这个节的属性特征,具体数值定义如下:
第八节:章节练习
2. 通过编写控制台程序,将一个EXE文件读取到内存(FileBuffer),在内存中将它进行拉伸(ImageBuffer),再压缩(NewFileBuffer),然后将压缩后的NewFileBuffer存盘并可以正常运行,实现PE加载过程。
3. 通过编写控制台程序,将一个EXE文件读取到内存,在它的节表中新增一个节表和节区,存盘后让他可以正常运行。
4. 通过编写控制台程序,将一个EXE文件读取到内存,把该文件的最后一个节扩大1000h,并保证程序的正常运行。
5. 通过编写控制台程序,将一个EXE文件读取到内存,把该文件的所有节进行合并,并保证程序的正常运行。
6. 通过编写控制台程序,将一个EXE文件读取到内存,在它的可执行节(代码节)中加一个弹出对话框(MessgeBox)的ShellCode,通过修改程序执行入口实现文件感染,可以正常运行。
读取文件的关键代码:
//函数声明
//**************************************************************************
//MyReadFile:将文件读取到缓冲区
//参数说明:
//pFileAddress 缓冲区地址
//返回值说明:
//成功返回0
//**************************************************************************
int MyReadFile(void** pFileAddress);
{
int ret = 0;
DWORD Length = 0;
//打开文件
FILE* pf = fopen(FILE_PATH, "rb");
if (pf == NULL)
{
ret = -1;
printf("func ReadFile() Error!\n");
return ret;
}
//获取文件长度
ret = GetFileLength(pf, &Length);
if (ret != 0 && Length == -1)
{
ret = -2;
printf("func GetFileLength() Error!\n");
return ret;
}
//分配空间
*pFileAddress = (PVOID)malloc(Length);
if (*pFileAddress == NULL)
{
ret = -3;
printf("func malloc() Error!\n");
return ret;
}
memset(*pFileAddress, 0, Length);
//读取文件进入内存
fread(*pFileAddress, Length, 1, pf);
fclose(pf);
return ret;
}
2、
1)、根据SizeOfImage的大小,开辟一块缓冲区(ImageBuffer).
2)、根据SizeOfHeader的大小,将头信息从FileBuffer拷贝到ImageBuffer
3)、根据节表中的信息循环讲FileBuffer中的节拷贝到ImageBuffer中.
读取文件的关键代码:
//函数声明
//**************************************************************************
//MyWriteFile:将缓存写入硬盘
//参数说明:
//pFileAddress 缓冲区地址
//FileSize 写入大小
//FilePath 写入路径
//返回值说明:
//成功返回0
//**************************************************************************
int MyWriteFile(PVOID pFileAddress, DWORD FileSize, LPSTR FilePath)
{
int ret = 0;
FILE *pf = fopen(FilePath, "wb");
if (pf == NULL)
{
ret = -5;
printf("func fopen() error :%d!\n", ret);
return ret;
}
fwrite(pFileAddress, FileSize, 1, pf);
fclose(pf);
return ret;
}
3、
1)、判断是否有足够的空间,可以添加一个节表.
判断条件:
剩余空间 = SizeOfHeader -
(e_lfanew + 4/*PE标记*/ +
sizeof(IMAGE_FILE_HEADER) +
SizeOfOptionalHeader + /*NT头*/
sizeof(IMAGE_SECTION_HEADER) * NumberOfSections) /*节表大小*/
>= sizeof(IMAGE_SECTION_HEADER) * 2 /*2个节表的大小*/
2)、需要修改的数据
1> 添加一个新的节(可以copy一份)
2> 在新增节后面 填充一个节大小的0x00
3> 修改PE头中节的数量
4> 修改sizeOfImage的大小
5> 再原有数据的最后,新增一个节的数据(内存对齐的整数倍).
6> 修正新增节表的属性
4、
1)、拉伸到内存
2)、分配一块新的空间:SizeOfImage + 1000h
3)、修改最后一个节的SizeOfRawData和VirtualSize
4)、修改SizeOfImage的大小
5、
1)、拉伸到内存
2)、将第一个节的内存大小、文件大小改成一样
3)、将第一个节的属性改为包含所有节的属性
4)、修改节的数量为1
6、
1)、获取MessageBox地址,构造ShellCode代码.
2)、E8 E9计算公式
3)、在代码区手动添加代码
4)、修改OEP,指向ShellCode
char shellcode[] =
{
0x6A, 00, 0x6A, 00, 0x6A, 00, 0x6A, 00,
0xE8, 00, 00, 00, 00,
0xE9, 00, 00, 00, 00
};
注:0xE8 是call的机器码;0xE9是jmp的机器码;它们后面跟着的4个字节需要通过计算获得。
计算方式: X = 真正要跳转的地址 - 这条指令的下一行地址, 修改地址时注意小端对齐。
AddressOfEntryPoint是一个RAV
//RVA与FOA相互转换相关代码:
//函数声明
//**************************************************************************
//FOA_TO_RVA:将FOA转换成RVA
//参数说明:
//FileAddress 缓冲区地址
//FOA FOA值
//pRVA RVA地址
//返回值说明:
//成功返回0
//**************************************************************************
int FOA_TO_RVA(PVOID FileAddress, DWORD FOA, PDWORD pRVA)
{
int ret = 0;
PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)(FileAddress);
PIMAGE_FILE_HEADER pFileHeader = (PIMAGE_FILE_HEADER)((DWORD)pDosHeader + pDosHeader->e_lfanew + 4);
PIMAGE_OPTIONAL_HEADER32 pOptionalHeader = (PIMAGE_OPTIONAL_HEADER32)((DWORD)pFileHeader + sizeof(IMAGE_FILE_HEADER));
PIMAGE_SECTION_HEADER pSectionGroup = (PIMAGE_SECTION_HEADER)((DWORD)pOptionalHeader + pFileHeader->SizeOfOptionalHeader);
//FOA在文件头中 或 SectionAlignment 等于 FileAlignment 时RVA等于FOA
if (FOA < pOptionalHeader->SizeOfHeaders || pOptionalHeader->SectionAlignment == pOptionalHeader->FileAlignment)
{
*pRVA = FOA;
return ret;
}
//FOA在节区中
for (int i = 0; i < pFileHeader->NumberOfSections; i++)
{
if (FOA >= pSectionGroup[i].PointerToRawData && FOA < pSectionGroup[i].PointerToRawData + pSectionGroup[i].SizeOfRawData)
{
*pRVA = pSectionGroup[i].VirtualAddress + FOA - pSectionGroup[i].PointerToRawData;
return ret;
}
}
//没有找到地址
ret = -4;
printf("func FOA_TO_RAV() Error: %d 地址转换失败!\n", ret);
return ret;
}
//函数声明
//**************************************************************************
//RVA_TO_FOA:将RVA转换成FOA
//参数说明:
//FileAddress 缓冲区地址
//RVA RVA值
//pFOA FOA地址
//返回值说明:
//成功返回0
//**************************************************************************
int RVA_TO_FOA(PVOID FileAddress, DWORD RVA, PDWORD pFOA)
{
int ret = 0;
PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)(FileAddress);
PIMAGE_FILE_HEADER pFileHeader = (PIMAGE_FILE_HEADER)((DWORD)pDosHeader + pDosHeader->e_lfanew + 4);
PIMAGE_OPTIONAL_HEADER32 pOptionalHeader = (PIMAGE_OPTIONAL_HEADER32)((DWORD)pFileHeader + sizeof(IMAGE_FILE_HEADER));
PIMAGE_SECTION_HEADER pSectionGroup = (PIMAGE_SECTION_HEADER)((DWORD)pOptionalHeader + pFileHeader->SizeOfOptionalHeader);
//RVA在文件头中 或 SectionAlignment 等于 FileAlignment 时RVA等于FOA
if (RVA < pOptionalHeader->SizeOfHeaders || pOptionalHeader->SectionAlignment == pOptionalHeader->FileAlignment)
{
*pFOA = RVA;
return ret;
}
//RVA在节区中
for (int i = 0; i < pFileHeader->NumberOfSections; i++)
{
if (RVA >= pSectionGroup[i].VirtualAddress && RVA < pSectionGroup[i].VirtualAddress + pSectionGroup[i].Misc.VirtualSize)
{
*pFOA = pSectionGroup[i].PointerToRawData + RVA - pSectionGroup[i].VirtualAddress;
return ret;
}
}
//没有找到地址
ret = -4;
printf("func RAV_TO_FOA() Error: %d 地址转换失败!\n", ret);
return ret;
}
第二章:打造自己的PE解释器——目录信息
本章目的:在上一章中我们留下来一个知识点没有讲解,那就是IMAGE_OPTIONAL_HEADER32中的最后一个成员DataDirectory。
虽然他只是一个结构体数组,每个结构体的大小也不过是个字节,但是它却是PE文件中最重要的成员。PE装载器通过查看它才能准确的找到某个函数或某个资源。
重点掌握:
(1) 0x01 IMAGE_EXPORT_DIRECTORY——导出表
(2) 0x03 IMAGE_BASE_RELOCATION——重定位表
(3) 0x05 IMAGE_IMPORT_DESCRIPTOR——导入表
(4) 0x07 IMAGE_BOUND_IMPORT_DESCRIPTOR——绑定导入表
(5) 0x09 IMAGE_RESOURCE_DIRECTORY——资源表
第一节:IMAGE_DATA_DIRECTORY——数据目录结构
DWORD VirtualAddress; /**指向某个数据的相对虚拟地址 RAV 偏移0x00**/
DWORD Size; /**某个数据块的大小 偏移0x04**/
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
//定位目录项的方法(以导出表为例):所有操作都在FileBuffer状态下完成
//1、指向相关内容
PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)(FileAddress);
PIMAGE_FILE_HEADER pFileHeader = (PIMAGE_FILE_HEADER)((DWORD)pDosHeader + pDosHeader->e_lfanew + 4);
PIMAGE_OPTIONAL_HEADER32 pOptionalHeader = (PIMAGE_OPTIONAL_HEADER32)((DWORD)pFileHeader + sizeof(IMAGE_FILE_HEADER));
//2、获取导出表的地址(目录项的第0个成员)
DWORD ExportDirectory_RAVAdd = pOptionalHeader->DataDirectory[0].VirtualAddress;
DWORD ExportDirectory_FOAAdd = 0;
// (1)、判断导出表是否存在
if (ExportDirectory_RAVAdd == 0)
{
printf("ExportDirectory 不存在!\n");
return ret;
}
// (2)、获取导出表的FOA地址 转换函数看上一章作业提示
ret = RVA_TO_FOA(FileAddress, ExportDirectory_RAVAdd, &ExportDirectory_FOAAdd);
if (ret != 0)
{
printf("func RVA_TO_FOA() Error!\n");
return ret;
}
//3、指向导出表
PIMAGE_EXPORT_DIRECTORY ExportDirectory = (PIMAGE_EXPORT_DIRECTORY)((DWORD)FileAddress + ExportDirectory_FOAAdd);
第二节: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; // **重要 导出函数地址表RVA
DWORD AddressOfNames; // **重要 导出函数名称表RVA
DWORD AddressOfNameOrdinals; // **重要 导出函数序号表RVA
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
HMODULE hModule = LoadLibraryA("User32.dll");
//1、函数名获取
DWORD FuncAddress = GetProcAddress(hModule, "MessageBoxA");
//2、序号获取
DWORD FuncAddress = GetProcAddress(hModule, 12);
这个值是一个4字节的RVA地址,他可以用来定位导出表中所有函数的地址表,这个地址表可以当作一个成员宽度为4的数组进行处理。
AddressOfNames
这个值是一个4字节的RVA地址,他可以用来定位导出表中所有函数的名称表,这个名称表也可以当作一个成员宽度为4的数组进行处理。
AddressOfNameOrdinals
这个值是一个4字节的RVA地址,他可以用来定位导出表中所有函数的序号表,这个序号表可以当作一个成员宽度为2的数组进行处理,它的长度由NumberOfNames进行限定,名称表的成员是一个函数序号,该序号用于通过名称获取函数地址。
NumberOfFunctions
注意,这个值并不是真的函数数量,他是通过函数序号表中最大的序号减去最小的序号再加上一得到的,例如:一共导出了3个函数,序号分别是:0、2、4,NumberOfFunctions = 4 - 0 + 1 = 5个。
(1) 首先定位函数名表,然后通过函数名表中的RVA地址定位函数名,通过比对函数名获取目标函数名的在函数名表中的索引。
(2) 通过获取函数名表的索引获取函数序号表中对应索引中的函数序号。
(3) 通过把该序号当作函数地址表的下标,就可以得到该下标中的函数地址。
注:通过序号获取函数地址不需要使用函数名称表和函数序号表就可以直接获取函数地址,实现上相对来说比较方便。
第三节:导出表小练习
2. 写出按名字查找函数地址、按序号查找函数地址相关函数。
3. 在PE文件中创建一个新节,然后将导出表的所有信息移动到新节中。最后将文件写入硬盘,并可以正确解析导出表。
// 1)定位导出表
// 2)打印导出表所有信息
// 3)定位函数地址表,并打印相关信息
// 4)定位函数序号表,并打印相关信息
// 5)定位函数名称表,并打印相关信息
2、可以通过上一节的图片提示编程
//参考代码声明
//**************************************************************************
//GetProcAddressByName:按名字查找函数地址
//参数说明:
//FileAddress 缓冲区地址
//pFuncName 要查找的函数名
//FuncAddressRVA 查找出的函数地址指针
//返回值说明:
//成功返回0
//**************************************************************************
int GetProcAddressByName(PVOID FileAddress, PCHAR pFuncName, PDWORD FuncAddressRVA);
//**************************************************************************
//GetProcAddressByOrdinal:按序号查找函数地址
//参数说明:
//FileAddress 缓冲区地址
//wFuncOrdinal 要查找的函数序号
//FuncAddressRVA 查找出的函数地址指针
//返回值说明:
//成功返回0
//**************************************************************************
int GetProcAddressByOrdinal(PVOID FileAddress, WORD wFuncOrdinal, PDWORD FuncAddressRVA);
3、移动导出表的步骤:
1)创建一个新节
2)移动函数地址表到新节区
3)移动函数序号表
4)移动函数名称表
5)将函数名称移动到函数名称表之后,并修正函数名表中的数据
6)将文件名移动到函数名后
7)将整个导出表移动到文件名后
8)修复导出表数据:Name、AddressOfFunctions、AddressOfNames、NumberOfFunctions
9)修正目录项的RVA地址
第四节:IMAGE_BASE_RELOCATION——重定位表
DWORD VirtualAddress; 重定位数据所在页的RVA
DWORD SizeOfBlock; 当前页中重定位数据块的大小
} IMAGE_BASE_RELOCATION;
typedef IMAGE_BASE_RELOCATION UNALIGNED * PIMAGE_BASE_RELOCATION;
修正方法:需要重定位的地址 - 以前的基址 + 当前的基址。
printf("Helloworld %s", "hahaha");
013E64B2 68 98 BC 3E 01 push offset string "hahaha" (013EBC98h)
013E64B7 68 9C D4 3E 01 push offset string "Helloworld %s" (013ED49Ch)
013E64BC E8 A3 AB FF FF call _printf (013E1064h)
013E64C1 83 C4 08 add esp,8
这个虚拟地址是一组重定位数据的开始RVA地址,只有重定位项的有效数据加上这个值才是重定位数据真正的RVA地址。
SizeOfBlock
它是当前重定位块的总大小,因为VirtualAddress和SizeOfBlock都是4字节的,所以(SizeOfBlock - 8)才是该块所有重定位项的大小,(SizeOfBlock - 8) / 2就是该块所有重定位项的数目。
重定位项
重定位项在该结构中没有体现出来,他的位置是紧挨着这个结构的,可以把他当作一个数组,宽度为2字节,每一个重定位项分为两个部分:高4位和低12位。
第五节:重定位表小练习
(1)在PE文件中创建一个新节
(2)将重定位表的数据块循环拷贝到新的节区
(3)修复目录项对应的虚拟地址
第六节:IMAGE_IMPORT_DESCRIPTOR——导入表
union {
DWORD Characteristics;
DWORD OriginalFirstThunk; //导入名称表(INT)的RVA地址
} DUMMYUNIONNAME;
DWORD TimeDateStamp; //时间戳多数情况可忽略 如果是0xFFFFFFFF表示IAT表被绑定为函数地址
DWORD ForwarderChain;
DWORD Name; //导入DLL文件名的RVA地址
DWORD FirstThunk; //导入地址表(IAT)的RVA地址
} IMAGE_IMPORT_DESCRIPTOR;
typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;
这个值是一个4字节的RVA地址,这个地址指向了导入名称表(INT),INT是一个IMAGE_THUNK_DATA结构体数组,这个结构体的最后一个成员内容为0时数组结束。
Name
DLL名字的指针,是一个RVA地址,指向了一个以0结尾的ASCII字符串。
这个值是一个4字节的RVA地址,这个地址指向了导入地址表(IAT),这个IAT和INT一样,也是一个IMAGE_THUNK_DATA结构体数组。
union {
DWORD ForwarderString; // PBYTE
DWORD Function; // PDWORD
DWORD Ordinal;
DWORD AddressOfData; // PIMAGE_IMPORT_BY_NAME
} u1;
} IMAGE_THUNK_DATA32;
typedef IMAGE_THUNK_DATA32 * PIMAGE_THUNK_DATA32;
//注:这个结构体是联合类型的,每一个成员都是4字节,所以为了编程方便,完全可以用一个4字节的数组取代它。
WORD Hint;
CHAR Name[1];
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;
//注:这个结构体由两个成员组成,大致一看它的大小是3个字节,其实它的大小是不固定的,
// 因为无法判断函数名的长度,所以最后一个成员是一个以0结尾的字符串。
第七节:导入表小练习
(1)定位导入表
(2)打印导入文件名
(3)遍历INT表,打印出序号和函数名
(4)遍历IAT表,打印出序号和函数名(不要使用notepad.exe等系统程序练习,等学完绑定导入表后进行修改代码)
2、移动流程:
(1)增加新的节区,注意节区的大小等于导入表、INT表和各种名字的总大小,如果觉得计算麻烦可以直接在导入表大小的基础上加上0x5000并对齐。
(2)将所有的导入表移动到新节
(3)获取INT表中项目的个数
(4)将INT表移动到新节,同时移入函数名结构,修正数据。
(5)将导入文件名移入新节
(6)依次修复导入表数据信息(OriginalFirstThunk和Name)
(7)修复目录项中导入表的RVA地址
注:大家可能注意到我们没有移动IAT表,这是因为IAT表在程序加载后存储的是函数地址。程序通过访问IAT表才能获取函数的地址,如果移动IAT表,程序就无法获取函数地址,要想解决这个问题由两种方法:一是把程序中所有访问IAT表的地方进行修正,事实上目前来说无法实现。二是放弃移动IAT表。
第八节:IMAGE_BOUND_IMPORT_DESCRIPTOR——绑定导入表
DWORD TimeDateStamp; //时间戳
WORD OffsetModuleName; //DLL名的地址偏移
WORD NumberOfModuleForwarderRefs; //该结构后IMAGE_BOUND_FORWARDER_REF数组的数量
// Array of zero or more IMAGE_BOUND_FORWARDER_REF follows
} IMAGE_BOUND_IMPORT_DESCRIPTOR, *PIMAGE_BOUND_IMPORT_DESCRIPTOR;
这个时间戳相对来说还是比较重要的,因为这个值只有和导入DLL的IMAGE_FILE_HEADER中的TimeDateStamp值相同才能起到绑定导入的效果,如果不一致加载器就会重新计算IAT表中的函数地址。(由于DLL文件的版本不同或者DLL文件的ImageBase被重定位时,IAT绑定的函数的地址就会发生变化)
这个偏移不是RVA页不是FOA,所以模块名的定位与之前的方法不同,它的定位方式是以第一个IMAGE_BOUND_IMPORT_DESCRIPTOR的地址为基址,加上OffsetModuleName的值就是模块名所在的地址了,这个模块名是以0结尾的ASCII字符串。
这个值是在IMAGE_BOUND_IMPORT_DESCRIPTOR结构后跟随的IMAGE_BOUND_FORWARDER_REF结构的数量。
DWORD TimeDateStamp; //时间戳
WORD OffsetModuleName; //DLL名的地址偏移
WORD Reserved; //保留
} IMAGE_BOUND_FORWARDER_REF, *PIMAGE_BOUND_FORWARDER_REF;
//注:
// 该结构中的成员和绑定导入表的成员含含义一致,所以不再过多叙述。
// 由于IMAGE_BOUND_IMPORT_DESCRIPTOR和IMAGE_BOUND_FORWARDER_REF的大小结构相同,所以可以相互转型,方便编程。
绑定导入表结构图:
第九节:绑定导入表小练习
第十节:IMAGE_RESOURCE_DIRECTORY——资源表
typedef struct _IMAGE_RESOURCE_DIRECTORY {
DWORD Characteristics; //资源属性 一般为0
DWORD TimeDateStamp; //资源创建时间戳 一般为0
WORD MajorVersion;
WORD MinorVersion;
WORD NumberOfNamedEntries; //以名称命名的目录项数量 重要
WORD NumberOfIdEntries; //以ID命名的目录项数量 重要
// IMAGE_RESOURCE_DIRECTORY_ENTRY DirectoryEntries[];
} IMAGE_RESOURCE_DIRECTORY, *PIMAGE_RESOURCE_DIRECTORY;
//资源目录项
typedef struct _IMAGE_RESOURCE_DIRECTORY_ENTRY {
union {
struct {
DWORD NameOffset:31; //字符串的偏移(不是RVA、FOA,相对特殊)
DWORD NameIsString:1; //判断名字是否是字符串 1:是 0:不是
} DUMMYSTRUCTNAME;
DWORD Name;
WORD Id; //目录项的ID(在一级目录指资源类型,二级目录指资源编号,三级目录指代码的页号)
} DUMMYUNIONNAME;
union {
DWORD OffsetToData; //如果不是目录,这里指数据的偏移(不是RVA、FOA,相对特殊)
struct {
DWORD OffsetToDirectory:31;//目录的偏移(不是RVA、FOA,相对特殊)
DWORD DataIsDirectory:1; //判断子资源项是否是目录 1:是 0:不是
} DUMMYSTRUCTNAME2;
} DUMMYUNIONNAME2;
} IMAGE_RESOURCE_DIRECTORY_ENTRY, *PIMAGE_RESOURCE_DIRECTORY_ENTRY;
//数据项
typedef struct _IMAGE_RESOURCE_DATA_ENTRY {
DWORD OffsetToData; //数据的偏移 重要
DWORD Size; //数据的大小 重要
DWORD CodePage; //代码页(一般为0)
DWORD Reserved; //保留
} IMAGE_RESOURCE_DATA_ENTRY, *PIMAGE_RESOURCE_DATA_ENTRY;
//名字字符串结构
typedef struct _IMAGE_RESOURCE_DIR_STRING_U {
WORD Length; //Unicode字符串长度
WCHAR NameString[ 1 ]; //Unicode字符串
} IMAGE_RESOURCE_DIR_STRING_U, *PIMAGE_RESOURCE_DIR_STRING_U;
一级目录是按照资源类型分类的,如位图资源、光标资源、图标资源。
二级目录是按照资源编号分类的,同样是菜单资源,其子目录通过资源ID编号分类,例如:IDM_OPEN的ID号是2001h,IDM_EXIT的ID号是2002h等多个菜单编号。
三级目录是按照资源的代码页分类的,即不同语言的代码页对应不同的代码页编号,例如:简体中文代码页编号是2052。
三级目录下是节点,也称为资源数据,这是一个IMAGE_RESOURCE_DATA_ENTRY的数据结构,里面保存了资源的RVA地址、资源的大小,对所有资源数据块的访问都是从这里开始的。
注:资源表的一级目录、二级目录、三级目录的目录结构是相同的都是由一个资源目录头加上一个资源目录项数组组成的,可以将这个结构称作资源目录结构单元。
在资源目录项中该字段是一个联合体类型,大小为4个字节,它决定这个资源目录的名字是字符串还是ID号。
在资源目录项中该字段是一个联合体类型,大小为4个字节,它决定这个资源目录的目录中子节点的类型(是目录还是节点)。
注:为了编程方便,IMAGE_RESOURCE_DIRECTORY_ENTRY的联合体中出现了一组特殊的struct结构体,其成员声明格式为:[类型] [变量名] : [位宽表达式], 这个格式就是C语言中位段的声明格式。
这个结构体就是目录资源的三级目录下的子目录,里面存储的就是资源文件的信息,如OffsetToData字段存储的就是资源文件的RVA地址,它指向了资源的二进制信息,Size字段存储的就是资源文件的大小,CodePage字段存储资源的代码页但大多数情况为0。
注:在其指向的资源数据中,字符串都是Unicode的编码方式,每个字符都是由一个16位(一个单字)的值表示,并且都是以UNICODE_NULL结束(其实就是两个0x00)。
IMAGE_RESOURCE_DIR_STRING_U
该结构体就是目录资源的名称结构,里面存在两个字段,都是2个字节,Length字段存储的是目录资源名称的长度,以2个字节为单位。NameString字段是一个Unicode字符串的第一个字符,并不以0结尾,其长度是由Length字段限制。
(2) 在第一个目录项中以ID号命名,资源类型位2也就是位图资源。
(3) 在目录项的二个字段可以得知该目录的子节点也是目录,偏移是0x58,RVA = 0x2B000 + 0x58 = 0x2B058。
(2) 目录项以ID号命名,ID号为0x80。
(3) 在目录项的二个字段可以得知该目录的子节点也是目录,偏移是0x268,RVA = 0x2B000 + 0x268 = 0x2B268。
(1) 可以得到资源目录头中一共有1个目录项。
(2) 目录项以ID号命名,ID号为0x804,表示使用简体中文的代码页。
(3) 在目录项的二个字段可以得知该目录的子节点是数据项,偏移是0x6E8,RVA = 0x2B000 + 0x6E8 = 0x2B6E8。
(2) 资源数据的大小:0x680.
第十一节:资源表小练习
(1)定位资源表
(2)打印资源表目录头信息,并循环打印目录项。(一级目录)
(3)定位二级目录头,并打印资源表目录头信息,循环打印目录项。(二级目录)
(4)定位三级目录头,并打印资源表目录头信息,循环打印目录项。(三级目录)
(5)定位资源数据项,并打印资源数据项信息。
2、移动资源表的步骤:
(1)在PE文件中创建一个新节
(2)将资源表的一级目录全部copy到新节
(3)循环一级资源表信息,定位二级目录。
(4)将资源表的二级目录全部copy到指定位置。
(5)循环二级资源表信息,定位三级目录。
(6)将资源表的三级目录全部copy到指定位置。
(7)定位资源数据项,将资源数据项全部copy到指定位置。
(8)将资源数据copy到新节中,并修复数据项。
(9)修复目录项信息。
注:由于资源表都是通过首个资源目录头定位数据的,而且都是在资源目录头之后,所以可以直接按照目录项中的资源大小,将资源从首个资源目录头开始全部copy到新节中。但是不推荐这样完成。
第三章:打造自己的PE解释器——全文总结
- End -
看雪ID:QiuJYu
https://bbs.pediy.com/user-813468.htm
*本文由看雪论坛 QiuJYu 原创,转载请注明来自看雪社区
往期热门回顾
﹀
﹀
﹀
公众号ID:ikanxue
官方微博:看雪安全
商务合作:wsc@kanxue.com
↙点击下方“阅读原文”,查看更多干货