R0 CREW

Анализ keygenme от Ra$cal на базе виртуальной машины

Источник: habrahabr.ru

0. Инфо

Описание: Crackme with simple vm. Key check algorithm is simple, so main target — vm. Difficult of pcode is growing from start to end. first part seems like emulator, but then it looks like like machine with another logic, registers, commands =) Good luck and have fun.

Сложность: 4 (необходимы специальные знания)
Платформа: Windows
Язык: C/C++

Скачать: здесь

1. Поверхностный анализ

Начнем свое исследование с просмотра кода, который выполняется когда мы нажимаем кнопку “Check” на форме. Так как в основе CrackMe лежит обычное диалоговое окно, то первым делом смотрим в его зарегистрированную оконную функцию.

Рис 1. Часть оконной функции диалога, отвечающая за обработку WM_COMMAND

Нажатие кнопки приводит к посылке приложению сообщения WM_COMMAND с ID того элемента, у которого произошло событие. В данном случае происходит получение введенных значений используя API GetDlgItemTextA, а затем вызывается функция осуществляющая проверку введенных значений.

Рис 2. Функция Check вызываемая из оконной функции

Смотря на большое число команд NOP (403F3E – 4035A9 = 995h) можно предположить что исходный код алгоритма, который был завиртуален размещался изначально здесь, а в дальнейшем был затерт.

Функция sub_4017C0 является оберткой над вызовом виртуальной машины, основной цикл которой расположен в функции sub_401890 псевдокод которой показан ниже.

Рис 3. Основной цикл виртуальной машины в функции sub_401890

Все эти функции внутри switch/case являются реализациями команд.

Сам виртуализированный код хранится в секции .rvmpc, начиная с адреса 00407000.

2. Формат команд

Определение структуры виртуальной машины, определение ее архитектуры, а также формата используемых ею команд — это первая и самая главная задача при девиртуализации кода.

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

Формат команд у этой виртуальной машины не фиксированной длины и довольно избыточен. У команд всех типов имеется общий заголовок следующего вида:

----------------------------------------------------
| Смещение | Тип   | Размер | Описание             |
----------------------------------------------------
| +0       | word  |   2    | код операции         |
| +2       | dword |   4    | ID команды           |
| +6       | byte  |   1    | размер аргументов    |
| +7       | dword |   4    | ID следующей команды |
| +Bh      | dword |   4    | неизвестно           |
----------------------------------------------------

Поле “ID команды” содержит уникальное значение для всего байткода. Соответственно для перемещения к следующей команде виртуальная машина берет значение из поля “ID следующей команды” и ищет во всем массиве кода команду, у которой ID совпадет с указанной. После чего используя switch/case от значения поля “код операции” происходит интерпретация каждой из команд.

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

--------------------------------------------------------------------------
| opcode | 	Действие                                                 |
--------------------------------------------------------------------------
| 0x00   | call                                                          |
| 0x01   | call                                                          |
| 0x02   | Условный переход                                              |
| 0x03   | push dword                                                    |
| 0x04   | sub/add ESP                                                   |
| 0x05   | push dword                                                    |
| 0x06   | mov [esp], dword                                              |
| 0x07   | jmp ID                                                        |
| 0x08   | mov [ebp+?], dword                                            |
| 0x09   | native                                                        |
| 0x0A   | native                                                        |
| 0x0B   | -                                                             |
| 0x0C   | Модификация внутреннего флага dw4052AC                        |
| 0x0D   | -                                                             |
| 0x0E   | -                                                             |
| 0x0F   | -                                                             |
| 0x10   | mov REGn,dword-reg / mov REGn, dword [reg+X]                  |
| 0x11   | mov dword [addr], (reg/dword) / mov dword [REGn], (reg/dword) |
| 0x12   | Различные команды пересылок mov                               |
| 0x13   | Различные команды пересылок mov                               |
| 0x14   | MOV _32[A], unpack(REGn)                                      |
| 0x15   | MOV REGn, pack(_32[A])                                        |
| 0x16   | XOR _32[A][i], _32[B][j]                                      |
| 0x17   | mov [REGn], reg / mov reg, [REGn]                             |
| 0x18   | XOR _32[A][c:d], _32[B][e:f]                                  |
| 0x19   | MOV _32[A], 0                                                 |
--------------------------------------------------------------------------

Команды 0x14 – 0x19 работают с дополнительной областью памяти, позволяющей производить побитовые операции на 32-битными числами.

3. Дизассемблирование кода ВМ

Теперь когда у нас есть описание формата команд и мы знаем какие функции они выполняют стоит написать дизассемблер для данной виртуальной машины. Команды могут быть совсем не похожи на систему команд ранее известных архитектур, поэтому можно транслировать в понятные Вам мнемоники.

Исходники дизассемблера с использованием библиотеки BeaEngine (+exe)
Дизассемблерный листинг команд составил у меня 961 строку.

4. Девиртуализация

Данный этап необязателен если завиртуаленый код небольшой и можно понять алгоритм без перекодирования его в нативный код. Как уже сказал выше процесс девиртуализации заключается в перекодировании кода для виртуальной машины в код для нативного процессора. Для этого дизассемблер вместо мнемоник виртуальной машины должен выдавать одну или несколько мнемоник нативного процессора. После чего следует скомпилировать полученный код. Эта операция даст поддержку уже имеющихся инструментов – отладчиков, декомпиляторов и т.д.

Впринципе, алгоритм я разобрал по большей мере по дизассемблерному листингу, но мне стало интересно как поведет себя HexRays, если собрать в нативный x86 код полученный листинг. Для этого я избавился от всяких внутренних регистров в мнемониках команд, переписал часть кода работающего с битовыми значениями и скомпилировал все в ехе. Я не добивался идеального поведения, поэтому в приведенном ниже декомпилированном коде имеются некоторые логические ошибки.

Получившийся исходник на x86 asm
Исходник собирается при помощи FASM.

Так например пара команд

MOV REGn, EBP-x
MOV reg, [REGn]

превращаются в одну

MOV reg, [EBP-x]

и команды перемещения в обратную сторону

MOV REGn, EBP-x
MOV [REGn], reg

превращаются в

MOV [EBP-x], reg

Больше же всего сокращается конструкция вида:

MOV REG9, DWORD PTR [ebp-E0]
MOV eax, DWORD PTR [REG9]
MOV [REG2], eax
MOV _32[1], 0
MOV _32[0], unpack(REG2)
XOR _32[0][0:7], _32[1][18:1F]
XOR _32[0][8:F], _32[1][10:17]
XOR _32[0][10:17], _32[1][8:F]
XOR _32[0][18:1F], _32[1][0:7]
MOV REG2, pack(_32[1])
MOV eax, [REG2]
MOV REG9, DWORD PTR [ebp-E0]
MOV DWORD PTR [REG9], eax

На x86 ассемблере она будет выглядеть так:

MOV eax, DWORD PTR [ebp-E0]
BSWAP eax
MOV DWORD PTR [ebp-E0], eax

После чего натравил на скомпилированный EXE плагин Hex-Rays и получаю исходник как на картинке ниже:

Рис 4. Декомпилированный код пересобранный под x86 архитектуру

5. Обращение алгоритма

Это самая унылая часть исследования, потому что алгоритм не представляет из себя ничего интересного и основан на XOR и циклических сдвигах.

Ключ имеет следующий вид:

SSSS-11111111HHHHHHHH22222222-???

Где:

  • SSSS – числовое значение в 10тичной системе счисления;
  • 11111111, 22222222, HHHHHHHH – значения в 16ричной системе счисления;
  • ??? – произвольное значение, произвольной длины

Алгоритм:

  1. Считаем сумму от символов введенного имени
 for ( i = 0; i < nLenName; i++ )
        dwNameSum += name[i]
  1. Сравниваем полученное значение с первым блоком ключа;
  2. Считаем хэш от имени компьютера
for ( j = 0; j < nCompNameLen; j++ ) {
      dwHashCompName ^= j ^ szCompName[j];
      dwHashCompName = __ROL__(dwHashCompName, 3);
    }
  1. От секций 11111111, 22222222 и HHHHHHHH получаем из бинарное представление (hexdec) – аналог dwHash1, dwHash2, dwHash3.
  2. Для каждого из значений производим операцию:
dwHashN = dwHashN xor dwHashCompName xor htonl(dwHashCompName)
  1. Хэш от ключа считается следующим образом:
dwHashSerial = dwHash3 xor dwHash1 xor htonl(dwHash1) xor dwHash2 xor htonl(dwHash2) xor dwHashCompName xor htonl(dwHashCompName)
  1. Хэш от имени считается по алгоритму:
for ( idx = 0; idx < nLenName; idx++ ) {
        if ( idx % 2 )
            dwHashName ^= 0x4F620AEC ^ (idx + dwHash1) ^ szName [idx];
        else
            dwHashName ^= 0x4F620AEC ^ (dwHash2 - idx) ^ szName[idx];
        dwHashName = __ROL__(dwHashName, idx);
    }

Все что нужно нам сделать – это сгенерировать случайно 2 значения для блоков 1 и 2. После чего считаем dwHashName и dwHashSerial, принимая dwHash3 равным 0. И останется только посчитать недостающее значение по формуле dwHash3 = dwHashName ^ dwHashSerial

Ну и конечно же результат всего исследования:

Скачать EXE и исходники кейгена можно тут

P.S. Яндекс палит как-то прямые линки, поэтому если показывает 404, открывайте ссылки в новой вкладке.