Windows內核開發-2-開始內核開發-2-內核開發入門
- 2021 年 7 月 18 日
- 筆記
- Windows內核安全與驅動開發
Windows內核開發-2-開始內核開發-2-
第一個驅動程序:
直接採用vs2019中的Empty WDM Driver 模塊創建:
初始的項目文件夾中有一個Driver Files裏面會有一個.inf的文件,沒用直接刪除就好,然後在源文件裏面創建一個.cpp的源文件。
DriverEntry和Unload Routines
DriverEntry:
每個驅動都有一個入口點,叫做DriverEntry,就好比平常寫的C/C++代碼裏面的main函數。DriverEntry是由一個叫做IRQL_PASSIVE_LEVEL(0)的系統進程調用出來的。DriverEntry函數原型:
NTSTATUS
DriverEntry(_In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING
RegistryPath);
代碼原型里的_In _是源代碼注釋語言(SAL)中的一部分,用來描述函數如何使用其參數,SAL對於編譯器來說可以直接忽略,但是對程序員很有幫助。
相關鏈接:
這裡的最小的DriverEntry示例可以只返回一個狀態,比如:
NTSTATUS DriverEntry(_In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath) {
return STATUS_SUCCESS;
}
該DriverEntry函數包含在<ntddk.h>頭文件中,但是添加了頭文件後仍然是編譯失敗的,因為編譯器會把警告當場錯誤來報錯,但是不建議刪除該功能,因為有時候警告就是會導致錯誤誕生:
可以對應修改這些警告,比如這裡將形參刪除,但是這樣僅對於C++好用,因為C++有函數重載,所以這裡用不上。這裡有一個很經典的解決辦法,就是採用一個宏函數:
UNREFERENCED_PARAMETER();
這樣就可以暫時解決掉前面的報錯說形參沒有使用了:
NTSTATUS DriverEntry(_In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath) {
UNREFERENCED_PARAMETER(DriverObject);
UNREFERENCED_PARAMETER(RegistryPath);
return STATUS_SUCCESS;
}
但是這樣仍然不行:
可以很明顯得看出,編譯器沒問題,但是Linker鏈接器出了問題,DriverEntry是一個C函數,必須用C的linker來link,但是這裡我們採用的是C++的默認,所以必須給該函數設置為C的默認LInker才行
。
extern "C"
NTSTATUS DriverEntry(_In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath) {
UNREFERENCED_PARAMETER(DriverObject);
UNREFERENCED_PARAMETER(RegistryPath);
return STATUS_SUCCESS;
}
這樣最簡單的驅動程序的源代碼就寫好了,就相當於C語言中的:
#include<stdio.h>
int main()
{
return 0;
}
Unload Routines:
這是一個驅動的卸載函數,就相當於C++中類的析構函數一樣,當驅動被卸載的時候就會自動調用該函數。在DriverEntry函數中創建的東西需要由Unload Routines來釋放,這就非常像C++類中的構造函數和析構函數的關係了。如果沒有該函數來釋放驅動加載時所開闢的內容就會導致泄露,直到下次電腦重啟時內核產生的泄露才會清楚。
該函數的函數指針,必須在DriverEntry給DriverEntry的參數DriverObject中的DriverUnload字段賦值才行。Unload函數和DriverEntry函數一樣都需要接受一個_In _ PDRIVER_OBJECT DriverObject 參數,但是Unload函數不需要返回值,直接用void 定義就好。
比如:
#include <ntddk.h>
void SampleUnload(_In_ PDRIVER_OBJECT DriverObject)
{
UNREFERENCED_PARAMETER(DriverObject);
}
extern "C"
NTSTATUS DriverEntry(_In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath) {
DriverObject->DriverUnload = SampleUnload;
UNREFERENCED_PARAMETER(RegistryPath);
return STATUS_SUCCESS;
}
就是一個非常簡單但是可以用的驅動了。
部署驅動程序Deploying the Driver
前面已經寫好了一個驅動程序,但是我們還需要把它跑起來,驅動程序不像平時寫的普通程序一樣,採用IDE就可以正常使用了,需要將其加載到系統裏面,通常為了避免風險,採用虛擬機來部署驅動程序。
安裝驅動程序就像是在User用戶態安裝服務.exe一樣,需要調用CreateService API或者採用現有的工具,這裡採用比較常用的Sc.exe來進行部署內核驅動。
註冊驅動
採用管理員權限的命令行:
sc create sample type=kernel binPath=C:\DriverTest\MyDriver3.sys
其中 sample是創建的名字,然後type表示創建的權限,binPath後面的是驅動的路徑。如果沒問天會彈出一個成功的標識符。
並且可以在註冊表裏面查到:
使用Win+R的彈出框裏面輸入regedit.exe,查看路徑\HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\sample,這就是我們剛剛寫好且用sc創造的驅動:
啟動驅動
前面是註冊了該驅動,還得使用它。這裡也和User用戶態的服務Service類似,需要採用StartService API來使用或者採用一些軟件,這裡也可以用Sc.exe來繼續使用。
sc start sample
這裡的sample就是前面註冊的驅動的名字。
但是這個通常會失效,因為對於64bit位的windows系統,加載驅動必須得要有驅動的簽名才行,這裡為了學習方便,避開簽名這個東西,可以直接把系統置為測試版本。
bcdedit /set testsigning on
如果你要生成除了Windows10以外的版本,可以在項目屬性裏面配置你的驅動要部署在的系統環境裏面:
最後再使用前面sc來加載驅動時會看到一個關於驅動的輸出:
有了這個輸出就表明我們的驅動已經成功加載了,可以使用Process Explorer工具來確認是否加載成功(下載地址://docs.microsoft.com/zh-cn/sysinternals/downloads/process-explorer)
這裡的驅動名稱是你自己的sys驅動名稱。
卸載驅動
不用了將驅動程序卸下,同樣的可以採用 ControlService API或者Sc.exe來處理。Sc指令:
sc stop sample
就OK了。
簡單跟蹤
為了確保函數有確切被調用,這裡提供一種基礎的跟蹤辦法來確保函數被使用,驅動採用KdPrint這個宏來輸出類似於printf風格的文本,該宏的內容可以被內核的調試器,或者其它工具查看到。
KdPrint這個宏只在debug模式下採用,它的底層調用的其實是DbgPrint 內核Kernel API。
下面更新一下DriverEntry和Unload函數:
void SampleUnload(_In_ PDRIVER_OBJECT DriverObject)
{
UNREFERENCED_PARAMETER(DriverObject);
KdPrint(("Sample driver Unload called! \n"));
}
NTSTATUS DriverEntry(_In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath) {
DriverObject->DriverUnload = SampleUnload;
UNREFERENCED_PARAMETER(RegistryPath);
KdPrint(("Sample driver initialized successfully\n !"));
return STATUS_SUCCESS;
}
需要注意的是該宏函數在調用時採用了兩個括號,因為它是一個宏函數,但是又顯然它是可以接受任意變量的,由於宏函數不能接受可變的變量參數,所以編譯器實際上調用的是DbgPrint函數。這裡理解不了沒關係,先這樣用着就行。
重新生成驅動並加載來查看這些Print信息,這裡需要採用一個內核的調試器才行,但是為了方便,先採用一個系統的內部工具:DebugView來查看。在使用DebugView之前,需要先給它在註冊表裏面配置內容不然用不上。
在\HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\里新建一個Key(項)Key名為DebugPrintFilter,並且添加一個DWORD類型的值名為DEFAULT,這個DEFAULT要和默認的一個值區別開來,後面那個默認的值是註冊表中的每一個項都有的,那個值暫時先不用管,然後給該名為DEFAULT類型為DWORD的變量賦值為8,如下圖所示:
然後下載DebugView(
這樣,再使用Sc.exe來重新加載驅動就可以看到KdPrint打印的內容了。
總結Summary