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 лежит обычное диалоговое окно, то первым делом смотрим в его зарегистрированную оконную функцию.
Рис 1. Часть оконной функции диалога, отвечающая за обработку WM_COMMAND.
Нажатие кнопки приводит к посылке приложению сообщения WM_COMMAND с ID того элемента, у которого произошло событие. В данном случае происходит получение введенных значений используя API GetDlgItemTextA, а затем вызывается функция осуществляющая проверку введенных значений.
Рис 2. Функция Check вызываемая из оконной функции.
Смотря на большое число команд NOP (403F3E – 4035A9 = 995h) можно предположить что исходный код алгоритма, который был завиртуален размещался изначально здесь, а в дальнейшем был затерт.
Функция sub_4017C0 является оберткой над вызовом виртуальной машины, основной цикл которой расположен в функции 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ричной системе счисления;
??? – произвольное значение, произвольной длины
Алгоритм:
- Считаем сумму от символов введенного имени
for ( i = 0; i < nLenName; i++ ) dwNameSum += name[i]
- Сравниваем полученное значение с первым блоком ключа;
- Считаем хэш от имени компьютера
for ( j = 0; j < nCompNameLen; j++ ) { dwHashCompName ^= j ^ szCompName[j]; dwHashCompName = __ROL__(dwHashCompName, 3); }
- От секций 11111111, 22222222 и HHHHHHHH получаем из бинарное представление (hexdec)– аналог dwHash1, dwHash2, dwHash3.
- Для каждого из значений производим операцию:
dwHashN = dwHashN xor dwHashCompName xor htonl(dwHashCompName)
- Хэш от ключа считается следующим образом:
dwHashSerial = dwHash3 xor dwHash1 xor htonl(dwHash1) xor dwHash2 xor htonl(dwHash2) xor dwHashCompName xor htonl(dwHashCompName)
- Хэш от имени считается по алгоритму:
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 и исходники кейгена
Автор: Veliant