Оригинальный DVD-ROM: eXeL@B DVD !
eXeL@B ВИДЕОКУРС !

ВИДЕОКУРС ВЗЛОМ
выпущен 12 ноября!


УЗНАТЬ БОЛЬШЕ >>
Домой | Статьи | RAR-cтатьи | FAQ | Форум | Скачать | Видеокурс
Новичку | Ссылки | Программирование | Интервью | Архив | Связь

Реверсинг и кейгенинг pl sql developer в прямом эфире

Обсудить статью на форуме

Очень удобно, когда все крэкерские инструменты, книги и статьи в одном месте. Используйте сборник от EXELAB - вот тут.

Автор: Ra$cal <nez@chem.com>

ВНИМАНИЕ: ВСЕ, ЧТО НАПИСАНО НИЖЕ, НАПИСАНО ТОЛЬКО В ОБРАЗОВАТЕЛЬНЫХ ЦЕЛЯХ. ЕСЛИ ВЫ ИСПОЛЬЗУЕТЕ ЭТИ ДАННЫЕ С КОРЫСТНОЙ ЦЕЛЬЮ(ОБХОД ЗАЩИТЫ, ИСПОЛЬЗОВАНИЕ СГЕНЕРИРОВАННОГО КЛЮЧА, ПОЛУЧЕННОГО НЕ ОТ ПРОИЗВОДИТЕЛЯ), ВЫ БЕРЕТЕ ВСЮ ОТВЕТСТВЕННОСТЬ ПЕРЕД ЗАКОНОМ НА СЕБЯ.

Вступление:
Понадобилась по учебе софтина для работы с oracle db. SQLDeveloper не устроил, т.к. в нем одно поле запроса, и держать из-за этого файл с мелкими скриптами и копипастить быстро надоело. На форуме вспомнилась тема не помню кого, про поиск кряка к этой софтине. Тема правила нарушала, но название программки запомнил. Скачал, поюзал - понравилась. Плюсов много, за всеми ими смотрите сайт производителя. Мне пригодилось - множественные окна запросов, отладка pl/sql программ, ну и по мелочам типа построения диаграммы из базы итп. Вобщем программа полезная и удобная. Но из-за разгара учебы трогать ее не стал - только поправил триал патчем. Ну и в сессию одолело желание покопаца глубже.
Писал статью так сказать в прямом эфире - по ходу прогресса дописывал то, что узнавал. Старое не правил. В итоге статья по сути сожержит процесс понимания мной защиты. С недочетами, ошибками, заблуждениями. Начал писать естсественно до удачного взлома, так что если бы кейген не получился - статья была бы не закончена. Про размер статьи - написал ровно столько, сколько тербовалось для понимания защиты, так же извиняйте за объемность.
Ну и последнее - если вы работаете с oracle db и используете pl sql developer, то не пожалейте денег и купите лицензию - она того стоит.

Процесс:
Начнем.
Инструментарий ничем необычным не выделяется:
OllyDbg, IDA, Microsoft Visual Studio.
Исследуемая программа:
PL/SQL Developer v7.1.5.1397
Потребуется предварительно распаковать. Чем упакована честно говоря уже забыл - распаковал давно, чтоб снять триал.

Запускаем plsqldeveloper и открываем диалог регистрации. Будем использовать следующие регистрационные данные:
product code - rascal
serial number - QWERTYUI
password - password


Поймать процесс проверки легко через поиск строки о неудачной регистрации:


00B27385  |.  MOV EDX, plsqldev.00B27544 ;  UNICODE "Registration failed!
Please make sure you enter all details correct
and that your license is up to"


Чуть выше видим метку, что сюда ведет прыжок


00B27379  |> 83FE 01               CMP ESI, 1
Jump from 00B2722F

вот где начинается проверка


00B2721D  |.  CALL plsqldev.00AFC568
00B27222  |.  MOV ESI, EAX
00B27224  |.  TEST ESI, ESI
00B27226  |.  JE plsqldev.00B273BB
00B2722C  |.  CMP ESI, 3
00B2722F  |.  JNZ plsqldev.00B27379
00B27235  |.  LEA EAX, DWORD PTR SS:[EBP-C]
00B27238  |.  CALL plsqldev.005F53D4
00B2723D  |.  LEA ECX, DWORD PTR SS:[EBP-10]
00B27240  |.  MOV EDX, plsqldev.00B27444   ;  UNICODE "Registration successful"

Как видим не прошла проверка cmp esi, 3. В esi кладется регистр eax, а он очевидно возвращается из функции. Запомним, что функция должна вернуть 3, чтобы регистрация удалась.

Внутри call’a


00AFC5D2  |.  CALL NEAR DWORD PTR DS:[EDX+FC]   ;  plsqldev.004D8284

Это показ окна регистрации. Дальше получение из него данных.


00AFC65A  |.  MOV EAX, DWORD PTR DS:[]
00AFC65F  |.  CALL 
00AFC664  |.  TEST AL, AL
00AFC666  |.  JE SHORT plsqldev.00AFC6A0
.
.
.
00AFC67E  |.  CALL   MOV EBX, 1
00AFC693  |.  MOV EAX, DWORD PTR DS:[BA9208]
00AFC698  |.  CMP BYTE PTR DS:[EAX], 0
00AFC69B  |.  JE SHORT plsqldev.00AFC6A5
00AFC69D  |.  INC EBX
00AFC69E  |.  JMP SHORT plsqldev.00AFC6A5
00AFC6A0  |>  MOV EBX, 1

Что в итоге. Функция check_license_data должна вернуть не ноль. В самой функции сначала идут проверки наличия стркои с product_code, далее видимо попытка загрузки этих данных в случае срабатывания этой функции при запуске программы, а не при вводе данных в диалог. Далее начинается самое веселое.


005F502E  |>  MOV EAX, ESI
005F5030  |.  CALL 

В esi лежит указатель на структуру регистрации. Создается она тут:


005F4FEA  |.  MOV EAX, DWORD PTR DS:[5F349C]
005F4FEF  |.  CALL plsqldev.00404EEC

В 5F349C лежит адрес, указывающий на константу 0x0E. Суть ее установить не удалось.

Далее идет сохранение product_code


005F5016  |> LEA EAX, DWORD PTR DS:[ESI+58]
005F5019  |.  MOV EDX, DWORD PTR SS:[EBP-4]
005F501C  |.  CALL plsqldev.00405FB4

Как повились остальные поля увидим по ходу дела (added - потом мне надоело структурировать, ниже будет список полей со смещениями относительно начала структуры REG_INFO).

Поля структуры:

struct REG_INFO{
	DWORD* unused1; 			// та самая константа после кола попала в перое же поле
	char* product_code_bin_str; // 0x04 - product_code, преобразованная в набор '1' и '0'
	DWORD unused2 [0x02];
	DWORD sums [0x0A];			// 0x10
	DWORD unused3 [0x20];
	char* product_code; 		// 0x58
};

Мелких пакостей в программе немного - просто после трех неудачных регистраций лочит менюшку Register. Делается это тут:


00B276D5  |.  CMP DWORD PTR DS:[BA80DC], 3
00B276DC  |.  SETL DL
00B276DF  |.  MOV EAX, DWORD PTR DS:[EBX+D00]
00B276E5  |.  CALL plsqldev.004E4D2C

Само увеличение тут:


00B273AF  |>  INC DWORD PTR DS:[BA80DC]

Нопим и продолжаем...

.:Исследование формата ключа:.

Заходим внутрь проверки 005F3A3C. Для удобства вот список локальных переменных и их имена. pc - сокращение для product_code. остальное должно быть понятно. Так проще будет ориентироваться в коде.

.text:005F3A3C aplh_symbol_str 				= dword ptr -20h
.text:005F3A3C temp_str_for_pc_symbol		= dword ptr -1Ch
.text:005F3A3C product_code_symbol_str		= dword ptr -18h
.text:005F3A3C alphabet_loc    				= dword ptr -14h
.text:005F3A3C product_code_lenght			= dword ptr -10h
.text:005F3A3C product_symbol_index_in_alph	= dword ptr -0Ch
.text:005F3A3C i               				= dword ptr -8
.text:005F3A3C var_1           				= byte ptr -1

Важное место:


005F3A50  |.  MOV EBX, EAX

В ebx кладется указатель на REG_INFO. Запомним и продолжим.


005F3A6F  |.  TEST EAX, EAX
005F3A71  |.  JE SHORT plsqldev.005F3A78
005F3A73  |.  SUB EAX, 4
005F3A76  |.  MOV EAX, DWORD PTR DS:[EAX]
005F3A78  |>  TEST EAX, EAX
005F3A7A  |.  JLE plsqldev.005F3B63

Такие места будут встречаться довольно часто. Это типичная работа с vcl строками. По адресу str-4 лежит DWORD, в котором хранится длина стркои. А так как проверка строится на строках - нужно знать врага в лицо, чтобы не отвлекаться на такие мелочи.


005F3A80  |.  MOV DWORD PTR SS:[EBP-10], EAX                     ;  ebp-10 - product_code_lenght

И вот первый важный цикл:


005F3A80  |.  MOV DWORD PTR SS:[EBP-10], EAX                     ;  ebp-10 - product_code_lenght
005F3A83  |.  MOV DWORD PTR SS:[EBP-8], 1                        ;  i=1

005F3A8A ... 005F3AA9 проверки допустимых символов в product_code


005F3AAF  |.  |XOR EAX, EAX
005F3AB1  |.  |MOV DWORD PTR SS:[EBP-C], EAX
005F3AB4  |.  |MOV EAX, DWORD PTR DS:[]
005F3AB9  |.  |MOV DWORD PTR SS:[EBP-14], EAX

005F3ABC ... 005F3ACC проверки наличия алфавита


005F3AD3  |> /|/LEA EAX, DWORD PTR SS:[EBP-1C]
005F3AD6  |. |||MOV EDX, DWORD PTR DS:[EBX+58]                   ;  edx <- product_code
005F3AD9  |. |||MOV ECX, DWORD PTR SS:[EBP-8]                    ;  i
005F3ADC  |. |||MOVZX EDX, BYTE PTR DS:[EDX+ECX-1]               ;  dl <- product_code[i]
005F3AE1  |. |||CALL                         ;  ch_to_str
005F3AE6  |. |||MOV EAX, DWORD PTR SS:[EBP-1C]                   ;  temp_str_for_pc_symbol
005F3AE9  |. |||LEA EDX, DWORD PTR SS:[EBP-18]
005F3AEC  |. |||CALL                   ;  [edx] = eax
005F3AF1  |. |||MOV EAX, DWORD PTR SS:[EBP-18]                   ;  [ebp-18] - product_code_symbol_str
005F3AF4  |. |||PUSH EAX
005F3AF5  |. |||LEA EAX, DWORD PTR SS:[EBP-20]
005F3AF8  |. |||MOV EDX, DWORD PTR DS:[]               ;  plsqldev.005F3514
005F3AFE  |. |||MOVZX EDX, BYTE PTR DS:[EDX+ESI-1]               ;  dl <- alphabet[i]
005F3B03  |. |||CALL 
005F3B08  |. |||MOV EDX, DWORD PTR SS:[EBP-20]                   ;  [ebp-20] - aplh_symbol_str
005F3B0B  |. |||POP EAX                                          ;  product_code_symbol_str из стека
005F3B0C  |. |||CALL                    ;  сравнить символ из pc с символом из alphabet
005F3B11  |. |||JNZ SHORT plsqldev.005F3B16
005F3B13  |. |||MOV DWORD PTR SS:[EBP-C], ESI                    ;  [ebp-c] - индекс символа из product_code в alphabet
005F3B16  |> |||INC ESI
005F3B17  |. |||DEC EDI                                          ;  aplhabet_lenght
005F3B18  |.^|JNZ SHORT plsqldev.005F3AD3
005F3B1A  |>  |CMP DWORD PTR SS:[EBP-C], 0						 ;  если не нашли букву из product_code в алфавите
005F3B1E  |.  |JE   							 ;  то ошибка регистрации
005F3B24  |.  |DEC DWORD PTR SS:[EBP-C]
005F3B27  |.  |MOV EDI, 10                                       ;  edi = 0x10. в данноv цикле роль у edi - битовая маска
005F3B2C  |.  |MOV ESI, 5
005F3B31  |>  |/TEST DWORD PTR SS:[EBP-C], EDI                   ;  product_symbol_index_in_alph && bitmask
005F3B34  |.  ||JNZ SHORT plsqldev.005F3B45
005F3B36  |.  ||LEA EAX, DWORD PTR DS:[EBX+4]
005F3B39  |.  ||MOV EDX, 				 ;  если бит 0 - добавляем к строке product_code_bin_str '0'
005F3B3E  |.  ||CALL 
005F3B43  |.  ||JMP SHORT plsqldev.005F3B52
005F3B45  |>  ||LEA EAX, DWORD PTR DS:[EBX+4]
005F3B48  |.  ||MOV EDX, 				 ;  если есть бит 1 - добавляем к строке product_code_bin_str '1'
005F3B4D  |.  ||CALL 
005F3B52  |>  ||SHR EDI, 1                                       ;  edi = edi >> 2;
005F3B54  |.  ||DEC ESI
005F3B55  |.^ |JNZ SHORT plsqldev.005F3B31
005F3B57 >|>  |INC DWORD PTR SS:[EBP-8]
005F3B5A  |.  |DEC DWORD PTR SS:[EBP-10]
005F3B5D  |.^ JNZ plsqldev.005F3A8A

А вот и сам алфавит.

char alphabet [] = {"abc2def3ghjk4lmnp5qrs6tu7vw8xy9z"};

Итак, что мы имеем. Если в product_code встретится символ, которого нет в алфавите - будет ошибка регистрации. Это первое требование, которое надо выполнить. Далее идет преобразование строки в набор 0 и 1. Длина product_code не фиксирована. На выходе имеем строку из 0 и 1. Для rascal имеем следующее: 100110000010100000100000001101.
Теперь осталось понять, что логически делает этот код. По сути этот цикл - нечто вроде декодирования из системы счисления с другим основанием. Аналог base64, а точнее это base32. Из сокращенного алфавита получаем строку битов. Т.е. product code видимо содержит в себе какие то данные, закодированные таким способом. Это значит что надо копать дальше - разбирать, какие биты для чего используются. Биты наверняка должны быть преобразованы в человеческие символы, разделены например на слова, а дальше будет работа уже с обычными аски кодами. Так ли это, увидим дальше.
Я написал программку для выбора всех вариантов индексов, итого вот что получилось:


a - 00000       b - 00001       c - 00010       2 - 00011
d - 00100       e - 00101       f - 00110       3 - 00111
g - 01000       h - 01001       j - 01010       k - 01011
4 - 01100       l - 01101       m - 01110       n - 01111
p - 10000       5 - 10001       q - 10010       r - 10011
s - 10100       6 - 10101       t - 10110       u - 10111
7 - 11000       v - 11001       w - 11010       8 - 11011
x - 11100       y - 11101       9 - 11110       z - 11111


Это пригодится в дальнейшем. Считаем что это препятствие прошли, идем дальше


005F3B63  |> MOV EAX, EBX
005F3B65  |. CALL plsqldev.005F37CC

Список локальных переменных:

.text:005F37CC result_summ     			= dword ptr -14h
.text:005F37CC i               			= dword ptr -10h
.text:005F37CC result          			= byte ptr -9
.text:005F37CC pc_2_symbols    			= dword ptr -8
.text:005F37CC product_code_bin_str_11	= dword ptr -4

Интересное место тут:


005F3805  |.  LEA EAX, DWORD PTR SS:[EBP-4]     ;  [ebp-4] - двоичная строка от pc
005F3808  |.  MOV ECX,   ;  ASCII "11"
005F380D  |.  MOV EDX, DWORD PTR DS:[EBX+4]
005F3810  |.  CALL            ;  сложить 2 строки, результат в [eax] (т.е. к бин строке от pc добавляется 11)

product_code_bin_str дополняется парой символов "11"

И тут


005F3826  |.  MOV EAX, DWORD PTR DS:[EAX]
005F3828  |>  TEST AL, 1
005F382A  |.  JNZ 

Проверка наличия в длине младшего бита, т.е. проверка четности длины двоичной строки. Отсюда еще одно требование к данным - (pc_len * 5 + 2) должно быть четным числом.


005F3836  |.  SUB EAX, 4
005F3839  |.  MOV EAX, DWORD PTR DS:[EAX]
005F383B  |>  MOV EDI, EAX
005F383D  |.  SAR EDI, 1                                         ;  edi = edi / 2

В eax лежит указатель на product_code_bin_str_11. Далее видим получение длины строки и деление пополам. К чему бы это =)


005F384C  |.  MOV DWORD PTR SS:[EBP-10], 1                       ;  [ebp-10] - i

005F3853  |>  /LEA EAX, DWORD PTR SS:[EBP-8]                     ;  [ebp-8] - 2 символа из бинарной строки от product_code
005F3856  |.  |PUSH EAX
005F3857  |.  |MOV EAX, DWORD PTR SS:[EBP-10]					 ;  [ebp-10] - i
005F385A  |.  |DEC EAX
005F385B  |.  |MOV EDX, EAX
005F385D  |.  |ADD EDX, EDX
005F385F  |.  |INC EDX
005F3860  |.  |MOV ECX, 2
005F3865  |.  |MOV EAX, DWORD PTR SS:[EBP-4]
005F3868  |.  |CALL 

Опять vcl функция. ECX - длина строки, которую извлекаем.EDX - индекс в строке, откуда начинается вырезаемая подстрока. В стеке - адрес для получения указателя на извлеченную подстроку. EAX - product_code_bin_str_11. Теперь взглянем на алгоритм получения индекса:

index = (i - 1) * 2 + 1;

По сути это проход по массиву с шагом в 2 байта. i изначально = 1, поэтому для первой итерации значение выражения будет = 1. Т.е. индексация в строке идет не от 0, а от 1 =) Могло быть конечно пропускание первого символа, но наблюдение за кодом показало именно первый вариант.


005F386D  |.  |MOV ESI, 2
005F3872  |.  |MOV EAX, DWORD PTR SS:[EBP-8]
005F3875  |.  |MOV EDX,                   ;  ASCII "01"
005F387A  |.  |CALL 
005F387F  |.  |JNZ SHORT plsqldev.005F3886
005F3881  |.  |MOV ESI, 1
005F3886  |>  |MOV EAX, DWORD PTR SS:[EBP-8]
005F3889  |.  |MOV EDX,                   ;  ASCII "10"
005F388E  |.  |CALL 
005F3893  |.  |JNZ SHORT plsqldev.005F3897
005F3895  |.  |XOR ESI, ESI
005F3897  |>  |MOV EAX, DWORD PTR SS:[EBP-8]
005F389A  |.  |MOV EDX,                   ;  ASCII "00"
005F389F  |.  |CALL 
005F38A4  |.  |JNZ SHORT plsqldev.005F38AC
005F38A6  |.  |CMP DWORD PTR DS:[EBX+C], 0
005F38AA  |.  |JNZ SHORT                      ;  если sums_index != 0, то ошибка регистрации
005F38AC  |>  |CMP ESI, 2										 ;  проверка, если не сработал ни один из предыдущих прыжков. значит в буфере "11"
005F38AF  |.  |JGE SHORT plsqldev.005F38BB
005F38B1  |.  |MOV EAX, DWORD PTR SS:[EBP-14]
005F38B4  |.  |ADD EAX, EAX                                      ;  result_summ = result_summ * 2 + esi
005F38B6  |.  |ADD ESI, EAX
005F38B8  |.  |MOV DWORD PTR SS:[EBP-14], ESI
005F38BB  |>  |MOV EAX, DWORD PTR SS:[EBP-8]
005F38BE  |.  |MOV EDX,                   ;  ASCII "11"
005F38C3  |.  |CALL 
005F38C8  |.  |JNZ SHORT plsqldev.005F38E2
005F38CA  |.  |MOV EAX, DWORD PTR DS:[EBX+C]
005F38CD  |.  |MOV EDX, DWORD PTR SS:[EBP-14]                    ;  [ebp-14] - result_summ
005F38D0  |.  |MOV DWORD PTR DS:[EBX+EAX*4+10], EDX
005F38D4  |.  |INC DWORD PTR DS:[EBX+C]                          ;  увеличить sums_index
005F38D7  |.  |CMP DWORD PTR DS:[EBX+C], 9                       ;  если больше 9, то ошибка. регистрация завалена
005F38DB  |.  |JG SHORT 
005F38DD  |.  |XOR EAX, EAX
005F38DF  |.  |MOV DWORD PTR SS:[EBP-14], EAX                    ;  стереть result_summ
005F38E2  |>  |INC DWORD PTR SS:[EBP-10]                         ;  i++
005F38E5  |.  |DEC EDI                                           ;  str_bin_len--
005F38E6  |.^ JNZ 

Регистры используются следующим образом: ebx - указатель на REG_INFO, edi - счетчик пар символов в product_code_bin_str_11, esi - хранит ключ для функции вычисления result_summ.
Механизм работы кода следующий: читаем 2 символа из product_code_bin_str_11, проверяем значение. Если == "01", то в выражение расчета result_summ передаем 1, если == "10", то 0 и выражение просто удвоит текущее значение result_summ. Если == "00" и sums_index != 0, то ошибка регистрации, если == "11", то заносим result_summ в структуру REG_INFO по смещению + 0x10 + sums_index*4. Далее sums_index инкрементируется. Всего допустимо 10 ячеек для result_summ.
Что из этого можно понять - судя по всему "11" - символ разделитель. "00" - обрывает проверку после расчета первой суммы. Т.е. комбинация из "00" допустима только перед "11", после она запрещена. Это типа управляющие символы. Символы "01" и "10" отвечают за формирование result_summ. Каково должно быть значение, сколько таких сумм надо - пока не известно.
Для проверки управляющих кодов можно немного изменить строку, отдаваемую на проверку. Смотрим на таблицу соответствия битов символам алфавита. Нам нужно после 11 получить 00. p - 10000 подходит. вводим rascalp и вываливаемся на проверке четности длины строки. Чтобы удовлетворить проверку четности число символов в product_code должно быть четным, так как нечетное+2->нечетное. Добавляем еще p - rascalpp. Вываливаемся из регистрации. Это следующее важное условие про прохода регистрации. Теперь нада подумать, как избежать таких оплошностей. Для этого пригодится приведенная выше таблица. Казалось бы, все просто - смотрим, в каких символах алфавита нету подряд идущих 00. Не использовать их после 11 и все. Но это только один момент. Если мы посмотрим на соответствие алфавита бинарным строчкам, увидим, что длина бин строк - 5 символов. А это значит, что будут неприятные моменты стыка двух букв. Например:
j - 01010 - нету парных нулей
t - 10110 - нету парных нулей
Но tj -> 10 11 00 10 10. Получим ошибку. Значит нада учитывать пятерки не отдельно, а сгруппированными по двое. Вспоминая ограничение четности можно предположить, что единицей данных служат как раз таки пары символов. И вполне вероятно, что разделитель у кадой пары должен быть на конце. Т.е. коды должны заканчиваться на 11 (на 2, 3). В общем получится следующая картина - 2 символа по 5 бит, итого 10 бит. 8 бит значащих и 2 управляющих. Итого имеем закодированный байт. Но это пока только предположения. Для интереса меняем строку rascar:
10 01 01 10 00 00 10 10 10 11 - это первый блок. здесь можно использовать нули. теперь второй
01 01 10 10 10 10 10 01 01 01 - второй блок. 11 допишется автоматически
теперь преобразовываем в символы алфавита

10010 11000 00101 01011 -> q7ek
01011 01010 10100 10101 -> kjs6

Итого q7ekkjs6

Теперь топаем сюда:


005F3B6A  |.  MOV EDX, DWORD PTR DS:[EBX+10] ; кладем сумму от первого блока в edx
005F3B6D  |.  MOV EAX, EBX
005F3B6F  |.  CALL plsqldev.005F3950

005F3976  |> /8D45 FC               /LEA EAX, DWORD PTR SS:[EBP-4]               ;  этот цикл проходит по бинарной строке до первого набора 11
...
005F39A5  |.^7C CF                 JL SHORT plsqldev.005F3976

Вобщем типа проходим первый блок, просто чтобы его не покоцать в следующем цикле.


005F39BA  |. /JLE SHORT plsqldev.005F3A0B                  ;  этот цикл продолжает смотреть строку, отбросив первый блок
005F39BC  |> |/MOV EAX, DWORD PTR SS:[EBP-8]               ;  result_summ из первой ячейки
005F39BF  |. ||AND EAX, EDI                                ;  eax = summ & bit_mask
005F39C1  |. ||ADD EDI, EDI                                ;  edi = edi << 1
005F39C3  |. ||CMP EDI, 80
005F39C9  |. ||JNZ SHORT plsqldev.005F39D4
005F39CB  |. ||ADD DWORD PTR SS:[EBP-8], 3
005F39CF  |. ||MOV EDI, 1
005F39D4  |> ||TEST EAX, EAX
005F39D6  |. ||JNZ SHORT          ;  то инверсии бита не будет
005F39D8  |. ||MOV EAX, DWORD PTR DS:[ESI+4]               ;  eax <- product_code_bin_str
005F39DB  |. ||CMP BYTE PTR DS:[EAX+EBX-1], 30             ;  product_code_bin_str[i] == '0'
005F39E0  |. ||JNZ SHORT plsqldev.005F39F1
005F39E2  |. ||LEA EAX, DWORD PTR DS:[ESI+4]               ;  если 0 - заменяем на 1
005F39E5  |. ||CALL 
005F39EA  |. ||MOV BYTE PTR DS:[EAX+EBX-1], 31
005F39EF  |. ||JMP SHORT 
005F39F1  |> ||LEA EAX, DWORD PTR DS:[ESI+4]
005F39F4  |. ||CALL 
005F39F9  |. ||MOV BYTE PTR DS:[EAX+EBX-1], 30             ;  1 меняем на 0
005F39FE  |> ||INC EBX									   ;  do_not_inverse
005F39FF  |. ||MOV EAX, DWORD PTR DS:[ESI+4]
005F3A02  |. ||CALL 
005F3A07  |. ||CMP EBX, EAX
005F3A09  |.^|JL SHORT plsqldev.005F39BC

Замена единиц на нули и нулей в единицы. После этой функции опять вызовется расчет суммы и проверка парных нулей в оставшемся блоке. Какие проблемы нас будут ждать? Главная задача - после инвертирования не получить пары нолей. Так как мы формировали строку из этого же принципа, то чтобы получить пару нулей должно произойти инвертирование только одного бита из пары. Например было у нас 01, и проинвертировалась единица. Получили 00, и прощай регистрация. Но в коде вроде как инвертится все... Ан нет. Есть тут подлянка


005F39BF  |. ||AND EAX, EDI                                ;  eax = summ & bit_mask
005F39D4  |> ||TEST EAX, EAX
005F39D6  |. ||JNZ SHORT          ;  то инверсии бита не будет

Итак, как видим summ используется как маска инвертирования. Какие биты в summ = 1, такие биты будут инвертированы в product_code_bin_str, причем циклически. Т.е. чтобы не было досадной ошибки надо иметь в summ парные единицы и нули. Вспомним алгоритм получения summ:
summ = summ * 2 + esi;
На вскидку не очень понятно, но запишем немного подругому
summ = (summ << 1) + esi;
Так гораздо понятнее. На каждом шаге обработки product_code_bin_str берется пара символов, и если они = "01", то в esi будет 1, если "10", то в esi будет 0. Суммирование по сути есть установка младшего бита в 1. Т.е. просто дополнение summ единицей. Важно заметить, что бит в summ используется для инвертирования одного бита в product_code_bin_str, а получаем мы бит в summ из двух в product_code_bin_str. Это говорит о том, что спаренные биты должны быть одинаковыми. Например, у нас имеется последовательность
pcbs = {00 11 - первый блок, который не повлияет на значение summ} 01 10 01 10. summ = 1010. разбиваем pcbs на биты и получаем 0 1 1 0 0 1 1 0. маска инвертирования(развернутый summ) - 0 1 0 1

0 1 1 0 0 1 1 0 - pcbs
0 1 0 1 0 1 0 1 - mask
---------------
0 0 1 1 0 0 1 1

слияние в пары -> 00 11 00 11. как видим, у нас пары нулей. регистрация провалится. Чтобы обойти этот неприятный момент делаем пары в pcbs одинаковыми. 01 01 10 10 - pcbs. summ = 0011

0 1 0 1 1 0 1 0 - pcbs
1 1 0 0 1 1 0 0 - mask
---------------
1 0 0 1 0 1 1 0 

слияние в пары -> 10 01 01 10. Пар нулей нету. Вот вроде бы и достигли цели. Но и это еще не все.

Есть еще одно место:


005F39C3  |. ||CMP EDI, 80
005F39C9  |. ||JNZ SHORT plsqldev.005F39D4
005F39CB  |. ||ADD DWORD PTR SS:[EBP-8], 3
005F39CF  |. ||MOV EDI, 1

Когда edi достигнет значения 1000 0000, он будет сброшен в 0000 0001, и все бы ничего, но к summ добавится число 3. Чем это чревато. summ = 0110. В summ нарушена парность - 11 и 00.к чему это приведет


0 1 0 1 1 0 1 0 - pcbs
0 1 1 0 0 1 1 0
---------------
0 0 1 1 1 1 0 0

слияние в пары -> 00 11 11 00. Опять пары нулей и единиц. Зная маску мы могли бы подобрать правильный набор pcbs


0 1 1 0 0 1 1 0 - mask
1 1 1 1 1 0 1 1 - result
---------------
1 0 0 1 1 1 0 1

- pcbs. Маской для инверсии служит младший байт в summ. Summ - это сумма от первого блока. Поэтому мы можем получить значение маски заранее. И знать все ее изменения. Например:



Вернемся к нашим баранам. Наша задача - выйти из функции разбора битовой строки без ошибки и продолжить проверку регистрации. Я немного пошаманил с блокнотом и программой и получил следующие выражения:

00011 01010 10101 00011 - 2j62. summ = 01000000. Почему именно 01000000? Ответ ниже

Я построил таблицу преобразований битов с учетом маски, а так же с учетом приращения маски на 3 при достижении edi числа 0x40. В качестве начального значения маски взято число 01000000 (см. выше)


01 01 10 10 11 10 01 11 10 11 pcbs -> 01011 01011 10011 11011 -> kkr8
00 00 00 11 10 00 01 01 10 0 mask
10 10 01 10 10 01 11 01 11 01 result -> 10100 11010 01110 11101 -> swmy
         /          / 
          edi=1       edi=1
		  summ+=3     summ+=3
2j62kkr8

Загоняем эту строку и вуаля - мы не пошли на выход. Только считался бред, и отнюдь не 9 блоков данных.
Один напрягающий момент - символ конца блока "11" отнюдь не совпадает с положением в конце 8 битов. Ни в исходной строке, ни в инвертированной. И на самом деле невозможно подобрать так эти строки, чтобы 11 был там, где ему вроде как положено быть. Либо первая строка будет правильной, либо последняя, но не обе сразу. Что за гадость...
Но это навело меня на мысль...


005F3B63  |> MOV EAX, EBX
005F3B65  |. CALL 
005F3B6A  |. MOV EDX, DWORD PTR DS:[EBX+10]
005F3B6D  |. MOV EAX, EBX
005F3B6F  |. CALL 
005F3B74  |. MOV EAX, EBX
005F3B76  |. CALL 
005F3B7B  |. TEST AL, AL
005F3B7D  |. JE 

Я затачивал параметры под первую проверку. Но зачем? Её возвращаемое значение не проверяется. Тогда зачем она отрабатывает? Да она просто считает summ. А дальше строка инвертируется используя этот самый summ. В следующей проверке используется инвертированная строка и учитывается возвращенное значение. О чем это говорит? О том, что строка с данными ожидается инвертированной. Значит правильно отформатированной должна быть строка после инверта, а на вход может быть любого вида. Дописываем к списку требований ключа... Собственно с форматом ключа все:
1) Алфавит ключа ограничен alphabet
2) Ключ должен передаваться зашифрованным инвертированием через summ
3) summ высчитывается от первой части ключа
4) summ меняется в процессе расшифровки, поэтому шифровать надо итеративно
5) После инвертирования в ключе не должно быть "00" после первого блока
6) В ключе хранятся числа, получаемые по формуле y = y << 1 + x. Причем сдвига может не быть только в первом блоке, потому как сдвиг блокируется парой символов "00"
7) В ключе должно быть 8 блоков, включая первичный, разделенные "11"
. Почему 8? После прохождения проверки формата product_code и чтения из нее данных большой фрагмент кода читает 9 полей из структуруы REG_INFO. Так же в самой функции четко указан лимит - 9 ячеек для рассчитываемых сумм. Но 8 полей задать должны мы, птому как перед проверкой добавляется терминатор "11", добавляя тем самым еще один блок к счетчику. Если мы сделаем в ключе 9 блоков, то при проверке их окажется 10 и будет ошибка регистрации.

Теперь немного отвлечемся и озабитмся работой с числами. Нада понять принцип формирования пар, так как чтобы сделать себе ключ, придется писать генератор, а значит нам придется кодировать числа в формат девелопера. Еще раз напомню функцию
y = y ^ 2 + x;
На самом деле это просто перевод из строковой пременной в числовую. Из-за того, что числа хранятся парами, они немного непривычны взгляду, и путают моск. Посмотрим на это дело трезво. Для начала напомню правила работы с ними - 00 просто пропуск, 01 есть единица в x, 10 есть ноль в x, 11 есть признак конца блока. Нас интересуют только 01->1 и 10->0. Берем число, которое я закодировал.
2j62 -> 00 01 10 10 10 10 10 10 00 11
00 - просто для выравнивания. Теперь перепишем эту строку в соответствии с указаниями выше
00 01 10 10 10 10 10 10 00 11
- 1 0 0 0 0 0 0 - end
Вот и все. Если вы поотлаживаете, то увидите, что summ будет равно 0x40h, что и есть 1000000b. Теперь мы знаем, как кодировать человеческие числа в удлиненные.

Теперь к вопросу о функции 005F3B6F |. CALL . Как видите, назвал я ее decrypt_pc. Это функция преобразования product_code по ключу. Чтобы сделать генератор ключей намм нужно шифровать product_code таким алгоритмом, чтобы после decrypt_pc получался исходный код. Значит надо разобрать алгоритм дешифрования и обратить его. Хотя если посмотреть работу функции можно увидеть, что она симметричная, и нам достаточно рипнуть ее код. Получим вот такой код:


void CKeyGeneartor::CryptBitsString(CString& str_bits, UINT key)
{
	BYTE mask = CRYPT_MASK_START;
	UINT bits_len = str_bits.GetLength()-1;
	UINT cur_key = key;

	for(UINT i = 0; i < bits_len; i++){
		if ((cur_key & mask) == 0){
			if(str_bits.GetAt(i) == STR_1)
				str_bits.SetAt(i, STR_0);
			else
				str_bits.SetAt(i, STR_1);
		}

		mask <<= 1;
		if(mask == CRYPT_MASK_END){
			mask = CRYPT_MASK_START;
			cur_key += CRYPT_KEY_STEP;
		}
	}
}

Прообраз генератора написан. Он работает, правильно формирует строку из данных, правильно криптует данные, учитывается четность. При отладке данные преобразуются правильно, собсно этап разбора формата ключа завершен. Так же завершен этап создания программы, создающей ключи в этом формате. Ах да, product key сгенерировал следующий - sw6kkjhp6p6f3n3vev. Ключ = 64, поля соотв от 2 до 8. Поле 9 = 0, т.к. последовательно идут 2 терминатора. Это поможет идентифицировать поля при отладке. Продолжим.

.:Исследование проверки данных ключа:.

Проверку ключа проходим, и видим следующий фрагмент:


005F3B83  |.  MOV EAX, DWORD PTR DS:[EBX+10]               ;  key
005F3B86  |.  MOV DWORD PTR DS:[EBX+8], EAX
005F3B89  |.  MOV EAX, DWORD PTR DS:[EBX+14]               ;  v2
005F3B8C  |.  MOV DWORD PTR DS:[EBX+50], EAX
005F3B8F  |.  MOV EAX, DWORD PTR DS:[EBX+18]               ;  v3
005F3B92  |.  MOV DWORD PTR DS:[EBX+38], EAX
005F3B95  |.  MOV EAX, DWORD PTR DS:[EBX+1C]               ;  v4
005F3B98  |.  MOV DWORD PTR DS:[EBX+40], EAX
005F3B9B  |.  MOV EAX, DWORD PTR DS:[EBX+20]               ;  v5
005F3B9E  |.  MOV DWORD PTR DS:[EBX+3C], EAX
005F3BA1  |.  MOV EAX, DWORD PTR DS:[EBX+24]               ;  v6
005F3BA4  |.  MOV DWORD PTR DS:[EBX+4C], EAX
005F3BA7  |.  MOV EAX, DWORD PTR DS:[EBX+28]               ;  v7
005F3BAA  |.  MOV DWORD PTR DS:[EBX+48], EAX
005F3BAD  |.  MOV EAX, DWORD PTR DS:[EBX+2C]               ;  v8
005F3BB0  |.  MOV DWORD PTR DS:[EBX+44], EAX
005F3BB3  |.  MOV EAX, DWORD PTR DS:[EBX+30]               ;  v9
005F3BB6  |.  MOV DWORD PTR DS:[EBX+54], EAX
005F3BB9  |.  MOV ESI, DWORD PTR DS:[EBX+38]               ;  v3
005F3BBC  |.  ADD ESI, DWORD PTR DS:[EBX+40]               ;  v4
005F3BBF  |.  ADD ESI, DWORD PTR DS:[EBX+3C]               ;  v5
005F3BC2  |.  ADD ESI, DWORD PTR DS:[EBX+44]               ;  v8
005F3BC5  |.  ADD ESI, DWORD PTR DS:[EBX+4C]               ;  v6
005F3BC8  |.  ADD ESI, DWORD PTR DS:[EBX+48]               ;  v7
005F3BCB  |.  CMP EAX, 3                                   ;  v9 != 3
005F3BCE  |.  JNZ SHORT plsqldev.005F3C06

Итак, видим, что поля копируются в другие ячейки памяти, а так же вычисляется сумма от элементов. в eax лежит поле v9. Дальше идет множество проверок:

if(v9 != 3){
	if((key != v_summ) ||(magic != 8) || (hard_calc(v4, v5, v6, v7, v8) == 0)) // 005F3C22 CALL  ; 005F3558
		return license_error;
	else
	return license_ok;
}
else{
	if((hash(v_summ) != key) || (magic != 9) || (v3 == 0) || (v4 == 0) || (v5 == 0)) // 005F3BD2  CALL  ; 005F3538
		return license_error;
	else{
		v5v6_change(REG_INFO* ri); // 005F3BFF  CALL plsqldev.005F3C7C
		return license_ok;
	}
}

Чтобы пройти регистрацию удачно, нам надо вернуть license_ok. Как видим тут есть два пути вернуть license_ok. И все бы ничего, но тут есть 2 непонятных места: 1) функции hash и hard_calc. Если с хэшем все понятно, то с hard_calc проблемы. Функция вполне такая себе большая, и ее надо реверсить. 2) имеется поле magic из структуры REG_INFO. где и как оно задается - непонятно. Это основное препятствие. Значит нада найти место, где в поле загоняется неустраивающее нас значение.
Посмотрим на содержимое этой переменной - там 9. Знакомое число... Зайдем ка в функцию decode_and_check_product_code.


005F38D4  |.  |INC DWORD PTR DS:[EBX+C]                          ;  увеличить sums_index

Вуаля. В этой переменной лежит число считанных блоков. Значит magic = sums_index. Осталась одна проблема - обуздать функции. С хэш все просто - при генерировании product_key сначала заполняются поля с 2 по 8. Заполняется поле 9 значением 3, но в конце не ставится терминатор. Высчитывается хэш от суммы полей - это и будет поле 1, оно же key. Так мы пройдем и эту проверку. Для случая с восемью блоками надо озаботится функцией hard_calc

Для начала проходим с 9 блоками. Мы выявили налагаемые на поля ограничения. Раньше все поля мы вводили руками, и они были случайны. Теперь мы определили значения двух полей:
1) key или v1 = hash_key ( sum(v2...v8) );
2) v9 = 3;
Удовлетворяющий этим требованиям ключ выглядит так: 6f6386m5h9tv8gtbjj6h. Теперь мы попадаем в v5v6_change:


005F3C7C >/$  CMP DWORD PTR DS:[EAX+54], 3                 ;  v9 != 3
005F3C80  |.  JNZ SHORT plsqldev.005F3C93
005F3C82  |.  ADD DWORD PTR DS:[EAX+3C], 1B58              ;  v5 + 7000
005F3C89  |.  CMP DWORD PTR DS:[EAX+4C], 0                 ;  v6 == 0
005F3C8D  |.  JE SHORT plsqldev.005F3C93
005F3C8F  |.  ADD DWORD PTR DS:[EAX+4C], 18                ;  v6 + 24
005F3C93  >  RETN

Функция выполнится только для кода продукта с 9 блоками. За это отвечает первая проверка.

void v5v6_change{
	if(v9 != 3)
		return;
	v5 += 7000;
	if(v6 == 0)
		return;
	v6 += 24;
	return
}

Пока не ясно, нафига это делается, так что топаем дальше. Ура, мы вышли из функции разбора ключа (правда не рассмотрели еще один путь выполнения, когда блоков 8, а не 9):


005F5030  |.  CALL 
005F5035  |.  TEST AL, AL
005F5037  |.  JNZ SHORT plsqldev.005F504F

Вышли мимо такой нехорошей строки:


005F503E  |.  MOV EDX, plsqldev.005F5144                   ;  ASCII "Invalid license"

И попали сюда


005F504F  |>  MOV EAX, DWORD PTR DS:[ESI+3C]               ;  v5
005F5052  |.  CALL 
005F5057  |.  TEST AL, AL
005F5059  |.  JNZ SHORT plsqldev.005F5071
005F505B  |.  MOV EAX, plsqldev.00B96CDC
005F5060  |.  MOV EDX, plsqldev.005F515C                   ;  ASCII "Incorrect license"

Если вернется 0 - значит у нас кривая лицензия. Смотрим внутрь, ищем где возвращается 0, а где не ноль


005F4F98 >/$  PUSH ESI
005F4F99  |.  MOV ESI, EAX                                 ;  esi = v5
005F4F9B  |.  MOV AL, 1
005F4F9D  |.  MOV ECX, 1D4                                 ;  число элементов в массиве (468)
005F4FA2  |.  MOV EDX, OFFSET   ;  массив констант (00B96D24)
005F4FA7  |>  /CMP ESI, DWORD PTR DS:[EDX]
005F4FA9  |.  |JNZ SHORT plsqldev.005F4FAF                 ;  if(v5 != array[i]) // если не равно, то гуд
005F4FAB  |.  |XOR EAX, EAX                                ;  иначе обнуление и выход
005F4FAD  |.  |POP ESI
005F4FAE  |.  |RETN
005F4FAF  |>  |ADD EDX, 4                                  ;  i++
005F4FB2  |.  |DEC ECX
005F4FB3  |.^ JNZ SHORT plsqldev.005F4FA7
005F4FB5  |.  POP ESI
005F4FB6  .  RETN

Здесь все просто. v5 не должна равняца любому элементу массива v5_check_bad_arr. Напомню, что в v5v6_change идет правка v5: v5 += 7000;
3) v5+7000 not in v5_check_bad_arr
Есть несколько способов получать валидные v5. Самый простой - берем случайное число из v5_check_bad_arr, прибавляем 1 и вычитаем 7000. Но пока нас вполне устроит значение 5... Следующая проверка будет злее. Заходим в первый call:


005F5071  |>  MOV EAX, DWORD PTR DS:[ESI+48]               ;  v7
005F5074  |.  MOV DWORD PTR DS:[], EAX
005F5079  |.  MOV EAX, ESI
005F507B  |.  CALL 


005F3C94 >/$  PUSH EBP
005F3C95  |.  MOV EBP, ESP
005F3C97  |.  ADD ESP, -8
005F3C9A  |.  PUSH EBX
005F3C9B  |.  MOV ECX, EAX
005F3C9D  |.  MOV EBX, DWORD PTR DS:[ECX+4C]               ;  v6
005F3CA0  |.  TEST EBX, EBX
005F3CA2  |.  JNZ SHORT plsqldev.005F3CAE                  ;  если v6 не ноль
005F3CA4  |.  XOR EAX, EAX                                 ;  иначе пишется ноль в QWORD
005F3CA6  |.  MOV DWORD PTR SS:[EBP-8], EAX
005F3CA9  |.  MOV DWORD PTR SS:[EBP-4], EAX
005F3CAC  |.  JMP SHORT plsqldev.005F3CDD
005F3CAE  |>  MOV EAX, EBX                                 ;  eax = v6 = 30
005F3CB0  |.  DEC EAX                                      ;  eax -= 1 = 29
005F3CB1  |.  MOV EBX, 0C                                  ;  ebx = 12 (0x0c)
005F3CB6  |.  CDQ
005F3CB7  |.  IDIV EBX                                     ;  eax(29) / ebx(12) = 2
005F3CB9  |.  MOV EDX, EAX                                 ;  edx = 2
005F3CBB  |.  ADD EDX, EDX                                 ;  edx(2) * 2 = 4
005F3CBD  |.  ADD EDX, EDX                                 ;  edx(4) * 2 = 8
005F3CBF  |.  LEA EDX, DWORD PTR DS:[EDX+EDX*2]            ;  edx = edx(8) * 2 + edx(8) = 24
005F3CC2  |.  PUSH EDX
005F3CC3  |.  MOVZX EDX, WORD PTR DS:[ECX+4C]              ;  v6(30)
005F3CC7  |.  POP ECX
005F3CC8  |.  SUB DX, CX                                   ;  v6(30) - mul(24) = 6
005F3CCB  |.  MOV CX, 1
005F3CCF  |.  ADD EAX, 7CF                                 ;  eax(2) + 1999 = 2001
005F3CD4  |.  CALL plsqldev.0040F460
005F3CD9  |.  FSTP QWORD PTR SS:[EBP-8]
005F3CDC  |.  WAIT
005F3CDD  |>  FLD QWORD PTR SS:[EBP-8]
005F3CE0  |.  POP EBX
005F3CE1  |.  POP ECX
005F3CE2  |.  POP ECX
005F3CE3  |.  POP EBP
005F3CE4  .  RETN

Используется измененный v6. v6 += 24; Вот так он меняется в v5v6_change. Обращаем внимание на деление. v6-1 делится на 12. Если немного напряжем моск можем заметить закономерность. 24 - это 2*12. 12 - число месяцев в году. Делится на число месяцев в году. Вполне логично предположить, что тут какой-то замут со сроком лицензии. Посмотрим, что творится дальше. Умножение пока опустим. Перейдем в последнему пункту - eax(2) + 1999 = 2001. Видим интересное число 1999, похожее на год. Дальше идет вызов еще одной функции - plsqldev.0040F460. Параметры следующие:
eax = ((v6 - 1) / 12 + 1999)
ecx = 1
dx = 6


0040F466  |.  MOV EDI, ECX                                 ;  
0040F468  |.  MOV ESI, EDX                                 ;  
0040F46A  |.  MOV EBX, EAX                                 ;  
0040F46C  |.  PUSH ESP                                     ; /Arg1
0040F46D  |.  MOV ECX, EDI                                 ; |
0040F46F  |.  MOV EDX, ESI                                 ; |
0040F471  |.  MOV EAX, EBX                                 ; |
0040F473  |.  CALL plsqldev.0040F394                       ; plsqldev.0040F394

Собсно опять камень в сторону компилятора. Куча лишних действий - edi, esi, ebx сохраняются в стеке, потом в них скидываются ecx, eax, edx, дальше из них достаются те же значения и кладутся обратно в регистры ecx, eax, ecx, ну и потом значения регистров восстанавливаются. Притом нигде не используются в этой функции. Это так, к слову. Теперь к реверсингу...


0040F39D  |.  MOV EBX, ECX                                 ;  1
0040F39F  |.  MOV EDI, EDX                                 ;  v6 - mul
0040F3A1  |.  MOV WORD PTR SS:[EBP-2], AX                  ;  eax+1999
0040F3A5  |.  MOV BYTE PTR SS:[EBP-3], 0
0040F3A9  |.  MOVZX EAX, WORD PTR SS:[EBP-2]
0040F3AD  |.  CALL plsqldev.0040F358					   ; check year (1)
0040F3B2  |.  AND EAX, 7F								   ; (2)
0040F3B5  |.  LEA EAX, DWORD PTR DS:[EAX+EAX*2]
0040F3B8  |.  LEA ESI, DWORD PTR DS:[EAX*8+B6F834]
0040F3BF  |.  CMP WORD PTR SS:[EBP-2], 1
0040F3C4  |.  JB 
0040F3CA  |.  CMP WORD PTR SS:[EBP-2], 270F                ;  9 999
0040F3D0  |.  JA SHORT 
0040F3D2  |.  CMP DI, 1
0040F3D6  |.  JB SHORT 
0040F3D8  |.  CMP DI, 0C                                   ;  12
0040F3DC  |.  JA SHORT 
0040F3DE  |.  CMP BX, 1
0040F3E2  |.  JB SHORT 
0040F3E4  |.  MOVZX EAX, DI
0040F3E7  |.  CMP BX, WORD PTR DS:[ESI+EAX*2-2]			   ; (3)
0040F3EC  |.  JA SHORT 
0040F3EE  |.  MOVZX EAX, DI								   ; (4)start
0040F3F1  |.  DEC EAX
0040F3F2  |.  TEST EAX, EAX
0040F3F4  |.  JLE SHORT plsqldev.0040F404
0040F3F6  |.  MOV ECX, 1
0040F3FB  |>  /ADD BX, WORD PTR DS:[ESI+ECX*2-2]
0040F400  |.  |INC ECX
0040F401  |.  |DEC EAX
0040F402  |.^ JNZ SHORT plsqldev.0040F3FB				   ; (4)end
0040F404  |>  MOVZX ECX, WORD PTR SS:[EBP-2]			   ; (5)start
0040F408  |.  DEC ECX
0040F409  |.  MOV EAX, ECX
0040F40B  |.  MOV ESI, 64                                  ;  100
0040F410  |.  CDQ
0040F411  |.  IDIV ESI
0040F413  |.  IMUL ESI, ECX, 16D                           ;  365 (5)end
0040F419  |.  MOV EDX, ECX								   ; (6)start
0040F41B  |.  TEST EDX, EDX
0040F41D  |.  JNS SHORT plsqldev.0040F422
0040F41F  |.  ADD EDX, 3
0040F422  |>  SAR EDX, 2
0040F425  |.  ADD ESI, EDX
0040F427  |.  SUB ESI, EAX								   ; (6)end
0040F429  |.  MOV EAX, ECX								   ; (7)start
0040F42B  |.  MOV ECX, 190                                 ;  400
0040F430  |.  CDQ
0040F431  |.  IDIV ECX
0040F433  |.  ADD ESI, EAX
0040F435  |.  MOVZX EAX, BX
0040F438  |.  ADD ESI, EAX
0040F43A  |.  SUB ESI, 0A955A							   ; (7)end
0040F440  |.  MOV DWORD PTR SS:[EBP-8], ESI
0040F443  |.  FILD DWORD PTR SS:[EBP-8]
0040F446  |.  MOV EAX, DWORD PTR SS:[EBP+8]
0040F449  |.  FSTP QWORD PTR DS:[EAX]
0040F44B  |.  WAIT
0040F44C  |.  MOV BYTE PTR SS:[EBP-3], 1
0040F450 >|>  MOVZX EAX, BYTE PTR SS:[EBP-3]


Большая функция получилась. Она делает ровно ту работу, которая нам важна, ибо возвращаемое ей значение используется дальше.

Важное место номер раз (1)


0040F3AD  |.  CALL plsqldev.0040F358					   ; check year

0040F35C  |.  0FB7C1                MOVZX EAX, CX
0040F35F  |.  83E0 03               AND EAX, 3
0040F362  |.  85C0                  TEST EAX, EAX

Проверяется 2 младших бита на наличие в них единиц, иначе говоря кратность четырем, причем у переданного ей года. Что вам говорит кратность четырем в контексте работы с годами? Правильно, это проверка високосности года. Дальше математига для учета високосного. Из нашего ключа получается не високосный, так что не обращаем внимания. Вернулись, глядим дальше.
Место номер (2)

0040F3B2  |.  AND EAX, 7F
0040F3B5  |.  LEA EAX, DWORD PTR DS:[EAX+EAX*2]
0040F3B8  |.  LEA ESI, DWORD PTR DS:[EAX*8+B6F834]

Результат проверки високосности года участвует в формировании указателя в таблице. Начало таблицы B6F834. Если в eax лежит 1, то указатель переедет на 3*8 байт (24) ниже(т.е. B6F84C), если же в eax 0, то указатель будет равен B6F834. Опять число, кратное 12. Посмотрим, что там такого магического в табличке(правой мышкой -> Short -> Unsigned Decimal ;) ):

00B6F834      31     28     31     30     31     30     31     31
00B6F844      30     31     30     31     31     29     31     30
00B6F854      31     30     31     31     30     31     30     31

Угадайте, на что это похоже... При високосном году число дней в феврале стоит 29, для этого и делается поправка указателя в таблице. Запомнили, что в esi лежит таблица длин месяцев. Дальше проверка на вхождение в диапозоны года и разности v6(30) - mul(24) = 6 на вхождение в диапозон от 1 до 12. Сталобыть это число - месяц. Вообще глядя на аргументы и результат v6(30) - mul(24) = 6 выглядит как v6 % 12. Правда это только для параметра v6 = 6, после добавления 24. Потом проведу эксперимент с другими числами. Но пока не отвлекаемся.

Место (3) - проверка знчаения в bx с длительностью месяца, номер которого указан в eax. Т.е. разность это не число месяцев, а указание конкретного месяца. Запомним.

Место (4) - цикл, в котором суммируются длины месяцев вплоть до di, т.е. до разности. Значит предположение об указании месяца - верно. Хотя можете интерпритировать его как число месяцев с января. Но суть ясна - это граница диапозона месяцев. В bx на момент первой итерации лежит число 1. Суммируем...

Место (5) - берем год - 1 (текущий год еще не закончился, прошло только столькото месяцев, та самая разность), и делим на 100. Что получаем? Прально, век.

0040F413  |.  IMUL ESI, ECX, 16D                           ;  365

Год умножается на 365 и кладется результат в esi. Получаем примерное число дней с нулевого года. Не учитываются високосные годы.

Место (6) - здесь собсно видимо и правится високосность.

0040F422  |>  SAR EDX, 2 - фактически знаковое деление на 4.

0040F425  |.  03F2                  ADD ESI, EDX
0040F427  |.  2BF0                  SUB ESI, EAX

К числу дней прибавляем результат деления года на 4 и вычитаем век.

Место (7) - делим год на 400 и результат опять добавляем к числу дней, ну и наконец добавляем bx, число дней, прошедшее с начала указанного года до указанного месяца. Последняя важная строка - вычитается из числа прошедших дней константа 0A955A (693594). Магическое число. Чтоб понять, что этот кусок делает поделим это число на 365 (логично считать эту контанту днями, если вычитаем из дней), получаем почти круглое число(не учитываем високосные годы) 1900,25753. Что это получается? Число дней до начала 20 века. Сталобыть мы оставляем от общей суммы дней лишь дни, начиная с 1900 года.
Вот назначение функции. Меняем ее метку на более логичную:

005F507B  |.  CALL 

Ну и собсно заключение о назначении переменной v6
4) v6 - число месяцев, добавляемых к 1999 году.

Вполне вероятно, что разобранная нами функция относится к vcl, но из-за нераспознанности нам пришлось потратить время на ручной разбор кода.

Возврщаемся на уровни выше.


005F5087  |.  MOV EAX, DWORD PTR DS:[ESI+48]               ;  v7
005F508A  |.  TEST EAX, EAX
005F508C  |.  JE SHORT plsqldev.005F509A
005F508E  |.  CMP EAX, DWORD PTR DS:[B96CC8]
005F5094  |.  JGE SHORT plsqldev.005F509A
005F5096  |.  XOR EBX, EBX
005F5098  |.  JMP SHORT plsqldev.005F509C
005F509A  |>  MOV BL, 1

Проверка v7. Если меньше, чем константа в памяти по адресу B96CC8, то ebx сбрасывается в ноль, а это между прочим метка успешности проверки. У нас v7 = 7, а в памяти лежит ни много ни мало DS:[00B96CC8]=00000047. Поиск по ссылкам не дал результатов, откуда туда заносится число. Т.е. не понятно, то ли это константа, то ли число меняется в зависимости от чего то. Тогда ставим хард бряк на акцес и перезапускаем.


00B3280E   .  MOV EAX, DWORD PTR DS:[BA9F1C]
[00BA9F1C]=00B96CC8
00B32813   .  MOV DWORD PTR DS:[EAX], 47

Сталобыть это константа. Вокруг этого места видим еще интересные места. Есть еще ряд констант. Заносятся значения хитро - чем промежуточный адрес. Интересно, это компилер виноват или программисты. Пока вроде никаких ухищрений не было заметно...


00B327F8   .  MOV EAX, DWORD PTR DS:[BA9CB0]
00B327FD   .  MOV DWORD PTR DS:[EAX], 1
00B32803   .  MOV EAX, DWORD PTR DS:[BA9DE8]
00B32808   .  MOV DWORD PTR DS:[EAX], 0A
00B3280E   .  MOV EAX, DWORD PTR DS:[BA9F1C]
00B32813   .  MOV DWORD PTR DS:[EAX], 47
00B32819   .  MOV EAX, plsqldev.00B34F18                   ;  ASCII "01022007"
00B3281E   .  CALL plsqldev.00B08ABC
00B32823   .  MOV EAX, DWORD PTR DS:[BA91D8]
00B32828   .  FSTP QWORD PTR DS:[EAX]

И самая для нас насущная - последняя, строковая. Функция, которой она передается, высчитывает то же самое число дней, только для разобранной строки даты. На выходе имеем - 39114. Запомним. Теперь к вопросу о v7 - меняем значение в заготовке кейгена на 77 (0x4d) и получаем новый ключ, с которым будем забавляться - stquu9xexkk65wdwh7fs8n

Проверку на 0x47 прошли. Опять вызывается расчет числа дней.


005F50A9  |.  DC1D CC6CB900         FCOMP QWORD PTR DS:[B96CCC]
ST=37043.000000000000000
DS:[00B96CCC]=39114.00000000000

Приходим к такой проверке. В стеке лежит насчитаное число дней, и видим сравнение с константой. Как получилась эта константа - мы уже увидели выше. Видимо здесь проверка чего - то ли проверка, катит ли лицензия по числу (например это лицензия для старой версии и ее нах), либо это еще что нить. Пока топаем так.


005F50B3  |. JNB SHORT plsqldev.005F50B9
005F50B5  |. XOR EDX, EDX
005F50B7  |. JMP SHORT plsqldev.005F50BB
005F50B9  |> MOV DL, 1

Если наша дата больше, то в dl будет 1, иначе (как и происходит сейчас) в dl будет 0.
Продолжаем и приходим к последней проверке в этом блоке


005F50BB  |> MOV EAX, DWORD PTR DS:[ESI+40]               ;  v4
005F50BE  |. CMP EAX, DWORD PTR DS:[B96CD4]				   ; DS:[00B96CD4]=00000001
005F50C4  |. JE SHORT plsqldev.005F50D2
005F50C6  |. CMP EAX, DWORD PTR DS:[B96CD8]				   ; DS:[00B96CD8]=0000000A
005F50CC  |. JE SHORT plsqldev.005F50D2
005F50CE  |. XOR EAX, EAX
005F50D0  |. JMP SHORT plsqldev.005F50D4
005F50D2  |> MOV AL, 1
005F50D4  |> TEST AL, AL
005F50D6  |. JE SHORT plsqldev.005F50EA
005F50D8  |. OR BL, DL
005F50DA  |. JE SHORT plsqldev.005F50EA
005F50DC  |. MOV EAX, plsqldev.00B96CDC
005F50E1  |. CALL plsqldev.00405F60
005F50E6  |. MOV BL, 1
005F50E8  |. JMP SHORT plsqldev.005F50FB
005F50EA  |> MOV EAX, plsqldev.00B96CDC
005F50EF  |. MOV EDX, plsqldev.005F5178                   ;  ASCII "Your license does not allow you to use this version"
005F50F4  |. CALL plsqldev.00405FB4

v4 у нас равно 4. Поэтому мы выходим с ошибкой "Your license does not allow you to use this version". Значит нада менять v4 например на 10.
stk3yxd8ew2zqw8nspgu8v


005F50DC  |.  MOV EAX, plsqldev.00B96CDC
005F50E1  |.  CALL plsqldev.00405F60
005F50E6  |.  MOV BL, 1
005F50E8  |.  JMP SHORT 
...
005F511D  	  MOV EAX, EBX


Вот мы и дошли до выхода. Вызываемая функция явно из vcl, пропустим ее мимо ушей. Получаем долгожданный bl = 1, перекидываем в eax и выходим.
Обращу внимание еще на один кусок кода


005F50D8  |. OR BL, DL

Лицензия покатит если дата, полученная от суммирования, больше 01222007 или v7 > 0x47.
Теперь вызывается


00AFC668  |.  PUSH 0
00AFC66A  |.  PUSH 1
00AFC66C  |.  PUSH 417
00AFC671  |.  MOV EAX, DWORD PTR DS:[BA8A9C]
00AFC676  |.  MOV EAX, DWORD PTR DS:[EAX]
00AFC678  |.  CALL plsqldev.004C78CC
00AFC67D  |.  PUSH EAX                                     ; |hWnd
00AFC67E  |.  CALL               ; SendMessageA
00AFC683  |.  TEST AL, AL
00AFC685  |.  JE SHORT 
00AFC687  |.  MOV EBX, 3

И по результату показывается месажбокс. К сожалению возвращается не то, что надо, поэтому мы видем сообщение о плохой регистрации. А это говорит, что разобранная нами часть алгоритма - еще не конец. Теперь надо найти оконную процедуру окна, которому шлется сообщение, и копаца уже там... А получить мы должны не ноль, чтобы в ebx попала тройка, т.к. проверка, решающая, какой показать месажбокс, поздравит нас только если получит код 3...

Итого имеем следующие ограничения на ключ:
1) key или v1 = hash_key ( sum(v2...v8) );
2) v9 = 3;
3) v5+7000 not in v5_check_bad_arr
4) v6 - число месяцев, добавляемых к 1999 году.
5) v4 = 1 или v4 = 10
6) v7 >= 0x47

Ограничения ключа разобранны. Продолжаем...

.:Разбор проверки в оконной процедуре:.

Теперь наша задача - найти оконную процедуру, которая получает сообщение 0x417. Оконную процедуру определить просто - GetWindowLong (hwnd, GWL_WNDPROC). Есть путь еще проще inqsoft windowscanner может показывать инфу об окнах. Теперь собсно надо найти, какому окну шлется сообщение. В олли жмем View->Windows. Хэндл окна у меня такой

0012F9EC   00061890   |hWnd = 61890

Handle=00061890
Title=PL/SQL Developer - (Not logged on)
WinProc=FFFF1227  (subclassed)
ClsProc=FFFF124F

Шлется сообщение главному окну. Теперь к вопросу об оконной процедуре: WinProc=FFFF1227 (subclassed). Такой страницы памяти нету. Когда то давно я сталкивался с таким значением оконной процедуры, и мне пришлось изрядно понапрягать память, прежде чем я вспомнил, что такое значение возвращала апи GetWindowLongA, обращаясь к Unicode окну. У InqSoft Window Scanner та же бага (может ее и починили, но у меня старая версия сканера). Из сканирующих программ остался один интсрумент - Spy++ из комплекта VisualStudio. Итог - оконная процедура лежит тут "02750B77"


02750B77    CALL 02750004

Странновато для оконной процедуры. Запоминаем адрес, жмем регистрацию, вводим код, останавливаемся на вызове SendMessage и ставим бряк на этот адрес(если поставим бряк раньше, будем ловить все оконные сообщения, что нам не надо, ибо заебемся). Жмем F9. Вывалились на бряке. Смотрим, то ли сообщение получили.


0012F858   7E368724   RETURN to USER32.7E368724
0012F85C   00061890
0012F860   00000417 <--- Оно
0012F864   00000001
0012F868   00000000

Попадаем по колу сюда
02750004    59                      POP ECX
02750005  - E9 A626CEFD             JMP plsqldev.004326B0

Теперь придется долго лопатить кучу колов...


0053680C   .  CALL            ; CallWindowProcW

Наше сообщение прокидывается дальше. Смотрим в стек, какой процедуре оно достанется.


0012F814   02750B84   |PrevProc = 02750B84

02750B84   CALL 02750004

Забавно. Опять туда же, куда мы пришли в первый раз. Но дальше нас выкинет не на CallWindowProcW, так что придется заходить во все колы и смотреть внимательно сравнения, когда встретим сравнения 0x0000417, то значит мы оказались близко к цели. На сравнения кода сообщения мы выходим быстро, но это все не те проверки, которые нам нужны. Заходим во все колы...


00405188   .  PUSH EAX
00405189   .  MOV EAX, DWORD PTR DS:[EAX]
0040518B   .  CALL plsqldev.004050E8
00405190   .  POP EAX
00405191   .  JE SHORT plsqldev.00405198
00405193   .  MOV ECX, ESI
00405195   .  POP ESI
00405196   .  JMP NEAR ECX

Перед вызовом 004050E8 в si лежит код сообщения. Функция из структур и кода сообщения получает адрес обработчика - 0xB27180


00B27186  |.  MOV EAX, DWORD PTR DS:[EBX+4]				 ; берется wparam
00B27189  |.  DEC EAX                                      ;  Switch (cases 1..2)
00B2718A  |.  JE SHORT plsqldev.00B27191					 ; переход выполняется
00B2718C  |.  DEC EAX
00B2718D  |.  JE SHORT plsqldev.00B2719F
00B2718F  |.  JMP SHORT plsqldev.00B271D9
00B27191  |>  CALL            ;  Case 1 of switch 00B27189

00AFC244 - адрес функции, которая будет делать проверку. Если прокрутить вниз, увидим любопытные строки...


00AFC37C  |.  MOV EDX, plsqldev.00AFC4E0                   ;  ASCII "ProductCode"
00AFC39C  |.  MOV EDX, plsqldev.00AFC4F4                   ;  ASCII "SerialNumber"
00AFC3BC  |.  MOV EDX, plsqldev.00AFC50C                   ;  ASCII "Licenses"
00AFC3DC  |.  MOV EDX, plsqldev.00AFC520                   ;  ASCII "BaseVersion"
00AFC407  |.  MOV EDX, plsqldev.00AFC534                   ;  ASCII "ServiceContract"
00AFC427  |.  MOV EDX, plsqldev.00AFC54C                   ;  ASCII "Dealer"
00AFC458  |.  MOV EDX, plsqldev.00AFC55C                   ;  ASCII "TimeLimit"

Начало не очень радужное. Еще стока ковырять... Пока начнем с малого


00AFC299  |.  8B45 F4               MOV EAX, DWORD PTR SS:[EBP-C]
00AFC29C  |.  BA C4C4AF00           MOV EDX, plsqldev.00AFC4C4                   ;  ASCII "xs374ca"
00AFC2A1  |.  E8 F6A090FF           CALL 
00AFC2A6  |.  0F85 CF010000         JNZ 

В eax лежит строка "password". Значит пассворд должен быть xs374ca. Исправляем.


00AFC2CA  |.  CALL 
00AFC2CF  |.  TEST AL, AL
00AFC2D1  |.  JE 

Повторная обработка продукт кода


00AFC2D7  |.  MOV EDX, DWORD PTR DS:[]
00AFC2DD  |.  MOV EAX, plsqldev.00AFC4D4
00AFC2E2  |.  CALL plsqldev.00406594

Это первая проверка ключа. Кладем на нее и топаем к следующей проверке.


00AFC343  |.  LEA EAX, DWORD PTR SS:[EBP-110]
00AFC349  |.  CALL plsqldev.005A1B74
00AFC34E  |.  CMP EAX, DWORD PTR DS:[ESI+3C]               ;  changed v5
00AFC351  |.  JNZ 

Сосредоточимся тут. Интерес представляет эта функция


0040425C >/$  PUSH EBX

Идет череда проверок на '-', '+', '$' ... Это нам нах не надо. Я ввел в поле число 123321, в итоге вывалился на этот цикл:


004042AE  |>  /SUB BL, 30
004042B1  |. ||CMP BL, 9
004042B4  |. ||JA SHORT plsqldev.004042DB
004042B6  |. ||CMP EAX, EDI
004042B8  |. ||JA SHORT plsqldev.004042DB
004042BA  |. ||LEA EAX, DWORD PTR DS:[EAX+EAX*4]
004042BD  |. ||ADD EAX, EAX
004042BF  |. ||ADD EAX, EBX
004042C1  |. ||MOV BL, BYTE PTR DS:[ESI]
004042C3  |. ||INC ESI
004042C4  |. ||TEST BL, BL
004042C6  |.^\JNZ SHORT plsqldev.004042AE

Результат возвращается и проверяется на ...


00AFC34E  |.  CMP EAX, DWORD PTR DS:[ESI+3C]               ;  changed v5

Вот и все. Этот алгоритм по сути есть atoi. Т.е. чтобы пройти проверку, нада в поле serial number написать то число, которое мы вшили в ключ в v5.
Еще одно место интересное. Если в поле +40 задано число 0xa, то будет высчитываться число оставшихся дней. Напомню, что там допустимо только 2 значения - 1 и 10.


00AFC433  |.  CMP DWORD PTR DS:[ESI+40], 0A
00AFC437  |.  JNZ SHORT plsqldev.00AFC464
...
00AFC458  |.  MOV EDX, plsqldev.00AFC55C                   ;  ASCII "TimeLimit"

Эти строки, которых так много - на самом деле описания полей ключа =) Нам можно не напрягаться, придумывая названия вместо v1, v2, v3..., просто используем то, что нам дали разработчики.


Заключение:Это было весьма приятное приключение. Довольно длительное из-за сесии. Давно не попадались интересные защиты, более-менее объемные, с нетривиальными алгоритмами, способные отвлечь меня в напряженные моменты учебы. Спасибо авторам за их творение. Из неприятных моментов можно выделить странные действия компилятора да кутерьму с оконными прцедурами.
Я прошел не всеми возможными путями исполнения (например 9 поле ключа может быть не задано), можете сами проверить и дополнить алгоритм для этого случая.
Исходный код генератора можновзять тут: http://ifolder.ru/6857513
Использовать только в образовательных целях!
Спасибо за внимание. Надеюсь, вам было интересно так же, как и мне.



Обсуждение статьи: Реверсинг и кейгенинг pl sql developer в прямом эфире >>>


Комментарии к статье: Реверсинг и кейгенинг pl sql developer в прямом эфире

huckfuck 10.09.2008 17:58:51
Ну ты и монстр ! Респект !
У меня бы терпения не хватило, я бы просто пропатчил.
---
PKVL 08.07.2009 23:54:33
МОЛОДЕЦ!!!
Я-б тоже не выдержал....
---
moroshkin 17.05.2010 10:31:39
Только что написал кейгер к PL/SQL Developer 8.0.2.1505 - хотел проверить сложно ли разобрать программу используя только OllyDbg.
В результате кроме OllyDbg 2.0 использовал еще и PEiD 0.95 (девелопер запакован UPX)

Мои дополнения:
1.
Защита в новой версии точно такая же как описано в этой статье. Даже пароль тот же.
2.
Я не искал оконную процедуру (точнее я не сообразил что её искать нужно) - я просто поставил хард бряки на адреса где сохранились из формы серийный номер и пароль. И практически сразу вывалился на сравнениях пароля и серийного номера.
---

Материалы находятся на сайте https://exelab.ru



Оригинальный DVD-ROM: eXeL@B DVD !


Вы находитесь на EXELAB.rU
Проект ReactOS