Автор: (c)Крис Касперски
В настоящей книге все атаки рассматривались исключительно на примерах, специально написанных для демонстрации того или иного алгоритма программ "CrackMe". При этом многие из них были слишком искусственными и далекими от реальных защитных механизмов. Это было удобно для изложения материала, но не отражало реальных существующих защит.
Поэтому я решил включить в приложения некоторые примеры реальных взломов. Все эти программы широко распространены и отражают средний уровень защиты условно-бесплатных программ. Заметим, что он существенно ниже, чем многие из предложенных в книге реализаций.
Напоминаю, что взлом в той или иной мере конфликтует с российским и международным законодательством. Поэтому необходимо помнить, что взлом не освобождает от регистрации и может быть использован только в образовательных целях, но никак не для получения какой-либо выгоды. В этом случае конфликтов с законодательсвом не возникнет.
Рисунок 1. Логотип компилятора Intel C++.
Прежде чем приступать к обсуждению аспектов стойкости защиты компилятора Intel C++ 5.0.1, считаю своим долгом заявить, что я глубоко восхищен этим великолепным программным продуктом и ломать его, на мой взгляд, по меньшей мере кощунственно. Впрочем, сегодня только ленивый не найдет в Сети кряк (один только Google по запросу "Intel C++ crack" выдает свыше 12 тысячи ссылок!), так что никакого вреда от данной публикации не будет.
Немного грустных новостей для начала. Приобрести легальную версию данного компилятора для жителей России оказывается чрезвычайно затруднительно. И вопрос упирается даже не в то "сколько он стоит" (а стоит он, если мне не изменяет память, что-то в районе тысячи долларов) - компания Intel просто игнорирует данный сегмент рынка. Обращения в российское представительство компании с просьбой предоставить (за деньги!) данный компилятор для его же описания (читай - рекламы и продвижения) в книге "Техника оптимизации программ" положительных результатов не дали. Даже после того, как к этому вопросу подключились, прямо-таки скажем, не мелкие отечественные издательства BHV и Солон - Р. Ладно, не хотят продавать - ну и не надо! Благо, хоть с сервера компании можно свободно утянуть 30-дневный триал. Не густо, конечно, но для сравнительного тестирования вполне достаточно (а для других целей мне этот компилятор и не нужен!).
Впрочем, все оказалось не так просто! С Web-сервера компилятор запросто так не отдался, а после заполнения регистрационной формы меня вежливо поблагодарили и сообщили, что сейчас ко мне на "мыло" упадет письмо с триальной лицензией и инструкцией по ее установке. Это "сейчас" заняло у севера аж несколько дней (такое впечатление, что анкеты просматриваются вручную). Ок! Лицензия получена! Начинаем скачивать файл... Как это так - докачка не поддерживается?! А вот не поддерживается и все! Учитывая, что у меня лишь хлипкий Dial-Up по каналу в 19.200 (да и тот - по межгороду), скачать полста мегабайт без единого разрыва просто нереально. К тому же, работа над книгой уже близится к завершению и вносить в нее еще один компилятор (а значит, переписывать кучу текста заново) мне становится просто "влом". Да и Intel C++ - это далеко не самый популярный в кругах российских программистов компилятор и книга без него как-нибудь переживет (хотя, посмотреть, как Intel оптимизирует код под свои процессоры, очень хотелось, да и документация по компилятору вдохновляла). Самое смешное, что когда я все-таки скачал компилятор через своих московских знакомых (ну, для Москвы 45 мегабайт - это вообще ничто), он наотрез отказался работать, мотивируя свое поведение тем, что срок демонстрационной лицензии уже истек...
Разозлившись на весь свет (и на парней из Intel в частности), я отправился на ftp-сервер компании, откуда наскоро, всего за каких-то три дня, слил полнофункциональную (хотя и шибко несвежую) версию компилятора, находящуюся по следующему адресу: ftp://download.intel.com/software/products/downloads/C5.0.1-15.exe. (приятно, что ftp докачку исправно поддерживал и многократные разрывы никаких проблем не вызывали). Польстившись на размер, я скачал именно пятую версию компилятора, которая была в полтора раза легче шестой (под которую у меня имелась неиспользованная триальная лицензия) и аж в два раза компактнее седьмой - новейшей на момент написания этих строк версии, ломать которую из "политических" соображений, я все равно бы не рискнул, так зачем же ее зря качать?
Теперь, собственно, мы и подходим к известному философскому вопросу: этично ли ломать программный продукт уважаемой тобой компании или без этого можно обойтись? Да, если бы без этого было возможно обойтись, я бы, честное слово, без тени сожаления выложил за этот замечательный продукт пачку вечнозеленых, но, увы... компания не проявляет ко мне, как к покупателю, никакого интереса и, кроме как ломать, ничего другого просто не остается!
Итак, инсталлируем Intel C++ и, предварительно скопировав лицензию от шестой версии в папку \Intel\Licenses, запускаем головной файл программы:
Как и следовало ожидать: "could not checkout FLEX lm license" ("не могу проверить FLEX lm лицензию") - компилятор ругается и прекращает свою работу. Ага, стало быть программа защищена FLEX'ом - достаточно известным в хакерских кругах менеджером лицензий от компании Globetrotter Inc, представляющим собой достаточно продвинутую защиту интегрированного типа. Разработчик защищаемого приложения получает в свое распоряжение SDK, содержащее как тривиальные функции проверки валидности ключевого файла (лицензии), так и развитые средства динамической шифровки файла. При грамотном подходе к защите запустить защищенную программу без наличия соответствующей ей лицензии доподлинно невозможно. Если часть программы зашифрована, то пытаться расшифровать ее без ключа - дохлое дело. Правда, не факт, что парни из Intel действительно использовали шифрование, к тому же зашифрованные фрагменты иногда удается восстановить по косвенным данным. Это, смотря что еще зашифровано!
Разумеется, при наличии триальной лицензии шифровка снимается без труда, но в том-то все и дело, что триальной лицензии у меня не было! Тем не менее, надежда меня не покидала и, перекусив для смелости батоном докторской колбасы, сдобренной значительным количеством кетчупа, я запустил свой любимый дизассемблер IDA, и... не знаю у кого как, а у меня вид консольной IDA, распахнутой на весь экран, всегда вызывает чувство благоговения. Ок, ну-ка посмотрим, где скрываются те текстовые строки, которые выводятся при отсутствии лицензии на экран. Результат: ни "No such feature exists", ни "could not checkout" в ASCII-строках (т.е. тех строках, что сумел распознать автоматический анализатор IDA) не найдено. Хорошо, зайдем с другого конца. Нажимаем <F4> для переключения в hex-режим и давим <ALT-T> для поиска текстовых строк в "сыром" виде. Что ж, на этот раз поиск "could not checkout" увенчался успехом!
Нажимаем <F4> еще один раз для возврата в режим дизассемблера, подводим курсор к адресу 42D9C0h и нажимаем <A> для преобразования цепочки байт в ASCII-строку. В результате мы получаем:
А как узнать, кто же выводит строку-ругательство на экран? Нет ничего проще! Вновь переключившись в режим дизассемблера, по <F4>, давим <ALT-T> для поиска последовательности "C0 D9 40 00" - адрес строки, представленный в обратном (с учетом порядка следования старших байтов) виде. О-па! Мы видим код наподобие следующего:
Косвенный вызов строки! Ну, собственного, этого и следовало ожидать (иначе, с чего бы это автоматический анализатор IDA их не распознал?). Хорошо, преобразуем двойные слова в смещения, руководствуясь тем, что число "42h" должно выпадать на младший байт старшего слова (иначе адрес ссылки уйдет за диапазон предельно допустимых значений) и получаем:
Попробуем теперь найти тот код, что обращается к указателю (на ругательную строку), расположенному по адресу 420CE8h? Не надо спешить! По виду полученной таблицы смещений можно с уверенностью заключить, что прямого обращения к ее элементам не будет. Можно предположить, что числа, стоящие возле ссылок на строки - коды ошибок, а сами строки - соответствующие тексты сообщений. Если так, то с вероятностью, близкой к единице, разработчиками программы использовалась относительная адресация, т.е. для вычисления эффективного адреса элемента ее смещение в таблицы суммируются с базовым адресом таблицы - единственным адресом, который загружается явно.
Прокручивая экран дизассемблера вверх, мы внезапно натыкаемся на длинную последовательность нулей, интерпретируемую нами как начало таблицы:
Ага! Есть две перекрестных ссылки! Это хорошо. Теперь поднимемся по ним вверх, прямиком к вызывающему их коду? Можно, конечно, поступить и так, но есть и более универсальное решение - запустив SoftIce, мы устанавливаем точку останова на чтение ячейки 420DE8h (если вы еще не забыли - это адрес элемента таблицы, ссылающийся на искомую ругательную строку). Теперь, кто бы к ней не обращался, SoftIce обязательно всплывет и ведь действительно он всплывает! Пару раз отдав команду "P RET", поднимающую нас из дебрей глубоко вложенных процедур поближе к свету, наконец, мы взбираемся на вершину стека и очередной "P RET" приводит к завершению программы. Ок, повторяем все заново, делая на этот раз на один "P RET" меньше. Записываем любой из близлежащих адресов (пусть это будет для определенности адрес 4031C4h) и натравливаем на него IDA.
"Вот это да!", восклицаем мы, пришибленно уставившись на экран. Многое мы ожидали от IDA, но вот чтобы она, так запросто представила символьные имена защитных функций, все говорящие за себя: lc_chekout, lc_perror, lc_auth_data... Черт, возьми, как? Вдохновленные смутной надеждой, мы неуверенно подгоняем курсор к lc_chekout и нажимаем на <ENTER>.
Святой Кондратий! И это они еще называют защитой?! Все защитные функции вынесены в отдельную динамическую библиотеку (наверное, чтобы взломщику разбираться было легче?) - LMGR327A.DLL, в названии которой угадывается "Library ManaGeR", причем это штатные функции FLEX lm, описание которых можно найти в его же SDK (хоть SDK на FLEX ln с компилятором и не поставляется, найти его в Сети - плевое дело).
Отыскав в текущем каталоге этот самый LMGR327A.DLL, мы открываем его с помощью HIEW на предмет полного переписывания функции lc_checkout. Ну, насчет "переписывания" автор, ясное дело, загнул. Всего-то и требуется - заставить lc_checkout всегда возвращать нуль, для чего первые две команды ее тела должны выглядеть приблизительно так: "XOR EAX, EAX / RETN". Записываемся и с дрожью в сердце запускаем icl.exe на выполнение. Критическая ошибка приложения? А чего мы хотели?! Ведь теперь функция lc_auth_data получает неверные данные и гробит все к черту. Впрочем, не будем спешить. Беглое исследование процедуры sub_40A6F8 как будто не выявляет никаких следов шифрования и поэтому ее можно смело удалить, не забыв тоже самое, "на всякий пожарный" случай, проделать и с lc_auth_data (самое простое - впихнуть в ее начало RETN). Сохраняемся, запускам icl.exe и... компилятор работает! Все! Больше тут нечего ломать!
Самое забавное, что размер защитного механизма (413 Кб) в два с половиной раза превышает размер защищенной с его помощью программы (176 Кб)! Как говорится - no comment.
Рисунок 2. Логотип Intel Fortran Compiler.
Ситуация с этим компилятором вкратце такова. В процессе работы над третьим томом "Образа мышления IDA" я исследовал большое количество компиляторов на предмет особенностей их кодогенерации и вытекающих отсюда трудностей восстановления исходного кода. Не избежал этой участи и "Intel Fortran Compiler", обнаруженный на диске "Научись сам программировать на FORTRAN". Краткая аннотация на буклете гласила "Intel FORTRAN Compiler 4.5 - новейшая версия знаменитого компилятора. Для регистрации программы смотрите поддиректорию CRACK". Ну, на счет "новейшего" составители диска явно приврали, т.к. на тот момент уже вышла седьмая версия, да и CRACK оказался некорректным. Вместо того, чтобы ломать защиту, он ломал сам компилятор, необратимо его гробя. К счастью, оригинальный ifl.exe на диске все-таки имелся и это давало возможность заставить работать компилятор мне самому. В конце концов, использовать в коммерческих целях этот, бесспорно замечательный программный продукт, я все равно не собирался, а для серии тестовых прогонов не то что месяца (положенного мне по праву), даже нескольких дней было вполне предостаточно, поэтому с этической точки зрения ничего кощунственного я не совершал (просто мне очень уж не хотелось тянуть ~160 метров из Интернета - с моим междугородним Интернетом это действительно проблематично).
Итак, запускаем оригинальный файл компилятора на выполнение и лицезреем, как он "спускает на нас Полкана" (ругается, в смысле):
Ни слова о FLEX lm! (см. "Компилятор Intel С++ 5.0.1") и файл LMGxxx.DLL отсутствует. Странно! Похоже, что Fortran Compiler защищен иначе, что, собственно, и не удивительно, поскольку их делали разные группы.
Что ж, запускаем IDA и натравливаем на нее исполняемый файл, который, кстати, занимает всего 176,128 Кб, что с точностью до байта соответствует размеру Intel C++ 5.1 Compiler. Странно! Но, как бы там ни было, ASCII-строки "The evaluation period has expired" автоматический анализатор IDA в тексте дизассемблируемого файла так и не нашел. Что ж, тогда мы сделаем это сами. <F4>, <ALT-T>, "The evaluation period" и...
Теперь, вновь нажимаем <ALT-T> для поиска последовательности "20 A2 42 00" - адрес начала строки, заданный в обратном виде. Результат не заставляет себя долго ждать:
Переключаемся обратно в дизассемблер, трижды жмем <D> для преобразования цепочки байт в двойное слово, затем <O> для перевода его в смещение и... в результате таких манипуляций получаем приблизительно такую же таблицу, как и в нашем предыдущем случае с Intel C++:
А посему и действовать мы будем точно также: поставим бряк на адрес 0419390h и дождемся, пока отладчик не получит управления. Кстати, насчет отладчика. В момент написания этих строк у автора как раз закачивалась скачивание седьмой версии компилятора Intel C++ и от использования SoftIce пришлось воздержаться (в момент своей активации SoftIce полностью "замораживает" операционную систему, что пагубно влияет на Интернет, а точнее на установленные TCP/IP соединения). И вместо SoftIce автор решил для разнообразия использовать Microsoft WDB, который, кстати, справился со своей задачей ничуть не хуже.
Запускам WDB на выполнение, нажимаем <Ctrl-E>, указываем имя загружаемого файла, переходим в окно команд ("Command Window") и устанавливаем точку остановка на адрес 0419398h, для чего отдаем команду "BA r4 0x0419398" (что расшифровывается как: "Break on Access of Read 4 bytes long"). Затем, для продолжения выполнения программы пишем "G" и с полсекунды ждем...
Рисунок 3. Внешний вид отладчика MS WBD в процессе ломания программы.
Ага, отладчик говорит "Hard coded breakpoint hit" ("сработала аппаратная точка останова") и приостанавливает выполнение отлаживаемой программы. Сама же отлаживаемая программа к этому моменту уже успела вывести на экран:
Обратите внимание на строки, выделенные синим цветом! Очевидно, они свидетельствуют о том, что мы попали не в самое начало защитной процедуры, а где-то в ее середину. Кстати, а что у нас там лежит на стеке? Смотрим (~View Stack, см. Рис. 3). Всего три адреса - довольно неглубокий уровень вложения, не так ли? Причем (обратив свой взор к окну дизассемблера), сейчас уровень вложения еще понизится, т.к. следующей командой мы выходим из этой процедуры:
Теперь неспешно трассируем код, попеременно поглядывая то на дизассемблированный листинг, то на консоль отлаживаемой программы. Следующая трассируемая функция (внутрь которой мы не заходим, а "заглатываем" ее одним нажатием <F10>) выводит на экран "The evolution period has expired", но не завершает программу, а продолжает ее выполнение. Что ж! Тогда и мы продолжим трассировку! Вызов функции 040F5FEh проходит без каких либо внешних проявлений. Так и не поняв, зачем она собственно нужна, мы поднимается на еще один уровень вверх, куда нас забрасывает завершающий функцию RET:
...и таким образом мы трассируем код до тех пор, пока не наткнемся на следующую конструкцию:
Что в ней необычного? А то, что это первая встретившаяся нам материнская процедура, которая анализирует код возврата дочерней функции. В нашем случае регистр EAX содержит значение "ноль" и, стало быть, следующий условный переход выполняется. Но не тот ли это переход который нам нужен? Что ж, сейчас мы это узнаем - нажимаем клавишу <F10> еще несколько раз... Оп-ля! Наш условный переход перебрасывает нас на ту ветку программы, которая спустя несколько команд скоропостижно сдыхает, захлопывая окно программы. А что произойдет, если команду "JE" в строке 401064h заменить на противоположную (или, как вариант, просто удалить этот условный переход)? Пробуем...
Компилятор по прежнему смачно ругается на "evaluation expired", но... он работает! Работает!! Работает!!! По соображениям экономии экранного места (в самом деле, ругательство занимает чуть ли не половину экрана и смотрится крайне некрасиво) мы забиваем вызов процедуры 0409140h командами NOP. Проверяем - сработало ли? Ну... это смотря как посмотреть. Трехэтажный мат действительно исчез, но вот лаконичная строка "Evaluation Copy" так и осталась. Найдем что за код ее выводит? Зачем? Лучше найти саму эту строку и тем же HIEW"ом ее переписать во что ни будь более привычное, например: "hacked by mother-fucker guy". Переписываем, и... пользуемся компилятором в свое удовольствие, не забывая, однако, о том, что по истечении 30-дневного срока вы будете должны его стереть, в противном случае вы поступите очень и очень нехорошо, да и незаконно.
...компилятор Intel C++ 7.0 докачался глубокой ночью, часу где-то в пятом утра. Спать хотелось неимоверно, но и любопытство - была ли усилена защита или нет, тоже раздирало. Решив, что до тех пор пока не разберусь с защитой, все равно не усну, я, открыв новую консоль, и переустановив системные переменные TEMP и TMP на каталог C:\TEMP, наскоро набил неприлично длинное имя инсталлятора W_CC_P_7.0.073.exe в командной строке (необходимость в установке переменных TEMP и TMP объясняется тем, что в Windows 2000 они по умолчанию указывают на очень глубоко вложенный каталог, а инсталлятор Intel C++ - да и не только он - не поддерживает путей такого огромного размера).
Сразу же выяснилось, что политика защиты была кардинально пересмотрена и теперь наличие лицензии проверялось уже на стадии установки программы (в версии 5.x установка осуществлялось без проблем). Ок, даем команду dir и смотрим на содержимое того, с чем нам сейчас предстоит воевать:
Ага! Программа установки setup.exe занимает всего сорок с хвостиком килобайт. Очень хорошо! В такой объем серьезную защиту вряд ли спрячешь, а если даже и так - этот крохотный файл ничего не стоит проанализировать целиком до последнего байта дизассемблерного листинга. Впрочем, не факт, что защитный код расположен именно в setup.exe, он может находится и в другом месте, вот например... ChkLic.dll/ChkLic.exe, занимающими в совокупности немногим менее семисот килобайт. Постой, какой такой ChkLic? Это сокращение от Check License, что ли?! Гм, у ребят из Intel, очевидно, серьезные проблемы с чувством юмора. Уж лучше бы они назвали этот файл "Hack Me", честное слово! Ладно, судя по объему, ChkLic - это тот самый FLEX lm и есть, а с ним мы уже сталкивались (см. "Intel C++ 5.0 Compiler") и приблизительно представляем как его ломать.
Даем команду "dumpbin /EXPORTS ChkLic.dll" для исследования экспортируемых функций и... крепко держимся за Клаву, чтобы не упасть со стула:
Черт побери! Защита экспортирует всего одну-единственную функцию с замечательным именем CheckValidLicense. "Замечательным", потому что назначение функции становится понятным из ее названия и появляется возможность избежать кропотливого анализа дизассемблерного кода. Ну вот, отбили весь интерес... Уж лучше бы они ее по номеру экспортировали, что ли, или, по крайней мере, окрестили ее каким-нибудь отпугивающим именем типа DES Decrypt...
...Эх, размечтались! Ладно, вернемся к нашим баранам. Давайте рассуждать логически: если весь защитный код сосредоточен непосредственно в ChkLic.dll (а судя по "навесному" характеру защиты, это действительно так), то вся "защита" сводится к вызову CheckValidLicense из Setup.exe и проверке возращенного ею результата. Поэтому для "взлома" достаточно лишь пропадчить ChkLic.dll, заставляя функцию ChekValidLicense всегда возвращать... Да, кстати, что она должна возвращать? Точнее: какое именно возвращаемое значение соответствует успешной проверке лицензии? Нет, не торопитесь дизассемблировать setup.exe для определения - ведь возможных вариантов не так уже и много: либо FALSE, либо TRUE. Вы делаете ставку на TRUE? Что ж, в каком-то смысле это логично, но с другой стороны - а почему мы, собственно, решили, что функция CheckValidLicense возвращает именно флаг успешности операции, а не код ошибки? Ведь должна же она как-то мотивировать причины отказа устанавливать компилятор: файл с лицензией не найден, файл поврежден, лицензия просрочена и так далее? Хорошо, попробуем возвратить ноль, а если это не прокатит, возвратим единицу.
ОК, пристегивайтесь, поехали! Запускаем HIEW, открываем файл ChkLic.dll (если же он не открывается - трижды помянув сусликов, временно скопируем его в корневую или любую другую директорию, не содержащую в своем имени спецсимволов, которые так не нравятся HIEW). Затем, обратившись еще раз к таблице экспорта, полученной с помощью dumpbin, определяем адрес функции CheckValidLicense (в данном случае 010A0h) и через <F5>, "10A0" переходим в ее начало. Теперь режем по "живому", перезаписывая поверх старого кода "XOR EAX, EAX/RETN 4". Почему именно "RETN 4", а не просто "RET"? Да потому, что функция поддерживает соглашение stdcall, о чем можно узнать, взглянув в HIEW на ее эпилог (просто пролистывайте экран дизассемблера вниз до тех пор, пока не встретите RET).
Проверяем... Это работает!!! Несмотря на отсутствие лицензии, инсталлятор, не задавая лишних вопросов, начинает установку! Стало быть, защита пала. Ой, не верится нам, что все так просто и чтобы не сидеть, тупо уставившись в монитор в ожидании завершения процесса инсталляции программы, мы натравливаем на setup.exe свой любимый дизассемблер IDA. Первое, что бросается в глаза - отсутствие CheckValidLicense в списке импортируемых функций. Может быть, она файл ChkLic.exe как-то запускает? Пробуем найти соответствующую ссылку среди автоматически распознанных строк: "~View Names", "ChkLic"... Ага, строки "Chklic.exe" здесь вообще нет, но зато обнаруживается "Chklic.dll". Понятно, значит библиотека ChkLic загружается явной компоновкой через LoadLibrary. И переход по перекрестной ссылке подтверждает это:
Невероятно, но эта, до ужаса примитивная защита построена именно так! Причем полуметровый файл ChkLic.exe вообще не нужен! И чего ради стоило тащить его из Интернета? Кстати, если вы надумаете сохранять дистрибутив компилятора (внимание: я не говорил "распространять"!), то для экономии дискового места ChkLic.* можно стереть - либо пропатчив setup.exe, навсегда отучив его к ним обращаться, либо же просто создав свою собственную ChkLic.dll, экспортирующую stdcall функцию CheckValidLicence вида: int CheckValidLicence(int some_flag) {return 0;}
Так-с, пока мы все это обсуждали, инсталлятор закончил установку компилятора и благополучно завершил свою работу. Интересно запустится ли компилятор или все самое интересное только начинается? Лихорадочно спускаемся вниз по разветвленной иерархии вложенных папок, находим icl.exe, который, как и следовало ожидать, находится в каталоге bin, нажимаем <ENTER> и... Компилятор, естественно, не запускается, ссылаясь на то, что "icl: error: could not checkout FLEX lm license", без которой он не может продолжить свою работу.
Выходит, что Intel применила многоуровневую защиту и первый уровень оказался грубой защитой от дураков. Что ж! Мы принимаем этот вызов и, опираясь на свой предыдущий опыт, машинально ищем файл LMGR*.DLL в каталоге компилятора. Бесполезно! На этот раз такого файла здесь не оказывается, зато выясняется, что icl.exe сильно прибавил в весе, перевалив за отметку шестиста килобайт... Стоп! А не прилинковали ли разработчики компилятора, этот самый FLEX lm статической компоновкой? Смотрим: в Intel C++ 5.0 сумма размеров lmgr327.dll и icl.exe составляла 598 Кб, а сейчас одни лишь icl.exe занимает 684 Кб. С учетом поправки на естественное старческое "ожирение", цифры очень хорошо сходятся. Значит, все-таки FLEX lm! Ой-ой! А ведь теперь без символических имен функций ломать защиту будет намного труднее... Впрочем, не будем раньше времени паниковать! Давайте думать, только спокойно! Вряд ли команда разработчиков полностью переписала весь код, взаимодействующей с этой "конвертной" защитой. Скорее всего, ее "усовершенствование" одной лишь сменой типа компоновки и закончилось. А раз так, то шансы взломать программу по-прежнему велики!
Памятуя о том, что в прошлый раз защитный код находился в функции main, мы, определив ее адрес, просто устанавливаем точку останова и, дождавшись всплытия отладчика, тупо трассируем код, попеременно поглядывая то на отладчик, то на окно вывода программы: не появилось ли там ругательное сообщение? При этом все встретившиеся нам условные переходы мы отмечаем на отдельном листке бумаги (или откладываем в своей собственной памяти, если вы так хотите), не забыв указать, выполнялся ли каждый условный переход или нет... Стоп! Что-то заболтались мы с вами, а ведь ругательное сообщение уже выскочило! Ок, хорошо! Посмотрим, какой условный переход ему соответствовал. Наши записи показывают, что последним, встретившимся переходом, был условный переход JNZ, расположенный по адресу 0401075h и "реагирующий" на результат, возращенный процедурой sub_404C0E:
Очевидно, что sub_404C0E и есть та самая защитная процедура, которая осуществляет проверку лицензии на ее наличие. Как ее обхитрить? Ну, тут много вариантов... Во-первых, можно, вдумчиво и скрупулезно проанализировать содержимое sub_404C0E на предмет выяснения: что именно и как именно она проверяет. Во-вторых, можно просто заменить JNZ short loc_40107F на JZ short loc_40107F или даже NOP, NOP. В-третьих, команду проверки результата возврата TEST EAX, EAX можно превратить в команду установки нуля: XOR EAX, EAX. В-четвертых, можно пропатчить саму sub_404C0E, чтобы она всегда возвращала ноль. Не знаю как вы, но мне больше всех приглянулся способ номер три. Меняем два байта и запускаем компилятор. Если никаких других проверок его "лицензионности" в защите нет, то программа заработает и, соответственно, наоборот. (Как мы помним, в пятой версии таких проверок было две). Поразительно, но компилятор больше не ругается и работает!!! Действительно, как и следовало ожидать - его разработчики ничуть не усилили защиту, а напротив - даже ослабили ее!
...и угораздило же меня приобрести "писец" (то бишь CD-RW) в OEM-поставке! И ведь спрашивал продавца: а где, позвольте, тут пишущий софт или, по крайней мере, драйвера? На что продавец, удивленно так пожимая плечами, ответил - какие драйвера? Втыкаете - работает. А пишущие программы подходят любые, вот купите в соседнем магазине диск с Nero CD. Мне - обладателю retail-"писца" от PHILPS, еще тогда это показалось странным, поскольку я хорошо помнил, что диск с драйверами в коробке PHILPS'а был, а Easy CD Creator - непосредственно сам пишущий софт, - располагался совсем на другом диске. Но ведь как-то же справляются с OEM-продукцией другие люди, подумал я, и... купил.
Наскоро воткнув новехонький 40-скоростной NEC в свой компьютер, я был немало удивлен, когда Nero CD наотрез отказался признать его "писцом". Не помог тут и Easy CD Creator, взятый с Филечкиного CD. Провозившись битый час и, ничего ровным счетом так и не выяснив, я, зверски разозленный на продавца, решил сделать ход конем, установив NEC на компьютер с "девяносто восьмой" Windows, вернув PHILPS'а себе. Никаких изменений! Собравшись было отдавать привод назад продавцу, я неожиданно вспомнил, что в одном из последних номеров "Компьютер Пресс" был обзор пишущих программ, причем демонстрационные версии всех этих программ содержались на прилагаемом к журналу компакт-диске. Из всех программ NEC'овый писец опознала лишь одна: Record NOW, которая, к счастью, не имела никаких функциональных ограничений, исключая, правда 30-дневный триальный период. Причем программа оказалась такой уютной и удобной, что расставаться с ней мне не захотелось, но и расставаться со своими деньгами мне не хотелось тоже.
Как выглядит защита? При каждом запуске программа выводит противный nag- screen, напоминающий сколько дней ей еще "жить" осталось, и тем самым страшно нервирующий. Хорошо, ищем фразу "Number of days remaining in evaluation" во всех файлах программы и, если наша искалка поддерживает юникод, быстро выясняется, что данный текст содержится в файле lockers.dll, открыв который любым редактором ресурсов, мы обнаруживаем в нем тот самый заветный диалог! Остается выяснить: кто же выводит этот диалог на экран? Ищем строку "lockres.dll" во всех файлах программы. ОК, это lockout.dll. Да... и эти разработчики не в ладах с юмором. Запускам dumpbin и смотрим список экспортируемых функций:
Сурово! Во-первых, обращает на себя внимание пара функций DES Encrypt/DES Decrypt, что-то (как и следует из ее названия) зашифровывающая/расшифровывающая. Во-вторых, тройственный подход к наименованию функций наводит на мысль, что мы имеем дело с "конвертной" защитой, разработаной независимо от защищенной с ее помощью программы и поддерживающий все основные языки программирования: Си/Си++, Дельфи и, конечно же, Visual Basic, узнаваемый по суффиксу VB. В-третьих, такое обилие всевозможных проверочных функций предвещает, что исследование защиты и защищенной программы окажется делом отнюдь нелегким! Причем, в те три сотни килобайт, которые занимает файл lockout.dll можно много всяких ловушек и хитростей понапхать, так что на скорый успех нам рассчитывать не приходится. Но... глаза страшатся, а руки делают. Начнем с того, что посмотрим - какие именно функции защитной библиотеки использует программа.
Вот тебе и раз! Защищенная-то программа состряпана на Визуальном Бейсике, о чем красноречиво свидетельствует единственная, явно загружаемая ею библиотека MSVBVM60.DLL! Ах, так?! Хорошо, пойдем напролом. Просто удаляем lockout.dll из каталога программы и подсовываем ей любую другую DLL, предварительно переименованную в данную. Запускаем программу. На экране незамедлительно появляется сообщение об ошибке: среда Visual Basic'а ругается, что не может найти функцию EvalModeTestVB. Что ж, это уже кое-что! Загружаем lockout.dll в дизассемблер, находим в нем эту самую "Eval", быстро выясняем, что она является "переходником" к EvalModeTest, которая... которая... Ой-ой-ой, которая занимает до черта килобайт и содержит в себе крайне запутанный, с большим количеством глубоко вложенных друг в друга процедур программный код. Да чтобы проанализировать все это, и месяца не хватит! А кто сказал, что этот код вообще следует анализировать?! Достаточно просто подсунуть нужный код возврата и все! Весь вопрос в том - какой именно код нужный. Беглый просмотр содержимого функции показал, что существуют, как минимум три различных кода возврата: "0", "2" и "3". Если это так, то скорее всего одному из них соответствует состояние "программа не зарегистрирована, но лицензия еще не истекла", "программа не зарегистрирована и лицензия уже истекла", и, наконец, "программа зарегистрирована". Что ж, на перебор трех вариантов не уйдет много времени! Взяв в руки HIEW, переписываем код защитной функции "с нуля": XOR EAX, EAX/RETN.
Возвращаем lockout.dll на ее прежнее место, запускаем Record NOW и... не можем поверить свои глазам - программа исправно работает! "Исправно" - в том смысле, что nag-screen уже не выводится и по истечении положенных тридцати дней писец попрежнему живет, а не умирает.
Хорошо, а если бы разработчик защищенного приложения не поленился бы воткнуть проверку на успешность загрузки функции EvalModeTestVB и при ее отсутствии немотивировано прекращал свою работу? Смогли бы мы тогда узнать - какие функции библиотеки lockout используется, а какие нет? Уговорили! Взломаем программу другим путем! Подгоняем курсор к MyCDPro.exe и, нажав на <F3>, пытаемся найти lockout.dll прямым контекстным поиском. Вот, пожалуйста:
Рисунок 4. Поиск ссылки на lockout.dll в защищенной программе.
Прямым текстом: "lockout.dll" и рядышком с ней EvalModeTestVB. Имена остальных защитных функций в исследуемой программе отсутствуют. Самое забавное, что в модуле lockout.dll присутствует огромное количество строк типа: "User has turned back their clock, so calculating days based on last and init", "The CRC file is valid", "Failed to update the Last Accessed time" - т.е. защита составлена довольно грамотно и в состоянии как следует за себя постоять. Если, конечно, разработчик защищаемого приложения использовал все, предоставленные ею возможности сполна. Увы, этого не произошло и на этот раз...
Баста! Надоело! Все эти уродские защиты... (см. описания четырех предыдущих взломов) только портят настроение и еще, чего доброго, вызывают у читателей смутное сомнение: а не специально ли автор подобрал такие простые программы? Может быть, он, автор, вообще не умеет ничего серьезного ломать?! Уметь-то он (вы уж поверьте) умеет, но публично описывать взлом "серьезных" программ - боязно, а в "несерьезных" хороших защит мне как-то и не попадалось. Хотя, стоп! Ведь есть же такой программный продукт как UniLink, созданный опытнейшим системщиком Юрием Хароном (хорошо известным всем членам тусовки FIDO7.SU.C-CPP; если же вы никогда не заглядывали туда ранее, не поленитесь, сходите на Google, поднимите архив конференции и почитайте. Уверяю вас, вы не пожалеете). Достаточно сказать, что один лишь bag-list на UniLink - настоящий кладезь информации, перечисляющий больше количество ошибок операционной системы и ее окружения.
Наша цель - отучить UniLink ругаться на trial expired при запуске (из уважения к Харону необходимо отметить, что взлом проводится исключительно из спортивного интереса и природного любопытства. Какие либо корыстные цели тут не причем - линкер абсолютно бесплатен и может быть свободно скачан по следующему адресу: ftp://ftp.styx.cabel.net/pub/UniLink/ulnbXXXX.zip, где XXXX - номер версии). Цитирую со слов Харона - "Любая бета через полтора месяца начнёт 'ругаться', что, мол, она - expired :). Сделано это, просто как напоминание, в силу заинтересованности в том, чтобы тестировались последнии билды". Так что, ломая линкер, помните, что взлом еще не освобождает от beta-тестирования ;-).
Несмотря на бесплатность линкера, Харон очень неплохо его защитил. Во всяком случае, у меня на полный анализ защиты (включая развернутое описания взлома и отвлечения на повседневную текучку) ушла добрая неделя! Сейчас, когда пишутся эти строки, даже жалко, что защита так быстро сломалась и то интересное, во что еще можно вонзить свои зубы, закончилось. Впрочем, лучше отложим всю эту ностальгию до лучших времен в сторону и вспомним, как эта неделя "эротических развлечений с защитой", собственно, и начиналась...
...Привычным движением руки загружаем исполняемый файл линкера в свою любимую IDA 4.1.7. и... IDA грязно ругается по поводу того, что... "can't find translation for virtual address 00000000, continue?". Хм, ну что нам еще остается делать - покорно жмем "Yes", чтобы сделать "continue". Увы! Наш фокус не увенчался успехом - на экране возникает еще одно ругательство "File read error at 0004C7AC (may be bad PE structure), continue?". Обречено жмем "Yes" и... ...IDA просто исчезает. Да-да! Именно исчезает, даже не успев перед смертью выдать сообщение о критической ошибке!!! (В последующих верcиях IDA это было исправлено.)
Интересный формат файла, однако! Пытаясь выяснить, что же в нем содержится такого нехорошего, что так не понравилось IDA, мы решаем натравить на него утилиту dumpbin. Ага, разбежались! При попытке вывести таблицу импорта dumpbin выдает сообщение о внутренней ошибке: "DUMPBIN: error: Internal error during DumpImports", и, только что успев скинуть контекст, аварийно прекращает свою работу. Вот, значит, как?! Ну, защита, держись! Сейчас мы заглянем внутрь тебя "вручную" каким-нибудь низкоуровневым инструментом. Ну, например, HIEW...
Облом-с! При попытке сделать "prepare import data" HIEW скручивает дулю и, выдав нам на прощание трогательно красное окошко с надписью "Import name No free memory" банально виснет. Конкурирующий с ним QVIEW умирает и вовсе без каких либо пояснений. Утилита PEDUMP от Мэта Питтрека (известнейшего исследователя недр Windows) хоть и не виснет, но выдает сообщение о критической ошибке приложения и автоматически прибивается операционной системой. Так, чем еще можно исследовать внутренний формат PE-файла? На ум приходит efd (Executable File Dumper) от Ильфака, но даже эта утилита не справляется. Выдав сообщение "Can't find translation for 000002F6 (758)", она просто прекращает свою работу. Dump PE от Clive Turvey поступает аналогично. Дизассемблер De Win от Милюкова - виснет. Win DASM не виснет, но и не дизассемблирует. Даже знаменитый PROCDUMP распаковывать этот файл отказывается, правда позволяет сделать rebuild PE-заголовка, однако, после такой операции полученный файл становится неработоспособным. В общем, этот список можно продолжать бесконечно.
Кошмар! Защиты, срывающие крышу отладчику - это я еще понимаю, но вот чтобы так агрессивно сопротивляться дизассемблеру! Причем, не какому-то одному, конкретно взятому дизассемблеру, а всем дизассемблерам сразу. И в это же самое время защита ухитряется работать в любой, Windows-совестимой операционной системе, включая NT и w2k, а значит никаких грязных хаков не использует. Харон, по определению, гений!
Вот мы и столкнулись с тем самым случаем, когда приходится дизассемблировать не готовым дизассемблером, а своими собственными руками и головой! (Вообще-то, анализировать PE-заголовок руками я ринулся чисто с перепугу. Тот же EXEVIEW от Randy Kath пусть и не совсем корректно обрабатывает защищенный файл, но, по крайней мере, не виснет и не завершает свою работу. К тому же он распространяется вместе с исходниками (см. MSDN) и у нас есть возможность оперативно исправить баг). Тяпнув для храбрости пивка, запускаем IDA и загружаем нашего подопытного в бинарном режиме, то есть без анализа заголовков файла. Файл, естественно, успешно загружается. Теперь, открываем свой MSDN на странице "Microsoft Portable Executable and Common Object File Format Specification" и вдумчиво читаем все, что там написано. Без четкого представления о структуре и порядке загрузки PE-файлов, Харонову защиту нам ни за что не сломать. Если чтение фирменных спецификаций вызывает проблемы, попробуйте обратиться к сторонним источникам. В том же MSDN содержится масса статей, посвященных исследованию PE-формата, в частности: "The Portable Executable File Format from Top to Bottom" by Randy Kath, русский перевод которой ("Исследование переносимого формата исполнимых файлов сверху вниз") легко найти в Сети. На худой конец, можно обойтись и одним лишь заголовочным файлом WINNT.H, входящим в штатный комплект поставки любого Windows-компилятора (но разобраться с "голым" WINNT.H сумеет лишь гений!)
Наша задача состоит в том, чтобы вручную проанализировать все заголовки, все секции и все поля исследуемого файла, пытаясь определить: что же такого необычного есть в каждом из них. Спрашиваете: "необычное" - это вообще как? Навскидку можно предположить по крайней мере три варианта: а) защита использует документированные, но малоизвестные возможности PE-файлов, не поддерживаемые распространенными дизассемблерами; б) защита использует недокументированные особенности (и/или поля) PE-файлов, не поддерживаемые дизассемблерами, но корректно обрабатываемые операционной системой; в) разночтения спецификаций PE-формата привели к тому, что разработчики ОС трактовали отдельные поля заголовков по-своему, а разработчики дизассемблеров - по-своему, в результате чего появилась возможность создать такой извращенный файл, корректно загрузить который сумеет одна лишь система, а все остальные исследовательские программы конкретно обломаются на его анализе.
Из пункта "а" со всей очевидностью следует, что для анализа защищенного файла одной лишь документации явно недостаточно, ведь нам требуется не только убедиться в соответствии всех полей исследуемого файла фирменной спецификации, но и выяснить насколько эти поля вообще типичны. Другими словами, нам необходим практический опыт работы с PE-файлами, а если его нет - что ж, возьмите несколько заведомо неизвращенных PE-файлов и основательно проштудируйте их от пола до потолка.
С пунктом "б" справится сложнее. Допустим, в фирменной спецификации такое-то поле помечено как неиспользуемое, а в защищенном файле здесь прописано некоторое значение. Как быть? (Дизассемблировать загрузчик операционной системы не предлагать). Да очень просто! Берем HIEW старой версии - той, которая ничего не знает о PE и никак его не анализирует, и перебиваем "неиспользуемое" поле нулями или любым другим значением, пришедшимся нам по вкусу. Если это не нарушит работоспособности защищенного файла - по всей видимости это поле действительно не используется и, соответственно, наоборот.
Пункт "в" еще более сложен. Никакие прямолинейные решения тут не действуют и все, что нам остается - вдумчиво читать каждую букву исходной спецификации и... нет, не стремиться "понять" ее, а пытаться представить себе: как она вообще должна быть понята, чтобы загрузчик операционной системы работал, а дизассемблер - нет. Дайте волю своему воображению, напрягите интуицию - всех многих тонкостей PE-форматов составители документации просто не описали. С другой стороны, сами разработчики ОС данный формат не с потолка брали и по тем же самым спецификациям его и реализовывали. Задумайтесь, а как бы вы реализовали загрузку PE-файла в память? Какие бы комбинации свойств PE-файла вы могли бы использовать для его защиты?
Первое, что нам приходит в голову - инициализация некоторых критических ячеек памяти посредством добавления их адреса в таблицу перемещаемых элементов. А что, это мысль! Особенно привлекательной в этом плане выглядит таблица перемещаемых элементов из old exe - заглушки, расположенной перед PE-файлом и большинством дизассемблеров просто игнорируемой. Но обращает ли системный загрузчик внимание на эти элементы или нет - вот ведь в чем вопрос! Хорошо, давайте посмотрим на восстановленный old exe заголовок, извлеченный нами из защищенного файла.
Баста, карапузики! Нас обломали! Никаких перемещаемых элементов в DOS-заглушке нет, о чем поле e_ovno красноречиво и свидетельствует (в дизассемблерном листинге оно выделено синим шрифтом). Да и во всех остальных отношениях old exe заголовок выглядит вполне корректным и приличным. Ладно, лиха беда начало! Отталкиваясь от значения поля e_lfanew, переходим по содержащемуся в нем смещению на заголовок PE-файла.
Вот мы и выяснили, что PE-заголовок защищенного файла не содержит абсолютно ничего интересно, и если кто и завешивает HIEW и срывает IDA крышу, то уж точно не он. Что ж, сделав короткий перерыв (для пивка), продолжим наше утомительное исследование формата PE-файла, на сей раз взявшись за так называемый опциональный заголовок (optional header), следующий за концом PE-заголовка.
...и опциональный заголовок не содержит ничего интересного, но вот IMAGE DATA DIRECTORY, расположенная за ним следом - дело другое и буквально с третьей по счету строки мы выходим на след защиты:
Вот она - ссылка на таблицу импорта, ту самую таблицу, которая приводит к буйному замешательству огромное количество дизассемблеров и срывает крышу всем PE-утилитам вместе взятым. Посмотрим на нее?
Пошла вода в хату! Оказывается, в таблице импорта вместо нормальных полей содержится какой-то голимый "мусор", который кое-что проясняет. С такой таблицей импорта дизассемблеры работать просто не могут и... если проверка корректности содержимого таблицы импорта отсутствует, они виснут, в противном же случае аварийно прерывают свою работу с сообщением об ошибке.
Но это совершенно не объясняет - как с такой защитой ухитряется работать загрузчик операционной системы? Уж не имеем ли мы дело с некоторыми недокументированными особенностями? Или, быть может, по этим "мусорным" адресам в оперативной памяти расположено что-то особенное? Последнее навряд ли! Поскольку защита успешно функционирует во всех Windows-подобных системах, представляется сомнительным, что содержимое данных адресов всегда и везде одно и то же (кстати, беглая проверка отладчиком, это допущение с треском опровергает). Недокументированные возможности? Хм, не похоже... но, даже если и так - где прикажете искать реально импортируемые адреса?! Ладно, двигаемся дальше, может быть нам и повезет...
Ага! Держи Тигру за хвост! Защита использует документированное, но малоизвестное поле bound import, представляющее собой альтернативный механизм импорта функций из DLL. Смотрим, что у нас там...
Вот это - уже явно не мусор, а вполне удобоваримая таблица импорта, загружающая динамическую библиотеку kernel32.dll и импортирующая.... Как это так - никаких функций?! Странно... Но ведь защита все-таки работает (пусть, час от часу становится все менее и менее понятно - как?). Хорошо, давайте рассуждать логически. Программ, не импортирующих никаких функций, под Windows NT существовать в принципе не может. Даже если защита использует native API (т.е. обращается к системным функциям напрямую через прерывание 2Eh), операционный загрузчик окажется не в состоянии загрузить такое приложение, поскольку ему необходимо, чтобы на адресное пространство загружаемого процесса была спроецирована библиотека kernel32.dll. Это в Windows 9x, где системные библиотеки автоматически отображаются на адресные пространства процессов, "голые" файлы работают безо всяких проблем, а в NT, отображающей только явно загруженные библиотеки, такой фокус уже не проходит. А, знаете, это многое объясняет! Теперь становится понятно, в частности, почему таблица импорта не содержит в себе ни одной функции - они просто не нужны! Ссылка на kernel32.dll присутствует лишь затем, чтобы спроецировать эту библиотеку на адресное пространство процесса, как этого требует системный загрузчик. Хорошо, но как быть с "мусором" в стандартной таблице импорта? Как ни крути, а с такими извращениями системный загрузчик скорее удавится, чем обработает... Увы, нам нечего ответить на этот вопрос и, скрепя сердце, его вновь приходится откладывать, надеясь, что последующий анализ отделит свет от тьмы и все расставит по своим местам...
Вот мы и добрались до каталога сегментов! IMAGE HEADER секции ".text" выглядит вполне типично и никаких подозрений у нас не вызывает, но вот следующая за ним секция ".data" очень многое проясняет...
"Ну и что здесь интересного?" - спросит иной читатель. А вот что - присмотритесь повнимательнее: куда именно грузится содержимое данной секции? Если верить выделенной жирным шрифтом строке, то по адресу IMAGE_BASE + 0x4B000. Ничего не напоминает? Во-первых, адрес 0x4B000 "волшебным" образом совпадает с адресом "мусорной" таблицы импорта (те, кто поимел секс с защитой, этот адрес надолго запомнят, кстати, Харону не мешало бы его немножко замаскировать, чтобы он не так бросался в глаза). Во-вторых, изобразив процесс проецирования секций графически (см. Рис. 5), мы с удивлением обнаружим, что секция .data расположена не следом за секцией .text (как это обычно и бывает), а находится внутри нее. Действительно, давайте подсчитаем: виртуальный адрес секции .text равен 0x1000, а ее размер - 0x49817, и последний байт секции приходится на адрес 0x59817, что превышает виртуальный адрес секции .data, равный 0x4B000.
Так вот оно что! Поскольку секции отображаются на память в порядке их перечисления в каталоге (недокументированно, но факт!), то содержимое секции .data затирает область адресов 0x4B000 - 0x4E008! А что там у нас расположено?! ТАБЛИЦА ИМПОРТА!!! В дисковом файле по смещению 0x4B000 действительно расположен чистейшей воды мусор (и это косвенно подтверждается тем, что изменения первых 0x14 байт работу программы не нарушают), а истинная таблица импорта расположена непосредственно в секции .data, которой соответствует смещение 0x49E00 дискового файла. Заглянем - что у нас там?!
Вот, это действительно похожее на таблицу импорта со ссылкой на IAT. Кстати, не мешает посмотреть, что за функции импортирует IAT. Подгоняем курсор к "IAT" и, нажав на <ENTER>, смотрим:
Мать родная! Ну почему ты не родишь меня обратно?! Опять вместо символических имен (или на худой конец - ординалов) нам попадается этот проклятый мусор! Хотя - подождите минуточку - давайте попробуем определить, что будет расположено по данному адресу после загрузки программы? Возвращаясь к описанию секции .data, мы обнаруживаем, что упустили один очень важный момент. Виртуальный размер секции .data (0x3008 байт) намного больше ее физического размера (0x14 байт) и поэтому регион 0x4B014 - 49E008 будет заполнен нулями, а ведь "мусорная" IAT как раз и расположена по адресу 0x4B014! Следовательно, после загрузки ее содержимое окажется заполнено одними нулями, что соответствует пустой таблице импорта функций. Фу-х! Невероятно, но мы действительно в этом разобрались!!! Кстати, подобный прием широко используется и авторами упаковщиков исполняемых файлов.
Аналогичным образом поступает и секция .rsrc, внедряясь в середину секции .text (но секцию .data она не перекрывает), причем для ослепления некоторых дизассемблеров тут используется еще один хитрый прием: указанный "физический" размер секции .rsrc "вылетает" за пределы дискового файла. Системному загрузчику - хоть бы что, а вот некоторые исследовательские утилиты от этого и крышей поехать могут.
Рисунок 5. Динамическое замещение таблицы импорта в процессе загрузки PE-файла.
Настало время проверить наши предположения на практике. Давайте загрузим эту извращенную программу отладчиком и посмотрим, что содержится в памяти по адресу IMAGE_BASE + 0x4B000 = 0x44B000: мусор или нормальная таблица импорта? Отладчик SoftIce (как это и следовало ожидать) обламывается с отладкой этого извращенного файла, просто проскакивая точку входа, а вот WDB сполна оправдывая репутацию фирмы Microsoft (это не ирония!), пусть и не без ругательств, но все-таки загружает наш подопытный файл и послушно останавливается в точке входа.
Обратите внимание на выделенную жирным шрифтом строку. Отладчику показалось, что отлаживаемая программа импортирует некоторые функции... из самой себя! Но мы-то, излазившие защищенный файл вдоль и поперек, хорошо знаем, что за исключением kernel32.dll, никаких других экспортируемых и/или импортируемых библиотек здесь нет и такое поведение отладчика, судя по всему, объясняется все тем же самым "мусором". Ок, переключаем свое внимание на окно с дампом памяти, заставляя его отобразить содержимое таблицы импорта:
Ура! Открываем на радостях пиво! Содержимое памяти доказательно подтверждает, что загрузка файла действительно происходит именно так, как мы и предполагали! Хорошо, но что же нам теперь делать? То бишь, найти-то причину помешательства дизассемблеров мы нашли, но вот как ее нейтрализовать? Ну, это не вопрос! Достаточно лишь скопировать 0x14 байт памяти с адреса 0x49E00 по адресу 0x4B000 и скорректировать ссылку на IAT, направив ее на любое заполненное нулями место.
...HIEW теперь заглатывает защищенную программу и даже не пикает! А IDA... а IDA по прежнему отказываться обрабатывать этот файл и с завидным упорством слетает. В чем же причина? Вы, конечно, будете смеяться, но истинный виновник есть ни кто иной, как Microsoft! Если бы не ее жутко прогрессивная платформа .NET... А, впрочем, чего это я разворчался? Сами смотрите:
Вот это да! Сроду такого не было! Чтобы IDA, да неправильно опознала формат файла? Перемещаем радио-кнопку на одну позицию вниз (ведь мы имеем дело отнюдь не с Microsoft Net assembly, а с PE!) и... IDA успешно открывает файл. Причем с восстановлением таблицы импорта можно было и не возиться - IDA просто ругнулась бы на мусор и все! Но кто ж знал?! Задним умом все мы крепки...
Короче, возвращаясь к нашим баранам (в данном случае - к терпеливо ожидающему нас отладчику), в точке входа дизассемблерный текст выглядит так:
Не очень-то это похоже на осмысленный код программы! Может быть, это снова мусор? Маловероятно - ведь отладчик использует штатный системный загрузчик PE-файлов и потому показывает образ файла таким, какой он в действительности есть, ну... если, конечно, защита тем или иным образом не противостоит отладке. Ладно, отставив разговорчики в строю, начинам трассировать код и... с первых же строк впадаем в некоторое замешательство. Защита опрашивает начальное значение регистра EAX, которое (если верить отладчику!) как будто бы равно нулю, но полной уверенности в этом у нас нет. Еще со времен старушки MS-DOS многие отладчики славились тем, что самостоятельно инициализировали регистры после загрузки, чем и выдавали себя (в частности, при нормальной загрузке файла регистр SI содержал в себе адрес первой исполняемой команды, а при загрузке под отладчиком Turbo Debugger и иже с ним, был равен нулю). Вообще-то, закладываться на "предопределенные" значения регистров - дурной тон. Никто не гарантирует, что в следующих версиях Windows что-нибудь не изменится, и если такое вдруг произойдет, то защита откажет в работе, обломав не только хакеров, но и легальных пользователей. Впрочем, начальное значение регистра EAX(AX) по жизни равно нулю и с некоторой натяжкой за это можно зацепиться.
Далее защита непонятно зачем увеличивает старшее слово, только что закинутое в стек, на единицу и вызывает абсолютно бесполезные команды XLAT, DAA, ADD, SBB и... загружает регистр флагов в EAX. Уж не пытается ли она этим самым обнаружить флаг трассировки? Затем делает RETN для передачи управления по адресу: (0x42CFAE + 0x10000) + 0x9753 == 0x446701:
...отладчик доходит лишь до RETF и после этого сразу же "дохнет". К тому же, остается совершенно непонятным - что же, собственно, делает этот запутанный и витиеватый код? При желании, конечно, с ним можно разобраться, но... нужно ли? Ведь отладить нашу подопытную мы все равного не сможем, во всяком случае в WDB.
Хорошо, зайдем с другого конца. Предположим, что программа работает с операционной системой не напрямую (через native API), а через подсистему win32 (win32 API). Тогда, установив точку останова на любую API-функцию, вызываемому программой, мы автоматически попадем в гущу "нормального" программного кода, уже распакованного (расшифрованного?) защитой. Весь вопрос в том: какие именно API-функции вызывает программа. Ну, пусть это будет GetVersion, с вызова которой начинается стартовый код практически любой программы. Запускаем SoftIce, нажимаем <Ctrl-D>, даем команду "bpx GetVersion", выходим из отладчика, вызываем unlink.exe и... ничего не происходит! Отладчик не всплывает! Выходит, исследуемая нами программа не использует GetVersion! Что ж, удаляем предыдущую точку останова и пытаемся "забрейкать" CreateFileA (ну должен же линкер как-то открывать файлы!!!). Так, <Ctrl-D>, bpx CreateFileA<ENTER>, x<ENTER>... Ура! Это срабатывает! Отладчик перехватывает вызов защищенной программы и, после выхода из тела CreateFileA по команде P RET (в CreateFileA для нас действительно нет ничего интересного), мы оказывается в следующем коде:
Обратите внимание: несмотря на отсутствие таблицы импорта, программа каким-то загадочным образом все-таки импортирует из kernell32.dll все необходимые ей API-функции. Очень хорошо! Секс с native API и прочими извратами программистской хитрости отменяется! И мы остаемся в среде привычной нам подсистемы win32 API. Как именно осуществляется импорт - вот это уже другой вопрос! Кстати, давайте заглянем в одну такую функцию дизассемблером:
Смотрите! В дисковом файле адресов импортируемых функций просто нет и таблица импорта, судя по всему, заполняется защитой динамически. А это значит, что в дизассемблере мы просто не сможем разобраться: какая именно функция в какой точке программы вызывается? Или... все-таки сможем?! Достаточно просто скинуть импорт работающей программы в дамп, а затем просто загрузить его в IDA! Затем, отталкиваясь от адресов экспорта, выданных "dumpbin /EXPORTS kernel32.dll", мы без труда приведем таблицу импорта в нормальный вид. Итак, прокручивая экран дизассемблера вверх, находим, где у этой таблицы расположено ее начало или нечто на него похожее (если мы ошибемся - ничего странного не произойдет, просто часть функций останется нераспознанными и когда мы с ними столкнемся лицом к лицу, эту операцию придется повторять вновь). Вот, кажется, мы нашли, что искали, смотрите:
Условимся считать адрес 0044CC14h началом. Используя точку останова на CreateFileA, вновь вламываемся в программу и, отключив окно "data" командой wd, скидываем таблицу импорта в хистори: "d 44CC14". Выходим из Айса, запускаем NuMega Symbol Loader и записываем историю команд в файл winice.log (или любой другой, по вашему вкусу). И как со всем этим нам теперь работать? Рассмотрим это на примере функции "call dword_44CC78". Прежде всего, мы должны выяснить - какое значение находится в загруженной программе по адресу: 0x44CC87? Открываем winice.log по <F3> и смотрим:
Теперь, обратившись к таблице экспорта kernel32.dll, определяем: а) базовый адрес ее загрузки (в данном случае: 0x77E80000); б) имя функции, сумма RVA и IMAGE BASE которой совпадает со значением 0x77E8668C. Вычитаем из 0x77E8668C базовый адрес загрузки - 0x77E80000 и получаем: 0x668C. Ищем строку 0x668C простым контекстным поиском и...
...это оказывается ни кто иной, как GetLastError, что и требовалось доказать. Конечно, восстанавливать весь импорт вручную - крайне скучно и утомительно. Но кто нам сказал, что мы должны это делать именно вручную?! Ведь дизассемблер IDA поддерживает скрипты, что позволяет автоматизировать всю рутинную работу (подробнее о языке скрпитов можно прочитать в моей книге "Образ мышления - дизассемблер IDA").
Ок, еще один барьер успешно взят. Воодушевленные успехом и доверху наполненные выпитым во время хака пивом, мы продолжаем! В плане возвращения к нашим баранам, сосредоточим свои усилия на загрузчике таблице импорта, расположенном, по всей видимости, где-то недалеко от точки входа. Несмотря на то, что SoftIce по-прежнему упорно проскакивает Entry Point, обламываясь с загрузкой защищенного файла (впрочем, другие версии SoftIce с этим справляются на ура), мы можем легко обхитрить защиту, просто воткнув в точку входа бряк-поинт. Поскольку, бряк-поинт должен устанавливаться во вполне определенном контексте, используем уже известную нам нычку с CreateFileA. Итак, "bpx CreateFileA", <Ctrl-D>, запускаем unlink и, когда SoftIce "всплывает" даем: "bpx 0x446673" (адрес точки входа), выходим из SoftIce и... запускаем ulink вновь. Отладчик тут же всплывает:
Знакомые места! Трассируем код до тех пор, пока нам не встретится подозрительный RETF (от RET FAR - далекий возврат), передающий управление по следующему адресу:
Судя по адресу, этот код принадлежит непосредственно самой операционной системе (а точнее - NTDLL.DLL) и представляет собой функцию KiUserExceptionDispatcher. Но что это за функция? Ее описание отсутствует в SDK, хотя поиск по MSDN обнаруживает пару статей Мета Питтрека, посвященных механизмам функционирования SEH и функции KiUserExceptionDispatcher, в частности.
Структурные исключения! Ну конечно же! Какая защита обходится без них? Ладно, разберемся, ворчим мы себе под нос, продолжая трассировку защиты дальше. Увы! В той же точке, где WDB терял над программой контроль, SoftIce просто слетает. Ах, вот значит как? Ну, защита, держись!!!
Продолжение следует...