Техника хакерских атак
Фундаментальные основы хакерства
Крис Касперски
Светлой памяти Сергея Иванова – главного редактора издательства "Солон" – посвящается эта книга.
Автор.
Аннотация
Книга, которую вы сейчас держите в руках, открывает двери в удивительный мир защитных механизмов, рассказывая о том, как создаются и вскрываются защиты. Она адресована всем, кто любит захватывающие дух головоломки. Всем, кто проводит свободное (и несвободное) время за копанием в недрах программ и операционной системы. Наконец, всем, кто по роду своей деятельности занимается (постоянно и/или эпизодически) написанием защит и хочет узнать как грамотно и гарантированно противостоять вездесущим хакерам.
Настоящий том посвящен базовым основам хакерства – технике работы с отладчиком и дизассемблером. Подробно описаны приемы идентификации и реконструкции ключевых структур исходного языка – функций (в т.ч. виртуальных), локальных и глобальных переменных, ветвлений, циклов, объектов и их иерархий, математических операторов и т.д.
Предисловие редактора
"The only secure computer is one that's unplugged, locked in a safe, and buried 20 feets under the ground in a secret location... and I'm not even too sure about that one…"
Дэннис Хьюжз (Dennis Huges),
ФБР США
Эпиграф выбран неслучайно. Информационная безопасность сегодня представляет одну из весьма “горячих” тем. Ее актуальность весьма велика, и каждое пособие связанное с этой темой подвергается анализу со стороны обычно весьма скептически настроенных специалистов. Исследование программ связано с вопросами информационной безопасности напрямую. Когда автор этой книги пригласил меня, как специалиста, стать ее научным редактором, я отнесся к этой затее с большим интересом.
Сама мысль о возможности опубликования подобных материалов допускает для многих некоторую крамолу, как некогда было, к примеру, с криптографией и некоторыми областями теории чисел. Более того, тематика данной книги до некоторого времени расценивалась как близкая к широко обсуждаемым криминальным темам и лишь в последнее время вернулась в свое естественное научное русло.
На мой взгляд, эта книга будет интересна весьма широкому кругу читателей. Наверняка ею заинтересуются и те, кто лишь начинает свой восход к Олимпу знаний, и уже “матерые” специалисты в области программирования и исследования программ (или на иностранный манер “reverse engineering”). Хочется особенно отметить, что материалы книги устроены таким образом, что будут полезны и обычному программисту (как пособие по оптимизации программ для современных интеллектуальных компиляторов), и специалистам различных направлений (например, специалистам информационной защиты ‑ в качестве пособия по поиску так называемых “закладок”). Стиль изложения “от простого к сложному” позволяет говорить также и о том, что данная книга послужит также и учебным пособием для начинающих исследователей и “кодокопателей”.
Книга содержит бесценное количество уникального по своей сути практического материала. Множество поверхностных работ за рубежом представляет очень мало практического интереса для тех, кто с интересом изучает прикладную математику, программирование и устройство компьютеров. Да и среди публикаций современного российского научного сообщества читатель не найдет лучшего пособия по изучению техники исследования программ.
Однако, я все-таки рекомендую читателю подвергнуть сомнению все вышесказанное и убедиться во всем самостоятельно, прочитав данную книгу.
С уважением,
Хади Р.А.
Что нового во втором издании
"Как бы плохо вы ни написали вашу повесть, у вас обязательно найдутся читатели, тысячи читателей, которые сочтут ее шедевром... Как бы хорошо вы ни написали свою повесть, обязательно найдутся читатели, и это будут тысячи читателей, которые сочтут ее чистым барахлом"
Борис Hатанович Стругацкий
Первое издание "Техники и философии хакерских атак" – довольно фривольное и хаотичное – по стилю изложения напоминало собой "Путевые заметки охотника" – читается, может быть и интересно, но вот на учебник, увы не тянет. К моему огромному удивлению книга имела ощутимый успех и множество одобрительных откликов. Одно, конечно, понятно – на безрыбье и рак рыба – за последнее время ничего путного по данной тематике не выходило.
Когда же тираж книги был полностью распродан, но заявки на нее по-прежнему продолжали поступать, встал вопрос – что делать дальше: выпускать "в один к одному" допечатку или переработанное и дополненное второе издание? Издатель склонялся к последнему, да и я в желании утолить свой профессиональный зуд, признаться, тоже. Однако за время, прошедшее с момента первого издания, я стал писать значительно структурней и "чище". Поэтому, после долгих колебаний, сомнений и размышлений решил полностью переписать книгу "с нуля", превратив ее в реальную настольную книгу хакера. Своеобразный справочник кодокопателя, но вместе с тем и учебник, помогающий начинающим сделать в мире хакерства свои первые шаги.
Попутно – движимый просьбами читателей, ожидающих поскорее увидеть продолжение трилогии "Образ мышления – дизассемблер IDA", я рискнул включить в настоящее издание двадцать глав из моей будущей книги "Искусство дизассемблирования" (название рабочее), которая увидит свет в своем полном объеме не раньше чем через три – пять лет.
Объем книги увеличился настолько, что ее пришлось разбить на несколько томов. Этот, первый из них, посвящен базовым основам хакерства – технике работы с отладчиком и дизассемблером. Затронуты вопросы защиты программ от изучения и техника нейтрализации защит, впрочем, подробный рассказ о методике создания и снятия защитных механизмов – тема последующих томов.
Кто такие хакеры
…Назови ты меня вчера быком, я был бы быком. Назвал бы ты меня лошадью -- и я был бы лошадью. Если люди дают имя какой-то сущности, то, не приняв этого имени, навлечешь на себя беду.
Приписывается китайскому мудрецу Лао-цзы
Прежде чем подавать на стол блюда хакерской кухни, неплохо бы разобраться кто, собственно, эти хакеры и что они едят? Заглянув в толковый словарь английского языка, например в "The American Heritage Dictionary", мы убедимся, что глагол "hack" возник в английском лексиконе задолго до появления компьютеров и в прямом смысле обозначал "бить, рубить, кромсать" (но не уродовать!) топором, мотыгой или молотом. Т.е. делать физически тяжелую, монотонную, занудную, интеллектуально непритязательную работу – удел батраков, неудачников и бездарей. Неудивительно, что производные от глагола "хак" обозначали "бить баклуши", "халтурить", "выполнять работу наспех" – ведь наемные рабочие испокон веков "фунциклировали" из-под палки! Термин считался пренебрежительным, если не ругательным: "хак" стало даже синонимом нашего "кляча"! Словом, в докомпьютерную эпоху титулом "хакера" ни один здравомыслящий человек ни возгордился бы…
Сегодня же "хакер" звучит практически так же как "национальный герой", пускай и преступный, но все же крутой малый, которому не грех подражать. Чем же объясняется такая метаморфоза?
По одной из гипотез в щелчке, издаваемом реле, американцам слышалось "хак - хак". Динозавры машинной эры состояли из многих тысяч реле и "хакали" во всю, особенно когда оператор ЭВМ запускал очередную программу на выполнение. Возможно, именно за это операторов и прозвали "хакерами". Или, говоря по-русски "клацальщиками". По другой гипотезе звук "хак" приписывается перфоратору, кромсающему перфоленту на мелкие куски, так что щепки (такие аккуратненькие круглые "щепочки") во все стороны летят!
На ассоциативном уровне обе гипотезы вполне правдоподобны. И реле, и перфоратор издают повторяющиеся монотонные удары, чем-то напоминающие кашель, а выражение "кашлять сухим кашлем" - одно из значений слова "hack". К тому же, программировали "динозавров" исключительно в машинных кодах, подчас с помощью переключателей или перетыкивания разъемов, - физически тяжелая, нудная, неблагодарная работа, достающаяся наименее привилегированной части персонала. Какой там романтизм? Какое изящество решений или полет мысли? Халтура сплошная… Редкая программа обходится без ошибок, а программа, составленная в машинных кодах – тем более. При желании любого оператора было можно назвать халтурщиком – "хакером" в ругательном смысле этого слова. "Вот, наделал кучу ошибок, хакер ты наш!"
Обыватели же, далекие от вычислительной техники, и знакомые с ней исключительно по фантастическим романам, испытывали перед ЭВМ благоговейное уважение, подогреваемое гордостью за научно-технические достижения всего рода homo sapiens в целом и американской нации в частности. "Белые воротнички" – цвет нации, управляющие махиной размером с супермаркет и стоящей дороже тысячи таких супермаркетов, вызывали у рядового американца смесь восторга, зависти и стремления к подражанию. Вроде как "я тоже хочу быть космонавтом", не задумываясь о том, что космонавтика это только с виду романтика, а в действительности – каторжная работа.
Но, если желание побывать в космосе до сих пор смогли реализовать лишь единицы, то ЭВМ стали широко доступными уже в начале шестидесятых. К тому времени их было можно встретить и в подвалах университетов, и в стенах крупных корпораций, и практически во всех исследовательских учреждениях. Очутиться за пультом ЭВМ в создании студента означало практически то же самое, что и "сесть за штурвал реактивного бомбардировщика". Программирование ассоциировалось отнюдь не с "батрачеством", а с интеллектуальной игрой. И "старшие наставники" студентов – операторы ЭВМ были не только их руководителями, но и кумирами. Студенты, одержимые вычислительной техникой, стремились во всем копировать персонал, обслуживающий большие ЭВМ, часто без понимания сути происходящего. Прознав жаргонное прозвище операторов, студенты, не догадываясь о его иронично – оскорбительном оттенке, с достоинством стали называть хакерами и себя и своих товарищей, и даже свою работу окрестили "хакерством". Но в их устах слово "хакер" звучало отнюдь не насмешкой, а расценивалось как титул. Ты – хакер, значит, ты такой же мастер, как и настоящий оператор ЭВМ. Значит, ты крутой парень и перед тобой не стыдно снять шляпу.
Так "хакеры" из работяг превратились в программистов – энтузиастов, помешенных на компьютерах и выделывающих на них такое… такое, что другим и не снилось. Термин продолжал видоизменяться, мигрируя своими значениями в сторону "крутого трюка", "забавного эффекта", "выполненного со вкусом розыгрыша". Этот дух подхватили и другие факультеты, порой и вовсе не связанные ни с электроникой, ни с вычислительной техникой, ни даже с точными науками вообще. "Хаком" стали называть любой классный розыгрыш или нестандартное решение знакомой задачи, – жаргонный термин технического языка превратился в модное словечко, употребляемое всеми кому не лень.
Тем временем мутация "хакера" продолжалась… Чтобы понять ее причины мысленно перенесемся в конец шестидесятых – начало семидесятых, а, может, даже чуточку позже. В те годы среди западной молодежи витал дух борьбы. Борьбы с кем? Да разве это важно! Протестовали против войны во Вьетнаме (кто не хотел служить в армии – жгли повестки), ломали пуританские устои старого мира, провозглашая свободу любви, презирали деньги (или только делали вид, что презирали, завистливо поглядывая в сторону того, у кого они есть). По большому счету вся борьба сводилась к суете в песочнице и власть имущих в общем-то ничуть не раздражала. Молодежные лидеры не имели в руках никакого оружия – ни политического, ни экономического, ни идеологического, не говоря уже об огнестрельном. К тому же, через десяток лет дух борьбы покинул Америку и весь шум закончился.
"Счастливое исключение" составили программисты. В те дни компьютерные системы еще не успели обзавестись достойной защитой, но уже управляли стратегически и экономическими важными объектами. Власть над компьютерами позволяла дать хорошего пинка и правительственным организациям, и финансовым магнатам, и корпорациям, и другим сильным мира сего, причем, оставаясь безнаказанным. Не существовало ни соответствующих законов, ни компьютерной полиции, способной "вычислить" преступника…
Словом, дикий запад времен разбоя, романтики и беспредела, когда человек с кольтом мог заставить шерифа мирного уездного городка "слушать Шопена лежа". У американцев надо сказать, по поводу освоения Америки очень сильный комплекс – одних вестернов они сняли больше, чем мы фильмов про Великую Отечественную Войну. Понятно дело, каждый юный американец в душе мнит себя полноправным ковбоем!
Компьютеры же позволили воплотить эту мечту в жизнь. Освой ЭВМ и носись по электронным сетям, как "неуловимый Джо", отстреливающий индейцев (банкиров, ЦРУ-шников и т.д.). Да и как не носиться, когда на книжных лотках как грибы появлялись фантастические романы, главными героями которых были компьютерные взломщики – хакеры. Писатели, никогда в жизни не видевшие ЭВМ, плохо разбирались в техническом жаргоне и употребляли его на интуитивно-бессознательном уровне безо всякого понимания. Достаточно перелистать "The Shockware Rider" Джона Бруннера (John Brunner) 1975 года, "The Adolescence of P‑1" Томаса Риана (Thomas Ryan) 1977 года или "Necromancer" Вильяма Гибсона (Wilam Gibson), опубликованный в 1984 году, чтобы убедиться насколько их авторы были далеки от вычислительной техники. Впрочем, литературных достоинств произведений это ничуть не ущемляло, а читатели в своей массе были от вычислительной техники еще более далеки, чем писатели, и у них сложился устойчивый образ "ЭВМ – это круто", а "хак – это вообще круто". Нейроматик, кстати, был самой любимой книгой Роберта Тапплана Морриса, создавшего своей знаменитый вирус – червь, надо полагать, не без влияния Вильяма Гибсона.
Журналисты, не обременение ни знаниями ЭВМ, ни лингвистическим образованием, из всего этого поняли только одно: некто, называющие себя хакерами, ломают компьютеры по всей стране, причем ломают весьма круто с убытками в особо крупных размерах.
Слово "хакер" вырвалось на страницы газет, но в широких массах глагол "хак" по-прежнему означал все те же "бить--кромсать", и американцы, вполне естественно, заключили, что хакер -- это тот, кто вламывается в чужие системы и раздалбывает их в пух и прах.
Вот, собственно, и все… Кольцо замкнулось, - термин "хакер" вернул свое "историческое" значение, но не прекратил эволюцию! Хакерам прошлого поколения (т.е. энтузиастам программирования) очень не понравилось, что их титул смешали, мягко выражаясь, с дерьмом, и при его упоминании от них все стали шарахаться как от огня. Стремясь реабилитировать себя в глазах общественности, хакеры предприняли попытку разделить всех своих на "хороших" и "плохих", оставив за "хорошими" парнями право называться "хакерами", для "плохих" придумав специальный термин "кракер" – от слова "crack" – ломать (кстати, почему не "брейкер" от слова "break"?), в буквальном смысле обозначающий "ломатель". Затея с треском провалилось, - далеко не каждый взломщик был готов нацепить на себя ярлык плохого паря. Называться хакером по-прежнему считалось и модно, и престижно, пускай все "хакерство" ограничилось "wannabe" (в дословном русском переводе "хочубытькак", т.е. подражанием). Предметы хакерской культуры обожествлялись, становясь предметом поклонения, догматом, иконой на стене.
Эта ветка генеалогического древа "хакеров" не имеет будущего и обречена на медленное, но неотвратимое вымирание. Уже сегодня, в начале первого десятилетия двадцать первого века, термин "хакер" стал всеобъемлющим и утратил всякий смысл. Кто пишет вирусы? Хакеры! Кто ломает программы? Хакеры! Кто крадет деньги из банков? Хакеры! Кто пакостит в Сети? Хакеры! Кто программирует на ассемблере? Хакеры! Кто знает все тонкости операционной системы и железа? Хакеры! Сказать собеседнику, что ты хакер, не уточив, что конкретно ты имеешь под этим ввиду, все равно, что ничего не сказать.
Термин "хакер" умер, но ведь хакеры – остались! Остались и работяги-кодеры, пускай уже не клацающие реле, но зато шумящие пропеллерами вентиляторов, остались и энтузиасты программирования, упоенно программирующие и на древних, и на современных языках, остались и исследователи защит, и умельцы по их взлому… Люди есть, а термина, определяющего их принадлежность, уже нет.
Почему бы не назвать определенную категорию компьютерщиков "кодокопателями"? Этот термин, впервые употребленный Безруковым, на мой взгляд, очень удачен и интуитивно понятен без дополнительный объяснений. Любой, кто любит копаться в коде (не обязательно машинном) по праву может считать себя кодокопателем.
Таким людям, собственно и посвящена эта книга…
Чем мы будем заниматься
На протяжении всей книги мы будем заниматься занимательной интеллектуальной игрой – созданием защитных механизмов и исследованием их стойкости. Скажу сразу - ничего общего со взломом коммерческих программ или кражей денег из банка это не занятие не имеет. Автор искренне надеется, что его читатели – граждане в своей массе законопослушные и обладающие высокой нравственностью люди.
Умение нейтрализовать защиты еще не дает права применять это умение в преступных целях. Какие же цели являются преступными, а какие нет – вопрос, относящийся уже не к хакерству, а юриспруденции в которой автор не силен и все, что может он порекомендовать – если имеются какие-то сомнения на счет правомерности совершения некоторых действий, – обратитесь к юристам.
Однако экспериментировать с вашей личной интеллектуальной собственностью – программами, написанными вами самими, – ни один закон не вправе запретить, да ни один закон этого, собственно, и не запрещает.
А раз так, на плечи – рюкзак, охотничий ножик в карман и – в густой таежный лес…
Что нам понадобиться
Выбор рабочего инструментария – дело сугубо личное и интимное. Тут на вкус и цвет товарищей нет. Поэтому, примите все нижесказанное не как догму, а как рекомендацию к действию. Итак, для чтения книги нам понадобиться:
– отладчик Soft-Ice версии 3.25 или более старший,
– дизассемблер IDA версии 3.7х (рекомендуется 3.8, а еще лучше 4.x),
– HEX-редактор HIEW любой версии,
– пакеты SDK и DDK (последний не обязателен, но очень желателен),
– операционная система – любая из семейства Windows, но настоятельно рекомендуется Windows 2000,
– любой Си\Си++ и Pascal компилятор по вкусу (в книге подробно описываются особенности компиляторов Microsoft Visual C++, Borland C++, WATCOM C, GNU C, FreePascal, а за основу взят Microsoft Visual C++ 6.0).
Теперь обо всем этом подробнее:
::Soft-Ice. Отладчик Soft-Ice – основное оружие хакера. Хотя, с ним конкурируют бесплатные WINDEB от Microsoft и TRW от LiuTaoTao – Soft-Ice много лучше и удобнее всех их вместе взятых. Для наших экспериментов подойдет практически любая версия Айса, например, автор использует давно апробированную и устойчиво работающую 3.26, замечательно уживающуюся с Windows 2000. Новомодная 4.x не очень-то дружит с моим видеоадаптером (Matrox Millennium G450 для справки) и вообще временами "едет крышей". К тому же, из всех новых возможностей четвертой версии полезна лишь поддержка FPO (Frame point omission – см. "Идентификация локальных стековых переменных") – локальных переменных, напрямую адресуемых через регистр ESP, – бесспорно полезная фишка, но без нее можно и обойтись. Найти Soft-Ice можно и на дисках известного происхождения, и у российского дистрибьютора - http://www.quarta.ru/bin/soft/winntutils/softicent.asp?ID=59. Купите, не пожалеете (хакерство это ведь не то же самое, что пиратство и честность еще никто не отменял).
::IDA Pro. Бесспорно самый мощный дизассемблер в мире – это IDA. Прожить без нее, конечно, можно, но… нужно ли? IDA обеспечивает удобную навигацию по исследуемому тексту, автоматически распознает библиотечные функции и локальные переменные, в том числе и адресуемые через ESP, поддерживает множество процессоров и форматов файлов. Одним словом, хакер без IDA – не хакер. Впрочем, агитации излишни, - единственная проблема: где же эту IDA взять? На пиратских дисках она встречается крайне редко (самая последняя виденная мной версия 3.74, да и то нестабильно работающая), на сайтах в Интернете – еще реже. Фирма-разработчик жестоко пресекает любые попытки несанкционированного распространения своего продукта и единственный надежный путь его приобретения – покупка в самой фирме или у российского дистрибьютора ("GelioSoft Ltd" ). К сожалению, с дизассемблером не распространяется никакой документации (не считая встроенного хелпа – очень короткого и бессистемного), поэтому мне ничего не остается, как порекомендовать собственный трехтомник "Образ мышления – дизассемблер IDA", подробно рассказывающей и о самой IDA, и о дизассемблировании вообще.
::HIEW. "Хьювев" – это не только HEX-редактор, но и дизассемблер, ассемблер и крипт "в одном флаконе". Он не избавит от необходимости приобретения IDA, но с лихвой заменит ее в ряде случаев (IDA очень медленно работает и обидно тратить кучу времени, если все, что нам нужно – посмотреть на препарируемый файл "одним глазком"). Впрочем, основное назначение "хьюева" отнюдь не дизассемблирование, а bit hack – небольшое хирургическое вмешательство в двоичный файл, – обычно вырезание жизненного важного органа защитного механизма, без которого он не может функцилировать.
::SDK (Software Development Kit – комплект прикладного разработчика). Из пакета SDK нам, в первую очередь, понадобится документация по Win32 API и утилита для работы с PE-файлами DUMPBIN. Без документации ни хакерам, ни разработчикам никак не обойтись. Как минимум, необходимо знать прототипы и назначение основных функций системы. Эту информацию, в принципе, можно почерпнуть и из многочисленных русскоязычных книг по программированию, но ни одна из них не может похвастаться полнотой и глубиной изложения. Поэтому, рано или поздно, вам придется обратиться к SDK. Правда, некоторым перед этим потребуется плотно засесть за английский, поскольку все документация написана именно на английском языке и ждать ее перевода все равно, что караулить у моря погоду (правда, с некоторых времен на сайте Microsoft стало появляться много информации для разработчиков и на русском языке). Где приобрести SDK? Во-первых, SDK входит в состав MSDN, а сам MSDN ежеквартально издается на компакт-дисках и распространяется по подписке (подробнее об условиях его приобретения можно узнать на официальном сайте msdn.Microsoft.com). Во-вторых, MSDN прилагается и к компилятору Microsoft Visual C++ 6.0, правда далеко не в первой свежести. Впрочем, для чтения данной книги его будет вполне достаточно.
::DDK. (Driver Development Kit – комплект разработчика драйверов). Какую пользу может извлечь хакер из пакета DDK? Ну, в первую очередь, он поможет разобраться: как устроены, работают (и ломаются) драйвера. Помимо основополагающей документации и множества примеров, в него входит очень ценный файл NTDDK.h, содержащий определения большинства недокументированных структур и буквально нашпигованный комментариями, раскрывающих некоторые любопытные подробности функционирования системы. Не лишним будет и инструментарий, прилагающийся к DDK. Среди прочего сюда входит и отладчик WINDEB. Весьма неплохой, кстати, отладчик, но все же значительно уступающий Soft-Ice, поэтому и не рассматриваемый в данной книге (но если вы не найдете Айса – сгодится и WINDEB). Не бесполезным окажется ассемблер MASM, на котором собственно и пишутся драйвера, а так же маленькие полезные программки, облегчающие жизнь хакеру. Последнюю версию DKK можно бесплатно скачать с сайта Microsoft, только имейте ввиду, что для NT полный DKK занимает свыше 40 мегабайт в упакованном виде и еще больше места требует на диске.
::операционная система. Вовсе не собираясь навязывать читателю собственные вкусы и пристрастия, я, тем не менее, настоятельно рекомендую установить именно Windows 2000. Мотивация – это действительно стабильная и устойчиво работающая операционная система, мужественно переносящая все критические ошибки приложений. Специфика работы хакера такова, что хирургические вмешательства в недра программ частенько срывают им "крышу", доводя ломаемое приложение до буйного помешательства с непредсказуемым поведением. ОС Windows 9x, демонстрируя социалистическую солидарность, зачастую очень часто "ложится" рядом с зависшей программой. Порой компьютер приходится перезагружать не один десяток раз за деньпо дню! И хорошо, если только перезагружать, а не восстанавливать разрушенные сбоем диски (такое, хотья и редко, но случается). Завесить же Windows 2000 на порядок сложнее, – мне это "удается" не больше пары чаще одного-двух раз за месяц, да и то с недосыпу или по небрежности. Потом, Windows 2000 позволяет загружать Soft-Ice в любой момент без необходимости перезагрузки, что очень удобно! Наконец, весь материал этой книги рассчитан именно на Windows 2000, – а ее отличия от других систем упоминаются далеко не всегда. Все равно, все мы когда-нибудь перейдем на Windows 2000 и забудем о Windows 9x как о страшном сне, так стоит ли хвататься за эту умирающую платформу? К слову сказать, Windows Me это не то же самое, что Windows 2000 и ставить ееMe на свой компьютер я никому не рекомендую (такое впечатление, что Windows Me вообще не тестировали, а о том, что ее писали садисты – кто ставил, тот поймет – я вообще молчу).
Итак, Худо-бедно разобравшись с инструментарием, поговорим о сером веществе, ибо в его отсутствии весь собранный инструмент бесполезен. Автор предполагаетполагает, что читатель уже знаком с ассемблером и, если не пишет программ на этом языке, то, по крайней мере, представляет себе что такое регистры, сегменты, машинные инструкции и т.д. В противном случае эта книга рискует показаться через чур сложной и непонятной. Отыщите в магазине любой учебник по ассемблеру (например: В. Юрова "ASSEMBLER – учебник", П.И. Рудакова "Программируем на языке ассемблера IBM PC" или "Assembler – язык неограниченных возможностей" Зубкова С.В) и основательно проштудируйте его.
Помимо значения ассемблера так же потребуется иметь хотя бы общие понятия о функционировании операционной системы. Купите и вдумчиво изучите (если не сделали этого до сих пор) "Windows для профессионалов" Джефри Рихтера {>>>> сноска см "Приложение", "Ошибки Джефри Рихтера"} и (если найдете) "Секреты системного программирования в Windows 95" Мэта Питрека. Хотья, его книга посвящена Windows 95, частично она справедлива и для Windows 2000. Для знакомства с архитектурой самой же Windows 2000 рекомендуется ознакомиться с шедевром Хелен Кастер "Основы Windows NT" и брошюрой "Недокументированные возможности Windows NT" А.В. Коберниченко.
Касаемо общей теории информатики и алгоритмов – бесспорный авторитет Кнут. Впрочем, на мой вкус монография М. Броя "Информатика" куда лучше, - при том что она намного короче, круг охватываемых ей тем и глубина изложения – намного шире. Зачем хакеру теория информатики? Да куда же без нее! Вот, скажем, встретится ему защита со движком-встроенным эмулятором машины Тьюринга. или Маркова. Слету ее не сломать, - надо как минимум опознать сам алгоритм: что это вообще такое – Тьюринг, Марков, или сеть Петри, а потом затем – отобразить его на язык высокого уровня, дабы в удобочитаемом виде анализировать работу защиты. Куда же тут без теории информатики!
За сим все., Ну, разве что стоит дополнить наш походный рюкзачок паруой учебников по английскому (они пригодятся, поверьте) и выкачать с сайтов Intel и AMD всю имеющуюся там документацию по процессорам. На худой конец подойдет и ее русский перевод, например, Ровдо А.А. "Микропроцессоры от 8086 до Pentium III Xeon и AMD K6-3".
Ну-с, рюкзачок на плечо и в путь…
Знакомство с базовыми приемами работы хакера
Введение.
Классификация защит
"Стать хакером очень просто. Достаточно выучить и понять: математические дисциплины (математический анализ, теорию функций комплексного переменного, алгебру, геометрию, теорию вероятностей, математическую статистику, математическую логику и дискретную математику...)".
Борис Леонтьев "Хакеры & Internet".
Проверка аутентичности (от греческого "authentikos" – подлинный) – "сердце" подавляющего большинства защитных механизмов. Проверка аутентичности необходима,Должны же мы чтобы удостовериться,: то ли лицо работает с программой, за которое оно себя выдает, и разрешено ли этому лицу работать с программой вообще.! В зависимости от результатов проверки защита либо передает управление основной ветке программы, либо "ругается" и блокирует работу.
В качестве "лица" может выступать не только пользователь, но и его компьютер или носитель информации, хранящий лицензионную копию программы. Таким образом, все защитные механизмы можно разделить на две основных категории:
• защиты, основанные на знании (пароля, серийного номера)
• защиты, основанные на обладании (ключевой диск, документация)
Защиты, основанные на знании, бесполезны, если обладатель защищенной с их помощью программы, не заинтересован в сохранении ее секретности. Он может сообщить пароль (серийный номер) кому угодно, после чего любой сможет запустить такую программу на своем компьютере.
Поэтому, парольные защиты для предотвращения пиратского копирования программ непригодны. Почему же тогда практически все крупные производители в обязательном порядке используют серийные номера? Ответ прост – для защиты своей интеллектуальной собственности грубой физической силой. Происходит это приблизительно так: …рабочая тишина такой-то фирмы внезапно нарушается топотом сапог парней в камуфляже, сверяющих лицензионные номера Windows (Microsoft Office, Microsoft Visual Studio) с лицензионными соглашениями, и стоит обнаружиться хотя бы одной "левой" копии, как появившийся, словно из-под земли, сотрудник фирмы начинает радостно потираеть руки в предвкушении дождя вечнозеленых… В лучшем случае – заставят купить все "левые" копии, в худшем же…
К домашним пользователям в квартиру, понятное дело, никто не врывается – частная собственность и все такое, да к этому никто собственно и не стремится. Что с домашнего пользователя возьмешь-то? К тому же, самим фирмам выгодно массовое распространение их продукции, а кто его обеспечит лучше пиратов? Но и здесь серийные номера не лишние – они разгружают службу технической поддержки от "левых" звонков незарегистрированных пользователей, одновременно с этим склоняя последних к покупке легальной версии.
Такая схема защиты идеальна для корпораций -гигантов, но она не подходит для мелких программистских коллективов и индивидуальных разработчиков, особенно если они зарабатывают на жизнь написанием узкоспециализированных программ с ограниченным рынком назначения сбыта (скажем, анализаторов звездных спектров или системы моделирования ядерных реакций). Не имея достаточного влияния, "раскачать" сотрудников известных органов на облаву по проверки лицензионности своего ПО нереально, а "выбить" деньги из нелегальных пользователей можно разве что с помощью криминальных структур, да и то навряд ли. Вот и приходится рассчитывать только лишь на собственныесобственную силыу и смекалку.
Тут лучше подходит тип защит, основанных на обладании некоторым уникальным предметом, скопировать который очень чрезвычайно тяжело, а в идеале – вообще невозможно. Первые ласточки этой серии – ключевые дискеты, записанные с таким расчетом, чтобы при их копировании копия чем-нибудь да отличалась от оригинала. Самое простое (но не самое лучше) слегка изуродовать дискету гвоздем (шилом, перочинным ножом), а затем, определив местоположение дефекта относительно сектора (это можно сделать записью-чтением некоторой тестовой информации – до какого-то момента чтение будет идти нормально, а потом начнется "мусор"), жестко прописать его в программе и при каждом запуске проверять – на том же самом месте дефект или нет? Когда же дискеты вышли из употребления, эта же техника была адоптирована и для компакт-дисков. Кто побогаче уродует их лазером, кто победнее – все тем же шилом или гвоздем.
Таким образом, программа жестко привязана к диску (дискете) и требует ее присутствия для своей работы, а, поскольку скопировать такой диск нереально (попробуй-ка, добиться идентичных дефектов на копиях), пираты "отдыхают".
Защитные механизмы, основанные на обладании, часто модифицирует предмет обладания в процессе работы программы, ограничивая количество запусков программы или время ее использования. Особенно часто такая "фишка" используется в инсталляторах – чтобы не нервировать пользователя, ключ запрашивается лишь однажды – на стадии установки программы, а работать с ней можно и без него. Если количество инсталляций ограничено, ущербом от несанкционированных установок одной копии программы на несколько компьютеров можно пренебречь.
Единственная проблема – все это ущемляет права легального пользователя. Кому понравится ограничение на количество инсталляций? (А ведь некоторые люди переустанавливают систему и все ПО буквально каждый месяц, а то и несколько раз на дню). Ключевые диски распознаются не всеми типами приводов, зачастую "не видимы" по сети, а, если защитный механизм для увеличения стойкости к взлому, обращается к оборудованию напрямую, в обход драйверов, такая программа наверняка не будет функционировать под Windows NT\2000 и весьма вероятно откажет в работе под Windows 9x (если, конечно, она не была заранее спроектирована соответствующим образом, но если так – это хуже, ибо некорректно работающая защита, исполняющаяся с наивысшими привидениями, может причинить немалый урон системе). Помимо этого, ключевой предмет можно потерять, его могут украсть, да и сам он может выйти из строя (дискеты склонны сыпаться и размагничиваться, диски – царапаться, а электронные ключи – "сгорать").
Конечно, эти претензии относится к качеству реализации, а не к идее ключей вообще, но конечным пользователям от этого ничуть не легче! Если же защита создает неудобства, у пользователей появляется очень сильная мотивация к посещению ближайшего доступного пирата на предмет приобретения у него контрфактного программного обеспечения. И никакие разговоры о морали, этике, добропорядочности и т.д. не подействуют – своя рубашка ближе к телу, а о добропорядочности нужно в первую очередь задуматься разработчикам таких защит. Тов…, тьфу, господа, не отравляйте жизнь пользователям! Пользователи – тоже люди!
В последнее время наибольшую популярность обрели защиты, основанные на регистрационных номерах – удачно сочетающие защиты обоих типов. при первом запуске программа привязывается к компьютеру и включает "счетчик" (вариант – блокирует некоторые функциональные возможности). А чтобы ее "освободить" необходимо ввести пароль, сообщаемый разработчиком за некоторое материальное вознаграждение. Часто для предотвращения пиратского копирования пароль представляет собой некоторую производную от ключевых параметров компьютера (или производную от имени пользователя в простейшем случае).
Разумеется, этот краткий обзор типов защит очень много оставил за кадром, но подробный разговор о классификации защит выходит за рамки этой книги, так что отложим его до второго тома.
Рисунок 1 0x026 Основные типы защит
Философия стойкости
Однажды один из друзей сказал Катону Старшему: "Какое безобразие, что в Риме тебе до сих пор не воздвигли памятника! Я обязательно позабочусь об этом".
"Не надо, - ответил Катон, - я предпочитаю, чтобы люди спрашивали, почему нет памятника Катону, чем почему он есть.
Т. Мессон
Если защита базируется на одном лишь предположении, что ее код не будет изучен и/или изменен – это плохая защита. Отсутствие исходных текстов отнюдь не является непреодолимым препятствием для изучения и модификации приложения. Современные технологии обратного проектирования позволяют автоматически распознавать библиотечные функции, локальные переменные, стековые аргументы, типы данных, ветвления, циклы и т.д. А в недалеком будущем дизассемблеры, вероятно, вообще научатся генерировать листинги близкие по внешнему виду к языкам высокого уровня.
Но даже сегодня трудоемкость анализа двоичного кода не настолько велика, чтобы надолго остановить злоумышленников. Огромное количество постоянно совершаемых взломов – лучшее тому подтверждение. В идеальном случае знание алгоритма работы защиты не должно влиять на ее стойкость, но это достижимо далеко не всегда. Например, если разработчик серверной программы решит установить в демонстрационной версии ограничение на количество одновременно обрабатываемых соединений (как часто и случается), злоумышленнику достаточно найти инструкцию процессора, осуществляющую такую проверку и удалить ее. Модификации программы можно воспрепятствовать постоянной проверкой контрольной суммы, но опять-таки, код, который вычисляет эту контрольную сумму и сверяет ее с эталоном, может быть найден и удален.
Сколько бы уровней защиты ни существовало, один или миллион, программа может быть взломана! Это только вопрос времени и усилий. Но в отсутствии реально действующих законов защиты интеллектуальной собственности разработчикам приходится больше полагаться на стойкость своей защиты, чем на помощь правоохранительных органов. Бытует мнение, дескать, что если затраты на нейтрализацию защитного механизма, будут не ниже стоимости легальной копии, ее никто не будет ломать. Это неверно! Материальный стимул – не единственное, что движет хакером. Гораздо более сильной мотивацией оказывается интеллектуальная борьба (кто умнее: я или автор защиты?), спортивный азарт (кто из хакеров сломает больше всего защит?), любопытство (а как это работает?), повышение своего профессионализма (чтобы научится создавать защиты, сначала нужно научиться их снимать), да и просто интересное времяпровождение (если его нечем занять). Многие молодые люди могут неделями корпеть над отладчиком, снимая защиту с программы стоимостью в несколько долларов, а то и вовсе распространяемой бесплатно (пример, файл - менеджер FAR для жителей России и СНГ абсолютно бесплатен, но это не спасает его взлома).
Целесообразность защиты ограничивается конкуренцией – при прочих равных условиях клиент всегда выбирает незащищенный продукт, даже если защита не ущемляет его прав. В настоящее время спрос на программистов значительно превышает предложение, но в отдаленном будущем разработчикам придется либо сговориться, либо полностью отказаться от защит. И специалисты по защитам будут вынуждены искать себе другую работу.
Это не значит, что данная книга бесполезна, напротив, полученные знания следует применить как можно быстрее, пока в защитах еще не отпала необходимость.
Шаг первый. Разминочный.
Бороться со своими мыслями, это уподобиться одному глупцу, который в целях аккуратности и гигиены решил больше не какать. День не какал, два не какал. Потом, конечно не выдержал, но всех продолжал уверять, что не какает.
Аноним
Алгоритм простейшего механизма аутентификации состоит в посимвольном сравнении введенного пользователем паролем с эталонным значением, хранящимся либо в самой программе (как часто и бывает), либо вне ее, например, в конфигурационном файле или реестре (что встречается реже).
Достоинство такой защиты – крайне простая программная реализация. Ее ядро состоит фактически из одной строки, котораяую на языке Си обычно выглядит можно записать так: – "if (strcmp(&введенный пароль, &эталонный пароль)) { /* Пароль неверен */} else {/* Пароль ОК */}"
Давайте дополним этот код процедурами запроса пароля и вывода результатов сравнения, а затем испытаем полученную программу на "прочность", т.е. стойкость к взлому.
// Простейшая система аутентификации
// посимвольное сравнение пароля
#include
#include
#define PASSWORD_SIZE 100
#define PASSWORD "myGOODpassword\n"
// этот перенос нужен затем, чтобы ^^^^
// не выкусывать перенос из строки,
// введенной пользователем
int main()
{
// Счетчик неудачных попыток аутентификации
int count=0;
// Буфер для пароля, введенного пользователем
char buff[PASSWORD_SIZE];
// Главный цикл аутентификации
for(;;)
{
// Запрашиваем и считываем пользовательский
// пароль
printf("Enter password:");
fgets(&buff[0],PASSWORD_SIZE,stdin);
// Сравниваем оригинальный и введенный пароль
if (strcmp(&buff[0],PASSWORD))
// Если пароли не совпадают – "ругаемся"
printf("Wrong password\n");
// Иначе (если пароли идентичны)
// выходим из цикла аутентификации
else break;
// Увеличиваем счетчик неудачных попыток
// аутентификации и, если все попытки
// исчерпаны – завершаем программу
if (++count>3) return –1;
}
// Раз мы здесь, то пользователь ввел правильный пароль
printf("Password OK\n");
}
Листинг 1 Пример простейшей системы аутентификации
В популярных кинофильмах крутые хакеры легко проникают в любые жутко защищенные системы, каким-то непостижимым образом угадывая искомый пароль с нескольких попыток. Почему бы неи попробовать пойти их путем?
Не так уж редко пароли представляют собой осмысленные слова, наподобие "Ferrari", "QWERTY", имена любимых хомячков, названия географических пунктов и т.д. Угадывание пароля сродни гаданию на кофейной гуще – никаких гарантий на успех нет, и остается рассчитывать на одно лишь везение. А удача, как известно, птица гордая – палец ей в рот не клади. Нет ли более надежного способа взлома?
Давайте подумаем – раз эталонный пароль хранится в теле программы, то, если он не зашифрован каким-нибудь хитрым образом, его можно обнаружить тривиальным просмотром двоичного кода программы. Перебирая все, встретившиеся в ней текстовые строки, начиная с тех, что более всего смахивают на пароль, мы очень быстро подберем нужный ключ и "откроем" им программу!
Причем, область просмотра можно существенно сузить, – в подавляющем большинстве случаев компиляторы размешают все инициализированные переменные в сегменте данных (в PE-файлах он размещается в секции ".data"). Исключение составляют, пожалуй, ранние Багдадские (Borland-вые в смысле) компиляторы с их маниакальной любовью всовывать текстовые строки в сегмент кода – непосредственно по месту их вызова. Это упрощает сам компилятор, но порождает множество проблем. Современные операционные системы, в отличие от старушки MS-DOS, запрещают модификацию кодового сегмента, и все, размешенные в нем переменные, доступны лишь для чтения. К тому же, на процессорах с раздельной системой кэширования (на тех же Pentium-ах, например) они "засоряют" кодовый кэш, попадая туда при упреждающем чтении, но при первом же к ним обращении вновь загружаются из медленной оперативной памяти (кэша второго уровня) в кэш данных. В результате – тормоза и падение производительности.
Что ж, пусть это будет секция данных! Остается только найти удобный инструмент для просмотра двоичного файла. Можно, конечно, нажать в своей любимой оболочке (FAR, DOS Navigator) и, придавив кирпичом любоваться бегущими циферками до тех пор, пока не надоест. Можно воспользоваться любым hex-редактором (QVIEW, HIEW…) – кому какой по вкусу, но в книге по соображениям наглядности я приведурезультат работы утилиты DUMPBIN из штатной поставки Microsoft Visual Studio.
Попросим ее распечатать секцию данных (ключ "/SECTION:.data") в "сыром" виде (ключ "/RAWDATA:BYTES"), указав значок ">" для перенаправления вывода в файл (ответ программы занимает много места и на экране помещается один лишь "хвост").
Смотрите! Среди всего прочего тут есть одна строка до боли похожая на эталонный пароль (в тексте она выделена жирным шрифтом). Испытаем ее? Впрочем, какой смысл – судя по исходному тексту программы, это действительно искомый пароль, открывающий защиту, словно Золотой Ключик. Только Слишком уж видное место выбрал компилятор для его хранения – пароль не мешало бы запрятать получше.
Один из способов сделать это – насильно поместить эталонный пароль в собственноручно выбранную нами секцию. Такая возможность не предусмотрена стандартном и потому каждый разработчик компилятора (строго говоря, не компилятора, а линкера, но это не суть важно) волен реализовывать ее по-своему (или не реализовывать вообще). В Microsoft Visual C++ для этой цели предусмотрена специальная прагма data_seg, указывающая в какую секцию помещать следующие за ней инициализированные переменные. Неинициализированные переменные по умолчанию располагаются в секции ".bbs" и управляются прагмой bss_seg соответственно.
Добавим в Листинг 1 следующие строки и посмотрим, что из этого у нас получится.
int count=0;
// С этого момента все инициализированные переменные будут
// размещаться в секции ".kpnc"
#pragma data_seg(".kpnc") // точку перед именем ставить
// не обязательно – просто так
// принято
char passwd[]=PASSWORD;
#pragma data_seg()
// Теперь все инициализированные переменные вновь будут
// размещаться в секции по умолчанию, т.е. ".data"
char buff[PASSWORD_SIZE]="";
...
if (strcmp(&buff[0],&passwd[0]))
Ага, теперь в секции данных пароля нет и хакеры "отдыхают"! Но не спешите с выводами. Давайте сначала выведем на экран список всех секций, имеющихся в файле:
Вот он, пароль! Спрятали, называется… Можно, конечно, извратится и засунуть секретные данные в секцию неинициализированных данных (".bss"), служебную RTL-секцию (".rdata") или даже секцию кода (".text") – не все там догадаются поискать, а работоспособность программы такое размещение не нарушит. Но не стоит забывать о возможности автоматизированного поиска текстовых строк в двоичном фале. Пример реализации такого фильтра приведен в "Приложении" (см. "исходный текст filter.c"). В какой бы секции ни содержался эталонный пароль – фильтр без труда его найдет (единственная проблема – определить какая из множества текстовых строк представляет собой искомый ключ; возможно, потребуется перебрать с десяток-другой потенциальных "кандидатов").
Правда, если пароль записан в уникоде, его поиск несколько осложняется, т.к. не все утилиты поддерживают эту кодировку, но надеяться, что это препятствие надолго задержит хакера – несколько наивно.
Шаг второй. Знакомство с дизассемблером
Надо ли милостивого бога все время просить о пощаде?
Велимир
О'кей, пароль мы узнали. Но как же утомительно вводить его каждый раз с клавиатуры перед запуском программы! Хорошо бы ее хакнуть так, чтобы никакой пароль вообще не запрашивался или любой введенный пароль программа воспринимала бы как правильный.
Хакнуть говорите?! Что ж, это не сложно! Куда проблематичнее определиться – чем именно ее хакать. Инструментарий хакеров чрезвычайно разнообразен – чего тут только нет: и дизассемблеры, и отладчики, и API-, и message- шпионы, и мониторы обращений к файлам (портам, реестру), и распаковщики исполняемых файлов, и… Попробуй-ка, начинающему кодокопателю со всем этих хозяйством разобраться!
Впрочем, шпионы, мониторы, распаковщики – второстепенные утилиты заднего плана, а основное оружие взломщика – отладчик и дизассемблер. Рассмотрим их поближе.
Как и следует из его названия, диз-ассемблер, предназначен для диз-ассемблирования или "раз-ассемблирования" если перейти с латыни на русский {ДИС…, ДИЗ… [лат. dis, ге. dys] – приставка, обозначающая разделение отделение, отрицание; соответствует русским "раз…", "не…", сообщает понятию, к которому прилагается, отрицательный или противоположный смысл, напр. дизассоциация, дисгармония – "словарь иностранных слов"}. То есть если ассемблирование – перевод ассемблерных команд в машинный код, то дизассемблирование, напротив, перевод машинного кода в ассемблерные команды.
Но пусть название не вводит вас в заблуждение: дизассемблер пригоден для изучения не только тех программ, что были написаны на ассемблере, – круг его применения очень широк, хотя и не безграничен. Спрашиваете – где же пролегает эта граница? Отвечаю.
Грубо говоря, все реализации языков программирования делятся на компиляторы и интерпретаторы.
::Интерпретаторы исполняют программу в том виде, в каком она была набрана программистом. Другими словами говоря – интерпретаторы "пережевывают" исходный текст, при этом код программы доступен для непосредственного изучения безо всяких дополнительных средств. Примером могут служить приложения, написанные на Бацике или Перле. Как известно, для их запуска требуется помимо исходного текста программы требуется иметь еще и сам интерпретатор, что неудобно ни пользователям (для исполнения программы в 10 килобайт приходится устанавливать интерпретатор в 10 мегабайт), ни разработчикам (в здравом уме и трезвой памяти раздавать всем исходные тексты своей программы!), к тому же синтаксический разбор отнимает много времени и ни один интерпретатор не может похвастаться производительностью.
::Компиляторы ведут себя иначе – при первом запуске они "перемалывают" программу в машинный код, исполняемый непосредственно самим процессором без обращений к исходным текстам или самому компилятору. С человеческой точки зрения человека откомпилированная программа представляет бессмысленную мешанину шестнадцатеричных байт, разобраться в которой неспециалисту абсолютно невозможно. Это облегчает разработку защитных механизмов – не зная алгоритма, вслепую защиту не сломаешь, ну разве что она будет совсем простая.
Можно ли из машинного кода получить исходный текст программы? Нет,! Компиляция – процесс однонаправленный. И дело тут не только в том, что безвозвратно удаляются метки и комментарии (ррразберемся и без комментариев – хакеры мы или нет?!), основной камень преткновения – неоднозначность соответствия машинных инструкций конструкциям языков высокого уровня. Более того, ассемблирование так же являет собой однонаправленный процесс и автоматическое дизассемблирование принципиально невозможно. Впрочем, не будем сейчас забивать голову начинающих кодокопателей такими тонкостями и оставим эту проблему на потом.
::Ряд систем разработки занимает промежуточное положение между компиляторами и интерпретаторами, – исходная программа преобразуется не в машинный код, а в некоторый другой интерпретируемый язык, для исполнения которого к "откомпилированному" файлу дописывается собственный интерпретатор. Именно по такой схеме функционируют FoxPro, Clipper, многочисленные диалекты Бацика и некоторые другие языки.
Да, код программы по-прежнему исполняется в режиме интерпретации, но теперь из него удалена вся избыточная информация – метки, имена переменных, комментарии, а осмысленные названия операторов заменены их цифровыми кодами. Этот "выстрел" укладывает сразу двух зайцев: а) язык, на который переведена программа, заранее "заточен" под быструю интерпретацию и оптимизирован по размеру; б) код программы теперь недоступен для непосредственного изучения (и/или модификации).
Дизассемблирование таких программ невозможно – дизассемблер нацелен именно на машинный код, а неизвестный ему интерпретируемый язык (так же называемый -кодом) он "не переваривает". Разумеется, -код не переваривает и процессор!, Его исполняет интерпретатор, дописанный к программе. Вот интерпретатор-то дизассемблер и "возьмет"! Изучая алгоритм его работы, можно понять "устройство" -кода и выяснить назначение всех его команд. Это очень трудоемкий процесс! Интерпретаторы порой так сложны и занимают столько много мегабайт, что их анализ растягивается на многие месяцы, а то и годы. К счастью, нет нужны анализировать каждую программу – ведь интерпретаторы одной версии идентичны, а сам -код обычно мало меняется от версии к версии, во всяком случае его ядро не переписывается каждый день. Поэтому, вполне возможно создать программу, занимающуюся переводом -кода обратно в исходный язык. Конечно, символьные имена восстановить не удастся, но в остальном листинг будет выглядеть вполне читабельно.
Итак, дизассемблер применим для исследования откомпилированных программ и частично пригоден для анализа "псевдокомпилированного" кода. Раз так – он должен подойти для вскрытия парольной защиты simple.exe. Весь вопрос в том, – какой дизассемблер выбрать.
Не все дизассемблеры одинаковы. Есть среди них и "интеллектуалы", автоматически распознающие многие конструкции как-то: прологи и эпилоги функций, локальные переменные, перекрестные ссылки и т.д., а есть и "простаки" чьи способности ограничены одним лишь переводом машинных команд в ассемблерные инструкции.
Логичнее всего воспользоваться услугами дизассемблера - интеллектуала (если он есть), но… давайте не будем спешить, а попробуем выполнить весь анализ вручную. Техника, понятное дело, – штука хорошая, на то она и придумана, чтобы решать проблемы, а не создавать новые, да вот только не всегда она оказывается под рукой и неплохо бы заранее научиться работе "в полевых условиях". на том, что всегда есть под рукой. К тому же, общение с плохим дизассемблером как нельзя лучше подчеркивает "вкусности" хорошего.
Воспользуемся уже знакомой нам утилитой DUMPBIN, настоящим "Швейцарским ножиком" со множеством полезных функций, среди которых притаился и дизассемблер. Дизассемблируем секцию кода (как мы помним, носящую имя ".text"), перенаправив вывод в файл, т.к. на экран он, очевидно, не помститься.
Так, менее чем через секунду образовался файл ".code" с размером… с размером в целых триста с четвертью килобайт. Да исходная программа была на два порядка короче! Это же сколько времени потребуется, чтобы со всей этой шаманской грамотой разобраться?! Самое обидное – подавляющая масса кода никакого отношения к защитному механизму не имеет и представляет собой функции стандартных библиотек компилятора, анализировать которые нам ни к чему. Но как же их отличить от "полезного" кода?
Давайте подумаем. Мы не знаем, где именно расположена процедура сравнения паролей и нам неизвестно ее устройство, но можно с уверенностью утверждать, что один из ее аргументов – указатель на эталонный пароль. Остается только выяснить – по какому адресу расположен этот пароль в памяти – он-то и будет искомым значением указателя.
Заглянем еще раз в секцию данных (или в другую – в зависимости от того, где хранится пароль):
Ага, пароль расположен по смещению 0x406040 (левая колонка чисел), стало быть и указатель на него равен 0x406040. Попробуем найти это число в дизассемблированном листинге тривиальным контекстным поиском в любом текстовом редакторе.
Нашли? Вот оно (в тексте выделено жирным шрифтом):
Это один из двух аргументов функции 0х04010A0, заносимых в стек машинной командой push. Второй аргумент – указатель на локальный буфер, вероятно, содержащий введенный пользователем пароль.
Тут нам придется немного отклониться от темы разговора и подробно рассмотреть передачу параметров. Наиболее распространенны всего два следующие способаы передачи аргументов функции – через регистры и через стек.
Передача параметров через регистры наиболее быстра, но не лишена недостатков – во-первых, количество регистров весьма ограничено, а во-вторых, это затрудняет реализацию рекурсии – вызова функции из самой себя. Прежде чем заносить в регистры новые аргументы, необходимо предварительно сохранить старые в оперативной памяти. А раз так – не проще ли сразу передать аргументы через оперативную память, не мучаясь с регистрами?
Подавляющее большинство компиляторов передает аргументы через стек. Единого мнения по вопросам передачи у разработчиков компиляторов нет и встречаются по крайней мере два различных механизма, именуемые соглашениями "Си" и "Паскаль".
::Си-соглашение предписывает заталкивать в стек аргументы справа на лево, т.е. самый первый аргумент функции заносится в стек последним и оказывается на его верхушке. Удаление аргументов из стека возложено не на саму функцию, а на вызываемый ее код. Это довольно расточительное решение, т.к. каждый вызов функции утяжеляет программу на несколько байт кода, но зато оно это позволяет создавать функции с переменным числом аргументов – ведь удаляет-то их из стека не сама функция, а вызывающий ее код, который наверняка знает точное количество переданных аргументов.
Очистка стека обычно выполняется командой "ADD ESP,xxx" – где 'xxx' количество удаляемых байт. Поскольку, в 32-разрядном режиме каждый аргумент, как правило, занимает четыре байта, количество аргументов функции вычисляется так: . Оптимизирующие компиляторы могут использовать более хитрый код – для очистки стека от нескольких аргументов они частенько из "выталкивают" в неиспользуемые регистры командой "POP" или и вовсе очищают стек не сразу же после выхода из функции, а совсем в другом месте – где это удобнее компилятору.
::Паскаль-соглашение предписывает заносить аргументы в стек слева на право, т.е. самый первый аргумент функции заносится в стек в первую очередь и оказывается в самом его "низу". Удаление аргументов из функции возложено на саму функцию, и обычно осуществляется командой "RET xxx" – т.е. возврат из подпрограммы со снятием xxx байт со стека.
Возвращаемое функцией значение в обоих соглашениях передается через регистр EAX (или EDX:EAX при возвращении 64-разрядных переменных).
Поскольку, исследуемая нами программа написана на Си и, стало быть, заносит аргументы справа налево, ее исходный текст выглядел приблизительно так:
(*0x4010A0) (ebp-68, "myGOODpassword")
В том, что аргументов именно два, а не, скажем, четные или десять, нас убеждает команда "ADD ESP,8", расположенная вслед за CALL.
Остается выяснить назначение функции 0x4010A0, хотя… если поднапрячь свою интуицию этого можно и не делать.! И так ясно – это функция сравнивает пароль, иначе, зачем бы ей его передавали? Как она это делает – вопрос десятый, а вот что нас действительно интересует – возвращенное ею значение. Так, опускаемся на одну строчку ниже:
Что мы видим? Команда TEST EAX,EAX проверяет возвращенное функцией значение на равенство нулю, и если оно действительно равно нулю следующая за ней команда JE совершает прыжок на 0x401096 строку.
В противном же случае (т.е. если EAX !=0)…
0040105A: 68 50 60 40 00 push 406050h
Похоже еще на один указатель. Не правда ли? Проверим это предположение, заглянув в сегмент данных:
Уже теплее! Указатель вывел нас на строку "Wrong password", очевидно выводимую следующей функцией на экран. Значит, ненулевое значение EAX свидетельствует о ложном пароле, а нуль – об истинном.
О'кей, тогда переходим к анализу валидной ветви программы…
Так еще, один указатель. Ну, а с функцией 0x401234 мы уже встречались выше – она (предположительно) служит для вывода строк на экран. Ну а сами строки можно отыскать в сегменте данных. На этот раз там притаилась "Password OK"
Оперативные соображения следующие: если заменить команду JE на JNE, то программа отвергнет истинный пароль, как неправильный, а любой неправильный пароль воспримет как истинный. А если заменить "TEST EAX,EAX" на "XOR EAX,EAX", то после исполнения этой команды регистр EAX будет всегда равен нулю, какой бы пароль не вводился.
Дело за малым – найти эти самые байтики в исполняемом файле и малость поправить их.
Шаг третий. Хирургический
Не торопитесь на встречу с Богом, еще встретитесь.
Народная мудрость
Внесение изменений непосредственно в исполняемый файл – дело серьезное. Стиснутыми уже существующим кодом, нам приходится довольствоваться только тем, что есть – и ни раздвинуть команды, ни даже "сдвинуть" их, выкинув из защиты "лишние запчасти", не получится. Ведь это привело бы к "сдвигу" смещений всех остальных команд, тогда как значения указателей и адресов переходов останутся остались без изменений , – они будут и стали указывать совсем не туда, куда нужно!
Ну, с "выкидываем запчастей" справится как раз таки просто – достаточно забить код командами NOP (опкод который 0x90, а вовсе не 0х0, как почему-то думают многие начинающие кодокопатели) – т.е. пустой операцией (вообще-то NOP это просто другая форма записи инструкции XCHG EAX,EAX – если интересно). С "раздвижкой" куда сложнее! К счастью, в WindowsPE-файлах всегда присутствует множество "дыр", оставшихся от выравнивания – в них-то и можно разместить свой код или данные.
Но не проще ли просто откомпилировать ассемблированный файл, предварительно внеся в него требуемые изменения? Нет, не проще, и вот почему – если ассемблер не распознает указатели, передаваемые функции (а, как мы видели, наш дизассемблер не смог отличить их от констант), он, соответственно, не позаботится должным образом их скорректировать и, естественно, программа работать не будет.
Приходится "резать" программу в "живую". Легче всего это сделать с помощью утилиты HIEW, "переваривающей" PE-формат файлов и упрощающей тем самым поиск нужного фрагмента. Запустим его, указав имя файла в командной строке "hiew simple.exe", двойным нажатием переключимся в режим ассемблера и по перейдем к требуемому адресу. Как мы помним, команда "TEST", проверяющая результат, возвращенный функцией на равенство нулю, располагалась по адресу 0x401056.
Чтобы HIEW мог отличить адрес от смещения в самом файле, предварим его символом точки: ".401056"
00401056: 85C0 test eax,eax
00401058: 740F je .000401069 -------- (1)
Ага, как раз то, что нам надо! Нажмем для перевода HIEW в режим правки, подведем курсор к команде "TEST EAX,EAX" и, нажав , заменим ее на "XOR EAX,EAX".
00001056: 33C0 xor eax,eax
00001058: 740F je 000001069
С удовлетворением заметив, что новая команда в аккурат вписалась в предыдущую, нажмем для сохранения изменений на диске, а затем выйдет из HIEW и попробуем запустить программу, вводя первый пришедший на ум пароль.
>simple.exe
Enter password:Привет, шляпа!
Password OK
Получилось! Защита пала! Хорошо, а как бы мы действовали, не умей HIEW "переваривать" PE-файлы? Тогда бы пришлось прибегнуть к контекстному поиску. Обратим свой взор на шестнадцатеричный дамп, расположенный дизассемблером слева от ассемблерных команд. Конечно, если пытаться найти последовательность "85 C0" – код команды "TEST EAX,EAX" ничего путного из этого не выйдет, – этих самых TEST-ов в программе может быть несколько сотен, а то и больше. Комбинация "ADD ESP,8\TEST EAX,EAX" так же вряд ли будет уникальна, поскольку встречается во многих типовых конструкциях языка Си "if (func(arg1,arg2))…", "if (!func(arg1,arg2))…", "while(func(arg1,arg2)" и т.д. А вот адрес перехода, скорее всего, во всех ветках программы различен и подстрока "ADD ESP,8/TEST EAX,EAX/JE 00401069" имеет хорошие шансы на уникальность. Попробуем найти в файле соответствующий ей код: "83 C4 08 85 C0 74 0F" (в HIEW-е для этого достаточно нажать ).
Опп-с! Найдено только одно вхождение, что нам собственно и нужно. Давайте теперь попробуем модифицировать файл непосредственно в hex-режиме, не переходя в ассемблер. Попутно возьмем себе на заметку – инверсия младшего бита кода команды приводит к изменению условия перехода на противоположное. Т.е. 74 JE 75 JNE.
Работает? (В смысле защита свихнулась окончательно – не признает истинные пароли, зато радостно приветствует остальные). Замечательно! Остается решить: как эту взломанную программу распространять. То есть, распространить-то ее дело не хитрое – на то и существуют CDR-писцы, BBS-ы, сеть Интернет, наконец! Заливай, пиши, нарезай – не хочу. Не хотите – и правильно! Незаконное это дело – распространять программное обеспечение в обход его владельца. Эдак, и засадить могут (причем прецеденты уже имеются). Куда безопаснее возложить распространение программы на ее дистрибьюторов, но до каждого пользователя донести: как эту программу сломать. Ковыряться в законном образом приобретенном приложении потребитель вправе, а распространение информации о взломе не запрещено в силу закона о свободе информации. Правда, при ближайшем рассмотрении выясняется, что этот закон и у нас, и за океаном действует лишь формально, и, если не посадить, то по крайне мере попытаться это сделать, право охранительные органы вполне могут (и не только могут, но и делают). Когда дело касается чьих-то финансовых интересов, правосудие "отдыхает". Наивно думать, что соблюдение закона автоматически дает некие гарантии. Нет, и еще раз нет! Чувствовать себя в относительной безопасности можно лишь при условии соблюдения кодекса "да не навреди сильным мира сего".
В любом случае – информация о взломе это не совсем то же, что сам взлом и за это труднее привлечь к ответственности. Единственная проблема – попробуй-ка, объясни этим пользователям: как пользоваться hex-редактором и искать в нем такие-то байтики. Запорют же ведь файл за милую душу! Вот для этой цели и существуют автоматические взломщики.
Для начала нужно установить, какие именно байты были изменены. Для этого нам вновь потребуется оригинальная копия модифицированного файла (предусмотрительно сохраненная перед его правкой) и какой-нибудь "сравниватель" файлов. Наиболее популярными на сегодняшний день являются c2u by Professor Nimnul и MakeCrk by Doctor Stein's labs. Первый гораздо предпочтительнее, т.к. он не только более точно придерживается наиболее популярного "стандарта", но и умеет генерировать расширенный xck формат. На худой конец можно воспользоваться и штатной утилитой, входящей в поставку MS-DOS\Windows – fc.exe (сокращение от FileCompare).
Запустим свой любимый компаратор (это уж какой кому больше по душе) и посмотрим на результат его работы:
> fc simple.exe simple.ex_ > simple.dif
^-оригинальный ^ файл ^
└- хакнутыйфайл
└- файл различий
> type simple.dif
Сравнение файлов simple.exe и SIMPLE.EX_
00001058: 74 75
Первая слева колонка указывает смещение байта от начала файла, вторая – содержимое байта оригинального файла, а третья – его значение после модификации. Теперь сравним это с отчетом утилиты c2u:
>c2u simple.exe simple.ex_
Все исправления заносятся в файл *.crx, где "*" – имя оригинального файла. Рассмотрим результат сравнения поближе:
>type simple.crx
[BeginXCK]───────────────────────────────────
■ Description : $) 1996 by Professor Nimnul
■ Crack subject :
■ Used packer : None/UnKn0wN/WWPACK/PKLITE/AINEXE/DIET/EXEPACK/PRO-PACK/LZEXE
■ Used unpacker : None/UNP/X-TRACT/iNTRUDER/AUT0Hack/CUP/TR0N
■ Comments :
■ Target OS : D0S/WiN/WNT/W95/0S¤/UNX
■ Protection : [███▓░░░░░░░░░░░░░░░░] %17
■ Type of hack : Bit hack/JMP Correction
■ Language : UnKn0wN/Turbo/Borland/Quick/MS/Visual C/C++/Pascal/Assembler
■ Size : 28672
■ Price : $000
■ Used tools : TD386 v3.2, HiEW 5.13, C2U/486 v0.10
■ Time for hack : 00:00:00
■ Crack made at : 21-07-2001 12:34:21
■ Under Music : iR0N MAiDEN
[BeginCRA]───────────────────────────────────
Difference(s) between simple.exe & simple.ex_
SIMPLE.EXE
00001058: 74 75
[EndCRA]─────────────────────────────────────
[EndXCK]─────────────────────────────────────
Собственно, сам результат сравнений ничуть не изменился, разве что к файлу добавился текстовой заголовок, поясняющий, что это за серверный олень такой и с чем его едят. Все поля не стандартизированы, и их набор сильно разнится от одного взломщика к другому, – при желании вы можете снабдить заголовок своими собственными полями или же, напротив, выкинуть из него чужие. Однако не стоит злоупотреблять этим без серьезной необходимости и лучше придерживаться какого-то одного шаблона.
Итак. "Description" – пояснение к взлому, заполняемое в меру буйства фантазии и уровня распущенности. В нашем случае оно может выглядеть, например, так: "Тестовой взлом N1".
"Crack subject" – предмет крака, - т.е. что собственно мы только что сломали. Пишем "Парольная защита simple.exe"
"Used packer" – используемый упаковщик. Еще во времена старушки MS-DOS существовали и были широко распространены упаковщики исполняемых файлов, автоматически разжимающие файл в памяти при его запуске. Этим достигалась экономия дискового пространства (вспомните: какими смехотворными по нынешним временам были размеры винчестеров конца восьмидесятых-начала девяностых?) и параллельно с этим усиливалась защита – ведь упакованный файл недоступен для непосредственного изучения, а тем более – правки. Прежде, чем начать что-то делать, файл необходимо распаковать, причем это делать приходится и самому ломателю, и всем пользователям этого crk-файла. Поскольку, наш файл не был упакован – оставим это поле пустым, или запишем в него "None".
"Used unpacker" – рекомендуемый распаковщик (если он необходим). Дело в том, что не все распаковщики одинаковы, многие упаковщики весьма продвинуты в технике защиты и умело сопротивляются попыткам их "снять". Понятное дело, распаковщики то же не лыком шиты, и держат своих "тузов" в рукавах, но… автоматическая распаковка – штука капризная. Бывает "интеллектуальный" unpacker легко расправляется со всеми "крутыми" packer-ми, но тихо сдыхает на простых защитах, и, соответственно, случается и наоборот. Дабы не мучить пользователей утомительным перебором всех имеющихся у них распаковщиков (пользователь – он ведь то же человек!) правила хорошо тона обязывают указывать по крайней мере один заведомо подходящий unpacker, а лучше – два или три сразу (вдруг какого-то из них у пользователя и не будет). Если же распаковщик не требуется – оставляйте это поле пустым или "None".
"Comments" – комментарии. Вообще-то это поле задумано для перечисления дополнительных действий, которые пользователь должен выполнить перед взломом, ну, например, снять с файла атрибут "системный" или, напротив, установить его. Но, поскольку, какие-либо дополнительные действия требуются только в экзотических случаях, в это поле обычно помещают разнообразные лозунги и комментарии (да, правильно, бывает и нецензурную брань по поводу умственных способностей разработчика защиты).
"Target OS" – операционная система для которой предназначен и (внимание!) в которой хакер тестировал сломанный продукт. Вовсе не факт, что программа сохранит после взлома черты своей прежней совместимости. Так, например, поле контрольной суммы Win 9x всегда игнорирует, а Win NT – нет и если его не скорректировать, файл запускаться не будет! В нашем случае контрольная сумма заголовка PE-файла равна нулю (так ведет себя компилятор), что означает – целостность файла не проверяется и он, после хака, будет успешно работать как под Win 9x, так и под Win NT.
"Protection" – степень "крутизны" защиты, выражаемой в процентах. 100% по идее соответствуют пределу интеллектуальных возможностей хакера – но кто же в этом захочет признаваться? Неудивительно, что "крутизну" защиты обычно занижают, порой даже больше, чем на порядок (смотрите все, вот я какой крутой хакер, для меня что угодно взломать не сложнее чем кончик хвоста обмочить!). Нечестность – не порок, но…
"Type of hack" – тип хака, - поле полезное, скорее для других хакеров, чем для пользователей, ничего не смыслящих в защитах и типах их взлома. Впрочем, с типами взломов не все гладко и у самих хакеров – общепризнанных классификацией нет. Наиболее употребляемый термин "bit-hack", как и следует из его названия, обозначает взлом посредством изменения одного или нескольких бит в одном или нескольких байтах. Частный случай bit-hack-а – JMP correction (jumping) – модификация адреса или условия перехода (то, что мы только что и проделали). "NOPing" – это bit-hack с заменой прежних инструкций на команду NOP или вставку незначащих команд, например для затирания двухбайтового JZ xxx можно применить сочетание однобайтовых INC EAX/DEC EAX.
"Language" – язык, а точнее компилятор, на котором написана программа. В нашем случае Microsoft Visual C++ (мы это знаем, поскольку только что ее компилировали), а вот как быть с чужими программами? Первое, что приходит на ум, – поискать в файле копирайты – их оставляют очень многие компиляторы, в том числе и Visual C++ - сморите: "000053d9:Microsoft Visual C++ Runtime Library". Если же компиляторов нет, то пробуем прогнать файл через IDA – она автоматически распознает большинство стандартных библиотек даже с указанием конкретной версии. В крайнем случае – пробует определить язык по самому коду, вспоминая о соглашениях Си и Паскаль, и пытаясь найти знакомые черты известных вам компиляторов (у каждого компилятора свой "почерк" и опытный хакер можно узнать не только чем компилировалась программа, но даже определить ключ оптимизации).
"Size" – размер ломаемой программы, служащий для контроля версии (чаще всего, хотя и не всегда, каждая версия программы имеет свой размер). Размер автоматически определяется утилитой c2u и самостоятельно его вставлять нет никакой нужды.
"Price" – стоимость лицензионной копии программы (должен же пользовать знать: сколько денег ему сэкономитсэкономил этот крак!)
"Used tools" – используемые инструменты. Не заполнение этого поля считается дурным тоном – действительно же, интересно, чем именно была хакнута программа! Особенно этим интересуются пользователи, наивно полагающие, что если они раздобудут тот же DUMPBIN и HIEW защита сама собой сломается.
"Time for hack" – время, затраченное на хак, включая перерывы на "перекурить" и "сходить водички попить". Интересно, какой процент людей честно заполняет это поле, не пытаясь показаться "куче" в чужих глазах. Так что особенно доверять ему не следует…
"Crack made at" – дата завершения крака. Подставляется автоматически и править ее нет необходимости (разве что вы "жаворонок" и хотите выдать себя за "сову", проставляя время окончания взлома 3 часами ночи 31 декабря)
"Under Music" – музыка, прослушиваемая во время хака (еще не хватает поля "Имя любимого хомячка"). Вы слушали музыку во время хака? Если да – то пишете – пусть все знают ваши вкусы (за одно не забудьте цвет майки и температуру воздуха за ботом выше нуля).
В результате всех мучений у нас должно получится приблизительно следующее:
[BeginXCK]───────────────────────────────────
■ Description : Тестовый взлом №1
■ Crack subject : Парольная защита simple.exe
■ Used packer : None
■ Used unpacker : None
■ Comments : Hello, Sailor! Ты слишклм долго плавал!
■ Target OS : WNT/W95
■ Protection : [█░░░░░░░░░░░░░░░░░░░] %1
■ Type of hack : JMP Correction
■ Language : Visual C/C++
■ Size : 28672
■ Price : $000
■ Used tools : DUMPBIN, HiEW 6.05, C2U/486 v0.10 & Brain
■ Time for hack : 00:10:00
■ Crack made at : 21-07-2001 12:34:21
■ Under Music : Paul Mauriat L'Ete Indeien "Africa"
[BeginCRA]───────────────────────────────────
Difference(s) between simple.exe & simple.ex_
SIMPLE.EXE
00001058: 74 75
[EndCRA]─────────────────────────────────────
[EndXCK]─────────────────────────────────────
Теперь нам потребуется другая утилита, цель которой прямо противоположна: используя crk (xcrk) файл, изменить эти самые байты в оригинальной программе. Таких утилит на сегодняшний день очень много, что не лучшим образом сказывается на их совместимости с различными crk форматами. Самые известные из них, – cra386 by Professor и pcracker by Doctor Stein's labs.
Из современных Windows-разработок можно отметить "Patch maker" с продвинутым пользовательским интерфейсом (см. Рисунок 2). Он включает в себя сравниватель файлов, crk-редактор, hex-редактор (для ручной замены?) и компилятор crk в исполняемые файлы, чтобы пользователям не приходилось ломать голову: что это за крак такой и как им ломать.
Может, кому-то такой интерфейс и понравится, а вот хакеры в свой массе мышь органически не переносят и любят текстовые (консольные) приложения и тетю Клаву.
Рисунок 2 0x001 Patch Maker за работой!
Шаг четвертый. Знакомство с отладчиком
Оставь свои мозги за дверью и внеси сюда только тело
Фредерик Тейлор
Помимо дизассемблирования существует и другой способ программ – отладка. Изначально под отладкой понималось пошаговое исполнение кода, так же называемое трассировкой. Сегодня же программы распухли настолько, что трассировать их бессмысленно – вы тут же утоните в омуте вложенных процедур, так и не поняв, что они собственно делают. Отладчик – не лучше средство изучения алгоритма программы – с этим лучше справляется интерактивный дизассемблер (например, IDA).
Подробный разговор об устройстве отладчика мы отложим на потом (см. "Приемы против отладчиков"), а здесь ограничимся лишь перечнем основных функциональных возможностей типовых отладчиков (без этого невозможно их осмысленное применение):
– отслеживание обращений на запись/чтение/исполнение к заданной ячейке (региону) памяти, далее по тексту именуемое "бряком" ("брейком");
– отслеживание обращений на запись/чтение к портам ввода-вывода (уже не актуально для современных операционных систем, запрещающих пользовательским приложениям проделывать такие трюки – это теперь прерогатива драйверов, а очень на уровне драйверов реализованы очень немногие защиты);
– отслеживание загрузки DLL и вызова из них таких-то функций, включая системные компоненты (как мы увидим далее – это основное оружие современного взломщика);
– отслеживание вызова программных/аппаратных прерываний (большей частью уже не актуально, - не так много защит балуется с прерываниями);
Как именно делает отладчик – пока знать необязательно, достаточно знать, что он это умеет и все. Куда актуальнее вопрос, – какой отладчик умеет это делать? Широко известный в пользовательских кругах Turbo Debugger на само деле очень примитивный и никчемный отладчик – очень мало хакеров им что-то ломает.
Самое мощное и универсальное средство – Soft-Ice, сейчас доступный для всех Windows-платформ (а когда он поддерживал лишь одну Windows 95, но не Windows NT). Последняя на момент написания книги, четвертая версия, не очень-то стабильно работалает с моим видеоадаптером, поэтому пришлось приходится ограничитваться более ранней, но зато устойчивой версией 3.25.
Способ 0. Бряк на оригинальный пароль.
Используя поставляемую вместе с "Айсом" утилиту "wldr" загрузим ломаемый нами файл, указав его имя в командной строке, например, так:
>wldr simple.exe
Да, я знаю, что wldr – 16-разрядный загрузчик, и NuMega рекомендует использовать его 32-разрядную версию loadrer32, специально разработанную для Win 9x\NT. Это так, но loader32 частенько глючит (в частности не всегда останавливается на первой строчке запускаемой программы), а wldr успешно работает и 32-разрядными приложениями, единственный присущий ему недостаток – отсутствие поддержки длинных имен файлов.
Если отладчик настроен корректно, на экране появится черное текстовое окно, обычно вызывающее большое удивление у начинающих – это в нашу-то это эпоху визуальщины серый текст и командный язык a la command.com.! А почему бы и нет? Набрать на клавиатуре нужную команду куда быстрее, чем отыскать ее в длинной веренице вложенных меню, мучительно вспоминая где же вы ее в последний раз видели. К тому же язык – это естественное средство выражения мыслей, а меню – оно годится разве что для выбора блюд в ресторане. Вот хороший пример – попробуйте с помощью проводника Windows вывести на печать список файлов такой-то директории. Не получается? А в MS-DOS это было так просто dir >PRN и никаких лаптей!
Если в окне кода видны одни лишь инструкции "INVALID" (а оно так и будет) не пугайтесь – просто Windows еще не успела спроецировать исполняемый файл в память и выделилавыделить ему страницы. Стоит нажать (аналог команды "P" – трассировка без заходов в функцию) или (аналог команды "T" – трассировка с заходами в функции) как все сразу же станет на свои места.
Обратите внимание: в отличие от дизассемблера DUMPBIN, Айс распознает имена системных функций, чем существенно упрощает анализ. Впрочем, анализировать всю программу целиком, нет никакой нужды. Давайте попробуем наскоро найти защитный механизм, и, не вникая в подробности его функционирования, напрочь отрубить защиту. Легко сказать, но сделать еще проще! Вспомним: по какому адресу расположен в памяти оригинальный пароль. Э… что-то плохо у нас с этим получается – то ли память битая, то ли медведь на лапоть наступил, но точный адрес никак не хочет вспоминаться. Не хочет – не надо. Найдем-ка мы его самостоятельно!
В этом нам поможет команда "map32" выдающая карту памяти выбранного модуля (наш модуль называется "simple" – по имени исполняемого файла за вычетом расширения).
Вот он, адрес начала секции ".data". То, что пароль находится в секции ".data", надеюсь, читатель все еще помнит. Даем команду "d 23:406000" (возможно предварительно придется создать окно командой "wc" – если окна данных нет) и, нажав, для перехода в это окно, прокрутим его содержимое <стрелкой вниз> или кирпичом на . Впрочем, кирпич излишен, – долго искать не придется:
Есть контакт! Задумаемся еще раз (второй раз за этот день) чтобы проверить корректность введенного пользователем пароля защита, очевидно должна сравнить его с оригинальным. А раз так – установив точку останова на чтение памяти по адресу 0x406040, мы поймаем "за хвост" сравнивающий механизм. Сказано – сделано.
:bmpm 406040
Теперь нажимаем для выхода из отладчика (или отдаем команду "x") и вводим любой пришедший на ум пароль, например, "KPNC++". Отладчик "всплывает" незамедлительно:
Break due to BPMB #0023:00406040 RW DR3 (ET=752.27 milliseconds)
MSR LastBranchFromIp=0040104E
MSR LastBranchToIp=004010A0
В силу архитектурных особенностей процессоров Intel, бряк срабатывает после инструкции, выполнившей "поползновение", т.е. CS:EIP указывают на следующую выполняемую команду. В нашем случае – JNZ 004010E4, а к памяти, стало быть, обратилась инструкция CMP AL, [ECX]. А что находится в AL? Поднимаем взгляд еще строкой выше – "MOV EAX,[EDX]". Можно предположить, что EСX содержит указатель на строку оригинального пароля (поскольку он вызвал всплытие отладчика), а EDX в таком случае – указатель на введенный пользователем пароль. Проверим наше предположение.
И правда – догадка оказалась верна. Теперь вопрос – а как это заломить? Вот, скажем, JNZ можно поменять на JZ или, еще оригинальнее, заменить EDX на ECX – тогда оригинальный пароль будет сравниваться сам с собой! Погодите, погодите… не стоит так спешить! А что если мы находится не в теле защиты, а в библиотечной функции (действительно, мы находится в теле strcmp), – ее изменение приведет к тому, что программа любые строки будет воспринимать как идентичные. Любые – а не только строки пароля. Это не повредит нашему примеру, где strcmp вызывалась лишь однажды, но завалит нормальное полнофункциональное приложение. Что же делать?
Выйти из strcmp и подкорректировать тот самый "IF", который анализирует правильный – не правильный пароль. Для этого служит команда "P RET" (трассировать пока не встреться ret – инструкция возврата из функции).
Знакомые места! Помните, мы их посещали дизассемблером? Алгоритм действий прежний – запоминаем адрес команды "TEST" для последующей замены ее на "XOR" или записываем последовательность байт, идентифицирующую… эй, постойте, а где же наши байты – шестнадцатеричное представление команд? Коварный Айс по умолчанию их не выводит, и заставить его это делать помогает команда "CODE ON"
Вот, теперь совсем другое дело! Но можно ли быть уверенным, что эти байтики по этим самым адресам будут находиться в исполняемом файле? Вопрос не так глуп, каким кажется на первый взгляд. Попробуйте сломать описанным выше методом пример "crackme0x03". На первый взгляд он очень похож на simple.exe, - даже оригинальный пароль располагается по тому же самому адресу. Ставим на него бряк, дожидаемся всплытия отладчика, выходим из сравнивающей процедуры и попадаем на точно такой же код, который уже встречался нам ранее.
Сейчас мы запустим HIEW, перейдем по адресу 0x421053 и… эй, постой, HIEW ругается и говорит, что в файле нет такого адреса! Последний байт заканчивается на 0x407FFF. Быть может, мы находимся в теле системной функции Windows? Но нет – системные функции Windows расположены значительно выше – начиная с адреса 0x80000000.
Фокус весь в том, что PE-файл может быть загружен по адресу отличному от того, для которого он был создан (это свойство называется перемещаемостью), - при этом система автоматически корректирует все ссылки на абсолютные адреса, заменяя их новыми значениями. В результате – образ файла в памяти не будет соответствовать тому, что записано на диске. Хорошенькое начало! Как же теперь найти место, которое нужно править?
Задачу несколько облегчает тот факт, что системный загрузчик умеет перемещать только DLL, а исполняемые файлы всегда пытается загрузить по "родному" для них адресу. Если же это невозможно – загрузка прерывается с выдачей сообщения об ошибке. Выходит, мы имеем дело с DLL, загруженной исследуемой нами защитой. Хм… вроде бы не должно быть здесь никаких DLL – да и откуда бы им взяться?
Что ж, изучим листинг 2 на предмет выяснения: как же он работает.
if (strcmp(&buff[0],PASSWORD))
printf("Wrong password\n");
else break;
if (++count>2) return -1;
}
printf("Password OK\n");
}
main()
{
HMODULE hmod;
void (*zzz)();
if ((hmod=LoadLibrary("crack0~1.exe"))
&& (zzz=(void (*)())GetProcAddress(h,"Demo")))
zzz();
}
Листинг 2 Исходный текст защиты crackme 0x3
Какой, однако, извращенный способ вызова функции! Защита экспортирует ее непосредственно из самого исполняемого файла и этот же файл загружает как DLL (да, один и тот же файл может быть одновременно и исполняемым приложением и динамической библиотекой!).
"Все равно ничего не сходится", - возразит программист средней квалификации, - "всем же известно, что Windows не настолько глупа, чтобы дважды грузить один и тот же файл, - LoadLibrary всего лишь возвратит базовый адрес модуля crackme0x03, но не станет выделять для него память". А вот как бы не так! Хитрая защита обращается к файлу по его альтернативному короткому имени, вводя системный загрузчик в глубокое заблуждение!
Система выделяет память и возвращает базовый адрес загружаемого модуля в переменной hmod. Очевидно, код и данные этого модуля смещены на расстояние hmod – base, где base – базовый адрес модуля – тот, с которым работают HIEW и дизассемблер. Базовый адрес узнать нетрудно – достаточно вызвать тот же DUMPBIN с ключом "/HEADERS" (его ответ приведен в сокращенном виде)
Значит, базовый адрес – 0x400000 (в байтах). А опередить адрес загрузки можно командой "mod -u" отладчика: (ключ u разрешает выводить только прикладные, т.е. не системные модули).
:mod -u
hMod Base PEHeader Module Name File Name
00400000 004000D8 crack0x0 \.PHCK\src\crack0x03.exe
00420000 004200D8 crack0x0 \.PHCK\src\crack0x03.exe
^^^^^^^^
77E80000 77E800D0 kernel32 \WINNT\system32\kernel32.dll
77F80000 77F800C0 ntdll \WINNT\system32\ntdll.dll
Смотрите, загружено сразу две копии crack0x03, причем последняя расположена по адресу 0x420000, как раз что нам надо! Теперь нетрудно посчитать, что адрес 0x421056 (тот, что мы пытались последний раз найти в ломаемом файле) "на диске" будет соответствовать адресу 0x421056 – (0x42000 – 0x400000) == 0x421056 – 0x20000 == 0x401056. Смотрим:
00401056: 85C0 test eax,eax
00401058: 740F je .000401069 -------- (1)
Все верно – посмотрите, как хорошо это совпадает с дампом отладчика:
001B:00421056 85C0 TEST EAX,EAX
001B:00421058 740F JZ 00421069
Разумеется, описанная методика вычислений применима к любым DLL, а не только тем, что представляют собой исполняемый файл.
А вот, если бы мы пошли не путем адресов, а попытались найти в ломаемой программе срисованную с отладчика последовательность байт, включая и ту часть, которая входит в CALL 00422040 – интересно, нашли бы мы ее или нет?
001B:0042104E E87D000000 CALL 004210D0
001B:00421053 83C408 ADD ESP,08
001B:00421056 85C0 TEST EAX,EAX
001B:00421058 740F JZ 00421069
:Образ файла в памяти.
.0040104E: E87D000000 call .0004010D0 -------- (1)
.00401053: 83C408 add esp,008 ;"◘"
.00401056: 85C0 test eax,eax
.00401058: 740F je .000401069 -------- (2)
:Образ файла на диске
Вот это новость – командам CALL 0x4210D0 и CALL 0x4010D0 соответствует один и тот же машинный код – E8 7D 00 00 00! Как же такое может быть?! А вот как – аргумент операнд процессорной инструкции "0xE8" представляет собой не смещение подпрограммы, а разницу смещений подпрограммы и инструкции, следующей за командой call. Т.е. в первом случае: 0x421053 (смещение инструкции, следующей за CALL) + 0x0000007D (не забываем об обратном порядке байтов в двойном слове) == 0x4210D0, - вот он, искомый адрес. Таким образом, при изменении адреса загрузки, коррекция кодов команд CALL не требуется.
"Оценка по аналогии основывается на предположении, что если два или более объекта согласуются друг с другом в некоторых отношениях, то они, вероятно, согласуются и в других отношениях"
Ганс Селье "От мечты к открытию"
Рассуждения по аналогии – опасная штука. Увлеченные стройностью аналогии мы подчас даже не задумываемся о проверке. Между тем, аналогии лгут чаще, чем этого хотелось бы.
В примере crack0x03 среди прочего кода есть и такая строка (найдите ее с помощью hiew):
004012C5: 89154C694000 mov [00040694C],edx
Легко видеть, что команда MOV обращается к ячейке не по относительному, а по абсолютному адресу. Вопрос: а) выясните, что произойдет при изменении адреса загрузки модуля; б) как вы думаете – будет ли теперь совпадать образ файла на диске и в памяти?
Заглянув отладчиком по адресу 0x4212C5 (0x4012C5 + 0x2000) мы увидим, что обращение идет совсем не к ячейке 0x42694C, а – 0x40694C! Наш модуль самым бессовестным образом вторгается в чужие владения, модифицируя их по своему усмотрению. Так и до краха системы докатиться недолго! В данном случае этого не происходит только потому, что искомая строка расположена в Startup-процедуре (стартовом коде) и выполняется лишь однажды – при запуске приложения, а из загруженного модуля не вызывается.
Другое дело, если бы функция Demo() обращалась к какой-нибудь статической переменной – компилятор, подставив ее непосредственное смещение, сделал бы модуль неперемещаемым! После сказанного становится непонятно: как же тогда ухитряются работать динамически подключаемые библиотеки (DLL), адрес загрузки которых заранее неизвестен? Поразмыслив некоторое время, мы найдем, по крайней мере, два решения проблемы:
Первое – вместо непосредственной адресации использовать относительную, например: [reg+offset_val], где reg – регистр, содержащий базовый адрес загрузки, а offset_val – смещение ячейки от начала модуля. Это позволит модулю грузится по любому адресу, но заметно снизит производительность программы уже хотя бы за счет потери одного регистра….
Второе – научить загрузчик корректировать непосредственные смещения в соответствии с выбранным базовым адресом загрузки. Это, конечно, несколько замедлит загрузку, но зато не ухудшит быстродействие самой программы. Не факт, что временем загрузки можно свободно пренебречь, но парни из Microsoft выбрали именно этот способ.
Единственная проблема – как отличить действительные непосредственные смещения от констант, совпадающих с ними по значению? Не дизассемблировать же в самом деле DLL, чтобы разобраться какие именно ячейки в ней необходимо "подкрутить"? Верно, куда проще перечислить их адреса в специальной таблице, расположенной непосредственно в загружаемом файле и носящей гордое имя "Таблицы перемещаемых элементов" или (Relocation [Fix Up] table по-английски). За ее формирование отвечает линкер (он же – компоновщик) и такая таблица присутствует в каждой DLL.
Чтобы познакомиться с ней поближе откомпилируем и изучим следующий пример:
Таблица перемещаемых элементов-то не пуста! И первая же ее запись указывает на ячейку 0x100001007, полученную алгебраическим сложением смещения 0x7 с RVA-адресом 0x1000 и базовым адресом загрузки 0x10000000 (получите его с помощью DUMPBIN самостоятельно). Смотрим – ячейка 0x100001007 принадлежит инструкции "MOV [0x10005030],EAX" и указывает на самый старший байт непосредственного смещения. Вот это самое смещение и корректирует загрузчик в ходе подключения динамической библиотеки (разумеется, если в этом есть необходимость).
Хотите проверить? Пожалуйста, - создадим две копии одной DLL (например, copy fixupdemo.dll fixupdemo2.dll) и загрузим их поочередной следующей программой:
::fixupload.c
#include
main()
{
void (*demo) (int a);
HMODULE h;
if ((h=LoadLibrary("fixupdemo.dll")) &&
(h=LoadLibrary("fixupdemo2.dll")) &&
(demo=(void (*)(int a))GetProcAddress(h,"meme")))
demo(0x777);
}
> cl fixupload
Листинг 4 Исходный текст fixupload
Поскольку, по одному и тому же адресу две различные DLL не загрузишь (откуда же системе знать, что это одна и та же DLL!), загрузчику приходится прибегать к ее перемещению. Загрузим откомпилированную программу в отладчик и установим точку останова на функцию LoadLibraryA. Это, – понятное дело, – необходимо чтобы пропустить Startup-код и попасть в тело функции main. (Как легко убедиться исполнение программы начинается отнюдь не с main, а со служебного кода, в котором очень легко утонуть). Но откуда взялась загадочная буква 'A' на конце имени функции? Ее происхождение тесно связано с введением в Windows поддержки уникода – специальной кодировки, каждый символ в которой кодируется двумя байтами, благодаря чему приобретает способность выражать любой из 216 = 65.536 знаков, – количество достаточно для вмещения практически всех алфавитов нашего мира. Применительно к LoadLibrary – теперь имя библиотеки может быть написано на любом языке, а при желании и на любом количестве любых языков одновременно, например, на русско-француско-китайском. Звучит заманчиво, но не ухудшает ли это производительность? Разумеется, ухудшает, еще как – уникод требует жертв! Самое обидное – в подавляющем большинстве случаев вполне достаточно старой доброй кодировки ASCII (во всяком случае нам, – русским, и американцам). Так какой же смысл бросать драгоценные такты процесса на ветер? Ради производительности было решено поступиться размером, создав отдельные варианты функций для работы с уникодом и ASCII-символами. Первые получили суффикс 'W' (от Wide – широкий), а вторые – 'A' (от ASCII). Эта тонкость скрыта от прикладных программистов – какую именно функцию вызывать 'W' или 'A' решает компилятор, но при работе с отладчиком необходимо указывать точное имя функции – самостоятельно определить суффикс он не в состоянии. Камень преткновения в том, что некоторые функции, например, ShowWindows вообще не имеют суффиксов – ни 'A', ни 'W' и их библиотечное имя совпадает с каноническим. Как же быть?
Самое простое – заглянуть в таблицу импорта препарируемого файла и отыскать там вашу функцию. Например, применительно к нашему случаю:
Из приведенного выше фрагменты видно, что LoadLibrary все-таки 'A', а вот функции ExitProcess и TerminateProcess не имеют суффиксов, поскольку вообще не работают со строками.
Другой путь – заглянуть в SDK. Конечно, библиотечное имя функций в нем отсутствует, но в "Quick Info" мимоходом приводится информация и поддержке уникода (если таковая присутствует). А раз есть уникод – есть суффиксы 'W' и 'A', соответственно, наоборот – где нет уникода, нет и суффиксов. Проверим?
Вот так выглядит Quick Info от LoadLibrary:
QuickInfo
Windows NT: Requires version 3.1 or later.
Windows: Requires Windows 95 or later.
Windows CE: Requires version 1.0 or later.
Header: Declared in winbase.h.
Import Library: Use kernel32.lib.
Unicode: Implemented as Unicode and ANSI versions on Windows NT.
На чистейшем английском языке здесь сказано – "Реализовано как Unicode и ANSI версии на Windows NT". Стоп! С NT все понятно, а как насчет "народной" девяносто восьмой (пятой)? Беглый взгляд на таблицу экспорта KERNEL32.DLL показывает: такая функция там есть, но, присмотревшись повнимательнее, мы с удивлением обнаружим, что ее точка входа совпадает с точками входа десятка других функций!
ordinal hint RVA name
556 1B3 00039031 LoadLibraryW
Третья колонка в отчете DUMPBIN это RVA-адрес – виртуальный адрес начала функции за вычетом базового адреса загрузки файла. Простой контекстный поиск показывает, что он встречается не единожды. Воспользовавшись программой-фильтром srcln (см. Приложения Исходные тексты) для получения связного протокола, мы увидим следующее:
Вот это сюрприз! Все уникодеовые – функции под одной крышей! Поскольку, трудно поверить в идентичность реализаций LoadLibraryW и, скажем, DeleteFileW, остается предположить, что мы имеем дело с "заглушкой", которая ничего не делает, а только возвращает ошибку. Следовательно, в 9x действительно, функция LoadLibraryW не реализована.
Но, вернемся, к нашим баранам от которых нам пришлось так далеко отойти. Итак, вызываем отладчик, ставим бряк на LoadLibraryA, выходим из отладчика и терпеливо дожидаемся его всплытия. Должно ждать, к счастью, не приходится…
Обратите внимание на содержимое регистра EAX – функция возвратила в нем адрес загрузки (на моем компьютере равный 0x10000000). Продолжая трассировку (), дождитесь выполнения второго вызова LoadLibraryA – не правда ли, на этот раз адрес загрузки изменился? (на моем компьютере он равен 0x0530000).
Приблизившись к вызову функции demo (в отладчике это выглядит как PUSH 00000777\ CALL [EBP-04] – "EBP-04" ни о чем не говорит, но вот аргумент 0x777 определенно что-то нам напоминает, - см. исходный текст fixupload.c), не забудьте переменить руку с на , чтобы войти внутрь функции.
Вот оно! Системный загрузчик скорректировал адрес ячейки согласно базовому адресу загрузки самой DLL. Это, конечно, хорошо, да вот проблема – в оригинальной DLL нет ни такой ячейки, ни даже последовательности "A3 30 50 53 00", в чем легко убедиться контекстным поиском. Допустим, вознамерились бы мы затереть эту команду NOP-ми. Как это сделать?! Вернее, как найти это место в оригинальной DLL?
Обратим свой взор выше, на команды, заведомо не содержащие перемещаемых элементов – PUSH EBP/MOV EBP, ESP/MOV EAX,[EBP+08]. Отчего бы не поискать последовательность "55 8B EC xxx A3"? В данном случае это сработает, но если бы перемещаемые элементы были густо перемешаны "нормальными" ничего бы не вышло. Опорная последовательность оказалась бы слишком короткой для поиска и выдала бы множество ложных срабатываний.
Более изящно и надежно вычислить истинное содержимое перемещаемых элементов, вычтя их низ разницу между действительным и рекомендуемым адресом загрузки. В данном случае: 0x535030 /модифицированный загрузчиком адрес/ – (0x530000 /базовый адрес загрузки/ - 0x10000000 /рекомендуемый адрес загрузки/) == 0x10005030. Учитывая обратный порядок следования байт, получаем, что инструкция MOV [10005030], EAX в машинном коде должна выглядеть так: "A3 30 50 00 10". Ищем ее HIEW-ом, и чудо – она есть!
Способ 1. Прямой поиск введенного пароля в памяти
Был бы омут, а черти будут.
народная поговорка
Пароль, хранящийся в теле программы открытым текстом, – скорее из ряда вон выходящее исключение, чем правило. К чему услуги хакера, если пароль и без того виден невооруженным взглядом? Поэтому, разработчики защиты всячески пытаются скрыть его от посторонних глаз (о том, как именно они это делают, мы поговорим позже). Впрочем, учитывая размер современных пакетов, программист может, не особо напрягаясь, поместить пароль в каком-нибудь завалявшемся файле, попутно снабдив его "крякушами" – строками, выглядевшими как пароль, но паролем не являющимися. Попробуй, разберись, где тут липа, а где нет, тем паче, что подходящих на эту роль строк в проекте средней величины может быть несколько сотен, а то и тысяч!
Давайте подойдем к решению проблемы от обратного – будем искать не оригинальный пароль, который нам не известен, а ту строку, которую мы скормили программе в качестве пароля. А, найдя – установим на нее бряк, и дальше все точно так же, как и раньше. Бряк всплывает на обращение по сравнению, мы выходим из сравнивающей процедуры, корректируем JMP, и…
Взглянем еще раз на исходный текст ломаемого нами примера "simple.c"
if (strcmp(&buff[0],PASSWORD))
printf("Wrong password\n");
else break;
if (++count>2) return -1;
}
Обратите внимание – в buff читается введенный пользователем пароль, сравнивается с оригиналом, затем (при неудачном сравнении) запрашивается еще раз, но (!) при этом buff не очищается! Отсюда следует, что если после выдачи ругательства "Wrong password" вызвать отладчик и пройтись по памяти контекстным поиском, можно обнаружить тот заветный buff, а остальное уже – дело техники!
Итак, приступим (мы еще не знаем, во что мы ввязываемся – но, увы – в жизни все сложнее, чем в теории). Запускам SIMPLE.EXE, вводим любой пришедший на ум пароль (например, "KPNC Kaspersky++"), пропускаем возмущенный вопль "Wrong" мимо ушей и нажимаем - "горячую" комбинацию клавиш для вызова Айса. Так, теперь будем искать? Подождите, не надо бежать впереди лошадей: Windows 9x\NT – это не Windows 3.x и, тем более, не MS-DOS с единым адресным пространством для всех процессоров. Теперь, по соображениям безопасности, - дабы один процесс ненароком не залез во владения другого, каждому из них предоставляется собственное адресное пространство. Например, у процесса A по адресу 23:0146660 может быть записано число "0x66", у процесса B по тому же самому адресу 23:0146660 может находиться "0x0", а у процесса C и вовсе третье значение. Причем, процессы А, B и C не будет даже подозревать о существовании друг друга (ну, разве что воспользуются специальными средствами межпроцессорного взаимодействия).
Подробнее обо всем этом читайте у Хелен или Рихтера, здесь же нас больше заботит другое – вызванный по отладчик "всплывает" в произвольном процессе (скорее всего Idle) и контекстный поиск в памяти ничего не даст. Необходимо насильно переключить отладчик в необходимый контекст адресного пространства и лишь затем что-то предпринимать.
Из прилагаемой к Айсу документации можно узнать, что переключение контекстов осуществляется командой ADDR, за которой следует либо имя процесса, урезанное до восьми символов, либо его PID. Узнать и то, и другое можно с помощью другой команды – PROC (В том, случае если имя процесса синтаксически неотличимо от PID, например, "123", приходится использовать PID процесса – вторая колонка цифр слева, в отчете PROC).
:addr simple
Отдаем команду "addr simple" и… ничего не происходит, даже значения регистров остаются неизменными! Не волнуйтесь – все ОК, что и подтверждает надпись 'simple' в правом нижнем углу, идентифицирующая текущий процесс. А регистры… это небольшой глюк Айса. Он них игнорирует, переключая только адреса. В частности поэтому, трассировка переключенной программы невозможна. Вот поиск – другое дело. Это – пожалуйста!
:s 23:0 L -1 "KPNC Kaspersky"
Пояснения: первый слева аргумент после s – адрес, записанный в виде "селектор: смещение". Под Windows 2000 для адресации данных и стека используется селектор номер 23, в других операционных системах он может отличаться (и отличается!). Узнать его можно загрузив любую программу, и списав содержимое регистра DS. Смещение – вообще-то, начинать поиск с нулевого смещения – идея глупая. Судя по карте памяти, здесь расположен служебный код и искомого пароля быть не может. Впрочем, это ничему не вредит, и так гораздо быстрее, чем разбираться: с какого адреса загружена программа, и откуда именно начинать поиск. Третий аргумент – "L –1" – длина региона для поиска. "-1", как нетрудно догадаться, – поиск "до победы". Далее - обратите внимание, что мы ищем не всю строку – а только ее часть ("KPNC Kaspersky++" против "KPNC Kaspersky") . Это позволяет избавиться от ложных срабатываний – Айс любит выдавать ссылки на свои внутренние буфера, содержащие шаблон поиска. Вообще-то они всегда расположены выше 0х80000000. Там – где никакой нормальный пароль "не живет", но все же будет нагляднее если по неполной подстроке находится именно наша строка.
Pattern found at 0023:00016E40 (00016E40)
Так, по крайней мере, одно вхождение уже найдено. Но вдруг в памяти есть еще несколько? Проверим это, последовательно отдавая команды "s" вплоть до выдачи сообщения "Pattern not found" или превышении адреса поиска 0x80000000.
:s
Pattern found at 0023:0013FF18 (0013FF18)
:s
Pattern found at 0023:0024069C (0024069C)
:s
Pattern found at 0023:80B83F18 (80B83F18)
Целых два вхождения, да еще одно "в уме" – итого три! Не много ли для нас, начинающих? Во-первых, неясно – вводимые пароли они, что плоятся ака кролики? Во-вторых, ну не ставить же все три точки останова. В данном случае четырех отладочных регистров процессора хватит, а как быть, если бы мы нашли десяток вхождений? Да и в трех бряках немудрено заблудиться с непривычки!
Итак – начинаем работать головой. Вхождений много, вероятнее всего потому, что при чтении ввода с клавиатуры символы сперва попадают в системные буфера, которые и дают ложные срабатывания. Звучит вполне правдоподобно, но вот как отфильтровать "помехи"?
На помощь приходит карта памяти – зная владельца региона, которому принадлежит буфер, об этом буфере очень многое можно сказать. Наскоро набив команду "map32 simple" мы получим приблизительно следующее.
Ура, держи Тигру за хвост, есть одно отождествление! Буфер на 0x16E40 принадлежит сегменту данных и, видимо, это и есть то, что нам нужно. Но не стоит спешить! Все не так просто. Поищем-ка адрес 0x16E40 в самом файле simple.exe (учитывая обратный порядок байт это будет "40 E6 01 00"):
Есть, да? Даже два раза! Посмотрим теперь, кто на него ссылается – попробуем найти в дизассемблированном тексте подстроку "16070" – адрес первого двойного слова, указывающего на наш буфер.
В общем, все ясно, за исключением загадочного указателя на указатель 0x16070. Заглянув в MSDN, где описан прототип этой функции, мы обнаружим, что "таинственный незнакомец" – указатель на структуру FILE (аргументы по Си-соглашению, как мы помним заносятся в стек справа налево). Первый член структуры FILE – указатель на буфер (файловый ввод-вывод в стандартной библиотеке Си буферизован, и размер буфера по умолчанию составляет 4 Кб). Таким образом, адрес 0x16E40 – это указатель на служебный буфер и из списка "кандидатов в мастера" мы его вычеркиваем.
Двигаемся дальше. Претендент номер два – 0x24069C. Легко видеть он выходит за пределы сегмента данных и вообще непонятно чему принадлежит. Почесав затылок, мы вспомним о такой "вкусности" Windows как куча (heap). Посмотрим, что у нас там…
:heap 32 simple
Base Id Cmmt/Psnt/Rsvd Segments Flags Process
00140000 01 0003/0003/00FD 1 00000002 simple
00240000 02 0004/0003/000C 1 00008000 simple
00300000 03 0008/0007/0008 1 00001003 simple
Ну, Тигр, давай на счастье хвост! Есть отождествление! Остается выяснить, кто выделил этот блок памяти – система под какие-то свои нужды или же сам программист. Первое, что бросается в глаза – какой-то подозрительно-странный недокументированный флаг 0x8000. Заглянув в WINNT.H можно даже найти его определение, которое, впрочем, мало чем нам поможет, разве что намекнет на системное происхождение оного.
#define HEAP_PSEUDO_TAG_FLAG 0x8000
А чтобы окончательно укрепить нашу веру, загрузим в отладчик любое подвернувшееся под лапу приложение и тут же отдадим команду "heap 32 proc_name". Смотрите – система автоматически выделяет из кучи три региона! Точь-в-точь такие, как и в нашем случае. ОК, значит, и этот кандидат ушел лесом.
Остается последний адрес – 0x13FF18. Ничего он не напоминает? Постой-ка, постой. Какое было значение ESP при загрузке?! Кажется 0x13FFC4 или около того (внимание, в Windows 9x стек расположен совершенно в другом месте, но все рассуждения справедливы и для нее – необходимо лишь помнить местоположение стека в собственной операционной системе и уметь навскидку его узнавать).
Поскольку, стек растет снизу вверх (т.е. от старших адресов к младшим), адрес 0x13FF18 явно находится в стеке, а потому очень сильно похож на наш буфер. Уверенность подогревает тот факт, что большинство программистов размешают буфера в локальных переменных, ну а локальные переменные, в свою очередь, размешаются компилятором в стеке.
Ну что, попробуем установить сюда бряк?
:bpm 23:13FF18
:x
Break due to BPMB #0023:0013FF18 RW DR3 (ET=369.65 microseconds)
MSR LastBranchFromIp=0001144F
MSR LastBranchToIp=00011156
И вот мы в теле уже хорошо нам знакомой (развивайте зрительную память!) процедуры сравнения. На всякий случай, для пущей убежденности, выведем значение указателей EDX и ECX, чтобы узнать, что с чем сравнивается:
Ну, а остальное мы уже проходили. Выходим из сравнивающей процедуры по P RET, находим условный переход, записываем его адрес (ключевую последовательность для поиска), правим исполняемый файл и все ОК.
Итак, мы познакомились с одним более или менее универсальным способом взлома защит основанных на сравнении пароля (позже мы увидим, что он так же подходит и для защит, основанных на регистрационных номерах). Его основное достоинство – простота. А недостатки… недостатков у него много.
– если программист очистит буфера после сравнения, поиск веденного пароля ничего не даст. Разве что останутся системные буфера, которые так просто не затрешь, но отследить перемещения пароля из системных буферов в локальные не так-то просто!
– ввиду изобилия служебных буферов, очень трудно определить: какой из них "настоящий". Программист же может располагать буфер и в сегменте данных (статический буфер), и в стеке (локальный буфер), и в куче, и даже выделять память низкоуровневыми вызовами типа VirtualAlloc или… да мало ли как разыграется его фантазия. В результате, под час приходится "просеивать" все найденные вхождения тупым перебором.
В качестве тренировки разберем другой пример – "crackme 01". Это то же самое, что simple.exe, только с GUI-рым интерфейсом и ключевая процедура выглядит так:
MessageBox("Password OK");
}
CDialog::OnOK();
}
Листинг 5 Исходный текст ядра защитного механизма crackme 01
Кажется, никаких сюрпризов не предвидится. Что ж, вводим пароль (как обычно "KPNC Kaspersky++"), выслушиваем "ругательство" и, до нажатия ОК, вызываем отладчик, переключаем контекст…
:s 23:0 L -1 'KPNC Kaspersky'
Pattern found at 0023:0012F9FC (0012F9FC)
:s
Pattern found at 0023:00139C78 (00139C78)
Есть два вхождения! И оба лежат в стеке. Подбросим монетку, чтобы определить с какого из них начать? (Правильный ответ – с первого). Устанавливаем точку останова и терпеливо ждем всплытия отладчика. Всплытие ждать себя не заставляет, но показывает какой-то странный, откровенно "левый" код. Ждем "x" для выхода, - следует целый каскад всплытий одно непонятнее другого.
Лихорадочно подергивая бородку (варианты – усики, волосы в разных местах) соображаем: функция "CCrackme_01Dlg::OnOK" вызывается непосредственно в момент нажатия на "ОК" – ей отводится часть стекового пространства под локальные переменные, которая автоматически "экспроприируется" при выходе из функции – переходя во всеобщее пользование. Таким образом, локальный буфер с введенным нами паролем существует только в момент его проверки, а потом автоматически затирается. Единственная зацепка – модальный диалог с ругательством. Пока он на экране – буфер еще содержит пароль и его можно найти в памяти. Но это не сильно помогает в отслеживании когда к этому буферу произведет обращение… Приходится терпеливо ждать, отсеивая ложные всплытия один за другим. Наконец, в окне данных искомая строка, а в окне кода – какой-то осмысленный код:
Остается "пропадчить" исполняемый файл, и тут (как и следовало ожидать по закону бутерброда) нас ждут очередные трудности. Во-первых, хитрый компилятор заоптимизировал код, подставив код функции strcmp вместо ее вызова, а во-вторых, условных переходов… да ими все кишит! Попробуй-ка, найди нужный. На этот раз бросать монетку мы не станем, а попытаемся подойти к делу по-научному. Итак, перед нами дизассемблированный код, точнее его ключевой фрагмент, осуществляющий анализ пароля:
0040303C: 4D 79 47 6F 6F 64 50 61 73 73 77 6F 72 64 00 MyGoodPassword
В регистр ESI помещается указатель на оригинальный пароль
004013DF: 8D 44 24 10 lea eax,[esp+10h]
В регистр EAX – указатель на пароль, введенный пользователем
004013E3: 8A 16 mov dl,byte ptr [esi]
004013E5: 8A 1E mov bl,byte ptr [esi]
004013E7: 8A CA mov cl,dl
004013E9: 3A D3 cmp dl,bl
Проверка первого символа на совпадение
004013EB: 75 1E jne 0040140B ---(3) --- (1)
Первый символ уже не совпадает – дальше проверять бессмысленно!
004013ED: 84 C9 test cl,cl
Первый символ первой строки равен нулю?
004013EF: 74 16 je 00401407 -- (2)
Да, достигнут конец строки – значит, строки идентичны
004013F1: 8A 50 01 mov dl,byte ptr [eax+1]
004013F4: 8A 5E 01 mov bl,byte ptr [esi+1]
004013F7: 8A CA mov cl,dl
004013F9: 3A D3 cmp dl,bl
Проверяем следующую пару символов
004013FB: 75 0E jne 0040140B --- (1)
Если не равна – конец проверке
004013FD: 83 C0 02 add eax,2
00401400: 83 C6 02 add esi,2
Перемещаем указатели строк на два символа вперед
00401403: 84 C9 test cl,cl
Достигнут конец строки?
00401405: 75 DC jne 004013E3 - (3)
Нет, еще не конец, сравниваем дальше.
00401407: 33 C0 xor eax,eax --- (2)
00401409: EB 05 jmp 00401410 -- (4)
Обнуляем EAX (strcmp в случае успеха возвращает ноль) и выходим
0040140B: 1B C0 sbb eax,eax --- (3)
0040140D: 83 D8 FF sbb eax,0FFFFFFFFh
Эта ветка получат управление при несовпадении строк. EAX устанавливает равным в ненулевое значение (подумайте почему).
00401410: 85 C0 test eax,eax --- (4)
Проверка значения EAX на равенство нулю
00401412: 6A 00 push 0
00401414: 6A 00 push 0
Что-то заносим в стек…
00401416: 74 38 je 00401450 <<<< ---(5)
Прыгаем куда-то….
00401418: 68 2C 30 40 00 push 40302Ch
0040302C: 57 72 6F 6E 67 20 70 61 73 73 77 6F 72 64 00 .Wrong password
Ага, "Вронг пысворд". Значит, прыгать все-таки надо…. Смотрим, куда указывает je (а код ниже – уже не представляет интереса – и так ясно: это "матюгальщик").
Теперь, когда алгоритм защиты в общих чертах ясен, можно ее и сломать, например, поменяв условный переход в строке 0x401416 на безусловный jump short (код 0xEB).
Способ 2. Бряк на функции ввода пароля
Вы боитесь творить, потому что творения ваши отражают вашу истинную суть.
Фрэнк Херберт "Ловец душ"
При всем желании метод прямого поиска пароля в памяти элегантным назвать нельзя, да и практичным тоже. А, собственно, зачем искать сам пароль, спотыкаясь об беспорядочно разбросанные буфера, когда можно поставить бряк непосредственно на функцию, его считывающую? Хм, можно и так… да вот угадать какой именно функцией разработчик вздумал читать пароль, вряд ли будет намного проще.
На самом деле одно и тоже действие может быть выполнено всего лишь несколькими функциями и их перебор не займет много времени. В частности, содержимое окна редактирование обычно добывается либо GetWinodowTextA (что чаще всего и происходит), либо GetDlgItemTextA (а это – значительно реже).
Раз уж речь зашла за окна, запустим наш GUI "крякмис" и установим точку останова на функцию GetWindowTextA ("bpx GetWinodwTextA"). Поскольку, эта функция – системная, точка останова будет глобальной, т.е. затронет все приложения в системе, поэтому, заблаговременно закройте все лишнее от греха подальше. Если установить бряк до запуска "крякмиса", то мы словим несколько ложных всплытий, возникающих вследствие того, что система сама читает содержимое окна в процессе формирования диалога.
Вводим какой-нибудь пароль ("KPNC Kaspersky++" по обыкновению), нажимаем - отладчик незамедливает всплыть:
Во многих руководствах по взлому советуется тут же выйти из функции по P RET, мол, что ее анализировать-то, но не стоит спешить! Сейчас самое время выяснить: где расположен буфер вводимой строки и установить на него бряк. Вспомним какие аргументы и в какой последовательности принимает функция (а, если не вспомним, то заглянем в SDK):
int GetWindowText(
HWND hWnd, // handle to window or control with text
LPTSTR lpString, // address of buffer for text
int nMaxCount // maximum number of characters to copy
);
Может показаться, раз программа написана на Си, то и аргументы заносятся в стек по Си-соглашению. А вот и нет! Все API функции Windows всегда вызываются по Паскаль- соглашению, на каком бы языке программа ни была написана. Таким образом, аргументы заносятся в стек слева направо, а последним в стек попадает адрес возврата. В 32-разрядной Windows все аргументы и сам адрес возврата занимают двойное слово (4 байта), поэтому, чтобы добраться до указателя на строку, необходимо к регистру указателю вершины стека (ESP) добавить восемь (одно двойное слово на nMaxCount, другое – на сам lpString). Нагляднее это изображено на рис. 3
Рисунок 3 0х02 Состояние стека на момент вызова GetWindowsText
Получить содержимое ячейки по заданному адресу в Айсе можно с помощью оператора "звездочка", вызов которого в нашем случае выглядит так (подробнее – см. документацию, прилагаемую к отладчику):
В буфере мусор – так и следовало ожидать, ведь строка еще не считана. Давайте выйдем из функции по p ret и посмотрим что произойдет (только потом уже нельзя будет пользоваться конструкцией d *esp+8, т.к. после выхода из функции аргументы будут вытолкнуты из стека):
ОК, это действительно тот буфер, который нам нужен. Ставим бряк на его начало и дожидаемся всплытия. Смотрите, с первого же раза мы очутились именно так, где и надо (узнаете код сравнивающей процедуры?):
Замечательно! Вот так, безо всяких ложных срабатываний, элегантно, быстро и красиво мы победили защиту!
Этот способ – универсален и впоследствии мы еще не раз им воспользуемся. Вся соль – определить ключевую функцию защиты и поставить на нее бряк. Под Windows все "поползновения" (будь то обращения к ключевому файлу, реестру и т.д.) сводятся к вызову API-функций, перечень которых хотя и велик, но все же конечен и известен заранее.
Способ 3. Бряк на сообщения
Любая завершенная дисциплина имеет свои штампы, свои модели, свое влияние на обучающихся.
Френк Херберт "Дюна"
Если у Вас еще не закружилась голова от количества выпитого во время хака пива, с вашего позволения мы продолжим. Каждый, кто хоть однажды программировал под Windows, наверняка знает, что в Windows все взаимодействие с окнами завязано на сообщениях. Практически все оконные API-функции на самом деле представляют собой высокоуровневые "обертки", посылающие окну сообщения. Не является исключением и GetWindowTextA, – аналог сообщения WM_GETTEXT.
Отсюда следует – чтобы считать текст из окна вовсе не обязательно обращаться к GetWindowTextA, - можно сделать это через SendMessageA(hWnd, WM_GETTEXT, (LPARAM) &buff[0]). Именно так и устроена защита в примере "crack 02". Попробуйте загрузить его и установить бряк на GetWindowTextA (GetDlgItemTextA). Что, не срабатывает? Подобная мера используется разработчиками для запутывания совсем уж желторотых новичков, бегло изучивших пару faq по хаку и тут же бросившихся в бой.
Так может, поставить бряк на SendMessageA? В данном случае в принципе можно, но бряк на сообщение WM_GETTEXT – более универсальное решение, срабатывающее независимо от того, как читают окно.
Для установки бряка на сообщение в Айсе предусмотрена специальная команда – "BMSG", которой мы и пользовались в первом издании этой книги. Но не интереснее ли сделать это своими руками?
Как известно, с каждым окном связана специальная оконная процедура, обслуживающая это окно, т.е. отвечающая за прием и обработку сообщений. Вот если бы узнать ее адрес, да установить на него бряк! И это действительно можно сделать! Специальная команда "HWND" выдает всю информацию об окнах указанного процесса.
Быстро обнаруживает себя окно редактирования, с адресом оконной процедуры 0x6C291B81. Поставим сюда бряк? Нет, еще не время – ведь оконная процедура вызывается не только при чтении текста, а гораздо чаще. Как бы установить бряк на то, что нам нужно, отсеяв все остальные сообщения? Для начала изучим прототип этой функции:
LRESULT CALLBACK WindowProc(
HWND hwnd, // handle to window
UINT uMsg, // message identifier
WPARAM wParam, // first message parameter
LPARAM lParam // second message parameter
);
Как нетрудно подсчитать, в момент вызова функции, аргумент uMsg – идентификатор сообщения будет лежать по смещению 8 относительно указателя вершины стека ESP. Если он равен WM_GETTEXT (непосредственное значение 0xD) – недурно бы всплыть!
Вот и настало время познакомиться с условными бряками. Подробнее об их синтаксисе рассказано в прилагаемой к отладчику документации. А, впрочем, программисты, знакомые Си вряд ли к ней обратится, ибо синтаксис лаконичен и интуитивно - понятен.
:bpx 6C291B81 IF (esp->8)==WM_GETTEXT
:x
Выходим их отладчика, вводим какой-нибудь текст в качесвте пароля, скажем "Hello", нажимаем , отладчик тут же "всплывает"
Break due to BPX #0008:6C291B81 IF ((ESP->8)==0xD) (ET=2.52 seconds)
Вот, он хвост Тигры и уши плюшевого медведя! Остается определить адрес буфера, в который возвращается считанная строка. Начинаем соображать: указатель на буфер передается через аргумент lParam (см. в SDK описание WM_GETTEXT), а сам lParam размещается в стеке по смещению 0x10, относительно ESP:
:bpm 23:12EB28
Установив точку останова, мы ловим одно откровенно "левое" всплытие отладчика (это видно по явно не "юзерскому" значению селектора CS, равного 8) и, уже тянем руку, чтобы нажать 'x' продолжив отслеживание нашего бряка, как вдруг краем глаза замечаем….
Эге, буфер-то не "сквозной", - система не отдает его "народу", а копирует в другой буфер. Это видно потому, как из указателя на "наш" буфер EDX символ копируется в CL (то, что EDX – указатель на "наш" буфер следует из того, что он вызвал всплытие отладчика), а из CL он копируется в [EAX], где EAX – какой-то указатель (о котором пока мы еще не можем сказать ничего определенного). Далее – оба указателя увеличиваются на единицу и CL (последний считанный символ) проверяется на равенство нулю – если конец строки не достигнут, то все повторяется. Что ж, суждено нам следить сразу за двумя буферами – ставим еще один бряк.
:bpm EAX
:x
На втором бряке отладчик вскорости всплывает, и мы узнаем нашу родную процедуру сравнения. Ну, а дальнейшее – дело техники.
В Windows 9x обработка сообщений реализована несколько иначе, чем в NT. В частности, оконная процедура окна редактирования находится в 16-разрядном коде. А это – сегментная модель памяти (треска хвостом вперед под хвост Тигре) a la сегмент : смещение. Представляется любопытным механизм передачи адреса – в какой же параметр засунут сегмент? Чтобы ответить на это, взглянем на отчет Айса:
Break due to BMSG 0428 WM_GETTEXT (ET=513.11 milliseconds)
hWnd=0428 wParam=0666 lParam=28D70000 msg=000D WM_GETTEXT
^ ^ ^--^^--^
| | сегмент/ \смещение
дескриптор окна |
|
макс. кол-во символов для чтения
Адрес целиком умещается в 32-разрядном аргументе lParam – 16-разрядный сегмент и 16-разрядное смещение. Посему, точка останова должна выглядеть так: "bpm 28D7:0000"
Шаг пятый. На сцене появляется IDA
"Реальность такова, какой ее описывает язык" тезис лингвистической относительности Б.Л. Уорфа
С легкой руки Дениса Ричи повелось начинать освоение нового языка программирования с создания простейшей программы “Hello, World!”, -- и здесь не будет нарушена эта традиция. Оценим возможности IDA Pro следующим примером (для совместимости с книгой рекомендуется откомпилировать его с помощью Microsoft Visual C++ 6.0 вызовом “cl.exe first.cpp” в командной строке):
#include
void main()
{
cout<<"Hello, Sailor!\n";
}
a) исходный текст программы first.cpp
Компилятор сгенерирует исполняемый файл размером почти в 40 килобайт, большую часть которого займет служебный, стартовый или библиотечный код! Попытка дизассемблирования с помощью таких дизассемблеров как W32DASM (или аналогичных ему) не увенчается быстрым успехом, поскольку над полученным листингом размером в пятьсот килобайт (!) можно просидеть не час и не два. Легко представить сколько времени уйдет на серьезные задачи, требующие изучения десятков мегабайт дизассемблированного текста.
Попробуем эту программу дизассемблировать с помощью IDA. Если все настройки оставить по умолчанию, после завершения анализа экран (в зависимости от версии) должен выглядеть следующим образом:
Рисунок 4 “0x000.bmp” Так выглядит результат работы консольной версии IDA Pro 3.6
Рисунок 5 “0x001.bmp” Так выглядит результат работы консольной версии IDA Pro 4.0
Рисунок 6 “0x002.bmp” Так выглядит результат работы графической версии IDA Pro 4.0
С версии 3.8x1 в IDA появилась поддержка «сворачивания» (Collapsed) функций. Такой прием значительно упрощает навигацию по тексту, позволяя убрать с экрана не интересные в данный момент строки. По умолчанию все библиотечные функции сворачиваются автоматически.
Развернуть функцию можно подведя к ней курсор и нажав <+> на дополнительной цифровой клавиатуре, расположенной справа. Соответственно, клавиша <-> предназначена для сворачивания.
По окончании автоматического анализа файла “first.exe”, IDA переместит курсор к строке “.text:00401B2C” – точке входа в программу. Среди начинающих программистов широко распространено заблуждение, якобы программы, написанные на Си, начинают выполняться с функции “main”, но в действительности это не совсем так. На самом деле сразу после загрузки файла управление передается на функцию “Start”, вставленную компилятором. Она подготавливает глобальные переменные _osver (билд), _winmajor (старшая версия операционной системы), _winminor (младшая версия операционной системы), _winver (полная версия операционной системы), __argc (количество аргументов командной строки), __argv (массив указателей на строки аргументов), _environ (массив указателей на строки переменных окружения); инициализирует кучи (heap); вызывает функцию main, а после возращения управления завершает процесс с помощью функции Exit.
Наглядно продемонстрировать инициализацию переменных, совершаемую стартовым кодом, позволяет следующая программа.
#include
#include
void main()
{
int a;
printf(">Версия OS:\t\t\t%d.%d\n\
>Билд:\t\t\t%d\n\
>Количество агрументов:\t%d\n",\
_winmajor,_winminor,_osver,__argc);
for (a=0;a<__argc;a++)
printf(">\tАгрумент %02d:\t\t%s\n",a+1,__argv[a]);
a=!a-1;
while(_environ[++a]) ;
printf(">Количество переменных окружения:%d\n",a);
while(a) printf(">\tПеременная %d:\t\t%s\n",a,_environ[--a]);
}
a) исходный текст программы CRt0.demo.c
Прототип функции main как будто указывает, что приложение не принимает ни каких аргументов командной строки, но результат работы программы доказывает обратное и на машине автора выглядит так (приводится в сокращенном виде):
>Версия OS: 5.0
>Билд: 2195
>Количество агрументов: 1
> Агрумент 01: CRt0.demo
>Количество переменных окружения: 30
> Переменная 29: windir=C:\WINNT
>...
b) результат работы программы CRt0.demo.c
Очевидно, нет никакой необходимости анализировать стандартный стартовый код приложения, и первая задача исследователя – найти место передачи управления на функцию main. К сожалению, гарантированное решение этой задачи требует полного анализа содержимого функции “Start”. У исследователей существует множество хитростей, но все они базируются на особенностях реализации конкретных компиляторов2 и не могут считаться универсальными.
Рекомендуется изучить исходные тексты стартовых функций популярных компиляторов, находящиеся в файлах CRt0.c (Microsoft Visual C) и c0w.asm (Borland C) – это упросит анализ дизассемблерного листинга.
Ниже, в качестве иллюстрации, приводится содержимое стартового кода программы “first.exe”, полученное в результате работы W32Dasm:
Referenced by a (U)nconditional or (C)onditional Jump at Address:
|:00401B8D(C)
|
:00401B97 8365FC00 and dword ptr [ebp-04], 00000000
:00401B9B E8D70C0000 call 00402877
Иначе выглядит результат работы IDA, умеющей распознавать библиотечные функции по их сигнатурам (приблизительно по такому же алгоритму работает множество антивирусов). Поэтому, способности дизассемблера тесно связаны с его версией и полнотой комплекта поставки – далеко не все версии IDA Pro в состоянии работать с программами, сгенерированными современными компиляторами. (Перечень поддерживаемых компиляторов можно найти в файле “%IDA%/SIG/list”).
С приведенным примером IDA Pro успешно справляется, о чем свидетельствует стока “Using FLIRT signature: VC v2.0/4.x/5.0 runtime” в окне сообщений
Рисунок 7 "0x003" Загрузка библиотеки сигнатур
Дизассемблер сумел определить имена всех функций вызываемых стартовым кодом, за исключением одной, расположенной по адресу 0х0401BDB. Учитывая передачу трех аргументов и обращение к _exit, после возращения функцией управления, можно предположить, что это main и есть.
Перейти по адресу 0x0401000 для изучения содержимого функции main можно несколькими способами – прокрутить экран с помощью стрелок управления курсором, нажать клавишу и ввести требуемый адрес в появившемся окне диалога, но проще и быстрее всего воспользоваться встроенной в IDA Pro системой навигации. Если подвести курсор в границы имени, константы или выражения и нажать , IDA автоматически перейдет на требуемый адрес.
В данном случае требуется подвести к строке “sub_401000” (аргументу команды call) и нажать на , если все сделано правильно, экран дизассемблера должен выглядеть следующим образом:
00401000 ; -------------- S U B R O U T I N E ----------------------
00401000
00401000 ; Attributes: bp-based frame
00401000
00401000 sub_401000 proc near ; CODE XREF: start+AFp
00401000 push ebp
00401001 mov ebp, esp
00401003 push offset aHelloSailor ; "Hello, Sailor!\n"
00401008 mov ecx, offset dword_408748
0040100D call ??6ostream@@QAEAAV0@PBD@Z ; ostream::operator<<(char const *)
00401012 pop ebp
00401013 retn
00401013 sub_401000 endp
Дизассемблер сумел распознать строковую переменную и дал ей осмысленное имя “aHelloSailor”, а в комментарии, расположенном справа, для наглядности привел оригинальное содержимое “Hello, Sailor!\n”. Если поместить курсор в границы имени “aHelloSailor”:и нажать , IDA автоматически перейдет к требуемой строке:
00408040 aHelloSailor db 'Hello, Sailor!',0Ah,0 ; DATA XREF: sub_401000+3o
Выражение “DATA XREF: sub_401000+3o” называется перекрестной ссылкой и свидетельствует о том, что в третьей строке процедуры sub_401000, произошло обращение к текущему адресу по его смещению (“o” от offset), а стрелка, направленная вверх, указывает на относительное расположение источника перекрестной ссылки.
Если в границы выражения “sub_401000+3” подвести курсор и нажать , IDA Pro перейдет к следующей строке:
Нажатие клавиши отменяет предыдущее перемещение, возвращая курсор в исходную позицию. (Аналогично команде “back” в web-браузере). Смещение строки “Hello, Sailor!\n”, передается процедуре “??6ostream@@QAEAAV0@PBD@Z”, представляющей собой оператор “<<” языка С++. Странное имя объясняется ограничениями, наложенными на символы, допустимые в именах библиотечных функций. Поэтому, компиляторы автоматически преобразуют (замангляют) такие имена в “абракадабру”, пригодную для работы с линкером, и многие начинающие программисты даже не догадываются об этой скрытой “кухне”.
Для облегчения анализа текста, IDA Pro в комментариях отображает «правильные» имена, но существует возможность заставить ее везде показывать незамангленные имена. Для этого необходимо в меню “Options” выбрать пункт “Demangled names” и в появившемся окне диалога переместить радио кнопку на “Names”, после этого вызов оператора “<<” станет выглядеть так:
0040100D call ostream::operator<<(char const *)
На этом анализ приложения “first.cpp” можно считать завершенным. Для полноты картины остается переименовать функцию “sub_401000” в main. Для этого необходимо подвести курсор к строке 0x0401000 (началу функции) и нажать клавишу , в появившемся диалоге ввести “main”. Конечный результат должен выглядеть так:
00401000 ; --------------- S U B R O U T I N E ---------------------------------------
00401000
00401000 ; Attributes: bp-based frame
00401000
00401000 main proc near ; CODE XREF: start+AFp
00401000 push ebp
00401001 mov ebp, esp
00401003 push offset aHelloSailor ; "Hello, Sailor!\n"
00401008 mov ecx, offset dword_408748
0040100D call ostream::operator<<(char const *)
00401012 pop ebp
00401013 retn
00401013 main endp
Для сравнения результат работы W32Dasm выглядит следующим образом (ниже приводится лишь содержимое функции main):
:00401000 55 push ebp
:00401001 8BEC mov ebp, esp
Possible StringData Ref from Data Obj ->"Hello, Sailor!"
|
:00401003 6840804000 push 00408040
:00401008 B948874000 mov ecx, 00408748
:0040100D E8AB000000 call 004010BD
:00401012 5D pop ebp
:00401013 C3 ret
Другое важное преимущество IDA – способность дизассемблировать зашифрованные программы. В демонстрационном примере ??? “/SRC/Crypt.com” использовалась статическая шифровка, часто встречающаяся в “конвертных” защитах. Этот простой прием полностью “ослепляет” большинство дизассемблеров. Например, результат обработки файла “Crypt.com” SOURCER-ом выглядит так:
Crypt proc far
7E5B:0100 start:
7E5B:0100 83 C6 06 add si,6
7E5B:0103 FF E6 jmp si ;*
;*No entry point to code
7E5B:0105 B9 14BE mov cx,14BEh
7E5B:0108 01 AD 5691 add ds:data_1e[di],bp ; (7E5B:5691=0)
7E5B:010C 80 34 66 xor byte ptr [si],66h ; 'f'
7E5B:010F 46 inc si
7E5B:0110 E2 FA loop $-4 ; Loop if cx > 0
7E5B:0112 FF E6 jmp si ;*
;* No entry point to code
7E5B:114 18 00 sbb [bx+si],al
7E5B:116 D2 6F DC shr byte ptr [bx-24h],cl ; Shift w/zeros fill
7E5B:119 6E 67 AB 47 A5 2E db 6Eh, 67h,0ABh, 47h,0A5h, 2Eh
7E5B:11F 03 0A 0A 09 4A 35 db 03h, 0Ah, 0Ah, 09h, 4Ah, 35h
7E5B:125 07 0F 0A 09 14 47 db 07h, 0Fh, 0Ah, 09h, 14h, 47h
7E5B:12B 6B 6C 42 E8 00 00 db 6Bh, 6Ch, 42h, E8h, 00h, 00h
7E5B:131 59 5E BF 00 01 57 db 59h, 5Eh, BFh, 00h, 01h, 57h
7E5B:137 2B CE F3 A4 C3 db 2Bh, CEh, F3h, A4h, C3h
Crypt endp
SOURCER половину кода вообще не смог дизассемблировать, оставив ее в виде дампа, а другую половину дизассемблировал неправильно! Команда “JMP SI” в строке :0x103 осуществляет переход по адресу :0x106 (значение регистра SI после загрузки com файла равно 0x100, поэтому после команды “ADD SI,6” регистр SI равен 0x106). Но следующая за “JMP” команда расположена по адресу 0x105! В исходном тексте в это место вставлен байт-пустышка, сбивающий дизассемблер с толку.
Start:
ADD SI,6
JMP SI
DB 0B9h ;
LEA SI,_end ; На начало зашифрованного фрагмента
SOURCER не обладает способностью предсказывать регистровые переходы и, встретив команду “JMP SI” продолжает дизассемблирование, молчаливо предполагая, что команды последовательно расположены вплотную друг к другу. Существует возможность создать файл определений, указывающий, что по адресу:0x105 расположен байт данных, но подобное взаимодействие с пользователем очень неудобно.
Напротив, IDA изначально проектировалась как дружественная к пользователю интерактивная среда. В отличие от SURCER-подобных дизассемблеров, IDA не делает никаких молчаливых предположений, и при возникновении затруднений обращается за помощью к человеку. Поэтому, встретив регистровый переход по неизвестному адресу, она прекращает дальнейший анализ, и результат анализа файла “Crypt.com” выглядит так:
seg000:0100 start proc near
seg000:0100 add si, 6
seg000:0103 jmp si
seg000:0103 start endp
seg000:0103
seg000:0103 ; ------------------------------------------------------------------------
seg000:0105 db 0B9h ; ¦
seg000:0106 db 0BEh ; -
seg000:0107 db 14h ;
seg000:0108 db 1 ;
seg000:0109 db 0ADh ; í
seg000:010A db 91h ; Ñ
...
Необходимо помочь дизассемблеру, указав адрес перехода. Начинающие пользователи в этой ситуации обычно подводят курсор к соответствующей строке и нажимают клавишу , заставляя IDA дизассемблировать код с текущей позиции до конца функции. Несмотря на кажущуюся очевидность, такое решение ошибочно, ибо по-прежнему остается неизвестным куда указывает условный переход в строке :0x103 и откуда код, расположенный по адресу :0x106 получает управление.
Правильное решение – добавить перекрестную ссылку, связывающую строку :0x103, со строкой :0x106. Для этого необходимо в меню “View” выбрать пункт “Cross references” и в появившемся окне диалога заполнить поля “from” и “to” значениями seg000:0103 и seg000:0106 соответственно.
После этого экран дизассемблера должен выглядеть следующим образом (в IDA версии 4.01.300 содержится ошибка, и добавление новой перекрестной ссылки не всегда приводит к автоматическому дизассемблированию):
seg000:0100 public start
seg000:0100 start proc near
seg000:0100 add si, 6
seg000:0103 jmp si
seg000:0103 start endp
seg000:0103
seg000:0103 ; -----------------------------------------------------------------------
seg000:0105 db 0B9h ; ¦
seg000:0106 ; -----------------------------------------------------------------------
seg000:0106
seg000:0106 loc_0_106: ; CODE XREF: start+3u
seg000:0106 mov si, 114h
seg000:0109 lodsw
seg000:010A xchg ax, cx
seg000:010B push si
seg000:010C
seg000:010C loc_0_10C: ; CODE XREF: seg000:0110j
seg000:010C xor byte ptr [si], 66h
seg000:010F inc si
seg000:0110 loop loc_0_10C
seg000:0112 jmp si
seg000:0112 ; ----------------------------------------------------------------------
seg000:0114 db 18h ;
seg000:0115 db 0 ;
seg000:0116 db 0D2h ; T
seg000:0117 db 6Fh ; o
...
Поскольку IDA Pro не отображает адреса-приемника перекрестной ссылки, то рекомендуется выполнить это самостоятельно. Такой примем улучшит наглядность текста и упростит навигацию. Если повести курсор к строке :0x103 нажать клавишу <:>, введя в появившемся диалоговом окне любой осмысленный комментарий (например “переход по адресу 0106”), то экран примет следующий вид:
seg000:0103 jmp si ; Переход по адресу 0106
Ценность такого приема заключается в возможности быстрого перехода по адресу, на который ссылается “JMP SI”, - достаточно лишь подвести курсор к числу “0106” и нажать . Важно соблюдать правильность написания – IDA Pro не распознает шестнадцатеричный формат ни в стиле Си (0x106), ни в стиле MASM\TASM (0106h).
Что представляет собой число “114h” в строке :0x106 – константу или смещение? Чтобы узнать это, необходимо проанализировать следующую команду – “LODSW”, поскольку ее выполнение приводит к загрузке в регистр AX слова, расположенного по адресу DS:SI, очевидно, в регистр SI заносится смещение.
seg000:0106 mov si, 114h
seg000:0109 lodsw
Однократное нажатие клавиши преобразует константу в смещение и дизассемблируемый текст станет выглядеть так:
seg000:0106 mov si, offset unk_0_114
seg000:0109 lodsw
…
seg000:0114 unk_0_114 db 18h ; ; DATA XREF: seg000:0106o
seg000:0115 db 0 ;
seg000:0116 db 0D2h ; T
seg000:0117 db 6Fh ; o
…
IDA Pro автоматически создала новое имя “unk_0_114”, ссылающееся на переменную неопределенного типа размером в байт, но команда “LODSW” загружает в регистр AX слово, поэтому необходимо перейти к строке :0144 и дважды нажать пока экран не станет выглядеть так:
seg000:0114 word_0_114 dw 18h ; DATA XREF: seg000:0106o
seg000:0116 db 0D2h ; T
Но что именно содержится в ячейке “word_0_144”? Понять это позволит изучение следующего кода:
В строке :0x10A значение регистра AX помещается в регистр CX, и затем он используется командой “LOOP LOC_010C” как счетчик цикла. Тело цикла представляет собой простейший расшифровщик – команда “XOR” расшифровывает один байт, на который указывает регистр SI, а команда “INC SI” перемещает указатель на следующий байт. Следовательно, в ячейке “word_0_144” содержится количество байт, которые необходимо расшифровать. Подведя к ней курсор, нажатием клавиши можно дать ей осмысленное имя, например “BytesToDecrypt”.
После завершения цикла расшифровщика встречается еще один безусловный регистровый переход.
seg000:0112 jmp si
Чтобы узнать куда именно он передает управление, необходимо проанализировать код и определить содержимое регистра SI. Часто для этой цели прибегают к помощи отладчика – устанавливают точку останова в строке 0x112 и дождавшись его «всплытия» просматривают значения регистров. Специально для этой цели, IDA Pro поддерживает генерацию map-файлов, содержащих символьную информацию для отладчика. В частности, чтобы не заучивать численные значения всех «подопытных» адресов, каждому из них можно присвоить легко запоминаемое символьное имя. Например, если подвести курсор к строке “seg000:0112”, нажать и ввести “BreakHere”, отладчик сможет автоматически вычислить обратный адрес по его имени.
Для создания map-файла в меню “File” необходимо кликнуть по «Produce output file» и в развернувшемся подменю выбрать «Produce MAP file» или вместо всего этого нажать на клавиатуре «горячую» комбинацию «. Независимо от способа вызова на экран должно появится диалоговое окно следующего вида. Оно позволяет выбрать какого рода данные будут включены в map-файл – информация о сегментах, имена автоматически сгенерированные IDA Pro (такие как, например, “loc_0_106”, “sub_0x110” и т.д.) и «размангленные» (т.е. приведенные в читабельный вид) имена. Содержимое полученного map-файла должно быть следующим:
Start Stop Length Name Class
00100H 0013BH 0003CH seg000 CODE
Address Publics by Value
0000:0100 start
0000:0112 BreakHere
0000:0114 BytesToDecrypt
Program entry point at 0000:0100
Такой формат поддерживают большинство отладчиков, в том числе и популярнейший Soft-Ice, в поставку которого входит утилита “msym”, запускаемая с указанием имени конвертируемого map-файла в командной стоке. Полученный sym-файл необходимо разместить в одной директории с отлаживаемой программой, загружаемой в загрузчик без указания расширения, т.е., например, так “WLDR Crypt”. В противном случае символьная информация не будет загружена!
Затем необходимо установить точку останова командой “bpx BreakHere” и покинуть отладчик командной “x”. Спустя секунду его окно вновь появиться на экране, извещая о достижении процессором контрольной точки. Посмотрев на значения регистров, отображаемых по умолчанию вверху экрана, можно выяснить, что содержимое SI равно 0x12E.
С другой стороны, это же значение можно вычислить «в уме», не прибегая к отладчику. Команда MOV в строке 0x106 загружает в регистр SI смещение 0x114, откуда командой LODSW считывается количество расшифровываемых байт – 0x18, при этом содержимое SI увеличивается на размер слова – два байта. Отсюда, в момент завершения цикла расшифровки значение SI будет равно 0x114+0x18+0x2 = 0x12E.
Вычислив адрес перехода в строке 0x112, рекомендуется создать соответствующую перекрестную ссылку (from: 0x122; to: 0x12E) и добавить комментарий к строке 0x112 (“Переход по адресу 012E”). Создание перекрестной ссылки автоматически дизассемблирует код, начиная с адреса seg000:012E и до конца файла.
seg000:012E loc_0_12E: ; CODE XREF: seg000:0112u
seg000:012E call $+3
seg000:0131 pop cx
seg000:0132 pop si
seg000:0133 mov di, 100h
seg000:0136 push di
seg000:0137 sub cx, si
seg000:0139 repe movsb
seg000:013B retn
Назначение команды “CALL $+3” (где $ обозначает текущее значение регистра указателя команд IP) состоит в заталкивании в стек содержимого регистра IP, откуда впоследствии оно может быть извлечено в любой регистр общего назначения. Необходимость подобного трюка объясняется тем, что в микропроцессорах серии Intel 80x86 регистр IP не входит в список непосредственно адресуемых и читать его значение могут лишь команды, изменяющие ход выполнения программы, в том числе и call.
Для облегчения анализа листинга можно добавить к стокам 0x12E и 0x131 комментарий – “MOV CX, IP”, или еще лучше – сразу вычислить и подставить непосредственное значение – “MOV CX,0x131”.
Команда “POP SI” в строке 0x132 снимает слово из стека и помещает его в регистр SI. Прокручивая экран дизассемблера вверх в строке 0x10B можно обнаружить парную ей инструкцию “PUSH SI”, заносящую в стек смещение первого расшифровываемого байта. После этого становится понятным смысл последующих команд “MOV DI, 0x100\SUB CX,SI\REPE MOVSB”. Они перемещают начало расшифрованного фрагмента по адресу, начинающегося со смещения 0x100. Такая операция характерна для «конвертных» защит, накладывающихся на уже откомпилированный файл, который перед запуском должен быть размещен по своим «родным» адресам.
Перед началом перемещения в регистр CX заносится длина копируемого блока, вычисляемая путем вычитания смещения первого расшифрованного байта от смещения второй команды перемещающего кода. В действительности, истинная длина на три байта короче и по идее от полученного значения необходимо вычесть три. Однако, такое несогласование не нарушает работоспособности, поскольку содержимое ячеек памяти, лежащих за концом расшифрованного фрагмента, не определено и может быть любым.
Пара команд “0x136:PUSH DI” и “0x13B:RETN” образуют аналог инструкции “CALL DI” – “PUSH” заталкивает адрес возврата в стек, а “RETN” извлекает его оттуда и передает управление по соответствующему адресу. Зная значение DI (оно равно 0x100) можно было бы добавить еще одну перекрестную ссылку (“from:0x13B; to:0x100”) и комментарий к строке :0x13B – “Переход по адресу 0x100”, но ведь к этому моменту по указанным адресам расположен совсем другой код! Поэтому, логически правильнее добавить перекрестную ссылку “from:0x13B; to:0x116” и комментарий “Переход по адресу 0x116”.
Сразу же после создания новой перекрестной ссылки IDA попытается дизассемблировать зашифрованный код, в результате чего получится следующее:
seg000:0116 loc_0_116: ; CODE XREF: seg000:013Bu
seg000:0116 shr byte ptr [bx-24h], cl
seg000:0119 outsb
seg000:011A stos word ptr es:[edi]
seg000:011C inc di
seg000:011D movsw
seg000:011E add cx, cs:[bp+si]
seg000:0121 or cl, [bx+di]
seg000:0123 dec dx
seg000:0124 xor ax, 0F07h
seg000:0127 or cl, [bx+di]
seg000:0129 adc al, 47h
seg000:0129;──────────────────────────────────────────────────────
seg000:012B db 6Bh ; k
seg000:012C db 6Ch ; l
seg000:012D db 42h ; B
seg000:012E;──────────────────────────────────────────────────────
Непосредственное дизассемблирование зашифрованного кода невозможно – предварительно его необходимо расшифровать. Подавляющее большинство дизассемблеров не могут модифицировать анализируемый текст налету и до загрузки в дизассемблер исследуемый файл должен быть полностью расшифрован. На практике, однако, это выглядит несколько иначе – прежде чем расшифровывать необходимо выяснить алгоритм расшифровки, проанализировав доступную часть файла. Затем выйти из дизассемблера, тем или иным способом расшифровать «секретный» фрагмент, вновь загрузить файл в дизассемблер (причем предыдущие результаты дизассемблирования окажутся утеряны) и продолжить его анализ до тех пор, пока не встретится еще один зашифрованный фрагмент, после чего описанный цикл «выход из дизассемблера –расшифровка – загрузка - анализ» повторяется вновь.
Достоинство IDA заключается в том, что она позволяет выполнить ту же задачу значительно меньшими усилиями, никуда не выходя из дизассемблера. Это достигается за счет наличия механизма виртуальной памяти, – если не вдаваться в технические тонкости, упрощенно можно изобразить IDA в виде «прозрачной» виртуальной машины, оперирующей с физической памятью компьютера. Для модификации ячеек памяти необходимо знать их адрес, состоящий из пары чисел – сегмента и смещения.
Слева каждой строки указывается ее смещение и имя сегмента, например “seg000:0116”. Узнать базовый адрес сегмента по его имени можно, открыв окно «Сегменты» выбрав в меню «View» пункт «Segments».
╔═[■]═══════════════════════════ Program Segmentation ══════════════════════════3═[↑]═╗
║ Name Start End Align Base Type Cls 32es ss ds ▲
║ seg000 00000100 0000013C byte 1000 pub CODE N FFFF FFFF 1000 00010100 0001013C ▓
║ ▓
║ ▼
╚1/1 ═════════════════◄■▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒►─┘
Рисунок 8 Окно «Сегменты»
Искомый адрес находится в столбце “Base” и для наглядности на приведенной копии экрана выделен жирным шрифтом. Обратится к любой ячейке сегмента поможет конструкция “[segment:offset]”, а для чтения и модификации ячеек предусмотрены функции Byte и PatchByte соответственно. Их вызов может выглядеть, например, так: a=Byte([0x1000,0x100]) – читает ячейку, расположенную по смещению 0x100 в сегменте с базовым адресом 0x1000; PatchByte([0x1000,0x100],0x27) – присваивает значение 0x27 ячейке памяти, расположенной по смещению 0x100 в сегменте с базовым адресом 0x1000. Как следует из названия функций, они манипулируют с ячейками размером в один байт.
Знания этих двух функций вполне достаточно для написания скрипта -расшифровщика при условии, что читатель знаком с языком Си. Реализация IDA-Си не полностью поддерживается стандарта – подробнее об этом рассказывается в главе «Язык скриптов IDA-Си», здесь же достаточно заметить, что в частности IDA не позволяет разработчику задавать тип переменной и определяет его автоматически по ее первому использованию, а объявление осуществляется ключевым словом “auto”. Например, “auto MyVar, s0” объявляет две переменных – MyVar и s0.
Для создания скрипта необходимо нажать комбинацию клавиш или выбрать в меню “File” пункт “IDC Command” и в появившемся окне диалога ввести исходный текст программы:
auto a;
for (a=0x116;a<0x12E;a++)
PatchByte([0x1000,a],Byte([0x1000,a])^0x66);
a) исходный текст скрипта - расшифровщика
Пояснение: как было показано выше алгоритм расшифровщика сводится к последовательному преобразованию каждой ячейки зашифрованного фрагмента операцией XOR 0x66, (см. ниже – выделено жирным шрифтом)
Сам же зашифрованный фрагмент начинается с адреса seg000:0x116 и продолжается вплоть до seg000:0x12E. Отсюда – цикл расшифровки на языке Си выглядит так: for (a=0x116;a<0x12E;a++) PatchByte([0x1000,a],Byte([0x1000,a])^0x66);
В зависимости от версии IDA для выполнения скрипта необходимо нажать либо (версия 3.8x и старше), либо в более ранних версиях. Если все сделано правильно, после выполнения скрипта экран дизассемблера должен выглядеть так (b).
Возможные ошибки – несоблюдение регистра символов (IDA к этому чувствительна), синтаксические ошибки, базовый адрес вашего сегмента отличается от 0x1000 (еще раз вызовете окно «Сегменты» чтобы узнать его значение). В противном случае необходимо подвести курсор к строке “seg000:0116”, нажать клавишу для удаления результатов предыдущего дизассемблирования зашифрованного фрагмента и затем клавишу для повторного дизассемблирования расшифрованного кода.
seg000:0116 loc_0_116: ; CODE XREF: seg000:013Bu
seg000:0116 mov ah, 9
seg000:0118 mov dx, 108h
seg000:011B int 21h ; DOS - PRINT STRING
seg000:011B ; DS:DX -> string terminated by "$"
seg000:011D retn
seg000:011D ; ───────────────────────────────────────────────────────────────────────────
seg000:011E db 48h ; H
seg000:011F db 65h ; e
seg000:0120 db 6Ch ; l
seg000:0121 db 6Ch ; l
seg000:0122 db 6Fh ; o
seg000:0123 db 2Ch ; ,
seg000:0124 db 53h ; S
seg000:0125 db 61h ; a
seg000:0126 db 69h ; i
seg000:0127 db 6Ch ; l
seg000:0128 db 6Fh ; o
seg000:0129 db 72h ; r
seg000:012A db 21h ; !
seg000:012B db 0Dh ;
seg000:012C db 0Ah ;
seg000:012D db 24h ; $
seg000:012E ; ───────────────────────────────────────────────────────────────────────────
b) результат работы скрипта расшифровщика