PE文件:導出表

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;//沒有進函數轉發器
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章