Взлом криптографических инсталляторов

Как обычно, субботним вечером за ящиком пива я решил побродить по просторам Crackmes.de в поисках какого-нибудь новенького crackme. Честно сказать, я уже немного устал от этого обилия «плоских» защит. Большинство из них их я уже переломал вдоль и поперек. Хотелось чего-то новенького, пусть даже не суперсложного, но интересного :). И я нашел, что искал! Сегодня мы вместе сломаем софтину, упакованную в криптографическом инсталлере.

Взлом криптографических инсталляторов

Как обычно, субботним вечером за ящиком пива я решил побродить по просторам Crackmes.de в поисках какого-нибудь новенького crackme. Честно сказать, я уже немного устал от этого обилия «плоских» защит. Большинство из них их я уже переломал вдоль и поперек. Хотелось чего-то новенького, пусть даже не суперсложного, но интересного :). И я нашел, что искал! Сегодня мы вместе сломаем софтину, упакованную в криптографическом инсталлере.

Лиха беда начало

Итак, в архиве с крякмисом лежит еще один запароленный архив, ключ к которому и есть наша цель. Сам Elf в описалове говорит что-то про путешествия, склепы, тайники и секретное слово. Как ты понимаешь, путешествия и склепы заинтересовали меня меньше всего. Сейчас я покажу тебе экстремальное выковыривание секретного слова… Заранее предупреждаю, crackme сжат самописным упаковщиком, использует неплохую криптографию и содержит полиморфный код. Полиморфизм в данном случае выражается в том, что программа сама изменяет свой собственный код прямо во время выполнения.

Слабонервных, детей и беременных женщин просим листать дальше. Все остальные бегут в магазин за пивом и открывают PEID.

Осматриваемся

Итак, открываем кряк через PEID и видим, что он ничего не нашел. Ни пакера, ни протектора ни языка программирования. Плохо, но не смертельно. Идем в Olly и открываем нашего подопытного кролика. По «F9» запускаем на выполнение и тут же тормозим на ошибке доступа по адресу 00000000. Хорошо, в лоб не получается.

Как говорится, умный в гору не пойдет, умный гору обойдет (или на@#$т - примечание редактора). Запускаем крякми уже без Olly, а в Olly жмем «File -> Attach». Находим в списке наш процесс и цепляемся к нему. Сразу же вываливаемся в модуль htdll… Теперь нам нужно открыть содержимое памяти «View -> Memory». Там и лежит наш экзешник, к которому нужно прицепиться. После того как окажемся в нем, нажмем «Ctrl-A» для того, чтобы убедиться, что это то, что нам нужно.

Распаковка

Немного опишу довольно стандартную для всех пакеров процедуру распаковки.

Для того чтобы код, который ты сейчас видишь, поместить в экзешник, можно пойти двумя способами:

1. способ извращенцев и джедаев - вручную скопировать шестнадцатеричный дамп в память и также вручную в PE-заголовке указать точку входа :);

2. способ ленивых и мой - снять дамп с помощью плагина OllyDump и там же указать точку входа.

Извращенцы и джедаи опять бегут за пивом, а остальные читают дальше.

После того как окажемся в модуле crackme.exe, нажимаем «Plugins -> OllyDump -> Dump Debugged Process». В качестве OEP рекомендую поставить 100C. Почему? Я не смогу до конца ответить на этот вопрос - это приходит с опытом… Но из кода видно, что программа написана на чистой асме (хотя Elf это тоже не скрывал), и первое, что нужно программе, - это получить свой хэндл, что и делает GetModuleHandle, а SetUnhandledExceptionFilter работает для других нужд программы. Для каких - читай дальше.

Смело жмем «Dump» и сохраняем в новый файл. Из нового файла программа должна запуститься без проблем.

Все, на этом процесс распаковки закончен. И не надо спрашивать меня, почему мы не восстанавливали импорт и не удаляли лишние секции. Это тебе не ASPack или UPX. Этот пакер написан самим автором Crackme, то есть Elf’ом. Если тебе интересен код пакера, скажу заранее, исходники его и Сrackme лежат в запароленном архиве:). Так что стимул есть в любом случае!

Исследование

Немного поясню, чего от нас требует программа. В окне есть небольшой квадрат серого цвета, на котором мы должны поводить грызуном и в результате получить либо сообщение, либо строку с поздравлением (так я думал сначала). Но лично я не нашел в программе ни одной функции, которая бы работала с файлом и ставила на него пароль, хотя Elf конкретно говорит про секретное слово, а значит оно есть. В String References было чисто. Я искал его недолго. Скажу больше - я его вообще не искал. Это было бы слишком просто, да и вообще, я давно отвык от таких простых путей. Иногда, кстати, это очень даже зря. Но не будем отступать от темы.

Код программы довольно простой, и становится не совсем понятно, в каком месте может генерироваться какой-то набор символов. Ладно, посмотрим на программу в действии. Жмем «F9» и пробуем поводить грызуном по серому квадратику.

Опа! Произошло то, чего я ожидал меньше всего. Программа вывалилась по адресу 004011AA, где находится привилегированная инструкция INVD. Если говорить откровенно, я никогда раньше не встречался с этой инструкцией. Вот это я нашел в поисковике (цитата):

«Очистка внутренней кэш-памяти при сквозной записи (обнуление бит достоверности всех строк) осуществляется внешним сигналом FLUSH# за один такт системной шины (и, конечно же, по сигналу RESET). Кроме того, имеются инструкции аннулирования INVD и WBINVD. Инструкция INVD аннулирует строки внутреннего кэша без выгрузки модифицированных строк, поэтому ее неосторожное использование при включенной политике обратной записи может привести к нарушению целостности данных в иерархической памяти. Инструкция WBINVD предварительно выгружает модифицированные строки в основную память (при сквозной записи ее действие совпадает с INVD)».

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

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

Вскрытие показало, что больной умер от вскрытия

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

Я решил пойти старым проверенным способом, который уже не раз выручал меня в подобных ситуевинах. Если программа проходит адрес 004011AA, где раньше стояла INVD, то дальше можно пройти вручную по «F8». Хотя этот номер тоже прошел не с первого раза, в один прекрасный момент я попал на 00401319. Если честно, то я этого и хотел. Уж больно интересная концовка короткого кода там находилась.

По этому адресу располагался call, который оканчивался на 0040134C командой RETN (возврат). Ниже лежал не менее интересный код, представляющий собой цикл, а еще ниже валялся код, очень непохожий на обычные опкоды ассемблера. Скорее всего, этот участок был забит вручную, либо содержал какие-то промежуточные данные, использующиеся для нужд шифрования. Чуть выше второй точки возврата можно заметить зацикливающую команду LOOPD на адрес 0040135C. Сама команда находится по адресу 00401369. Если прочитать код повнимательнее, то очень легко просматривается следующая цепочка:

00401340 . 5E POP ESI

00401341 . 8BC3 MOV EAX,EBX

00401343 . F7EA IMUL EDX

00401345 > 3D 27132958 CMP EAX,58291327 ;Сравниваем eax с числом 58291327

0040134A . 74 01 JE SHORT __elf_cm.0040134D ;Если равно, прыгаем на 0040134D, то есть не возвращаемся из процедуры

0040134C . C3 RETN ;Если не равно, возвращаемся, откуда пришли

0040134D > BF 6B134000 MOV EDI,__elf_cm.0040136B

00401352 . 8BC2 MOV EAX,EDX ;Приходим сюда, если eax равен 58291327

00401354 . B9 15010000 MOV ECX,115

00401359 . C1E9 02 SHR ECX,2

0040135C > 8B1F MOV EBX,DWORD PTR DS:[EDI] <------

0040135E . 33D8 XOR EBX,EAX |

00401360 . 891F MOV DWORD PTR DS:[EDI],EBX |

00401362 . 03C7 ADD EAX,EDI | Циклимся, пока ecx не обнулится (ecx содержит количество проходов)

00401364 . 2BC3 SUB EAX,EBX |

00401366 . 83C7 04 ADD EDI,4 |

00401369 .^E2 F1 LOOPD SHORT __elf_cm.0040135C <-----

0040136B . C149 3B 78 ROR DWORD PTR DS:[ECX+3B],78

0040136F . D9E8 FLD1

00401371 . 68 ECCEDE48 PUSH 48DECEEC

00401376 . 07 POP ES

00401377 . C3 RETN

Ты спросишь, почему я выбрал именно этот кусок кода из общей кучи? Все очень просто - это единственное место, где стоит команда сравнения регистра с числом (CMP EAX,58291327).

Далее, схема очень проста. Меняем конструкцию:

00401345 > 3D 27132958 CMP EAX,58291327

0040134A . 74 01 JE SHORT __elf_cm.0040134D

на следующую:

00401345 > 3D 27132958 MOV EAX,58291327

0040134A . 74 01 JMP SHORT __elf_cm.0040134D

Зачем я поставил MOV EAX,58291327, если ниже идет безусловный переход? Дело в том, что контрольное число может быть как контрольной суммой, так и строкой, с которой дальше могут проводиться любые операции. Нет никакой гарантии, что, поменяв переход, мы получим в EAX то значение, в результате работы с которым не произойдет ошибки. В рамках этой процедуры регистр EAX более не затрагивается, но кто знает, куда может привести нас выход из процедуры. Лично мне не очень хотелось прослеживать цепочку. Гораздо правильнее вставить контрольное число явным образом.

Дальше, чтобы не пропустить самое интересное, я поставил точку останова на наш безусловный переход и активировал окно программы. Бряк не заставил себя ждать.

Интересное кино…

После остановки, понятное дело, зажимаем «F8», чтобы пройти цикл, и… о чудо! Все, что идет после 0040136F, прямо на глазах преображается в абсолютно нормальный ассемблерный код!!! Это и называется полиморфизм.

Важно не засмотреться на это чудо и успеть остановиться, когда ecx станет равным единице. Как только это произойдет, необходимо притормозить на секунду, чтобы изучить код. Olly сама анализирует программу только при открытии. Проанализировать ее еще раз можно нажатием «Ctrl-A». Так и сделай.

Теперь всмотрись в код как можно внимательнее, отключи воображение и включи соображаловку. Заметь, что первая API-функция начинается по адресу 004013BB, но после ее выполнения не стоит никакого перехода или call’а на функции, которые лежат ниже. Вместо этого там находятся неопределенные опкоды, на которых мы обязательно брякнемся, если продолжим выполнение.

Закрой глаза и нажми «F9». Потом открой, выпей с горя пива и перезапускай программу. Как и ожидалось, после наших хитрых манипуляций, программа повела себя не так, как должна была. Первая же команда, следующая за циклом, вызвала исключение. Почему, где и когда была ошибка, разбираться уже поздно. Мы зашли уже слишком далеко и не собираемся останавливаться. Вместо этого мы постараемся ручками поправить код таким образом, чтобы программа прошла по функциям, не вызвав ошибку.

Фиксим код

Как я говорил раньше, первая функция находится по адресу 004013BB. Значит, после цикла мы должны попасть сразу на нее. Есть только один способ сделать это - JMP 004013BB. Выходить из BitBlt тоже не спеши. Посмотри, где лежит следующая порция функций, а заодно обрати внимание на одну из них - DrawTextA. Как ты думаешь, что она делает? Элементарно, Ватсон! DrawTextA рисует на форме текст, указанный в параметре text. Если ты видишь то же самое, что и я, то тебе, наверняка, захотелось посмотреть на это творение в дампе. Щелкай на строке с текстом правой кнопкой, «Follow in Dump -> Immediate constant» (показать в дампе строковую константу). Самое вразумительное, что ты увидишь в дампе, начинается с адреса 00401384 («You sex text carved…»). Вот так мы и нашли тот текст, который нам нужен. Дело за малым. В параметре text поменяй адрес на тот, который нам нужен. Вот, что у тебя должно получиться:

0040142D . 6A 00 PUSH 0 ; /pDrawTextParams = NULL

0040142F . 6A 11 PUSH 11 ; |Flags = DT_CENTER|DT_TOP|DT_WORDBREAK

00401431 . 68 0C304000 PUSH __elf_cm.0040300C ; |pRect = 0040300C {5.,35.,305.,145.}

00401436 . 6A FF PUSH -1 ; |Count = FFFFFFFF (-1.)

00401438 . 68 84134000 PUSH __elf_cm.00401384 ; |Text = "You seе text carved in wall:

0040143D . FF35 50314000 PUSH DWORD PTR DS:[403150] ; |hDC = 0A0109A2

00401443 . E8 AA000000 CALL <JMP.&user32.DrawTextExA> ; \DrawTextExA

А теперь фокус-покус. Нажми еще раз «Ctrl-A» и возрадуйся, ибо весь код стал читабелен. И в связи с этим я предлагаю пересмотреть нашу траекторию полета. Посмотри на параметры функции BitBlt и обрати внимание, что один из них мы не захватили. Надо было читать MSDN. Теперь мы должны сделать переход от цикла к адресу 004013B6. Перезапускаем программу, вносим все изменения, жмем «Ctrl-A» и любуемся результатом. Да, мы сделали это!

Теперь окно Crackme значительно преобразилось. Но не спеши радоваться - это только начало конца. Нам дали подсказку, но не пароль от архива. Elf неплохо придумал, расположив буквы на доске в том порядке, что ты видишь в подсказке. Он разделил квадрат на девять маленьких квадратов, в которых размещены буквы. И теперь наша задача сводится к тому, чтобы найти нужное ключевое слово из набора имеющихся. Я помню, как в детстве любил такие головоломки…

Занавес открывается

Теперь у нас есть два варианта. Можно взять имеющийся набор символов, вписать в брутфорс и пойти пить пиво дальше, а можно посидеть и поводить мышью в оригинальном Crackme, подбирая нужную комбинацию. Повторяю для тех, кто в бронепоезде: экспериментировать нужно в оригинальном крякми. Причина в том, что мы многое поменяли после распаковки, и не факт, что оно будет работать правильно.

Я пошел вторым путем. После пары часов банального смыслового подбора я нашел то что искал. Пароль на архив – «ANTIVIVITUS?». Без кавычек, естественно.