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

в 15:46, , рубрики: keygen, keygenme, reverse engineering, метки: , , ,

0. Инфо

Страница KeygenMe на crackmes.de
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.
Difficulty: 4 — Needs special knowledge
Platform: Windows
Language: C/C++

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

Анализ keygenme от Ra$cal на базе виртуальной машины
Рис 1. Часть оконной функции диалога, отвечающая за обработку WM_COMMAND.

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

Анализ keygenme от Ra$cal на базе виртуальной машины
Рис 2. Функция Check вызываемая из оконной функции.

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

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

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

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

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

Смещение Тип Размер Описание
+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

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

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

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

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

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

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

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

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

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

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

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

Ключ имеет следующий вид:
SSSS-11111111HHHHHHHH22222222-???
, где:
SSSS – числовое значение в 10тичной системе счисления;
11111111, 22222222, HHHHHHHH – значения в 16ричной системе счисления;
??? – произвольное значение, произвольной длины

Алгоритм:

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

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

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

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

Автор: Veliant

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js