其他
原创 | 深入解析pe结构(下)
数据目录表结构
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress; //内存偏移
DWORD Size; //大小
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
这16个表是指PE文件中描述各种数据结构的数据目录表,具体如下:
静态链接库与动态链接库
新建项目时选择静态链接库
cpp文件
int Plus(int x,int y)
{
return x+y;
}
int Sub(int x,int y)
{
return x-y;
}
int Mul(int x,int y)
{
return x*y;
}
int Div(int x,int y)
{
return x/y;
}
#pragma once
int Plus(int x,int y);
int Sub(int x,int y);
int Mul(int x,int y);
int Div(int x,int y);
头文件
extern "C" _declspec(dllexport) int Plus (int x,int y);
extern "C" _declspec(dllexport) int Sub (int x,int y);
extern "C" _declspec(dllexport) int Mul (int x,int y);
extern "C" _declspec(dllexport) int Div (int x,int y);
int Plus(int x,int y)
{
return x+y;
}
int Sub(int x,int y)
{
return x-y;
}
int Mul(int x,int y)
{
return x*y;
}
int Div(int x,int y)
{
return x/y;
}
#pragma comment(lib,"dllmy.lib")
extern "C" __declspec(dllimport) int Plus (int x,int y);
extern "C" __declspec(dllimport) int Sub (int x,int y);
extern "C" __declspec(dllimport) int Mul (int x,int y);
extern "C" __declspec(dllimport) int Div (int x,int y);
int main(int argc, char* argv[])
{
int x=Plus(1,1);
printf("%d\n",x);
return 0;
}
定位导入表
导出表
typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics;// 未使用
DWORD TimeDateStamp;// 时间戳
WORD MajorVersion;// 未使用
WORD MinorVersion;// 未使用
DWORD Name;// 指向该导出表文件名字符串
DWORD Base;// 导出函数起始序号
DWORD NumberOfFunctions;// 所有导出函数的个数
DWORD NumberOfNames;// 以函数名字导出的函数个数
DWORD AddressOfFunctions; // 导出函数地址表RVA
DWORD AddressOfNames; // 导出函数名称表RVA
DWORD AddressOfNameOrdinals; // 导出函数序号表RVA
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
名字导出:
根据序号导出和AddressOfNameOrdinals没有关系,如果给出的值为5,那么真正的地址位置就是5-base,之后去AddressOfFunctions寻找下标即可
注意:导出时要进行RVA转FOA的转换
#include <iostream>
#include <windows.h>
LPVOID ReadPE(
IN LPCSTR lpszName
){
FILE* file = nullptr;
fopen_s(&file, lpszName, "rb");
if (!file)
{
printf("打开文件失败!\n");
return nullptr;
}
fseek(file, 0, SEEK_END);
size_t size = ftell(file);
fseek(file, 0, SEEK_SET);
LPVOID fileBuff = malloc(size);
if (!fileBuff)
{
printf("申请内存空间失败!\n");
fclose(file);
return nullptr;
}
fread_s(fileBuff, size, 1, size, file);
WORD mz = * ((PWORD)fileBuff);
if (mz != 0x5a4d)
{
printf("该文件不是pe可执行程序!\n");
fclose(file);
free(fileBuff);
return nullptr;
}
return fileBuff;
}
void AnlyzePE(
IN LPVOID pe,
OUT PIMAGE_DOS_HEADER& dos,
OUT PIMAGE_FILE_HEADER& file,
OUT PIMAGE_OPTIONAL_HEADER32& optional,
OUT PIMAGE_SECTION_HEADER*& section
) {
dos = (PIMAGE_DOS_HEADER)pe;
file = (PIMAGE_FILE_HEADER)((PCHAR)pe + dos->e_lfanew + 4);
optional = (PIMAGE_OPTIONAL_HEADER32)((PCHAR)pe + dos->e_lfanew + 4 + 20);
section = (PIMAGE_SECTION_HEADER*)malloc(file->NumberOfSections * sizeof(IMAGE_SECTION_HEADER));
if (section != nullptr)
{
for (int i = 0; i < file->NumberOfSections; i++)
{
*(section + i) = (PIMAGE_SECTION_HEADER)((PCHAR)pe + dos->e_lfanew + 4 + 20 + file->SizeOfOptionalHeader + (i * sizeof(IMAGE_SECTION_HEADER)));
}
}
}
DWORD RvaToFoa(
IN LPVOID pe,
IN UINT_PTR rva
){
PIMAGE_DOS_HEADER dos = nullptr;
PIMAGE_FILE_HEADER file = nullptr;
PIMAGE_OPTIONAL_HEADER32 optional = nullptr;
PIMAGE_SECTION_HEADER* section = nullptr;
AnlyzePE(pe, dos, file, optional, section);
DWORD foa = -1;
for (int i = 0; i < file->NumberOfSections; i++)
{
UINT_PTR begin = (*section + i)->VirtualAddress;
UINT_PTR end = (*section + i)->VirtualAddress + (*section + i)->SizeOfRawData;
if (begin <= rva && rva <= end)
{
foa = rva - begin + (*section + i)->PointerToRawData;
break;
}
}
free(section);
return foa;
}
void PrintExport(
IN LPVOID fileBuff
){
PIMAGE_DOS_HEADER dos = nullptr;
PIMAGE_FILE_HEADER file = nullptr;
PIMAGE_OPTIONAL_HEADER32 optional = nullptr;
PIMAGE_SECTION_HEADER* section = nullptr;
AnlyzePE(fileBuff, dos, file, optional, section);
DWORD offset = RvaToFoa(fileBuff, optional->DataDirectory[0].VirtualAddress);
PIMAGE_EXPORT_DIRECTORY exportTable = (PIMAGE_EXPORT_DIRECTORY)((PCHAR)fileBuff + offset);
printf(">>>> 导出表 <<<<\n");
printf("Characteristics =%x\n", exportTable->Characteristics);
printf("TimeDateStamp =%x\n", exportTable->TimeDateStamp);
printf("MajorVersion =%x\n", exportTable->MajorVersion);
printf("MinorVersion =%x\n", exportTable->MinorVersion);
printf("Name =%x\n", exportTable->Name);
printf("Base =%x\n", exportTable->Base);
printf("NumberOfFunctions =%x\n", exportTable->NumberOfFunctions);
printf("NumberOfNames =%x\n", exportTable->NumberOfNames);
printf("AddressOfFunctions =%x\n", exportTable->AddressOfFunctions);
printf("AddressOfNames =%x\n", exportTable->AddressOfNames);
printf("AddressOfNameOrdinals =%x\n", exportTable->AddressOfNameOrdinals);
DWORD(*function)[1];
function = (DWORD(*)[1])((PCHAR)fileBuff + RvaToFoa(fileBuff, exportTable->AddressOfFunctions));
printf(">>>> Functions <<<<\n");
for (int i = 0; i < exportTable->NumberOfFunctions; i++)
{
printf("%d = %x\n", i, *(*(function)+i));
}
WORD(*ordinal)[1];
ordinal = (WORD(*)[1])((PCHAR)fileBuff + RvaToFoa(fileBuff, exportTable->AddressOfNameOrdinals));
printf(">>>> Ordinals <<<<\n");
for (int i = 0; i < exportTable->NumberOfFunctions; i++)
{
printf("%d = %x\n", i, *(*(ordinal)+i));
}
DWORD(*name)[1];
name = (DWORD(*)[1])((PCHAR)fileBuff + RvaToFoa(fileBuff, exportTable->AddressOfNames));
printf(">>>> Names <<<<\n");
for (int i = 0; i < exportTable->NumberOfFunctions; i++)
{
printf("%d = %s\n", i, (PCHAR)fileBuff + RvaToFoa(fileBuff, *(*(name)+i)));
}
free(section);
return;
}
bool M_strcmp(
IN char* s1,
IN char* s2
) {
int length = strlen(s1);
if (length != strlen(s2))
{
return false;
}
else
{
for (int i = 0; i < length; i++)
{
if (s1[i] != s2[i])
{
return false;
}
}
}
return true;
}
LPVOID GetFunctionAddrByName(
IN LPVOID pe,
IN LPCSTR funcName
){
PIMAGE_DOS_HEADER dos = nullptr;
PIMAGE_FILE_HEADER file = nullptr;
PIMAGE_OPTIONAL_HEADER32 optional = nullptr;
PIMAGE_SECTION_HEADER* section = nullptr;
AnlyzePE(pe, dos, file, optional, section);
DWORD offset = RvaToFoa(pe, optional->DataDirectory[0].VirtualAddress);
PIMAGE_EXPORT_DIRECTORY exportTable = (PIMAGE_EXPORT_DIRECTORY)((PCHAR)pe + offset);
DWORD(*function)[1];
function = (DWORD(*)[1])((PCHAR)pe + RvaToFoa(pe, exportTable->AddressOfFunctions));
WORD(*ordinal)[1];
ordinal = (WORD(*)[1])((PCHAR)pe + RvaToFoa(pe, exportTable->AddressOfNameOrdinals));
DWORD(*name)[1];
name = (DWORD(*)[1])((PCHAR)pe + RvaToFoa(pe, exportTable->AddressOfNames));
for (int i = 0; i < exportTable->NumberOfFunctions; i++)
{
LPCSTR tempName = (PCHAR)pe + RvaToFoa(pe, *(*(name)+i));
if (M_strcmp((char*)tempName, (char*)funcName))
{
DWORD funcIndex = *(*(ordinal)+i);
free(section);
return (LPVOID)*(*(function)+funcIndex);
}
}
free(section);
return nullptr;
}
LPVOID GetFunctionAddrByOrdinal(
IN LPVOID pe,
IN DWORD exportNumber
){
PIMAGE_DOS_HEADER dos = nullptr;
PIMAGE_FILE_HEADER file = nullptr;
PIMAGE_OPTIONAL_HEADER32 optional = nullptr;
PIMAGE_SECTION_HEADER* section = nullptr;
AnlyzePE(pe, dos, file, optional, section);
DWORD offset = RvaToFoa(pe, optional->DataDirectory[0].VirtualAddress);
PIMAGE_EXPORT_DIRECTORY exportTable = (PIMAGE_EXPORT_DIRECTORY)((PCHAR)pe + offset);
DWORD(*function)[1];
function = (DWORD(*)[1])((PCHAR)pe + RvaToFoa(pe, exportTable->AddressOfFunctions));
free(section);
return (LPVOID)*(*(function)+(exportNumber - exportTable->Base));
}
int main()
{
LPVOID fileBuff = ReadPE(R"(D:\Data\Project\VisualStudio2022\WaterDroplet\Debug\ds.dll)");
if (fileBuff) {
PrintExport(fileBuff);
LPVOID addAddress = GetFunctionAddrByName(fileBuff, "add");
LPVOID maxAddress = GetFunctionAddrByOrdinal(fileBuff, 5);
printf("null\n");
}
free(fileBuff);
system("pause");
return 0;
}
重定位表
修正方法:需要重定位的地址 + 偏移(当前基址 - PE的基址) 开了随机基址的程序才需要重定位,而DLL通常都有重定位表,因为不一定能够加载到DLL指定的ImageBase上。
先查看随机地址标志,标志开启,地址重定位 再查看数据目录项 5 是否位NULL,不为NULL,基址重定位。
怎么保存需要重定位的数据地址呢? 按分页存,每个分页中需要重定位的地址(基于分页值的偏移) 优点:存储空间小,一个分页有多个地址需要重定位时,只需要存一个分页即可。2字节,和全是2 字节的偏移值。
00001000 0000 0024 0078
需要修的位置有 :1000 + 24 = 1024,1078;
重定位表描述待修复的值所在的地方,这个值是一个RVA。数据目录处的Size字段有用,是重定位表的总大小。
重定位表位于数据目录第3项。
typedef struct _IMAGE_BASE_RELOCATION {
DWORD VirtualAddress;//重定位数据所在页的RVA
DWORD SizeOfBlock;//当前页中重定位数据块的总大小
// WORD TypeOffset[1];//重定位项数组
} IMAGE_BASE_RELOCATION;
VirtualAddress 这个虚拟地址是一组重定位数据的开始RVA地址,只有重定位项的有效数据加上这个值才是重定位数据真正的RVA地址 SizeOfBlock 它是当前重定位块的总大小,因为VirtualAddress和SizeOfBlock都是4字节的,所以(SizeOfBlock - 8)才是该块所有重定位项的大小,(SizeOfBlock - 8) / 2就是该块所有重定位项的数目。 TypeOffset[1] 重定位项在该结构中没有体现出来,他的位置是紧挨着这个结构的,可以把他当作一个数组,宽度为2字节。表示该地址处有一个地址需要进行重定位
每一个重定位项分为两个部分:高4位和低12位
高4位表示了重定位数据的类型(0x00没有任何作用仅仅用作数据填充,为了4字节对齐。0x03表示这个数据是重定位数据,需要修正。0x0A出现在64位程序中,也是需要修正的地址)
低12位就是重定位数据相对于VirtualAddress的偏移,也就是上面所说的有效数据。之所以是12位,是因为12位的大小足够表示该块中的所有地址(每一个数据块表示一个页中的所有重定位数据,一个页的大小位0x1000)。
:(VA - ImageBase) + NewImageBase
重定位表应用之 LoadDll
介绍:Dll 加载器 ,单独装载一个独立的DLL 返回值:返回模块的实例句柄 思考:
Dll的代码装载到哪个内存?申请一块内存,进行装载。 处理重定位数据,遍历重定位表
申请Dll 装载所需的内存空间 拷贝PE头 根据节表拷贝节,对齐空隙使用 00 填充 拷贝节 处理导入表 处理重定位表 清理资源 返回模块句柄
处理重定位 获取分页内偏移数组地址 和 需要重定位的偏移个数 判断 TypeOffset 是否为填充 00。 是:跳过不处理,进行下一个 否:需要重定位,修正该地址处的重定位数据
GetprocAddress 从模块链里面找模块。TEB里面 DLL加载成功后,不能摸出DLL的MZ 和 PE 标志么? 不能。有的API调用的时候会检测这两个标志。 应用:API模拟,反dump
不调用LoadLibary,使用远程注入的方式 DLL的内容注入到别人的进程里,然后调用DllMian 远程线程调用Dllmain。
导入表
IAT与INT
导入表结构
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics; // 0 for terminating null import descriptor
DWORD OriginalFirstThunk; // RVA to original unbound IAT (PIMAGE_THUNK_DATA)
} DUMMYUNIONNAME;
DWORD TimeDateStamp; // 0 if not bound,
// -1 if bound, and real date\time stamp
// in IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (new BIND)
// O.W. date/time stamp of DLL bound to (Old BIND)
DWORD ForwarderChain; // -1 if no forwarders
DWORD Name;
DWORD FirstThunk; // RVA to IAT (if bound this IAT has actual addresses)
} IMAGE_IMPORT_DESCRIPTOR;
typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;
OriginalFirstThunk: 指向导入函数名称表(Import Name Table,INT)的指针,这个表中存储了需要导入DLL中的函数名字。 TimeDateStamp:DLL文件的创建时间或重新绑定时间戳。 ForwarderChain:指向一个链表,记录该DLL导入的其他DLL列表(也就是链表的下一个DLL)。 Name:DLL的名称。 FirstThunk:指向导入地址表(Import Address Table,IAT)的指针,这个表中存储了需要导入DLL中的函数的地址。
导入表遍历
检查Name 和FirstThunk ,如果任一为NULL,则停止遍历 取FirstThunk 的项(数组中的元素),如果为NULL, 就取OriginalFirstThunk 对应的项,如果为NULL,则遍历下一项 判断项的最高位,如果为1,则取低WORD为序号,如果为0,则作为RVA 取出IMAGE_IMPORT_BY_NAME 中的函数名 循环遍历下一项
while(Name != NULL && FirstThunK != NULL)
{
IMAGE_DATA_THUNK* pTmpThunk = OriginalFirstThunk;
if(OriginalFirstThunk == NULL)
{
pTmpThunk = FirstThunk;
}
while(*pTmpThunk != NULL)
{
if(*pTmpThunk & 0x80000000)
{
WORD dwNumber = *pTmpThunk & 0xffff; //低字为序号
}
else
{
IMAGE_IMPORT_BY_NAME* pName = *pTmpThunk;//获取导入函数名称的RVA
}
pTmpThunk++;
}
}
隐藏导入函数
#include <Windows.h>
#include <stdio.h>
typedef int (__stdcall *pMessageBoxA)(
_In_opt_ HWND hWnd,
_In_opt_ LPCSTR lpText,
_In_opt_ LPCSTR lpCaption,
_In_ UINT uType);
int main() {
char arr[] = { 'M','e','s','s','a','g','e','B','o','x','A',0 };
HMODULE hdll = LoadLibraryA("user32.dll");
pMessageBoxA pfun0 = (pMessageBoxA)GetProcAddress(hdll, "MessageBoxA");
pfun0(0, "hhhhh", "ccccc", 0);
}
绑定导入表
一般情况下,在程序加载前IAT表和INT表中的内容相同,都是程序引用的dll中的函数的函数名或序号;
加载完成后IAT表中将替换为函数的真正地址;
但在加载前IAT表中直接写绝对地址是可以实现的;
加载前在IAT表中保存绝对地址的优点:
启动程序快; 在启动程序时需要:申请4gb内存空间、贴exe、贴dll、将IAT表修复为地址等等; 如果直接用绝对地址,则省去了修复IAT表的操作;
dll重定位时,如果dll没能占据自身ImageBase处的地址,则需要修复绝对地址; dll被修改时,dll被修改,IAT表中对应的函数地址可能被改,需要修复函数地址;
在导入表中结构中有个属性:TimeDateStamp;
该属性表示时间戳;
如果值为0则表示当前的dll的函数没有被绑定,在程序加载时会调用系统函数获取函数地址;
如果值为-1则表示当前的dll的函数已经绑定,而且绑定的时间存在另外一张表里;那张表就是绑定导入表;
typedef struct _IMAGE_BOUND_IMPORT_DESCRIPTOR {
DWORD TimeDateStamp;
WORD OffsetModuleName;
WORD NumberOfModuleForwarderRefs; // Array of zero or more IMAGE_BOUND_FORWARDER_REF follows
} IMAGE_BOUND_IMPORT_DESCRIPTOR, *PIMAGE_BOUND_IMPORT_DESCRIPTOR;
#include "PE.h"
bool StructIsNull(
IN LPVOID obj,
IN size_t size
) {
for (DWORD i = 0; i < size; i++)
{
if (*((PCHAR)obj + i) != 0)
{
return false;
}
}
return true;
}
void PrintBoundImportTable(
IN LPVOID pe
) {
PIMAGE_DOS_HEADER pDosHeader = nullptr;
PIMAGE_FILE_HEADER pFileHeader = nullptr;
PIMAGE_OPTIONAL_HEADER32 pOptionalHeader = nullptr;
PIMAGE_SECTION_HEADER* pSectionHeaderArr = nullptr;
AnlyzePE(pe, pDosHeader, pFileHeader, pOptionalHeader, pSectionHeaderArr);
PIMAGE_BOUND_IMPORT_DESCRIPTOR pBoundImportTable = (PIMAGE_BOUND_IMPORT_DESCRIPTOR)((PCHAR)pe + RvaToFoa(pe, pOptionalHeader->DataDirectory[12].VirtualAddress));
PIMAGE_BOUND_IMPORT_DESCRIPTOR pFirstBoundImportTable = pBoundImportTable;
while (!StructIsNull(pBoundImportTable, sizeof(*pBoundImportTable)))
{
printf(">>>>>>>>>> 主DLL <<<<<<<<<<\n");
printf("主DLL绑定时间戳 = %d\n", pBoundImportTable->TimeDateStamp);
printf("主DLL名称 = %s\n", (PCHAR)pFirstBoundImportTable + pBoundImportTable->OffsetModuleName);
printf("主DLL依赖数量 = %d\n", pBoundImportTable->NumberOfModuleForwarderRefs);
printf(">>>>> 副DLL <<<<<\n");
for (DWORD i = 0; i < pBoundImportTable->NumberOfModuleForwarderRefs; i++)
{
PIMAGE_BOUND_FORWARDER_REF rely = (PIMAGE_BOUND_FORWARDER_REF)((PCHAR)pBoundImportTable + (sizeof(IMAGE_BOUND_FORWARDER_REF) * i));
printf("依赖DLL绑定时间戳 = %d\n", rely->TimeDateStamp);
printf("依赖DLL名称 = %s\n", (PCHAR)pFirstBoundImportTable + rely->OffsetModuleName);
}
pBoundImportTable = (PIMAGE_BOUND_IMPORT_DESCRIPTOR)((PCHAR)pBoundImportTable + (sizeof(IMAGE_BOUND_FORWARDER_REF) * pBoundImportTable->NumberOfModuleForwarderRefs));
}
free(pSectionHeaderArr);
}
int main()
{
LPVOID pe = ReadPE("xxx");
if (pe)
{
PrintBoundImportTable(pe);
free(pe);
}
return 0;
}
往期推荐
原创 | 2023 CISCN 第十六届全国大学生信息安全竞赛初赛 WriteUp