数据目录表结构
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress; //内存偏移
DWORD Size; //大小
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
这16个表是指PE文件中描述各种数据结构的数据目录表,具体如下:
导出表(Export Table) 导入表(Import Table) 资源表(Resource Table) 异常处理表(Exception Table) 安全相关表(Certificate Table) 重定位表(Base Relocation Table) 调试信息表(Debugging Information Table) 版权信息表(Architecture-Specific Data Table) 全局指针寄存器表(Global Pointer Register Table) TLS表(Thread Local Storage Table) 负载配置表(Load Configuration Table) 网络地址转换表(Bound Import Table) 导出地址表(Import Address Table) 延迟加载导入表(Delay Import Table) COM运行时描述表(COM Runtime Descriptor Table) 保留(Reserved) 其中导入表,导出表,重定位表,IAT表等表比较重要
静态链接库与动态链接库
新建项目时选择静态链接库
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;
}
头文件 int Plus(int x,int y);
int Sub(int x,int y);
int Mul(int x,int y);
int Div(int x,int y);
新建项目将生成的lib文件和头文件复制到新项目中 引用即可
但是这种静态链接库是直接加载到了程序中,没有实现模块化 动态链接库的生成与使用
头文件
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);
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;
}
将生成的lib和dll文件放入到新建项目中进行导入
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("%dn",x);
return 0;
}
已经在OD发现我们的自定义dll了
定位导入表
这16个数据目录表所描述的各种数据结构,实际上是存储在文件的不同区段(节表)中的,根据上面的数据目录表结构VirtualAddress是内存地址,那如何定位在文件中的地址呢 根据软件查看导入表的RVA为2D0C0
通过查看节表发现第二个区块的 VirtualAddress+VirtualSize是大于导入表的RVA,因此导入表是包含在第二个区块内的
因此导入表的RVA-区块的RVA得出导入表相对于区块的位置,然后加上区块的PointerToRawData也就是导入表在区块中的实际位置
导出表
导出表结构: 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;
pe结构的导出方式一共分为两种,一种是名字导出,一种是序号导出
名字导出:如图为: AddressOfFunctions,AddressOfNameOrdinals,AddressOfNames的表结构
其中AddressOfFunctions存的是函数的地址 AddressOfNameOrdinals为序号表 AddressOfNames中存的是函数名称的地址 假如我们要导出test函数,那么计算机首先会向AddressOfNames的地址进行遍历,如果遍历到了并且发现在下标为2遍历到的,那么就会去寻找AddressOfNameOrdinals下标为2存储的序号,发现为4,然后去AddressOfFunctions寻找下标为4的地址,这个地址就是函数的真正地址 序号导出:
根据序号导出和AddressOfNameOrdinals没有关系,如果给出的值为5,那么真正的地址位置就是5-base,之后去AddressOfFunctions寻找下标即可
注意:导出时要进行RVA转FOA的转换编写程序打印所有的导出表信息:
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 =%xn", exportTable->Characteristics);
printf("TimeDateStamp =%xn", exportTable->TimeDateStamp);
printf("MajorVersion =%xn", exportTable->MajorVersion);
printf("MinorVersion =%xn", exportTable->MinorVersion);
printf("Name =%xn", exportTable->Name);
printf("Base =%xn", exportTable->Base);
printf("NumberOfFunctions =%xn", exportTable->NumberOfFunctions);
printf("NumberOfNames =%xn", exportTable->NumberOfNames);
printf("AddressOfFunctions =%xn", exportTable->AddressOfFunctions);
printf("AddressOfNames =%xn", exportTable->AddressOfNames);
printf("AddressOfNameOrdinals =%xn", 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 = %xn", 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 = %xn", 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 = %sn", 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:DataProjectVisualStudio2022WaterDropletDebugds.dll)");
if (fileBuff) {
PrintExport(fileBuff);
LPVOID addAddress = GetFunctionAddrByName(fileBuff, "add");
LPVOID maxAddress = GetFunctionAddrByOrdinal(fileBuff, 5);
printf("nulln");
}
free(fileBuff);
system("pause");
return 0;
}
重定位表
定义:记录需要绝对地址修正的表,大多数绝对地址如果imagebase变化的话就无法使用,需要修正程序所调用的那些绝对地址。
-
修正方法:需要重定位的地址 + 偏移(当前基址 - 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 datetime 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;
该结构体定义了一个DLL的导入信息,包括以下几项:
-
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++;
}
}
隐藏导入函数
把MessageBoxA当做常量还是能找到 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;
打印绑定导入表
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绑定时间戳 = %dn", pBoundImportTable->TimeDateStamp);
printf("主DLL名称 = %sn", (PCHAR)pFirstBoundImportTable + pBoundImportTable->OffsetModuleName);
printf("主DLL依赖数量 = %dn", 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绑定时间戳 = %dn", rely->TimeDateStamp);
printf("依赖DLL名称 = %sn", (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;
}
pe结构是学习二进制以及免杀必备的知识,不能只看理论要多练多敲才能掌握真谛。 往期推荐
原文始发于微信公众号(SecIN技术平台):原创 | 深入解析pe结构(下)
免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论