SpacezVM Девиртуализация Part 1 Линейный дизассемблер

В этой статье будет расмотрен подход к девиртуализации относительно простой виртуальной машины с одного из ctf соревнований.

Мы разработали новый язык программирования SpaceZ. Нам он кажется очень простым и удобным, но у нас до сих пор нет документации… Однако мы обещаем, что она появится через пару дней (когда наш единственный разработчик, знающий, как это все работает, вернется из отпуска). Но вы все равно можете пока протестировать нашу бета версию :)

В этом задании нам дано 2 файла: исполняемый файл под архитектуру mips и какой-то дополнительный файл.

После быстрого анализа, становится понятным, что исполняемый файл представляет собой виртуальную машину, а дополнительный - байткод для этой вм.

Анализ

Перед выполнением вм, программа выделяет память под байткод, загружает его и декодирует. Сначала проверяется кол-во аргументов (argc), их должно быть 2.

Затем байткод считывается в память и декодируется. Закодированный байткод выглядит следующим образом:

Каждый опкод разделен 0x0A символом и длина пробелов считается как номер опкода.

Как выглядит декодированный байткод:

После чего вызывается 2 функции - init_queue и init_regs. Первая инициализирует глобальную память виртуальной машины, вторая инициализирует регистры.

Структура VM

Виртуальная машина имеет 4 32-битных регистра и указатель текущей инструкции. Вместо стэка, она использует очередь-подобную (FIFO) структуру данных и все операции с памятью выполняются над этой очередью.

Register Purpose
R0 General purpose
R1 General purpose
R2 General purpose
R3 General purpose
PC Program counter
SP Our Virtual “Queue” pointer, points to the top of the queue
QP Our Virtual “Queue” pointer, points to the bottom of the queue

process_vm - Функция с большим свитч кейсом, которая в цикле выполняет байткод.

Всего можно насчитать 24 инструкции. Я придумал для них следующие имена:

Opcode Operand count Name Meaning
0 0 CLS Clear Queue
1 1 STRD Put 4 bytes in Queue
2 1 STRB Put 1 bytes in Queue
3 1 STRW Put 2 bytes in Queue
4 1 LDRD Load 4 bytes from Queue
5 1 LDRB Load 1 bytes from Queue
6 1 LDRW Load 2 bytes from Queue
7 2 MOV RegisterN = Immediate
8 2 MOV RegisterN = RegisterM
9 2 ADD RegisterN += RegisterM
10 2 SUB RegisterN -= RegisterM
11 2 MUL RegisterN *= RegisterM
12 2 DIV RegisterN /= RegisterM
13 2 MOD RegisterN %= RegisterM
14 2 AND RegisterN &= RegisterM
15 2 OR RegisterN |= RegisterM
16 2 XOR RegisterN ^= RegisterM
17 2 JMP Jump Immediate
18 4 JE Jump Immediate if RegisterN == RegisterM
19 4 JNE Jump Immediate if RegisterN != RegisterM
20 4 JGE Jump Immediate if RegisterN >= RegisterM
21 4 JL Jump Immediate if RegisterN < RegisterM
22 2 READ_INPUT Reads input with size N into Queue
23 2 PRN Prints contents of Queue
24 2 VMEXIT Virtual Exit (end of execution)

Дизассемблирование байткода

Поскольку байткод довольно простой, мне не составило труда написать линейный дизассемблер для него.

def disassembler(bytecode):
  pc = 0
  while pc < len(bytecode) - 1:
    print('%02s:\t' % str(pc), end=' ')
    opcode = bytecode[pc]
    if opcode == 0:
      print('cls')
      pc += 1
    elif opcode == 1:
      reg = bytecode[pc + 1]
      print(f'str reg{reg} (4 byte)')
      pc += 2

    elif opcode == 2:
      reg = bytecode[pc + 1]
      print(f'str reg{reg} (1 byte)')
      pc += 2

    elif opcode == 3:
      reg = bytecode[pc + 1]
      print(f'str reg{reg} (2 bytes)')
      pc += 2

    elif opcode == 4:
      reg = bytecode[pc + 1]
      print(f'ldr reg{reg}(4 bytes)')
...
...
...

Дизассемблированный код:

 0:      cls
 1:      mov reg0, 104
 4:      str reg0 (1 byte)
 6:      mov reg1, 105
 9:      str reg1 (1 byte)
11:      mov reg2, 33
14:      str reg2 (1 byte)
16:      mov reg0, 10
19:      str reg0 (2 bytes)
21:      call puts(Queue)
22:      call malloc(32)
22:      call scanf("%s", malloc_addr)
24:      ldr reg0 (1 byte)
26:      str reg0 (1 byte)
28:      mov reg2, 1
31:      mov reg1, 6
34:      mul reg1, reg2
37:      add reg1, reg2
40:      xor reg0, reg1
43:      ldr reg1 (1 byte)
45:      xor reg1, reg0
48:      str reg1 (1 byte)
50:      mov reg0, reg1
53:      mov reg1, 1
56:      mov reg3, 32
59:      add reg2, reg1
62:      cmp reg2, reg3
62:      je 67
62:      jmp 31
67:      jmp 92
92:      ldr reg0 (1 byte)
94:      ldr reg0 (1 byte)
96:      mov reg1, 99
99:      cmp reg0, reg1
99:      je 104
99:      jmp 69
104:     ldr reg0 (1 byte)
106:     mov reg1, 16
109:     cmp reg0, reg1
109:     je 114
...
...

Анализ виртуализированной функции

Как можно заметить, первым делом программа загружает hi!\x00 в очередь и выводит ее на экран (строка 21)

После этого считывается 32 байта в очередь и первый байт из нее загружается в конец. (строка 26)

На данный момент очередь будет выглядеть следующим образом: Конец -> [c|}|a|a|a|a|a|a|a|a|a|a|a|a|a|a|a|a|a|a|a|a|a|a|a|a|{|p|u|c|f|t] <- начало

Должен сказать, что формат флага ctfcup{, поэтому первые 7 символов и 1 последний нам известны.

После чего начинается цикл:

31:      mov reg1, 6
34:      mul reg1, reg2
37:      add reg1, reg2
40:      xor reg0, reg1
43:      ldr reg1 (1 byte)
45:      xor reg1, reg0
48:      str reg1 (1 byte)
50:      mov reg0, reg1
53:      mov reg1, 1
56:      mov reg3, 32
59:      add reg2, reg1
62:      cmp reg2, reg3
62:      je 67
62:      jmp 31

В регистре 2 хранится индекс, а в регистре 0 - последний байт.

Если перевести этот цикл на питон:

one_byte = flag.pop()
for i in range(1, len(flag)):
    t = 6
    t *= i
    t += i
    c = flag.pop()
    one_byte ^= t ^ c
    flag.put(one_byte)

После цикла идет посимвольное сравнение с уже заданнами значениями:

94:      ldr reg0 (1 byte)
96:      mov reg1, 99
99:      cmp reg0, reg1
99:      je 104
99:      jmp 69

104:     ldr reg0 (1 byte)
106:     mov reg1, 16
109:     cmp reg0, reg1
109:     je 114
109:     jmp 69

114:     ldr reg0 (1 byte)
116:     mov reg1, 120
119:     cmp reg0, reg1
119:     je 124
119:     jmp 69

В случае правильного ввода, пишется yes!, в случае неправильного no!.

Решение

Поскольку цикл кодировки флага довольно простой, нам не составит труда получить исходный флаг:

flag = [99, 16, 120, 14, 103, 52, 101, 2, 11, 70, 116, 108, 89, 78, 29, 46, 106, 105, 38, 147, 113, 189, 75, 166, 58, 251, 42, 226, 18, 190, 9, 173]

known = [ord(i) for i in 'ctfcup{']
last_byte = known[0]
out = ''
for i in range(1, len(flag)):
  for j in range(32, 127):
    t = 6
    t *= i
    t += i
    if last_byte ^ t ^ j == flag[i]:
      out += chr(j)
      last_byte = last_byte ^ t ^ j
      break
    
print(out)

ctfcup{V1rtUaL1Z4t10n_lL4ngu4ge}

Во второй части мы придумаем псевдоязык для этой вм, опишем его в miasm и поднимим в miasm IR для последующего эмулирования и оптимизации.


Конец.