Различия

Здесь показаны различия между двумя версиями данной страницы.

Ссылка на это сравнение

books:tekhnika-i-filosofiya-hakerskih-atak-text [2017/09/05 02:55] (текущий)
Строка 1: Строка 1:
 +====== tekhnika-i-filosofiya-hakerskih-atak-text ======
 +<​sub>​{{tekhnika-i-filosofiya-hakerskih-atak-text.odt|Original file}}</​sub>​
 +
 +====== Техника хакерских атак\\ Фундаментальные основы хакерства ======
 +
 +Крис Касперски
 +
 +//​Светлой памяти Сергея Иванова – главного редактора издательства "​Солон"​ –  посвящается эта книга.//​
 +
 +Автор.
 +
 +====== Аннотация ======
 +
 +Книга, которую вы сейчас держите в руках, открывает двери в удивительный мир защитных механизмов,​ рассказывая о том, как создаются и вскрываются защиты. Она адресована всем, кто любит захватывающие дух головоломки. Всем, кто проводит свободное (и несвободное) время за копанием в недрах программ и операционной системы. Наконец,​ всем, кто по роду своей деятельности занимается (постоянно и/или эпизодически) написанием защит и хочет узнать как грамотно и гарантированно противостоять вездесущим хакерам.
 +
 +Настоящий том посвящен базовым основам хакерства – технике работы с отладчиком и дизассемблером. Подробно описаны приемы идентификации и реконструкции ключевых структур исходного языка – функций (в т.ч. виртуальных),​ локальных и глобальных переменных,​ ветвлений,​ циклов,​ объектов и их иерархий,​ математических операторов и т.д.
 +
 +====== Предисловие редактора ======
 +
 +"//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),
 +
 +ФБРСША
 +
 +Эпиграф выбран неслучайно. Информационная безопасность сегодня представляет одну из весьма “горячих” тем. Ее актуальность весьма велика,​ и каждое пособие связанное с этой темой подвергается анализу со стороны обычно весьма скептически настроенных специалистов. Исследование программ связано с вопросами информационной безопасности напрямую. Когда автор этой книги пригласил меня, как специалиста,​ стать ее научным редактором,​ я отнесся к этой затее с большим интересом.
 +
 +Сама мысль о возможности опубликования подобных материалов допускает для многих некоторую крамолу,​ как некогда было, к примеру,​ с криптографией и некоторыми областями теории чисел. Более того, тематика данной книги до некоторого времени расценивалась как близкая к широко обсуждаемым криминальным темам и лишь в последнее время вернулась в свое естественное научное русло.
 +
 +На мой взгляд,​ эта книга будет интересна весьма широкому кругу читателей. Наверняка ею заинтересуются и те, кто лишь начинает свой восход к Олимпу знаний,​ и уже “матерые” специалисты в области программирования и исследования программ (или на иностранный манер “reverseengineering”). Хочется особенно отметить,​ что материалы книги устроены таким образом,​ что будут полезны и обычному программисту (как пособие по оптимизации программ для современных интеллектуальных компиляторов),​ и специалистам различных направлений (например,​ специалистам информационной защиты ‑ в качестве пособия по поиску так называемых “закладок”). Стиль изложения “от простого к сложному” позволяет говорить также и о том, что данная книга послужит также и учебным пособием для начинающих исследователей и “кодокопателей”.
 +
 +Книга содержит бесценное количество уникального по своей сути практического материала. Множество поверхностных работ за рубежом представляет очень мало практического интереса для тех, кто с интересом изучает прикладную математику,​ программирование и устройство компьютеров. Да и среди публикаций современного российского научного сообщества читатель не найдет лучшего пособия по изучению техники исследования программ.
 +
 +Однако,​ я все-таки рекомендую читателю подвергнуть сомнению все вышесказанное и убедиться во всем самостоятельно,​ прочитав данную книгу.
 +
 +С уважением,​
 +
 +Хади Р.А.
 +
 +===== Что нового во втором издании =====
 +
 +"//​Как бы плохо вы ни написали вашу повесть,​ у вас обязательно найдутся читатели,​ тысячи читателей,​ которые сочтут ее шедевром... Как бы хорошо вы ни написали свою повесть,​ обязательно найдутся читатели,​ и это будут тысячи читателей,​ которые сочтут ее чистым барахлом//"​
 +
 +Борис Hатанович Стругацкий
 +
 +Первое издание "​Техники и философии хакерских атак"​ – довольно фривольное и хаотичное – по стилю изложения напоминало собой "​Путевые заметки охотника"​ – читается,​ может быть и интересно,​ но вот на учебник,​ увы не тянет. К моему огромному удивлению книга имела ощутимый успех и множество одобрительных откликов. Одно, конечно,​ понятно – на безрыбье и рак рыба – за последнее время ничего путного по данной тематике не выходило.
 +
 +Когда же тираж книги был полностью распродан,​ но заявки на нее по-прежнему продолжали поступать,​ встал вопрос – что делать дальше:​ выпускать "в один к одному"​ допечатку или переработанное и дополненное второе издание?​ Издатель склонялся к последнему,​ да и я в желании утолить свой профессиональный зуд, признаться,​ тоже. Однако за время, прошедшее с момента первого издания,​ я стал писать значительно структурней и "​чище"​. Поэтому,​ после долгих колебаний,​ сомнений и размышлений решил полностью переписать книгу "с нуля",​ превратив ее в реальную настольную книгу хакера. Своеобразный справочник кодокопателя,​ но вместе с тем и учебник,​ помогающий начинающим сделать в мире хакерства свои первые шаги.
 +
 +Попутно – движимый просьбами читателей,​ ожидающих поскорее увидеть продолжение трилогии "//​Образ мышления – дизассемблер ////​IDA//",​ я рискнул включить в настоящее издание двадцать глав из моей будущей книги "//​Искусство дизассемблирования//"​ (название рабочее),​ которая увидит свет в своем полном объеме не раньше чем через три – пять лет.
 +
 +Объем книги увеличился настолько,​ что ее пришлось разбить на несколько томов. Этот, первый из них, посвящен базовым основам хакерства – технике работы с отладчиком и дизассемблером. Затронуты вопросы защиты программ от изучения и техника нейтрализации защит, впрочем,​ подробный рассказ о методике создания и снятия защитных механизмов – тема последующих томов.
 +
 +====== Кто такие хакеры ======
 +
 +//​…Назови ты меня вчера быком, я был бы быком. Назвал бы ты меня лошадью -- и я был бы лошадью. Если люди дают имя какой-то сущности,​ то, не приняв этого имени, навлечешь на себя беду.//
 +
 +//​Приписывается китайскому мудрецу Лао-цзы//​
 +
 +Прежде чем подавать на стол блюда хакерской кухни, неплохо бы разобраться кто, собственно,​ эти хакеры и что они едят? Заглянув в толковый словарь английского языка, например в "//​The////​American////​Heritage////​Dictionary//",​ мы убедимся,​ что глагол "//​**hack**//"​ возник в английском лексиконе задолго до появления компьютеров и в прямом смысле обозначал "​бить,​ рубить,​ кромсать"​ (но не уродовать!) топором,​ мотыгой или молотом. Т.е. делать физически тяжелую,​ монотонную,​ занудную,​ интеллектуально непритязательную работу – удел батраков,​ неудачников и бездарей. Неудивительно,​ что производные от глагола "​хак"​ обозначали "​бить баклуши",​ "​халтурить",​ "​выполнять работу наспех"​ – ведь наемные рабочие испокон веков "​фунциклировали"​ из-под палки! Термин считался пренебрежительным,​ если не ругательным:​ "​хак"​ стало даже синонимом нашего "​кляча"​! Словом,​ в докомпьютерную эпоху титулом "​хакера"​ ни один здравомыслящий человек ни возгордился бы…
 +
 +Сегодня же "​хакер"​ звучит практически так же как "​национальный герой",​ пускай и преступный,​ но все же крутой малый, которому не грех подражать. Чем же объясняется такая метаморфоза?​
 +
 +По одной из гипотез в щелчке,​ издаваемом реле, американцам слышалось "​хак - хак"​. Динозавры машинной эры состояли из многих тысяч реле и "​хакали"​ во всю, особенно когда оператор ЭВМ запускал очередную программу на выполнение. Возможно,​ именно за это операторов и прозвали "​хакерами"​. Или, говоря по-русски "​клацальщиками"​. По другой гипотезе звук "​хак"​ приписывается перфоратору,​ кромсающему перфоленту на мелкие куски, так что щепки (такие аккуратненькие круглые "​щепочки"​) во все стороны летят!
 +
 +На ассоциативном уровне обе гипотезы вполне правдоподобны. И реле, и перфоратор издают повторяющиеся монотонные удары, чем-то напоминающие кашель,​ а выражение "​кашлять сухим кашлем"​ - одно из значений слова "​hack"​. К тому же, программировали "​динозавров"​ исключительно в машинных кодах, подчас с помощью переключателей или перетыкивания разъемов,​ - физически тяжелая,​ нудная,​ неблагодарная работа,​ достающаяся наименее привилегированной части персонала. Какой там романтизм?​ Какое изящество решений или полет мысли? Халтура сплошная… Редкая программа обходится без ошибок,​ а программа,​ составленная в машинных кодах – тем более. При желании любого оператора было можно назвать халтурщиком – "​хакером"​ в ругательном смысле этого слова. "​Вот,​ наделал кучу ошибок,​ хакер ты наш!"​
 +
 +Обыватели же, далекие от вычислительной техники,​ и знакомые с ней исключительно по фантастическим романам,​ испытывали перед ЭВМ благоговейное уважение,​ подогреваемое гордостью за научно-технические достижения всего рода homosapiens в целом и американской нации в частности. "​Белые воротнички"​ – цвет нации, управляющие махиной размером с супермаркет и стоящей дороже тысячи таких супермаркетов,​ вызывали у рядового американца смесь восторга,​ зависти и стремления к подражанию. Вроде как "я тоже хочу быть космонавтом",​ не задумываясь о том, что космонавтика это только с виду романтика,​ а в действительности – каторжная работа.
 +
 +Но, если желание побывать в космосе до сих пор смогли реализовать лишь единицы,​ то ЭВМ стали широко доступными уже в начале шестидесятых. К тому времени их было можно встретить и в подвалах университетов,​ и в стенах крупных корпораций,​ и практически во всех исследовательских учреждениях. Очутиться за пультом ЭВМ в создании студента означало практически то же самое, что и "​сесть за штурвал реактивного бомбардировщика"​. Программирование ассоциировалось отнюдь не с "​батрачеством",​ а с интеллектуальной игрой. И "​старшие наставники"​ студентов – операторы ЭВМ были не только их руководителями,​ но и кумирами. Студенты,​ одержимые вычислительной техникой,​ стремились во всем копировать персонал,​ обслуживающий большие ЭВМ, часто без понимания сути происходящего. Прознав жаргонное прозвище операторов,​ студенты,​ не догадываясь о его иронично – оскорбительном оттенке,​ с достоинством стали называть хакерами и себя и своих товарищей,​ и даже свою работу окрестили "​хакерством"​. Но в их устах слово "​хакер"​ звучало отнюдь не насмешкой,​ а расценивалось как титул. Ты – хакер, значит,​ ты такой же мастер,​ как и настоящий оператор ЭВМ. Значит,​ ты крутой парень и перед тобой не стыдно снять шляпу.
 +
 +Так "​хакеры"​ из работяг превратились в программистов – энтузиастов,​ помешенных на компьютерах и выделывающих на них такое… такое, что другим и не снилось. Термин продолжал видоизменяться,​ мигрируя своими значениями в сторону "​крутого трюка",​ "​забавного эффекта",​ "​выполненного со вкусом розыгрыша"​. Этот дух подхватили и другие факультеты,​ порой и вовсе не связанные ни с электроникой,​ ни с вычислительной техникой,​ ни даже с точными науками вообще. "​Хаком"​ стали называть любой классный розыгрыш или нестандартное решение знакомой задачи,​ – жаргонный термин технического языка превратился в модное словечко,​ употребляемое всеми кому не лень.
 +
 +Тем временем мутация "​хакера"​ продолжалась… ​ Чтобы понять ее причины мысленно перенесемся в конец шестидесятых – начало семидесятых,​ а, может, даже чуточку позже. В те годы среди западной молодежи витал дух борьбы. Борьбы с кем? Да разве это важно! Протестовали против войны во Вьетнаме (кто не хотел служить в армии – жгли повестки),​ ломали пуританские устои старого мира, провозглашая свободу любви, презирали деньги (или только делали вид, что презирали,​ завистливо поглядывая в сторону того, у кого они есть). По большому счету вся борьба сводилась к суете в песочнице и власть имущих в общем-то ничуть не раздражала. Молодежные лидеры не имели в руках никакого оружия – ни политического,​ ни экономического,​ ни идеологического,​ не говоря уже об огнестрельном. К тому же, через десяток лет дух борьбы покинул Америку и весь шум закончился.
 +
 +"​Счастливое исключение"​ составили программисты. В те дни компьютерные системы еще не успели обзавестись достойной защитой,​ но уже управляли стратегически и экономическими важными объектами. Власть над компьютерами позволяла дать хорошего пинка и правительственным организациям,​ и финансовым магнатам,​ и корпорациям,​ и другим сильным мира сего, причем,​ оставаясь безнаказанным. Не существовало ни соответствующих законов,​ ни компьютерной полиции,​ способной "​вычислить"​ преступника…
 +
 +Словом,​ дикий запад времен разбоя,​ романтики и беспредела,​ когда человек с кольтом мог заставить шерифа мирного уездного городка "​слушать Шопена лежа"​. У американцев надо сказать,​ по поводу освоения Америки очень сильный комплекс – одних вестернов они сняли больше,​ чем мы фильмов про Великую Отечественную Войну. Понятно дело, каждый юный американец в душе мнит себя полноправным ковбоем!
 +
 +Компьютеры же позволили воплотить эту мечту в жизнь. Освой ЭВМ и носись по электронным сетям, как "​неуловимый Джо",​ отстреливающий индейцев (банкиров,​ ЦРУ-шников и т.д.). Да и как не носиться,​ когда на книжных лотках как грибы появлялись фантастические романы,​ главными героями которых были компьютерные взломщики – хакеры. Писатели,​ никогда в жизни не видевшие ЭВМ, плохо разбирались в техническом жаргоне и употребляли его на интуитивно-бессознательном уровне безо всякого понимания. Достаточно перелистать "​TheShockwareRider"​ Джона Бруннера (JohnBrunner) 1975 года, "​TheAdolescenceofP‑1"​ Томаса Риана (ThomasRyan) 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 компилятор по вкусу (в книге подробно описываются особенности компиляторов MicrosoftVisualC**++**,​ BorlandC++, WATCOMC, GNUC, FreePascal, а за основу взят MicrosoftVisualC++ 6.0).
 +
 +Теперь обо всем этом подробнее:​
 +
 +//::////​Soft////​-////​Ice//​. Отладчик Soft-Ice – основное оружие хакера. Хотя, с ним конкурируют бесплатные WINDEB от Microsoft и TRW от LiuTaoTao – Soft-Ice много лучше и удобнее всех их вместе взятых. Для наших экспериментов подойдет практически любая версия Айса, например,​ автор использует давно апробированную и устойчиво работающую 3.26, замечательно уживающуюся с Windows 2000. Новомодная 4.x не очень-то дружит с моим видеоадаптером (MatroxMillenniumG450 для справки) и вообще временами "​едет крышей"​. К тому же, из всех новых возможностей четвертой версии полезна лишь поддержка 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" <​gav@geliosoft.mtu-net.ru>//​). К сожалению,​ с дизассемблером не распространяется никакой документации (не считая встроенного хелпа – очень короткого и бессистемного),​ поэтому мне ничего не остается,​ как порекомендовать собственный трехтомник "//​Образ мышления – дизассемблер ////​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 прилагается и к компилятору MicrosoftVisualC++ 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 как о страшном сне, так стоит ли хвататься за эту умирающую платформу?​ К слову сказать,​ WindowsMe это не то же самое, что Windows 2000 и ставить Me на свой компьютер я никому не рекомендую (такое впечатление,​ что WindowsMe вообще не тестировали,​ а о том, что ее писали садисты – кто ставил,​ тот поймет – я вообще молчу).
 +
 +Худо-бедно разобравшись с инструментарием,​ поговорим о сером веществе,​ ибо в его отсутствии весь собранный инструмент бесполезен. Авторполагает,​ что читатель уже знаком с ассемблером и, если не пишет программ на этом языке, то, по крайней мере, представляет себе что такое регистры,​ сегменты,​ машинные инструкции и т.д. В противном случае эта книга рискует показаться через чур сложной и непонятной. Отыщите в магазине любой учебник по ассемблеру (например:​ В. Юрова "//​ASSEMBLER////​ – учебник//",​ П.И. Рудакова "//​Программируем на языке ассемблера ////​IBM////​PC//"​ или "//​Assembler////​ – язык неограниченных возможностей//"​ Зубкова С.В) и основательно проштудируйте его.
 +
 +Помимо знания ассемблера так же потребуется иметь хотя бы общие понятия о функционировании операционной системы. Купите и вдумчиво изучите (если не сделали этого до сих пор) "//​Windows////​ для профессионалов//"​ Джефри Рихтера {>>>>​ сноска см "​Приложение",​ "//​Ошибки Джефри Рихтера//"​} и (если найдете) "//​Секреты системного программирования в ////​Windows////​ 95//" Мэта Питрека. Хотя его книга посвящена Windows 95, частично она справедлива и для Windows 2000. Для знакомства с архитектурой самой же Windows 2000 рекомендуется ознакомиться с шедевром Хелен Кастер "//​Основы ////​Windows////​NT//"​ и брошюрой "//​Недокументированные возможности ////​Windows////​NT//"​ А.В. Коберниченко.
 +
 +Касаемо общей теории информатики и алгоритмов – бесспорный авторитет Кнут. Впрочем,​ на мой вкус монография М. Броя "//​Информатика//"​ куда лучше, - при том что она намного короче,​ круг охватываемых ей тем и глубина изложения – намного шире. Зачем хакеру теория информатики?​ Да куда же без нее! Вот, скажем,​ встретится ему защита со встроенным эмулятором машины Тьюринга.. Слету ее не сломать,​ - надо как минимум опознать сам алгоритм:​ что это вообще такое – Тьюринг,​ Марков,​ сеть Петри, а затем – отобразить его на язык высокого уровня,​ дабы в удобочитаемом виде анализировать работу защиты. Куда же тут без теории информатики!
 +
 +За сим все. Ну, разве что стоит дополнить наш походный рюкзачок парой учебников по английскому (они пригодятся,​ поверьте) и выкачать с сайтов Intel и AMD всю имеющуюся там документацию по процессорам. На худой конец подойдет и ее русский перевод,​ например,​ Ровдо А.А. "//​Микропроцессоры от 8086 до ////​Pentium////​III////​Xeon////​ и ////​AMD////​K////​6-3//"​.
 +
 +Ну-с, рюкзачок на плечо и в путь…
 +
 +====== Знакомство с базовыми приемами работы хакера ======
 +
 +===== Введение. =====
 +
 +==== Классификация защит ====
 +
 +//"​Стать хакером очень просто. Достаточно выучить и понять:​ математический анализ,​ теорию функций комплексного переменного,​ алгебру,​ геометрию,​ теорию вероятностей,​ математическую статистику,​ ////​математическую логику и дискретную математику..."​. //
 +
 +Борис Леонтьев "​Хакеры & Internet"​.
 +
 +Проверка //​**аутентичности**//​ (от греческого "//​authentikos//"​ – подлинный) – "​сердце"​ подавляющего большинства защитных механизмов. Должны же мы удостовериться:​ то ли лицо работает с программой,​ за которое оно себя выдает,​ и разрешено ли этому лицу работать с программой вообще! В качестве "​лица"​ может выступать не только пользователь,​ но и его компьютер или носитель информации,​ хранящий лицензионную копию программы. Таким образом,​ все защитные механизмы можно разделить на две основных категории:​
 +
 +  - защиты,​ основанные на //​**знании**//​(пароля,​ серийного номера)
 +  - защиты,​ основанные на //​**обладании**//​(ключевой диск, документация)
 +Защиты,​ основанные на знании,​ бесполезны,​ если обладатель защищенной с их помощью программы,​ не заинтересован в сохранении ее секретности. Он может сообщить пароль (серийный номер) кому угодно,​ после чего любой сможет запустить такую программу на своем компьютере.
 +
 +Поэтому,​ парольные защиты для предотвращения пиратского копирования программ непригодны. Почему же тогда практически все крупные производители в обязательном порядке используют серийные номера?​ Ответ прост – для защиты своей интеллектуальной собственности грубой физической силой. Происходит это приблизительно так: …рабочая тишина такой-то фирмы внезапно нарушается топотом парней в камуфляже,​ сверяющих лицензионные номера Windows (MicrosoftOffice,​ MicrosoftVisualStudio) с лицензионными соглашениями,​ и стоит обнаружиться хотя бы одной "​левой"​ копии, как появившийся,​ словно из-под земли, сотрудник фирмы начинает радостно потирать руки в предвкушении дождя вечнозеленых… В лучшем случае – заставят купить все "​левые"​ копии, в худшем же…
 +
 +К домашним пользователям в квартиру,​ понятное дело, никто не врывается – частная собственность и все такое, да к этому никто собственно и не стремится. Что с домашнего пользователя возьмешь-то?​ К тому же, самим фирмам выгодно массовое распространение их продукции,​ а кто его обеспечит лучше пиратов?​ Но и здесь серийные номера не лишние – они разгружают службу технической поддержки от "​левых"​ звонков незарегистрированных пользователей,​ одновременно с этим склоняя последних к покупке легальной версии.
 +
 +Такая схема защиты идеальна для корпораций-гигантов,​ но она не подходит для мелких программистских коллективов и индивидуальных разработчиков,​ особенно если они зарабатывают на жизнь написанием узкоспециализированных программ с ограниченным рынком сбыта (скажем,​ анализаторов звездных спектров или системы моделирования ядерных реакций). Не имея достаточного влияния,​ "​раскачать"​ сотрудников известных органов на облаву по проверки лицензионности своего ПО нереально,​ а "​выбить"​ деньги из нелегальных пользователей можно разве что с помощью криминальных структур,​ да и то навряд ли. Вот и приходится рассчитывать лишь на собственную силу и смекалку.
 +
 +Тут лучше подходит тип защит, основанных на обладании некоторым уникальным предметом,​ скопировать который чрезвычайно тяжело,​ а в идеале – вообще невозможно. Первые ласточки этой серии – ключевые дискеты,​ записанные с таким расчетом,​ чтобы при их копировании копия чем-нибудь да отличалась от оригинала. Самое простое (но не самое лучше) слегка изуродовать дискету гвоздем (шилом,​ перочинным ножом),​ а затем, определив местоположение дефекта относительно сектора (это можно сделать записью-чтением некоторой тестовой информации – до какого-то момента чтение будет идти нормально,​ а потом начнется "​мусор"​),​ жестко прописать его в программе и при каждом запуске проверять – на том же самом месте дефект или нет? Когда же дискеты вышли из употребления,​ эта же техника была адоптирована и для компакт-дисков. Кто побогаче уродует их лазером,​ кто победнее – все тем же шилом или гвоздем.
 +
 +Таким образом,​ программа жестко привязана к диску (дискете) и требует ее присутствия для своей работы,​ а, поскольку скопировать такой диск нереально (попробуй-ка,​ добиться идентичных дефектов на копиях),​ пираты "​отдыхают"​.
 +
 +Защитные механизмы,​ основанные на обладании,​ часто модифицирует предмет обладания в процессе работы,​ ограничивая количество запусков программы или время ее использования. Особенно часто такая "​фишка"​ используется в инсталляторах – чтобы не нервировать пользователя,​ ключ запрашивается лишь однажды – на стадии установки программы,​ а работать с ней можно и без него. Если количество инсталляций ограничено,​ ущербом от несанкционированных установок одной копии программы на несколько компьютеров можно пренебречь.
 +
 +Единственная проблема – все это ущемляет права легального пользователя. Кому понравится ограничение на количество инсталляций?​ (А ведь некоторые люди переустанавливают систему и все ПО буквально каждый месяц, а то и несколько раз на дню). Ключевые диски распознаются не всеми типами приводов,​ зачастую "не видимы"​ по сети, а, если защитный механизм для увеличения стойкости к взлому,​ обращается к оборудованию напрямую,​ в обход драйверов,​ такая программа наверняка не будет функционировать под WindowsNT\2000 и весьма вероятно откажет в работе под Windows 9x (если, конечно,​ она не была заранее спроектирована соответствующим образом,​ но если так – это хуже, ибо некорректно работающая защита,​ исполняющаяся с наивысшими привидениями,​ может причинить немалый урон системе). Помимо этого ключевой предмет можно потерять,​ его могут украсть,​ да и сам он может выйти из строя (дискеты склонны сыпаться и размагничиваться,​ диски – царапаться,​ а электронные ключи – "​сгорать"​).
 +
 +Конечно,​ эти претензии относится к качеству реализации,​ а не к идее ключей вообще,​ но конечным пользователям от этого ничуть не легче! Если же защита создает неудобства,​ у пользователей появляется очень сильная мотивация к посещению ближайшего доступного пирата на предмет приобретения у него контрфактного программного обеспечения. И никакие разговоры о морали,​ этике, добропорядочности и т.д. не подействуют – своя рубашка ближе к телу, а о добропорядочности нужно в первую очередь задуматься разработчикам таких защит. Тов…, тьфу, господа,​ не отравляйте жизнь пользователям! Пользователи – тоже люди!
 +
 +В последнее время наибольшую популярность обрели защиты,​ основанные на регистрационных номерах –при первом запуске программа привязывается к компьютеру и включает "​счетчик"​ (вариант – блокирует некоторые функциональные возможности). А чтобы ее "​освободить"​ необходимо ввести пароль,​ сообщаемый разработчиком за некоторое материальное вознаграждение. Часто для предотвращения пиратского копирования пароль представляет собой некоторую производную от ключевых параметров компьютера (или производную от имени пользователя в простейшем случае).
 +
 +Разумеется,​ этот краткий обзор типов защит очень много оставил за кадром,​ но подробный разговор о классификации защит выходит за рамки этой книги, так что отложим его до второго тома.
 +
 +{{tekhnika-i-filosofiya-hakerskih-atak-text_Image_0.png}}
 +
 +Рисунок 1 0x026 Основные типы защит
 +
 +==== Философия стойкости ====
 +
 +//​Однажды один из друзей сказал Катону Старшему:​ "​Какое безобразие,​ что в Риме тебе до сих пор не воздвигли памятника! Я обязательно позабочусь об этом"​.//​
 +
 +//"​Не надо, - ответил Катон, - я предпочитаю,​ чтобы люди спрашивали,​ почему нет памятника Катону,​ чем почему он есть//.
 +
 +Т. Мессон
 +
 +Если защита базируется на одном лишь предположении,​ что ее код не будет изучен и/или изменен – это плохая защита. Отсутствие исходных текстов отнюдь не является непреодолимым препятствием для изучения и модификации приложения. Современные технологии обратного проектирования позволяют автоматически распознавать библиотечные функции,​ локальные переменные,​ стековые аргументы,​ типы данных,​ ветвления,​ циклы и т.д. А в недалеком будущем дизассемблеры,​ вероятно,​ вообще научатся генерировать листинги близкие по внешнему виду к языкам высокого уровня.
 +
 +Но даже сегодня трудоемкость анализа двоичного кода не настолько велика,​ чтобы надолго остановить злоумышленников. Огромное количество постоянно совершаемых взломов – лучшее тому подтверждение. В идеальном случае знание алгоритма работы защиты не должно влиять на ее стойкость,​ но это достижимо далеко не всегда. Например,​ если разработчик серверной программы решит установить в демонстрационной версии ограничение на количество одновременно обрабатываемых соединений (как часто и случается),​ злоумышленнику достаточно найти инструкцию процессора,​ осуществляющую такую проверку и удалить ее. Модификации программы можно воспрепятствовать постоянной проверкой контрольной суммы, но опять-таки,​ код, который вычисляет эту контрольную сумму и сверяет ее с эталоном,​ может быть найден и удален.
 +
 +Сколько бы уровней защиты ни существовало,​ один или миллион,​ программа //​может//​ быть взломана! Это только вопрос времени и усилий. Но в отсутствии реально действующих законов защиты интеллектуальной собственности разработчикам приходится больше полагаться на стойкость своей защиты,​ чем на помощь правоохранительных органов. Бытует мнение,​ что если затраты на нейтрализацию защитного механизма будут не ниже стоимости легальной копии, ее никто не будет ломать. Это неверно! Материальный стимул – не единственное,​ что движет хакером. Гораздо более сильной мотивацией оказывается //​интеллектуальная борьба//​ (кто умнее: я или автор защиты?​),​ //​спортивный азарт//​ (кто из хакеров сломает больше всего защит?​),​ //​любопытство//​ (а как это работает?​),​ //​повышение своего профессионализма //​(чтобы научится создавать защиты,​ сначала нужно научиться их снимать),​ да и просто //​интересное времяпровождение//​ (если его нечем занять). Многие молодые люди могут неделями корпеть над отладчиком,​ снимая защиту с программы стоимостью в несколько долларов,​ а то и вовсе распространяемой бесплатно (пример,​ файл - менеджер FAR для жителей России и СНГ абсолютно бесплатен,​ но это не спасает его взлома).
 +
 +Целесообразность защиты ограничивается конкуренцией – при прочих равных условиях клиент всегда выбирает незащищенный продукт,​ даже если защита не ущемляет его прав. В настоящее время спрос на программистов значительно превышает предложение,​ но в отдаленном будущем разработчикам придется либо сговориться,​ либо полностью отказаться от защит. И специалисты по защитам будут вынуждены искать себе другую работу.
 +
 +Это не значит,​ что данная книга бесполезна,​ напротив,​ полученные знания следует применить как можно быстрее,​ пока в защитах еще не отпала необходимость.
 +
 +===== Шаг первый. Разминочный. =====
 +
 +//​Бороться со своими мыслями,​ это уподобиться одному глупцу,​ который в целях аккуратности и гигиены решил больше не какать. День не какал, два не какал. Потом, конечно не выдержал,​ но всех продолжал уверять,​ что не какает.//​
 +
 +Аноним
 +
 +Алгоритм простейшего механизма аутентификации состоит в посимвольном сравнении введенного пользователем паролем с эталонным значением,​ хранящимся либо в самой программе (как часто и бывает),​ либо вне ее, например,​ в конфигурационном файле или реестре (что встречается реже).
 +
 +Достоинство такой защиты – крайне простая программная реализация. Ее ядро состоит фактически из одной строки,​ которую на языке Си можно записать так: – "//​if////​ (////​strcmp////​(введенный пароль,​ эталонный пароль)) {/* Пароль неверен */} ////​else////​ {/* Пароль ОК */}//"
 +
 +Давайте дополним этот код процедурами запроса пароля и вывода результатов сравнения,​ а затем испытаем полученную программу на "​прочность",​ т.е. стойкость к взлому.
 +
 +// Простейшая система аутентификации
 +
 +// посимвольное сравнение пароля
 +
 +#include <​stdio.h>​
 +
 +#include <​string.h>​
 +
 +#define PASSWORD_SIZE 100
 +
 +#define PASSWORD ​ "​myGOODpassword\n"​
 +
 +// этот перенос нужен затем, чтобы ​ ^^^^
 +
 +// не выкусывать перенос из строки,​
 +
 +// введенной пользователем
 +
 +int main()
 +
 +{
 +
 +// Счетчик неудачных попыток аутентификации
 +
 +intcount=0;
 +
 +// Буфер для пароля,​ введенного пользователем
 +
 +char buff[PASSWORD_SIZE];​
 +
 +// Главный цикл аутентификации
 +
 +for(;;)
 +
 +{
 +
 +// Запрашиваем и считываем пользовательский ​
 +
 +// пароль
 +
 +printf("​Enter password:"​);​
 +
 +fgets(&​buff[0],​PASSWORD_SIZE,​stdin);​
 +
 +// Сравниваем оригинальный и введенный пароль
 +
 +if (**strcmp(&​buff[0],​PASSWORD)**)
 +
 +// Если пароли не совпадают – "​ругаемся"​
 +
 +printf("​Wrong password\n"​);​
 +
 +// Иначе (если пароли идентичны)
 +
 +// выходим из цикла аутентификации
 +
 +elsebreak;
 +
 +// Увеличиваем счетчик неудачных попыток
 +
 +// аутентификации и, если все попытки
 +
 +// исчерпаны – завершаем программу
 +
 +if (++count>​3) return –1;
 +
 +}
 +
 +// Раз мы здесь, то пользователь ввел правильный пароль
 +
 +printf("​Password OK\n"​);​
 +
 +}
 +
 +Листинг 1 Пример простейшей системы аутентификации
 +
 +В популярных кинофильмах крутые хакеры легко проникают в любые жутко защищенные системы,​ каким-то непостижимым образом угадывая искомый пароль с нескольких попыток. Почему бы ни попробовать пойти их путем?
 +
 +Не так уж редко пароли представляют собой осмысленные слова, наподобие "​Ferrari",​ "​QWERTY",​ имена любимых хомячков,​ названия географических пунктов и т.д. Угадывание пароля сродни гаданию на кофейной гуще – никаких гарантий на успех нет, остается рассчитывать на одно лишь везение. А удача, как известно,​ птица гордая – палец ей в рот не клади. Нет ли более надежного способа взлома?​
 +
 +Давайте подумаем – раз эталонный пароль хранится в теле программы,​ то, если он не зашифрован каким-нибудь хитрым образом,​ его можно обнаружить тривиальным просмотром двоичного кода программы. Перебирая все, встретившиеся в ней текстовые строки,​ начиная с тех, что более всего смахивают на пароль,​ мы очень быстро подберем нужный ключ и "​откроем"​ им программу!
 +
 +Причем,​ область просмотра можно существенно сузить,​ – в подавляющем большинстве случаев компиляторы размешают все инициализированные переменные в сегменте данных (в PE-файлах он размещается в секции "​**.****data**"​). Исключение составляют,​ пожалуй,​ ранние Багдадские (Borland-вые в смысле) компиляторы с их маниакальной любовью всовывать текстовые строки в сегмент кода – непосредственно по месту их вызова. Это упрощает сам компилятор,​ но порождает множество проблем. Современные операционные системы,​ в отличие от старушки MS-DOS, запрещают модификацию кодового сегмента,​ и все, размешенные в нем переменные,​ доступны лишь для чтения. К тому же, на процессорах с раздельной системой кэширования (на тех же Pentium-ах,​ например) они "​засоряют"​ кодовый кэш, попадая туда при упреждающем чтении,​ но при первом же к ним обращении вновь загружаются из медленной оперативной памяти (кэша второго уровня) в кэш данных. В результате – тормоза и падение производительности.
 +
 +Что ж, пусть это будет секция данных! Остается только найти удобный инструмент для просмотра двоичного файла. Можно, конечно,​ нажать <​**F****3**>​ в своей любимой оболочке (FAR, DOSNavigator) и, придавив кирпичом <​**Page****Down**>​ любоваться бегущими циферками до тех пор, пока не надоест. Можно воспользоваться любым hex-редактором (QVIEW, HIEW…) – кому какой по вкусу, но в книге по соображениям наглядности я приведу результат работы утилиты DUMPBIN из штатной поставки MicrosoftVisualStudio.
 +
 +Попросим ее распечатать секцию данных (ключ "/​SECTION:​.data"​) в "​сыром"​ виде (ключ "/​RAWDATA:​BYTES"​),​ указав значок ">"​ для перенаправления вывода в файл (ответ программы занимает много места и на экране помещается один лишь "​хвост"​).
 +
 +> dumpbin /​RAWDATA:​BYTES /​SECTION:​.data simple.exe >​filename
 +
 +RAW DATA #3
 +
 +00406000: 00 00 00 00 00 00 00 00 00 00 00 00 3B 11 40 00  ............;​.@.
 +
 +00406010: A4 40 40 00 00 00 00 00 00 00 00 00 E0 11 40 00  д@@.........р.@.
 +
 +00406020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
 +
 +00406030: 45 6E 74 65 72 20 70 61 73 73 77 6F 72 64 3A 00  Enter password:.
 +
 +00406040: 6D 79 47 4F 4F 44 70 61 73 73 77 6F 72 64 0A 00  **myGOODpassword**..
 +
 + ​^^^^^^^^^^^^^^  ​
 +
 +00406050: 57 72 6F 6E 67 20 70 61 73 73 77 6F 72 64 0A 00  Wrong password..
 +
 +00406060: 50 61 73 73 77 6F 72 64 20 4F 4B 0A 00 00 00 00  Password OK.....
 +
 +00406070: 40 6E 40 00 00 00 00 00 40 6E 40 00 01 01 00 00  @n@.....@n@.....
 +
 +Смотрите! Среди всего прочего тут есть одна строка до боли похожая на эталонный пароль (в тексте она выделена жирным шрифтом). Испытаем ее? Впрочем,​ какой смысл – судя по исходному тексту программы это действительно искомый пароль,​ открывающий защиту,​ словно Золотой Ключик. Слишком уж видное место выбрал компилятор для его хранения – пароль не мешало бы запрятать получше.
 +
 +Один из способов сделать это – насильно поместить эталонный пароль в собственноручно выбранную нами секцию. Такая возможность не предусмотрена стандартом и потому каждый разработчик компилятора (строго говоря,​ не компилятора,​ а линкера,​ но это не суть важно) волен реализовывать ее по-своему (или не реализовывать вообще). В MicrosoftVisualC++ для этой цели предусмотрена специальная прагма **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]**))
 +
 +> dumpbin /​RAWDATA:​BYTES /​SECTION:​.data simple2.exe >​filename
 +
 +RAW DATA #3
 +
 + ​00406000:​ 00 00 00 00 00 00 00 00 00 00 00 00 9B 11 40 00  ............Ы.@.
 +
 + ​00406010:​ 04 41 40 00 00 00 00 00 00 00 00 00 40 12 40 00  .A@.........@.@.
 +
 + ​00406020:​ 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
 +
 + ​00406030:​ 45 6E 74 65 72 20 70 61 73 73 77 6F 72 64 3A 00  Enter password:.
 +
 + ​00406040:​ 57 72 6F 6E 67 20 70 61 73 73 77 6F 72 64 0A 00  Wrong password..
 +
 + ​00406050:​ 50 61 73 73 77 6F 72 64 20 4F 4B 0A 00 00 00 00  Password OK.....
 +
 + ​00406060:​ 20 6E 40 00 00 00 00 00 20 6E 40 00 01 01 00 00  n@..... n@.....
 +
 + ​00406070:​ 00 00 00 00 00 00 00 00 00 10 00 00 00 00 00 00  ................
 +
 +Ага, теперь в секции данных пароля нет и хакеры "​отдыхают"​! Но не спешите с выводами. Давайте сначала выведем на экран список всех секций,​ имеющихся в файле:
 +
 +> dumpbin simple2.exe
 +
 +Summary
 +
 +2000 .data
 +
 +1000 **.kpnc**
 +
 + ^^^^
 +
 +1000 .rdata
 +
 +4000 .text
 +
 +Нестандартная секция "​**.****kpnc**"​ сразу же приковывает к себе внимание. А ну-ка глянем,​ что там в ней?
 +
 +dumpbin /​SECTION:​.kpnc /RAWDATA simple2.exe
 +
 +RAW DATA #4
 +
 + ​00408000:​ 6D 79 47 4F 4F 44 70 61 73 73 77 6F 72 64 0A 00  **myGOODpassword**..
 +
 + ​^^^^^^^^^^^^^^
 +
 +Вот он, пароль! Спрятали,​ называется… Можно, конечно,​ извратится и засунуть секретные данные в секцию неинициализированных данных ("​.bss"​),​ служебную RTL-секцию ("​.rdata"​) или даже секцию кода ("​.text"​) – не все там догадаются поискать,​ а работоспособность программы такое размещение не нарушит. Но не стоит забывать о возможности автоматизированного поиска текстовых строк в двоичном фале.. В какой бы секции ни содержался эталонный пароль – фильтр без труда его найдет (единственная проблема – определить какая из множества текстовых строк представляет собой искомый ключ; возможно,​ потребуется перебрать с десяток-другой потенциальных "​кандидатов"​).
 +
 +Правда,​ если пароль записан в уникоде,​ его поиск несколько осложняется,​ т.к. не все утилиты поддерживают эту кодировку,​ но надеяться,​ что это препятствие надолго задержит хакера – несколько наивно.
 +
 +===== Шаг второй. Знакомство с дизассемблером =====
 +
 +//Надо ли милостивого бога все время просить о пощаде?//​
 +
 +Велимир
 +
 +О'​кей,​ пароль мы узнали. Но как же утомительно вводить его каждый раз с клавиатуры перед запуском программы! Хорошо бы ее хакнуть так, чтобы никакой пароль вообще не запрашивался или любой введенный пароль программа воспринимала бы как правильный.
 +
 +Хакнуть говорите?​! Что ж, это не сложно! Куда проблематичнее определиться – //**чем именно **//ее хакать. Инструментарий хакеров чрезвычайно разнообразен – чего тут только нет: и дизассемблеры,​ и отладчики,​ и API-, и message- шпионы,​ и мониторы обращений к файлам (портам,​ реестру),​ и распаковщики исполняемых файлов,​ и… Попробуй-ка,​ начинающему кодокопателю со всем этих хозяйством разобраться!
 +
 +Впрочем,​ шпионы,​ мониторы,​ распаковщики – второстепенные утилиты заднего плана, а основное оружие взломщика – отладчик и дизассемблер. Рассмотрим их поближе.
 +
 +Как и следует из его названия,​ диз-ассемблер,​ предназначен для диз-ассемблирования или "​раз-ассемблирования"​ если перейти с латыни на русский {ДИС…, ДИЗ… [лат. dis, ге. dys] – приставка,​ обозначающая разделение отделение,​ отрицание;​ соответствует русским "​раз…",​ "​не…",​ сообщает понятию,​ к которому прилагается,​ отрицательный или противоположный смысл, напр. //​дизассоциация//,​ //​дисгармония – "​словарь иностранных слов"//​}. То есть если ассемблирование – перевод ассемблерных команд в машинный код, то дизассемблирование,​ напротив,​ перевод машинного кода в ассемблерные команды.
 +
 +Но пусть название не вводит вас в заблуждение:​ дизассемблер пригоден для изучения не только тех программ,​ что были написаны на ассемблере,​ – круг его применения очень широк, хотя и не безграничен. Спрашиваете – где же пролегает эта граница?​ Отвечаю.
 +
 +Грубо говоря,​ все реализации языков программирования делятся на //​**компиляторы**//​ и //​**интерпретаторы**//​.
 +
 +//::​Интерпретаторы //​исполняют программу в том виде, в каком она была набрана программистом. Другими словами говоря – интерпретаторы "​пережевывают"​ исходный текст, при этом код программы доступен для непосредственного изучения безо всяких дополнительных средств. Примером могут служить приложения,​ написанные на Бацике или Перле. Как известно,​ для их запуска помимо исходного текста программы требуется иметь еще и сам интерпретатор,​ что неудобно ни пользователям (для исполнения программы в 10 килобайт приходится устанавливать интерпретатор в 10 мегабайт),​ ни разработчикам (в здравом уме и трезвой памяти раздавать всем исходные тексты своей программы!),​ к тому же синтаксический разбор отнимает много времени и ни один интерпретатор не может похвастаться производительностью.
 +
 +//::​Компиляторы//​ ведут себя иначе – при первом запуске они "​перемалывают"​ программу в машинный код, исполняемый непосредственно самим процессором без обращений к исходным текстам или самому компилятору. С человеческой точки зрения откомпилированная программа представляет бессмысленную мешанину шестнадцатеричных байт, разобраться в которой неспециалисту абсолютно невозможно. Это облегчает разработку защитных механизмов – не зная алгоритма,​ вслепую защиту не сломаешь,​ ну разве что она будет совсем простая.
 +
 +Можно ли из машинного кода получить исходный текст программы?​ Нет! Компиляция – процесс однонаправленный. И дело тут не только в том, что безвозвратно удаляются метки и комментарии (ррразберемся и без комментариев – хакеры мы или нет?!), основной камень преткновения – //​неоднозначность соответствия машинных инструкций конструкциям языков высокого уровня//​. Более того, ассемблирование так же являет собой однонаправленный процесс и автоматическое дизассемблирование принципиально невозможно. Впрочем,​ не будем сейчас забивать голову начинающих кодокопателей такими тонкостями и оставим эту проблему на потом.
 +
 +::Ряд систем разработки занимает промежуточное положение между компиляторами и интерпретаторами,​ – исходная программа преобразуется не в машинный код, а в некоторый другой интерпретируемый язык, для исполнения которого к "​откомпилированному"​ файлу дописывается собственный интерпретатор. Именно по такой схеме функционируют FoxPro, Clipper, многочисленные диалекты Бацика и некоторые другие языки.
 +
 +Да, код программы по-прежнему исполняется в режиме интерпретации,​ но теперь из него удалена вся избыточная информация – метки, имена переменных,​ комментарии,​ а осмысленные названия операторов заменены их цифровыми кодами. Этот "​выстрел"​ укладывает сразу двух зайцев:​ а) язык, на который переведена программа,​ заранее "​заточен"​ под быструю интерпретацию и оптимизирован по размеру;​ б) код программы теперь недоступен для непосредственного изучения (и/или модификации).
 +
 +Дизассемблирование таких программ невозможно – дизассемблер нацелен именно на машинный код, а неизвестный ему интерпретируемый язык (так же называемый -кодом) он "не переваривает"​. Разумеется,​ -код не переваривает и процессор! Его исполняет интерпретатор,​ дописанный к программе. Вот интерпретатор-то дизассемблер и "​возьмет"​! Изучая алгоритм его работы,​ можно понять "​устройство"​ -кода и выяснить назначение всех его команд. Это очень трудоемкий процесс! Интерпретаторы порой так сложны и занимают столько много мегабайт,​ что их анализ растягивается на многие месяцы,​ а то и годы. К счастью,​ нет нужны анализировать //​**каждую**//​ программу – ведь интерпретаторы одной версии идентичны,​ а сам -код обычно мало меняется от версии к версии,​ во всяком случае его ядро не переписывается каждый день. Поэтому,​ вполне возможно создать программу,​ занимающуюся переводом -кода обратно в исходный язык. Конечно,​ символьные имена восстановить не удастся,​ но в остальном листинг будет выглядеть вполне читабельно.
 +
 +Итак, дизассемблер применим для исследования откомпилированных программ и частично пригоден для анализа "​псевдокомпилированного"​ кода. Раз так – он должен подойти для вскрытия парольной защиты simple.exe. Весь вопрос в том, – какой дизассемблер выбрать.
 +
 +Не все дизассемблеры одинаковы. Есть среди них и "​интеллектуалы",​ автоматически распознающие многие конструкции как-то:​ прологи и эпилоги функций,​ локальные переменные,​ перекрестные ссылки и т.д., а есть и "​простаки"​ чьи способности ограничены одним лишь переводом машинных команд в ассемблерные ​ инструкции.
 +
 +Логичнее всего воспользоваться услугами дизассемблера - интеллектуала (если он есть), но… давайте не будем спешить,​ а попробуем выполнить весь анализ вручную. Техника,​ понятное дело, – штука хорошая,​ да вот не всегда она оказывается под рукой и неплохо бы заранее научиться работе "в полевых условиях"​. К тому же, общение с плохим дизассемблером как нельзя лучше подчеркивает "​вкусности"​ хорошего.
 +
 +Воспользуемся уже знакомой нам утилитой DUMPBIN, настоящим "​Швейцарским ножиком"​ со множеством полезных функций,​ среди которых притаился и дизассемблер. Дизассемблируем секцию кода (как мы помним,​ носящую имя "​.text"​),​ перенаправив вывод в файл, т.к. на экран он, очевидно,​ не помститься.
 +
 +> dumpbin /​SECTION:​.text /DISASM **simple.exe** >​**.code**
 +
 +Так, менее чем через секунду образовался файл "​.code"​ с размером… с размером в целых триста с четвертью килобайт. Да исходная программа была на //**два порядка **//​короче! Это же сколько времени потребуется,​ чтобы со всей этой шаманской грамотой разобраться?​! Самое обидное – подавляющая масса кода никакого отношения к защитному механизму не имеет и представляет собой функции стандартных библиотек компилятора,​ анализировать которые нам ни к чему. Но как же их отличить от "​полезного"​ кода?
 +
 +Давайте подумаем. Мы не знаем, где именно расположена процедура сравнения паролей и нам неизвестно ее устройство,​ но можно с уверенностью утверждать,​ что один из ее аргументов – указатель на эталонный пароль. Остается только выяснить – по какому адресу расположен этот пароль в памяти – он-то и будет искомым значением указателя.
 +
 +Заглянем еще раз в секцию данных (или в другую – в зависимости от того, где хранится пароль):​
 +
 +> dumpbin /​SECTION:​.data /RAWDATA simple.exe >.data
 +
 +RAW DATA #3
 +
 + ​00406000:​ 00 00 00 00 00 00 00 00 00 00 00 00 7B 11 40 00  ............{.@.
 +
 + ​00406010:​ E4 40 40 00 00 00 00 00 00 00 00 00 20 12 40 00  ф@@......... .@.
 +
 + ​00406020:​ 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
 +
 + ​00406030:​ 45 6E 74 65 72 20 70 61 73 73 77 6F 72 64 3A 00  Enter password:.
 +
 + ​**00406040**:​ 6D 79 47 4F 4F 44 70 61 73 73 77 6F 72 64 0A 00  myGOODpassword..
 +
 + ​^^^^^^^^^ ​ ^^^^^^^^^^^^^^^
 +
 + ​00406050:​ 57 72 6F 6E 67 20 70 61 73 73 77 6F 72 64 0A 00  Wrong password..
 +
 + ​00406060:​ 50 61 73 73 77 6F 72 64 20 4F 4B 0A 00 00 00 00  Password OK.....
 +
 +Ага, пароль расположен по смещению 0x406040 (левая колонка чисел),​ стало быть и указатель на него равен 0x406040. Попробуем найти это число в дизассемблированном листинге тривиальным контекстным поиском в любом текстовом редакторе.
 +
 +Нашли? Вот оно (в тексте выделено жирным шрифтом):​
 +
 + ​00401045:​ 68 40 60 40 00  push  **406040h**
 +
 + ​0040104A:​ 8D 55 98  lea  edx,​[ebp-68h]
 +
 + ​0040104D:​ 52  push  edx
 +
 + ​0040104E:​ E8 4D 00 00 00  call  004010A0
 +
 + ​00401053:​ 83 C4 08  add  esp,8
 +
 + ​00401056:​ 85 C0  test  eax,eax
 +
 + ​00401058:​ 74 0F  je  00401069
 +
 +Это один из двух аргументов функции 0х04010A0, заносимых в стек машинной командой push. Второй аргумент – указатель на локальный буфер, вероятно,​ содержащий введенный пользователем пароль.
 +
 +Тут нам придется немного отклониться от темы разговора и подробно рассмотреть передачу параметров. Наиболее распространенны следующие способы передачи аргументов функции – //​**через регистры **//и через //​**стек**//​.
 +
 +Передача параметров через регистры наиболее быстра,​ но не лишена недостатков – во-первых,​ количество регистров весьма ограничено,​ а во-вторых,​ это затрудняет реализацию рекурсии – вызова функции из самой себя. Прежде чем заносить в регистры новые аргументы,​ необходимо предварительно сохранить старые в оперативной памяти. А раз так – не проще ли сразу передать аргументы через оперативную память,​ не мучаясь с регистрами?​
 +
 +Подавляющее большинство компиляторов передает аргументы через стек. Единого мнения по вопросам передачи у разработчиков компиляторов нет и встречаются по крайней мере два различных механизма,​ именуемые соглашениями "//​**Си**//"​ и "//​**Паскаль**//"​.
 +
 +:://​Си-соглашение//​ предписывает заталкивать в стек аргументы справа налево,​ т.е. самый первый аргумент функции заносится в стек последним и оказывается на его верхушке. Удаление аргументов из стека возложено не на саму функцию,​ а на вызываемый ее код. Это довольно расточительное решение,​ т.к. каждый вызов функции утяжеляет программу на несколько байт кода, но зато это позволяет создавать функции с переменным числом аргументов – ведь удаляет-то их из стека не сама функция,​ а вызывающий ее код, который наверняка знает точное количество переданных аргументов.
 +
 +Очистка стека обычно выполняется командой "​ADDESP,​xxx"​ – где '​xxx'​ количество удаляемых байт. Поскольку,​ в 32-разрядном режиме каждый аргумент,​ //**как правило,​ **//​занимает четыре байта, количество аргументов функции вычисляется так: . Оптимизирующие компиляторы могут использовать более хитрый код – для очистки стека от нескольких аргументов они частенько из "​выталкивают"​ в неиспользуемые регистры командой "​POP"​ или и вовсе очищают стек не сразу же после выхода из функции,​ а совсем в другом месте – где это удобнее компилятору.
 +
 +//::​Паскаль-соглашение //​предписывает заносить аргументы в стек слева направо,​ т.е. самый первый аргумент функции заносится в стек в первую очередь и оказывается в его "​низу"​. Удаление аргументов из функции возложено на саму функцию,​ и обычно осуществляется командой "​RETxxx"​ – т.е. возврат из подпрограммы со снятием xxx байт со стека.
 +
 +Возвращаемое функцией значение в обоих соглашениях передается через регистр EAX (или EDX:EAX при возвращении 64-разрядных переменных).
 +
 +Поскольку,​ исследуемая нами программа написана на Си и, стало быть, заносит аргументы справа налево,​ ее исходный текст выглядел приблизительно так:
 +
 +(*0x4010A0) (ebp-68, "​myGOODpassword"​)
 +
 +В том, что аргументов именно два, а не, скажем,​ четные или десять,​ нас убеждает команда "​ADDESP,​8",​ расположенная вслед за CALL.
 +
 + ​0040104E:​ E8 4D 00 00 00  call  004010A0
 +
 + ​00401053:​ 83 C4 08  **add  esp,8**
 +
 +Остается выяснить назначение функции 0x4010A0, хотя… если поднапрячь свою интуицию этого можно и не делать! И так ясно – это функция сравнивает пароль,​ иначе, зачем бы ей его передавали?​ //Как// она это делает – вопрос десятый,​ а вот что нас действительно интересует – возвращенное ею значение. Так, опускаемся на одну строчку ниже:
 +
 + ​0040104E:​ E8 4D 00 00 00  call  004010A0
 +
 + ​00401053:​ 83 C4 08  add  esp,8
 +
 + ​00401056:​ 85 C0  test  eax,eax
 +
 + ​00401058:​ 74 0F je **00401069**
 +
 +Что мы видим? Команда TESTEAX,​EAXпроверяет возвращенное функцией значение на равенство нулю, и если оно действительно равно нулю следующая за ней команда JE совершает прыжок на 0x401096 строку.
 +
 +В противном же случае (т.е. если EAX !=0)…
 +
 + ​0040105A:​ 68 50 60 40 00  push **406050**h
 +
 +Похоже еще на один указатель. Не правда ли? Проверим это предположение,​ заглянув в сегмент данных:​
 +
 + ​00406050:​ 57 72 6F 6E 67 20 70 61 73 73 77 6F 72 64 0A 00  **Wrong password**..
 +
 +Уже теплее! Указатель вывел нас на строку "//​Wrong////​password//",​ очевидно выводимую следующей функцией на экран. Значит,​ ненулевое значение EAX свидетельствует о ложном пароле,​ а нуль – об истинном.
 +
 +О'​кей,​ тогда переходим к анализу валидной ветви программы…
 +
 + ​0040105F:​ E8 D0 01 00 00  call  00401234
 +
 + ​00401064:​ 83 C4 04  add  esp,4
 +
 + ​00401067:​ EB 02  jmp  0040106B
 +
 + ​**00401069**:​ EB 16  jmp  00401081
 +
 +
 +
 + ​**00401081**:​ 68 60 60 40 00  push  **406060h**
 +
 + ​00401086:​ E8 A9 01 00 00  call  00401234
 +
 +Так еще, один указатель. Ну, а с функцией 0x401234 мы уже встречались выше – она (предположительно) служит для вывода строк на экран. Ну а сами строки можно отыскать в сегменте данных. На этот раз там притаилась "​PasswordOK"​
 +
 +Оперативные соображения следующие:​ если заменить команду JE на JNE, то программа отвергнет истинный пароль,​ как неправильный,​ а любой неправильный пароль воспримет как истинный. А если заменить "​TESTEAX,​EAX"​ на "​XOREAX,​EAX",​ то после исполнения этой команды регистр EAX будет //​всегда//​ равен нулю, какой бы пароль не вводился.
 +
 +Дело за малым – найти эти самые байтики в исполняемом файле и малость поправить их.
 +
 +===== Шаг третий. Хирургический =====
 +
 +//Не торопитесь на встречу с Богом, еще встретитесь.//​
 +
 +Народная мудрость
 +
 +Внесение изменений непосредственно в исполняемый файл – дело серьезное. Стиснутым уже существующим кодом, нам приходится довольствоваться только тем, что есть – и ни раздвинуть команды,​ ни даже "​сдвинуть"​ их, выкинув из защиты "​лишние запчасти",​ не получится. Ведь это привело бы к "​сдвигу"​ смещений всех остальных команд,​ тогда как значения указателей и адресов переходов остались без изменений и стали указывать совсем не туда, куда нужно!
 +
 +Ну, с "​выкидываем запчастей"​ справится как раз таки просто – достаточно забить код командами NOP (опкод который 0x90, а вовсе не 0х0, как почему-то думают многие начинающие кодокопатели) – т.е. пустой операцией (вообще-то NOPэто просто другая форма записи инструкции XCHGEAX,​EAX– если интересно). С "​раздвижкой"​ куда сложнее! К счастью,​ в PE-файлах всегда присутствует множество "​дыр",​ оставшихся от выравнивания – в них-то и можно разместить свой код или данные.
 +
 +Но не проще ли просто откомпилировать ассемблированный файл, предварительно внеся в него требуемые изменения?​ Нет, не проще, и вот почему – если ассемблер не распознает указатели,​ передаваемые функции (а, как мы видели,​ наш дизассемблер не смог отличить их от констант),​ он, соответственно,​ не позаботится должным образом их скорректировать и, естественно,​ программа работать не будет.
 +
 +Приходится "​резать"​ программу в "​живую"​. Легче всего это делать с помощью утилиты HIEW, "​переваривающей"​ PE-формат файлов и упрощающей тем самым поиск нужного фрагмента. Запустим его, указав имя файла в командной строке "​hiewsimple.exe",​ двойным нажатием <​**Enter**>​ переключимся в режим ассемблера и по <​**F****5**>​ перейдем к требуемому адресу. Как мы помним,​ команда "​TEST",​ проверяющая результат,​ возвращенный функцией на равенство нулю, располагалась по адресу 0x401056.
 +
 + ​0040104E:​ E8 4D 00 00 00  call  004010A0
 +
 + ​00401053:​ 83 C4 08  add  esp,8
 +
 + ​**00401056**:​ 85 C0  **test ​ eax,eax**
 +
 + ​^^^^^^^^^ ​ ^^^^^^^^^^^^^^^^^^^^^
 +
 + ​00401058:​ 74 0F je 00401069
 +
 +Чтобы HIEW мог отличить адрес от смещения в самом файле, предварим его символом точки: "​**.401056**"​
 +
 +00401056: 85C0  test  eax,eax
 +
 +00401058: 740F je .000401069 ​ -------- (1)
 +
 +Ага, как раз то, что нам надо! Нажмем <​**F****3**>​ для перевода HIEW в режим правки,​ подведем курсор к команде "​TESTEAX,​EAX"​ и, нажав <​**Enter**>,​ заменим ее на "​XOREAX,​EAX"​.
 +
 +00001056: 33C0  xor  eax,eax
 +
 +00001058: 740F  je  000001069
 +
 +С удовлетворением заметив,​ что новая команда в аккурат вписалась в предыдущую,​ нажмем <​**F****9**>​ для сохранения изменений на диске, а затем выйдет из HIEW и попробуем запустить программу,​ вводя первый пришедший на ум пароль.
 +
 +>​simple.exe
 +
 +Enter password:​**Привет****,​ ****шляпа****!**
 +
 +Password OK
 +
 +Получилось! Защита пала! Хорошо,​ а как бы мы действовали,​ не умей HIEW "​переваривать"​ PE-файлы?​ Тогда бы пришлось прибегнуть к контекстному поиску. Обратим свой взор на шестнадцатеричный дамп, расположенный дизассемблером слева от ассемблерных команд. Конечно,​ если пытаться найти последовательность "85 C0" – код команды "​TESTEAX,​EAX"​ ничего путного из этого не выйдет,​ – этих самых TEST-ов в программе может быть несколько сотен, а то и больше. Комбинация "​ADDESP,​8\TESTEAX,​EAX"​ так же вряд ли будет уникальна,​ поскольку встречается во многих типовых конструкциях языка Си "if (func(arg1,​arg2))…",​ "if (!func(arg1,​arg2))…",​ "​while(func(arg1,​arg2)"​ и т.д. А вот адрес перехода,​ скорее всего, во всех ветках программы различен и подстрока "​ADDESP,​8/​TESTEAX,​EAX/​JE 00401069"​ имеет хорошие шансы на уникальность. Попробуем найти в файле соответствующий ей код: "83 C4 08 85 C0 74 0F" (в HIEW-е для этого достаточно нажать <​**F****7**>​).
 +
 +Опп-с! Найдено только одно вхождение,​ что нам собственно и нужно. Давайте теперь попробуем модифицировать файл непосредственно в hex-режиме,​ не переходя в ассемблер. Попутно возьмем себе на заметку – инверсия младшего бита кода команды приводит к изменению условия перехода на противоположное. Т.е. 74 JE 75 JNE.
 +
 +Работает?​ (В смысле защита свихнулась окончательно – не признает истинные пароли,​ зато радостно приветствует остальные). Замечательно! Остается решить:​ как эту взломанную программу распространять. То есть, распространить-то ее дело не хитрое – на то и существуют CDR-писцы,​ BBS-ы, сеть Интернет,​ наконец! Заливай,​ пиши, нарезай – не хочу. Не хотите – и правильно! Незаконное это дело – распространять программное обеспечение в обход его владельца. Эдак, и засадить могут (причем прецеденты уже имеются). Куда безопаснее возложить распространение программы на ее дистрибьюторов,​ но до каждого пользователя донести:​ как эту программу сломать. Ковыряться в законном образом приобретенном приложении потребитель вправе,​ а распространение информации о взломе не запрещено в силу закона о свободе информации. Правда,​ при ближайшем рассмотрении выясняется,​ что этот закон и у нас, и за океаном действует лишь формально,​ и, если не посадить,​ то по крайне мере попытаться это сделать,​ право охранительные органы вполне могут (и не только могут, но и делают). Когда дело касается чьих-то финансовых интересов,​ правосудие "​отдыхает"​. Наивно думать,​ что соблюдение закона автоматически дает некие гарантии. Нет, и еще раз нет! Чувствовать себя в относительной безопасности можно лишь при условии соблюдения кодекса "//​**да не навреди сильным мира сего**//"​.
 +
 +В любом случае – информация о взломе это не совсем тоже, что сам взлом и за это труднее привлечь к ответственности. Единственная проблема – попробуй-ка,​ объясни этим пользователям:​ как пользоваться hex-редактором и искать в нем такие-то байтики. Запорют же ведь файл за милую душу! Вот для этой цели и существуют автоматические взломщики.
 +
 +Для начала нужно установить,​ какие именно байты были изменены. Для этого нам вновь потребуется оригинальная копия модифицированного файла (предусмотрительно сохраненная перед его правкой) и какой-нибудь "​сравниватель"​ файлов. Наиболее популярными на сегодняшний день являются **c2u** by //​Professor////​Nimnul//​и **MakeCrk** by //​Doctor////​Stein////'////​s////​labs//​. Первый гораздо предпочтительнее,​ т.к. он не только более точно придерживается наиболее популярного "​стандарта",​ но и умеет генерировать расширенный xck формат. На худой конец можно воспользоваться и штатной утилитой,​ входящей в поставку MS-DOS\Windows – fc.exe (сокращение от //​**F**////​ile////​**C**////​ompare//​).
 +
 +Запустим свой любимый компаратор (это уж какой кому больше по душе) и посмотрим на результат его работы:​
 +
 +> fc simple.exe simple.ex_ > simple.dif
 +
 + ​^-оригинальный ^ файл ​ ^
 +
 + └- хакнутый‌файл
 +
 + └- файл различий
 +
 +> typesimple.dif
 +
 +Сравнение файлов simple.exe и SIMPLE.EX_
 +
 +00001058: 74 75
 +
 +Первая слева колонка указывает смещение байта от начала файла, вторая – содержимое байта оригинального файла, а третья – его значение после модификации. Теперь сравним это с отчетом утилиты c2u:
 +
 +>c2u simple.exe simple.ex_
 +
 +Все исправления заносятся в файл *.crx, где "​*"​ – имя оригинального файла. Рассмотрим результат сравнения поближе:​
 +
 +>​typesimple.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 всегда игнорирует,​ а WinNT – нет и если его не скорректировать,​ файл запускаться не будет! В нашем случае контрольная сумма заголовка PE-файла равна нулю (так ведет себя компилятор),​ что означает – целостность файла не проверяется и он, после хака, будет успешно работать как под Win 9x, так и под WinNT.
 +
 +"//​Protection//"​ – степень "​крутизны"​ защиты,​ выражаемой в процентах. 100% по идее соответствуют пределу интеллектуальных возможностей хакера – но кто же в этом захочет признаваться?​ Неудивительно,​ что "​крутизну"​ защиты обычно занижают,​ порой даже больше,​ чем на порядок (смотрите все, вот я какой крутой хакер, для меня что угодно взломать не сложнее чем кончик хвоста обмочить!). Нечестность – не порок, но…
 +
 +"//​Type////​of////​hack//"​ – тип хака, - поле полезное,​ скорее для других хакеров,​ чем для пользователей,​ ничего не смыслящих в защитах и типах их взлома. Впрочем,​ с типами взломов не все гладко и у самих хакеров – общепризнанных классификацией нет. Наиболее употребляемый термин "​**bit****-****hack**",​ как и следует из его названия,​ обозначает взлом посредством изменения одного или нескольких бит в одном или нескольких байтах. Частный случай bit-hack-а – JMPcorrection (jumping) – модификация адреса или условия перехода (то, что мы только что и проделали). "​NOPing"​ – это bit-hack с заменой прежних инструкций на команду NOP или вставку незначащих команд,​ например для затирания двухбайтового JZxxx можно применить сочетание однобайтовых INCEAX/​DECEAX.
 +
 +"//​Language//"​ – язык, а точнее компилятор,​ на котором написана программа. В нашем случае MicrosoftVisualC++ (мы это знаем, поскольку только что ее компилировали),​ а вот как быть с чужими программами?​ Первое,​ что приходит на ум, – поискать в файле копирайты – их оставляют очень многие компиляторы,​ в том числе и VisualC++ - сморите:​ "​000053d9:​MicrosoftVisualC++ RuntimeLibrary"​. Если же компиляторов нет, то пробуем прогнать файл через 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 в исполняемые файлы, чтобы пользователям не приходилось ломать голову:​ что это за крак такой и как им ломать.
 +
 +Может, кому-то такой интерфейс и понравится,​ а вот хакеры в свой массе мышь органически не переносят и любят текстовые (консольные) приложения и тетю Клаву.
 +
 +{{tekhnika-i-filosofiya-hakerskih-atak-text_Image_1.png}}
 +
 +Рисунок 2 0x001 PatchMaker за работой!
 +
 +===== Шаг четвертый. Знакомство с отладчиком =====
 +
 +//​Оставь свои мозги за дверью и внеси сюда только тело//
 +
 +Фредерик Тейлор
 +
 +Помимо дизассемблирования существует и другой способ программ – отладка. Изначально под отладкой понималось пошаговое исполнение кода, так же называемое //​**трассировкой**//​. Сегодня же программы распухли настолько,​ что трассировать их бессмысленно – вы тут же утоните в омуте вложенных процедур,​ так и не поняв, что они собственно делают. Отладчик – не лучше средство изучения алгоритма программы – с этим лучше справляется интерактивный дизассемблер (например,​ IDA).
 +
 +Подробный разговор об устройстве отладчика мы отложим на потом (см. "//​Приемы против отладчиков//"​),​ а здесь ограничимся лишь перечнем основных функциональных возможностей типовых отладчиков (без этого невозможно их осмысленное применение):​
 +
 +– отслеживание обращений на запись/​чтение/​исполнение к заданной ячейке (региону) памяти,​ далее по тексту именуемое "​бряком"​ ("​брейком"​);​
 +
 +– отслеживание обращений на запись/​чтение к портам ввода-вывода (уже не актуально для современных операционных систем,​ запрещающих пользовательским приложениям проделывать такие трюки – это теперь прерогатива драйверов,​ а  на уровне драйверов реализованы очень немногие защиты);​
 +
 +– отслеживание загрузки DLL и вызова из них таких-то функций,​ включая системные компоненты (как мы увидим далее – это основное оружие современного взломщика);​
 +
 +– отслеживание вызова программных/​аппаратных прерываний (большей частью уже не актуально,​ - не так много защит балуется с прерываниями);​
 +
 +– отслеживание сообщений посылаемых приложением окну;
 +
 +– и, разумеется,​ контекстный поиск в памяти.
 +
 +Как именно делает отладчик – пока знать необязательно,​ достаточно знать, что он это умеет и все. Куда актуальнее вопрос,​ – какой отладчик умеет это делать?​ Широко известный в пользовательских кругах TurboDebugger на само деле очень примитивный и никчемный отладчик – очень мало хакеров им что-то ломает.
 +
 +Самое мощное и универсальное средство – Soft-Ice, сейчас доступный для всех Windows-платформ (а когда он поддерживал лишь одну Windows 95, но не WindowsNT). Последняя на момент написания книги, четвертая версия,​ не очень-то стабильно работает с моим видеоадаптером,​ поэтому приходится ограничиваться более ранней,​ но зато устойчивой версией 3.25.
 +
 +==== Способ 0. Бряк на оригинальный пароль. ====
 +
 +Используя поставляемую вместе с "​Айсом"​ утилиту "​wldr"​ загрузим ломаемый нами файл, указав его имя в командной строке,​ например,​ так:
 +
 +>​wldrsimple.exe
 +
 +Да, я знаю, что wldr – 16-разрядный загрузчик,​ и NuMega рекомендует использовать его 32-разрядную версию loader32, специально разработанную для Win 9x\NT. Это так, но loader32 частенько глючит (в частности не всегда останавливается на первой строчке запускаемой программы),​ а wldr успешно работает и 32-разрядными приложениями,​ единственный присущий ему недостаток – отсутствие поддержки длинных имен файлов.
 +
 +Если отладчик настроен корректно,​ на экране появится черное текстовое окно, обычно вызывающее большое удивление у начинающих – это в нашу-то это эпоху визуальщины серый текст и командный язык alacommand.com! А почему бы и нет? Набрать на клавиатуре нужную команду куда быстрее,​ чем отыскать ее в длинной веренице вложенных меню, мучительно вспоминая где же вы ее в последний раз видели. К тому же язык – это естественное средство выражения мыслей,​ а меню – оно годится разве что для выбора блюд в ресторане. Вот хороший пример – попробуйте с помощью проводника Windows вывести на печать список файлов такой-то директории. Не получается?​ А в MS-DOS это было так просто dir >PRNи никаких лаптей!
 +
 +Если в окне кода видны одни лишь инструкции "​INVALID"​ (а оно так и будет) не пугайтесь – просто Windows еще не успела спроецировать исполняемый файл в память и выделить ему страницы. Стоит нажать <​**F****10**>​ (аналог команды "​P"​ – трассировка без заходов в функцию) или <​**F****8**>​ (аналог команды "​T"​ – трассировка с заходами в функции) как все сразу же станет на свои места.
 +
 +001B:​00401277 ​ INVALID
 +
 +001B:​00401279 ​ INVALID
 +
 +001B:​0040127B ​ INVALID
 +
 +001B:​0040127D ​ INVALID
 +
 +:P
 +
 +001B:​00401285 ​ PUSH  EBX
 +
 +001B:​00401286 ​ PUSH  ESI
 +
 +001B:​00401287 ​ PUSH  EDI
 +
 +001B:​00401288 ​ MOV  [EBP-18],​ESP
 +
 +001B:​0040128B ​ **CALL ​ [KERNEL32!GetVersion]**
 +
 +001B:​00401291 ​ XOR  EDX,EDX
 +
 +001B:​00401293 ​ MOV  DL,AH
 +
 +001B:​00401295 ​ MOV  [0040692C],​EDX
 +
 +Обратите внимание:​ в отличие от дизассемблера DUMPBIN, Айс распознает имена системных функций,​ чем существенно упрощает анализ. Впрочем,​ анализировать всю программу целиком нет никакой нужды. Давайте попробуем наскоро найти защитный механизм,​ и, не вникая в подробности его функционирования,​ напрочь отрубить защиту. Легко сказать,​ но сделать еще проще! Вспомним:​ по какому адресу расположен в памяти оригинальный пароль. Э… что-то плохо у нас с этим получается – то ли память битая, то ли медведь на лапоть наступил,​ но точный адрес никак не хочет вспоминаться. Не хочет – не надо. Найдем-ка мы его самостоятельно!
 +
 +В этом нам поможет команда "​map32"​ выдающая карту памяти выбранного модуля (наш модуль называется "​simple"​ – по имени исполняемого файла за вычетом расширения). ​
 +
 +:map32 simple
 +
 +Owner  Obj Name  Obj#  Address ​ Size  Type
 +
 +simple ​ .text  0001  001B:​00401000 ​ 00003F66 ​ CODE  RO
 +
 +simple ​ .rdata ​ 0002  0023:​00405000 ​ 0000081E ​ IDATA RO
 +
 +simple ​ **.data** 0003  **0023:​00406000** 00001E44 ​ IDATA RW
 +
 + ​^^^^ ​ ^^^^^^^^^^^^^
 +
 +Вот он, адрес начала секции "​.data"​. То, что пароль находится в секции "​.data",​ надеюсь,​ читатель все еще помнит. Даем команду "d 23:​406000"​ (возможно предварительно придется создать окно командой "​wc"​ – если окна данных нет) и, нажав, <​**ALT****-****D**>​ для перехода в это окно, прокрутим его содержимое <​**стрелкой вниз**>​ или кирпичом на <​**Page****Down**>​. Впрочем,​ кирпич излишен,​ – долго искать не придется:​
 +
 +0023:​**00406040** 6D 79 47 4F 4F 44 70 61-73 73 77 6F 72 64 0A 00  myGOODpassword..
 +
 +0023:​00406050 57 72 6F 6E 67 20 70 61-73 73 77 6F 72 64 0A 00  Wrong password..
 +
 +0023:​00406060 50 61 73 73 77 6F 72 64-20 4F 4B 0A 00 00 00 00  Password OK.....
 +
 +0023:​00406070 47 6E 40 00 00 00 00 00-40 6E 40 00 01 01 00 00  Gn@.....@n@.....
 +
 +0023:​00406080 00 00 00 00 00 00 00 00-00 10 00 00 00 00 00 00  ................
 +
 +0023:​00406090 00 00 00 00 00 00 00 00-00 00 00 00 02 00 00 00  ................
 +
 +0023:​004060A0 01 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
 +
 +0023:​004060B0 00 00 00 00 00 00 00 00-00 00 00 00 02 00 00 00  ................
 +
 +Есть контакт! Задумаемся еще раз (второй раз за этот день) чтобы проверить корректность введенного пользователем пароля защита,​ очевидно должна сравнить его с оригинальным. А раз так – установив точку останова на чтение памяти по адресу 0x406040, мы поймаем "за хвост"​ сравнивающий механизм. Сказано – сделано.
 +
 +:bpm 406040
 +
 +Теперь нажимаем <​**CTRL****-****D**>​ для выхода из отладчика (или отдаем команду "​x"​) и вводим любой пришедший на ум пароль,​ например,​ "​KPNC++"​. Отладчик "​всплывает"​ незамедлительно:​
 +
 +001B:​004010B0 ​ MOV  EAX,[EDX]
 +
 +001B:​004010B2 ​ CMP  AL,[ECX]
 +
 +**001B:​004010B4 ​ JNZ  004010E4 ​ (JUMP ****↑****)**
 +
 +001B:​004010B6 ​ OR  AL,AL
 +
 +001B:​004010B8 ​ JZ  004010E0
 +
 +001B:​004010BA ​ CMP  AH,[ECX+01]
 +
 +001B:​004010BD ​ JNZ  004010E4
 +
 +001B:​004010BF ​ OR  AH,AH
 +
 +Break due to BPMB #​0023:​00406040 RW DR3  (ET=752.27 milliseconds)
 +
 + MSR LastBranchFromIp=0040104E
 +
 + MSR LastBranchToIp=004010A0
 +
 +В силу архитектурных особенностей процессоров Intel, бряк срабатывает //​**после**//​ инструкции,​ выполнившей "​поползновение",​ т.е. CS:EIP указывают на следующую выполняемую команду. В нашем случае – JNZ 004010E4, а к памяти,​ стало быть, обратилась инструкция CMPAL, [ECX]. А что находится в AL? Поднимаем взгляд еще строкой выше – "​MOVEAX,​[EDX]"​. Можно предположить,​ что EСX содержит указатель на строку оригинального пароля (поскольку он вызвал всплытие отладчика),​ а EDX в таком случае – указатель на введенный пользователем пароль. Проверим наше предположение.
 +
 +:d edx
 +
 +0023:​00406040 6D 79 47 4F 4F 44 70 61-73 73 77 6F 72 64 0A 00  myGOODpassword..
 +
 +:d edx
 +
 +0023:​0012FF18 4B 50 4E 43 2B 2B 0A 00-00 00 00 00 00 00 00 00  KPNC++..........
 +
 +И правда – догадка оказалась верна. Теперь вопрос – а как это заломить?​ Вот, скажем,​ JNZ можно поменять на JZ или, еще оригинальнее,​ заменить EDX на ECX – тогда оригинальный пароль будет сравниваться сам с собой! Погодите,​ погодите… не стоит так спешить! А что если мы находится не в теле защиты,​ а в библиотечной функции (действительно,​ мы находится в теле strcmp), – ее изменение приведет к тому, что программа //​**любые**//​ строки будет воспринимать как идентичные. Любые – а не только строки пароля. Это не повредит нашему примеру,​ где strcmp вызывалась лишь однажды,​ но завалит нормальное полнофункциональное приложение. Что же делать?​
 +
 +Выйти из strcmp и подкорректировать тот самый "​IF",​ который анализирует правильный – не правильный пароль. Для этого служит команда "​**P****RET**"​ (трассировать пока не встреться ret – инструкция возврата из функции).
 +
 +:P RET
 +
 +001B:​0040104E ​ CALL  004010A0
 +
 +**001B:​00401053 ​ ADD  ESP,08**
 +
 +001B:​00401056 ​ TEST  EAX,EAX
 +
 +001B:​00401058 ​ JZ  00401069
 +
 +001B:​0040105A ​ PUSH  00406050
 +
 +001B:​0040105F ​ CALL  00401234
 +
 +001B:​00401064 ​ ADD  ESP,04
 +
 +001B:​00401067 ​ JMP  0040106B
 +
 +Знакомые места! Помните,​ мы их посещали дизассемблером?​ Алгоритм действий прежний – запоминаем адрес команды "​TEST"​ для последующей замены ее на "​XOR"​ или записываем последовательность байт, идентифицирующую… эй, постойте,​ а где же наши байты – шестнадцатеричное представление команд?​ Коварный Айс по умолчанию их не выводит,​ и заставить его это делать помогает команда "​**CODE****ON**"​
 +
 +code on
 +
 +001B:​0040104E ​ E84D000000 ​ CALL  004010A0
 +
 +001B:​00401053 ​ 83C408 ​ ADD  ESP,08
 +
 +001B:​00401056 ​ 85C0  TEST  EAX,EAX
 +
 +001B:​00401058 ​ 740F  JZ  00401069
 +
 +001B:​0040105A ​ 6850604000 ​ PUSH  00406050
 +
 +001B:​0040105F ​ E8D0010000 ​ CALL  00401234
 +
 +001B:​00401064 ​ 83C404 ​ ADD  ESP,04
 +
 +001B:​00401067 ​ EB02  JMP  0040106B
 +
 +Вот, теперь совсем другое дело! Но можно ли быть уверенным,​ что эти байтики по этим самым адресам будут находиться в исполняемом файле? Вопрос не так глуп, каким кажется на первый взгляд. Попробуйте сломать описанным выше методом пример "​crackme0x03"​. На первый взгляд он очень похож на simple.exe, - даже оригинальный пароль располагается по тому же самому адресу. Ставим на него бряк, дожидаемся всплытия отладчика,​ выходим из сравнивающей процедуры и попадаем на точно такой же код, который уже встречался нам ранее.
 +
 +001B:​0042104E ​ E87D000000 ​ CALL  004210D0
 +
 +001B:​00421053 ​ 83C408 ​ ADD  ESP,08====
 +
 +001B:​00421056 ​ 85C0  TEST  EAX,EAX
 +
 +001B:​00421058 ​ 740F  JZ  00421069
 +
 +Сейчас мы запустим HIEW, перейдем по адресу 0x421053 и… эй, постой,​ HIEW ругается и говорит,​ что в файле нет такого адреса! Последний байт заканчивается на 0x407FFF. Быть может, мы находимся в теле системной функции Windows? Но нет – системные функции Windows расположены значительно выше – начиная с адреса 0x80000000.
 +
 +Фокус весь в том, что PE-файл может быть загружен по адресу отличному от того, для которого он был создан (это свойство называется //​**перемещаемостью**//​),​ - при этом система автоматически корректирует все ссылки на абсолютные адреса,​ заменяя их новыми значениями. В результате – образ файла в памяти не будет соответствовать тому, что записано на диске. Хорошенькое начало! Как же теперь найти место, которое нужно править?​
 +
 +Задачу несколько облегчает тот факт, что системный загрузчик умеет перемещать только DLL, а исполняемые файлы всегда пытается загрузить по "​родному"​ для них адресу. Если же это невозможно – загрузка прерывается с выдачей сообщения об ошибке. Выходит,​ мы имеем дело с DLL, загруженной исследуемой нами защитой. Хм… вроде бы не должно быть здесь никаких DLL – да и откуда бы им взяться?​
 +
 +Что ж, изучим листинг 2 на предмет выяснения:​ как же он работает.
 +
 +#include <​stdio.h>​
 +
 +#include <​windows.h>​
 +
 +__declspec(dllexport) void Demo()
 +
 +^^^^^^^^^^^^^^^^^^^^^
 +
 +{
 +
 +#define PASSWORD_SIZE 100
 +
 +#define PASSWORD ​ "​myGOODpassword\n"​
 +
 +int count=0;
 +
 +char buff[PASSWORD_SIZE]="";​
 +
 +for(;;)
 +
 +{
 +
 +printf("​Enter password:"​);​
 +
 +fgets(&​buff[0],​PASSWORD_SIZE-1,​stdin);​
 +
 +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"​ (его ответ приведен в сокращенном виде)
 +
 +>dumpbin /HEADERS crack0x03
 +
 +OPTIONAL HEADER VALUES
 +
 +...
 +
 +400000 image base
 +
 +^^^^^^^^^^^^^^^^^
 +
 +...
 +
 +Значит,​ базовый адрес – 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: 8915**4C694000** 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.
 +
 +Чтобы познакомиться с ней поближе откомпилируем и изучим следующий пример:​
 +
 +::​fixupdemo.c
 +
 +__declspec(dllexport) void meme(int x)
 +
 +{
 +
 +static int a=0x666;
 +
 +a=x;
 +
 +}
 +
 +> cl fixupdemo.c /LD 
 +
 +Листинг 3 Исходный текст fixupdemo.c
 +
 +
 +
 +Откомпилируем и тут же дизассемблируем его: "​DUMPBIN /​DISASMfixupdemo.dll"​ и "​DUMPBIN /​SECTION:​.data /​RAWDATA"​.
 +
 + ​10001000:​ 55  push  ebp
 +
 + ​10001001:​ 8B EC  mov  ebp,esp
 +
 + ​10001003:​ 8B 45 08  mov  eax,dword ptr [ebp+8]
 +
 + ​10001006:​ A3 30 50 00 10  mov  [10005030],​eax
 +
 + ​^^^^^^^^^^^ ​ ^^^^^^^^
 +
 + ​1000100B:​ 5D  pop  ebp
 +
 + ​1000100C:​ C3  ret
 +
 +RAW DATA #3
 +
 +10005000: 00 00 00 00 00 00 00 00 00 00 00 00 33 24 00 10  ............3$..
 +
 +10005010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
 +
 +10005020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
 +
 +10005030: 66 06 00 00 E3 11 00 10 FF FF FF FF 00 00 00 00  f...у...    ....
 +
 + ^^^^^
 +
 +Судя по коду, запись содержимого EAX всегда происходит в ячейку 0x10005030, но не торопите с выводами! "​DUMPBIN /​RELOCATIONS fixupdemo.dll":​
 +
 +BASE RELOCATIONS #4
 +
 + 1000 RVA,  154 SizeOfBlock
 +
 + **7** HIGHLOW
 +
 + ​^  ​
 +
 + 1C HIGHLOW
 +
 + 23 HIGHLOW
 +
 + 32 HIGHLOW
 +
 + 3A HIGHLOW
 +
 +Таблица перемещаемых элементов-то не пуста! И первая же ее запись указывает на ячейку 0x100001007,​ полученную алгебраическим сложением смещения 0x7 с RVA-адресом 0x1000 и базовым адресом загрузки 0x10000000 (получите его с помощью DUMPBIN самостоятельно). Смотрим – ячейка 0x100001007 принадлежит инструкции "MOV [0x10005030],​EAX"​ и указывает на самый старший байт непосредственного смещения. Вот это самое смещение и корректирует загрузчик в ходе подключения динамической библиотеки (разумеется,​ если в этом есть необходимость).
 +
 +Хотите проверить?​ Пожалуйста,​ - создадим две копии одной DLL (например,​ copyfixupdemo.dllfixupdemo2.dll) и загрузим их поочередной следующей программой:​
 +
 +::​fixupload.c
 +
 +#include <​windows.h>​
 +
 +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 поддержки уникода – специальной кодировки,​ каждый символ в которой кодируется двумя байтами,​ благодаря чему приобретает способность выражать любой из 2<​sup>​16</​sup>​ = 65.536 знаков,​ – количество достаточно для вмещения практически всех алфавитов нашего мира. Применительно к LoadLibrary – теперь имя библиотеки может быть написано на любом языке, а при желании и на любом количестве любых языков одновременно,​ например,​ на русско-француско-китайском. Звучит заманчиво,​ но не ухудшает ли это производительность?​ Разумеется,​ ухудшает,​ еще как – уникод требует жертв! Самое обидное – в подавляющем большинстве случаев вполне достаточно старой доброй кодировки ASCII (во всяком случае нам – русским и американцам). Так какой же смысл бросать драгоценные такты процесса на ветер? Ради производительности было решено поступиться размером,​ создав отдельные варианты функций для работы с уникодом и ASCII-символами. Первые получили суффикс '​W'​ (от //​**W**////​ide//​ – //​широкий//​),​ а вторые – '​A'​ (от ASCII). Эта тонкость скрыта от прикладных программистов – какую именно функцию вызывать '​W'​ или '​A'​ решает компилятор,​ но при работе с отладчиком необходимо указывать точное имя функции – самостоятельно определить суффикс он не в состоянии. Камень преткновения в том, что некоторые функции,​ например,​ ShowWindows вообще не имеют суффиксов – ни '​A',​ ни '​W'​ и их библиотечное имя совпадает с каноническим. Как же быть?
 +
 +Самое простое – заглянуть в таблицу импорта препарируемого файла и отыскать там вашу функцию. Например,​ применительно к нашему случаю:​
 +
 +> DUMPBIN /IMPORTS fixupload.exe > filename
 +
 +> type filename
 +
 + ​19D ​ HeapDestroy
 +
 + ​1C2 ​ LoadLibrary**A**
 +
 + ​CA ​ GetCommandLine**A**
 +
 + ​174 ​ GetVersion
 +
 + ​7D ​ ExitProcess
 +
 + ​29E ​ TerminateProcess
 +
 +...
 +
 +Из приведенного выше фрагменты видно, что LoadLibrary все-таки '​A',​ а вот функции ExitProcess и TerminateProcess не имеют суффиксов,​ поскольку вообще не работают со строками.
 +
 +Другой путь – заглянуть в SDK. Конечно,​ библиотечное имя функций в нем отсутствует,​ но в "​QuickInfo"​ мимоходом приводится информация и поддержке уникода (если таковая присутствует). А раз есть уникод – есть суффиксы '​W'​ и '​A',​ соответственно,​ наоборот – где нет уникода,​ нет и суффиксов. Проверим?​
 +
 +Вот так выглядит QuickInfo от 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 для получения связного протокола,​ мы увидим следующее:​
 +
 + ​21: ​ 118  1 00039031 AddAtomW
 +
 + ​116: ​ 217  60 00039031 DeleteFileW
 +
 + ​119: ​ 220  63 00039031 DisconnectNamedPipe
 +
 + ​178: ​ 279  9E 00039031 FindAtomW
 +
 + ​204: ​ 305  B8 00039031 FreeEnvironmentStringsW
 +
 + ​260: ​ 361  F0 00039031 GetDriveTypeW
 +
 + ​297: ​ 398  115 00039031 GetModuleHandleW
 +
 + ​341: ​ 442  141 00039031 GetStartupInfoW
 +
 + ​377: ​ 478  165 00039031 GetVersionExW
 +
 + ​384: ​ 485  16C 00039031 GlobalAddAtomW
 +
 + ​389: ​ 490  171 00039031 GlobalFindAtomW
 +
 + ​413: ​ 514  189 00039031 HeapLock
 +
 + ​417: ​ 518  18D 00039031 HeapUnlock
 +
 + ​440: ​ 541  1A4 00039031 IsProcessorFeaturePresent
 +
 + ​455: ​ 556  1B3 00039031 LoadLibraryW
 +
 + ​508: ​ 611  1E8 00039031 OutputDebugStringW
 +
 + ​547: ​ 648  20F 00039031 RemoveDirectoryW
 +
 + ​590: ​ 691  23A 00039031 SetComputerNameW
 +
 + ​592: ​ 693  23C 00039031 SetConsoleCP
 +
 + ​597: ​ 698  241 00039031 SetConsoleOutputCP
 +
 + ​601: ​ 702  245 00039031 SetConsoleTitleW
 +
 + ​605: ​ 706  249 00039031 SetCurrentDirectoryW
 +
 + ​645: ​ 746  271 00039031 SetThreadLocale
 +
 + ​678: ​ 779  292 00039031 TryEnterCriticalSection
 +
 +Вотэтосюрприз! Все уникодовые ​ функции под одной крышей! Поскольку,​ трудно поверить в идентичность реализаций LoadLibraryW и, скажем,​ DeleteFileW,​ остается предположить,​ что мы имеем дело с "​заглушкой",​ которая ничего не делает,​ а только возвращает ошибку. Следовательно,​ в 9x действительно,​ функция LoadLibraryW не реализована.
 +
 +Но, вернемся,​ к нашим баранам от которых нам пришлось так далеко отойти. Итак, вызываем отладчик,​ ставим бряк на LoadLibraryA,​ выходим из отладчика и терпеливо дожидаемся его всплытия. Должно ждать, к счастью,​ не приходится… ​
 +
 +KERNEL32!LoadLibraryA                   ​
 +
 +001B:​77E98023 ​ PUSH  EBP
 +
 +001B:​77E98024 ​ MOV  EBP,ESP
 +
 +001B:​77E98026 ​ PUSH  EBX
 +
 +001B:​77E98027 ​ PUSH  ESI
 +
 +001B:​77E98028 ​ PUSH  EDI
 +
 +001B:​77E98029 ​ PUSH  77E98054
 +
 +001B:​77E9802E ​ PUSH  DWORD PTR [EBP+08]
 +
 +Отдаем команду "​**P****RET**"​ для выхода из LoadLibraryA (анализировать ее, в самом деле, ни к чему) и оказываемся в легко узнаваемом теле функции main.
 +
 +001B:​0040100B ​ CALL  [KERNEL32!LoadLibraryA]
 +
 +001B:​00401011 ​ MOV  [EBP-08],​EAX           
 +
 +001B:​00401014 ​ CMP  DWORD PTR [EBP-08],00
 +
 +001B:​00401018 ​ JZ  00401051
 +
 +001B:​0040101A ​ PUSH  00405040
 +
 +001B:​0040101F ​ CALL  [KERNEL32!LoadLibraryA]
 +
 +001B:​00401025 ​ MOV  [EBP-08],​EAX
 +
 +001B:​00401028 ​ CMP  DWORD PTR [EBP-08],00
 +
 +Обратите внимание на содержимое регистра EAX – функция возвратила в нем адрес загрузки (на моем компьютере равный 0x10000000). Продолжая трассировку (<​**F****10**>​),​ дождитесь выполнения второго вызова LoadLibraryA – не правда ли, на этот раз адрес загрузки изменился?​ (на моем компьютере он равен 0x0530000).
 +
 +Приблизившись к вызову функции demo (в отладчике это выглядит как PUSH 00000777\ CALL [EBP-04] – "​EBP-04"​ ни о чем не говорит,​ но вот аргумент 0x777 определенно что-то нам напоминает,​ - см. исходный текст fixupload.c),​ не забудьте переменить руку с <​**F****10**>​ на <​**F****8**>,​ чтобы войти внутрь функции.
 +
 +001B:​00531000 ​ 55  PUSH  EBP
 +
 +001B:​00531001 ​ 8BEC  MOV  EBP,ESP
 +
 +001B:​00531003 ​ 8B4508 ​ MOV  EAX,​[EBP+08]
 +
 +001B:​00531006 ​ A3**30505300** MOV  [**00535030**],​EAX
 +
 +001B:​0053100B ​ 5D  POP  EBP
 +
 +001B:​0053100C ​ C3  RET
 +
 +
 +
 +Вот оно! Системный загрузчик скорректировал адрес ячейки согласно базовому адресу загрузки самой DLL. Это, конечно,​ хорошо,​ да вот проблема – в оригинальной DLL нет ни такой ячейки,​ ни даже последовательности "A3 30 50 53 00", в чем легко убедиться контекстным поиском. Допустим,​ вознамерились бы мы затереть эту команду NOP-ми. Как это сделать?​! Вернее,​ как найти это место в оригинальной DLL?
 +
 +Обратим свой взор выше, на команды,​ заведомо не содержащие перемещаемых элементов – PUSHEBP/​MOVEBP,​ ESP/​MOVEAX,​[EBP+08]. Отчего бы не поискать последовательность "​55 ​ 8BEC xxx A3"? В данном случае это сработает,​ но если бы перемещаемые элементы были густо перемешаны "​нормальными"​ ничего бы не вышло. Опорная последовательность оказалась бы слишком короткой для поиска и выдала бы множество ложных срабатываний.
 +
 +Более изящно и надежно вычислить истинное содержимое перемещаемых элементов,​ вычтя их низ разницу между действительным и рекомендуемым адресом загрузки. В данном случае:​ 0x535030 /​модифицированный загрузчиком адрес/ – (0x530000 /​базовый адрес загрузки/​ - 0x10000000 /​рекомендуемый адрес загрузки/​) == 0x10005030. Учитывая обратный порядок следования байт, получаем,​ что инструкция MOV [10005030], EAXв машинном коде должна выглядеть так: "A3 30 50 00 10". Ищем ее HIEW-ом, и чудо – она есть!
 +
 +==== Способ 1. Прямой поиск введенного пароля в памяти ====
 +
 +//Был бы омут, а черти будут.//​
 +
 +народная поговорка
 +
 +Пароль,​ хранящийся в теле программы открытым текстом,​ – скорее из ряда вон выходящее исключение,​ чем правило. К чему услуги хакера,​ если пароль и без того виден невооруженным взглядом?​ Поэтому,​ разработчики защиты всячески пытаются скрыть его от посторонних глаз (о том, как именно они это делают,​ мы поговорим позже). Впрочем,​ учитывая размер современных пакетов,​ программист может, не особо напрягаясь,​ поместить пароль в каком-нибудь завалявшемся файле, попутно снабдив его "​крякушами"​ – строками,​ выглядевшими как пароль,​ но паролем не являющимися. Попробуй,​ разберись,​ где тут липа, а где нет, тем паче, что подходящих на эту роль строк в проекте средней величины может быть несколько сотен, а то и тысяч!
 +
 +Давайте подойдем к решению проблемы от обратного – будем искать не оригинальный пароль,​ который нам не известен,​ а ту строку,​ которую мы скормили программе в качестве пароля. А, найдя – установим на нее бряк, и дальше все точно так же, как и раньше. Бряк всплывает на обращение по сравнению,​ мы выходим из сравнивающей процедуры,​ корректируем JMP, и…
 +
 +Взглянем еще раз на исходный текст ломаемого нами примера "​simple.c"​
 +
 +for(;;)
 +
 +{
 +
 +printf("​Enter password:"​);​
 +
 +fgets(**&​buff[0],​**PASSWORD_SIZE,​stdin);​
 +
 +if (strcmp(**&​buff[0]**,​PASSWORD))
 +
 +printf("​Wrong password\n"​);​
 +
 +else break;
 +
 +if (++count>​2) return -1;
 +
 +}
 +
 +Обратите внимание – в buff читается введенный пользователем пароль,​ сравнивается с оригиналом,​ затем (при неудачном сравнении) запрашивается еще раз, но (!) при этом buff не очищается! Отсюда следует,​ что если после выдачи ругательства "​Wrongpassword"​ вызвать отладчик и пройтись по памяти контекстным поиском,​ можно обнаружить тот заветный buff, а остальное уже – дело техники!
 +
 +Итак, приступим (мы еще не знаем, во что мы ввязываемся – но, увы – в жизни все сложнее,​ чем в теории). Запускам SIMPLE.EXE, вводим любой пришедший на ум пароль (например,​ "​KPNCKaspersky++"​),​ пропускаем возмущенный вопль "​Wrong"​ мимо ушей и нажимаем <​**Ctrl****-****D**>​ - "​горячую"​ комбинацию клавиш для вызова Айса. Так, теперь будем искать?​ Подождите,​ не надо бежать впереди лошадей:​ Windows 9x\NT – это не Windows 3.x и, тем более, не MS-DOS с единым адресным пространством для всех процессоров. Теперь,​ по соображениям безопасности,​ - дабы один процесс ненароком не залез во владения другого,​ каждому из них предоставляется собственное адресное пространство. Например,​ у процесса A по адресу 23:0146660 может быть записано число "​0x66",​ у процесса B по //​**тому же самому адресу**//​ 23:0146660 может находиться "​0x0",​ а у процесса C и вовсе третье значение. Причем,​ процессы А, B и C не будет даже подозревать о существовании друг друга (ну, разве что воспользуются специальными средствами межпроцессорного взаимодействия).
 +
 +Подробнее обо всем этом читайте у Хелен или Рихтера,​ здесь же нас больше заботит другое – вызванный по <​**Ctrl****-****D**>​ отладчик "​всплывает"​ в произвольном процессе (скорее всего Idle) и контекстный поиск в памяти ничего не даст. Необходимо насильно переключить отладчик в необходимый контекст адресного пространства и лишь затем что-то предпринимать.
 +
 +Из прилагаемой к Айсу документации можно узнать,​ что переключение контекстов осуществляется командой **ADDR**, за которой следует либо имя процесса,​ урезанное до восьми символов,​ либо его PID. Узнать и то, и другое можно с помощью другой команды – **PROC** (В том, случае если имя процесса синтаксически неотличимо от PID, например,​ "​123",​ приходится использовать PID процесса – вторая колонка цифр слева, в отчете PROC).
 +
 +:addrsimple
 +
 +Отдаем команду "//​addr////​simple//"​ и… ничего не происходит,​ даже значения регистров остаются неизменными! Не волнуйтесь – все ОК, что и подтверждает надпись '​simple'​ в правом нижнем углу, идентифицирующая текущий процесс. А регистры… это небольшой глюк Айса. Он них игнорирует,​ переключая только адреса. В частности поэтому,​ трассировка переключенной программы невозможна. Вот поиск – другое дело. Это – пожалуйста!
 +
 +:s 23:0 L -1 "​KPNCKaspersky"​
 +
 +Пояснения:​ первый слева аргумент после s – адрес, записанный в виде "​селектор:​ смещение"​. Под Windows 2000 для адресации данных и стека используется селектор номер 23, в других операционных системах он может отличаться (и отличается!). Узнать его можно загрузив любую программу,​ и списав содержимое регистра DS. //​Смещение//​ – вообще-то,​ начинать поиск с нулевого смещения – идея глупая. Судя по карте памяти,​ здесь расположен служебный код и искомого пароля быть не может. Впрочем,​ это ничему не вредит,​ и так гораздо быстрее,​ чем разбираться:​ с какого адреса загружена программа,​ и откуда именно начинать поиск. Третий аргумент – "//​L////​ –1//"​ – длина региона для поиска. "​-1",​ как нетрудно догадаться,​ – поиск "до победы"​. Далее - обратите внимание,​ что мы ищем не всю строку – а только ее часть ("//​KPNC////​Kaspersky////​++//"​ против "//​KPNC////​Kaspersky//"​) . Это позволяет избавиться от ложных срабатываний – Айс любит выдавать ссылки на свои внутренние буфера,​ содержащие шаблон поиска. Вообще-то они всегда расположены выше 0х80000000. Там – где никакой нормальный пароль "не живет",​ но все же будет нагляднее если по неполной подстроке находится именно наша строка.
 +
 +Pattern found at 0023:​00016E40 (00016E40)
 +
 +Так, по крайней мере, одно вхождение уже найдено. Но вдруг в памяти есть еще несколько?​ Проверим это, последовательно отдавая команды "​s"​ вплоть до выдачи сообщения "​Patternnotfound"​ или превышении адреса поиска 0x80000000.
 +
 +:s
 +
 +Pattern found at 0023:​0013FF18 (0013FF18)
 +
 +:s
 +
 +Pattern found at 0023:​0024069C (0024069C)
 +
 +:s
 +
 +Pattern found at 0023:​80B83F18 (80B83F18)
 +
 +Целых два вхождения,​ да еще одно "в уме"​ – итого //​**три**//​! Не много ли для нас, начинающих?​ Во-первых,​ неясно – вводимые пароли они, что плоятся ака кролики?​ Во-вторых,​ ну не ставить же все три точки останова. В данном случае четырех отладочных регистров процессора хватит,​ а как быть, если бы мы нашли десяток вхождений?​ Да и в трех бряках немудрено заблудиться с непривычки!
 +
 +Итак – начинаем работать головой. Вхождений много вероятнее всего потому,​ что при чтении ввода с клавиатуры символы сперва попадают в системные буфера,​ которые и дают ложные срабатывания. Звучит вполне правдоподобно,​ но вот как отфильтровать "​помехи"?​
 +
 +На помощь приходит карта памяти – зная владельца региона,​ которому принадлежит буфер, об этом буфере очень многое можно сказать. Наскоро набив команду "map32 simple"​ мы получим приблизительно следующее.
 +
 +:map32 simple
 +
 +Owner  Obj Name  Obj#  Address ​ Size  Type
 +
 +simple ​ .text  0001  001B:​00011000 ​ 00003F66 ​ CODE  RO
 +
 +simple ​ .rdata ​ 0002  0023:​00015000 ​ 0000081E ​ IDATA RO
 +
 +**simple ​ .data  0003  0023:​00016000 ​ 00001E44 ​ IDATA RW**
 +
 +Ура, держи Тигру за хвост, есть одно отождествление! Буфер на 0x16E40 принадлежит сегменту данных и, видимо,​ это и есть то, что нам нужно. Но не стоит спешить! Все не так просто. Поищем-ка адрес 0x16E40 в самом файле simple.exe (учитывая обратный порядок байт это будет "40 E6 01 00"):
 +
 +> dumpbin /​SECTION:​.data /RAWDATA simple.exe
 +
 +RAW DATA #3
 +
 + ​00016030:​ 45 6E 74 65 72 20 70 61 73 73 77 6F 72 64 3A 00  Enter password:.
 +
 + ​00016040:​ 6D 79 47 4F 4F 44 70 61 73 73 77 6F 72 64 0A 00  myGOODpassword..
 +
 + ​00016050:​ 57 72 6F 6E 67 20 70 61 73 73 77 6F 72 64 0A 00  Wrong password..
 +
 + ​00016060:​ 50 61 73 73 77 6F 72 64 20 4F 4B 0A 00 00 00 00  Password OK.....
 +
 + ​00016070:​ **40 6E 01 00 **00 00 00 00 **40 6E 01 00 **01 01 00 00  @n......@n......
 +
 + ​00016080:​ 00 00 00 00 00 00 00 00 00 10 00 00 00 00 00 00  ................
 +
 +Есть, да? Даже два раза! Посмотрим теперь,​ кто на него ссылается – попробуем найти в дизассемблированном тексте подстроку "​16070"​ – адрес первого двойного слова, указывающего на наш буфер.
 +
 + ​00011032:​ 68 70 60 01 00  push **16070****h**;​ <<<​
 +
 + ​00011037:​ 6A 64  push 64h; Макс. длина пароля (== 100 dec)
 +
 + ​00011039:​ 8D 4D 98  lea ecx,​[ebp-68h]
 +
 +;​Указатель ^^^^^^ на буфер куда записывать пароль
 +
 + ​0001103C:​ 51  push  ecx
 +
 + ​0001103D:​ E8 E2 00 00 00  call  00011124; fgets 
 +
 + ​00011042:​ 83 C4 0C add esp,0Ch; Выталкиваем три аргумента
 +
 +В общем, все ясно, за исключением загадочного указателя на указатель 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
 +
 +001B:​000110B0 ​ MOV  EAX,[EDX]
 +
 +001B:​000110B2 ​ CMP  AL,​[ECX]     
 +
 +001B:​000110B4 ​ JNZ  000110E4
 +
 +001B:​000110B6 ​ OR  AL,AL
 +
 +001B:​000110B8 ​ JZ  000110E0
 +
 +001B:​000110BA ​ CMP  AH,[ECX+01]
 +
 +001B:​000110BD ​ JNZ  000110E4
 +
 +001B:​000110BF ​ OR  AH,AH
 +
 +И вот мы в теле уже хорошо нам знакомой (развивайте зрительную память!) процедуры сравнения. На всякий случай,​ для пущей убежденности,​ выведем значение указателей EDX и ECX, чтобы узнать,​ что с чем сравнивается:​
 +
 +:d edx
 +
 +0023:​0013FF18 4B 50 4E 43 2D 2D 0A 00-70 65 72 73 6B 79 2B 2B  KPNC Kaspersky++
 +
 +:d ecx
 +
 +0023:​00016040 6D 79 47 4F 4F 44 70 61-73 73 77 6F 72 64 0A 00  myGOODpassword..
 +
 +Ну, а остальное мы уже проходили. Выходим из сравнивающей процедуры по PRET, находим условный переход,​ записываем его адрес (ключевую последовательность для поиска),​ правим исполняемый файл и все ОК. 
 +
 +Итак, мы познакомились с одним более или менее универсальным способом взлома защит основанных на сравнении пароля (позже мы увидим,​ что он так же подходит и для защит, основанных на регистрационных номерах). Его основное достоинство – простота. А недостатки… недостатков у него много.
 +
 +– если программист очистит буфера после сравнения,​ поиск веденного пароля ничего не даст. Разве что останутся системные буфера,​ которые так просто не затрешь,​ но отследить перемещения пароля из системных буферов в локальные не так-то просто!
 +
 +– ввиду изобилия служебных буферов,​ очень трудно определить:​ какой из них "​настоящий"​. Программист же может располагать буфер и в сегменте данных (статический буфер),​ и в стеке (локальный буфер),​ и в куче, и даже выделять память низкоуровневыми вызовами типа VirtualAlloc или… да мало ли как разыграется его фантазия. В результате,​ под час приходится "​просеивать"​ все найденные вхождения тупым перебором.
 +
 +В качестве тренировки разберем другой пример – "​crackme 01". Это то же самое, что simple.exe, только с GUI-рым интерфейсом и ключевая процедура выглядит так:
 +
 +
 +
 +void CCrackme_01Dlg::​OnOK() ​
 +
 +{
 +
 +char buff[PASSWORD_SIZE];​
 +
 +m_password.GetWindowText(&​buff[0],​PASSWORD_SIZE);​
 +
 +if (strcmp(&​buff[0],​PASSWORD))
 +
 +{
 +
 +MessageBox("​Wrong password"​);​
 +
 +m_password.SetSel(0,​-1,​0);​
 +
 +return;
 +
 +}
 +
 +else
 +
 +{
 +
 +MessageBox("​Password OK");
 +
 +}
 +
 +CDialog::​OnOK();​
 +
 +}
 +
 +Листинг 5 Исходный текст ядра защитного механизма crackme 01
 +
 +Кажется,​ никаких сюрпризов не предвидится. Что ж, вводим пароль (как обычно "​KPNCKaspersky++"​),​ выслушиваем "​ругательство"​ и, до нажатия ОК, вызываем отладчик,​ переключаем контекст…
 +
 +:s 23:0 L -1 'KPNC Kaspersky'​
 +
 +Pattern found at 0023:​0012F9FC (0012F9FC)
 +
 +:s
 +
 +Pattern found at 0023:​00139C78 (00139C78)
 +
 +Есть два вхождения! И оба лежат в стеке. Подбросим монетку,​ чтобы определить с какого из них начать?​ (Правильный ответ – с первого). Устанавливаем точку останова и терпеливо ждем всплытия отладчика. Всплытие ждать себя не заставляет,​ но показывает какой-то странный,​ откровенно "​левый"​ код. Ждем "​x"​ для выхода,​ - следует целый каскад всплытий одно непонятнее другого.
 +
 +Лихорадочно подергивая бородку (варианты – усики, волосы в разных местах) соображаем:​ функция "​CCrackme_01Dlg::​OnOK"​ вызывается непосредственно в момент нажатия на "​ОК"​ – ей отводится часть стекового пространства под локальные переменные,​ которая автоматически "​экспроприируется"​ при выходе из функции – переходя во всеобщее пользование. Таким образом,​ локальный буфер с введенным нами паролем существует только в момент его проверки,​ а потом автоматически затирается. Единственная зацепка – модальный диалог с ругательством. Пока он на экране – буфер еще содержит пароль и его можно найти в памяти. Но это не сильно помогает в отслеживании когда к этому буферу произведет обращение… Приходится терпеливо ждать, отсеивая ложные всплытия один за другим. Наконец,​ в окне данных искомая строка,​ а в окне кода – какой-то осмысленный код:
 +
 +0023:​0012F9FC 4B 50 4E 43 20 4B 61 73-70 65 72 73 6B 79 2B 2B  KPNC Kaspersky++
 +
 +0023:​0012FA0C 00 01 00 00 0D 00 00 00-01 00 1C C0 A8 AF 47 00  ..............G.
 +
 +0023:​0012FA1C 10 9B 13 00 78 01 01 00-F0 3E 2F 00 00 00 00 00  ....x....>/​.....
 +
 +0023:​0012FA2C 01 01 01 00 83 63 E1 77-F0 AD 47 00 78 01 01 00  .....c.w..G.x...
 +
 +001B:​004013E3 ​ 8A10  MOV  DL,[EAX]
 +
 +001B:​004013E5 ​ 8A1E  MOV  BL,[ESI] 
 +
 +001B:​004013E7 ​ 8ACA  MOV  CL,DL
 +
 +001B:​004013E9 ​ 3AD3  CMP  DL,BL
 +
 +001B:​004013EB ​ 751E  JNZ  0040140B
 +
 +001B:​004013ED ​ 84C9  TEST  CL,CL
 +
 +001B:​004013EF ​ 7416  JZ  00401407
 +
 +001B:​004013F1 ​ 8A5001 ​ MOV  DL,[EAX+01]
 +
 +На всякий "​пожарный"​ смотрим,​ на что указывает ESI: 
 +
 +:d esi
 +
 +0023:​0040303C 4D 79 47 6F 6F 64 50 61-73 73 77 6F 72 64 00 00  MyGoodPassword..
 +
 +Остается "​пропадчить"​ исполняемый файл, и тут (как и следовало ожидать по закону бутерброда) нас ждут очередные трудности. Во-первых,​ хитрый компилятор заоптимизировал код, подставив код функции strcmp вместо ее вызова,​ а во-вторых,​ условных переходов… да ими все кишит! Попробуй-ка,​ найди нужный. На этот раз бросать монетку мы не станем,​ а попытаемся подойти к делу по-научному. Итак, перед нами дизассемблированный код, точнее его ключевой фрагмент,​ осуществляющий анализ пароля:​
 +
 +>dumpbin /DISASM crackme_01.exe
 +
 + ​004013DA:​ BE 3C 30 40 00  mov  esi,40303Ch
 +
 + ​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 на безусловный jumpshort (код 0xEB). ​
 +
 +==== Способ 2. Бряк на функции ввода пароля ====
 +
 +//Вы боитесь творить,​ потому что творения ваши отражают вашу истинную суть.//
 +
 +Фрэнк Херберт "​Ловец душ"​
 +
 +При всем желании метод прямого поиска пароля в памяти элегантным назвать нельзя,​ да и практичным тоже. А, собственно,​ зачем искать сам пароль,​ спотыкаясь об беспорядочно разбросанные буфера,​ когда можно поставить бряк непосредственно на функцию,​ его считывающую?​ Хм, можно и так… да вот угадать какой именно функцией разработчик вздумал читать пароль,​ вряд ли будет намного проще.
 +
 +На самом деле одно и тоже действие может быть выполнено всего лишь несколькими функциями и их перебор не займет много времени. В частности,​ содержимое окна редактирование обычно добывается либо GetWinodowTextA (что чаще всего и происходит),​ либо GetDlgItemTextA (а это – значительно реже).
 +
 +Раз уж речь зашла за окна, запустим наш GUI "​крякмис"​ и установим точку останова на функцию GetWindowTextA ("//​bpx GetWinodwTextA//"​). Поскольку,​ эта функция – системная,​ точка останова будет глобальной,​ т.е. затронет все приложения в системе,​ поэтому,​ заблаговременно закройте все лишнее от греха подальше. Если установить бряк до запуска "​крякмиса",​ то мы словим несколько ложных всплытий,​ возникающих вследствие того, что система сама читает содержимое окна в процессе формирования диалога.
 +
 +Вводим какой-нибудь пароль ("//​KPNC////​Kaspersky////​++//"​ по обыкновению),​ нажимаем <​**ENTER**>​ - отладчик незамедливает всплыть:​
 +
 +USER32!GetWindowTextA                                 
 +
 +001B:​77E1A4E2 ​ 55  PUSH  EBP         
 +
 +001B:​77E1A4E3 ​ 8BEC  MOV  EBP,ESP
 +
 +001B:​77E1A4E5 ​ 6AFF  PUSH  FF
 +
 +001B:​77E1A4E7 ​ 6870A5E177 ​ PUSH  77E1A570
 +
 +001B:​77E1A4EC ​ 68491DE677 ​ PUSH  77E61D49
 +
 +001B:​77E1A4F1 ​ 64A100000000 ​ MOV  EAX,​FS:​[00000000]
 +
 +001B:​77E1A4F7 ​ 50  PUSH  EAX
 +
 +Во многих руководствах по взлому советуется тут же выйти из функции по PRET, мол, что ее анализировать-то,​ но не стоит спешить! Сейчас самое время выяснить:​ где расположен буфер вводимой строки и установить на него бряк. Вспомним какие аргументы и в какой последовательности принимает функция (а, если не вспомним,​ то заглянем в 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
 +
 +
 +
 +{{tekhnika-i-filosofiya-hakerskih-atak-text_Image_2.png}}
 +
 +Рисунок 3 0х02 Состояние стека на момент вызова GetWindowsText
 +
 +Получить содержимое ячейки по заданному адресу в Айсе можно с помощью оператора "​звездочка",​ вызов которого в нашем случае выглядит так (подробнее – см. документацию,​ прилагаемую к отладчику):​
 +
 +:**d *(esp+8)**
 +
 +0023:​0012F9FC 1C FA 12 00 3B 5A E1 77-EC 4D E1 77 06 02 05 00  ....;​Z.w.M.w....
 +
 +0023:​0012FA0C 01 01 00 00 10 00 00 00-01 00 2A C0 10 A8 48 00  ..........*...H.
 +
 +0023:​0012FA1C 10 9B 13 00 0A 02 04 00-E8 3E 2F 00 00 00 00 00  .........>/​.....
 +
 +0023:​0012FA2C 01 02 04 00 83 63 E1 77-08 DE 48 00 0A 02 04 00  .....c.w..H.....
 +
 +В буфере мусор – так и следовало ожидать,​ ведь строка еще не считана. Давайте выйдем из функции по pret и посмотрим что произойдет (только потом уже нельзя будет пользоваться конструкцией d *esp+8, т.к. после выхода из функции аргументы будут вытолкнуты из стека):​
 +
 +: p ret
 +
 +:d 0012F9FC
 +
 +0023:​0012F9FC 4B 50 4E 43 20 4B 61 73-70 65 72 73 6B 79 2B 2B  **KPNC Kaspersky++**
 +
 +0023:​0012FA0C 00 01 00 00 0D 00 00 00-01 00 1C 80 10 A8 48 00  ..............H.
 +
 +0023:​0012FA1C 10 9B 13 00 0A 02 04 00-E8 3E 2F 00 00 00 00 00  .........>/​.....
 +
 +0023:​0012FA2C 01 02 04 00 83 63 E1 77-08 DE 48 00 0A 02 04 00  .....c.w..H.....
 +
 +ОК, это действительно тот буфер, который нам нужен. Ставим бряк на его начало и дожидаемся всплытия. Смотрите,​ с первого же раза мы очутились именно так, где и надо (узнаете код сравнивающей процедуры?​):​
 +
 +001B:​004013E3 ​ 8A10  MOV  DL,[EAX]
 +
 +001B:​004013E5 ​ 8A1E  MOV  BL,[ESI]
 +
 +001B:​004013E7 ​ 8ACA  MOV  CL,DL
 +
 +001B:​004013E9 ​ 3AD3  CMP  DL,BL
 +
 +001B:​004013EB ​ 751E  JNZ  0040140B
 +
 +001B:​004013ED ​ 84C9  TEST  CL,CL
 +
 +001B:​004013EF ​ 7416  JZ  00401407
 +
 +001B:​004013F1 ​ 8A5001 ​ MOV  DL,[EAX+01]
 +
 +Замечательно! Вот так, безо всяких ложных срабатываний,​ элегантно,​ быстро и красиво мы победили защиту!
 +
 +Этот способ – универсален и впоследствии мы еще не раз им воспользуемся. Вся соль – определить ключевую функцию защиты и поставить на нее бряк. Под 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"​ выдает всю информацию об окнах указанного процесса.
 +
 +<​**Ctrl-D**>​
 +
 +:addr crack02
 +
 +:hwnd crack02
 +
 +Handle ​ Class  WinProc ​ TID  Module
 +
 + ​050140 ​ #32770 (Dialog) ​ 6C291B81 ​ 2DC crack02
 +
 + ​05013E ​ Button ​ 77E18721 ​ 2DC crack02
 +
 + ​**05013C ​ Edit  6C291B81 ​ 2DC crack02**
 +
 + ​05013A ​ Static ​ 77E186D9 ​ 2DC crack02
 +
 +Быстро обнаруживает себя окно редактирования,​ с адресом оконной процедуры 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//",​ нажимаем <​**ENTER**>,​ отладчик тут же "​всплывает"​
 +
 +Break due to BPX #​0008:​6C291B81 ​ IF ((ESP->​8)==0xD) (ET=2.52 seconds)
 +
 +Вот, он хвост Тигры и уши плюшевого медведя! Остается определить адрес буфера,​ в который возвращается считанная строка. Начинаем соображать:​ указатель на буфер передается через аргумент lParam (см. в SDK описание WM_GETTEXT),​ а сам lParam размещается в стеке по смещению 0x10, относительно ESP:
 +
 +адрес возврата ​ ESP
 +
 +hwnd ESP + 0x4
 +
 +uMsg   ESP + 0x8
 +
 +wParam ​  ESP + 0xC
 +
 +lParam ESP + 0x10
 +
 +Даем команду вывода этого буфера в окно данных,​ выходим из оконной процедуры по PRET и… видим только что введенный нами текст "//​Hello//"​
 +
 +:d *(esp+10)
 +
 +:p ret
 +
 +0023:​0012EB28 48 65 6C 6C 6F 00 05 00-0D 00 00 00 FF 03 00 00  Hello...........
 +
 +0023:​0012EB38 1C ED 12 00 01 00 00 00-0D 00 00 00 FD 86 E1 77  ...............w
 +
 +0023:​0012EB48 70 3C 13 00 00 00 00 00-00 00 00 00 00 00 00 00  p<​..............
 +
 +0023:​0012EB58 00 00 00 00 00 00 00 00-98 EB 12 00 1E 87 E1 77  ...............w
 +
 +:bpm 23:12EB28
 +
 +Установив точку останова,​ мы ловим одно откровенно "​левое"​ всплытие отладчика (это видно по явно не "​юзерскому"​ значению селектора CS, равного 8) и, уже тянем руку, чтобы нажать '​x'​ продолжив отслеживание нашего бряка, как вдруг краем глаза замечаем…. ​
 +
 +0008:​A00B017C ​ 8A0A  MOV  CL,[EDX]
 +
 +0008:​A00B017E ​ 8808  MOV  [EAX],CL
 +
 +0008:​A00B0180 ​ 40  INC  EAX
 +
 +0008:​A00B0181 ​ 42  INC  EDX
 +
 +0008:​A00B0182 ​ 84C9  TEST  CL,CL
 +
 +0008:​A00B0184 ​ 7406  JZ  A00B018C
 +
 +0008:​A00B0186 ​ FF4C2410 ​ DEC  DWORD PTR [ESP+10]
 +
 +0008:​A00B018A ​ 75F0  JNZ  A00B017C
 +
 +Эге, буфер-то не "​сквозной",​ - система не отдает его "​народу",​ а копирует в другой буфер. Это видно потому,​ как из указателя на "​наш"​ буфер EDX символ копируется в CL (то, что EDX – указатель на "​наш"​ буфер следует из того, что он вызвал всплытие отладчика),​ а из CL он копируется в [EAX], где EAX – какой-то указатель (о котором пока мы еще не можем сказать ничего определенного). Далее – оба указателя увеличиваются на единицу и CL (последний считанный символ) проверяется на равенство нулю – если конец строки не достигнут,​ то все повторяется. Что ж, суждено нам следить сразу за двумя буферами – ставим еще один бряк.
 +
 +:bpm EAX
 +
 +:x
 +
 +На втором бряке отладчик вскорости всплывает,​ и мы узнаем нашу родную процедуру сравнения. Ну, а дальнейшее – дело техники. ​
 +
 +001B:​004013F2 ​ 8A1E  MOV  BL,[ESI]
 +
 +001B:​004013F4 ​ 8ACA  MOV  CL,​DL    
 +
 +001B:​004013F6 ​ 3AD3  CMP  DL,BL
 +
 +001B:​004013F8 ​ 751E  JNZ  00401418
 +
 +001B:​004013FA ​ 84C9  TEST  CL,CL
 +
 +001B:​004013FC ​ 7416  JZ  00401414
 +
 +001B:​004013FE ​ 8A5001 ​ MOV  DL,[EAX+01]
 +
 +001B:​00401401 ​ 8A5E01 ​ MOV  BL,[ESI+01]
 +
 +{{tekhnika-i-filosofiya-hakerskih-atak-text_Image_3.png}}В Windows 9x обработка сообщений реализована несколько иначе, чем в NT. В частности,​ оконная процедура окна редактирования находится в 16-разрядном коде. А это – сегментная модель памяти (треска хвостом вперед под хвост Тигре) ala сегмент : смещение. Представляется любопытным механизм передачи адреса – в какой же параметр засунут сегмент?​ Чтобы ответить на это, взглянем на отчет Айса: ​
 +
 +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////​ 28////​D////​7:​0000//"​
 +
 +===== Шаг пятый. На сцене появляется IDA =====
 +
 +"//​Реальность такова,​ какой ее описывает язык//"​ тезис лингвистической относительности Б.Л. Уорфа
 +
 +С легкой руки Дениса Ричи повелось начинать освоение нового языка программирования с создания простейшей программы “Hello,​ World!”,​ -- и здесь не будет нарушена эта традиция. Оценим возможности IDA Pro следующим примером (для совместимости с книгой рекомендуется откомпилировать его с помощью Microsoft Visual C++ 6.0 вызовом “cl.exe first.cpp” в командной строке):​
 +
 +#include <​iostream.h>​
 +
 +void main()
 +
 +{
 +
 +cout<<"​Hello,​ Sailor!\n";​
 +
 +}
 +
 +a) исходный текст программы first.cpp
 +
 +Компилятор сгенерирует исполняемый файл размером почти в 40 килобайт,​ большую часть которого займет служебный,​ стартовый или библиотечный код! Попытка дизассемблирования с помощью таких дизассемблеров как W32DASM (или аналогичных ему) не увенчается ​ быстрым успехом,​ поскольку над полученным листингом размером **в пятьсот ****килобайт** (!) можно просидеть не час и не два. Легко представить сколько времени уйдет на серьезные задачи,​ требующие изучения десятков мегабайт дизассемблированного текста.
 +
 +Попробуем эту программу дизассемблировать с помощью IDA. Если все настройки ​ оставить по умолчанию,​ после завершения анализа экран (в зависимости от версии) должен выглядеть следующим образом:​
 +
 +Рисунок 4 “0x000.bmp” Так выглядит результат работы консольной версии IDAPro 3.6
 +
 +Рисунок 5 “0x001.bmp” Так выглядит результат работы консольной версии IDAPro 4.0
 +
 +Рисунок 6 “0x002.bmp” Так выглядит результат работы графической версии IDAPro 4.0
 +
 +С версии 3.8x(( ​ А может и чуточку раньше )) в IDA появилась поддержка «**сворачивания**» (//​Collapsed//​) функций. Такой прием значительно упрощает навигацию по тексту,​ позволяя убрать с экрана не интересные в данный момент строки. По умолчанию все библиотечные функции сворачиваются автоматически.
 +
 +Развернуть функцию можно подведя к ней курсор и нажав <+> на дополнительной цифровой клавиатуре,​ расположенной справа. Соответственно,​ клавиша <-> предназначена для сворачивания.
 +
 +По окончании автоматического анализа файла “first.exe”,​ IDA переместит курсор к строке “.text:​00401B2C” – точке входа в программу. Среди начинающих программистов широко распространено заблуждение,​ якобы программы,​ написанные на Си, начинают выполняться с функции “main”, но в действительности это не совсем так. На самом деле сразу после загрузки файла управление передается на функцию “Start”,​ вставленную компилятором. Она подготавливает глобальные переменные _osver (билд), _winmajor (старшая версия операционной системы),​ _winminor (младшая версия операционной системы),​ _winver (полная версия операционной системы),​ __argc (количество аргументов командной строки),​ __argv (массив указателей на строки аргументов),​ _environ (массив указателей на строки переменных окружения);​ инициализирует кучи (heap); вызывает функцию main, а после возращения управления завершает процесс с помощью функции Exit.
 +
 +Наглядно продемонстрировать инициализацию переменных,​ совершаемую стартовым кодом, позволяет следующая программа.
 +
 +#include <​stdio.h>​
 +
 +#include <​stdlib.h>​
 +
 +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”. У исследователей существует множество хитростей,​ но все они базируются на особенностях реализации конкретных компиляторов(( ​ Например,​ MicrosoftVisualC всегда,​ независимо от прототипа функции main передает ей три аргумента – указатель на массив указателей переменных окружения,​ указатель на массив указателей аргументов командной строки и количество аргументов командной строки,​ а все остальные функции стартового кода принимают меньшее количество аргументов )) и не могут считаться универсальными.
 +
 +Рекомендуется изучить исходные тексты стартовых функций популярных компиляторов,​ находящиеся в файлах CRt0.c (Microsoft Visual C) и c0w.asm (Borland C) – это упросит анализ дизассемблерного листинга.
 +
 +Ниже, в качестве иллюстрации,​ приводится содержимое стартового кода программы “first.exe”,​ полученное в результате работы W32Dasm:
 +
 +//​******************** Program Entry Point ********
 +
 +:00401B2C 55  push ebp
 +
 +:00401B2D 8BEC  mov ebp, esp
 +
 +:00401B2F 6AFF  push FFFFFFFF
 +
 +:00401B31 6870714000 ​ push 00407170
 +
 +:00401B36 68A8374000 ​ push 004037A8
 +
 +:00401B3B 64A100000000 ​ mov eax, dword ptr fs:​[00000000]
 +
 +:00401B41 50  push eax
 +
 +:00401B42 64892500000000 ​ mov dword ptr fs:​[00000000],​ esp
 +
 +:00401B49 83EC10 ​ sub esp, 00000010
 +
 +:00401B4C 53  push ebx
 +
 +:00401B4D 56  push esi
 +
 +:00401B4E 57  push edi
 +
 +:00401B4F 8965E8 ​ mov dword ptr [ebp-18], esp
 +
 +Reference To: KERNEL32.GetVersion,​ Ord:0174h
 +
 +|
 +
 +:00401B52 FF1504704000 ​ Call dword ptr [00407004]
 +
 +:00401B58 33D2  xor edx, edx
 +
 +:00401B5A 8AD4  mov dl, ah
 +
 +:00401B5C 8915B0874000 ​ mov dword ptr [004087B0], edx
 +
 +:00401B62 8BC8  mov ecx, eax
 +
 +:00401B64 81E1FF000000 ​ and ecx, 000000FF
 +
 +:00401B6A 890DAC874000 ​ mov dword ptr [004087AC], ecx
 +
 +:00401B70 C1E108 ​ shl ecx, 08
 +
 +:00401B73 03CA  add ecx, edx
 +
 +:00401B75 890DA8874000 ​ mov dword ptr [004087A8], ecx
 +
 +:00401B7B C1E810 ​ shr eax, 10
 +
 +:00401B7E A3A4874000 ​ mov dword ptr [004087A4], eax
 +
 +:00401B83 6A00  push 00000000
 +
 +:00401B85 E8D91B0000 ​ call 00403763
 +
 +:00401B8A 59  pop ecx
 +
 +:00401B8B 85C0  test eax, eax
 +
 +:00401B8D 7508  jne 00401B97
 +
 +:00401B8F 6A1C  push 0000001C
 +
 +:00401B91 E89A000000 ​ call 00401C30
 +
 +:00401B96 59  pop ecx
 +
 +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
 +
 +
 +
 +Reference To: KERNEL32.GetCommandLineA,​ Ord:00CAh
 +
 +|
 +
 +:00401BA0 FF1560704000 ​ Call dword ptr [00407060]
 +
 +:00401BA6 A3E49C4000 ​ mov dword ptr [00409CE4], eax
 +
 +:00401BAB E8811A0000 ​ call 00403631
 +
 +:00401BB0 A388874000 ​ mov dword ptr [00408788], eax
 +
 +:00401BB5 E82A180000 ​ call 004033E4
 +
 +:00401BBA E86C170000 ​ call 0040332B
 +
 +:00401BBF E8E1140000 ​ call 004030A5
 +
 +:00401BC4 A1C0874000 ​ mov eax, dword ptr [004087C0]
 +
 +:00401BC9 A3C4874000 ​ mov dword ptr [004087C4], eax
 +
 +:00401BCE 50  push eax
 +
 +:00401BCF FF35B8874000 ​ push dword ptr [004087B8]
 +
 +:00401BD5 FF35B4874000 ​ push dword ptr [004087B4]
 +
 +:00401BDB E820F4FFFF ​ call 00401000
 +
 +:00401BE0 83C40C ​ add esp, 0000000C
 +
 +:00401BE3 8945E4 ​ mov dword ptr [ebp-1C], eax
 +
 +:00401BE6 50  push eax
 +
 +:00401BE7 E8E6140000 ​ call 004030D2
 +
 +:00401BEC 8B45EC ​ mov eax, dword ptr [ebp-14]
 +
 +:00401BEF 8B08  mov ecx, dword ptr [eax]
 +
 +:00401BF1 8B09  mov ecx, dword ptr [ecx]
 +
 +:00401BF3 894DE0 ​ mov dword ptr [ebp-20], ecx
 +
 +:00401BF6 50  push eax
 +
 +:00401BF7 51  push ecx
 +
 +:00401BF8 E8AA150000 ​ call 004031A7
 +
 +:00401BFD 59  pop ecx
 +
 +:00401BFE 59  pop ecx
 +
 +:00401BFF C3  ret
 +
 +a) стартовый код программы “first.exe”,​ полученный дизассемблером W32Dasm
 +
 +Иначе выглядит результат работы IDA, умеющей распознавать библиотечные функции по их сигнатурам (приблизительно по такому же алгоритму работает множество антивирусов). Поэтому,​ способности дизассемблера тесно связаны с его версией и полнотой комплекта поставки – далеко не все версии IDA Pro в состоянии работать с программами,​ сгенерированными современными компиляторами. (Перечень поддерживаемых компиляторов можно найти в файле “%IDA%/​SIG/​list”).
 +
 +00401B2C start  proc near
 +
 +00401B2C ​
 +
 +00401B2C var_20 ​ = dword ptr -20h
 +
 +00401B2C var_1C ​ = dword ptr -1Ch
 +
 +00401B2C var_18 ​ = dword ptr -18h
 +
 +00401B2C var_14 ​ = dword ptr -14h
 +
 +00401B2C var_4  = dword ptr -4
 +
 +00401B2C ​
 +
 +00401B2C ​ push  ebp
 +
 +00401B2D ​ mov  ebp, esp
 +
 +00401B2F ​ push  0FFFFFFFFh
 +
 +00401B31 ​ push  offset stru_407170
 +
 +00401B36 ​ push  offset __except_handler3
 +
 +00401B3B ​ mov  eax, large fs:0
 +
 +00401B41 ​ push  eax
 +
 +00401B42 ​ mov  large fs:0, esp
 +
 +00401B49 ​ sub  esp, 10h
 +
 +00401B4C ​ push  ebx
 +
 +00401B4D ​ push  esi
 +
 +00401B4E ​ push  edi
 +
 +00401B4F ​ mov  [ebp+var_18],​ esp
 +
 +00401B52 ​ call  ds:​GetVersion  ​
 +
 +00401B58 ​ xor  edx, edx
 +
 +00401B5A ​ mov  dl, ah
 +
 +00401B5C ​ mov  dword_4087B0,​ edx
 +
 +00401B62 ​ mov  ecx, eax
 +
 +00401B64 ​ and  ecx, 0FFh
 +
 +00401B6A ​ mov  dword_4087AC,​ ecx
 +
 +00401B70 ​ shl  ecx, 8
 +
 +00401B73 ​ add  ecx, edx
 +
 +00401B75 ​ mov  dword_4087A8,​ ecx
 +
 +00401B7B ​ shr  eax, 10h
 +
 +00401B7E ​ mov  dword_4087A4,​ eax
 +
 +00401B83 ​ push  0
 +
 +00401B85 ​ call  __heap_init
 +
 +00401B8A ​ pop  ecx
 +
 +00401B8B ​ test  eax, eax
 +
 +00401B8D ​ jnz  short loc_401B97
 +
 +00401B8F ​ push  1Ch
 +
 +00401B91 ​ call  sub_401C30 ​ ; _fast_error_exit
 +
 +00401B96 ​ pop  ecx
 +
 +00401B97 ​
 +
 +00401B97 loc_401B97: ​ ; CODE XREF: start+61j
 +
 +00401B97 ​ and  [ebp+var_4],​ 0
 +
 +00401B9B ​ call  __ioinit
 +
 +00401BA0 ​ call  ds:​GetCommandLineA
 +
 +00401BA6 ​ mov  dword_409CE4,​ eax
 +
 +00401BAB ​ call  ___crtGetEnvironmentStringsA
 +
 +00401BB0 ​ mov  dword_408788,​ eax
 +
 +00401BB5 ​ call  __setargv
 +
 +00401BBA ​ call  __setenvp
 +
 +00401BBF ​ call  __cinit
 +
 +00401BC4 ​ mov  eax, dword_4087C0
 +
 +00401BC9 ​ mov  dword_4087C4,​ eax
 +
 +00401BCE ​ push  eax
 +
 +00401BCF ​ push  dword_4087B8
 +
 +00401BD5 ​ push  dword_4087B4
 +
 +00401BDB ​ call  sub_401000
 +
 +00401BE0 ​ add  esp, 0Ch
 +
 +00401BE3 ​ mov  [ebp+var_1C],​ eax
 +
 +00401BE6 ​ push  eax
 +
 +00401BE7 ​ call  _exit
 +
 +00401BEC ; ------------------------------------------------------
 +
 +00401BEC ​
 +
 +00401BEC loc_401BEC: ​ ; DATA XREF: _rdata:​00407170o
 +
 +00401BEC ​ mov  eax, [ebp-14h]
 +
 +00401BEF ​ mov  ecx, [eax]
 +
 +00401BF1 ​ mov  ecx, [ecx]
 +
 +00401BF3 ​ mov  [ebp-20h], ecx
 +
 +00401BF6 ​ push  eax
 +
 +00401BF7 ​ push  ecx
 +
 +00401BF8 ​ call  __XcptFilter
 +
 +00401BFD ​ pop  ecx
 +
 +00401BFE ​ pop  ecx
 +
 +00401BFF ​ retn  ​
 +
 +00401BFF start  endp ; sp = -34h
 +
 +b) стартовый код программы “first.exe”,​ полученный дизассемблером IDAPro 4.01
 +
 +С приведенным примером IDA Pro успешно справляется,​ о чем свидетельствует стока “UsingFLIRTsignature:​ VCv2.0/​4.x/​5.0 runtime” в окне сообщений ​
 +
 +Рисунок 7 "​0x003"​ Загрузка библиотеки сигнатур
 +
 +Дизассемблер сумел определить имена всех функций вызываемых стартовым кодом, за исключением одной, расположенной по адресу 0х0401BDB. Учитывая передачу трех аргументов и обращение к _exit, после возращения функцией управления,​ можно предположить,​ что это main и есть.
 +
 +Перейти по адресу 0x0401000 для изучения содержимого функции main можно несколькими способами – прокрутить экран с помощью стрелок управления курсором,​ нажать клавишу <​**G**>​ и ввести требуемый адрес в появившемся окне диалога,​ но проще и быстрее всего воспользоваться встроенной в IDA Pro системой навигации. Если подвести курсор в границы имени, константы или выражения и нажать <​**Enter**>,​ IDA автоматически перейдет на требуемый адрес.
 +
 +В данном случае требуется подвести к строке “sub_401000” (аргументу команды call) и нажать на <​**Enter**>,​ если все сделано правильно,​ экран дизассемблера должен выглядеть следующим образом:​
 +
 +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+AFp
 +
 +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”:​и нажать <​Enter>,​ IDA автоматически перейдет к требуемой строке:​
 +
 +00408040 aHelloSailor ​ db '​Hello,​ Sailor!',​0Ah,​0 ; DATA XREF: sub_401000+3o
 +
 +Выражение “DATAXREF:​ sub_401000+3o” называется перекрестной ссылкой и свидетельствует о том, что в третьей строке процедуры sub_401000, произошло обращение к текущему адресу по его смещению (“o” от offset), а стрелка,​ направленная вверх, указывает на относительное расположение источника перекрестной ссылки.
 +
 +Если в границы выражения “sub_401000+3” подвести курсор и нажать <​**Enter**>,​ IDA Pro перейдет к следующей строке:​
 +
 +00401003 ​ push  offset aHelloSailor ; "​Hello,​ Sailor!\n"​
 +
 +Нажатие клавиши <Ecs> отменяет предыдущее перемещение,​ возвращая курсор в исходную позицию. (Аналогично команде “back” в web-браузере). Смещение строки “Hello, Sailor!\n”,​ передается процедуре “??​6ostream@@QAEAAV0@PBD@Z”,​ представляющей собой оператор “<<​” языка С++. Странное имя объясняется ограничениями,​ наложенными на символы,​ допустимые в именах библиотечных функций. Поэтому,​ компиляторы автоматически преобразуют (**замангляют**) такие имена в “абракадабру”,​ пригодную для работы с линкером,​ и многие начинающие программисты даже не догадываются об этой скрытой “кухне”.
 +
 +Для облегчения анализа текста,​ IDA Pro в комментариях отображает «правильные» имена, но существует возможность заставить ее везде показывать незамангленные имена. Для этого необходимо в меню “Options” выбрать пункт “Demanglednames” и в появившемся окне диалога переместить радио кнопку на “Names”,​ после этого вызов оператора “<<​” станет выглядеть так:
 +
 +0040100D ​ call  ostream::​operator<<​(char const *)
 +
 +На этом анализ приложения “first.cpp” можно считать завершенным. Для полноты картины остается переименовать функцию “sub_401000” в main. Для этого необходимо подвести курсор к строке 0x0401000 (началу функции) и нажать клавишу <​**N**>,​ в появившемся диалоге ввести “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+AFp
 +
 +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-ом выглядит так:
 +
 +Cryptprocfar
 +
 +7E5B:​0100start:​
 +
 +7E5B:​0100 ​ 83 C6 06addsi,6
 +
 +7E5B:​0103 ​ FF E6jmpsi;*
 +
 +;*No entry point to code
 +
 +7E5B:​0105 ​ B9 14BEmovcx,​14BEh
 +
 +7E5B:​0108 ​ 01 AD 5691addds:​data_1e[di],​bp;​ (7E5B:​5691=0)
 +
 +7E5B:​010C ​ 80 34 66xorbyte ptr [si],66h; '​f'​
 +
 +7E5B:​010F ​ 46incsi
 +
 +7E5B:​0110 ​ E2 FAloop$-4; Loop if cx > 0
 +
 +7E5B:​0112 ​ FF E6jmpsi;*
 +
 +;* No entry point to code
 +
 +7E5B:114 18 00sbb[bx+si],​al
 +
 +7E5B:116 D2 6F DCshrbyte ptr [bx-24h],​cl;​ Shift w/zeros fill
 +
 +7E5B:119 6E 67 AB 47 A5 2Edb 6Eh, 67h,0ABh, 47h,0A5h, 2Eh
 +
 +7E5B:11F 03 0A 0A 09 4A 35db 03h, 0Ah, 0Ah, 09h, 4Ah, 35h
 +
 +7E5B:125 07 0F 0A 09 14 47db 07h, 0Fh, 0Ah, 09h, 14h, 47h
 +
 +7E5B:12B 6B 6C 42 E8 00 00db 6Bh, 6Ch, 42h, E8h, 00h, 00h
 +
 +7E5B:131 59 5E BF 00 01 57db 59h, 5Eh, BFh, 00h, 01h, 57h
 +
 +7E5B:137 2B CE F3 A4 C3db 2Bh, CEh, F3h, A4h, C3h
 +
 +Cryptendp
 +
 +SOURCER половину кода вообще не смог дизассемблировать,​ оставив ее в виде дампа, а другую половину дизассемблировал неправильно! Команда “JMP SI” в строке :0x103 осуществляет переход по адресу :0x106 (значение регистра SI после загрузки com файла равно 0x100, поэтому после команды “ADD SI,6” регистр SI равен 0x106). Но следующая за “JMP” команда расположена по адресу 0x105! В исходном тексте в это место вставлен байт-пустышка,​ сбивающий дизассемблер с толку.
 +
 +Start:
 +
 +ADDSI,6
 +
 +JMPSI
 +
 +DB0B9h;
 +
 +LEASI,_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 ; Ñ
 +
 +...
 +
 +Необходимо помочь дизассемблеру,​ указав адрес перехода. Начинающие пользователи в этой ситуации обычно подводят курсор к соответствующей строке и нажимают клавишу <​**C**>,​ заставляя IDA дизассемблировать код с текущей позиции до конца функции. Несмотря на кажущуюся очевидность,​ такое решение ошибочно,​ ибо по-прежнему остается неизвестным куда указывает условный переход в строке :0x103 и откуда код, расположенный по адресу :0x106 получает управление.
 +
 +Правильное решение – добавить перекрестную ссылку,​ связывающую строку :0x103, со строкой :0x106. Для этого необходимо в меню “View” выбрать пункт “Crossreferences” и в появившемся окне диалога заполнить поля “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+3u
 +
 +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:​0110j
 +
 +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” и нажать <​**Enter**>​. Важно соблюдать правильность написания – IDA Pro не распознает шестнадцатеричный формат ни в стиле Си (0x106), ни в стиле MASM\TASM (0106h).
 +
 +Что представляет собой число “114h” в строке :0x106 – константу или смещение?​ Чтобы узнать это, необходимо проанализировать следующую команду – “LODSW”,​ поскольку ее выполнение приводит к загрузке в регистр AX слова, расположенного по адресу DS:SI, очевидно,​ в регистр SI заносится смещение. ​
 +
 +seg000:​0106 ​ mov  si, 114h
 +
 +seg000:​0109 ​ lodsw
 +
 +Однократное нажатие клавиши <​**O**>​ преобразует константу в смещение и дизассемблируемый текст станет выглядеть так:
 +
 +seg000:​0106 ​ mov  si, offset unk_0_114
 +
 +seg000:​0109 ​ lodsw
 +
 +
 +
 +seg000:0114 unk_0_114 ​ db  18h ;  ; DATA XREF: seg000:​0106o
 +
 +seg000:​0115 ​ db  0 ;  ​
 +
 +seg000:​0116 ​ db 0D2h ; T
 +
 +seg000:​0117 ​ db  6Fh ; o
 +
 +
 +
 +IDA Pro автоматически создала новое имя “unk_0_114”,​ ссылающееся на переменную неопределенного типа размером в **байт**,​ но команда “LODSW” загружает в регистр AX**слово**,​ поэтому необходимо перейти к строке :0144 и дважды нажать <​**D**>​ пока экран не станет выглядеть так:
 +
 +seg000:0114 word_0_114 ​ dw 18h  ; DATA XREF: seg000:​0106o
 +
 +seg000:​0116 ​ db 0D2h ; T
 +
 +Но что именно содержится в ячейке “word_0_144”?​ Понять это позволит изучение следующего кода:
 +
 +seg000:​0106 ​ mov  si, offset word_0_114
 +
 +seg000:​0109 ​ lodsw
 +
 +seg000:​010A ​ xchg  ax, cx
 +
 +seg000:​010B ​ push  si
 +
 +seg000:​010C ​
 +
 +seg000:010C loc_0_10C: ​ ; CODE XREF: seg000:​0110j
 +
 +seg000:​010C ​ xor  byte ptr [si], 66h
 +
 +seg000:​010F ​ inc  si
 +
 +seg000:​0110 ​ loop  loc_0_10C
 +
 +В строке :0x10A значение регистра AX помещается в регистр CX, и затем он используется командой “LOOP LOC_010C” как счетчик цикла. Тело цикла представляет собой простейший расшифровщик – команда “XOR” расшифровывает один байт, на который указывает регистр SI, а команда “INC SI” перемещает указатель на следующий байт. Следовательно,​ в ячейке “word_0_144” содержится количество байт, которые необходимо расшифровать. Подведя к ней курсор,​ нажатием клавиши <​**N**>​ можно дать ей осмысленное имя, например “BytesToDecrypt”.
 +
 +После завершения цикла расшифровщика встречается еще один безусловный регистровый переход.
 +
 +seg000:​0112 ​ jmp  si
 +
 +Чтобы узнать куда именно он передает управление,​ необходимо проанализировать код и определить содержимое регистра SI. Часто для этой цели прибегают к помощи отладчика – устанавливают точку останова в строке 0x112 и дождавшись его «всплытия» просматривают значения регистров. Специально для этой цели, IDA Pro поддерживает генерацию map-файлов,​ содержащих символьную информацию для отладчика. В частности,​ чтобы не заучивать численные значения всех «подопытных» адресов,​ каждому из них можно присвоить легко запоминаемое символьное имя. Например,​ если подвести курсор к строке “seg000:​0112”,​ нажать <N> и ввести “BreakHere”,​ отладчик сможет автоматически вычислить обратный адрес по его имени.
 +
 +Для создания map-файла в меню “File” необходимо кликнуть по «Produceoutputfile» и в развернувшемся подменю выбрать «ProduceMAPfile» или вместо всего этого нажать на клавиатуре «горячую» комбинацию <​**Shift****-****F****10**>​. Независимо от способа вызова на экран должно появится диалоговое окно следующего вида. Оно позволяет выбрать какого рода данные будут включены в 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-файл необходимо разместить в одной директории с отлаживаемой программой,​ загружаемой в загрузчик **без указания расширения**,​ т.е., например,​ так “WLDRCrypt”. В противном случае символьная информация не будет загружена!
 +
 +Затем необходимо установить точку останова командой “bpxBreakHere” и покинуть отладчик командной “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:​012Ecall$+3
 +
 +seg000:​0131popcx
 +
 +seg000:​0132popsi
 +
 +seg000:​0133movdi,​ 100h
 +
 +seg000:​0136pushdi
 +
 +seg000:​0137subcx,​ si
 +
 +seg000:​0139repemovsb
 +
 +seg000:​013Bretn
 +
 +Назначение команды “CALL $+3” (где $ обозначает текущее значение регистра указателя команд IP) состоит в заталкивании в стек содержимого регистра IP, откуда впоследствии оно может быть извлечено в любой регистр общего назначения. Необходимость подобного трюка объясняется тем, что в микропроцессорах серии Intel 80x86 регистр IP не входит в список непосредственно адресуемых и читать его значение могут лишь команды,​ изменяющие ход выполнения программы,​ в том числе и call.
 +
 +Для облегчения анализа листинга можно добавить к стокам 0x12E и 0x131 комментарий – “MOVCX, IP”, или еще лучше – сразу вычислить и подставить непосредственное значение – “MOVCX,​0x131”.
 +
 +Команда “POPSI” в строке 0x132 снимает слово из стека и помещает его в регистр SI. Прокручивая экран дизассемблера вверх в строке 0x10B можно обнаружить парную ей инструкцию “PUSHSI”,​ заносящую в стек смещение первого расшифровываемого байта. После этого становится понятным смысл последующих команд “MOV DI,​ 0x100\SUB CX,​SI\REPE MOVSB”. Они перемещают начало расшифрованного фрагмента по адресу,​ начинающегося со смещения 0x100. Такая операция характерна для «конвертных» защит, накладывающихся на уже откомпилированный файл, который перед запуском должен быть размещен по своим «родным» адресам.
 +
 +Перед началом перемещения в регистр CX заносится длина копируемого блока, вычисляемая путем вычитания смещения первого расшифрованного байта от смещения второй команды перемещающего кода. В действительности,​ истинная длина на три байта короче и по идее от полученного значения необходимо вычесть три. Однако,​ такое несогласование не нарушает работоспособности,​ поскольку содержимое ячеек памяти,​ лежащих за концом расшифрованного фрагмента,​ не определено и может быть любым.
 +
 +Пара команд “0x136:​PUSHDI” и “0x13B:​RETN” образуют аналог инструкции “CALLDI” – “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:​0116shrbyte ptr [bx-24h], cl
 +
 +seg000:​0119outsb
 +
 +seg000:​011Astosword ptr es:[edi]
 +
 +seg000:​011Cincdi
 +
 +seg000:​011Dmovsw
 +
 +seg000:​011Eaddcx,​ cs:[bp+si]
 +
 +seg000:​0121orcl,​ [bx+di]
 +
 +seg000:​0123decdx
 +
 +seg000:​0124xorax,​ 0F07h
 +
 +seg000:​0127orcl,​ [bx+di]
 +
 +seg000:​0129adcal,​ 47h
 +
 +seg000:​0129;​──────────────────────────────────────────────────────
 +
 +seg000:​012Bdb6Bh ; k
 +
 +seg000:​012Cdb6Ch ; l
 +
 +seg000:​012Ddb42h ; 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 не позволяет разработчику задавать тип переменной и определяет его автоматически по ее первому использованию,​ а объявление осуществляется ключевым словом “auto”. Например,​ “autoMyVar,​ s0” объявляет две переменных – MyVar и s0.
 +
 +Для создания скрипта необходимо нажать комбинацию клавиш <​**Shift****-****F****2**>​ или выбрать в меню “File” пункт “IDCCommand” и в появившемся окне диалога ввести исходный текст программы:​
 +
 +╔═[■]════════════════ Notepad ═════════════════════╗
 +
 +║  EnterIDCstatement(s) ​ ║
 +
 +║ auto a;  ▲  ║
 +
 +║ for (a=0x116;​a<​0x12E;​a++) ​ ▓  ║
 +
 +║ PatchByte([0x1000,​a], ​ ▓  OK  ▄ ║
 +
 +║ Byte([0x1000,​a])^0x66); ​ ▓  ▀▀▀▀▀▀▀▀ ║
 +
 +║  ▓  ║
 +
 +║  ▓  ║
 +
 +║  ▓  Cancel ▄ ║
 +
 +║  ▓  ▀▀▀▀▀▀▀▀ ║
 +
 +║  ▓  ║
 +
 +║  ▓  ║
 +
 +║  ▓  Help  ▄ ║
 +
 +║  ▼  ▀▀▀▀▀▀▀▀ ║
 +
 +║☼═════ 5:1 ═══◄■▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒► ​ ║
 +
 +╚══════════════════════════════════════════════════╝
 +
 +Рисунок9Встроенныйредакторскриптов
 +
 +auto a;
 +
 +for (a=0x116;​a<​0x12E;​a++)
 +
 +PatchByte([0x1000,​a],​Byte([0x1000,​a])^0x66);​
 +
 +a) исходный текст скрипта - расшифровщика
 +
 +//​**Пояснение**//​**:​ **как было показано выше алгоритм расшифровщика сводится к последовательному преобразованию каждой ячейки зашифрованного фрагмента операцией XOR 0x66, (см. ниже – выделено жирным шрифтом)
 +
 +seg000:​010C ​ **xor  byte ptr [si], 66h**
 +
 +seg000:​010F ​ inc  si
 +
 +seg000:​0110 ​ loop  loc_0_10C
 +
 +Сам же зашифрованный фрагмент начинается с адреса seg000:​0x116 и продолжается вплоть до seg000:​0x12E. Отсюда – цикл расшифровки на языке Си выглядит так: //for//// (////​a////​=0////​x////​116;////​a////<​0////​x////​12////​E////;////​a////​++) ////​PatchByte////​([0////​x////​1000,////​a////​],////​Byte////​([0////​x////​1000,////​a////​])^0////​x////​66);//​
 +
 +В зависимости от версии IDA для выполнения скрипта необходимо нажать либо <​**Enter**>​ (версия 3.8x и старше),​ либо <​**Ctrl****-****Enter**>​ в более ранних версиях. Если все сделано правильно,​ после выполнения скрипта экран дизассемблера должен выглядеть так (b).
 +
 +Возможные ошибки – несоблюдение регистра символов (IDA к этому чувствительна),​ синтаксические ошибки,​ базовый адрес вашего сегмента отличается от 0x1000 (еще раз вызовете окно «Сегменты» чтобы узнать его значение). В противном случае необходимо подвести курсор к строке “seg000:​0116”,​ нажать клавишу <​**U**>​ для удаления результатов предыдущего дизассемблирования зашифрованного фрагмента и затем клавишу <​**C**>​ для повторного дизассемблирования расшифрованного кода.
 +
 +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) результатработыскриптарасшифровщика
 +
 +Цепочку символов,​ расположенную начиная с адреса “seg000:​011E” можно преобразовать в удобочитаемый вид, подведя к ней курсор и нажав клавишу “<​**A**>​”. Теперь экран дизассемблера будет выглядеть так:
 +
 +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 **aHelloSailor ​ db '​Hello,​Sailor!',​0Dh,​0Ah,'​$'​**
 +
 +seg000:012E ; ───────────────────────────────────────────────────────────────────────────
 +
 +с) создание ASCII-строки
 +
 +Команда “MOVAH,​9” в строке :0116 подготавливает регистр AH перед вызовом прерывания 0x21, выбирая функцию вывода строки на экран, смещение которой заносится следующей командой в регистр DX. Т.е. для успешного ассемблирования листинга необходимо заменить константу 0x108 соответствующим смещением. Но ведь выводимая строка на этапе ассемблирования (до перемещения кода) расположена совсем в другом месте! Одно из возможных решений этой проблемы заключается в создании нового сегмента с последующим копированием в него расшифрованного кода – в результате чего достигается эмуляции перемещения кода работающей программы.
 +
 +Для создания нового сегмента можно выбрать в меню «View» пункт «Segments» и в раскрывшемся окне нажать клавишу <​Insert>​. Появится диалог следующего вида (см. рис. 10):
 +
 +╔═[■]════════════ Create a new segment ════════════════╗
 +
 +║  ║
 +
 +║  Start address and end address should be valid. ​ ║
 +
 +║  End address > Start address ​ ║
 +
 +║  ║
 +
 +║ Segment name  MySeg  ▐↓▌ ​ ║
 +
 +║ Start address ​ 0x20100 ​ ▐↓▌ C-notation: ​ ║
 +
 +║ End  address ​ 0x20125 ​ ▐↓▌ ​ hex is 0x...  ║
 +
 +║ Base  0x2000 ​ ▐↓▌ in paragraphs ​ ║
 +
 +║ Class  ▐↓▌ (class is any text)║
 +
 +║  ║
 +
 +║  [ ] 32-bit segment ​ ║
 +
 +║  ║
 +
 +║  OK ▄  Cancel ▄  F1 - Help ▄  ║
 +
 +║  ▀▀▀▀ ​ ▀▀▀▀▀▀▀▀ ​ ▀▀▀▀▀▀▀▀▀▀▀ ​ ║
 +
 +╚══════════════════════════════════════════════════════╝
 +
 +Рисунок10 IDAC: Созданиеновогосегмента
 +
 +//​**Пояснение**//:​ //​Базовый адрес сегмента может быть любым если при этом не происходит перекрытия сегментов ////​seg////​000 и ////​MySeg////;​ начальный адрес сегмента задается так, чтобы смещение первого байта было равно 0////​x////​100;​ разница между конечным и начальным адресом равна длине сегмента,​ вычислить которую можно вычитанием смещения начала расшифрованного фрагмента от смещения его конца – 0////​x////​13////​B////​ – 0////​x////​116 = 0////​x////​25.//​
 +
 +Скопировать требуемый фрагмент в только что созданный сегмент можно скриптом следующего содержания.
 +
 +auto a;
 +
 +for (a=0x0;​a<​0x25;​a++) PatchByte([0x2000,​a+0x100],​Byte([0x1000,​a+0x116]));​
 +
 +a) исходный текст скрипта - копировщика
 +
 +Для его ввода необходимо вновь нажать <​**Shift****-****F****2**>,​ при этом предыдущий скрипт будет утерян (IDA позволяет работать не более чем с один скриптом одновременно). После завершения его работы экран дизассемблера будет выглядеть так:
 +
 +MySeg:0100 MySeg  segment byte public ''​ use16
 +
 +MySeg:​0100 ​ assume cs:MySeg
 +
 +MySeg:​0100 ​ ;org 100h
 +
 +MySeg:​0100 ​ assume es:nothing, ss:nothing, ds:nothing, fs:nothing, gs:nothing
 +
 +MySeg:​0100 ​ db 0B4h ; ┤
 +
 +MySeg:​0101 ​ db  9 ;
 +
 +MySeg:​0102 ​ db 0BAh ; ║
 +
 +MySeg:​0103 ​ db  8 ;
 +
 +MySeg:​0104 ​ db  1 ;
 +
 +MySeg:​0105 ​ db 0CDh ; ═
 +
 +MySeg:​0106 ​ db  21h ; !
 +
 +MySeg:​0107 ​ db 0C3h ; ├
 +
 +MySeg:​0108 ​ db  48h ; H
 +
 +MySeg:​0109 ​ db  65h ; e
 +
 +MySeg:​010A ​ db  6Ch ; l
 +
 +MySeg:​010B ​ db  6Ch ; l
 +
 +MySeg:​010C ​ db  6Fh ; o
 +
 +MySeg:​010D ​ db  2Ch ; ,
 +
 +MySeg:​010E ​ db  53h ; S
 +
 +MySeg:​010F ​ db  61h ; a
 +
 +MySeg:​0110 ​ db  69h ; i
 +
 +MySeg:​0111 ​ db  6Ch ; l
 +
 +MySeg:​0112 ​ db  6Fh ; o
 +
 +MySeg:​0113 ​ db  72h ; r
 +
 +MySeg:​0114 ​ db  21h ; !
 +
 +MySeg:​0115 ​ db  0Dh ;
 +
 +MySeg:​0116 ​ db  0Ah ;
 +
 +MySeg:​0117 ​ db  24h ; $
 +
 +MySeg:0117 MySeg ends
 +
 +b) результат работы скрипта-копировщика
 +
 +Теперь необходимо создать перекрестную ссылку “from:​seg000:​013B;​ to:​MySeg:​0x100”,​ преобразовать цепочку символов в удобочитаемую строку,​ подведя курсор к строке MySeg:0108 и нажав клавишу <​**A**>​. Экран дизассемблера должен выглядеть так:
 +
 +MySeg:0100 loc_1000_100: ​ ; CODE XREF: seg000:​013Bu
 +
 +MySeg:​0100 ​ mov  ah, 9
 +
 +MySeg:​0102 ​ mov  dx, **108h**
 +
 +MySeg:​0105 ​ int  21h  ; DOS - PRINT STRING
 +
 +MySeg:​0105 ​ ; DS:DX -> string terminated by "​$"​
 +
 +MySeg:​0107 ​ retn
 +
 +MySeg:0107 ; ───────────────────────────────────────────────────────────────────────────
 +
 +MySeg:​**0108** aHelloSailorS ​ db '​Hello,​Sailor!',​0Dh,​0Ah
 +
 +MySeg:​0108 ​ db '​$'​
 +
 +MySeg:0118 MySeg ends
 +
 +с) результат дизассемблирования скопированного фрагмента
 +
 +Результатом всех этих операций стало совпадение смещения строки со значением,​ загружаемым в регистр DX (в тексте они выделены жирным шрифтом). Если подвести курсор к константе “108h” и нажать клавишу <​Ctrl-O>​ она будет преобразована в смещение:​
 +
 +MySeg:​0102 ​ mov  dx, **offset** aHelloSailorS ; "​Hello,​Sailor!\r\n$ш"​
 +
 +MySeg:​0105 ​ int  21h  ; DOS - PRINT STRING
 +
 +MySeg:​0105 ​ ; DS:DX -> string terminated by "​$"​
 +
 +MySeg:​0107 ​ retn
 +
 +MySeg:0107 ; ───────────────────────────────────────────────────────────────────────────
 +
 +MySeg:0108 aHelloSailorS ​ db '​Hello,​Sailor!',​0Dh,​0Ah ; DATA XREF: MySeg:0102o
 +
 +d) преобразование константы в смещение
 +
 +Полученный листинг удобен для анализа,​ но все еще не готов к ассемблированию,​ хотя бы уже потому,​ что никакой ассемблер не в состоянии зашифровать требуемый код. Конечно,​ эту операцию можно выполнить вручную,​ после компиляции,​ но IDA позволит проделать то же самое не выходя из нее и не прибегая к помощи стороннего инструментария.
 +
 +Демонстрация получится намного нагляднее,​ если в исследуемый файл внести некоторые изменения,​ например,​ добавить ожидание клавиши на выходе. Для этого можно прибегнуть к интегрированному в IDA ассемблеру,​ но прежде,​ разумеется,​ необходимо несколько «раздвинуть» границы сегмента MySeg, дабы было к чему дописывать новый код.
 +
 +Выберете в меню “View” пункт “Segments” и в открывшемся окне подведите курсор к стоке “MySeg”. Нажатие <​**Ctrl****-****E**>​ открывает диалог свойств сегмента,​ содержащий среди прочих полей конечный адрес, который и требуется изменить. Не обязательно указывать точное значение – можно «растянуть» сегмент с небольшим запасом от предполагаемых изменений.
 +
 +Если попытаться добавить к программе код “XORAX,​AX;​ INT 16h” он неминуемо затрет начало строки “Hello, Sailor!”, поэтому,​ ее необходимо заблаговременно передвинуть немного «вниз» (т.е. в область более старших адресов),​ например,​ с помощью скрипта следующего содержания «//​for////​(////​a////​=0////​x////​108;////​a////<​0////​x////​11////​A////;////​a////​++) ////​PatchByte////​([0////​x////​2000,////​a////​+0////​x////​20],////​Byte////​([0////​x////​2000,////​a////​]);//​».
 +
 +**Пояснение**:​ //​объявление переменной ////a//// для краткости опущено (сами должны понимать,​ не маленькие :-), длина строки,​ как водится,​ берется с запасом,​ чтобы не утомлять себя лишними вычислениями и перемещение происходит справа налево,​ поскольку исходный и целевой фрагменты заведомо не пересекаются.//​
 +
 +Подведя к курсор к строке :0128 нажатием <​**A**>​ преобразуем цепочку символов к удобно-читаемому виду; подведем курсор к строке :0102 и, выбрав в меню “Edir” пункт “Pathprogram”,​ “Assembler”,​ введем команду “MOVDX,​128h”,​ где «128h» - новое смещение строки,​ и тут же преобразуем его в смещение нажатием <​**Ctrl****-****O**>​.
 +
 +Вот теперь можно вводить новый текст – переместив курсор на инструкцию “ret”, вновь вызовем ассемблер и введем “//​XOR////​AX////,////​AX////<////​ENTER////>////​INT////​ 16////​h////<////​Enter////>////​RET////<////​Enter////><////​Esc////>//​”. На последок рекомендуется произвести «косметическую» чистку – уменьшить размер сегмента до необходимого и переместить строку “Hello, Sailor” вверх, прижав ее вплотную к коду. ​
 +
 +**Пояснение**:​ //​удалить адреса,​ оставшиеся при уменьшении размеров сегмента за его концом можно взводом флажка “////​Disable////​Address////​” в окне свойств сегмента,​ вызываемом нажатием <////​Alt////​-////​S////>//​
 +
 +Если все было сделано правильно конечный результат должен выглядеть как показано ниже:
 +
 +seg000:0100 ; File Name  : F:​\IDAN\SRC\Crypt.com
 +
 +seg000:0100 ; Format ​ : MS-DOS COM-file
 +
 +seg000:0100 ; Base Address: 1000h Range: 10100h-1013Ch Loaded length: 3Ch
 +
 +seg000:​0100 ​
 +
 +seg000:​0100 ​
 +
 +seg000:0100 ; ===========================================================================
 +
 +seg000:​0100 ​
 +
 +seg000:0100 ; Segment type: Pure code
 +
 +seg000:0100 seg000 ​ segment byte public '​CODE'​ use16
 +
 +seg000:​0100 ​ assume cs:seg000
 +
 +seg000:​0100 ​ org 100h
 +
 +seg000:​0100 ​ assume es:nothing, ss:nothing, ds:seg000, fs:nothing, gs:nothing
 +
 +seg000:​0100 ​
 +
 +seg000:0100 ; --------------- S U B R O U T I N E ---------------------------------------
 +
 +seg000:​0100 ​
 +
 +seg000:​0100 ​
 +
 +seg000:​0100 ​ public start
 +
 +seg000:0100 start  proc near
 +
 +seg000:​0100 ​ add  si, 6
 +
 +seg000:​0103 ​ jmp  si  ; Ïåðåõîä ïî àäðåñó 0106
 +
 +seg000:0103 start  endp
 +
 +seg000:​0103 ​
 +
 +seg000:0103 ; ---------------------------------------------------------------------------
 +
 +seg000:​0105 ​ db 0B9h ; ¦
 +
 +seg000:0106 ; ---------------------------------------------------------------------------
 +
 +seg000:​0106 ​ mov  si, offset BytesToDecrypt
 +
 +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 ​
 +
 +seg000:0112 BreakHere: ​ ; Ïåðåõîä ïî àäðåñó 012E
 +
 +seg000:​0112 ​ jmp  si
 +
 +seg000:0112 ; ---------------------------------------------------------------------------
 +
 +seg000:0114 BytesToDecrypt ​ dw 18h  ; DATA XREF: seg000:​0106o
 +
 +seg000:0116 ; ---------------------------------------------------------------------------
 +
 +seg000:​0116 ​
 +
 +seg000:0116 loc_0_116: ​ ; CODE XREF: seg000:​013Bu
 +
 +seg000:​0116 ​ mov  ah, 9
 +
 +seg000:​0118 ​ mov  dx, 108h  ; "​Hello,​Sailor!\r\n$"​
 +
 +seg000:​011B ​ int  21h  ; DOS - PRINT STRING
 +
 +seg000:​011B ​ ; DS:DX -> string terminated by "​$"​
 +
 +seg000:​011D ​ retn  ​
 +
 +seg000:011D ; ---------------------------------------------------------------------------
 +
 +seg000:011E aHelloSailor ​ db '​Hello,​Sailor!',​0Dh,​0Ah,'​$'​ ; DATA XREF: seg000:​0118o
 +
 +seg000: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  ​
 +
 +seg000:013B seg000 ​ ends
 +
 +seg000:​013B ​
 +
 +MySeg:0100 ; ---------------------------------------------------------------------------
 +
 +MySeg:0100 ; ===========================================================================
 +
 +MySeg:​0100 ​
 +
 +MySeg:0100 ; Segment type: Regular
 +
 +MySeg:0100 MySeg  segment byte public ''​ use16
 +
 +MySeg:​0100 ​ assume cs:MySeg
 +
 +MySeg:​0100 ​ ;org 100h
 +
 +MySeg:​0100 ​ assume es:nothing, ss:nothing, ds:nothing, fs:nothing, gs:nothing
 +
 +MySeg:​0100 ​
 +
 +MySeg:0100 loc_1000_100: ​ ; CODE XREF: seg000:​013Bu
 +
 +MySeg:​0100 ​ mov  ah, 9
 +
 +MySeg:​0102 ​ mov  dx, offset aHelloSailor_0 ; "​Hello,​Sailor!\r\n$"​
 +
 +MySeg:​0105 ​ int  21h  ; DOS - PRINT STRING
 +
 +MySeg:​0105 ​ ; DS:DX -> string terminated by "​$"​
 +
 +MySeg:​0107 ​ xor  ax, ax
 +
 +MySeg:​0109 ​ int  16h  ; KEYBOARD - READ CHAR FROM BUFFER, WAIT IF EMPTY
 +
 +MySeg:​0109 ​ ; Return: AH = scan code, AL = character
 +
 +MySeg:​010B ​ retn  ​
 +
 +MySeg:010B ; ---------------------------------------------------------------------------
 +
 +MySeg:010C aHelloSailor_0 ​ db '​Hello,​Sailor!',​0Dh,​0Ah,'​$'​ ; DATA XREF: MySeg:0102o
 +
 +MySeg:010C MySeg  ends
 +
 +MySeg:​010C ​
 +
 +MySeg:​010C ​
 +
 +MySeg:​010C ​ end start
 +
 +a) окончательно дизассемблированный текст
 +
 +Структурно программа состоит из следующих частей – расшифровщика,​ занимающего адреса seg000:​0x100 – seg000:​0x113,​ переменной размером в слово, содержащей количество расшифровываемых байт, занимающей адреса seg000:​0x114-seg000:​0x116,​ исполняемого кода программы,​ занимающего целиком сегмент MySeg и загрузчика,​ занимающего адреса seg000:​0x12E-seg000:​0x13B. Все эти части должны быть в перечисленном порядке скопированы в целевой файл, причем исполняемый код программы необходимо предварительно зашифровать,​ произведя над каждым его байтом операцию XOR 0x66.
 +
 +Ниже приведен пример скрипта,​ автоматически выполняющего указанные действия. Для его загрузки достаточно нажать <​**F****2**>​ или выбрать в меню “File” пункт “Load file”, “IDC file”.
 +
 +// Компилятор для файла Crypt
 +
 +//
 +
 +static main()
 +
 +{
 +
 +auto a,f;
 +
 +// Открывается файл Crtypt2.com для записи в двоичном режиме
 +
 +f=fopen("​crypt2.com","​wb"​);​
 +
 +// В файл Crypt2 копируется расшифровщик
 +
 +for (a=0x100;​a<​0x114;​a++) fputc(Byte([0x1000,​a]),​f);​
 +
 +// Определяется и копируется в файл слово, содержащее число
 +
 +// байтов для расшифровки
 +
 +fputc( SegEnd([0x2000,​0x100]) - SegStart([0x2000,​0x100]),​f);​
 +
 +fputc(0,f);
 +
 +// Копируется и налету шифруется расшифрованный фрагмент
 +
 +for(a=SegStart([0x2000,​0x100]);​a!=SegEnd([0x2000,​0x100]);​a++)
 +
 +fputc(Byte(a) ^ 0x66,f);
 +
 +// Дописывается загрузчик
 +
 +for(a=0x12E;​a<​0x13C;​a++)
 +
 +fputc(Byte([0x1000,​a]),​f);​
 +
 +// Закрывается файл.
 +
 +fclose(f);
 +
 +}
 +
 +a) исходный код скрипта-компилятора
 +
 +Выполнение скрипта приведет к созданию файла “Crypt2.com”,​ запустив который можно убедиться в его работоспособности – он выводит строку на экран и, дождавшись нажатия любой клавиши,​ завершает свою работу.
 +
 +Огромным преимуществом такого подхода является «сквозная» компиляция файла, т.е. дизассемблированный листинг в действительности не ассемблировался! Вместо этого из виртуальной памяти байт-за-байтом читалось оригинальное содержимое,​ которое за исключением модифицированных строк доподлинно идентично исходному файлу. Напротив,​ повторное ассемблирование практически никогда не позволяет добиться полного сходства с дизассемблируемым файлом.
 +
 +IDA – очень удобный инструмент для модификации файлов,​ исходные тексты которых утеряны или отсутствуют;​ она практически единственный дизассемблер,​ способный анализировать зашифрованные программы,​ не прибегая к сторонним средствам;​ она обладает развитым пользовательским интерфейсом и удобной системой навигации по исследуемому тексту;​ она дает может справится с любой мыслимой и немыслимой задачей…
 +
 +…но эти, и многие другие возможности,​ невозможно реализовать в полной мере, без владения языком скриптов,​ что и подтвердил приведенный выше пример.
 +
 +___Рассказать о языке комментариев. "​Дом который построил Джек"​
 +
 +___Трассированное дизасссемблирование
 +
 +___Большинство защит вскрываются стандартными приемами,​ которые вовсе не требуют понимания "​как это работает"​. Мой тезка (широко известный среди спектрумистов уже едва ли не десяток лет) однажды сказал "​Умение снимать защиту,​ еще не означает умения ее ставить"​. Это типично для кракера,​ которому,​ судя по всему, ничто не мешает ломать и крушить. Хакер же не ставит целью взлом (т.е. способ любой ценой заставить программу работать),​ а интересуется именно МЕХАНИЗМОМ:​ "​как оно работает"​. Взлом для него вторичен. ​
 +
 +===== Шаг шестой. Дизассемблер & отладчик в связке =====
 +
 +"//​Кот с улыбкой - и то редкость,​ но уж улыбка без кота - это я прямо не знаю что такое//"​
 +
 +Льюис Кэрролл. Алиса в стране чудес
 +
 +Существует два способа исследования программ,​ распространяющихся без исходных текстов:​ дизассемблирование (статический анализ) и отладка (динамический анализ). Вообще-то,​ любой отладчик обязательно включает в себя дизассемблер,​ – иначе отлаживать программу пришлось непосредственно в машинных кодах!
 +
 +Однако тот дизассемблер,​ что включен в отладчик,​ обычно слишком примитивен и не может похвастаться богатыми функциональными возможностями. Во всяком случае,​ дизассемблер,​ встроенный в популярнейший отладчик Soft-Ice, недалеко ушел от DUMPBIN, с недостатками которого мы уже имели честь столкнуться. Насколько же понятнее становится код, если его загрузить в IDA!
 +
 +Чем же тогда ценен отладчик?​ Дело в том, что дизассемблер в силу своей статичности имеет ряд ограничений. Во-первых,​ исследователю приходится выполнять программу на "​эмуляторе"​ процессора,​ "​зашитом"​ в их собственной голове,​ следовательно,​ необходимо знать и назначение всех команд процессора,​ и все структуры операционной системы (включая недокументированные),​ и… Во-вторых,​ начать анализ с произвольного места программы не так-то просто – требуется знать содержимое регистров и ячеек памяти на данный момент,​ а как их узнать?​ С регистрами и локальными переменными еще бы куда ни шло – прокрутим экран дизассемблера вверх и посмотрим какие значения им присваиваются,​ но этот фокус не пройдет с глобальными переменными,​ модифицировать которые может кто угодно и когда угодно. Вот бы установить точку останова… но какая же в дизассемблере может быть точка останова?​ В третьих,​ дизассемблирование вынуждает на полную реконструкцию алгоритма каждой функции,​ в то время как отладка позволяет рассматривать ее как "​черный ящик"​ со входом и выходом. Допустим,​ имеется у нас функция,​ которая расшифровывает основной модуль программы. В дизассемблере нам придется сначала разобраться в алгоритме шифрования (что может оказаться совсем не просто),​ затем "​переложить"​ эту функцию на IDA-Си, отладить ее, запустить расшифровщик… В отладчике же можно поручить выполнение этой функции процессору,​ не вникая в то, как она работает,​ и дождавшись ее завершения,​ продолжить анализ расшифрованного модуля программы. Можно перечислять бесконечно,​ но и без того ясно, что отладчик отнюдь не конкурент дизассемблеру,​ а партнер.
 +
 +Опытные хакеры всегда используют эти два инструмента в паре. Алгоритм реконструируется в дизассемблере,​ а все непонятные моменты оперативно уточняются,​ прогоном под отладчиком. При этом возникает естественное желание видеть в отладчике все те символические имена, которые были внесены в дизассемблерный листинг.
 +
 +И IDAPro действительно позволяет это сделать! Выберем в меню "​Fail"​ подменю "​Produceoutputfile",​ а в нем пункт "​ProduceMAPfile"​ (или нажмем "​горячую"​ клавишу <​**Shift****-****F****10**>​). На экране появится окно с запросом имени файла (введем,​ например,​ "//​simple////​.////​map//"​),​ а затем возникнет модальный диалог,​ уточняющий какие именно имена стоит включать в map-файл. Нажмем <​**Enter**>,​ оставив все галочки в состоянии по умолчанию (подробно о назначении каждой из них можно прочитать в моей книге "//​Образ мышления – дизассемблер ////​IDA//"​). Парой секунд спустя на диске образуется "​simple.map"​ файл, содержащий всю необходимую отладочную информацию,​ представленную в map-формате Borland. Отладчик Soft-ice не поддерживает такой формат,​ поэтому,​ перед его использованием файл необходимо конвертировать в sym-формат специально на то предназначенной утилитой idasym, которую можно бесплатно скачать с сайта www.idapro.com или получить у дистрибьютора,​ продавшего вам IDA.
 +
 +Набрав в командной строке "​idasymsimple.map"​ и, с удовлетворением убедившись,​ что файл "​simple.sym"​ действительно создан,​ запустим загрузим исследуемое приложение "​simple.exe"​ в отладчик любым возможным способом. Дождавшись появления Soft-Ice на экране,​ отладим ему команду "​SYM"​ для отображения содержимого таблицы символов. Если все было сделано правильно,​ ответ Soft-Ice должен выглядеть приблизительно так (ниже приведен сокращенный вариант):​
 +
 +:sym
 +
 +CODE(001B)
 +
 + ​001B:​00401000 start
 +
 + ​001B:​00401074 __GetExceptDLLinfo
 +
 + ​001B:​0040107C _Main
 +
 + ​001B:​00401104 _memchr
 +
 + ​001B:​00401124 _memcpy
 +
 + ​001B:​00401148 _memmove
 +
 + ​001B:​00401194 _memset
 +
 + ​001B:​004011C4 _strcmp
 +
 + ​001B:​004011F0 _strlen
 +
 + ​001B:​0040120C _memcmp
 +
 + ​001B:​00401250 _strrchr
 +
 + ​001B:​00403C08 _printf
 +
 +DATA(0023)
 +
 + ​0023:​00407000 aBorlandCCopyri
 +
 + ​0023:​004070D9 aEnterPassword
 +
 + ​0023:​004070E9 aMygoodpassword
 +
 + ​0023:​004070F9 aWrongPassword
 +
 + ​0023:​00407109 aPasswordOk
 +
 + ​0023:​00407210 aNotype
 +
 + ​0023:​00407219 aBccxh1
 +
 +Wow! Это функциклирует! Теперь символьные имена не только отображаются на экране,​ упрощая понимание кода, – на любое из них можно быстро и с комфортом установить точку останова,​ скажем "​bpmaMygoodpassword"​ и отладчик поймет,​ что от него хотят! Нет больше нужны держать в серо-мозговой памяти эти трудно запоминаемые шестнадцатеричные адреса!
 +
 +===== Шаг седьмой. Идентификация ключевых структур языков высокого уровня =====
 +
 +"//​Если твое оружие стоит только малой части энергии,​ затраченной ​ твоим врагом,​ ты имеешь мощный рычаг, который может одолеть непреодолимые с виду трудности//"​
 +
 +Френк Херберт "​Дом глав Дюны"​
 +
 +Исследование алгоритма работы программ,​ написанных на языках высокого уровня,​ традиционно начинается с реконструкции ключевых структур исходного языка – //​**функций**//,​ //​**локальных**//​ и //​**глобальных**////​**переменных**//,​ //​**ветвлений**//,​ //​**циклов**//​ и т.д. Это делает дизассемблерный листинг более наглядным и значительно упрощает его анализ.
 +
 +Современные дизассемблеры достаточно интеллектуальны и львиную долю работы по распознаванию ключевых структур берут на себя. В частности,​ IDAPro успешно справляется с идентификацией стандартных библиотечных функций,​ локальных переменных,​ адресуемых через регистр ESP, case-ветвлений и т.д. Однако порой она ошибается,​ вводя исследователя в заблуждение,​ к тому же ее высокая стоимость не всегда оправдывает применение. Так, например,​ студентам,​ изучающим ассемблер (а лучше средство изучение ассемблера – дизассемблирование чужих программ),​ она едва ли по карману.
 +
 +Разумеется,​ на IDA свет клином не сошелся – существуют же и другие дизассемблеры,​ скажем тот же DUMPBIN, входящий в штатную поставку SDK, - почему бы на худой конец не воспользоваться им? Конечно,​ если под рукой нет ничего лучшего,​ сойдет и DUMPBIN, но в этом случае об интеллектуальности дизассемблера придется забыть и все делать своей головой.
 +
 +Первым делом мы познакомимся с не оптимизирующими компиляторами – анализ их кода относительно прост и вполне доступен для понимания даже новичкам программирования. Затем же, освоившись с дизассемблером,​ перейдем к вещам более сложным – оптимизирующим компиляторам,​ генерирующих очень хитрый,​ запутанный и витиеватый код.
 +
 +==== Идентификация функций ====
 +
 +//"​Для некоторых людей программирование является такой же внутренней потребностью,​ подобно тому, как коровы дают молоко,​ или писатели стремятся писать//​
 +
 +Николай Безруков
 +
 +//​**Функция**//​ (так же называемая процедурой или подпрограммой) – основная структурная единица процедурных и объективно-ориентированных языков,​ поэтому дизассемблирование кода обычно начинается с отождествления функций и идентификации передаваемых им аргументов.
 +
 +Строгого говоря,​ термин "​функция"​ присутствует не во всех языках,​ но даже там, где он присутствует,​ его определение варьируется от языка к языку. Не вдаваясь в детали,​ мы будем понимать под функцией обособленную последовательность команд,​ вызываемую из различных частей программы. Функция может принимать один и более аргументов,​ а может не принимать ни одного;​ может возвращать результат своей работы,​ а может и не возвращать,​ - это уже не суть важно. Ключевое свойство функции – возвращение управления на место ее вызова,​ а ее характерный признак – множественный вызов из различных частей программы (хотя некоторые функции вызываются лишь из одного места).
 +
 +Откуда функция знает: куда следует возвратить управление?​ Очевидно,​ вызывающий код должен предварительно сохранить адрес возврата и вместе с прочими аргументами передать его вызываемой функции. Существует множество способов решения этой проблемы:​ можно, например,​ перед вызовом функции поместить в ее конец безусловный переход на адрес возврата,​ можно сохранить адрес возврата в специальной переменной и после завершения функции выполнить косвенный переход,​ используя эту переменную как операнд инструкции jump,… Не останавливаясь на обсуждении сильных и слабых сторон каждого метода,​ отметим,​ что компиляторы в подавляющем большинстве случаев используют специальные машинные команды CALL и RET соответственно предназначенные для вызова и выхода из функции.
 +
 +Инструкция CALL закидывает адрес следующей за ней инструкции на вершину стека, а RET стягивает и передает на него управление. Тот адрес, на который указывает инструкция CALL, и есть адрес начала функции. А замыкает функцию инструкция RET (но, внимание:​ не всякий RET обозначает конец функции! подробнее об этом см. "//​Идентификация значения,​ возращенного функцией//"​).
 +
 +Таким образом,​ распознать функцию можно двояко:​ по //​**перекрестным ссылкам**//,​ ведущим к машинной инструкции CALL и по ее //​**эпилогу**//,​ завершающемуся инструкцией RET. Перекрестные ссылки и эпилог в совокупности позволяют определить адреса начала и конца функции. Немного забегая вперед (см. "//​Идентификация локальных стековых переменных//"​) заметим,​ что в начале многих функций присутствует характерная последовательность команд,​ называемая //​**эпилогом**//,​ которая так же пригодна для идентификации функций. А теперь расскажем обо всем этом поподробнее.
 +
 +//::​Перекрестные ссылки. //​Просматривая дизассемблерный код, находим все инструкции CALL – содержимое их операнда и будет искомым адресом начала функции. Адрес не виртуальных функций,​ вызываемых по имени, вычисляется еще на стадии компиляции и операнд инструкции CALL в таких случаях представляет собой непосредственное значение. Благодаря этому адрес начала функции выявляется простым синтаксическим анализом:​ ищем контекстным поиском все подстроки "​CALL"​ и запоминаем (записываемым) непосредственные операнды.
 +
 +Рассмотрим следующий пример:​
 +
 +func();
 +
 +main(){
 +
 +int a;
 +
 +func();
 +
 +a=0x666;
 +
 +func();
 +
 +}
 +
 +func(){
 +
 +int a;
 +
 +a++;
 +
 +}
 +
 +Листинг 6 Пример,​ демонстрирующий непосредственный вызов функции
 +
 +Результат его компиляции должен выглядеть приблизительно так:
 +
 +.text:​00401000pushebp
 +
 +.text:​00401001movebp,​ esp
 +
 +.text:​00401003pushecx
 +
 +.text:​00401004call401019
 +
 +.text:​00401004 ; Вот мы выловили инструкцию callc непосредственным операндом,​
 +
 +.text:​00401004 ; представляющим собой адрес начала функции. Точнее - ее смещение
 +
 +.text:​00401004 ; в кодовом сегменте (в данном случае в сегменте "​.text"​)
 +
 +.text:​00401004 ; Теперь можно перейти к строке "​.text:​00401019"​ и, дав функции
 +
 +.text:​00401004 ; собственное имя, заменить операнд инструкции call на конструкцию
 +
 +.text:​00401004 ; "call offset //​Имя////​функции//"​
 +
 +.text:​00401004 ;
 +
 +.text:​00401009movdword ptr [ebp-4], 666h
 +
 +.text:​00401010call401019
 +
 +.text:​00401010 ; А вот еще один вызов функции! Обратившись к строке "​.text:​401019"​
 +
 +.text:​00401010 ; мы увидим,​ что эта совокупность инструкций уже определена как функция
 +
 +.text:​00401010 ; и все, что потребуется сделать,​ – заменить call 401019 на 
 +
 +.text:​00401010 ; "call offset //​Имя////​функции//"​
 +
 +.text:​00401010
 +
 +.text:​00401015mov ​ esp, ebp
 +
 +.text:​00401017pop ​ ebp
 +
 +.text:​00401018retn
 +
 +.text:​00401018 ; Вот нам встретилась инструкция возврата из функции,​ однако,​ не факт
 +
 +.text:​00401018 ; что это действительно конец функции – ведь функция может иметь и
 +
 +.text:​00401018 ; и несколько точек выхода. Однако,​ смотрите:​ следом за ret
 +
 +.text:​00401018 ; расположено начало функции "​моя функция",​ отождествленное по
 +
 +.text:​00401018 ; операнду инструкции call.
 +
 +.text:​00401018 ; Поскольку,​ функции не могут перекрываться,​ выходит,​ что данный ret -
 +
 +.text:​00401018 ; конец функции!
 +
 +.text:​00401018 ;
 +
 +.text:​00401019push ​ ebp
 +
 +.text:​00401019 ; На эту строку ссылаются операнды нескольких инструкций call.
 +
 +.text:​00401019 ; Следовательно,​ это – адрес начала функции.
 +
 +.text:​00401019 ; Каждая функция должна иметь собственное имя – как бы нам ее назвать?​
 +
 +.text:​00401019 ; Назовем ее "​моя функция"​ :-)
 +
 +.text:​00401019 ;
 +
 +.text:​0040101Amovebp,​ esp; <-
 +
 +.text:​0040101Cpushecx;​ <-
 +
 +.text:​0040101Dmoveax,​ [ebp-4]; <-
 +
 +.text:​00401020addeax,​ 1; <- Это – тело "​моей функции"​
 +
 +.text:​00401023mov[ebp-4],​eax;​ <-
 +
 +.text:​00401026movesp,​ ebp; <-
 +
 +.text:​00401028popebp;​ <-
 +
 +.text:​00401029retn
 +
 +.text:​00401029;​ Конец "​моей функции"​
 +
 +Листинг 7
 +
 +Как мы видим, все очень просто. Однако задача заметно усложняется,​ если программист (или компилятор) использует косвенные вызовы функций,​ передавая их адрес в регистре и динамически вычисляя его (адрес,​ не регистр!) на стадии выполнения программы. Именно так, в частности,​ реализована работа с виртуальными функциями (см. "//​Идентификация виртуальных функций//"​),​ однако,​ в любом случае компилятор должен каким-то образом сохранить адрес функции в коде, значит,​ его можно найти и вычислить! Еще проще загрузить исследуемое приложение в отладчик,​ установить на "​подследственную"​ инструкцию CALL точку останова и, дождавшись всплытия отладчика,​ посмотреть по какому адресу она передаст управление.
 +
 +Рассмотрим следующий пример:​
 +
 +func();
 +
 +main(){
 +
 +int (a*)();
 +
 +a=func;
 +
 +a();
 +
 +}
 +
 +Листинг 8 Пример,​ демонстрирующий вызов функции по указателю
 +
 +Результат его компиляции должен в общем случае выглядеть так:
 +
 +.text:​00401000pushebp
 +
 +.text:​00401001movebp,​ esp
 +
 +.text:​00401003pushecx
 +
 +.text:​00401004movdword ptr [ebp-4], 401012
 +
 +.text:​0040100Bcalldword ptr [ebp-4]
 +
 +.text:​0040100B ; Вот инструкция CALL, осуществляющая косвенный вызов функции
 +
 +.text:​0040100B ; по адресу,​ содержащемуся в ячейке [EBP-4].
 +
 +.text:​0040100B ; Как знать – что же там содержится?​ Прокрутим экран дизассемблера
 +
 +.text:​0040100B ; немного вверх, пока не встретим строку "//​mov////​dword////​ptr////​ [////​ebp////​-4],​401012//"​
 +
 +.text:​0040100B ; Ага! Значит,​ управление передается по адресу "​.text:​ 401012",​ -
 +
 +.text:​0040100B ; это и есть адрес начала функции!
 +
 +.text:​0040100B ; Даем функции имя и заменяем "​movdwordptr [ebp-4], 401012"​ на
 +
 +.text:​0040100B ; "​movdwordptr [ebp-4], offset Имя функции"​
 +
 +.text:​0040100B ;
 +
 +.text:​0040100Emovesp,​ ebp
 +
 +.text:​00401010popebp
 +
 +.text:​00401011retn
 +
 +Листинг 9
 +
 +В некоторых,​ достаточно немногочисленных,​ программах встречается и косвенный вызов функции с комплексным вычислением ее адреса. Рассмотрим следующий пример:​
 +
 +func_1();
 +
 +func_2();
 +
 +func_3();
 +
 +main()
 +
 +{
 +
 +int x;
 +
 +int a[3]={(int) func_1,​(int) func_2, (int) func_3};
 +
 +int (*f)();
 +
 +for (x=0;x < 3;x++)
 +
 +{
 +
 +f=(int (*)()) a[x];
 +
 +f();
 +
 +}
 +
 +}
 +
 +Листинг 10 Пример,​ демонстрирующий вызов функции по указателю с комплексным вычислением целевого адреса
 +
 +Результат его дизассемблирования в общем случае должен выглядеть так:
 +
 +.text:​00401000pushebp
 +
 +.text:​00401001movebp,​ esp
 +
 +.text:​00401003subesp,​ 14h
 +
 +.text:​00401006mov[ebp+0xC],​ offset sub_401046
 +
 +.text:​0040100Dmov[ebp+0x8],​ offset sub_401058
 +
 +.text:​00401014mov[ebp+0x4],​ offset sub_40106A
 +
 +.text:​0040101Bmov[ebp+0x14],​ 0
 +
 +.text:​00401022jmpshort loc_40102D
 +
 +.text:​00401024moveax,​ [ebp+0x14]
 +
 +.text:​00401027addeax,​ 1
 +
 +.text:​0040102Amov[ebp+0x14],​ eax
 +
 +.text:​0040102Dcmp[ebp+0x14],​ 3
 +
 +.text:​00401031jgeshort loc_401042
 +
 +.text:​00401033movecx,​ [ebp+0x14]
 +
 +.text:​00401036movedx,​ [ebp+ecx*4+0xC]
 +
 +.text:​0040103Amov[ebp+0x10],​ edx
 +
 +.text:​0040103D**call[ebp+0x10]**
 +
 +.text:​0040103D ; Так-с, косвенный вызов функции. А что у нас в [EBP+0x10]?
 +
 +.text:​0040103D ; Поднимаем глаза на строку вверх – в [EBP+0x10] у нас значение EDX.
 +
 +.text:​0040103D ; А чем равен сам EDX? Прокручиваем еще одну строку вверх – EDX равен
 +
 +.text:​0040103D ; содержимому ячейки [EBP+ECX*4+0xC]. Вот дела! Мало, что нам надо
 +
 +.text:​0040103D ; узнать содержимое этой ячейки,​ так еще предстоит вычислить ее адрес!
 +
 +.text:​0040103D ; Чему равен ECX? Содержимому [EBP+0x14]. А оно чему равно?
 +
 +.text:​0040103D ; "​Сейчас выясним…"​ бормочем мы себе под нос, прокручивая экран
 +
 +.text:​0040103D ; дизассемблера вверх. Ага, нашли, - в строке 0x40102A в него
 +
 +.text:​0040103D ; загружается содержимое EAX! Какая радость! И долго мы по коду так
 +
 +.text:​0040103D ; блуждать будем?
 +
 +.text:​0040103D ; Конечно,​ можно затратив неопределенное количество времени и усилий
 +
 +.text:​0040103D ; реконструировать весь ключевой алгоритм целиком (тем более, что мы
 +
 +.text:​0040103D ; практически подошли к концу анализа),​ но где гарантия,​ что при этом
 +
 +.text:​0040103D ; не будут допущены ошибки?​
 +
 +.text:​0040103D ; Гораздо быстрее и надежнее загрузить исследуемую программу в
 +
 +.text:​0040103D ; отладчик,​ установить бряк на строку "​text:​0040103D"​ и,
 +
 +.text:​0040103D ; дождавшись всплытия отладчика,​ посмотреть:​ что у нас расположено
 +
 +.text:​0040103D ; в ячейке [EBP+0х10]. Отладчик будут всплывать трижды,​ причем каждый
 +
 +.text:​0040103D ; раз показывать новый адрес! Заметим,​ что определить этот факт в
 +
 +.text:​0040103D ; дизассемблере можно только после полной реконструкции алгоритма!
 +
 +.text:​0040103D ; Однако не стоит по поводу мощи отладчика питать излишних иллюзий!
 +
 +.text:​0040103D ; Программа может тысячу раз вызывать одну и ту же функцию,​ а на 
 +
 +.text:​0040103D ; тысяче первый – вызвать совсем другую! Отладчик бессилен это
 +
 +.text:​0040103D ; определить. Ведь вызов такой функции может произойти в
 +
 +.text:​0040103D ; непредсказуемый момент,​например,​ при определенном сочетании времени,​
 +
 +.text:​0040103D ; данных,​ обрабатываемых программой и текущей фазы Луны. Ну не будем же
 +
 +.text:​0040103D ; мы целую вечность гонять программу под отладчиком?​
 +
 +.text:​0040103D ; Дизассемблер – дело другое. Полная реконструкция алгоритма позволит
 +
 +.text:​0040103D ; однозначно и гарантированно отследить все адреса косвенных вызовов.
 +
 +.text:​0040103D ; Вот потому,​ дизассемблер и отладчик должны скакать в одной упряжке!
 +
 +.text:​0040103D ;
 +
 +.text:​00401040jmpshort loc_401024
 +
 +.text:​00401042 ​
 +
 +.text:​00401042movesp,​ ebp
 +
 +.text:​00401044popebp
 +
 +.text:​00401045retn
 +
 +Самый тяжелый случай представляют "​ручные"​ вызовы функции командой JMP с предварительной засылок в стек адреса возврата. Вызов через JMP в общем случае выглядит так: "​PUSHret_addrr/​JMPfunc_addr",​ где "​ret_addrr"​ и "​func_addr"​ – непосредственные или косвенные адреса возврата и начала функции соответственно. (Кстати,​ заметим,​ что команды PUSHи JPMне всегда следует одна за другой,​ и порой бывают разделены другими командами)
 +
 +Возникает резонный вопрос – чем же там плох CALL, и зачем прибегать к JMP? Дело в том, что функция,​ вызванная по CALL, после возврата управления материнской функции всегда передает управление команде,​ следующей за CALL. В ряде случаев (например,​ при структурной обработке исключений) возникает необходимость после возврата из функции продолжать выполнение не со следующей за CALLкомандой,​ а совсем с другой ветки программы. Тогда-то и приходится "​вручную"​ заносить требуемый адрес возврата и вызывать дочернею функцию через JMP.
 +
 +Идентифицировать такие функции (особенно если они не имею пролога – см. "//​Пролог//"​) очень сложно – контекстный поиск ничего не даст, поскольку команд JMP, использующихся для локальных переходов,​ в теле любой программы очень и очень много – попробуй-ка,​ проанализируй их все! Если же этого не сделать – из поля зрения выпадут сразу две функции – вызываемая функция и функция,​ на которую передается управление после возврата. К сожалению,​ быстрых решений этой проблемы не существует – единственная зацепка – вызывающий JMPпрактически всегда выходит за границы функции,​ в теле которой он расположен. Определить же границы функции можно по эпилогу (см. "//​Эпилог//"​).
 +
 +Рассмотрим следующий пример: ​
 +
 +funct();
 +
 +main()
 +
 +{
 +
 +__asm
 +
 +{
 +
 +LEA ESI, return_addr
 +
 +PUSH ESI
 +
 +JMP funct
 +
 +return_addr:​
 +
 +}
 +
 +}
 +
 +Листинг 11 Пример,​ демонстрирующий "​ручной"​ вызов функции инструкцией JPM
 +
 +Результат его компиляции в общем случае должен выглядеть так:
 +
 +.text:​00401000pushebp
 +
 +.text:​00401001movebp,​ esp
 +
 +.text:​00401003pushebx
 +
 +.text:​00401004pushesi
 +
 +.text:​00401005pushedi
 +
 +.text:​00401006leaesi,​ [401012h]
 +
 +.text:​0040100Cpushesi
 +
 +.text:​0040100Djmp401017
 +
 +.text:​0040100D ; Смотрите – казалось бы тривиальный условный переход,​ - что в нем
 +
 +.text:​0040100D ; такого?​ Ан, нет! Это не простой переход,​ - это замаскированный
 +
 +.text:​0040100D ; вызов функции! Откуда это следует?​ А давайте перейдем по адресу
 +
 +.text:​0040100D ; 0x401017 и посмотрим
 +
 +.text:​0040100D;​ .text:​00401017pushebp
 +
 +.text:​0040100D ; .text:​00401018movebp,​ esp
 +
 +.text:​0040100D ; .text:​0040101Apopebp
 +
 +.text:​0040100D ; .text:​0040101Bretn
 +
 +.text:​0040100D ;  ^^^^
 +
 +.text:​0040100D ; Как вы думаете,​ куда этот ret возвращает управление?​ Естественно,​
 +
 +.text:​0040100D ; по адресу,​ лежащему на верхушке стека. А что у нас лежит на стеке?
 +
 +.text:​0040100D ; PUSHEBP из строки 401017 обратно выталкивается инструкцией POP
 +
 +.text:​0040100D ; из строки 40101B, так… возвращаемся назад, к месту безусловного
 +
 +.text:​0040100D ; перехода и начинаем медленно прокручивать экран дизассемблера вверх
 +
 +.text:​0040100D ; отслеживая все обращения к стеку. Ага, попалась птичка! Инструкция
 +
 +.text:​0040100D ; PUSHESI из строки 401000C закидывает на вершину стека содержимое
 +
 +.text:​0040100D ; регистра ESI, а он сам, в свою очередь,​ строкой выше принимает
 +
 +.text:​0040100D ; "на грудь"​ значение 0x401012 – это и есть адрес начала функции,​
 +
 +.text:​0040100D ; вызываемой командой "​JMP"​ (вернее,​ не адрес, а смещение,​ но это не
 +
 +.text:​0040100D ; принципиально важно).
 +
 +.text:​0040100D ;
 +
 +.text:​00401012popedi
 +
 +.text:​00401013popesi
 +
 +.text:​00401014popebx
 +
 +.text:​00401015popebp
 +
 +.text:​00401016retn
 +
 +Листинг 12
 +
 +//​Автоматическая идентификация функций посредством ////​IDA////​Pro//​. Дизассемблер IDAPro способен анализировать операнды инструкций CALL, что позволяет ему автоматически разбивать программу на функции. Причем,​ IDA вполне успешно справляется с большинством косвенных вызовов! С комплексными вызовами и "​ручными"​ вызовами функций командой JMP она, правда,​ совладеть пока не в состоянии,​ но это не повод для огорчения – ведь подобные конструкции крайне редки и составляют менее процента от "​нормальных"​ вызов функций,​ тех, которые IDA без труда распознает!
 +
 +//::​Пролог//​. Большинство не оптимизирующих компиляторов помешают в начало функции следующий код, называемый //​**прологом**//​.
 +
 +pushebp
 +
 +movebp, esp
 +
 +subesp, xx
 +
 +Листинг 13 Обобщенный код пролога функции
 +
 +В общих чертах назначение пролога сводиться к следующему:​ если регистр EBP используется для адресации локальных переменных (как часто и бывает),​ то перед его использованием он должен быть сохранен в стеке (иначе вызываемая функция "​сорвет крышу"​ материнской),​ затем в EBP копируется текущее значение регистра указателя вершины стека (ESP) – происходит,​ так называемое,​ //​открытие кадра стека//,​ и значение ESP уменьшается на размер области памяти,​ выделенной под локальные переменные.
 +
 +Последовательность PUSHEBP/​MOVEBP,​ESP/​SUBESP,​xx может служить хорошей сигнатурой для нахождения всех функций в исследуемом файле, включая и тех, на которые нет прямых ссылок. Такой прием, в частности,​ использует в своей работе IDAPro, однако,​ оптимизирующие компиляторы умеют адресовать локальные переменные через регистр ESPи используют EBP как и любой другой регистр общего назначения. Пролог оптимизированных функций состоит из одной лишь команды SUBESP, xxx– последовательность слишком короткая для использования ее в качестве сигнатуры функции,​ - увы. Более подробный рассказ об эпилогах функций нас ждет впереди (см. "//​Идентификация локальных стековых переменных//"​),​ поэтому,​ во избежание никому не нужного дублирования,​ не будем здесь на нем останавливаться.
 +
 +//::​Эпилог//​. В конце своей жизни функция закрывает кадр стека, перемещая указатель вершины стека "​вниз",​ и восстанавливает прежнее значение EBP (если только оптимизирующий компилятор не адресовал локальные переменные через ESP, используя EBP как обычный регистр общего назначения). Эпилог функции может выглядеть двояко:​ либо ESPувеличивается на нужное значение командой ADD, либо в него копируется значение EBP, указывающие на низ кадра стека:
 +
 +pop  ebpmov ​ esp, ebp
 +
 +add  esp, 64hpop ​ ebp
 +
 +retnretn
 +
 +Эпилог 1Эпилог 2
 +
 +Листинг 14 Обобщенный код эпилога функции
 +
 +Важно отметить:​ между командами POPEBP/​ADDESP,​ xxx и MOVESP,​EBP/​POPEBP могут находиться и другие команды – они не обязательно должны следовать вплотную друг к другу. Поэтому,​ для поиска эпилогов контекстный поиск непригоден – требуется применять поиск //по маске//​.
 +
 +Если функция написана с учетом соглашение PASCAL, то ей приходится самостоятельно очищать стек от аргументов. В подавляющем большинстве случаев это осуществляется инструкцией RETn, где n – количество байт, снимаемых из стека после возврата. Функции же, соблюдающие Си-соглашение,​ предоставляют очистку стека вызывающему их коду и всегда оканчиваются командой RET. API-функции Windows представляют собой комбинацию соглашений Си и PASCAL – аргументы заносятся в стек справа налево,​ но очищает стек сама функция (подробнее обо всем этом см. "//​Идентификация аргументов функций//"​).
 +
 +Таким образом,​ RET может служить достаточным признаком эпилога функции,​ но не всякий эпилог – это конец. Если функция имеет в своем теле несколько операторов return (как часто и бывает) компилятор в общем случае генерирует для каждого из них свой собственный эпилог. Посмотрите – находится ли за концом эпилога новый пролог или продолжается код старой функции?​ Не забывайте и о том, что компиляторы обычно не помещают в исполняемый файл код, никогда не получающий управления. Т.е. у функции будет всего один эпилог,​ а все, находящееся после первого return, будет выброшено как ненужное:​
 +
 +int func(int a)push ​ ebp
 +
 +{mov  ebp, esp
 +
 +mov  eax, [ebp+arg_0]
 +
 +return a++;​mov ​ ecx, [ebp+arg_0]
 +
 +a=1/​a;​add ​ ecx, 1
 +
 +return a;mov  [ebp+arg_0],​ ecx
 +
 +pop ebp
 +
 +}**retn**
 +
 +Листинг 15 Пример,​ демонстрирующий выбрасывание компилятором кода, расположенного за безусловным оператором return
 +
 +Напротив,​ если внеплановый выход из функции происходит при срабатывании некоторого условия,​ – такой return будет сохранен компилятором и "​окаймлен"​ условным переходом,​ прыгающим через эпилог.
 +
 +int func(int a)
 +
 +{
 +
 +if (!a) return a++;
 +
 +return 1/a;
 +
 +}
 +
 +Листинг 16 Пример,​ демонстрирующий функцию с несколькими эпилогами
 +
 + ​push ​ ebp
 +
 + ​mov ​ ebp, esp
 +
 + ​cmp ​ [ebp+arg_0],​ 0
 +
 + ​**jnz ​ short loc_0_401017**
 +
 + ​mov ​ eax, [ebp+arg_0]
 +
 + ​mov ​ ecx, [ebp+arg_0]
 +
 + ​add ​ ecx, 1
 +
 + ​mov ​ [ebp+arg_0],​ ecx
 +
 + pop ebp
 +
 + ​**retn**
 +
 +; Да, это ^^^^^^^^^^^^^^ -- явно эпилог функции,​ но,
 +
 +; смотрите:​ следом идет //​продолжение кода функции//,​ а
 +
 +; вовсе не новый пролог!
 +
 +** ****loc_0_401017:​** ; CODE XREF: sub_0_401000+7↑j
 +
 +; Данная перекрестная ссылка,​ приводящая нас к условному переходу,​
 +
 +; говорит о том, что этот код – продолжение прежней функции,​ а отнюдь не
 +
 +; начало новой, ибо "​нормальные"​ функции вызываются не jump, а CALL!
 +
 +; А если это "​ненормальная"​ функция?​ Что ж, это легко проверить – достаточно
 +
 +; выяснить:​ лежит ли адрес возврата на вершине стека или нет? Смотрим –
 +
 +; нет, не лежит, следовательно,​ наше предположение относительно продолжения
 +
 +; кода функции верно.
 +
 + ​mov ​ eax, 1
 +
 + cdq
 +
 + ​idiv ​ [ebp+arg_0]
 +
 + ​loc_0_401020: ​ ; CODE XREF: sub_0_401000+15↑j
 +
 + ​pop ​ ebp
 +
 + retn
 +
 +Листинг17
 +
 +//​**Специальное замечание:​ **//​начиная с 80286-процессора,​ в наборе команд появились две инструкции ENTERи LEAVE, предназначенные специально для открытия и закрытия кадра стека. Однако они практически никогда не используются современными компиляторами. Почему?​ Причина в том, что ENTERи LEAVEочень медлительны,​ намного медлительнее PUSHEBP/​MOVEBP,​ESP/​SUB ESB, xxx и MOVESP,​EBP/​POPEBP. Так, на PentiumENTERвыполняется за десять тактов,​ а приведенная последовательность команд – за семь. Аналогично,​ LEAVEтребует пять тактов,​ хотя туже операцию можно выполнить за два (и даже быстрее,​ если разделить MOVESP,​EBP/​POPEBPкакой-нибудь командой). Поэтому,​ современный читатель никогда не столкнется ни с ENTER, ни с LEAVE. Хотя, помнить об их назначении будет нелишне (мало ли, вдруг придется дизассемблировать древние программы,​ или программы,​ написанные на ассемблере,​ – не секрет,​ что многие пишущие на ассемблере очень плохо знают тонкости работы процессора и их "​ручная оптимизация"​ заметно уступает компилятору по производительности).
 +
 +//​**"​Голые"​ (**////​**naked**////​**) функции.**//​ Компилятор MicrosoftVisualC++ поддерживает нестандартный квалификатор "//​naked//",​ позволяющий программистам создавать функции без пролога и эпилога. Без пролога и эпилога //​**вообще**//​! Компилятор даже не помещает в конце функции RET и это придется делать вручную,​ прибегая к ассемблерной вставке "​__asm{ret}"​ (Использование return не приводит к желаемому результату).
 +
 +Вообще-то,​ поддержка naked-функций задумывалась исключительно для написания драйверов на чистом Си (ну, почти чистом,​ с небольшой примесью ассемблерных включений),​ но она нашла неожиданное признание и среди разработчиков защитных механизмов. Действительно,​ приятно иметь возможность "​ручного"​ создания функций,​ не беспокоясь,​ что их непредсказуемым образом "​изуродует"​ компилятор.
 +
 +Для нас же, кодокопателей,​ в первом приближении это обозначает,​ что в программе может встретиться одна (или несколько) функций,​ не содержащих ни пролога,​ ни эпилога. Ну и что в этом страшного?​ Оптимизирующие компиляторы так же выкидывают пролог,​ а от эпилога оставляют один лишь RET, - но функции элементарно идентифицируются по вызывающей их инструкции CALL.
 +
 +//​**Идентификация встраиваемых (**////​**inline**////​**) функций**//​. Самый эффективный способ избавится от накладных расходов на вызов функций – не вызывать их. В самом деле – почему бы ни встроить код функции непосредственно в саму вызывающую функцию?​ Конечно,​ это ощутимо увеличит размер (и тем ощутимее,​ чем из больших мест функция вызывается),​ но зато значительно увеличит скорость выполнения программы (и тем значительнее,​ чем чаще "​развернутая"​ функция вызывается).
 +
 +Чем плоха "​развертка"​ функций для исследования программы?​ Прежде всего – она увеличивает размер "​материнской"​ функции и делает ее код менее наглядным,​ - вместо "​CALL\TESTEAX,​EAX\JZxxx"​ с бросающимся в глаза условным переходом,​ – теперь куча ничего не напоминающих инструкций,​ в логике работы которых еще предстоит разобраться!
 +
 +Вспомним:​ мы уже сталкивались с таким приемом при анализе crackme02:
 +
 +movebp, ds:​SendMessageA
 +
 +pushesi
 +
 +pushedi
 +
 +movedi, ecx
 +
 +pusheax
 +
 +push666h
 +
 +movecx, [edi+80h]
 +
 +push0Dh
 +
 +pushecx
 +
 +callebp ; SendMessageA
 +
 +leaesi, [esp+678h+var_668]
 +
 +moveax, offset aMygoodpassword ; "​MyGoodPassword"​
 +
 +loc_0_4013F0:;​ CODE XREF: sub_0_4013C0+52j
 +
 +**movdl, [eax]**
 +
 +**movbl, [esi]**
 +
 +**movcl, dl**
 +
 +**cmpdl, bl**
 +
 +**jnzshort loc_0_401418**
 +
 +**testcl, cl**
 +
 +**jzshort loc_0_401414**
 +
 +**movdl, [eax+1]**
 +
 +**movbl, [esi+1]**
 +
 +**movcl, dl**
 +
 +**cmpdl, bl**
 +
 +**jnzshort loc_0_401418**
 +
 +**addeax, 2**
 +
 +**addesi, 2**
 +
 +**testcl, cl**
 +
 +**jnzshort loc_0_4013F0**
 +
 +**loc_0_401414:;​ CODE XREF: sub_0_4013C0+3Cj**
 +
 +**xoreax, eax**
 +
 +**jmpshort loc_0_40141D**
 +
 +**loc_0_401418:;​ CODE XREF: sub_0_4013C0+38j**
 +
 +**sbbeax, eax**
 +
 +**sbbeax, 0FFFFFFFFh**
 +
 +loc_0_40141D:;​ CODE XREF: sub_0_4013C0+56j
 +
 +testeax, eax
 +
 +push0
 +
 +push0
 +
 +jzshort loc_0_401460
 +
 +Листинг 18
 +
 +Встроенные функции не имеют ни собственного пролога,​ ни эпилога,​ их код и локальные переменные (если таковые имеются) полностью "​вживлены"​ в вызывающую функцию,​ – результат компиляции выглядит в точности так, как будто бы никакого вызова функции и не было. Единственная зацепка – встраивание функции неизбежно приводит к дублированию ее кода во всех местах вызова,​ а это хоть с трудом,​ но можно обнаружить. "С трудом"​ – потому,​ что встраиваемая функция,​ становясь частью вызывающей функции,​ "в сквозную"​ оптимизируется в контексте последней,​ что приводит к значительным вариациям кода. Рассмотрим такой пример:​
 +
 +#include <​stdio.h>​
 +
 +__inline int max( int a, int b )
 +
 +{
 +
 +if( a > b ) return a;
 +
 +return b;
 +
 +}
 +
 +int main(int argc, char **argv)
 +
 +{
 +
 +printf("​%x\n",​max(0x666,​0x777));​
 +
 +printf("​%x\n",​max(0x666,​argc));​
 +
 +printf("​%x\n",​max(0x666,​argc));​
 +
 +return 0;
 +
 +}
 +
 +Листинг 19 Пример,​ демонстрирующий,​ сквозную оптимизацию встраиваемых функций
 +
 +Результат его компиляции в общем случае должен выглядеть так:
 +
 +pushesi
 +
 +pushedi
 +
 +**push777h;​**код 1-говызова max 
 +
 +; Компилятор вычислил значение функции max еще на этапе компиляции и
 +
 +; вставил его в программу,​ избавившись от лишнего вызова функции
 +
 +pushoffset aProc; "​%x\n"​
 +
 +callprintf
 +
 +movesi, [esp+8+arg_0]
 +
 +addesp, 8
 +
 +**cmpesi, 666h; ******код 2-говызова max
 +
 +**movedi, 666h**; ****код 2-говызова max
 +
 +**jlshort loc_0_401027;​ ******код 2-говызова max
 +
 +**movedi, esi**; код 2-говызова max
 +
 +loc_0_401027:;​ CODE XREF: sub_0_401000+23j
 +
 +pushedi
 +
 +pushoffset aProc; "​%x\n"​
 +
 +callprintf
 +
 +addesp, 8
 +
 +**cmpesi, 666h; ******код 3-говызова max
 +
 +**jgeshort loc_0_401042;​ ******код 2-говызова max
 +
 +**movesi, 666h; ******код 2-говызова max
 +
 +; Смотрите – как изменился код функции! Во-первых,​ нарушилась очередность
 +
 +; выполнения инструкций – было "CMP -> MOV – Jx", а стало "CMP -> Jx, MOV"
 +
 +; А во-вторых,​ условный переход JL загадочным образом превратился в JGE!
 +
 +; Впрочем,​ ничего загадочного тут нет – просто идет сквозная оптимизация!
 +
 +; Поскольку,​ после третьего вызова функции max переменная argc, размещенная
 +
 +; компилятором в регистре ESI, более не используется,​ у компилятора появляется
 +
 +; возможность непосредственно модифицировать этот регистр,​ а не вводить
 +
 +; временную переменную,​ выделяя под нее регистр EDI
 +
 +; (см. "​Идентификация регистровых и временных переменных"​)
 +
 +loc_0_401042:;​ CODE XREF: sub_0_401000+3Bj
 +
 +pushesi
 +
 +pushoffset aProc; "​%x\n"​
 +
 +callprintf
 +
 +addesp, 8
 +
 +moveax, edi
 +
 +popedi
 +
 +popesi
 +
 +retn
 +
 +Листинг 20
 +
 +Смотрите,​ - при первом вызове компилятор вообще выкинул весь код функции,​ вычислив результат ее работы еще на стадии компиляции (действительно,​ 0x777 всегда больше 0x666 и не за чем тратить процессорные такты на их сравнение). А второй вызов очень мало похож на третий,​ несмотря на то, что в обоих случаях функции передавались один и те же аргументы! Тут не то, что поиск по маске (не говоря уже о контекстном поиске),​ человек не разберется – одна и та же функция вызывается или нет!
 +
 +{{tekhnika-i-filosofiya-hakerskih-atak-text_Image_4.png}}//​**Модели памяти и 16-разрядные компиляторы.**//​ Под "​адресом"​ функции в данной главе до настоящего момента подразумевалось исключительно ее //​смещение//​ в кодовом сегменте. Плоская (//flat//) модель памяти 32-разрядной Windows 9х\NT "​упаковывает"​ все три сегмента – сегмент кода, сегмент стека и сегмент данных – в единое четырех гигабайтное адресное пространство,​ позволяя вообще забыть о существовании сегментов.
 +
 +Иное дело – 16-разрядные приложения для MS-DOS и Windows 3.x. В них максимально допустимый размер сегментов составляет всего лишь 64 килобайта,​ чего явно недостаточно для большинства приложений. В крошечной (//tiny//) модели памяти сегменты кода, стека и данных так же расположены в одном адресном пространстве,​ но в отличие от плоской модели это адресное пространство чрезвычайно ограничено в размерах,​ и мало-мальски серьезное приложение приходится рассовывать по нескольким сегментам.
 +
 +Теперь для вызова функции уже не достаточно знать ее смещение,​ – требуется указать еще и сегмент,​ в котором она расположена. Однако сегодня об этом рудименте старины можно со спокойной совестью забыть. На фоне грядущей 64-разрядной версии Windows, подробно описывать 16-разрядный код просто смешно!
 +
 +//​**___Порядок трансляции функций:​**//​ Большинство компиляторов располагают функции в исполняемом файле в том же самом порядке,​ в котором они были объявлены в программе.
 +
 +
 +
 +==== Идентификация стартовых функций ====
 +
 +//​…чтобы не наделать ошибок в  работе,​ богу понадобился свет. Судя по этому, в предшествовавшие века он сидел в полной ​ темноте. К счастью,​ он не рисковал обо что-либо стукнуться,​ ибо вокруг ничего не было.//
 +
 +Лео Таксиль "​Забавная Библия"​
 +
 +Если первого встречного программиста спросить "//С какой функции начинается выполнение ////​Windows////​-программы?//",​ вероятнее всего мы услышим в ответ "С //​WinMain//"​ и это будет ошибкой. На самом же деле, первым управление получает //​стартовый код//, скрыто вставляемый компилятором,​ – выполнив необходимые инициализационные процедуры,​ в какой-то момент он вызывает WinMain, а после ее завершения вновь получает управление и выполняет "​капитальную"​ деинициализацию.
 +
 +В подавляющем большинстве случаев стартовый код не представляет никакого интереса и первой задачей анализирующего становится поиск функции WinMain. Если компилятор входит в число "​знакомых"​ IDA, она опознает WinMain автоматически,​ в противном же случае это приходится делать руками и головой. Обычно в штатную поставку компилятора входят исходные тексты его библиотек,​ в том числе и процедуры стартового кода. Например,​ у MicrosoftVisualC++ стартовый код расположен в файлах "​CRT\STC\CRT0.C"​ – версия для статичной компоновки,​ "​CRT\SRC\CRTEXE.C"​ – версия для динамичной компоновки (т.е. библиотечный код не пристыкуется к файлу, а вызывается из DLL), "​CRT\SRC\wincmdln.c"​ – версия для консольных приложений. У BorlandC++ все файлы со start-up кодом хранятся в отдельной одноименной директории,​ в частности,​ стартовый код для Windows-приложений содержится в файле "​c0w.asm"​. Разобравшись с исходными текстами,​ понять дизассемблерный листинг будет намного легче!
 +
 +А как быть, если для компиляции исследуемой программы использовался неизвестный или недоступный вам компилятор?​ Прежде,​ чем приступать к утомительному ручному анализу,​ давайте вспомним:​ какой прототип имеет функция WinMain:
 +
 +**int WINAPI WinMain(**
 +
 +** ****HINSTANCE **hInstance, ​ // handle to current instance
 +
 +** ****HINSTANCE** hPrevInstance, ​ // handle to previous instance
 +
 +** ****LPSTR** lpCmdLine, ​ // pointer to command line
 +
 +** ****int** nCmdShow ​ // show state of window
 +
 +);
 +
 +Во-первых,​ четыре аргумента (см. "//​Идентификация аргументов функций//"​) – это достаточно много и в большинстве случаев WinMain оказывается самой "​богатой"​ на аргументы функцией стартового кода. Во-вторых,​ последний заносимый в стек аргумент – hInstance – чаще всего вычисляется "на лету"​ вызовом GetModuleHandleA,​ - т.е. встретив конструкцию типа "​CALLGetModuleHandleA"​ можно с высокой степенью уверенности утверждать,​ что следующая функция – и есть WinMain. Наконец,​ вызов WinMain обычно расположен практически в самом конце кода стартовой функции. За ней бывает не более двух-трех "​замыкающих"​ строй функций таких как "​exit"​ и "​XcptFilter"​.
 +
 +Рассмотрим следующий фрагмент кода. Сразу бросается в глаза множество инструкций PUSH, заталкивающих в стек аргументы,​ последний из которых передает результат завершения GetModuleHandleA. Значит,​ перед нами ни что иное, как вызов WinMain (и IDA подтверждает,​ что это именно так):
 +
 +.text:​00401804pusheax
 +
 +.text:​00401805pushesi
 +
 +.text:​00401806pushebx
 +
 +.text:​00401807pushebx
 +
 +.text:​00401808**callds:​GetModuleHandleA**
 +
 +.text:​0040180Epusheax
 +
 +.text:​0040180Fcall_WinMain@16
 +
 +.text:​00401814mov [ebp+var_68],​ eax
 +
 +.text:​00401817pusheax
 +
 +.text:​00401818callds:​exit
 +
 +Листинг 21 Идентификация функции WinMain по роду и количеству передаваемых ей аргументов
 +
 +Но не всегда все так просто,​ - многие разработчики,​ пользуясь наличием исходных текстов start-up кода, модифицируют его (под час весьма значительно). В результате – выполнение программы может начинаться не с WinMain, а любой другой функции,​ к тому же теперь стартовый код может содержать критические для понимания алгоритма программы операции (например,​ расшифровщик основного кода)! Поэтому,​ //​**всегда**//​ хотя бы мельком следует изучить start-up код – не содержит ли он чего-нибудь необычного?​
 +
 +Аналогичным образом обстоят дела и с динамическими библиотеками – их выполнение начинается вовсе не с функции DllMain (если она, конечно,​ вообще присутствует в DLL), а с //​__////​DllMainCRTStartup//​(по умолчанию). Впрочем,​ разработчики под час изменяют умолчания,​ назначая ключом "/​ENTRY"​ ту стартовую функцию,​ которая им нужна. Строго говоря,​ неправильно называть DllMain//​стартовой//​ функций – она вызывается не только при загрузке DLL, но так же и при выгрузке,​ и при создании/​уничтожении подключившим ее процессором нового потока. Получая уведомления об этих событиях,​ разработчик может предпринимать некоторые действия (например,​ подготавливать код к работе в многопоточной среде). Весьма актуален вопрос – имеет ли все это значение для анализа программы?​ Ведь чаще всего требуется проанализировать не всю динамическую библиотеку целиком,​ а исследовать работу некоторых экспортируемых ею функций. Если DllMain выполняет какие-то действия,​ скажем,​ инициализирует переменные,​ то остальные функции,​ на которых распространяется влияние этих переменных,​ будут содержать на них прямые ссылки,​ ведущие прямиком к DllMain. Таким образом,​ не стоит вручную искать DllMain, - она сама себя обнаружит! Хорошо,​ если бы //​**всегда**//​ это было так! Но жизнь сложнее всяких правил. Вдруг в DllMain находится некий деструктивный код или библиотека помимо основной своей деятельности шпионит за потоками,​ отслеживая их появление?​ Тогда без непосредственного анализа ее кода не обойтись!
 +
 +Обнаружить DllMain на порядок труднее,​ чем WinMain, если ее не найдет IDA – пиши пропало. Во-первых,​ прототип DllMain достаточно незамысловат и не содержит ничего характерного:​
 +
 +**BOOL WINAPI DllMain(**
 +
 +** ****HINSTANCE** hinstDLL, ​ // handle to DLL module
 +
 +** ****DWORD **fdwReason, ​ // reason for calling function
 +
 +** ****LPVOID **lpvReserved ​ // reserved
 +
 +);
 +
 +А, во-вторых,​ ее вызов идет из самой гущи довольно внушительной функции __DllMainCRTStartup и быстро убедиться,​ что это именно тот CALL, который нам нужен – нет никакой возможности. Впрочем,​ некоторые зацепки все-таки есть. Так, при неудачной инициализации DllMain возвращает FALSE, и код __DllMainCRTStartupобязательно проверит это значение,​ в случае чего прыгая аж к концу функции. Подробных ветвлений в теле стартовой функции не так уж много и обычно только одно из них связано с функций,​ принимающей три аргумента.
 +
 +**.text:​1000121C ​ push  edi**
 +
 +**.text:​1000121D ​ push  esi**
 +
 +**.text:​1000121E ​ push  ebx**
 +
 +.text:​1000121F ​ call  _DllMain@12
 +
 +.text:​10001224 ​ cmp  esi, 1
 +
 +.text:​10001227 ​ mov  [ebp+arg_4],​ eax
 +
 +.text:​1000122A ​ jnz  short loc_0_10001238
 +
 +**.text:​1000122C ​ test  eax, eax**
 +
 +**.text:​1000122E ​ jnz  short loc_0_10001267**
 +
 +Листинг 22 Идентификация DllMain по коду неудачной инициализации
 +
 +Прокрутив экран немного вверх, нетрудно убедиться,​ что регистры EDI, ESI и EBX содержат lpvReserved,​ fdwReason и hinstDLLсоответственно. А значит,​ перед нами и есть функция DllMain (Для справки,​ исходный текст __DllMainCRTStartupсодержится в файле "​dllcrt0.c",​ который настоятельно рекомендуется изучить).
 +
 +Наконец,​ мы добрались и до функции main консольных Windows-приложений. Как всегда,​ выполнение программы начинается не с нее, а c функции mainCRTStartup,​ инициализирующей //​кучу//,​ систему ввода-вывода,​ подготавливающую аргументы командной строки и только потом предающей управление main. Функция main принимает всего два аргумента:​ "//​int////​ main(////​int////​argc////,​ ////​char////​ **////​argv////​)//"​ – этого слишком мало, чтобы выделить ее среди остальных. Однако приходит на помощь тот факт, что ключи командной строки доступны не только через аргументы,​ но и через глобальные переменные – __argc и __argv соответственно. Поэтому,​ вызов main обычно выглядит так:
 +
 +**.text:​00401293 ​ push  dword_0_407D14**
 +
 +**.text:​00401299 ​ push  dword_0_407D10**
 +
 +.text:​0040129F ​ call  _main
 +
 +.text:​0040129F ; Смотрите:​ оба аргумента функции – указатели на глобальные переменные
 +
 +.text:​0040129F ; (см. "//​Идентификация глобальных переменных//"​)
 +
 +.text:​0040129F
 +
 +.text:​004012A4 ​ add  esp, 0Ch
 +
 +.text:​004012A7 ​ mov  [ebp+var_1C],​ eax
 +
 +**.text:​004012AA ​ push  eax**
 +
 +.text:​004012AA ; Смотрите:​ возвращаемое функцией знаечние,​ передается функции exit
 +
 +.text:​004012AA ; как код завершения процесса
 +
 +.text:​004012AA ; Значит,​ это и //main// и есть!
 +
 +.text:​004012AA ​
 +
 +.text:​004012AB ​ call  _exit
 +
 +Листинг 23 Идентификация main
 +
 +Обратите внимание и на то, что результат завершения main передается следующей за ней функции (это, как правило,​ библиотечная функция exit).
 +
 +Вот мы и разобрались с идентификацией основных типов стартовых функций. Конечно,​ в жизни бывает не все так просто,​ как в теории,​ но в любом случае,​ описанные выше приемы заметно упростят анализ.
 +
 +__дописать идентификацию стартовых функций FreePascal, Fortran….
 +
 +==== Идентификация виртуальных функций ====
 +
 +//А мы летим орбитами,​ путями неизбитыми,//​
 +
 +//​Прошит метеоритами простор.//​
 +
 +//​Оправдан риск и мужество,​ космическая музыка//​
 +
 +//​Вплывает в деловой наш разговор.//​
 +
 +"​Трава у дома"​ Земляне
 +
 +Виртуальная функция по определению обозначает "//​определяемая по время выполнения программы//"​. При вызове виртуальной функции выполняемый код должен соответствовать динамическому типу объекта,​ из которого вызывается функция. Поэтому,​ адрес виртуальной функции не может быть определен на стадии компиляции – это приходится делать непосредственно в момент ее вызова. Вот почему вызов виртуальной функции – всегда //​косвенный//​ вызов (исключение составляют лишь виртуальные функции статических объектов,​ - см. "//​Статическое связывание//"​).
 +
 +В то время как не виртуальные функции вызываются в точности так же, как и обычные Си-функции,​ вызов виртуальных функций кардинально отличается. Конкретная схема зависит от реализации конкретного компилятора,​ но общем случае ссылки на все виртуальные функции помещаются в специальный массив – //​**виртуальную таблицу**//​ (//​**virtual**////​**table**////​** –**//// сокращенно ////​**VTBL**//​),​ а в каждый экземпляр объекта,​ использующий хотя бы одну виртуальную функцию,​ помещается //​**указатель на виртуальную таблицу **//​(//​**virtual**////​**table**////​**pointer**//​– сокращенно //​**VPRT**//​). Причем,​ независимо от числа виртуальный функций,​ каждый объект имеет только один указатель.
 +
 +Вызов виртуальных функций всегда происходит косвенно,​ через ссылку на виртуальную таблицу – например:​ CALL [EBX+0х10],​ где EBX – регистр,​ содержащий смещение виртуальной таблицы в памяти,​ а 0x10 – смещение указателя на виртуальную функцию внутри виртуальной таблицы.
 +
 +Анализ вызова виртуальных функций наталкивается на ряд сложностей,​ самая коварная из которых,​ – необходимость обратной трассировки кода для отслеживания значения регистра,​ используемого для косвенной адресации. Хорошо,​ если он инициализируется непосредственным значением типа "​MOVEBX,​ offsetVTBL"​ недалеко от места использования,​ но значительно чаще указатель на VTBL передается функции как неявный аргумент или (что еще хуже) один и тот же указатель используется для вызова двух различных виртуальных функций и возникает неопределенность – какое именно значение (значения) он имеет в данной ветке программы?​
 +
 +Разберем следующий пример (предварительно вспомнив,​ что если одна и та же не виртуальная функция присутствует и базовом,​ и в производном классе – всегда вызывается функция базового класса).
 +
 +#include <​stdio.h>​
 +
 +class Base{
 +
 + ​public:​
 +
 +virtual void demo(void)
 +
 +{
 +
 +printf("​BASE\n"​);​
 +
 +};
 +
 +virtual void demo_2(void)
 +
 +{
 +
 +printf("​BASE DEMO 2\n");
 +
 +};
 +
 +void demo_3(void)
 +
 +{
 +
 +printf("​Non virtual BASE DEMO 3\n");
 +
 +};
 +
 +};
 +
 +class Derived: public Base{
 +
 + ​public:​
 +
 +virtual void demo(void)
 +
 +{
 +
 +printf("​DERIVED\n"​);​
 +
 +};
 +
 +virtual void demo_2(void)
 +
 +{
 +
 +printf("​DERIVED DEMO 2\n");
 +
 +};
 +
 +void demo_3(void)
 +
 +{
 +
 +printf("​Non virtual DERIVED DEMO 3\n");
 +
 +};
 +
 +};
 +
 +main()
 +
 +{
 +
 +Base *p = new Base;
 +
 +p->​demo();​
 +
 +p->​demo_2();​
 +
 +p->​demo_3();​
 +
 +p = new Derived;
 +
 +p->​demo();​
 +
 +p->​demo_2();​
 +
 +p->​demo_3();​
 +
 +}
 +
 +Листинг 24 Демонстрация вызова виртуальных функций
 +
 +Результат ее компиляции в общем случае должен выглядеть так:
 +
 +mainproc near; CODE XREF: start+AFp
 +
 +pushesi
 +
 +push4
 +
 +call??​2@YAPAXI@Z;​ operator new(uint)
 +
 +; EAX c- указатель на выдел. блок памяти
 +
 +; Выделяем четыре байта памяти для экземпляра нового объекта.
 +
 +; Объект состоит из одного лишь указателя на VTBL.
 +
 +addesp, 4
 +
 +testeax, eax
 +
 +jzshort loc_0_401019 ; --> Ошибка выделения памяти
 +
 +; проверка успешности выделения памяти
 +
 +movdword ptr [eax], offset BASE_VTBL
 +
 +; Вот здесь в только что созданный экземпляр объекта копируется
 +
 +; указатель на виртуальную таблицу класса BASE.
 +
 +; То, что это именно виртуальная таблица класса BASE, можно узнать
 +
 +; проанализировав элементы этой таблицы – они указывают на члены
 +
 +; класса BASE, следовательно,​ сама таблица – виртуальная таблица
 +
 +; класса BASE
 +
 +movesi, eax; ESI = **BASE_VTBL
 +
 +; заносим в ESI указатель на экземпляр объекта (указатель на указатель ​
 +
 +; на BASE_VTBL
 +
 +; Зачем? Дело в том, что на самом деле в ESI заносится указатель на
 +
 +; экземпляр объекта (см. "​Идентификация объектов,​ структур и массивов),​
 +
 +; но нам на данном этапе все эти детали ни к чему, поэтому,​ мы просто
 +
 +; говорим,​ что в ESI – указатель на указатель на виртуальную таблицу
 +
 +; базового класса,​ не вникая для чего понадобился этот двойной указатель.
 +
 +jmpshort loc_0_40101B
 +
 +loc_0_401019:;​ CODE XREF: sub_0_401000+Dj
 +
 +xoresi, esi
 +
 +; принудительно обнуляем указатель на экземпляр объекта (эта ветка получает управление
 +
 +; только в случае неудачного выделения памяти для объекта) нулевой указатель
 +
 +; словит обработчик структурных исключений при первой же попытке обращения
 +
 +loc_0_40101B:;​ CODE XREF: sub_0_401000+17j
 +
 +moveax, [esi]; EAX = *BASE_VTBL == *BASE_DEMO
 +
 +; заносим в EAX указатель на виртуальную таблицу класса BASE,
 +
 +; не забывая о том, что указатель на виртуальную таблицу одновременно
 +
 +; является указателем и на первый элемент этой таблицы.
 +
 +; А первый элемент виртуальной таблицы,​ содержащий указатель
 +
 +; на первую (в порядке объявления) виртуальную функцию класса.
 +
 +movecx, esi; ECX = this
 +
 +; заносим в ECX указатель на экземпляр объекта,​ передавая вызываемой функции
 +
 +; неявный аргумент – указатель this (см. "​Идентификация аргументов функций"​)
 +
 +calldword ptr [eax]; CALL BASE_DEMO
 +
 +; Вот он – вызов виртуальной функции! Чтобы понять – какая именно функция
 +
 +; вызывается,​ мы должны знать значение регистра EAX. Прокручивая экран
 +
 +; дизассемблера вверх, мы видим – EAX указывает на BASE_VTBL, а первый
 +
 +; член BASE_VTBL (см. ниже) указывает на функцию BASE_DEMO. Следовательно:​
 +
 +; а) этот код вызывает именно функцию BASE_DEMO
 +
 +; б) функция BASE_DEMO – это //​**виртуальная**//​ функция
 +
 +movedx, [esi]; EDX =*BASE_DEMO
 +
 +; заносим в EDX указатель на первый элемент виртуальной таблицы класса BASE
 +
 +movecx, esi; ECX = this
 +
 +; заносим в ECX указатель на экземпляр объекта
 +
 +; Это неявный аргумент функции – указатель this (см. "​Идентификация this")
 +
 +calldword ptr [edx+4] ; CALL [BASE_VTBL+4] (BASE_DEMO_2)
 +
 +; Еще один вызов виртуальной функции! Чтобы понять – какая именно функция
 +
 +; вызывается,​ мы должны знать содержимое регистра EDX. Прокручивая экран
 +
 +; дизассемблера вверх, мы видим, что он указывает на BASE_VTBL, а EDX+4,
 +
 +; стало быть, указывает на второй элемент виртуальной таблицы класса BASE.
 +
 +; Он же, в свою очередь,​ указывает на функцию BASE_DEMO_2
 +
 +pushoffset aNonVirtualBase ; "Non virtual BASE DEMO3\n"​
 +
 +callprintf
 +
 +; а вот вызов не виртуальной функции. Обратите внимание – он происходит
 +
 +; как и вызов обычной Си функции. (Обратите внимание,​ что эта функция -
 +
 +; встроенная,​ т.к. объявленная непосредственно в самом классе и вместо ее
 +
 +; вызова осуществляется подстановка кода)
 +
 +push4
 +
 +call??​2@YAPAXI@Z;​ operator new(uint)
 +
 +; Далее идет вызов функций класса DERIVED. Не будем здесь подробно
 +
 +; его комментировать – сделайте это самостоятельно. Вообще же, класс
 +
 +; DERIVED понадобился только для того, чтобы показать особенности компоновки
 +
 +; виртуальных таблиц
 +
 +
 +
 +addesp, 8; Очистка послеprintf & new
 +
 +testeax, eax
 +
 +jzshortloc_0_40104A;​ Ошибка выделения памяти
 +
 +movdword ptr [eax], offset DERIVED_VTBL
 +
 +movesi, eax; ESI == **DERIVED_VTBL
 +
 +jmpshort loc_0_40104C
 +
 +loc_0_40104A:;​ CODE XREF: sub_0_401000+3Ej
 +
 +xoresi, esi
 +
 +loc_0_40104C:;​ CODE XREF: sub_0_401000+48j
 +
 +moveax, [esi]; EAX =*DERIVED_VTBL
 +
 +movecx, esi; ECX = this
 +
 +calldword ptr [eax]; CALL [DERIVED_VTBL] (DERIVED_DEMO)
 +
 +movedx, [esi]; EDX =*DERIVED_VTBL
 +
 +movecx, esi; ECX=this
 +
 +calldword ptr [edx+4] ; CALL [DERIVED_VTBL+4] (DERIVED_DEMO_2)
 +
 +**pushoffset aNonVirtualBase ; "Non virtual BASE DEMO 3\n"**
 +
 +**call********printf**
 +
 +; Обратите внимание – вызывается функция BASE_DEMO базового, ​
 +
 +; а не производного класса!!!
 +
 +addesp, 4
 +
 +popesi
 +
 +retn
 +
 +mainendp
 +
 +BASE_DEMOproc near; DATA XREF: .rdata:​004050B0o
 +
 +pushoffset aBase; "​BASE\n"​
 +
 +callprintf
 +
 +popecx
 +
 +retn
 +
 +BASE_DEMOendp
 +
 +BASE_DEMO_2proc near; DATA XREF: .rdata:​004050B4o
 +
 +pushoffset aBaseDemo2 ; "BASE DEMO 2\n"
 +
 +callprintf
 +
 +popecx
 +
 +retn
 +
 +BASE_DEMO_2endp
 +
 +DERIVED_DEMOproc near; DATA XREF: .rdata:​004050A8o
 +
 +pushoffset aDerived; "​DERIVED\n"​
 +
 +callprintf
 +
 +popecx
 +
 +retn
 +
 +DERIVED_DEMOendp
 +
 +DERIVED_DEMO_2proc near; DATA XREF: .rdata:​004050ACo
 +
 +pushoffset aDerivedDemo2 ; "​DERIVEDDEMO 2\n"
 +
 +callprintf
 +
 +popecx
 +
 +retn
 +
 +DERIVED_DEMO_2endp
 +
 +DERIVED_VTBLdd offset DERIVED_DEMO;​ DATA XREF: sub_0_401000+40o
 +
 +dd offset DERIVED_DEMO_2
 +<