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

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


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

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

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

Массу крэкерских инструментов, видеоуроков и статей вы сможете найти на видеокурсе от нашего сайта. Подробнее здесь.

Автор: CyberManiac <_cybermaniac_@mail.ru>

Введение

Что тебя смутит – то ложь
Е. Летов



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

Несмотря на то, что крэкеры, в большинстве своем, работают под одной хорошо известной операционной системой и пользуются сходными инструментами, я буду делать упор не на описание этих инструментов и правил работы с ними, а на универсальные идеи (хотя в качестве примеров я, понятное дело, буду приводить то, что наиболее доступно для самостоятельных экспериментов). Думаю, что Вы сами определитесь, какие инструменты Вам больше по душе.

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

Так или иначе, статей от том, «что делать», то есть как взломать конкретную программу или тип защиты, во много раз больше, чем руководств «как и почему надо делать именно это». Образно говоря, статьи о том, «что делать» - это уровень начальной школы, «почему был выбран именно такой прием» - уровень выпускных классов. Но ведь есть еще и высшая школа - изучение идей, которые не привязаны к каким-либо программам и типам защит, но могут быть адаптированы для решения любой конкретной задачи. Выше – только «научная работа», то есть область чистого творчества, генерация оригинальных идей, и никакой «учебник» в этой области принципиально невозможен. По моему глубокому убеждению, основная проблема, возникающая у начинающих крэкеров, заключается в огромном количестве пособий уровня «делай, как я», совершенно не объясняющих, почему автор пособия поступил именно так. В результате начинающий крэкер «на ура» ломает новые версии одной и той же программы, но теряется перед подобной же защитой, но реализованной слегка иначе. Разумеется, существуют и весьма качественные «учебники», предлагающие именно систему, а не только набор технических приемов (те же +ORC Cracking Tutorialz или руководство от the Keyboard Caper’а) – но абсолютное большинство их них написаны на английском языке. Поскольку каждый человек имеет право получать необходимую информацию на своем родном языке (а для меня, как и для многих других, родным языком является русский), рано или поздно должны были появиться русскоязычные тексты, систематизирующие опыт крэкинга. Именно такой материал я и старался написать, а насколько хорошо это у меня получилось - решать вам.



Глава 0. Дзен-крэкинг. Теория.


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

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

Однако Вы не можете строить предположения о работе программы на пустом месте – Вам нужен некий стартовый набор знаний. Поэтому Вы должны собрать как можно больше информации о самой программе – выяснить, упакована она или нет, какие ограничения содержатся в незарегистрированной программе и как выглядит процесс регистрации; узнать, что будет, если Вы попытаетесь использовать программу дольше, чем это предусмотрено триальными ограничениями; проанализировать, какие текстовые строки и ресурсы содержатся внутри программы; поинтересоваться, к каким файлам и ключам реестра программа обращается при запуске, и многое другое. Не поленитесь заглянуть в справочную систему программы – там Вы можете найти описания различий между зарегистрированной и незарегистрированной версией. Значительная часть этой информации Вам скорее всего не пригодится, но Вы не можете знать заранее, какой путь окажется наиболее удобным и какие знания о программе Вам понадобятся. Более того, почти наверняка все необходимые данные о работе программы Вы с первой попытки не соберете, и уже в процессе изучения кода Вам придется возвращаться к этому этапу, чтобы узнать, например, сколько с какими параметрами программа в заданной точке вызывает функцию создания файла, куда считываются введенные серийные номера, сколько раз и откуда программа вызывает функцию проверки регистрации и т.п. А потому – собирайте информацию!


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

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

Как это работает на практике? Представьте себе программу, которая делает нечто. Например, отказывается запускаться после 30 дней использования, выдавая стандартное окошко с сообщением (широко известное как MessageBox). Чтож, у нас есть первое наблюдение. Простой перевод системного времени не помогает – обмануть программу и заставить ее работать дольше, чем положено, не удается. Это второе наблюдение. Из него следует, что программа проверяет текущую дату не на основе показаний внутренних часов Windows. Предполагаем, что программа либо уже сделала пометку «больше не запускаться» где-нибудь в реестре или на диске, либо все-таки определяет текущее время, но каким-либо хитрым способом. Например, читая дату последнего доступа или модификации какого-либо файла. У нас уже есть целых два смелых предположения, которые можно брать за основу в дальнейшем расследовании вредительской деятельности защиты. Если программа не просто «задумывается» при запуске, но еще и шуршит винчестером, вероятность второго варианта сильно повышается. Теперь начинаем проверять эти варианты. В первом случае нам однозначно проще докопаться до истины, установив точки останова на все виды MessageBox’ов и выяснять, какой из условных переходов позволяет избежать появления этого сообщения. Во втором случае в качестве отправной точки можно использовать всевозможные GetFileTime, CompareFileTime (а чем не способ – сравнить дату создания файла программы, т.е. дату инсталляции с датой последней модификации какого-либо файла) и FindFirstFile/FindNextFile (они ведь тоже способны читать временные характеристики файлов).

Абсолютное большинство защит, как бы аккуратно они не были реализованы, все-таки имеют «ахиллесову пяту». Эта уязвимость может быть запрятана глубоко в недрах кода, размазана по нескольким десяткам процедур или же быть совершенно неочевидной – но она есть. Стоит только ее обнаружить и нанести точный удар – и защита развалится, как карточный домик. Следовательно, залогом успешного взлома является отыскание уязвимых мест в защите.


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

«Регистрация» программ, где защитная функция возвращает единственное несколько сложнее – в этом случае требуется выявить все точки, в которых функция возвращает какое-либо значение, и подправить это значение. Нужно только помнить, что куски кода вроде


 xor eax,eax
 …
 ret
 


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

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

Теперь Вам известны наиболее часто встречающиеся в защитах дыры, в выявлении которых заключена половина успеха крэкера. Пришло время рассмотреть трудности и «подводные камни», которые могут ожидать Вас в нелегком кэкерском труде. Прежде всего это проблема неверной интерпретации собранной информации. Например, Вы обнаружили, что программа поддерживает использование плагинов и при запуске сканирует все файлы с расширением DLL в собственной директории. Вы вполне логично предполагаете, что программа строит список плагинов для дальнейшей загрузки и подключения. А потом Вы можете очень долго искать механизм определения даты первого запуска – и не найти его. Потому что он уже отработал – как раз при поиске плагинов. Как такое может быть? Да очень просто: в комплект программы включается как минимум один плагин. Далее – обычная привязка к дате последней модификации файла этого плагина; саму дату модификации файла несложно получить в ходе поиска через FindFirst/FindNext. Вот так иногда авторы защит прячут свой вредительский код, что называется, «на самом видном месте».

Другой пример неверной интерпретации собранных данных встречается еще чаще; более того, это неизбежное следствие подхода, принятого в дзен-крэкинге. Если Вы нашли условный переход, который начисто «выключает» все сообщения о незарегистрированности программы, из этого не обязательно следует, что программа будет вести себя в точности как зарегистрированная. Убедиться в стопроцентной надежности (или принципиальной неправильности) исправления этого перехода – подчас задача много более сложная, чем найти этот самый условный переход. Что интересно, это утверждение работает и в обратную сторону: если Вы что-то сделали, но не увидели результат, это совершенно не означает того, что Вы ошиблись. Создатели защит не так уж редко создают многоуровневую оборону, и для одержания победы недостаточно разрушить внешние рубежи защитного кода. Если вы что-то сделали, но не получили желаемого результата, не стоит сразу же бросать избранный путь; возможно, что Вы абсолютно правы и необходимо двигаться тем же путем и дальше. Даже если после Ваших манипуляций подопытная программа рушится с «ужасным» GPF, этот GPF может быть всего лишь еще одной, еще не выявленной уловкой создателя защиты.



Глава 1. Орудия крэкера.


Ваше слово, товарищ «Маузер»
В. Маяковский


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

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

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

К вашему счастью, инструментов, пригодных для использования в крэкинге, не так уж мало, и проблема состоит не в том, чтобы их раздобыть, а в том, чтобы отобрать из них наилучшие, изучить их возможности и определить для себя, в какой ситуации тот или иной инструмент лучше всего применить. Поясню эту мысль на примере: на данный момент наиболее мощным из дизассемблеров является IDA Pro, которая способен не просто дизассемблировать код, но и находить в нем вызовы стандартных функций тех или иных компиляторов. Однако если мне необходимо покопаться в программе, написанной на Delphi версии выше третьей, я наверняка не буду использовать IDA Pro, предпочтя ему Delphi Decompiler. Почему? Во-первых, скорость работы IDA Pro и DeDe различается в десятки раз в пользу последнего; используя DeDe, я скорее всего получу желаемый результат раньше, чем закончилось бы дизассемблирование в IDA Pro. Во-вторых, DeDe позволяет анализировать работающие процессы «на лету», что позволяет анализировать сжатые программы, не отвлекаясь на распаковку, восстановление таблицы импорта и прочие вспомогательные действия. Не углубляясь в дальнейшее перечисление достоинств DeDe, начисто отсутствующих в IDA, скажу, что в большинстве случаев специализированная программа позволяет решать свой «родной» класс задач значительно эффективнее, чем программы «общего назначения», ориентированные на ручную работу. Конечно, никто не запретит вам распаковывать программы, вручную копируя секции из памяти в файл, но не проще ли воспользоваться дампером?

Наверняка найдутся те, кто возразит, что широкое использование готовых утилит якобы мешает самостоятельному мышлению, чрезмерно упрощает процесс взлома и вообще «настоящие хакеры дизассемблируют в уме». Так вот, мое принципиальное мнение по этому поводу такое: используйте все программы, какие только сочтете нужными, если это поможет вам добиться желаемого. В конце-концов, цель крэкера обычно состоит в получении работающей программы, а никак не в тренировке памяти или демонстрации собственной крутизны. А всем «настоящим хакерам» посоветуйте дизассемблировать в уме winword.exe из состава самого последнего MS Office, и до тех пор, пока они это не сделают, не беспокоить вас древними суевериями. Разумеется, рано или поздно Вы перейдете от использования чужих программ к написанию собственных патчеров, распаковщиков, дамперов и прочих утилит – но такой переход должен быть продиктован насущной необходимостью, а не обезьяним «чтобы быть не хуже других». В конце-концов, большинство программ было написано именно для того, чтобы люди ими пользовались.

Итак, какие инструменты и для чего нам потребуются?

1. Отладчики и дизассемблеры. Традиционно оба этих инструмента используются в паре, поскольку дизассемблер выдает лишь «чистый код», хотя современные дизассемблеры способны также распознать вызовы стандартных функций, выделить локальные переменные в процедурах и предоставляют прочий подобный сервис. Пользуясь дизассемблером, Вы можете лишь догадываться о том, какие данные получает та или иная функция в качестве параметров и что они означают; чтобы выяснить это, чаще всего требуется изучения если не всей программы, то довольно значительной ее части. Отладчики выполняют принципиально иные функции, они позволяют анализировать код в процессе его работы, отслеживать и изменять состояние регистров и стека, править код «на лету» - в общем, наблюдать за «личной жизнью» программы и даже активно в нее вмешиваться. Обратной стороной медали является «неинтеллектуальность» многих отладчиков – их врожденные способности к анализу кода редко простираются дальше определения направления перехода. SoftIce, «лучший отладчик всех времен и народов», например, ничего не знает о типах данных и не способен отличить обычный DWORD от указателя на ASCIIZ-строку, хотя и предоставляет пользователю возможность проверить это вручную. Впрочем, вполне реальны отладчики, сочетающие высокое качество дизассемблирования и анализа кода с широкими возможностями по его отладке. Примером может служить OllyDebug, выполняющий эвристический анализ кода, выделяющий локальные переменные, «знающий» о типах данных, передаваемых функциям WinAPI и при этом способный во многих случаях автоматически отличить обычное число от указателя на строку.
2. Декомпиляторы и узкоспециализированные отладчики. С ростом мощности ЭВМ довольно широкое распространение получили компиляторы, создающие не «чистый» машинный код, а некий набор условных инструкций, который выполняется при помощи интерпретатора. Интерпретатор может как поставляться отдельно (Java), так и быть «прикрепленным» к самой программе (хотя формально не интерпретатор прикрепляется к программе, а программа к интерпретатору. Примером может служить Visual Basic при компиляции в p-code). Возможны и более экзотические варианты, например, применяемый в Форте «шитый код»; компиляция программы в цепочку команд push\call или преобразование текста программы в такую цепочку непосредственно при запуске этой программы. Интерпретаторами являются практически все инсталляторы (в их основе лежит интерпретатор инсталляционного скрипта, хотя сам процесс создания такого скрипта может быть скрыт при помощи использования визуальных средств). Да и «обычные» компилирующие языки могут создавать код, прямой анализ которого весьма затруднителен. Для анализа таких программ используются специализированные утилиты, переводящие код, понятный лишь интерпретатору, в форму, более удобную для понимания человеком. Также некоторые декомпиляторы могут извлекать информацию об элементах интерфейса, созданных визуальными средствами. В любом случае, не следует ожидать от декомпиляторов восстановления исходного текста программы; если декомпилированная программа успешно компилируется и сохраняет работоспособность – это исключение, а не правило.
3. Распаковщики и утилиты для дампинга процессов. Дизассемблировать запакованную или зашифрованную программу невозможно, но если очень хочется получить хоть какой-то листинг, можно попытаться извлечь из памяти компьютера «снимок» (дамп) программы в момент ее работы. Этот дамп уже можно более или менее успешно дизассемблировать. Более того, на основе дампа можно воссоздать исполняемый файл программы, причем этот файл будет успешно загружаться, запускаться и работать. Именно на этом принципе и основана работа большинства современных распаковщиков: подопытная программа запускается под управлением распаковщика; распаковщик ждет некоторого события, говорящего о том, что программа полностью распаковалась, и тут же «замораживает» программу и сбрасывает ее дамп на диск. Защитные системы нередко пытаются противодействовать получению работоспособного дампа при помощи манипуляций с сегментами и таблицами импорта-экспорта. В этих случаях приходится PE-реконструкторы, т.е. утилиты, обнаруживающие в дампе некорректные ссылки на функции и пытающиеся их восстановить. Многие продвинутые дамперы и распаковщики имеют встроенные средства восстановления секций импорта.
4. Утилиты анализа файлов. Очень часто требуется быстро узнать, каким упаковщиком или защитным софтом обработана та или иная программа, найти все текстовые строки в дампе памяти, просмотреть содержимое файла в виде таблицы записей, вывести в файл список импортируемых и экспортируемых программой функций и многое другое. Для всех этих целей существует огромное количество специализированных утилит, позволяющих быстро проанализировать файл на наличие тех или иных признаков. Эти утилиты, как правило, не являются жизненно необходимыми, но, при надлежащем качестве, способны сэкономить огромное количество времени и сил.
5. Шестнадцатеричные редакторы и редакторы ресурсов. Это, несомненно, наиболее древние (по идее, но не обязательно по исполнению) из программистских инструментов, ведущие свою славную историю с тех времен, когда программисты еще умели «читать» и править исполняемый код, не прибегая к помощи дизассемблеров. Тайное искусство чтения кода еще сохранилось в отдаленных уголках Вселенной, но в последние годы практически утратило актуальность. И теперь лишь крэкеры практикуют этот мистический обряд, в соответствии со священной традицией исправляя в неправильных программах идеологически чуждый опкод 75h на истинный и совершенный 0EBh. Редакторы ресурсов, в принципе, занимаются тем же самым, но по отношению к прилинкованным к исполняемому файлу ресурсам. Именно при помощи редакторов ресурсов выполняется значительная часть работ по «независимой» русификации программ и доработке интерфейсов. Рука об руку с редакторами идут всевозможные патчеры, которые позволяют создать небольшой исполняемый файл, автоматически вносящий изменения в оригинальный файл программы либо в код этой программы непосредственно в памяти.
6. API-шпионы и другие утилиты мониторинга. Очень часто бывает нужно узнать, какие именно действия выполняет та или иная программа, откуда читает и куда записывает данные, какие стандартные функции и с какими параметрами она вызывает. Получить эти знания как раз и помогают утилиты мониторинга. Такие утилиты делятся на две большие группы: те, которые отслеживают сам факт возникновения каких-либо событий и те, которые позволяют выявить один или несколько специфических типов изменений, произошедших в системе за некий промежуток времени. К первой группе относятся всевозможные API-шпионы, перехватывающие вызовы системных (а более продвинутые - и не только системных) функций, хорошо известные утилиты Reg-, File-, PortMon и т.д., перехватчики системных сообщений и многие другие. Эти утилиты, как правило, предоставляют весьма подробную информацию об отслеживаемых событиях, но генерируют весьма объемные и неудобные для анализа логи, если отслеживаемые события происходят достаточно часто. Вторая группа представлена всевозможными программами, создающими снимки реестра, жесткого диска, системных файлов и т.п. Эти программы позволяют сравнить состояние компьютера «до» и «после» некоего события, построить список различий между состояниями и на основе этого списка сделать определенные выводы. Основная проблема при работе с такими утилитами хорошо описывается словами “«после» – не значит «вследствие»” (т.е. кроме интересующей Вас деятельности программы Вы можете получить лог «жизнедеятельности» других параллельно работающих программ и операционной системы).
7. Прочие утилиты. Существует огромное количество утилит, не вписывающихся в приведенные выше категории или попадающие в несколько категорий. Одна лишь полная классификация этих инструментов потребовала бы значительных усилий, но моя цель не в том, чтобы плодить «мертвые буквы», а в том, чтобы дать Вам знания и навыки, достаточные для самостоятельных занятий. Крэкинг – занятие весьма многогранное, и столь же многогранны инструменты, в нем используемые. Более того, некоторые из утилит, которые могут быть полезны для крэкера, создавались для совершенно иных целей (хорошим примером может служить программа GameWizard32, которая вообще-то была предназначена, чтобы мухлевать в компьютерных играх, но оказалась полезна при вскрытии программы с ограничением на максимальное число вводимых записей). Поэтому еще раз обращу Ваше внимание: важно не только качество инструмента, но и умение творчески и нетривиально его применить.



Глава 2.Почти начинаем ломать.


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

Итак, многие программы в настоящее время поставляются в виде инсталляционного пакета. Для установки программы, как правило, требуется либо запустить один из файлов пакета (это, в частности, отлично всем известные Setup.exe), либо открыть при помощи другой заранее установленной программы (к примеру, файлы с расширением MSI, созданные Microsoft’овским инсталлятором или RPM-пакеты в Linux). В инсталляционные пакеты кроме самих файлов, которые требуется установить на машину пользователя, содержится также описание сценария инсталляции в том или ином виде (назовем это описание сценария для простоты «инсталляционным скриптом», тем более, что чаще всего так оно и есть). Разумеется, инсталлятор может быть и обычной программой, написанной для установки конкретного приложения, но написание собственного инсталлятора – дело достаточно трудоемкое, и потому на практике такие инсталляторы встречаются весьма редко.

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

Если Вы взламываете какую-либо программу, оснащенную ограничением на время использования или число запусков, один из «корней зла» может гнездиться именно в инсталляционном скрипте. Представьте себе такую ситуацию: программа хранит дату первого запуска и/или какую-либо иную информацию, необходимую для проверки на истечение срока пробного использования, в реестре. Разумеется, информация закодирована, предприняты меры, чтобы защиту не могло обмануть «подкручивание» системной даты, возможно даже соответствующий ключ реестра хорошо замаскирован (Вам ничего не напоминает мое описание? Да это же ASProtect – ломаный-переломаный, но, как ни странно, все еще популярный). Но, тем не менее, одна лазейка все-таки осталась – если триальный ключ в реестре отсутствует, защита считает, что раньше программа не запускалась. Поэтому защиту можно обмануть, просто удалив из реестра лишние ключики. А теперь представьте, что триальный ключ создается в процессе инсталляции, и если он отсутствует, программа не запускается вообще! Если Вы ставите целью взлома ликвидацию триальных ограничений, Вам могут потребоваться весьма значительные усилия, чтобы отыскать этот маленький, но зловредный ключик в огромном реестре еще более огромной Windows. Как вам нравится такая перспектива? Если встроенных средств инсталлятора оказывается недостаточно для создания триальной «метки», это может быть реализовано при помощи небольшого исполняемого файла, который распаковывается в процессе инсталляции, запускается, делает свое черное дело и сразу после этого удаляется. Упрощенный вариант этого приема может выглядеть как автоматический запуск приложения после окончания инсталляции, чтобы защита смогла создать триальные «метки» на компьютере пользователя.

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

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

Однако созданием триальных ключей функции программ, запускаемых в процессе инсталляции, не ограничиваются. Дело в том, что набор функций, поддерживаемых инсталляторами, обычно довольно невелик, и некоторые действия (например, проверку серийного номера с использованием достаточно сложного алгоритма) выполнить средствами инсталляционных скриптов просто невозможно. Один из возможных приемов, применяемых в этом случае – запуск исполняемого файла, который и выполняет все необходимые операции, а затем возвращает управление инсталлятору. В частности, существуют защиты, в которых проверка серийного номера реализована именно так. В некоторых инсталляторах для этих же целей предусмотрен интерфейс, позволяющий использовать плагины (плагин в виде динамически загружаемой библиотеки также упрятываются внутрь инсталлятора, в нужный момент распаковываются во временную директорию и после использования удаляются). Такие исполняемые файлы и плагины, разумеется, невозможно модифицировать напрямую и чаще всего не удается извлечь из инсталлятора для дизассемблирования и изучения, т.к. они хранятся внутри инсталлятора в сжатом виде, а многие коммерческие инсталляторы несовместимы по формату с обычными архиваторами. Если Вы хотите исследовать такой исполняемый файл, Вам почти наверняка потребуется снять с него дамп, чтобы получить материал для загрузки в дизассемблер. Сделать это совсем несложно – запустите инсталлятор под отладчиком и поставьте точки останова на все функции, связанные с загрузкой модулей и библиотек (в случае плагина) или создания процесса (для EXE). В Windows это будут LoadLibrary[Ex], LoadModule[Ex], CreateProcess или устаревший WinExec соответственно. Запомните, откуда инсталлятор пытается загрузить файл, и затем «заморозьте» работу инсталлятора непосредственно перед исполнением этой функции патчем

     MySelf: jmp MySelf
 

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


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

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

Часто в инсталлятор «зашит» не один серийный номер, а несколько – и это способно существенно облегчить нам задачу. Существует три метода проверки серийного номера:
1. Вычислить хэш-функцию от серийного номера и проверить, удовлетворяет ли результат какому-либо условию.
2. Пропустить результат через хэш-функцию и сравнить его со списком эталонов.
3. Пропустить зашифрованные серийные номера, спрятанные внутри программы, через процедуру расшифровки и затем сравнить результат с значением серийного номера, который ввел пользователь. Очевидно, что это наиболее простой случай. Фактически, требуется лишь обнаружить точку, в которой происходит сравнение введенного серийного номера с правильным, и прочитать правильное значение.

Различие между п.1 и п.2 не очевидно, но оно есть: именно на втором методе обычно основаны всевозможные «черные списки» серийных номеров. Первый способ проверки в инсталляторах применяется сравнительно редко из-за слабых математических возможностей интерпретаторов инсталляционных скриптов и ориентации на максимальную простоту процесса создания инсталляции (грамотная установка защиты, к нашему счастью, достаточно сложна и при этом все равно не дает гарантированного результата). Так что в итоге внутри большинства инсталляторов упрятан все тот же список серийных номеров (возможно, из одного элемента). Что интересно, абсолютное большинство инсталляторов никак не упаковывают инсталляционные скрипты (хотя исходный текст скрипта вполне может быть откомпилирован в байт-код), поэтому Вы можете без особых сложностей эти скрипты модифицировать.

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

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

В некоторых случаях наиболее прямым путем к получению полнофункциональной программы из урезанного варианта является именно вскрытие инсталлятора. Поясню эту идею примером. Допустим, что у Вас есть демо-версия какой-либо хорошей программы, но Вам этого мало, и Вы хотите иметь полностью функциональный вариант продукта. При этом у Вас нет никакого желания вручную восстанавливать недостающий код, заботливо выдранный автором. Однако на сайте производителя иногда можно найти обновления для коммерческих версий нужной Вам программы, и эти обновления, как правило, содержат полные и исправленные варианты основных файлов программы. Что из этого следует? А то, что если неким таинственным образом Вам удастся установить апдейт на демо-версию, есть ненулевая вероятность, что свою демонстрационную версию Вы превратите в программу, по функциональности практически не отличающуюся от полной! Разумеется, программа обновления перед своей установкой выполнит проверку на возможность обновления (в том числе и для того, чтобы пользователь случайно не «обновил» демо-версию) – но ведь Вы решили изучать крэкинг именно для решения проблем именно такого рода. Так что Вам нужно лишь немного «доработать» инсталлятор, ликвидировав в нем проверку на возможность обновления.

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



Глава 3. «Критические дни» программ.


Все боится времени, но само время боится Пирамид
Египетская пословица


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

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

Возможны и другие причины, по которым Вам может потребоваться снять ограничение на время использования программы. Для некоторых программ процедура регистрации не предусмотрена в принципе, но при этом в течение испытательного срока они выполняют свои функции в полном объеме (часто такие программы помечены как Demo- и Evaluation-версии), и вполне приемлемым решением было бы неограниченное продление триального срока. Случается также, что необходимо установить бета-версию программы, у которой истек «срок годности», но более новой беты нет, а посмотреть на программу очень хочется. Так или иначе, снятие ограничений на время использования или число запусков программ – одна из актуальных задач современного крэкинга.

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

В принципе, идеологических различий между реестром Windows, файлами и атрибутами отдельных файлов нет, все эти объекты могут использоваться для хранения используемых защитой данных. Действительно, аналогии в устройстве реестра и дисковой подсистемы очевидны: «ветви» реестра играют роль логических дисков, разделы – практически полные аналоги папок (убедиться на наглядном примере можно, взглянув на иконки разделов в «Редакторе реестра»), имена ключей – это имена файлов, а значения ключей – содержимое этих файлов. Аналогии можно продолжить, но для нас важно другое: поскольку файловая система подобна реестру, принципы поиска и ликвидации защитных механизмов, основанных на сокрытии информации на дисках или в реестре, во многом будут сходны. Поэтому далее я буду говорить в основном о реестре, оставляя читателю самому разобраться в том, как аналогичные механизмы могут быть реализованы на основе файловой системы (или почему они не могут быть реализованы).

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

Первое, что приходит в голову – во время первого запуска сохранить в файле или в реестре дату (или вычислить дату, после которой программа не должна работать) и при каждом запуске сравнивать текущую дату с сохраненной, проверяя, не истек ли «срок годности» программы. Такие программы, не обнаружив в реестре пометки о дате первого запуска, как правило, считают, что текущий запуск – первый. Такое простейшее решение, как правило, и обходится простейшими средствами – достаточно обнаружить и удалить соответствующий файл или ключ реестра. Одна из модификаций этого метода – определение даты первого запуска через чтение атрибутов файлов: при создании любого файла атрибут CreationTime (дата создания файла) устанавливается автоматически, что позволяет непосредственно в процессе инсталляции «промаркировать» все устанавливаемые файлы датой установки программы. Затем программа просто проверяет при каждом запуске дату создания какого-либо файла или папки (или вообще всех файлов, созданных в процессе инсталляции) и на основе этой информации вычисляет количество дней до истечения испытательного периода. Что интересно, свойства файлов могут использоваться не только для определения даты установки программы, но и для определения текущей даты: в процессе своей работы отдельные компоненты ОС нередко ведут всевозможные «журналы» (логи) в файлах с заведомо известными именами. Проверив дату последней модификации такого файла или его содержимое, можно с некоторой погрешностью узнать текущую дату.

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

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

Многие из ныне существующих защит создают «метки» в момент первого запуска программы. Факт первого запуска обычно определяется простейшим способом: если программа при запуске не обнаруживает «метку», она считает, что запущена впервые, и создает «метки». Обычно создание «меток» выполняется сразу после начала исполнения программы, но изредка встречаются программы, которые выполняют эти действия при завершении программы (возможно, что существуют защиты, которые делают это в случайный момент времени). Скорее всего, таким образом разработчики защит пытались затруднить выявление защитного кода, но на деле они добились прямо противоположного эффекта: большинство операций по загрузке данных из реестра выполняется именно при запуске или изменении настроек программы, а вот запись в реестр в конце сеанса работы – операция менее распространенная. При завершении сеанса обычно сохраняется только информация о положении окон, настройках интерфейса программы и прочие подобные данные, которые, как правило, несложно отличить от защитных механизмов программы. Очевидно, что если защита использует исключительно «метки», создаваемые при первом запуске, после удаления этих «меток» программа считает, что запущена впервые, и начинает отсчет триального срока заново. Для того, чтобы пользователи не обходили эту защиту совсем уж элементарными средствами вроде удаления соответствующего ключа реестра, ключ, содержащий в себе защитную информацию, может быть «плавающим», то есть имя ключа может генерироваться случайным образом в зависимости от аппаратной конфигурации компьютера или каких-либо иных параметров. Найти вручную такой ключ среди десятков подобных практически нереально. В частности, именно такой механизм использует ASProtect. Поскольку «плавающий» ключ, сколь успешно бы он не был замаскирован, все-таки отличается от окружающих его ключей, определив признаки, которые отличают «плавающий» счетчик от обычных ключей реестра, возможно создать программу, которая бы автоматически выявляла подозрительные ключи, что подтверждается наличием как минимум трех независимо разработанных утилит, способных выявлять и удалять ключи, созданные ASprotect’ом. В любом случае, изменения в реестре, возникшие после первого запуска программы, нетрудно обнаружить при помощи программ мониторинга реестра.

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

Разумеется, программа, содержащая ограничение на число запусков, должна увеличивать либо уменьшать значение этого счетчика после каждого запуска. Кроме того, запись в реестр при каждом запуске возможна также и при ограничении по времени использования. Такая запись играет роль дополнительной защиты от изменения системного времени: если дата текущего запуска меньше даты предыдущего или отличается от него на считанные секунды, программа может предположить, что пользователь пытается использовать программу сверх установленного срока. Обнаружить такую защиту сравнительно легко – многократную модификацию одного и того же ключа скрыть практически невозможно, не помогают даже такие приемы, как дублирование счетчика и «плавающий» счетчик. Сбрасывается такой счетчик тоже без особых сложностей – достаточно один раз запомнить состояние соответствующих ключей реестра и затем восстанавливать его перед каждым запуском.

Интересно отметить, что даже столь простую идею, как отслеживание времени, некоторые разработчики ухитряются реализовать некорректно (по крайней мере, под ОС Windows такое встречается не так уж редко). Windows позволяет оперировать двумя типами времени: системным (оно же «всемирное», UTC) и местным (Local). Причем во многих странах местное время может быть зимним и летним. И если защита ориентируется на местное время, в день перевода часов пользователя может ожидать сюрприз: после автоматического перевода часов, выполняемого ОС, программа может просто перестать работать. Для этого нужно лишь, чтобы пользователь запустил программу непосредственно перед переходом с летнего времени на зимнее, и потом – еще один раз в течение ближайшего часа. По этому нехитрому признаку можно в известной степени судить о квалификации разработчика защиты.

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

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

1. Сделать снимки состояния системы до и после установки программы.
2. Сделать снимок системы перед первым запуском программы, запустить программу, сразу же закрыть ее (чтобы в снимок системы не попали изменения, возникающие при работе с программой и не имеющие отношения к функционированию защиты) и тут же сделать еще один снимок системы. Если программа запускается после инсталляции автоматически, и выполнить этот пункт не представляется возможным, мы, тем не менее, сможем обнаружить изменения, произошедшие при первом запуске, сравнив снимки, сделанные в предыдущем пункте (другое дело, что мы не сможем отличить изменения, произошедшие при инсталляции, от модификаций, внесенных в систему программой при первом запуске).
3. Запустить программу во второй раз, сделав снимки до запуска и после завершения программы. Если программа содержит ограничение на число запусков, путем несложного анализа мы сможем легко выявить счетчик запусков. Если программа содержит ограничение по времени использования, то мы можем установить, использует ли программа какие-либо дополнительные механизмы отслеживания времени. О наличии таких механизмов может свидетельствовать появление в реестре новых ключей или изменение уже существующих, появившихся в ходе инсталляции или первого запуска. Если такие ключи обнаружатся, есть вероятность, что программа кроме даты первого запуска отслеживает еще и дату предыдущего запуска, число дней, в течение которых использовалась программа или другую подобную информацию. Для максимальной надежности и достоверности результатов эту операцию можно повторить несколько раз.
4. Выяснить, какие изменения в системе происходят, когда программу запускают впервые после истечения триального срока. Это необходимо для поиска меток, которые могли бы воспрепятствовать нормальной работе программы после окончания времени, отведенного на ее пробную эксплуатацию. Важно отметить, что для программ с ограниченным числом запусков необходимо отслеживать изменения не только при первом запуске сверх установленного лимита, но и при последнем «законном» запуске.
5. По возможности, необходимо выяснить, как программа реагирует на попытки обойти ограничение времени использования при помощи изменения системной даты. Если программа успешно «проглатывает» любое изменение даты, высока вероятность того, что защита реализована максимально простым способом, и обойти ее будет нетрудно. Разумеется, все эти действия должны сопровождаться созданием снимков системы: если программа обнаружит Ваши манипуляции с системным временем, она может отреагировать на это созданием «метки», свидетельствующей о попытке обойти защиту.
6. Проанализировать все собранные данные, выявить подозрительные ключи реестра и другие объекты, которые могли бы использоваться для хранения защитных данных, и затем на основе собранной информации попытаться восстановить работоспособность программы, последовательно удаляя или восстанавливая первоначальные значения подозрительных ключей.
7. Проверить, нет ли в программе неочевидных ограничений по времени использования. Нередко встречаются программы (как правило, это демонстрационные, пробные и бета-версии), в которых помимо обычного ограничения на время пользования имеется дополнительная проверка на максимальную дату запуска, после которой программа считается устаревшей и перестает работать.

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

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


Глава 4. Переменные и константы.


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

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

Приблизительный рейтинг изменений по степени их влияния на логику программы (от минимального к максимальному) выглядит следующим образом:

- Модификация константы
- Изменение значения предварительно инициализированной переменной
- Изменение условия перехода на противоположное
- Удаление линейной, то есть не содержащей ветвлений и вызовов нетривиальных подпрограмм, последовательности команд
- Дописывание в программу собственного кода; изменение значения, возвращаемого функцией
- Модификация внедренного в программу ресурса, важного для логики программы (т.е. такого, изменение которого меняет поведение программы)
- Удаление из программы логического блока (например, вызова нетривиальной функции); удаление внедренного в программу ресурса

Этот принцип, как любое другое широкое обобщение, требует достаточно осторожного отношения и в некоторых случаях может повести Вас по неоптимальному пути. Но на практике принцип минимального вмешательства обычно работает вполне успешно. Да и с точки зрения простого здравого смысла очевидно, что найти и исправить условие, которое проверяет зарегистрированность программы – прием куда более безобидный и надежный, чем вырывание с корнем nag-screen’ов и триальных ограничений при помощи глубокого патчинга.

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

Наверное, наиболее распространенным типом данных, используемым в программах, в настоящее время являются целые числа. Абсолютное большинство существующих процессоров, в том числе и наши любимые x86, изначально ориентировались на работу с целыми числами определенной разрядности (в настоящее время на платформе x86 наиболее актуальны 32-битные целые со знаком или без). Именно целые числа являются наиболее естественных форматом для хранения всевозможных счетчиков и контрольных сумм. Однако возможны и более неординарные применения: в тридцати двух битах можно успешно хранить числа с фиксированной точкой и даже логические значения. Когда я переходил от программирования под ДОС к программированию под Win32, меня сильно удивляла расточительность фирмы Microsoft, отводившей под простую булевскую переменную целых 4 байта, и только более глубокое изучение архитектуры 32-разрядных процессоров и ассемблера расставило все по своим местам.

Что Вы будете делать, если Вам потребуется найти в программе сравнение чего-либо (например, регистра или содержимого ячейки памяти) с определенной целочисленной константой? Для некоторых целых чисел достаточно обычного поиска блока двоичных данных в файле. Однако двоичный поиск далеко не со всеми числами работает одинаково хорошо: если Вы ищете число 3B9ACA00h, вероятность ложного срабатывания будет весьма небольшой, но вот если Вы попытаетесь найти в исполняемом файле число 10 или 15, то, скорее всего, просто устанете нажимать на кнопку «Найти далее». Если вспомнить, что числа 10 и 15 могут храниться не только как 32-битные, но и как одно- и двухбайтные, становится ясно, что двоичный поиск небольших чисел в исполняемых файлах – далеко не самая лучшая идея. Кроме того, при таком способе поиска никак не учитывается структура исполняемого файла программы, поскольку Вы ищете нужную Вам константу не только в коде программы, но и в PE-заголовке, секции данных, секции ресурсов и прочих областях, имеющих к коду программы самое отдаленное отношение. Хотя в принципе эта проблема, конечно, решаема: нужно лишь ограничить область поиска секциями, содержащими данные и код.

Однако есть и другой, более эффективный метод поиска в программе известных заранее значений. Как ни странно, но в реальных программах широко используется лишь сравнительно небольшой набор целочисленных констант: это, прежде всего, небольшие положительные числа от 0 до 7 (а также небольшие отрицательные от -3 до -1) и степени двойки: 8, 16, 32 и т.д. Другие константы в программах встречаются значительно реже. Попробуйте сами провести эксперимент – дизассемблируйте какую-нибудь достаточно большую программу и найти в ней какое-нибудь сравнительно небольшое число, например, 32h, которое в этой программе заведомо имеется. Для этого эксперимента я написал простейшую программку на Delphi 7, вся функциональность которой концентрировался в следующем коде, имитирующем простейшее ограничение на число строк в документе:

 procedure TForm1.Button1Click(Sender: TObject);
 begin
  if Memo1.Lines.Count>50 then
    begin
     Application.MessageBox(’More than 50 items not available’,’Demo version’);
     Close;
    end
   else Memo1.Lines.Add(Edit1.Text);
 end;
 

В результате компиляции этот весьма нехитрый текст превратился в исполняемый файл размером более 350 килобайт (я намеренно создал проект с визуальными компонентами, а также использовал режим компиляции без runtime packages, чтобы мой собственный код составлял в исполняемом файле очень малую долю по сравнению с библиотеками Delphi). Затем я дизассемблировал откомпилированную программу при помощи W32Dasm и получил листинг текст длиной более 180 000 строк. Казалось бы, обнаружить область, где происходит сравнение с числом 50 в этом листинге ничуть не проще, чем найти иголку в стоге сена. Но я воспользовался функцией поиска в тексте строки, в качестве параметра поиска указав 00000032 (так в W32Dasm отображается число 50; заодно это позволило отсеять команды вроде mov eax,[ebx+32h], обычно использующиеся для доступа к элементам массивов и полям структур). Реальность превзошла самые смелые ожидания: четырехбайтное число 32h встретилось в листинге всего два (!!!) раза:

 :004478C6 6A32                    push 00000032
 и
 :004505BB 83F832                  cmp eax, 00000032
 

Чтобы догадаться, что нужное нам сравнение выполняется во второй строке, достаточно самых минимальных познаний в ассемблере. Из этого следует вывод: поиск нужной константы в дизассемблированной программе вполне реален, даже несмотря на огромный объем листинга.

Далее: Вам наверняка интересен не сам факт наличия константы где-то в недрах кода, а то, в каком контексте эта постоянная используется. Иными словами, если Вы знаете, что программа сравнивает число записей в базе данных с некоторым значением (в нашем случае - 32h), то среди всех строк, в которых присутствует эта константа, в первую очередь следует рассматривать команды сравнения (cmp) и вычисления разности (sub и sbc). Хотя нельзя забывать о существовании менее очевидных способов сравнения, например, таких:

 mov ebx,50
 cmp eax, ebx
 
 или
 
 push 50
 push eax
 call Compare2dwords
 


Ну и, раз уж речь зашла о сравнениях, нельзя не упомянуть об альтернативных вариантах реализации этой, казалось бы, нехитрой операции. Поразмыслим над приведенным выше примером сравнения содержимого регистра eax с числом 50. В самом деле, условия eax>50 и eax>=51 в приложении к целым числам имеют один и тот же смысл, а код

 cmp  eax,50
 jg my_label
 

работает совершенно аналогично коду

 cmp eax,51
 jge my_label
 

Если необходимо выяснить, больше ли содержимое регистра EAX, чем 31, или нет, то проверка может выглядеть даже так:

 and eax, 0FFFFFFE0h
 jnz my_label
 


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


 if  my_var=[число] then <переменная_равна_числу>
   else if my_var>[число] then <переменная_больше_числа>
           else <переменная_меньше_числа>
 


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


 cmp my_var,[число]
 jz is_equal
 jg is_more_than
 


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

Если рассматривать области возможного практического применения вышеописанного приема, то лучше всего поиск известной константы в дизассемблированном тексте работает на триальных ограничениях типа «не допускается создание больше N элементов в базе данных». Как правило, N больше 7 и является целым числом, что облегчает поиск нужной константы. Исходя из принципа минимального вмешательства, для обезвреживания таких ограничений я предпочитаю исправлять не команды сравнения, а сами константы. Действительно, если программа для проектирования интерьера комнаты не способна работать более, чем с 20 объектами, для практического применения она вряд ли будет пригодна. Но вот та же программа, где максимальное количество обрабатываемых объектов увеличено до двух с хвостиком миллиардов наверняка удовлетворит даже самого взыскательного пользователя.

Одним из наиболее частых вопросов, возникающих у начинающего крэкера, звучит так: «Программа работает 30 дней, но я так и не нашел в листинге сравнения с числом 30. Что делать?». Один из факторов я уже описал выше – там могло быть сравнение не с числом 30, а с числом 31. Однако этим список возможных причин неудачи не исчерпывается. Как мы все знаем, день состоит из 24 часов, каждый из которых состоит из 60 минут, каждая из которых состоит из 60 секунд. Более того, продолжительность секунд также может измеряться во всевозможных «условных единицах», например, в миллисекундах. Тысячные доли секунд, в частности, используются в таких функциях WinAPI, как SetTimer (таймеры Windows часто используются для установки ограничений на продолжительность одного сеанса работы с программой) или Sleep. А вот в функциях, возвращающих время в виде структуры типа FILETIME, используются уже другие «условные единицы», равные ста наносекундам. Так что пресловутые 30 дней – это не только 30 дней, но еще и 720 часов, 43200 минут, 2592000 секунд, ну и так далее. И каждое из этих значений может быть использовано в программе как один из аргументов операции сравнения. Надо отметить, что в «условных единицах» может быть представлено не только время, но и многие другие величины: масса, географические координаты, денежные единицы и т.д.

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

Вообще в Windows существует несколько разновидностей таймеров – кроме обычного таймера, создаваемого функцией SetTimer, существует еще высокоточный мультимедийный таймер и специфические таймерные функции DirectX. Эти таймеры срабатывают с некоторой заданной частотой, вызывая функцию-обработчик (она же callback-функция), внутри которой и выполняются необходимые действия, например, тот же обратный отсчет секунд до исчезновения окна с предложением зарегистрироваться. Периодичность срабатывания таймера почти всегда является константой, однако взаимосвязь между тем, что происходит внутри программы и тем, что Вы можете видеть на экране, не всегда очевидна. Чтобы пояснить эту мысль и заодно продемонстрировать на практике, как можно обращаться с таймерами, приведу несколько примеров.

Первый пример – простейший: программа, которая при запуске в течение пяти секунд показывала баннер, при этом поверх баннера выводился обратный счетчик секунд. Регистрация в программе не предусматривалась. Дизассемблирование показало, что таймер срабатывает каждые 1000 миллисекунд, при каждом вызове callback-функции значение переменной, изначально равной пяти, уменьшалось на единицу, и результат проверялся на равенство с нулем. В той конкретной программе баннер можно было просто «выломать», убрав функцию создания и отображения рекламного окна, но в общем случае это решение было бы не лучшим (вспомните принцип минимального вмешательства). И вот почему: на последнее срабатывание таймера могло быть «подвешено» не только закрытие окна с баннером, но и инициализация каких-либо объектов внутри программы или другие критичные действия, без которых программа могла бы работать некорректно. Так что немного усложним задачу – будем считать, что полностью убирать вызов окна с рекламой нельзя. Первое, что нам приходит в голову – уменьшить число секунд, в течение которых показывается баннер. Сказано – сделано, цифру 5 исправляем на единицу. Однако баннер все равно висит целую секунду – ведь первое срабатывание таймера наступает только через секунду после его создания. Теперь уменьшим период таймера до нуля (хотя лучше все-таки до одной миллисекунды, «таймер с периодом 0 миллисекунд», согласитесь, штука довольно странная). В результате мы получили баннер, появляющийся при запуске программы лишь на мгновение и не заставляющий тратить целых пять секунд на праздное разглядывание рекламных лозунгов.

В качестве второго примера я возьму одну из старых версий TVTools. В справке к программе было четко указано, что незарегистрированная версия работает только 10 минут; дизассемблирование и анализ листинга выявили, что программа создает два таймера с периодами 60 секунд (что навело меня на мысли о защитном назначении этого таймера) и 2 секунды. Без особых сложностей обезвредив первый таймер, я запустил программу и обнаружил, что она все равно больше 10 минут не работала. Тогда я более пристально изучил callback-функцию второго таймера, и наткнулся в ней на такой код:


 inc     dword_40D5A7
 cmp     dword_40D5A7, 136h
 jbe     short loc_405CAA
 


Нетрудно догадаться, что это увеличение некоего счетчика, который затем сравнивается с числом 310. Поскольку период таймера – 2 секунды, а 310*2=620 (т.е. чуть больше 10 минут), логично было предположить, что это и есть второй уровень защиты, дублировавший первый. Очевидно, что если бы я принял на веру, что программа перестает работать ровно через 10 минут (а не через 10 минут 20 секунд, как это оказалось в действительности) и стал бы искать сравнение с числом 300, я бы не смог обнаружить таким способом вторую проверку времени работы программы. Этот пример демонстрирует один из неочевидных приемов, который может быть использован для реализации такой, казалось бы, простой операции, как отсчет 10-минутного интервала. Также из этих примеров следует и другой, не менее важный вывод: далеко не всегда следует искать известную константу, чтобы найти код, в которой она используется. Иногда следует поступать прямо противоположным образом – сначала искать код, выполняющий нужные действия, и лишь затем выяснять, какая константа внутри этого кода ответственна за интересующие нас действия.

Поиск констант с плавающей точкой – занятие с одной стороны более сложное, чем поиск целочисленной константы, но с другой – куда более простое. В чем сложность и в чем простота этого занятия? По традиции начнем с плохого. Во-первых, формат представления чисел с плавающей точкой весьма нетривиален, и Вы вряд ли сможете в уме привести шестнадцатиричный дамп такого числа в «человеческий» вид (возьмите документацию по процессорам Intel и попробуйте перевести число 1.23 в машинное представление, а затем проделать обратную операцию – Вы сами убедитесь, насколько сложна эта задача). Более того, даже целые числа в представлении с плавающей точкой выглядят весьма неординарно: к примеру, дамп самого что ни на есть обычного числа 123, приведенного к типу Double, выглядит как 00 00 00 00 00 C0 5E 40. Если Вы способны с первого взгляда отличить число с плавающей точкой от кода программы или каких-либо иных данных и оценить величину этого числа – я рад за Вас, но большинство людей, к сожалению, такими способностями не обладают.

Во-вторых, при работе с дробными числами нередко возникают проблемы, связанные с машинным округлением и потерей точности. Самым ярким примером, наверное, может служить особенность математических программ ПЗУ некоторых моделей Spectrum: с точки зрения такого Спектрума выражение 1/2=0.5 было ложным. Это, конечно, было давно, но не следует считать, что современные компьютеры полностью свободны от этой проблемы. И вот практическое тому подтверждение.

Откомпилируйте под Delphi следующий код: i:=sin(1); i:=arcsin(i) и посмотрите, как будет меняться результат при изменении типа переменной I от Single до Extended. Например, если I имеет тип single, в результате вычислений получим, что arcsin(sin(1))= 0,999999940395355. Такие «спецэффекты» – следствие все той же потери точности в процессе вычислений.

В-третьих, округлением чисел процессор может заниматься не только по собственному желанию, но и по велению программы. К примеру, в большинстве бухгалтерских программ всевозможные ставки налогов выводятся с точностью до копеек. Однако из того, что Вы видите на экране число 10.26, совершенно не следует, что результат расчетов представлен в памяти ЭВМ именно как 10.26. Реальное значение соответствующей переменной может быть равно 10.258 или 10.26167, которое и участвует в реальных расчетах, и лишь при выводе на экран для удобства пользователя было произведено округление до двух знаков после запятой.

Я не случайно столько места уделил округлению и точности представления чисел – именно эти особенности чисел с плавающей точкой в наибольшей мере затрудняют поиск нужных значений в памяти программы. Программисты знают, что при работе с действительными числами для проверки условия равенства некоторой вычисляемой величины другой величине не рекомендуется использовать сравнения вида f(a)=b. Причина этого лежит все в той же проблеме округления и потери точности расчетах – вспомните вышеприведенные примеры со Спектрумом или арксинусом синуса единицы. Вместо простой проверки равенства обычно используется условие «значения считаются равными, если абсолютная величина разности между ними не превышает некоторой величины»: abs(f(a)-b)<=delta, где delta – максимально допустимая величина разности, после которой числа не считаются равными. Поэтому если Вы хотите найти в памяти некоторое число с плавающей точкой F, Вы в действительности должны искать все числа из промежутка [F-delta; F+delta], причем определить значение delta чаще всего можно лишь опытным путем. Это утверждение распространяется и на тот случай, когда Вы знаете округленное значение переменной, но в этом случае величина delta будет зависеть от того, до скольки знаков округлено значение переменной. Так, если число округлено до сотых, нетрудно догадаться, что delta=0.005.

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

И, наконец, нельзя забывать, что кроме стандартных для платформы x86 типов Single, Double и Extended (32-, 64- и 80-битных соответственно) существует еще несколько довольно экзотических, но все еще используемых форматов. Это, к примеру, Currency (64-битные, с фиксированным положением десятичной точки) или 48-битные паскалевские Real. Возможно также использование «самодельных» форматов; особенно часто встречаются числа с фиксированным положением десятичной точки (обычно такое делается для повышения скорости работы программы и применяется в основном в процедурах кодирования/декодирования аудио- и видеоинформации). Знать о таких вещах совсем не лишне, хотя, конечно, вероятность столкнуться с такими числами в современных программах довольно низка.

Теперь немного поговорим о том хорошем, что есть в числах с плавающей точкой. Как известно, изначально в процессорах x86 встроенных аппаратных и программных средств для обработки чисел с плавающей точкой не предусматривалось. Низкая скорость расчетов, в которых использовались действительные числа, вызвала к жизни математические сопроцессоры, как традиционные x87, так и весьма экзотические девайсы Weitek. Победившая линейка сопроцессоров x87 (они с некоторых пор стали интегрироваться в ядро процессора и потому перестали существовать как отдельные устройства) имела следующую особенность: новые «математические» команды активно использовали для обмена информацией оперативную память. Посмотрите, к примеру, на важнейшие команды сопроцессора fst и fld – в качестве параметра этих команд могут выступать указатели на области памяти, которые предполагается использовать для чтения/записи данных. Более того, использование указателей в качестве одного из параметров характерно и для многих других команд сопроцессора. Поэтому ищите ссылки, используемые командами сопроцессора в качестве параметров – и Вы легко доберетесь до данных, на которые эти ссылки указывают.

Из этого следует вывод: хороший дизассемблер или отладчик способен «догадаться», что по адресу, указанному в аргументах этих команд, находится число с плавающей точкой и отобразить это число. Если же Ваш дизассемблер/отладчик об этом не догадывается – Вам придется вручную (точнее говоря, при помощи соответствующих программ) вычислить значение, которое находится по этому адресу. И пока Вы будете копировать байтики из одной программы в другую, у Вас будет достаточно времени подумать об обновлении инструментария.

Но и здесь не обошлось без ложки дегтя – компиляторы фирмы Borland, видимо, ради особой оригинальности, для загрузки констант в стек сопроцессора могут воспользоваться комбинациями вроде


 mov [ I ],$9999999a
 mov [i+$4],$c1999999
 mov word ptr [i+$8],$4002
 fld tbyte ptr [ I ]
 


Хотя, казалось бы, ничто не мешало положить несчастное число в секцию инициализированных данных… Тут уж не до «умного» поиска – разобраться бы, чего и куда вообще загружается. Хотя, при желании и умении обращаться с регулярными выражениями (или умении программировать) можно искать даже в таком коде.

Другим свойством действительных чисел, облегчающим автоматический поиск известной величины, является само их внутреннее устройство. Достаточно большая длина этих чисел (32 бита, а чаше всего – 64 или 80) и сложный формат хранения позволяет искать числа с плавающей точкой в любых файлах, в том числе и в исполняемых файлах программ, непосредственно в двоичном виде, причем вероятность ложного срабатывания будет незначительной. Даже существование нескольких различных форматов представления действительных чисел не представляет серьезного препятствия – соответствующая программа очень проста и пишется за считанные минуты. Народная мудрость гласит: «лучше один раз увидеть, чем сто раз услышать», поэтому в качестве практики я рекомендую Вам самим написать и отладить такую программу – это не только усовершенствует Ваши навыки в программировании, но и позволит поближе познакомиться с миром действительных чисел. Затем, если захотите, Вы сможете доработать эту программу таким образом, чтобы она могла осуществлять нечеткий поиск в файле, о котором я говорил выше, то есть поиск всех значений, подходящих под заданный пользователем интервал.

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

Хранение строк в блоках фиксированного размера имело два принципиальных недостатка: неэффективное расходование памяти при хранении большого числа строк различной длины и жесткое ограничение на максимальную длину строки. Всех этих недостатков были лишены строки с завершающим символом. Идея была проста – выбирается какой-либо малоиспользуемый символ, который интерпретируется программой как признак конца строки. В языке Си таким символом стал символ с кодом 0 (а строки, оканчивающиеся нулем, окрестили ASCIIZ-строками); некоторые системные функции MS-DOS в качестве завершающего символа использовали символ “$”. Несмотря на ряд недостатков, строки с завершающим символом претерпели ряд усовершенствований и используются до сих пор. С началом активного использования UNICODE появилась модификация строк с завершающим символом и для этой кодировки. Зная образ мышления программистов на Си, нетрудно догадаться, что в качестве завершающего символа была использована пара нулевых байтов: (0,0). Нужно отметить, что если возникает необходимость укоротить такую строку в тексте программы на несколько символов, то обычно для этого достаточно всего лишь вписать в нужную позицию завершающий символ. То есть, если у Вас есть программа, написанная на C/С++, в заголовке окна которой написано что-то вроде «Cool Program - Unregistered», и Вы не хотите видеть напоминание о том, что она «Unregistered», просто замените в файле программы пробел после слова Program на символ с кодом 0. После этого слово «Unregistered» Вы почти наверняка больше не увидите. Этим же способом ненужную строку можно вообще превратить в пустую, просто поставив в ее начало завершающий символ!

Описанная техника укорачивания и «обнуления» строк пригодна не только для того, чтобы убирать неэстетичные надписи в заголовках программ, в действительности ее возможности гораздо шире. Приведу пару примеров из собственной практики. Один раз мне в руки попалась некая программа, которая очень любила при печати документов в заголовок вставлять надпись «This report created by demo version …» шрифтом аршинного размера. Разумеется, мне это не понравилось и при помощи нехитрых манипуляций в шестнадцатиричном редакторе я «обнулил» строку с надписью, оскорблявшей мои эстетические чувства. В другом случае я подобным же образом расправился с одной программой-генератором справок, которая считала, что незарегистрированность – это повод вставлять рекламный текст в каждую статью справочной системы. Небольшой memory patch, исправлявший в программе «на лету» несколько байт, смог убедить капризную программу в ее принципиальной неправоте.

Более эффективными по сравнению с ASCIIZ-строками являются строки с указанием длины. Такие строки позволяют использовать в тексте все 256 ASCII-символов, хранить не только текстовые, но и любые другие двоичные данные, а также применять по отношению к этим данным строковые функции. Кроме того, вычисление длины строки требует лишь одной операции чтения данных по ссылке, в отличие от ASCIIZ-строк, где для определения длины необходимо последовательно сканировать все символы строки до тех пор, пока не встретится завершающий символ. Как такового, стандарта на строки с указанием длины не существует – можно лишь говорить о конкретных реализациях таких строк в различных компиляторах и библиотеках. В частности, в коде программ на Delphi 7 строковые константы хранятся следующим образом:
• 4 байта: длина строки в байтах (для UNICODE-текстов это значение в два раза больше длины строки в символах).
• Содержимое строки.
• Завершающий символ (#0 для ANSI-строк, #0#0 для UNICODE-строк). Завершающий символ никак не используется в «родных» функциях и процедурах Delphi, но значительно упрощает вызов функций WinAPI (которые используют строки с завершающим символом) и использование сторонних библиотек.
Зная все это, нетрудно разработать способ укорачивания Delphiйских строк: для этого требуется изменить длину строки в первых четырех байтах и поставить еще один завершающий символ в нужную позицию. Надо отметить, что тексты-свойства компонентов в ресурсах программ на Delphi хранятся в несколько ином формате, поэтому прежде чем пытаться вмешиваться в код программы, стоит побольше узнать об особенностях реализации встроенных типов в компиляторе, при помощи которого создана исследуемая программа.

Поиск текстовых констант в программе – совсем не такое простое дело, как это могло бы показаться с первого взгляда. Прежде всего, не следует полагаться на «интеллектуальность» дизассемблера: поиск текстовых строк при дизассемблировании обычно основан на анализе ссылок, встречающихся в коде программы. Поэтому, если в коде программы нет прямой ссылки на строку, дизассемблер эту строку может просто «не увидеть». Чтобы убедиться в этом, рассмотрим несложный пример:


 .data
 line1 db "Line 1",0
 line2 db "Line 2",0
 line3 db "Line 3",0
 LineArr dd OFFSET line1, OFFSET line2, OFFSET line3
 
 .code
 …
 GetMsgAddr proc MessageIndex:DWORD
 mov ebx,MessageIndex
 mov eax, OFFSET LineArr
 mov eax,[eax+ebx*4]
 ret
 GetMsgAddr endp
 …
 


Этот код представляет собой максимально упрощенную реализацию списка сообщений и функции, получающей адрес текстовой строки по номеру сообщения. Откомпилировав этот пример, загрузим его в W32Dasm и посмотрим, что получится. Получилось следующее: дизассемблер успешно распознал строку «Line 1», но строки «Line 2» и «Line 3» не обнаружил. А вот IDA успешно распознал все три строки, и создал для них именованные метки. Впрочем, и IDA при большом желании можно обмануть: достаточно лишь вписать перед текстом самой строки ее длину в байтах (именно так хранит строки Delphi). После этого IDA хотя и обнаруживает сам факт наличия текстовых строк в программе (в окне Strings эти строки видны), но в дизассемблированном тексте программы эти строки выглядят как последовательность db… , которые нужно приводить в желаемый вид вручную. Кстати, W32Dasm после этой модификации не увидит вообще ни одной строки. Если же Вам и этого мало, вместо «Line 1» напишите «Строка 1» - все тексты на русском языке знаменитый дизассемблер гордо проигнорирует. И это только начало. А ведь текстовые строки могут находиться не только в сегменте кода/инициализированных данных, но и в секции ресурсов программы…

Здесь могут помочь специализированные программы, сканирующие указанный файл и вычленяющие из него все текстовые строки (или то, что похоже на текстовые строки). Кроме того Вам потребуются смещения этих строк от начала файла, поэтому Ваш инструмент должен предоставлять и такой сервис. Однако использование таких программ (и самостоятельное их написание) осложняется двумя факторами: разнообразием существующих кодировок текста и существованием национальных символов в некоторых языках (классический strings.exe и многие другие подобные программы «не понимают» русскую секцию UNICODE). Те же проблемы с UNICODE и национальными кодировками характерны и для программного обеспечения, осуществляющего поиск в текстовых файлах. К тому же русские тексты в UNICODE совершенно нечитабельны в шестнадцатиричных редакторах и просмотрщиках. Все это необходимо учитывать при выборе инструментов поиска текстовых строк, а выбранный инструмент перед использованием желательно проверить на подходящем «пробном камне».

Напоследок расскажу про весьма простой, но весьма эффективный в некоторых случаях способ поиска численных переменных в работающей программе. Этот способ основан на многократном сканировании адресного пространства программы, отслеживании и анализе всех изменений в этом пространстве. Лучше всего этот прием работает на программах, в которых установлено ограничение на количество тех или иных действий, вроде ограничения на число записей, добавляемых в документ. И используется для этого совсем не крэкерский инструментарий. Вы, наверное, знакомы с программами типа Game Wizard или ArtMoney, которые позволяют искать в работающей компьютерной игре количество денег или оставшихся жизней. Для тех, кто не сталкивался с такими программами, вкратце опишу алгоритм их работы:

1. Пользователь выбирает из списка работающих в данный момент программ подопытную игру.
2. Пользователь вводит в программу поиска начальное количество денег (хитов и т.п.), которое в данный момент существует в игре.
3. Программа сканирует адресное пространство и строит список всех значений (точнее, адресов, по которым расположены эти значения), совпадающих с введенными пользователем.
4. Пользователь выполняет в игре какое-либо действие, в результате которого количество денег изменяется.
5. В программу поиска вводится новое количество денег.
6. Программа проверяет все значения из построенного списка и исключает из него те значения, которые не соответствуют введенному пользователем.
7. Пункты 4-6 повторяются до тех пор, пока список адресов не укоротится настолько, чтобы можно было проверить назначение каждого элемента списка вручную.
8. Пользователь проверяет каждый элемент списка, записывая по найденным адресам новые значения и наблюдая, как это повлияет на количество денег в игре.

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

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

Глава 5. Структуры, массивы, ссылки.


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

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

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

Теперь перейдем к практическим аспектам использования указателей в программах. Здесь работают три очень простых правила:

• к любой константе длиннее 4 байт обращаются по ее адресу
• к любой константе составного или строкового типа обращаются по ее адресу
• в коде любой программы адрес начала статической переменной (т.е. переменной, память под которую выделяется в момент запуска программы) явно указан как минимум один раз независимо от типа этой переменной

Это, конечно, очень широкое обобщение (и не всегда верное – достаточно вспомнить пример с загрузкой 10-байтной вещественной константы в Delphi из предыдущей главы), но, в общем, современные компиляторы действительно используют ссылки весьма широко. И если в программе по адресу X находится строковая константа ‘My text’, то почти наверняка где-то в коде программы найдется команда push X, mov eax,X или что-либо подобное. Именно на этом факте наличия «указателей на все, к чему можно обратиться по указателю», а также на отсутствии путаницы с сегментами и смещениями и основана «разумность» дизассемблеров под Win32, поначалу сильно удивляющая тех, кто успел привыкнуть к маловразумительным листингам дизассемблированных 16-разрядных программ для MS DOS.

Хотя при наличии очень большого желания (и еще большего терпения, чтобы это отладить) все-таки можно обращаться к данным, не используя явные ссылки на эти данные. Не верите? Тогда попробуйте разобраться в следующем коде для платформы Win32 и найти в нем хоть одну ссылку на выводимые в MessageBox’е строки:

 push 0
 call $+1Ah
 db ’Code sample’,0,’Any text’,0
 mov eax,[esp]
 add eax,0Ch
 push eax
 push 0
 call MessageBoxA
 



Такие могучие коды начисто «срывают башню» даже IDA Pro, который оказывается совершенно неспособен более или менее логично дизассемблировать это нечто. Вот уж воистину «горе от ума» - древний, по нынешним меркам, W32DAsm дизассемблировал этот код гораздо адекватнее. Разумеется, знаменитому дизассемблеру при желании можно (и даже не очень сложно) объяснить, где в действительности находятся данные, а где – код, который к этим данным обращается, но чтобы это сделать придется сначала разобраться в приведенном коде самому. Понятно, что, написать целую программу таким «высоким штилем» вряд ли у кого-то получится (да и языки высокого уровня мало приспособлены к подобным экзерсисам), но определение адреса блока данных при помощи пары команд

 call $+5
 pop eax
 


в защитных процедурах (в частности – в процедурах расшифровки кусков кода) встречается довольно часто.

Другим важным моментом, который необходимо помнить при «расшифровке» структур и массивов, хранимых в коде программ, является то, что популярные компиляторы не поддерживают использование структур переменного размера. То есть длина структуры жестко фиксирована и постоянна для всех структур одного и того же типа: если переменная A типа MY_TYPE занимает 100 байт, то переменная B того же типа также будет занимать 100 байт независимо от того, какие данные в ней хранятся. Возникает вполне естественный вопрос: а как же тогда хранятся строки или динамические массивы, размер которых заранее неизвестен? В действительности, в современных компиляторах строки не хранятся непосредственно внутри структур. Вместо этого хранятся лишь указатели на статически или динамически выделенные блоки памяти, а непосредственно текст строки размещается именно в этих блоках. Этим и объясняется смущающий начинающих программистов на Delphi и С++ Builder эффект, когда при попытке определить размер строки при помощи SIZEOF получается, что любая строка занимает 4 байта, а при записи структуры, содержащей поля строкового типа, в файл, вместо текстовых строк появляется странного вида «мусор» длиной в четыре байта. Исключением из этого правила являются только старые паскалевские строки фиксированной длины и массивы символов (char) в Си, но оба эти типа в настоящее время употребляются довольно редко. Кроме того, ссылки на данные вместо самих данных также используются для хранения объектов (тех, которые «экземпляры классов») и динамических массивов.

Если Вы захотите, Вы можете набросать небольшую программку для поиска всех возможных ссылок в коде программ. Базовая идея проста: код программы и статические переменные, как правило, имеют весьма небольшой объем по сравнению с максимально возможным размером адресного пространства в Win32 (4 гигабайта, если не вдаваться в тонкости устройства Windows). А потому мы можем с высокой вероятностью считать ссылками на код или данные все 32-разрядные числа в программе, которые попадают в промежутки адресов, занятые программой или данными. Чтобы проверить все это на практике, так сказать, «потрогать руками», нужна программа, извлекающая из исполняемого файла все четырехбайтные значения, которые теоретически могут быть адресами. Если Вы уже попробовали написать утилиту для поиска нецелочисленных данных и проверили ее в действии, проблем с программированием у Вас не возникнет. Самое сложное – извлечь из заголовка PE-файла адреса начала каждой секции в памяти, размеры этих секций и смещения начала каждой секции в файле. Если Вы не хотите сразу же погружаться в изучение структуры PE-заголовка (а рано или поздно этим все же придется заниматься), на первых порах Вы можете ограничиться ручным вводом этих данных.

А теперь разберемся, что Вы собственно написали. А написали Вы ни что иное, как одну из частей обыкновенного дизассемблера, занимающуюся поиском ссылок в коде программ. Разумеется, настоящие дизассемблеры используют для поиска ссылок гораздо более сложные алгоритмы, но нам сейчас нужен как раз такой простейший инструмент. Разумеется, поиск вообще всех возможных ссылок на данные – само по себе занятие малополезное, но если немного поразмыслить… Если немного поразмыслить, у такой программы появляется довольно неожиданное применение: поиск начальных адресов структур и массивов по одному или нескольким известным полям (элементам). Так что не откладывайте эту программу в дальний угол винчестера – она нам очень скоро пригодится.

Итак, предположим, что Вы знаете о структурах и массивах все, что положено знать начинающему программисту, а именно: что они есть, что в них можно хранить данные, и, самое главное, как в программах обращаются к отдельным элементам этих структур и массивов. Более того, Вы даже знаете, что массивы и структуры можно комбинировать весьма удивительными и изящными способами: создавать массивы массивов (они же двухмерные массивы), массивы структур, массивы массивов структур, ну и так далее. Абсолютное большинство компилирующих языков при хранении данных структурированных типов придерживаются принципа: поля и элементы массивов хранятся в памяти в том порядке, в каком они определены в исходных текстах программы. Ну и, разумеется, порядок полей неизменен для всех данных одного типа. Само по себе это, конечно, мало что дает – ведь исходников-то у нас нет, но тут в игру вступает психология. Да-да, программисты - тоже люди, а изучение их психологии, даже поверхностное, иногда помогает лучше понять творения программистов (то бишь программы). В частности, многим программистам свойственно стремление к логике и элементарному порядку в исходных текстах. Например, если в ходе исследования неких данных Вам удалось установить, что первое и второе поле структуры – это указатели на фамилию и имя, то третье поле структуры скорее всего окажется указателем на отчество, а не закодированным размером обуви или цветом волос. Или, если речь идет о заголовке архива, за именем файла наверняка последует смещение сжатого файла в архиве (плюс-минус некоторая константа), размер этого файла в сжатом виде и контрольная сумма. Причем именно в таком порядке – это традиция, которую программисты нарушать не любят. Если Вы кроме крэкинга занимаетесь программированием, поразмыслите о том, как бы Вы разместили информацию, будь Вы на месте автора программы – и, возможно, это будет наиболее короткий путь к пониманию структуры данных. Именно понимание программистских традиций, «неписанных законов» позволили исследователям недр Windows NT без особых сложностей разобраться с параметрами вызовов Native API – им потребовалось лишь изучить общедоступную документацию, понять, как мыслят программисты в Microsoft и немного повозиться с отладчиком.

Но вернемся к нашим структурам. Проведем небольшой эксперимент: возьмем все тот же Delphi и определим пользовательский тип my_record (в Паскале структуры принято называть записями):


 type my_record=record
  a:byte;
  b:word;
  c:dword;
 end;
 


А теперь попробуем подсчитать, какова длина такой записи в байтах. Если просто сложить длины полей, входящих в запись, должно получиться 1+2+4=7 байт. Но в действительности все обстоит несколько иначе: sizeof (my_record)=8! Чтобы выяснить, почему так случилось, определим в программе переменную my_var типа my_record и попытаемся присвоить полям этой переменной значения: my_var.a:=1; my_var.b:=2; my_var.c:=$ABCDEF10 (надеюсь, Вы внимательно читали предыдущую главу и уже догадались, зачем я присвоил третьему полю столь странное значение). После компиляции мы получим следующий код:


 :00452100 C605005C450001          mov byte ptr [00455C00], 01
 :00452107 66C705025C45000200      mov word ptr [00455C02], 0002
 :00452110 C705045C45004E61BC00    mov dword ptr [00455C04], ABCDEF10
 


Возникает закономерный вопрос: чем так плох адрес 455С01, что по этому адресу компилятор «не захотел» хранить данные. Ответ на этот вопрос лежит в недрах архитектуры x86. С незапамятных времен процессоры x86 выбирали данные, лежащие по четным адресам, немного быстрее, по сравнению с такими же данными, лежащие по нечетным адресам. Чуть позже процессорам стали «нравиться» адреса, кратные четырем. С совершенствованием процессоров список «хороших» адресов продолжал расширяться, появились «особенные» последовательности команд, которые выполнялись быстрее «обыкновенных» и в результате предельная оптимизация программ стала занятие настолько сложным, что стало проще доверить ее компилятору. Для достижения максимальной производительности программисты старались размещать часто используемые данные именно по таким «хорошим» адресам. А чтобы программисту не приходилось раскладывать данные по нужным адресам вручную, в компиляторы была введена такая опция, как «выравнивание данных». Эта опция заставляет компилятор принудительно размещать данные по адресам, кратным величине выравнивания. В нашем случае данные выровнены по четным адресам, поэтому ячейка с адресом 455С01, находящаяся между однобайтным полем a и двухбайтным b, осталась не у дел. Однако если программисту требуется хранить в памяти достаточно большое количество записей, потери памяти из-за выравнивания могут оказаться неприемлемо большими, и в таких случаях выравнивание либо отключают вообще, либо при помощи служебных слов «объясняют» компилятору, к структурам каких типов выравнивание применять не надо.

Использование выравнивания в программах дает один интересный побочный эффект, облегчающий изучение и извлечение данных, хранящихся в программах. Хотя формально значение байтов, находящихся в «дырках» между полями константы-структуры, не определено, на практике все известные мне компиляторы записывают туда нули (что интересно, эти «дырки» при желании тоже можно использовать для хранения данных). В результате достаточно длинный массив таких структур довольно легко определить среди прочих данных «на глаз». Лучше всего для таких целей подходят редакторы, позволяющие при просмотре изменять количество байт, отображаемых в одной строке и выделять цветом характерные последовательности байт (в частности, таким свойством обладает Hex Workshop 4) – цветные пятна образуют характерный узор, который Вы начнете легко замечать после минимальной практики. Тем более, что если отформатировать дамп так, чтобы в строке умещалось столько байт, сколько занимает одна структура, «дырки» выстроятся в вертикальную полосу. Чтобы Вам было понятнее, о чем я говорю, приведу пример массива записей и продемонстрирую, какими путями можно попытаться определить размер и назначение полей структур. Вот код, присутствующий в одной из старых версий моей программы InqSoft Sign 0f Misery:


 28AB5100 74AB5100 8CAB5100 03030D0D 00000000 01002E04 09000000 80000000
 B0AB5100 D0AB5100 00000000 03000F00 00000000 0100CE04 08000000 81000000
 ECAB5100 D0AB5100 00000000 03000F00 00000000 0100C404 08000000 A2000000
 0CAC5100 30AC5100 48AC5100 03030D0D 00000000 0100A604 08000000 A3000000
 


Как видите, факт структурированности данных заметен невооруженным глазом, хотя мы ничего не знаем о том, какие именно данные хранятся в этом массиве. Достаточно очевидно, что размер одного элемента массива равен тридцати двум байтам. Надо отметить, здесь нам очень повезло в том, что размер структуры совпал с числом байт, отображаемых в одной строке. Впрочем, многие шестнадцатеричные редакторы позволяют менять этот параметр и группировать байты произвольным образом (т.е. не обязательно по 4, как это сделано в примере). Попробуем рассуждать логически. Первым делом заглянем в PE-заголовок файла программы и посмотрим начальные адреса и длины (в терминологии PE Explorer’а - Virtual address и Size of Raw Data соответственно) секций. Просуммировав эти характеристики, в первом приближении мы можем считать, что наша программа после загрузки занимает в памяти адреса с 410000h по 6005FFh (хотя, в общем случае, между секциями в памяти могут быть «дыры»). Поэтому числа, попадающие в этот промежуток, с большой вероятностью являются указателями на данные.

Внимательно посмотрев на первый, второй и третий столбец, Вы можете заметить, что в этих столбцах как раз находятся числа из промежутка 410000h..6005FFh, т.е. это потенциальные указатели на данные. Нули, встречающиеся в третьем столбце - это «пустые» указатели; такие указатели широко известны в языках программирования под различными именами. В C/C++ такие указатели обозначаются как NULL, в Паскале/Delphi - nil. Попробуем посмотреть, что находится по адресам из первых трех столбцов первой строки. А находятся по этим адресам ASCIIZ-строки:


 0051AB28: «Ожидать появления окна с указанным текстом в заголовке и классом»
 0051AB74: «Имя класса окна»
 0051AB8C: «Текст в заголовке окна»
 


Если проверить остальные указатели, мы также обнаружим по соответствующим адресам текстовые строки. Отлично! Теперь нам известны длина и тип трех полей структуры. Теперь обратите внимание на шестую тетраду. Числа 042E0001, 04CE0001, 04C40001 мало похожи на осмысленные данные, что в шестнадцатеричной системе, что в десятичной. Но вот если четырехбайтные последовательности интерпретировать не как DWORD, а как два идущих подряд WORD’а, то данные начинают выглядеть менее странно: (1,1070), (1,1230), (1,1220). Правда, мы не можем быть уверенными (как я уже говорил, такова обратная сторона метода, базирующегося на наблюдениях и предположениях), что первые два байта – это действительно одно поле типа DWORD, а не два поля типа BYTE – имеющаяся выборка слишком мала. Чтобы проверить это, необходимо исследовать код программы, который обращается к этому массиву. Но это - тема следующей главы.

Если бы я привел более длинный кусок массива, было бы более очевидно, что четвертая тетрада на самом деле состоит из четырех полей типа BYTE, причем первые два из них принимают значения от нуля до пяти. А также то, что если первый или второй байт четвертой тетрады равен нулю, то вторая или третья тетрада соответственно будет хранить пустой указатель. В общем, определение типов полей по их значениям и структуре массива – занятие, требующее прежде всего наблюдательности и логического мышления.

А пока разберем некоторые тонкости, о которых я умалчивал до этого момента. Вы, наверняка заметили, что, приведя в качестве примера некий кусок кода, я просто сказал «это массив структур», не приведя никаких объяснений, каким образом я обнаружил этот код в программе. На практике задача обычно ставится несколько иначе: по некоторым известным данным необходимо найти в программе область, где эти данные хранятся в структурированном виде, разобраться, какие еще данные, помимо уже известных, содержит эта структура, и затем извлечь то, что нам было неизвестно.

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

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

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

Возьмем любую команду, например «Ожидать появления окна с указанным текстом в заголовке и классом» и попробуем найти этот текст в коде программы. Никаких особых ухищрений для этого не потребуется, при помощи функции поиска в HexWorkshop находим, что искомая строка встречается в файле в единственном экземпляре по смещению 1154856 байт от начала файла. Далее, при помощи любого Offset->RVA конвертера (утилиты, преобразующей смещения в файле в относительные виртуальные адреса (RVA)) определяем, по какому адресу будет размещен 1154856-й байт программы после загрузки этой программы. Получаем, что эта строка в памяти располагается начиная с адреса 51AB28h. Теперь вспомните то, что говорилось о методах хранения строк внутри структур, и Вам станут очевидны наши дальнейшие действия.

Следующий этап будет заключаться в поиске по всему коду программы ссылок на нужную строку, то есть 32-битной константы 51AB28h. Такая константа нашлась сразу же, причем в единственном экземпляре по смещению 1229C0h от начала файла. 128-байтный блок, начинающийся с этой константы, я и привел в качестве примера несколькими абзацами выше. Теперь Вы имеете представление, при помощи каких приемов можно добраться до нужного массива. Как видите, здесь все достаточно просто.

Теперь нам надо выяснить, с какого байта начинается какой-либо элемент приведенного массива. На практике обычно проще всего найти адрес первого элемента массива (или нулевого, если использовать нумерацию, принятую в Ассемблере или Си). Сделать это можно двумя способами: поиском ссылок на начало массива либо анализом самих данных с целью поиска верхней и/или нижней границы массива.

Первый способ базируется на том, что в программе почти наверняка найдется явная ссылка на первый элемент массива, и что адрес этого элемента больше либо равен адресу одного из известных нам элементов, входящих в массив. В применении к нашему примеру это означает, что если смещение одного из найденных нами элементов структуры равно 1229C0h (RVA для этого смещения равно 522BC0h), то начало массива, очевидно, находится не ниже этого адреса. Следовательно, если выбрать из программы все константы меньше либо равные 522BC0h (а для этого нам и нужна соответствующая программа, о которой я уже упоминал), среди них окажется и ссылка на первый элемент массива. При этом, если рассортировать найденные ссылки по убыванию их величин, велика вероятность, что ссылка на начало массива окажется в числе первых.

Продемонстрирую это примером. Допустим, Вы выяснили, что ячейка по адресу 450080h входит в состав массива. Выбрав все ссылки, указывающие на адреса не ниже 450080h, Вы получили следующий набор: 450000h, 49FFD0h, 49FCD4, 49FCD0h и так далее. Из всех найденных Вами адресов теоретически наиболее вероятным адресом будет являться 450000h. Почему? Причина в том, что информация из длинных предварительно инициализированных массивов обычно (но не всегда!) считывается при помощи конструкций вида my_arr[ i ], где значение i явно не указано и на этапе компиляции определено быть не может. И потому, чтобы обратиться к i-му элементу массива, в общем случае программа должна вычислить адрес этого элемента по формуле адрес_элемента = начальный_адрес_массива + размер_элемента * (номер_элемента - номер_первого_элемента). Как мы видим, в этой формуле в явном виде присутствует начальный адрес массива, который мы и ищем в программе.

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

Однако «гладко было на бумаге», а в реальности существует немало «подводных камней», заметно осложняющих использование предложенного метода, а иногда даже и делающего этот метод неприменимым. Прежде всего, проблему могут создать обращения к элементам с явно указанными номерами. Допустим, что в программе кроме конструкций вроде my_arr[ i ] имеются обращения к конкретным элементам, например a=my_arr[2] или if b>my_arr[10] then do_something. Если адреса значений my_arr[2] и my_arr[10] могут быть вычислены на этапе компиляции, то хороший оптимизирующий компилятор их вычислит, подставит в код, а в программе, кроме начального адреса массива, появятся также адреса второго и десятого элемента массива. И тогда при поиске начала массива по предложенному методу Вы можете найти не первый элемент, а второй или десятый – это уж как повезет.

Во что хороший оптимизирующий компилятор может превратить исходный код – это вообще тема большая и интересная. Например, в Delphi допустимы массивы, начальный элемент которых может иметь любой целочисленный индекс, в том числе и отрицательный, т.е. вполне корректно использование выражений вида a:=my_arr[-10]. Да и менее экзотичный массив вроде array [10..100] of my_record не так прост, как может показаться с первого взгляда: посмотрим, как будет выглядеть приведенная выше формула адреса произвольного элемента массива для этого случая.
адрес_элемента = начальный_адрес_массива + размер_структуры_my_record * (номер_элемента – 10)

Очевидно, что эту формулу можно переписать как: адрес_элемента = (начальный_адрес_массива – размер_структуры_my_record * 10) + (размер_структуры_my_record * номер_элемента)

Вы, наверное, заметили, что выражение в первой скобке – константа, которая для предварительно инициализированного массива может быть легко вычислена на этапе компиляции. Эта константа представляет собой адрес виртуального нулевого элемента массива (не секрет, что большинство компиляторов без специальных указаний не проверяют выход за границы массива, а потому Вы можете обратиться в программе даже к тем элементам, которые в действительности не существуют). Поэтому в программах на Паскале/Delphi вместо ссылок на истинное начало массива для экономии одной операции вычитания может использоваться вычисленный таким образом адрес «нулевого» элемента, не существующего в действительности. Подобные приемы иногда используются и при программировании на ассемблере, когда для ускорения программы вместо пары команд
dec ebx
mov eax, [OFFSET my_arr+ebx*4]
используется единственная команда mov eax, [(OFFSET my_arr-4)+ebx*4], в которой значение (OFFSET my_arr-4) вычисляется на этапе компиляции.

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

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

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

 80000000 00800000 20000000 00400000 40000000 78985100 88985100 98985100
 A8985100 B8985100 C8985100 E4985100 00995100 1C995100 30995100 68000000
 C49B5100 E09B5100 00000000 03000E00 01000000 00006400 00000000 97000000
 009C5100 E09B5100 3C9C5100 03030E0D 01000000 00006E00 00000000 6B000000
 


Внимательно посмотрев на приведенный кусок, Вы можете видеть, что массив начинается с восьмой тетрады второй строки. Почему? Во-первых, достаточно очевидно, что первые две строки заметно отличаются от третьей и четвертой, следовательно, границу между массивами нужно искать именно во второй либо в третьей строке. Первая и вторая тетрада второй и третьей строки похожи на указатели на текстовые строки (хотя, если копнуть чуть глубже, можно обнаружить, что только в третьей строке эти ссылки действительно указывают на названия команд). А вот четвертая, пятая, шестая и седьмая тетрады также являются указателями и внешне сильно отличаются от соответствующих тетрад третьей и четвертой строки. Очевиден вывод: все данные по седьмую тетраду второй строки включительно не принадлежат массиву. Осталось разобраться с восьмой тетрадой: она ничем не выделяется на фоне последующих двух, поэтому мы с чистой совестью можем отнести ее к нашему массиву и сказать, что массив начинается именно с нее. Дальше, если проанализировать весь массив, выяснится, что первое поле – уникально для каждого элемента массива, и его значение не превышает 255 (то есть первый элемент структуры - байт). Логично предположить, что это и есть код команды, который мы и искали.

Конец массива отыскивается аналогичным способом; поиск конца массива, пожалуй, даже проще, поскольку очевидно, что размер массива в целом должен быть кратен размеру одного элемента этого массива.

Увы, и этот метод не свободен от недостатков. Во-первых, для успешного использования этого метода необходимо, чтобы элемент массива можно было легко отличить от данных, не имеющих к массиву никакого отношения. Однако так бывает далеко не всегда, например, таблица констант для реализации CRC по алгоритму WiseByte выглядит как набор из 256 псевдослучайных чисел, не имеющих каких-либо характерных признаков, видимых невооруженным глазом. Во-вторых, проблему представляют случаи, когда в одном массиве хранятся разнородные данные. Возможность хранения разнородных данных в структуре предусматривается некоторыми языками программирования («структуры с вариантами» в Паскале и объединения (union) в Си). И в-третьих, желательно, чтобы данные, предшествующие массиву и следующие за ним, заметно отличались по внешнему виду, что совершенно не обязательно выполняется на практике. Более того, программисты, исходя из стремления к порядку в исходных текстах, очень часто располагают однотипные массивы последовательно, чем отнюдь не облегчают жизнь крэкерам.

Как видите, оба способа обладают существенными недостатками, сильно зависят от «человеческого фактора», не гарантируют стопроцентного результата (впрочем, в крэкинге результат вообще никто гарантировать не может), а область их действия – ограничена. Что еще можно сделать, чтобы поднять эффективность этих техник? Ответ прост – необходим синтез обоих методов! Результаты, полученные при помощи второго метода, могут быть проверены поиском указателя на первый элемент в коде программы. И наоборот, если Вы нашли возможную ссылку на начало массива, посмотрите, как выглядит тот участок программы, на который указывает эта ссылка, похож ли он на начало массива, или необходимо внести поправки в свои рассуждения. Вообще, крэкеру (да и не только крэкеру) стоит взять на вооружение следующий принцип: если результат, полученный одним способом, может быть проверен другим способом, эту нужно выполнить проверку. В конечном итоге следование этому принципу позволит Вам избежать ошибок, и из-под Вашей руки выйдет гораздо меньше недопатченных и криво взломанных программ.

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

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

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

Глава 6. Куда попадают данные.


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

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

Когда-то давным-давно, когда 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… А впрочем, почему только «представьте»? Внедряйте, получайте доступ – и считывайте вожделенную информацию!

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

Глава 7. Искусство разбивать окна.


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

Федор Чистяков, «Человек и кошка»


И вот, пройдя долгий и трудный путь исследователя ценных данных, упрятанных в недрах программ, мы, наконец, подошли к вратам «чистого крэкинга». Вспомните, с чего начинаются едва ли не все платные программы: с предложения зарегистрироваться. Это предложение может быть написано аршинными буквами в отдельном окне, появляющемся при запуске, или же в виде маленького MessageBox’а, выскакивающего в самый неподходящий момент во время работы. Оно может быть мерцающим баннером вверху или внизу экрана. Но, как бы оно ни выглядело, цель его существования во всех этих случаях одна: раздражать и давить на психику незарегистрированного пользователя, дабы он, зажав в кулак свои кровные, отправился на почту и перевел их автору программы в знак благодарности за будущее избавление от назойливого окна. Однако далеко не у всех людей, созерцавших эти безыскусные творения, возникали столь теплые чувства к разработчику. И эти раздраженные пользователи дали надоедливым окнам то самое имя, под которым они и известны современному крэкеру. Называются эти окна «nag screens» (от английского «nag» - «надоедать, приставать, придираться»), и в этой главе речь как раз пойдет о способах борьбы с такими окнами.

Я, увы, лично не застал рождение крэкинга под платформу Win32, но опоздал ненамного, а потому в моей коллекции имеются многие популярные руководства тех времен. Тогда первым словом едва ли не каждого крэкера была классическая команда bpx MessageBoxA – другие варианты были очень, очень редким исключением. Тридцатидвухразрядные Delphi были лишь светлой мечтой, монструозный MFC, спроектированный на базе логики пришельцев с Альфы Центавра, не пользовался особой популярностью, и потому большинство программистов тогда обходилось одним лишь Win32 API. А из всех функций WinAPI MessageBox была едва ли не самой популярной – ибо трудно было придумать более простое и универсальное средство вывести какое-нибудь нехитрое сообщение, да так, чтобы пользователь не смог это сообщение проигнорировать. Разумеется, разработчики shareware-программ не могли пройти мимо такой возможности, и первые «надоедливые экраны» были теми самыми MessageBox’ами. Мы же, в свою очередь, нещадно bpx’или эти MessageBox’ы – и выискивали точку ветвления, в которой расходились жизненные пути зарегистрированной и незарегистрированной инкарнации программы. И если когда-нибудь появится мемориал в честь крэкеров Вселенной – на нем, несомненно, огромными золотыми буквами будет выбито bpx MessageBoxA.

Но все-таки я начну рассказ о борьбе с нежелательными визуальными спецэффектами в программах не с MessageBox’ов. Ибо я стараюсь следовать принципу «от простого – к сложному», а стандартные окна с сообщениями – это все-таки не самое простое из того, с чем Вы можете столкнуться. Согласитесь, чтобы удалить из программы лишний MessageBox, все-таки надо приложить некоторые усилия по обнаружению этого MessageBox’а в коде программы при помощи отладчика или дизассемблера – поэтому я начну с рассказа о таких способах удаления nag screen’ов, которые не предполагают выхода на уровень ассемблера. Вы спросите – возможно ли такое? Конечно, возможно! И, как это ни удивительно, такую возможность подарила нам операционная система Windows.

Прежде чем переходить к рассмотрению методов борьбы с нежелательными окнами и конкретных примеров их применения, рассмотрим проблему баннеров и nag screen’ов, что называется, в перспективе. Я бы выделил четыре подхода к задаче ликвидации рекламных вставок в программах:
1. Изменение свойств объектов (окон, визуальных компонентов) таким образом, чтобы они не отображались на экране. На практике это может быть достигнуто тремя способами: выносом нежелательного объекта за пределы экрана (проще всего использовать отрицательные координаты – при любых размерах монитора объект все равно будет за пределами видимой части экрана); изменением размеров объекта (к примеру, дочерние окна нулевой длины и ширины на экране не видны); модификацией шаблонов диалогов с тем, чтобы придать нежелательным объектам свойства невидимости и неактивности.
2. Удаление шаблонов объектов из исполняемого файла.
3. Модификация кода с целью предотвратить создание или отображение нежелательного объекта.
4. Модификация кода таким образом, чтобы нежелательный объект самоликвидировался сразу после появления.
Вот о первых двух подходах, в основном, и пойдет речь в этой главе.

Ради того, чтобы упростить жизнь программистам, в ОС Windows реализована возможность интегрировать в исполняемый файл ресурсы всевозможных типов – текстовые строки, иконки, изображения в стандартных форматах и, что самое интересное, так называемые «шаблоны» (templates) диалоговых окон. Эти шаблоны описывают внешний вид диалогов: размеры, атрибуты, находящиеся внутри диалога управляющие элементы (меню, кнопки, поля редактирования и т.п.) и надписи на этих элементах. При помощи соответствующих функций Windows API на основе этих шаблонов могут быть созданы реальные окна, внешний вид которых будет в точности соответствовать их описанию внутри шаблона. Открыв такой исполняемый файл при помощи какого-либо редактора ресурсов (наиболее известны Resource Hacker, Restorator и eXeScope), Вы можете попытаться модифицировать ресурсы программы, например, перевести все надписи на русский язык (если быть до конца точным, то у ресурсов имеется еще один атрибут – идентификатор языка, который, возможно, также потребуется модифицировать). Если Вы все сделаете правильно, и редактор ресурсов корректно сохранит изменения, после запуска программы соответствующие надписи будут русифицированы. Таким же образом Вы можете изменять расположение и свойства элементов интерфейса (например, наличие рамки и ее цвет) в диалоговых окнах. Более того, Вы можете заменить не только надписи, но и хранящиеся в ресурсах изображения, своими собственными. Нужно отметить, что ресурсы могут храниться не только внутри исполняемого файла, но и в любом другом файле формата Portable Executable. Чаще всего это делается для облегчения локализации – реализовать выбор одной из возможных ресурсных DLL гораздо проще, чем выпускать множество локализованных версий одного и того же исполняемого файла. Кроме того, ресурсные DLL – довольно удобный способ хранения «шкурок» (skin’ов) для приложений, использующих эту технологию модификации интерфейса.

Вообще все ресурсы делятся на типы, среди которых есть такие, как иконки (RT_ICON), курсоры (RT_CURSOR), шаблоны диалоговых окон (RT_DIALOG) и многое другое. Для идентификации каждого конкретного ресурса среди других ресурсов того же типа используются числовые либо символьные идентификаторы. Именно на основе идентификаторов программа и различает ресурсы между собой. Что интересно, программе совершенно нет дела до тех данных, которые прячутся за идентификаторами – поэтому, поменяв местами идентификаторы у пары однотипных ресурсов, можно получить весьма занятные эффекты: например, вместо сообщения об успешном завершении той или иной операции будет появляться сообщение об ошибке (и наоборот).

Теперь немного поговорим о том, какие тайны скрываются в недрах шаблонов диалоговых окон. Прежде всего, умение обращаться с шаблонами позволит Вам полностью преобразить интерфейсы очень многих программ, причем сделать это с минимальными затратами сил. Большинство редакторов ресурсов умеют не только декодировать шаблоны в исходный текст, понятный компиляторам ресурсов (при желании Вы можете «одолжить» особенно понравившееся окно из чужой программы), но и непосредственно показывать, как этот диалог будет выглядеть на мониторе. А наиболее продвинутые редакторы позволяют даже править эти шаблоны визуальными средствами. Кто знаком с современными визуальными средствами разработки, тот, наверняка, догадался, о чем идет речь. А те, кто не догадался, могут просто взять Калькулятор из состава Windows 98 (с Калькулятором от Windows XP такой номер может не пройти – всяческие MUI могут отравить Вам радость познания), программку Resource Hacker и вдоволь поэкспериментировать над первой программой при помощи второй: например, поменять местами все кнопки в Калькуляторе. Причем настоятельно рекомендую Вам попытаться проделать эту операцию не только тасканием-бросанием кнопок а-ля Visual Basic, но и через ручную правку ресурсного скрипта (resource script).

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

Довольно часто авторы shareware пользуются следующим приемом: сразу после запуска программы выводится nag screen, кнопка закрытия которого изначально неактивна и активизируется лишь спустя несколько секунд. Внутри программы это может быть реализовано следующим образом: в шаблоне, по которому создается nag screen, стиль этой кнопки изначально определен как неактивный (он называется WS_DISABLED). После создания окна запускается таймер, который через установленное время активизирует нужную кнопку, и пользователь получает возможность закрыть окно. Однако если мы уберем у кнопки стиль WS_DISABLED, пользователю больше не потребуется ждать несколько томительных секунд, чтобы получить возможность нажать на кнопку. Аналогичным же образом иногда удается включить неактивные функциональные элементы не только на nag screen’ах, но и непосредственно в самой ломаемой программе. Увы, всвязи с появлением некоторого количества программ, способных принудительно включать неактивные элементы управления, закрывать «лишние» окна и проделывать прочие столь же милые вещицы (ваш покорный слуга со своим проектом Sign 0f Misery тоже отметился на этом поприще), авторы shareware все чаще вводят различные дополнительные проверки, призванные оградить назязчивую рекламу от священного права пользователя эту рекламу не смотреть.

Однако самое интересное заключено даже не в манипуляциях с идентификаторами и стилями. Куда более важным представляется вопрос – а что если просто взять и удалить из секции ресурсов «лишний» диалог? Будем рассуждать логически: создание диалоговых окон производится при помощи семейства функций CreateDialog*** (которые затем требуют принудительно отобразить окно при помощи ShowWindow) либо при помощи DialogBox*** (это семейство функций все операции по отображению окна берет на себя). Причем для создания модальных окон (т.е. таких, которые пользователь не смог бы проигнорировать) чаще используется именно второе семейство функций.

Предположим, что программа попыталась создать nag screen на основе шаблона диалога при помощи функции DialogBoxParam (или любой другой функции из семейства DialogBox***), и у нее это не получилось. Большинству разработчиков такой вариант обычно даже в голову не приходит – а потому и проверку на существование шаблона nag screen’а программа обычно не делает, а сразу переходит созданию и выводу окна на экран. Что бы Вы сделали, если бы Вас попросили создать окно на основе «никакого» шаблона? Правильно, абсолютно ничего! Вот и Windows поступает точно так же – а именно, ничего не делает. Хотя на самом деле такое «ничего» все-таки может иметь далеко идущие последствия: ни разу не выполнится, к примеру, оконная процедура выдранного с корнем диалога, что, в свою очередь может повлечь за собой труднопредсказуемые эффекты. Вспомните принцип минимального вмешательства – и ужаснитесь тому, что предлагаю Вам содеять: вырвать из программы особо ценный (с точки зрения разработчика, разумеется) ресурс, отключить «не глядя» оконную процедуру, спровоцировать передачу некорректных данных как минимум в одну функцию WinAPI…

Но «суха теория, мой друг…», а потому все же посмотрим, что там у нас выросло на зеленеющем «древе жизни». В качестве наглядного пособия я возьму программу SkyMap Pro 8 Demo, которая обладает одним весьма ценным для нас качеством – наличием nag screen’а, шаблон которого упрятан глубоко в недрах секции ресурсов и имеет идентификатор 3006. В качестве орудия, при помощи которого производилась ампутация надоедливого диалога, я воспользовался программой Resource Hacker (в принципе, подошел бы и любой другой редактор ресурсов). Сама операция заняла не более 15 секунд, после чего исполняемый файл «похудел» на 4 килобайта. После того, как я произвел пробный запуск исправленной программы, оказалось, что nag screen как рукой сняло – и мне для этого не потребовались отладчики, дизассемблеры и прочая крэкерская «тяжелая артиллерия»! Видите: все далеко не так страшно, как может показаться. В принципе, если Вы считаете, что выдирание диалога с корнем – слишком уж варварская операция, шаблон можно оставить на месте, лишь заменив его идентификатор на другой, не используемый в программе. Результат будет аналогичный – программа не сможет найти ресурс с измененным идентификатором и не создаст nag screen. Этот прием даже предпочтительнее – практика показала, что не всегда и не во всех редакторах ресурсов операция удаления проходит успешно.

А ведь есть еще и третий путь избавиться от диалога путем изменения идентификатора! Ничто не мешает исправить идентификатор не в секции ресурсов, а непосредственно в исполняемом коде, в том месте, где этот идентификатор передается в функции WinAPI. Поскольку наш идентификатор имеет довольно редкое значение 3006, он как нельзя лучше подходит для поиска внутри исполняемого файла. Найдя среди всех подходящих значений то, которое отвечает за создание nag screen’а – замените его каким-нибудь другим, не указывающим ни на один диалог, например – на 12345. Когда Вы запустите исправленное приложение, оно, конечно, попытается найти и загрузить шаблон с ID=12345 – но у программы это все равно не получится. Впрочем, я считаю такой путь излишне изощренным и рассказал о нем главным образом в познавательных целях – гораздо проще забить вызов функции создания диалога и все, что с ней связано, nop’ами и получить тот же самый эффект.

Успешность такого метода борьбы с «лишними» окнами, вообще, сильно зависит от внутренней логики приложения. Nag screen, к примеру, может оказаться не «пустышкой», предназначенной исключительно для раздражения пользователя, а исполнять какие-либо неочевидные функции. Например, на обработку сообщения WM_INITDIALOG в оконной процедуре nag screen’а может быть подвешена инициализация каких-либо критически важных для программы структур. В общем случае такая инициализация может происходить при возникновении практически любого события связанного с этим окном: при нажатии на какую-либо кнопку в окне, при закрытии окна, в момент поступления первого сообщения WM_PAINT – перечислять варианты можно очень долго. И если просто удалить nag screen, это «волшебное» событие не произойдет – следовательно, не будет произведена инициализация, отчего, в итоге, программа будет работать некорректно, если вообще будет работать.

На практике такая защитная техника в чистом виде встречается достаточно редко – ведь программисту, использовавшему подобный прием, придется учитывать при программировании различия в работе между полной/зарегистрированной (без nag screen’а) и урезанной/незарегистрированной версиями программы. Тем не менее, если Вы все же столкнетесь с реализацией такого алгоритма, Вам вряд ли станет легче от осознания редкости встреченной проблемы. Поэтому давайте подумаем о том, как эту проблему можно было бы решить. В теории решение довольно простое: Вам нужно найти функции, которые выполняют все нужные действия по инициализации, и вызвать их вручную. На практике Вам скорее всего понадобится добраться до цикла выборки сообщений внутри оконной процедуры и проанализировать его содержимое. Обратите также внимание на следующий факт: если Вам удалась обнаружить, что инициализация программы происходит в ответ на WM_INITDIALOG, из этого отнюдь не следует, что Вам необходимо выполнить все действия, которые программа выполняет при поступлении этого сообщения. Посмотрите на следующий пример:

 .IF uMsg==WM_INITDIALOG
  invoke GetDlgItem, hWnd, TRIALUSE_BTN
  invoke SetFocus,eax
  invoke InitProgram
 


После того, как мы умозрительно удалили диалог, которому должно было поступать сообщение, попытка установить фокус на кнопку TRIALUSE_BTN становится совершенно бессмысленна – поскольку больше нет диалога, нет и кнопки которая в этом диалоговом окне находилась. Поэтому в нашем простейшем примере вместо вызова функций создания диалога было бы достаточно просто написать call InitProgram, а все лишние байты забить nop’ами. Однако представьте себе, что оконная процедура содержит какие-либо локальные переменные (например, вычисленное ранее число дней до истечения триального срока), используемые внутри InitProgram или же проделывает какие-либо операции с созданным диалогом – и Вы поймете, почему простейший путь не является лучшим. Что делать в таком случае? Из каждой безвыходной ситуации есть как минимум два выхода. Можно углубиться в дебри процедуры InitProgram и проверить, не обращается ли она к каким-либо объектам, которые мы так лихо снесли вместе с диалогом и потом долго заниматься художественной штопкой машинного кода. Я сам пару раз применял такую технику и могу сказать определенно: этот процесс мне совершенно не понравился. Поэтому мы поступим хитрее – попробуем поискать другое, более простое решение.

Практически в любом nag screen’е изначально заложен способ от него избавиться. Этим способом может быть клик по кнопке в диалоге, нажатие «горячей клавиши» или же истечение определенного времени с момента появления окна. Все эти случаи объединяет одно - nag screen ждет некоего события, наступление которого станет для него сигналом к исчезновению. «Приемник» этого сигнала, который и выполняет все действия по убиранию nag screen’а с экрана, чаще всего прячется во все той же оконной процедуре, о которой уже столько раз говорилось и еще не раз будет сказано. Для тех, кто не особо интересовался программированием с использованием «чистого» WinAPI, дам некоторые пояснения относительно традиционного устройства оконной процедуры.

«Сердцем» оконной процедуры, несомненно, является обработка сообщений, поступающих от окна, которая на ассемблере выглядит приблизительно так:

 .IF uMsg==WM_DESTROY
   invoke PostQuitMessage,NULL
 .ELSEIF uMsg==WM_INITDIALOG
  ...
 .ELSEIF uMsg==WM_COMMAND
   mov eax,wParam
   .IF ax==ID1
    ...
   .ELSEIF ax==ID2
    ...
   .ENDIF
 .ELSE
   invoke DefWindowProc,hWnd,uMsg,wParam,lParam
   ret
 .ENDIF
 


Не вдаваясь в подробности (которые Вы можете найти в документации и в книгах по программированию под Windows), скажу, что обычно все самое интересное - обработка нажатий на кнопки или реакция на выбор пунктов меню - скрывается в блоке, анализирующем сообщение WM_COMMAND. Внутри этого блока обычно выполняется последовательное сравнение параметров сообщения wParam и lParam с идентификаторами и хэндлами управляющих элементов (кнопок, элементов меню, тулбаров и т.п.). Если значения хэндла и идентификатора указывают, что пользователь нажал на некий элемент - программа выполняет соответствующие действия. Допустим, что мы знаем, где располагается оконная процедура интересующего нас окна. Что мы можем сделать с нашей находкой? Самое простое – это, конечно, возможность менять местами функции управляющих элементов: если в приведенном выше примере поменять местами значения ID1 и ID2, соответственно изменятся и функции элементов с идентификаторами ID1 и ID2. Менее очевиден, но тоже достаточно прост для понимания тот факт, что можно «переключить» реакцию программы с одного сообщения на другое. К примеру, заменив в нашем примере WM_INITDIALOG на WM_PAINT, мы заставим процедуру инициализации выполняться не единожды при создании диалога, а при поступлении каждой команды на перерисовку содержимого окна. Зачем такое может понадобиться? С точки зрения среднего программиста это действо совершенно нелогично, нефункционально, и даже более того – опасно. Но крэкинг – это искусство, существующее по ту сторону обычного программирования. И именно благодаря своей «потусторонней» сущности даже во внешне бессмысленных операциях крэкер способен узреть потаенные возможности и раскрыть их к своей пользе.

Итак, представим также, что кнопка ID1 закрывает nag screen (на самом деле представлять придется только Вам – у меня код этого примера сейчас перед глазами). Наша задача – убрать nag screen любыми доступными средствами. Как можно решить эту задачу? Да очень просто - базовую идею я описал парой строчек выше. Перво-наперво заменим в оконной процедуре константу WM_COMMAND на WM_PAINT. Следующее, что нам нужно – это чтобы оконная процедура реагировала на сообщение WM_PAINT как на нажатие кнопки с идентификатором ID1. Добиться этого можно, подправив условие IF ax==ID1 таким образом, чтобы оно выполнялось в любом случае, независимо от величины wParam. Думаю, проделать такой фокус не составит никакого труда даже для самого начинающего крэкера, только-только научившегося заменять условный переход на безусловный. Если в оконной процедуре присутствует «настоящий» обработчик WM_PAINT, есть смысл при помощи безусловного перехода выполнить и его – ради соблюдения принципа минимального вмешательства. На этом нашу миссию можно считать завершенной – после внесенных исправлений nag screen будет закрываться сам собой сразу же после появления. Главная хитрость заключается в том, что как только nag screen пытается стать видимым (а Вы когда-нибудь видели невидимые nag screen’ы?), окно этого screen’а автоматически получит команду на перерисовку содержимого клиентской области - сообщение WM_PAINT. Мы же, движимые возвышенными эстетическими чувствами, протестующими против созерцания рекламных банальностей, преобразовали это безобидное сообщение в приказ немедленно убрать раздражающее окно с экрана.

Все, о чем я говорил выше, относится к классическим средствам работы с окнами и диалогами Windows и к объектно-ориентированным «оберткам» для WinAPI. Однако фирма Borland внесла в такую консервативную область, как графические интерфейсы Windows-программ, заметное оживление. Я имею в виду прежде всего фирменную борландовскую разработку: Visual Component Library (она же VCL) – объектно-ориентированную библиотеку, предназначенную для создания графического интерфейса. Впервые эта библиотека появилась в Delphi 1 и с тех пор стала неотъемлемой частью всех версий Delphi и C++ Builder. В VCL широко используются такие нетрадиционные для Windows вещи, как «безоконные» управляющие элементы (non-windowed controls), по сути представляющие собой картинки на экране, и потому напрямую не подчиняющиеся функциям WinAPI. Другим новшеством, внедренным Borland, является специфический формат хранения шаблонов форм – они хранятся как данные типа RCData, причем в качестве идентификаторов используются названия классов этих форм: к примеру, шаблон формы класса TMyForm хранится в ресурсах под именем TMYFORM. Ну и, наконец, VCL отличается от большинства библиотек аналогичной направленности обширной и довольно сложной иерархией классов.

Давайте возьмем какую-нибудь программу на Delphi (я взял одну из своих разработок) и попробуем повторить эксперимент с удалением шаблона какого-нибудь диалога. Открываем файл программы в Resource Hacker, выбираем любую понравившуюся форму – и видим, что Resource Hacker нещадно глючит, пытаясь перевести шаблон из внутреннего борландовского формата в более удобочитаемое нечто. На самом деле Resource Hacker довольно успешно понимает формат Borland’овских шаблонов – просто я использовал слишком новую версию Delphi, о которой наш редактор ресурсов, увы, ничего не знает. Кстати, если взглянуть на тот же ресурс в шестнадцатиричном редакторе, Вы увидите перед собой малопонятную кучу байт, в которую вкраплены имена компонентов и их свойств, а также текстовые значения. Но мы пока не будем пытаться дешифровать шаблон, а просто удалим его. Удаляем, запускаем, пытаемся вызвать соответствующее окно – и получаем сообщение Resource <имя удаленного нами ресурса> not found. Да, это определенно не то, о чем мы мечтали – nag screen, конечно, исчез, но окошко с сообщением об ошибке смотрится ничуть не лучше. Увы, так просто от nag screen’ов в программах на Delphi не избавиться.

Поскольку Resource Hacker показал себя не с лучшей стороны, внесем коррективы в наш инструментарий: возьмем eXeScope 6.41 (последняя версия на момент написания данной главы) – эта программа более-менее успешно переваривает шаблоны Delphi 7 с надписями в кодировке Unicode. Если Вы не хотите нарушать ничьих авторских прав хотя бы в процессе обучения, раздобудьте для экспериментов мою программку InqSoft Window Scanner – лицензионное соглашение дает Вам полный карт-бланш на ее ломание в учебных целях. Распакуйте ее UPX’ом, откройте полученный исполняемый файл в eXeScope и найдите шаблон формы TAboutForm. Вы увидите что-то вроде:


 object AboutForm: TAboutForm
   Left = 265
   Top = 185
   BorderIcons = [biSystemMenu]
   BorderStyle = bsSingle
   Caption = #1054’ ’#1087#1088#1086#1075#1088#1072#1084#1084#1077’...’
   ClientHeight = 192
   ClientWidth = 318
   ...
 end
 …
 


Если Вы раньше программировали на Delphi или C++ Builder, Вы наверняка уже догадались, что это – текст типичного dfm-файла, в котором описан шаблон формы вместе со всеми ее компонентами, как визуальными, так и не очень. Если же Вы не программировали ни на Delphi, ни на C++ Builder – Вам будет непросто разобраться в ломании программ, написанных при помощи этих средств разработки. Однако в этой главе мы будем разбирать достаточно простые вещи, для понимания которых достаточно знания английского языка, основ ООП и знакомства с любым визуальным средством разработки. В конце-концов, тот, кто умеет помещать управляющие элементы на шаблоны диалогов в RadAsm’е, вряд ли испытает какие-либо трудности в понимании того, как бросать точно такие же компоненты на формы в Delphi. Но в общем случае работает совершенно обратное правило: чтобы эффективно исследовать программу, необходимо иметь представление об инструментах, с использованием которых эта программа была написана. То есть, чтобы взломать программу, написанную на С++ Builder, нужно знать характерные особенности С++ Builder.

Одной из таких характерных особенностей, к примеру, является то, что имена классов окон (понятие «класс окна» здесь используется в том же смысле, что и в документации по Windows API) в Delphi/С++ Builder начинаются с буквы T и совпадают с именами классов форм. Проще говоря, если программа создает окно как экземпляр класса TMyForm, то имя класса окна, возвращаемое WinAPI’шной функцией GetClassName, тоже будет TMyForm. Собственно, InqSoft Window Scanner в качестве объекта для экспериментов я посоветовал не случайно – эта программа, помимо прочих функций, как раз умеет определять имена классов окон. Как Вы помните, имена классов форм в программе совпадают со строковыми идентификаторами шаблонов этих форм в секции ресурсов, поэтому «подглядев» средствами WinAPI имя класса окна, Вы смело можете шерстить секцию ресурсов на предмет наличия ресурса типа RCDATA с соответствующим идентификатором, и, скорее всего, Вы такой ресурс обнаружите.

Строго говоря, использование механизмов наследования в Delphi/C++ Builder позволяет создавать формы, у которых имя класса окна отличается от символьного идентификатора шаблона, но в большинстве программ эта возможность не используется. Да и распознать форму-наследника по dfm-описанию родителя обычно бывает не так уж сложно, поэтому я не буду заострять внимание на этом достаточно специфическом случае.

Итак, что нам показал eXeScope? А показал он описание формы и свойства всех ее компонентов, причем в самом что ни на есть текстовом виде. Вы наверняка уже поняли, что слово object начинает описание объекта (или компонента, если быть до конца точным), а слово end это описание завершает. И что строки вроде Left = 265, BorderIcons = [biSystemMenu] или BorderStyle = bsSingle представляют собой ни что иное, как имена свойств компонентов и значения этих свойств, как они были заданы во время разработки приложения. И, что самое приятное, Вы можете не только смотреть на эти свойства, но и редактировать их. Исправить числовые или текстовые значения – дело нехитрое, стираем старое значение, вписываем новое, сохраняем результат, и можно любоваться эффектом. Но вот если понадобится исправить свойство вроде BorderStyle – скорее всего придется заглянуть в документацию по Delphi, чтобы узнать, что такое bsSingle и какие еще значения может принимать это свойство. Пока что все довольно просто – и чем-то напоминает наши эксперименты по редактированию шаблонов диалогов в Калькуляторе.

Впрочем, сказав, что изменить значение свойства – дело нехитрое, я был не совсем прав. Состояние современного инструментария таково, что если пользоваться специализированными программами (тем же eXeScope, например), то эта операция не всегда выполняется корректно. Поэтому на практике мне нередко приходилось пользоваться следующим приемом – оригинальное значение я читал при помощи eXeScope, но вот новые данные вписывал уже в шестнадцатиричном редакторе; о том, как я определял, какие байты и где надо было менять, я думаю, в очередной раз повторяться не стоит. В самых тяжелых случаях можно даже экспортировать шаблон формы в dfm-файл, а затем попытаться при помощи той же версии Delphi/C++ Builder впихнуть этот dfm-файл в тестовый проект, откомпилировать и затем перенести шаблон формы из тестового проекта на его «родное» место в подопытной программе. При этом, разумеется, придется обеспечить совпадение версий компилятора и, если на форме имеются нестандартные компоненты, раздобыть и установить точно такие же. Как Вы догадываетесь, весь этот процесс весьма хлопотный, и такая игра вряд ли стоит свеч – я подобную «хирургическую операцию» по пересадке ресурсов проделывал лишь единожды, и то в основном в порядке эксперимента. Но вот изучение тестовых примеров перед тем, как начинать править свойства – вещь очень и очень полезная, поскольку помимо чисто практической пользы это помогает лучше понять внутреннюю логику инструментальных средств, при помощи которых создана программа.

После того, как мы убедились в возможности редактировать отдельные свойства компонентов, было бы нелишне узнать, что произойдет в том случае, если удалить описание какого-либо свойства. Как ни странно, если выполнить удаление корректно – то обычно ничего страшного не случается. Хотя, справедливости ради надо отметить, что в некоторых случаях удаление свойства приводит к катастрофическим последствиям – прежде всего, когда удаляемое свойство ссылается на другой компонент. Но, к счастью, такие специфические компоненты чаще всего не имеют никакого отношения в nag screen’ам и баннерам.

Причина «нечувствительности» программ к исчезновению свойств следующая: в Delphi и С++ Builder создание объекта на основе шаблона производится в два этапа. На первом этапе собственно создается объект, а все его свойства и внутренние структуры инициализируются значениями по умолчанию. И лишь на втором этапе новые значения свойств, которые хранятся в шаблоне диалога, помещаются на место значений по умолчанию. Неудивительно и то, что в eXeScope Вы видите у каждого компонента намного меньше свойств, чем видит разработчик, редактируя форму в IDE: для уменьшения объема исполняемого файла в ресурсах сохраняются не все значения свойств, а только те, которые отличаются от значений по умолчанию. Например, в приведенном выше отрывке описания формы AboutForm Вы не увидите свойство AutoSize, несмотря на то, что такое свойство у формы имеется. Причина этого проста: значение свойства AutoSize по умолчанию было равно false, и автору (то есть мне) не пришлось его менять, поскольку оно вполне меня устраивало.

Итак, теперь мы знаем, что описания свойств удалять можно. Осталось лишь разобраться, зачем это нам может понадобиться. В этот раз я отступлю от принципа «сначала - теория, потом - практика» и начну с описания небольшого эксперимента. Возьмите все тот же InqSoft Window Scanner, запустите и нажмите в нем кнопку «О программе…». Вы увидите окно с информацией о программе, в левой части которого будет находиться логотип. А теперь представьте себе, что это не безобидная картинка, которая никому не мешает, а ужасный черно-зелено-оранжевый баннер размером в пол-экрана, от которого мы хотели бы избавиться.

Следующим шагом будет определение идентификатора шаблона. Запустите вторую копию Window Scanner’а и «просканируйте» окно «О программе…». В очередной раз убедившись, что класс окна носит имя TAboutForm (да, я знаю, что написал про это несчастное окно уже как минимум пять килобайт – но мы будем действовать так, как будто видим программу впервые), лезем в секцию ресурсов и ищем там шаблон формы, спрятавшийся за идентификатором TABOUTFORM. Теперь нам нужно найти в шаблоне формы описание нашего «баннера». Нетрудно догадаться, что в этой роли выступает объект Image1 (поскольку картинка на форме одна, догадаться, что Image1 и есть наш «баннер», нетрудно). Дешифрованное описание этого объекта выглядит следующим образом:

 object Image1: TImage
   Left = 0
   Top = 0
   Width = 57
   Height = 192
   AutoSize = True
   Picture.Data = {
     0A544A504547496D616765F5260000FFD8FFE000104A46494600010200000100
     ............
     3EEBFFD9}
 end
 


Теперь можно сделать то, ради чего мы и затевали все эти поиски: удалите свойство Picture.Data, запустите программу и посмотрите, что случилось с нашей формой. А случилось именно то, чего мы и хотели добиться - логотип исчез, как будто его и не было. Сам объект, разумеется, никуда не делся – мы лишь удалили связанное с ним изображение, «подчистив» свойство Picture. Вспомните, что я говорил о двух этапах создания форм в Delphi - и Вы легко поймете, каким образом изменилась логика работы программы: стерев значение, которое было назначено свойству Picture.Data программистом, Вы не оставили программе иного выхода, как использовать значение этого свойства по умолчанию. А значением по умолчанию в нашем случае оказалось «никакое» изображение (т.е. отсутствие изображения), которое программа успешно отобразила. Более того, удалять можно не только свойства объектов, но и объекты целиком. Однако такое действо является куда более грубым вмешательством в код программы, да и ограничений в этом случае намного больше.

А теперь давайте посмотрим, как простым редактированием ресурсов можно избавиться от настоящего, вполне реального баннера. Для этого нам потребуется программа Ghost Installer Free Edition. XML-редактор, входящий в состав этой программы, как раз содержит баннер. И этот баннер, кроме того, что занимает довольно много места внизу экрана и сокращает поле редактирования, еще и постоянно мерцает, раздражая пользователя. Так что если Вы активно пользуетесь бесплатной редакцией Ghost Installer’а, Вам придется либо проявлять чудеса терпеливости, либо спасти огромное количество нервных клеток, избавившись от созерцания зловредного баннера.

Сначала при помощи InqSoft Window Scanner определим имя класса окна той формы, которую мы собираемся править: TfrmProjectSource. Заодно будет нелишне просканировать и сам баннер – вдруг это позволит нам узнать хотя бы тип компонента, на основе которого этот баннер был создан. А если нам удастся узнать тип компонента, это заметно облегчит задачу поиска самого компонента в дешифрованном шаблоне формы. Пробуем просканировать окно баннера, и узнаем, что на самом деле наш баннер – ни что иное, как кусок Internet Explorer’а, засунутый в программу при помощи технологии ActiveX. Чтож, с первого захода мы получили явно недостаточно полезной информации. Поэтому попробуем копнуть глубже и просканить всю ветку дерева окон, имеющую отношение к нашему баннеру. InqSoft Window Scanner выдал следующий результат:

 +[00050356] {TPanel}
  *[00010414] {TElSplitter} 
  +[00050370] {TPanel}
   +[0001040E] {TPanel}
    +[00010410] Panel1 {TPanel}
     +[0043036A] {Shell Embedding}
      +[000103D4] {Shell DocObject View}
       *[00010412] {Internet Explorer_Server}
 

Что это значит? Всего лишь то, что наш ошметок Internet Explorer’а лежит на компоненте Panel1 типа TPanel (тут программист явно поленился стереть свойство Caption у объекта Panel1, чем сильно облегчил нам задачу поиска нужного объекта). Сам объект Panel1 в свою очередь лежит на другой панели, которая лежит на панели, которая… В общем, всяких панелей там много, и разбираться в них можно долго. Поэтому я предлагаю начать наше расследование с той панели, которую мы идентифицировали однозначно. Откроем файл GIEditor.exe в Restorator 2.52 (да, Вам опять придется обновить инструментарий – практика показала, что eXeScope с редактированием шаблона в данной программе не справился, а вот Restorator сработал как нельзя лучше), выберем шаблон нашей формы и в этом шаблоне найдем следующее:

 object panMainBanner: TPanel
   Left = 0
   Top = 287
   Width = 553
   Height = 126
   Align = alBottom
   BevelOuter = bvNone
   TabOrder = 1
   object Panel1: TPanel
     Left = 0
     Top = 4
     Width = 553
     Height = 122
     Align = alBottom
     BevelOuter = bvLowered
     Caption = ’Panel1’
     TabOrder = 0
     object webMainBanner: TEmbeddedWB
       Left = 1
       Top = 1
       Width = 551
       Height = 120
       Align = alClient
       ...
       end
     end
 end
 

Как видно из приведенного куска текста, это и есть описание нашей Panel1, баннера, который на этой панели находится (как оказалось, он представлен объектом webMainBanner типа TEmbeddedWB) и некой панели panMainBanner, на которой лежит Panel1. Было бы логично предположить, что имя panMainBanner расшифровывается как panel for Main Banner («панель для главного баннера»), и что если ликвидировать эту панель вместе со всем ее содержимым, то назойливое мерцание баннера более не будет смущать наш взор. Сказано – сделано, переходим в режим редактирования и удаляем вышеприведенный блок текста из ресурса. Сохраняем, запускаем – и получаем GPF. Чтож, как я и предупреждал, безоглядное удаление компонентов – операция отнюдь не безобидная. Поэтому давайте поищем другой путь.

В самом начале главы я выделил четыре подхода к проблеме, и мы только что испробовали тот из них, который стоит под номером два. Это, конечно, было несколько непоследовательно с моей стороны, но иначе Вы бы не смогли поэкспериментировать с удалением объектов и увидеть, к чему это иногда приводит. И вот пришло время пойти правильным путем. Действительно – это ведь всего лишь баннер, он не выпрыгивает в самый неподходящий момент на передний план, не блокирует работу с программой, даже не требует, чтобы по нему кликали. Так что нам нет принципиальной необходимости совершенно изничтожать этот баннер –достаточно просто его не видеть. В нашем случае, кстати, придется убрать не только баннер, но и панели, на которых он расположен, чтобы они не занимали место, и самый простой способ сделать это – уменьшить высоту панелей до нуля. Если быть до конца точным – нам надо спрятать лишь панель panMainBanner, поскольку компоненты webMainBanner и Panel1 находятся внутри этой панели и после «исчезновения» panMainBanner тоже не будут видимы (в общем случае этому могло бы помешать свойство AutoSize=true, но у наших подопытных компонентов это свойство равно false). В Restorator’е исправляем строчку Height=126 на Height=0, сохраняем результат и запускаем многострадальную программу. Баннера больше нет!

Читая эту главу, Вы, наверное, отметили, что я меньше, чем обычно, говорил про общие подходы к проблеме рекламных окон. Действительно, чем более частные вопросы крэкинга мы рассматриваем, тем сильнее приходится «привязываться» к особенностям операционных систем и средств разработки. Но даже в этом случае «за бортом» моего повествования осталось очень многое: визуальные средства для разработки под ДОС (да, были и такие – и некоторые из них, вроде старых версий FoxPro, все еще используются), использование шаблонов диалогов в Visual Basic/VBA и т.д. Вы сами, при желании, сможете узнать об этих вещах через самостоятельные эксперименты ничуть не меньше, чем я мог бы Вам сообщить. А поскольку сама идея хранить шаблоны интерфейсов программ в унифицированном виде давно и прочно вошла в практику программирования, было бы наивным предполагать, что новые средства разработки не принесут с собой новых форматов хранения этих шаблонов. Но, я надеюсь, и в этом случае Вы сможете извлечь пользу из информации, изложенной в этой главе – разумеется, не как из руководства по конкретным инструментам, а как из источника идей.

Глава 8. И вновь продолжается бой…


Контролирует ценность тот, кто может ее уничтожить
Ф. Херберт, «Дюна»


Ну вот, все, что мы могли сделать простыми средствами, мы сделали. И теперь пришло время приступить к отладке. Да-да, эта глава будет практически полностью посвящена искусству дебагить, bpx’ить, трассировать - в общем, одному из аспектов той рутинной деятельности, которая ожидает каждого крэкера на его светлом пути к торжеству высокого искусства над коммерческими интересами. Осмелюсь предположить, что Вы имеете общее представление о том, на что похож процесс отладки программ, о точках останова, пошаговой отладке с заходом внутрь процедур и без оного и прочих столь же элементарных вещах. В интерфейсах и «горячих клавишах» отдельных представителей обширного семейства отладчиков, я думаю, Вы тоже разберетесь без особого труда – скажу лишь, что мы будем ориентироваться на работу под Windows. А наиболее достойными представителями своего племени под этой ОС на сегодня являются SoftIce (бессменный чемпион в течение многих лет – но чемпион довольно капризный) и OllyDebug (сравнительно новая, но очень качественная и совершенно бесплатная программа). Поэтому перейдем к изучению идей, которые, надеюсь, позволят Вам усовершенствовать свои умения в исследовании кодов, о полезных же особенностях конкретных отладчиков я буду упоминать тогда, когда эти полезные особенности будут нами востребованы. А поскольку методы борьбы с nag screen’ами содержанием предыдущей главы не исчерпывается, мы будем изучать техники отладки параллельно с искусством ликвидации рекламных окон.

Как я уже говорил, долгие годы первым словом, которое знаменовало рождение нового крэкера, было «bpx MessageBoxA» - то есть команда установки breakpoint’а на некую функцию Windows API. И в этом, несомненно, сокрыт глубокий смысл – точки останова (они же брейкпойнты) являются одним из важнейших средств отладки, позволяющим остановить программу в нужной точке. Системные вызовы ОС являются связующим звеном между программой и операционной системой. И если во времена DOS еще существовала возможность написать программу, выполняющую некие полезные действия, выводящую результаты и при этом ни разу не обращающуюся к средствам операционной системы, то сейчас, в эпоху многозадачных операционок, блокирующих прямой доступ к абсолютному большинству аппаратных ресурсов, сделать такое практически невозможно. Чтобы зажечь один-единственный пиксель в углу экрана, программам теперь приходится идти на поклон к операционной системе с вежливой просьбой «нарисуйте, пожалуйста, белую точку по указанным экранным координатам». С другой стороны, операционная система предлагает широкий выбор различных полезных функций – от мелочей вроде строковых операций (надо отметить, что выбор этих операций в Windows мог бы быть и побогаче) до таких высокоуровневых функций, как работа с изображениями или уже упомянутое создание диалоговых окон по шаблонам в секции ресурсов. В общем, сделано все для удобства программиста (другое дело, что разработчики ОС иногда понимают это удобство весьма странным образом) – и разработчики этим пользуются. Пользуются этим и крэкеры. Когда какая-нибудь программка лезет в реестр, чтобы провериться на истечение триального срока – она вызывает функции API. Когда читает серийник из поля редактирования – обращается к одной из оконных функций. Даже сообщение «Зарегистрируй меня!» - и то выводится при помощи API. И вот тут-то мы их и ловим – провоцируем приложение чем-нибудь выдать свою коммерческую природу, ставим точки останова – и терпеливо ждем, пока программа не попадется в расставленные сети. Однако чтобы поймать по-настоящему хитрую дичь, ловушки нужно расставлять умело.

Давайте посмотрим, каким образом в уме крэкера рождается великая идея набрать в командной строке SoftIce’а ту самую эпохальную команду – bpx MessageBoxA. Сначала крэкер изучает повадки зверя, на которого идет охота: пытается вводить невалидные серийные номера (мы ведь не в рулетку играем – так что на дурную удачу рассчитывать не приходится), вызвать заблокированные функции (некоторые программы не отключают соответствующие элементы управления) или сделать еще что-либо подобное. Конечная цель всего этого – вынудить программу проявить свою незарегистрированность каким-нибудь хорошо заметным и легко идентифицируемым способом, например – выводом стандартного окна с сообщением. Впрочем, нередко бывает и так, что специально ничего делать не надо – программа сама при запуске или в процессе работы выплюнет на экран окошко с предложением зарегистрироваться. Допустим, что это окошко имеет специфический вид «MessageBox’а обыкновенного». Что такое «MessageBox обыкновенный», я думаю, объяснять никому не надо - единожды в жизни увидев хотя бы один MessageBox, Вы уже больше никогда не сможете забыть это ужасающее зрелище, оно будет преследовать Вас годами, лишая покоя и сна, пока Вы не решите раз и навсегда «завязать» с компьютерами. Если каким-то чудом Вам за все время работы с компьютером удалось избежать этого счастья, Вы всегда можете посмотреть на MessageBox, попытавшись обратиться из «Проводника» к дисководу, в котором нет дискеты. Посмотрели? Вот и отлично, тогда продолжим.

Одна из важнейших для крэкера областей знаний есть знания о том, при помощи каких функций Windows API выполняются те или иные действия. Чтобы успешно поставить брейкпойнт на какую-либо функцию и получить от этого полезный результат, необходимо выполнение двух условий: Вы должны знать, что делает требуемая функция (сия мысль выглядит несколько банальной – но такова правда жизни) и как функция эта называется. И вот теперь Вам потребуются именно знания, одними только хорошими идеями, как мы это делали раньше, тут не обойтись. Есть два пути к обретению необходимых знаний: Вы можете либо с головой погрузиться в изучение программирования с использованием различных разделов Windows API – и после этого будете способны не только влепить брейкпойнт «не глядя», но и в первом приближении оценить функции того или иного куска кода, просто посмотрев на вызываемые системные функции и передаваемые им параметры. Именно этим путем, насколько будет возможно, я и рекомендую Вам следовать – поскольку, когда Вы перейдете от обезвреживания простых защит к исследованию более защищенных программ, навыки программирования с использованием Windows API Вам очень пригодятся. Однако столь обширная информация, как программирование под Windows, не уместилась бы в рамки данной статьи – поэтому мы будем учиться в условиях, «максимально приближенных к боевым». Итак: у нас есть MessageBox с сообщением, и нам нужно куда-то поставить брейкпойнт, чтобы выявить, откуда этот MessageBox появляется.

Первое, что Вам надо уяснить – это то, что одно и то же действие нередко может выполняться несколькими разными функциями WinAPI. Например, наш любимый MessageBox может создаваться не только функцией MessageBoxA, но и функциями MessageBoxExA и MessageBoxIndirectA. Наиболее широкими возможностями в области управления внешним видом MessageBox’а обладает функция MessageBoxIndirectA, наименьшими – собственно MessageBoxA (но зато последнюю гораздо удобнее вызывать – ведь у нее всего четыре параметра). В действительности MessageBoxA – не самостоятельная функция, а лишь удобная «обертка» для вызова MessageBoxExA. Да и сама MessageBoxExA далеко не самодостаточна: в Windows XP, например, она реализована через вызов функции MessageBoxTimeoutA. Что же у нас получилось: мы охотились за одной функцией создания окна с сообщением, а нашли целое гнездо функций, имеющих близкое назначение! Да, именно так – ради удобства программистов и компактности кода в Windows API входит немало функций, частично дублирующих друг друга. И если Вы хотите при помощи точек останова отследить выполнение той или иной операции – Вам нужно ставить точки останова на все функции API, которые эту операцию могут выполнять. В нашем примере, если Вы поставите точку останова на MessageBox, но забудете про экзотичный MessageBoxIndirect, Вы можете просто не обнаружить точку, в которой выводится сообщение. Поэтому рекомендую Вам пользоваться следующим правилом: точки останова лучше ставить не на отдельные функции API, а на всю группу функций, выполняющих близкие по смыслу действия.

Но как узнать, какие функции являются «близкими по смыслу» - спросите Вы. В этом обычно нет ничего сложного. Для начала Вам понадобится раздобыть документацию по программированию под Windows. В принципе, подойдет Win32 SDK (и даже он нужен не весь, а лишь справочные файлы по функциям Windows API). Нужные файлы поставляется совместно с компиляторами под Windows от Borland или Microsoft, также нередко встречаются в Интернете (в том числе – частично переведенными на русский язык). Недостаток Win32 SDK в том, что он давно не обновляется – и, соответственно, не содержит информации по API последних версий Windows. Более обширным источником, несомненно, является MSDN Library – ежеквартально обновляемый сборник документации по программированию под Windows с использованием компиляторов, созданных этой фирмой. Встречается MSDN либо в составе Visual Studio, либо отдельно, и занимает, как правило, несколько CD. Если Вы не испытываете хронический дефицит места на жестком диске, я бы рекомендовал использовать именно MSDN, причем как можно более новой версии. Помимо более свежей и подробной информации о системных вызовах Windows там Вы найдете еще и подробную информацию по классам MFC (что может сильно пригодиться Вам при исследовании программ, созданных с использованием MFC).

Как только у Вас на «винчестере» появится требуемая документация, поиск близких по смыслу команд для Вас перестанет представлять какую-либо сложность. Вам нужно будет лишь нажать кнопку «Group» в окне справки или заглянуть в конец справочной статьи и изучить назначение функций, имена которых стоят после слов «See also». Разумеется, можно (и нужно!) применить и другие приемы работы с документацией – просмотреть дерево словарных статей, воспользоваться контекстным поиском по заголовкам или просто выполнить поиск текста по всей справочной системе. Для тех, у кого пока нет под рукой нужной документации, я составил небольшой «поминальник» наиболее часто используемых функций WinAPI и областей, в которых эти функции используются. Однако не думайте, что Вы сможете обойтись лишь этим списком – функции WinAPI имеют не только имена, но и параметры, понимание назначения которых ничуть не менее важно, чем знание имен функций. Так что описание перечисленных функций читать все равно придется – свою же задачу я вижу в том, чтобы указать, какие разделы стоит изучить в первую очередь.

Упрощенный вывод сообщений
MessageBox, MessageBoxEx, MessageBoxIndirect, MessageBeep (эта функция не выводит сообщение, а только издает соответствующий звуковой сигнал)

Создание и уничтожение окон
CreateWindow (наиболее популярная функция создания окон), CreateWindowEx
CloseWindow (функция закрытия окна), DestroyWindow

Создание и уничтожение диалогов
CreateDialog, CreateDialogIndirect, CreateDialogIndirectParam, CreateDialogParam (эти функции только создают диалог)
DialogBox, DialogBoxIndirect, DialogBoxIndirectParam, DialogBoxParam (эта группа функций позволяет создавать модальные диалоги; управление не возвращается программе, пока диалог не будет закрыт)
EndDialog, DestroyWindow

Чтение и изменение текстов окон
GetWindowText, GetDlgItemText (чтение текста окна или элемента диалога)
GetWindowTextLength (чтение длины текста окна)
GetDlgItemInt (чтение текста элемента диалога как 32-битного числа)
SetWindowText, SetDlgItemText, SetDlgItemInt (установка нового текста окна или элемента диалога)

Изменение видимости, позиции и прочих подобных свойств окна
EnableWindow (активация/деактивация окна)
ShowWindow, ShowWindowAsync (изменение видимости и состояния окна, в частности – позволяет минимизировать или наоборот развернуть окно)
SetWindowPos, MoveWindow (изменение положения окна)
SetWindowWord (устаревшая и практически не используемая функция), SetWindowLong – две функции, позволяющие модифицировать весьма широкий спектр параметров окна. GetWindowWord, GetWindowLong – функции чтения этих параметров.

Загрузка ресурсов
LoadImage (универсальная функция загрузки изображений, иконок и курсоров), LoadBitmap, LoadIcon, LoadCursor
FindResource, FindResourceEx, LoadResource (функции загрузки ресурсов любого типа в память)

Отображение изображений и текстов на экране
BitBlt, StretchBlt, MaskBlt (функции копирования BITMAP’ов на экран)
DrawText, TextOut, TabbedTextOut (обычный вывод текста)
GrayString (редко используемая функция, выводит на экран строку со стилем надписи на неактивном управляющем элементе)

Работа с файлами
OpenFile (устаревшая функция), CreateFile (основная функция открытия файлов; несмотря на свое название способна открывать файлы и даже директории)
ReadFile, ReadFileEx (функции чтения из файлов)
WriteFile, WriteFileEx (функции записи в файл)
SetFilePointer (перемещение по файлу)
GetFileTime, GetFileAttributes, SetFileTime, SetFileAttributes (чтение и модификация времени создания и атрибутов файлов/директорий)
SetEndOfFile (изменение размеров файла)

Операции с реестром
RegOpenKey, RegOpenKeyEx, RegCreateKey, RegCreateKeyEx (открытие и создание ключей реестра)
RegQueryInfoKey (запрос информации о ключе, в частности – для проверки факта существования подключа)
RegQueryValue,RegQueryValueEx (чтение значений из реестра)
RegSetValue, RegSetValueEx (запись ключей в реестр)
RegCloseKey (закрытие ключа реестра)

Чтение и запись INI-файлов
GetProfileSection, WriteProfileSection, GetProfileInt, GetProfileString, WriteProfileString, WriteProfileInt (функции для работы с файлом Win.ini, в настоящее время считаются устаревшими, но иногда используются)
GetPrivateProfileSection, GetPrivateProfileSectionNames, WritePrivateProfileSection, GetPrivateProfileInt, GetPrivateProfileString, GetPrivateProfileStruct, WritePrivateProfileString, WritePrivateProfileInt, WritePrivateProfileStruct (функции работы с областью реестра, отведенной для хранения настроек программ, либо с произвольным INI-файлом; эта группа функций считается устаревшей)

Работа с датой и временем
GetSystemTime, GetLocalTime, GetSystemTimeAsFileTime (чтение текущего времени)
SetSystemTime, SetLocalTime (установка нового времени)
LocalTimeToFileTime, FileTimeToLocalTime, FileTimeToSystemTime, SystemTimeToFileTime (преобразование формата времени)
CompareFileTime (сравнение двух переменных, хранящих время)
GetFileTime, SetFileTime (запись и чтение времени создания, последней модификации и последнего доступа к файлу)

Процессы и потоки: создание и управление
WinExec (устаревшая функция запуска исполняемых файлов), CreateProcess (функция, обычно используемая для запуска исполняемых файлов), ShellExecute, ShellExecuteEx (пара «альтернативных» функций для запуска приложений (применительно к исполняемым файлам) или открытия, печати и т.п. папок и документов).
ExitProcess («стандартное» завершение процесса, эта функция способна завершить только тот процесс, внутри которого она вызвана), TerminateProcess (принудительное завершение процесса; эта функция способна «убить» любой процесс (в NT – при наличии соответствующих привилегий), что иногда используется защитами для подавления крэкерского софта)
CreateThread (штатная функция создания нового потока), CreateRemoteThread (эта функция «живет» только под NT-подобными и на самом деле в защитах практически не используется. Зато очень, очень (я не забыл сказать «очень»?) широко используется самими крэкерами для внедрения в чужой процесс. Так что обойти ее вниманием в этом поминальнике было бы несправедливо)
ExitThread, TerminateThread (штатное завершение и аварийное уничтожение потоков соответственно)

Загрузка и выгрузка DLL
LoadLibrary, LoadLibraryEx (функции загрузки динамических библиотек)
LoadModule (устаревшая функция загрузки DLL)
GetProcAddress (функция, возвращающая адрес функции или переменной, объявленной в экспорте DLL, по имени этой функции/переменной (разумеется, соответствующая DLL должна быть подгружена текущим процессом). Эта функция широко используется как в защитах чтобы вызов какой-либо функции не «светился» в дизассемблированном листинге, а также для приемов типа push <желаемый адрес возврата>; jmp <адрес функции, полученный через GetProcAddress>, используемых для сокрытия точки, откуда была вызвана функция)
FreeLibrary (функция принудительной выгрузки DLL)

Надо сказать, список этот отнюдь не полный: в нем нет, к примеру, функций выделения блоков памяти (извлечь практическую пользу из информации о распределении памяти удается довольно редко, и те, области, где это актуально, отнюдь не относятся к «основам» крэкинга) и функций работы со строками (эти функции почти всегда дублируются в коде программы ради повышения быстродействия). Нет в этом списке и специфических библиотечных функций Microsoft’овской MFC и Borland’овских VCL/RTL – если включить в список и их, «поминальник» стал бы совершенно неподъемным. Все это мы оставим за рамками – тем более, что средства разработки не стоят на месте, и, возможно, через пару лет будут актуальны совершенно другие API. Попробуйте понять, из чего я исходил, составляя этот «поминальник» - и, поняв это, Вам станет заметно легче найти подход к ранее неизвестной системе программных интерфейсов. Я выбрал WinAPI лишь по той причине, что это – «основа основ» программирования под платформу Win32, которая в настоящее время наиболее распространена. Однако даже к изложенному выше стоит относиться с известной долей осторожности – поскольку линейка Windows 9x угасает, в ближайшем будущем вполне могут стать актуальными специфические для линейки NT системные вызовы, а Вы будете искать правды среди стандартных функций Win32 – и не найдете ее. Прецедент уже имел место – в многих крэкерских руководствах пятилетней давности рекомендовалось для гарантированного «отлова» считывания текста из окна устанавливать брейкпойнт на функцию hmemcpy. И это работало – но только под Windows 9x, поскольку в линейке NT вместо вызова этой функции для повышения скорости использовались inline-вставки. До тех пор, пока все семейство NT-подобных ограничивалось доисторической NT 3.51 и неудобоваримой NT 4, проблемы как бы не существовало. Но вот на наши десктопы пришли более симпатичные Windows 2000 и Windows XP – и противоречие между старыми руководствами и наблюдаемой реальностью встало в полный рост.

Как Вы можете видеть, имена функций являются сокращенным описанием тех действий, которые эти функции выполняют. Таким образом, если Вы предполагаете существование некой функции WinAPI и хотите поставить на нее брейкпойнт, но не знаете (или просто забыли), как она называется, Вы можете легко это узнать, если владеете принятой программистами под Win32 терминологией и английским языком (или хотя бы русско-английским словарем). Кроме того, если всем другим отладчикам Вы предпочитаете SoftIce, у Вас есть возможность воспользоваться командой EXP <текстовая_строка>, которая позволяет вывести на консоль отладчика все функции, имена которых начинаются с указанной Вами строки. Например, команда EXP MessageBox покажет Вам все MessageBox’ы, какие только встречаются в символьной информации, импортированной из DLL (список DLL для импортирования информации должен быть заранее прописан в инициализационном файле отладчика). Надо отметить, что команда EXP имеет несколько более широкие возможности, чем простой вывод списка функций, начинающихся с определенных символов – Вы можете также просмотреть список модулей, по которым имеется символьная информация, проверить, присутствует ли нужная функция в некотором модуле и многое другое, о чем можно прочесть в руководстве по этому отладчику.

Вы наверняка заметили, что в «поминальнике» отсутствуют функции MessageBoxA и MessageBoxExA, о которых я упоминал в начале статьи, а вместо них описаны лишь MessageBox, MessageBoxEx. Разумеется, это не опечатка. Если Вы уже попробовали «на вкус» SoftIce’овую команду EXP и приложили ее к нашему измученному брейкпойнтами (то ли еще будет!) MessageBox’у, то наверняка заметили, что в списке функций, экспортируемых из USER32.DLL присутствуют MessageBoxA и MessageBoxW (а вот просто MessageBox’а нет и в помине). Откуда же взялись буквы A и W в именах функций и что они вообще значат?

Расшифровка этих букв проста: A – это ANSI, W – это WIDE. А появились эти буквы после того, как в Microsoft взяли курс на внедрение кодировки UNICODE, и возникла необходимость как-то отличать «старые» варианты функций, работающие с традиционными текстовыми строками, от «новых», использующих кодировку UNICODE. А символы в UNICODE имеют тип WСHAR длиной в 16 бит против обычных восьми – поэтому к именам функций добавили букву W, а не U, как можно было бы предположить. Вот и получается, что если функция принимает или передает строковые параметры, программист должен указать, какой из вариантов функции нужно использовать. Разумеется, если функция WinAPI не получает и не возвращает никаких символьных параметров, буквы A и W ей совершенно ни к чему. Так что единственная причина того, что в моем «поминальнике» начисто отсутствуют упоминания об ANSI- и UNICODE-вариантах одной и той же функции весьма банальна. Если бы я подробно расписывал оба варианта имени каждой функции, мне бы пришлось набирать этот список почти в два раза дольше, а Вам – во столько же раз дольше его читать, и при этом Вы бы не получили никакой новой информации. Поэтому я и решил не упражняться в крючкотворстве, а вместо этого объяснить причины использования окончаний A и W. Надеюсь, что после моих объяснений Вы сможете подставить нужные буквы в имена функций самостоятельно.

Вряд ли Вам пригодится на практике эта информация, однако стремление к истине обязывает меня сообщить Вам великую тайну. Знайте, что все-таки они существуют – я имею ввиду MessageBox’ы без буквы A или W. Ибо давным-давно, когда на Земле жили 16-разрядные динозавры Windows 3.1 и Windows for Workgroups, скрывавшиеся в недрах этих ОС функции ничего не ведали ни об ANSI, ни об UNICODE – а потому не нуждались в буквах A и W. И от тех древних времен в include-файлах все еще сохранились строчки вроде lstrcmp equ <lstrcmpA> - в целях совместимости и упрощения переноса старых исходников под новые компиляторы.

Практическое использование этого списка функций выглядит очень просто. Предположим, что у Вас есть некая программа, и на этапе первоначального сбора информации Вы узнали, что она работает 30 дней, выводит при запуске MessageBox со счетчиком дней до окончания триального срока, и, если триальный срок закончился, после нажатия кнопки OK завершает программу. Дальше Вы можете рассуждать примерно следующим образом:
- Если программа проверяет, сколько дней она работает – весьма вероятно, что она считывает текущую дату. Поэтому смотрим в раздел «Работа с датой и временем» и выбираем оттуда функции, предназначенные для чтения времени.
- Программа показывает MessageBox – следовательно, есть смысл поставить точки останова на функции создания MessageBox’ов.
- При истечении триального срока программа завершается – значит можно попытаться отловить вызов функции ExitProcess.
После расстановки брейкпойнтов на упомянутые функции и запуска программы отладчик скорее всего всплывет где-то внутри той ветки программы, которая ответственна за появление nag screen’а и проверку триала. После этого Вы можете ликвидировать сам nag screen (просто забив nop’ами все, что относится к вызову функции, создающей MessageBox) а затем методом обратной трассировки в уме (о сути этого метода я подробно расскажу чуть позже) добраться от точки вызова ExitProcess до условия, по которому программа определяет, что триальный срок еще не кончился (и продолжает работать) или уже истек (и тогда вызывается ветка с ExitProcess). После этого, внеся исправления в исполняемый файл, Вы сможете организовать себе «вечный триал».

Практика показала, что «отловить» появление nag screen’ов проще всего, отслеживая при помощи точек останова следующие типы событий:
1. Появление MessageBox’ов любых типов
2. Создание диалогов на основе ресурсов
3. Вызовы функций отображения окон (успешно работает только в том случае, если окон сравнительно немного – иначе становится сложно отследить среди всех вызовов соответствующих функций те, которые создают нужное окно)
4. Считывание текущего времени (в тех случаях, когда программа имеет ограничение по времени использования, и nag screen содержит информацию о том, когда истечет испытательный срок)
5. Создание таймеров (например, для активации управляющего элемента)
6. Изменение состояния окон Windows (например, активация кнопки или помещение в заголовок окна какой-либо надписи)
7. Вызовы вспомогательных функций, использующихся для изменения текста окон (к примеру, если в заголовке nag screen’а присутствует надпись «Осталось N дней пробного периода», можно предположить, что для подстановки числа дней в надпись может использоваться функция wsprintf).
Разумеется, не следует принимать приведенный список как «истину в последней инстанции» и даже как «руководство к действию» - реальность бывает весьма разнообразна, и втиснуть ее в семь нумерованных пунктов, ничего не потеряв, вряд ли возможно. Однако как отправная точка или в качестве информации к размышлению, я думаю, этот список будет небесполезен. Однако скажу, что полезность предлагаемой последовательности подтверждена практикой – последовательная проверка каждого из пунктов этого списка во многих случаях позволяла мне легко добраться либо до процедуры создания/отображения нужного окна, либо до цикла обработки сообщений, получаемых этим окном.

Теперь Вам, пожалуй, известно о функциях WinAPI достаточно, чтобы приступить к рассмотрению примеров, демонстрирующих, как эти знания применить для решения практическим задачам крэкинга. Так что в порядке эксперимента давайте немного взломаем какую-нибудь распространенную программу, например, Notepad. Это будет довольно несложный, но весьма поучительный эксперимент, демонстрирующий одну из техник ликвидации nag screen’ов в «настоящих» программах. Итак, возьмем обыкновенный Блокнот (я взял тот, который входит в состав Windows XP), скопируем его в надежное место, чтобы в случае чего можно было восстановить файл из резервной копии (вообще, возьмите себе за правило каждый успешный шаг отмечать резервными копиями файлов, дампами успешно модифицированных кусков и т.д. – очень неприятно бывает терять из-за ошибок результаты длительной и кропотливой работы) и запустим его. Теперь нажмите в Блокноте Ctrl-F (оно же «Правка|Найти…»), легонько стукните по клавиатуре левой пяткой (или просто наберите любой текст) и нажмите кнопку «Найти далее». Поскольку нашего «любого» текста в поле редактирования нет (потому что там вообще ничего нет), Блокнот ругнется сообщением вроде «Не удается найти "340t80543t"». Будем считать, что это и есть nag screen, который нам предстоит ликвидировать.

Очевидно, что этот «nag screen» очень сильно похож на MessageBox – поэтому вполне логичным было бы начать поиск нашего окошка с установки брейкпойнта на функции вывода стандартных окон с сообщениями (MessageBoxA, MessageBoxW, MessageBoxExA – и далее по списку). Грузим подопытную программу в отладчик (я в качестве отладчика буду использовать OllyDebug, но те, кто предпочитает SoftIce, тоже смогут без всяких сложностей повторить этот эксперимент) и устанавливаем точки останова. В SoftIce это делается очень просто: «BPX имя_функции» - и так много-много раз, для всех подходящих функций из «поминальника». В OllyDebug эта операция выполняется несколько сложнее, зато Вы получите весьма неожиданный, но очень приятный сюрприз. Но приятные сюрпризы будут позже, а пока щелкните правой кнопкой мыши в окне кода и вызовите всплывающее меню. В этом меню вызовите пункт «Search for > Name (label) in current module» - этот пункт заставляет отладчик пробежаться по загруженному модулю (в нашем случае – по EXE’шнику Блокнота) и найти в нем все ссылки на «именованные» адреса (в частности, такими адресами являются вызовы WinAPI). Отладчик покажет окно с весьма немаленьким списком функций, на которые удалось найти ссылки в коде программы, и теперь в этом списке нам нужно найти наши МessageBox’ы. А вот и наступило время для обещанного приятного сюрприза: в списке нашлась только одна подходящая функция из «поминальника» - MessageBoxW. Так что в то время, как счастливые обладатели SoftIce сбивали в кровь руки, разбрасывая брейкпойнты на функции, которые вообще не используются в программе, пользователи OllyDebug без всяких усилий получили информацию о том, с какого вызова API лучше всего начать подбираться к нашему nag screen’у. Пользователям же SoftIce чтобы получить список вызываемых программой функций API придется воспользоваться каким-нибудь дополнительным софтом, позволяющим просмотреть список импортируемых функций (благо таких программ немало).

В SoftIce брейкпойнт на функцию ставится элементарно: BPX MessageBoxW и никаких гвоздей; в OllyDebug эта операция немного сложнее: найдя нужную строчку в списке импортированных функций, щелкните правой кнопкой мыши и в всплывшем меню выберите пункт «Follow import in disassembler». После этого отладчик мгновенно перенесет Вас в глубины одной из важнейших системных библиотек операционной системы прямо к первому байту функции MessageBoxW. После этого останется лишь нажать клавишу F2 и увидеть, как начальный адрес этой функции окрасится в красный цвет. На этом подготовительные операции закончены – пора отпустить программу на волю и начать охоту на наш «nag screen».

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

 77D70956   >  6A 00         push    0
 77D70958   .  FF7424 14     push    dword ptr ss:[esp+14] 
 77D7095C   .  FF7424 14     push    dword ptr ss:[esp+14]            ; |Title = "????"
 77D70960   .  FF7424 14     push    dword ptr ss:[esp+14]            ; |Text = "????"
 77D70964   .  FF7424 14     push    dword ptr ss:[esp+14]            ; |hOwner = 00010015
 77D70968   .  E8 03000000   call    USER32.MessageBoxExW             ; \MessageBoxExW
 77D7096D   .  C2 1000       retn    10
 

И это есть ни что иное, как код функции MessageBoxW с комментариями, которые в него подставил OllyDebug (поскольку SoftIce никаких комментариев, кроме заранее определенных пользователем, подставлять не умеет, пользователям Айса придется немного напрячь воображение). При помощи кнопки F8 начинаем трассировать этот код («Ура! Мы отлаживаем ядро!») без захода в процедуры, и, дотрассировав до адреса 77D70968 с удивлением видим, что отладчик остановился, зато на экране появился наш MessageBox. А это значит, что наше исходное предположение насчет MessageBox’овой сущности «nag screen’а» было верно. Закроем этот MessageBox и продолжим трассировать до тех пор, пока не выйдем из процедуры по команде retn 10.

Пока Вы продавливаете до пола несчастную клавишу F8, я сделаю небольшое лирическое отступление. Вы наверняка уже прочувствовали, что много раз подряд нажимать F8 довольно скучно. А ведь MessageBoxW - это далеко не самая длинная процедура, нередко встречаются шедевры, в дизассемблированном виде занимающие десятки и сотни строк! И у Вас возник естественный вопрос – «неужели в таком большом и сложном отладчике нет какой-нибудь маленькой кнопки, чтобы сразу добраться до точки выхода из процедуры». Не подумайте о разработчиках плохого - они позаботились о нас, поэтому столь нужная кнопка в обеих отладчиках есть! В OllyDebug эта «кнопка» (она находится не только на панели инструментов, но и продублирована соответствующим пунктом меню) называется «Execute till return» и вызывается комбинацией Ctrl-F9. Заодно обратите внимание и на пункт меню «Execute till user code»: если Вы когда-нибудь заплутаете в недрах чужой DLL и захотите одним махом выполнить весь код до того знаменательного момента, когда управление вернется в основную программу – смело жмите Alt-F9, и OllyDebug Вам поможет. В SoftIce для того, чтобы добраться до точки выхода из функции, служит команда p ret, по умолчанию «подвешенная» на клавишу F12. Вы можете подумать, что если есть команда p ret, которая заставляет программу работать до первого ret’а, то должны существовать команды p mov, p push и т.п. Увы, это не так – разработчики SoftIce сочли, что и одного отслеживания ret’ов более чем достаточно.

Но вернемся к нашему Блокноту. После того, как Вы успешно выйдете из процедуры, OllyDebug покажет примерно следующее:

 01001F8C  |.  FF75 14       push    [arg.4]                          ;  notepad.01009800
 01001F8F  |.  FF75 10       push    [arg.3]
 01001F92  |.  E8 58FFFFFF   call    notepad.01001EEF
 01001F97  |.  FF75 18       push    [arg.5]                          ; /Style = MB_OK|MB_ICONASTERISK|MB_APPLMODAL
 01001F9A  |.  FF75 0C       push    [arg.2]                          ; |Title = "Блокнот"
 01001F9D  |.  56            push    esi                              ; |Text = "Не удается найти "oierg""
 01001F9E  |.  FF75 08       push    [arg.1]                          ; |hOwner = 000D063A (’Найти’,class=’#32770’,parent=004C0360)
 01001FA1  |.  FF15 4C120001 call    dword ptr ds:[<&USER32.MessageBo>; \MessageBoxW
 01001FA7  |.  56            push    esi                              ; /hMemory = 0009BCB0
 01001FA8  |.  8BF8          mov     edi, eax                         ; |
 01001FAA  |.  FF15 F4100001 call    dword ptr ds:[<&KERNEL32.LocalFr>; \LocalFree
 

Если у Вас вместо OllyDebug установлен SoftIce, Вы все равно увидите нечто подобное, но уже без подсказок о параметрах функций MessageBox и LocalFree – SoftIce излишней дружелюбностью не страдает, а потому услужливо выискивать и расшифровывать для Вас параметры системных вызовов не станет. А жаль. Один из способов сделать так, чтобы MessageBox не больше появлялся, заключается в забивании nop’ами «лишних» команд – а именно вызова call dword ptr ds:[<&USER32.MessageBoxW>]. Однако мало ликвидировать лишь сам CALL – нельзя забывать и о параметрах вызываемой функции. Потому что, если эти параметры не прибить тоже – они попадут в стек и так там и останутся до тех пор, пока процедуре, внутри которой эти параметры были положены на стек, не вздумается выполнить команду ret (или retn). Вот тут-то и начнется самое веселое: по-хорошему в момент выполнения команды ret на вершине стека должен находиться адрес возврата, а в действительности окажется мусор, который должен был «уйти» в функцию MessageBoxW и благополучно исчезнуть в ее недрах. В зависимости от Вашей удачливости, настроения программы и текущей фазы Луны Вы получите либо банальный GPF (почти наверняка), либо какой-нибудь экзотический спецэффект вплоть до форматирования винчестера. Насчет форматирования - это, конечно, шутка, но шутка с долей правды – представьте, что случится, если на стеке в качестве адреса возврата окажется адрес какого-нибудь осмысленного куска кода. Так что после успешной ликвидации системного вызова не поленитесь разобраться и с PUSH’ами – пусть волшебная команда XCHG EAX,EAX (более известная как NOP) станет Вашим лучшим другом. Если же Вы питаете суеверный страх перед командой NOP или Вам просто лень много-много раз набирать код 90 – просто пропишите по адресу 01001F97 команду jmp 01001FA7; результат будет тот же самый, а пальцы устанут значительно меньше.

А потому, чтобы Ваш винчестер случайно не отформатировался, хорошенько запомните следующее правило:

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

На практике такую проверку можно выполнить следующим образом: засечь значение регистра ESP на входе в процедуру и на выходе из нее, вычислить разность между этими значениями (тут полезно помнить, что соглашения вызова PASCAL и STDCALL предусматривают удаление параметров процедуры со стека внутри процедуры, а CDECL – коррекцию стека после завершения процедуры, уже в теле программы). Если после внесения модификаций разность между значениями ESP не изменилась – значит, все сделано правильно, если изменилась – ищите ошибку в своих действиях.

Разумеется, совершенно необязательно замерять разность между значениями ESP именно в начале и в конце процедуры. К примеру, если процедура, внутри которой мы «стираем» обращение к другой процедуре, вызывается при помощи обычного CALL, можно промерить значения ESP непосредственно перед выполнением этого CALL и сразу после него. Чтобы корректно проверить, не «застряло» ли чего-нибудь в стеке, или наоборот, не удалили ли мы чего-нибудь лишнего, необходимо соблюсти следующие условия:

1. При любом ходе исполнения кода (т.е. независимо от того, какие ветки срабатывают между точками, в которых проверяется положение стека) изменение величины ESP должно быть одинаковым.
2. Вы должны быть уверены, что все операции по помещению параметров на стек, относящиеся к проверяемому вызову, находятся между точками замеров. И вот здесь-то Вас могут подстерегать довольно неприятные неожиданности. Языки высокого уровня не позволяют по-настоящему изощренно работать со стеком (компиляторы С\C++, правда, позволяет динамически зарезервировать на стеке некоторую область и даже соорудить в этой области объект – но это максимум, на что может рассчитывать программист). Даже «очень оптимизирующие» компиляторы обычно генерируют помещение параметров на стек в непосредственной близости от вызова функции, которая эти параметры принимает – поэтому найти их не так уж сложно. Но вот ассемблер… Да, ассемблер способен изменить ситуацию коренным образом. Вместо банального MyFunction (my_param) Вы вольны написать что-то вроде

 push my_param
 <сотня строчек кода, не относящегося к делу>
 call MyFunction
 

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

Раз уж зашла речь об антидизассемблерных приемах (хотя на самом деле эти приемы направлены не столько на то, чтобы сбить с толку дизассемблер, сколько на то, чтобы запутать крэкера, пытающегося осмыслить код), еще немного уклонюсь от основной темы главы и расскажу о паре хитростей, иногда использующихся для помещения параметров на стек. Основаны эти приемы на том, что регистр ESP мало чем отличается от регистров общего назначения, а область стека – от всех прочих областей памяти. Зная, что над регистром ESP вполне возможно производить арифметические операции сложения и вычитания, а также обращаться к содержимому стека при помощи косвенной адресации командами вроде mov [ESP+8],eax, нетрудно догадаться, что команду push eax можно заменить, к примеру, последовательностью

 sub esp,4
 mov [esp+4],eax
 

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

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

А напоследок я расскажу об одном очень-очень простом способе, в некоторых случаях позволяющем найти место, в котором локализован вызов nag screen’а. Суть способы очень проста: Вы загружаете программу и начинаете трассировать ее без захода внутрь функций, при каждом нажатии клавиши F8 запоминая текущий адрес (на практике почти всегда достаточно запоминать только адреса выполняемых call’ов). При выполнении одной из таких функции появится наш nag screen. Снова загружаем программу, вспоминаем, каким был адрес того call’а, который вызвал появление окна и «прогоняем» программу до этого адреса в ускоренном режиме.

Перейдем ко второму шагу: зная, что создание nag screen’а сидит где-то в глубинах вызываемой функции, войдем внутрь этой функции при помощи команды трассировки с заходом в функцию. Теперь начинаем трассировать содержимое этой функции, все так же запоминая адреса исполняемых команд. Рано или поздно мы наткнемся на очередной вызов, после которого выскочит nag screen. Этот процесс постепенного погружения в код можно продолжать до тех пор, пока Вы не доберетесь до вызова WinAPI или другой библиотечной функции, создающей окно; не попадете в цикл обработки сообщений или ожидания закрытия окна nag screen’а (что будет означать «перелет», но из этого результата тоже можно извлечь определенную пользу) либо не придете к выводу, что данная техника в Вашем случае неприменима.

Вообще говоря, эта методика применима не только для поиска вызовов nag screen’ов, выводимых сразу после запуска. Если Вы знакомы с численными методами решения уравнений, Вы наверняка заметили некоторую аналогию с методом Ньютона: последовательно двигаясь вглубь кода и проверяя эффекты от вызовов подпрограмм, мы констатируем «недолет» либо «перелет» и постепенно сужаем область, в которой находится интересующий нас блок команд. Метод Ньютона изначально был предназначен для поиска решения на некотором промежутке значений, и, по аналогии, предлагаемый метод также может быть использован для поиска некоей функции «на промежутке кода». В качестве границ промежутка удобно использовать «знаковые» и легко отслеживаемые события, такие, как чтение текста из управляющего элемента, обращение к ячейке памяти, получение информации из реестра или из файла и отображение результатов этих действий в виде nag screen’а, сообщения о неверном серийном номере и т.п. Проще говоря, если Вы ввели серийный номер, нажали кнопку «ОК» и программа в ответ сказала что-то вроде «Неправильно ты, дядя Федор, серийники вводишь» - значит, проверка правильности серийного номера лежит где-то между считыванием введенного серийника и выводом сообщения. И если Вам удастся зафиксировать оба этих события при помощи брейкпойнтов, трассируя и изучая код, лежащий между этими бряками Вы с большой вероятностью обретете желаемый адрес процедуры проверки серийника. А уж что Вы с этим адресом сделаете – зависит лишь от Ваших исходных целей и изобретательности.

Предлагаемый мной способ имеет несколько существенных ограничений: во-первых, трассируемый код должен исполняться последовательно (т.е. создание окна по таймеру или какому-либо иному событию таким способом отследить не получится или, по крайней мере, будет достаточно сложно). Во-вторых, очень желательно, чтобы создание и отображение nag screen’а происходило в главном потоке программы (если nag screen отображается в отдельном потоке, придется сначала выискивать место создания этого потока). И, в-третьих, если Вы активно практикуете дзен-крэкинг (то бишь Ваш любимый modus operandi – «я не знаю, как это работает, но я все равно это сломаю»), Вы можете совершить следующую ошибку: «отключая» nag screen, можно копнуть слишком глубоко и случайно «вынести» не процедуру отображения nag screen’а, а более универсальную процедуру отображения окна вообще, которая по сути ни в чем не виновна. Для того, чтобы Вам было понятнее, о чем идет речь, продемонстрирую идею следующим псевдокодом:


 ShowNagScreen proc
   …
   invoke ShowAnyWindow, nag_screen_handle
   …
 ShowNagScreen endp
 
 ShowAnyWindow proc hwnd:DWORD
   invoke ShowWindow, hwnd, SW_SHOWNORMAL
 ShowAnyWindow endp
 


Предотвратить создание nag screen’а в этом случае можно тремя способами:
1. Убрать вызов самой процедуры ShowNagScreen
2. В коде процедуры ShowNagScreen убрать вызов ShowAnyWindow
3. Удалить вызов WinAPI’шной функции ShowWindow внутри ShowAnyWindow
Первый и второй способы, в принципе, вполне корректны (причем второй даже несколько лучше – в реальной процедуре создания nag screen’а могут выполняться какие-нибудь дополнительные операции), а вот третий… Да, своего мы бы, конечно, добились – но вместе с nag screen’ом исчезли бы и все другие окна, которые отображаются процедурой ShowAnyWindow. А если учесть, что в современных программах вызовы WinAPI нередко упрятаны глубоко в недра всевозможных библиотек, вопрос о том, как бы случайно не перестараться с поиском «корня зла» и от избытка чувств не пропатчить библиотечную функцию – отнюдь не праздный. И универсального решения этого вопроса, по-видимому, не существует (если, конечно, не считать таким решением доскональный анализ кода программы).

Однако я могу предложить Вашему вниманию два подхода, которые с достаточно высокой вероятностью позволяют определить, используется ли функция исключительно для единственной цели (в частности – для отображения nag screen’а), или же является универсальной в рамках приложения. Первый подход основывается на том, что библиотечные и просто широко используемые функции, как правило, вызываются многократно из разных областей исполняемого кода, в то время, как ссылки на узкоспециализированные процедуры (такие, как проверка серийного номера на валидность или вывод сообщения об ограничениях в программе) обычно присутствуют лишь в двух-трех экземплярах на всю программу, и во время исполнения кода приложения срабатывают считанные разы. Выполнить проверку подозрительной функции на количество вызовов можно как при помощи дизассемблера, так и прямо в процессе отладки. Поскольку большинство дизассемблеров позволяют получить список ссылок на процедуру (а также точек, в которых эти ссылки находятся), анализ при помощи дизассемблера сводится к простому поиску нужной процедуры в выходном листинге дизассемблера и визуальной оценке количества ссылок. Не могу еще раз не проагитировать Вас за использование OllyDebug: этот отладчик содержит несколько весьма удобных инструментов, скрывающихся внутри пункта меню «Find references to…». В данном контексте нам, несомненно, особенно интересен подпункт «Selected address», позволяющий найти все ссылки на команду под курсором. То есть, для выполнения описанной проверки Вы можете даже обойтись без дизассемблера; чтобы найти все прямые ссылки на некий адрес (например, на адрес первого байта функции), достаточно установить курсор на этот адрес и нажать Crtl-R. Если ссылок немного – значит, Вы нашли то, что искали. А вот если их количество перевалит за 5-8 штук – у Вас есть веские основания подозревать, что проверяемая функция выполняет некие общие функции, и потому Вам нужно подняться по дереву вызовов на уровень выше либо вообще искать нужный код в другой области. Раз уж речь зашла о дереве вызовов, добавлю еще следующее: даже удостоверившись в том, что проверяемая функция A вызывается один-единственный раз из функции B, не поленитесь посмотреть, что «растет» на дереве вызовов сверху – может оказаться, что сама функция B вызывается в программе десятки раз. В этом случае вывод очевиден – Вы слишком увлеклись «глубоким бурением». Выполнить подобную проверку при помощи отладчика ничуть не сложнее – нужно всего лишь поставить точку останова на подозрительную функцию и запустить программу «с нуля». По количеству срабатываний точки останова, а также соотнося эти срабатывания с всплыванием nag screen’а, Вы сможете сделать вывод о том, является функция «знаковой» для защиты или же просто исполняет некие более общие функции, прямого отношения к защитным механизмам не имеющие.

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

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

Глава 9. Если бряк оказался вдруг…


Наверное, Вы уже попытались что-нибудь взломать. Может быть даже, Вам это удалось – за счет знаний и способностей к анализу, благодаря интуиции, или же в силу Вашего трудолюбия и настойчивости. Возможно также, что Вам просто очень повезло с первой в жизни программой, и защита оказалась слабее, чем в большинстве других программ. Однако тех, кто не смог с первой попытки одержать победу над мегабайтами кода, гораздо больше. Кто-то споткнулся об антиотладочные приемы, кому-то «повезло» встретиться с запакованной программой, кто-то принял близко к сердцу огромнейшие возможности, предоставляемые OllyDebug и SoftIce, и погрузился в изучение этих инструментов, отложив до времени собственно копание в коде. Некоторые отступили, не добравшись до подходящей зацепки, с которой можно было бы начать «раскручивать» защиту. Свежие ощущения, новые знания, предвкушение будущих побед – все, что знаменовало рождение крэкера, осталось в светлом прошлом, куда Вы сможете вернуться лишь в мечтах. В общем, одни радуются своей первой победе, другие – переводят дыхание и с тоской глядят на заоблачные выси, которые не удалось достичь. Если Вы попали в число «других», значит, у нас есть кое-что общее – свою первую программу я взломал далеко не с первой попытки. Надеюсь, после этих слов у Вас появился повод для оптимизма – возможно, именно Вам в будущем суждено написать свои собственные «Теоретические основы…». Однако сейчас Вас, наверняка, больше интересует другой вопрос – «почему мне не удалось сломать программу?» Причем нередко этот вопрос обретает еще более конкретную формы – «почему я ставлю брейкпойнты, а они не срабатывают?» и «как отлаживаемая программа может обнаружить мои точки останова?» И вопросы о неработающих (или «странно» работающих) брейкпойнтах – это отнюдь не повод упрекнуть в невнимательности начинающего крэкера, но основание для подробного разговора об особенностях Win32 API, тонкостях работы точек останова и антиотладочных приемах.

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

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

Ненамного отстают по популярности среди авторов защит всевозможные приемы определения присутствия отладчиков, от откровенно примитивной проверки наличия определенных файлов/ключей реестра (отдельные разработчики защит даже удаляют эти ключи, нимало не утруждая свой беспросветно могучий интеллект мыслями о том, что SoftIce можно приобрести легально и использовать не для взлома их поделок) до довольно изощренных антиотладочных приемов, использующих особенности конкретных отладчиков. Примерами таких особенностей могут служить «черные ходы» в SoftIce для взаимодействия с Bounds Checker’ом или нездоровая реакция на вызов IsDebuggerPresent в OllyDebug и всех остальных отладчиках, использующих Debug API. Кстати, признаки наличия отладчика могут быть не только информационными, но и физическими: программа может «догадаться» о том, что ее ломают, по ненормально большому времени выполнения тех или иных процедур. Задумайтесь над тем, сколько времени уходит на выполнение десятка команд в «ручном режиме», когда Вы исступленно давите кнопку F8 в OllyDebug - и Вы сразу поймете, что я имею в виду. К этой же группе можно отнести использование в защитных механизмах отладочных регистров процессора: поскольку эти регистры используются отладчиком для установки брейкпойнтов, одновременная их эксплуатация программой и отладчиком невозможна, если попытаться проделать такое, либо отладчик «забудет» аппаратные точки останова, либо защитные процедуры выдадут некорректный результат со всеми вытекающими из этого последствиями. Большинство антиотладочных приемов, разумеется, давно и хорошо известны, и их описание несложно найти в руководствах по крэкингу и написанию защит. Впрочем, авторы защит на такие приемы обычно всерьез не рассчитывают, поскольку идентифицировать (а часто – и обойти) антиотладочный код в дизассемблерном листинге обычно несложно (например, если прикладная программа пытается оперировать с отладочными регистрами, это очевидный признак того, что в коде «что-то нечисто»), а некоторые крэкерские инструменты среди своих функций имеют отслеживание популярных антиотладочных приемов (примером может служить старая утилита FrogsIce, которая умела выявлять и побеждать множество защитных трюков, направленных против SoftIce).

Наиболее популярным среди начинающих крэкеров, по-видимому, еще долго будет оставаться вопрос: «я поставил брейкпойнты на GetWindowText’ы и GetDlgItemText’ы, и все равно не могу поймать момент чтения серийника из окна». Действительно, формально все вроде бы сделано правильно, и все подходящие функции из «поминальника» обвешаны точками останова, как новогодняя елка – игрушками, но отладчик все равно не подает ни малейших признаков активности. При этом все точки останова находятся на своих местах и вполне успешно срабатывают – но, увы, не по тому поводу, который Вам интересен. В общем, у неопытного кодокопателя может сложиться впечатление, что серийный номер считывается при помощи телепатии или, как минимум, весьма недокументированным способом, чтобы обнаружить который нужно иметь не меньше семи пядей во лбу. Однако в действительности никаких телепатических датчиков в Вашем компьютере нет (а если даже и есть, то вряд ли они используются для чтения текста из диалоговых окон), да и подозревать недокументированные приемы я бы тоже не торопился, поскольку существует куда более простое объяснение этого явления. В Windows с давних пор сосуществуют два различных механизма, позволяющих управлять окнами и некоторыми другими объектами. Об одном из этих механизмов – системных вызовах Windows API я уже говорил, и даже дал в предыдущей главе небольшой список наиболее употребительных функций с комментариями по поводу области их применения. Другая же сторона Windows до настоящего момента как-то оставалась в тени, за исключением эпизодических упоминаний «по поводу». Вы, наверное, уже догадались, что это за «другая сторона Windows»: я говорю о широко используемых в нашей любимой ОС сообщениях (хотя, если быть до конца точным, сообщения в том или ином виде присутствуют в большинстве современных операционных систем).

Если функции WinAPI безраздельно властвуют в темном и мрачном царстве невизуальных объектов, таких, как файлы, процессы, средства синхронизации и прочее, то в «оконной» области ситуация отличается разительным образом. Сравнительно небольшой набор системных вызовов общего назначения («создать-включить-удалить окно») с лихвой компенсируется огромным разнообразием системных сообщений (в англоязычной документации – «messages»; общее число документированных сообщений уж перевалило за тысячу), подчас дублирующих функции WinAPI (например, сообщение WM_GETTEXT, которое способно читать текст окна не хуже, чем уже известная Вам функция GetWindowText). Некоторые типы управляющих элементов, такие, как обычные или выпадающие списки, вообще не имеют полноценной «обвязки» функциями Win32 API и управляются с ними именно при помощи сообщений. Вы не сможете добавить в такой список строчку или перейти к нужной позиции, вызвав WinAPI’шную функцию с названием вроде ComboBoxAddString или ComboBoxSetPos – таких функций в системных библиотеках Windows просто нет. Зато есть сообщения CB_ADDSTRING и CB_SETCURSEL соответственно, воспользовавшись которыми, Вы легко выполните задуманное. То есть, сообщения играют роль параллельного механизма управления объектами ОС, на работу которого совершенно не влияют традиционные брейкпойнты, которые мы щедрой рукой сеяли в предыдущей главе.

Поскольку сообщение – не функция, брейкпойнт на него поставить нельзя. Но очень хочется. А если очень хочется – значит, все-таки можно, хотя и не так просто, как хотелось бы. Прежде всего, Вам нужно определиться, что именно Вы хотите отловить – момент и точку отправки сообщения, либо подпрограмму обработки этого сообщения. Если Ваc интересует второй вариант и Вы являетесь поклонником SoftIce – считайте, что о Вас уже позаботилась фирма NuMega (ныне - Compuware). Встроенная в SoftIce команда BMSG как раз для этого и предназначена, но чтобы успешно ее использовать, Вам понадобится узнать хэндл окна, которому предназначено сообщение. Если нужные данные у Вас имеются – просто набирайте BMSG <хэндл_окна> <код_сообщения>, и ждите, когда «всплывет» отладчик. Разумеется, команда BMSG, как и любая другая команда установки брейкпойнтов, позволяет создавать условные точки останова, срабатывающие, например, при поступлении сообщений только с определенными значениями wParam и lParam.

А что делать тем, кто пользуется другими отладчиками, в которых аналог BMSG отсутствует? Ответ на этот вопрос находится, как ни странно, именно в руководстве по SoftIce. В частности, там написано, что действие команды BMSG может быть воспроизведено установкой условного брейкпойнта на оконную процедуру, причем в качестве условия нужно указать следующее: IF (esp->8)==<имя_сообщения>; адаптация этого условия под синтаксис, принятый в конкретном отладчике, обычно сложности не представляет, хотя вместо символьного имени сообщения скорее всего придется подставить его код (коды сообщений можно найти в файлах windows.inc, winnt.h или Messages.pas – в зависимости от того, компилятор какого языка у Вас есть под рукой; те, кто не обзавелся подходящим компилятором и не планируют им обзаводиться в ближайшем будущем, могут заглянуть в файл messages.lst из состава InqSoft Window Scanner).

Такой подход несколько сложнее, чем вызвать команду BMSG с нужными параметрами, но зато он дает одно немаловажное преимущество. В предыдущем абзаце я сделал замечание насчет необходимости знать хэндл окна для успешного применения этой команды. Однако хэндл окна может быть Вам известен далеко не во всех случаях. Рассмотрим, к примеру, ситуацию, когда Вам нужно оттрассировать обработчик сообщения WM_INITDIALOG. Вы не сможете просто взять и посмотреть нужный Вам хэндл, поскольку окно только-только создано и могло еще даже не появиться на экране. Конечно, можно «заморозить» все исследуемое приложение и затем предпринять поиск среди всех окон в системе, но не кажется ли Вам, что это несколько сложнее, чем хотелось бы?

Кроме того, при каждом запуске программы хэндлы меняются, что тоже отнюдь не упрощает отладку. А вот оконная процедура всегда находится на одном и том же месте (справедливости ради надо отметить, что создание «плавающей» по адресному пространству от запуска к запуску процедуры теоретически возможно, хотя я такое и не встречал). И потому адрес этой процедуры можно просто записать на бумажке, чтобы затем восстанавливать соответствующий бряк без каких-либо сложностей. Нам осталось только раздобыть адрес этой самой процедуры. Тут тоже, в принципе, ничего сложного нет – разумеется, если под рукой имеются соответствующие инструменты (к примеру, Microsoft Spy++ или все тот же InqSoft Window Scanner). Наведите «прицел» программы на интересующее Вас окно и прочитайте желанный адрес оконной процедуры собственно окна (обычно этот адрес обозначается как WndProc) или оконной процедуры, сопоставленной классу окна.

Иной путь получения адреса оконной процедуры заключается в том, чтобы при помощи API-шпионов обнаружить системный вызов, при помощи которого производится регистрация класса окна (функции WinAPI RegisterClass и RegisterClassEx) либо непосредственно создание окна (список соответствующих функций я приводил в предыдущей главе). Операция эта выполняется в три этапа:
1. Запускаем под API-шпионом, настроенным на отслеживание процедур создания окон, и ждем появления нужного окна.
2. Как только окно появится – останавливаем работу шпиона и при помощи любого сканера окон получаем хэндл этого окна.
3. Если адрес оконной процедуры находится среди параметров функции создания окна - ищем в логе, сгенерированном API-шпионом, функцию, которая возвращает значение нашего хэндла, и считываем ее параметры, среди которых находим искомый адрес оконной процедуры.
4. Если адрес оконной процедуры находится в структуре, указатель на которую передается в функцию регистрации классов – считываем адрес, откуда был произведен вызов этой функции. Затем загружаем программу в отладчик, ставим точку останова на этот адрес (или чуть выше – на том месте, где происходит запись в стек указателя на структуру, это уж как Вам больше понравится) и запускаем программу. Как только исполнение программы прервется на нашем брейкпойнте – находим в памяти структуру, указатель на которую передается в RegisterClass[Ex] и аккуратно переписываем содержимое поля этой структуры, содержащее адрес оконной процедуры для регистрируемого класса.

Вас может смутить сложность четвертого пункта, который выполняет весьма несложные функции, но при этом его описание едва ли не длиннее предыдущих трех. Казалось бы, что нам мешает просто вытащить из лога API-шпиона значение указателя на структуру, по-быстрому снять дамп нужной области и прочитать желанный адрес? В принципе, ничего не мешает – но вот истинное содержимое структуры WNDCLASSEX Вы таким способом вряд ли прочитаете – потому что скорее всего в момент снятия дампа эта структура уже давно будет затерта другими данными. Дело в том, что регистрация класса – событие разовое, и потому память под структуру, описывающую класс, редко выделяют статически; обычно же программист обходится для этих целей куском стека. Так что когда Вы заберетесь своим дампером в адресное пространство процесса, в том месте, где находилась желанная структура, давно уже будут лежать другие данные. И единственным решением в данном случае мог бы быть интеллектуальный API-шпион, которому можно было бы объяснить правила извлечения полей структур из памяти. К сожалению, на данный момент API-шпионы с такими свойствами мне не известны.

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

Я уже приводил пример того, как программа считывала дату своей установки под видом поиска плагинов в своей директории, и, разумеется, этим список возможных приемов маскировки одних действий под другие не исчерпывается. Программа eXeScope, например, в качестве сообщения об ограничении в незарегистрированной версии выдает окно, внешним видом точь-в-точь повторяющее стандартный MessageBox, но в действительности нарисованное визуальными средствами в Delphi. Отображение файла в память вместо обычного чтения в буфер – прием известный, и, тем не менее, чтение файла лицензии таким способом вполне может поставить в тупик начинающего крэкера. Я уж не говорю о таких изощренных техниках, как парсинг ini-файлов «вручную» (после чего можно очень долго возиться с точками останова на GetPrivateProfile* - разумеется, с нулевым результатом) или экспорт кусков реестра при помощи утилиты regedit во временный файл с последующим анализом этого файла (что позволяет обойтись без вызова функций работы с реестром внутри программы).

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

В свое время самым популярным отладчиком для «Спектрума» был MONS (впрочем, некоторые люди, включая меня, предпочитали MON) – восьмикилобайтное порождение программистской мысли, способное загружаться в ОЗУ с любого адреса и управляемое из командной строки (прямо как SoftIce – внешнее сходство этих двух отладчиков вообще сложно не заметить). И, разумеется, MONS позволял ставить брейкпойнты – еще бы, не имея в своем арсенале такой возможности, этот отладчик вряд ли стал бы столь популярен. Но поскольку процессор Z80, на основе которого был сделан «Спектрум», никаких отладочных средств не предоставлял, авторам MONS пришлось реализовывать точки останова чисто программными средствами. Реализация эта красотой отнюдь не блистала – «установка брейкпойнта» по-Спектрумовски заключалась в подстановке в нужное место кода трехбайтной команды CALL xxxx, которая передавала исполнение в недра самого отладчика и таким образом приостанавливала исполнение пользовательского кода. Старые команды, код которых затирался брейкпойнтом, копировались в специальный буфер и дополнялись командой JP (аналог jmp из набора команд x86) для возврата к следующей команде, не испорченной CALL’ом. Исполнение в пошаговом режиме выглядело не менее оригинальным – исполняемая команда перебрасывалась в отдельный буфер, дополнялась все тем же JP, после чего отладчик передавал управление в этот буфер. Если еще вспомнить, что в Z80 существовали недокументированные команды, которые были известны далеко не всем отладчикам (и потому могли обрабатываться некорректно), отлаживаемая программа даже при абсолютно корректной работе могла испортить код отладчика, а под сам отладчик могло элементарно не хватить свободной памяти, и потому его загружали на место «ненужных» данных – Вы поймете, что представляла собой отладка в старые добрые времена.

Разработчики линейки x86 проявили больше заботы о программистах. В этой линейке процессоров вместо самодельной «затычки» в виде команды вызова подпрограммы для отладочных целей ввели отдельное прерывание с номером 3, которое вызывалось однобайтной командой (опкод команды int 3 – СС), в отличие от всех прочих прерываний, которые менее чем двумя байтами вызвать не получится. Другим полезным нововведением стала возможность исполнять код в пошаговом режиме через управление флагом трассировки (эта возможность, впрочем, мало актуальна для отладчиков пользовательского уровня под современные ОС). Однако, несмотря на такой, казалось бы, очевидный прогресс в развитии средств отладки, обыкновенные точки останова все так же, как и десятилетия назад, модифицируют исполняемый код, а потому легко обнаруживаются даже простейшими способами, например, проверкой контрольной суммы всех байтов (не говоря уже о CRC32 и использовании иных, еще более сложных и надежных хэш-функций). Самостоятельно убедиться в том, что точки останова модифицируют код, Вы можете за считанные секунды: откомпилируйте при помощи любого ассемблера следующие две строчки, возьмите OllyDebug и загрузите в него откомпилированный код.

 addr1: mov eax,addr1
 mov al, byte ptr [eax]
 

Если Вы просто выполните этот код в пошаговом режиме, то в регистре AL окажется число 0B8h.В этом нет ничего удивительного, B8 – это опкод команды mov eax, <число>. А теперь попробуйте поставить брейкпойнт на команду mov eax,addr1 и снова оттрассируйте этот код. После выполнения второй команды Вы увидите, что в регистре AL находится число 0CCh, хотя код в окне отладчика внешне совершенно не изменился (если, конечно, не считать изменением подсветку адреса, на который поставлен брейкпойнт). Самое интересное, что отладчики могут «приукрашать реальность» не только в окне кода, но и при просмотре данных.

Давайте проделаем еще один весьма поучительный в этом смысле эксперимент: загрузим наш пример из двух команд, поставим точку останова на первую и запишите адрес этой точки останова. Затем берем InqSoft Window Scanner и читаем байт по записанному адресу. Получаем, разумеется, 0CCh. А теперь взглянем на ту же область глазами отладчика (в OllyDebug это пункт меню Follow in dump|Selection) – и очень сильно удивляемся. Отладчик показывает нам совсем не то, что реально читается из памяти в регистр AL, a то, что должно было бы находиться по указанному адресу, если бы мы не поставили точку останова. Но и это еще не все! Посмотрите на динамические подсказки под окном кода – там-то как раз содержимое памяти отображается как надо.

Вот так «умные» отладчики помогают самообманываться начинающим крэкерам: отсутствие видимых изменений в коде наводит человека, не знакомого с тайнами устройства брейкпойнтов, на мысль о том, что прерывание исполнения программы в точке останова происходит по воле неких таинственных сил, с которыми отладчик находится в телепатической связи. Хотя на самом деле «классические» точки останова – это ни что иное, как обычные memory patch’и – а потому и обнаруживаются теми же самыми способами, что и любые другие исправления в коде.

Кстати, из того, что обычный (не аппаратный) брейкпойнт является ничем иным, как исправлением программы, есть одно интересное следствие. Дело в том, что SoftIce’у в общем-то без разницы, каким образом в программе появилась команда int 3 – главное, что он может на этой команде остановиться не хуже, чем на настоящем брейкпойнте. А после того, как отладчик остановится, можно внести любые поправки в содержимое регистра EIP и код программы, после чего продолжить исполнение как ни в чем не бывало (собственно, в OllyDebug тоже можно проделать такую операцию при помощи пункта New origin here из всплывающего меню). Польза от такого эрзац-брейкпойнта (после срабатывания которого, к тому же, нужно вручную восстанавливать код, который находился на месте int 3 и править EIP), на первый взгляд кажется весьма сомнительной, но она есть. Я уже упоминал глюк в SoftIce, когда отлаживаемая программа после загрузки Symbol loader’ом начинает немедленно выполняться, хотя крэкеру хотелось бы ее в этот момент притормозить. Так вот, если в Entry point исполняемого файла воткнуть опкод 0CCh, у подопытной программы не будет ни единого шанса избегнуть процесса отладки – поскольку первой командой окажется наш int 3, принудительно активирующий отладчик.

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

На практике дело обстоит еще хуже – для удаления некоторых точек останова не нужны ни коды коррекции ошибок, ни резервные копии. И именно к таким точкам останова относятся всеми нами любимые BPX’ы на вызовы функций WinAPI (и, если смотреть шире, на вызовы практически любых функций). Поскольку «брейкпойнт на функцию» - это на самом деле всего лишь брейкпойнт на первый байт этой функции, самый простой из приемов, удаляющих точки останова, выглядит следующим образом: заранее узнать адреса нужных функций при помощи GetProcAddress, прочитать их первые байты (если речь идет о внутренних функциях программы – то просто прочитать содержимое соответствующей ячейки) и сохранить значения этих эталонных байтов. Затем перед особо критичными вызовами нужно лишь сравнивать первые байты процедур с эталонным, и, если обнаружится несоответствие, восстанавливать их. Сам факт того, что процедура начинается с опкода 0CCh говорит о том, что на эту процедуру поставлена точка останова, что может побудить программу предпринять некоторые действия по самозащите. Если учитывать, что многие процедуры начинаются стандартной последовательностью команд push ebp; mov ebp, esp (в шестнадцатиричном редакторе эти команды выглядят как последовательность 55 8B EC), то «действия по самозащите» могут быть простой записью в первые три байта процедуры той самой стандартной последовательности 55 8B EC. После этой операции точка останова, разумеется, исчезнет. Разумеется, выявить защиту от брейкпойнтов, основанную на проверке содержимого неких адресов в памяти, не слишком сложно – нужно лишь поставить аппаратную точку останова на чтение/запись первого байта функции и посмотреть, где этот «капкан на защиту» сработает.

Другой способ постановки бряков на импортированные из DLL функции основан на том, что вызов импортированной функции почти всегда выполняется не напрямую, а через «переходник». На практике вызовы через «переходник» обычно выполняются одним из двух способов.

Первый способ:
call <переходник_к_MyFunc> ; Вызов функции API

переходник_к_MyFunc: jmp MyFunc
(этот способ вызова функций наиболее распространен; переходники вида «jmp истинный_адрес_функции» обычно собраны в конце программы)

Второй способ:
mov edi, dword ptr ds:[элемент_в_таблице]
call edi

элемент_в_таблице: dd <истинный_адрес_функции_MyFunc>
(данная техника вызова функций обычно встречается в продуктах Microsoft)

Идея заключается в том, что в первом случае точку останова можно поставить не на первый байт функции, а на переходник, через который вызывается эта функция. В первом случае это будет обычный BPX на адрес команды jmp MyFunc, во втором случае придется прибегнуть к аппаратной точке останова на чтение двойного слова по адресу «элемент_в_таблице». Поскольку это не будут брейкпойнты на функцию в прямом смысле слова, этот метод имеет одно существенное ограничение: если нужная функция вызывается не через «переходник», а непосредственно по значению ее адреса (получаемому, например, при помощи GetProcAddress), то такой брейкпойнт, понятное дело, не сработает. Разумеется, также существует возможность, что программа попытается проверить целостность «переходников», но как такие попытки обнаруживать, Вы уже знаете.

Если некая процедура вызывается внутри программы стандартным образом, то большинство современных компиляторов генерирует последовательность команд push для помещения параметров функции на стек, собственно переход к процедуре выполняется при помощи команды CALL, а после того, как функция отработает, управление возвращается на команду, следующую за CALL. Однако если программист имеет достаточно высокую квалификацию, он может внести заметное разнообразие в эту картину при помощи «ручного» вызова функций средствами ассемблера. Хотя великий Intel завещал нам вызывать процедуры и функции при помощи специально для этого придуманной команды CALL, отдельным гражданам закон не писан (надо отметить, в число этих граждан входят не только авторы защит, но и любители предельной оптимизации, а также фанаты нетрадиционного программирования). И вот эти странные граждане сочинили несколько имитаций несчастной команды CALL, и эти имитации давно и прочно вошли в арсенал разработчиков защит. Из универсальных нетрадиционных средств вызова подпрограмм прежде всего нужно назвать следующие:

 push <адрес возврата>
 jmp <адрес процедуры>
 
 или
 
 push <адрес возврата>
 push <адрес процедуры>
 ret
 


Более сложные техники неявной передачи управления основаны на умышленном создании и обработке исключительных ситуаций, вызове прерываний и эксплуатации особенностей конкретных ОС. Эти техники сами по себе представляют весьма значительный интерес – с точки зрения как крэкера, так и программиста, однако их количество практически бесконечно, а сложность нередко выходит далеко за пределами «основ». Чтобы Вы имели представление о том, насколько обширна эта тема, сообщу, что любые функции WinAPI (да и вообще любого другого API), в параметрах которых фигурирует callback-функция, могут служить инструментом неочевидного вызова пользовательского кода.

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

 push <адрес возврата>
 push ebp
 mov ebp, esp
 jmp MyProc+3 ; (1)
 …
 
 MyProc:
 push ebp
 mov ebp,esp
 … ; При вызове процедуры в точке (1) будет выполнен переход в эту точку
 


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

Другая проблема, которую порождают нетрадиционные способы вызова процедур, заключается в том, что адрес возврата может быть совершенно любым, а не только адресом команды, следующей за командой вызова. Этот прием встречается довольно часто, когда автор защиты хочет скрыть адрес какой-либо функции, к примеру, проверки корректности введенного серийного номера. Рассуждая логически, он понимает, что необходимо максимально осложнить крэкеру нахождение связи между появлением сообщения о неверном серийнике и процедурой, этот серийник проверяющей. А поскольку традиционным приемом поиска такой процедуры является BPX MessageBox с последующим наблюдением, куда вернется программа из MessageBox’а – программист делает вывод, что хорошо бы сделать так, чтобы программа вернулась «не туда», т.е. как можно дальше от процедуры проверки серийника. В этом случае даже поставив брейкпойнт «куда надо», мы не узнаем, по какому поводу был произведен вызов функции – на стеке будет лежать совершенно другой адрес возврата. И особенно неприятно для начинающего крэкера, когда в качестве адреса возврата оказывается что-нибудь вроде адреса функции ExitProcess.

В общем случае «победить» такой прием можно либо через долгую медитацию с массированным применением шестнадцатиричного редактора/дизассемблера для поиска всех точек, в которых программа явно или неявно оперирует адресами нужных функций WinAPI, либо через поиск «модифицированным методом Ньютона», описанным в предыдущей главе, либо применив средства трассировки кода, имеющиеся в SoftIce или OllyDebug. Собственно, трассировка в таких случаях является орудием, по свойствам приближающимся к термоядерной бомбе: редкий код способен выдержать такой удар, но чтобы получить желаемый эффект, требуется весьма серьезное техническое обеспечение (процесс трассировки требует немалых объемов памяти и достаточно быстрого процессора) и грамотный выбор области применения. Так что, прежде чем пускать в ход «тяжелое вооружение», есть смысл подумать о решении проблемы более простыми средствами.

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

По традиции, для начала разберемся, что эти следы из себя представляют. Представьте себе следующую широко распространенную ситуацию: A=>B=>C, где «A=>B» расшифровывается как «процедура A вызывает процедуру B». При вызове процедуры B стандартными средствами, т.е. командой CALL, в момент начала исполнения процедуры B на вершине стека будет лежать адрес возврата из процедуры B в процедуру A. Аналогичный процесс происходит при вызове процедуры C процедурой B. Получается, что если мы поставим брейкпойнт на точку входа в процедуру C, мы в этот момент сможем наблюдать в стеке следующую картину (вершина стека - вверху):
• Адрес возврата из процедуры C в процедуру B
• Адрес возврата из процедуры B в процедуру A

Немного усложним картину, и допустим, что в процедуры B и C передаются некие параметры (порядок передачи параметров для нас в данном примере несущественен). Стек в этом случае будет выглядеть следующим образом:
• Адрес возврата из процедуры C в процедуру B
• Параметры, переданные в процедуру C
• Адрес возврата из процедуры B в процедуру A
• Параметры, переданные в процедуру B


На практике процедуры обычно занимаются чем-то более сложным, чем простой вызов других процедур с параметрами, а потому довольно часто резервируют на стеке место под локальные переменные. Допустим, что процедуры A и B используют локальные переменные, место под которые выделяется на том же стеке, и посмотрим, что после этого будет твориться в стеке:
• Адрес возврата из процедуры C в процедуру B
• Параметры, переданные в процедуру C
• Область локальных переменных процедуры B
• Адрес возврата из процедуры B в процедуру A
• Параметры, переданные в процедуру B
• Область локальных переменных процедуры A

А теперь представим, что автор защиты на этапе B=>С подменил адрес возврата из процедуры С в процедуру B своим собственным значением, и возврат теперь происходит не в B, а некую процедуру D. Что от этого изменится? Да только одна, самая верхняя строчка! А вот адрес возврата в процедуру A, локальные переменные и параметры вызова какими были, такими и останутся, и это можно использовать в качестве зацепки, позволяющей выявить все этапы пути от процедуры A к процедуре C. Другое дело, что информация, лежащая в стеке, никак не структурирована, поэтому Вам придется самому угадывать, что там – локальные переменные, что – параметры вызовов, а что – адреса возврата. И хотя процесс проверки этих догадок может быть весьма трудоемким, лучше иметь хотя бы такую беспорядочную информацию, чем не иметь никакой. Иногда бывает полезно посмотреть, что находится выше вершины стека – там нередко тоже удается обнаружить следы деятельности процедур, отработавших перед тем, как мы остановили программу.

Глава 10. Слишком хорошо – тоже не хорошо.


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

Если Вы входите в число поклонников отладчика SoftIce, то Вы не могли не заметить одну милую особенность этого инструмента: если Вы поставили брейкпойнт непосредственно на системную функцию, SoftIce будет «всплывать» при каждой попытке исполнить эту функцию независимо от того, внутри какого процесса функция была вызвана. Такое поведение отладчика, несомненно, бывает полезным при отладке драйверов, хук-процедур и самой операционной системы. Но наша беда (или счастье – это уж с какой стороны посмотреть) в том, что мы отлаживаем «обычную» программу, и постоянные выпадения в отладчик по совершенно неинтересным поводам – это далеко не то, о чем мы мечтали. Вот если бы удалось сделать так, чтобы брейкпойнты работали внутри только одного процесса… Те, кто начал знакомиться с SoftIce по пакету NuMega DriverStudio 2.6, скорее всего не увидят в этом никакой проблемы: BPX <имя_функции> IF PID=<ID_нужного_процесса> - и дело в шляпе. Набрав это заклинание, они, в общем-то, будут полностью правы, ибо это вполне хороший и, в общем-то, самый короткий путь к цели. И я никогда не скажу дурного слова про тех, кто последует этим путем. Однако этот путь – далеко не единственный из возможных, и если Вам интересны иные способы решения проблемы постановки брейкпойнта – добро пожаловать в музей истории SoftIce.

Мое общение с этим отладчиком началось с версии 3.23 для Windows 9x, случайно обнаруженной на свежем «хакерском» диске. Пусть по нынешним меркам тот СофтАйс совершенно не производит впечатления - тогда это был шедевр! Впрочем, то был шедевр не без недостатков, самым главным из которых была его неразборчивость в срабатывании брейкпойнтов. Как я отмечал, поставить брейкпойнт на функцию WinAPI, срабатывающий исключительно внутри нужного процесса (по-научному брейкпойнты с такими свойствами называются «address-context sensitive», они же «контекстно-зависимые»), напрямую было невозможно. Чтобы почувствовать, насколько серьезной была проблема, представьте себе следующую картину: бряк, поставленный на функции чтения из реестра (которая называется RegQueryValue[Ex], надеюсь, Вы еще не забыли мой «поминальник»), работает настолько хорошо, что отлавливает абсолютно все попытки чтения из реестра, независимо от того, выполняет их отлаживаемая Вами программа или какая-либо другая. Любые попытки осмысленной отладки в такой ситуации заведомо обречены на провал, единственное, чем Вы будете заниматься – это нажимание клавиш Ctrl-D, ибо Windows настолько сильно любит читать данные из реестра, что делает это много раз в секунду (если хотите своими глазами посмотреть на эту странную любовь, RegMon Вам в этом поможет). Так что же, поставить бряк на RegQueryValueEx и получить от этого удовлетворительный результат совсем никак невозможно? Как бы не так! «Если нельзя, но очень хочется, то все-таки можно».

Начнем с небольшого, но очень важного определения: адресный контекст (он же контекст процесса) – это все виртуальное адресное пространство, выделенное данному процессу.

«Военная хитрость», при помощи которой мы «проапгрейдим» контекстно-независимый брейкпойнт до контекстно-зависимого, основана на том факте, что хотя SoftIce и игнорирует контекст в момент срабатывания контекстно-независимых брейкпойнтов (в число которых входят и брейкпойнты на функции WinAPI), сам текущий адресный контекст от этого никуда не исчезает. И потому в момент срабатывания условного брейкпойнта отладчик вполне способен прочитать любые данные из существующего в этот момент виртуального адресного пространства. Проще говоря, если брейкпойнт сработал внутри программы X, то отладчик сможет «увидеть» адресное пространство программы X вместе со всеми данными, содержащимися в этом пространстве, а заодно и содержимое всех регистров, каким оно было в момент срабатывания брейкпойнта. А если отладчик «видит» все адресное пространство процесса и способен читать исполняемый код программы, значит, можно попытаться идентифицировать процессы по особенностям их исполняемого кода! На практике эта «идентификация по особенностям исполняемого кода» выглядит весьма прозаично: нужно забраться при помощи шестнадцатиричного редактора внутрь секции кода (впрочем, для идентификации можно использовать и любые другие заведомо не изменяющиеся при работе программы данные), выдернуть оттуда первый попавшийся DWORD (назовем его My_DWORD) и запомнить виртуальный адрес (My_Addr соответственно), по которому этот DWORD находился. Дальнейшие операции, выполняемые уже в отладчике, ненамного сложнее: BPX имя_функции_WinAPI IF (*My_Addr)==My_DWORD. Все.

Думаю, с пониманием того, что делает эта команда, ни у кого сложностей не возникло: мы соорудили условную точку останова, которая в момент срабатывания проверяет, не лежит ли по адресу My_Addr двойное слово, равное My_DWORD. И если такое двойное слово по нужному адресу обнаруживается, то отладчик приостанавливает исполнение программы. Поскольку случайно встретить пару программ, у которых в секции кода по одним и тем же виртуальным адресам находились бы одинаковые DWORD’ы, вряд ли возможно, такой простейший способ различения процессов в абсолютном большинстве случаев отлично срабатывает. Сложности могут возникнуть только в двух случаях: при отладке упакованных программ и если нужно параллельно отлаживать два экземпляра («экземпляр» следует понимать как «instance») одного и того же приложения – поскольку код обеих процессов идентичен, в общем случае различить их по содержимому адресного пространства затруднительно.

В настоящее время применение описанного выше метода для определения, в контексте какого из процессов сработал брейкпойнт, в общем-то, не требуется – в современных версиях SoftIce (вообще, сам отладчик SoftIce, особенности работы с ним и различия между разными его версиями – это отдельная большая тема, которая будет закрыта не раньше, чем прекратится развитие самого SoftIce) эта проблема решается безо всяких ухищрений. Однако поскольку особенности отдельных инструментов имеют весьма слабое отношение к теоретическим вопросам крэкинга, которым посвящена данная работа, я в дальнейшем не буду акцентировать внимание на этих особенностях – разобравшись в предлагаемом материале, Вы сами найдете способ с максимальной эффективностью использовать эти особенности. Для того же, чтобы избежать путаницы при изложении материала этой главы, мы будем считать, что все брейкпойнты, о которых ниже пойдет речь, являются контекстно-зависимыми, то есть работают исключительно в рамках того процесса, в котором они установлены. Таким образом, мы «уравняем в правах» SoftIce и отладчики третьего кольца защиты («ring3 debuggers»), к которым, в частности, относится OllyDebug.

Само по себе создание условного брейкпойнта, срабатывающего исключительно при появлении по некоему адресу известного значения – прием весьма часто употребляемый и полезный во множестве ситуаций. Самое известное из применений этой техники: «остановить программу, если переменная равна некоему значению» мы рассматривать не будем по причине крайней его банальности, такие вещи Вы способны проделывать самостоятельно, и, возможно, даже с закрытыми глазами. Куда менее очевидна возможность использования брейкпойнтов чтобы «притормозить» упакованную программу сразу после начала ее исполнения. Возможно, Вы уже встречали в статьях по крэкингу сокращение «OEP», которое расшифровывается как Original Entry Point, «оригинальная («истинная») точка входа». Возможно, Вы также читали о том, что распаковка сжатых исполняемых файлов включает в себя поиск адреса этой самой OEP. Если же Вы ничего такого еще не читали и не встречали, то Вам необходимо запомнить следующие базовые сведения:
1. Entry point – это точка, с которой начинается исполнение программы после загрузки. Адрес точки входа в Win32-приложениях хранится в PE-заголовке исполняемого файла.
2. Абсолютное большинство упаковщиков и навесных защит изменяют значение адреса точки входа таким образом, чтобы управление передавалось распаковщику или защитному модулю соответственно.
3. Original entry point – это entry point исполняемого файла до того, как файл был сжат/зашифрован и значение точки входа было модифицировано упаковщиком.
4. Для успешной распаковки программы в общем случае требуется узнать адрес OEP, остановить программу в тот момент, когда распаковщик передаст управление на OEP и в этот момент снять дамп со всех секций процесса. Также довольно часто после снятия дампа приходится восстанавливать таблицу импорта, в некоторых случаях может потребоваться ручная правка параметров секций.

Один из приемов отыскания OEP заключается в следующем: нужно поставить на функции WinAPI брейкпойнты таким образом, чтобы один из них сработал заведомо недалеко от точки входа в программу. Выяснив, откуда был произведен этот первый вызов WinAPI’шной функции, Вы сделаете первый (и самый важный) шаг на пути к адресу OEP. После этого обратной трассировкой в уме или каким-либо иным методом Вы будете «раскручивать» программу, как бы заставляя ее исполняться «задом наперед», и таким образом шаг за шагом подбираясь к OEP. Идея, лежащая в основе этого метода поиска OEP, крайне проста: современные компиляторы устроены так, что в коде абсолютного большинства программ неподалеку от точки входа содержится как минимум один вызов функции WinAPI (обычно такой функцией является GetModuleHandle или GetCommandLine).

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

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

Наблюдая за работой запакованных программ, Вы могли заметить, что они после распаковки всегда располагаются в памяти по одним и тем же адресам. Применив против такой программы какую-нибудь утилиту вроде ProcDump, Вы даже можете прочитать параметры секций программы, частности - начальный адрес и размер. После этого вполне естественным кажется вопрос: «а что, если проверять, принадлежит ли адрес возврата, лежащий на стеке, промежутку адресов, в котором расположен код программы?» И вопрос этот отнюдь не праздный. Дело в том, что нередки упаковщики и навесные защиты, в которых блок, выполняющий дешифровку и распаковку кода, располагается в области, не пересекающейся с той, в которой в итоге будет расположен исполняемый код программы. Поэтому, поставив брейкпойнт на функцию и в качестве условия указав что-нибудь вроде ([esp]>401000) && ([esp]<501000) Вы добьетесь того, чтобы Ваш брейкпойнт активизировался лишь в том случае, если при выходе из функции предполагается возврат в код отлаживаемой программы. Данный метод, также как и описанный выше, может использоваться для поиска OEP через обратную трассировку от первого вызова функции WinAPI. Однако этим возможности условных точек останова, проверяющих адрес возврата, отнюдь не ограничиваются.

Очень часто возникает необходимость посмотреть параметры вызова некой часто используемой функции в тех случаях, когда вызов этой функции был сделан из одной или нескольких точек, и при этом проигнорировать все прочие вызовы (которых может быть очень много). Если решать задачу «в лоб», то необходимо найти все ссылки на данную функцию и установить по брейкпойнту на каждый интересующий нас вызов. Обычно такой подход вполне удовлетворителен, но мы не будем искать легких путей и посмотрим, как ту же задачу можно решить при помощи одного единственного брейкпойнта. Вы уже знаете, что для распознавания, откуда был сделан вызов функции, можно использовать адрес возврата, лежащий на вершине стека, и потому без труда напишете соответствующее условие для брейкпойнта. Такое условие может выглядеть следующим образом:
([esp]==ret_addr1) || ([esp]==ret_addr2) || …,
где ret_addr1 и ret_addr2 – адреса возврата. Все это достаточно очевидно, и Вы можете задать вопрос, зачем нужно было изобретать очередной велосипед, если традиционный подход дает ничуть не худшие результаты? Первая причина - «человеческий фактор»: работать с большим количеством точек останова не всегда удобно даже в насквозь визуальном OllyDebug, а уж «рулить» десятком-другим брейкпойнтов в SoftIce – занятие, что называется, на любителя. Так что если есть возможность значительно уменьшить число брейкпойнтов и облегчить себе жизнь, почему бы этой возможностью не воспользоваться? Кроме того, перед Вами может встать задача, обратная по отношению к вышеприведенной: отслеживать все вызовы процедуры, за исключением нескольких. И в этом случае сформировать строку с условием вида
([esp]!=ret_addr1) && ([esp]!=ret_addr2) && …,
в которой ret_addr1, ret_addr2 – адреса возврата, при которых точка останова не должна срабатывать, представляется гораздо более простым, чем искать в коде программы все подозрительные ссылки на интересующую Вас процедуру и «обвешивать» их брейкпойнтами.

Однако более важным представляется применение условных брейкпойнтов для обнаружения неочевидных вызовов функций. Я уже рассказывал в предыдущей главе о приемах, при помощи которых разработчик защиты может вызывать процедуры неявным образом, из-за чего такие обращения к процедурам становятся «невидимы» для дизассемблеров и прочих инструментов анализа «мертвого кода». В качестве дополнительного средства маскировки авторами защит могут параллельно использоваться как обычные вызовы процедур, так и неочевидные; я встречал такое в приложении к MessageBoxA и к функции чтения из реестра, но вообще эта техника может быть применена к любой достаточно часто вызываемой процедуре, используемой в защитных механизмах. В результате в дизассемблированном коде мы увидим несколько ничем не примечательных явных вызовов – но, скорее всего, не заметим самого интересного. Поставив «обычный» брейкпойнт на вызываемую функцию, мы можем столкнуться с тем, что эта функция вызывается десятки раз, и потому тоже не сможем определить, задействована ли эта функция в защитном механизме – интересующие нас неявные вызовы потеряются среди десятков и сотен вызовов явных. Вот если бы был какой-нибудь способ отделить явные вызовы от неявных…

И такой способ есть. Базовые идеи, используемые для различения явных и неявных вызовов, могут быть сформулированы следующим образом:
1. Практически все явные вызовы функций легко обнаруживаются дизассемблером или вспомогательными инструментами для поиска ссылок (к примеру, в OllyDebug эта операция элементарно выполняется из контекстного меню: Find references to|selected command). Построив список явных вызовов, можно записать условие-фильтр, в котором будут перечислены все адреса возврата после явных вызовов, и использовать этот фильтр в качестве условия срабатывания для точки останова. Проще говоря, нам нужно будет установить условный брейкпойнт с фильтром вида ([esp]!=ret_addr1) && ([esp]!=ret_addr2) && …, который бы «пропускал» явные вызовы, но срабатывал на всех остальных, то есть неявных.

2. Набор «штатных» команд ассемблера, предназначенных для вызова подпрограмм, сравнительно невелик, поэтому, проанализировав несколько байт, предшествующих адресу возврата, можно строить предположения о способе вызова. В частности, если опкод, находящийся на пять байт «выше» адреса возврата, равен 0E8h (CALL xxxx), скорее всего вызов был сделан стандартным способом. Если же в окрестностях адреса возврата ни одна из разновидностей команды CALL не обнаружена – либо вызов был неявным, либо внутри отработавшей процедуры присутствовал код, искажающий адрес возврата.

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

Поясню эту мысль на следующем коде:

 cmp eax, ebx
 ja _no_call
 call MyProc
 
 _no_call:
 …
 


Нетрудно заметить, что если вызов процедуры MyProc был сделан из вышеприведенного куска кода, то в момент входа в процедуру значение регистра eax должно быть меньше либо равно ebx (в противном случае выполнилась бы команда ja, обходящая call). И если это соотношение между регистрами в каких-то случаях окажется недействительным, для крэкера будет совершенно очевидно, что в этих «подозрительных» случаях вызов подпрограммы был произведен откуда угодно, но только не из продемонстрированного кода.

Разумеется, при большом желании, возможно построить код программы таким образом, что приложение вышеприведенные идей не даст положительного результата. Однако это потребует от программиста дополнительных и довольно серьезных усилий – разработчику придется проанализировать собственный код так, как это сделал бы крэкер (и отнюдь не факт, что это у него получится – принципиально разные цели порождают разный образ мышления). Затем все найденные уязвимые участки придется переписать, причем наверняка - на ассемблере, поскольку высокоуровневые языки обычно не дают необходимой гибкости. После всего этого все переписанные связки между процедурами придется заново отладить протестировать, чтобы убедиться в полной корректности работы исправленного кода. В общем, объем работ нешуточный, а результат… Результат, как водится, заведомо неизвестен – никто не может гарантировать, что разработчик не упустит какой-либо важный момент, или крэкер не найдет особо нетрадиционный подход, после которого все усилия автора защиты пойдут прахом. Так что обычно разработчики обычно не озабочиваются сокрытием «тонких эффектов» при неявных вызовах, и Вы, при случае, можете обратить это себе на пользу.

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

До настоящего момента мы как-то обходили вниманием точки останова, срабатывающие при попытке доступа к определенным областям памяти вообще, и аппаратные брейкпойнты в частности. Вы, возможно, даже начали беспокоиться из-за того, что, говоря о точках останова, я так долго не упоминал волшебное слово «BPM». И вот пришло время поближе узнать, что такие брейкпойнты собой представляют и какую практическую пользу из них можно извлечь. «Законный» способ установки брейкпойнтов на области памяти основывается на использовании специальных отладочных регистров, обозначаемых как DR0-DR7. Каждый из брейкпойнтов может отслеживать любой (но только один) из следующих типов обращения к памяти: запись, чтение, запись или чтение, исполнение кода. Операции «чтение» и «исполнение» процессор считает принципиально разными, несмотря на то, что здравый смысл говорит нам: прежде чем исполнить код, нужно его прочитать. «Это невозможно понять, это нужно запомнить» - поэтому временно отложите здравый смысл в сторонку и запомните это правило. По этой же причине одновременно отслеживать запись, чтение и исполнение при помощи одного-единственного брейкпойнта у Вас не получится. Это первое существенное ограничение, наложенное инженерами из Intel на использование отладочных регистров.

Впервые отладочные регистры появились в процессорах 80386 именно для отслеживания обращений к памяти, но в Pentium возможности этих регистров были распространены и на порты ввода-вывода. Поскольку собственно адреса точек останова (или номера портов, обращение к которым будет отслеживаться) задаются в регистрах DR0-DR3, таких брейкпойнтов может быть не более четырех – это второе ограничение. Еще одна проблема состоит в том, что отладочные регистры позволяют установить брейкпойнт только на байты, слова (WORD) или двойные слова (DWORD), аппаратных брейкпойнтов на обращения к более крупным блокам памяти не предусмотрено. Если Вам нужно отследить обращения к переменным «длинных» нецелочисленных типов (Double, Extended), Вы моежете поставить брейкпойнт в середину переменной; в этом случае любое обращение к такой переменной «зацепит» брейкпойнт. Также важно помнить следующее: если Вы устанавливаете бряк на слово, адрес брейкпойнта будет автоматически выровнен на ближайший «снизу» четный адрес, а если Вам нужен бряк на DWORD – приготовьтесь к тому, что процессор выровняет адрес брейкпойнта на адрес, кратный четырем.

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

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

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

Если Вы пробовали поставить брейкпойнт на упакованный код, Вы не могли не заметить, что после этого действа программа отчего-то перестает корректно распаковываться (впрочем, с вероятностью приблизительно 1/256, программа распакуется даже после такого издевательства, но брейкпойнт, разумеется, работать не будет). Прочитав предыдущую главу, Вы наверняка осознали всю глубину и тяжесть Вашей ошибки, чистосердечно раскаялись в этом ужасном деянии и, положив руку на руководство по отладчику, трижды произнесли торжественное обещание никогда больше так не поступать. А потом, подобно классику, задались вопросом: «что делать?» Разыскивая ответ на этот глубоко философский вопрос, Вы могли обнаружить в руководстве по SoftIce раздел, повествующий о команде BPM и ее параметрах, либо добраться до таинственных пунктов Breakpoint|Hardware, on… в контекстном меню OllyDebug. Если Вы еще не проделали этих операций – прочитайте документацию по отладчику, посмотрите, как правильно ставить аппаратные точки останова на чтение, запись и исполнение и немного потренируйтесь на первой попавшейся программе, дабы убедиться, что такие точки останова действительно существуют и даже работают. А потом освежите в памяти эксперимент, в котором мы при помощи дампера наблюдали изменения в коде, вызываемые командой BPX, и попытайтесь повторить его над к аппаратными точками останова. Как и следовало ожидать, аппаратные брейкпойнты не оставляют в коде программы никаких следов. Их не видно – но наши маленькие аппаратные друзья существуют и работают! И как бы программа ни утюжила свой код проверками, сколько бы ни высчитывала контрольные суммы – против аппаратных точек останова эти приемы бесполезны, так что теперь Вы сможете сколько угодно отлаживать программу, не опасаясь, что защитные процедуры в один миг изничтожат любовно расставленные Вами бряки.

Интересно, что аппаратные брейкпойнты отлично работают не только с «обычными» участками памяти, но и со стеком. В принципе, ничего удивительного в этом нет – память, зарезервированная под стек, с точки зрения процессора и ОС ничем принципиально не отличается от памяти, предназначенной для кода или данных. Однако для некоторых начинающих крэкеров, разум которых опутан предрассудками, сама идея установки аппаратного брейкпойнта на чтение/запись данных в стек выглядит чем-то странным и противоестественным. А ведь установка аппаратного брейкпойнта на стек – это отнюдь не оригинальничание, а вполне действенная техника, часто используемая при «ручной» распаковке сжатых программ, а также позволяющая обнаруживать подмену адресов возврата в стеке.

Знания об аппаратных брейкпойнтах позволяют нам по-новому взглянуть на проблему подмены адресов возврата. Вспомните последний пример из предыдущей главы – тот, где функция A вызывает функцию B, функция B вызывает функцию C, а функция C делает «финт ушами», подменяя адрес возврата, и возвращается в функцию D (а не в функцию B). Теперь, когда Вы вооружены необходимой информацией о том, как отслеживать обращения к определенным адресам памяти, для Вас не составит труда обнаружить попытки процедуры подправить свой адрес возврата – просто «накройте» брейкпойнтом на чтение/запись двойное слово, хранящее адрес возврата, и Вы без проблем найдете команду, которая выполняет подмену.

Аппаратные точки останова внутри стека могут помочь Вам победить еще один довольно неприятный защитный прием – переход со «сбросом» части стека. В вышеприведенном примере демонстрировалось искажение адреса возврата, лежащего на стеке, однако автор защиты может и не возиться с искажением адресов, а волевым решением «выбросить» со стека часть параметров, переменных и адресов возврата (это может быть сделано, к примеру, командой ADD ESP,xxxx или несколькими PUSH’ами), после чего переход в нужную точку кода выполнить простым JMP. Поразмыслив над содержимым стека, Вы даже можете приблизительно определить границы стековых фреймов. Если предположить, что стековые фреймы «сбрасываются» целиком (в принципе, это не обязательно, но такой код проще в отладке), то Вы можете составить список возможных значений регистра ESP после «сброса» фреймов. Затем методом «научного тыка» протестируйте каждое из этих значений, устанавливая аппаратные брейкпойнты на чтение/запись двойного слова из каждого из этих значений ESP, а также на 4 байта ниже; можно также добавить брейкпойнты на предполагаемые адреса локальных переменных. В результате Вы получите одну из трех ситуаций:
1. Если после «сброса» стека программа попытается выполнить возврат из процедуры, она «споткнется» о брейкпойнт, стоящий по адресу ESP.
2. Если программа попытается вызвать подпрограмму, и при этом поместит какое-либо значение (параметр или адрес возврата) на стек, команда, выполняющая запись данных в стек, вызовет срабатывание брейкпойнта по адресу ESP-4.
3. Если брейкпойнт установлен на локальную переменную, исполнение программы будет прервано при первом же обращении к этой переменной.
Смысл всех этих действия заключается в том, чтобы как можно раньше остановить программу после «сброса» стека и узнать, что пытался скрыть автор защиты при помощи этого приема. Поскольку современные программы стеком пользуются достаточно активно, можно надеяться, что первое срабатывание нашей «ловушки» будет не слишком далеко отстоять от той точки, в которую был выполнен переход. Если выяснится, что адресов, на которых срабатывают ловушки, несколько, Вам нужно выбрать тот из них, обращение к которому выполняется раньше других – очевидно, что он наиболее близок к искомой точке.

Описанный метод также традиционно используется для определения OEP упакованных программ: заклинание «BPM ESP-4», которое следует набирать в SoftIce сразу после загрузки распаковываемой программы, так или иначе упоминается в большинстве статей, посвященных ручной распаковке кода. А вот смысл сего заклинания, увы, поясняется намного реже, и сейчас я попытаюсь исправить это недоразумение. Реализация абсолютного большинства навесных защит такова, что значение ESP в момент завершения работы защитного модуля и передачи управления на OEP в точности равно значению ESP сразу после загрузки запакованной программы. Причины этого лежат где-то в глубинах сознания разработчиков защит, поскольку уменьшение значения ESP перед исполнением программы на величину, кратную четырем, никаких отрицательных эффектов не вызывает, поскольку место под стек обычно резервируется с запасом (а вот увеличение значения ESP уже может быть чревато). Однако большинство упаковщиков считают, что значение ESP лучше передавать пользовательскому коду в неизменном виде, а потому свято блюдут принцип «сколько на стек положено – столько должно быть снято», причем окончательная коррекция стека скорее всего будет проведена непосредственно перед выполнением перехода на OEP. И если эта коррекция выполняется командами чтения данных из стека (POP, POPAD и т.п.), а не простой записью в ESP ранее сохраненного значения, Ваш брейкпойнт сработает в этот знаменательный момент. Таким образом, Вам останется лишь отсеять ложные срабатывания брейкпойнта, если таковые будут, и трассировать код до тех пор, пока управление не будет передано из распаковщика в основную программу.

Мой рассказ об аппаратных точках останова был бы неполон, если бы я не упомянул об одной специфической разновидности брейкпойнтов останавливающих исполнение программы при обращении к произвольной области памяти. Такие брейкпойнты в SoftIce под ОС Windows 9x создавались при помощи команды BPR (в современных версиях этого отладчика команда BPR, к сожалению, отсутствует, что особенно странно в свете того, что в арсенале OllyDebug такие точки останова имеются). Внешне использование таких брейкпойнтов ничем не отличается от работы с обычными аппаратными точками останова, если не считать возможности «накрыть» брейкпойнтами практически неограниченное количество участков памяти совершенно любого размера (что выгодно отличает данный тип брейкпойнтов от «обычных» аппаратных, которых может быть не более четырех). Гораздо больший интерес представляет знание о том, каким образом реализован этот тип брейкпойнтов, которое, возможно, пригодится Вам в будущем (к примеру, если Вы захотите написать собственный отладчик).

Как Вы знаете, процессоры линейки x86 в защищенном режиме (некоторые источники называют этот режим расширенным) содержат множество средств, облегчающих создание многозадачных программ и позволяющих защитить данные от некорректных операций над ними. Для нас особенно интересной представляется возможность изменять атрибуты защиты отдельных страниц памяти, то есть разрешать или запрещать определенный тип действий (запись, чтение, исполнение кода) над информацией, хранящейся на той или иной странице памяти. При этом любая попытка выполнить запрещенную операцию, к примеру, записать данные на страницу, для которой запись запрещена, вызовет исключительную ситуацию. Более того, чтобы ради этого нехитрого действа Вам не пришлось выбираться в нулевое кольцо защиты, в Windows API включены функции VirtualProtect и VirtualProtectEx, позволяющие изменять атрибуты страниц памяти (правда, флаг запрета на исполнение кода на платформе x86 бесполезен, но необходимости в таком запрете обычно и не возникает). Как видите, создать «область останова» не так уж трудно, основную сложность в реализации таких «областей останова» представляет обработка исключительных ситуаций, возникающих при обращении к данным из защищенной области. Основным препятствием в практической реализации является то, что обычно размеры «области останова» не выровнены на границы страниц и кратны размерам страницы. По этой причине возникает необходимость в написании достаточно изощренного кода, распознающего, к какой ячейке памяти произошло обращение и обеспечивающего корректное продолжение работы программы после ошибки нарушения прав доступа к странице (по сути требуется написать нечто среднее между простым дизассемблером и виртуальной машиной).

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

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

Глава 11. Трассировка во сне и наяву.


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

Прежде всего договоримся о терминологии. Само слово «трассировка» часто употребляется в различных смыслах – от синонима «отладки» до обозначения процесса пошагового исполнения программы (см. команды вроде «trace into» и «trace over» в некоторых отладчиках). Я не буду оригинален и введу еще одно значение слова «трассировка», которое и будет использоваться на протяжении данной главы: трассировка – это процесс определения «траектории» исполнения кода с некой заранее заданной точностью и наблюдение за изменениями во время исполнения этого кода. Вот такое определение – незамысловатое, но с замахом на глобальность. И, разумеется, без подробной расшифровки выглядящее достаточно туманно – как и любое другое достаточно широкое определение. Сам процесс трассировки может быть направлен на получение следующих знаний:
• Определение траекторий исполнения кода, как всех теоретически возможных, так и активирующихся только при неких начальных условиях.
• Наблюдение за изменениями каких-либо параметров (значений регистров, переменных, флагов) и определение их влияния на порядок исполнения команд.
• Нахождение точек ветвления и условий, приводящих к активации или деактивации того или иного участка траектории исполнения кода.

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

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

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

 mov eax,255
 
 _loop_i:
 mov edx,eax
 mov cl,8
 
 _loop_j:
  shr edx,1
  jnc @@1
  xor edx,0EDB88320h
 @@1:
  dec cl
  jnz _loop_j
 
 mov [CRC32_Table+eax*4],edx
 dec eax
 jns _loop_i
 


Распечатайте этот код на листе бумаги (в принципе, можно выполнять все операции мысленно, но это будет не так удобно, как работать с «твердой копией»). Возьмите карандаш и напротив каждой из команд нарисуйте кружок – это будут точки возможной траектории исполнения кода. Теперь по полученным точкам попробуем построить совокупность всех возможных траекторий исполнения кода. Как известно, большинство процессоров исполняет команды последовательно (или же успешно изображают последовательное исполнение), одну за другой, по направлению от «нижних» адресов памяти к «верхним». И такой порядок исполнения может быть нарушен лишь командами переходов/возвратов, вызова прерываний, а также различными исключительными ситуациями. Вооружившись этим знанием, предположим, что исполнение данного куска кода начинается с команды mov eax,255 и начнем соединять наши кружочки стрелками в том порядке, в каком будут исполняться команды. Когда Вы доберетесь до команды jnc @@1, у Вас наверняка возникнет вопрос, куда нарисовать стрелку - ведь для флага SF может находиться в одном из двух возможных состояний, и в зависимости от этого состояния следующей исполняемой командой будет либо xor edx,0EDB88320h, либо dec cl. Выход из этой ситуации прост – рисуйте обе стрелки. После того, как Вы закончите это упражнение, должен получиться приблизительно такой рисунок:



Для удобства понимания переходы «вперед» по коду (такой переход обнаружился только один) я вынес влево, а переходы «назад» - вправо. Что мы видим? Прежде всего – то, что в каждый кружок входит как минимум одна стрелочка, а это означает, что каждая из нарисованных нами команд получает управление явным образом. Если бы после завершения нашего высокохудожественного шедевра обнаружилось, что какой-либо кружок «висит в воздухе», это было бы веским основанием для проведения подробного расследования на тему «зачем нужен программный блок, который не получает управления явным образом» (техническую сторону таких расследований мы рассматривали в предыдущих двух главах). Далее: окинув беглым взглядом картинку, Вы легко заметите два цикла, один из которых вложен в другой и одну конструкцию ветвления, «обходящую» команду xor edx,0EDB88320h при выполнении некоего условия (в языках высокого уровня такая последовательность обычно реализуется конструкциями вида IF <условие> THEN <действия>). Вот так при помощи карандаша и бумаги можно за считанные минуты выделить логические единицы внутри довольно абстрактной процедуры. Несмотря на то, что метод кажется очень простым и даже в чем-то «игрушечным», в действительности такие схемы с кружочками-стрелочками очень удобны, особенно если Вы сравнительно недавно занялись исследованием программ, и Ваш глаз еще не натренирован на вычленение управляющих конструкций в бесконечных листингах. А если Вам не хочется слишком уж часто прибегать к помощи карандаша и бумаги, есть смысл обзавестись дизассемблером IDA – последние версии этого продукта тоже умеют проставлять стрелочки напротив кода (правда, там это реализовано не так удобно, как на нашем рисунке). Или же можно написать собственную программу, которая бы на основе листинга рисовала такие вот картинки; если не пытаться сразу создать дизассемблер (и не просто дизассемблер, а как минимум аналог W32Dasm), а анализировать уже готовые листинги, программа даже получится не слишком сложной. Надо сказать, что инструменты, способные на основе ассемблерного листинга построить картинку, подобную приведенной выше, в настоящее время очень редки и потому более чем востребованы общественностью. Кстати, наш набор стрелочек на самом деле – отнюдь не изобретение «для личного пользования», а наглядное изображение весьма научной штуковины под названием «граф» (имеется в виду математический термин, а не дворянский титул). Так что если Вам близок раздел математики под названием «теория графов», Вы можете попробовать приложить для анализа нашей картинки всю мощь этой теории; особенно актуально это для тех, кого заинтересует тема визуализации и автоматического анализа кода.

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

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

Как я уже говорил, обычно мы изучаем не весь код построчно, а лишь те его участки, которые могут привести к интересующим нас эффектам, причем задача чаще всего стоит следующим образом: по известному эффекту необходимо найти траекторию исполнения кода, которая приводит к появлению этого эффекта. Предположим, что Вы успешно обнаружили, где в программе расположена код реализации нужного эффекта (вывод MessageBox’а, запись единицы в регистр EAX и т.п.), и Вам хочется понять, каким образом программа передает управление на этот код и каким образом этого можно избежать или наоборот – получать такой результат при любых исходных данных. Для решения таких задач обычно используется обратная трассировка в уме. Идея обратной трассировки очень проста: мы начинаем читать листинг «задом наперед», то есть движемся от следствия (которое нам известно) к причине. По ходу дела отмечаем ключевые точки, к которым относятся:
• Вызовы функций Win32 API, а также других стандартных функций, какие сможет распознать Ваш дизассемблер или Вы сами.
• Обращения к глобальным переменным, которые чаще всего выглядят как чтение или запись данных по указанному явным образом адресу.
• Вызовы подпрограмм, непосредственно за которыми следует проверка некоего условия (внешне выглядят как связка команд CALL-CMP-Jxx).
Перечисленные три группы отличаются от всех прочих кодов тем, что их назначение сравнительно легко идентифицируется. Действительно, если Вы видите вызов API’шной функции чтения командной строки, для Вас будет очевидно, что следующие за вызовом команды почти наверняка будут оперировать именно с текстом командной строки, а не с фазами Луны или курсом доллара. И вот тому пример:

 call    GetCommandLineA
 mov     edi, eax
 cmp     byte ptr [edi], 22h
 jnz     short loc_401B08
 

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

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

Как Вы понимаете, использование глобальной переменной для хранения статуса программы в наше время – редкая и счастливая для крэкера случайность. Гораздо чаще разработчики, начитавшись руководств «как защитить свою программу от хакеров за 1 час», усваивают, что глобальную переменную использовать в качестве «переключателя» нехорошо. Вот функция, вызываемая к месту и не к месту – это совсем-совсем другое дело. Сказано-сделано, и в программе появляются многочисленные куски вида «if (!RegistaProggie()) ShowMessage (“Wanna getta munnee!”)» (если кто не понял текст сообщения, приблизительный перевод с нетрадиционного английского звучит как: «хочу бабки!»). А во что такие куски превращаются при ассемблировании? Правильно – в цепочку CALL-CMP-Jxx, о которой я говорил парой абзацев выше. Вот такие-то интересные кусочки мы и будем высматривать при обратной трассировке. Разумеется, одним лишь поиском регистрационных процедур дело не ограничивается – цепочки «вызов-проверка-ветвление» могут быть проверкой на корректность введенных данных (как-то раз мне пришлось поправить программу игры «Жизнь», которая могла, но отчего-то не хотела работать с полями больше, чем 100*100), и конструкцией SWITCH (она же CASE в Паскале), да и много чем еще. Причем, если у функции есть параметры, они могут стать отличной подсказкой, позволяющей установить назначение этой функции (особенно хорошо этот прием работает с функциями, выполняющими преобразование строк). Для этого нужно под отладчиком исследовать, что именно передается в функцию и какой результат она возвращает. Нередко это даже оказывается проще, чем догадаться, что хранится в глобальной переменной.

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

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

Трассируя программы, Вам придется немало поводить пальцем по распечатке или экрану дисплея, взбираясь по дереву вызовов и переходов. А если к тому же Ваш дизассемблер «не умеет» выделять цветом команды переходов, Вы можете запросто пропустить что-нибудь важное. В общем, блуждание по бескрайним полям кода – дело, требующее внимания, сосредоточенности, и при этом достаточно утомительное. Однако если Вы решаете типовую задачу «как заставить программу выполнить некий код независимо от правильности исходных данных», Вам может помочь все та же карта всех возможных путей исполнения кода, построение которой я демонстрировал в самом начале статьи. Как только Вы составите такую карту, Вам останется лишь выполнить три несложных действия:
1. Выбрать в программе и отметить на этой карте исходную точку. В качестве исходной точки лучше всего выбирать код, имеющий непосредственное отношение к исследуемой защите, который может быть легко идентифицирован и вызван из программы. Если речь идет о старых добрых серийных номерах, вводимых с клавиатуры, то лучше всего искать код чтения серийника из окна – этот код обычно легко «ловится» в отладчике и заведомо вызывается защитой (надо же ей как-то узнавать, с какими параметрами пользователь пытается зарегистрироваться).
2. Отметить конечную точку, путь к которой требуется найти (например, вызов MessageBoxA, сообщающий об успешной регистрации).
3. Найти на карте путь (а лучше – все возможные пути) из начальной точки в конечную.
Как только такой путь будет найден, можете начинать соответствующим образом патчить код и выяснять, что из этого получится. Интересно, что выполнять второй пункт можно не только вручную (это чем-то похоже на детскую головоломку «найди путь в лабиринте»), но и в автоматизированном режиме – в этом случае потребуется решить типовую задачу из курса теории графов. Теоретически это позволяет поставить взлом защит, использующих только переключатель «зарегистрировано/не зарегистрировано» даже не на поток – на конвейер!

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

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

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

Технический прогресс не стоял на месте: расставлять брейкпойнты и гонять по ним программу вручную было неудобно, и программистам не могла не прийти в голову мысль «а почему бы не усовершенствовать отладчик таки образом, чтобы собственно трассировка выполнялась в автоматическом режиме». Прообразом современных средств трассировки был режим «анимации» (то есть замедленного исполнения) программы – при некотором навыке в мелькании содержимого регистров и переменных можно было попытаться отловить полезную информацию и «притормозить» программу в нужный момент. Впрочем, пользы от этого режима в те времена было немного – изучать в режиме анимации сложные программы было неудобно, а анимировать программы, работающие с графикой - и вовсе невозможно (в те времена отладчики функционировали исключительно в текстовом режиме). Однако в некоторых современных отладчиках режим анимации все-таки сохранился, и, надо отметить, толку от него куда больше, чем во времена DOS. Значительно возросшее быстродействие современной техники превратило анимацию из слайд-шоу для особо терпеливых, сопровождавшегося лихорадочным мерцанием экрана, в весьма динамичное действо, наблюдение за которым доставляет лишь удовольствие. Некоторые люди считают, что исполнение кода в режиме анимации никакой практической пользы не приносит, но я придерживаюсь несколько иного мнения: в этом режиме хорошо заметны длинные циклы, а также циклы со счетчиком, особенно если счетчик расположен в одном из регистров. Начинающим, думаю, интересно будет «вживую» понаблюдать работу распаковщика исполняемых программ – лучше один раз увидеть работу несложной процедуры распаковки, чем сто раз прочитать «книжное» описание этого процесса. Для просмотра этого шоу лучше всего взять небольшую программу, сжатую UPX’ом, поскольку «навесная» процедура распаковки UPX – одна из самых простых, и даже если у Вас не получится полностью разобраться в ней при помощи отладчика/дизассемблера, все сложные моменты можно разъяснить по исходным текстам, находящимся в свободном доступе.

Читая эту главу, Вы могли заметить, что многие из описанных методов, несмотря на всю их внешнюю простоту, довольно неудобны для практического применения, поскольку требуют от пользователя отличной памяти и внимательности, а также способностей к рекурсивному «чтению» кода. Попробуйте «пробежаться» отладчиком по достаточно сложной процедуре, при этом строя траекторию исполнения кода в уме. Если у Вас получилось – значит, либо процедура оказалась не очень сложной, либо большинству людей остается лишь позавидовать Вашей памяти. Так или иначе, но программисты пришли к идее автоматической трассировки, то есть пошагового исполнения программы с одновременным «запоминанием» всех сделанных шагов. Основным препятствием на пути к осуществлению этой идеи долгое время был сравнительно небольшой объем оперативной памяти старых ЭВМ и недостаточная мощность процессора. Даже такой старый процессор, как 8086, способен был выполнять десятки и сотни тысяч команд в секунду – представьте, какой объем памяти потребовался бы, чтобы запомнить всего лишь последовательность адресов исполненных команд, не говоря уже о состоянии регистров. Кроме того, на одну команду, выполненную отлаживаемой программой в режиме трассировки, приходятся десятки и сотни команд, выполненных отладчиком – и отсюда возникает заметное падение производительности трассируемой программы. В общем, до некоторого времени реализация такого способа трассировки машинного кода была практически нереальна. Однако когда объем памяти компьютеров начал измеряться мегабайтами, воплощение этой идеи наконец стало возможным. Режим трассировки появился сначала в SoftIce, а затем и в OllyDebug, причем по возможностям трассировки и связанных с ней функций OllyDebug определенно превзошел все прочие известные автору отладчики.

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

 Address Thread  Command Registers and comments
     Flushing gathered information
 01006AEC        Main    xor     ebx, ebx        EBX=00000000
 01006AEE        Main    push    ebx     pModule = NULL
 01006AEF        Main    mov     edi, dword ptr ds:[<&KERNEL32.GetModuleHandleA>]        EDI=77E7AD86
 01006AF5        Main    jnz     short NOTEPAD.01006B16  EAX=01000000
 01006AF7        Main    mov     ecx, dword ptr ds:[eax+3C]
 01006AFC        Main    add     ecx, eax
 01006AFE        Main    cmp     dword ptr ds:[ecx], 4550        ECX=000000E8
 01006B01        Main    jnz     short NOTEPAD.01006B15  ECX=010000E8
 

Как распорядиться столь подробным отчетом – зависит только от Ваших целей и изобретательности. Я могу лишь подсказать самые общие направления поиска. Прежде всего, стоит проанализировать значения, находящиеся в первой колонке, то есть адреса команд и «наложить» список адресов выполненных команд на листинг дизассемблирования программы (для W32Dasm, который формирует этот листинг в виде текстового файла, технически осуществить это несложно). Такое слияние «мертвого» листинга с отчетом отладчика о работе «живого» кода способно значительно облегчить понимание того, в какой точке программа повела себя «не так», и как наставить ее на путь истинный. Также бывает удобным «спроецировать» конкретную траекторию исполнения кода на схему всех возможных путей исполнения кода (например, обвести часть стрелочек красным карандашом) – сочетая этот прием с патчингом кода и/или модификацией данных для принудительной активации тех или иных траекторий, можно последовательно отсеивать траектории, не ведущие к желаемой цели.

Здесь следует сделать небольшое отступление и рассказать об одном принципиальном преимуществе текстового представления дизассемблерных листингов перед всевозможными упакованными двоичными форматами. Текст – это один из самых старых и универсальных способов передачи информации, и для его обработки создано огромное количество всевозможных утилит. Инструменты работы с текстовыми файлами сами по себе очень стары, но, тем не менее, практически не устаревают, алгоритмы, в них использующиеся, «вылизаны» и доведены до совершенства поколениями программистов, и потому было бы неразумно отвергать столь огромный пласт программистской культуры. Конечно, использование узкоспециализированных двоичных форматов в дизассемблерах позволяет сэкономить дисковое пространство, ускорить обработку данных и хранить вместе с листингом различную дополнительную информацию, например, списки перекрестных ссылок, но за это приходится расплачиваться неоправданной интеграцией собственно дизассемблера и средства просмотра дизассемблированного текста. А просмотрщики дизассемблерных листингов, увы, обычно проектируются по остаточному принципу. Если рассматривать наиболее популярные в настоящее время W32Dasm и IDA Pro, то можно обнаружить не всегда удобную навигацию, ориентацию на исключительно текстовый режим работы (как я уже говорил, построение блок-схем в IDA Pro реализовано в виде программы сторонних разработчиков, не поддерживающей интерактивную работу с кодом), и некоторые другие недостатки. Авторы дизассемблеров, впрочем, вполне осознают недостатки двоичных форматов и полезность прямой работы с текстом, а потому предусмотрели возможность экспорта в текстовый формат и даже «обычного» текстового поиска в окне дизассемблера. В IDA Pro реализован даже более сложный вариант поиска с использованием регулярных выражений и специальный скриптовый язык, который по идее должен дать пользователю возможность самому добавить в дизассемблер недостающую функциональность. Однако хороший просмотрщик текстов, набор двоичных утилит *NIX’ового происхождения и навыки в программировании способны творить с «сырыми» листингами такие чудеса, какие традиционным дизассемблерам и не снились – от простого просмотра с подсветкой синтаксиса до форматирования «лесенкой» текстов на ассемблере. А поскольку наш отчет о трассировке как раз имеет текстовый формат и является, по сути, специфической формой дизассемблерного листинга, к нему вполне применимы все изложенные выше соображения о работе с текстом.

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

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

Глава 12. Патчить или не патчить?


Быть или не быть? Вот в чем вопрос.
У. Шекспир


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

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

1. Использование пространства между процедурами или массивами. Во многих компиляторах для повышения быстродействия по умолчанию включены опции выравнивания начала процедур и массивов. К примеру, если процедуры выровнены по границам 16-байтных областей, Вы можете обнаружить до 15 байт свободного места перед или после каждой процедуры. Найти такие промежутки несложно – они инициализированы последовательностью одинаковых байт, обычно с кодом 0 или 90h (этому коду соответствует команда nop), реже - 0ССh (команда int3) или 0FFh. В такую область вполне возможно вписать пару-тройку команд для загрузки данных в регистры или изменения значения переменной. Поскольку процедуры встречаются достаточно часто, передать управление на такой блок и затем вернуться обратно, скорее всего, получится при помощи двухбайтного варианта команды jmp, что сведет к минимуму расход памяти на переходы туда-обратно. Кроме того, при помощи все тех же двухбайтных переходов можно связать воедино несколько таких свободных кусков и таким образом увеличить количество команд. Недостатки этого метода: без ухищрений невозможно вписать в программу достаточно длинную последовательность команд; связывать вручную свободные куски довольно неудобно; нет никакой гарантии, что поблизости окажется достаточно длинный кусок свободной памяти; при включенной оптимизации по размеру компилятор может размещать процедуры вплотную, что сделает данный метод неприменимым. Кроме того, практически невозможен вызов функции, не импортируемой явным образом: Вам просто надоест вручную сооружать «обвязку» для вызова GetProcAddress, параллельно размазывая ее по щелям между процедурами.
2. Использование места, высвободившегося после «вывода из обращения» защитных процедур или ненужных тестовых строк. В процессе взлома в программах нередко обнаруживаются «лишние» защитные процедуры, которые крэкеру приходится просто отключать, а также всевозможные надписи, предлагающие обменять некоторую сумму денежных единиц на серийный номер или ключевой файл. При необходимости память, занимаемая этими процедурами и надписями, может быть использована для размещения небольших блоков кода. Возможен и другой вариант использования этого метода – исключить из программы какую-нибудь функцию, которая Вам заведомо не понадобится, а на занимаемое ей место поместить собственный код. Кроме того, есть смысл исследовать подфункции вызываемой функции: некоторые из них могут использоваться исключительно внутри удаляемой функции, и потому тоже могут послужить источником дополнительных байтов. Недостатки метода: не всегда возможно получить достаточный объем памяти; нужна крайняя аккуратность при переписывании команд, содержащих явное указание адреса в памяти. Метод в принципе неприменим, если Вы используете крэкинг «в мирных целях», то есть Ваши задачи предполагают не выламывание из программы защитных процедур, а только лишь усовершенствование программы за счет добавления в нее собственного кода.
3. Размещение кода в неиспользуемых областях в концах секций. Программисты на ассемблере под Win32 наверняка хорошо знакомы со следующим эффектом: при добавлении в программу новых строк ее размер до поры-до времени не меняется, а потом вдруг скачкообразно увеличивается на полкилобайта, а то и больше (конкретное число зависит от настроек линковщика). Причина этого эффекта в том, что код и данные в программах под Win32 (да и не только под Win32) расположены внутри исполняемого файла в секциях, и размеры этих секций должны быть кратны определенному (и довольно большому) числу байт. Нередки ситуации, когда реальный размер кода программы или ее инициализированных данных на несколько десятков, а то и сотен байт меньше, чем размер выделенной для этого кода секции, и кусок памяти в «хвосте» секции ничем полезным не занят. А если в программе есть «лишние» байты, то почему бы не использовать их для своих целей? Недостатки метода: объем неиспользованного пространства в конце секции совершенно непредсказуем и может колебаться от нуля до нескольких килобайт; следует быть крайне осторожным при записи данных в конец секции инициализированных данных, чтобы случайно не испортить эти самые данные: к примеру, свободное место и длинный массив байтов, инициализированный нулями, внешне выглядят совершенно одинаково.
4. Расширение существующих секций или создание новых. При таком подходе в принципе невозможно случайно разрушить полезные данные – весь Ваш код будет размещаться в областях, которых в непатченой программе даже не существовало. Более того, таким способом можно внедрить собственный код даже в упакованную программу, если распаковщик не содержит специальных средств для контроля целостности исходного файла (в этом случае лучше создать отдельную секцию и размещать свой код в ней). Размер внедряемого кода при использовании этого метода ограничен разве что здравым смыслом и объемом памяти ЭВМ. Основной недостаток заключается в том, что создание или расширение секций предполагает модификацию PE-заголовка и изменение размеров файла, из-за чего антивирусные программы могут не слишком благосклонно отнестись к таким переменам. Кроме того, поскольку размер файла изменяется за счет вставок в середину, возможны проблемы с созданием исполняемых файлов, выполняющих автоматический патчинг (такие файлы в просторечии называются «крэками»). Большинство утилит, создающих такие файлы, ограничивается побайтным сравнением с оригиналом и потому «не переваривают» вставку даже одного-единственного байта в середину программы. Кроме того, если Вам потребуется вызвать функцию API, которая отсутствует в таблице импорта, Вам придется либо править эту таблицу, либо получать адрес функции при помощи вызова GetProcAddress, что не слишком удобно. Этот недостаток в полной свойственен и трем предыдущим стратегиям патчинга, причем в еще большей мере – по причине ограниченного объема памяти, который эти стратегии позволяют получить. Кроме того, операции по расширению и вставке секций технически очень сложно выполнить в памяти над загруженной программой, поскольку это требует очень глубоких знаний системы и весьма значительных трудозатрат, совершенно не адекватных получаемому результату.
5. Подгрузка кода, размещенного во внешних модулях. Это наименее «жесткий» по отношению к модифицируемой программе метод внедрения кода. Идея метода заключается в том, чтобы вынести весь добавляемый в программу код во внешний модуль (если речь идет про Windows – то в DLL), а потом «попросить» программу загружать этот внешний модуль вместо одной из библиотек, например, исправив один-единственный байт в имени загружаемой DLL. «Подменная» DLL с нашим кодом вклинивается между программой и «настоящей» библиотекой, причем в функции инициализации DLL можно разместить что-нибудь полезное, например, код патчинга основного процесса. Более подробно методы внедрения модулей мы рассмотрим чуть позже, а пока отметим, что при использовании данного метода Вы получаете наиболее широкий выбор инструментария для написания внедряемого кода: от шестнадцатеричного редактора до самых современных RAD-средств (в отличие от предыдущих четырех стратегий, где Вашим основным инструментом будет ассемблер, встроенный внутрь редактора или отладчика). Главный недостаток заключается в необходимости держать этот самый внешний модуль рядом с программой, что не всегда удобно.

Рассказывая об использовании пространства, занимаемого «ненужными» процедурами, для размещения своего кода, я упомянул о проблеме, связанной с затиранием команд условных переходов, вызова подпрограмм и чтения содержимого фиксированных адресов. Изучая ассемблер, Вы наверняка заметили, что одним и те же мнемоникам «call» и «jmp» соответствует множество разных опкодов. Также Вы могли заметить, что в зависимости от опкода может варьироваться не только длина операнда, но и сам способ указания адреса, на который будет выполнен переход: адрес может указываться либо явным образом, либо в виде смещения относительно начала следующей команды. В современных версиях Windows исполняемые файлы всегда загружаются с адреса, указанного в заголовке исполняемого файла. Однако это правило распространяется только на исполняемые файлы, а вот DLL должны уметь загружаться с любого адреса (традиционно свойство программ или библиотек загружаться с любого адреса называется «перемещаемость»). Если учесть, что Windows – не единственная, а лишь одна из множества ОС с разными моделями памяти и механизмами загрузки приложений и библиотек, а задача обеспечения перемещаемости программных модулей возникает не так уж редко, реализация относительных переходов и вызовов «в железе» сильно облегчило жизнь разработчикам-первопроходцам.

Однако обходиться одной лишь относительной адресацией невозможно при всем желании – прежде, чем начинать отсчитывать смещения, необходимо явным образом указать расположение хотя бы одной реперной точки, от которой эти смещения будут отсчитываться. Да и с переменными удобнее работать традиционным способом, обращаясь к ним по абсолютным адресам. В общем, относительная адресация – отнюдь не панацея, гарантирующая перемещаемость любой программы, а всего лишь удобный инструмент.

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

Этот пересчет адресов можно рассматривать как акт патчинга со стороны операционной системы, осуществляемый во время загрузки. Теперь представьте, что произойдет, если Вы, ни о чем не подозревая, впишете вместо команды, обращающейся к какой-либо ячейке памяти, свой код. Загрузчику в общем-то без разницы, какие именно байты он исправляет – поэтому он заглянет в таблицу перемещаемости, выберет оттуда адреса ячеек, которые нужно исправить, а потом честно попытается откорректировать содержимое этих ячеек. Однако мы наполнили эти ячейки совершенно иным содержанием, и потому там лежит не адрес, который надо пересчитать, а нечто совершенно иное – написанные нами коды команд. Если алгоритмы работы загрузчика предполагают проверку корректности полученных при пересчете адресов, Вы можете получить сообщение об ошибке уже на этапе пересчета, если не предполагает – программа почти наверняка «рухнет» при попытке исполнить такой дважды исправленный кусок кода. Выходов – два: либо перетрясти таблицу перемещаемости и исключить из нее ненужные нам ссылки, либо при патчинге не затрагивать последовательности байт, похожие на абсолютные адреса в памяти, если таковые встретятся на Вашем пути. Надо отметить, что практически все программные «полуфабрикаты» - библиотеки, объектные файлы, предварительно откомпилированные модули Delphi (то есть файлы с расширением .dcu), компоненты – изначально рассчитаны на внедрение в неизвестное место будущей программы и потому являются перемещаемыми.

Другим интересным вопросом, непосредственно связанным с патчингом, является вызов функций API из внедряемого в программу кода. Как я уже упоминал в девятой главе, при статической загрузке библиотек (то есть когда библиотеки подгружаются уже в процессе загрузки программы на основе информации из таблицы импорта) вызов внешних функций осуществляется не прямым переходом по нужному адресу, а через «переходники», которые и передают управление на библиотечные функции. Понятное дело, что если такие «переходники» есть, то ими можно воспользоваться. Но как быть, если таких «переходников» нет (хотя заведомо известно, что нужная функция все-таки вызывается) или использовать нельзя их по каким-либо причинам? Есть как минимум два пути, позволяющих вызвать функции API даже в таких стесненных условиях. Обычный программистский подход к решению проблемы – вызвать функцию LoadLibrary , а вслед за ней – GetProcAddress и получить адрес искомой функции, после чего можно смело помещать параметры на стек и затем выполнить call [eax]. Для этого, разумеется, нужно, чтобы программа импортировала функции LoadLibrary и GetProcAddress, но тут обычно проблем не возникает: подавляющее большинство программ эти функции импортирует, а те, которые не импортируют, можно заставить это делать редактированием таблицы импорта.

Однако я предпочитаю использовать другой способ, как мне кажется, более удобный и безопасный. Основа метода заключается в том, чтобы вызывать функции не напрямую, а «одалживать» нужные вызовы у исследуемой программы. Если программа импортирует функцию MyFunc статически, это почти наверняка означает, что в коде программы есть команда call MyFunc. А если в программе есть команда call MyFunc, значит, когда мы хотим вызвать MyFunc, нам нужно всего лишь прочитать четыре байта (которые представляют собой адрес нашей функции) в регистр eax, поместить на стек параметры и выполнить вызов. На практике эта операция выглядит примерно так:

 FAddr:	call ExitProcess	; Нужная нам функция
 …
 
 OurPatch:
 mov eax, dword ptr [FAddr+2]	; Читаем из памяти адрес функции
 push 0	; Помещаем на стек параметр вызова
 call [eax]	; Вызываем «одалживаемую» функцию
 

Однако вызовами статически импортированных функций API возможности метода «функций взаймы» отнюдь не ограничиваются. «Заимствовать» из исследуемой программы можно совершенно любые функции – от элементарного сравнения строк до функций извлечения файлов из архива неизвестного формата или генерации серийных номеров (изредка встречаются программы с такой ошибкой в защите). Единственное, что для этого необходимо – выяснить тип передаваемых функции параметров и используемое соглашение вызова. Однако нужно понимать, что заимствование функций из программы или из несистемных DLL не вполне известного назначения – операция более опасная, чем использование импортированных функций документированных API. Дело в том, что работа функций API не привязана ни к каким переменным и функциям внутри программы (если, конечно, не считать callback-функции), а вот к функциям программы это в общем случае не относится. Их работа может зависеть от состояния локальных или глобальных переменных, а также других объектов, созданных в процессе работы программы; более того – сами программные вызовы тоже вполне могут изменять состояние переменных, критически важных для работы программы. Поэтому если какой-либо объект в момент вызова окажется в «неправильном» состоянии, результат вызова будет совершенно непредсказуем. Точно таким же образом никто не может гарантировать, что «несвоевременный» вызов не нарушит функционирование всей программы. Хотя, с другой стороны, встроенные процедуры, как правило, достаточно корректно обращаются с переменными и объектами, созданными программой. Так что если Вам вдруг понадобится форсировать загрузку какого-либо плагина, лучше попытаться это сделать это «родными» для программы средствами, и лишь если это у Вас не получится, прибегнуть к средствам API. В любом случае, при заимствовании из основной программы функций в качестве постоянного решения следует быть очень осторожным, хотя сам по себе этот метод нередко бывает полезен.

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

Как Вы можете видеть, приведенный список возможных целей покрывает весьма широкую область – от предотвращения нежелательных вызовов до наблюдения за вызовами функций (частным случаем этого являются API-шпионы). Собственно, некоторые техники, используемые в API-шпионах, и основаны на патчинге «переходников» к функциям API, осуществляемом сразу после загрузки программного модуля. Принцип действия большинства API-шпионов следующий:
1. Загрузить процесс в память, не запуская его (например, создав процесс с флагом CREATE_SUSPENDED, если речь идет о Win32).
2. Внедрить «шпионский» модуль в адресное пространство исследуемого процесса, например при помощи хуков или функции CreateRemoteThread (только под Windwos линейки NT).
3. Пропатчить в памяти процесса «переходники» к функциям API таким образом, чтобы они указывали на обработчики соответствующих функций внутри «шпионского» модуля.
4. Аналогичным образом перебросить переходник к функции GetProcAddress на собственный обработчик. Эта операция очень важна, поскольку только таким образом можно перехватить вызовы функций, адреса которых программа динамически запрашивает в процессе работы.
5. «Разморозить» процесс, после чего программа начнет исполняться.

Однако патчинг и перехват системных вызовов – отнюдь не прерогатива одних лишь крэкеров. Разработчики защит, хотя и с изрядным опозданием, тоже взяли на вооружение идею перенаправления системных вызовов на собственный код. Как я уже упоминал, сравнительно недавно в большом количестве появились навесные защиты, которые позволяют упрятывать любую программу и все необходимые для ее работы файлы в упакованном и зашифрованном виде внутрь одного-единственного EXE. При этом сама программа может обращаться к своим файлам как средствами Win32 API, так и при помощи высокоуровневых функций (которые по сути являются «обертками» для все тех же системных вызовов). О том, каким образом работают такие защиты, Вы уже наверняка догадались: в исполняемый файл дописывается секция, где хранится код, обрабатывающий вызовы системных функций для работы с файлами. Необходимые для работы программы файлы упаковываются и цепляются в «хвост» программы, а таблица импорта дорабатывается таким образом, чтобы вызовы функций работы с файлами перенаправлялись на обработчики, находящиеся во внедренной секции. Когда защищенная программа попытается обратиться к файлу, задача обработчиков заключается в том, чтобы проверить, к какому именно файлу происходит обращение, и либо передать это обращение операционной системе в неизменном виде (если программа обращается к файлу вне «хранилища»), либо имитировать работу системного вызова, но в действительности считывание данных осуществлять из упакованного «хранилища». Однако ирония судьбы заключается в том, что метод взлома полностью аналогичен методу защиты. Действительно, если разработчик «отклонил» вызовы функций работы с файлами на собственный код, то и почему бы и крэкеру не проделать ту же самую операцию? То есть вклинить между программой и обработчиком свой собственный «обработчик обработчика», который будет сбрасывать все «спрятанные» файлы в надежное место. Основную проблему составляет поиск входных и выходных точек этих обработчиков, но здесь могут помочь «особые приметы», которые способны выдать чужеродный код:
1. Вклинить «левый» код в середину практически невозможно, поэтому остаются варианты с расширением секций (тогда нужный код окажется перед началом или в самом конце программы), созданием новой секции (что иногда заметно по «странным» адресам, заметно отличающимся от адресов основной программы) или динамическим выделением куска памяти и размещением там обработчика (самый сложный для реализации способ – а потому самый маловероятный).
2. Если защищенная программа читает упакованные файлы «по требованию», каждый вызов функций чтения файлов внутри программы будет сопровождаться обращениями программы к «хвосту» своего EXE-файла. Если программа читает все упакованные файлы в память сразу, такое обращение будет выполнено во время запуска программы. В обеих случаях эти вызовы вполне «уязвимы» для точек останова.
3. Наиболее уязвимы те вызовы, которые передаются в систему в неизменном виде (т.е. обращения к файлам, не находящимся внутри упакованного «хранилища») – установка брейкпойнтов на функции работы с файлами позволяет найти их без особого труда, после чего можно добраться и до точек входа и выхода в защитные процедуры-обработчики системных вызовов.
4. В памяти почти наверняка будут застревать куски спрятанных файлов, и если Вы знаете, какая информация в этих файлах может оказаться, Вы можете попытаться найти эти куски памяти и попытаться выяснить, каким образом эти куски там появляются (например, при помощи поиска ссылок на начала таких кусков или установкой брейкпойнтов на запись в память).

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

Я уже говорил, из любой безвыходной ситуации существует как минимум два выхода. Этот афоризм верен и в нашем случае, однако от Вас все же потребуются определенные навыки в программировании. Итак, наша задача – вклинить свой код между системной DLL и программой, которая ее вызывает. Одна из первых идей, которые приходят в голову, заключается в том, чтобы вместо «родной» DLL подсунуть свою собственную, которая содержала бы функции с такими же именами, что и «настоящая». Эта библиотека должна помимо вызова «родных» функций из оригинальной библиотеки выполнять еще и те операции, которые Вы в нее заложите. А уж в собственной DLL Вы вольны запрограммировать все, что угодно – от сбора статистики вызовов до анализа и подмены параметров функций. Такой подход к перехвату вызовов из DLL, основанный на подмене оригинальных библиотек, называется DLL wrapping’ом. Чисто технически создание подменной DLL выполняется следующим образом:
1. Необходимо получить список всех функций (в том числе и тех, которые не имеют имен, а только ординалы), экспортируемых той DLL, которую Вы планируете подменить. Если Вы уверены, что программа не импортирует функции этой DLL при помощи GetProcAddress или каким-либо более изощренным способом, Вы можете обойтись и списком функций, импортируемых подопытной программой из DLL. Получить такой список можно, к примеру, при помощи утилиты DUMPBIN.
2. Изготовить на основе этого списка болванку будущей подменной DLL. В простейшем случае эта болванка будет выглядеть как набор одинаковых кусков. Поскольку болванка имеет регулярную структуру, ее можно сгенерировать автоматически. Для простейшего случая, когда нужно перехватить одну лишь функцию MessageBoxA из user32.dll, наша подменная библиотека, написанная на MASM, будет выглядеть примерно так:

 PUBLIC MessageBoxA
 
 .data:
 IsLoaded dd FALSE
 aMessageBoxA dd 0
 huser32	dd 0
 u32 db "e:\Windows\System32\user32.dll",0
 nMessageBoxA db "MessageBoxA",0
 
 .code:
 
 DllEntry proc hInstance:HINSTANCE, reason:DWORD, reserved1:DWORD
         mov  eax,TRUE
         ret
 DllEntry Endp 
 
 CheckAndImport proc
 	
 	.if !IsLoaded
 		mov IsLoaded,TRUE
 		invoke LoadLibrary,ADDR u32
 		mov huser32,eax
 		invoke GetProcAddress,huser32,ADDR nMessageBoxA
 		mov aMessageBoxA,eax
 	.endif
 	ret
 
 CheckAndImport endp
 
 MessageBoxA::
 ; Адрес метки MessageBoxA и прочих аналогичных нужно включить в таблицу экспорта
 ; при помощи директив
 ; LIBRARY user32
 ; EXPORTS MessageBoxA,
 ; помещенных в def-файл
 
 	invoke CheckAndImport
 	
 ; Здесь Вы можете разместить код, исполняемый перед вызовом перехватываемой функции
 	; Например, поменять местами заголовок и текст сообщения,
 ; как это сделано ниже
 mov eax,[esp+8]
 xchg [esp+12],eax
 xchg [esp+8],eax
 
 	mov eax, MessageBoxA_
 	jmp eax
 

Процедура CheckAndImport выполняет важную функцию - во время первого вызова любой из функций она подгружает настоящую user32.dll, получает адрес функции MessageBoxA и помещает этот адрес в переменную аMessageBoxA. Если Вам известно, какая из функций DLL будет вызвана первой, можно сократить код, разместив вызов функции CheckAndImport только в этой функции. Вообще, внешне этот код выглядит довольно тяжеловесно – экспорт меток, специфический способ обращения к параметрам вызова через смещение относительно значения ESP, динамическая загрузка библиотек – и тут же отсутствие возможности выполнить собственный код после вызова MessageBox. Причина тяжеловесности проста: этот код представляет собой максимально упрощенную адаптацию реально использовавшейся мной подменной DLL. Что же касается мнимой невозможности выполнить собственный код после вызова MessageBoxA, то эту проблему проще всего решить при помощи подмены лежащего на стеке адреса возврата: нужно подправить его таким образом, чтобы после выполнения команды jmp <имя_функции> возврат выполнялся не в программу, а на следующую за jmp команду. Разумеется, старый адрес возврата тоже надо где-то сохранять - он Вам понадобится, чтобы вернуть управление программе. Если Вам известно количество параметров, помещаемых на стек при вызове, и их размерность, никаких сложностей с подменой адреса возврата не возникнет. Но вот если в процедуру передается неизвестное заранее количество параметров (такое, в частности, возможно в программах, написанных на C), общего решения этой проблемы не существует, так что Вам придется действовать по обстоятельствам и изобретать метод определения количества параметров на стеке самостоятельно.

Но почему я вдруг отклонился от темы этой главы и уделил столько времени технике подмены DLL? Причина в том, что DLL wrapping нередко используется совместно с патчингом исполняемого файла. Внимательно посмотрев на текст подменной DLL, Вы увидите, что загрузка user32.dll производится с явным указанием расположения этой библиотеки. Однако даже если скомпилировать эту DLL с именем «user32.dll» и положить рядом с подопытной программой, единственным результатом которой является вывод MessageBox’а с неким сообщением, Вы все равно не добьетесь желаемого результата – программа вызовет эту функцию прямиком из системной библиотеки, проигнорировав Вашу приманку. Что же делать?

Самый простой и доступный выход заключается в том, чтобы переименовать нашу подменную библиотеку в user33.dll, а потом забраться в исполняемый файл шестнадцатеричным редактором и там поменять имя импортируемой библиотеки аналогичным же образом. Теперь наша программа будет вместо стандартной библиотеки Windows подгружать нашу user33.dll и вызывать MessageBoxA именно из нее. Теоретически можно было поступить и несколько иначе – переименовать системную библиотеку user32.dll в user33.dll, а на место user32.dll положить нашу подменную библиотеку (разумеется, исправив путь к «настоящей» динамически загружаемой библиотеке), но на практике проделывать фокусы с заменой системных библиотек крайне нежелательно. Хотя если речь идет не о системной библиотеке, то действительно можно обойтись одним лишь переименованием «настоящей» DLL и помещением на ее место «поддельной».

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

Модификация кода программы в памяти может выполняться двумя путями: записью данных в адресное пространство процесса извне или же внедрением в адресное пространство процесса собственного кода, который уже будет работать внутри подопытного процесса и выполнять необходимые действия по патчингу. В подавляющем большинстве случаев вторжение в чужое адресное пространство извне осуществляется при помощи последовательности вызовов VirtualProtectEx-WriteProcessMemory, хотя возможны и более сложные варианты с выходом в нулевое кольцо (правда, после того, как линейка Windows 9x начала утрачивать актуальность, простые в осуществлении способы выхода в Ring0 остались не у дел). Для того, чтобы добраться до процесса, Вам почти наверняка потребуется его дескриптор (он же «хэндл» - «handle»). Если запуск программы и патчинг ее процесса выполняется одной и той же программой, особых сложностей не возникает: функция CreateProcess[Ex] возвращает хэндл порождаемого ей процесса. Но это не единственный подход к добыче желанного дескриптора – до него также можно добраться через получение идентификатора процесса, породившего окно (при помощи функции GetWindowThreadProcessId) либо через анализ «снимка» всех процессов, полученных при помощи функций CreateToolhelp32Snapshot, Process32First и Process32Next.

Правка кода путем предварительного внедрения в адресное пространство подопытного процесса применяется значительно реже, однако эти тоже знания могут Вам понадобиться. Наиболее распространенными способами попасть в чужое адресное пространство являются: использование хуков, запуск программы в режиме отладки и создание потока при помощи функции CreateRemoteThread (этот прием применим только под Windows линейки NT). Однако ничто не ограничивает Ваше творчество – Вы вольны комбинировать различные приемы для достижения наилучшего эффекта, например, пропатчить программу в районе точки входа так, чтобы происходила загрузка некой DLL и вызов функции, которая восстановит испорченные патчингом байты, создаст дополнительный поток и затем вернет управление основной программе.

Выше мы рассматривали всевозможные приемы, позволяющие, образно говоря, залезать в чужое адресное пространство через окно. Но иногда разработчики программ предоставляют бесценную возможность попасть внутрь их детища с «парадного входа», не таясь. И возможность эта - «родимое пятно» практически всех программ, использующих широко распространенный механизм расширения возможностей программы за счет плагинов. Плагин обычно представляет собой самую обычную DLL, построенную в соответствии с теми или иными стандартами, задаваемыми разработчиком программы. Однако что именно делает плагин, программу обычно не волнует – он может как выполнять штатные функции по обработке изображения, звука или чего-нибудь еще, так и быть «троянским конем», созданным с одной лишь целью – в момент первого же обращения к нему исправить внутри программы-«хозяина» несколько байт. Поскольку Ваш плагин будет «жить» внутри подопытной программы, для записи данных в адресное пространство Вам не нужны будут никакие ухищрения, достаточно всего лишь поместить правильные значения по известным адресам при помощи самой обычной команды mov. Однако как добиться, чтобы Ваш код выполнился как можно быстрее? Точные сведения могут быть получены лишь через эксперименты, но приблизительное направление поиска следующее: многие программы после загрузки плагина первым делом «спрашивают» свежезагруженный модуль о его названии, назначении, поддерживаемых функциях и прочих подобных параметрах. А поскольку поиск и загрузка плагинов нередко выполняется сразу после старта программы (а если очень повезет – и до срабатывания защитных механизмов или параллельно с их работой), у Вас есть хорошие шансы пропатчить программу уже на первых стадиях ее работы.

Общим требованием при патчинге программы в памяти является полная «неподвижность» программы, чего проще всего достигнуть, отправив все потоки модифицируемого процесса в состояние «замороженности» вызовом функции SuspendThread или при помощи старого фокуса «MySelf: jmp MySelf». Связано это с тем, что модификация кода с точки зрения программы должна производиться одномоментно, чтобы во время патчинга программа случайно не попыталась выполнить не до конца модифицированный код (результатом чего почти наверняка будет сбой). Из этого правила есть важное следствие: если патчинг выполняется одной ассемблерной командой (а одной командой можно переписать один, два, четыре, а при большом желании – 8 или 10 байт), то такая модификация будет одномоментной и потому вполне допустимой. К примеру, «волшебная» комбинация MySelf: jmp MySelf (в шестнадцатеричном виде этот код выглядит как EB FE) как паз является двухбайтной. Однако если Вы выполняете патчинг чужого процесса при помощи функций WinAPI, Вы не можете наверняка знать, блоками какого размера будет осуществляться запись в чужое адресное пространство, а потому пытаться переписывать более одного байта без «заморозки» процесса не стоит.

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

Самым оригинальным подходом к проблеме отличился, пожалуй, довольно старый патчер процессов, выпущенный T3X: все обязанности по отслеживанию загрузки эта утилитка возлагала на конечного пользователя. Созданный при помощи этой утилиты launcher просто запускал нужную программу, а затем выводил MessageBox с сообщением вроде «Нажми ОК, когда программа загрузится». От пользователя, соответственно, требовалось кликнуть по кнопке ОК, когда он сочтет, что подопытная программа уже загрузилась и распаковалась. Несмотря на откровенный примитивизм, этот патчер вполне соответствовал духу популярных защит того времени и позволял решать насущные задачи.

Естественно, следующим шагом в определении наилучшего момента для патчинга стала привязка к появлению на экране какого-либо окна, создаваемого модифицируемой программой. Поскольку большинство Windows-приложений являются оконными и при запуске создают окно, в котором предстоит работать пользователю, появление такого окна может служить недвусмысленным указанием на то, что распаковка программы уже завершилась и потому можно приступать к патчингу. Одним из первых такой патчер выпустил yoda, причем его патчер позволял не только обнаруживать появление окна, но и принудительно его закрывать (что было полезным для борьбы с nag screen’ами). Однако этот инструмент имел ряд недостатков, в частности – требовал указания полного заголовка окна, что не позволяло работать с окнами с изменяющимся заголовком. А поскольку окна со счетчиками оставшихся дней «испытательного срока» встречались в программах все чаще и чаще, такая ситуация сподвигла меня на написание собственного инструмента – интерпретатора скриптов с механизмом поиска окон по содержащейся в заголовке окна подстроке, возможностью имитировать нажатие кнопок в окне, активировать отключенные управляющие элементы и т.п. Однако и этот инструмент не во всех случаях способен решить проблему своевременного патчинга: защита может быть устроена таким образом, что в момент появления окна будет уже поздно что-либо предпринимать. Кроме того, нельзя сбрасывать со счетов безоконные программы, которые хоть и редко, но все же встречаются в мире Win32-приложений. И тогда приходится применять старый, весьма расточительный с точки зрения расхода ресурсов процессора, но проверенный временем и практически безотказный прием.

Традиционно проблема определения момента распаковки программы решается следующим образом: подопытная программа запускается при помощи launcher’а с флагом CREATE_SUSPENDED, этому процессу и его главному потоку назначается наименьший возможный приоритет, после чего главный поток «размораживается» функцией ResumeThread. Программа начинает распаковываться, но делает это очень медленно: во-первых, по причине минимального приоритета, а, во-вторых, потому что launcher в это время постоянно считывает содержимое байтов, которые предполагается пропатчить и сравнивает их с эталонными значениями, заранее добытыми из «живой» программы при помощи дампера или отладчика вроде SoftIce. Цель этих действий заключается в следующем: исправление необходимо провести как можно раньше после того, как подвергаемые патчингу участки будут распакованы. Как только содержимое нужных ячеек памяти совпадет с эталонами, программу можно считать готовой к внесению модификаций. В этот момент патчер стопорит исполнение программы и модифицирует код, а затем возвращает в исходное состояние приоритеты, размораживает все потоки и позволить программе выполняться дальше как ни в чем не бывало.

Блок, выполняющий постоянное сканирование, желательно оптимизировать по скорости настолько, насколько это возможно: если наш launcher не успеет внести в программу изменения до того, как управление будет передано на изменяемые участки, всю операция по патчингу можно считать проваленной. Именно для того, чтобы снизить вероятность «слишком быстрого» запуска программы и увеличить промежуток времени, пригодный для патчинга, мы и понижаем приоритет программы, отбирая у нее кванты процессорного времени в пользу нашего launcher’а и других программ. Из всего вышесказанного следует интересный практический вывод, который, к сожалению, не учитывается в большинстве существующих патчеров процессов: если патч предполагается использовать на мультипроцессорной системе, Ваш launcher и подопытную программу (или хотя бы ее главный поток) лучше исполнять на одном и том же процессоре. Добиться этого под Windows NT можно при помощи функций SetProcessAffinityMask, SetThreadAffinityMask и GetProcessAffinityMask.

Дочитав предыдущий абзац, Вы могли задаться вопросом, насколько вообще эффективно использование API’шных функций для чтения-записи в адресное пространство чужого процесса для наших целей и нельзя ли воспользоваться каким-либо более «прямым» методом работы в чужом адресном пространстве. Такие вопросы вполне уместны: операции по «залезанию» в чужое адресное пространство вообще требуют значительных накладных расходов, из-за чего функция ReadProcessMemory действительно работает не слишком быстро (хотя обычно и такой скорости бывает более чем достаточно).

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

Глава 13. Заключение.


Не было начала – не было конца…
Е. Летов


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

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

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

Очевидно, что разрыв между желаниями широкой пользовательской общественности и возможностями крэкеров будет тем шире, чем больше программного обеспечения будут выпускать разработчики (собственно, единственный смысл установки защиты в наше время не в том, чтобы «не смогли сломать», а чтобы не ломали все, кому не лень, т.к. «любая программа, которую можно запустить, может быть взломана»). Каким образом можно переломить эту тенденцию? На ближайшую перспективу выход видится в том, чтобы дать квалифицированному пользователю возможность решить хотя часть его проблем самостоятельно. То, что пользователь, вооруженный подходящим программным обеспечением, способен сделать некоторые вещи, которые ранее считалось исключительно прерогативой «ребят с отладчиками», показала программа GameWizard и ее многочисленные клоны, предназначенные для «взлома» игр. Хотя с высот «чистого искусства» такое достижение кажется совершенно несерьезным, не стоит забывать, что совсем недавно проблема бесконечной жизни решалась лишь двумя способами – с помощью «знакомого хакера» или никак. Поэтому не стоит запираться в «башне из слоновой кости», оставляя пользователей наедине с их бедами, чтобы в итоге оказаться на обочине прогресса, но есть смысл заняться созданием инструментов, которые могут реально помочь широкому кругу пользователей (тем более, что в этом кругу рано или поздно оказывается и сам крэкер).

Многообещающим направлением видится создание программ, позволяющих пользователю самостоятельно устранять проблемы, связанные с ограничениями в программах. Проще говоря, обходить всевозможные триальные механизмы, причем по возможности – в полностью автоматическом режиме. «Первыми ласточками» в этой области были многочисленные утилиты, менявшие системное время перед запуском программы и через некоторое время возвращавшие его обратно. По вполне понятным причинам такой подход не всегда приемлем, но до недавнего времени ничего другого не предлагалось. В теории одним из путей, позволяющих «двигать» время внутри одной программы, не затрагивая все остальные, является перехват функций Windows API, так или иначе возвращающих локальное или системное время, и модификация возвращаемых значений, возвращаемых этими функциями. Однако путь от теории к практике оказался неблизким: долгое время эта идея не получала практической реализации, по крайней мере, готовые программные продукты, пригодные для использования рядовым пользователем, автору не встречались. И лишь сравнительно недавно появилась программа Hall of the Mountain King, которая позволяет не только менять значение «внутренних часов» для любого процесса, но и «закольцовывать» течение времени внутри программ.

Весьма многообещающе выглядят и инструменты, которые позволяют скрывать и защищать те или иные ключи реестра от доступа из определенных процессов, используя технику перехвата вызовов WinAPI. В настоящее время автору не известны программы такого рода, которые могли бы использоваться для борьбы с триальными ограничениями, но сама по себе эта идея выглядит заманчиво: запретив, к примеру, чтение из ключей HKEY_LOCAL_MACHINE\Software\Classes\CLSID и HKEY_CURRENT_USER\Software\ASProtect для программ, защищенных ASProtect, можно добиться того, что защита «не увидит» собственные триальные метки и будет считать каждый запуск первым.

Если довести идею «учета и контроля», на которой основаны две описанных выше технологии, до логического завершения, мы в итоге придем к мысли о создании «firewall’а для системных вызовов», то есть программы, надзирающей за обращениями к функциям WinAPI, ведущей журнал этих вызовов и блокирующей либо корректирующей «неправильные» обращения к системе. Максимальную гибкость такого инструмента можно было бы обеспечить встраиванием быстрого скрипт-интерпретатора, при помощи которого пользователь мог бы программировать логику работы с системными вызовами. Возможности, предоставляемые такой программой, совмещающей в себе возможности API-шпиона и DLL-враппера, было бы трудно переоценить. К примеру, вместо того, чтобы долго выламывать nag screen из строптивой программы (почти наверняка столкнувшись при этом с необходимостью ее распаковки), можно было бы просто запустить программу в режиме снятия лога системных вызовов, чтобы понять, каким образом формируется и отображается nag screen. После чего, основываясь на полученной информации, выяснить в какой точке программы производится вывод nag screen’а и написать скрипт, который бы отказывал этому вызову в праве на создание окна, но при этом «пропускал» все остальные. Подобным же образом можно было бы неограниченно продлять триальные сроки, «доработав» функции работы с реестром так, чтобы в действительности запись в некоторые ключи реестра не производилась, но при этом подопытная программа пребывала в полной уверенности, что триальные метки успешно расставлены. Нужно отметить, что такая программа будет уже не орудием исключительно крэкинга, но инструментом двойного назначения, который может быть использован для обнаружения утечек ресурсов или как средство автоматизации.

Другой подход к решению проблемы триалов, окончательно оформившийся сравнительно недавно, заключается в автоматизации поиска и удаления «меток», по которым программы определяют количество запусков или промежуток времени, оставшийся до истечения срока пробного использования. О технологиях выявления таких меток мы уже говорили в третьей главе, однако там делался упор на «ручную работу» по поиску лишних ключей в реестре и аккуратное с ними обращение, чего весьма трудно добиться при работе с большим количеством защищенных программ. Такой подход неприемлем для рядового пользователя, которого совсем не прельщает необходимость перед каждым запуском делать снимки реестра и потом долго их сравнивать, размышляя о том, за какое из изменений в реестре ответственна защита. Современный стиль жизни таков, что любая проблема должна решаться «в два клика», без многочасовых медитаций над снимками реестра и отчетами RegMon’а. Но есть ли пути к достижению этого идеала? Разумеется, есть! Широкое распространение навесных защит, реализующих триальные ограничения, привело к тому, что способы нанесения триальных меток тоже «унифицировались». Каждый тип навесных защит имеет свои собственные «предпочтения» в том, где эти метки создавать и каким образом их генерировать. Следовательно, если собрать достаточно большую коллекцию программ, защищенных одним и тем же инструментом, и как следует погонять их во всех возможных режимах, можно собрать некоторую статистику. К примеру, ASProtect традиционно мусорит в ветке реестра HKEY_LOCAL_MACHINE\Software\Classes\CLSID, причем старые версии этой защиты создавали ключи с одним-единственным текстовым значением длиной 4, 8 или 16 байт; главная надежда разработчика, видимо, заключалась в том, что огромное количество ключей в этой ветке помешает найти среди них «лишние». Как показала практика, это мнение было глубоко ошибочным и более новые версии этой же защиты пытаются имитировать полезные ключи гораздо более тщательно, что, впрочем, тоже не слишком хорошо помогает, потому что исследователи защит тоже не сидят сложа руки.

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

Основываясь на опыте разработки одной из таких программ-очистителей реестра, я сделал вывод о том, что для инструментов подобного рода важно соблюдение принципа максимального разделения интерфейсной части и функциональной. Именно «монолитность» Die, ASProtect, Die! и привела к тому, что после некоторого момента эту программу было бы проще переписать «с нуля», чем поддерживать, основываясь на исходной идеологии хранения нескольких пользовательских интерфейсов и алгоритмов поиска в одном исполняемом файле. Изложение проекта «идеального очистичеля реестра» слишком далеко увело бы нас от основной темы, поэтому я ограничусь лишь несколькими тезисами:
Разделение пользовательского интерфейса и функциональной части программы. Интерфейсная часть должна представлять пользователю максимально удобные средства для выбора алгоритмов поиска ключей, ввода параметров поиска, просмотра результатов работы функциональной части, удаления найденных триальных меток и т.п. Функциональная часть должна лишь принимать от интерфейсной части введенные пользователем параметры, выполнять поиск следов защиты и возвращать интерфейсной части список подозрительных объектов, судьбу которых предстоит решать пользователю.
Реализация функциональной части программы в максимально «легком» (т.е. небольшом по размеру и удобном для модификации) виде. Мир программных защит весьма динамичен, и не стоит надеяться, что единожды «освоив» какую-либо защитную схему, можно до скончания века пользоваться плодами своих усилий. В еще большей мере это относится к созданию программ, ориентированных на полную автоматизацию обхода защит: если действия крэкеров для разработчика есть нечто неприятное, но неизбежное, то самое существование инструментов, дающих рядовому пользователю власть над триальными ограничениями, воспринимается как покушение на основы мироздания. Когда разработчик защит видит, что его изделие в достаточной мере «освоено», ему не остается ничего иного, как выпустить новую версию своего продукта, неуязвимую для существующего антитриального софта. Следовательно, Ваши программы должны быть максимально легки для обновления и расширения, чего можно достичь через использование плагинов или интерпретатора сценариев. Преимущество плагинов, написанных на компилируемых языках, в скорости их работы; сила скриптов – в легкости модификации (в качестве тестера и соавтора скрипта может выступить даже квалифицированный пользователь) и минимальных размерах. С другой стороны, плагины в виде двоичных исполняемых модулей неудобны для доработки и распространения, а интерпретируемые скрипты проигрывают плагинам в быстродействии и к тому же весьма уязвимы для обратного инжениринга (было бы крайне наивным полагать, что разработчики защит не анализируют инструменты «другой стороны», даже если в лицензионным соглашении такие действия явным образом запрещены).
Использование максимально унифицированных интерфейсов для обмена данными между частями программы. Весьма вероятно, что программные модули, ориентированные на удаление меток разных типов защит, будут иметь и разные наборы пользовательских настроек, однако следует стремиться к созданию единого для всех типов защит способа передачи пользовательских настроек в программные модули, которые непосредственно будут искать «лишние» ключи. В любом случае, следует избегать включения в функциональную часть средств ведения диалога с пользователем: если планируется развитие и расширение программы, такой «симбиоз», скорее всего, станет препятствием, ограничивающим гибкость программы.

Однако «пользовательским» уровнем грядущие возможности отнюдь не исчерпываются. Уже сейчас можно видеть, как возвращаются в новом обличье старые, но до времени не актуальные идеи. Около десяти лет назад была весьма популярна концепция эмулирующих отладчиков, которые практически полностью имитировали работу центрального процессора (исключение делалось разве что для команд вызова прерываний) и позволяли исполнить программу так, как она исполнялась бы на настоящем процессоре, но при этом иметь абсолютный контроль над состоянием отлаживаемой программы. Следы работы такого отладчика было практически невозможно обнаружить, не прибегая к ухищрениям; многие приемы, направленные на нарушение стабильности отладчика, также оставались не у дел. Однако с наступлением эры многозадачных ОС этот род отладчиков как-то тихо канул в Лету. Долгие годы эмуляция была не в почете, однако некоторые признаки указывают на то, что в ближайшем будущем возможен ренессанс эмулирующих отладчиков. Эмуляция длинных кусков кода, не содержащего обращений к WinAPI, освоена довольно давно: одних только плагинов для Interactive Disassembler, позволяющих имитировать исполнение кода, насчитывается уже несколько штук. Главная проблема, ожидающая своего решения – организация взаимодействия между эмулятором и вызовами «неудобных» функций системного API внутри эмулируемой программы (попробуйте поразмыслить над тем, как можно эффективно проэмулировать обращение к функции, порождающей новый поток или вызывающей callback-функцию, чтобы представить уровень сложности задачи).

Другой приметой времени следует признать все более и более широкое распространение виртуальных машин, более или менее точно воспроизводящих поведение настоящих, «железных» ЭВМ. Из программных продуктов этого рода, эмулирующих платформу x86, наибольшей популярностью пользуются Virtual PC и VMWare, причем VMWare существует не только в Windows-, но и в Linux-версии, что позволяет крэкеру не ограничивать себя в выборе основной операционной системы. Думаю, что многие уже оценили удобства, предоставляемые этим инструментом: отладку внутри гостевой ОС параллельно с работой в хост-системе, возможность в любой момент восстановить систему вместе со всеми необходимыми инструментами, защищенность хост-системы от действий деструктивного кода (некоторые разработчики все еще встраивают в свое ПО такие закладки, не считаясь с возможными последствиями) и другие полезные мелочи вроде возможности снятия скриншотов окна SoftIce. Однако потенциально виртуальные машины способны дать крэкеру гораздо больше, чем предлагают существующие инструменты. В первую очередь я имею в виду возможность интеграции отладчика с виртуальным процессором. Разработчикам SoftIce пришлось немало потрудиться, чтобы создать почти полностью «прозрачный» для системы отладчик, но даже этот замечательный инструмент имеет немало ограничений. Одни из них накладываются самим устройством процессора (например, невозможность установки более четырех аппаратных точек останова), другие заключаются отсутствии поддержки того или иного «железа», третьи кроются в недрах программного кода отладчика (проявляющаяся иногда нестабильность в работе, «дыры», позволяющие обнаружить присутствие SoftIce, невозможность просматривать тексты в национальных кодировках и т.п.). Корнем всех этих бед является то, что отладчик работает на той же самой программно-аппаратной системе, что и отлаживаемая программа. Вот если бы каким-то образом удалось вынести отладчик за пределы компьютера... Если подойти к решению этой проблемы «в лоб», мы придем к идее аппаратной приставки, берущей под контроль все процессы, протекающие внутри системного блока. И такой подход действительно иногда используется, но не для взлома, а для тестирования всевозможного встраиваемого ПО - от новых версий BIOS до управляющих программ бортовых ЭВМ космических спутников. Гонять такие «железки» (надо отметить, в настоящее время весьма не дешевые) исключительно в целях крэкинга – это, пожалуй, явный перебор. Но вот если реализовать такой комплекс исключительно программно, да подключить его к виртуальному же процессору не менее виртуального PC, то можно будет получать любую информацию о состоянии нашей эмулированной машины. Это было бы очень изящное и практичное решение: Вас не волновали бы ограничения на число отладочных регистров, не нужно было бы думать о том, поддерживается ли Ваша видеокарта отладчиком, а сам отладчик смог бы предоставить такие сервисные возможности, какие невозможны в SoftIce, «завязанном» на работу исключительно в текстовых режимах. Аналогичный же вариант можно было бы проделать и с прочим оборудованием, например, предоставив API для создания собственных программных модулей, эмулирующих внутренние и внешние устройства. Разумеется, это породило бы виток соревнования «брони и снаряда», и к бесчисленным антикрэкерским пособиям «как определить наличие отладчика» добавились бы изыскания о проверке обрудования, с которым работает программа, на виртуальность (а защиты с такими механизмами уже существуют, правда, направлены они в основном против эмуляторов CD-ROM), но тут уж ничего не поделаешь – таковы законы жанра. Собственно, отладчики, интегрированные с виртуальными машинами, не являются чем-то ранее невиданным: наиболее продвинутые эмуляторы машин линейки Spectrum, БК и Atari позволяют не только запускать программы, но и отлаживать их «по живому» со всеми удобствами. Возможно, в эпоху 128-разрядных ЭВМ эмуляторы PC обретут такие же возможности, но современному ПО в этой области пока похвалиться нечем.

Не стоит забывать и о многочисленной армии специализированных мобильных устройств. Лавинообразный рост количества таких аппаратов и расширение их функциональности позволяет предположить, что «мобильный крэкинг» в ближайшее время станет столь же актуален, как и классический, ориентированный на софт для персональных ЭВМ. И если «наладонники» достаточно мощны и сложны, чтобы позволять реализовывать сложные защиты, то во многих мобильных телефонах и смартфонах в настоящее время широко используется урезанный вариант языка Java. А там, где есть Java и прочие интерпретаторы байт-кода, всевозможные декомпиляторы приобретают просто убийственную эффективность: сравнительно низкая производительность и ограниченные возможности мобильных устройств сильно мешают использованию «замусоривателей» (obfuscator’ов) кода и интеграции защитных механизмов в код программы, а технологии декомпиляции Java-программ настолько хорошо отработаны, что позволяют получить листинг, идентичный исходному. Из всего этого, в свою очередь, следует исполнение вековечной мечты крэкеров, а именно – возможность декомпилировать программу в исходный текст на языке высокого уровня, внести в него любые исправления и с минимальными усилиями скомпилировать обратно без потери работоспособности. Кроме того, для многих популярных мобильных платформ существуют общедоступные эмуляторы и SDK, позволяющие исследовать и дорабатывать программы для мобильников даже не имея в наличии настоящего, «железного» телефона.

И, наконец, последние направление, обещающее огромные возможности, связано с появлением open-source ОС, совместимой с Windows. Если Вы уже пытались применить свои знания на практике, Вы не могли не заметить ограниченность либо нестабильность многих крэкерских инструментов. Зависания отладчиков, вызовы API, которые в упор не замечают API-шпионы, невозможность нормально снять дамп памяти – такова объективная реальность, с которой приходится сражаться крэкеру. Все это можно было бы списать на ограниченность и недостаточное качество используемого программного обеспечения, тем более, что авторы многих из этих программ изначально не ориентировались на потребности крэкеров и потому было бы странно требовать от них возможностей, которые не были заявлены в проекте. Однако если рассматривать вопрос с точки зрения архитектора, нам откроется, что «корень зла» лежит не в несовершенстве программ (хотя наличие такового несовершенства нельзя отрицать), но в способах, при помощи которых эти программы взаимодействуют с ОС. А взаимодействие это нередко осуществляется «с черного хода», при помощи приемов, работоспособность которых держится на многочисленных «авось». И в этом нет ничего удивительного: при разработке ОС штатные механизмы перехвата системных вызовов не были запроектированы в принципе, поэтому единственным методом удовлетворения возникших потребностей оказался метод «грязного хака» (который, впрочем, впоследствии был санкционирован самим производителем ОС). Образно говоря, система «ОС+отладчик» или «ОС+API-шпион» с высоты птичьего полета выглядит не как готический замок, в котором каждая башенка является частью единого целого, а как глинобитный сарай, прилепленный к стене суперсовременного небоскреба. Неудивительно, что прочность такой конструкции – совершенно никакая, и рушится она чаще всего именно в месте, где «небоскреб» соединяется с «сараем». Наиболее естественным выходом из сложившейся ситуации была бы интеграция средств наблюдения за взаимодействием между пользовательскими программами и операционной системой непосредственно в ядро ОС, но по вполне понятным причинам корпорация Microsoft на такой шаг не пойдет. Однако в последнее время появилось несколько весьма интересных проектов с доступными исходными текстами - от эмуляторов Windows (Wine/WineX/Cedega) до разработки полноценной «другой Windows» (проект ReactOS). Наличие исходных текстов позволяет встраивать непосредственно в ядро ОС практически любые механизмы слежения за пользовательскими программами и самой ОС, а также API для управления этими механизмами. Причем такие «усовершенствования» ОС совершенно необязательно внедрять в код жестко, их можно оформить как патчи, накладываемые на исходные тексты ядра и применяемые при необходимости. Возможности же, предоставляемые отладчиком или API-шпионом, интегрированным в ядро операционной системы, трудно переоценить.

Однако изменятся не только орудия крэкерского труда – изменится и сам крэкинг, и, изучая предмет, этот факт не стоит сбрасывать со счетов. Уже сейчас очевидны две тенденции: с одной стороны, продвижение наиболее простых крэкерских технологий в массы (о чем я говорил выше), и с другой стороны – нарастание специализации, сначала - в областях знаний, а затем – и во взломе конкретных продуктов. Десять-пятнадцать лет назад крэкер вынужден был становиться «мастером на все руки» и не только уметь разбираться в кодах чужих программ, но и обеспечивать себя необходимыми инструментами для этой деятельности – Интернета в современном его виде, откуда всегда можно выкачать свежий отладчик или распаковщик, тогда просто не существовало. Разумеется, такая ситуация сложилась не от хорошей жизни – в отсутствие Интернета свободный обмен информацией и, тем более, инструментами был крайне затруднителен – всевозможные BBS и «флоппинеты» мало чем способны были помочь, да и существовали далеко не везде. Разработчикам защит, впрочем, было не легче: «навесные» защиты не то, чтобы совсем отсутствовали, но были не слишком популярны, и в итоге каждая защищенная программа была единственной в своем роде, но с другой стороны набор защитных приемов был куда более ограничен по причине простоты процессоров и операционных систем того времени. Нынешняя ситуация отличается в корне: с одной стороны, количество программ выросло на порядки но с другой – сами авторы ПО обычно идут одним из двух проторенных путей: либо читают всевозможные руководства «как защитить программу» и затем более или менее успешно воспроизводят книжные схемы, либо покупают готовый продукт и «в два клика» прикручивают защиту, нимало не интересуясь, что они прилепили к своему коду. Следствием такой защитной стратегии стало то, что знание о том, как ломается та или иная типовая защита, является ключом не к единственной программе, но к множеству. Итогом всего этого становится ярко выраженная ориентация на изучение именно типовых, популярных защит (потому как это – способ минимальными силами «разобраться» с максимумом программ и ломать их конвейерным методом), а это есть ни что иное, как специализация. Впрочем, какой бы путь Вы для себя не выбрали, существуют области, в которых необходимо разбираться каждому, кто так или иначе собирается уделить время крэкингу, и на изучение именно этих вещей и стоит делать упор в первую очередь. Не стоит совершенно игнорировать и малопонятную или неактуальную информацию: даже если сейчас Вы не можете применить эти знания, постарайтесь усвоить их хотя бы в общих чертах – возможно, в будущем именно эти «заметки на полях памяти» лягут в основу изящного подхода к какой-нибудь проблеме. Многие из старых пособий по крэкингу сейчас видятся пыльным антиквариатом, поскольку те программы, которые в них описаны, давно и бесповоротно канули в прошлое. Но первое впечатление обманчиво: хотя коды из этих статей устарели, иные решения, приведенные в этих статьях, вызывают восхищение и сейчас. Помните, что не стоит зацикливаться на одних лишь крэкерских материалах; очень часто бывает полезно взглянуть на проблемы защиты и взлома с «другой стороны баррикад». Так уж исторически сложилось, что авторы защит не слишком охотно предоставляют информацию о своих изделиях, однако даже беглое изучение содержимого соответствующих руководств и форумов даст Вам представление, чем дышит «вероятный противник», какие идеи сейчас популярны в среде разработчиков ПО и - если повезет - в чем заключаются огрехи и недоработки популярных защитных схем. Знание о новом защитном приеме, кроме всего прочего, представляет собой отличный повод поразмыслить о том, как соответствующий код может быть идентифицирован в реальной программе и нейтрализован. Я уже давал этот совет в одной из первых глав, но повторю его еще раз в завершении: во всяком источнике информации нужно искать не столько приемы, сколько идеи, а знакомство с оригинальными идеями в свою очередь стимулирует способность находить решения самостоятельно. И какой бы Вы ни избрали путь – скромного разработчика инструментов, кующего «оружие Победы» или «бойца невидимого фронта», раскалывающего особо прочные защиты, потрошителя двадцатидолларовых утилит или мирного исследователя недр операционной системы – не бойтесь отойти от канонов и проявить неоправданную оригинальность. В конце-концов, изящные решения – это те нити, из которых соткано будущее. И это будущее обещает быть интересным – так почему бы не добавить в него что-то от себя?


Автор: CyberManiac <_cybermaniac_@mail.ru>




Обсуждение статьи: Теоретические основы крэкинга. Книга. >>>


Комментарии к статье: Теоретические основы крэкинга. Книга.

agentru 24.07.2005 16:09:54
Молодец, титанический труд проделал..
---
KeChan 24.07.2005 19:55:52
Круче только горы...
---
zerg 23.07.2005 20:13:36
м-да...
мне еще лезть и лезть....
---
Bitfry 25.07.2005 05:41:25
Знаю не понаслышке, чего стоит написать хорошую вещь. Коллега, давно хотел вас поблагодарить. Вот и выдалась возможность - спасибо!
---
AA92 25.07.2005 12:18:28
круче только тучи
---
QIce 26.07.2005 17:08:54
однозначно шедевр. сделано с любовью. стиль подачи восхищает. молодец!
---
QIce 27.07.2005 10:04:36
однозначно шедевр. сделано с любовью. стиль подачи восхищает. молодец!
---
[Anonim] 28.07.2005 13:45:35
Спасибо за хорошую книгу!
---
[Anonim] 28.07.2005 13:52:31
Крису Касперкому дотокого шедевра далеко!
---
8) 04.08.2005 05:21:33
благодарность, хоть как не птаюсь из меня никакой крекер...
---
Alex 10.08.2005 08:10:12
Это просто ШЕДЕВР. Огромное спасибо автору.
---
COdEXpLOrER [AHTeam] 13.08.2005 02:31:59
Респект автору, написано очень грамотно, интересно, а главное, нормальным русским языком.
---
tolik 14.08.2005 01:08:47
СПАСИБО АВТОРУ!!! наверное самое полное и в тоже время краткое, понятное объяснение какое только есть!!!
---
Old_Cyberpunk 15.08.2005 01:48:01
Мда, побольше бы таких книг...
---
I am 16.08.2005 15:37:50
CyberManiac Book is very BEST !!!
---
hazard0us 17.08.2005 19:28:31
SUPA_BOOK!!!!!!!!!!!!!
---
Max 24.08.2005 12:42:48
SUPER BOOK!!!
---
mifodix 26.08.2005 18:59:58
Потрясающая книга!!!Просто, понятно. CyberManiac, ты настоящий молодец!Можно памятник при жизни ставить!:)))
---
DeLL 30.08.2005 12:37:26
Просто слов нету
---
minya 02.09.2005 14:38:04
Креатиф Гениален / Афтар Маладетц
---
$anter 16.09.2005 17:54:49
Очень помогла статья...Круче ещё не видел
---
Zozk 17.09.2005 21:45:37
афтар крут... не то что я:)
---
Hacker 29.09.2005 17:04:25
Очень хорошая кника.
Такую бы еще и по хакерство было бы
ваще супер!
---
Alex 04.10.2005 15:34:08
Толково написано, спасибо!
---
Nelson 30.10.2005 14:49:48
Пытаюсь научиться крекингу и считаю, что столь титанческий труд по праву можно считать одним из лучших, если не лучшим...
Заранее благодарен!!! СПАСИБО!!!
---
Keks 01.11.2005 09:18:11
CyberManiac - суперчеловек
---
tekton 12.07.2008 23:01:06
Я пока новичёк в этом деле ;). Этот труд перечитал уже несколько раз, её можно использовать как настольную и читать... читать пока не выучишь! Спасибо за ШЕДЕВР! Крекерская класика! С нетерпением ждём новых творений Автора!!!
---

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



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


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