最近在學習驅動程式設計方面的内容,在這将自己的一些心得分享出來,供大家參考,與大家共同進步,本人學習驅動主要是通過兩本書——《獨釣寒江 windows安全程式設計》 和 《windows驅動開發技術詳解》。
驅動開發過程中,主要使用的C語言,雖說C中定義了許多資料類型,但是一般來說在編碼上還是習慣與使用WDK的規範,雖說這個不是必須的,比如有這樣一句
unsigned long ul = ;
這個資料的大小根據不同的機器不同的編譯器環境略有不同,這樣代碼就産生了不可控的行為,但是WDK上專門定義了相關的宏,環境不同,隻需要修改一下宏定義,這樣就避免了這個問題。
在這列舉一些常用的資料類型,以免以後在編寫代碼或者檢視例子代碼時犯迷糊:
普通資料類型
#define ULONG unsigned long
#define UCHAR unsigned char
#define UINT unsigned int
#define VOID void
#define PULONG unsigned *
#define PUCHAR unsigned char*
#define PUINT unsigned int*
#define PVOID void*
字元串類型
在驅動的程式設計中,為字元串操作專門定義了一個資料類型UNICODE_STRING ANSI_STRING,他們的定義大緻相同,隻是一個是表示UNICODE字元串,一個表示ANSI字元串,下面主要來說明一下UNICODE_STRING
typedef struct _UNICODE_STRING {
USHORT Length; // 字元串的中字元所占的記憶體大小
USHORT MaximumLength;//用來存儲字元串緩沖的大小
PWCHAR Buffer;//緩沖的位址
} UNICODE_STRING;
這個結構體在使用是需要注意的是上述兩個大小機關是位元組數而不是字元個數,另外在操作UNICODE_STRING 的時候隻是簡單的操作Buffer指向的記憶體,并不會特意的為其配置設定另外的空間,字元串處理函數主要有這樣幾個:
RtlInitUnicodeString(&uStr1, str1);
RtlCopyUnicodeString(&uStr1, &uStr2);
RtlAppendUnicodeToString(&uStr1, str1);
RtlAppendUnicodeStringToString(&uStr1, &uStr2);
RtlCompareUnicodeString(&uStr1, &uStr2, TRUE/FALSE);
RtlAnsiStringToUnicodeString(&uStr1, &aStr1, TRUE/FALSE);
RtlFreeUnicodeString(&uStr1);
這些函數從字面上就可以知道它們是幹什麼用的,需要注意的是,除了Init,這些函數隻是簡單的操作Buffer已指向的記憶體,并不會改變指針的指向。是以在使用時要特别注意不要試圖改變靜态常量區的内容,也要特别注意指向的記憶體是在棧中還是在堆中。下面是一個簡單的例子:
UNICODE_STRING uStr1 = { };
UNICODE_STRING uStr2 = { };
UNICODE_STRING uStr3 = { };
ANSI_STRING aStr = { };
RtlInitUnicodeString(&uStr1, L"Hello");
RtlInitUnicodeString(&uStr2, L"Goodbye");
//列印字元串結構用%Z表示%wZ表示是寬字元
DbgPrint("uStr1 = %wZ\n", &uStr1);
DbgPrint("uStr2 = %wZ\n", &uStr2);
RtlInitAnsiString(&aStr, "Hello World");
DbgPrint("aStr = %Z\n", &aStr);
/*這個操作是由于uStr3中的Buffer指向NULL,是以會失敗*/
RtlCopyUnicodeString(&uStr3, &uStr1);
DbgPrint("uStr3 = %wZ\n", &uStr3); //失敗
/*下面兩個失敗是由于Str1 Str2 指向的是字元串常量區,不可修改*/
RtlAppendUnicodeToString(&uStr1, &uStr2);
DbgPrint("uStr1 = %wZ\n", &uStr1); //失敗
RtlAppendUnicodeStringToString(&uStr1, L"World");
DbgPrint("uStr1 = %wZ\n", &uStr1); //失敗
LARGE_INTEGER
這個結構就像它的名字一樣,用來表示一個比較大的整數,它的定義如下:
typedef union _LARGE_INTEGER {
struct {
DWORD LowPart;
LONG HighPart;
};
struct {
DWORD LowPart;
LONG HighPart;
} u;
LONGLONG QuadPart;
} LARGE_INTEGER, *PLARGE_INTEGER;
這是一個公用體,可以認為它是由兩部分組成高32位的HighPart和低32位的LowPart,它分了高位優先和低位優先兩種情況,也可以認為它是一個64位的整形。在使用時根據需求來決定
NTSTATUS
絕大多數驅動函數都傳回這個值,用來表示目前處理的狀态,一般STATUS_SUCCESS表示成功,其餘的都表示失敗。微軟根據不同情況定義了它的狀态值,一般常用的有下面幾個
值 | 含義 |
---|---|
STATUS_SUCCESS | 函數執行成功 |
STATUS_UNSUCCESSFUL | 函數執行不成功 |
STATUS_NOT_IMPLEMENTED | 函數違背實作 |
STATUS_INVALID_INFO_CLASS | 輸入參數是無效的類别 |
STATUS_ACCESS_VIOLATION | 不允許通路 |
STATUS_IN_PAGE_ERROR | 發生頁面故障 |
STATUS_INVALID_HANDLE | 輸入的是無效的句柄 |
STATUS_INVALID_PARAMETER | 輸入的是無效的參數 |
STATUS_NO_SUCH_DEVICE | 指定的裝置不存在 |
STATUS_NO_SUCH_FILE | 指定的檔案不存在 |
STATUS_INVALID_DEVICE_REQUEST | 無效的裝置請求 |
STATUS_END_OF_FILE | 檔案已到結尾 |
STATUS_INVALID_SYSTEM_SERVICE | 無效的系統調用 |
STATUS_ACCESS_DENIED | 通路被拒絕 |
STATUS_BUFFER_TOO_SMALL | 輸入的緩沖區過小 |
STATUS_OBJECT_TYPE_MISMATCH | 輸入的對象類型不比對 |
STATUS_OBJECT_NAME_INVALIE | 輸入的對象名無效 |
STATUS_OBJECT_NAME_NOT_FOUND | 輸入的對象沒有找到 |
STATUS_PORT_DISCONNNECTED | 需要連接配接的端口沒有被連接配接 |
STATUS_OBJECT_PATH_INVALID | 輸入的對象路徑無效 |
另外在使用WinDbg進行調試的時候,一般都會得到函數調用的錯誤碼,根據錯誤碼可以找到對應的錯誤資訊,微軟提供了一種解決方案:
LPVOID lpMessageBuffer;
HMODULE Hand = LoadLibrary(_T("NTDLL.DLL"));
DWORD dwErrCode = ;
//擷取錯誤碼
FormatMessage(
FORMAT_MESSAGE_ALLOCATE_BUFFER |
FORMAT_MESSAGE_FROM_SYSTEM |
FORMAT_MESSAGE_FROM_HMODULE,
Hand,
dwErrCode,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
(LPTSTR) &lpMessageBuffer,
,
NULL );
// Now display the string.
// Free the buffer allocated by the system.
LocalFree( lpMessageBuffer );
FreeLibrary(Hand);
驅動對象
驅動程式的入口函數是DriverEntry,函數會傳入一個驅動對象的指針——PDRIVER_OBJECT,每個驅動都有一個唯一的驅動對象,就好像每個Win32應用程式有一個唯一的執行個體句柄。它的定義如下:
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 + ];
} DRIVER_OBJECT;
下面主要對幾個重要的部分做介紹:
1. DeviceObject:儲存的是驅動中裝置對象的指針,另外每個裝置對象又有一個指向下一個裝置對象的指針,這樣同一個驅動程式中的不同裝置對象就構成了一個連結清單
2. DriverName:這個裡面存儲的是驅動程式的名稱,該字元串一般為“\Driver\驅動名稱”
3. HardwareDatabase:這裡記錄的是裝置的硬體資料庫鍵名,這個資料庫一般是系統資料庫,字元串一般為“REGISTRY\MACHINE\HARDWARE\DESCRIPTION\SYSTEM”
4. DriverStartIo:記錄StartIo這個例程回調函數的位址
5. DriverUnload:當驅動解除安裝時會調用這個指針所指向的函數
6. MajorFunction,這是一個回調函數的指針數組,處理IRP包的不同請求,就好像應用層裡面的消息處理函數,根據不同的請求,調用不同的函數。
裝置對象
在windows平台将每個裝置抽象為一個裝置對象,驅動層一般通過裝置對象來操作具體的裝置,每個驅動可以有多個裝置對象。
typedef struct DECLSPEC_ALIGN(MEMORY_ALLOCATION_ALIGNMENT) _DEVICE_OBJECT {
...
struct _DRIVER_OBJECT *DriverObject;
struct _DEVICE_OBJECT *NextDevice;
struct _DEVICE_OBJECT *AttachedDevice;
struct _IRP *CurrentIrp;
ULONG Flags;
PVOID DeviceExtension;
DEVICE_TYPE DeviceType;
CCHAR StackSize;
...
} DEVICE_OBJECT;
裝置對象本身定義是十分複雜的,在這我們隻列舉出部分,以後寫程式會經常使用的部分,下面是對這些部分的說明:
1. DriverObject: 指向所屬驅動的驅動對象的指針
2. NextDevice:指向下一個裝置驅動的指針
3. AttachedDevice:指向它被附加的驅動的指針,裝置對象之上還可以在附加上其他的裝置對象,這樣每當有消息傳來時總會由附加在它之上的裝置對象處理,然後才會交由它自身處理,這個指針就是指向附加在它之上的裝置對象的指針
4. CurrentIrp:指向目前IRP域的指針
5. Flags:表名該裝置的一些标志資訊,主要有下面幾個值:
标志 | 描述 |
---|---|
DO_BUFFERED_IO | 讀寫使用緩沖方式,核心層在使用使用者緩沖區時會将使用者分區中的資料拷貝到核心分區中 |
DO_EXCLUSIVE | 一次隻允許一個線程使用這個裝置對象 |
DO_DIRECT_IO | 讀寫直接方式,應用層将某塊記憶體鎖定在記憶體,然後将記憶體映射到核心空間中,這種方式是最快的方式 |
DO_DEVICE_INITIALIZING | 裝置正在初始化 |
DO_POWER_PAGABLE | 裝置必須在PASSIVE_LEVEL上處理IRP_MJ_PNP請求 |
DO_POWER_INRUSH | 裝置上電期間需要大電流 |
6. DeviceExtension:指向一塊擴充的記憶體,系統允許使用者在建立裝置對象時自定義一塊區域用來儲存結構體中沒有但是使用者自己感興趣的内容。在驅動程式中需要盡量避免使用全局變量,是以可以通過使用這塊擴充記憶體來傳輸全局變量
7. DeviceType:驅動的類型,主要有下面幾個值
裝置類型 | 描述 |
---|---|
FILE_DEVICE_BEEP | 該裝置是一個蜂鳴器 |
FILE_DEVICE_CD_ROM | 該裝置時一個CD光驅 |
FILE_DEVICE_CD_ROM_FILE_SYSTEM | CD光驅檔案系統裝置 |
FILE_DEVICE_CONTROLLER | 控制器裝置 |
FILE_DEVICE_DATALINK | 資料鍊裝置 |
FILE_DEVICE_DFS | DFS裝置對象 |
FILE_DEVICE_DISK | 磁盤裝置對象 |
FILE_DEVICE_DISK_FILE_SYSTEM | 磁盤檔案系統裝置對象 |
FILE_DEVICE_FILE_SYSTEM | 檔案系統裝置對象 |
FILE_DEVICE_INPORT_PORT | 輸入端口裝置對象 |
FILE_DEVICE_KEYBOARD | 鍵盤裝置對象 |
FILE_DEVICE_MAILSLOT | 郵件曹裝置對象 |
FILE_DEVICE_MIDI_IN | MIDI輸入裝置對象 |
FILE_DEVICE_MIDI_OUT | MIDI輸出裝置對象 |
FILE_DEVICE_MOUSE | 滑鼠裝置對象 |
FILE_DEVICE_MULTI_UNC_PROVIDER | 多UNC裝置對象 |
FILE_DEVICE_NAMED_PIPE | 命名管道裝置對象 |
FILE_DEVICE_NETWORK | 網絡裝置對象 |
FILE_DEVICE_NETWORK_BROWSER | 網絡浏覽器裝置對象 |
FILE_DEVICE_NETWORK_FILE_SYSTEM | 網絡檔案系統裝置對象 |
FILE_DEVICE_NULL | 空裝置對象 |
FILE_DEVICE_PARALLEL_PORT | 并口裝置對象 |
FILE_DEVICE_PHYSICAL_NETCARD | 實體網卡裝置對象 |
FILE_DEVICE_PRINTER | 列印機裝置對象 |
FILE_DEVICE_SCANNER | 掃描器裝置對象 |
FILE_DEVICE_SERIAL_MOUSE_PORT | 序列槽滑鼠裝置對象 |
LE_DEVICE_SERIAL_PORT | 序列槽裝置對象 |
FILE_DEVICE_SCREEN | 螢幕裝置對象 |
FILE_DEVICE_SOUND | 聲音裝置對象 |
FILE_DEVICE_STREAMS | 流裝置對象 |
LE_DEVICE_TAPE | 錄音帶裝置對象 |
FILE_DEVICE_TAPE_FILE_SYSTEM | 錄音帶檔案系統裝置對象 |
FILE_DEVICE_TRANSPORT | 傳輸裝置對象 |
FILE_DEVICE_UNKNOWN | 未知裝置對象 |
FILE_DEVICE_VIDEO | 視訊裝置對象 |
FILE_DEVICE_VIRTUAL_DISK | 虛拟磁盤裝置對象 |
FILE_DEVICE_WAVE_IN | 聲音輸入裝置對象 |
FILE_DEVICE_WAVE_OUT | 聲音輸出裝置對象 |
在建立裝置對象時如果不知道這個裝置對象是何種類型,可以直接給FILE_DEVICE_UNKNOWN;
8. StackSize:之前說到過,裝置對象存在附加的情況,附加時每個裝置對象會存儲它上層的裝置對象的指針,這樣就形成了類似堆棧的結構,而這個值就表示從該裝置對象到棧底還有多少個裝置對象
為了便于了解我們做了這樣一個示意圖:
