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字节

wKg0C2JCavAAkVVAADJFq3lcZY041.png

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成员的值才是,这里通过导出函数序号表中的值来找函数地址。

运行结果

wKg0C2JCd72AKIfDAAFwEkvKBwA119.png

0x4结语

主要是介绍了pe文件结构导出表和导出表的一些成员,以及如何使用代码打印出它们,涉及到指针和结构体相关的知识。需要注意的是指针的类型和三张子表存储值的宽度。

由于作者水平有限,文章如有错误欢迎指出。

posted @ 2022-05-30 15:45  SecIN社区  阅读(54)  评论(0编辑  收藏  举报