一丶驅動是如何運行的
1.服務注冊驅動
我們編寫驅動.一定要知道驅動是如何運行的
首先在我們安裝一個驅動的時候,會創建一個服務.(注冊表)
在
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\SrvNmae
最后一個是是你驅動的名字.
如下:
里面有一個StartType 它是按照GroupOrder順序來啟動的
StartType值越小.代表越早啟動.總共是0 - 4
數值 | 啟動時機 | 說明 |
---|---|---|
0 | 系統核心裝載器裝載 | 系統在啟動的時候優先加載. |
1 | IO子系統裝載 | 稍微晚一些加載.加載完核心驅動才加載我們的 |
2 | 自動啟動 | 在登錄界面出現的時候開始加載.電腦好驅動還沒加載也會登錄到桌面系統中. |
3 | 手工啟動 | 如果設置為3.重啟之后不會再加載,你需要自己重新加載一次 |
4 | 禁止啟動 | 代表我們驅動不會加載.比如設置start值小於4才可以 |
其中設置Start的值是在我們3環加載驅動的時候設置的 調用 CreateService安裝驅動的時候,傳遞的參數值.其中有個參數就是Start.如下:
SC_HANDLE CreateServiceA(
SC_HANDLE hSCManager,
LPCSTR lpServiceName,
LPCSTR lpDisplayName,
DWORD dwDesiredAccess,
DWORD dwServiceType,
DWORD dwStartType, 這個值設置
DWORD dwErrorControl,
LPCSTR lpBinaryPathName,
LPCSTR lpLoadOrderGroup, GroupOrder注意這個值
LPDWORD lpdwTagId,
LPCSTR lpDependencies,
LPCSTR lpServiceStartName,
LPCSTR lpPassword
);
關於如何使用代碼加載我們的驅動.前邊也有說過.請參考前面資料.
https://www.cnblogs.com/iBinary/p/8280912.html
GroupOrder值
這個值是在注冊表
HEEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\GroupOrderList
這個值是越靠前的驅動.啟動越早.
如下:
怎么結合的.
如果Start 值位0那么就看 GroupOrder 那個在上邊就加載誰.
Start值為0. 另一個為1 則值為0的先啟動.
2.對象管理器生成驅動對象
上面說了我們的服務會放在注冊表中. 但是我們的寫的驅動是怎么加載或執行的那.
對象管理器生成驅動對象 (DriverObject)並且把它傳遞給DriverEntry(). 執行 DriverEntry()入口函數.
3.創建控制設備對象
4.創見控制設備符號鏈接(Ring可以操作的)
5.綁定過濾驅動
如果我們有過濾驅動.則會創建過濾設備對象.並且進行綁定.
6.注冊分發函數
7.完成初始化動作.
8.應用程序通過符號鏈接打開設備對象.並且通過IRP發送讀寫請求.
二丶Ring3跟Ring0通訊的幾種方式
1.IOCTRL_CODE 控制代碼的幾種IO
1.METHOD_BUFFERED 通訊方式
METHOD_BUFFERED
在我們內核中 Ring3可以傳遞控制碼給內核層.其中需要指明我們的讀寫方式
如下:
#define IOCTRL_BASE 0x800
#define MYIOCTRL_CODE(i)\
CTL_CODE(FILE_DEVICE_UNKNOWN,IOCTRL_BASE+i,METHOD_BUFFERED,FILE_ANY_ACCESS)
4個參數:
參數1: 驅動的類型
參數2: 驅動的控制碼.
參數3: 以哪種緩沖方式進行通訊
參數4: 權限
其中我們這里說的就是參數3.指定什么方式進行通訊.
ME_THOD_BUFFERED 則我們的數據都會會封裝在IRP頭部的SystemBuffer中.
pIrp->AssociatedIrp.SystemBuffer;
2.METHOD_IN_DIRECT 只讀緩沖 METHOD_OUT_DIRECT 只寫緩沖
如果我們的CTRL_CODE指定的是這兩種的其中一種.看如下解釋
METHOD_IN_DIRECT
只讀緩沖的方式.則我們的緩沖區還是會封裝到IRP頭部的SystemBuffer緩沖區.
IN pIrp->AssociatedIrp.SystemBuffer;
如上.我們 ring3 輸入的數據都會放在這個SystemBuffer中.
METHOD_OUT_DIRECT
只寫緩存
如果是這種方式.則我們的數據也會封裝到IRP頭部.但是會設置的 IRP
頭部MdlAddress中.我們輸出的數據都要放在MDL中.
如下:
OUT PVOID pOutBuffer = MmGetSystemAddressForMdlSafe(pIrp->MdlAddress,NormalPagePriority);
MDL是 3環虛擬地址,映射到內核中的物理地址. 我們不能直接使用.
比如通過MmGetSystemAddressForMdlSafe這個函數.將映射的物理地址. 轉換為內核中的 "虛擬地址" 可以這樣理解. 然后對這個內存進入輸出即可.我們Ring3則可以接受到數據.
區別:
如果是只讀權限打開設備的時候.METHOD_IN_DIRECT則會成功.METHOD_OUT_DIRECT則會失敗.
如果是讀寫的方式.則都會成功.
3.METHOD_NEITHER 其它方式
使用其它方式.則我們Ring3發送過來的數據 會在IRP堆棧中
我們獲取Ring3的數據
PIRPSTACK_LOCATIO pIrpStack;
pIrpStack = IoGetCurrentIrpStackLocation(pIrp);
pIrpStack->Parameters.DeviceIoControl.Type3InputBuffer
則我們的輸出的數據要放在 IRP頭部的UserBuffer中傳遞給三環
pIrp->UserBuffer;
2.非控制 緩沖區的三種方式.
我們上面說了.控制派遣函數可以傳遞不同的緩沖區方式.進而在內核中.
進行不同的 取緩沖區 寫緩沖區的操作.那么如果不是控制.則會有3種方式.
1.緩沖IO DO_BUFFERED_IO
當我們創建完畢設備對象的時候.可以給設備對象的 Flags字段設置一個緩沖區的方式..分別有三種.
如我們設置 緩沖區讀取.
pDevice->Flags |= DO_BUFFERED_IO;
如果是緩沖區方式.則 我們Ring3發送的數據就會封裝到
IRP頭部的SystemBuffer中.
也就是說說我們只需要取出 IRP頭部的SystemBuffer就可以了.
緩沖IO的意思就是 3環 發送數據到0環. 0環開辟一個空間.用來接收.
這種方式很安全.但是效率差.如果一次發送很多字節.不建議使用這種方式.
因為你進入了內核.那么內核空間就是共享了.如果你在創建這種很多字節的緩沖區.那么就讓原本已經很緊張的內核空間負擔更重.而且如果內存來不及釋放.則會永久占據.除非你重啟電腦.
2.DO_DIRECT_IO 直接IO
直接IO的方式就是 將ring3數據所在的虛擬地址,映射到內核空間中.
內核進行讀取.這種方式效率快.一般內核廠家都是這種.
聽到映射.大家應該知道數據怎么傳遞了.
如果使用這種方式.那么數據 會在IRP頭部的MdlAddress中.
我們取出來就是用 "API"進行獲取即可.
這里的API指的是使用內核API.不是ring3的.注意
PVOID pBuffer = MmGetSystemAddressForMdlSafe(pIrp->MdlAddress,NormalPagePriority);
3.其它IO方式.
如果是其它IO方式.
我們輸入的數據則會在 IRP堆棧中的DeviceContrl字段中Type3InputBuffer中
pIrpStack->DeviceControl.Type3InputBuffer中.
輸出的數據則會在 IRP的頭部中的UserBuffer中
PIrp->UserBuffer
使用這種方式很危險.這種方式是內核直接讀取ring3虛擬地址數據.
必須保證ring3進程跟內核進程處於同一運行狀態中.
對此我們對其內存必須進行檢查.
有兩個API函數
ProbeForRead(); 檢查內存是否可讀
ProbeForWrite(); 檢查內存是否可寫.
三丶Ring3跟Ring0開發區別
1.什么是Ring3 什么是Ring0
CPU提供了4層. 而微軟只用了2層.
分別是Ring0到Rign3.
而微軟只用Ring0. 因為這個設計所以我們寫的驅動才能跟操作系統的權限一樣.這樣做也是因為不過與依賴Cpu的設計.否則以后CPU架構一改.操作系統就廢了.
X64系統
在X64系統中.CPU廠商因為操作系統沒有使用. 所以CPU直接把
RING1 RING2給去掉了.
所以在X64下,只剩下ring0 跟 ring3了.
虛擬技術 (VT技術)
虛擬技術有三種方式 0 1 3
內核運行在0層. 虛擬機運行在1層 應用程序運行在3層.
VT模式: 虛擬機運行在 -1級別. 根模式. -1就是在0環旁邊加了一個新的模式. -1級別就是權限很大的.比內核權限更大.
2.RING3 與 Ring0開發的區別
在內核中
printf scanf fopen fclose fwrite fread malloc free
都不可以使用.
但是與內存相關與字符串相關的可以使用
strcmp strcpy wcslen memset
但是不建議使用.在內核中有專門的操作函數.
這種函數是Rtl開頭.
如:
RtlStringcbCatA /W 字符串拼接
RtlStringcbCopy();字符串拷貝
RtlStringcbLength();求長度
RtlStringcbPrnt(); 字符串打印
如果是UNICODE_STRING則如下
RtlStringcbCopyUnicodeString();
RtlUnicodeStringCat();
在內核中我們的字符串數據結構有得新的數據類型
UNICODE_STRING 跟 ANSI_STRING
其實就是一個結構體.
如下:
typedef struct _STRING {
USHORT Length;
USHORT MaximumLength;
PCHAR Buffer;
} ANSI_STRING, *PANSI_STRING;
記錄了長度.最大長度.以及一個緩沖區. UNICODE_STRING也一樣.
對此針對這個結構體有了新的初始化 函數
ANSI_STRING string;
RtlInitAnsiString(string,"HelloWorld"");
UNICODE則是
RtlInitUnicodeString(string,L"Hello");
3.返回值的判斷
在內核中使用函數.則會返回一個狀態值.
如:
ntStatuse =IoCreateDevice();
需要使用宏判斷
if (!NT_SUCESS(ntStatus))
xxxx
常見的幾個狀態值
|狀態|含義|
|---|---||
|STATUS_SUCCESS|代表此次調用成功|
|STATUS_UNSUCCESSFUL|代表失敗|
|STATUS_ACCESS_DENIED|代表訪問拒絕|
|STATUS_INSUFFICIENT_RESOURCES|資源不足|
這些狀態嗎都在 <ntstatus.h>中定義的.
4.內存的使用與申請
在內核中申請內存跟ring3不同.
內核中申請內存使用
PVOID
ExAllocatePoolWithTag(
IN POOL_TYPE PoolType, 申請內存的類型.內存是分頁還是不可以
IN SIZE_T NumberOfBytes, 申請的字節
IN ULONG Tag 4個字節的內存標識,隨便寫.
);
其中參數1需要你指定類型.
分頁內存.就是內存可以交換到磁盤使用.
非分頁內存.就是內存不能交換.就是固定的.不能變.但是非分頁內存很寶貴.只有100-200MB.給我們操作系統使用.所以建議使用分頁內存.
四丶IRQL中斷級別
這一講我很多博客也說過了.就是說我們調用的 內核函數都有級別一說.
如下表:
了解:
編碼 | 級別 | 說明 |
---|---|---|
PASSIVE_LEVE | 無中斷 | 常規線程執行 |
APC_LEVEL | 軟中斷 | 異步過程調用執行 |
DISPATC_LEVEL | 軟中斷 | 線程調度延時過程調用執行 |
DIRQL | 硬中斷 | 設備中斷請求級處理程序執行 |
PROFILE_LEVEL | 硬中斷 | 配置文件定時器 |
CLOCK2_LEVEL | 硬中斷 | 時鍾 |
SYNCH_LEVEL | 硬中斷 | 同步級 |
IPI_LEVE | 硬中斷 | 處理器之間的中斷級 |
POWER_LEVEL | 硬中斷 | 電源故障級別 |
除了硬中斷是最高的. 我們只看軟中斷.
PASSIVER_LEVE APC_LEVEL DISPATCH_LEVEL 級別. 分別是 0 1 2
在軟中斷中Dispath級別最高.
如我們編寫內核的時候給的派遣函數.以及入口點函數.
可以如下圖:
調用源函數 | 級別 |
---|---|
DriverEntry,DriverUnLoad0 | Passive級別 |
各種分發函數 | Passiver級別 |
完成函數 | Dispatch級別 |
各種NDIS回調函數 | Dispatch級別 |
在使用函數的時候.應該查詢WDK文檔.看看級別.
五丶驅動函數的分類
在驅動中有一些函數前綴都是固定的
如:
Ex開頭的.
函數 | 函數說明 |
---|---|
ExAllocatePoolWithTag | 分配內存 |
ExAcquireFastMutex | 獲取快速互斥鎖 |
ExGetPreviousMode | 獲取前一個請求者的運行模式. 判斷來自Ring0還是Ring3,攔截ring3.過濾ring0 |
Io開頭的 屬於Io管理器的
函數 | 函數說明 |
---|---|
IoCreateDevice | 創建設備對象 |
IoCreateSymbolicLink | 創建符號鏈接 |
IoGetCurrentIrpStackLocation | 獲取Irp堆棧 |
IoAttachDeviceToDeviceStack | 設備綁定,自己生成的設備綁定到別人的設備上,做過濾用的. |
IoAllocateIrp | 自己分配一個IRP. |
IoSetCompletionRoutine | 為IRP設置完成例程的. |
Ke開頭的
函數 | 函數說明 |
---|---|
KeWaitForSingleObject | 等待事件發生.做同步用的. |
KeSetEvent | 設置事件信號 |
KeInitializeEvent | 初始化一個事件對象. |
Mm開頭的. 與Memory相關的.
函數 | 函數說明 |
---|---|
MmGetSystemRoutineAddress | 內核中獲取函數的內存地址. 跟ring3 GetProcAddress相似.一個ring3一個ring0 |
MmIsAddressValid | 判斷函數地址是否無效. |
Ob開頭 與內核對象相關的.
函數 | 函數說明 |
---|---|
ObReferenceObjectByHandle | 把一個內核對象的Handle轉化成內核它的內核對象. 句柄不能誇進程.所以轉換為內核對象. |
ObQueryNameString | 查詢名字跟路徑.可能會死鎖 |
Ps開頭的. 與進程相關的.
函數 | 函數說明 |
---|---|
PsGetCurrentProcess | 獲取當前進程的EPROCESS未導出的結構 |
PsGetCurrentProcessId | 獲取當前進程Pid |
PsCreateSystemThead | 在內核中創建線程的 |
PsLookUpProcessByProcessId | 進程進程PID獲取這個PID的EPROCESS結構 |
Rtl開頭的 一組重寫的函數.可以操作內存跟字符串
函數 | 函數說明 |
---|---|
RtlZeroMemory | 對一塊內存清空位0.跟memset一樣. |
RtlInitUnicodeString | 初始化UNICODE字符串 |
Zw開頭. 系統服務的. 文件系統.注冊表.
函數 | 函數說明 |
---|---|
ZwOpenKey | 打開注冊表鍵 |
ZwCreateFile | 創建文件或打開一個文件 |
ZwOpenProcess | 打開一個進程 |
ZwQuerySystemInformation | 遍歷進程的一些信息 |
Flt開頭的. 文件過濾相關的一組函數 (minfilter)
Ndis 防火牆中用的一些函數
六丶編寫內核中的注意事項
1 不要使用 MmIsAddressValid函數.這個函數對於校驗內存沒有意義
這個函數只能判斷一個字節地址的有效性
if (MmisAddressValid(Buffer))
{
memcmp(BUffer,BufferTwo,Length);
}
它只判斷地址字節的第一個地址.只要你的地址在這個分頁.那么可以.
但是就怕分頁.后面分頁不對就會出錯.
他還會對 Page Out不能准確的判斷. 所以攻擊者可以利用你的判斷.來繞過你的保護.
2.保證我們的代碼在 tye _except中完成.否則藍屏.
編寫驅動代碼一定要注意不要產生異常.否則就會藍屏.
如:
try
{
ProbeFroRead(Buffer,len,alig);
if (memcpy(Buffer,buffer2,len){};//這行出錯就會在except.
}
_except(EXECUTE_HANDLER_EXCEPTION)
{
//如果出錯就會在這.
}
3.注意長度為0的緩存. 以及為NULL的緩存指針與緩存對其
緩存長度為0
ProbeForread跟Write. 如果我們Buffer長度穿的為0.這兩個函數是不工作的.很容易被別人攻擊.所以要小心len為0的情況.
如下漏洞代碼:
try
{
ProbeForRead(Buffer1,len,sizeof(char));
if (strcmp(Buffer1,Buffer2,len){}
}_except(EXECUTE_HANDLER_EXCEPTION)
{
xxx
}
上面的代碼會產生問題.因為當ProbeForRead的時候.長度傳遞為0
則這個函數不工作.但是我們的strcmp至少會訪問一個字節.這樣就造成了崩潰藍屏. 繞過你的保護.所以最好使用Rtl之類的函數操作.
緩存指針如空
不要使用下面的代碼
if (userBuffer == NULL){xxx};
windows操作系統運行用戶態申請的一個地址為0的內存. 攻擊者可以以它來繞過檢查過保護.
在我們以前講調用們的時候也說過. ring3可以使用0內存.
在Windows8以后內存不能申請為NULL.
緩存對齊
ProbeForRead(Address,length,Alignm);
在函數的第三個參數是對其. 默認是按照1對齊.如果使用 Sizeof(ULONG) 也會出問題.導致過保護.
4.注意不正確的內核調用引發的問題
如函數:
ObReferenceObjectByHandle();
如果使用這個函數.不指定類型.任然可以獲得對應的對象地址.但是如果你直接訪問這個對象.就會引發漏洞.
如:
HookZwSetInformationFile();
ObReferenceObjectByHandle(FileHandle,Access,NULL);
//ObReferenceObjectByHandle(FileHandle,Access,&Fileobject);
if (wscnicmp(fileObject->FileName){}
如上,參數如果傳遞為NULL. 攻擊者可以傳入非文件類型的句柄.如果你沒有校驗.就會導致悲劇.所以使用必須給指定對象類型.會影響第一個參數.
第一個參數攻擊者可以傳入任何Handle.
這就是拒絕服務攻擊.一句話你就藍屏.
不正確的Zw函數使用
使用Zw函數的時候.不能將用戶態的內存給它. 因為Zw函數不會進行校驗.
就算你進行了校驗.傳遞這樣的內存給系統也可以引發崩潰. 比如內存也在調用的時候突然無效. 就算你進行異常駁貨.也可以造成內存泄漏.對象泄漏.甚至權限提升等問題.
不要下發內核對象給內核
我們Ring3的內核對象.不要通過 DeviceControl 進行傳遞.
如果這樣寫.很可能讓攻擊者可以做到任意地址寫入.提升權限.
5.給驅動提供的功能性接口必須小心
如果對注冊表 文件 內核內存.進程線程等操作的功能性接口.一定要非常小心才可以.禁止一切受信進程的調用. 不然你暴露接口就會被利用.
6.數據傳輸盡量使用 BUFFERED_IO 緩存的方式.
我們內核中的最好使用緩沖IO.也就是說使用SystemBuffer.如果不使用BUFFERED_IO而使用UserBuffer一定注意使用 Pro等檢查函數.
7.發布的驅動必須通過內核校驗
微軟提供的驅動校驗工具: verifier
在CMD命令中輸入即可.打開后界面如下:
使用的時候
創建自定義設置(供程序開發人員使用) -> 從一個完整列表選擇單個設置
->出來很多檢查. -> 從一個列表中選擇驅動程序 ->有的話你選擇.沒有的話你自動選擇一個.選中之后則會重啟.自動進行檢測. 如果出錯.就會藍屏.
隊友掛鈎內核函數的驅動. 還可以使用 BSOD HOOK 一類的Fuzz工具
來進行檢查.