PE文件结构解析3
0x0导读
今天的内容是pe文件结构的导出表,和如何使用代码打印出导出表。
0x1环境
编译器:VirsualStudio2022
16进制查看工具:winhex
0x2导出表
0x1导出表是啥
导出表就是当前的PE文件有哪些函数可以被别人使用. 比如去饭店吃饭一般都会有一个菜单来说这家饭店都有什么菜,pe文件相当于这家饭店,而导出表就相当于菜单,来说明这个pe文件都有哪些函数可以被别人使用,注意并不是只有DLL才有导出表,并不是只有DLL才可以提供函数给别人使用Exe也可以。
0x2导出表结构
导出表的定义
typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics;
DWORD TimeDateStamp;
WORD MajorVersion;
WORD MinorVersion;
DWORD Name;
DWORD Base;
DWORD NumberOfFunctions;
DWORD NumberOfNames;
DWORD AddressOfFunctions; // RVA from base of image
DWORD AddressOfNames; // RVA from base of image
DWORD AddressOfNameOrdinals; // RVA from base of image
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
Characteristics
保留,一般为0.
TimeDateStamp
时间戳,导出表生成的时间。
MajorVersion
主版本号。
MinorVersion
次版本号
Name
一个rva,指向当前dll的名字。
Base
导出函数起始序号
NumberOfFunctions
导出函数的个数。
NumberOfNames
以名字导出函数个数。
AddressOfFunctions
一个rva,指向导出函数地址表,这个表里面存的是函数地址,表中的值大小是4字节
AddressOfNames
一个rva,指向导出函数名称表,这个表里面放的是函数名字地址,是一个rva,表中的值大小是4字节
AddressOfNameOrdinals
一个rva,指向导出函数序号表,放的是函数的序号,通过这个序号可以找到函数地址,表中值大小是2字节
0x3代码解析
大致思路:读取文件到内存中,定位到导出表,定位导出函数地址表,定位导出函数序号表,定位导出函数名字表,循环NumberOfSections
成员的值,拿这个值去和导出函数序号表中的值比对,如果一样就得到当前导出函数序号的下标,拿这个下标去函数名字表中找到对应的地址,的到函数名,再拿导出函数序号表的值加上base
成员得到导出函数序号,在通过导出函数序号的值得到函数地址。
#include <stdio.h>
#include <Windows.h>
#define path "C:\\Users\\blue\\Desktop\\CmdBar.dll"
DWORD rtf(char* buffer, DWORD rva)
{
PIMAGE_DOS_HEADER doshd = (PIMAGE_DOS_HEADER)buffer;
PIMAGE_NT_HEADERS nthd = (PIMAGE_NT_HEADERS)(buffer + doshd->e_lfanew);
PIMAGE_FILE_HEADER filehd = (PIMAGE_FILE_HEADER)(buffer + doshd->e_lfanew + 4);
PIMAGE_OPTIONAL_HEADER32 optionhd = (PIMAGE_OPTIONAL_HEADER32)(buffer + doshd->e_lfanew + 24);
PIMAGE_SECTION_HEADER sectionhd = IMAGE_FIRST_SECTION(nthd);
for (int i = 0; i < filehd->NumberOfSections; i++)
{
if (rva >= sectionhd[i].VirtualAddress && rva <= sectionhd[i].VirtualAddress + sectionhd[i].SizeOfRawData)
{
return rva - sectionhd[i].VirtualAddress + sectionhd[i].PointerToRawData;
}
}
}
void main()
{
FILE* fp = fopen(path, "rb");
fseek(fp, 0, SEEK_END);
int size = ftell(fp);
rewind(fp);
char* ptr = (char*)malloc(size + 0x1000);
memset(ptr, 0, size + 0x1000);
fread(ptr, size, 1, fp);
PIMAGE_DOS_HEADER Dos = (PIMAGE_DOS_HEADER)ptr;
PIMAGE_NT_HEADERS Nt = (PIMAGE_NT_HEADERS)(ptr + Dos->e_lfanew);
PIMAGE_FILE_HEADER File = (PIMAGE_FILE_HEADER)(ptr + Dos->e_lfanew + 4);
PIMAGE_OPTIONAL_HEADER Option = (PIMAGE_OPTIONAL_HEADER)(ptr + Dos->e_lfanew + 24);
PIMAGE_DATA_DIRECTORY Data = Option->DataDirectory;
PIMAGE_SECTION_HEADER Sec = IMAGE_FIRST_SECTION(Nt);
PIMAGE_EXPORT_DIRECTORY Exp = (PIMAGE_EXPORT_DIRECTORY)(ptr + rtf(ptr, Data[0].VirtualAddress));
PBYTE DllName = (PBYTE)ptr + rtf(ptr, Exp->Name);
printf("导出表\n");
printf("Characteristics:%x\n", Exp->Characteristics);
printf("TimeDateStamp:%x\n", Exp->TimeDateStamp);
printf("MajorVersion:%x\n", Exp->MajorVersion);
printf("MinorVersion:%x\n", Exp->MinorVersion);
printf("Name_Rva:%x\n", Exp->Name);
printf("Name:%s\n", DllName);
printf("Base:%x\n", Exp->Base);
printf("NumberOfFunctions:%x\n", Exp->NumberOfFunctions);
printf("NumberOfNames:%x\n", Exp->NumberOfNames);
printf("AddressOfFunctions:%x\n", Exp->AddressOfFunctions);
printf("AddressOfNames:%x\n", Exp->AddressOfNames);
printf("AddressOfNameOrdinals:%x\n", Exp->AddressOfNameOrdinals);
PBYTE AddressOfFunctions = (PBYTE)(ptr + rtf(ptr, Exp->AddressOfFunctions));
PWORD AddressOfOrdinals = (PWORD)(ptr + rtf(ptr, Exp->AddressOfNameOrdinals));
PDWORD AddressOfNames = (PDWORD)(ptr + rtf(ptr, Exp->AddressOfNames));
BOOL flag = FALSE;
for (int i = 0; i < Exp->NumberOfFunctions; i++)
{
int k = 0;
for (; k < Exp->NumberOfNames; k++)
{
if (i == AddressOfOrdinals[k])
{
flag = TRUE;
break;
}
}
if (flag == TRUE)
{
PBYTE FunName = (PBYTE)ptr + rtf(ptr, AddressOfNames[k]);
printf("Function:[%s][%d][%x]\n", FunName, k + Exp->Base, AddressOfFunctions[AddressOfOrdinals[k]]);
}
}
getchar();
}
因为读取文件的代码和rva转换foa的代码,定位头的代码已经讲过了,关于这一部分代码的解释请移步我的上一篇文章,如果有不懂的可以评论,现在咱们直接看下面的代码。
PIMAGE_EXPORT_DIRECTORY Exp = (PIMAGE_EXPORT_DIRECTORY)(ptr + rtf(ptr, Data[0].VirtualAddress));
PBYTE DllName = (PBYTE)ptr + rtf(ptr, Exp->Name);
printf("导出表\n");
printf("Characteristics:%x\n", Exp->Characteristics);
printf("TimeDateStamp:%x\n", Exp->TimeDateStamp);
printf("MajorVersion:%x\n", Exp->MajorVersion);
printf("MinorVersion:%x\n", Exp->MinorVersion);
printf("Name_Rva:%x\n", Exp->Name);
printf("Name:%s\n", DllName);
printf("Base:%x\n", Exp->Base);
printf("NumberOfFunctions:%x\n", Exp->NumberOfFunctions);
printf("NumberOfNames:%x\n", Exp->NumberOfNames);
printf("AddressOfFunctions:%x\n", Exp->AddressOfFunctions);
printf("AddressOfNames:%x\n", Exp->AddressOfNames);
printf("AddressOfNameOrdinals:%x\n", Exp->AddressOfNameOrdinals);
PBYTE AddressOfFunctions = (PBYTE)(ptr + rtf(ptr, Exp->AddressOfFunctions));
PWORD AddressOfOrdinals = (PWORD)(ptr + rtf(ptr, Exp->AddressOfNameOrdinals));
PDWORD AddressOfNames = (PDWORD)(ptr + rtf(ptr, Exp->AddressOfNames));
BOOL flag = FALSE;
for (int i = 0; i < Exp->NumberOfFunctions; i++)
{
int k = 0;
for (; k < Exp->NumberOfNames; k++)
{
if (i == AddressOfOrdinals[k])
{
flag = TRUE;
break;
}
}
if (flag == TRUE)
{
PBYTE FunName = (PBYTE)ptr + rtf(ptr, AddressOfNames[k]);
printf("Function:[%s][%d][%x]\n", FunName, k + Exp->Base, AddressOfFunctions[AddressOfOrdinals[k]]);
}
}
PIMAGE_EXPORT_DIRECTORY Exp = (PIMAGE_EXPORT_DIRECTORY)(ptr + rtf(ptr, Data[0].VirtualAddress));
定义一个PIMAGE_EXPORT_DIRECTORY
类型的结构体指针指向,基址+数据目录第一项的VirtualAddress转换为Foa的值从而定位到导出表开始的地方,Data[0].VirtualAddress就是取导出表第一项的地址。
PBYTE DllName = (PBYTE)ptr + rtf(ptr, Exp->Name);
定义一个指针用来指向当前DLL的名字,因为上面说过了导出表的Name
成员是一个rva所以把它转换成foa加上基址即可,下面的printf就不看了,主要就是打印出导出表的成员,直接来到这一部分。
PBYTE AddressOfFunctions = (PBYTE)(ptr + rtf(ptr, Exp->AddressOfFunctions));
因为上面说过导出表成员AddressOfFunctions
是一个rva,这个rva指向导出函数地址表,所以要先把AddressOfFunctions
转换成foa在加上基址即可定位到导出函数地址表。
PWORD AddressOfOrdinals = (PWORD)(ptr + rtf(ptr, Exp->AddressOfNameOrdinals));
定义一个PWORD
类型的指针指向导出函数序号表,因为上面说过导出表成员AddressOfNameOrdinals
是一个rva,这个rva指向导出函数序号表,所以要先把AddressOfFunctions
转换成foa在加上基址即可定位到导出函数序号表。
PDWORD AddressOfNames = (PDWORD)(ptr + rtf(ptr, Exp->AddressOfNames));
定义一个PDWORD
类型的指针指向导出函数名称表,因为上面说过导出表成员AddressOfNames
是一个rva,这个rva指向导出函数名称表,所以要先把AddressOfNames
转换成foa在加上基址即可定位到导出函数名称表。
我们先大概说下,下面的代码解释,在一行一行的看。
定义一个布尔型的变量,先循环NumberOfFunctions
的值得到导出函数地址表的下标,在定义变量k用于循环NumberOfNames
的值,现在k代表导出函数序号表的下标,通过if判断来判断导出函数地址表的下标与导出函数序号表中的值一样不一样,如果一样就拿k去导出函数名称中找到对应地址,得到函数名,在拿k加上Base成员的值得到导出序号,再拿导出函数序号表的值去导出函数地址表中找到对应的函数地址。
BOOL flag = FALSE;
for (int i = 0; i < Exp->NumberOfFunctions; i++)
{
int k = 0;
for (; k < Exp->NumberOfNames; k++)
{
if (i == AddressOfOrdinals[k])
{
flag = TRUE;
break;
}
}
if (flag == TRUE)
{
PBYTE FunName = (PBYTE)ptr + rtf(ptr, AddressOfNames[k]);
printf("Function:[%s][%d][%x]\n", FunName, k + Exp->Base, AddressOfFunctions[AddressOfOrdinals[k]]);
}
}
for (int i = 0; i < Exp->NumberOfFunctions; i++)
{
}
定义一个for循环用于得到导出函数地址表的下标。
int k = 0;
定义一个变量k
,作为导出函数序号表的下标,通过下标可以得到导出函数序号表中的值,
for (; k < Exp->NumberOfNames; k++)
{
if (i == AddressOfOrdinals[k])
{
flag = TRUE;
break;
}
}
这里又定义了一个循环,来判断导出函数地址表的下标和导出函数序号表中的值一样不一样,如果一样代表这个函数存在,修改掉flag
的值为true并跳出当前循环。
if (flag == TRUE)
{
PBYTE FunName = (PBYTE)ptr + rtf(ptr, AddressOfNames[k]);
printf("Function:[%s][%d][%x]\n", FunName, k + Exp->Base, AddressOfFunctions[AddressOfOrdinals[k]]);
}
判断flag的值是不是true如果是就执行下面的代码。
PBYTE FunName = (PBYTE)ptr + rtf(ptr, AddressOfNames[k]);
定义一个PBYTE
类型的指针来指向当前函数名,先获取到函数名称表中k
对应的地址,因为函数名称表中的地址是一个rva所以要进行rva到foa的转换,在加上基址(ptr)即可定位到函数名字的地址。
printf("Function:[%s][%d][%x]\n", FunName, k + Exp->Base, AddressOfFunctions[AddressOfOrdinals[k]]);
打印函数名字,和导出序号,和函数地址,因为导出函数序号表中存的不是真正的导出函数序号加上Base成员的值才是,这里通过导出函数序号表中的值来找函数地址。
运行结果
0x4结语
主要是介绍了pe文件结构导出表和导出表的一些成员,以及如何使用代码打印出它们,涉及到指针和结构体相关的知识。需要注意的是指针的类型和三张子表存储值的宽度。
由于作者水平有限,文章如有错误欢迎指出。