Pull to refresh

Исследуем защиту программ на практике

Level of difficultyMedium
Reading time4 min
Views4.4K

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

Tags:
Hubs:
Total votes 3: ↑2 and ↓1+1
Comments26

Articles