驅動開發:核心R3與R0記憶體對映拷貝

2022-10-11 15:01:16

在上一篇博文《驅動開發:核心通過PEB得到程序引數》中我們通過使用KeStackAttachProcess附加程序的方式得到了該程序的PEB結構資訊,本篇文章同樣需要使用程序附加功能,但這次我們將實現一個更加有趣的功能,在某些情況下應用層與核心層需要共用一片記憶體區域通過這片區域可打通核心與應用層的隔離,此類功能的實現依附於MDL記憶體對映機制實現。

應用層(R3)資料對映到核心層(R0)

先來實現將R3記憶體資料拷貝到R0中,功能實現所呼叫的API如下:

  • IoAllocateMdl 該函數用於建立MDL(類似初始化)
  • MmProbeAndLockPages 用於鎖定建立的地址其中UserMode代表使用者層,IoReadAccess以讀取的方式鎖定
  • MmGetSystemAddressForMdlSafe 用於從MDL中得到對映記憶體地址
  • RtlCopyMemory 用於記憶體拷貝,將DstAddr應用層中的資料拷貝到pMappedSrc
  • MmUnlockPages 拷貝結束後解鎖pSrcMdl
  • IoFreeMdl 釋放MDL

記憶體拷貝SafeCopyMemory_R3_to_R0函數封裝程式碼如下:

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

// 分配記憶體
void* RtlAllocateMemory(BOOLEAN InZeroMemory, SIZE_T InSize)
{
	void* Result = ExAllocatePoolWithTag(NonPagedPool, InSize, 'lysh');
	if (InZeroMemory && (Result != NULL))
		RtlZeroMemory(Result, InSize);
	return Result;
}

// 釋放記憶體
void RtlFreeMemory(void* InPointer)
{
	ExFreePool(InPointer);
}

/*
將應用層中的記憶體複製到核心變數中

SrcAddr  r3地址要複製
DstAddr  R0申請的地址
Size     拷貝長度
*/
NTSTATUS SafeCopyMemory_R3_to_R0(ULONG_PTR SrcAddr, ULONG_PTR DstAddr, ULONG Size)
{
	NTSTATUS status = STATUS_UNSUCCESSFUL;
	ULONG nRemainSize = PAGE_SIZE - (SrcAddr & 0xFFF);
	ULONG nCopyedSize = 0;

	if (!SrcAddr || !DstAddr || !Size)
	{
		return status;
	}

	while (nCopyedSize < Size)
	{
		PMDL pSrcMdl = NULL;
		PVOID pMappedSrc = NULL;

		if (Size - nCopyedSize < nRemainSize)
		{
			nRemainSize = Size - nCopyedSize;
		}

		// 建立MDL
		pSrcMdl = IoAllocateMdl((PVOID)(SrcAddr & 0xFFFFFFFFFFFFF000), PAGE_SIZE, FALSE, FALSE, NULL);
		if (pSrcMdl)
		{
			__try
			{
				// 鎖定記憶體頁面(UserMode代表應用層)
				MmProbeAndLockPages(pSrcMdl, UserMode, IoReadAccess);

				// 從MDL中得到對映記憶體地址
				pMappedSrc = MmGetSystemAddressForMdlSafe(pSrcMdl, NormalPagePriority);
			}
			__except (EXCEPTION_EXECUTE_HANDLER)
			{
			}
		}

		if (pMappedSrc)
		{
			__try
			{
				// 將MDL中的對映拷貝到pMappedSrc記憶體中
				RtlCopyMemory((PVOID)DstAddr, (PVOID)((ULONG_PTR)pMappedSrc + (SrcAddr & 0xFFF)), nRemainSize);
			}
			__except (1)
			{
				// 拷貝記憶體異常
			}

			// 釋放鎖
			MmUnlockPages(pSrcMdl);
		}

		if (pSrcMdl)
		{
			// 釋放MDL
			IoFreeMdl(pSrcMdl);
		}

		if (nCopyedSize)
		{
			nRemainSize = PAGE_SIZE;
		}

		nCopyedSize += nRemainSize;
		SrcAddr += nRemainSize;
		DstAddr += nRemainSize;
	}

	status = STATUS_SUCCESS;
	return status;
}

呼叫該函數實現拷貝,如下程式碼中首先PsLookupProcessByProcessId得到程序EProcess結構,並KeStackAttachProcess附加程序,宣告pTempBuffer指標用於儲存RtlAllocateMemory開闢的記憶體空間,nSize則代表讀取應用層程序資料長度,ModuleBase則是讀入程序基址,呼叫SafeCopyMemory_R3_to_R0即可將應用層資料拷貝到核心空間,並最終BYTE* data轉為BYTE位元組的方式輸出。

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

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

	NTSTATUS status = STATUS_UNSUCCESSFUL;
	PEPROCESS eproc = NULL;
	KAPC_STATE kpc = { 0 };

	__try
	{
		// HANDLE 程序PID
		status = PsLookupProcessByProcessId((HANDLE)4556, &eproc);

		if (NT_SUCCESS(status))
		{
			// 附加程序
			KeStackAttachProcess(eproc, &kpc);

			// -------------------------------------------------------------------
			// 開始對映
			// -------------------------------------------------------------------

			// 將使用者空間記憶體對映到核心空間
			PVOID pTempBuffer = NULL;
			ULONG nSize = 0x1024;
			ULONG_PTR ModuleBase = 0x0000000140001000;

			// 分配記憶體
			pTempBuffer = RtlAllocateMemory(TRUE, nSize);
			if (pTempBuffer)
			{
				// 拷貝資料到R0
				status = SafeCopyMemory_R3_to_R0(ModuleBase, (ULONG_PTR)pTempBuffer, nSize);
				if (NT_SUCCESS(status))
				{
					DbgPrint("[*] 拷貝應用層資料到核心裡 \n");
				}

				// 轉成BYTE方便讀取
				BYTE* data = pTempBuffer;

				for (size_t i = 0; i < 10; i++)
				{
					DbgPrint("%02X \n", data[i]);
				}
			}

			// 釋放空間
			RtlFreeMemory(pTempBuffer);

			// 脫離程序
			KeUnstackDetachProcess(&kpc);
		}
	}
	__except (EXCEPTION_EXECUTE_HANDLER)
	{
		Driver->DriverUnload = UnDriver;
		return STATUS_SUCCESS;
	}

	Driver->DriverUnload = UnDriver;
	return STATUS_SUCCESS;
}

程式碼執行後即可將程序中0x0000000140001000處的資料讀入核心空間並輸出:


核心層(R0)資料對映到應用層(R3)

與上方功能實現相反SafeCopyMemory_R0_to_R3函數則用於將一個核心層中的緩衝區寫出到應用層中,寫出過程:

  • IoAllocateMdl 分別呼叫MDL分配,源地址SrcAddr目標地址DstAddr均建立
  • MmBuildMdlForNonPagedPool 該 MDL 指定非分頁虛擬記憶體緩衝區,並對其進行更新以描述基礎物理頁
  • MmGetSystemAddressForMdlSafe 呼叫兩次得到源地址,分別獲取pSrcMdl,pDstMdl兩個MDL的
  • MmProbeAndLockPages 以寫入方式鎖定使用者層中pDstMdl的地址

記憶體拷貝SafeCopyMemory_R0_to_R3函數封裝程式碼如下:

// 分配記憶體
void* RtlAllocateMemory(BOOLEAN InZeroMemory, SIZE_T InSize)
{
	void* Result = ExAllocatePoolWithTag(NonPagedPool, InSize, 'lysh');
	if (InZeroMemory && (Result != NULL))
		RtlZeroMemory(Result, InSize);
	return Result;
}

// 釋放記憶體
void RtlFreeMemory(void* InPointer)
{
	ExFreePool(InPointer);
}

/*
將記憶體中的資料複製到R3中

SrcAddr  R0要複製的地址
DstAddr  返回R3的地址
Size     拷貝長度
*/
NTSTATUS SafeCopyMemory_R0_to_R3(PVOID SrcAddr, PVOID DstAddr, ULONG Size)
{
	PMDL  pSrcMdl = NULL, pDstMdl = NULL;
	PUCHAR pSrcAddress = NULL, pDstAddress = NULL;
	NTSTATUS st = STATUS_UNSUCCESSFUL;

	// 分配MDL 源地址
	pSrcMdl = IoAllocateMdl(SrcAddr, Size, FALSE, FALSE, NULL);
	if (!pSrcMdl)
	{
		return st;
	}

	// 該 MDL 指定非分頁虛擬記憶體緩衝區,並對其進行更新以描述基礎物理頁。
	MmBuildMdlForNonPagedPool(pSrcMdl);

	// 獲取源地址MDL地址
	pSrcAddress = MmGetSystemAddressForMdlSafe(pSrcMdl, NormalPagePriority);


	if (!pSrcAddress)
	{
		IoFreeMdl(pSrcMdl);
		return st;
	}

	// 分配MDL 目標地址
	pDstMdl = IoAllocateMdl(DstAddr, Size, FALSE, FALSE, NULL);
	if (!pDstMdl)
	{
		IoFreeMdl(pSrcMdl);
		return st;
	}

	__try
	{
		// 以寫入的方式鎖定目標MDL
		MmProbeAndLockPages(pDstMdl, UserMode, IoWriteAccess);

		// 獲取目標地址MDL地址
		pDstAddress = MmGetSystemAddressForMdlSafe(pDstMdl, NormalPagePriority);
	}
	__except (EXCEPTION_EXECUTE_HANDLER)
	{
	}

	if (pDstAddress)
	{
		__try
		{
			// 將源地址拷貝到目標地址
			RtlCopyMemory(pDstAddress, pSrcAddress, Size);
		}
		__except (1)
		{
			// 拷貝記憶體異常
		}
		MmUnlockPages(pDstMdl);
		st = STATUS_SUCCESS;
	}

	IoFreeMdl(pDstMdl);
	IoFreeMdl(pSrcMdl);

	return st;
}

呼叫該函數實現拷貝,此處除去附加程序以外,在拷貝之前呼叫了ZwAllocateVirtualMemory將記憶體屬性設定為PAGE_EXECUTE_READWRITE可讀可寫可執行狀態,然後在向該記憶體中寫出pTempBuffer變數中的內容,此變數中的資料是0x90填充的區域。

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

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

	NTSTATUS status = STATUS_UNSUCCESSFUL;
	PEPROCESS eproc = NULL;
	KAPC_STATE kpc = { 0 };

	__try
	{
		// HANDLE 程序PID
		status = PsLookupProcessByProcessId((HANDLE)4556, &eproc);

		if (NT_SUCCESS(status))
		{
			// 附加程序
			KeStackAttachProcess(eproc, &kpc);

			// -------------------------------------------------------------------
			// 開始對映
			// -------------------------------------------------------------------

			// 將使用者空間記憶體對映到核心空間
			PVOID pTempBuffer = NULL;
			ULONG nSize = 0x1024;
			PVOID ModuleBase = 0x0000000140001000;

			// 分配記憶體
			pTempBuffer = RtlAllocateMemory(TRUE, nSize);
			if (pTempBuffer)
			{
				memset(pTempBuffer, 0x90, nSize);

				// 設定記憶體屬性 PAGE_EXECUTE_READWRITE
				ZwAllocateVirtualMemory(NtCurrentProcess(), &ModuleBase, 0, &nSize, MEM_RESERVE, PAGE_EXECUTE_READWRITE);
				ZwAllocateVirtualMemory(NtCurrentProcess(), &ModuleBase, 0, &nSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE);

				// 將資料拷貝到R3中
				status = SafeCopyMemory_R0_to_R3(pTempBuffer, &ModuleBase, nSize);
				if (NT_SUCCESS(status))
				{
					DbgPrint("[*] 拷貝核心資料到應用層 \n");
				}
			}

			// 釋放空間
			RtlFreeMemory(pTempBuffer);

			// 脫離程序
			KeUnstackDetachProcess(&kpc);
		}
	}
	__except (EXCEPTION_EXECUTE_HANDLER)
	{
		Driver->DriverUnload = UnDriver;
		return STATUS_SUCCESS;
	}

	Driver->DriverUnload = UnDriver;
	return STATUS_SUCCESS;
}

拷貝成功後,應用層程序內將會被填充為Nop指令。