Два года назад взялся писать программу, что помогает выставлять счета за аренду ковров. Прежде бухгалтер держал текст договоров в 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 = 0

var_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 Мб - изучим входные и выходные параметры и подчиним себе код.