Теоретические основы крэкинга

Куда попадают данные.


 

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

 

Нетрудно заметить, что любые данные, хранящиеся на диске, в действительности предназначены для обработки какой-либо программой, а, стало быть, рано или поздно будут загружены в оперативную память (если, конечно, это полезные данные, а не мусор, подлежащий удалению), где над ними будут производиться всяческие действия. А результаты этих действий так или иначе отобразятся в мире «по нашу сторону экрана» при помощи одного из многочисленных устройств ввода-вывода, чтобы пользователь мог их увидеть, услышать или ощутить каким-либо иным образом. И пока эти данные будут проходить свой непростой путь от загрузки с винчестера до отображения на экране монитора, их можно «выловить» из адресного пространства программы или даже с экрана (то есть, конечно, не совсем с экрана, а из видеопамяти). Более того, информация, которую очень непросто расшифровать, разглядывая файл в шестнадцатиричном редакторе, при загрузке соответствующей программой нередко бывает представлена в памяти в виде структур с весьма незамысловатым внутренним устройством. Да и само наблюдение за процессом загрузки данных может дать множество полезной информации, и в этой главе мы посмотрим, как такую информацию можно извлекать.

 

Когда-то давным-давно, когда Windows еще был девяносто пятым, защиты – простыми, а авторы защит - наивными, серийные номера извлекались из программ следующим образом: устанавливались точки останова на все функции WinAPI, при помощи которых мог считываться серийный номер (благо их не так много).
Затем нужно было вызвать окно регистрации, ввести в него любые данные и посмотреть, какая из точек останова сработает. Дальше начиналось самое интересное: поскольку то были старые добрые времена, непуганые разработчики для проверки правильности серийных номеров частенько использовали обычное сравнение двух текстовых строк, причем для сравнения использовался банальный вызов функции lstrcmp (или ее самодельного аналога), два параметра которой являлись указателями на сравниваемые строки. И чтобы получить правильный серийник, требовалось лишь найти нужную функцию и посмотреть на ее параметры.   Конечно, те времена давно прошли, и ныне очень, очень редко встречаются программы, в которых серийный номер хранился бы в открытом или «как бы зашифрованном» при помощи команды XOR виде. Но для крэкера как раз важен не столько сам факт хранения данных в открытом виде, сколько идея: скормив программе заведомо неверные данные, пронаблюдать за тем, как программа эти данные будет «переваривать» и проверять на корректность. Да и наблюдение за процессом «заглатывания» данных программой может стать источником ценных идей. Приведу пример из собственной практики.   Однажды я изучал некую программу на предмет «исправить пару переходов, чтобы она лучше и дольше работала». Нужную «пару переходов» я вычислил за считанные минуты, а патчинг этих байт непосредственно в памяти успешно решал мою проблему на время одной сессии работы с программой. Но вот исправление тех же байтов в исполняемом файле неизбежно приводило к «падению» программы сразу после запуска. Нетрудно было догадаться, что программа неким образом контролировала собственную целостность, и, скорее всего – проверкой контрольной суммы. Это предположение подтверждалось и подозрительно большим временем загрузки программы (компьютеры тогда были намного медленнее, поэтому иногда следы работы защитных средств были видны, что называется, невооруженным глазом). Решение тоже было достаточно очевидным – найти функцию вычисления контрольной суммы, посмотреть, какой результат эта функция должна была возвращать в норме и либо обойти сравнение реальной контрольной суммы с эталоном, либо заставить функцию возвращать эталонное значение в любом случае.


Но как найти нужную функцию?   Для начала я попытался выяснить, каким образом программа проверяет свою контрольную сумму – сканирует образ непосредственно в памяти, или все-таки проверяет то, что лежит на диске. Поскольку программа была не запакована (в те времена упаковщики вообще встречались нечасто), я просто загрузил программу при помощи loader’а из состава SoftIce (одна из полезных крэкеру функций этого loader’а как раз в том, что он передает управление отладчику сразу после загрузки подопытной программы в память).  Затем я поставил аппаратные точки останова на чтение тех байт, которые я хотел изменить в файле (тут логика проста: если программа проверяет саму себя в памяти, то для этого ей придется прочитать себя) и на запись (на всякий случай) и отпустил программу на волю (то есть на исполнение). Ни одна из точек останова не сработала, из чего следовало, что программа либо не проверяет себя в памяти, либо это очень хитрая программа, которая на мою уловку не попалась. Запустив программу под filemon’ом, я увидел, что сразу после запуска эта программа поблочно читает свой собственный исполняемый файл, что навело меня на мысль о встроенной в программу проверке контрольной суммы. Дальнейшее было делом техники: прогнав программу под Bounds Checker’ом, я выяснил, что нужный мне вызов функции чтения из файла в действительности производится не из самой программы, а из DLL, которая в случае успешной проверки возвращала некое значение (а в случае неуспешной проверки – тоже значение, но уже другое) и что для работоспособности программы величина этого значения было критически важной. В этой ситуации я счел наилучшим решением выкинуть вычисление контрольной суммы файла (это ощутимо ускорило загрузку) и немного «помог» этой DLL всегда возвращать нужное мне значение.   О чем эта история? Ну разумеется, не о том, что глупо помещать код проверки в DLL, где его несложно поправить. Прежде всего, я хотел показать, как наблюдение за переходом данных из «мертвого» состояния в «живое» (а именно таким переходом и является поблочная загрузка файла для вычисления контрольной суммы) может помочь обнаружить защитные механизмы.


Действительно, стоило мне понаблюдать за процессом проверки целостности файла (о котором я ранее ничего не знал, кроме факта его наличия) под API-шпионом, как я сразу же получил информацию о типе защиты и местонахождении защитной процедуры. А после недолгих экспериментов и размышлений я также узнал, какова величина контрольной суммы программы до и после внесения в нее модификаций.   Вылавливание нужных данных из оперативной памяти уже давно стало неотъемлемой частью крэкинга и получило весьма широкое распространение. Если Вы уже пробовали самостоятельно взломать или хотя бы посмотреть на внутренности какой-либо программы, то, возможно, уже столкнулись с упаковщиками исполняемых файлов (или, если быть до конца точным, с файлами, обработанными такими упаковщиками). Разумеется, крэкеру во всех этих упаковщиках и навесных защитах интересно одно: методы их снятия. Очевидно, что упаковка программ – процесс обратимый и проблема лишь в том, чтобы найти способ обращения этого процесса, проще говоря – распаковать ее. Существует два подхода к распаковке. Можно проанализировать алгоритмы работы встроенного в программу навесного модуля, осуществляющего раcпаковку и самостоятельно воспроизвести эти алгоритмы в виде независимой программы. Этот метод обычно долог и труден.   А можно оставить все хлопоты по распаковке «навесному» модулю-распаковщику, встроенному в исполняемый файл, а потом воспользоваться результатами его трудов, «выдернув» распакованную и готовую к употреблению программу из памяти компьютера. В таком подходе мне определенно видится изящество и утонченность – вместо того, чтобы брать штурмом алгоритмы распаковки, мы, фактически, заставляем автора навесной защиты сражаться с собственным творением. Конечно, авторы защит тоже не дремлют – редкий упаковщик не уродует до неузнаваемости таблицу импорта, не содержит в антиотладочного кода или средств противодействия дампингу, и преодоление этих трудностей требует от крэкера гораздо больших усилий, чем собственно снятие дампа.


Однако сама идея как нельзя лучше раскрывает тему этой главы – если нет возможности (сил, времени, желания) «расшифровать» нужные данные вручную, стоит подумать о том, где можно найти готовые алгоритмы декодирования, каким образом их применить и как воспользоваться результатом их работы.   Особенно интересные и впечатляющие результаты дает сочетание предлагаемой технологии с глубоким патчингом программ в памяти. Недавно мне в руки попался экземпляр MoleBox –представителя (надо сказать, не самого совершенного) нового поколения защит, где упаковке подвергается не только исполняемый файл приложения, но и все остальные файлы, входящие в комплект программы, после чего все эти упакованные файлы сливаются в один монолитный исполняемый файл («ящик» в терминологии MoleBox). Сам EXE-файл программы модифицируется таким образом, что вызовы функций API для работы с файлами подменяются  вызовами внутренних функций защиты, после чего программа может одинаково успешно обращаться как к файлам на жестком диске, так и к файлам, находящимся внутри «ящика» (в MoleBox к файлам из «ящика» возможен доступ только на чтение).  Кстати, базовая информация о принципах работы MoleBox честно приведена в документации к программе, поэтому позволю себе в очередной раз повторить совет внимательно читать документацию к исследуемым программам. После недолгих экспериментов удалось выяснить, что «виртуальная директория», в которой работает защищенное приложение, содержит все файлы программы, и извлечь их оттуда не составляет никакого труда. При помощи манипуляции значениями регистров и содержимым стека в SoftIce мне удалось вызвать FindFirstFile/FindNextFile и вручную прочитать список имен всех файлов, находящихся в «ящике» программы, кроме самого исполняемого файла (который пришлось выковыривать более традиционными методами). Дальше все было еще проще в теории и еще тяжелее и нуднее на практике: выделение памяти под буфер, чтение файлов в этот буфер и последующее сохранение в другой файл.




Конечно, проделывать все эти операции вручную – занятие крайне трудоемкое, и если Вы захотите повторить мой эксперимент, я советую Вам не упражняться в играх с регистрами, а набросать соответствующую программку на ассемблере, внедрить ее в адресное пространство «жертвы», и получить тот же самый результат, но в несколько раз быстрее.   Еще одно применение предлагаемого метода – декодирование данных, имеющих сложную или неочевидную структуру. Например, при сохранении множества записей, содержащих как текстовую, так и числовую информацию, формат результирующего файла может быть совершенно неочевиден. К примеру, массив структур, состоящих из одного текстового (обозначим его буквой T) и одного числового поля (обозначим его как N) может сохраняться в файле как минимум двумя способами:   T1, N1, T2, N2, T3, ТN3, … или как N1, N2, N3, … T1, T2, T3, …, где Tn, Nn – текстовое и числовое поле соответственно n-й записи в массиве. Поскольку текстовые данные отличить от числовых несложно даже по внешнему виду, в данном конкретном примере никаких сложностей с извлечением из файла элементов массива скорее всего не возникнет. Но представим, что текстовых полей – несколько, а сохраняемые в этих полях значения – внешне очень похожи. И что каждая из структур в массиве содержит подструктуры, сохраняемые в том же самом файле подобным же образом. Задача расшифровки внутреннего формата файла уже не кажется такой тривиальной, не правда ли?   Однако вспомним наш краткий курс психологии программиста и попробуем представить, как нормальный программист организует хранение тех же данных в памяти. Скорее всего, он создаст банальный массив структур, поэлементно заполнит его значениями из файла и будет обращаться к нему так, как обращаются к любым другим массивам. Если логика задачи предполагает, что в загруженные данные потребуется добавлять новые элементы или удалять имеющиеся, то вместо обычного массива скорее всего будет одно- или двухсвязный список, в котором каждый элемент помимо собственно структуры будет содержать еще указатель на предыдущий и последующий элементы списка.


А дальше… Дальше задача полностью аналогична описанной в предыдущей главе задаче по «раскалыванию» неизвестного формата и извлечению данных, с той лишь разницей, что работать мы будем не с кодом приложения, а с обрабатываемыми этим приложением данными. Увы, данный прием не универсален – например, буферизованная обработка данных (т.е. такая, при которых обрабатываемые данные не переносятся в память целиком, а подгружаются по мере необходимости) не позволит расшифровать весь исследуемый файл целиком за один раз. Вообще, количество всевозможных «особых случаев» весьма велико, и рассмотреть их все в этой небольшой работе практически нереально. И если Вам придется столкнуться с такой нетривиальной программой, успех будет полностью зависеть от Вашей сообразительности, настойчивости и терпения.   С технической точки зрения изучать данные в памяти гораздо менее удобно, чем препарировать исполняемый файл на «винчестере», да и инструментов для интеллектуального поиска данных в чужом адресном пространстве не так уж много. Кроме того, если информация загружается в динамически выделяемые области памяти, исследуемые данные будут от запуска к запуску «плавать» по адресному пространству программы, располагаясь каждый раз по новым адресам. А это будет совсем уж нехорошо – проделывать массу рутинной работы только из-за того, что программу угораздило выделить очередной кусок памяти на сотню байт выше, чем в предыдущий сеанс. Поэтому позаботимся о создании элементарных удобств для работы. Прежде всего нам понадобится перенести наше поле деятельности из чрезвычайно нежной оперативной памяти на гораздо более жесткий диск, где можно будет проводить любые эксперименты не опасаясь, что случайное нажатие «не той» клавиши приведет к потере результатов длительных исследований. Вы наверняка уже догадались, что самым простым решением было бы снятие дампа с нужных областей памяти и сохранение этого дампа на «винчестере». Увы, все далеко не так просто, как хотелось бы.   Во-первых, Вам придется озадачиться поиском подходящего инструмента.


Классические дамперы из крэкерского арсенала Вам не помогут, поскольку они предназначены для снятия дампа программы, но не данных с которыми эта программа работает. Более того, случайный захват данных для классического дампера – явление крайне нежелательное, поскольку основное назначение дамперов – распаковка программ и получение работоспособного EXE-файла с минимальным количеством избыточной информации внутри, а не анализ «мусора», перемалываемого программой в процессе ее работы. А вот нам нужен именно этот «мусор». Поэтому придется либо писать программу для «правильного» дампинга своими силами, либо извлекать нужные данные вручную при помощи отладчика, позволяющего сохранять содержимое кусков памяти в файлах.   Во-вторых, данные могут быть разбросаны по адресному пространству программы, но сдампить их нужно за один сеанс отладки. Не так уж редки программы, в которых часть информации хранится, к примеру, в секции инициализированных данных, а другая часть – в динамически выделяемых блоках памяти, и если сдампить содержимое динамической памяти программы, но забыть про инициализированные данные, такой дамп скорее всего можно выбросить. Нет никакой гарантии, что при следующем запуске, когда Вы выясните, что указатели на динамические блоки находятся в секции инициализированных данных, и что эту секцию тоже нужно дампить, эти указатели будут указывать на те же адресам, что и в прошлый сеанс работы. Поэтому я настоятельно не рекомендую экономить место на диске, и делать полный снимок всех секций программы, поскольку лишнее всегда можно выкинуть, а вот недостающие данные взять будет неоткуда.   И в-третьих, снимая дамп, никогда не забывайте записывать базовые адреса тех кусков памяти, которые Вы дампите – в самом ближайшем будущем они Вам определенно понадобятся.   Но допустим, что мы аккуратно сделали полный снимок подопытной программы, и теперь все ее секции аккуратно разложены на нашем винчестере в идеальном порядке. Что дальше? Возьмите хороший шестнадцатиричный редактор и загрузите в него какую-либо из секций.


Теперь настройте этот редактор так, чтобы вместо смещений в файле он показывал смещения относительно базового адреса этой секции в памяти. То есть если Вы сбрасывали на винчестер кусок памяти с адреса 401000h по 402000h, после соответствующей настройки смещение первого байта файла должно отображаться именно как 401000h, а не как 0. В частности, такую операцию умеет выполнять HIEW: для этого необходимо нажать Ctrl-F5 и ввести новую базу. Если Ваш шестнадцатиричный редактор делать такие фокусы не умеет, значит, Вы выбрали недостаточно хороший шестнадцатиричный редактор и Вам будет заметно сложнее постигать разверзнувшиеся перед Вами глубины программы. Возможно даже, что несовершенство Вашего инструментария подвигнет Вас на написание нового, уникального шестнадцатиричного редактора с доселе невиданными возможностями – великие дела совершались по куда менее значительным поводам, чем отсутствие подходящего инструмента для копания в кодах. В принципе, можно обойтись даже без автоматического пересчета базового смещения, но тогда Вам придется проделывать необходимые вычисления в уме, и за всей этой шестнадцатирично-арифметической рутиной Вы можете не прочувствовать до конца всю силу и эффективность предлагаемого метода.   После того, как Вы проделаете все, о чем я говорил, внимательно посмотрите на экран монитор, включите на полную мощность свое воображение и представьте, что Вы разглядываете не кучу байтов, тонким слоем рассыпанных по поверхностям жесткого диска, а мгновение из жизни программы, которое Вы вольны сделать сколь угодно долгим. И то, что Вы видите в окне шестнадцатиричного редактора, по сути, ничем не отличается от того, что Вы бы увидели в окне отладчика, разглядывая память «живой» программы. Вы точно так же можете, следуя указателям, бродить по адресному пространству, дизассемблировать куски кода, искать константы и переменные (поскольку «замороженная» переменная есть ни что иное, как константа) по их значению – в общем, делать с программой все то, о чем я говорил в двух предыдущих главах.   Однако помимо «честных» методов поиска, требующих хотя бы минимального знания о структуре и типизации искомых данных, есть еще один  нехитрый прием, не требующий ничего, кроме терпения.


Суть метода проста: Вы вырываете из файла на жестком диске небольшой кусок и пытаетесь найти точно такой же кусок в памяти программы. При некотором везении в памяти идентичный кусок обнаружится, а его местоположение укажет Вам, куда программа загрузила соответствующие байтики из файла. После этого Вы можете попытаться логически проанализировать наблюдаемую картину либо просто влепить аппаратную точку останова на чтение всей прилегающей памяти (это будет очень, очень большая «точка») и посмотреть, что будет делать с данными подопытная программа. Несмотря на то, что такой поиск внешне сильно напоминает пресловутый «метод научного тыка», в его основе лежит вполне логичная идея: если информация из файла переносится в память без потерь и существенных изменений, соответствующие элементы структур в файлах и в памяти будут идентичны. Проще говоря если где-то в файле хранилось двухбайтное число 12345, есть вероятность, что оно и в памяти будет выглядеть двухбайтным числом 12345. Хотя, конечно, вполне возможны программы, загружающие числа типа «байт», но обрабатывающие их как 10-байтные с плавающей точкой. Разумеется, этот метод можно усовершенствовать, заметно повысив его эффективность: например, брать не случайные куски, а содержащие осмысленный текст – тогда Вы будете знать, что ищете текстовое поле, на которое почти наверняка будет существовать указатель, а сам этот указатель скорее всего будет входить в структуру, а структуры будут организованы в массив или список… Как видите, хотя набор базовых приемов не так уж велик, и каждый из них отнюдь не свободен от недостатков и ограничений, но, комбинируя и адаптируя их под особенности конкретных программ, можно весьма многого добиться.   Рассмотренные выше техники добывания данных из недр «живой» программы имели одно общее свойство – по отношению к программе их можно было охарактеризовать как «принуждение». Посудите сами – программа спокойно себе работает, никого не обижает, но тут в ее спокойную и размеренную жизнь врывается крэкер с отладчиком наголо и начинает направо и налево дампить секции и разбрасывать точки останова.


Такой подход, конечно, приносит свои плоды – но в некоторых случаях проблемы извлечения данных проще решать не «грубой силой», даже если это сила интеллекта, но хитростью, использованием всевозможных лазеек в коде программы, или даже через нетривиальное использование стандартных средств ОС или самой программы. Практика показала, что если программу вежливо и в изысканной форме попросить, она вполне может поделиться с Вами нужными Вам данными.   В Windows роль вежливых просьб играют системные сообщения – традиционное средство, используемое для огромного количества всевозможных действий – от элементарного закрытия окна до рассылки программам уведомлений о выходе операционной системы из «спячки», иначе именуемой Hibernate. Сила сообщений в Windows весьма велика, и, овладев и правильно распорядившись ей, можно получать весьма интересные результаты. Например, при помощи сообщений можно вытащить все строки из выпадающего списка (ComboBox) или таблицы (ListView), если автор программы забыл предусмотреть в своем детище более традиционный способ сохранения данных. Что для этого нужно? Только документация и некоторые навыки в программировании с использованием WinAPI. А теперь мы плавно перейдем от теории к практике и рассмотрим пример того, как можно применить эту технику для решения конкретной задачи. Но для начала – немного истории.   Как-то раз у меня возникла необходимость получить полный список имен всех сообщений Windows и числовых значений, которые за этими именами скрываются. Задача, надо сказать, была совсем не праздная – этот список был мне жизненно необходим, чтобы включить его в состав моих программ. Но вот незадача – в заголовочном файле из комплекта поставки MASM32 эти имена были разбросаны по всему windows.inc в совершенном беспорядке, и меня совершенно не радовала перспектива проявлять чудеса трудолюбия, вручную выискивая и обрабатывая несколько сотен строк. Полный список, разумеется, можно было бы извлечь из заголовочных файлов последней версии Visual Studio, но, кроме того, что я вообще не являюсь поклонником данного продукта, в частности у меня не было никакого желания искать где-то дистрибутив оной «студии» и устанавливать его ради одного-единственного файла.


Однако так уж исторически сложилось, что у меня все-таки была одна небольшая часть Visual Studio – а именно утилитка, именуемая Spy++. Одна из функций этой утилиты заключалась в том, чтобы отслеживать сообщения, которые пользователь указывал в специальном окне, по-научному называемом ListBox. В этом окне как раз и отображался полный список сообщений, среди которых можно было «мышкой» отметить те сообщения, которые требовалось отлавливать. Иными словами, было совершенно очевидно, что Spy++ содержал всю необходимую мне информацию, и требовалось лишь найти способ эту информацию извлечь.   Первой, что пришло мне в голову, это пропустить файл spyxx.exe через утилиту, вычленяющую текстовые строки, и затем выбрать из всех найденных строк имена сообщений. Однако после некоторых размышлений я отверг этот путь: во-первых, мне хотелось получить список сообщений в отсортированным по алфавиту точно в том порядке, в каком они находились в Spy++, а во-вторых, у меня не было желания разбирать ту кучу малу, которую обычно вываливают утилиты поиска текстовых строк. Поэтому я решил поступить проще: написал программку, которая при помощи сообщения LB_GETCOUNT определяла количество строк в нужном мне ListBox’е, а потом построчно считывала содержимое ListBox’а, в цикле отправляя ему сообщения LB_GETTEXT. Через считанные минуты у меня на винчестере покоился в виде текстового файла полный список сообщений из Spy++. После этого оставалось только извлечь из исполняемого файла числовые значения, соответствующие именам сообщений, что я и сделал при помощи методов, о которых я говорил в предыдущей главе. Если у Вес есть желание попрактиковаться в применении этих методов – можете самостоятельно попробовать извлечь эти данные, особой сложности это не представляет.   Нередко для обработки и отображения данных программисты под ОС Windows используют ActiveX-компоненты, одним из полезных свойств которых является возможность получить доступ к интерфейсам такого компонента без всякой документации и заголовочных файлов.


Например, импортировав нужный ActiveX-компонент в Delphi, Вы сразу же сможете увидите свойства и методы, присущие этому компоненту. И, запрашивая значение нужных свойств и вызывая соответствующие методы, Вы скорее всего сможете научиться извлекать данные, которые этот ActiveX отображает. Более того, Вы получите возможность экспериментировать с этим компонентом в «лабораторных условиях» собственных тестовых примеров, имитирующих работу программы, из которой Вы собираетесь вытащить данные, а не непосредственно на «поле битвы» с чужим кодом.  Вы можете подумать «ну и какая польза от этих экспериментов – ведь нужные данные находятся в другой программе» - но не спешите с выводами. Представьте себе, что Вам удалось внедрить свой код в исследуемую программу и получить доступ к интерфейсам нужного ActiveX… А впрочем, почему только «представьте»? Внедряйте, получайте доступ – и считывайте вожделенную информацию!   И, наконец, не бойтесь пользоваться простейшими методами. Может случиться так, что один из множества инструментов автоматизации, умеющий листать страницы в указанном окне и делать скриншоты, объединенный с программой распознавания текста, поможет Вам получить распечатку защищенного от копирования телефонного справочника быстрее, чем  извлечение той же информации из глубин адресного пространства при помощи отладчика. В конце-концов, если информация где-то отображается – значит, ее можно оттуда извлечь и сохранить в желаемом виде – нужно лишь изобрести подходящий метод. Но это уже совсем другая, далекая от крэкинга история.     [C] CyberManiac Содержание Далее

Содержание раздела