驅動開發:核心解析PE結構匯出表

2023-05-31 12:02:00

在筆者的上一篇文章《驅動開發:核心特徵碼掃描PE程式碼段》LyShark帶大家通過封裝好的LySharkToolsUtilKernelBase函數實現了動態獲取核心模組基址,並通過ntimage.h標頭檔案中提供的系列函數解析了指定核心模組的PE節表引數,本章將繼續延申這個話題,實現對PE檔案匯出表的解析任務,匯出表無法動態獲取,解析匯出表則必須讀入核心模組到記憶體才可繼續解析,所以我們需要分兩步走,首先讀入核心磁碟檔案到記憶體,然後再通過ntimage.h中的系列函數解析即可。

當PE檔案執行時Windows裝載器將檔案裝入記憶體並將匯入表中登記的DLL檔案一併裝入,再根據DLL檔案中函數的匯出資訊對可執行檔案的匯入表(IAT)進行修正。匯出函數在DLL檔案中,匯出資訊被儲存在匯出表,匯出表就是記載著動態連結庫的一些匯出資訊。通過匯出表,DLL檔案可以向系統提供匯出函數的名稱、序號和入口地址等資訊,以便Windows裝載器能夠通過這些資訊來完成動態連結的整個過程。

匯出函數儲存在PE檔案的匯出表裡,匯出表的位置存放在PE檔案頭中的資料目錄表中,與匯出表對應的專案是資料目錄中的首個IMAGE_DATA_DIRECTORY結構,從這個結構的VirtualAddress欄位得到的就是匯出表的RVA值,匯出表同樣可以使用函數名或序號這兩種方法匯出函數。

匯出表的起始位置有一個IMAGE_EXPORT_DIRECTORY結構,與匯入表中有多個IMAGE_IMPORT_DESCRIPTOR結構不同,匯出表只有一個IMAGE_EXPORT_DIRECTORY結構,該結構定義如下:

typedef struct _IMAGE_EXPORT_DIRECTORY {
    DWORD   Characteristics;
    DWORD   TimeDateStamp;        // 檔案的產生時刻
    WORD    MajorVersion;
    WORD    MinorVersion;
    DWORD   Name;                  // 指向檔名的RVA
    DWORD   Base;                  // 匯出函數的起始序號
    DWORD   NumberOfFunctions;     // 匯出函數總數
    DWORD   NumberOfNames;         // 以名稱匯出函數的總數
    DWORD   AddressOfFunctions;    // 匯出函數地址表的RVA
    DWORD   AddressOfNames;        // 函數名稱地址表的RVA
    DWORD   AddressOfNameOrdinals; // 函數名序號表的RVA
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;

上面的_IMAGE_EXPORT_DIRECTORY 結構如果總結成一張圖,如下所示:

在上圖中最左側AddressOfNames結構成員指向了一個陣列,陣列裡儲存著一組RVA,每個RVA指向一個字串即匯出的函數名,與這個函數名對應的是AddressOfNameOrdinals中的結構成員,該對應項儲存的正是函數的唯一編號並與AddressOfFunctions結構成員相關聯,形成了一個匯出鏈式結構體。

獲取匯出函數地址時,先在AddressOfNames中找到對應的名字MyFunc1,該函數在AddressOfNames中是第1項,然後從AddressOfNameOrdinals中取出第1項的值這裡是1,然後就可以通過匯出函數的序號AddressOfFunctions[1]取出函數的入口RVA,然後通過RVA加上模組基址便是第一個匯出函數的地址,向後每次相加匯出函數偏移即可依次遍歷出所有的匯出函數地址。

其解析過程與應用層基本保持一致,如果不懂應用層如何解析也可以去看我以前寫過的《PE格式:手寫PE結構解析工具》裡面具體詳細的分析瞭解析流程。

首先使用InitializeObjectAttributes()開啟檔案,開啟後可獲取到該檔案的控制程式碼,InitializeObjectAttributes宏初始化一個OBJECT_ATTRIBUTES結構體, 當一個例程開啟物件時由此結構體指定目標物件的屬性,此函數的微軟定義如下;

VOID InitializeObjectAttributes(
  [out]          POBJECT_ATTRIBUTES   p,      // 許可權
  [in]           PUNICODE_STRING      n,      // 檔名
  [in]           ULONG                a,      // 輸出檔案
  [in]           HANDLE               r,      // 許可權
  [in, optional] PSECURITY_DESCRIPTOR s       // 0
);

當許可權控制程式碼被初始化後則即呼叫ZwOpenFile()開啟一個檔案使用許可權FILE_SHARE_READ開啟,開啟檔案函數微軟定義如下;

NTSYSAPI NTSTATUS ZwOpenFile(
  [out] PHANDLE            FileHandle,         // 返回開啟檔案的控制程式碼
  [in]  ACCESS_MASK        DesiredAccess,      // 開啟的許可權,一般設為GENERIC_ALL。
  [in]  POBJECT_ATTRIBUTES ObjectAttributes,   // OBJECT_ATTRIBUTES結構
  [out] PIO_STATUS_BLOCK   IoStatusBlock,      // 指向一個結構體的指標。該結構體指明開啟檔案的狀態。
  [in]  ULONG              ShareAccess,        // 共用的許可權。可以是FILE_SHARE_READ 或者 FILE_SHARE_WRITE。
  [in]  ULONG              OpenOptions         // 開啟選項,一般設為 FILE_SYNCHRONOUS_IO_NONALERT。
);

接著檔案被開啟後,我們還需要呼叫ZwCreateSection()該函數的作用是建立一個Section節物件,並以PE結構中的SectionALignment大小對齊對映檔案,其微軟定義如下;

NTSYSAPI NTSTATUS ZwCreateSection(
  [out]          PHANDLE            SectionHandle,            // 指向 HANDLE 變數的指標,該變數接收 section 物件的控制程式碼。
  [in]           ACCESS_MASK        DesiredAccess,            // 指定一個 ACCESS_MASK 值,該值確定對 物件的請求存取許可權。
  [in, optional] POBJECT_ATTRIBUTES ObjectAttributes,         // 指向 OBJECT_ATTRIBUTES 結構的指標,該結構指定物件名稱和其他屬性。
  [in, optional] PLARGE_INTEGER     MaximumSize,              // 指定節的最大大小(以位元組為單位)。
  [in]           ULONG              SectionPageProtection,    // 指定要在 節中的每個頁面上放置的保護。 
  [in]           ULONG              AllocationAttributes,     // 指定確定節的分配屬性的SEC_XXX 標誌的位掩碼。 
  [in, optional] HANDLE             FileHandle                // (可選)指定開啟的檔案物件的控制程式碼。
);

最後讀取匯出表就要將一個磁碟中的檔案對映到記憶體中,記憶體對映核心檔案時ZwMapViewOfSection()該系列函數在應用層名叫MapViewOfSection()只是一個是核心層一個應用層,這兩個函數引數傳遞基本一致,以ZwMapViewOfSection為例,其微軟定義如下;

NTSYSAPI NTSTATUS ZwMapViewOfSection(
  [in]                HANDLE          SectionHandle,          // 接收一個節物件
  [in]                HANDLE          ProcessHandle,          // 程序控制程式碼,此處使用NtCurrentProcess()獲取自身控制程式碼
  [in, out]           PVOID           *BaseAddress,           // 指定填充地址
  [in]                ULONG_PTR       ZeroBits,               // 0
  [in]                SIZE_T          CommitSize,             // 每次提交大小 1024
  [in, out, optional] PLARGE_INTEGER  SectionOffset,          // 0
  [in, out]           PSIZE_T         ViewSize,               // 瀏覽大小
  [in]                SECTION_INHERIT InheritDisposition,     // ViewShare
  [in]                ULONG           AllocationType,         // 分配型別 MEM_TOP_DOWN
  [in]                ULONG           Win32Protect            // 許可權 PAGE_READWRITE(讀寫)
);

將如上函數研究明白那麼程式碼就變得很容易了,首先InitializeObjectAttributes設定檔案許可權與屬性,然後呼叫ZwOpenFile開啟檔案,接著呼叫ZwCreateSection建立節物件,最後呼叫ZwMapViewOfSection將磁碟檔案對映到記憶體,這段程式碼實現起來很簡單,完整案例如下所示;

// 署名權
// right to sign one's name on a piece of work
// PowerBy: LyShark
// Email: [email protected]

#include <ntifs.h>
#include <ntimage.h>
#include <ntstrsafe.h>

// 記憶體對映檔案
NTSTATUS KernelMapFile(UNICODE_STRING FileName, HANDLE *phFile, HANDLE *phSection, PVOID *ppBaseAddress)
{
	NTSTATUS status = STATUS_SUCCESS;
	HANDLE hFile = NULL;
	HANDLE hSection = NULL;
	OBJECT_ATTRIBUTES objectAttr = { 0 };
	IO_STATUS_BLOCK iosb = { 0 };
	PVOID pBaseAddress = NULL;
	SIZE_T viewSize = 0;

	// 設定檔案許可權
	InitializeObjectAttributes(&objectAttr, &FileName, OBJ_CASE_INSENSITIVE | OBJ_KERNEL_HANDLE, NULL, NULL);

	// 開啟檔案
	status = ZwOpenFile(&hFile, GENERIC_READ, &objectAttr, &iosb, FILE_SHARE_READ, FILE_SYNCHRONOUS_IO_NONALERT);
	if (!NT_SUCCESS(status))
	{
		return status;
	}

	// 建立節物件
	status = ZwCreateSection(&hSection, SECTION_MAP_READ | SECTION_MAP_WRITE, NULL, 0, PAGE_READWRITE, 0x1000000, hFile);
	if (!NT_SUCCESS(status))
	{
		ZwClose(hFile);
		return status;
	}
	// 對映到記憶體
	status = ZwMapViewOfSection(hSection, NtCurrentProcess(), &pBaseAddress, 0, 1024, 0, &viewSize, ViewShare, MEM_TOP_DOWN, PAGE_READWRITE);
	if (!NT_SUCCESS(status))
	{
		ZwClose(hSection);
		ZwClose(hFile);
		return status;
	}

	// 返回資料
	*phFile = hFile;
	*phSection = hSection;
	*ppBaseAddress = pBaseAddress;

	return status;
}

VOID UnDriver(PDRIVER_OBJECT driver)
{
	DbgPrint("驅動解除安裝 \n");
}

NTSTATUS DriverEntry(IN PDRIVER_OBJECT Driver, PUNICODE_STRING RegistryPath)
{
	DbgPrint("hello lyshark.com \n");

	NTSTATUS status = STATUS_SUCCESS;

	HANDLE hFile = NULL;
	HANDLE hSection = NULL;
	PVOID pBaseAddress = NULL;
	UNICODE_STRING FileName = {0};

	// 初始化字串
	RtlInitUnicodeString(&FileName, L"\\??\\C:\\Windows\\System32\\ntoskrnl.exe");

	// 記憶體對映檔案
	status = KernelMapFile(FileName, &hFile, &hSection, &pBaseAddress);
	if (NT_SUCCESS(status))
	{
		DbgPrint("讀取記憶體地址 = %p \n", pBaseAddress);
	}

	Driver->DriverUnload = UnDriver;
	return STATUS_SUCCESS;
}

執行這段程式,即可讀取到ntoskrnl.exe磁碟所在檔案的記憶體映像基地址,效果如下所示;

如上程式碼讀入了ntoskrnl.exe檔案,接下來就是解析匯出表,首先將pBaseAddress解析為PIMAGE_DOS_HEADER獲取DOS頭,並在DOS頭中尋找PIMAGE_NT_HEADERS頭,接著在NTHeader頭中得到資料目錄表,此處指向的就是匯出表PIMAGE_EXPORT_DIRECTORY通過pExportTable->NumberOfNames可得到匯出表的數量,通過(PUCHAR)pDosHeader + pExportTable->AddressOfNames得到匯出表的地址,依次迴圈讀取即可得到完整的匯出表。

// 署名權
// right to sign one's name on a piece of work
// PowerBy: LyShark
// Email: [email protected]

NTSTATUS DriverEntry(IN PDRIVER_OBJECT Driver, PUNICODE_STRING RegistryPath)
{
	DbgPrint("hello lyshark.com \n");

	NTSTATUS status = STATUS_SUCCESS;
	HANDLE hFile = NULL;
	HANDLE hSection = NULL;
	PVOID pBaseAddress = NULL;
	UNICODE_STRING FileName = { 0 };
	LONG FunctionIndex = 0;

	// 初始化字串
	RtlInitUnicodeString(&FileName, L"\\??\\C:\\Windows\\System32\\ntoskrnl.exe");

	// 記憶體對映檔案
	status = KernelMapFile(FileName, &hFile, &hSection, &pBaseAddress);
	if (NT_SUCCESS(status))
	{
		DbgPrint("[LyShark] 讀取記憶體地址 = %p \n", pBaseAddress);
	}

	// Dos 頭
	PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)pBaseAddress;

	// NT 頭
	PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)((PUCHAR)pDosHeader + pDosHeader->e_lfanew);

	// 匯出表
	PIMAGE_EXPORT_DIRECTORY pExportTable = (PIMAGE_EXPORT_DIRECTORY)((PUCHAR)pDosHeader + pNtHeaders->OptionalHeader.DataDirectory[0].VirtualAddress);

	// 有名稱的匯出函數個數
	ULONG ulNumberOfNames = pExportTable->NumberOfNames;
	DbgPrint("[LyShark.com] 匯出函數個數: %d \n\n", ulNumberOfNames);

	// 匯出函數名稱地址表
	PULONG lpNameArray = (PULONG)((PUCHAR)pDosHeader + pExportTable->AddressOfNames);
	PCHAR lpName = NULL;

	// 開始遍歷匯出表(輸出ulNumberOfNames匯出函數)
	for (ULONG i = 0; i < ulNumberOfNames; i++)
	{
		lpName = (PCHAR)((PUCHAR)pDosHeader + lpNameArray[i]);

		// 獲取匯出函數地址
		USHORT uHint = *(USHORT *)((PUCHAR)pDosHeader + pExportTable->AddressOfNameOrdinals + 2 * i);
		ULONG ulFuncAddr = *(PULONG)((PUCHAR)pDosHeader + pExportTable->AddressOfFunctions + 4 * uHint);
		PVOID lpFuncAddr = (PVOID)((PUCHAR)pDosHeader + ulFuncAddr);

		// 獲取SSDT函數Index
		FunctionIndex = *(ULONG *)((PUCHAR)lpFuncAddr + 4);

		DbgPrint("序號: [ %d ] | Hint: %d | 地址: %p | 函數名: %s \n", i, uHint, lpFuncAddr, lpName);
	}

	// 釋放指標
	ZwUnmapViewOfSection(NtCurrentProcess(), pBaseAddress);
	ZwClose(hSection);
	ZwClose(hFile);

	Driver->DriverUnload = UnDriver;
	return STATUS_SUCCESS;
}

程式碼執行後即可獲取到當前ntoskrnl.exe程式中的所有匯出函數,輸出效果如下所示;

  • SSDT表通常會解析\\??\\C:\\Windows\\System32\\ntoskrnl.exe
  • SSSDT表通常會解析\\??\\C:\\Windows\\System32\\win32k.sys

根據上方的函數流程將其封裝為GetAddressFromFunction()使用者傳入DllFileName指定的PE檔案,以及需要讀取的pszFunctionName函數名,即可輸出該函數的匯出地址。

// 署名權
// right to sign one's name on a piece of work
// PowerBy: LyShark
// Email: [email protected]

// 尋找指定函數得到記憶體地址
ULONG64 GetAddressFromFunction(UNICODE_STRING DllFileName, PCHAR pszFunctionName)
{
	NTSTATUS status = STATUS_SUCCESS;
	HANDLE hFile = NULL;
	HANDLE hSection = NULL;
	PVOID pBaseAddress = NULL;

	// 記憶體對映檔案
	status = KernelMapFile(DllFileName, &hFile, &hSection, &pBaseAddress);
	if (!NT_SUCCESS(status))
	{
		return 0;
	}
	PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)pBaseAddress;
	PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)((PUCHAR)pDosHeader + pDosHeader->e_lfanew);
	PIMAGE_EXPORT_DIRECTORY pExportTable = (PIMAGE_EXPORT_DIRECTORY)((PUCHAR)pDosHeader + pNtHeaders->OptionalHeader.DataDirectory[0].VirtualAddress);
	ULONG ulNumberOfNames = pExportTable->NumberOfNames;
	PULONG lpNameArray = (PULONG)((PUCHAR)pDosHeader + pExportTable->AddressOfNames);
	PCHAR lpName = NULL;

	for (ULONG i = 0; i < ulNumberOfNames; i++)
	{
		lpName = (PCHAR)((PUCHAR)pDosHeader + lpNameArray[i]);
		USHORT uHint = *(USHORT *)((PUCHAR)pDosHeader + pExportTable->AddressOfNameOrdinals + 2 * i);
		ULONG ulFuncAddr = *(PULONG)((PUCHAR)pDosHeader + pExportTable->AddressOfFunctions + 4 * uHint);
		PVOID lpFuncAddr = (PVOID)((PUCHAR)pDosHeader + ulFuncAddr);

		if (_strnicmp(pszFunctionName, lpName, strlen(pszFunctionName)) == 0)
		{
			ZwUnmapViewOfSection(NtCurrentProcess(), pBaseAddress);
			ZwClose(hSection);
			ZwClose(hFile);

			return (ULONG64)lpFuncAddr;
		}
	}
	ZwUnmapViewOfSection(NtCurrentProcess(), pBaseAddress);
	ZwClose(hSection);
	ZwClose(hFile);
	return 0;
}

VOID UnDriver(PDRIVER_OBJECT driver)
{
	DbgPrint("驅動解除安裝 \n");
}

NTSTATUS DriverEntry(IN PDRIVER_OBJECT Driver, PUNICODE_STRING RegistryPath)
{
	DbgPrint("hello lyshark.com \n");

	UNICODE_STRING FileName = { 0 };
	ULONG64 FunctionAddress = 0;

	// 初始化字串
	RtlInitUnicodeString(&FileName, L"\\??\\C:\\Windows\\System32\\ntdll.dll");

	// 取函數記憶體地址
	FunctionAddress = GetAddressFromFunction(FileName, "ZwQueryVirtualMemory");
	DbgPrint("ZwQueryVirtualMemory記憶體地址 = %p \n", FunctionAddress);

	Driver->DriverUnload = UnDriver;
	return STATUS_SUCCESS;
}

如上程式所示,當執行後即可獲取到ntdll.dll模組內ZwQueryVirtualMemory的匯出地址,輸出效果如下所示;