0x00 導出表簡述
導出表是數據目錄的第一項。
導出表提供了一些函數供調用者使用。一般來說DLL提供了一些函數可以供外部使用,這些函數通過導出表被調用。
一般來說,dll都有導出表,exe都沒有導出表,但是也有情況,dll沒有導出表,exe有導出表。
0x01 導出表結構
typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics; // 1) 保留,恆爲0x00000000
DWORD TimeDateStamp; // 2) 時間戳,導出表創建的時間(GMT時間)
WORD MajorVersion; // 3) 主版本號:導出表的主版本號
WORD MinorVersion; // 4) 子版本號:導出表的子版本號
DWORD Name; // 5) 指向模塊名稱的RVA,指向模塊名(導出表所在模塊的名稱)的ASCII字符的RVA
DWORD Base; // 6) 導出表用於輸出導出函數序號值的基數: 導出函數序號 = 函數入口地址數組下標索引值 + 基數
DWORD NumberOfFunctions; // 7) 導出函數入口地址表的成員個數
DWORD NumberOfNames; // 8) 導出函數名稱表中的成員個數
DWORD AddressOfFunctions; // 9) 函數入口地址表的相對虛擬地址(RVA),每一個非0的項都對應一個被導出的函數名稱或導出序號(序號+基數等於導出函數序號)
DWORD AddressOfNames; // 10) 函數名稱表的相對虛擬地址(RVA),存儲着指向導出函數名稱的ASCII字符的RVA
DWORD AddressOfNameOrdinals; // 11) 存儲着函數入口地址表的數組下標索引值(序號表),跟導出函數名稱表的成員順序對應
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
Characteristics:現在沒有用到,一般爲0。
TimeDateStamp:導出表生成的時間戳,由連接器生成。
MajorVersion,MinorVersion:看名字是版本,實際貌似沒有用,都是0。
Name:模塊的名字。
Base:序號的基數,按序號導出函數的序號值從Base開始遞增。
NumberOfFunctions:所有導出函數的數量。
NumberOfNames:按名字導出函數的數量。
AddressOfFunctions:一個RVA,指向一個DWORD數組,數組中的每一項是一個導出函數的RVA,順序與導出序號相同。
AddressOfNames:一個RVA,依然指向一個DWORD數組,數組中的每一項仍然是一個RVA,指向一個表示函數名字。
AddressOfNameOrdinals:一個RVA,還是指向一個WORD數組,數組中的每一項與AddressOfNames中的每一項對應,表示該名字的函數在AddressOfFunctions中的序號。
0x02 查找導出函數入口地址
1.按函數索引導出
1.定位到PE 文件頭
2.從PE 文件頭中的 IMAGE_OPTIONAL_HEADER32 結構中取出數據目錄表,並從第一個數據目錄中得到導出表的RVA
3.從導出表的 Base 字段得到起始序號
4.將需要查找的導出序號減去起始序號,得到函數在入口地址表中的索引
5.檢測索引值是否大於導出表的 NumberOfFunctions 字段的值,如果大於後者的話,說明輸入的序號是無效的
6.用這個索引值在 AddressOfFunctions 字段指向的導出函數入口地址表中取出相應的項目,這就是函數入口地址的RVA 值,當函數被裝入內存的時候,這個RVA 值加上模塊實際裝入的基地址,就得到了函數真正的入口地址
2.按函數名稱導出
1.最初的步驟是一樣的,那就是首先得到導出表的地址
2.從導出表的 NumberOfNames 字段得到已命名函數的總數,並以這個數字作爲循環的次數來構造一個循環
3.從 AddressOfNames 字段指向得到的函數名稱地址表的第一項開始,在循環中將每一項定義的函數名與要查找的函數名相比較,如果沒有任何一個函數名是符合的,表示文件中沒有指定名稱的函數
4.如果某一項定義的函數名與要查找的函數名符合,那麼記下這個函數名在字符串地址表中的索引值,然後在 AddressOfNamesOrdinals 指向的數組中以同樣的索引值取出數組項的值,我們這裏假設這個值是x
5.最後,以 x 值作爲索引值,在 AddressOfFunctions 字段指向的函數入口地址表中獲取的 RVA 就是函數的入口地址
3.函數轉發器導出
0x03 導出表的用處
1.知道了導出表的位置,我們可以得到導出函數的地址,進而對這些函數進行Hook。
2.dll劫持時,我們需要在自己的dll中建立一個和原dll一樣的導出表。
附上代碼:
void* QzGetProcessAddress(HMODULE ModuleBase, const char *Keyword)//NtQuerySystemInformation
{
char *v1 = (char *)ModuleBase;
PIMAGE_DOS_HEADER ImageDosHeader = (IMAGE_DOS_HEADER *)v1;
PIMAGE_NT_HEADERS ImageNtHeaders = (IMAGE_NT_HEADERS *)((size_t)v1 + ImageDosHeader->e_lfanew);
PIMAGE_OPTIONAL_HEADER ImageOptionalHeader = &ImageNtHeaders->OptionalHeader;
PIMAGE_DATA_DIRECTORY ImageDataDirectory = (IMAGE_DATA_DIRECTORY *)(&ImageOptionalHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT]);
PIMAGE_EXPORT_DIRECTORY ImageExportDirectory = (IMAGE_EXPORT_DIRECTORY *)((size_t)v1 + ImageDataDirectory->VirtualAddress);
if (ImageExportDirectory->NumberOfNames == 0 || ImageExportDirectory->NumberOfFunctions == 0)
{
return NULL;
}
DWORD* AddressOfFunctions = (DWORD*)((size_t)v1 + ImageExportDirectory->AddressOfFunctions);
//+ AddressOfFunctions ntdll.dll!0x772f0a68 (加載符號以獲取其他信息) {0x0002c860} unsigned long *
DWORD* AddressOfNames = (DWORD*)((size_t)v1 + ImageExportDirectory->AddressOfNames);
//+ AddressOfNames ntdll.dll!0x772f2f9c (加載符號以獲取其他信息) {0x00106818} unsigned long *
WORD* AddressOfNameOrdinals = (WORD *)((size_t)v1 + ImageExportDirectory->AddressOfNameOrdinals);
//+ AddressOfNameOrdinals ntdll.dll!0x772f54d0 (加載符號以獲取其他信息) {0x0007} unsigned short *
void *FunctionAddress = NULL;
DWORD i;
//索引導出
// (ULONG_PTR)Keyword >> 16 0x0000002f unsigned long
if (((ULONG_PTR)Keyword >> 16) == 0) //>> 向右位移16位
{
/*
#define LOWORD(l) ((WORD)((DWORD_PTR)(l) & 0xffff))
#define HIWORD(l) ((WORD)((DWORD_PTR)(l) >> 16))
這是windef.h頭文件中對宏LOWORD和HIWORD的定義。
作用分別是取出無符號長整型參數的高16位和低16位。
因爲一個長整型佔32位,其中高低16位的值可能有不同的意義,需要通過這2個宏分別取出來使用。取出來的結果是一個無符號短整型的值。
其原理正如定義那樣,取低16位的宏LOWORD使用按位與操作符與數字0xffff運算,而數字0xffff是一個低16位全爲1的數字,那麼對其位與操作可以得到參數的低16位。
而取高16位的宏HIWORD則更簡單,只需將參數右移16位,剩下的就是原高16位的值了。
*/
//#define LOWORD(l) ((WORD)(((DWORD_PTR)(l)) & 0xffff))
WORD Ordinal = LOWORD(Keyword);
ULONG_PTR Base = ImageExportDirectory->Base;//得到起始序號
if (Ordinal < Base || Base > Base + ImageExportDirectory->NumberOfFunctions)
{
return NULL;
}
FunctionAddress = (void*)((size_t)v1 + AddressOfFunctions[Ordinal - Base]);//函數編號-起始序號=函數在AddressOfFunction中的索引號
//入口地址=虛擬地址+該動態鏈接庫被導入到地址空間的基地址
}
else //函數名稱導出
{
//ImageExportDirectory->NumberOfNames = 0x0000094d
for (i = 0; i < ImageExportDirectory->NumberOfNames; i++)
{
//獲得函數名稱
char* FunctionName = (char*)((size_t)v1 + AddressOfNames[i]);
//v1 "MZ"+
//FunctionName = 0x772f927f "NtQuerySystemInformation"
if (_stricmp(Keyword, FunctionName) == 0)//FunctionName = 0x00007ffe6fcba5c7 "NtCreateThreadEx"
{
//獲得函數地址
FunctionAddress = (void*)((size_t)v1 + AddressOfFunctions[AddressOfNameOrdinals[i]]);
//FunctionAddress = ntdll.dll!0x7725ac30 (加載符號以獲取其他信息)
break;
}
}
}
//函數轉發器 //屬於這個區域內,就是轉發器,不屬於這個區域的就是真正的導出函數
if ((char *)FunctionAddress >= (char*)ImageExportDirectory && (char*)FunctionAddress < (char*)ImageExportDirectory + ImageDataDirectory->Size)
{
HMODULE v2 = NULL;
//獲得轉發模塊的名稱
//FunctionAddress = //Dll.Sub_1........ Dll.#2
char* v3 = _strdup((char*)FunctionAddress);//????
//_strdup:重複的字符串。這些函數中的每個函數都返回一個指向複製字符串存儲位置的指針,如果不能分配存儲,則返回NULL。
if (!v3)
{
return NULL;
}
char* FunctionName = strchr(v3, '.');//在字符串中找到一個字符。
*FunctionName++ = 0;//++爲了越過.
FunctionAddress = NULL;
//構建轉發模塊的路徑
char ModuleFullPath[MAX_PATH] = { 0 };
strcpy_s(ModuleFullPath, v3);
//
strcat_s(ModuleFullPath, strlen(v3) + 4 + 1, ".dll");
//判斷是不是當前進程已經加載了這個轉發模塊
v2 = (HMODULE)QzGetModuleHandle(ModuleFullPath);
if (!v2)
{
//如果沒有得到,就要重新加載這個模塊
v2 = LoadLibraryA(ModuleFullPath);
}
if (!v2)
{
return NULL;
}
BOOL v4 = strchr(v3, '#') == 0 ? FALSE : TRUE;
if (v4)
{
//函數索引轉發
WORD FunctionOrdinal = atoi(v3 + 1);//將給定的字符串轉換爲整數。
//遞歸自己
FunctionAddress = QzGetProcessAddress(v2, (const char*)FunctionOrdinal);
}
else
{
//函數名稱轉發 遞歸自己
FunctionAddress = QzGetProcessAddress(v2, FunctionName);
}
free(v2);
}
return FunctionAddress;//沒有進函數轉發器
}