《Undocumented Windows 2000 Secrets》翻譯 --- 第三章(3)
第三章 編寫內核模式驅動程序
翻譯: Kendiv
更新: Thursday, February 10, 2005
設備 I/O 控制
就像在本章開頭的簡介中提到的,在本書中,我們不會構建某一具體硬件的驅動程序。替代的是,我們將利用功能強大的內核驅動程序來研究 Windows 2000 的秘密。從實際結果來看,驅動程序的強大之處在于它們能在 CPU 的最高特權級別上運行。這意味著內核驅動可以訪問所有的系統資源,可以讀取所有的內存空間,而且也被允許執行 CPU 的特權指令,如,讀取 CPU 控制寄存器的當前值等。而處于用戶模式下的程序如果試圖從內核空間中讀取一個字節或者試圖執行像 MOV EAX,CR3 這樣的匯編指令都會被立即終止掉。不過,這種強大的底線是驅動程序的一個很小的錯誤就會讓整個系統崩潰。即使是非常小的錯誤發生,也會讓系統藍屏,因此開發內核程序的人員必須比 Win32 應用程序或 DLL 的開發人員更加仔細的處理錯誤。還記得我們在第一章里使用的導致系統藍屏的 Windows 2000 Killer device driver 嗎?它所作的一切只是觸及了虛擬內存地址 0x00000000 ,然后就 ---Boom ?。?!你應該意識到在開發內核驅動時,你會比以往更頻繁的重啟你的機器。
在隨后章節中,我給出的驅動程序代碼將采用稱為設備 I/O 控制( IOCTL )的技術,以允許用戶模式下的代碼實現一定程序的“遠程控制”。如果應用程序需要訪問在用戶模式下無法觸及的系統資源,那內核驅動程序將可很好的完成此項工作,而 IOCTL 則是聯系二者的橋梁。事實上, IOCTL 并不是 Windows 2000 采用的新技術。即使舊的操作系統 ---Dos 2.11 也具有這種能力, 0x44 函數及其子函數構成了 DOS 的 IOCTL 。基本上, IOCTL 是通過控制通路和設備通訊的一中手段,控制通路在邏輯上獨立于數據通路。想象一個硬盤設備通過其主數據通路傳遞磁盤扇區中的內容。如果客戶想獲取當前設備使用的媒體信息,它就必須使用另一個不同的通路。例如, DOS 函數 0x44 ,其子函數 0x0d 、 0x66 構成了 DOS 的 IOCTL ,調用這些函數就可讀取磁盤的 32 位連續數據(參考 Brown and Kyle 1991 , 1993 )。
設備 I/O 控制根據要控制的設備,可以有多種實現方式。就其一般形式來說, IOCTL 有如下幾類:
l 客戶端通過一個特殊的進入點來控制設備。在 DOS 中,這個進入點為 INT 21h 、函數號 0x44 。在 Windows 2000 中,則通過 Kernel32.dll 導出的 Win32 函數 DeviceIoControl() 。
l 客戶端通過提供設備的唯一標識符、控制代碼以及一個存放輸入數據的緩沖區、一個存放輸出數據的緩沖區來調用 IOCTL 的進入點。對于 Windows 2000 ,設備標識符是成功打開的設備的句柄( HANDLE )。
l 控制代碼用于告訴目標設備的 IOCTL 分派器( dispatcher ),客戶端請求的是哪一個控制函數。
l 輸入緩沖區中可包含任意地附加數據,設備可能需要這些數據來完成客戶所請求的操作。
l 客戶所請求的操作產生的任何數據,都會保存在客戶端提供的輸出緩沖區中。
l IOCTL 操作的整體結果通過返回給客戶端的狀態代碼來表示
很 顯然這是一種強大的通用機制,這種機制可以適用于很大范圍的控制請求。例如,應用程序在訪問系統內核所占用的內存空間時會被禁止,這是因為當程序觸及該內 存空間時會立即拋出一個異常,不過程序可以通過加載一個內核驅動程序來完成此項工作,這樣就可避免出現異常。交互的兩個模塊都需遵循 IOCTL 協議來管理數據的傳輸。例如,程序可能通過給驅動程序發送控制代碼 0x80002000 來讀取內存或發送 0x80002001 來向內存中寫入數據。對于讀取請求, IOCTL 輸 入緩沖區或許要提供基地址和要讀取的字節數。內核驅動程序能獲取這些請求并通過控制代碼來判斷是讀取操作還是寫入操作。對于讀取請求,內核驅動程序會將請 求的內存范圍內的數據復制到調用者提供的輸出緩沖區中,如果輸出緩沖區足夠容納這些數據,則返回成功代碼。對于寫入請求,驅動程序會將輸入緩沖區中的數據 復制到指定的內存中(該內存的起始位置也由輸入緩沖區指定)。在第四章,我將提供一個 Memory Spy 的示列代碼。
現在,可以看出 IOCTL 是 Win32 應用程序的一種后門,通過 IOCTL ,程序可以執行幾乎所有的操作,而在此之前,這些操作僅允許特權模塊執行。當然,這需要首先編寫一個特權級的模塊,但是,一旦你擁有一個運行于系統中的 Spy 模塊,一切就變得很簡單了。本書的兩個目標是:詳細展示如何編寫內核模式的驅動程序以及一個可以完成很多讓人驚異的事的驅動程序的示例代碼。
Windows 2000 的 Killer Device
在開始更高級的驅動程序工程之前,讓我們先看看一個非常簡單的驅動程序。在第一章中,我介紹了 Windows 2000 的 Killer Device----w2k_kill.sys ,它被設計為引發一個良性的系統崩潰。這個驅動程序并不需要 示例 3-3 中的大多數代碼,因為它在有機會收到第一個 I/O 請求包之前就會使系統崩潰。 示例 3-7 給出了它的實現代碼。這里沒有給出 w2k_kill.h 文件,因為它不不包含任何我們感興趣的代碼。
示列 3-7 中的代碼沒有在 DriverEntry() 中執行初始化操作,因為系統會在 DriverEntry() 返回前就崩潰,所以沒有必要進行這些額外的工作。
#define _W2K_KILL_SYS_
#include <ddkntddk.h>
#include 'w2k_kill.h'
// =================================================================
// DISCARDABLE FUNCTIONS
// =================================================================
NTSTATUS DriverEntry (PDRIVER_OBJECT pDriverObject,
PUNICODE_STRING pusRegistryPath);
#ifdef ALLOC_PRAGMA
#pragma alloc_text (INIT, DriverEntry)
#endif
// =================================================================
// DRIVER INITIALIZATION
// =================================================================
NTSTATUS DriverEntry (PDRIVER_OBJECT pDriverObject,
PUNICODE_STRING pusRegistryPath)
{
return *((NTSTATUS *) 0);
}
// =================================================================
// END OF PROGRAM
// =================================================================
示列 3-7. 一個小巧的系統崩潰者
加載 / 卸載驅動程序
在完成一個內核驅動程序之后,你可能會想立即執行它。怎么做呢?典型的做法是,在系統啟動時加載驅動程序并執行之。但這是不是就意味著我們每次更新驅動程序后,都必須重新啟動系統呢?很幸運,這并不是必須的。 Windows 2000 的一個特色就是提供了一個 Win32 接口以允許在運行時加載或卸載驅動程序。這是由服務控制管理器( Service Control Manager , SCM )完成的,下面的將詳細介紹它的用法。
服務控制管理器
“服務控制管理器”這個名字容易讓人誤解,因為它暗示該組件僅用于服務的管理。服務( Service )是 Windows 2000 的一類非常強大的模塊,它們在后臺運行配套的程序,并且不需要用戶交互(也就是說沒有常見的用戶界面或者控制臺)。換句話說,一個服務就是一個始終運行于系統中的 Win32 進程,即使沒有用戶登陸進來也如此。盡管開發服務是一個令人興奮的話題,但它并不屬于本書的范疇。想進一步了解服務的開發,請閱讀 Windows Developer's Journal ( WDJ )( Tomlinson 1996a )中 Paula Tomlinson 提供的非常不錯的教程,以及隨后在她的 WDJ 專欄 ----Understanding NT 中發表的有關服務的論文。
SC 管理器(即服務控制管理器)可以控制服務和驅動程序。為了簡單起見,我在這里使用“服務”一詞來代表 SC 管理器控制的所有對象,這包括嚴格意義上的服務和內核驅動程。 SC 的接口對于 Win32 程序是可用的,它由 Win32 子系統組件 ----advapi32.dll 提供,這個 DLL 還提供了很多有趣的 API 函數。 表 3-3 給出了用于加載、控制和卸載服務的 API 函數的名稱,同時還給出了簡單的描述。在你可以加載或訪問任何服務之前,你必須獲取 SC 管理器的句柄(通過調用 OpenSCManager() ),在隨后的討論中,該句柄將被稱為:管理器句柄。 CreateService() 和 OpenService() 都需要此句柄,而這些函數返回的句柄將被稱為:服務句柄。這種類型的句柄可以傳遞給需要引用一個服務的函數,如 ControlService() 、 DeleteService() 和 StartService() 。這兩種類型的 SC 句柄都通過 CloseServiceHandle() 函數來釋放。
名 稱
描 述
CloseServiceHandle
關閉來自 OpenSCManager() 、 CreateService() 或 OpenService() 的句柄
ControlService
停止、暫停、繼續、查詢或通知已加載的服務 / 驅動程序
CreateService
加載一個服務 / 驅動程序
DeleteService
卸載一個服務 / 驅動程序
OpenSCManager
獲取 SC 管理器的句柄
OpenService
獲取一個已加載的服務 / 驅動程序的句柄
QueryServiceStatus
查詢一個服務 / 驅動程序的屬性和當前狀態
StartService
啟動一個已加載的服務 / 驅動程序
表 3-3. 基本的服務控制函數
加載和運行一個服務需要執行的典型操作步驟:
1. 調用 OpenSCManager() 以獲取一個管理器句柄
2. 調用 CreateService() 來向系統中添加一個服務
3. 調用 StartService() 來運行一個服務
4. 調用 CloseServiceHandle() 來釋放管理器或服務句柄
要確保當一個錯誤發生時,要回滾到最后一個成功的調用,然后再開始。例如,你在調用 StartService() 時 SC 管理器報告了一個錯誤,你就需要調用 DeleteService() 。否則,服務將保持在一個非預期的狀態。另一個使用 SC 管理器 API 易犯的錯誤是,必須為 CreateService() 函數提供可執行文件的全路徑名,否則,如果該函數在當前目錄中沒有找到可執行文件的話,就會失敗。因此,你應該使用 Win32 函數 ---GetFullPathName() 來規格化傳遞給 CreateService() 的所有文件名,除非可以保證它們已經是全路徑的。
高層的驅動程序管理函數
為了更容易的和 SC 管理器進行交互,本書附帶的 CD 提供了多個更高級的外包函數,這些函數屏蔽了原有的一些不方便的特殊要求。這些函數是本書提供的龐大的 Windows 2000 工具庫(位于隨書 CD 中的 srcw2k_lib )中的一部分。 w2k_lib.dll 導出的所有函數都有一個全局的名字前綴 w2k ,服務和驅動程序管理函數都使用 w2kService 前綴。 列表 3-8 給出了本書提供的工具庫中實現的加載、控制和卸載服務 / 驅動程序的函數的細節。
// =================================================================
// SERVICE/DRIVER MANAGEMENT
// =================================================================
SC_HANDLE WINAPI w2kServiceConnect (void)
{
return OpenSCManager (NULL, NULL, SC_MANAGER_ALL_ACCESS);
}
// -----------------------------------------------------------------
SC_HANDLE WINAPI w2kServiceDisconnect (SC_HANDLE hManager)
{
if (hManager != NULL) CloseServiceHandle (hManager);
return NULL;
}
// -----------------------------------------------------------------
SC_HANDLE WINAPI w2kServiceManager (SC_HANDLE hManager,
PSC_HANDLE phManager,
BOOL fOpen)
{
SC_HANDLE hManager1 = NULL;
if (phManager != NULL)
{
if (fOpen)
{
if (hManager == NULL)
{
*phManager = w2kServiceConnect ();
}
else
{
*phManager = hManager;
}
}
else
{
if (hManager == NULL)
{
*phManager = w2kServiceDisconnect (*phManager);
}
}
hManager1 = *phManager;
}
return hManager1;
}
// -----------------------------------------------------------------
SC_HANDLE WINAPI w2kServiceOpen (SC_HANDLE hManager,
PWord pwName)
{
SC_HANDLE hManager1;
SC_HANDLE hService = NULL;
w2kServiceManager (hManager, &hManager1, TRUE);
if ((hManager1 != NULL) && (pwName != NULL))
{
hService = OpenService (hManager1, pwName,
SERVICE_ALL_ACCESS);
}
w2kServiceManager (hManager, &hManager1, FALSE);
return hService;
}
// -----------------------------------------------------------------
BOOL WINAPI w2kServiceClose (SC_HANDLE hService)
{
return (hService != NULL) && CloseServiceHandle (hService);
}
// -----------------------------------------------------------------
BOOL WINAPI w2kServiceAdd (SC_HANDLE hManager,
PWORD pwName,
PWORD pwInfo,
PWORD pwPath)
{
SC_HANDLE hManager1, hService;
PWORD pwFile;
WORD awPath [MAX_PATH];
DWORD n;
BOOL fOk = FALSE;
w2kServiceManager (hManager, &hManager1, TRUE);
if ((hManager1 != NULL) && (pwName != NULL) &&
(pwInfo != NULL) && (pwPath != NULL) &&
(n = GetFullPathName (pwPath, MAX_PATH, awPath, &pwFile)) &&
(n < MAX_PATH))
{
if ((hService = CreateService (hManager1, pwName, pwInfo,
SERVICE_ALL_ACCESS,
SERVICE_KERNEL_DRIVER,
SERVICE_DEMAND_START,
SERVICE_ERROR_NORMAL,
awPath, NULL, NULL,
NULL, NULL, NULL))
!= NULL)
{
w2kServiceClose (hService);
fOk = TRUE;
}
else
{
fOk = (GetLastError () ==
ERROR_SERVICE_EXISTS);
}
}
w2kServiceManager (hManager, &hManager1, FALSE);
return fOk;
}
// -----------------------------------------------------------------
BOOL WINAPI w2kServiceRemove (SC_HANDLE hManager,
PWORD pwName)
{
SC_HANDLE hService;
BOOL fOk = FALSE;
if ((hService = w2kServiceOpen (hManager, pwName)) != NULL)
{
if (DeleteService (hService))
{
fOk = TRUE;
}
else
{
fOk = (GetLastError () ==
ERROR_SERVICE_MARKED_FOR_DELETE);
}
w2kServiceClose (hService);
}
return fOk;
}
// -----------------------------------------------------------------
BOOL WINAPI w2kServiceStart (SC_HANDLE hManager,
PWORD pwName)
{
SC_HANDLE hService;
BOOL fOk = FALSE;
if ((hService = w2kServiceOpen (hManager, pwName)) != NULL)
{
if (StartService (hService, 1, &pwName))
{
fOk = TRUE;
}
else
{
fOk = (GetLastError () ==
ERROR_SERVICE_ALREADY_RUNNING);
}
w2kServiceClose (hService);
}
return fOk;
}
// -----------------------------------------------------------------
BOOL WINAPI w2kServiceControl (SC_HANDLE hManager,
PWORD pwName,
DWORD dControl)
{
SC_HANDLE hService;
SERVICE_STATUS ServiceStatus;
BOOL fOk = FALSE;
if ((hService = w2kServiceOpen (hManager, pwName)) != NULL)
{
if (QueryServiceStatus (hService, &ServiceStatus))
{
switch (ServiceStatus.dwCurrentState)
{
case SERVICE_STOP_PENDING:
case SERVICE_STOPPED:
{
fOk = (dControl == SERVICE_CONTROL_STOP);
break;
}
case SERVICE_PAUSE_PENDING:
case SERVICE_PAUSED:
{
fOk = (dControl == SERVICE_CONTROL_PAUSE);
break;
}
case SERVICE_START_PENDING:
case SERVICE_CONTINUE_PENDING:
case SERVICE_RUNNING:
{
fOk = (dControl == SERVICE_CONTROL_CONTINUE);
break;
}
}
}
fOk = fOk ||
ControlService (hService, dControl, &ServiceStatus);
w2kServiceClose (hService);
}
return fOk;
}
// -----------------------------------------------------------------
BOOL WINAPI w2kServiceStop (SC_HANDLE hManager,
PWORD pwName)
{
return w2kServiceControl (hManager, pwName,
SERVICE_CONTROL_STOP);
}
// -----------------------------------------------------------------
BOOL WINAPI w2kServicePause (SC_HANDLE hManager,
PWORD pwName)
{
return w2kServiceControl (hManager, pwName,
SERVICE_CONTROL_PAUSE);
}
// -----------------------------------------------------------------
BOOL WINAPI w2kServiceContinue (SC_HANDLE hManager,
PWORD pwName)
{
return w2kServiceControl (hManager, pwName,
SERVICE_CONTROL_CONTINUE);
}
// -----------------------------------------------------------------
SC_HANDLE WINAPI w2kServiceLoad (PWORD pwName,
PWORD pwInfo,
PWORD pwPath,
BOOL fStart)
{
BOOL fOk;
SC_HANDLE hManager = NULL;
if ((hManager = w2kServiceConnect ()) != NULL)
{
fOk = w2kServiceAdd (hManager, pwName, pwInfo, pwPath);
if (fOk && fStart)
{
if (!(fOk = w2kServiceStart (hManager, pwName)))
{
w2kServiceRemove (hManager, pwName);
}
}
if (!fOk)
{
hManager = w2kServiceDisconnect (hManager);
}
}
return hManager;
}
// -----------------------------------------------------------------
SC_HANDLE WINAPI w2kServiceLoadEx (PWORD pwPath,
BOOL fStart)
{
PVS_VERSIONDATA pvvd;
PWORD pwPath1, pwInfo;
WORD awName [MAX_PATH];
DWORD dName, dExtension;
SC_HANDLE hManager = NULL;
if (pwPath != NULL)
{
dName = w2kPathName (pwPath, &dExtension);
lstrcpyn (awName, pwPath + dName,
min (MAX_PATH, dExtension - dName + 1));
pwPath1 = w2kPathEvaluate (pwPath, NULL);
pvvd = w2kVersionData (pwPath1, -1);
pwInfo = ((pvvd != NULL) && pvvd->awFileDescription [0]
? pvvd->awFileDescription
: awName);
hManager = w2kServiceLoad (awName, pwInfo, pwPath1, fStart);
w2kMemoryDestroy (pvvd);
w2kMemoryDestroy (pwPath1);
}
return hManager;
}
// -----------------------------------------------------------------
BOOL WINAPI w2kServiceUnload (PWORD pwName,
SC_HANDLE hManager)
{
SC_HANDLE hManager1 = hManager;
BOOL fOk = FALSE;
if (pwName != NULL)
{
if (hManager1 == NULL)
{
hManager1 = w2kServiceConnect ();
}
if (hManager1 != NULL)
{
w2kServiceStop (hManager1, pwName);
fOk = w2kServiceRemove (hManager1, pwName);
}
}
w2kServiceDisconnect (hManager1);
return fOk;
}
// -----------------------------------------------------------------
BOOL WINAPI w2kServiceUnloadEx (PWORD pwPath,
SC_HANDLE hManager)
{
DWORD dName, dExtension;
WORD awName [MAX_PATH];
PWORD pwName = NULL;
if (pwPath != NULL)
{
dName = w2kPathName (pwPath, &dExtension);
lstrcpyn (pwName = awName, pwPath + dName,
min (MAX_PATH, dExtension - dName + 1));
}
return w2kServiceUnload (pwName, hManager);
}
// -----------------------------------------------------------------
列表 3-8. 服務和驅動管理庫函數
