UNIT7
Overview
A VM-within-a-VM with two inner programs chained together. The outer stack machine (Rev 4 ISA) prints the banner. Inner VM program 1 computes the passphrase into registers via a cross-register dependency chain. Inner VM program 2 takes that register state and computes the flag character by character through arithmetic, it is never stored anywhere.
There is no ciphertext array. There is no flag wrapper. The flag exists only as the arithmetic output of program 2 at runtime. Every shortcut fails:
strings: flag and passphrase both absent- Patch passphrase check: program 1 runs before the check and fills registers correctly regardless; patching the check changes nothing about the output
- Extract ciphertext: there is none
- Brute force offline: nothing to check a guess against
The only path is to reverse both VMs completely, trace both programs, and either submit the passphrase or manually compute the flag from the register arithmetic.
Step 1: Run it
./UNIT7.elf
# === DEADROP ASSET EVALUATION SYSTEM ===
# // UNIT7-LANG v0.1 :: UNIT7 ONLINE
# ...HR complaints...
# PASSPHRASE REQUIRED:
strings finds nothing useful. No passphrase, no flag, no ciphertext.
Step 2: Identify the structure in Ghidra
Four key functions:
run_outer_vm(): stack machine, Rev 4 ISA. Runsouter_bytecode[].run_inner_vm(state, bc, len): UNIT7-LANG register machine. Called twice inmain()with different bytecode arrays.main(): calls prog1, checks passphrase, calls prog2 if correct.
The two run_inner_vm calls in main() are the critical observation, two
separate bytecode programs, same register state, chained.
Step 3: Reverse the UNIT7-LANG ISA
The switch in run_inner_vm() reveals 8 opcodes:
| Opcode | Format | Action |
|---|---|---|
| 0x01 | STORE reg, imm8 | r[reg] = imm |
| 0x02 | ADD reg, imm8 | r[reg] += imm |
| 0x03 | XOR reg, imm8 | r[reg] ^= imm |
| 0x04 | MUL reg, imm8 | r[reg] *= imm |
| 0x05 | MOV dst, src | r[dst] = r[src] |
| 0x06 | SUB reg, imm8 | r[reg] -= imm |
| 0x07 | PRINTCHAR reg | putchar(r[reg]) |
| 0xFF | HALT | stop |
Step 4: Locate and disassemble program 1
inner_bytecode_1[] in .rodata starts with STORE r0, 200 = 01 00 C8,
followed immediately by XOR r0, 0x8B = 03 00 8B. Search for that sequence.
data = open('UNIT7.elf', 'rb').read()
idx = data.find(bytes([0x01,0x00,0xC8,0x03,0x00,0x8B]))
Disassemble program 1 and trace register values:
| Reg | Derivation | Value | ASCII |
|---|---|---|---|
| r0 | 200 ^ 0x8B | 67 | C |
| r1 | r0 + 9 | 76 | L |
| r2 | r1 - 7 | 69 | E |
| r3 | r2 - 4 | 65 | A |
| r4 | r3 + 17 | 82 | R |
| r5 | MOV r3 | 65 | A |
| r6 | 6 × 13 | 78 | N |
| r7 | MOV r0 | 67 | C |
| r8 | MOV r2 | 69 | E |
| r9 | 5 × 19 | 95 | _ |
| r10 | r4 + 4 | 86 | V |
| r11 | 11 × 5 | 55 | 7 |
| r12 | 3 × 11 | 33 | ! |
Passphrase: CLEARANCE_V7!
Step 5: Locate and disassemble program 2
inner_bytecode_2[] starts immediately after program 1 in .rodata (or find it
via the second run_inner_vm call in main()). It begins with
MOV r13, r0 = 05 0D 00.
Program 2 carries a running value in r13 across most characters. It depends
entirely on r[0] from program 1: an error in program 1 cascades into every
character program 2 produces.
Disassembly and trace:
MOV r13, r0 ; r13 = 67
ADD r13, 18 ; r13 = 85 → 'U' PRINTCHAR
MOV r14, r4 ; r14 = 82
SUB r14, 4 ; r14 = 78 → 'N' PRINTCHAR
MOV r15, r2 ; r15 = 69
ADD r15, 4 ; r15 = 73 → 'I' PRINTCHAR
SUB r13, 1 ; r13 = 84 → 'T' PRINTCHAR
MOV r14, r11 ; r14 = 55 → '7' PRINTCHAR
MOV r14, r9 ; r14 = 95 → '_' PRINTCHAR
SUB r13, 5 ; r13 = 79 → 'O' PRINTCHAR
SUB r13, 2 ; r13 = 77 → 'M' PRINTCHAR
SUB r13, 8 ; r13 = 69 → 'E' PRINTCHAR
ADD r13, 2 ; r13 = 71 → 'G' PRINTCHAR
SUB r13, 6 ; r13 = 65 → 'A' PRINTCHAR
MOV r14, r9 ; r14 = 95 → '_' PRINTCHAR
SUB r13, 16 ; r13 = 49 → '1' PRINTCHAR
ADD r13, 2 ; r13 = 51 → '3' PRINTCHAR
MOV r14, r13 ; r14 = 51 → '3' PRINTCHAR
ADD r14, 4 ; r14 = 55 → '7' PRINTCHAR
STORE r14, 10 ; → '\n' PRINTCHAR
HALT
Flag: UNIT7_OMEGA_1337
Why every shortcut fails
| Attack | Why it fails |
|---|---|
strings |
Flag and passphrase computed at runtime, never stored |
| Patch passphrase check | Program 1 fills registers before the check, bypass changes nothing |
| Extract ciphertext | There is none, flag exists only as program 2 output |
| Known-plaintext | No DEADROP{} wrapper means no known bytes to work from |
| Brute offline | Nothing to check a guess against without running the binary |
| Run program 2 with wrong state | r13 starts from r[0], zeroed state gives non-printable garbage |
Key Takeaway
The flag is behaviour, not data. It only exists as the runtime output of a program that depends entirely on another program's computed state. Without reversing both VM layers and tracing both programs, there is nothing to work with. Unit 7 finally got their HR forms processed.