Некоторое время назад привезли из Москвы диск с базой данных о машинах Mercedes. Но незадача, что бы ее поставить нужно знать код ответа.
Пример экрана установки.
Как быть, базу хочется, а денег на ключ нет. Решено было посмотреть, что там за защита.
Посмотрели оказалось, что интерпретатор выполняет команды записанные в файле настроек. Начали с простейшего дизасемблировали инсталяшку. Два часа шел процесс на P166. Затем начали искать. И нашли функцию, которая переваривает введенное число и выдает правильный ключ или нет. Раз нашли значит надо патчить и запатчили - любое число стало правильным.
Поставили, запустили - ругается, говорит мол ключ неверный. Хорошо стали смотреть дальше уже основную программу. Опять длительный процесс дизасемблирования - ищем, где же она проверяет правильность. Нашли опять запачили. Запустили, показывает, но не все. Где проблема ?:.
Решили заняться детальным анализом кода проверки. Что же выяснилось. Ключ проверяется примерно таким кодом:
void getdatafromkey()
{
int i,i1;
expandkey();
// тут происходит превращение введенного ключа в 28 байтную
// последовательность
for(i=0;i<28;i++)
key1[i]=key1[i]^0xAA;
key1[28]=key1[28]^0x2A;
// Ну это тривиальный способ спрятать данные.
copy();
// А вот тут происходит перемешивание. Создалось впечатление, что кто-то начитался книжек по основам криптографии и пытался, чего-то наваять.
i=checkkey();
i1=(key1[33]<<8)+key1[32];
// это контрольная сумма ключа (попытки реализовать CRC ?)
if (i==i1) printf("Check OK! \n");
else printf("Check Error! (%04X/%04X) \n",i,i1);
// Вот таки образом проверялся ключ в инсталяшке.
// Этого кода уже не было в инсталяционной программе.
copy2data();
printf("App ID: %04X \n", (data[1]<<8)+data[0]); // Тут должно быть 1 или 3
printf("Type : %04X \n", (data[0x15]<<8)+data[0x14]); // тут разрешенные языки
printf("Count : %04X \n", (data[0x19]<<8)+data[0x18]);
printf("Left : %04X \n", (data[0x19]<<8)+data[0x18]);
// эти два поля в проге одинаковые, но менеджер лицензий понимает их по-разному. Общее количество лицензий и оставшееся количество лицензий.
printf("Exp.D.: %04X \n", (data[0x25]<<8)+data[0x24]);
// ну а это вообще прозрачно - дата окончания работы, количество дней от 1970 года.
printf("Lan ID: 0800 %04X %04X \n",(data[0x21]<<8)+data[0x20],(data[0x1D]<<8)+data[0x1C]);
// А вот это самое интересное - вот это и есть то самое число, которое показывается как LanID в стартовом окне. А точнее его младшие 8 цифр. Так как 4 старших, постоянны.
}
Далее, раз алгоритм известен. Начинаем детально разбираться в исходных текстах каждой процедуры.
Смотрим, что делается expandkey();
void expandkey()
{
int i,i1;
int ch;
int len=0;
memset(arr,'0',232);
for(i=0;i<46;i++)
{
ch=code(key[i]);
// тут происходит обычная подстановка по статической таблице.
if (ch==-1) ch=0;
to5(ch);
for(i1=0;i1<5;i1++,len++)
arr[i*5+i1]=digi5[i1];
}
for(i=0;i<36;i++)
key1[i]=from8(&arr[i*8]);
}
Процедура to5 представляет число в пяти разрядный двоичный код. А процедура from8 наоборот из текстового представления преобразует в 8 разрядный двоичный код. Этим они уменьшают избыточность. К слову похожий алгоритм используется в популярных программах uuencode.
Процедура copy();
void copy()
{
int i,i1;
char tkey[37];
memset(tkey,0,36);
for(i=0;i<11;i++)
memcpy(&tkey[dst[i]],&key1[src[i]],src[i+1]-src[i]);
memset(key1,0,36);
memcpy(&key1[4],&tkey[4],4);
memcpy(&key1[12],&tkey[12],4);
memcpy(&key1[20],&tkey[20],4);
memcpy(&key1[28],&tkey[28],4);
key1[0]=tkey[0];
key1[1]=tkey[1];
key1[8]=tkey[9];
key1[9]=tkey[8];
key1[16]=tkey[17];
key1[17]=tkey[16];
key1[24]=tkey[25];
key1[25]=tkey[24];
key1[26]=tkey[27];
key1[27]=tkey[26];
key1[32]=tkey[33];
key1[33]=tkey[32];
}
Тут они делают перемешивание. Сходу становится видно, что всего 12 байт из 36 важны для ключа, а остальные представляют собой обычный мусор.
Далее они проверяют правильность полученных данных.
int checkkey()
{
int i;
int t=0;
unsigned int t1;
for(i=0;i<(30*8);i++)
{
t1=1<<(i%8);
if ((key1[i>>3]&t1)!=0) t++;
}
return(t);
}
Легко понять, что это не самый оптимальный алгоритм проверки. Так как возможно много вариантов изменения содержимого, при неизменности контрольной суммы.
В вот теперь начинается самое интересное. Начали все это обращать. Результатом реверсного программирования явилась программка, которая может по заданным параметрам генерирует правильный ключ. Программку вы можете взять у нас на сайте здесь, а полный исходный текст со всякими разными извратами - здесь.
Теперь рассмотрим основные ошибки защитников J
- И самый главный не было сделано, ни одной попытки как либо спрятать код программы, защитив, хотя бы от дизасемблирования, не говоря уже о том, что бы защититься от отладки.
- Создалась впечатление, что использовался стандартный модуль лицензирования, что-то типа FLEX. Это позволит создавать валидные ключики и на следующие версии программы.
- В блоке лицензирования использовались примитивные алгоритмы, допускающие обращение. Сейчас перспективными являются системы с открытым ключом длиной хотя бы 64-128 бита.
- Слабая проверка целостности ключа.
Список можно продолжать. Но если у вас появилось желание защитить свою программу надежно, лучше не использовать модуль лицензирования использованный в этой программе.
HIT! Они даже не удалили из модуля лицензирования, отладочную информацию, что позволило легче идентифицировать, что каждая процедура делает.