EvilCode

Midnight Sun CTF 2023: HFSHyperRAM

hfshyperram

HFSHyperRAM provides anti-cheat features including secure storage within the hypervisor. To capture the flag, simply execute the ‘flag’ command on the host operating system!

HFSHyperRam Intro

HFSHyperRAM was an emulated device for VirtualBox that provided “secure” storage in the host Operating System (OS) of the challenge Virtual Machine (VM). This was part two of my previous HFSAntiCheat challenge. The players could submit a “game cheat” via a Portable Executable (PE) file. In addition to the anti-cheat driver, the player could also communicate with the secure storage device via Memory-mapped I/O (MMIO).

The players had access to:

  • A Vagrant file to reproduce the environment
  • The files from the HFSAntiCheat challenge
  • The HFSHyperRam.so device plugin

The challenge was to leverage the vulnerabilities in the secure storage device to escape VirtualBox and execute the “flag” process on the host OS.

HFSHyperRam MMIO Communication

The HFSHyperRAM emulated device registered a physical address range for its MMIO handlers, as shown in the console output below:

mk@cursed:~/evilcode$ VBoxManage debugvm hfshyperram info mmio

MMIO registrations: 7 (46 allocated)
## Ctx   Size Mapping      PCI                               Description
0 R3+0   0000000000084000  00000000ffc00000-00000000ffc83fff Flash Memory
1 R3+0   0000000000001000  00000000fee00000-00000000fee00fff APIC
...
6 R3     0000000000001000  00000000e4700000-00000000e4700fff HFSHyperRam

The players were given this console output as a hint and the HFSHyperRam.so device file for reverse-engineering its functionality. The emulated device, much like other MMIO devices, associated offsets in the physical address space with registers or functions. The secure storage device had the following offsets:

Name Offset Description
HFS_CONFIG_RW 0 Read or write to current Storage buffer
HFS_CONFIG_KEY_OFFSET 16 Set the current encryption key
HFS_CONFIG_BUF_OFFSET 48 Set the offset in the current Storage Buffer
HFS_CONFIG_MODE_OFFSET 80 Set the Storage Buffer mode (IO_ENCRYPT, IO_PLAIN, IO_VERSION)
HFS_CONFIG_BUF_LEN_OFFSET 112 Set Storage Buffer length
HFS_CONFIG_STATUS_OFFSET 144 Not used
HFS_CONFIG_STORAGE_IDX_OFFSET 176 Set the current Storage Buffer

As an example, to set the encryption key register:

  1. Create a handle to the HFSAntiCheat driver to re-use its functionality of reading and writing to physical memory
  2. Trigger the HFSHypeRam MMIO write handler by writing an 8-byte key to 0x00000000e4700000 with the offset 0x10 (16).

HFSHyperRam Exploitation

HFSHyperRam.so was loaded as a plugin by VirtualBox in user-mode (R3). It had the following protections:

mk@cursed:~/evilcode$ checksec --file ./HFSHyperRam.so
[*] './HFSHyperRam.so'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      PIE enabled

Exploitation

The following steps could be taken to exploit the secure storage driver:

  1. Disclose the base address of HFSHyperRam.so, as PIE is enabled
  2. Gain an arbitrary read-write primitive
  3. Disclose the memory address of system()
  4. Gain arbitrary code execution by overwriting a function pointer in the got.plt of HFSHyperRam.so with system(). This is possible as only partial RELRO is enabled.
  5. Execute the process flag on the host OS

Disclosing the Base address of HFSHyperRam

The IO_VERSION mode could be used to read the version of the secure storage device. The variable bufOffset was user-controlled via the HFS_CONFIG_BUF_OFFSET register. As no bounds checks were in place, the player could read out-of-bounds via the version variable:

 1static const char version[24] = "Version: 1.0.0; Prod";
 2...
 3static DECLCALLBACK(VBOXSTRICTRC)
 4    HFSHyperRAMMMIORead(PPDMDEVINS pDevIns, void *pvUser, RTGCPHYS off,
 5                        void *pv, unsigned cb) {
 6  ...
 7  uint64_t bufOffset = g_aHFSConfigMap[1].value;
 8
 9  if (off == 0 && cb == 8) {
10  ...
11    if (g_aHFSConfigMap[2].value == IO_VERSION) {
12      memcpy(pv, &version[bufOffset], 8);
13    }

The version variable was stored in the .rodata section of HFSHyperRam.so. Another variable of interest in that section was the structure g_DeviceHFSHyperRAM. It contained function pointers to the modules constructor and deconstructor:

1static const PDMDEVREG g_DeviceHFSHyperRAM = {
2    /* .u32Version = */ PDM_DEVREG_VERSION,
3    ...
4#if defined(IN_RING3)
5    ...
6    /* .pfnConstruct = */ HFSHyperRAMConstruct,
7    /* .pfnDestruct = */ HFSHyperRAMDestruct,

The players could disclose the HFSHyperRAMConstruct() function pointer via the out-of-bounds read in IO_VERSION and use its offset to determine the base address of the HFSHyperRam.so module on the remote system.

Out of Bounds Read-Write on the Heap

The g_arrHFSStorages variable kept track of all allocated storages and associated data in an array of HFS_STORAGE structures. The structure looked as follows:

typedef struct HFS_STORAGE {
  int id;
  int len;
  char key[8];
  uint64_t *data;
} HFS_STORAGE, *PHFS_STORAGE;

The g_arrHFSStorages array could be traversed with the HFS_CONFIG_IDX_OFFSET register, as it set the storageIndex variable. Two modes of storage was available IO_ENCRYPT and IO_PLAIN. In IO_PLAIN mode, the player could perform out-of-bounds reads and writes via the HFS_STORAGE data buffers. This was due to the bufOffset variable being controllable via the HFS_CONFIG_BUF_OFFSET register and no bounds checks were done when reading or writing:

 1static DECLCALLBACK(VBOXSTRICTRC)
 2    HFSHyperRAMMMIOWrite(PPDMDEVINS pDevIns, void *pvUser, RTGCPHYS off,
 3                         void const *pv, unsigned cb) {
 4  uint64_t storageIndex = g_aHFSConfigMap[5].value;
 5  uint64_t bufOffset = g_aHFSConfigMap[1].value;
 6  ...
 7  if (off == 0 && cb == 8) {
 8      if (g_aHFSConfigMap[2].value == IO_ENCRYPT) {
 9        // Convert the char key[8] to uint64_t for XOR operation
10        uint64_t keyVal = *(uint64_t *)(g_arrHFSStorages[storageIndex]->key);
11        // XOR the value of pv with the key before assignment
12        g_arrHFSStorages[storageIndex]->data[bufOffset] =
13            *(uint64_t *)pv ^ keyVal;
14      } else {
15        g_arrHFSStorages[storageIndex]->data[bufOffset] = *(uint64_t *)pv;
16      }

Arbitrary Read-and-Write

To achieve a fully controlled arbitrary read-and-write primitive, the player could corrupt a HFS_STORAGE structure in heap memory. More precisely the data pointer. The data member would normally point allocated heap memory where the data of the HFS_STORAGE was stored. Once corrupted it could then be used to read and write to an arbitrary memory address.

The player could do the following steps to achieve this:

  1. Set a unique crypto key with HFS_CONFIG_KEY_OFFSET. This was used as a needle to find the HFS_STORAGE in memory.
  2. Spray the stack with HFS_STORAGE structures by writing to the HFS_CONFIG_RW offset and incrementing the storageIndex with HFS_CONFIG_IDX_OFFSET for each write.
  3. Select the first HFS_STORAGE by setting HFS_CONFIG_IDX_OFFSET back to 0
  4. Use the out-of-bounds vulnerability described above to search for one of the sprayed HFS_STORAGE structures in the heap with the unique crypto key as the needle
  5. If the unique crypto key is found, use the out-of-bounds read the disclose the id from the HFS_STORAGE structure. This is now the target structure.
  6. The first HFS_STORAGE can now be used to corrupt the target structure. Use the out-of-bounds write to set the HFS_STORAGE structure member data to an arbitrary address
  7. Set HFS_CONFIG_IDX_OFFSET to the id of the target HFS_STORAGE structure and use HFS_CONFIG_RW to perform arbitrary read-and-writes
  8. Repeat step 6-7 to perform additional arbitrary read-and-writes

Arbitrary Code Execution

To achieve arbitrary code execution the player could swap the RTMemAlloc() pointer in the got.plt section of HFSHyperRam.so with system() from libc.

The following steps could be taken to achieve arbitrary code execution:

  1. Use the arbitrary read-and-write primitive to disclose a VBoxRT.so function pointer from the .got.plt section of HFSHyperRam.so
  2. Disclose a libc.so function pointer from VBoxRT.so and apply offsets to determine the address of system()
  3. Overwrite RTMemAlloc() in the got.plt section of HFSHyperRam.so
  4. Write the string flag to a read-and-writeable location. For example the .data section of HFSHyperRAM.so
  5. Use HFS_CONFIG_BUF_LEN_OFFSET to set the storage data buffer length to the address of the flag string, as this is a valid integer
  6. Set the current HFS_CONFIG_IDX_OFFSET to a currently unused storage buffer. Use HFS_CONFIG_RW to write to it.

This would trigger the following code path where a data buffer is allocated for a new HFS_STORAGE. The len variable contained the address of the flag string and RTMemAlloc() had been replaced with system():

 1static DECLCALLBACK(VBOXSTRICTRC)
 2    HFSHyperRAMMMIOWrite(PPDMDEVINS pDevIns, void *pvUser, RTGCPHYS off,
 3                         void const *pv, unsigned cb) {
 4...
 5if (off == 0 && cb == 8) {
 6    if (g_arrHFSStorages[storageIndex] == NULL) {
 7      g_arrHFSStorages[storageIndex] =
 8          (PHFS_STORAGE)RTMemAlloc(sizeof(HFS_STORAGE));
 9      uint64_t len =
10          (g_aHFSConfigMap[3].value == 0) ? 8 : g_aHFSConfigMap[3].value;
11      void *data = (uint64_t *)RTMemAlloc(len);

This would result in a VirtualBox escape as the flag process was spawned on the host OS.

Exploit

Alt text for my gif

Exploit Source Gist