驅動開發:核心列舉LoadImage映像回撥

2022-10-20 21:00:44

在筆者之前的文章《驅動開發:核心特徵碼搜尋函數封裝》中我們封裝實現了特徵碼定位功能,本章將繼續使用該功能,本次我們需要列舉核心LoadImage映像回撥,在Win64環境下我們可以設定一個LoadImage映像載入通告回撥,當有新驅動或者DLL被載入時,回撥函數就會被呼叫從而執行我們自己的回撥例程,映像回撥也儲存在陣列裡,列舉時從陣列中讀取值之後,需要進行位運算解密得到地址。

我們來看一款閉源ARK工具是如何實現的:

如上所述,如果我們需要拿到回撥陣列那麼首先要得到該陣列,陣列的符號名是PspLoadImageNotifyRoutine我們可以在PsSetLoadImageNotifyRoutineEx中找到。

第一步使用WinDBG輸入uf PsSetLoadImageNotifyRoutineEx首先定位到,能夠找到PsSetLoadImageNotifyRoutineEx這裡的兩個位置都可以被參照,當然了這個函數可以直接通過PsSetLoadImageNotifyRoutineEx函數動態拿到此處不需要我們動態定位。

我們通過獲取到PsSetLoadImageNotifyRoutineEx函數的記憶體首地址,然後向下匹配特徵碼搜尋找到488d0d88e8dbff並取出PspLoadImageNotifyRoutine記憶體地址,該記憶體地址就是LoadImage映像模組的基址。

如果使用程式碼去定位這段空間,則你可以這樣寫,這樣即可得到具體特徵地址。

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

#include <ntddk.h>
#include <windef.h>

// 指定記憶體區域的特徵碼掃描
PVOID SearchMemory(PVOID pStartAddress, PVOID pEndAddress, PUCHAR pMemoryData, ULONG ulMemoryDataSize)
{
	PVOID pAddress = NULL;
	PUCHAR i = NULL;
	ULONG m = 0;

	// 掃描記憶體
	for (i = (PUCHAR)pStartAddress; i < (PUCHAR)pEndAddress; i++)
	{
		// 判斷特徵碼
		for (m = 0; m < ulMemoryDataSize; m++)
		{
			if (*(PUCHAR)(i + m) != pMemoryData[m])
			{
				break;
			}
		}
		// 判斷是否找到符合特徵碼的地址
		if (m >= ulMemoryDataSize)
		{
			// 找到特徵碼位置, 獲取緊接著特徵碼的下一地址
			pAddress = (PVOID)(i + ulMemoryDataSize);
			break;
		}
	}

	return pAddress;
}

// 根據特徵碼獲取 PspLoadImageNotifyRoutine 陣列地址
PVOID SearchPspLoadImageNotifyRoutine(PUCHAR pSpecialData, ULONG ulSpecialDataSize)
{
	UNICODE_STRING ustrFuncName;
	PVOID pAddress = NULL;
	LONG lOffset = 0;
	PVOID pPsSetLoadImageNotifyRoutine = NULL;
	PVOID pPspLoadImageNotifyRoutine = NULL;

	// 先獲取 PsSetLoadImageNotifyRoutineEx 函數地址
	RtlInitUnicodeString(&ustrFuncName, L"PsSetLoadImageNotifyRoutineEx");
	pPsSetLoadImageNotifyRoutine = MmGetSystemRoutineAddress(&ustrFuncName);
	if (NULL == pPsSetLoadImageNotifyRoutine)
	{
		return pPspLoadImageNotifyRoutine;
	}

	// 查詢 PspLoadImageNotifyRoutine  函數地址
	pAddress = SearchMemory(pPsSetLoadImageNotifyRoutine, (PVOID)((PUCHAR)pPsSetLoadImageNotifyRoutine + 0xFF), pSpecialData, ulSpecialDataSize);
	if (NULL == pAddress)
	{
		return pPspLoadImageNotifyRoutine;
	}

	// 先獲取偏移, 再計算地址
	lOffset = *(PLONG)pAddress;
	pPspLoadImageNotifyRoutine = (PVOID)((PUCHAR)pAddress + sizeof(LONG) + lOffset);

	return pPspLoadImageNotifyRoutine;
}

VOID UnDriver(PDRIVER_OBJECT Driver)
{
}

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

	PVOID pPspLoadImageNotifyRoutineAddress = NULL;
	RTL_OSVERSIONINFOW osInfo = { 0 };
	UCHAR pSpecialData[50] = { 0 };
	ULONG ulSpecialDataSize = 0;

	// 獲取系統版本資訊, 判斷系統版本
	RtlGetVersion(&osInfo);
	if (10 == osInfo.dwMajorVersion)
	{
		// 48 8d 0d 88 e8 db ff
		// 查詢指令 lea rcx,[nt!PspLoadImageNotifyRoutine (fffff804`44313ce0)]
		/*
		nt!PsSetLoadImageNotifyRoutineEx+0x41:
		fffff801`80748a81 488d0dd8d3dbff  lea     rcx,[nt!PspLoadImageNotifyRoutine (fffff801`80505e60)]
		fffff801`80748a88 4533c0          xor     r8d,r8d
		fffff801`80748a8b 488d0cd9        lea     rcx,[rcx+rbx*8]
		fffff801`80748a8f 488bd7          mov     rdx,rdi
		fffff801`80748a92 e80584a3ff      call    nt!ExCompareExchangeCallBack (fffff801`80180e9c)
		fffff801`80748a97 84c0            test    al,al
		fffff801`80748a99 0f849f000000    je      nt!PsSetLoadImageNotifyRoutineEx+0xfe (fffff801`80748b3e)  Branch
		*/
		pSpecialData[0] = 0x48;
		pSpecialData[1] = 0x8D;
		pSpecialData[2] = 0x0D;
		ulSpecialDataSize = 3;
	}

	// 根據特徵碼獲取地址 獲取 PspLoadImageNotifyRoutine 陣列地址
	pPspLoadImageNotifyRoutineAddress = SearchPspLoadImageNotifyRoutine(pSpecialData, ulSpecialDataSize);
	DbgPrint("[LyShark] PspLoadImageNotifyRoutine = 0x%p \n", pPspLoadImageNotifyRoutineAddress);

	Driver->DriverUnload = UnDriver;
	return STATUS_SUCCESS;
}

將這個驅動拖入到虛擬機器器中並執行,輸出結果如下:

有了陣列地址接下來就是要對陣列進行解密,如何解密?

  • 1.首先拿到陣列指標pPspLoadImageNotifyRoutineAddress + sizeof(PVOID) * i此處的i也就是下標。
  • 2.得到的新地址在與pNotifyRoutineAddress & 0xfffffffffffffff8進行與運算。
  • 3.最後*(PVOID *)pNotifyRoutineAddress取出裡面的引數。

增加解密程式碼以後,這段程式的完整程式碼也就可以被寫出來了,如下所示。

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

#include <ntddk.h>
#include <windef.h>

// 指定記憶體區域的特徵碼掃描
PVOID SearchMemory(PVOID pStartAddress, PVOID pEndAddress, PUCHAR pMemoryData, ULONG ulMemoryDataSize)
{
	PVOID pAddress = NULL;
	PUCHAR i = NULL;
	ULONG m = 0;

	// 掃描記憶體
	for (i = (PUCHAR)pStartAddress; i < (PUCHAR)pEndAddress; i++)
	{
		// 判斷特徵碼
		for (m = 0; m < ulMemoryDataSize; m++)
		{
			if (*(PUCHAR)(i + m) != pMemoryData[m])
			{
				break;
			}
		}
		// 判斷是否找到符合特徵碼的地址
		if (m >= ulMemoryDataSize)
		{
			// 找到特徵碼位置, 獲取緊接著特徵碼的下一地址
			pAddress = (PVOID)(i + ulMemoryDataSize);
			break;
		}
	}

	return pAddress;
}

// 根據特徵碼獲取 PspLoadImageNotifyRoutine 陣列地址
PVOID SearchPspLoadImageNotifyRoutine(PUCHAR pSpecialData, ULONG ulSpecialDataSize)
{
	UNICODE_STRING ustrFuncName;
	PVOID pAddress = NULL;
	LONG lOffset = 0;
	PVOID pPsSetLoadImageNotifyRoutine = NULL;
	PVOID pPspLoadImageNotifyRoutine = NULL;

	// 先獲取 PsSetLoadImageNotifyRoutineEx 函數地址
	RtlInitUnicodeString(&ustrFuncName, L"PsSetLoadImageNotifyRoutineEx");
	pPsSetLoadImageNotifyRoutine = MmGetSystemRoutineAddress(&ustrFuncName);
	if (NULL == pPsSetLoadImageNotifyRoutine)
	{
		return pPspLoadImageNotifyRoutine;
	}

	// 查詢 PspLoadImageNotifyRoutine  函數地址
	pAddress = SearchMemory(pPsSetLoadImageNotifyRoutine, (PVOID)((PUCHAR)pPsSetLoadImageNotifyRoutine + 0xFF), pSpecialData, ulSpecialDataSize);
	if (NULL == pAddress)
	{
		return pPspLoadImageNotifyRoutine;
	}

	// 先獲取偏移, 再計算地址
	lOffset = *(PLONG)pAddress;
	pPspLoadImageNotifyRoutine = (PVOID)((PUCHAR)pAddress + sizeof(LONG) + lOffset);

	return pPspLoadImageNotifyRoutine;
}

VOID UnDriver(PDRIVER_OBJECT Driver)
{
}

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

	PVOID pPspLoadImageNotifyRoutineAddress = NULL;
	RTL_OSVERSIONINFOW osInfo = { 0 };
	UCHAR pSpecialData[50] = { 0 };
	ULONG ulSpecialDataSize = 0;

	// 獲取系統版本資訊, 判斷系統版本
	RtlGetVersion(&osInfo);
	if (10 == osInfo.dwMajorVersion)
	{
		// 48 8d 0d 88 e8 db ff
		// 查詢指令 lea rcx,[nt!PspLoadImageNotifyRoutine (fffff804`44313ce0)]
		/*
		nt!PsSetLoadImageNotifyRoutineEx+0x41:
		fffff801`80748a81 488d0dd8d3dbff  lea     rcx,[nt!PspLoadImageNotifyRoutine (fffff801`80505e60)]
		fffff801`80748a88 4533c0          xor     r8d,r8d
		fffff801`80748a8b 488d0cd9        lea     rcx,[rcx+rbx*8]
		fffff801`80748a8f 488bd7          mov     rdx,rdi
		fffff801`80748a92 e80584a3ff      call    nt!ExCompareExchangeCallBack (fffff801`80180e9c)
		fffff801`80748a97 84c0            test    al,al
		fffff801`80748a99 0f849f000000    je      nt!PsSetLoadImageNotifyRoutineEx+0xfe (fffff801`80748b3e)  Branch
		*/
		pSpecialData[0] = 0x48;
		pSpecialData[1] = 0x8D;
		pSpecialData[2] = 0x0D;
		ulSpecialDataSize = 3;
	}

	// 根據特徵碼獲取地址 獲取 PspLoadImageNotifyRoutine 陣列地址
	pPspLoadImageNotifyRoutineAddress = SearchPspLoadImageNotifyRoutine(pSpecialData, ulSpecialDataSize);
	DbgPrint("[LyShark] PspLoadImageNotifyRoutine = 0x%p \n", pPspLoadImageNotifyRoutineAddress);

	// 遍歷回撥
	ULONG i = 0;
	PVOID pNotifyRoutineAddress = NULL;

	// 獲取 PspLoadImageNotifyRoutine 陣列地址
	if (NULL == pPspLoadImageNotifyRoutineAddress)
	{
		return FALSE;
	}

	// 獲取回撥地址並解密
	for (i = 0; i < 64; i++)
	{
		pNotifyRoutineAddress = *(PVOID *)((PUCHAR)pPspLoadImageNotifyRoutineAddress + sizeof(PVOID) * i);
		pNotifyRoutineAddress = (PVOID)((ULONG64)pNotifyRoutineAddress & 0xfffffffffffffff8);
		if (MmIsAddressValid(pNotifyRoutineAddress))
		{
			pNotifyRoutineAddress = *(PVOID *)pNotifyRoutineAddress;
			DbgPrint("[LyShark] 序號: %d | 回撥地址: 0x%p \n", i, pNotifyRoutineAddress);
		}
	}

	Driver->DriverUnload = UnDriver;
	return STATUS_SUCCESS;
}

執行這段完整的程式程式碼,輸出如下效果:

目前系統中只有兩個回撥,所以列舉出來的只有兩條,開啟ARK驗證一下會發現完全正確,忽略pyark這是後期開啟的。