SHCTF23: Veiled Dimensions

Untitled

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!”.

Untitled

Untitled

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.

Untitled

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.

Untitled

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:

  1. The machine has 10 opcodes numbered 0x000xa.

    This is a rough deconstruction of each opcode:

    Untitled

  2. There are two obfuscated strings that are xor’d by 0x54.

Untitled

Untitled

Untitled

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])

SHCTF23: SecureNotes3

Case 08 → Caught a Runner’s High 😶‍🌫️

comments powered by Disqus