驅動開發:核心遠端堆分配與銷燬

2023-05-15 12:04:07

在開始學習核心記憶體讀寫篇之前,我們先來實現一個簡單的記憶體分配銷燬堆的功能,在核心空間內使用者依然可以動態的申請與銷燬一段可控的堆空間,一般而言核心中提供了ZwAllocateVirtualMemory這個函數用於專門分配虛擬空間,而與之相對應的則是ZwFreeVirtualMemory此函數則用於銷燬堆記憶體,當我們需要分配核心空間時往往需要切換到對端程序棧上再進行操作,接下來LyShark將從API開始介紹如何運用這兩個函數實現記憶體分配與使用,並以此來作為驅動讀寫篇的入門知識。

首先以記憶體分配為例ZwAllocateVirtualMemory()函數,該系列函數在ntifs.h標頭檔案內,且如果需要使用則最好提前在程式頭部進行宣告,該函數的微軟官方定義如下所示;

NTSYSAPI NTSTATUS ZwAllocateVirtualMemory(
  [in]      HANDLE    ProcessHandle,           // 程序控制程式碼
  [in, out] PVOID     *BaseAddress,            // 指向將接收已分配頁面區域基址的變數的指標
  [in]      ULONG_PTR ZeroBits,                // 節檢視基址中必須為零的高順序地址位數
  [in, out] PSIZE_T   RegionSize,              // 指向將接收已分配頁面區域的實際大小
  [in]      ULONG     AllocationType,          // 包含指定要執行的分配型別的標誌的位掩碼
  [in]      ULONG     Protect                  // 包含頁面保護標誌的位掩碼
);

引數ProcessHandle用於傳入一個程序控制程式碼此處我們可以通過NtCurrentProcess()獲取到當前自身程序的控制程式碼。

引數BaseAddress則用於接收分配堆地址的首地址,此處指向將接收已分配頁面區域基址的變數的指標。

引數RegionSize則用於指定需要分配的記憶體空間大小,此引數的初始值指定區域的大小(以位元組為單位)並向上舍入到下一個主機頁大小邊界。

引數AllocationType用於指定分配記憶體的屬性,通常我們會用到的只有兩種MEM_COMMIT指定為提交型別,MEM_PHYSICAL則用於分配實體記憶體,此標誌僅用於地址視窗擴充套件AWE記憶體。 如果設定了MEM_PHYSICAL則還必須設定MEM_RESERVE不能設定其他標誌,並且必須將保護設定為PAGE_READWRITE

引數Protect用於設定當前分批堆的保護屬性,通常當我們需要分配一段可執行指令的記憶體空間時會使用PAGE_EXECUTE_READWRITE,如果無執行需求則推薦使用PAGE_READWRITE屬性。

在對特定程序分配堆時第一步就是要切入到該程序的程序棧中,此時可通過KeStackAttachProcess()切換到程序棧,於此對應的是KeUnstackDetachProcess()脫離程序棧,這兩個函數的具體定義如下;

// 附加到程序棧
void KeStackAttachProcess(
        PRKPROCESS   PROCESS,      // 傳入EProcess結構
  [out] PRKAPC_STATE ApcState      // 指向KAPC_STATE結構的不透明指標
);
// 接觸附加
void KeUnstackDetachProcess(
  [in] PRKAPC_STATE ApcState       // 指向KAPC_STATE結構的不透明指標
);

此處如果需要附加程序棧則必須提供該程序的PRKPROCESS也就是EProcess結構,此結構可通過PsLookupProcessByProcessId()獲取到,該函數接收一個程序PID並將此PID轉為EProcess結構,函數定義如下;

NTSTATUS PsLookupProcessByProcessId(
  [in]  HANDLE    ProcessId,          // 程序PID
  [out] PEPROCESS *Process            // 輸出EP結構
);

基本的函數介紹完了,那麼這段程式碼應該不難理解了,如下程式碼中需要注意一點,引數OUT PVOID Buffer用於輸出堆地址而不是輸入地址。

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

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

// 定義宣告
NTSTATUS ZwAllocateVirtualMemory(
	__in HANDLE  ProcessHandle,
	__inout PVOID  *BaseAddress,
	__in ULONG_PTR  ZeroBits,
	__inout PSIZE_T  RegionSize,
	__in ULONG  AllocationType,
	__in ULONG  Protect
);

// 分配記憶體空間
NTSTATUS AllocMemory(IN ULONG ProcessPid, IN SIZE_T Length, OUT PVOID Buffer)
{
	NTSTATUS Status = STATUS_SUCCESS;
	PEPROCESS pEProcess = NULL;
	KAPC_STATE ApcState = { 0 };
	PVOID BaseAddress = NULL;

	// 通過程序PID得到程序EProcess
	Status = PsLookupProcessByProcessId((HANDLE)ProcessPid, &pEProcess);
	if (!NT_SUCCESS(Status) && !MmIsAddressValid(pEProcess))
	{
		return STATUS_UNSUCCESSFUL;
	}

	// 驗證記憶體可讀
	if (!MmIsAddressValid(pEProcess))
	{
		return STATUS_UNSUCCESSFUL;
	}
	__try
	{
		// 附加到程序棧
		KeStackAttachProcess(pEProcess, &ApcState);

		// 分配記憶體
		Status = ZwAllocateVirtualMemory(NtCurrentProcess(), &BaseAddress, 0, &Length, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
		RtlZeroMemory(BaseAddress, Length);

		// 返回記憶體地址
		*(PVOID*)Buffer = BaseAddress;

		// 脫離程序棧
		KeUnstackDetachProcess(&ApcState);
	}
	__except (EXCEPTION_EXECUTE_HANDLER)
	{
		KeUnstackDetachProcess(&ApcState);
		Status = STATUS_UNSUCCESSFUL;
	}

	ObDereferenceObject(pEProcess);
	return Status;
}

VOID UnDriver(PDRIVER_OBJECT driver)
{
	DbgPrint(("Uninstall Driver Is OK \n"));
}

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

	DWORD process_id = 4160;
	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);

	Driver->DriverUnload = UnDriver;
	return STATUS_SUCCESS;
}

執行如上程式碼片段,則會在程序PID=4160中開闢一段堆空間,輸出效果如下;

與建立堆相對ZwFreeVirtualMemory()則用於銷燬一個堆,其微軟定義如下所示;

NTSYSAPI NTSTATUS ZwFreeVirtualMemory(
  [in]      HANDLE  ProcessHandle,    // 程序控制程式碼
  [in, out] PVOID   *BaseAddress,     // 堆基址
  [in, out] PSIZE_T RegionSize,       // 銷燬長度
  [in]      ULONG   FreeType          // 釋放型別
);

相對於建立來說,銷燬堆則必須傳入堆空間BaseAddress基址,以及堆空間的RegionSize長度,需要格外注意FreeType引數,這是一個位掩碼,其中包含描述ZwFreeVirtualMemory將為頁面指定區域執行的任意操作型別。如果傳入MEM_DECOMMIT則將取消提交頁面的指定區域,頁面進入保留狀態。而如果設定為MEM_RELEASE則堆地址將被立即釋放。

銷燬堆空間FreeMemory()的完整程式碼如下所示,銷燬是我們使用MEM_RELEASE引數即立即銷燬。

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

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

// 申請堆
NTSTATUS ZwAllocateVirtualMemory(
	__in HANDLE  ProcessHandle,
	__inout PVOID  *BaseAddress,
	__in ULONG_PTR  ZeroBits,
	__inout PSIZE_T  RegionSize,
	__in ULONG  AllocationType,
	__in ULONG  Protect
);

// 銷燬堆
NTSYSAPI NTSTATUS ZwFreeVirtualMemory(
	__in      HANDLE  ProcessHandle,
	__inout PVOID   *BaseAddress,
	__inout PSIZE_T RegionSize,
	__in      ULONG   FreeType
);

// 分配記憶體空間
NTSTATUS AllocMemory(IN ULONG ProcessPid, IN SIZE_T Length, OUT PVOID Buffer)
{
	NTSTATUS Status = STATUS_SUCCESS;
	PEPROCESS pEProcess = NULL;
	KAPC_STATE ApcState = { 0 };
	PVOID BaseAddress = NULL;

	// 通過程序PID得到程序EProcess
	Status = PsLookupProcessByProcessId((HANDLE)ProcessPid, &pEProcess);
	if (!NT_SUCCESS(Status) && !MmIsAddressValid(pEProcess))
	{
		return STATUS_UNSUCCESSFUL;
	}

	// 驗證記憶體可讀
	if (!MmIsAddressValid(pEProcess))
	{
		return STATUS_UNSUCCESSFUL;
	}
	__try
	{
		// 附加到程序棧
		KeStackAttachProcess(pEProcess, &ApcState);

		// 分配記憶體
		Status = ZwAllocateVirtualMemory(NtCurrentProcess(), &BaseAddress, 0, &Length, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
		RtlZeroMemory(BaseAddress, Length);

		// 返回記憶體地址
		*(PVOID*)Buffer = BaseAddress;

		// 脫離程序棧
		KeUnstackDetachProcess(&ApcState);
	}
	__except (EXCEPTION_EXECUTE_HANDLER)
	{
		KeUnstackDetachProcess(&ApcState);
		Status = STATUS_UNSUCCESSFUL;
	}

	ObDereferenceObject(pEProcess);
	return Status;
}

// 登出記憶體空間
NTSTATUS FreeMemory(IN ULONG ProcessPid, IN SIZE_T Length, IN PVOID BaseAddress)
{
	NTSTATUS Status = STATUS_SUCCESS;
	PEPROCESS pEProcess = NULL;
	KAPC_STATE ApcState = { 0 };

	Status = PsLookupProcessByProcessId((HANDLE)ProcessPid, &pEProcess);
	if (!NT_SUCCESS(Status) && !MmIsAddressValid(pEProcess))
	{
	return STATUS_UNSUCCESSFUL;
	}

	if (!MmIsAddressValid(pEProcess))
	{
		return STATUS_UNSUCCESSFUL;
	}

	__try
	{
		// 附加到程序棧
		KeStackAttachProcess(pEProcess, &ApcState);

		// 釋放記憶體
		Status = ZwFreeVirtualMemory(NtCurrentProcess(), &BaseAddress, &Length, MEM_RELEASE);

		// 脫離程序棧
		KeUnstackDetachProcess(&ApcState);
	}
	__except (EXCEPTION_EXECUTE_HANDLER)
	{
		KeUnstackDetachProcess(&ApcState);
		Status = STATUS_UNSUCCESSFUL;
	}

	ObDereferenceObject(pEProcess);
	return Status;
}

VOID UnDriver(PDRIVER_OBJECT driver)
{
	DbgPrint(("Uninstall Driver Is OK \n"));
}

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

	DWORD process_id = 4160;
	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);

	Status = FreeMemory(process_id, create_size, ref_address);
	DbgPrint("銷燬堆地址: %p \n", ref_address);

	Driver->DriverUnload = UnDriver;
	return STATUS_SUCCESS;
}

編譯並執行如上這段程式碼,程式碼會首先呼叫AllocMemory()函數實現分配堆,然後呼叫FreeMemory()函數銷燬堆,並輸出銷燬地址,如下圖所示;