← Back to Writeups
CUSTOMN/AReversing

Fruit VM

XESXOR9/10/20264 min read
#reversing#custom#bytecode_constraint_solving#checksum_reconstruction#vm_opcode_recovery#linux

Fruit VM

Platform: cyberareactf | Category: Reversing | Type: Challenge | Difficulty: Na | OS: Linux | Author: XESXOR | Date: 2026-07-15 | Status: Solved Techniques: bytecode_constraint_solving, checksum_reconstruction, vm_opcode_recovery

Summary

Task: Reverse a fruit-themed stack VM whose accepted bytecode must print ASCII 0 and derive the correct decryption key. Solution: Reconstruct the opcodes and rolling checksum, then execute a six-token program that yields key 0x55.

Fruit VM — cyberareactf

Description

Разберись как работает фруктовая виртуальная машина...очень интересно

The supplied amd64 ELF parses fruit names into bytecode and executes them in a stack-based virtual machine. The goal is to provide a program that passes the visible output check while also producing the checksum needed to decrypt the embedded flag.

Analysis

The important functions in the ELF are:

  • vm_execute at 0x401cc8
  • decrypt_flag at 0x401e94
  • the fruit-token parser at 0x401f82
  • main at 0x40226a ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Reversing the parser and interpreter gives the following instruction set:

IDTokenOperation
1APPLEPush the following one-byte immediate
2BANANAPop
3CHERRYAdd two stack values modulo 256
4DRAGONXOR
5ELDERCompare
6FIGJump
7GRAPEJump if zero
8HONEYJump if nonzero
9KIWIPop, print, and save the value as the last output
10LEMONPush the next value from a stateful table
11MANGOHalt and return the last output
12OLIVEDuplicate the top value
13PEACHSwap the top two values
14QUINCESubtract modulo 256
15RAISINMultiply modulo 256
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

LEMON cycles through this table, beginning with decimal 19:

text
13 37 42 5a 6f 7b 2c 11 55 6e

Before dispatching each instruction, the interpreter updates an eight-bit checksum:

text
checksum = ROL8(checksum, 1) XOR opcode

An APPLE operand is data rather than an opcode. The APPLE handler consumes it by advancing the instruction pointer, so the operand does not enter the checksum.

main first requires the VM's last output to equal 0x30, the ASCII character 0. That condition alone is insufficient: the checksum of the executed opcode sequence is passed to decrypt_flag as its key. A program that merely prints 0 but follows a different instruction path decrypts the ciphertext with the wrong key and produces garbage. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

The decryptor processes the 28 embedded ciphertext bytes as follows:

text
plain[i] = cipher[i] XOR ((checksum + 7*i) & 0xff)

Therefore, the program must satisfy two constraints simultaneously:

  1. its last printed byte must be 0x30;
  2. its executed opcode checksum must be 0x55.

Solution

The accepted program is:

text
APPLE 29 LEMON CHERRY KIWI MANGO

Its data path is straightforward:

  1. APPLE 29 pushes decimal 29.
  2. The first LEMON pushes 0x13, or decimal 19.
  3. CHERRY adds them: (29 + 19) & 0xff = 48 = 0x30.
  4. KIWI pops and prints 0, saving it as the last output.
  5. MANGO halts and returns that output. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

The parsed bytecode is [1, 29, 10, 3, 9, 11], but the executed opcode IDs are only [1, 10, 3, 9, 11]. Starting from zero, the checksum develops as:

text
00 --APPLE(01)--> 01
01 --LEMON(0a)--> 08
08 --CHERRY(03)--> 13
13 --KIWI(09)--> 2f
2f --MANGO(0b)--> 55

The following exact solver emulates the relevant VM path, checks both acceptance conditions, and decrypts the flag:

python
#!/usr/bin/env python3

LEMON_VALUES = [0x13, 0x37, 0x42, 0x5A, 0x6F,
                0x7B, 0x2C, 0x11, 0x55, 0x6E]
CIPHERTEXT = bytes.fromhex(
    "1605212f23392dc3ccefddd0fcd9e3e1b789a39680ab8abb984a7f6f"
)
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

# Parsed bytes for: APPLE 29 LEMON CHERRY KIWI MANGO
# APPLE's immediate 29 is consumed by PUSH and is not checksumed as an opcode.
bytecode = [1, 29, 10, 3, 9, 11]
stack = []
ip = 0
lemon_index = 0
checksum = 0
last_output = -1

while ip < len(bytecode):
    opcode = bytecode[ip]
    checksum = (((checksum << 1) | (checksum >> 7)) & 0xff) ^ opcode

    if opcode == 1:                 # APPLE / PUSH immediate
        ip += 1
        stack.append(bytecode[ip])
    elif opcode == 3:               # CHERRY / ADD
        b, a = stack.pop(), stack.pop()
        stack.append((a + b) & 0xff)
    elif opcode == 9:               # KIWI / PRINT
        last_output = stack.pop()
    elif opcode == 10:              # LEMON
        stack.append(LEMON_VALUES[lemon_index % len(LEMON_VALUES)])
        lemon_index += 1
    elif opcode == 11:              # MANGO / HALT
        break
    else:
        raise ValueError(f"unexpected opcode {opcode}")
    ip += 1
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

flag = bytes(c ^ ((checksum + 7 * i) & 0xff)
             for i, c in enumerate(CIPHERTEXT)).decode()

assert last_output == ord("0")
assert checksum == 0x55
assert flag == "CYBERAREA{REDACTED}"
print("program: APPLE 29 LEMON CHERRY KIWI MANGO")
print(f"output: {chr(last_output)!r}; checksum/key: 0x{checksum:02x}")
print(f"flag: {flag}")

Run the solver with:

bash
python3 solve.py

The program was also supplied to the original ELF in a Linux amd64 container. It printed 0 followed by the same flag, confirming that the emulation matches the interpreter. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍