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.
Все эти функции внутри switch/case являются реализациями команд.
Сам виртуализированный код хранится в секции .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.
Так например пара команд
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 архитектуру.
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 и исходники кейгена
P.S. Яндекс палит как-то прямые линки, поэтому если показывает 404, открывайте ссылки в новой вкладке.
