Hexecution — HackTheBox Challenge Writeup
Challenge Overview
Challenge Description: My friend is always tampering with low-level things. So created something different, and going to challenge him. Before challenge him, can you try it and see if it works or not?
Hexecution is a classic HackTheBox hard-rated Reverse Engineering challenge centered around a custom Virtual Machine (VM). We are provided with two files:
- ▹
cook: A dynamically linked 64-bit Linux executable (the VM interpreter). - ▹
recipe.asm: A custom assembly bytecode program executed bycook.
Instead of writing flag-verification logic in standard x86-64 machine code that decompilers can solve immediately, the author implemented a kitchen-themed virtual Instruction Set Architecture (ISA). To solve the challenge, we must reverse-engineer the VM interpreter, document the custom ISA, translate the bytecode program into an algorithm, and algebraically invert that algorithm to recover the valid flag.
Binary Analysis & Enumeration
We begin our inspection by checking the binary properties and strings of the VM interpreter (cook):
Running strings cook reveals the kitchen-themed custom instruction names along with prompt and success strings:
- ▹Opcodes:
BOIL,AES256,QUICKMAFFS,SPELL,PEPEFROG,GOODBYE,WINDOW,LADDER,ROAST,GRIND,CHAIR,ID - ▹User-Facing Strings:
"Enter the flag: ","Invalid.","Wrong!","Nice! The flag is HTB{YOUR_INPUT} :)"
Running the binary against recipe.asm:
GLIBC Compatibility Note: The cook binary requires GLIBC >= 2.38. If your local Linux host runs an older version (e.g., Ubuntu 22.04 / Debian 11), execute the challenge cleanly using a container:
Reversing the VM Architecture (cook)
Opening cook in Ghidra shows that the main interpreter loop tokenizes each line of recipe.asm using strtok(line, " \n") and dispatches execution based on opcode string comparison:
::VM State & Virtual Registers
The VM maintains an internal stack struct and 6 global 16-bit registers located in the .bss segment:
| Register Name | Address | Description / Usage |
|---|---|---|
VEGETABLE | 0x40a0 | General purpose register |
FRUIT | 0x40a2 | General purpose register |
MEAT | 0x40a4 | General purpose register |
DAIRY | 0x40a6 | General purpose register |
PROTEIN | 0x40aa | Primary accumulator (QUICKMAFFS target) |
CARBO | 0x40ac | Base offset and index pointer register |
::Instruction Set Architecture (ISA) Reference
By decompiling the handler functions, we reconstruct the complete ISA semantics:
| Opcode | Arguments | Decompiled Implementation | Functional Description |
|---|---|---|---|
BOIL | reg, val | reg = strtoul(val, 0, 0) | Load immediate value into register |
AES256 | val | stack[index + CARBO] = val; index++ | Push single byte onto virtual stack |
SPELL | 1 | print(stack[CARBO .. PROTEIN]) | Print characters from stack to stdout |
SPELL | 0 | scanf("%s", &stack[CARBO]); len -> stack[CARBO+0x22] | Read user input; store length at CARBO+0x22 |
QUICKMAFFS | idx | PROTEIN = stack[idx] | Load byte from stack offset into PROTEIN |
WINDOW | reg | stack[CARBO] = reg | Store register byte into stack at offset CARBO |
LADDER | reg | reg++ | Increment register value by 1 |
ROAST | r1, r2 | r1 ^= r2 | Bitwise XOR two registers (often used to zero) |
GOODBYE | dst, src | dst = src | Copy source register value to destination |
GRIND | r1, r2 | if (r1 != r2) { puts("Wrong!"); exit(1); } | Assert equal registers (enforces input length!) |
PEPEFROG | r1, r2 | for (i=0..31) if (stack[r1+i] != stack[r2+i]) exit(1) | Assert 32-byte equality (final flag verification) |
CHAIR | — | dump_stack_numbers() | Debug helper: print stack as decimal values |
ID | — | dump_stack_chars() | Debug helper: print stack as ASCII characters |
Important Observation: The instruction GRIND is missing from the official challenge author
writeup PDF. However, static analysis proves it is the exact instruction responsible for enforcing
the 32-character input length check
Bytecode Analysis: Translating recipe.asm
With the ISA documented, we translate recipe.asm from top to bottom into structured pseudo-code:
::VM Stack Memory Map
| Hex Offset Range | Size | Contents & Description |
|---|---|---|
0x00 – 0x0f | 16 Bytes | "Enter the flag: " prompt string |
0x14 – 0x33 | 32 Bytes | User input flag body (32 ASCII characters) |
0x36 | 1 Byte | User input length (32, written by SPELL 0) |
0x42 – 0x61 | 32 Bytes | Transformed user input after Permutation 1 & Permutation 2 |
0x70 – 0x8f | 32 Bytes | Expected target ciphertext: 5maNcI4__U10_de5L13_Mn4U0u4trfn_ |
Why 0x70 and not 0x40? While BOIL CARBO, 0x40 sets the base register before pushing the
encrypted target string, the index variable inside struct stack was already incremented to
48 (16 prompt bytes + 32 input chars). Thus, the target string lands at offset 48 + 64 = 112 (0x70). Always verify actual addresses in Ghidra!
Algebraic Inversion & Key Recovery
The VM validates user input by evaluating:
To recover the valid input, we invert both permutation stages in reverse order:
- ▹
Invert Stage 2 (Global Reorder): Given
reordered[i] = shuffled[order[i]], the inverse mapping satisfies:~ / textshuffled[j] = enc_flag[inv[j]] where inv[order[i]] = i - ▹
Invert Stage 1 (4-Byte Group Shuffle): Forward permutation transforms each group
(a, b, c, d) -> (c, b, d, a). Letting(x0, x1, x2, x3) = (c, b, d, a), the inverse mapping restores the original positions:~ / text(a, b, c, d) = (x3, x1, x0, x2)
We implement this algebraic inversion concisely in Python:
Executing this inversion yields the challenge flag
Verification & Proof of Work
We verify our recovered flag using two independent methods:
::1. Pure Python VM Emulation
We feed the recovered flag string into a faithful Python emulator of the cook VM. The final PEPEFROG instruction compares our transformed buffer against the target string and outputs "Nice!".
::2. Runtime Execution against the Real Binary
We execute the native Linux binary inside an Ubuntu 24.04 container:
Flag Capture
Automation
Here is an interactive replay of htb_hexecution_solve.py in action:
Standalone Solver Script: View or download the complete automated Python solver and VM
emulator script
htb_hexecution_solve.py
in our /autosolve directory.
Reusable Methodology for Custom VM Challenges
- ▹Run
stringson the VM Interpreter: Custom opcodes and error messages are usually stored as plaintext strings in the.rodatasection. - ▹Decompile the Dispatcher Loop: Map every instruction opcode to its exact C/x86-64 semantics (pay close attention to stack index arithmetic and register sizes).
- ▹Check for Hidden Length Assertions: Seemingly standard comparison opcodes (like
GRIND) may hide input length requirements that aren't obvious in high-level diagrams. - ▹Annotate Bytecode Line-by-Line: Convert custom
.asmfiles into readable C/Python comments to identify loop boundaries and data structures. - ▹Invert Algebraically: When dealing with permutations, XOR layers, or linear transformations, solve algebraically rather than brute-forcing.
- ▹Look for Debug Leftovers: Opcodes like
CHAIRandIDare often left by challenge authors to dump stack states, making dynamic validation trivial. - ▹Emulate to Verify: Writing a lightweight 50-line Python emulator for custom ISAs allows instant testing without dealing with GLIBC version mismatches or GDB attachments.
References

Red Team Consultant · Penetration Tester · Bug Bounty Hunter
Offensive security professional with 250+ vulnerabilities reported across 50+ organizations including Atlassian, Vimeo, and AT&T. Sharing research, tools, and field notes.