← Back to Writeups
CUSTOMEasypwn

Write Me Not Reborn

XESXOR9/2/20263 min read
#pwn#custom#easy#logic_flaw_bypass#rodata_constant_reuse#strstr_attacker_controlled_poi

Write Me Not Reborn

Platform: pwn_spbctf | Category: pwn | Type: Challenge | Difficulty: Easy | OS: NA | Author: XESXOR | Date: 2026-06-17 | Status: Solved Techniques: logic_flaw_bypass, rodata_constant_reuse, strstr_attacker_controlled_pointer

Summary

Task: NON-PIE x86-64 pwn binary whose get_flag runs strstr(attacker_pointer, "root") and prints /flag on a match. Solution: point the haystack pointer at the in-binary "root" constant (relocated to 0x402067 in this 'Reborn' variant) so strstr matches and print_flag dumps the flag.

Write Me Not Reborn — pwn_spbctf

Description

Can you ask me well enough?

A 64-bit x86-64 ELF (write_me_not2) served over nc 109.233.56.90 11622. It is the "Reborn" sequel to Write Me Not. Protections: NON-PIE (loaded at 0x400000), NX, Partial RELRO, no stack canary. Author: Ilya Shilov (@Shtrikh17).

The goal is to make the program read and print the contents of /flag.

Analysis

The binary exposes a small menu (same structure as write_me_not1):

OptionFunctionBehavior
0read_func @ 0x4012d9arbitrary read: printf("%s", ptr)
1write_func @ 0x401395arbitrary 1-byte write: sprintf(dest, "%c", val)
2get_flag @ 0x40146athe intended "win" path
3help
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

scan_hex @ 0x4011ee requires every numeric input to start with the literal 0x.

The interesting (and flawed) function is get_flag:

c
void get_flag() {
    char *P = scan_hex("Address: ");   // attacker-controlled pointer
    if (strstr(P, "root") != NULL)     // needle "root" at .rodata 0x402067
        print_flag();                  // fopen("/flag"), fgets, puts
}

get_flag reads an attacker-supplied pointer P, then calls strstr(P, "root"). If the substring "root" is found in the memory pointed to by P, it calls print_flag(), which simply fopen("/flag"), fgets, puts. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

The flaw: the haystack pointer P is fully attacker-controlled. We do not need to find or place "root" anywhere — the needle string "root" literally exists inside the binary's .rodata (it is the constant fed to strstr). The disassembly of get_flag shows a lea rsi, [rip+...] loading the needle. In this version that constant resolves to address 0x402067.

Because the binary is NON-PIE, that address is fixed across runs. If we pass the haystack pointer equal to the "root" constant address, then strstr(0x402067, "root") trivially matches itself and print_flag() runs. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

This is the exact same logic flaw as in v1. The only thing the "Reborn" sequel changed was the .rodata layout — the "root" constant moved from 0x402077 (v1) to 0x402067 (v2). The bypass is identical once the new constant address is re-derived from the disassembly.

Solution

  1. Connect to the service.
  2. Select option 2 (get_flag).
  3. At the Address: prompt, send the address of the in-binary "root" constant, 0x402067.
  4. strstr matches, print_flag() dumps /flag.
python
#!/usr/bin/env python3
# exp_wmn2.py — Write Me Not Reborn (pwn_spbctf)
from pwn import *
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

# io = process('./write_me_not2')
io = remote('109.233.56.90', 11622)

ROOT = 0x402067  # address of the "root" constant in .rodata (NON-PIE, fixed)

io.recvuntil(b'> ')
io.sendline(b'2')                       # get_flag
io.recvuntil(b'Address: ')
io.sendline(b'%#x' % ROOT)              # point strstr haystack at "root" itself
print(io.recvall().decode(errors='replace'))

Running it prints the flag (and the flag was accepted as "correct" by the platform via POST flag). ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍