← Back to Writeups
HTBN/AReversing

Auto Cooker

XESXOR8/23/20265 min read
#reversing#htb#n/a

Auto Cooker

Platform: GPN CTF | Category: Reversing | Type: Challenge | Difficulty: Easy | OS: NA | Author: D3v0o0Nu11 | Date: 2026-06-05 | Status: Solved Techniques: buffer_reversal, constant_extraction, nibble_swap, static_analysis, transform_pipeline_inversion, xor_self_inverse

Summary

Task: intro reverse engineering ELF that runs the input flag through a 5-stage cooking-themed byte transform pipeline and compares against a hardcoded TARGET. Solution: each stage is reversible (XOR 0xAA, nibble swap, buffer reverse, padding mask), so invert the pipeline in reverse order from TARGET to recover the flag.

Recon

Port scan

nmap -p- -sV -sC <TARGET> --min-rate 1000 -Pn
PortServiceVersionNotes
<PORT><SVC><VER><notes>

Enumeration highlights

  • Event: GPNCTF 2026 | ID: 20260605_gpnctf_autocooker
  • Tags: xor, elf, not_stripped, rev, byte_transform, easy, intro
  • Indicators: not-stripped ELF with descriptive function names, salt/fry/trim/mix/taste pipeline, hardcoded TARGET array in .data, XOR key 0xAA (GRAIN_OF_SALT), nibble swap (b<<4)|(b>>4)
  • Source: 20260605_gpnctf_autocooker.md

Foothold

Vulnerability / Misconfiguration

  1. Buffer_reversal
  2. Constant_extraction
  3. Nibble_swap
  4. Static_analysis
  5. Transform_pipeline_inversion
<command>

Exploitation

  • See original writeup content for detailed exploitation.

Privilege Escalation

Enumeration

sudo -l
find / -perm -4000 2>/dev/null
getcap -r / 2>/dev/null
cat /etc/crontab
ps aux

Exploitation

  1. N/A for challenge-type writeup; see exploitation above.
  2. Flag obtained via challenge solve.
<command>

Flags

FlagLocationValue
flagREDACTED

Key Takeaways / Lessons

  • buffer_reversal
  • constant_extraction
  • nibble_swap
  • static_analysis
  • transform_pipeline_inversion
  • xor_self_inverse
  • Tags: xor, elf, not_stripped, rev, byte_transform, easy, intro

Original Writeup

<details><summary>Click to expand original content</summary>

Description

I always feel like cooking is such a chore... You have to chop up all your ingredients, cook them for hours and then make the plating look half-decent. But not with this new machine I got! You just have to put in your recipe (weirdly, the interface calls it a flag...) and it will get cooked for you. It's so easy, even someone with no experience in cooking reverse engineering can do it.

English summary: We are given a 64-bit ELF binary that reads a "recipe" (the flag), runs it through a series of cooking-themed transforms, and checks the result against a hardcoded target. The goal is to find the input flag that "cooks" into the expected output.

Analysis

The handout autocooker.tar.gz contains a single ELF:

  • ELF 64-bit LSB executable, x86-64, dynamically linked, GNU/Linux 3.2.0, NOT stripped, 16616 bytes.
  • checksec: Partial RELRO, No canary, NX enabled, No PIE (base 0x400000), not stripped.

Because the binary is not stripped, every transform function keeps its descriptive name, which essentially documents the whole algorithm:

check_recipe_length, explain_current_food, salt, fry, trim, mix, taste, main

Control flow in main

  1. Reads the user's recipe (the flag) via fgets into a 64-byte buffer RECIPE at 0x4040e0 (size 0x40).
  2. check_recipe_length: ensures RECIPE[TARGET_LENGTH] == 0 and RECIPE[TARGET_LENGTH-1] != 0 (length sanity check); otherwise it exits with "Your recipe is too complicated or too simple...".
  3. Copies RECIPE into the 64-byte FOOD buffer at 0x404120.
  4. Runs a 5-stage pipeline (with explain_current_food printing flavor text between stages), then taste compares FOOD to a hardcoded TARGET array and prints "Congratulations, you cooked a delicious plate of food!" on a full match.

The 5 transform stages (forward direction)

Each stage iterates i = 0..63 over the 64-byte FOOD buffer:

StageOperationProperty
saltFOOD[i] ^= GRAIN_OF_SALT (XOR 0xAA)self-inverse
fryFOOD[i] = ((FOOD[i] << 4) | (FOOD[i] >> 4)) & 0xFF (nibble swap)self-inverse
trimfor i in TARGET_LENGTH(61)..63: FOOD[i] &= 0x0Fonly touches padding bytes 61-63
mixreverse the entire 64-byte buffer (FOOD = FOOD[::-1])self-inverse
tasterequire FOOD[i] == TARGET[i] for all icomparison / exit

Constants extracted from the binary

  • TARGET_LENGTH @ 0x404060 = 61
  • GRAIN_OF_SALT @ 0x404064 = 0xAA
  • TARGET (64 bytes) @ 0x404080:
0a0a0a0a 7ddfa94c 5f9dfc2c 9db9ec5f d9f9fcee 8fe92e5f 8dff5c5f 8d5ecc5f
3feee9fc 8f5ffe8f bc5ffd5c 3f5ffe1e 3cb95f6c fc99cc5f 3c1dceef 9e4eafde

Solution

Every stage is reversible:

  • salt is XOR — its own inverse.
  • fry is a nibble swap — its own inverse.
  • mix is a buffer reversal — its own inverse.
  • trim only masks the 3 trailing padding bytes (indices 61-63). After mix reverses the buffer, those bytes land outside the 60-character flag, so trim is lossy but irrelevant to recovering the flag.

So we invert the whole pipeline by applying the inverse operations in reverse order, starting from TARGET:

  1. Undo mix: reverse the TARGET array
  2. Undo fry: nibble-swap each byte
  3. Undo salt: XOR each byte with 0xAA
#!/usr/bin/env python3
target = [0x0a,0x0a,0x0a,0x0a,0x7d,0xdf,0xa9,0x4c,0x5f,0x9d,0xfc,0x2c,0x9d,0xb9,0xec,0x5f,
          0xd9,0xf9,0xfc,0xee,0x8f,0xe9,0x2e,0x5f,0x8d,0xff,0x5c,0x5f,0x8d,0x5e,0xcc,0x5f,
          0x3f,0xee,0xe9,0xfc,0x8f,0x5f,0xfe,0x8f,0xbc,0x5f,0xfd,0x5c,0x3f,0x5f,0xfe,0x1e,
          0x3c,0xb9,0x5f,0x6c,0xfc,0x99,0xcc,0x5f,0x3c,0x1d,0xce,0xef,0x9e,0x4e,0xaf,0xde]

GRAIN = 0xAA
nibswap = lambda b: ((b << 4) | (b >> 4)) & 0xFF

food = target[::-1]                 # undo mix (reverse)
food = [nibswap(b) for b in food]   # undo fry (nibble swap)
food = [b ^ GRAIN for b in food]    # undo salt (XOR 0xAA)
print(bytes(food))
# b'GPNCTF{REDACTED}\n\n\n\n'

The first 60 bytes are the flag (7 for GPNCTF{ + 52 body + }). The trailing bytes are the fgets newline plus padding.

Forward verification

Feeding the recovered flag (plus a newline, padded to 64 bytes) through salt -> fry -> trim -> mix reproduces TARGET exactly, confirming correctness:

def forward(buf):
    buf = [b ^ GRAIN for b in buf]                 # salt
    buf = [nibswap(b) for b in buf]                # fry
    for i in range(61, 64):                        # trim
        buf[i] &= 0x0F
    return buf[::-1]                               # mix

flag = b"GPNCTF{REDACTED}\n"
buf = list(flag) + [0] * (64 - len(flag))
assert forward(buf) == target
print("verified")
</details>

Auto-tracked: saved to WriteUps; run /xesor-revise to fold lessons into XESXor_Methodology.md.

signed by XESXOR