Pharaoh's Curse - Rev
Points: 750 | Flag: 0xfun{ph4r40h_vm_1nc3pt10n} | Solved by: Smothy @ 0xN1umb

what we got
two files: tomb_guardian (ELF binary) and sacred_chamber.7z (password-protected archive). challenge description says some stuff about pharaohs and "speaking the old tongue". classic rev, lets go
the solve
part 1 - tomb_guardian (VM #1)
threw it into IDA. stripped binary, PIE, full protections, and has ptrace anti-debug. cute
the main function is a bytecode VM dispatcher:
// dispatch loop - classic switch-case VM
for (i = 0; i < program_length; i = PC) {
handler_table[bytecode[i]](vm_state);
if (halt_flag) break;
}reversed all 19 opcode handlers:
| opcode | operation |
|---|---|
| 0x00 | NOP |
| 0x01 | PUSH_IMM |
| 0x02 | POP_REG |
| 0x10 | ADD |
| 0x12 | XOR (with anti-debug key) |
| 0x20 | CMP_EQ |
| 0x31 | JZ |
| 0x40 | GETC (stdin) |
| 0x41 | PUTC (stdout) |
| 0xFE | HALT_OK |
| 0xFF | HALT_ERR |
the anti-debug is sneaky - ptrace(PTRACE_TRACEME) sets a XOR key to 0xFF if debugger detected, which corrupts the XOR operation. if no debugger, key stays 0x00 so XOR is clean. nice try lol
the bytecode has two phases:
phase 1 - input validation:
GETC ; read char
PUSH_IMM XX ; push constant
XOR ; xor input with constant
PUSH_IMM YY ; push expected
CMP_EQ ; compare
JZ fail ; jump to "No." if wrong
so input ^ XX == YY means input = XX ^ YY. extracted all 11 pairs:
pairs = [(0xAA,0x9A), (0xBB,0xCB), (0xCC,0xFF), (0xDD,0xB3),
(0xEE,0xB1), (0x11,0x62), (0x22,0x11), (0x33,0x40),
(0x44,0x70), (0x55,0x38), (0x66,0x55)]
password = ''.join(chr(a^b) for a,b in pairs)
# => "0p3n_s3s4m3"phase 2 - output message (ADD pairs into PUTC):
The sacred chamber awaits: Kh3ops_Pyr4m1d
"open sesame" in l33t and a cheops pyramid reference. the password Kh3ops_Pyr4m1d unlocks the 7z
$ 7z x -pKh3ops_Pyr4m1d sacred_chamber.7z
out comes hiero_vm and challenge.hiero. oh no. VM inception
part 2 - hiero_vm + challenge.hiero (VM #2)

the .hiero file is written in actual hieroglyphic unicode characters. ngl this is kinda beautiful:
š“
š¹ š
š
š“
š¹ š
š
...
š¹ š š š¹ š š š š¹ š š š š„ š
instead of reversing the 351KB Rust binary (lmao no thanks), i just pattern-matched the hieroglyphic program structure:
instruction set (hieroglyphic edition):
š“= READ input charš¹ X= LOAD value/memory[X]š= STOREš= PUSH to stackš= ADD (the lion does math apparently)š= COMPAREš X Y= conditional JUMPš³= SUCCESSšÆ= END
cuneiform characters (U+12000 base) encode both memory indices and literal values
the program:
- reads 27 chars into mem[0..26]
- checks 24 pairwise ADD constraints:
mem[i] + mem[j] == expected
since flag format is 0xfun{...}, we know mem[0..5] = 0xfun{ and mem[26] = }. that pins the first 6 chars and gives us enough info to solve the constraint chain
extracted all 24 constraints:
mem[6] + mem[7] = 0xD8 mem[6] + mem[10] = 0xA4 (cross)
mem[7] + mem[8] = 0x9C mem[8] + mem[15] = 0xA1 (cross)
mem[8] + mem[9] = 0xA6 mem[12] + mem[20] = 0x9B (cross)
mem[9] + mem[10] = 0xA6 mem[15] + mem[23] = 0x9E (cross)
mem[10] + mem[11] = 0x64 mem[7] + mem[18] = 0xD6 (cross)
mem[11] + mem[12] = 0x98
mem[12] + mem[13] = 0xC7
...
the cross-constraints + sequential chain = system of equations. used the cross constraint mem[6]+mem[10]=0xA4 combined with the chain mem[6]+mem[7]=0xD8, mem[9]+mem[10]=0xA6:
# 0xD8 + 0x9C - 2*mem[7] ā” 0xA4 (mod 256)
# => mem[7] = 0x68 = 'h'
# then propagate through the whole chainsolved in like 5 lines of python. all 24 constraints satisfied:
p h 4 r 4 0 h _ v m _ 1 n c 3 p t 1 0 n
$ echo -n "0xfun{ph4r40h_vm_1nc3pt10n}" | ./hiero_vm challenge.hiero
The ancient seals accept your offering
flag
0xfun{ph4r40h_vm_1nc3pt10n}
VM inside a VM. pharaoh themed. hieroglyphic programming language. "inception" in the flag. SwitchCaseAdvocate went hard on this one fr

smothy out āļø