64位內核開發第二講.內核編程注意事項,以及UNICODE_STRING


一丶驅動是如何運行的

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_STRINGANSI_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工具
來進行檢查.


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM