Сегодня я расскажу тебе, как следует бороться с донглами. Донгл – это такая штука, которая вставляется в LPT- или USB-порт и мешает честному пользователю пользоваться программой
:). Донглы производят много компаний, например, Rainbow Technologies, Aladdin,… Соответственно, донглы тоже бывают разные: Sentinel
PRO/Super PRO, HASP/HARDLOCK. Как видно из названия, нам предстоит побороть прогу, использующую в качестве защиты донгл Sentinel Super PRO производства компании Rainbow Technologies.
Crackme особенно удобен тем, что, исследуя его, мы не нарушаем лицензионное соглашение
:). Для начала следует узнать, что, собственно, представляет собой этот донгл, в этом нам поможет старый дядька Интернет. Также необходимо скачать драйвер с сайта
www.rainbow.msk.ru.

Super PRO имеет 128 байт памяти, которая делится на 64 DWORD-а (cells). Ячейки нумеруются от 0x0 до 0x3F, первые 8 ячеек
не перезаписываемые. Назначение ячеек показано в таблице.

Номер ячейки Описание Доступ
0x0 Серийный номер, уникальный для каждого ключа Чтение
0x1 Developer ID — идентификационный номер разработчика Чтение
0x2 Пароль перезаписи, 1-е слово
0x3 Пароль перезаписи, 2-е слово
0x4 Пароль записи
0x5 Зарезервировано
0x6 Зарезервировано
0x7 Зарезервировано
0x8-0x3F Пользовательские ячейки чтения/записи Чтение/Запись

Каждая пользовательская ячейка имеет свой тип доступа:

Тип доступа
0 Ячейка чтения/записи
1 Ячейка только чтения
2 Ячейка-счетчик, изменяется только в сторону уменьшения
3 Ячейка-алгоритм

После вызова API-функции она возвращает код ошибки, всегда в регистре EAX. Нас будут интересовать следующие коды ошибок:

Код возврата Описание
0 Успешное выполнение операции
3 Донгл не найден
12 Драйвер не инсталлирован

На этом разведку закончим и приступим непосредственно к исследованию crackme. Загружаем экзешник в IDA и ждем, пока закончится дизассемблирование, параллельно инсталлируя драйвер донгла. Когда анализ закончится, чтоб не нажить себе геморрой, лучше всего наложить сигнатуру Sentinel SuperPro (v6.0 lib) by CyberHeg.

В первых строках листинга видим, что crackme выводит в консоль информацию о себе, нам это не интересно. Интерес начинается ровно с адреса 0x004010C9 и, если ты наложил сигнатуры, то увидишь следующее:

.text:004010C9 push 404h
.text:004010CE push offset unk_410EB8
.text:004010D3 call sproFormatPacket ; _RNBOsproFormatPacket@8
.text:004010D8 push eax
.text:004010D9 mov word_4112BC, ax
.text:004010DF call sub_401230

Как видно из названия функции, она формирует пакет данных для общения с донглом. Сам пакет находится по адресу 0x410EB8. Процедура sub_401230 проверяет код возврата и, если он не равен 0, выводит соответствующее предупреждение. Идем дальше.

.text:004010E7 push offset unk_410EB8
.text:004010EC call sproInitialize ; _RNBOsproInitialize@4
.text:004010F1 push eax
.text:004010F2 mov word_4112BC, ax
.text:004010F8 call sub_401230

Происходит инициализация ключа и проверка кода возврата. Если запустить crackme, предварительно не установив драйвер, то увидим сообщение,
которое в переводе на человеческий язык означает, что драйвер не инсталлирован.

Продолжаем.

.text:00401100 call sub_401610
….
sub_401610 proc near
.text:00401610 mov ax, word_40E030
.text:00401616 push eax
.text:00401617 push offset unk_410EB8
.text:0040161C call sproFindFirstUnit ; _RNBOsproFindFirstUnit@8
.text:00401621 and eax, 0FFFFh
.text:00401626 mov dword_410E54, eax
.text:0040162B retn
.text:0040162B sub_401610 endp

Смотрим, что такое sproFindFirstUnit:

Эта функция ищет первый SuperPro-ключ с указанным developer
ID.

FUNCTION RNBOsproFindFirstUnit( ApiPacket : RB_SPRO_APIPACKET_PTR; devleoperID :WORD) : WORD

где:

ApiPacket — указатель на запись RB_SPRO_APIPACKET.
developerID – идентификационный номер производителя.

Мы уже знаем, что в unk_410EB8 лежит указатель на пакет данных, значит, в word_40E030 лежит DeveloperID и он равен 0x5684, а в dword_410E54 записывается код возврата, который будет проверяться дальше.

.text:00401105 mov ax, word_40E036
.text:0040110B push offset unk_40E040
.text:00401110 push eax
.text:00401111 push offset unk_410EB8
.text:00401116 call sproRead ; _RNBOsproRead@12
.text:0040111B and eax, 0FFFFh
.text:00401120 mov dword_410E60, eax

Смотрим описание:

Функция читает значение ячейки с указанным адресом из ключа

FUNCTION RNBOsproRead( ApiPacket : RB_SPRO_APIPACKET_PTR;
address : WORD;
data : POINTER ) : WORD

Делаем вывод: word_40E036 содержит номер читаемой ячейки (0х3F), а unk_40E040 – указатель на адрес, куда будет записано значение прочитанной ячейки. В dword_410E60 – код возврата.

Следующим идет вызов процедуры sub_401390

.text:00401390 sub_401390 proc near
.text:00401390 mov ax, word_40E03A
.text:00401396 push offset word_40E042
.text:0040139B push eax
.text:0040139C push offset unk_410EB8
.text:004013A1 call sproRead ; _RNBOsproRead@12
.text:004013A6 mov cx, word_40E03C
.text:004013AD push offset dword_40E044
.text:004013B2 and eax, 0FFFFh
.text:004013B7 push ecx
.text:004013B8 push offset unk_410EB8
.text:004013BD mov dword_410E64, eax
.text:004013C2 call sproRead ; _RNBOsproRead@12
.text:004013C7 mov dx, word_40E03E
.text:004013CE push offset dword_4112C0
.text:004013D3 and eax, 0FFFFh
.text:004013D8 push edx
.text:004013D9 push offset unk_410EB8
.text:004013DE mov dword_410E68, eax
.text:004013E3 call sproRead ; _RNBOsproRead@12
.text:004013E8 and eax, 0FFFFh
.text:004013ED mov dword_410E6C, eax
.text:004013F2 retn
.text:004013F2 sub_401390 endp

Тут все понятно: читаются ячейки 0х12, 0х23, 0х2A, записываются в word_40E042, word_40E044, word_4112С0, а коды ошибок заносятся в dword_410E64, dword_410E68, dword_410E6C.

.text:0040112A mov eax, dword_410E54
.text:0040112F xor esi, esi
.text:00401131 cmp eax, esi
.text:00401133 jnz short loc_40113D
.text:00401135 cmp dword_410E60, esi
.text:0040113B jz short loc_401145
.text:0040113D mov dword_410E0C, esi
.text:00401143 jmp short loc_401161

Проверяются коды ошибок после sproFindFirstUnit и чтения ячейки 0x3F, если они не равны 0, происходит переход на 0x0040113D, где записывается 0 в флаг наличия донгла dword_410E0C. В конце программы, если dword_410E0C равен 0, ты увидишь сообщение: Dongle not found. Идем дальше.

.text:00401145 mov cx, word_40E038
.text:0040114C push ecx
.text:0040114D push 0F649h
.text:00401152 push 6
.text:00401154 push offset unk_40E040
.text:00401159 call sub_401280

В этой процедуре происходит чтение серийного номера донгла (ячейка 0x1), сравнение его с 0х49B7 и, если они равны, установка флага по адресу 0x00410E08 в 1. Также отсюда можно понять, что значение ячейки 0х3F должно быть равным 0х4001.

.text:00401161 mov edx, dword_4112C0
.text:00401167 cmp dx, 2335h
.text:0040116C jb short loc_401175
.text:0040116E cmp dx, 24DFh
.text:00401173 jbe short loc_40117B
.text:00401175 mov dword_410E10, esi

Проверяется значение ячейки 0х2A на соответствие диапазону (0х2335, 0х24DF], в случае несоответствия флаг dword_410E10 устанавливается в 0. На участке кода 0х0040117B – 0x00401209, с помощью несложных математических операций, можно установить, что значения ячеек 0х12, 0х23, 0х2A могут быть равны соответственно 0х854A, 0x0030, 0x24DF.

.text:00401209 mov esi, 0Bh
.text:0040120E push 19h
.text:00401210 call sub_401360
.text:00401215 push eax
.text:00401216 call sub_401300
.text:0040121B add esp, 8
.text:0040121E dec esi
.text:0040121F jnz short loc_40120E

Функция sub_401360 занимается генерированием случайного числа по значению таймера, которое потом используется в функции sub_401300 и все это
происходит 0x0B раз :). Заглянем в sub_401300.

.text:00401300 sub esp, 8
.text:00401303 lea eax, [esp+8+var_8]
.text:00401307 push esi
.text:00401308 mov esi, [esp+0Ch+arg_0]
.text:0040130C shl esi, 4
.text:0040130F push edi
.text:00401310 push 8
.text:00401312 push 0
.text:00401314 lea ecx, dword_40E048[esi]
.text:0040131A push eax
.text:0040131B push ecx
.text:0040131C push 8
.text:0040131E push offset ApiPacket
.text:00401323 call _RNBOsproQuery@24 ; RNBOsproQuery(x,x,x,x,x,x)
.text:00401328 and eax, 0FFFFh
.text:0040132D mov ecx, 2
.text:00401332 lea edi, [esp+10h+var_8]
.text:00401336 lea esi, dword_40E050[esi]
.text:0040133C xor edx, edx
.text:0040133E mov dword_410E74, eax
.text:00401343 repe cmpsd
.text:00401345 mov eax, edx
.text:00401347 pop edi
.text:00401348 setz al
.text:0040134B mov dword_410E70, eax
.text:00401350 pop esi
.text:00401351 add esp, 8
.text:00401354 retn

Видим новую API RNBOsproQuery. Читаем:

Эта функция использует активный алгоритм в указанной ячейке ключа address. Указатель на queryData указывает на первый байт данных, передаваемый ключу. Длина указана в переменной length. В случае успешного выполнения функции результат будет находиться по адресу, указанному в переменной response, а последние 4 байта результата копируются в response32.

FUNCTION RNBOsproQuery( ApiPacket : RB_SPRO_APIPACKET_PTR; address : WORD; queryData : POINTER;
response : POINTER; response32 : POINTER; length : WORD ) : WORD

Именно эта функция является проблемой для создания эмулятора, т.к. неизвестно, по какому алгоритму происходит изменение входных данных. Хотя в сети ходят слухи, что кому-то все же удалось узнать и это.

Из кода видно, что номер ячейки, используемой в качестве алгоритма, равен 8, длина данных тоже равна 8. В качестве данных для преобразования используется набор байт из таблицы 0x0040E050, выбираемый на основе случайно сгенерированного числа. Тут же проверяется правильность результата функции: ответ функции должен быть равен 8 байтам из той же таблицы, что и входные данные, но со сдвигом на 0х8 от них! Результат функции помещается в dword_410E74, а результат проверки – в dword_410E70.

Продолжаем исследование.

.text:00401510 push esi
.text:00401511 mov esi, ds:Sleep
.text:00401517 push edi
.text:00401518 xor edi, edi
.text:0040151A push 0FFFFh
.text:0040151F call sub_401360
.text:00401524 add esp, 4
.text:00401527 mov word_4112BE, ax
.text:0040152D test ax, ax
.text:00401530 jz short loc_40151A
.text:00401532 mov cx, word_40E032
.text:00401539 push 0
.text:0040153B mov word ptr word_4112С2, ax
.text:00401541 push eax
.text:00401542 mov ax, word_40E034
.text:00401548 push eax
.text:00401549 push ecx
.text:0040154A push offset ApiPacket
.text:0040154F call _RNBOsproWrite@20 ; RNBOsproWrite(x,x,x,x,x)
.text:00401554 and eax, 0FFFFh
.text:00401559 push 14h ; dwMilliseconds
.text:0040155B mov dword_410E7C, eax
.text:00401560 call esi ; Sleep
.text:00401562 call sub_4015E0
.text:00401567 push 14h ; dwMilliseconds
.text:00401569 call esi ; Sleep
.text:0040156B mov dx, word_40E034
.text:00401572 push 4112C2h
.text:00401577 push edx
.text:00401578 push offset ApiPacket
.text:0040157D mov word ptr word_4112С2, 0
.text:00401586 call _RNBOsproRead@12 ; RNBOsproRead(x,x,x)
.text:0040158B and eax, 0FFFFh
.text:00401590 mov dword_410E78, eax
.text:00401595 call sub_4015B0
.text:0040159A inc edi
.text:0040159B cmp edi, 4
.text:0040159E jl loc_40151A
.text:004015A4 pop edi
.text:004015A5 pop esi
.text:004015A6 retn

Суть этого кода вот в чем: опять генерируется случайное число, записывается в ячейку 0х15 с паролем записи 0х73BA. В процедуре sub_4015E0 нам встречается API RNBOsproDecrement, которая просто уменьшает значение заданной ячейки на 1, в нашем случае это ячейка 0х15. Дальше происходит чтение этой же ячейки и проверяется, произошло ли уменьшение. Если все нормально, то в dword_410E80 запишется 1. Все это происходит в цикле, выполняющемся 4 раза, каждый раз — с разным случайным числом.

Ну а на следующем участке кода происходит проверка всех флагов, о которых я говорил выше и по результатам выводятся сообщения о присутствии
донгла и о доступности фич.

Итак, подведем итог всему, что мы узнали, сделав это в виде дампа памяти ключа. Вот что мы имеем:

Memory dw 00h,00h,0a07h,3021h,00h,2f2ch,00h,00h ;8..3F word
dw 00h,00h,854ah,00h,00h,00h,00h,00h
dw 00h,00h,00h,00h,00h,00h,00h,00h
dw 00h,00h,00h,030h,00h,00h,00h,00h
dw 00h,00h,24dfh,00h,0024h,00h,00h,00h
dw 00h,00h,00h,00h,0001h,00h,00h,00h
dw 00h,00h,2001h,00h,0001h,00h,00h,4001h
SerialNumber dw 49b7h

Плюс к этому результат RNBOsproQuery должен быть равен 8 байтам таблицы, сдвинутым на 8 от передаваемых ключу байт.

Дальше есть 3 пути:

  1. Просто пропатчить функции RNBOsproQuery, RNBOsproRead, RNBOsproWrite, RNBOsproDecrement, чтоб они работали с как бы «виртуальной» памятью ключа, которую можно создать прямо в экзешнике;
  2. Хучить DeviceIOControl, с помощью которой прога общается с драйвером и подставлять нужные ответы;
  3. Написать замену сентинеловского драйвера (в свое время, когда я впервые увидел этот crackme, я выбрал этот способ, правда, дровина писалась для популярной в то время Windows 98).

Выбрать путь, которым ты пойдешь, и реализовать его – это и будет твоим домашним заданием
:). Только сразу хочу сказать, что, если ты выберешь 2-й или 3-й способ, тебе придется гораздо больше поработать, нежели с первым способом. Это связано с тем, что пакеты, передаваемые в драйвер, шифруются случайным числом по определенному алгоритму, который тебе тоже придется ковырять самому. Нужно учитывать еще и то, что существует несколько способов расшифровки пакетов, отличающихся от версии к версии. Хотя в паблике уже появился исходник «Sentinel original packet decryption routines by MeteO», можешь заюзать его. На этом можно закончить.

В результате исследования crackme ты убедился, что донглы – это не так уж и страшно, просто к ним нужен определенный подход. Проблема для создателей программ и радость для нас заключается в том, что чаще всего программист не использует на полную возможности ключа, ограничившись несколькими вызовами sproFindFirstUnit да sproRead. Грамотно составленная защита с использованием sproQuery будет являться серьезной проблемой для реверсера, но, к счастью, таких защит мало и в большинстве случаев достаточно подправить вызовы нескольких API,
чтобы они всегда возвращали 0.

Теперь ты можешь взяться за исследование других донглов, например, Hasp или Hardlock, по ним тоже много информации в Интернете. Удачи в таком нелегком деле, как Reverse
Ingeneering!

Crack ME и
дополнительные материалы

Оставить мнение

Check Also

Как сделать игру. Выбираем движок и пишем клон тех самых «танчиков»

С каждым днем игры становятся все сложнее и навороченнее. Быть инди, а точнее соло-разрабо…