驅動開發:取程序模組的函數地址

2023-06-28 12:00:54

在筆者上一篇文章《驅動開發:核心取應用層模組基地址》中簡單為大家介紹瞭如何通過遍歷PLIST_ENTRY32連結串列的方式獲取到32位元應用程式中特定模組的基地址,由於是入門系列所以並沒有封裝實現太過於通用的獲取函數,本章將繼續延申這個話題,並依次實現通用版GetUserModuleBaseAddress()取遠端程序中指定模組的基址和GetModuleExportAddress()取遠端程序中特定模組中的函數地址,此類功能也是各類安全工具中常用的程式碼片段。

首先封裝一個lyshark.h標頭檔案,此類標頭檔案中的定義都是微軟官方定義好的規範,如果您想獲取該結構的詳細說明檔案請參閱微軟官方,此處不做過多的介紹。

// 署名權
// 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>

// 匯出未匯出函數
NTKERNELAPI PPEB NTAPI PsGetProcessPeb(IN PEPROCESS Process);
NTKERNELAPI PVOID NTAPI PsGetProcessWow64Process(IN PEPROCESS Process);

typedef struct _PEB32
{
  UCHAR InheritedAddressSpace;
  UCHAR ReadImageFileExecOptions;
  UCHAR BeingDebugged;
  UCHAR BitField;
  ULONG Mutant;
  ULONG ImageBaseAddress;
  ULONG Ldr;
  ULONG ProcessParameters;
  ULONG SubSystemData;
  ULONG ProcessHeap;
  ULONG FastPebLock;
  ULONG AtlThunkSListPtr;
  ULONG IFEOKey;
  ULONG CrossProcessFlags;
  ULONG UserSharedInfoPtr;
  ULONG SystemReserved;
  ULONG AtlThunkSListPtr32;
  ULONG ApiSetMap;
} PEB32, *PPEB32;

typedef struct _PEB_LDR_DATA
{
  ULONG Length;
  UCHAR Initialized;
  PVOID SsHandle;
  LIST_ENTRY InLoadOrderModuleList;
  LIST_ENTRY InMemoryOrderModuleList;
  LIST_ENTRY InInitializationOrderModuleList;
} PEB_LDR_DATA, *PPEB_LDR_DATA;

typedef struct _PEB
{
  UCHAR InheritedAddressSpace;
  UCHAR ReadImageFileExecOptions;
  UCHAR BeingDebugged;
  UCHAR BitField;
  PVOID Mutant;
  PVOID ImageBaseAddress;
  PPEB_LDR_DATA Ldr;
  PVOID ProcessParameters;
  PVOID SubSystemData;
  PVOID ProcessHeap;
  PVOID FastPebLock;
  PVOID AtlThunkSListPtr;
  PVOID IFEOKey;
  PVOID CrossProcessFlags;
  PVOID KernelCallbackTable;
  ULONG SystemReserved;
  ULONG AtlThunkSListPtr32;
  PVOID ApiSetMap;
} PEB, *PPEB;

typedef struct _PEB_LDR_DATA32
{
  ULONG Length;
  UCHAR Initialized;
  ULONG SsHandle;
  LIST_ENTRY32 InLoadOrderModuleList;
  LIST_ENTRY32 InMemoryOrderModuleList;
  LIST_ENTRY32 InInitializationOrderModuleList;
} PEB_LDR_DATA32, *PPEB_LDR_DATA32;

typedef struct _LDR_DATA_TABLE_ENTRY32
{
  LIST_ENTRY32 InLoadOrderLinks;
  LIST_ENTRY32 InMemoryOrderLinks;
  LIST_ENTRY32 InInitializationOrderLinks;
  ULONG DllBase;
  ULONG EntryPoint;
  ULONG SizeOfImage;
  UNICODE_STRING32 FullDllName;
  UNICODE_STRING32 BaseDllName;
  ULONG Flags;
  USHORT LoadCount;
  USHORT TlsIndex;
  LIST_ENTRY32 HashLinks;
  ULONG TimeDateStamp;
} LDR_DATA_TABLE_ENTRY32, *PLDR_DATA_TABLE_ENTRY32;

typedef struct _LDR_DATA_TABLE_ENTRY
{
  LIST_ENTRY InLoadOrderLinks;
  LIST_ENTRY InMemoryOrderLinks;
  LIST_ENTRY InInitializationOrderLinks;
  PVOID DllBase;
  PVOID EntryPoint;
  ULONG SizeOfImage;
  UNICODE_STRING FullDllName;
  UNICODE_STRING BaseDllName;
  ULONG Flags;
  USHORT LoadCount;
  USHORT TlsIndex;
  LIST_ENTRY HashLinks;
  ULONG TimeDateStamp;
} LDR_DATA_TABLE_ENTRY, *PLDR_DATA_TABLE_ENTRY;

GetUserModuleBaseAddress(): 實現取程序中模組基址,該功能在《驅動開發:核心取應用層模組基地址》中詳細介紹過原理,這段程式碼核心原理如下所示,此處最需要注意的是如果是32位元程序則我們需要得到PPEB32 Peb32結構體,該結構體通常可以直接使用PsGetProcessWow64Process()這個核心函數獲取到,而如果是64位元程序則需要將尋找PEB的函數替換為PsGetProcessPeb(),其他的列舉細節與上一篇文章中的方法一致。

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

#include <ntifs.h>
#include <windef.h>
#include "lyshark.h"

// 獲取特定程序內特定模組的基址
PVOID GetUserModuleBaseAddress(IN PEPROCESS EProcess, IN PUNICODE_STRING ModuleName, IN BOOLEAN IsWow64)
{
	if (EProcess == NULL)
		return NULL;
	__try
	{
		// 設定延遲時間為250毫秒
		LARGE_INTEGER Time = { 0 };
		Time.QuadPart = -250ll * 10 * 1000;

		// 如果是32位元則執行如下程式碼
		if (IsWow64)
		{
			// 得到PEB程序資訊
			PPEB32 Peb32 = (PPEB32)PsGetProcessWow64Process(EProcess);
			if (Peb32 == NULL)
			{
				return NULL;
			}

			// 延遲載入等待時間
			for (INT i = 0; !Peb32->Ldr && i < 10; i++)
			{
				KeDelayExecutionThread(KernelMode, TRUE, &Time);
			}

			// 沒有PEB載入超時
			if (!Peb32->Ldr)
			{
				return NULL;
			}

			// 搜尋模組 InLoadOrderModuleList
			for (PLIST_ENTRY32 ListEntry = (PLIST_ENTRY32)((PPEB_LDR_DATA32)Peb32->Ldr)->InLoadOrderModuleList.Flink; ListEntry != &((PPEB_LDR_DATA32)Peb32->Ldr)->InLoadOrderModuleList; ListEntry = (PLIST_ENTRY32)ListEntry->Flink)
			{
				UNICODE_STRING UnicodeString;
				PLDR_DATA_TABLE_ENTRY32 LdrDataTableEntry32 = CONTAINING_RECORD(ListEntry, LDR_DATA_TABLE_ENTRY32, InLoadOrderLinks);
				RtlUnicodeStringInit(&UnicodeString, (PWCH)LdrDataTableEntry32->BaseDllName.Buffer);

				// 找到了返回模組基址
				if (RtlCompareUnicodeString(&UnicodeString, ModuleName, TRUE) == 0)
				{
					return (PVOID)LdrDataTableEntry32->DllBase;
				}
			}
		}
		// 如果是64位元則執行如下程式碼
		else
		{
			// 同理,先找64位元PEB
			PPEB Peb = PsGetProcessPeb(EProcess);
			if (!Peb)
			{
				return NULL;
			}

			// 延遲載入
			for (INT i = 0; !Peb->Ldr && i < 10; i++)
			{
				KeDelayExecutionThread(KernelMode, TRUE, &Time);
			}

			// 找不到PEB直接返回
			if (!Peb->Ldr)
			{
				return NULL;
			}

			// 遍歷連結串列
			for (PLIST_ENTRY ListEntry = Peb->Ldr->InLoadOrderModuleList.Flink; ListEntry != &Peb->Ldr->InLoadOrderModuleList; ListEntry = ListEntry->Flink)
			{
				// 將特定連結串列轉換為PLDR_DATA_TABLE_ENTRY格式
				PLDR_DATA_TABLE_ENTRY LdrDataTableEntry = CONTAINING_RECORD(ListEntry, LDR_DATA_TABLE_ENTRY, InLoadOrderLinks);

				// 找到了則返回地址
				if (RtlCompareUnicodeString(&LdrDataTableEntry->BaseDllName, ModuleName, TRUE) == 0)
				{
					return LdrDataTableEntry->DllBase;
				}
			}
		}
	}
	__except (EXCEPTION_EXECUTE_HANDLER)
	{
		return NULL;
	}
	return NULL;
}

那麼該函數該如何呼叫傳遞引數呢,如下程式碼是DriverEntry入口處的呼叫方法,首先要想得到特定程序的特定模組地址則第一步就是需要PsLookupProcessByProcessId找到模組的EProcess結構,接著通過PsGetProcessWow64Process得到當前被操作程序是32位元還是64位元,通過呼叫KeStackAttachProcess附加到程序記憶體中,然後呼叫GetUserModuleBaseAddress並傳入需要獲取模組的名字得到資料後返回給NtdllAddress變數,最後呼叫KeUnstackDetachProcess取消附加即可。

// 署名權
// 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)
{
	HANDLE ProcessID = (HANDLE)7924;

	PEPROCESS EProcess = NULL;
	NTSTATUS Status = STATUS_SUCCESS;
	KAPC_STATE ApcState;

	DbgPrint("Hello LyShark.com \n");

	// 根據PID得到程序EProcess結構
	Status = PsLookupProcessByProcessId(ProcessID, &EProcess);
	if (Status != STATUS_SUCCESS)
	{
		DbgPrint("獲取EProcessID失敗 \n");
		return Status;
	}

	// 判斷目標程序是32位元還是64位元
	BOOLEAN IsWow64 = (PsGetProcessWow64Process(EProcess) != NULL) ? TRUE : FALSE;

	// 驗證地址是否可讀
	if (!MmIsAddressValid(EProcess))
	{
		DbgPrint("地址不可讀 \n");
		Driver->DriverUnload = UnDriver;
		return STATUS_SUCCESS;
	}

	// 將當前執行緒連線到目標程序的地址空間(附加程序)
	KeStackAttachProcess((PRKPROCESS)EProcess, &ApcState);

	__try
	{
		UNICODE_STRING NtdllUnicodeString = { 0 };
		PVOID NtdllAddress = NULL;

		// 得到程序內ntdll.dll模組基地址
		RtlInitUnicodeString(&NtdllUnicodeString, L"Ntdll.dll");
		NtdllAddress = GetUserModuleBaseAddress(EProcess, &NtdllUnicodeString, IsWow64);
		if (!NtdllAddress)
		{
			DbgPrint("沒有找到基址 \n");
			Driver->DriverUnload = UnDriver;
			return STATUS_SUCCESS;
		}

		DbgPrint("[*] 模組ntdll.dll基址: %p \n", NtdllAddress);
	}
	__except (EXCEPTION_EXECUTE_HANDLER)
	{
	}

	// 取消附加
	KeUnstackDetachProcess(&ApcState);

	Driver->DriverUnload = UnDriver;
	return STATUS_SUCCESS;
}

替換DriverEntry入口函數處的ProcessID並替換為當前需要獲取的應用層程序PID,執行驅動程式即可得到該程序內Ntdll.dll的模組基址,輸出效果如下;

GetModuleExportAddress(): 實現獲取特定模組中特定函數的基地址,通常我們通過GetUserModuleBaseAddress()可得到程序內特定模組的基址,然後則可繼續通過GetModuleExportAddress()獲取到該模組內特定匯出函數的記憶體地址,至於獲取匯出表中特定函數的地址則可通過如下方式迴圈遍歷匯出表函數獲取。

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

// 獲取特定模組下的匯出函數地址
PVOID GetModuleExportAddress(IN PVOID ModuleBase, IN PCCHAR FunctionName, IN PEPROCESS EProcess)
{
	PIMAGE_DOS_HEADER ImageDosHeader = (PIMAGE_DOS_HEADER)ModuleBase;
	PIMAGE_NT_HEADERS32 ImageNtHeaders32 = NULL;
	PIMAGE_NT_HEADERS64 ImageNtHeaders64 = NULL;
	PIMAGE_EXPORT_DIRECTORY ImageExportDirectory = NULL;
	ULONG ExportDirectorySize = 0;
	ULONG_PTR FunctionAddress = 0;

	// 為空則返回
	if (ModuleBase == NULL)
	{
		return NULL;
	}

	// 是不是PE檔案
	if (ImageDosHeader->e_magic != IMAGE_DOS_SIGNATURE)
	{
		return NULL;
	}

	// 獲取NT頭
	ImageNtHeaders32 = (PIMAGE_NT_HEADERS32)((PUCHAR)ModuleBase + ImageDosHeader->e_lfanew);
	ImageNtHeaders64 = (PIMAGE_NT_HEADERS64)((PUCHAR)ModuleBase + ImageDosHeader->e_lfanew);

	// 是64位元則執行
	if (ImageNtHeaders64->OptionalHeader.Magic == IMAGE_NT_OPTIONAL_HDR64_MAGIC)
	{
		ImageExportDirectory = (PIMAGE_EXPORT_DIRECTORY)(ImageNtHeaders64->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress + (ULONG_PTR)ModuleBase);
		ExportDirectorySize = ImageNtHeaders64->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].Size;
	}
	// 是32位元則執行
	else
	{
		ImageExportDirectory = (PIMAGE_EXPORT_DIRECTORY)(ImageNtHeaders32->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress + (ULONG_PTR)ModuleBase);
		ExportDirectorySize = ImageNtHeaders32->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].Size;
	}

	// 得到匯出表地址偏移和名字
	PUSHORT pAddressOfOrds = (PUSHORT)(ImageExportDirectory->AddressOfNameOrdinals + (ULONG_PTR)ModuleBase);
	PULONG  pAddressOfNames = (PULONG)(ImageExportDirectory->AddressOfNames + (ULONG_PTR)ModuleBase);
	PULONG  pAddressOfFuncs = (PULONG)(ImageExportDirectory->AddressOfFunctions + (ULONG_PTR)ModuleBase);

	// 迴圈搜尋匯出表
	for (ULONG i = 0; i < ImageExportDirectory->NumberOfFunctions; ++i)
	{
		USHORT OrdIndex = 0xFFFF;
		PCHAR  pName = NULL;

		// 搜尋匯出表下標索引
		if ((ULONG_PTR)FunctionName <= 0xFFFF)
		{
			OrdIndex = (USHORT)i;
		}
		// 搜尋匯出表名字
		else if ((ULONG_PTR)FunctionName > 0xFFFF && i < ImageExportDirectory->NumberOfNames)
		{
			pName = (PCHAR)(pAddressOfNames[i] + (ULONG_PTR)ModuleBase);
			OrdIndex = pAddressOfOrds[i];
		}
		else
		{
			return NULL;
		}

		// 找到設定返回值並跳出
		if (((ULONG_PTR)FunctionName <= 0xFFFF && (USHORT)((ULONG_PTR)FunctionName) == OrdIndex + ImageExportDirectory->Base) || ((ULONG_PTR)FunctionName > 0xFFFF && strcmp(pName, FunctionName) == 0))
		{
			FunctionAddress = pAddressOfFuncs[OrdIndex] + (ULONG_PTR)ModuleBase;
			break;
		}
	}
	return (PVOID)FunctionAddress;
}

如何呼叫此方法,首先將ProcessID設定為需要讀取的程序PID,然後將上圖中所輸出的0x00007FF9553C0000賦值給BaseAddress接著呼叫GetModuleExportAddress()並傳入BaseAddress模組基址,需要讀取的LdrLoadDll函數名,以及當前程序的EProcess結構。

NTSTATUS DriverEntry(IN PDRIVER_OBJECT Driver, PUNICODE_STRING RegistryPath)
{
	HANDLE ProcessID = (HANDLE)4144;
	PEPROCESS EProcess = NULL;
	NTSTATUS Status = STATUS_SUCCESS;

	// 根據PID得到程序EProcess結構
	Status = PsLookupProcessByProcessId(ProcessID, &EProcess);
	if (Status != STATUS_SUCCESS)
	{
		DbgPrint("獲取EProcessID失敗 \n");
		return Status;
	}

	PVOID BaseAddress = (PVOID)0x00007FF9553C0000;
	PVOID RefAddress = 0;

	// 傳入Ntdll.dll基址 + 函數名 得到該函數地址
	RefAddress = GetModuleExportAddress(BaseAddress, "LdrLoadDll", EProcess);
	DbgPrint("[*] 函數地址: %p \n", RefAddress);

	Driver->DriverUnload = UnDriver;
	return STATUS_SUCCESS;
}

執行這段程式,即可輸出如下資訊,此時也就得到了x64.exe程序內ntdll.dll模組裡面的LdrLoadDll函數的記憶體地址,如下所示;