在筆者的上一篇文章《驅動開發:核心特徵碼掃描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
程式中的所有匯出函數,輸出效果如下所示;
\\??\\C:\\Windows\\System32\\ntoskrnl.exe
\\??\\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
的匯出地址,輸出效果如下所示;