Description: Dive deep into the virtual realm and remember, duality hides the truth. Seek where they intertwine, and the solution will emerge.
Unintended Path
Fire up the program in gdb: gdb -q ./dim
Disassemble main
. There are 3 calls to puts
right before an exit()
.You find an interesting one and from objdump -sj .rodata dim
, you learn that this is the fail case prompt: “Incorrect key!”.
If you set a breakpoint, right before that call, you can inspect the rdx
and rax
registers to see what was being compared. In this case, the stack reveals a lot more.
And there you have a flag : SHCTF{7r1ck_0r?}
… quick but unsatisfactory 😕
Intended Path
In a perfect program where the set up is a lot more complex than this and leaks do not show this easily, you load this binary on [ghidra](https://ghidra-sre.org/)
for some static disassembly. Here is the decompiled program.
You notice a variable holding 24 bytes being loaded into a location VM_MEMORY + 0x70
. You notice a loop through that memory location and a switch case with each byte.
Each case performs some execution logic.
First crack: This hints at a basic machine for which the custom instructions are the 24 bytes and the execution logic is the switch case!
You need to perform a custom disassembly. Understand what each opcode does.
What we know:
The machine has 10 opcodes numbered
0x00
→0xa
.This is a rough deconstruction of each opcode:
There are two obfuscated strings that are xor’d by
0x54
.
You deconstruct the vm_instructions to this pseudo-decompilation. See the deconstruct.py in the appendix.
Running the script, you get “decompiled” code:
mov(reg_30, 0x10, 8)
reg_30 = 0xfd3a4a2141450b31
mov(reg_38, 0x40, 8)
reg_38 = 0xae720975073e3c43
xor(reg_30, reg_38, 8)
reg_30 = 0x53484354467b3772
load(reg_90, reg_30, 8)
reg_90 = 0x53484354467b3772
mov(reg_30, 0x18, 8)
reg_30 = 0x1516151515151515
mov(reg_38, 0x48, 8)
reg_38 = 0x1c4d564a1b5d2a68
add(reg_30, reg_38, 8)
reg_30 = 0x31636b5f30723f7d
load(reg_98, reg_30, 8)
reg_98 = 0x31636b5f30723f7d
mov(reg_30, 0x60, 8)
reg_30 = 0x60
mov(reg_38, reg_90, 8)
reg_38 = 0x53484354467b3772
cmp(reg_30, reg_38, 8)
reg_30 = 0x60
cmp(`, SHCTF{7r)
mov(reg_38, reg_90, 8)
reg_38 = 0x53484354467b3772
cmp(reg_30, reg_38, 8)
reg_30 = 0x60
cmp(`, SHCTF{7r)
mov(reg_30, 0x68, 8)
reg_30 = 0x68
mov(reg_38, reg_98, 8)
reg_38 = 0x31636b5f30723f7d
cmp(reg_30, reg_38, 8)
reg_30 = 0x68
cmp(h, 1ck_0r?})
print(Correct key!)
exit()
Behold! your key → SHCTF{7r1ck_0r?}
Appendix
The program decompiled on ghidra.
undefined8 main(void)
{
size_t sVar1;
undefined8 local_98;
undefined8 local_90;
char local_88 [32];
undefined8 local_68;
undefined6 local_60;
undefined2 uStack_5a;
undefined6 uStack_58;
int local_4c;
int local_48;
int local_44;
int local_40;
int local_3c;
undefined8 *local_38;
undefined1 *local_30;
byte *local_28;
undefined8 *local_20;
char *local_18;
ulong local_10;
puts(" ");
puts(" [+] ");
puts(" / \\ ");
puts(" /_____\\ ");
puts(" / \\ ");
puts(" / [_] \\ ");
puts(" [_______] ");
puts(" ");
puts(" [Veiled Dimensions] \n");
for (local_10 = 0; local_10 < 0x10; local_10 = local_10 + 1) {
VM_MEMORY[local_10 + 0x10] = obfuscated_string1[local_10] ^ 0x54;
VM_MEMORY[local_10 + 0x40] = obfuscated_string2[local_10] ^ 0x54;
}
VM_MEMORY[32] = 0;
VM_MEMORY[80] = 0;
local_68 = 0x1800040240011000;
local_60 = 0x600005034801;
uStack_5a = 0x806;
uStack_58 = 0xa0908076800;
printf("Enter the key: ");
fgets(local_88,0x12,(FILE *)stdin);
sVar1 = strcspn(local_88,"\n");
local_88[sVar1] = '\0';
local_18 = VM_MEMORY + 0x70;
local_20 = &local_68;
while (*(char *)local_20 != '\n') {
*local_18 = *(char *)local_20;
local_20 = (undefined8 *)((long)local_20 + 1);
local_18 = local_18 + 1;
}
strncpy(VM_MEMORY + 0x60,local_88,0x10);
local_28 = VM_MEMORY + 0x70;
local_30 = (undefined1 *)0x0;
local_38 = (undefined8 *)0x0;
local_90 = 0;
local_98 = 0;
do {
if (*local_28 == 10) {
return 0;
}
printf("%x ",(ulong)*local_28);
switch(*local_28) {
case 0:
local_28 = local_28 + 1;
local_30 = VM_MEMORY + *local_28;
break;
case 1:
local_28 = local_28 + 1;
local_38 = (undefined8 *)(VM_MEMORY + *local_28);
break;
case 3:
for (local_48 = 0; local_48 < 8; local_48 = local_48 + 1) {
local_30[local_48] = local_30[local_48] + *(char *)((long)local_38 + (long)local_48);
}
break;
case 4:
for (local_3c = 0; local_3c < 8; local_3c = local_3c + 1) {
local_88[(long)local_3c + -8] = local_30[local_3c];
}
break;
case 5:
for (local_40 = 0; local_40 < 8; local_40 = local_40 + 1) {
*(undefined1 *)((long)&local_98 + (long)local_40) = local_30[local_40];
}
case 2:
for (local_44 = 0; local_44 < 8; local_44 = local_44 + 1) {
local_30[local_44] = local_30[local_44] ^ *(byte *)((long)local_38 + (long)local_44);
}
break;
case 6:
local_38 = &local_90;
break;
case 7:
local_38 = &local_98;
break;
case 8:
for (local_4c = 0; local_4c < 8; local_4c = local_4c + 1) {
if (local_30[local_4c] != *(char *)((long)local_38 + (long)local_4c)) {
puts("Incorrect key!");
/* WARNING: Subroutine does not return */
exit(1);
}
}
case 9:
puts("Correct key!");
/* WARNING: Subroutine does not return */
exit(0);
default:
printf("Invalid instruction: %x\n",(ulong)*local_28);
/* WARNING: Subroutine does not return */
exit(1);
}
local_28 = local_28 + 1;
} while( true );
}
deconstruct.py
class VMParser:
def __init__(self):
self.vm_rodata = {}
self.registers = {
"reg_30": 0x0,
"reg_38": 0x0,
"reg_90": 0x0,
"reg_98": 0x0,
}
self.opcodes = self._initialize_opcodes()
@staticmethod
def reverse_qword_order(num):
hex_str = format(num, '016x')
return ''.join(reversed([hex_str[i:i+2] for i in range(0, len(hex_str), 2)]))
def _initialize_opcodes(self):
return {
0x00: ("mov: reg_30, 8", self.parse_ops),
0x01: ("mov: reg_38, 8", self.parse_ops),
0x02: ("xor: reg_30, reg_38, 8", self.parse_ops),
0x03: ("add: reg_30, reg_38, 8", self.parse_ops),
0x04: ("load: reg_90, reg_30, 8", self.parse_ops),
0x05: ("load: reg_98, reg_30, 8", self.parse_ops),
0x06: ("mov: reg_38, reg_90, 8", self.parse_ops),
0x07: ("mov: reg_38, reg_98, 8", self.parse_ops),
0x08: ("cmp: reg_30, reg_38, 8", self.parse_ops),
0x09: ("print(Correct key!)", None),
0x0a: ("exit()", None),
}
def get_addr_value(self, instructions, idx):
value = 0
addr = int(instructions[idx+2: idx+4], 16)
try:
round_addr = addr & 0xf0
offset = addr - round_addr
offset = 16 if offset > 0 else 0
value = self.vm_rodata[round_addr][offset: offset+16]
value = int(value, 16)
except:
value = addr
return hex(addr), value, 2
def parse_ops(self, instructions, idx, descr):
steps = 0
ops_params = descr.split(': ')
ops = ops_params[0]
params = ops_params[1].split(', ')
reg_1 = params[0]
reg_2 = params[1] if len(params) == 3 else None
size = int(params[-1])
if ops == "xor":
self.registers[reg_1] = self.registers[reg_1] ^ self.registers[reg_2]
elif ops == "add":
self.registers[reg_1] = self.registers[reg_1] + self.registers[reg_2]
elif ops == "load":
self.registers[reg_1] = self.registers[reg_2]
elif ops == "mov":
if len(params) == 2:
# get value from address
addr, value, steps = self.get_addr_value(instructions, idx)
else:
value = self.registers[reg_2]
self.registers[reg_1] = value
descr = f"{ops}({reg_1}, {addr if reg_2 is None else reg_2}, {size})\n {reg_1} = {hex(self.registers[reg_1])}\n"
if ops == "cmp":
# int to ascii
reg_1 = format(self.registers[reg_1], 'x')
reg_1 = bytes.fromhex(reg_1).decode("ascii")
reg_2 = format(self.registers[reg_2], 'x')
reg_2 = bytes.fromhex(reg_2).decode("ascii")
descr += f"cmp({reg_1}, {reg_2})\n"
return descr, steps
def execute(self, instructions):
i = 0
while i < len(instructions):
descr = ""
inst = int(instructions[i: i+2], 16)
if inst in self.opcodes:
descr += self.opcodes[inst][0]
if self.opcodes[inst][1]:
descr, skips = self.opcodes[inst][1](instructions, i, descr)
i += skips
else:
descr += f"Unknown instruction: {hex(inst)}"
print(descr)
i = i+ 2
def load_instructions(self, vm_instructions):
rev_instr = ''.join([self.reverse_qword_order(x) for x in vm_instructions])
self.vm_rodata[0x70] = rev_instr
def load_obfuscated_strings(self, obfuscated_string, address, key=0x5454545454545454):
processed_string = [self.reverse_qword_order(x ^ key) for x in obfuscated_string]
self.vm_rodata[address] = ''.join(processed_string)
# Example usage
parser = VMParser()
parser.load_instructions([0x1800040240011000, 0x0806600005034801, 0x0a09080768000806])
parser.load_obfuscated_strings([0x655F1115751E6EA9, 0x4141414141414241], 0x10)
parser.load_obfuscated_strings([0x17686A53215D26FA, 0x3C7E094F1E021948], 0x40)
parser.execute(parser.vm_rodata[0x70])