Написание кейгена для keygenme #00
Знаете, даже не знаю, с чего начать свой рассказ. Пока я дни и ночи напролёт реверсил сиё творение, в моей голове зародилось и погибло несколько вариантов этой статьи, а сейчас, когда кейген лежит передо мной (и действительно генерит валидные серийники) я как-то растерялся…
По собственному опыту знаю, что авторы таких keygenme ждут от реверсеров повествований эпических масштабов, поэтому я уж постараюсь не разочаровать ни KSDR – автора этого замечательного творения, которому респект и низкий поклон, ни вас, дорогие читатели.
Начало
Когда я увидел на форуме предложение закейгенить этот keygenme #00, я даже особо и не проникся идеей загрузить его в отладчик, так как таких кейгенми на http://www.crackmes.de состав, и если каждый пробовать сломать, то и здоровья не хватит. Но для интереса, конечно, качнул это добро. Когда я запустил его и увидел в заголовка главного диалогового окна надпись: KeygenMe for gwsoft.ahteam.org, мой интерес возрос. Стало любопытно, какое отношение это программное творение имеет к AHTeam’у. Правда, на этот вопрос я так и не получил исчерпывающего ответа, но не это главное. А главное - я скачал его и начал реверсить.
Инструментарий
Обязательным инструментом, пожалуй, в данном случае является IDA + HexRays. Дальше отладчик по вкусу – я использовал OllyDebug. В качестве компилятора я выбрал Microsoft Visual Studio 2008. 2008 потому, что она наиболее стабильна (если верить людям, которые использовали всё разнообразие версий этого пакета, я же могу сравнить её только с 6 версией – по сравнению с ней действительно стабильна), кейген написан на С (правда, на весьма корявом, за что просьба сильно не пинать) с ассемблерными вставками. Но, опять же, вас это ни к чему не обязывает – вы можете пользоваться тем компилятором, который удобен лично вам. Ещё я использовал Pe Tools, но это уже только по причине моей лени. Как вы сами увидите позже, в этом не было особой необходимости.
С инструментами определились – начинаем исследования.
Первичный досмотр
Это лишь анализ, но пока ещё не взлом.
Загрузив программу в отладчик, останавливаемся на ЕР:
Здесь абсолютно бесхитростно вызывается DialogBoxParamA, поэтому нас дальше будет интересовать исключительно то, что происходит в DlgProc, а именно – по адресу 0040102B.Code:00401000 >/$ 6A 00 PUSH 0 ; /pModule = NULL 00401002 |. E8 5B070000 CALL <JMP.&kernel32.GetModuleHandleA> ; \GetModuleHandleA 00401007 |. A3 60304000 MOV DWORD PTR DS:[403060],EAX 0040100C |. 6A 00 PUSH 0 ; /lParam = NULL 0040100E |. 68 2B104000 PUSH keygenme.0040102B ; |DlgProc = keygenme.0040102B 00401013 |. 6A 00 PUSH 0 ; |hOwner = NULL 00401015 |. 68 E8030000 PUSH 3E8 ; |pTemplate = 3E8 0040101A |. FF35 60304000 PUSH DWORD PTR DS:[403060] ; |hInst = NULL 00401020 |. E8 13070000 CALL <JMP.&user32.DialogBoxParamA> ; \DialogBoxParamA 00401025 |. 50 PUSH EAX ; /ExitCode 00401026 \. E8 31070000 CALL <JMP.&kernel32.ExitProcess> ; \ExitProcess
WM_INITDIALOG нас в данном случае не интересует:
WM_COMMAND более интересен, если пользователь не выбрал Exit, то последний переход не выполняется:Code:00401034 . 3D 10010000 CMP EAX,110 00401039 . 75 25 JNZ SHORT keygenme.00401060
Необычный момент – кейгенми первым делом считывает серийник, а не имя пользователя:Code:00401060 > \3D 11010000 CMP EAX,111 00401065 . 0F85 E9010000 JNZ keygenme.00401254 0040106B . 8B45 10 MOV EAX,DWORD PTR SS:[EBP+10] 0040106E . 3D EE030000 CMP EAX,3EE 00401073 . 0F85 C4010000 JNZ keygenme.0040123D
Теперь хитроумным способом проверяется длина введенного серийника. Проверка завершается успешно в случае, если в конце проверки еах равен нулю:Code:00401079 . 6A 23 PUSH 23 ; /Count = 23 (35.) 0040107B . 8D45 BA LEA EAX,DWORD PTR SS:[EBP-46] ; | 0040107E . 50 PUSH EAX ; |Buffer 0040107F . 68 ED030000 PUSH 3ED ; |ControlID = 3ED (1005.) 00401084 . FF75 08 PUSH DWORD PTR SS:[EBP+8] ; |hWnd 00401087 . E8 B8060000 CALL <JMP.&user32.GetDlgItemTextA> ; \GetDlgItemTextA
Признаюсь, что вначале для эстетики я хотел решить это уравнение на листе бумаги, а потом показать, что в школе у меня была пятёрка по математике. Но, видимо, пятёрка была мне подарена, поэтому пришлось идти другим путём. Этот код я прямо в отладчике запатчил следующим образом:Code:00401094 . 8945 B4 MOV DWORD PTR SS:[EBP-4C],EAX 00401097 . 0FAFC0 IMUL EAX,EAX 0040109A . C1C8 36 ROR EAX,36 ; Shift constant out of range 1..31 0040109D . 8BD8 MOV EBX,EAX 0040109F . 35 CA593201 XOR EAX,13259CA 004010A4 . 0FAFDB IMUL EBX,EBX 004010A7 . 81EB CA593201 SUB EBX,13259CA 004010AD . 2BC3 SUB EAX,EBX 004010AF . 2D 94B35402 SUB EAX,254B394
Т.е. по адресу [EBP-4C] я записал нуль, потом на адресе 004010B9 поставил брейкпоинт, и когда он сработал, на вершине стека лежала длина пароля – 32 символа. Вот такой минибрут.Code:00401094 FF45 B4 INC DWORD PTR SS:[EBP-4C] 00401097 8B45 B4 MOV EAX,DWORD PTR SS:[EBP-4C] 0040109A 0FAFC0 IMUL EAX,EAX 0040109D C1C8 36 ROR EAX,36 ; Shift constant out of range 1..31 004010A0 8BD8 MOV EBX,EAX 004010A2 35 CA593201 XOR EAX,13259CA 004010A7 0FAFDB IMUL EBX,EBX 004010AA 81EB CA593201 SUB EBX,13259CA 004010B0 2BC3 SUB EAX,EBX 004010B2 2D 94B35402 SUB EAX,254B394 004010B7 ^ 75 DB JNZ SHORT 00401094 ; keygenme.00401094 004010B9 90 NOP
Теперь, наконец-то, считывается имя пользователя:
Проверяется, не равна ли длина имени нулю, и если не равна, то имя копируется по адресу 00403064:Code:004010BA . 6A 23 PUSH 23 ; /Count = 23 (35.) 004010BC . 8D45 DD LEA EAX,DWORD PTR SS:[EBP-23] ; | 004010BF . 50 PUSH EAX ; |Buffer 004010C0 . 68 EB030000 PUSH 3EB ; |ControlID = 3EB (1003.) 004010C5 . FF75 08 PUSH DWORD PTR SS:[EBP+8] ; |hWnd 004010C8 . E8 77060000 CALL <JMP.&user32.GetDlgItemTextA> ; \GetDlgItemTextA
Дальше следует функция, которая вычисляет хэш от имени. Эту функцию я ласково назвал StrongCreateHash. С ней у меня возникло очень близкое знакомство в процессе реверсинга, но об этом позже.Code:004010CD . 0BC0 OR EAX,EAX 004010CF . 0F84 54010000 JE keygenme.00401229 004010D5 . 8BC8 MOV ECX,EAX 004010D7 . 8D75 DD LEA ESI,DWORD PTR SS:[EBP-23] 004010DA . BF 64304000 MOV EDI,keygenme.00403064 004010DF . F3:A4 REP MOVS BYTE PTR ES:[EDI],BYTE PTR DS:[>
Теперь 42h байта неизвестного пока содержания перемещаем из адреса 0040301A в 004011BC.Code:004010E1 . E8 14020000 CALL keygenme.004012FA
Сами эти байты вот:Code:004010E6 . B9 42000000 MOV ECX,42 004010EB . BE 1A304000 MOV ESI,keygenme.0040301A 004010F0 . BF BC114000 MOV EDI,keygenme.004011BC ; Entry address 004010F5 . F3:A4 REP MOVS BYTE PTR ES:[EDI],BYTE PTR DS:[>
Считываем указатель на длину серийника, разыменовываем, получая тем самым длину серийника, и делим её на 2:Code:0040301A 1F 7B 31 17 EF EF 04 40 E7 EF F6 FB 45 40 23 EF *{1пп @зпцыE@#п 0040302A F2 F7 F6 05 F4 E9 FC 05 F4 FD EF F2 F3 40 ED EF тчцфйьфэпту@нп 0040303A E9 40 F7 EF EF 04 40 03 F6 05 03 FB F9 F6 45 60 й@чпп @ ц ыщцE` 0040304A F8 9F 55 20 60 F8 71 63 60 60 5F E9 58 78 08 59 шџU `шqc``_йXxY 0040305A 60 60
Получаем адрес серийника и крутим цикл - счётчик цикла в есх равен 16. В цикле мы из серийника читаем по два байта, от символа цифры вычитаем 30h, от другого - 37h, после младшему байту делаем ROL AL,4, складываем два байта и пишем назад в серийник.Code:004010F7 . 8D4D B4 LEA ECX,DWORD PTR SS:[EBP-4C] 004010FA . 8B09 MOV ECX,DWORD PTR DS:[ECX] 004010FC . D1E9 SHR ECX,1
То, что случилось с серийным номером, я (опять всё также ласково) назвал преобразованием серийника. От этого преобразованного серийника вычисляем хэш, но уже с помощью функции WeakCreateHash. Простите, если мои названия слабо отражают суть алгоритма, но я старался. Вот и сам вышеописанный код:Code:004010FE . 8D75 BA LEA ESI,DWORD PTR SS:[EBP-46] 00401101 . 8BFE MOV EDI,ESI 00401103 > 66:AD LODS WORD PTR DS:[ESI] 00401105 . 3C 39 CMP AL,39 00401107 . 7F 04 JG SHORT 0040110D ; keygenme.0040110D 00401109 . 2C 30 SUB AL,30 0040110B . EB 02 JMP SHORT 0040110F ; keygenme.0040110F 0040110D > 2C 37 SUB AL,37 0040110F > 80FC 39 CMP AH,39 00401112 . 7F 05 JG SHORT 00401119 ; keygenme.00401119 00401114 . 80EC 30 SUB AH,30 00401117 . EB 03 JMP SHORT 0040111C ; keygenme.0040111C 00401119 > 80EC 37 SUB AH,37 0040111C > C0C0 04 ROL AL,4 0040111F . 02C4 ADD AL,AH 00401121 . AA STOS BYTE PTR ES:[EDI] 00401122 .^ E2 DF LOOPD SHORT 00401103 ; keygenme.00401103
Из хэша серийного номера мы вычисляем некое подобие контрольной суммы. Назовём её просто CRC. Если принять во внимание то, что хэш состоит из четырёх DWORD’ов, то CRC=DWORD1 - DWORD2 xor DWORD3 + DWORD4:Code:00401124 . 8D45 BA LEA EAX,DWORD PTR SS:[EBP-46] 00401127 . E8 3D010000 CALL 00401269 ; keygenme.00401269
Полученное значение взаимодействует с хэшем имени: CRC xor DWORD1 + DWORD2 - DWORD3 xor DWORD4. Здесь DWORD1 и т.д. – значения хэша имени:Code:0040112F . 8B18 MOV EBX,DWORD PTR DS:[EAX] 00401131 . 2B58 04 SUB EBX,DWORD PTR DS:[EAX+4] 00401134 . 3358 08 XOR EBX,DWORD PTR DS:[EAX+8] 00401137 . 0358 0C ADD EBX,DWORD PTR DS:[EAX+C]
Результирующее значение записывается в EDX, после чего вычисляется значение CRC1 (опять моё название) и записывается в EDX:Code:0040113A . 331D 64304000 XOR EBX,DWORD PTR DS:[403064] 00401140 . 031D 68304000 ADD EBX,DWORD PTR DS:[403068] 00401146 . 2B1D 6C304000 SUB EBX,DWORD PTR DS:[40306C] 0040114C . 331D 70304000 XOR EBX,DWORD PTR DS:[403070]
Т.е. сейчас в EDX - CRC1, а в EBX – CRCfixed. Отличаются они только тем, что их младшее и старшее слова стоят в разном порядке. Теперь получаем начала загадочных 42h байт и крутим цикл расшифровки каждого байта. DecryptByte = Byte - CRCfixed [1] xor CRC1[0] - CRCfixed [0] + CRC1[1]:Code:0401152 . 8BD3 MOV EDX,EBX 00401154 . C1CA 10 ROR EDX,10
Первые 20h расшифрованных байт переписывают хэш имени:Code:00401157 . BE BC114000 MOV ESI,4011BC ; Entry address 0040115C . 8BFE MOV EDI,ESI 0040115E . B9 42000000 MOV ECX,42 00401163 > AC LODS BYTE PTR DS:[ESI] 00401164 . 2AC7 SUB AL,BH 00401166 . 32C2 XOR AL,DL 00401168 . 2AC3 SUB AL,BL 0040116A . 02C6 ADD AL,DH 0040116C . AA STOS BYTE PTR ES:[EDI] 0040116D .^ E2 F4 LOOPD SHORT 00401163
Для первых 16 расшифрованных байт применяется StrongCreateHash:Code:0040116F . BE BC114000 MOV ESI,keygenme.004011BC ; Entry address 00401174 . BF 64304000 MOV EDI,keygenme.00403064 00401179 . B9 20000000 MOV ECX,20 0040117E . F3:A4 REP MOVS BYTE PTR ES:[EDI],BYTE PTR DS:[>
Если значения хэша совпали с константами, то выполняем расшифрованные байты как код, и они должны выполняться, не генерируя исключений. Любое исключение будет для кейгенми последним, т.к. ни структурированная, ни векторная обработка исключений в нём не применяется:Code:00401180 . E8 75010000 CALL 004012FA ; keygenme.004012FA
На этом первичное исследование кода закончено. Настало время продумать стратегию дальнейших действий, которые должны привести нас к кейгену.Code:00401185 . 812D 64304000>SUB DWORD PTR DS:[403064],A3C54A91 0040118F . 75 29 JNZ SHORT 004011BA ; keygenme.004011BA 00401191 . 812D 68304000>SUB DWORD PTR DS:[403068],C2F7E86A 0040119B . 75 1D JNZ SHORT 004011BA ; keygenme.004011BA 0040119D . 812D 6C304000>SUB DWORD PTR DS:[40306C],4084B41C 004011A7 . 75 11 JNZ SHORT 004011BA ; keygenme.004011BA 004011A9 . 812D 70304000>SUB DWORD PTR DS:[403070],96A2D976 004011B3 . 75 05 JNZ SHORT 004011BA ; keygenme.004011BA 004011B5 . E8 02000000 CALL 004011BC ; keygenme.004011BC
Атака перебором и радужные таблицы
Очевидно, что серийник должен обеспечивать правильную расшифровку байт по адресу 004011BC. Первоначально я хотел обратить функцию StrongCreateHash, чтоб по хэшу получать plain-text. Функция оказалась достаточно длинной, но через неделю я даже составил псевдокод на С её обратного аналога. Проблема в том, что работал бы этот аналог только лишь в том случае, если бы мне удалось решить систему нелинейных уравнений (соответственно, с большим количеством коллизий), а потом из получившихся вариантов отфильтровать прямым перебором правильные значения. Но куда уж мне – я даже уравнение длины серийника брутил. Так что со мной всё ясно – этот метод был от меня далёк, как секс от Советского Союза.
Но тут я понял, что, на самом деле, всё гораздо проще. CRC1 используется для манипуляций над зашифрованными байтами, и если мы точно знаем правильное значение CRC1, то мы однозначно получим правильные расшифрованные значения.
Если вы прокрутите немного вверх, то можете увидеть, что для декриптовки кода по адресу 004011BC используется ещё одно значение, названное мной как CRCfixed. Но, как я упоминал ранее, на самом деле, используется лишь младшее слово как CRC1, так и CRCfixed. Т.е. два младших слова обеих переменных. Но всё ещё упрощается, когда мы видим, что старшее слово CRC1 – это младшее слово CRCfixed.
Теперь суть ясна – наша задача методом перебора найти все валидные значения CRC1. «Почему все?» - спросите вы. Все потому, что их гораздо больше одного. Их много. Насколько много – навскидку сказать сложно, но после полного перебора мы будем знать точно.
Алгоритм перебора сводится к тому, что мы с помощью IDA Pro рипаем код StrongCreateHash, немного подправляем его, потому что в keygenme код этой функции настроен на жёстко прописанные в него смещения. далее делаем 4 вложенных цикла, каждый из которых ищет свой байт. В случае, когда все 4 байта, сложенные в правильном порядке, дают валидное значение CRC1, то мы записываем эти значения в файл, который я назвал RainbowTables.dat. Для чего нужен этот файл, и что он из себя представляет? Всё очень просто – это радужные таблицы.
Возможно, вы слышали, что радужные таблицы применяются для быстрого восстановления паролей из хэшей. В частности, радужные таблицы применяются против алгоритма md5, т.к. этот алго очень распространён на сегодня. Как выглядит их применение? Да очень просто – для наиболее популярных слов (god, admin, password и т.д.) вычисляются и сохраняются в таблице значения хэша. Далее, когда на вход поступает хэш, то его значение ищется в таблице, и если совпадение найдено – хэш удачно взломан. Думаю, преимущества и недостатки этого метода вы оцените сами.
У нас же радужные таблицы применяются в двух целях – для ускорения нахождения правильного значения CRC1, а также для рандомизации ключей. Сейчас я постараюсь объяснить, что я имел ввиду.
Понятное дело, что первый раз нам придётся перебирать значения CRC1, но это – достаточно длительная операция. У меня она занимает около 20 минут (2ГГц на двух ядрах Intel Pentium). Возможно, у вас она будет выполняться быстрее, а, возможно, и нет. Но представьте, что бы это был за кейген, который бы выдавал надпись: «Генерируется ключ, подождите 20 минут…». Радужные таблицы позволяют найти правильный CRC1 за долю секунды.
Второе – это получение разных ключей для одного и того же имени. Логично предположить, что если за основу ключа (а как мы позже увидим - CRC1 является основой ключа) берётся разное значение, то каждый раз и ключи будут разные. Радужные таблицы как раз и предоставляют возможность выбора этих самых разных значений.
Более подробно о программе генерации радужных таблиц повествуют её исходники, которые вы найдёте в приложении к статье.
Генерация правильных ключей
Зная правильное значение CRC1, обратить весь процесс проверки серийника несложно. Я хотел бы подробно пояснить лишь пару моментов.
Во-первых, нам всё-таки придётся обратить одну хэш-функцию – WeakCreateHash. Т.е. по определению если функцию можно обратить, то это уже не хэш-функция. Просто я назвал её так, потому что при беглом просмотре кода сложно было выявить, возможно ли её обратить. Сам обратный код можно найти в исходниках кейгена, поэтому я не буду здесь его приводить. Кстати, все вышеприведенные имена дублируются в кейгене, так что если где-то описывается WeakCreateHash или CRC1, то и в кейгене они имеют такой же смысл, как и в статье.
При написании обратной функции для WeakCreateHash я использовал Hex-Rays, поэтому задача обращения кода упростилась в разы. Правда, этот плагин не всегда корректно генерирует псевдокод, поэтому я рекомендую перепроверять результаты его работы на стадии отладки.
Следующим моментом, требующим пояснения, является генерация случайных чисел. Здесь я пошёл по пути наименьшего сопротивления (и наибольшего быстродействия) и использовал инструкцию rdtsc, что позволило получать более-менее случайные числа.
И последнее – в результате последним преобразованием будет преобразование, обратное тому, которое в самом начале делалось с серийником вот здесь:
Хотя я не делала никакой обратной функции, я поступил проще – написал аналог этой функции на С и перебором подбирал WORD’ы, соответствующие байтам после обращения WeakCreateHash. Перебор начинался со значения 0х3030, т.к. символы, лежащие ниже этого значения, иногда даже не хотели отображаться. Более того, кейген генерирует серийники, не выглядящие так уж красиво – иногда они содержат нечитаемые символы, а очень редко такие символы даже не копируются в буфер обмена (правда, в результате тестов у меня такое было только один раз, и после повторной генерации другой серийник уже нормально копировался), но, тем не менее, они работают, и даже дают такой вот результат:Code:004010FE . 8D75 BA LEA ESI,DWORD PTR SS:[EBP-46] 00401101 . 8BFE MOV EDI,ESI 00401103 > 66:AD LODS WORD PTR DS:[ESI] 00401105 . 3C 39 CMP AL,39 00401107 . 7F 04 JG SHORT 0040110D ; keygenme.0040110D 00401109 . 2C 30 SUB AL,30 0040110B . EB 02 JMP SHORT 0040110F ; keygenme.0040110F 0040110D > 2C 37 SUB AL,37 0040110F > 80FC 39 CMP AH,39 00401112 . 7F 05 JG SHORT 00401119 ; keygenme.00401119 00401114 . 80EC 30 SUB AH,30 00401117 . EB 03 JMP SHORT 0040111C ; keygenme.0040111C 00401119 > 80EC 37 SUB AH,37 0040111C > C0C0 04 ROL AL,4 0040111F . 02C4 ADD AL,AH 00401121 . AA STOS BYTE PTR ES:[EDI] 00401122 .^ E2 DF LOOPD SHORT 00401103 ; keygenme.00401103
И ещё – я писал выше, что использовал Pe Tools при написании кейгена. На самом деле, это громко сказано. Дело в том, что изначально я хотел написать кейген как консольное приложение. Но когда стало видно, какие там мутки с серийниками (т.е. какой они имеют неудобный для отображения вид) я решил написать графическое приложение. Но переделывать проект было лень, поэтому я добавил в начало вызов FreeConsole(), но меня раздражало это мигание, вот почему я с помощью Pe Tools подкорректировал поле Subsystem на GUI.
Финал
Вот и подошло к концу очередное повествование. Надеюсь, оно было интересным и полезным для вас. Как говорили в «Спокойной ночи, малыши»: «До новых встреч!». )))
Материалы к статье
ARCHANGEL © AHTeam, r0 Crew



Reply With Quote
Thanks

