PE檔案結構解析3

SecIN發表於2022-05-30

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檔案結構匯出表和匯出表的一些成員,以及如何使用程式碼列印出它們,涉及到指標和結構體相關的知識。需要注意的是指標的型別和三張子表儲存值的寬度。

由於作者水平有限,文章如有錯誤歡迎指出。

相關文章