驅動開發:探索DRIVER_OBJECT驅動物件

2023-04-03 21:00:34

本章將探索驅動程式開發的基礎部分,瞭解驅動物件DRIVER_OBJECT結構體的定義,一般來說驅動程式DriverEntry入口處都會存在這樣一個驅動物件,該物件內所包含的就是當前所載入驅動自身的一些詳細引數,例如驅動大小,驅動標誌,驅動名,驅動節等等,每一個驅動程式都會存在這樣的一個結構。

首先來看一下微軟對其的定義,此處我已將重要欄位進行了備註。

typedef struct _DRIVER_OBJECT {
    CSHORT Type;                                // 驅動型別
    CSHORT Size;                                // 驅動大小
    PDEVICE_OBJECT DeviceObject;                // 驅動物件
    ULONG Flags;                                // 驅動的標誌
    PVOID DriverStart;                          // 驅動的起始位置
    ULONG DriverSize;                           // 驅動的大小
    PVOID DriverSection;                        // 指向驅動程式映像的記憶體區物件
    PDRIVER_EXTENSION DriverExtension;          // 驅動的擴充套件空間
    UNICODE_STRING DriverName;                  // 驅動名字
    PUNICODE_STRING HardwareDatabase;
    PFAST_IO_DISPATCH FastIoDispatch;
    PDRIVER_INITIALIZE DriverInit;
    PDRIVER_STARTIO DriverStartIo;
    PDRIVER_UNLOAD DriverUnload;                 // 驅動物件的解除安裝地址
    PDRIVER_DISPATCH MajorFunction[IRP_MJ_MAXIMUM_FUNCTION + 1];
} DRIVER_OBJECT;

DRIVER_OBJECT結構體是Windows作業系統核心中用於表示驅動程式的基本資訊的結構體。它包含了一系列的欄位,用於描述驅動程式的特定屬性。

以下是DRIVER_OBJECT結構體中的一些重要欄位:

  • Type:該欄位標識該結構體的型別,始終設定為DRIVER_OBJECT_TYPE。
  • Size:該欄位表示該結構體的大小,以位元組為單位。
  • DeviceObject:該欄位是一個指標,指向驅動程式所建立的裝置物件連結串列的頭部。每個裝置物件代表著一個裝置或者驅動程式建立的一種虛擬裝置。
  • DriverStart:該欄位是一個指標,指向驅動程式程式碼的入口點,也就是驅動程式的DriverEntry函數。該函數會在驅動程式被載入時被呼叫。
  • DriverSize:該欄位表示驅動程式程式碼的大小,以位元組為單位。
  • DriverName:該欄位是一個UNICODE_STRING結構體,用於表示驅動程式的名稱。
  • Flags:該欄位是一個32位元的位掩碼,用於表示驅動程式的一些屬性。例如,可以設定DO_BUFFERED_IO標誌表示驅動程式支援緩衝I/O。

如果我們想要遍歷出當前自身驅動的一些基本資訊,我們只需要在驅動的頭部解析_DRIVER_OBJECT即可得到全部的資料,這段程式碼可以寫成如下樣子,其中的IRP_MJ_這一系列則是微軟的呼叫號,不同的RIP代表著不同的涵義,但一般驅動也就會用到如下這幾種呼叫號。

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

#include <ntifs.h>

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

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

	Driver->DriverUnload = UnDriver;

	DbgPrint("驅動名字 = %wZ \n", Driver->DriverName);
	DbgPrint("驅動起始地址 = %p | 大小 = %x | 結束地址 %p \n",Driver->DriverStart,Driver->DriverSize,(ULONG64)Driver->DriverStart + Driver->DriverSize);

	DbgPrint("解除安裝地址 = %p\n", Driver->DriverUnload);
	DbgPrint("IRP_MJ_READ地址 = %p\n", Driver->MajorFunction[IRP_MJ_READ]);
	DbgPrint("IRP_MJ_WRITE地址 = %p\n", Driver->MajorFunction[IRP_MJ_WRITE]);
	DbgPrint("IRP_MJ_CREATE地址 = %p\n", Driver->MajorFunction[IRP_MJ_CREATE]);
	DbgPrint("IRP_MJ_CLOSE地址 = %p\n", Driver->MajorFunction[IRP_MJ_CLOSE]);
	DbgPrint("IRP_MJ_DEVICE_CONTROL地址 = %p\n", Driver->MajorFunction[IRP_MJ_DEVICE_CONTROL]);

	// 輸出完整的呼叫號
	for (auto i = 0; i < IRP_MJ_MAXIMUM_FUNCTION; i++)
	{
		DbgPrint("IRP_MJ呼叫號 = %d | 函數地址 = %p \r\n", i, Driver->MajorFunction[i]);
	}

	Driver->DriverUnload = UnDriver;
	return STATUS_SUCCESS;
}

編譯這段程式,簽名並執行,我們即可看到如下輸出資訊,此時當前自身驅動的詳細引數都可以被輸出;

當然運用_DRIVER_OBJECT物件中的DriverSection欄位我們完全可以遍歷輸出當前系統下所有的驅動程式的具體資訊,DriverSection結構指向了一個_LDR_DATA_TABLE_ENTRY結構,結構的微軟定義如下;

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;
	union {
		LIST_ENTRY HashLinks;
		struct {
			PVOID SectionPointer;
			ULONG CheckSum;
		};
	};
	union {
		struct {
			ULONG TimeDateStamp;
		};
		struct {
			PVOID LoadedImports;
		};
	};
}LDR_DATA_TABLE_ENTRY, *PLDR_DATA_TABLE_ENTRY;

為了能夠遍歷出所有的系統驅動,我們需要得到pLdr結構,該結構可通過Driver->DriverSection的方式獲取到,獲取到之後通過pLdr->InLoadOrderLinks.Flink得到當前驅動的入口地址,而每一次呼叫pListEntry->Flink都將會指向下一個驅動物件,通過不斷地迴圈CONTAINING_RECORD解析,即可輸出當前系統內所有驅動的詳細資訊。這段程式的寫法可以如下所示;

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

#include <ntifs.h>

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;
	union {
		LIST_ENTRY HashLinks;
		struct {
			PVOID SectionPointer;
			ULONG CheckSum;
		};
	};
	union {
		struct {
			ULONG TimeDateStamp;
		};
		struct {
			PVOID LoadedImports;
		};
	};
}LDR_DATA_TABLE_ENTRY, *PLDR_DATA_TABLE_ENTRY;

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

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

	Driver->DriverUnload = UnDriver;

	PLDR_DATA_TABLE_ENTRY pLdr = NULL;
	PLIST_ENTRY pListEntry = NULL;
	PLIST_ENTRY pCurrentListEntry = NULL;

	PLDR_DATA_TABLE_ENTRY pCurrentModule = NULL;
	pLdr = (PLDR_DATA_TABLE_ENTRY)Driver->DriverSection;
	pListEntry = pLdr->InLoadOrderLinks.Flink;
	pCurrentListEntry = pListEntry->Flink;

	// 判斷是否結束
	while (pCurrentListEntry != pListEntry)
	{
		// 獲取LDR_DATA_TABLE_ENTRY結構
		pCurrentModule = CONTAINING_RECORD(pCurrentListEntry, LDR_DATA_TABLE_ENTRY, InLoadOrderLinks);

		if (pCurrentModule->BaseDllName.Buffer != 0)
		{
			DbgPrint("模組名 = %wZ | 模組基址 = %p | 模組入口 = %p | 模組時間戳 = %d \n",
				pCurrentModule->BaseDllName,
				pCurrentModule->DllBase,
				pCurrentModule->EntryPoint,
				pCurrentModule->TimeDateStamp);
		}
		pCurrentListEntry = pCurrentListEntry->Flink;
	}

	Driver->DriverUnload = UnDriver;
	return STATUS_SUCCESS;
}

編譯這段程式,簽名並執行,我們即可看到如下輸出資訊,此時當前自身驅動的詳細引數都可以被輸出;

通過使用上一篇文章《驅動開發:核心字串拷貝與比較》中所介紹的的RtlCompareUnicodeString函數,還可用於對比與過濾特定結果,以此來實現通過驅動名返回驅動基址的功能。

LONGLONG GetModuleBaseByName(PDRIVER_OBJECT pDriverObj, UNICODE_STRING ModuleName)
{
	PLDR_DATA_TABLE_ENTRY pLdr = NULL;
	PLIST_ENTRY pListEntry = NULL;
	PLIST_ENTRY pCurrentListEntry = NULL;

	PLDR_DATA_TABLE_ENTRY pCurrentModule = NULL;
	pLdr = (PLDR_DATA_TABLE_ENTRY)pDriverObj->DriverSection;
	pListEntry = pLdr->InLoadOrderLinks.Flink;
	pCurrentListEntry = pListEntry->Flink;

	while (pCurrentListEntry != pListEntry)
	{
		// 獲取LDR_DATA_TABLE_ENTRY結構
		pCurrentModule = CONTAINING_RECORD(pCurrentListEntry, LDR_DATA_TABLE_ENTRY, InLoadOrderLinks);

		if (pCurrentModule->BaseDllName.Buffer != 0)
		{
			// 對比模組名
			if (RtlCompareUnicodeString(&pCurrentModule->BaseDllName, &ModuleName, TRUE) == 0)
			{
				return (LONGLONG)pCurrentModule->DllBase;
			}
		}
		pCurrentListEntry = pCurrentListEntry->Flink;
	}
	return 0;
}

上這段程式碼的使用也非常簡單,通過傳入一個UNICODE_STRING型別的模組名,即可獲取到模組基址並返回,至於如何初始化UNICODE_STRING則在《驅動開發:核心字串轉換方法》中有詳細的介紹,此處你只需要這樣來寫。

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

	UNICODE_STRING unicode;

	// 獲取WinDDK驅動基地址
	RtlUnicodeStringInit(&unicode, L"WinDDK.sys");
	LONGLONG winddk_address = GetModuleBaseByName(Driver, unicode);
	DbgPrint("WinDDK模組基址 = %p \n", winddk_address);

	// 獲取ACPI驅動基地址
	RtlUnicodeStringInit(&unicode, L"ACPI.sys");
	LONGLONG acpi_address = GetModuleBaseByName(Driver, unicode);
	DbgPrint("ACPI模組基址 = %p \n", acpi_address);

	Driver->DriverUnload = UnDriver;
	return STATUS_SUCCESS;
}

執行這段驅動程式,即可分別輸出WinDDK.sys以及ACPI.sys兩個驅動模組的基地址;