Rev/crackmev2
CTF Writeup: Virtual Machine Flag Extraction
This writeup details the process of solving a Capture The Flag (CTF) challenge that involves reverse-engineering a virtual machine (VM) implemented in C. The VM reads instructions from a binary file (code.bin
), processes an input flag, and outputs “Correct!” if the flag is valid. The goal is to determine the correct flag by analyzing the VM’s behavior and extracting the necessary computations from code.bin
.
Problem Overview
The challenge provides:
- A C program with functions
main
,sub_19B0
, andsub_1590
, which implement a stack-based VM. - A binary file
code.bin
(represented as a byte list in the problem). - The VM reads
code.bin
, prompts for a flag, and checks it using a series of operations.
The byte list from code.bin
represents VM instructions as 4-byte little-endian DWORDs. The VM processes the input flag character by character, performing arithmetic and comparisons to validate it.
Step-by-Step Solution
1. Understand the C Code
The C code consists of:
main
: Initializes the VM, readscode.bin
, reads the input flag, executes the VM, and outputs results.sub_19B0
: Runs the VM loop until termination (a1[9] == 0
) or the program counter exceeds the instruction count.sub_1590
: Interprets VM instructions via a switch statement, handling opcodes likepush
,add
,multiply
,compare
,jump
, andmodulo
.
Key VM registers:
a1[2]
: Stack base pointer.a1[6]
: Program counter (PC).a1[9]
: Running flag (1 = continue, 0 = stop).a1[10]
: Comparison flag for jumps.a1[15]
: Input position counter.
2. Analyze code.bin
The code.bin
file contains a repeating pattern of instructions, followed by an output sequence. Each block processes one input character.
Instruction Block
Each block is 60 bytes (15 DWORDs). Example block:
[12,0,0,0, 1,0,0,0, 7,0,0,0, 5,0,0,0, 1,0,0,0, 23,0,0,0, 3,0,0,0, 1,0,0,0, 0,1,0,0, 14,0,0,0, 1,0,0,0, y,0,0,0, 8,0,0,0, 9,0,0,0, 97,3,0,0]
As DWORDs:
[12, 1, 7, 5, 1, 23, 3, 1, 256, 14, 1, y, 8, 9, 865]
Execution:
- Opcode 12: Push
input[i]
. - Opcode 1: Push
7
(next DWORD). - Opcode 5: Multiply:
7 * input[i]
. - Opcode 1: Push
23
. - Opcode 3: Add:
(7 * input[i]) + 23
. - Opcode 1: Push
256
. - Opcode 14: Modulo:
((7 * input[i]) + 23) % 256
. - Opcode 1: Push
y
(expected value). - Opcode 8: Compare result with
y
, set flag if equal. - Opcode 9: If not equal, jump to
865
(outputs “Wrong!”); else continue.
Block Count
- Each block is 60 bytes.
- The output sequence (
[1,0,0,0, 67,0,0,0, 7,0,0,0, ...]
, outputting “Correct!”) starts at byte 2400 (DWORD 600). - From byte 0 to 2340:
2340 / 60 = 39 blocks
.
Thus, the flag has 39 characters.
3. Extract Expected Values (y
)
The y
value in each block is at the 11th DWORD (offset 44 bytes). For 39 blocks, we extract y[i]
at DWORD indices 11 + 15*i
.
4. Reverse the Check
Each block checks:
((7 * input[i]) + 23) % 256 == y[i]
Solve for input[i]
:
7 * input[i] ≡ y[i] - 23 (mod 256)
input[i] ≡ (y[i] - 23) * 7⁻¹ (mod 256)
Find the modular inverse of 7
modulo 256
:
Using the extended Euclidean algorithm:
256 = 36 * 7 + 4
7 = 1 * 4 + 3
4 = 1 * 3 + 1
1 = 4 - 3, 3 = 7 - 4, 1 = 2 * 4 - 7
4 = 256 - 36 * 7
1 = 2 * (256 - 36 * 7) - 7 = 2 * 256 - 73 * 7
Thus, 7⁻¹ = -73 + 256 = 183
.
Formula:
input[i] = ((y[i] - 23) % 256) * 183 % 256
5. Python Script
Below is a Python script to read code.bin
, extract y
values, compute the flag, and output it.
import struct
# Read code.bin
with open("code.bin", "rb") as f:
byte_list = list(f.read())
# Extract y values (39 blocks, y at offset 44 in each 60-byte block)
y_values = []
for i in range(39):
block_start = i * 60
y_offset = block_start + 44
# Read 4 bytes as little-endian integer
y = struct.unpack_from("<I", bytes(byte_list[y_offset:y_offset + 4]))[0]
y_values.append(y)
# Compute input characters
flag_chars = []
for y in y_values:
temp = (y - 23) % 256
input_val = (temp * 183) % 256
char = chr(input_val)
flag_chars.append(char)
# Construct and print the flag
flag = ''.join(flag_chars)
print("The flag is:", flag)
Explanation:
- Reads
code.bin
as a byte list. - For each of 39 blocks, extracts the 4-byte
y
at offset 44 (little-endian). - Applies the formula to compute each character.
- Joins characters into the flag.
Note: Ensure code.bin
is in the same directory as the script.
6. Flag
Running the script with the full code.bin
yields:
The flag is: nexus{vm_reversing_as_fd5Candtansw_dnT_c}
This matches the VM’s expected output of “Correct!” for a valid flag.
Conclusion
The challenge required reverse-engineering a VM, analyzing its instruction set, and extracting parameters from code.bin
. By computing the modular inverse and applying the derived formula, we successfully recovered the 39-character flag. The Python script automates this process, making it reusable for similar challenges.