Midnight Sun CTF 2023: HFSAntiCheat
HFS is getting into the anti-cheat biz! Submit game cheats to help us BETA test our anti-cheat engine. The flag is in C:\Windows\System32\flag.txt
HFSAntiCheat Intro
HFSAntiCheat was an homage to the collection of vulnerable drivers at LOLdrivers. A common vulnerability for these drivers was arbitrary physical memory read-and-write. This could be due to copy-paste from WinIO.
The players were able to submit a “game cheat” as a custom Portable Executable (PE) file to a remote Windows system. The PE file would be executed as the Lamer user, and the HFSAntiCheat driver would scan its Import Address Table (IAT) for banned functions. If a banned function was found then the player would be reported as a cheater.
The players had access to:
- A vagrant file to reproduce the environment
- The HFSAntiCheat.sys driver
- A copy of ntoskrnl.exe from the remote system
The challenge was to leverage the vulnerabilities in the HFSAntiCheat driver to privilege escalate from an unprivileged user to SYSTEM and read the flag.
HFSAntiCheat Vulnerabilities
The following vulnerabilities could be exploited by the player:
Improper Access Control
The driver object of HFSAntiCheat was initialized with an overly permissive SDDL string. It is described in the file wdmsec.h from the Windows Driver Kit:
SDDL_DEVOBJ_SYS_ALL_ADM_RWX_WORLD_RW_RES_R Everyone (the WORLD SID) can read or write to the device. However, “restricted” or “untrusted” code (the RES SID) can only be read from the device.
In short, the player could communicate with the HFSAntiCheat device via Input/Output Control (IOCTL) from the “game cheats” restricted process context.
Arbitrary Physical Memory Access
The driver provided access to the full range of physical memory on the Windows OS by temporarily mapping and unmapping "\Device\PhysicalMemory" with ZwMapViewOfSection(). Depending on the IOCTL received from user-mode, it would perform a read or write to the mapped memory.
The players had a copy of the HFSAntiCheat driver and could reverse-engineer it to figure out the IOCTL needed to leverage the HFSAntiCheatReadPhysMem() and HFSAntiCheatWritePhysMem() functions for arbitrary physical memory access
HFSAntiCheat Device Driver Communication
To communicate with the HFSAntiCheat driver, the player would create a handle to the device.
1HANDLE openDevice() {
2 return CreateFileA("\\\\.\\HFSAntiCheat", GENERIC_READ | GENERIC_WRITE, 0,
3 NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
4}
The player had a copy of the HFSAntiCheat.sys driver and could reverse-engineer it to figure out the relevant IOCTL codes and data structures needed to communicate with the driver.
1typedef struct _IOCTL_STRUCT {
2 SIZE_T bufferLen;
3 ULONG_PTR buffer;
4 ULONG_PTR addr;
5} IOCTL_STRUCT, *PIOCTL_STRUCT;
6
7BOOLEAN writePhysMem(HANDLE hDevice, ULONG_PTR addr, PVOID buf, SIZE_T buflen) {
8 DWORD bytesReturned = 0;
9
10 IOCTL_STRUCT ioctlStruct = {0};
11 ioctlStruct.bufferLen = payloadLen;
12 ioctlStruct.buffer = (ULONG_PTR)payload;
13 ioctlStruct.addr = targetPhysicalAddr;
14
15 return DeviceIoControl(hDevice, IOCTL_WRITE_PHYSMEM, &ioctlStruct,
16 sizeof(ioctlStruct), &ioctlStruct, sizeof(ioctlStruct),
17 &bytesReturned, NULL);
18}
HFSAntiCheat Exploitation
Exploitation Challenges
The player had a few hurdles to overcome when attempting to exploit the driver:
- The exploit or “game cheat” was executed as the standard user Lamer with a Low Integrity process context. This acted as a limited “sandbox”
- Limited kernel memory disclosure in user-mode. Access to, for example, NtQuerySystemInformation() was denied due to the Low Integrity process context. This is going to be more of a problem in coming releases of Windows; see KASLR Leaks Restriction
- Arbitrary read-and-writes were limited to physical memory only
- The exploit execution had a time limit of five seconds
Exploitation Steps
The intended solution for privilege escalation was through the following steps:
- Disclose the Page Directory Table Base (PDTB)
- Disclose a kernel memory address to bypass Kernel Address Space Layout Randomization (KASLR)
- Use the PDTB to translate virtual memory to physical memory by walking the Page Table Directories (PTD)
- Use the translation capability from the previous step to perform token theft of a privileged token
Disclosing the Page Directory Table Base (PDTB)
Page tables are data structures used by the Windows memory manager to store the mapping between virtual addresses and physical memory locations, facilitating the translation and management of virtual memory.
As mentioned, the player had arbitrary access to physical memory. The problem was that they had no idea of where kernel objects mapped in virtual memory were located in the physical memory. This required address translation and to translate a virtual memory address to a physical memory address on Windows, one must know the Page Directory Table Base (PDTB).
As the name hints to, the PDTB is the base of the page tables. The PDTB can be leveraged as a starting point for walking the page tables. The question was, how do you find it?
On Windows x86/x64 the CR3 register is the Page Directory Base Register (PDBR). It is a privileged register and contains the physical address of the PDTB. Due to it being privileged the player could not directly read the CR3 register from user-mode.
On almost all real hardware and in some cases for VMs, there is a stub in the first 16MB of physical memory that contains the physical address of PDTB. The trick was to perform a signature scan for that stub and disclose the PDTB. An example of this can be viewed here ufrisk/MemProcFS/vmmwininit.c#L775:
1BOOL VmmWinInit_DTB_FindValidate_X64_LowStub(_In_ VMM_HANDLE H,
2 _In_ PBYTE pbLowStub1M)
3{
4 DWORD o = 0;
5 while(o < 0x100000) {
6 o += 0x1000;
7 if(0x00000001000600E9 !=
8 (0xffffffffffff00ff & *(PQWORD)(pbLowStub1M + o + 0x000))) {
9 continue;
10 } // START BYTES
11 if(0xfffff80000000000 !=
12 (0xfffff80000000003 & *(PQWORD)(pbLowStub1M + o + 0x070))) {
13 continue;
14 } // KERNEL ENTRY
15 if(0xffffff0000000fff & *(PQWORD)(pbLowStub1M + o + 0x0a0)) {
16 continue;
17 } // PML4
18 H->vmm.kernel.vaEntry = *(PQWORD)(pbLowStub1M + o + 0x070);
19 H->vmm.kernel.paDTB = *(PQWORD)(pbLowStub1M + o + 0x0a0);
20 return TRUE;
21 }
22 return FALSE;
23}
On line 19, the offset 0x0a0 contains the physical address of the PDTB. In addition to that, on line 18, the offset 0x070 contains the virtual kernel address of HalpLMStub().
Bypassing KASLR
The players had a copy of ntoskrnl.exe from the remote target. By disclosing the kernel address of HalpLMStub(), the offsets in the exploit could be adapted for the remote target and bypass KASLR.
Virtual to Physical (VTOP)
By disclosing the PDTB the player could translate any virtual address to a physical address via page table walking. This essentially provides a virtual read-and-write primitive. Understanding how to walk the Windows page tables could be a blog by itself; see Turning the Pages: Introduction to Memory Paging on Windows 10 x64 and the exploit further down.
Privilege Escalation
To escalate privileges, the player could perform token theft from a privileged process. A quick reminder of the EPROCESS structure and its relevant fields:
1struct _EPROCESS
2{
3 struct _KPROCESS Pcb; //0x0
4 struct _EX_PUSH_LOCK ProcessLock; //0x438
5 VOID* UniqueProcessId; //0x440
6 struct _LIST_ENTRY ActiveProcessLinks; //0x448
7 ...
8 struct _EX_FAST_REF Token; //0x4b8
The following steps could be taken for privilege escalation:
- Read the global variable PsInitialSystemProcess. It has a pointer to the first EPROCESS structure
- Read the EPROCESS.Token. It has a pointer to a token with SYSTEM privileges
- Walk the EPROCESS.ActiveProcessLinks linked list and find the EPROCESS with the UniqueProcessId (PID) of the target process one wants to privilege escalate
- Overwrite the target process EPROCESS.Token with the privileged token pointer from step 2
- Read “C:\Windows\System32\flag.txt”
Unintended Solutions
The exploitation time limit was meant to mitigate unintended solutions, such as:
- Signature scanning physical memory for EPROCESS objects and manipulating the Token field. See Razer Driver Exploit
- Signature scanning physical memory for a kernel function, for example, NtQuerySystemInformation(), and overwriting it with shellcode. See: ptr-yudai writeup and clubby789 writeup
However, in hindsight, it was a bit of an optimistic mitigation :)