驅動開發:核心LoadLibrary實現DLL注入

2023-06-13 12:01:04

遠端執行緒注入是最常用的一種注入技術,在應用層注入是通過CreateRemoteThread這個函數實現的,該函數通過建立執行緒並呼叫 LoadLibrary 動態載入指定的DLL來實現注入,而在核心層同樣存在一個類似的核心函數RtlCreateUserThread,但需要注意的是此函數未被公開,RtlCreateUserThread其實是對NtCreateThreadEx的包裝,但最終會呼叫ZwCreateThread來實現注入,RtlCreateUserThreadCreateRemoteThread的底層實現。

基於LoadLibrary實現的注入原理可以具體分為如下幾步;

  • 1.呼叫AllocMemory,在對端應用層開闢空間,函數封裝來源於《核心遠端堆分配與銷燬》章節;
  • 2.呼叫MDLWriteMemory,將DLL路徑字串寫出到對端記憶體,函數封裝來源於《核心MDL讀寫程序記憶體》章節;
  • 3.呼叫GetUserModuleAddress,獲取到kernel32.dll模組基址,函數封裝來源於《核心遠端執行緒實現DLL注入》章節;
  • 4.呼叫GetModuleExportAddress,獲取到LoadLibraryW函數的記憶體地址,函數封裝來源於《核心遠端執行緒實現DLL注入》章節;
  • 5.最後呼叫本章封裝函數MyCreateRemoteThread,將應用層DLL動態轉載到程序內,實現DLL注入;

總結起來就是首先在目標程序申請一塊空間,空間裡面寫入要注入的DLL的路徑字串或者是一段ShellCode,找到該記憶體中LoadLibrary的基址並傳入到RtlCreateUserThread中,此時程序自動載入我們指定路徑下的DLL檔案。

注入依賴於RtlCreateUserThread這個未到處核心函數,該核心函數中最需要關心的引數是ProcessHandle用於接收程序控制程式碼,StartAddress接收一個函數地址,StartParameter用於對函數傳遞引數,具體的函數原型如下所示;

typedef DWORD(WINAPI* pRtlCreateUserThread)(
    IN HANDLE                    ProcessHandle,          // 程序控制程式碼
    IN PSECURITY_DESCRIPTOR      SecurityDescriptor,
    IN BOOL                      CreateSuspended,
    IN ULONG                     StackZeroBits,
    IN OUT PULONG                StackReserved,
    IN OUT PULONG                StackCommit,
    IN LPVOID                    StartAddress,          // 執行函數地址 LoadLibraryW
    IN LPVOID                    StartParameter,        // 引數傳遞
    OUT HANDLE                   ThreadHandle,          // 執行緒控制程式碼
    OUT LPVOID                   ClientID
    );

由於我們載入DLL使用的是LoadLibraryW函數,此函數在執行時只需要一個引數,我們可以將DLL的路徑傳遞進去,並呼叫LoadLibraryW以此來將特定模組拉起,該函數的定義規範如下所示;

HMODULE LoadLibraryW(
  [in] LPCWSTR lpLibFileName
);

根據上一篇文章中針對注入標頭檔案lyshark.h的封裝,本章將繼續使用這個標頭檔案中的函數,首先我們實現這樣一個功能,將一段準備好的UCHAR字串動態的寫出到應用層程序記憶體,並以寬位元組模式寫出在對端記憶體中,這段程式碼可以寫為如下樣子;

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

#include "lyshark.h"

// 驅動解除安裝例程
VOID UnDriver(PDRIVER_OBJECT driver)
{
	DbgPrint("Uninstall Driver \n");
}

// 驅動入口地址
NTSTATUS DriverEntry(IN PDRIVER_OBJECT Driver, PUNICODE_STRING RegistryPath)
{
	DbgPrint("Hello LyShark \n");

	DWORD process_id = 7112;
	DWORD create_size = 1024;
	DWORD64 ref_address = 0;

	// 分配記憶體堆 《核心遠端堆分配與銷燬》 核心程式碼
	NTSTATUS Status = AllocMemory(process_id, create_size, &ref_address);

	DbgPrint("對端程序: %d \n", process_id);
	DbgPrint("分配長度: %d \n", create_size);
	DbgPrint("[*] 分配核心堆基址: %p \n", ref_address);

	UCHAR DllPath[256] = "C:\\hook.dll";
	UCHAR Item[256] = { 0 };

	// 將位元組轉為雙字
	for (int x = 0, y = 0; x < strlen(DllPath) * 2; x += 2, y++)
	{
		Item[x] = DllPath[y];
	}

	// 寫出記憶體 《核心MDL讀寫程序記憶體》 核心程式碼
	ReadMemoryStruct ptr;

	ptr.pid = process_id;
	ptr.address = ref_address;
	ptr.size = strlen(DllPath) * 2;

	// 需要寫入的資料
	ptr.data = ExAllocatePool(PagedPool, ptr.size);

	// 迴圈設定
	for (int i = 0; i < ptr.size; i++)
	{
		ptr.data[i] = Item[i];
	}

	// 寫記憶體
	MDLWriteMemory(&ptr);

	Driver->DriverUnload = UnDriver;
	return STATUS_SUCCESS;
}

執行如上方所示的程式碼,將會在目標程序7112中開闢一段記憶體空間,並寫出C:\hook.dll字串,執行效果圖如下所示;

此處你可以通過x64dbg附加到應用層程序內,並觀察記憶體0000000002200000會看到如下字串已被寫出,雙字型別則是每一個字元空一格,效果圖如下所示;

繼續實現所需要的子功能,實現動態獲取Kernel32.dll模組裡面LiadLibraryW這個匯出函數的記憶體地址,這段程式碼相信你可以很容易的寫出來,根據上節課的知識點我們可以二次封裝一個GetProcessAddress來實現對特定模組基址的獲取功能,如下是完整程式碼案例;

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

#include "lyshark.h"

// 實現取模組基址
PVOID GetProcessAddress(HANDLE ProcessID, PWCHAR DllName, PCCHAR FunctionName)
{
	PEPROCESS EProcess = NULL;
	NTSTATUS Status = STATUS_SUCCESS;
	KAPC_STATE ApcState;
	PVOID RefAddress = 0;

	// 根據PID得到程序EProcess結構
	Status = PsLookupProcessByProcessId(ProcessID, &EProcess);
	if (Status != STATUS_SUCCESS)
	{
		return Status;
	}

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

	// 驗證地址是否可讀
	if (!MmIsAddressValid(EProcess))
	{
		return NULL;
	}

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

	__try
	{
		UNICODE_STRING DllUnicodeString = { 0 };
		PVOID BaseAddress = NULL;

		// 得到程序內模組基地址
		RtlInitUnicodeString(&DllUnicodeString, DllName);

		BaseAddress = GetUserModuleAddress(EProcess, &DllUnicodeString, IsWow64);

		if (!BaseAddress)
		{
			return NULL;
		}

		DbgPrint("[*] 模組基址: %p \n", BaseAddress);

		// 得到該函數地址
		RefAddress = GetModuleExportAddress(BaseAddress, FunctionName, EProcess);
		DbgPrint("[*] 函數地址: %p \n", RefAddress);
	}
	__except (EXCEPTION_EXECUTE_HANDLER)
	{
		return NULL;
	}

	// 取消附加
	KeUnstackDetachProcess(&ApcState);
	return RefAddress;
}

VOID Unload(PDRIVER_OBJECT pDriverObj)
{
	DbgPrint("[-] 驅動解除安裝 \n");
}

NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegPath)
{
	DbgPrint("Hello LyShark.com \n");

	// 取模組基址
	PVOID pLoadLibraryW = GetProcessAddress(5200, L"kernel32.dll", "LoadLibraryW");

	DbgPrint("[*] 所在記憶體地址 = %p \n", pLoadLibraryW);

	DriverObject->DriverUnload = Unload;
	return STATUS_SUCCESS;
}

編譯並執行如上驅動程式碼,將自動獲取PID=5200程序中Kernel32.dll模組內的LoadLibraryW的記憶體地址,輸出效果圖如下所示;

實現注入的最後一步就是呼叫自定義函數MyCreateRemoteThread該函數實現原理是呼叫RtlCreateUserThread開執行緒執行,這段程式碼的最終實現如下所示;

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

#include "lyshark.h"

// 定義函數指標
typedef PVOID(NTAPI* PfnRtlCreateUserThread)
(
	IN HANDLE ProcessHandle,
	IN PSECURITY_DESCRIPTOR SecurityDescriptor,
	IN BOOLEAN CreateSuspended,
	IN ULONG StackZeroBits,
	IN OUT size_t StackReserved,
	IN OUT size_t StackCommit,
	IN PVOID StartAddress,
	IN PVOID StartParameter,
	OUT PHANDLE ThreadHandle,
	OUT PCLIENT_ID ClientID
);

// 實現取模組基址
PVOID GetProcessAddress(HANDLE ProcessID, PWCHAR DllName, PCCHAR FunctionName)
{
	PEPROCESS EProcess = NULL;
	NTSTATUS Status = STATUS_SUCCESS;
	KAPC_STATE ApcState;
	PVOID RefAddress = 0;

	// 根據PID得到程序EProcess結構
	Status = PsLookupProcessByProcessId(ProcessID, &EProcess);
	if (Status != STATUS_SUCCESS)
	{
		return Status;
	}

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

	// 驗證地址是否可讀
	if (!MmIsAddressValid(EProcess))
	{
		return NULL;
	}

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

	__try
	{
		UNICODE_STRING DllUnicodeString = { 0 };
		PVOID BaseAddress = NULL;

		// 得到程序內模組基地址
		RtlInitUnicodeString(&DllUnicodeString, DllName);

		BaseAddress = GetUserModuleAddress(EProcess, &DllUnicodeString, IsWow64);

		if (!BaseAddress)
		{
			return NULL;
		}

		DbgPrint("[*] 模組基址: %p \n", BaseAddress);

		// 得到該函數地址
		RefAddress = GetModuleExportAddress(BaseAddress, FunctionName, EProcess);
		DbgPrint("[*] 函數地址: %p \n", RefAddress);
	}
	__except (EXCEPTION_EXECUTE_HANDLER)
	{
		return NULL;
	}

	// 取消附加
	KeUnstackDetachProcess(&ApcState);
	return RefAddress;
}

// 遠端執行緒注入函數
BOOLEAN MyCreateRemoteThread(ULONG pid, PVOID pRing3Address, PVOID PParam)
{
	NTSTATUS status = STATUS_UNSUCCESSFUL;
	PEPROCESS pEProcess = NULL;
	KAPC_STATE ApcState = { 0 };

	PfnRtlCreateUserThread RtlCreateUserThread = NULL;
	HANDLE hThread = 0;

	__try
	{
		// 獲取RtlCreateUserThread函數的記憶體地址
		UNICODE_STRING ustrRtlCreateUserThread;
		RtlInitUnicodeString(&ustrRtlCreateUserThread, L"RtlCreateUserThread");
		RtlCreateUserThread = (PfnRtlCreateUserThread)MmGetSystemRoutineAddress(&ustrRtlCreateUserThread);
		if (RtlCreateUserThread == NULL)
		{
			return FALSE;
		}

		// 根據程序PID獲取程序EProcess結構
		status = PsLookupProcessByProcessId((HANDLE)pid, &pEProcess);
		if (!NT_SUCCESS(status))
		{
			return FALSE;
		}

		// 附加到目標程序內
		KeStackAttachProcess(pEProcess, &ApcState);

		// 驗證程序是否可讀寫
		if (!MmIsAddressValid(pRing3Address))
		{
			return FALSE;
		}

		// 啟動注入執行緒
		status = RtlCreateUserThread(ZwCurrentProcess(),
			NULL,
			FALSE,
			0,
			0,
			0,
			pRing3Address,
			PParam,
			&hThread,
			NULL);
		if (!NT_SUCCESS(status))
		{
			return FALSE;
		}

		return TRUE;
	}

	__finally
	{
		// 釋放物件
		if (pEProcess != NULL)
		{
			ObDereferenceObject(pEProcess);
			pEProcess = NULL;
		}

		// 取消附加程序
		KeUnstackDetachProcess(&ApcState);
	}

	return FALSE;
}

VOID Unload(PDRIVER_OBJECT pDriverObj)
{
	DbgPrint("[-] 驅動解除安裝 \n");
}

NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegPath)
{
	DbgPrint("Hello LyShark.com \n");

	ULONG process_id = 5200;
	DWORD create_size = 1024;
	DWORD64 ref_address = 0;

	// -------------------------------------------------------
	// 取模組基址
	// -------------------------------------------------------

	PVOID pLoadLibraryW = GetProcessAddress(process_id, L"kernel32.dll", "LoadLibraryW");
	DbgPrint("[*] 所在記憶體地址 = %p \n", pLoadLibraryW);

	// -------------------------------------------------------
	// 應用層開堆
	// -------------------------------------------------------

	NTSTATUS Status = AllocMemory(process_id, create_size, &ref_address);

	DbgPrint("對端程序: %d \n", process_id);
	DbgPrint("分配長度: %d \n", create_size);
	DbgPrint("分配的核心堆基址: %p \n", ref_address);

	// 設定注入路徑,轉換為多位元組
	UCHAR DllPath[256] = "C:\\lyshark_hook.dll";
	UCHAR Item[256] = { 0 };

	for (int x = 0, y = 0; x < strlen(DllPath) * 2; x += 2, y++)
	{
		Item[x] = DllPath[y];
	}

	// -------------------------------------------------------
	// 寫出資料到記憶體
	// -------------------------------------------------------

	ReadMemoryStruct ptr;

	ptr.pid = process_id;
	ptr.address = ref_address;
	ptr.size = strlen(DllPath) * 2;

	// 需要寫入的資料
	ptr.data = ExAllocatePool(PagedPool, ptr.size);

	// 迴圈設定
	for (int i = 0; i < ptr.size; i++)
	{
		ptr.data[i] = Item[i];
	}

	// 寫記憶體
	MDLWriteMemory(&ptr);

	// -------------------------------------------------------
	// 執行開執行緒函數
	// -------------------------------------------------------

	// 執行執行緒注入
	// 引數1:PID
	// 引數2:LoadLibraryW記憶體地址
	// 引數3:當前DLL路徑
	BOOLEAN flag = MyCreateRemoteThread(process_id, pLoadLibraryW, ref_address);
	if (flag == TRUE)
	{
		DbgPrint("[*] 已完成程序 %d 注入檔案 %s \n", process_id, DllPath);
	}

	DriverObject->DriverUnload = Unload;
	return STATUS_SUCCESS;
}

編譯這段驅動程式,並將其放入虛擬機器器中,在C槽下面放置好一個名為lyshark_hook.dll檔案,執行驅動程式將自動插入DLL到Win32Project程序內,輸出效果圖如下所示;

回到應用層程序,則可看到如下圖所示的注入成功提示資訊;