windows的文件格式
windows中的文件格式如下:
PE 文件
├── DOS 头 (IMAGE_DOS_HEADER)
│ ├── e_magic // "MZ"
│ └── e_lfanew // PE 头的偏移量
└── PE 头/NT 头 (IMAGE_NT_HEADERS)
├── Signature // "PE\0\0"
├── 标准文件头 (IMAGE_FILE_HEADER)
│ ├── Machine
│ ├── NumberOfSections
│ ├── TimeDateStamp
│ ├── PointerToSymbolTable
│ ├── NumberOfSymbols
│ ├── SizeOfOptionalHeader
│ └── Characteristics
└── 扩展文件头 (IMAGE_OPTIONAL_HEADER)
├── Magic
├── MajorLinkerVersion
├── MinorLinkerVersion
├── SizeOfCode
├── SizeOfInitializedData
├── SizeOfUninitializedData
├── AddressOfEntryPoint
├── BaseOfCode
├── BaseOfData
└── 其他字段...
DOS 头(IMAGE_DOS_HEADER)
读取文件到内存中,可以使用c++标准方法,也可以使用windows的API(这里必须将文件读取到内存中,不然后续获得的偏移地址获取不到数据)
std::ifstream file(filePath, std::ios::binary);
if (!file.is_open()) {
std::cerr << "Failed to open the file." << std::endl;
return;
}
// 获取文件大小
file.seekg(0, std::ios::end);
std::streampos fileSizePos = file.tellg();
std::streamsize fileSize = static_cast<std::streamsize>(fileSizePos);
// 分配内存
char* buffer = new char[fileSize];
// 读取文件内容到缓冲区
file.seekg(0, std::ios::beg);
file.read(buffer, fileSize);
if (file.fail()) {
std::cerr << "Failed to read the file." << std::endl;
delete[] buffer;
return;
}
也可以使用windows API来进行文件读取.
HANDLE hFile = CreateFile(szFilePath, GENERIC_ALL, NULL, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (hFile == INVALID_HANDLE_VALUE)
{
DWORD errorCode = GetLastError();
printf("Error Code:%d\r\n", errorCode);
return NULL;
}
DWORD dwFileSize = GetFileSize(hFile, NULL);
char* szBuffer = new char[dwFileSize];
memset(szBuffer, 0, dwFileSize);
DWORD dwReadSize = 0;
BOOL bRet = ReadFile(hFile, szBuffer, dwFileSize, &dwReadSize, NULL);
if (!bRet)
{
return NULL;
}
Dos 头解析,从上述PE结构可以看出,文件头即Dos头.直接强转即可.
// 读取 DOS头
PIMAGE_DOS_HEADER pDosHeader = reinterpret_cast<PIMAGE_DOS_HEADER>(buffer);
if (pDosHeader->e_magic != IMAGE_DOS_SIGNATURE) {
std::cerr << "Not a valid PE file." << std::endl;
return;
}
PE/NT 头(IMAGE_NT_HEADERS)
winnt.h中定义的结构体名称为IMAGE_NT_HEADERS,所以叫NT头,内存中NT头的Signature(NT头起始位置)是个0x50 0x45,转为字符串就是PE,所以叫PE头.NT头地址偏移位于Dos头中,我们取出偏移强转即可
// 读取 PE/NT 头
PIMAGE_NT_HEADERS pNtHeaders = reinterpret_cast<PIMAGE_NT_HEADERS>(buffer + pDosHeader->e_lfanew);
if (pNtHeaders->Signature != IMAGE_NT_SIGNATURE) {
std::cerr << "Not a valid PE signature." << std::endl;
return;
}
printf("PE TAG: %08lx\r\n", pNtHeaders->Signature);
标准文件头(IMAGE_FILE_HEADER)扩展文件头(IMAGE_OPTIONAL_HEADER)
// 获取标准文件头
printf("\r\n");
IMAGE_FILE_HEADER fileHeader = pNtHeaders->FileHeader;
// 获取扩展文件头
printf("\r\n");
IMAGE_OPTIONAL_HEADER optionalHeader = pNtHeaders->OptionalHeader;
标准文件头成员
-
Machine:
- 类型:
WORD - 描述:指示生成该文件的目标机器类型。例如,
IMAGE_FILE_MACHINE_X86表示 x86 架构。
- 类型:
-
NumberOfSections:
- 类型:
WORD - 描述:指示文件中段的数量。每个段包含不同类型的数据,如代码、数据、资源等。
- 类型:
-
TimeDateStamp:
- 类型:
DWORD - 描述:文件的时间戳,表示文件创建或修改的时间。通常是一个 UNIX 时间戳。
- 类型:
-
PointerToSymbolTable:
- 类型:
DWORD - 描述:指向符号表的指针。对于大多数 PE 文件,这个值通常为 0,因为不再使用符号表。
- 类型:
-
NumberOfSymbols:
- 类型:
DWORD - 描述:符号的数量。对于大多数 PE 文件,这个值通常为 0。
- 类型:
-
SizeOfOptionalHeader:
- 类型:
WORD - 描述:可选头的大小,通常是 224 或 240 字节,具体取决于是否是 32 位或 64 位可执行文件。
- 类型:
-
Characteristics:
- 类型:
WORD - 描述:文件的特性标志,指示文件的属性。例如,是否是可执行文件、是否是 DLL 等。
- 类型:
打印出目标机器类型以及文件特性:
IMAGE_OPTIONAL_HEADER optionalHeader = pNtHeaders->OptionalHeader;
printf("入口点(EntryPoint);%08X\r\n", optionalHeader.AddressOfEntryPoint);
printf("镜像基址(ImageBase);%016llx\r\n", optionalHeader.ImageBase);
printf("镜像大小(SizeOfImage);%08X\r\n", optionalHeader.SizeOfImage);
printf("代码基址(BaseOfCode);%08X\r\n", optionalHeader.BaseOfCode);
printf("内存对齐(SectionAlignment);%08X\r\n", optionalHeader.SectionAlignment);
printf("文件对齐(FileAlignment);%08X\r\n", optionalHeader.FileAlignment);
printf("标志(Magic);%04X\r\n", optionalHeader.Magic);
// 第二列
printf("\r\n");
printf("子系统(Subsystem);%04X\r\n", optionalHeader.Subsystem);
printf("区段数量(NumberOfSections);%08X\r\n", fileHeader.NumberOfSections);
printf("日期时间标志(TimeDateStamp);%08X\r\n", fileHeader.TimeDateStamp);
printf("部首大小(SizeOfHeaders);%08X\r\n", optionalHeader.SizeOfHeaders);
printf("特征值(Characteristics);%04X\r\n", fileHeader.Characteristics);
printf("扩展头大小(SizeOfOptionalHeader);%04X\r\n", fileHeader.SizeOfOptionalHeader);
printf("RVA数及大小(NumberOfRvaAndSizes);%08X\r\n", optionalHeader.NumberOfRvaAndSizes);
// 机器类型
printf("\r\n");
printf("目标机器类型(Machine);%08X\r\n", fileHeader.Machine);
printf("\r\n");
区段数据
// 区段信息
printf("\r\n");
PIMAGE_SECTION_HEADER pSectionHeader = IMAGE_FIRST_SECTION(pNtHeaders);
for (int i = 0; i < fileHeader.NumberOfSections; i++)
{
printf("区段名:%.8s\r\n", pSectionHeader[i].Name);
printf("VOffset(相对虚拟地址):%08X\r\n", pSectionHeader[i].VirtualAddress);
printf("VSize(区段大小):%08X\r\n", pSectionHeader[i].Misc.VirtualSize);
printf("ROffset(文件偏移):%08X\r\n", pSectionHeader[i].PointerToRawData);
printf("RSize(文件中区段的大小):%08X\r\n", pSectionHeader[i].SizeOfRawData);
printf("标记:%08X\r\n", pSectionHeader[i].Characteristics);
printf("\r\n");
}
数据目录表
数据目录固定为16个,宏为: IMAGE_NUMBEROF_DIRECTORY_ENTRIES.有一个是保留的,实际是15个.
IMAGE_DIRECTORY_ENTRY_EXPORT 0 // Export Directory
IMAGE_DIRECTORY_ENTRY_IMPORT 1 // Import Directory
IMAGE_DIRECTORY_ENTRY_RESOURCE 2 // Resource Directory
IMAGE_DIRECTORY_ENTRY_EXCEPTION 3 // Exception Directory
IMAGE_DIRECTORY_ENTRY_SECURITY 4 // Security Directory
IMAGE_DIRECTORY_ENTRY_BASERELOC 5 // Base Relocation Table
IMAGE_DIRECTORY_ENTRY_DEBUG 6 // Debug Directory
IMAGE_DIRECTORY_ENTRY_COPYRIGHT 7 // (X86 usage)
IMAGE_DIRECTORY_ENTRY_ARCHITECTURE 7 // Architecture Specific Data
IMAGE_DIRECTORY_ENTRY_GLOBALPTR 8 // RVA of GP
IMAGE_DIRECTORY_ENTRY_TLS 9 // TLS Directory
IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG 10 // Load Configuration Directory
IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT 11 // Bound Import Directory in headers
IMAGE_DIRECTORY_ENTRY_IAT 12 // Import Address Table
IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT 13 // Delay Load Import Descriptors
IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 14 // COM Runtime descriptor
我们可以用一个常量数组来定义:
const char* DataDirName[IMAGE_NUMBEROF_DIRECTORY_ENTRIES] = {
"Export Directory(导出目录)",// IMAGE_DIRECTORY_ENTRY_EXPORT
"Import Directory(导入目录)",// IMAGE_DIRECTORY_ENTRY_IMPORT
"Resource Directory(资源目录)",// IMAGE_DIRECTORY_ENTRY_RESOURCE
"Exception Directory(异常目录)",// IMAGE_DIRECTORY_ENTRY_EXCEPTION
"Security Directory(安全目录)",// IMAGE_DIRECTORY_ENTRY_SECURITY
"Base Relocation Table(基址重定位表)",// IMAGE_DIRECTORY_ENTRY_BASERELOC
"Debug Directory(调试目录)",// IMAGE_DIRECTORY_ENTRY_DEBUG
//"Copyright Directory(版权目录) X86",// IMAGE_DIRECTORY_ENTRY_COPYRIGHT (X86 usage)
"Architecture Specific Data(架构特定数据) X64",// IMAGE_DIRECTORY_ENTRY_ARCHITECTURE
"Global Pointer(全局指针)",// IMAGE_DIRECTORY_ENTRY_GLOBALPTR
"TLS Directory(线程局部存储目录)",// IMAGE_DIRECTORY_ENTRY_TLS
"Load Configuration Directory(加载配置目录)",// IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG
"Bound Import Directory(绑定导入目录)",// IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT
"Import Address Table(导入地址表)",// IMAGE_DIRECTORY_ENTRY_IAT
"Delay Load Import Descriptors(延迟加载导入描述符)",// IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT
"COM Runtime Descriptor(COM 运行时描述符)",// IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR
"Reserved(保留)"
};
输出数据目录表:
printf("\r\n");
printf("数据目录表:\r\n");
for (size_t i = 0; i < IMAGE_NUMBEROF_DIRECTORY_ENTRIES; i++)
{
printf("Name:%s\r\n", DataDirName[i]);
printf("VirtualAddress:%08X\r\n", optionalHeader.DataDirectory[i].VirtualAddress);
printf("Size:%08X\r\n", optionalHeader.DataDirectory[i].Size);
printf("\r\n");
}
解析导入表
导入表里面很多数据都是使用的虚拟地址,需要先将虚拟地址转为文件偏移地址.这里写一个公共的函数,进行虚拟地址转文件偏移.
虚拟地址转文件偏移函数
// 虚拟地址转为文件偏移
DWORD RvaToFoa(DWORD dwRva, PIMAGE_NT_HEADERS pNt)
{
PIMAGE_SECTION_HEADER pSectionHeader = IMAGE_FIRST_SECTION(pNt);
if (dwRva < pSectionHeader[0].VirtualAddress)
{
// 比最小的虚拟地址还小,直接返回
return dwRva;
}
// 遍历所有段
for (size_t i = 0; i < pNt->FileHeader.NumberOfSections; i++)
{
DWORD startVir = pSectionHeader[i].VirtualAddress;
DWORD virSize = pSectionHeader[i].Misc.VirtualSize;
DWORD secAddr = pSectionHeader[i].PointerToRawData;
if (dwRva >= startVir && dwRva <= startVir + virSize)
{
return dwRva - startVir + secAddr;
}
}
}
导入表解析
- 导入表,通过数据目录表地址偏移获取到导入表的地址
pNtHeaders->OptionalHeader.DataDirectory + IMAGE_DIRECTORY_ENTRY_IMPORT - 然后通过
RVA2Fov将虚拟地址转为文件偏移地址,来获取到第一个模块地址(PIMAGE_IMPORT_DESCRIPTOR)(RvaToFoa(pImportDir->VirtualAddress, pNtHeaders) + buffer) - 模块是一个数组,所以后续可以通过指针自增来实现模块的遍历.
- 通过模块的
OriginalFirstThunk虚拟地址,找到函数的文件偏移地址. `(PIMAGE_THUNK_DATA)(RvaToFoa(pImport->OriginalFirstThunk, pNtHeaders) + buffer); - 函数
PIMAGE_THUNK_DATA分为32位版本与64位版本,OriginalFirstThunk指向的是第一个函数地址(RVA),实际地址可以通过PIMAGE_THUNK_DATA的自增来获取,虚拟地址则通过OriginalFirstThunk地址偏移结构体大小来获取,文件偏移地址则通过RVA转Foa来获取. - 函数的导入有通过名称导入的,也有通过序号导入的.通过名称导入的
thunkData->u1.Ordinal的最高位为0即(thunkData->u1.Ordinal & 0x80000000) == 0,Function最高位也是一样的. - 为名称导入的,
thunkData->u1.AddressOfData地址存储的是PIMAGE_IMPORT_BY_NAME结构的RVA地址,转Foa后进行基址相加得到实际地址. - 序号导入的,则可以获取序号
thunkData->u1.Function - 0x80000000(这里需要减去最高位的1)
整体树状关系
PIMAGE_IMPORT_DESCRIPTOR
├── Name // 导入模块的名称(RVA)
├── TimeDateStamp // 时间戳
├── ForwarderChain // 转发链(通常为0)
├── OriginalFirstThunk // 指向原始导入函数的指针(RVA)
│ └── PIMAGE_THUNK_DATA // 原始导入项
│ ├── Ordinal // 序号或指向名称的地址
│ └── AddressOfData // 指向 PIMAGE_IMPORT_BY_NAME
│ └── PIMAGE_IMPORT_BY_NAME // 指向实际的导入名称
│ ├── Hint // 函数提示(如果有)
│ └── Name // 函数名称
└── FirstThunk // 指向实际导入函数的指针(RVA)
└── PIMAGE_THUNK_DATA // 实际导入项
├── Function // 实际函数地址(RVA)
└── Ordinal // 序号(如果是序号导入)
解析代码
void ImportTableParser(PIMAGE_NT_HEADERS pNtHeaders, char* buffer)
{
// 导入表目录(通过地址偏移找到导入表的地址)
PIMAGE_DATA_DIRECTORY pImportDir = pNtHeaders->OptionalHeader.DataDirectory + IMAGE_DIRECTORY_ENTRY_IMPORT;
// 导入表
PIMAGE_IMPORT_DESCRIPTOR pImport = (PIMAGE_IMPORT_DESCRIPTOR)(RvaToFoa(pImportDir->VirtualAddress, pNtHeaders) + buffer);
while (pImport->Name != NULL)
{
// 导入模块读取
char* szModuleName = (char*)(RvaToFoa(pImport->Name, pNtHeaders) + buffer);
printf("模块名称:%s\r\n", szModuleName);
printf("日期时间标志:%08X\r\n", pImport->TimeDateStamp);
printf("ForwarderChain:%08X\r\n", pImport->ForwarderChain);
printf("模块名称Offset:%08X\r\n", pImport->Name);
printf("\r\n");
printf("FirstThunk(模块运行时的地址(虚拟)):%08X\r\n", pImport->FirstThunk);
printf("OriginalFirstThunk(模块加载时的地址(虚拟)):%08X\r\n", pImport->OriginalFirstThunk);
// API 解析
PIMAGE_THUNK_DATA thunkData = (PIMAGE_THUNK_DATA)(RvaToFoa(pImport->OriginalFirstThunk, pNtHeaders) + buffer);
DWORD dwIndex = 0;
DWORD dwImportOffset = 0;
while (thunkData->u1.Ordinal != 0)
{
printf("\r\n");
// 从第一个ThunkData 开始,每次步进PIMAGE_THUNK_DATA大小(这里是找的RVA虚拟地址)
printf("ThunkRva:%08X\r\n", pImport->OriginalFirstThunk + dwIndex);
dwImportOffset = RvaToFoa(pImport->OriginalFirstThunk, pNtHeaders);
// 这里是文件偏移地址(未加上文件基址)
printf("ThunkOffset:%08X\r\n", dwImportOffset + dwIndex);
// 实际函数地址1
printf("ThunkOffset2:%016llX\r\n", (thunkData));
// 实际函数地址2 两者的值是一样的,一个是自增,一个是通过RVA自增然后转换
printf("ThunkOffset2:%016llX\r\n", ((RvaToFoa(pImport->OriginalFirstThunk + dwIndex, pNtHeaders) + buffer)));
// 目标是32位的IMAGE_THUNK_DATA32,64位的用IMAGE_THUNK_DATA64
dwIndex += sizeof(IMAGE_THUNK_DATA32);
// 最高位 为 1 时,为序号导入,为0时,为名称导入
if ((thunkData->u1.Ordinal & 0x80000000) == 0)
{
PIMAGE_IMPORT_BY_NAME pName = (PIMAGE_IMPORT_BY_NAME)(RvaToFoa(thunkData->u1.AddressOfData, pNtHeaders) + buffer);
__try
{
printf("API名称1:%s\r\n", pName->Name);
printf("HINT:%04X\r\n", pName->Hint);
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
// 按序号导入的API
int nOun = thunkData->u1.Function - 0x80000000;
char szNameBuffer[MAX_PATH] = { 0 };
sprintf_s(szNameBuffer, MAX_PATH, "序号:%Xh %dd", nOun, nOun);
printf("API名称1-2:%s\r\n", szNameBuffer);
printf("HINT(提示):-\r\n");
}
printf("ThunkValue:%016llX\r\n\r\n", thunkData->u1.Function);
}
else
{
// 序号导入
int nOun = thunkData->u1.Function - 0x80000000;
char szNameBuffer[MAX_PATH] = { 0 };
sprintf_s(szNameBuffer, MAX_PATH, "序号:%Xh %dd", nOun, nOun);
printf("API名称2:%s\r\n", szNameBuffer);
printf("HINT:-\r\n");
}
thunkData++;
}
pImport++;
}
}
导出表
- 导出表通过
pOptionalHeader->DataDirectory + IMAGE_DIRECTORY_ENTRY_EXPORT地址偏移即可获取到导出表的地址. - 通过
(PIMAGE_EXPORT_DIRECTORY)(RvaToFoa(pExportDir->VirtualAddress, pNtHeaders) + buffer)即可找到导出表的实际内存. AddressOfFunctions存储的函数地址,AddressOfNames存储函数名称地址.AddressOfNameOrdinals存储函数名称与序号映射关系地址.
解析代码如下:
void ExportTable(PIMAGE_NT_HEADERS pNtHeaders, char* buffer)
{
PIMAGE_OPTIONAL_HEADER pOptionalHeader = &pNtHeaders->OptionalHeader;
PIMAGE_DATA_DIRECTORY pExportDir = pOptionalHeader->DataDirectory + IMAGE_DIRECTORY_ENTRY_EXPORT;
PIMAGE_EXPORT_DIRECTORY pExport = (PIMAGE_EXPORT_DIRECTORY)(RvaToFoa(pExportDir->VirtualAddress, pNtHeaders) + buffer);
char* szName = (char*)(RvaToFoa(pExport->Name, pNtHeaders) + buffer);
if (pExport->AddressOfFunctions == 0)
{
printf("当前没有导出表!\r\n");
return;
}
printf("名称:%s\r\n", szName);
printf("导出表OFFSET%08X\r\n", RvaToFoa(pExportDir->VirtualAddress, pNtHeaders));
printf("特征值:%08X\r\n", pExport->Characteristics);
printf("起始序号:%08X\r\n", pExport->Base);
printf("名称OFFSET:%08X\r\n", pExport->Name);
printf("导出函数总数:%08X\r\n", pExport->NumberOfFunctions);
printf("具有函数名的导出函数总数:%08X\r\n", pExport->NumberOfNames);
printf("函数地址:%08X\r\n", pExport->AddressOfFunctions);
printf("函数序号地址:%08X\r\n", pExport->AddressOfNameOrdinals);
printf("函数名称地址:%08X\r\n", pExport->AddressOfNames);
DWORD dwFunCount = pExport->NumberOfFunctions;
DWORD dwNameCount = pExport->NumberOfNames;
DWORD dwBase = pExport->Base;
PDWORD pEat = (PDWORD)(RvaToFoa(pExport->AddressOfFunctions, pNtHeaders) + buffer);
PDWORD pEnt = (PDWORD)(RvaToFoa(pExport->AddressOfNames, pNtHeaders) + buffer);
PWORD pEot = (PWORD)(RvaToFoa(pExport->AddressOfNameOrdinals, pNtHeaders) + buffer);
for (DWORD i = 0; i < dwFunCount; i++)
{
if (pEat[i] == 0)
{
continue;
}
// 找出函数名称对应的序号
DWORD dwOrdinals = 0;
for (; dwOrdinals < dwNameCount; dwOrdinals++)
{
// 名称序号 == 函数序号,则找到名称
if (pEot[dwOrdinals] == i)
{
break;
}
}
// 没有找到序号对应的名称
if (dwOrdinals == dwNameCount)
{
printf("Ordinals:%x Addresss:0x%08X Name[NULL]\r\n", dwBase + i, pEat[i]);
}
else
{
char* szFuncName = (char*)(RvaToFoa(pEnt[dwOrdinals], pNtHeaders) + buffer);
printf("Ordinals:%x Addresss:0x%08X Name[%s]\r\n", dwBase + i, pEat[i], szFuncName);
}
}
}
资源表
- 首先通过数据目录表,偏移获取到资源地址:
PIMAGE_DATA_DIRECTORY pResDir = pOptionalHeader->DataDirectory + IMAGE_DIRECTORY_ENTRY_RESOURCE - 然后获取资源表的文件偏移基址:
(RvaToFoa(pResDir->VirtualAddress, pNtHeaders) + buffer) - 该基址指向的即为结构
IMAGE_RESOURCE_DIRECTORY的地址,即PIMAGE_RESOURCE_DIRECTORY pFirst = (PIMAGE_RESOURCE_DIRECTORY)resourceBaseAddr这里获取资源目录的信息,在目录信息后,紧跟随的是一个目录下的数据列表(数组) - 取出资源数据列表的对象,这里用指针.
(PIMAGE_RESOURCE_DIRECTORY_ENTRY)(pFirst + 1)这里+1表示步进一个IMAGE_RESOURCE_DIRECTORY大小. 也可以转为字符指针,步进IMAGE_RESOURCE_DIRECTORY结构大小. - 递归展开
整体结构如下:
PIMAGE_RESOURCE_DIRECTORY
├── NumberOfNamedEntries // 命名条目数量
├── NumberOfIdEntries // ID条目数量
├── PIMAGE_RESOURCE_DIRECTORY_ENTRY (Entry 1)
│ ├── Name (字符串或ID)
│ ├── OffsetToData (指向数据或子目录的偏移)
│ ├── (如果是字符串名称)
│ │ └── PIMAGE_RESOURCE_DIR_STRING_U (包含字符串长度和内容)
│ └── (如果是子目录)
│ └── PIMAGE_RESOURCE_DIRECTORY (指向子目录)
│ └── (如果是资源数据)
│ └── PIMAGE_RESOURCE_DATA_ENTRY (包含资源的 RVA 和大小)
│ ├── RVA // 资源的相对虚拟地址
│ ├── Size // 资源的大小
│ └── CodePage // 代码页
├── PIMAGE_RESOURCE_DIRECTORY_ENTRY (Entry 2)
│ ├── Name (字符串或ID)
│ ├── OffsetToData (指向数据或子目录的偏移)
│ ├── (如果是字符串名称)
│ │ └── PIMAGE_RESOURCE_DIR_STRING_U
│ └── (如果是子目录)
│ └── PIMAGE_RESOURCE_DIRECTORY
│ └── (如果是资源数据)
│ └── PIMAGE_RESOURCE_DATA_ENTRY
│ ├── RVA
│ ├── Size
│ └── CodePage
└── ...
解析代码如下:
const char* g_ResType[0x11] = {
"NULL",
"鼠标指针",// RT_CURSOR
"位图", // RT_BITMAP
"图标",
"菜单",
"对话框",
"字符串列表",
"字体目录",
"字体",
"快捷键",
"非格式化资源",
"消息列表",
"鼠标指针组",
"NULL",
"图标组",
"NULL",
"版本信息",
};
void ResourceTable(PIMAGE_NT_HEADERS pNtHeaders, char* buffer)
{
PIMAGE_OPTIONAL_HEADER pOptionalHeader = &pNtHeaders->OptionalHeader;
PIMAGE_DATA_DIRECTORY pResDir = pOptionalHeader->DataDirectory + IMAGE_DIRECTORY_ENTRY_RESOURCE;
char* resourceBaseAddr = (RvaToFoa(pResDir->VirtualAddress, pNtHeaders) + buffer);
PIMAGE_RESOURCE_DIRECTORY pFirst = (PIMAGE_RESOURCE_DIRECTORY)resourceBaseAddr;
SubDirResourceTable(pNtHeaders, buffer, resourceBaseAddr, pFirst);
}
// 资源目录解析
void SubDirResourceTable(PIMAGE_NT_HEADERS pNtHeaders, char* buffer, char* resourceBaseAddr, PIMAGE_RESOURCE_DIRECTORY dir)
{
printf("\r\n");
printf("\r\n");
PIMAGE_RESOURCE_DIRECTORY_ENTRY entry = (PIMAGE_RESOURCE_DIRECTORY_ENTRY)(dir + 1);
printf("目录名称条目:%04X\r\n", dir->NumberOfNamedEntries);
printf("目录ID条目:%04X\r\n", dir->NumberOfIdEntries);
// 总条目数
DWORD dwResNum = dir->NumberOfIdEntries + dir->NumberOfNamedEntries;
for (int i = 0; i < dwResNum; ++i)
{
// 节点类型解析
if (entry->NameIsString != 1)
{
if (entry->Id < 0x11)
{
printf("ResType:%s\r\n", g_ResType[entry->Id]);
}
else
{
printf("ResType:%d\r\n", entry->Id);
}
}
// 资源名称
if (entry->NameIsString != 1)
{
// 没有名称
printf("Name(Id):%d\r\n", entry->Id);
}
else
{
PIMAGE_RESOURCE_DIR_STRING_U pResName = (PIMAGE_RESOURCE_DIR_STRING_U)(resourceBaseAddr + entry->NameOffset);
wprintf(L"Resource Name: %.*s\n", pResName->Length, pResName->NameString);
}
// 判断是否是目录,如果是目录继续递归
if (entry->DataIsDirectory == 1)
{
// 递归读取目录
PIMAGE_RESOURCE_DIRECTORY nextDir = (PIMAGE_RESOURCE_DIRECTORY)(resourceBaseAddr + entry->OffsetToDirectory);
SubDirResourceTable(pNtHeaders, buffer, resourceBaseAddr, nextDir);
continue;
}
// 资源数据解析
PIMAGE_RESOURCE_DATA_ENTRY pStcData = (PIMAGE_RESOURCE_DATA_ENTRY)(resourceBaseAddr + entry->OffsetToData);
DWORD StcDataOffset = RvaToFoa(pStcData->OffsetToData, pNtHeaders);
printf("Rva:%08X\r\n", pStcData->OffsetToData);
printf("Foa:%08X\r\n", StcDataOffset);
printf("Size:%08X\r\n", pStcData->Size);
// 下一个资源
entry++;
}
}
重定位表解析
- 通过数据目录表偏移获取到重定位数据目录表
pNtHeaders->OptionalHeader.DataDirectory + IMAGE_DIRECTORY_ENTRY_BASERELOC - 根据重定位目录表的
VirtualAddress转文件偏移获取到第一个重定位块. (因为重定位块的大小是不一致的,所以查找下一个重定位块的时候,不能通过指针自增来获取,而应该通过大小计算之后的偏移来获取.) - 获取下一个重定位块:
(PIMAGE_BASE_RELOCATION)((char*)pReloc + pReloc->SizeOfBlock) - 根据重定位块
VirtualAddress的RVA地址可以找到对应的块地址,然后获取对应块的名称. - 在重定位块
IMAGE_BASE_RELOCATION后紧随的是重定位项RELOCATION_INFO的起始地址.这个重定位项其实就是一个16位(2字节)的结构,前12位表示偏移,后4位表示偏移类型.(32.64都是2字节) RELOCATION_INFO结构需要字节定义.
typedef struct _RELOCATION_INFO {
WORD Offset : 12; // 12 位偏移
WORD Type : 4; // 4 位重定位类型
} RELOCATION_INFO, * PRELOCATION_INFO;
- 通过
DWORD dwItems = (pReloc->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(RELOCATION_INFO)即可计算出重定位项的数量 - 重定位类型如下:
-
IMAGE_REL_BASED_ABSOLUTE (0) :
- 描述:不进行重定位。
- 解析:原始地址不变。
-
IMAGE_REL_BASED_HIGH (1) :
- 描述:高 8 位重定位。
- 解析:将高 8 位的偏移量加到目标地址的高 16 位。
-
IMAGE_REL_BASED_LOW (2) :
- 描述:低 8 位重定位。
- 解析:将低 8 位的偏移量加到目标地址的低 16 位。
-
IMAGE_REL_BASED_HIGHLOW (3) :
- 描述:32 位重定位。
- 解析:调整整个 32 位地址。
-
IMAGE_REL_BASED_HIGHADJ (4) :
- 描述:高位调整重定位,用于进行高 16 位和低 16 位的调整。
- 解析:调整高 16 位,并可能影响后续低 16 位的计算。
-
IMAGE_REL_BASED_MACHINE_SPECIFIC_5 (5) :
- 描述:机器特定的重定位,具体实现依赖于架构。
- 解析:根据特定机器的需求进行处理。
-
IMAGE_REL_BASED_RESERVED (6) :
- 描述:保留类型,未定义。
- 解析:不应用任何重定位。
-
IMAGE_REL_BASED_MACHINE_SPECIFIC_7 (7) :
- 描述:另一个机器特定的重定位。
- 解析:根据特定机器的需求进行处理。
-
IMAGE_REL_BASED_MACHINE_SPECIFIC_8 (8) :
- 描述:另一个机器特定的重定位。
- 解析:根据特定机器的需求进行处理。
-
IMAGE_REL_BASED_MACHINE_SPECIFIC_9 (9) :
- 描述:另一个机器特定的重定位。
- 解析:根据特定机器的需求进行处理。
-
IMAGE_REL_BASED_DIR64 (10) :
- 描述:用于 64 位重定位。
- 解析:调整 64 位地址。
重定位项的具体解析代码如下:
void ProcessRelocation(PRELOCATION_INFO relocInfo, DWORD baseAddress, PIMAGE_NT_HEADERS pNtHeaders, char* buffer) {
DWORD offset = relocInfo->Offset; // 获取偏移
DWORD dwRva = 0; // 初始化新虚拟地址
std::string str;
// 根据重定位类型处理不同的情况
switch (relocInfo->Type) {
case IMAGE_REL_BASED_ABSOLUTE:
// 不做任何调整,原始地址保持不变
str = "ABSOLUTE";
break;
case IMAGE_REL_BASED_HIGH:
// 高 8 位重定位:将高 8 位的偏移加入到基地址的高 16 位
dwRva = (baseAddress & 0xFFFF0000) | (offset << 12);
str = "HIGH";
break;
case IMAGE_REL_BASED_LOW:
// 低 8 位重定位:将低 8 位的偏移加入到基地址的低 16 位
dwRva = (baseAddress & 0xFFFF0000) | (offset & 0x0FFF);
str = "LOW";
break;
case IMAGE_REL_BASED_HIGHLOW:
// 32 位重定位:直接将偏移量加到基地址
dwRva = baseAddress + offset;
str = "HIGHLOW";
break;
case IMAGE_REL_BASED_HIGHADJ:
// 高位调整重定位:需要特殊处理
str = "HIGHADJ";
break;
case IMAGE_REL_BASED_MACHINE_SPECIFIC_5:
str = "SPECIFIC_5";
break;
case IMAGE_REL_BASED_MACHINE_SPECIFIC_7:
str = "SPECIFIC_7";
break;
case IMAGE_REL_BASED_MACHINE_SPECIFIC_8:
str = "SPECIFIC_8";
break;
case IMAGE_REL_BASED_MACHINE_SPECIFIC_9:
// 机器特定的重定位:根据特定机器的需求进行处理
str = "SPECIFIC_9";
break;
case IMAGE_REL_BASED_RESERVED:
// 保留类型:不进行任何调整
str = "RESERVED";
break;
case IMAGE_REL_BASED_DIR64:
// 64 位重定位:需要进行 64 位地址的调整
dwRva = baseAddress + offset;
str = "DIR64";
break;
default:
// 未知的重定位类型:输出错误信息
printf("Unknown relocation type: %d\n", relocInfo->Type);
str = "Unknown";
break;
}
printf("RVA(%s): 0x%lX \r\n", str.c_str(), dwRva);
if (dwRva != 0)
{
DWORD dwFoa = RvaToFoa(dwRva, pNtHeaders);
printf("FOA(%s): 0x%lX\r\n", str.c_str(), dwFoa);
DWORD64 dwFarCall = *(DWORD64*)(dwFoa + buffer);
printf("FarCall(%s): 0x%016llX\r\n\n", str.c_str(), dwFarCall);
// printf("data(%s): 0x%lX\r\n\n", szName, *(char*)dwFarCall);
}
}
整体结构如下:
PIMAGE_DATA_DIRECTORY
├── VirtualAddress // 数据目录的虚拟地址
├── Size // 数据目录的大小
└── (后续的 PIMAGE_BASE_RELOCATION) // 基址重定位表的条目
├── PIMAGE_BASE_RELOCATION (Entry 1)
│ ├── VirtualAddress // 需要重定位的虚拟地址
│ ├── SizeOfBlock // 该块的大小
│ └── (后续的重定位信息)
│ ├── RELOCATION_INFO (Entry 1)
│ │ ├── Offset // 12 位偏移
│ │ └── Type // 4 位重定位类型
│ └── RELOCATION_INFO (Entry 2)
│ ├── Offset
│ └── Type
└── PIMAGE_BASE_RELOCATION (Entry 2)
├── VirtualAddress
├── SizeOfBlock
└── (后续的重定位信息)
├── RELOCATION_INFO (Entry 1)
└── RELOCATION_INFO (Entry 2)
解析代码如下:
void RelocTable(PIMAGE_NT_HEADERS pNtHeaders, char* buffer)
{
DWORD dwRva = 0;
PWORD pRelocData = NULL;
// 获取重定位表目录
PIMAGE_DATA_DIRECTORY pRelocDir = pNtHeaders->OptionalHeader.DataDirectory + IMAGE_DIRECTORY_ENTRY_BASERELOC;
PIMAGE_BASE_RELOCATION pReloc = (PIMAGE_BASE_RELOCATION)(RvaToFoa(pRelocDir->VirtualAddress, pNtHeaders) + buffer);
while (pReloc->VirtualAddress != 0 && pReloc->SizeOfBlock != 0)
{
printf("Rva: %08X\r\n", pReloc->VirtualAddress);
char* szSectionName = GetSectionName(pReloc->VirtualAddress, pNtHeaders);
if (szSectionName) {
printf("Section: %s\r\n", szSectionName);
delete[] szSectionName; // 释放内存
}
else {
printf("Section: Not found\r\n");
}
// 获取重定位项的首地址
PRELOCATION_INFO pRelocation = (PRELOCATION_INFO)((char*)pReloc + sizeof(IMAGE_BASE_RELOCATION));
// 计算重定位项数量,每一项占用2字节(16位,前12位表示偏移,4位表示重定位类型)
DWORD dwItems = (pReloc->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(RELOCATION_INFO);
printf("Items: %X h / %d d\r\n", dwItems, dwItems);
for (DWORD i = 0; i < dwItems; i++) {
ProcessRelocation(pRelocation, pReloc->VirtualAddress, pNtHeaders, buffer);
pRelocation++;
}
// 找出下一个重定位块
pReloc = (PIMAGE_BASE_RELOCATION)((char*)pReloc + pReloc->SizeOfBlock);
}
}
未完待续....