Два года назад взялся писать программу, что помогает выставлять счета за аренду ковров. Прежде бухгалтер держал текст договоров в Word, расписание в Excel, а три дня каждого месяца убивал на подсчеты. Теперь программа сама рассчитает суммы и загрузит счета в 1С.
Компания такая не одна: защищу программу - продам ключи.
Защита программы - трудное дело. Исследую защитные алгоритмы на практике: вытащу из готовых программ, а чтобы никому не вредить, исследую программы, что специально написаны для взлома - crackme или keygenme.
Жребий пал на q_keygenme_1.0 by quetz.
Поймать защиту за хвост
Ищу код, что выводит сообщение "Invalid serial". Загружаю подопытного в IDA, по единственной ссылке на строку "Invalid serial" нахожу код вызова MessageBox. Отладчик подтверждает, что этот код выполняется при вводе неверного ключа. Условие перехода к MessageBox("Invalid serial") определить трудно: зависимости между флагами запутаны. Выясню, что делает процедура sub_4330E0 - возможно, удастся понять логику.
Дизассемблировать
sub_4330E0 обрабатывает сообщения окну: будь то нажатие кнопки Check или ввод символов name и serial. Возможно, программа проверяет name и serial после нажатия Check, а может и при вводе символа или перемещении курсора мыши, а после нажатия кнопки проверять флаг и показывать сообщение.
Например, на начало цикла похожа конструкция по адресу loc_433137: на этот адрес указывает ссылка снизу, то есть выполнение кода от старших адресов переходит назад к младшим. Это либо цикл, либо goto.
jz loc_433137 похоже на условие продолжение цикла, но если условие не выполняется, следует не выход из цикла, а прыжок в его середину. Конструкция больше похожа на if-else, вложенный в бесконечный цикл while(true).
Подробнее об идентификации циклов читайте в статье Идентификация циклов или в книге Криса Касперски "Искусство дизассемблирования".
Убил три часа на попытки понять код, нарисовал блок-схему, но схема оказалась ничем не проще запутанного графа в IDA. Нашел два вызова GetDlgItemText - чтение name и serial - два вызова MessageBox("Congratulations"), но ничего не выяснил об алгоритме вычисления serial.
Трассировать в отладчике
Трассирую программу в отладчике - остановлюсь на MessageBox("Invalid serial") и узнаю, какие условные переходы привели к этому коду.
Остановлюсь на последнем условном переходе 433F40 jz INVALIDSERIAL и сброшу ZF, чтобы переход не выполнился. Программа снова показывает "Invalid serial", значит, флаг var_B30 не влияет на результат.
Остановлюсь на предпоследнем переходе 4349F4 jz loc_433F16 и сброшу ZF - программа показывает "Yep, this serial is valid", значит, var_B49 - флаг, что отвечает за успех проверки.
Построить граф вычислений
var_B49 = (var_B44 ^ var_B3C) | (var_B40 ^ var_B38)
Построим граф вычислений для var_B49. Код запутывает вычисления, скрывая одни переменные в других, добавляя и вычитая из них константы. Отбросим лишнее и увидим, что var_B40 и var_B44 - результаты вызовов sub_430DB0, а var_B3C и var_B38 - это упакованный serial.
Код содержит два вызова sub_430DB0: с помощью трассировки выясним, что оба вызова - в цикле, который выполняется 100 раз. var_B40 и var_B44 - результаты двух последних вызовов sub_430DB0.
Код упаковывает serial длиной 16 символов в два dword так: каждый символ преобразуется в шестнадцатеричную цифру - полубайт. Символы, кроме [0-9A-Fa-f], обнуляются. Например:
Serial: 9FEYD6ZSRP9XEVG7
Packed: 90 E0 D6 00 00 90 E0 07
x86 CPU байты читает в обратном порядке, поэтому
var_B3C = 0x00D6E09F, var_B38 = 0x07E09000
Serial верный, когда var_B49 = 0.
(var_B44 ^ var_B3C) | (var_B40 ^ var_B38) = 0
Известно, что 0 | 0 = 0 и A ^ A = 0, значит
var_B44 ^ var_B3C = 0
var_B40 ^ var_B38 = 0var_B44 = var_B3C
var_B40 = var_B38
Обратим порядок байт у var_B44 и var_B40 и преобразуем полубайты в 16 символов - получим верный serial.
Два способа получить keygen:
изучить sub_430DB0 и восстановить исходный код алгоритма
пропатчить код так, чтобы программа показывала верный serial
Исследуем алгоритм защиты
Восстанавливать исходный код трудно. Процедура sub_430DB0 вызывает sub_401360, в которой автор реализовал хитрый генератор псевдослучайных чисел:
sub_401360(int* pCase, int* pResult) {
while (true) {
var = 0x4FB;
switch (var) {
case 0x4FB:
//...
var = *pCase;
// 2187 cases ...
default:
/* ... */
return;
}
}
}
Каждый case вычисляет следующее значение var, некоторые case вычисляют и записывают result. Потребуется исследовать 2188 case, чтобы в точности воспроизвести алгоритм.
Патчим программу
Проще использовать sub_430DB0. Пусть программа запрашивает только name, а serial генерирует. Сделаем так:
вместо вызова GetDlgItemText для поля serial подставим memset, чтобы заполнить буфер 16 символами
Вместо вычисления var_B49 = (var_B44 ^ var_B3C) | (var_B40 ^ var_B38) распакуем верный serial из var_B44 и var_B40
вместо MessageBox("Invalid serial") выполним SetDlgItemText - запишем верный serial в поле ввода.
Внедрим процедуру распаковки в конец секции .text - там нашлось свободное место. Писать будем по смещению в файле 00034850.
Необходимо отсчитать по меньшей мере 10h байт от последнего ненулевого символа, оставляя этот участок нетронутым (в конце некоторых структур присутствует до 10h нулей, искажение которых ни к чему хорошему не приведет).
https://samag.ru/archive/article/315
Код процедуры для распаковки верного serial:
#include <stdio.h>
typedef unsigned int uint;
void unpack_value(uint value, char* dest) {
for (int i = 0; i < 4; i++) {
char a = (value & 0xF0) >> 4;
a = (a <= 9) ? a + '0' : a - 0xA + 'A';
*dest++ = a;
unsigned char b = value & 0x0F;
b = (b <= 9) ? b + '0' : b - 0xA + 'A';
*dest++ = b;
value >>= 8;
}
}
char* unpack_serial(uint part1, uint part2, char* dest) {
dest[16] = '\0';
unpack_value(part1, dest);
unpack_value(part2, dest + 8);
return dest;
}
Правил код в отладчике и hex-редакторе: для страховки от ошибок написал автоматический patcher и выполнил такой патч .
Программа не импортирует SetDlgItemText, поэтому верный serial передаю MessageBox.
Заменю надпись Check на Generate serial и скрою поле ввода serial в редакторе ресурсов. Теперь это keygen.
Делаем выводы
Защита трудна там, где требует аккуратно распутать вычисление var_B49. Обфускация и оптимизация кода мешают дизассемблировать программу, но отладчик поможет выяснить, что и как выполняется. Недостаточно запутать код: противодействуйте отладке.
Объем кода не мешает сломать защиту, если программа не следит за целостностью кода. Пусть процедура занимает хоть 100 Мб - изучим входные и выходные параметры и подчиним себе код.