Home > Writeups > DEADROP Rev 6 - UNIT7

DEADROP Rev 6 - UNIT7

A VM-within-a-VM. An outer stack machine prints the banner. An inner register machine (UNIT7-LANG) runs two chained programs, program 1 computes the passphrase via a cross-register dependency chain, program 2 uses that state to compute and print the flag. No ciphertext stored, no flag wrapper, no shortcut.

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. Runs outer_bytecode[].
  • run_inner_vm(state, bc, len): UNIT7-LANG register machine. Called twice in main() 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.

< Back to All Writeups