RSS

Компьютерная терминология    1_9  A  B  C  D  E  F  G  H  I  J  K  L  M  N  O  P  Q  R  S  T  U  V  W  X  Y  Z  .....  A  Б  В  Г  Д  Ж  З  И  К  Л  М  Н  О  П  Р  С  Т  У  Ф  Х  Ц  Ч

Техника и философия хакерских атак

   
Примеры использования Социальной Инженерии

Техника и философия хакерских атак

Крис Касперски

О чем эта книга

... В моем уме не оставалось места для беспокойства об успехе или провале книги. Было лишь желание работать над ее созданием.

Ф. Херберт. "Еретики Дюны"

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

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

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

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

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

Я даже не буду пытаться научить читателей "ловить рыбу", а рискну пойти немного дальше и дать навыки самообучения. Из любой самой нестандартной ситуации и при самом скудном инструментарии всегда можно найти выход. Типовые схемы часто оказываются бессильными и бесполезными. Любые навыки слишком привязаны к конкретному окружению. В наше время больших перемен уже поздно хвататься за традиционные схемы обучения. На обучение просто нет времени. Большинству программистов приходится осваивать новые технологии "на ходу", и задолго до конца обучения они уже полностью устаревают.

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

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

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

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

Цель этой книги - научить читателя самостоятельно добывать необходимые ему знания и навыки, порой не имея соответствующей литературы и информации. Современное информационное изобилие приводит к атрофированию навыка самостоятельного получения необходимых знаний. Парадоксально на первый взгляд, но недостаток литературы развивает и тренирует мозги куда лучше, чем ее избыток. Сам я осваивал ассемблер 8086 с помощью утилиты debug.com: кроме нее (и свободного времени) у меня в ту пору не было НИЧЕГО. Логика работы команд изучалась анализом воздействия последних на регистры и память. Эта было утомительное занятие, и свободное владение ассемблером (без учета ряда некритичных команд) ко мне пришло приблизительно через три месяца. Наличие инструкции сократило бы этот срок до двух-трех дней (с учетом знания ассемблера других платформ), но зато не дало бы никаких полезных навыков.

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

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

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

История хакерства

Попытка понять Муаддиба, не поняв его смертельных врагов, - это попытка увидеть правду без знания лжи. Это попытка понять, что такое свет, не зная, что такое тьма. Это просто невозможно.

Принцесса Ирулэн<R>Руководство Муаддиба

"I remember the good old days, when computers were mainframes, analysts were magicians,and programmers punced cards..."

Philip Fites, Peter Johnston, Martin Kratz<R>"Computer viruses crysis".

1. Рассвет. Первые лучи
Первые хакеры появились задолго до возникновения компьютеров, более того задолго до зарождения цивилизации и даже появления человечества. Первым открытием хакеров было удивительное свойство палки, позволявшее использовать ее одновременно как орудие охоты, обороны и многихдругих целях. Нетривиальное, за гранью обычного, восприятие мира - вот главная черта хакеров. Им мало просто видеть предмет в трех измерениях. Для хакеров каждый предмет связан с определенными свойствами и включен в определенные причинно-следственные отношения.

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

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

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

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

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

Обратимся к лучшему на сегодняшний день исследованию хакерской культуры, Словарю Жаргона Э.С. Рэймонда (заметим, что моя трактовка никак не противоречит традиционной, но лишь расширяет ее):

":hacker: [originally, someone who makes furniture with an axe] n. 1. A person who enjoys exploring the details of programmable systems and how to stretch their capabilities, as opposed to most users, who prefer to learn only the minimum necessary. 2. One who programs enthusiastically (even obsessively) or who enjoys programming rather than just theorizing about programming. 3. A person capable of appreciating {hack value}. 4. A person who is good at programming quickly. 5. An expert at a particular program, or one who frequently does work using it or on it; as in `a UNIX hacker'. (Definitions 1 through 5 are correlated, and people who fit them congregate.) 6. An expert or enthusiast of any kind. One might be an astronomy hacker, for example. 7. One who enjoys the intellectual challenge of creatively overcoming or circumventing limitations. 8. [deprecated] A malicious meddler who tries to discover sensitive information by poking around. Hence `password hacker', `network hacker'. The correct term is {cracker}."

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

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

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

Документированная история американского хакерства берет начало от все того же Словаря Жаргона Рэймонда.

2. Лаборатория искусственного интеллекта в США и PDP-1

"Нет четкой грани между богами и людьми: одни переходят в других".

Ф.Херберт "Мессия Дюны".

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

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

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

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

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

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

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

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

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

Но и на этом фоне заметно выделялась лаборатория искусственного интеллекта MIT (Массачусетский технологический институт Massachusets Institute of Technology), в которой работали сливки компьютерной общественности. Со временем эти люди, разъедутся по другим городам и займутся другой работой, распространяя повсюду свой жаргон и стиль жизни. Но об этом расскажем несколько позже.

Вторым центром хакерства в Америке был Корнеллский университет, расположенный в штате Нью-Йорк. Лояльное отношение к студентам и предоставление "ночного машинного времени" способствовало развитию интереса к вычислительной технике и формированию хакерских традиций. Это поколение навсегда оставило след в истории "звездными войнами", открыв тем самым возможности компьютера как игровой платформы. Первые образцы ANSI-искусства были созданы именно Корнелловскими студентами. И хотя сегодня уже трудно установить, кто первым применил символьный терминал для передачи графического образа, этот человек несомненно был хакером. Способность заставить систему делать то, чего никак не ожидал ее разработчик, поистине заслуживает восхищения. В СССР же картины, созданные на принтерах типа "Ромашка", появились гораздо позже и явно были копиями западных.

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

Последнее явление вообще стало национальной чертой русского андерграунда. Отсутствие клубов и разобщенность людей не способствовали развитию собственной субкультуры.

Центрами возникновения отечественных хакеров стали не нищие ВУЗы, а производственные предприятия и НИИ. Не секрет, что в то время в журналы загрузки машинного времени часто писали "липу", а рабочее время использовали для удовлетворения собственного любопытства.

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

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

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

По непроверенным данным, первый PDP-1 был создан именно MIT. Если это верно, то не приходится удивляться, что именно тут сформировалась особая субкультура людей, которые были настоящими волшебниками больших машин. Язык определяет стиль мышления, поэтому мышление программиста постепенно преображается под воздействием используемой архитектуры в образ мышления ее создателя. Это настолько очевидно, что никто из психологов в этом не сомневается.

Немного утрируя можно сказать, что возникновением хакерства - такого, каким мы его знали в 60-х годах, - мы целиком и полностью обязаны человеку-легенде Кену Оулсену (Ken Olsen), основателю компании DEC и ведущему разработчику аппаратного обеспечения. Без сомненья, он был хакером и его образ мышления невольно копировали все пользователи его детища в течение двух десятков лет. Действительно, нужны чертовски нетривиальные мозги, чтобы создать компьютер, который стоил всего 120.000 $, тогда как майнфреймы IBM обходились в миллионы. Конечно, майнфреймы IBM заведомо превосходили PDP, но последняя имела неповторимую архитектуру, сохранившую привлекательность и до сих пор. Даже в наше время суперпроцессоров P PRO немало людей с упоением программируют на ассемблере PDP, используя эмулятор или даже сохранившееся аппаратное обеспечение.

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

Именно в PDP нашли свой рай компьютерные гуру. Ее успех способствовал росту поставок DEC, расширению фирмы, а вместе с ним - возникновению полноводного хакерского течения в лабораториях крупнейших университетов.

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

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

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

Но ростки, пущенные PDP-1, живы и по сей день.

3. Сеть
Ива сгибается под ветром, пока не разрастется и не встанет стеной на его пути. В этом ее предназначение.

Ф. Херберт. "Дюна".

До конца шестидесятых годов хакеров можно было сопоставить с античными Мастерами. Хак ассоциировался с высшим профессионализмом и вытекающей из него культурой поведения. Тесная связь культурного и интеллектуального уровней давно отмечалась психологами. Из этого правила, конечно, бывают исключения, но они редки и не меняют общей картины. А картина до конца шестидесятых годов была следующая - в условиях полной замкнутости и отсутствия какой-либо связи между компьютерными центрами страны каждый программист должен был получить необходимые ему знания САМ. Это был долгий и тернистый путь. Информатика тогда лишь зарождалась, и даже эффективные алгоритмы и приемы еще не были канонизированы и широко известны. Как бы ни был очевиден древовидный поиск или линейная сортировка, но до них нужно было додуматься самому и - увы - далеко не один раз. Как следвствие, все алгоритмы десятки раз открывались заново, прежде чем информация о них успевала дойти до адресатов "естественным" путем - через книги и университеты.

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

В лексиконе тогдашних хакеров еще не появилось оскорбительное слово "ламер". Не то чтобы среди них не было таковых, но программисты в то время намного лояльнее относились к непрофессионалам. Да и как бы можно было расценить такие оскорбления в тесных коллективах?

Сегодня, на фоне развитых сетевых коммуникаций, когда собеседники едва ли имеют шанс встретиться лицом к лицу в реальной жизни, ситуация кардинально изменилась. Электронное общение принесло вместе с неоспоримыми благами немало проблем, с которыми вынуждены сталкиваться сегодняшние хакеры. Первый камень этого здания был заложен в 1969 году. Тогда по инициативе Управления перспективных исследований Министерства Обороны США - Defense Advanced Research Projects Agency - создается первая вычислительная сеть, получившая название Advanced Research Projects Agency NETwork - ARPAnet.

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

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

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

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

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

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

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

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

Ярким примером могут служить первые прототипы будущих вирусов программы-кролики. Не причиняя разрушений, они тем не менее были сконструированы так, что многократно копируя себя захватывали большую часть ресурсов системы, отнимая процессорное время у других процессоров.История их создания доподлинно не известна. Возможно, они явились следствием программной ошибки, которая приводила к зацикливанию и наделяла программу репродуктивными свойствами. Первоначально кролики (rabbits) встречались только на локальных машинах, но с появлением Сети быстро "научились" размножаться по последней. В конце шестидесятых годов была обнаружена саморазмножающаяся по сети APRAnet программа, известная сегодня как CREEPER (вьюнок), которая будто бы была написана Бобом Томасом (Bob Thomas). Вьюнок проявлял себя текстовым сообщением

"I'M THE CREEPER ... CATCH ME IF YOU CAN"<R>("Я КРИПЕР ... ПОЙМАЙ МЕНЯ, ЕСЛИ СМОЖЕШЬ")

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

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

Первым шагом в борьбе против Вьюнка стал Жнец (REAPER), репродуцирующийся наподобие Creeper-а, но уничтожающий все встретившиеся ему копии последнего. Мы не знаем, чем закончилась борьба двух программ. Так или иначе от подобного подхода к защите впоследствии отказались. Однако копии обеих программ еще долго бродили по сети...

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

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

4. Си и UNIX
Легкие мои вдыхают ветер времени. Дующий над мертвыми песками...

Ф.Херберт "Дюна".

В 1969 г. усилиями двух талантливых программистов была создана система, сформировавшая не одно поколение хакеров и заложившая основы современных операционных систем. Я говорю о UNIX, разработанной К.Томпсоном и Д.Ричи, ведущими инженерами Bell Laboratories - подразделения American Telephone & Telegraph (AT&T). UNIX была первой системой, написанной на компилируемом языке. Так и хочется закончить - языке высокого уровня. Но уникальность Си и заключалась как раз в том, что он не походил на существующие в то время гипнотические и алгоритмические языки. Си сочетая в себе возможности низкоуровневого доступа, "отвязанные" от конкретной реализации, больше походил на крос-платформенный ассемблер. Такая идеология языка позволила создавать эффективные компиляторы и получить быстродействующий и компактный код.

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

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

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

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

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

Успех системы позволил AT&T значительно усилить свои позиции на рынке. UNIX, в противоположность системе Windows 90-х годов, не только был удачной платформой, но и дружелюбной к профессионалу средой, развивающей абстрактное мышление, способствующей умственному и в конечном счете культурному развитию.

5. Конец хакеров шестидесятых
"Я не должна бояться. Страх убивает разум. Страх - малая смерть, которая приносит полное уничтожение. Я смотрю в лицо моему страху..."

Ф. Херберт "Дюна".

Шагнем на пару лет вперед. Это были годы в непрерывного совершенстованния технологий программирования и аппаратного обеспечения. Росла вычислительная мощь ЭВМ, емкость переферийных накопителей и быстродействие центрального процессора. Собственно говор, в то время все упиралось скорее в деньги, чем в технологии. Если клиент хотел решать с помощью компьютера серьезные задачи, то получение необходимой машинной мощности зависело только от его кошелька. Производители компьютеров заметно обгоняли в те годы потребности клиентов.

И необходимость в высокопрофессональных программистах и тщательно оптимизированном коде мало-помалу начала отпадать. Ярким подтверждением тому была UNIX, полностью написанная на компилируемом языке. Могли ли программисты представить такое расточительство хотя бы пару лет назад? Си стал первым кирпичом в фундаменте быстрых средств проектирования,на котором сегодня построены такие чисто визуальные системы как Visual Basic и Delphi.

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

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

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

Если бы этим все и ограничилось, то было бы полбеды, в конце концов настоящий ХУДОЖНИК проявил бы себя и в таких условиях. И код, написанный даже в условиях этой сумбурной спешки настоящим хакером, все равно бы нес в себе долю оригинальности и собственного "я" программиста. Однако, стремясь максимально эффективно использоать труд людей, руководство разделяло коллектив на небольшие группы из трех-пяти человек, каждый из который решал свою локальную задачу, тесно зажатый ее рамками.

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

Коммерция и жажда наживы поглощала все. Главным критерием становилась скорость написания продукта, какого бы он ни был качества. Любые недостатки и ошибки программного обеспечения не обнаруживаются сразу и всегда есть время их устранить... потом, когда клиент, поверивший рекламе, вложит деньги в ваш товар и приобретет его. Кто первым выбрасывает на рынок продукт, тот и захватывает его. Таков закон. Собственно под "сопровождением ПО" почти любая фирма понимала его доработку и доведение из сырого состояния в некоторое подобие законченного продукта.

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

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

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

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

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

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

Хакеры шестидесятых вырождались. Не было больше "больших" машин и открытого духа свободного общения. С каждым днем все больше и больше находок и неординарных решений окутывались мраком коммерческой тайны и тщательно охранялись фирмой от окружающих.

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

6. RSX-11M
Подсмотреть будущее - значит украсть мистический огонь от священного костра.

Ф. Херберт "Дюна".

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

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

Однако "свято место пусто не бывает" и "природа не терпит пустоты". А вот в отношении операционных систем для PDP-11 появился заметный вакуум. UNIX требовала на несколько порядков больше мощности и попросту не могла работать на компьютерах такого класса. 32 килобайт памяти было недостаточно, чтобы разместить такого монстра.

И в 71 году компания DEC решилась создать собственную операционную систему RSX-11M. Необходимо было поддержать многозадачность, планировщик, иерархическую файловую систему, виртуальную память. На все это отводилось только 16 килобайт оперативной памяти, так как другие 16 килобайт было решено отдать приложениям.

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

Благодаря этой новой технологии лучшие специалисты DEC смогли в бешеном темпе завершить свою работу за 18 месяцев. При этом операционная система получилась в конечном счете очень простой и сильно уступающей набирающей популярность UNIX. Но своего потребителя все же нашла. Им стали многие средние предприятия, которые могли себе позволить купить несколько ЭВМ - по одному для каждого отдела. И их вполне устраивала мощность простого 16-разрядного процессора PDP.

Вскоре RSX-11M получила завидную популярность, и объемы продаж были весьма внушительными. Другие фирмы, пытаясь перенять этот успех, переносили существующие приложения на миникомпьютеры, чем вызвали первую волну переориентации разработчиков.

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

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

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

7. INTEL
"Солнце не просит о милосердии".

Ф. Херберт "Дюна".

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

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

Такой чип впервые появился в 1972 году и вышел под кодовым обозначением 8008. Это была первая и очень простая модель микропроцессора, которая могла служить калькулятором, и не более того. Тем не менее юный Билл Гейтс все же ухитрился создать на ее основе устройство для анализа городского траффика. Разумеется, назвать это устройство компьютером было нельзя даже с большой натяжкой.

Но время не стояло на месте, а инженеры Intel не сидели сложа руки. Через три года, весной 1974, журнал Electronics опубликовал сообщение о новом чипе Intel 8080. Это действительно был шедевр, который каждый мог приобрести всего за 200 долларов. Конечно, этому микропроцессору было далеко до "больших машин" как по скорости, так и по архитектуре, системе команд, но для "персонального" использования он вполне годился.

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

Однако это и был первый минимально функциональный ПК. Микропроцессор, немного оперативной памяти и сопрягающих микросхем - вот и все, что было нужно для работы этой груды железа. Единственным устройством вывода служили 16 индикаторов на передней панели, а устройством ввода - 16 переключателей. Собственно говоря, это была машина для радиолюбителей и электронщиков. Последние вообще могли просиживать ночи напролет, заставляя индикаторы перемигиваться самым причудливым образом. 4 килобайта памяти тогда казались более чем достаточными для таких целей. А вот отсутствие удобных средств ввода\вывода сильно обескураживало.

Поэтому практически каждый, купивший Альтаир, пытался в той или иной степени его модернизировать. Не будем забывать, что в то время он стоил 1000$ и позволить себе купить его могли только фанатично преданные электронике (или компьютерам) люди. Теперь они получали ЭВМ в свое личное распоряжение и могли экспериментировать с ней как им вздумается.

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

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

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

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

8. Хаос
Хаос существовал в виде сырья, из которого творится порядок.

Ф. Херберт "Дом Глав Дюны".

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

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

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

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

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

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

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

При этом качество его оставляло желать лучшего. Может быть, среди читателей найдутся такие, которые до сих пор помнят некоторый шедевр отечественного компьютеростроения "Агат-9" и его на редкость корявую программную оснастку. Агат-DOS 3.3 не выдерживала никакой конкуренции даже с существующей в то время CP\M.

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

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

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

Фактически было невыгодно покупать новые модели компьютеров. Увеличение вычислительной мощности не компенсировало необходимость переобучения персонала и переписывание всего необходимого ПО. Большинство предпочитало работь на старом оборудовании, попутно решая головоломную проблему ремонта вышедшей из строя техники. С каждым годом достать устаревшие (и уже снятые с производства) комплектующие становилось все сложнее.

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

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

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

Ф. Херберт "Бог - император Дюны".

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

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

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

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

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

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

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

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

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

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

Это, однако, особая тема.

10. Рождение современных хакеров, или снова INTEL

...он был протозойским творением, рождение и смерть которого по сути одновременны.

Ф. Херберт "Дети Дюны".

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

Но IBM имела богатый опыт в конструировании майн-фреймов и, кроме того, набрала в свой штат опытных инженеров, с которыми другим компаниям было нелегко тягаться. Действительно, все остальные допустили одну роковую ошибку, а именно - каждый компьютер с момента своего создания был обречен на немедленное и неминуемое старение. Можно было лишь купить новый компьютер...

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

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

Однако эффект был совершенно непредсказуемым - вместо того чтобы поддерживать новую перспективную модель, десятки фирм начали клонировать ее и продавать гораздо дешевле, чем оригинальный производитель. Пародоксально, но это только играло на руку компании. Убыток от "пиратства", конечно, был велик, но он способствал популярности этой модели.

За короткое время было продано несколько тысяч полностью совместимых между собой машин. Разработчики ПО не могли мечтать о лучшем и активно переходили на новую платформу. Это вызывало популярность и среди пользователей.

Сейчас фирма IBM утратила лидирующее положение в производстве компьютеров, да и архитекура современных машин уже мало напоминает древний XT\AT, но единственное, что осталось неизменным, - это сердце компьютера: микропроцессор фирмы Intel. Если бы выбор IBM пал на другой микропроцессор, облик современных компьютеров мог быть другим.

Сегодя идут ожесточенные дискуссии о том, что лучше: PowerPC или Intel, причем основная часть спорящих крайне агрессивно настроена против последнего. Сразу видно, что практически все спорящие являются простми пользователями, или, по крайне мере, прикладными программистами. Дело в том, что ни один хакер на трезвую голову никогда не будет выражать свою симпатию к RISC процессорам. Их ограниченный набор команд и адресаций загоняет ассемблерщиков в тиски и не позволяет проявить никакой индивидуальности. Да, для компиляторов подобный подход весьма практичен и упрощает их написание. Но человек не компилятор: он всегда хочет иметь свободу выбора и выражения собственных идей и решений.

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

Парадокс, но большая часть "хакеров" до сих пор ограничивается реальным режимом и не в состоянии хотя бы в общих чертах освоить защищенный. При этом за Intel'ом уже закреплен ярлык "нехакерского" микропроцессора. Но это всего лишь распространенное заблуждение! То же можно сказать и о Билле Гейтсе, которого сегодня склоняют на все лады, забыв, что именно он настоял на том, чтобы фирма IBM использовла не 8-разрядный процессор (как она первоначально планировала), а именно передовой 16-разрядный. Не окажись Билла - кто знает, может быть, IBM выпустила бы очередной "спектрум", который бы бесследно исчез и не было бы ничего, что окружает нас сейчас.

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

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

Современных хакеров, какие они есть, сформировал главным образом именно Билл Гейтс. Когда вы работаете с его продуктами, они вольно или невольно накладывают на вас отпечаток своего создателя. Впрочем, Билл лично не участвовал в разработке ни Dos, ни Windows, поэтому его присутствие ощущалось только косвенно. Между прочим, лишь редкие системные программисты под Windows ругают последнюю или ее создателя. И если ваш собеседник настроен против MicroSoft, то он скорее всего не имеет никакого отношения к хакерам.

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

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

Ф. Херберт "Мессия Дюны".

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

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

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

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

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

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

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

Со временем появились защищенные операционные системы типа Windows NT, но они не получили ожидаемой популярности именно по причинам плохой совместимости с уже существующим программным обеспечением. Большинство до сих пор работает на ничем не защищенной Windows 95\Windows 98. Впрочем, надо признать, что последняя все же обрела, хоть и с большим запозданием, защитные механизмы, предотвращающие некоторые деструктивные действия. К сожалению, это достигается ценой ухудшенной совместимости. Многие программы могут не работать или работать неправильно. В этом мире за все нужно платить.

Но оставим дискуссию о проблеме незащищенности "народной" операционной системы Windows 9x. Словами горю не поможешь: если вам требуется безопасность, переходите хотя бы на Windows NT. Это не решит всех проблем, но по крайней мере позволит спать спокойно.

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

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

К счастью, подавляющее большинство таких людей обладает весьма смутными знаниями. Они не представляли бы никакой угрозы, если бы пользователи не были так беспечны в отношении собственной безопасности. По разным оценкам, ущерб от вандалов составляет 5-10 процентов от общего числа случаев потери информации (при этом более 50% инцидентов происходит из-за ошибок или неверных действий пользователя).

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

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

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

Силой невозможно противостоять силе. И нетерпимость окружающих к вирусописателям и стремление "начистить последним функель" или "разобраться" только подливает масла в огонь. Человек, отвечающий агрессией на действия вирусописателя, уже не заслуживает жалости. Чем лучше он сам, обиженный и готовый избить первого подозреваемого вирусописателя, не особо утруждая себя выяснением, насколько тот виноват?

Фактически вандалов гораздо больше, и то, что часть из них не пишет ни вирусов, ни троянских программ - чистая случайность. Просто они еще не дошли до этой грани, но кто может утверждать, что этого никогда не будет? Очень сомнительно...

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

Каждый случай требует отдельного рассмотрения. Человеком могут двигать разные мотивы, а не только месть. Быть может, он одинок и хочет внимания, которого ему так не хватает? Конечно, садиться и писать вредоносные программы - это не выход, но не забывайте, что речь идет о людях с теми или иными психическими отклонениями, в которых они сами не виноваты.

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

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

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

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

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

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

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

То же можно сказать и о взломе программ. И в том и в другом случае многих сознательно или подсознательно привлекает власть. Вряд ли последнее утверждение нуждается в пояснении.

Введение в криптосистемы

Введение
"Стать хакером очень просто. Достаточно выучить и понять: математические дисциплины (математический анализ, теорию функций комплексного переменного, алгебру, геометрию, теорию вероятностей, математическую статистику, математическую логику и дискретную математику...)".

Борис Леонтьев "Хакеры & Internet".

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

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

Кроме того, обычно такие оценки исходят из предположения, что злоумышленник будет просто перебирать последовательно все пароли один за другим. Наивно, не так ли? Лобовая атака на современных вычислительных мощностях персональных компьютеров сегодня попросту невозможна. Сколь-нибудь удовлетворительные результаты могут быть получены только при объединении десятков тысяч компьютеров.

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

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

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

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

С другой стороны никакой математической подготовки книга не требует. Однако я надеюсь, что в процессе чтения читатель будет обращаться к соответствующим источникам для получения более детальной картины подаваемого материала.

Настоящий хакер никогда не откажет себе в удовольствии копаться не только в недрах кода, но и в литературе (бумажной или электронной). Соблазн использовать готовые схемы и решения "AS IS" достоин только коммерческих кракеров (поставивших взлом на поток и зарабатывающих деньги на эксплуатации чужих идей и инструментов). Я не хочу вдаваться в этическую полемику и задаваться извечным вопросом "что такое хорошо и что такое плохо?" Так или иначе это есть. Для использования любой законченной технологии не требуется понимать ее функционирование. Воплощенная в конкретный программный код, она становится в руках вандалов разрушающим программным оружием. Выход из ситуации "методом Страуса" невозможен. Сегодня вычислительная техника уже не контролируется, и такая программа будет создана и распространена - если не одним, так другим человеком. Распространение технологий и инструментов для взломов и атак является не актом вандализма, а напротив, указанием на уязвимость существующих систем,и побуждает к исправлению последних.

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

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

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

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

Хеши. Односторонние функции
"Ночь - это туннель,- подумала она. - Это дыра в завтра,если только оно наступит, это завтра".

Ф.Херберт. "Дюна"

Вся современная криптография основана на использовании методов хеширования. Метод хеширования позволяет хранить элементы множества А в линейном массиве Х. Математически это можно записать:

h: А --> {0,x-1}

Это читается: функция h отображает каждый элемент А в индекс множества Х. Поскольку число элементов А, как правило, намного больше Х, то функция h наверняка неинъективна. Однако возможно существование такого интервала на области определения функции, в границах которого она становится инъективной. Это означает, что только для одного элемента А существует индекс x1. Функция будет инъективной и в том случае, если ни один элемент А не отображается на интервал {x1,x2} при условии, что последний не равен нулю. В любом другом случае на каждый индекс множества Х отображается более одного элемента А. Это так называемая коллизия хеш-функции. Реверс хеш-функции заключается в поиске всех отображаемых на данный индекс элементов. Для любого конечного множества А это разрешимая задача, которая имеет наиболее простое решение на инъективных интервалах хеш-множества.

Давайте остановимся на инъективных интервалах. Невозможно практическое использование хеш-преобразований без понимания их значения. В качестве примера рассмотрим следующую задачу: пусть нам требуется эффективный алгоритм сравнения строк, допустим, для синтаксического анализатора. Простое посимвольное сравнение потребует значительной памяти для хранения образов строк. Представим все возможное множество строк в данной разрядной сетке массивом S, тогда мы можем хеш-преобразованием привести его к множеству b {0,0xFF}. Разумеется, b много меньше S и требует для хранения значительно меньше памяти.

Рассмотрим практическую реализацию для некоторого множества команд 'IF', 'THEN', 'BEGIN', 'END'. Выберем простейшую хеш-функцию, посимвольно складывающую элементы строки. Это плохая хеш-функция (ниже мы поясним почему), но для нашего примера ее будет достаточно.

Для начала нужно построить хеш-таблицу значений для всех элементов множества S. Воспользуемся для этого программой file://CD:/SRC/hash00.exe - "хеш-калькулятор". Рассмотрим ее ключевой фрагмент: BYTE GetHash(CString s0) { BYTE hash=0; for (int a=0;a<s0.GetLength();a++) hash=hash+s0[a]; // Вычисляем хеш-сумму return hash; }

Мы посимвольно складываем элементы множества символов s0, получая в результате некоторый элемент из множества calc {0,0xFF}. Как нетрудно видеть, на каждый элемент множества calc отображается не более чем один элемент S. Таким образом, в заданных рамках функция calc=calc+s0[a] инъективна. Следовательно, для заданного индекса единственным образом определяется элемент S. Этим доказывается корректность работы хеш-анализатора.

Ниже приведен фрагмент реально работающего эмулятора виртуального процессора GetMe01 (file://CD:\CrackMe\GetMe01.asm). LEA SI, Buffer ; Буфер исполнительных команд start_r: ; <--------------------------------------¬ CALL GetHashe ; Вычисление хеш-суммы очередной команды ¦ CALL CallIt ; Вызов обработчика для данной команды ¦ MOV SI,DI ; Позиционирование указателя команд ¦ CALL Seek ; ^ ¦ JMP short start_r ; --[uncond]------------------------------ GetHashe PROC NEAR PUSH SI ; Сохраняем виртуальный указатель команд XOR AX,AX ; AH == сумматор ~~6 gh_Repeat: ; <--------------------------------------¬ LODSB ; Берем очередной элемент множества s0 ¦ ADD AH,AL ; Добавляем в сумматор ¦ XOR AL,':' ; Проверка на конец команды ¦ JNZ gh_Repeat ; --([SI]<>':')--------------------------- POP DI ; si-->di; return SI RETN ; --\ ENDP Seek PROC NEAR s_repeat: ; <--------------------------------------¬ LODSB ; Взять следующий символ ¦ OR AL,AL ; Проверить на равенство нулю ¦ JNZ s_repeat ; --([SI]<>NULL)-------------------------- RETN ; --\ ENDP CallIt Proc NEAR PUSH SI ; Сохранить SI LEA SI,TableOfRange ; Таблица указателей на обработчики XCHG AX,BX ; BH == AL ci_rep: ; <--------------------------------------¬ LODSB ; Читаем очередную хеш-сумму ¦ OR AL,AL ; Конец таблицы? ¦ JZ _err ; --достигнут конец таблицы--> ¦ CMP AL,BH ; Хеш найден? ¦ LODSW ; Читаем очередное смещение обработчика ¦ JNZ ci_rep ; --(!Hash)------------------------------- XCHG AX,BX ; BX == AX == смещение обработчика POP SI ; Восстановить SI CALL BX ; Вызвать обработчик данной команды RET ; --\ _err: ; Исключительная ситуация. Неверная команда MOV AH,9 ; Печать строки MS-DOS LEA DX,errr ; Смещение печатаемой строки INT 21h ; MOV AH,4Ch ; Завершение работы INT 21h ; --\ errr DB 'Неизвестная команда',0Dh,0Ah,'$' ENDP ; ////////////////////////////////////////////////////////////////////////// ; // ТАБЛИЦА ПОДДЕРЖИВАЕМЫХ КОМАНД И СООТВЕТСТВУЮЩИХ ИМ ОБРАБОТЧИКОВ ; //------------------------------------------------------------------------ TableOfRange: DB 0EFh ; Хеш-сумма команды DW Offset Proc1 ; Обработчик данной команды DB 0E3h DW offset Proc2 DB 79h DW offset Proc3 DB 0E6h DW offset Proc4 DB 01h DW offset Proc5 DB 0D1h DW offset Proc7 DB 054h DW offset Proc8 DB 0ADh DW offset Proc9 DB 0 ; Конец таблицы Buffer: ; Буфер исполнительных команд

Данный пример хранит восьмибитную хеш-сумму каждой команды. Следовательно, независимо от длины используемых команд для хранения потребуется всего N байт памяти (где N - число команд), что существенно меньше объема, необходимого для полного хранения тех же самых команд. Теоретически восьмибитная хеш-сумма может вместить до 0xFF+1 команд. Однако практически предложенная реализация при добавлении некоторого числа команд окажется неработоспособной. Это случится, когда двум разным командам будет соответствовать одна и та же хеш-сумма. В таком случае математики говорят о коллизии хеш-функции. Следовательно, на выбранном интервале хеш-функция становится неинъективной и для рассматриваемой задачи непригодной.

Казалось бы, если множество S намного больше X, то любая хеш-функция на полной области определения S окажется неинъективной. На самом деле число элементов S практически всегда много меньше его мощности. Например, множество слов русского языка представляется множеством последовательностей от одного до десяти и более символов,но общее число существующих слов заведомо меньше 0xFFFFFFFF. Поэтому возможно существование 32-битной инъективной хеш функции для синтаксического анализатора русского языка.

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

Наиболее часто используется и хорошо изучен мультипликационный метод отображения. Математически его можно выразить как h(S[x])=C*S[x] mod N, где N - это число элементов хеш-множества или другими словами его мощность. С - любая константа из интервала [0,N). При C=1 эта формула приобретает вид h(S[x])=S[x] mod N и является самостоятельным хеш-преобразованием, называемым методом остатка. Данный алгоритм также широко используется и для генерации псевдослучайных чисел. И неудивительно, ведь генераторы случайных чисел являются нечем иным, как хеш-преобразованием!

К числу набиболее популярных принадлежит также необычайно мощная функция CRC 32 (crc16): cyclic redundancy check (код циклического контроля). Эта функция в первую очередь призвана подтверждать неискаженность выбранной числовой последовательности. Это еще одна область применения хеш-преобразований. В самом деле здесь осуществляется все то же хеш-сравнение. Конечно, существует определенная вероятность таких искажений, которые не отразятся на хеш-сумме, но она достаточно невелика при условии хорошего рассеяния и чувствительности к незначительным изменениям выбранного хеш-преобразования.

CRC 32 - это многократно проверенная, быстродействующая качественная хеш-функция, дающая превосходный результат. Для большинства последовательностей она будет инъективна. Так, например, многие текстовые редакторы реализуют синтаксическую "подсветку" именно на основе CRC32, не вызывая при этом коллизий.

Рассмотрим алгоритм этой функции. Целую беззнаковую 32-х битовую переменную инициализируем значением 0FFFFFFFFh. Далее умножаем на 2 аргумент функции и значение CRC. Если старшие биты окажутся не равны, тогда CRC = CRC XOR 0xEDB88320. И так до последнего бита аргумента. Если аргумент - строковая (или какая-либо другая) последовательность, то операции проводятся над двойными словами. В каноническом варианте в конце цикла требуют инвертировать все биты CRC, но это важно только для совместимости результатов, полученных разными функциями, и никак не сказывается на качестве. "Магическое число" 0xEDB88320 есть стандартный полином, менять который не следует, т.к. это ухудшит качество функции.

Может возникнуть такая ситуация, когда требуемое число элементов хеш-множества будет заметно меньше, чем 2^32. Чтобы избежать лишних расходов, умножают результат сам на себя и берут N старших бит так, чтобы 2^N было по возможности близко к требуемому значению. Смысл этой операции в том, чтобы получить битовую последовательность, чувствительную ко всем битам значения CRC.

Рассмотрим теперь применение хеш-преобразований в криптографии. Одной из задач последней является проверка достоверности пароля. Для этого используют особые хеш-функции. Почему поступают так мы поймем, рассмотрев принцип действия криптосистем. Пусть имеется некоторая криптографическая функция F, расшифровывающая паролем p последовательность Ai в последовательность Aj.

p

F(Ai) ------> Aj

Разумеется, только для одного-единственного p мы получим исходную последовательность Aj, а для всех остальных p - "мусор". Каким способом можно удостовериться в том, что полученная Aj и есть искомая последовательность (при условии что самой последовательности в наличие не имеется)? Можно сохранить фрагмент исходной последовательности и затем сверить его с полученным результатом. Однако это делает возможной атаку по открытому тексту на шифр и, кроме того, очень ненадежно, т.к. совпадение одного фрагмента не обязательно обеспечивает совпадение всей последовательности.

Вот тут-то на помощь и приходит хеш-преобразование. Мы можем вычислить CRC32 для исходного текста. Сравнивая его с CRC32 Aj, мы можем проверить правильность расшифровки. Поскольку количество Aj наверняка больше 2^32, то хеш-функция окажется неинъективной и хеш-значение не даст никакого представления об исходной последовательности.

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

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

Функция f: X -> Y называется односторонней, если f(x) может быть легко вычислена для любого элемента X, тогда для всех элементов Y вычисление такого аргумента x, для которого f(x) = y, не разрешимо полиномиально. Ясно, что CRC32 не является односторонней функцией и,невзирая на распространенное мнение, подлежит реверсированию. Поскольку для каждого значения х CRC32 существует бесконечное множество верных аргументов А, таких что F:CRC32(A) = x, то получение множества подходящих аргументов А' ничем не помогает нахождению в нем искомого элемента. Именно этим и вызвано широкое (и, между прочим, не оправданное) применение CRC32 в криптографии. Односторонность этой функции держится всего лишь на ее неинъективности. Однако, как было показано выше, возможно существование такого интервала I, на котором выбранная хеш-функция является инъективной. Практически во всех популярных криптосистемах не делается никаких попыток проверки принадлежности хешируемых данных к этому интервалу. Более того, для любого конечного перечисленного множества Z возможен как прямой (предвычисленный), так и обратный реверс хеш-функции. CRC32 успешно обращается полиномиально. И, как следствие, часто становится уязвимым местом криптозащит.

Рассмотрим табличное реверсирование. Для этого вычислим CRC32 каждого элемента перечисленного множества Z и сравним ее с исходной. Среди полученных элементов находится по крайней мере один действительный пароль. Множество Z в нашем случае это множество возможных паролей. Нетрудно вычислить, что даже для восьмисимвольных паролей потребуется по крайней мере 4^8 (или 2^16) байт памяти для хеш-сумм и еще больше для хранения образа паролей. Кроме того, потребуется очень большое число итераций.

Поэтому особый интерес представляет обратный полинормальный алгоритм. Заметим сразу - он является полинормальным только для конечных перечисленных множеств. А выглядит для каждого бита обращенного аргумента так:

b == !Hibit(xor (crc32,0xEDB88320)) | Hibit(crc32)

Предположим, что каждый бит аргумента может являться как нулем, так и единицей. Проверим оба результата на принадлежность к множеству Z и отбросим ненужные. Если оба значения принадлежат Z, то запоминаем оба и дальше вычисляем оставшиеся биты для обоих из них - и так до тех пор, пока не будет отброшен последний элемент, как не принадлежащий к множеству Z. Иначе говоря, мы строим бинарное дерево. Очень легко подсчитать необходимое число итераций для нахождения всех возможных паролей. Кроме того, для этой ситуации применимы все эффективные алгоритмы, работающие с двоичными деревьями. Сбалансированное двоичное дерево позволит эффективно реверсировать crc32 даже в случае большого рассеяния элементов Z. Т.е. мы можем быстро проверить, не является ли этот хеш контрольной суммой какого-нибудь словарного слова. Обратное преобразование осуществит эту проверку намного быстрее, чем прямой перебор множества словарных слов. Подробнее операции с двоичными деревьями изложены в книге Майкла Ласло "Вычислительная геометрия и компьютерная графика на C++" (Москва БИНОМ, 1997) и во многих других изданиях. Очень рекомендую ознакомиться с конспектами лекций Манфрейда Броя "ИНФОРМАТИКА" (Диалог-Мифи, 1998).

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

Чтобы научиться строить эффективные алгоритмы, необходимо хотя бы вкратце ознакомиться с теорией формальных языков. Алфавитом является конечное перечисленное множество A. Множество всех конечных последовательностей элементов А называется формальным языком. Языки оперируют словами. Для заданного множества A последовательность элементов x1..xn, принадлежащих A, образует слово длины n, которое записывается как <x1..xn>. Множество всех слов равняется AxAx...xA, что принято записывать как A*. Для каждого слова формального языка существует отображение length:A' --> N, где N - длина слова.

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

Для начала рассмотрим простейший алгоритм генерации паролей, построенный на последовательном множестве. Полный исходный текст находится на file://CD:/SRC/parole.asm, а ниже приводится только ключевой фрагмент. Предложенный алгоритм исключительно прост и быстр; при этом он достаточно гибок и может быть применен для произвольных перечисленных множеств. MOV AH, 9h ; Телетайпный вывод LEA SI, Password ; Начальный пароль MOV DX, SI ; для f.9/Int 21h NextPassword: ; > Генерация следующего пароля <--¬ XOR BX, BX ; С нулевого разряда ¦ CheckOver: ; > Проверка на переполнение <----¬¦ INC Byte Ptr DS:[SI+BX] ; Увеличиваем первый слева символ ¦¦ CMP Byte Ptr DS:[SI+BX],'z' ; Выход за допустимые границы? ¦¦ JB WritePassword ; --{Нет переноса}-----------------+ MOV Byte Ptr [SI+BX], ' ' ; Сбрасываем текущий разряд ¦¦ INC BX ; Перенос на следующий разряд ¦¦ JMP Short CheckOver ; ---------------------------------¦ WritePassword: ; > Вывод пароля на экран ¦ INT 21h ; ^ ¦ JMP SHORT NextPassword ; ---------------------------------- Password DB 8 Dup(' ') ; <- - начальный пароль DB 13, 10, '$' ; <- - конец строки

Тот же алгоритм изящно выражается одной строкой на языке Cи (file: ~~7 //CD:/SRC/PAROLE/Parole.cpp): while ((pasword[index++]++)>'z') pasword[index-1]=' ';

Математический смысл сводится к последовательному перебору всех элементов множества [' ','z'] путем добавления единицы с учетом переноса. Т.е. эту функцию иначе можно назвать INC x, где x - число практически неограниченной разрядности.

Данный алгоритм относится к числу простейших, но для практического применения не годится. Нам необходимо перебирать пароли из произвольного множества символов. Данный пример работает на единственном интервале [x1, xn] линейного множества C.

Вот тут нам и пригодится математика! В самом деле, усложнять алгоритм нет никакой нужды, достаточно лишь отобразить линейное множество C на перечисленное Z.

f

C ---> Z

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

Рассмотрим следующий аспект. Как правило, пароли состоят не из равновероятных символов. Хорошая перебирающая программа должна это учитывать. Может ли это обеспечить предложенный алгоритм? Рассмотрим такую функцию отображения f, которая отображает более одного индекса C на один и тот же элемент Z, задавая тем самым вероятность его появления в генерируемом пароле. Отметим, однако, что при этом возрастут накладные расходы. Пусть два элемента c1 и с2 отображаются на один и тот же элемент z1. Тогда мы получим дважды один и тот же пароль z1. Обычно этим можно пренебречь, т.к. доля дублирующихся паролей несущественна. Но когда она достигнет нескольких процентов от общего числа, то разумно запоминать все встретившиеся пароли и игнорировать повторные. Двоичное дерево обеспечивает приемлемую скорость поиска.

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

В качестве примера приведем усовершенствованный алгоритм с отображением, выполняемым функцией xlat (file://CD:/SRC/Parole01.asm). Repeat: MOV AL, Byte Ptr DS:[SI+BP] ; Взять элемент psw XLAT ; Отобразить его на мнж. alf MOV [DI+BP], AL ; Записать результат INC Byte ptr [SI+BP] ; Следующий CALL WritePassword ; Вывести пароль CMP [SI+BP],CL ; Проверка на переполнение JB Repeat

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

Теперь можно атаковать любую одностороннюю функцию, перебирая все допустимые аргументы и занося их в таблицу, после заполнения которой можно найти все аргументы А, для которых справедливо f:a = key, где key - известный результат функции. Если выбранная функция инъективна и существует не более одного аргумента для заданного кey, то в построении таблицы нет никакой необходимости. Первый совпадающий аргумент и будет искомым.

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

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

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

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

Рассмотрим еще одну область применения хеш-функций. Пусть у нас есть некоторый пароль p, который "зашит" в электронном ключе или введен пользователем. Сравнивать пароль типа if(UserPassword!=MyPassword) abort(); нельзя т.к. это очень просто ломается. Можно расшифровать паролем некоторый фрагмент кода или использовать его как константу. Если для шифрования выбрана криптостойкая схема, пароль можно найти только перебором всех возможных. Контроль правильности ввода можно обеспечить с помощью контрольных сумм. Эта схема очень надежна, но в большинстве приложений реализована с ошибками. Как уже догадался читатель, контрольная сумма снимается с пароля, а не с расшифрованных данных. Но это далеко не самая глупая ошибка! В целях безопасности электронный ключ не должен сообщать пароль в "прямом" виде. С него снимается хеш-сумма, которая и передается для расшифровки. Таким образом, мы имеем два значения разных хеш функций. Первое - для контроля правильности пароля, второе - для расшифровки. Мы получаем систему двух уравнений, решением которого будет пересечение двух множеств реверсированных хеш-преобразований. Это сокращает множество перебираемых паролей вплоть до единственного. Такой подход применим в случае идеальных хеш-функций. Другим решением будет попытка вычислить вторую контрольную сумму из первой.

Распространенной ошибкой является и использование "длинных" crc, сравнимых с длиной пароля. Читатель может сам подсчитать сколько возможных восьмисимвольных паролей имеют одно и то же значение 32-битной контролькой суммы. Однако использование более коротких хеш-сумм увеличивает вероятность того, что неверный пароль будет воспринят как правильный.

Популярная система Novell NetWare 3.x-4.x использовала уязвимый алгоритм. Суть его сводилась к тому, что с пароля снималась хеш-сумма, с которой затем сравнивался вводимый пароль. Эта функция была обратима. Таким образом можно получить МНОЖЕСТВО паролей, которые все воспринимались системой как верные.

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

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

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

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

Ниже приведен упрощенный алгоритм для получения всех допустимых паролей для заданной хеш-суммы посимвольного подсчета суммы всех элементов (см. функцию GetHashe в примере 'GetMe01'). ReHashe(unsigned char hashe) { for (unsigned char a='A';a<'a';a++) { if (!hashe-=a) return; ReHashe(hashe); } }

Данный пример не рекомендуется запускать. Легко доказать, что для любого hashe существует бесконечное множество таких ключей, что f(key) == hashe. Реально разрядность паролей ограничена емкостью стека. При исчерпании последнего возникнет исключительная ситуация и выполнение программы будет аварийно завершено.

Более того, легко доказать, что данный пример может "провалиться" в бесконечный рекурсивный спуск. Пусть hash - нечетное число, а A - четное. Тогда (hash-a) никогда не будет равно нулю.

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

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

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

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

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

Попробуем количественно выразить ожидаемое число паролей для другой популярной функции hashe(A)= a1 xor a2 xor a3...xor an. Для начала докажем, что для любого a, равного x1 xor x2, существуют только две пары x1 и x2, при условии что X [0,1]. Как известно из булевской алгебры операция xor может быть представлена следующей логической матрицей.

xor ¦ 0 1 ----+------------ 0 ¦ 0 1 ¦ 1 ¦ 1 0 ¦

Мы видим, что для каждого значения функции существуют ровно две пары x1, x2, что и требовалось доказать. Поэтому ожидаемое число паролей будет 2^n, где n - разрядность хеш-суммы. Разрядность пароля при этом не выше разрядности хеш-суммы. Т.е. мы провели простое побитовое обращение функции xor. Для восьмибитной хеш-суммы это число равно 0x100. Т.е. значение хеша нам абсолютно ничего не дает, т.к. x1 и x2 могут быть любыми! Однако, x1x2 это 2^16 вариантов, т.е. знание хеш суммы все же позволяет сократить число перебираемых паролей в 0x100 раз. Неплохо; но даже этот результат можно улучшить. Множество допустимых символов пароля, как правило, много меньше 0x100. Пусть ожидаемый пароль состоит только из символов 'A'..'Z'. Тогда для него существует не более 2^5 возможных пар x1 и x2!

Последним мы рассмотрим реверс мультипликационного алгоритма. Пусть A = CX mod N, тогда X = k*N/C+A, где k !=0. Но в пределах [0,N) мультипликационная функция инъективна! Для доказательства этого вспомним, что любое число можно представить как x*n+y единственным образом.

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

Все приведенные хеш-функции не являются односторонними и значительно уменьшают число возможных паролей. Совсем иначе дело обстоит с односторонними функциями, использующимися в криптостойких системах. Казалось бы, если обращение их дело безнадежное, то на этом можно ставить точку и даже не пытаться заниматься заведомо неразрешимой проблемой. Отчасти это верно. Действительно, необратимость большинства функций сомнений не вызывает, но другие, отличные от обращения, пути часто остаются неучтенными и не закрытыми, лишний раз подтверждая слабость большинства реализаций. Часто односторонние функции вычисляются очень быстро. Таким образом, выгоднее сначала последовательно перебрать все возможные пароли, дающие заданный хеш, а затем атаковать шифр заданным множеством паролей. Все "сильные" криптостойкие алгоритмы, как правило, очень медленны. Именно это и препятствует атаке. Так, например. на P-120 скорость перебора UNIX - MD5 не превышает 200 паролей\сек. Тогда как многие хеш-функции до 300.000 и выше! Однонаправленность сама по себе не спасает функцию. Да, реверс не возможен, но прямая атака (т.е. последовательный перебор всех аргументов до совпадения со значением функции) эффективнее прямой атаки на шифр. Впрочем, не стоит обольщаться. Весьма вероятно, что несмотря на грубые ошибки реализации криптосистемы взлом будет лежать за допустимой гранью. Скажем, мы найдем пароль не за биллион, а за десять лет. Так, вероятно, и рассуждают конструкторы защит. А ведь в эти рассуждения вкралась ошибка! На чем держится криптосистема? На малой скорости перебора паролей и большом количестве подходящих под хеш ключей. При условии использования бессмысленного пароля хакеру осталось бы только развести руками. Однако пользователи склонны выбирать осмысленные пароли. Если такой встретится в множестве подходящих под хеш паролей, то в дальнейшей атаке на систему, возможно, уже не будет нужды! Время, затраченное на атаку, в таком случае определяется только скоростью выполнения хеш-преобразования! Предложенный механизм очень близок к словарной атаке и основан на "слабых" паролях. Поиск "осмысленного" пароля представляет выборку всех паролей, включающих в себя по крайней мере трехсимвольный фрагмент словарного слова и отсортированных по убыванию длины подстроки.

Существует по крайней мере еще одно уязвимое место, введенное в некоторые криптосистемы под давлением правительства. Это так называемые "мастер-пароли". Предполагается, что они должны быть известны исключительно спецслужбам и не могут быть найдены иначе как методом перебора. Удивительно, но встречаются словарные мастер-пароли. Так, например, AWARD_SW. Забавно: когда даже пользователи начинают осознавать слабость словарных паролей и администраторы используют нечто вроде t%7Nh6i!!AMPER!!SrF, самое мощное оружие (ведь оно дает доступ ко ВСЕМ паролям) так уязвимо для атаки. Однако криптографы предпочитают использовать вместо мастер-паролей односторонние функции с секретом. Это означает, что в действительности эти функции совсем не односторонние и существует простое обратное решение. Но вот только найти его не более просто, чем атаковать пароль.

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

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

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

Ф. Херберт. "Дюна".

Данная глава является кратким обзором проблемы для неподготовленного читателя. Остальные могут ее смело пропустить.

Неоправданно популярный способ: if (!IsValidUser()) { Message("Invalid user! Aborting..."); Abort; }

транслируется компилятором приблизительно в следующий код: CALL IsValidUser OR AX,AX JZ continue ^^^^^^^^^^^^^ PUSH offset str_invalid_user CALL Message CALL Abort continue: ; нормальное продолжение исполнения программы ...........

и может быть легко взломан хакером изменением всего одного байта.Поменяв выделенную строку на JZ continue на JMP continue, злоумышленник получает работоспособную копию программы. Независимо от алгоритма функции IsValidUser - будь то проверка ключевого диска или ввод серийного номера - совершается безусловный переход на ветку нормального продолжения исполнения программы. На языке Си исправленная программа будет выглядеть так: IsValidUser(); if (!true) { Message("Invalid user! Aborting..."); Abort; }

Т.е. ветка {...} никогда не получит управления! На самом деле все не так просто, поскольку в исполняемом файле нужно еще найти эту инструкцию перехода. К тому же разработчики защиты это всячески пытаются затруднить. Запутывают алгоритм, используют самомодифицирующийся код, применяют недокументированные вызовы операционной системы... Однако такие препятствия хакера не смущают. Технологии противодействия заметно обгоняют эволюцию систем защиты. А с появлением дизассемблера IDA, отладчика Soft-Ice и распаковщика cup386 копание в чужом коде не только превратилось в удовольствие, но и стало доступно широкому кругу лиц. Десять лет назад всего выше перечисленного богатства еще не было, процессор работал только в реальном режиме, и не было никакой возможности уберечь отладчик от разрушающего воздействия со стороны изучаемой программы. Хакеры "старого поколения" работали в основном карандашом, листком бумаги и головой. Зачастую код приходилось дизассемблировать в уме, а недокументированные вызовы изучали, погружаясь в недра операционной системы. Это не только требовало высокого профессионализма, но и огромного количества свободного времени, которое было нечем занять.

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

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

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

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

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

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

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

a xor b xor a = b.

Для этого просто перечислим все возможные значения a и b в следующей табличке:

a\b ¦ 0 ¦ 1 -----+---------------------+--------------------- 0 ¦ 0 xor 0 xor 0 == 0 ¦ 0 xor 1 xor 0 == 1 ¦ ¦ 1 ¦ 1 xor 0 xor 1 == 0 ¦ 1 xor 1 xor 1 == 1 ¦

Заметим, что xor битовая операция. Аргументы a и b могут иметь только два значения: 0,1. Однако никто не запрещает проводить ту же операцию для последовательности битов. Команда процессора XOR word, const на самом деле представляет собой не word xor const, а последовательность операций над каждой парой битов двух переменных.

Теперь обратим внимание, что a xor 0 == a; a xor 1 == !a. Т.е. значащими в маске шифрования являются только единичные биты. Поэтому рекомендуется выбирать такую маску, в которой единичные и нулевые биты равномерно перемешаны. К примеру, 00001111b (0xF) будет плохой маской, т.к. оставляет неизменными четыре старшие бита в каждом символе шифротекста, что позволяет (как будет показано ниже) успешно атаковать шифр. Маска 01010011 полностью уничтожает вероятностное распределение исходных символов в шифротексте, поэтому считается хорошей.

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

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

Рассмотрим простейший пример (file://CD:/SRC/Crypt00.asm). LEA SI,beginCrypt ; Расшифровываем с этого адреса Repeat: ; <-----------------------------¬ XOR Byte ptr [SI],077h ; Расшифровать очередной байт ¦ INC SI ; Переместить указатель ¦ CMP SI,offset endCrypt ; ?Конец достигнут ¦ JNA Repeat ; --{SI<=offset endCrypt}-------

Чтобы полученная программа оказалась работоспособна, необходимо вручную зашифровать фрагмент [offset beginCrypt,offset endCrypt] по xor 0x77. Для этого можно воспользоваться утилитой HIEW, скриптом IDA или написать процедуру, которая это сделает автоматически.

********** рисунок p1 ******** рисунок p2 ******

Теперь сравним два дампа: до и после шифровки. Шифровка исказила исходный дамп до неузнаваемости, исчезла текстовая строка "Hello,World!". Этот прием может использоваться злоумышленником для сокрытия текстовых фрагментов в вирусах, троянских программах и т.д.

Шифровка затруднила и изучение программы. Вот что выдаст дизассемблер в нашем случае. 1AEF:0100 BE0D01 MOV SI,010D 1AEF:0103 803477 XOR BYTE PTR [SI],77 1AEF:0106 46 INC SI 1AEF:0107 81FE2401 CMP SI,0124 1AEF:010B 76F6 JBE 0103 1AEF:010D C3 RET ; < отсюда все зашифровано 1AEF:010E 7ECD JLE 00DD 1AEF:0110 62 DB 62 1AEF:0111 76BA JBE 00CD 1AEF:0113 56 PUSH SI 1AEF:0114 B43F MOV AH,3F 1AEF:0116 121B ADC BL,[BP+DI] 1AEF:0118 1B18 SBB BX,[BX+SI] 1AEF:011A 5B POP BX 1AEF:011B 57 PUSH DI 1AEF:011C 2018 AND [BX+SI],BL 1AEF:011E 051356 ADD AX,5613 1AEF:0121 7A7D JPE 01A0 1AEF:0123 53 PUSH BX

Как разобраться в этой дикой мешанине кода и данных? Что делать и как с этим жить?

Тут на помощь приходит уникальный дизассемблер IDA, поддерживающая встроенный Си-подобный язык. Следующий скрипт (file://CD:/SRC/crypt00. idc) выполнит все автоматически. Чтобы его запустить на выполнение, нужно дать команду : idax -Scrypt00.idc crypt00.com

Рассмотрим, как он работает: for (a=0x10D;a<0x124;a++) // Цикл дешифровки { c=Byte(MK_FP(0x1000,a)); // Взять байт c = c ^ 0x77; // Расшифровать PatchByte(MK_FP(0x1000,a),c); // Записать результат }

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

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

Для начала нам потребуется узнать начало и конец зашифрованного фрагмента. Переключим HIEW в режим дизассемблера и обратим внимание на следующие строки: 00000000: BE0D01 mov si,0010D ; <-- начало зашиф. фрагмента 00000003: 803477 xor b,[si],077 00000006: 46 inc si 00000007: 81FE2401 cmp si,00124 ; <-- конец зашиф. фрагмента 0000000B: 76F6 jbe 000000003

Поскольку в com файле базовое смещение 0x100, то истинным адресом начала будет 0x10D - 0x100 = 0xD. Переведем курсор на полученный адрес, введем следующий скрипт XOR AX,77 и начнем расшифровку. Код на глазах "оживет" и, словно феникс из пепла, одна за одной возникнут "родные" инструкции. Теперь файл можно сохранить на диск и обработать любым дизассемблером вплоть до debug.com! Вот только работать он больше не будет. Наша ошибка в том, что мы не удалили дешифровщик, который "не знает", что код уже расшифрован и портит его. Достаточно заменить маску шифрования 0х77 на 0х0, чтобы устранить это упущение.

Однако неограниченные возможности IDA позволяют проделать ту же самую операцию и на ее встроенном языке! Для этого нам необходимо воспользоваться файловым вводом\выводом. while (1) { c=fgetc(hIn); if (c==-1) break; c=c ^ 0x77; if (fputc(hOut,c),hOut)==-1) break; }

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

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

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

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

**************** рисунок p3 *************************

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

Однако над полученным дампом требуется еще поработать. Если не "убить" дескриптор, как уже было сказано выше, то дескриптор "убьет" код. Использование специализированных средств может ускорить и облегчить эту операцию. Обычно эту задачу возлагают на автоматические распаковщики, которые хорошо с ней справляются. Часто, но не всегда. Один из мощнейших распаковщиков Cup386 имеет ручной режим распаковки. По сути это полноценный интегрированный отладчик необычайной мощности. На данном этапе нас это еще не интересует, поэтому выберем простейшую пошаговую трассировку, для чего запустим cup с ключами /1 crypt00.com /d. Как и в первом случае, дожидаемся выхода из цикла (для этого можно подогнать курсор к строке 0х10D и нажать F4). В этот момент весь код расшифрован и может быть немедленно исполнен. Если бы существовал способ "сфотографировать" это состояние и записать, чтобы в любой момент можнобыло его восстановить, не начиная выполнения программы с самого начала... И cup как раз обеспечивает такой снимок! Он позволяет сохранять значение любых регистров и точку входа и сохранять их в exe файле. Для его создания необходимо выбрать пункт меню "Create executable file" и задать длину сохраняемого фрагмента. В нашем примере она равна 0xD. Никакие регистры нам сохранять не нужно, поэтому выбираем "Just create, no preserving" и нажимаем заветную кнопочку "ОК". Мы получим готовый работоспособный файл, который можно запускать, загружать в отладчик и дизассемблер в рекордно короткие сроки - на все действия вполне достаточно пяти-десяти секунд! И это при том самом впечатляющем результате, который мы получили!

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

Рассмотрим простой пример динамической шифровки на основе все той же операции xor (file://CD:/SRC/Crypt02.asm). Давайте перед выводом каждой процедуры ее сначала расшифровывать, а при выходе - вновь зашифровывать. Таким образом в любой момент будет открыта только небольшая часть кода, а остальная - зашифрована. EXECUTE PROC NEAR CALL CRYPT ; Расшифровываем процедуру PUSH BP ; Сохраняем BP для вложенных вызовов CALL Word ptr [BP+2] ; Запускаем расшифрованную проц. POP BP ; Восстанавливаем BP CALL CRYPT ; Зашифровываем проц. обратно RETN ; --\ ENDP

Чтобы данный пример мог работать, необходимо каким-то образом сообщить процедуре CRYPT начало и длину шифруемого фрагмента. Один из вариантов - перед каждой процедурой расположить блок данных, содержащий все необходимые сведения. CPB macro Begin,End,XorMask ; CPM (CRYPT PREDEF BLOCK) DW offset &End - offset &Begin DW offset &Begin DB XorMask endm

Определим для удобства метод run следующим образом: RUN macro ProcA MOV BP,offset &ProcA-5 CALL EXECUTE endm

Тогда вызов подпрограммы будет выглядеть как run proc1. Рассмотрим дизассемблированный листинг этого вызова: start proc near mov bp, 234h call sub_0_11A retn start endp

Процедура sub_0_11A и есть execute, - чтобы в этом убедиться, достаточно бегло взглянуть на нижеследующий фрагмент: sub_0_11A proc near call sub_0_107 push bp call word ptr [bp+2] pop bp call sub_0_107 retn sub_0_11A endp

Интересующая нас процедура находится по адресу [BP+2] == 0x234 + 0x2 = 0x236; [0x236] == 0x239. Однако дизассемблер не может восстановить код процедуры, т.к. он еще зашифрован. seg000:0236 dw 239h seg000:0239 db 0C9h ; г seg000:023A db 21h ; ! seg000:023B db 75h ; u seg000:023C db 0DAh ; - seg000:023D db 7Ch ; | seg000:023E db 0B7h ; ¬ seg000:023F db 3 ;

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

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

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

Запустим несложный скрипт на выполнение (file://CD:/SRC/crypt02.idc). static try() { auto a,p,l,c,mask; p = ScreenEA(); mask = Byte(p+4); l = Word (p); for (a=0;a<l;a++) { c = Byte(p+a+5); c = c ^ mask; PatchByte(p+a+5,c); } AnalyseArea(p+5,-1); }

Наведем курсор на начало CPB блока и вызовем функцию try. Это еще одна особенность IDA - с помощью своих функций мы безгранично можем расширять ее возможности. Функция try теперь будет доступна из любого выполняющегося скрипта. Или с консоли. Нажмем Shift-F2 и введем try(). Было бы утомительно проделывать это каждый раз, не поддерживай IDA макросы. Можно задать любую удобную для нас горячую клавишу для вызова этой функции. Например, Alt-C.

Однако этот метод грешит тем, что придется проанализировать все функции вручную, а это потребует больших усилий. В самом деле, неудобно работать "ручками" там, где можно использовать автономный скрипт. Попытаемся использовать тот факт, что процедура EXECUTE принимает входной аргумент в регистре BP, представляющем собой смещение CPB. Можно предположить, что использовалась конструкция MOV BP, offset Proc (LEA BP, Proc), и таким образом, найдя все вхождения MOV BP,xxxx\CALL EXECUTE мы можем автоматически расшифровать весь файл! В большинстве случаев этот способ безотказно срабатывает. Действительно, поскольку процедура расшифровки одна, то простым контекстным поиском мы можем обнаружить все ссылки на нее. Неужели все так просто? И все наши усилия по созданию динамически шифрующейся программы пропали даром? Другими словами: можно ли от всего этого защититься? Разумеется! Даже не потребуется менять алгоритм. Нужно, чтобы адрес процедуры был задан не в форме константы, а произвольным образом вычислялся. В этом случае контекстный поиск окажется бессильным. В нашем примере сингатурный поиск поможет расшифровать только часть процедур, большую часть которых составляют переходники к операционной системе такие как WriteString, ReadString и т.д,, не содержащие в себе ни йоты интересного кода. Остальные останутся зашифрованными. Приглядимся повнимательнее к тому, как работает функция main: LEA SI,scenario Main_repeat: LODSW OR AX,AX JZ main_exit XCHG BP,AX SUB BP,5 CALL EXECUTE JMP SHORT Main_repeat

Фактически это встроенный интерпретатор, который последовательно исполняет инструкции из списка scenario. Но содержит ли scenario полный перечень процедур? Быть может, вложенные процедуры также содержат свой локальный интерпретатор?

Но существует простое и элегантное решение, которое зачастую упускают разработчики защит. В большинстве случаев все существующие процедуры расположены вплотную одна за другой. За концом одной процедуры - начало следующей. Эта банальность предельно упрощает расшифровку. Очевидным решением будет помещение между соседними процедурами случайного числа незначащих байт. К сожалению, ни один из известных мне ассемблеров или компиляторов не поддерживал такой особенности. А вручную это делать очень утомительно? Но, допустим, автор защиты пошел на такие затраты времени и все соседние процедуры разнесены от нуля до N байт. Остановит ли это хакеров? Давайте еще раз внимательно посмотрим на CPB структуру: CPB macro Begin,End,XorMask DW offset &End - offset &Begin DW offset &Begin DB XorMask endm

Обнаруживаются две закономерности. Поле [2] численно равно смещению заголовка CPB и offset &End - offset &Begin, как правило, не больше 0xFFFF. Этой избыточной информации вполне достаточно для поиска блоков CPB и расшифровки всех существующих процедур простейшим автоматическим скриптом.

Сражение технологий взлома и защиты не прекращается ни на минуту. Каждый день рождаются все новые хитроумные скрывающие механизмы, чтобы спустя короткое время исчезнуть, освобождая место для новых. Приведенный выше пример оказался нестойким из-за ошибки в программной реализации. Существуют ли программы без ошибок? Хакеры отвечают: "Программ без ошибок не бывает. Бывает? Плохо искали". Неизвестно, чего больше в этой шутке - юмора или правды.

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

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

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

К счастью, MS-DOS никак не препятствует модификации программой своего собственного кода, и данная процедура будет успешно работать: void Crypt(char *Point, char *EndPoint) { while (Point<=EndPoint) Point[0]=(Point++)[0] ^ 0x77; }

Однако при этом будут наложены жесткие ограничения - программа должна компилироваться для TINY модели памяти. Будет очень жаль, если ваш любимый компилятор на это не рассчитан.

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

Модель SMALL размещает код и переменные в двух независимых сегментах. Используются только близкие указатели, адресуемые через DS. Сам DS строго указывает на сегмент данных.

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

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

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

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

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

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

Как атаковать шифр
При атаке на шифр считается, что криптоалгоритм известен с точностью до реализации и требуется найти пароль. В качестве примера рассмотрим программу crackme0.com (file://CD:/SRC/Crackme.com). Алгоритм расшифровки ничем не защищен и его легко можно изучить. Однако это нам не дает никакой информации об исходном пароле. CalcCRC: ; <--------------------------------¬ LODSB ; Читаем байт введенного пароля ¦ ADD AH,AL ; Суммируем ¦ LOOP CalcCRC ; --{CX}----------------------------

Эта процедура вычисляет CRC с введенного пароля. CRC очень простой и с плохим рассеиванием. Множество паролей будет иметь одно и то же CRC, поэтому нет никакой возможности предсказать на основе всего восьмибитного числа исходный пароль. LEA SI,Crypt ; На начало зашифрованных данных Decrypt: ; <--------------------------------¬ XOR [SI],AH ; Расшифровать байт ¦ INC SI ; Следующий байт ¦ CMP SI,offset Buffer; ?Мы уже все расшифровали ¦ JB Decrypt ; --{SI<offset Buffer---------------

Шифротекст расшифровывается значением контрольной суммы от пароля! Причем всего может существовать 0x100 (256) вариантов расшифровки! Нетрудно было бы перебрать их всех и найти исходный. Даже если бы использовалось слово (0x1000), то вариантов было бы все равно немного!

Между прочим, это довольно распространенный примем в некоммерческих программах. Удивительно, как это не учитывают авторы защит! Если перебирать не пароли, а ключи (т.е. сами хеш-значения), то можно достичь скорости порядка нескольких сотен тысяч ключей в секунду. Таким образом не спасет даже использование двойного слова (0x100000000). Оценим какое наибольшее время необходимо для перебора тридцатидвухбитного ключа. Исходя из средней скорости миллион ключей в секунду, мы найдем результат не более чем за 4295 секунд т.е. приблизительно за 71 минуту. Практически за час с небольшим, не так ли? Все дело в высокой скорости перебора ключей, обусловленной простотой алгоритма.

Автоматический перебор ключей опирается на возможность контроля идентичности расшифрованного и оригинального текста. Программа, очевидно, должна была контролировать правильность ввода пользователя и подсчитывать CRC либо пароля, либо расшифрованного текста. Рассмотрим следующий код: CmpCRC: ; <--------------------------------¬ DEC SI ; Следующий байт ¦ ADD CL,[SI] ; Суммировать ¦ CMP SI,offset Crypt ; ?Конец ¦ JNB CmpCRC ; --{SI>offset Crypt}--------------- CMP CL,0C3h ; ?Значение CRC верно JZ Crypt ; --CRC OK--> ; СRC FAIL

Ясно, что проверяется контрольная сумма исходного текста. Мы можем исследовать хеш-алгоритм и хеш-сумму исходного текста. В нашем случае она равна 0xC3.

Но, сколько существует неправильных вариантов расшифровки, для которых их контрольные суммы тем не менее совпадают? Очевидно, что больше одного! И с ростом числа вариантов расшифровки количество "ложных" ключей стремительно растет! Так, уже при 32-битных ключах основную проблему будет представлять не поиск подходящий ключей, а выборка единственного верного исходного текста среди расшифрованных вариантов! При этом у нас никаких достаточно строгих критериев, позволяющих автоматически отличить ложные варианты. Разработчик защиты добился-таки своего! Вероятность, что пользователь введет неправильный, но подходящий пароль, достаточно мала, поэтому ей можно пренебречь. Действительно, предположим, что хотя бы 1% паролей будет давать ложные срабатывания, тогда для 0x10000 (65536) ключей это составит 655 вариантов, а для 0x100000000 уже 42.949.672! Заметим, что с точки зрения криптографии все эти варианты равноценны!

Разумеется, мы в любом случае имеем хотя бы косвенное представление об исходном тексте. Но как его использовать? Можно по типу данных предугадать вероятность того или иного символа, проверить на совпадение со словарем, поискать некоторые закономерности... Но как же все это будет медленно работать! И в любом случае (особенно на коротких фрагментах) нет никакой гарантии, что даже самый надежный алгоритм всегда распознает исходный текст.

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

X xor Key = Y, Y xor Key = X, но X xor Y = Key!

В нашем примере с восьмибитным ключом достаточно достоверно знать всего лишь один байт открытого текста, чтобы получить Key, который можно применить ко всему шифротексту в целом. Но можем ли мы узнать какой-то байт чужого текста, и к тому же достоверно? Оказывается, можем, да еще как! Конечно, не достоверно, но с определенной степенью вероятности мы можем ПРЕДПОЛОЖИТЬ его наличие!

Весьма вероятно, что в приведенном шифротексте встретится инструкция INT 0x21. Ее двухбайтовый опкод 0x21CD (мы ведь помним про обратный порядок байт в слове!) превышает длину ключа, а значит, делает его на этом промежутке неслучайным. В самом деле, в уравнении X xor Y = key мы не знаем ни X, ни key, при выбранном Y они могут быть любыми. Если #Y >#Key, то мы ~~8 получим X1 xor Y1 == X2 xor Y2 == key!

Может быть это объяснение покажется туманным, поэтому приведем практический пример. С помощью утилиты hiew попробуем расшифровать секретный фрагмент, применяя операцию XOR 0x21CD.

************** рисунок 4 ************************ 00000030: C3 74¦08 B4-09 BA BE 01-CD 21 C3 B0-B5 E4 BB A9 00000040: 00 20_20 2E-0C E7 51 8C-72 9E 76 82-73 89 21 A2 00000050: 4A CC^01 E7-25 7D 01 CD-21 CD 00 00-00 00 00 00 00000060: 00 00¦00 00-00 00 00 00-00 00 00 00-00 00 00 00

Обратим внимание на последовательность 0x2020. Весьма верятно что в оригинальном тексте здесь находилось 0x21CD, зашифрованное ключом 0x20! Следует учитывать, что данный метод допускает ложные срабатывания. Поэтому предпочтительнее использовать по возможности более длинные последовательности. В этом случае мы получили только один возможный ключ, а что мы стали бы делать, если бы их оказалось несколько? Если мы знаем частоту появления выбранной последовательности (а именно такие и стоит выбирать), то мы можем определить истинный ключ простым сравнением! Однако допустим, что два или более ключей имеют очень близкую к ожидаемой вероятность появления. Как быть тогда? В этом случае необходимо поискать другую последовательность, ожидаемую в шифротексте. В нашем примере это может быть 0xA0D - перенос строки. Весьма вероятно, что пример выводит какую-то строку, которая завершается возвратом каретки. Предоставляю читателю провести этот эксперимент самостоятельно, а самзамечу, что на деле редкая программа обходится без вывода текстовых сообщений. Но ведь в любом языке не так уж много слов, а тем более распространенных! Это очень уязвимое место подобных систем. Не потребуется даже утомительного перебора. Поищем такие строки, как (c), Copyright, OK, Cancel, Yes, No... Хорошо действует двойной пробел, пробел+ запятая и т.д.

Кроме того, не забываем, что нам известен CRC исходного текста, - следовательно, можно быстро определить, какой ключ из нескольких - правильный. Я не буду приводить расчетов вероятности появления ложных паролей - скажу только, что при использовании последовательностей из двух и более символов она достаточно невелика. Таким образом криптостойкость данного шифра при атаке по открытому тексту (даже если неизвестно точное расположение последнего) равна нулю!

В данном примере использовался ключ 0x20. Внешне этот ключ абсолютно ничем не примечателен, но взгляните на зашифрованный фрагмент:

************** рисунок 5 ************************

И это ЗАШИФРОВАННЫЙ текст? Как же такое могло произойти? Магическое свойство xor 0x20 - переводить английский текст в противоположный регистр - сыграло с автором защиты очень злую шутку. У каждого алгоритма есть некоторые число СЛАБЫХ паролей, которые в разной степени снижают его криптостойкость. Об этом мы говорили выше. А теперь, если вспомнить, что 0x20 - 100000b, нетрудно убедиться, что такой ключ меняет всего один бит на каждый байт! Т.е. шифротекст будет мало чем отличаться от исходного. Хорошие криптосистемы делают проверку на слабые ключи, плохие - нет. Учитывая, что число слабых ключей у иных шифров не так мало, как кажется на первый взгляд, с вероятностью использования такого ключа стоит считаться.

Выше мы отмечали, что использование 32-битного ключа дает 0x100000000 вариантов и потребует около 71 минуты перебора. А если длина ключа все 64 бита? 0x10000000000000000 вариантов потребует 28014 секунд или почти восемь часов перебора (и выдаст еще больше ложных срабатываний). Если бы мы могли достоверно знать хотя бы одну шестнадцатибайтовую последовательность, присутствующую в исходном тексте...

Крайне маловероятно, что мы располагаем такой информацией! Однако на самом деле положение вовсе не безнадежно, и у нас по-прежнему хорошие шансы найти даже такой длинный ключ. Сначала заметим, что минимально необходимый фрагмент открытого текста в действительности равен длине ключа плюс единица. Естественно, это увеличит число ложных срабатываний, но не так сильно, как кажется на первый взгляд. Представим для начала, что нам известен лишь фрагмент A. Какова вероятность того, что он совпадет с A'?

---------------¬ L--------------- , TAT-------------TAT----- - - - +---------------+------- - - - ¦ ¦ ¦<---- L ------>¦

Давайте представим себе последовательность AA. Естественно, что она будет "вмещать" в себя #A^2 элементов. У скольких элементов левая "половина" равна правой? Обозначим левую часть как L, а правую как R. Легко видеть что для каждого L существует только один равный ему R. Число совпадающих вариантов равно множеству элементов А. Тогда вероятность совпадения произвольных А и А' равна #A/#A^2 == 1/#A, где # - число элементов множества.

Однако нас интересуют только те совпавшие числа, которые находятся строго на расстоянии L друг от друга, где L как видим, равна длине ключа. Задачу подсчета данной вероятности я оставляю любопытным читателям, отмечу только, что она на несколько порядков ниже общего числа ключей, которые нам пришлось бы перебирать в противном случае. Даже если #A равна одному биту (!), у нас неплохие шансы. И гораздо более высокие, чем показал расчет любознательных читателей. Точнее говоря они МОГУТ СТАТЬ несравненно выше. Используемая нами математическая модель исходила из предпосылки равновероятности всех символов. Это очень упрощает расчеты, но не соответствует действительности. Как правило, нам известно хотя бы приблизительное распределение вероятности встречаемых символов. Это поможет отбросить часть вариантов как заведомо ложные или, (что более правильно) начать перебор с наиболее вероятных ключей, а затем переходить к менее вероятным.

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

Допустим, автор защиты это учел и использовал неизвестный доселе компилятор или полностью реализовал весь код на ассемблере и отважился выбрать ну очень длинный ключ - не будем даже уточнять, насколько длинный. Восторжествует ли он на этот раз? Увы (или ура - в зависимости от ориентации читателя)! Злоумышленник применит масочную атаку! Суть ее сводится к следующему: пусть нам не известно сколь-нибудь длинной строки из зашифрованного текста, но мы наверняка знаем много коротких! И с некоторой достоверностью - расстояние L между ними.

Алгоритм атаки следующий: пусть s0 одна из существующих в шифротексте последовательностей. Применим атаку по открытому тексту и в результате получим ОГРОМНОЕ множество подходящих, но ложных ключей. То, что ни один из них не верен это ясно из того, что длина каждого равна #s0. По условию s0 короче пароля; следовательно, ни один ключ не является законченным паролем. Однако, ясно, что настоящий пароль включает в себя некоторые элементы полученного множества. Возьмем другую известную последовательность s1 и повторим аналогичную операцию. Теперь выберем общие для f(s0) и для f(s1) элементы. Вероятнее всего, что именно из них и составлен пароль. С каждой итерацией число символов, общее для всех последовательностей стремительно уменьшается, а вместе с ним и число вероятных паролей. Когда все последовательности исчерпаны, выберем только те, которые разнесены в границах предполагаемого расстояния (если такая информация имеется). Существует множество вариантов получения пароля из заданного множества фрагментов, однако нет нужды перебирать их все. Я доставлю читателю удовольствие самому решить несложную задачку уменьшения числа возможных вариантов. (Привычка получать все в готовом виде очень вредна, а для хакера - в корне неприемлема! В моем понимании хакер - это человек, который в любой ситуации привык рассчитывать только на себя. Готовые технологии и знания - непозволительная роскошь.

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

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

В заключении мы рассмотрим атаку по открытому тексту на зашифрованный архиватором arj файл. Несмотря на высокую скорость перебора существующих взломщиков требуется немало времени на поиски паролей более чем из восьми символов. В то же время, если есть хотя бы один незашифрованный файл из архива, задача решается очень просто. Часто в архивы попадают общедоступные файлы (pgp - подписи, рекламные проспекты, стандартные файлы типа dos4gw или *.DLL). Содержимое некоторых файлов иногда можно попытаться угадать. В первую очередь это относится к bat файлам. Самое главное то, что длина bat файлов обычно очень мала, а содержимое состоит из ограниченного набора словарных последовательностей.

Но перейдем собственно к реализации атаки. Для нее нам потребуется знать используемый криптоалгоритм и формат файлов архиватора. Этот формат добросовестно описывается в прилагаемой документации. А вот криптоалгоритм автором не разглашается. Что само по себе уже подозрительно. Как учит рыночная политика - если производитель о чем-то пытается умолчать, то можно не сомневаться в качестве (точнее, его отсутствии). Не должна стойкость криптосистемы зависеть от знания ее алгоритма, никак не должна. Правило, сформулированное голландцем Керкхоффом, гласит: стойкость шифра должна определяться только секретностью ключа. Роберт Янг к этому не прислушался и приложил гораздо больше услилий к сокрытию криптоалгоритма, чем к качеству реализации. В техническом описании он не раскрывает значение байта 0xB заголовка, оставляя его "зарезервированным". Между тем он активно используется при шифровке паролем!

Есть два пути для отождествления криптоалгоритма: анализ большого числа открытых и зашифрованных текстов или дизассемблирование и анализ кода программы. Я всегда выбираю последний. Это не всегда самый быстрый, но гарантированный вариант. Дизассемблирование - довольно трудоемкая работа, которая может растянуться на долгие недели, если нет соответствующих навыков. Подробнее об этом я собираюсь рассказать в следующей моей книге "Образ мышления - IDA". В рамках данной главы можно отметить, что не обязательно анализировать весь arj.exe, воспользуемся самораспакующимся архивом, который не в пример короче. Дизассемблирование последнего быстро покажет, что в arj используется система шифрования Вермана, которая сводится к следующему: для пароля S1S2S3 получим бесконечную последовательность s1s2s3s1s2s3s1s2s3, и тогда NN-ый символ открытого текста преобразуется в шифротекст простым XOR [Sn],[Nn]+ MAG_VALUЕ.

Что такое MAG_VALUE? А это и есть тот "зарезервированный" символ, назначение которого Роберт не раскрыл в документации.

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

Любопытно, что пароли abc и abcabc абсолютно идентичны! (И, кстати, об этом умалчивается в документации)! Отсюда вытекает, что выбор "неудачного" пароля может позволить "прямую" атаку на шифр!

Ниже приведен фрагмент атакующей программы, которая открывает используемый пароль. Константа MAX_AVIALABLE_PASSWORD_SIZE - это максимально возможная длина предполагаемого пароля. Как легко видеть, крипостойкость не зависит от длины. Кроме того используемый алгоритм настолько быстр, что практически не требует времени и исполняется МГНОВЕННО! for (int a=0;a<MAX_AVIALABLE_PASSWORD_SIZE;a++) { NormalByte = (unsigned char) pNormalBuffer[a]; ProtectByte = (unsigned char) pProtectBuffer[a]; ResByte=(unsigned char) (NormalByte ^ ProtectByte) - MagValue; st=st+(char) ResByte; }

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

Вместе с Янгом этот бой проиграли и все пользователи его криптосистемы. Зашифрованные данные оказались уязвимы без ведома их обладателей. Сколько архивов было (или могло быть) вскрыто за это время?

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

Логическая (программная) защита

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

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

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

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

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

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

Рассмотрим простейший пример (file://CD:/SRC/BREAK00). Это программа под Win32, использующая библиотеку MFC. Под MS-DOS используются в целом те же принципы защиты, но с небольшими расхождениями. Иногда я на них буду обращать внимание, иногда нет. Не то чтобы взлом досовских приложений перестал быть актуальным, но местами различия не настолько значительны, чтобы стоило отвлекать и запутывать читателя.

Запустим break00.exe на выполнение. Программа просит ввести пароль. Чтобы сравнить его с введенным, оригинальный пароль должен как-то храниться в программе. Очевидным способом является простое посимвольное сравнение:

if ((s0=ch)!="KPNC") cout << endl << "Password fail" <<endl;

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

Можно было бы просто просматривать текст программы в любом hex-вьювере, но это утомительно, особенно для больших файлов. Поэтому воспользуемся утилитой - "текстовым фильтром", которая анализирует файл и записывает все встретившиеся текстовые строки. Рекомендую воспользоваться собственной filter.com.

Рассмотрим полученный листинг. ------------> смещение в файле ¦ -------> текстовая строка ¦ ¦ 24FA:Winit 250C:MSVCP 3020:Password OK 3030:Password fail 3040:KPNC ^^^^^^^^^ 3048:Enter password 305C:CrackMe ^^^^^^^^^^^^ 3067: Try to path code of found valid password 3094:Fatal Error 30A0: MFC initialization failed

Обратим внимание на строку, находящуюся по адресу 0x3040. Не правда ли, она могла бы быть паролем? Чаще всего (но необязательно) искомая строка располагается близко к тексту "введите пароль". Ниже мы видим еще одного "кандидата".

Давайте проверим, подойдет ли хотя бы один из них? CrackMe01 : Try to path code of found valid password Enter password : KPNC Password OK! Press any key to continue

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

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

Первый шаг. От EXE до CRK
Бесспорно, среди существующих на сегодняшний день дизассемблеров лучшим является IDA. Особенно идеально она подходит для взлома и изучения защищенных программ. Очевидно, что Break00 не является таковой в полном смысле этого слова. В нем нет ни шифрованного кода, ни "ловушек" для дизассемблеров. SOURCER или любой другой справился бы с этой задачей не хуже. Поэтому окончательный выбор я оставляю за читателем.

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

Достаточно изучить и понять алгоритм защитного механизма, ответственного за сверку паролей. Нужно только найти этот механизм. Можно ли это сделать как-то иначе, чем полным анализом всей программы? Разумеется, можно! Для этого нужно найти ссылки на строки "неверный пароль", "пароль ОК", "введите пароль". Ожидается, что защитный механизм будет где-то поблизости. Сами строки находятся в сегменте данных. (В старых программах под DOS это правило часто не соблюдалось. В частности, компилятор Turbo Паскаля любил располагать константы непосредственно в кодовом сегменте.)

Для перехода в сегмент данных в IDA нужно в меню "View" выбрать "Segment Windows" и среди перечисленных в появившемся окне отыскать сегмент типа "DATA". Искомые строки бросаются в глаза даже при беглом просмотре. Перевести их в удобочитаемый вид можно перемещением курсора на начало строки и нажатием "A" (от слова ASCII).

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

Но IDA позволяет найти перекрестные ссылки и самостоятельно. Для этого нужно нажать Alt-T и ввести адрес интересующей нас строки. Попробуем найти код, который выводит 'Enter password'. Нажимаем Alt-T и вводим,'403048' (без кавычек) - адрес, по которому расположена эта строка. Теперь IDA обычным контекстным поиском будет искать все идентичные вхождения по всему дизассемблированному тексту.

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

Не то чтобы я был против такого подхода, но боюсь, что новичков он может только запутать. Существуют гораздо более красивые и быстрые методы, которые будут рассмотрены ниже. Но идеология их схожа - они всегда пытаются "поймать" защитный код на основе известных или предсказуемых данных. Если у нас есть строка, которая выводится, значит есть тот, кто ее выводит. Естественно предпологать, что он находится где-то в окрестностях сравнивающего механизма или непосредственно к нему относится.

Цель автора защиты - построить код так, чтобы не оставить хакеру никакой избыточной информации о работе последнего, по которой его можно вычислить. Данный пример ничего подобного не содержит, и IDA нам показывает следующий фрагмент: 004010C0 mov esi, ds:??6std!!AMPER!!!!AMPER!!YAAAV? $basic_ostream!!AMPER!!... ........ ... ... 004010E7 mov eax, ds:?cout!!AMPER!!std!!AMPER!!! !AMPER!!3V?$basic_ostream!!AMPER!!... 004010EC push 403048h ^^^^^^^^^^^^^^^ 004010F1 push eax 004010F2 call esi

Но чем является в данном случае 403048h - смещением или константой? Это можно узнать из прототипа функции basic_ostream<E, T> *tie(basic_ostream<E,T> *str). Пусть читателя не смущает небольшая разница в написании имен: причина в двух словах объяснена быть не может, и мне остается лишь отослать интересующихся этим вопросом к MSDN, который не только по-прежнему доступен в Он-Лайне, но и распространяется вместе с MS VC. Без MSDN и глубоких знаний Win32 говорить о хаке под Windows просто неэтично! Это, конечно, не означает, что каждый кракер обладает глубокими знаниями платформы, на которой он ломает. Большинство защит вскрываются стандартными приемами, которые вовсе не требуют понимания "как это работает". Мой тезка (широко известный среди спектрумистов уже едва ли не десяток лет) однажды сказал "Умение снимать защиту, еще не означает умения ее ставить". Это типично для кракера, которому, судя по всему, ничто не мешает ломать и крушить. Хакер же не ставит целью взлом (т.е. способ любой ценой заставить программу работать), а интересуется именно МЕХАНИЗМОМ: "как оно работает". Взлом для него вторичен.

Однако мы отвлеклись. Вернемся к прототипу функции basic_ostream. Компилятор языка Cи заносит в стек все аргументы справа налево. Поэтому 0x403048 будет указателем на строку (*str), которую затем функция и выводит. Таким образом, мы находимся в непосредственной близости от защитного механизма. Сделаем еще один шаг. 004010D8 mov edi, ds:??5std!!AMPER!!!!AMPER!!YAAAV? $basic_istream!!AMPER!!DU? ........ ... ... 004010F4 mov edx, ds:?cin!!AMPER!!std!!AMPER!!!!AMPER! !3V?$basic_istream!!AMPER!!DU? 004010FA lea ecx, [esp+1Ch] 004010FE push ecx 004010FF push edx 00401100 call edi

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

Я умышленно взял сложный для понимания пример. Вероятно, вы думаете, что буфер расположен по адресу [esp+1Ch]? И чтобы сравнить введенную строку с эталонной, необходимо передать указатель на [esp+1Ch]? Как бы не так! Коварный оптимизирующий компилятор использовал не регистр EBP, значение которого неизменно на протяжении всей процедуры, а указатель на верхушку стека esp! Занесение параметров в стек приводит к изменению esp, и нет никакой возможности предугадать его значение в произвольной точке кода. Приходиться отслеживать все операции со стеком и вычислять новое значение esp в уме (или с помощью хитрых скриптов для IDA, о которых мы поговорим в следующий раз).

Рассмотрим нижеследующий фрагмент. После того как [esp+1Ch] указал на буфер, содержащий введенную строку, в стек были переданы два двойных слова (см. выше). Заметим, что стек растет "снизу вверх", т.е. от старших адресов к младшим.

Команда add очищает стек от локальных параметров отработанной функции. Тогда новое значение esp равно -2*4+0x10 = +8; 0х1С - 0х8 = 0x14. Следовательно, теперь уже [esp+0x14] указывает на наш буфер! 00401102 add esp, 10h 00401105 lea eax, [esp+14h] ^^^^^^^^^^^^^^^^^^^^^^ 00401109 lea ecx, [esp+10h] 0040110D push eax 0040110E call j_??4CString!!AMPER!!!!AMPER!!QAEABV0!!AMPER! !PBD!!AMPER!!Z

Обратим внимание на подчеркнутую строку. Насколько же с первого взгляда неочевидно, куда указывает указатель eax! Попутно замечу, что даже сегодня не каждый компилятор способен генерировать такой код, ценность которого заключается практически в экономии всего одного регистра ebp. Но читатель должен быть готов, что со временем этому "научатся" все компиляторы и все операции с локальными переменными придется отслеживать указанным выше образом. К счастью, дизассемблеры не отстают в этом от компиляторов, и уже IDA 3.8b прекрасно справляется с этой задачей. Можно даже не задумываться о том, что, собственно, происходит у нее "внутри". В том-то и беда, что новые технологии могут освобождать от глубокого понимания предмета. Между тем такой подход делает человека зависимым от окружающих его инструментов. Без них он становится беззащитен перед дикой природой.

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

Поэтому, стремясь показать, как происходит обращение к локальным переменным, я начал объяснение с версии 3.6, не "умеющей" автоматически отслеживать изменение esp. Разумеется, это не повод для обязательного ее использования. Впрочем, нет оснований и отказываться от нее. Лично мне старая, проверенная и устойчивая версия ближе, чем неустойчивая и непривычная новая 3.8b. В то же время встроенный язык позволяет неограниченно расширять возможности дизассемблера и включать в него все новые технологии и достижения на свой вкус.

Использование последней версии IDA выгодно еще и тем, что позволяет получить символьные имена всех используемых функций в популярных библиотеках по их сигнатурам. Так, в вышеприведенном примере трудно понять, что же делает функция в строке 0х040110E. IDA 3.8 уверенно распознает эту функцию как CString::operator=(char const *). Следовательно, введенный пользователем пароль заносится в переменную типа CString, хранящуюся по адресу [esp+ 10h]. Само собой теперь необходимо ожидать процедуру сравнения. Более того, мы может предположить, что она будет одним из методов CString! 004010DE mov ebx, ds:_mbscmp ........ ... 0040110E call j_??4CString!!AMPER!!!!AMPER!!QAEABV0!!AMPER!!PBD!!AMPER!!Z 00401113 mov eax, [eax] 00401115 push 403040h 0040111A push eax 0040111B call ebx 0040111D add esp, 8 00401120 test eax, eax 00401122 mov eax, ds:?cout!!AMPER!!std!!AMPER!!!!AMPER!!3V ?$basic_ostream!!AMPER!!DU?.. 00401127 push eax 00401128 jz short loc_401144

Функция _mbscmp, как следует из ее названия, сравнивает строки и имеет очевидный прототип int _mbscmp( const char *string1, const char *string2 ). В строке 0x0401113 мы получим указатель на новую переменную CString, но что же тогда представляет собой число 0x403040? Очевидно, что это указатель на эталонную строку. Посмотрим, что находится по указанному смещению: 00403040 aKpnc db 'KPNC',0

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

Для этого в очередной раз обратимся к MSDN, где узнаем, что функция _mbscmp возвращает false (ноль), если строки идентичны, и true в противном случае. Если бы над кодом не поработал оптимизатор, то можно было бы ожидать непосредственно после CALL-а примерно следующую конструкцию: CALL xxxx TEST EAX,EAX JZ xxxx [JNZ xxx]

Однако оптимизатор расположил команды в несколько ином порядке - так, чтобы они выполнялись за меньшее число тактов. К сожалению, в ущерб читабельности. Условный переход находится на четыре команды ниже в строке 0х0401128. TEST EAX,EAX устанавливает флаг Zero в том случае, когда EAX == 0. Следовательно, переход JZ выполняется только тогда, когда сравниваемые строки идентичны. Думаю, что читатель сможет с удовольствием удостовериться, что код в ветке loc_401144 выводит "Password OK" и в законченном (а не демонстрационном) приложении продолжает нормальное выполнение программы.

Что будет, если мы заменим условный переход JZ на БЕЗУСЛОВНЫЙ JMP? Тогда независимо от результатов сравнения (а следовательно, и введенной строки) программа будет воспринимать любой пароль как правильный!

IDA 3.6 не может записывать отпаченный PE файл, поэтому нам придется этим заняться самостоятельно. Для этого нужно найти в файле тот же фрагмент, что мы видим в дизассемблере. HIEW позволяет искать непосредственно ассемблерные инструкции, облегчая взломщикам жизнь, но мы пойдем другим путем. Гораздо надежнее искать hex-последовательность, которую включает интересующий нас фрагмент. Для этого переключим IDA в режим показа опкода инструкций.

Строго говоря, теперь нам предстоит выбрать сигнатуру, т.е. по возможности короткую, но уникальную последовательность, которая повторяется в файле только один раз. Разумеется последовательности jz xxx (0x74 0x1A) окажется скорее всего недостаточно, поскольку ожидается, что она может встретиться более чем в одном контексте. Практика показывает, что обычно требуется последовательность не менее чем из трех инструкций. Конечно, чем короче файл, тем меньше вероятности ложных срабатываний. Давайте ограничимся всего двумя командами - push eax\jz xxx. Запишем на бумажку (или запомним) их опкод - 50 74 1A.

Теперь запускаем hiew, переводим его в hex-режим просмотра и пытаемся найти эту последовательность. Если все сделано правильно, то мы обнаружим ее по адресу 0x0401127. Удостоверимся, что это действительно единственное вхождение и больше совпадений нет. Если же в файле присутствует более одной строки, то возвращаемся в IDA и записываем более длинную последовательность. Впрочем иногда (чем больше опыта, тем чаще) можно определить, какие варианты оказались ложными, "на глаз" сравнив код в этом месте с тем фрагментом, что мы видели в дизассемблере.

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

Так или иначе, пришло время немного "похулиганить" и изменить ту заветную пару байт, которая мешает нелегальным пользователям (а так же всем легальным, но забывшим пароль) получить доступ к программе. Как уже было показано выше, изменение условного перехода на безусловный приведет к тому, что программа будет воспринимать любой пароль как правильный. Опкод команды JMP SHORT - 0xEB. Узнать это можно из руководства Intel по микропроцессорам 80x86. Впрочем, hiew позволяет обойтись и без этого. Достаточно перейти в режим ассемблера и ввести jmps с тем же адресом перехода, что и JZ. Сохраняем проделанные изменения и выходим.

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

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

Подумаем, что будет, если заменить JZ на JNZ? Ветви программы поменяются местами! Теперь, если будет введен неправильный пароль, то система воспримет его как истинный, а легальный пользователь, вводя настоящий пароль, с удивлением прочитает сообщение об ошибке.

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

--------------------------¬ ¦ Ввод и сравнение пароля ¦<--¬ +-------------------------+ ¦ ---+ JZ Password_ok ¦ ¦ ¦ +-------------------------+ ¦ ¦ ¦ ¦ ¦ ¦ ¦ "НЕВЕРНЫЙ ПАРОЛЬ" ¦ ¦ ¦ ¦ ¦ ¦ ¦ +-------------------------+ ¦ ¦ ¦ JMP enter&compare +---- ¦ +-------------------------+ ¦ ¦ ¦ L->¦ "ВЕРНЫЙ ПАРОЛЬ" ¦ ¦ ¦ L--------------------------

Что будет, если мы удалим два перехода (один условный, второй безусловный)? В этом случае последовательно отработают две ветки программы. Чтобы "убить" любую инструкцию, достаточно "забить" ее NOP (опкод который 0x90, а вовсе не 0, как почему-то думают многие начинающие кодокопатели). Обе команды в нашем примере двухбайтовые, и поэтому каждую придется заменить двумя инструкциями NOP.

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

Программа BREAK_X вызвала сбой при обращении к странице памяти в модуле MSVCP60.DLL по адресу 015f:780c278d.

Разочаровывающе малоинформативные сведения! Разумеется, ошибка никак не связана с MSVCP60.DLL, и указанный адрес, лежащий глубоко в недрах последней, нам совершенно ни о чем не говорит. Даже если мы рискнем туда отправиться с отладчиком, то следов причины мы не найдем. В действительности вызываемой функции передали неверные параметры, которые и привели к исключительной ситуации. Конечно, это говорит не в пользу фирмы MircroSoft: что же это за функция такая, если она не проверяет, какие аргументы ей передали! С другой стороны, именно сокращением числа проверок и вызвано некоторое ускорение win98 по сравнению с ее предшественницей. Но нужна ли такая оптимизация? Я бы твердо ответил "НЕТ". Жаль только, что Бил Гейтс меня не услышит.

Однако мы опять отвлеклись. Как же нам проникнуть внутрь Windows и выяснить, что там у нее не в порядке? В этом нам поможет другой продукт фирмы MicroSoft - MS VC. Будучи установленным в систему, он делает доступной кнопку "отладка" в окне аварийного завершения. Теперь мы можем не только закрыть некорректно работающее приложение, но и разобраться, в чем причина сбоя.

Дождемся появления этого окошка еще раз и вызовем интегрированный в MS VC отладчик. Пусть не самый мощный, но достаточно удобный во многих случаях. Как уже отмечалось, бессмысленно искать черную кошку там, где ее нет. Ошибка никак не связана с местом ее возникновения. Нам нужно выбраться из глубины вложенных функций "наверх", чтобы выяснить, что явилось причиной передачи некорректных параметров. Это можно сделать, используя адреса, занесенные в стек. В удобочитаемом виде эту информацию может предоставить мастер "Call Stack", результат работы которого показан ниже: std::basic_ostream<char,std::char_traits<char> >::opfx(std::basic_ostre... std::basic_ostream<char,std::char_traits<char> >::put(std::basic_ostrea... std::endl(std::basic_ostream<char,std::char_traits<char> > & {...}) BREAK_X! 0040114a() CThreadSlotData::SetValue(CThreadSlotData * const 0x00000000, int 4,....

Напомню, что стек растет снизу вверх, а нам, следовательно, нужно спускаться вниз. Первые три вызова можно смело пропустить (т.к. это библиотечные функции), а четвертый break_x принадлежит пользовательскому приложению (по имени исполняемого файла). Вот это - настоящий источник ошибки. Кликнем по нему мышкой и перейдем непосредственно в окно дизассемблера. 00401142 nop 00401143 nop 00401144 call dword ptr ds:[402038h] 0040114A push 403020h

Узнаете окружающий код? Да-да, это то самое место, где мы его модифицировали. Но в чем причина ошибки? Обратим внимание, что перед вызовом функции в строке 0x0401144 не были переданы параметры! Куда же они могли подеваться? А... Это хитрый оптимизирующий компилятор расположил их так, чтобы они оказывались в стеке только в том случае, если эта ветка получает управление. Вернемся к оригинальной копии, чтобы подтвердить наше предположение: 401122 mov eax, ds:?cout!!AMPER!!std!!AMPER!!!!AMPER!!3V? $basic_ostream... 401127 push eax 401128 jz short loc_401144

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

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

Изменив всего один байт 0x74 на 0xEB, мы грязно взломали программу. Кракер на этом остановится, но хакер пойдет дальше. Почему "грязно"? Программа по-прежнему спрашивает пароль. И хотя не имеет значения какой, все же это может сильно раздражать, да и просто выглядет не аккуратно. Давайте модифицируем программу так, чтобы она вообще не отвлекала нас запросом пароля.

Одним из решений будет удаление процедуры ввода пароля. Обращу внимание на важный момент: вместе с процедурой необходимо удалить и заносимые в стек параметры, иначе он окажется несбалансированным и последствия, скорее всего, не заставят себя ждать. Однако компиляторы Cи строят функции так, что очистку стека выполняет не сама функция. Рассмотрим внимательно еще раз процедуру ввода строки: 4010D8 mov edi, ds:??5std!!AMPER!!!!AMPER!!YAAAV? $basic_istream!!AMPER!!DU?... ...... ... ... 4010FA lea ecx, [esp+1Ch] 4010FE push ecx 4010FF push edx 401100 call edi 401102 add esp, 10h

Стек очищается командой ADD ESP,10h. Функция его не изменяет. Поэтому нам ничем не грозит удаление этой функции, и мы без последствий можем "забить" ее командами NOP. Кроме того, можно удалить две команды push (соответственно изменив add esp,10h на add esp,08h), но это вопрос стиля. Кому-то так может показаться красивее, а другой не захочет выполнять бесполезную работу.

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

Что же еще можно улучшить? Надпись Enter password: по-прежнему выводится и выглядит небрежной кляксой на фоне опрятного взлома. Отключим ее? Заметим, что это можно сделать изменив всего один байт, - поставить в начало выводимой строки завершающий символ 0. Это не потребует изменения кода, что безопаснее. А что если мы вместо 'Enter password' запишем свой копирайт? (Должны же пользователи знать, какого доброхота им следует благодарить!). Рассмотрим подробно эту простую операцию, ибо она далеко не так проста, какой кажется на первый взгляд. Было бы неплохо, если бы строка 'Enter password' была раза в два длиннее. А в таком ограниченном объеме мало что можно записать. На деле существующие ограничения легко обойти. Рассмотрим несколько наиболее очевидных вариантов. 0403020: 50 61 73 73-77 6F 72 64-20 4F 4B 21-00 00 00 00 Password OK! 0403030: 50 61 73 73-77 6F 72 64-20 66 61 69-6C 00 00 00 Password fail 0403040: 4B 50 4E 43-00 00 00 00-45 6E 74 65-72 20 70 61 KPNC Enter pa 0403050: 73 73 77 6F-72 64 20 3A-20 00 00 00-43 72 61 63 ssword : Crac 0403060: 6B 4D 65 30-31 20 3A 20-54 72 79 20-74 6F 20 70 kMe01 : Try to p 0403070: 61 74 68 20-63 6F 64 65-20 6F 66 20-66 6F 75 6E ath code of foun 0403080: 64 20 76 61-6C 69 64 20-70 61 73 73-77 6F 72 64 d valid password 0403090: 00 00 00 00-46 61 74 61-6C 20 45 72-72 6F 72 3A Fatal Error: 04030A0: 20 4D 46 43-20 69 6E 69-74 69 61 6C-69 7A 61 74 MFC initializat 04030B0: 69 6F 6E 20-66 61 69 6C-65 64 00 00-00 00 00 00 ion failed

Во взломанной программе строки 'Password fail!' и 'KPNC' уже не нужны. И мы их можем использовать для своих нужд. Для этого нужно изменить указатель на выводимую строку. Как помним, он расположен по адресу 0х04010EC: 004010EC push 403048h

Изменим смещение 0x403048 на 0x0403030. Тогда нам нам будет доступна вся область до 0х403059 (т.е. до начала строки 'CrackMe....'). Только не забудьте конец строки отметить завершающим нулем.

С другой стороны, в сегменте данных еще много свободного места (на этом дампе оно не показано). Если уж мы изменили смещение выводимой строки, почему бы тогда не расположить необходимую нам строку в любой свободной области и не установить на нее указатель?

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

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

Для начала нужно установить, какие именно байты были изменены. Для этого нам вновь потребуется оригинальная копия и какой-нибудь сравниватель файлов. Наиболее популярными на сегодняшний день являются c2u by Professor Nimnul и MakeCrk by Doctor Stein's labs. Первый гораздо предпочтительнее, т.к. он не только более точно придерживается наиболее популярного "стандарта" (если можно так сказать), но и позволяет генерировать расширенный xck формат.

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

Теперь нам потребуется другая утилита, цель которой будет прямо противоположна: используя crk файл, изменить эти самые байты в оригинальной программе. Таких утилит на сегодняшний день очень много. К сожалению, это не лучшим образом сказывается на их совместимости с различными crk форматами. Самые известные из них, скорее всего, cra386 by Professor и pcracker by Doctor Stein's labs.

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

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

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

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

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

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

Однако на самом деле ничего не изменилось. Вместо того чтобы сравнивать пароли, теперь защита сравнивает хеши. Чтобы убедиться, что пароль введен правильно и получена верная хеш сумма, программа должна сравнить полученное значение хеша с эталонным! В зависимости от результата сравнения выполняется та или иная ветка программы. Сравните: if ((s0=ch)!="KPNC") cout << "Password fail" << endl; if (hashe(&pasw[0])!=0x77) cout << "Password fail" << endl;

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

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

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

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

**************** рисунок p6 **************

Как в этом случае найти код, выводящий эти строки? Заглянув в SDK, мы узнаем, что загрузить строку из ресурса можно функцией LoadStringA (и LoadStringW для уникода). Чтобы понять, какая же из двух используется в нашей программе, изучим таблицу импорта функций исполняемого файла. Для этой цели подойдет утилита dumpbin или любая другая.

Странно, но приложение не импортирует функции LoadString, более того, не импортирует ни одной функции из USER32! Как же оно при этом может работать? Рассмотрим иерахрию импорта в 'Dependency Walker'.

**************** рисунок p7 **************

На самом деле вызывается функция LoadString - метод класса CString, которая уже в свою очередь вызывает LoadStringA Win32 API. Посмотрим, какие функции MFC42.DLL импортирует наша "подопытная" программа. MFC42.DLL 40200C Import Address Table 402140 Import Name Table 0 time date stamp 0 Index of first forwarder reference Ordinal 815 Ordinal 561 Ordinal 800 Ordinal 4160 Ordinal 540 Ordinal 1575

Какая жалость! Символьная информация недоступна и известен только ординал. Можно ли как-то определить, какая из этих функций - LoadString? Возможно, нам будет доступна символьная информация непосредственно в MFC42.DLL? Увы, там тоже не содержится ничего интересного кроме ординалов. Но как-то линкер справляется с этой ситуацией? Заглянем в MFC42.map и попробуем найти строку по контексту 'LoadString' 0001:00003042 ?LoadStringA!!AMPER!!CString!!AMPER!!!!AMPER! !QAEHI!!AMPER!!Z 5f404042 f

Замечательно! Но как же нам теперь получить ее ординал? Для этого уточним, что означает '0001:00003042'. Preferred load address is 5f400000 Start Length Name Class 0001:00000000 00099250H .text CODE

Следовательно, 0x3042 - это смещение относительно секции .text. Воспользуемся утилитой hiew и посмотрим, что там находится. Для этого сначала вызовем таблицу объектов, выберем '.text' и к полученному смещению прибавим 0x3042. Переведем hiew в режим дизассемблера. 5F404042: 55 push ebp 5F404043: 8BEC mov ebp,esp 5F404045: 81EC04010000 sub esp,000000104 5F40404B: 56 push esi

То, что мы сейчас видим до боли напоминает пролог функции. Но почему, собственно, напоминает? Это и есть точка входа в функцию LoadStringA!!AMPER!!CString!!AMPER!!!!AMPER!!QAEHI!!AMPER!!! Разумеется, теперь нетрудно в списке ординалов найти тот, чей адрес совпадает с полученным. Однако прежде чем приступить к поиску, уточним, что мы, собственно, намереваемся искать. Вычтем из полученного адреса рекомендуемый (0x5F404042 - 0x5f400000 = 0x4042): именно это значение будет присутствовать в таблице экспорта, а не 0x5F404042, как можно было подумать с первого взгляда.

Используем ранее полученный список экспорта функций MFC42.dll Просматривать вручную двух-мегабайтовый файл рапорта было бы чрезычайно утомительно, поэтому воспользуемся контекстным поиском. 4160 00004042 [NONAME]

Оказывается, ординал этой функции 4160 (0x1040). Не кажется ли вам, что это число мы уже где-то видели? Вспомним таблицу импорта crack02.exe MFC42.DLL 40200C Import Address Table 402140 Import Name Table 0 time date stamp 0 Index of first forwarder reference Ordinal 815 Ordinal 561 Ordinal 800 Ordinal 4160 ^^^^^^^^^^^^^ Ordinal 540 Ordinal 1575

Круг замкнулся. Мы выполнили поставленную задачу. Но можно ли было поступить как-то иначе? Неужели это никто не догадался автоматизировать?

Разумеется: с этим справляется любой хороший дизассемблер. Например, IDA. Но последняя надежно скрывает все эти операции под "капотом". Используя инструментарий подобного класса, можно даже не задумываться как это все происходит.

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

К счастью, у нас IDA под рукой, и мы избежим кропотливой и трудоемкой работы. Убедимся, что она правильно извлекла символьную информацию о импортируемых функциях. Для этого перейдем в сегмент "_rdata". ; Imports from MFC42.DLL ??1CWinApp!!AMPER!!!!AMPER!!UAE!!AMPER!!XZ ??0CWinApp!!AMPER!!!!AMPER!!QAE!!AMPER!!PBD!!AMPER!!Z ??1CString!!AMPER!!!!AMPER!!QAE!!AMPER!!XZ ?LoadStringA!!AMPER!!CString!!AMPER!!!!AMPER!!QAEHI!!AMPER!!Z ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ??0CString!!AMPER!!!!AMPER!!QAE!!AMPER!!XZ ?AfxWinInit!!AMPER!!!!AMPER!!YGHPAUHINSTANCE__!!AMPER!!!!AMPER! !0PADH!!AMPER!!Z

Переместим курсор на LoadStringA!!AMPER!!String, чтобы узнать, какой код ее вызывает. После недолгих путешествий мы должны увидеть приблизительно следующее: 0040119F push 1 004011A1 lea ecx, [esp+0Ch] 004011A5 mov [esp+2Ch], edi 004011A9 call j_?LoadStringA!!AMPER!!CString!!AMPER!! !!AMPER!!QAEHI!!AMPER!!Z

Похоже, что функция принимает всего один параметр. Уточним это с помощью MSDN - BOOL LoadString( UINT nID ). Передаваемый параметр представляет собой идентификатор ресурса. Используем любой редактор ресурсов (например, интегрированный в MS VC), чтобы узнать связанную с ним строку. Нетрудно убедиться, что эта та самая строка, которую программа первой выводит на экран. Следовательно, мы находимся в самом начале защитного механизма.

Продолжим наши исследования дальше - до тех пор, пока не обнаружим уже знакомую нам функцию ввода. 004011EA lea edx, [esp+14h] 004011EE push edx 004011EF push eax 004011F0 call ds:??5std!!AMPER!!!!AMPER!!YAAAV? $basic_istream!!AMPER!!DU...

Вычислим новое значение указателя после засылки двух аргументов в стек. Оно будет равно 14h+2*4 = 0x1C. Теперь нам становится понятен смысл следующего фрагмента: 04011F6 lea ecx, [esp+1Ch] ; указатель на введеный пароль 04011FA push ecx 04011FB call sub_4010E0 ; (1) 0401200 add esp, 14h ; 0x1C - 0x14 + 4 = 0xC 0401203 lea edx, [esp+0Ch] ; указатель на введенный пароль 0401207 push eax ; агрумент для функции (3) 0401208 push edx 0401209 call sub_4010E0 ; (2) 040120E add esp, 4 0401211 push eax 0401212 call sub_4010C0 ; (3) 0401217 add esp, 8 040121A cmp ax, 1F8h ; Сравниваем ответ 040121E jz short loc_401224 ; !ВОТ ИСКОМЫЙ ПЕРЕХОД!

Довольно витиеватый алгоритм, задействовавший три процедуры. Но даже не интересуясь, что делает каждая из них, мы с уверенностью можем сказать, что тот условный переход в строку 0x040121E и является "высшей инстанцией", которая выносит окончательный вердикт. Замена его на безусловный приводит к тому, что независимо от результата работы хеш-функции будет получать управление "правая" ветка программы.

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

Попробуем усилить реализацию. Для этого нам нужно непосредственно использовать введенный пароль в программе. Лучше всего подойдет для этой цели шифровка. Поскольку исполнямый код шифровать средствами языков высокого уровня очень затруднительно, то мы зашифруем данные. Однако сделать это непосредственно не позволяет ни один популярный компилятор! Поэтому всю работу (какой бы она ни показалась трудоемкой, нам предстоить выполнить вручную). Существует по крайней мере два диаметрально противоположных подхода к решению этой задачи. Можно зашифровать данные любой утилитой в исполняемом файле, а в самой программе их расшифровывать. Это может выглядеть, например, так: while (true) { if (!SecretString[pTxt]) break; if (Password[pPsw]<0x20) pPsw = 0; SecretString[pTxt++] = SecretString[pTxt] ^ Password[pPsw++]; }

При этом SecretString нормально выглядит в исходном тексте и зашифрована только после компиляции непосредственно в исполняемом файле. Однако для этого требуется написание утилиты автоматической зашифровки, что может быть утомительно, а кроме того, требует знания формата исполняемого файла и проведения этой операции после каждой компиляции. Однако можно включать зашифрованные данные прямо в исходный текст программы приблизительно таким образом: char SecretString[] = "\x1F\x38\x2B\x63\x49\x4E\x38\x24\x6E\x2A\x58\x0B"

Чтобы получить подобную строку, нужно воспользоваться специально написанным для этой цели шифровальщиком. Например, таким: (полный исходный текст file: //CD:/SRC/CRACK03/Crypt.asm). MOV DI,offset Text-1 Reset: LEA SI,Password Crypt: LODSB OR AL,AL JZ Reset INC DI XOR [DI],AL JMP Crypt

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

Конечно, это утомительная работа, заметно удлиняющая и удорожающая разработку. Оправдает ли этот подход затраченное время и усилия? Попытаемся его вскрыть, чтобы ответить утвердительно: "Да, оправдает". Поскольку crack03 отличается от crack02 только одной новой процедурой, мы можем продолжить с того самого места, на котором закончили изучение последней. Остальной код полностью идентичен и мы быстро найдем следующий ключевой фрагмент: 401276 call sub_401120 40127B add esp, 8 40127E cmp ax, 1F8h 401282 jz short loc_401291

Попробуем заменить условный переход на jmp short loc_401291, предполагая, что независимо от введенного пароля программа будет корректно работать. Посмотрим, что из этого получилось: Crack Me 03 Enter password : crack |JJ

Какое разочарование... Программа действительно воспринимает любой пароль, но работает некорректно. Мы лишь нейтрализовали механизм проверки, но отнюдь не сняли защиту. Основная (и самая тяжелая работа) еще впереди!

Теперь заменой одного-двух байт программу никак не взломаешь. Нужно разобраться, как она манипулирует паролем и как его можно найти. На эти вопросы невозможно ответить без глубокого и вдумчивого анализа механизма защиты и процедуры расшифровки. Цель хакера - сократить эти усилия до минимума. Подумаем, с чего начать изучение программы? С механизма проверки пароля? Нет конечно. Точный механизм проверки нас пока не интересует. А вот как используется пароль - это уже интересно. Чтобы это выяснить, продолжим анализ со строки 0x0401291, на которую ссылается условный переход. 00401291 loc_401291: 00401291 lea eax, [esp+10h] 00401295 lea ecx, [esp+0Ch] 00401299 push eax 0040129A push ecx 0040129B call sub_4010C0

Мы знаем, что [esp+10h] указывает на начало буфера, содержащего введенный пароль. Но что тогда [esp+0Ch]? Похоже, что это адрес буфера для возвращения результата работы процедуры. Для подтверждения этого предположения заглянем внутрь последней: 004010C0 sub esp, 28h 004010C3 push esi 004010C4 push edi 004010C5 mov ecx, 9 004010CA mov esi, 403020h 004010CF lea edi, [esp+0Ch] 004010D3 mov dword ptr [esp+8], 0 004010DB rep movsd 004010DD mov edi, [esp+38h] 004010E1 xor eax, eax 004010E3 lea esi, [esp+0Ch]

Что такое 0x0403020 - константа или смещение? Посмотрим. esi используется как указатель командой movsd, следовательно, это смещение. Посмотрим, что расположено по этому адресу: a8Cin8NX?8Cne_7 db '8+cIN8$n*X',0Bh,'?8+cNE.=7cDMk$&&',0Bh,'L$?*c',0

Нечто абсолютно нечитабельное. Однако завершающий нуль наводит на мысль, что это может быть строкой. Команда "mov dword ptr [esp+8], 0" еще больше укрепляет нашу уверенность. Действительно, ноль в конце строки не случайность, а часть структуры. С другой стороны, зная особенности используемого компилятора, нетрудно заметить, что рассматриваемый код декомпилируется в обычную конструкцию char MyString[]="It's my string". Но почему же эта строка так странно выглядит? Быть может, она зашифрована? Эту мысль подтверждает установка регистра edi на начало пароля. Наступает самый волнующий момент - мы переходим к изучению криптоалгоритма. Если он окажется недостаточно стойким, то можно будет подобрать подходящий пароль. Обратим внимание на следующий фрагмент кода: 004010F5 mov dl, [eax+edi] 004010F8 xor dl, cl 004010FA mov [esi], dl 004010FC inc esi 004010FD inc eax 004010FE jmp short loc_4010E7

Попробуем записать его в более удобочитаемом виде, чтобы легче было отождествить алгоритм: SecretString[pTxt++] = SecretString[pTxt] ^ Password[pPsw++];

Это хорошо известный шифр Вернама. Криптостойкость его уже рассматривалась в главе, посвященной криптографии. (равно как и методы атаки). Однако не зная, что за текст содержался в зашифрованной строке, мы имеем мало шансов быстро подобрать пароль. Быть может, удастся подобрать хеш-сумму или просто перебрать пароль? Последнее, уже внушает некоторую надежду. Если пароль окажется не очень длинным (от шести до восьми символов), то перебор скорее всего завершится гораздо быстрее словарной атаки на шифротекст. Чтобы написать переборщик паролей, необходимо с точностью до реализации знать алгоритм вычисления хеш-суммы. Возвратимся вновь к механизму проверки пароля. 04011F6 lea ecx, [esp+1Ch] ; указатель на введенный пароль 04011FA push ecx 04011FB call sub_4010E0 ; (1) 0401200 add esp, 14h ; 0x1C - 0x14 + 4 = 0xC 0401203 lea edx, [esp+0Ch] ; указатель на введенный пароль 0401207 push eax ; аргумент для функции (3) 0401208 push edx 0401209 call sub_4010E0 ; (2) 040120E add esp, 4 0401211 push eax 0401212 call sub_4010C0 ; (3) 0401217 add esp, 8 040121A cmp ax, 1F8h ; Сравниваем ответ 040121E jz short loc_401224 ; !ВОТ ИСКОМЫЙ ПЕРЕХОД!

Хеш-сумма на самом деле вычисляется дважды (что затрудняет ее реверсирование). Используемый автором алгоритм можно свести к следующему if (f(f1(&pasw[0]),f1(&pasw[0]))== 0x1F8) ....

Как работает функция f? Изучим следующий фрагмент: 00401120 sub_401120 proc near 00401120 00401120 mov eax, [esp+4] 00401124 mov ecx, [esp+8] 00401128 and eax, 0FFh 0040112D and ecx, 0FFh 00401133 imul eax, ecx 00401136 sar eax, 7 00401139 retn 00401139 sub_401120 endp

Она умножает аргументы друг на друга и берет старшие 9 бит (0x10-0x7). Это хорошая хеш-функция. Для ее обращения потребуется разлагать числа на множители, что нельзя эффективно реализовать. С другой стороны, прямое ее вычисление очень быстрое, что упрощает перебор паролей. Однако обратим внимание на то, что на самом деле ее аргументы РАВНЫ. Таким образом обращение функции сводится к элементарному вычислению квадратного корня. После чего останется перебрать 2^7 = 0x80 (128) вариантов (т.к. эти биты были отброшены хеш-функцией). Это смехотвороное число вселяет в нас уверенность, что и пароль мы сможем найти очень быстро. Но не будем спешить. Необходимо реверсировать еще одну хеш-функцию. Посмотрим, что за сюрприз приготовил нам автор на этот раз: 00401152 loc_401152: 00401152 mov al, [esi] 00401154 cmp al, 20h 00401156 jl short loc_40117F 00401158 mov cl, [esi-1] 0040115B mov [esp+14h], al 0040115F mov edx, [esp+14h] 00401163 mov [esp+0Ch], cl 00401167 mov eax, [esp+0Ch] 0040116B push edx 0040116C push eax 0040116D call sub_401120 00401172 lea ecx, [edi+esi] 00401175 add esp, 8 00401178 shl eax, cl 0040117A or ebx, eax 0040117C inc esi 0040117D jmp short loc_401152

Чтобы разобраться в алгоритме этого непонятного кода, попробуем его построчно перевести на Си-подобный язык. 00401152 while (true) { // Цикл 00401152 char _AL = String[idx+1]; // Берем один символ 00401154 if (_AL < 0x20) break; // Это конец строки? 00401158 char _CL = String[idx]; // Берем другой смвол 0040115B chat _s1 = _AL; // Сохраняем _AL в стеке 0040115F (DWORD) _EDX = _s1; // Расширяем до DWORD 00401163 char _s2 = _CL; // Сохраняем в стеке 00401167 (DWORD) _EAX = _s2; // Расширяем до DWORD 0040116D _EAX=f(_EAX,_EDX); // Уже знакомая нам функция! 00401172 CHAR _CL = idx; // см. объяснения ниже 00401178 _EAX = _EAX << _CL; // сдвигаем влево 0040117A DWORD _EBX = _EBX | _EAX; // накапливаем единичные биты 0040117C idx++ 0040117D }

Весь код понятен, кроме странной и совершенно непонятной на первый взгяд операции "00401172 lea ecx, [edi+esi]". Поскольку ESI - это однозначно указатель на текущий символ, то что же тогда edi? 00401141 mov eax, [esp+8] 00401148 or edi, 0FFFFFFFFh 0040114B xor ebx, ebx 0040114D lea esi, [eax+1] 00401150 sub edi, eax

Этот код наглядно показывает, какие перлы иногда может выдавать оптимизатор. Попробуем разобраться что же было на этом месте в исходном коде. edi = (or edi, 0FFFFFFFFh) = -1; тогда esi = (lea esi, [eax+1]) == &String+1; и edi = (sub edi, eax) == -1-(&String) = -1 - &String. Поэтому ecx = (lea ecx, [edi+esi]) == - 1 - &String + &String+idx + 1 == idx! Это хороший пример "магического кода". Т.е. такого, который работает, но с первого взгляда абсолютно нельзя понять, как и почему.

Многие хакеры любят писать программу в похожем стиле, и для ее анализа приходится с боем продираться через каждую строку. До недавнего времени это считалось своего рода искусством. Сегодня существующие компиляторы по "дикости" кода с легкостью обгоняют человека. Поэтому все чаще возникают сомнения: что это за искусство такое, в котором машина превосходит человека?

Как бы то ни было, приблизительный исходный текст программы восстановить не составляет труда. Вероятно, в оригинале он выглядел так: while (true) { if (String[idx+1]<0x20) break; x1 = x1 | (f (String[idx],String[idx+1]) << idx++); }

Используя полученные исходные тексты, можно написать программу, которая для всех возможных паролей вычислит хеш-сумму и распечатает только те, у которых она будет равна 1F8h, т.е. искомой.

Я настоятельно рекомендую попытаться решить эту задачу самостоятельно и только в крайнем случае прибегать к готовому решению (folder://CD/SRC/CRACK_3).

Собственно, алгоритм перебора паролей достаточно несложен и в не самой лучшей реализации может выглядеть так: while (1) { int idx=0; while ((Password[idx++]++)>'z') Password[idx-1]='!'; if (mult(hashe(&Password[0]),hashe(&Password[0]))==0х1F8) printf ("Password - %s \n",Password); }

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

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

Не будет исключением и этот случай. Наш простой перебощик начнет "плеваться" паролями, чья хеш сумма в точности равна 0x1F8, но настоящими паролями все они, разумеется, не являются. Их много, очень много, очень-очень много... Похоже, дальнейший перебор не имеет никакого смысла и его придется прекратить. Почему? Рассмотрим фрагмент протокола: Password - yuO Password - xvO Password - uwO Password - wwO Password - rxO Password - vxO Password - qyO Password - uyO Password - nzO Password - pzO

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

Это пик торжества разработчика защиты. Использовав ПЛОХУЮ хеш-функцию со слабым рассеянием, он обрубил нам все пути ко взлому своего приложения. Бесполезно даже обращение хеш-функции, ибо оно ничего не даст. Мы получим те же "левые" пароли - может быть, чуть быстрее.

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

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

Какой непредсказуемый поворот событий! Очередная незаметная лазейка разрушает всю воздвигнутую систему защиты. Строго говоря, даже не требуется атаки на шифротекст. Необходим лишь метод автоматического контроля, позволяющий отсеять максимально возможное число "ложных" паролей. Для этого используем тот факт, что оригинальный текст (а в нашем примере это должен быть какой-то лозунг или поздравление) с большой вероятностью содержит '_a_','_of_','_is_' и т.д. Если в расшифрованном тексте хоть одно из этих слов присутствует и нет ни одного символа, выходящего за интервал '!' - 'z', то это неплохой кандидат. Предложенный метод хотя и является крайне медленным, но, похоже единственно возможным. Добавим в существующий переборщик еще пару строк: if (mult(hashe(&Password[0]),hashe(&Password[0]))==0х1F8) { s0=Protect(&Password[0]); if (s0.Find(" is ")!=-1) printf ("Password - %s - %s\n",Password,s0); }

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

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

Но даже если алгоритм реализован без ошибок, вскоре будет найден не единственный верный пароль. Например: Password - KkEC++ - TSn besO+is tSn eneVr of Oce goTo

Однако теперь уже совсем нетрудно проанализировать полученный текст и угадать настоящий пароль. С большой вероятностью исходный текст можно просто угадать! Или, по крайней мере, продолжить словарный перебор. Благо теперь это не трудно. Предположим, что 'TSn' это искаженное 'The', следовательно, ожидаемый пароль 'KPNC++', а вся фраза читается так: 'The best is the enemy of the good'

Мы действительно смогли найти пароль и взломать далеко не самою простую систему защиты. Большинство полулярных приложений защищено гораздо проще и ломается быстрее.

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

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

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

Пародоксально, но этот способ крайне редко применяется разработчиками. Из всех программ, защищенных подобным образом, я навскидку могу вспомнить только FDR 2,1, в котором фрагмент кода, отвечающего за регистрацию, расшифровывался "магическим словом" 'Pink Floyd'. Обычно применяют более наивные защитные механизмы, которым посвящена следующая глава.

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

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

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

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

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

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

Рассмотрим простую реализацию данного механизма защиты на примере программы file://CD/SRC/CRACK04/Crack04.exe

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

Самым популярным на сегодняшний день отладчиком является Soft-Ice от NuMega. Это очень мощный профессиональный инструмент. Новички часто испытывают большие трудности при его настройке, поэтому в приложении подробно описывается, как это сделать.

Разумеется, никто не ограничивает свободу читателя в выборе отладчика, однако в настоящее время не существует программ, которые могли бы составить реальную конкуренцию Софт-Айсу. Это вовсе не означает, что другие программы не пригодны для взлома. Большинство из них могут решать рядовые задачи с не меньшим успехом, а узкоспециализированные - в своей области заметно обгоняют Айс. Но уникальность Айса в том, что он покрывает рекордно широкий круг задач и платформ. Кроме того, очень приятен и удобен. Я не знаю ни одного другого отладчика, поддерживающего командную строку.

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

************** Рисунок 8 **********************

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

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

Хорошая задачка! Откуда же узнать этот адрес? Неужели придется утомительно анализирать код? Разумеется, нет. Существуют гораздо более оригинальные приемы. Начнем с того, что содержимое окна редактирования надо как-то считать. Для этого нужно послать окну сообщение WM_GETTEXT и адрес буфера, куда этот текст следует принять. Однако этот способ не снискал популярности, и программисты обычно используют функции API. В SDK можно найти по крайней мере две функции, пригодные для этой цели, - GetWindowText и GetDlgItemText. Причем первая используется гораздо чаще.

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

Итак, нам нужно установить точку останова на вызываемую функцию. Чтобы узнать, какую, вновь заглянем в список импорта crack04.exe Как мы помним, это приложение использует MFC, а следовательно, крайне маловерятно, чтобы программист, писавший его, воспользовался напрямую Win32 API, а не библиотечной функцией.Вероятнее всего CWnd::GetWindowText, Попробуем найти ее среди списка импортируемых функций. Для этого можно воспользоваться любой утилитой (например IDA) или даже действовать вручную. Так или иначе, мы обнаружим, что ординал этой функции 0xF22. Этого достаточно, чтобы установить точку останова и перехватить чтение введенной строки.

Однако легко видеть, что CWnd::GetWindowText это всего лишь "переходник" от Win32 API GetWindowTextA. Поскольку нам нужно выяснить только сам адрес строки, то все равно перехватом какой функции мы это сделаем, т.к. и та и другая работают с одним и тем же буфером. Это применимо не только к MFC, но и к другим библиотекам. В любом случае на самом низком уровне приложений находятся вызовы Win32 API, поэтому нет никакой нужды досконально изучать все существующие библиотеки, достаточно иметь под рукой SDK. Однако это никак еще не означает, что можно вообще не интересоваться архитектурой высокоуровневых библиотек. Приведенный пример оказался "прозрачен" только потому, что GetWindowTextA передавался указатель на тот же самый буфер, в котором и возвращалась введенная строка. Но разве не может быть иначе? GetWindowTextA передается указатель локального буфера, который затем копируется в результирующий. Поэтому полезно хотя бы бегло ознакомиться с архитектурой популярных библиотек.

Но давайте, наконец, перейдем к делу. Для этого вызовем отладчик и (если это Софт_Айс) дадим команду bpx GetWindowTextA. Попутно укажем, откуда взялась буква 'A'. Она позволяет отличить 32-разрядные функции, работающие с unicode строками (W), от функций, работающих с ANSI строками (A). Нам это помогает отличать новые 32-разрядные фуккции от одноименных 16-разрядных. Подробности можно найти в SDK.

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

Сейчас мы находимся в точке входа в функцию GetWindowTextA. Как узнать адрес переданного ей буфера? Разумеется, через стек. Рассмотрим ее прототип: 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 );

Следовательно, стек будет выглядить так:

----------------------¬ 0x0 DWORD ¦ EIP ¦ +---------------------+ 0x4 DWORD ¦ nMaxCount ¦ +---------------------+ 0x8 DWORD ¦ lpString ¦ +---------------------+ 0xC ¦ .............. ¦

Переведем окно дампа для отображения двойных слов командой DD и командой d ss:esp+8 выведем искомый адрес. Запомним его (запишем на бумажке) или выделим мышью и скопируем в буфер. Теперь дождемся выхода из процедуры (p ret) и убедимся, что прочитанная строка соответствует введеному имени. (Вполне возможно, что программа сперва читает регистрационный номер и только потом имя).

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

Двойное слово lpString это указатель на строку. Однако это только 32-битное смещение. Но относительно какого сегмента? Разумеется, DS. Поэтому установка точки останова может выгядеть так: bpx ds:xxxxx r. Первый код, читающий строку, на самом деле не принадлежит к отлаживаемой программе. В этом можно убедиться, если несколько раз дать команду p ret, - до тех пор, пока мы не выйдем из функции MFC42!0F22. Как мы помним это ординал CWnd::GetWindowText. Теперь любой обращающийся к строке код будет принадлежать непосредственно защите. Мы, вероятно, уже находимся в непосредственной близости от защитного механизма, но иногда бывает так, что программист читает строку в одном месте программы, а использует результат совсем в другом. Поэтому дождемся повторного отладочного исключения. Рассмотрим код, вызвавший его: 015F:004015F7 8A0C06 MOV CL,[EAX+ESI]

Используемая адресация наталкивает нас на мысль, что eax, возможно, параметр цикла, а вся эта конструкция посимвольно читает строку. Очень похоже, что в самом центре генератора серийного номера. Если мы посмотрим чуть-чуть ниже, то в глаза бросится очень любопытная строка: 015F:0040164B 51 PUSH ECX 015F:0040164C 52 PUSH EDX 015F:0040164D FF15D0214000 CALL [MSVCRT!_mbscmp]

Вероятно, она сравнивает введенный нами и сгенерированный регистрационный номер! Переведем курсор на нее и дадим команду here. И последовательно дадим команды d ds:ecx и d ds:edx. В одном случае мы увидим свою строку, а во втором - истинный регистрационный номер. Выйдем из отладчика и попытаемся ввести его в программу. Получилось! Нас признали зарегистрированным пользователем!

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

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

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

Однако мы не закончили взлом программы. Да, мы узнали регистрационный код для нашего имени, но понравится ли это остальным пользователям? Ведь каждый из них хочет зарегистрировать программу на СЕБЯ. Кому будет приятно видеть чужое имя?

Вернемся к коду, сравнивающему эти строки: 015F:00401643 8B4C2410 MOV ECX,[ESP+10] 015F:00401647 8B54240C MOV EDX,[ESP+0C] 015F:0040164B 51 PUSH ECX 015F:0040164C 52 PUSH EDX 015F:0040164D FF15D0214000 CALL [MSVCRT!_mbscmp] 015F:00401653 83C408 ADD ESP,08 015F:00401656 85C0 TEST EAX,EAX 015F:00401658 5E POP ESI 015F:00401659 6A00 PUSH 00 015F:0040165B 6A00 PUSH 00 015F:0040165D 7507 JNZ 00401666

Давайте заменим в строке 0040164C 0х52 на 0x51, тогда защита будет сравнивать строку с ней самой. Разумеется, сама с собой строка не совпадать никак не может. Конечно, можно заменить JNZ на JMP или JZ, но это будет не так оригинально.

Замечу, что этот способ срабатывает очень редко. Чаще всего проверка будет не одна и в самых неожиданных местах. Достаточно вспомнить, что регистрационные данные запоминаются защитой в реестре или внешнем файле. Блокировав первую проверку, мы добьемся того, что позволим защите сохранить неверные данные. Очень вероятно, что при их загрузке автор предусмотрел проверку на валидность. Ее можно отследить аналогичным образом, перехватив вызовы функций, манипулирующих с реестром, однако это было бы очень утомительно. Впрочем, не так утомительно, как может показаться на первый взгляд. В самом деле, не интересуясь механизмом ввода данных, можно отследить все вызовы процедуры генерации. Возможны по крайней мере два варианта. Автор либо использовал вызов одной и той же процедуры из разных мест, либо дублировал ее по необходимости. В первом случае нас выручат перекрестные ссылки (наиболее полно их умеет отслеживать sourcer), во втором - сигнатурный поиск. Крайне маловероятно, что автор использовал не один, а несколько вариантов процедуры генератора. Но даже в этом случае не гарантировано отсутствие совпадающих фрагментов. И уж тем более на языках высокого уровня. Далеко не каждый программист знает, что (! a) ? b=0 : b=1 и if (!a) b=0; else b=1 генерируют идентичный код. Поэтому написать одну и ту же процедуру, но так, чтобы ни в одном из вариантов не было повторяющихся фрагментов кода, представляется очень нетривиальной задачей.

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

Вернемся немного назад: 015F:004015F7 8A0C06 MOV CL,[EAX+ESI] 015F:004015FA 660FBE440601 MOVSX AX,BYTE PTR [EAX+ESI+01] 015F:00401600 660FBEC9 MOVSX CX,CL 015F:00401604 0FAFC1 IMUL EAX,ECX 015F:00401607 25FFFF0000 AND EAX,0000FFFF 015F:0040160C B91A000000 MOV ECX,0000001A 015F:00401611 99 CDQ 015F:00401612 F7F9 IDIV ECX 015F:00401614 8D4C240C LEA ECX,[ESP+0C] 015F:00401618 80C241 ADD DL,41 015F:0040161B 88542414 MOV [ESP+14],DL 015F:0040161F 8B542414 MOV EDX,[ESP+14] 015F:00401623 52 PUSH EDX 015F:00401624 E805030000 CALL 0040192E ^^^^^^^^^^^^^^^^ 015F:00401629 8B442408 MOV EAX,[ESP+08]

Если мы попытаемся заглянуть в процедуру 0x040192E, то вероятнее всего утонем в условных переходах и вложенных вызовах. Сложность и витиеватость кода наталкивают на мысль, что это библиотечная процедура. Но какая? Дело в том, что отладчик не был правильно настроен и экспортировал только системные функции. Исследуемое приложение активно использует MFC42.DLL, поэтому для загрузки символьной информации о функциях последнего необходимо его явно загрузить. Это делается директивой EXP в файле winice.dat Посмотрим, что у нас получилось: 015F:0040161B 88542414 MOV [ESP+14],DL 015F:0040161F 8B542414 MOV EDX,[ESP+14] 015F:00401623 52 PUSH EDX 015F:00401624 E805030000 CALL MFC42!ORD_03AC ^^^^^^^^^^^^^^^^^^^^^^

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

Однако отладчики не предназначены для подробного анализа кода. Гораздо удобнее изучать логику программы с помощью дизассемблера. Найти же требуемый фрагмент очень просто. Достаточно вспомнить, что адрес уже известен. Переместим курсор на строку .text:0040161B, для чего в IDA дадим с консоли команду Jump(MK_FP(0,0x40161B)) и прокрутим экран немного вверх, пока не встретим следующие строки: .text:004015D3 call j_?GetWindowTextA!!AMPER!!CWnd! !AMPER!!!!AMPER!!QBEXAA .text:004015D8 mov eax, [esp+4] .text:004015DC mov ecx, [eax-8] .text:004015DF cmp ecx, 0Ah .text:004015E2 jge short loc_0_4015EF

Очевидно, последний условный переход выполняется, когда длина введенной строки больше девяти символов. Для понимания этого необходимо знать, что CString хранит свою длину в двойном слове, находящемся до начала строки. Итак, непосредственно относящийся к защите код начинается с адреса 0x4015EF. Рассмотрим его: .text:004015EF loc_0_4015EF: .text:004015EF push esi .text:004015F0 xor esi, esi .text:004015F2 dec ecx .text:004015F3 test ecx, ecx .text:004015F5 jle short loc_0_401636

Это типичный цикл for. Заглянем в его телo: .text:004015F7 loc_0_4015F7: .text:004015F7 mov cl, [esi+eax]

Загрузка очередного символа строки. Поскольку eax - содержит базовый адрес, то очевидно, что esi - смещение в строке. Выше видно, что начальное значение его равно нулю. Логично, что строка обрабатывается от первого до последнего символа, хотя часто бывает и наоборот. .text:004015FA movsx ax, byte ptr [esi+eax+1]

MOVe and Sign eXtension (пересылка со знаковым расширением) загружает байт в регистр AX, автоматически расширяя его до слова. .text:00401600 movsx cx, cl

Обратим внимание на несовершенство компилятора. Эту команду можно было записать более экономно как movsx cx, [esi+eax] .text:00401604 imul eax, ecx

Подставим всесто регистров их смысловые значения и получим String[idx]*String[idx+1]. .text:00401607 and eax, 0FFFFh

Преобразуем eax к машинному слову. .text:0040160C mov ecx, 20h .text:00401611 cdq

CDQ - Convert Double word to Quad word - Преобразование двойного слова в счетверенное слово .text:00401612 idiv ecx .text:00401614 lea ecx, [esp+28h+var_1C] .text:00401618 add dl, 41h

Поскольку 0x41 - это код символа 'A', то, вновь выполнив смысловую подстановку, получим: _dl = (String[idx]*String[idx+1]) % 0x20 + 'A'. Т.е автор вычисляет хеш-сумму строки. Обратим внимание, что она будет инъективна для интервала 'A'-'_' и, более того, нечувствительна к регистру!

Этот код можно назвать "кодом черной магии". С первого взгляда не понятно как он работает и чем обусловлена нечувствительность к регистру. Обычно для этого программист сначала переводит все буквы в заглавные и только потом начинает разбор строки. Или делает это на лету явным сравнением типа cmp xx, 'a'.

Оригинальные приемы всегда ценятся хакерами, особенно когда они позволяют сократить немного байт и тактов процессора. .text:0040161B mov byte ptr [esp+28h+var_14],dl .text:0040161F mov edx, [esp+28h+var_14] .text:00401623 push edx .text:00401624 call CString::operator+=(char)

Очередной перл компилятора. Можно было не вводить локальную переменную, а непосредственно передать dl (предварительно расширив его до двойного слова) в стек, что повысило бы скорость обработки за счет избавления от обращений к памяти. .text:0040162D inc esi

Перемещаем указатеь idx на следующий символ в строке. .text:0040162E mov ecx, [eax-8] .text:00401631 dec ecx .text:00401632 cmp esi, ecx .text:00401634 jl short loc_0_4015F7

Очевидно, что эти строки также относятся к циклу for. Поэтому уже можно восстановить исходный код генератора. for (int idx=0;idx<String.GetLength()-1;idx++) RegCode+= ((WORD) sName[a]*sName[a+1] % 0x20) + 'A';

Теперь нетрудно написать собственный генератор регистрационных номеров. Это можно сделать на любом симпатичном вам языке, например на ассемблере. На диске находится один вариант (file://CD/SRC/CRACK04/key_gen.asm). Без текстовых строк исполняемый файл занимает менее пятидесяти байт и еще оставляет простор для оптимизации. Ключевая процедура может выглядеть так: Reprat: ; LODSW ; Читаем слово MUL AH ; Password[si]*Password[si+1] XOR DX,DX ; DX == NULL DIV BX ; Password[si]*Password[si+1] % 0x20 ADD DL,'A' ; Переводим в символ XCHG AL,DL STOSB ; Записываем результат DEC SI LOOP Reprat

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

****************** рисунок 9 ****************

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

Я понимаю, что такая трактовка может встретить возражение. Действительно, зачем что-то ломать, если хакер все равно должен приобретать лицензиозный софт? Но разве в этом есть что-то нелогичное? Хакер - это взрослый ребенок, удовлетворяющий свое любопытство. Конечно, очень трудно, обладая такими знаниями и навыками, удержаться от соблазна нарушить закон. Более того, я не знаю ни одного человека, который поступал бы именно так. Увы, хакерство действительно оказывается тесно связанным с криминалом. Это, к сожалению, так.

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

Выбор программ-шпионов достаточно широк. Очень неплохо для этой цели подходит BC от NuMega, однако достаточно и более скромных средств. Например, распространяемый вместе с Microsoft Spy++.

****************** рисунок 0A ****************

Рассмотим полученный рапорт: 00000E9C S .WM_GETTEXT cchTextMax:30 lpszText:0063F750 00000E9C R .WM_GETTEXT cchCopied:14 lpszText:0063F750 ("Kris Kasperski")

Умница spyxx даже показал адрес, по которому считанная строка располагается в памяти. Впрочем, он мало что нам дает. Скорее всего буфер расположен в стеке, и активно используется приложением. Нам необходимо перехватить WM_GETTEXT непосредственно в отладчике. Для этого нужно знать дескриптор окна. В этом нам и поможет шпион.

Перехват сообщений в Софт-Айсе осуществляется командой BMSG. Подробности ее использования можно найти в документации или встроенной помощи. После ввода строки в окно редактирования и нажатия на ENTER отладчик всплывет со следующим сообщением: Break due to BMSG 0428 WM_GETTEXT (ET=513.11 milliseconds) hWnd=0428 wParam=001E lParam=28D70000 msg=000D WM_GETTEXT ^^^^^^^^^^^^^^^

Обратите внимание, что мы находимся в 16-разрядном сегменте и lParam это не 32-битное смещение, а 16-битное сегмент:смещение. Убедиться в этом можно, если вывести дамп этой области и дождаться выхода из процедуры. Если все сделано правильно, то в окне дампа окажется введенная строка. Теперь можно поставить на нее точку останова и обнаружить манипулирующий с ней код.

Впрочем, в данном примере он отсутствует. Crack0A просто демонстрирует один из вариантов обмена с окном. Аналогичным образом происходит и динамический обмен с окном. Подробное изложение его механизма несложно для понимания, и его можно найти в MSDN. Приблизительно же происходит следующее. Если содержимое окна изменено, то оно посылает сообщение EN_CHANGE (через WM_COMMAND), в ответ ему приходит запрос WM_GETTEXT. Такой механизм очень популярен и используется многими программистами. С другой стороны, все, что делает GetWindowText, - это посылает окну WM_GETTEXT и возвращает полученный результат.

Фактически удобнее и быстрее всегда перехватывать именно это сообщение, а не функции API или библиотек, которые очень трудно удержать в голове.

Ограничение времени использования
Другим популярным ограничением DEMO-версий является ограниченное время использования. Бывают по крайней мере два вида ограничений. В первом отсчет времени идет от момента первого запуска, а во втором программа работает до некоторой заранее установленной даты. Разумеется, первое гораздо удобнее, но и более уязвимо, т.к. необходимо где-то сохраниь дату первого запуска (причем убедиться, что он именно первый). Есть очень немного способов это сделать. Практически разработчики ограничены реестром или внешним файлом. Изменять код самой программы недопустимо, т.к. это вызовет протест со стороны антивирусов, а, значит, и со стороны использующих их клиентов. Под MS-DOS программы прошлого поколения могли писать в инженерные цилиндры жесткого диска, неиспользуемый конец последнего кластера файла, неиспользуемые поля CMOS. Сегодня ситуация изменилась. Современные операционные системы типа Windows NT вообще не дадут непривилегированному пользователю прямого доступа к диску. Идет активное внедрение сетевых технологий, а следовательно, защитный механизм должен успешно функционировать и на сетевой машине. Таким образом, практически единственной подходящей кандидатурой выглядит реестр. Однако все обращения к нему очень легко отследить и отредактировать. Или можно переустановить операционную систему, уничтожив реестр.

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

Рассмотрим для примера crack05 (file://CD/SRC/CRACK05/Crack05.exe) Программа при первом запуске запоминает текущую дату и по истечении 20 дней с этого момента прекращает работу. Переустановка (т.е. удаление и восстановление с оригинала) не помогает. Где же записан момент первого запуска? Быть может, в реестре? Это предположение нетрудно проверить любым монитором реестра. Запустим, например, "Regmon for Windows NT/9x" by Mark Russinovich. Теперь все обращения к реестру будут протоколироваться. Так выглядит протокол при первом запуске защиты: 40 Crack05 OpenKey HKCU\SOFTWARE\CRACK05 NOTFOUND 41 Crack05 CreateKey HKCU\SOFTWARE\CRACK05 SUCCESS hKey: 0xC29AF430 42 Crack05 SetValueEx HKCU\SOFTWARE\CRACK05 SUCCESS 0x36D3A94F 43 Crack05 CloseKey HKCU\SOFTWARE\CRACK05 SUCCESS

А так при последующих: 35 Crack05 OpenKey HKCU\SOFTWARE\CRACK05 SUCCESS hKey: 0xC29AFE60 36 Crack05 QueryValueEx HKCU\SOFTWARE\CRACK05 SUCCESS 0x36D3FC04 37 Crack05 CloseKey HKCU\SOFTWARE\CRACK05 SUCCESS

Попробуем удалить раздел HKEY_CURRENT_USER\SOFTWARE\CRACK05 (предварительно сделав резервную копию реестра). Последующий запуск защита воспримет как первый. На процедуру вскрытия ушло меньше пары минут. Однако, периодическое редактирование реестра утомительно и просто неудобно. Полноценный взлом предполагает полную блокировку защитного механизма, что мы сейчас и сделаем.

Протокол позволяет понять алгоритм работы защиты. Первоначально программа пытается найти в реестре раздел HKEY_CURRENT_USER\SOFTWARE\CRACK05. Если он отсутствует, то защита полагает, что на этом компьютере запущена впервые и записывает текущую дату. В противном случае вычисляется число дней с момента первого запуска. Можно изменить код так, чтобы независимо от результатов поиска управление всегда передавалось на ветку первого запуска.

Рассмотрим следующий код: 00401096 lea ecx, [esp+4] 0040109A lea edx, [esp+0Ch] 0040109E push ecx 0040109F push edx 004010A0 push 0 004010A2 push 0F003Fh 004010A7 push 0 004010A9 push 4031A4h 004010AE push 0 004010B0 push offset aSoftwareCrack0 004010B5 push 80000001h 004010BA call ds:RegCreateKeyExA 004010C0 test eax, eax 004010C2 jnz loc_4011C0

Найти в листинге дизассемблера его можно двояко - среди перекрестных ссылок на RegCreateKeyExA: 0040200C RegCreateKeyExA dd ? ;DATA XREF:sub_401040+7Ar

или по ссылке на строку aSoftwareCrack0: 00403088 aSoftwareCrack0 db 'SOFTWARE\CRACK05',0;DATA XREF:sub_401040+70o

Обратим внимание на строку 0x04010C2. Вопреки ожиданиям, изменять этот условый переход ни в коем случае не надо. Заглянув в SDK, можно узнать, что RegCreateKeyExA возвращает ненулевое значение в случае фатальной ошибки. А результат завершения операции передается через локальную переменную [esp+ 0x4]. Если раздел был успешно создан, то возращается единица, в противном случае раздел уже существует.

Тогда становится понятен смысл следующего фрагмента: 004010C8 cmp dword ptr [esp+4], 1 004010CD jnz short loc_401116

Чтобы защита каждый запуск считала первым, достаточно удалить условный переход jnz. Его можно заменить, например, на две однобайтовые операции nop. Попробуем сделать это сейчас. Переключим дизассемблер в режим hex-дампа и запишем, например, последовательность '83 7C 24 04 01 75 47'. Найдем ее в любом шестнадцатиричном редакторе и заменим на '83 7C 24 04 01 75 47'.

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

Рассмотрим другой пример реализации подобной защиты crack06. Использование монитора реестра нам ничего не дает. Быть может, программа сохранила дату в каком-нибудь файле? Обратимся к файловому монитору. Рассмотрим полученный протокол: 3495 Crack06 Open "C:\WINDOWS\SYSTEM\CRACK06.DAT" CREATENEW 3498 Crack06 Write "C:\WINDOWS\SYSTEM\CRACK06.DAT" Offset:0 Length:4 3499 Crack06 Close "C:\WINDOWS\SYSTEM\CRACK06.DAT"

Из него видно, что приложение создало новый файл в каталоге WINDOWS\SYSTEM. В нем легко затеряться среди сотен файлов, часто неясным образом созданных и неизвестно кому принадлежащих. В данном примере использовалось "говорящее" за себя имя, однако авторы защит склонны к бессмысленным комбинациям типа syswdg.dll Это признак низкой культуры программирования, не могущей служить образцом для подражания.

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

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

Рассмотрим несколько реализаций защитных механизмов, которые не так очевидны, как вышеописанные. Например, xformat 2.4 Криса Касперски сохранял месяц первого запуска в поле сотых долей секунды времени создания command.com . Антивирусы на это (как ни странно) не реагировали. Такое решение, очевидно, не слишком повышало стойкость защиты и не могло служить примером культурного программирования, но от неквалифицировнных пользоватетелей, вооруженных дисковыми сканерами, защищало надежно.

Некоторые защиты активно используют для этой цели незадействованные поля CMOS. Это очень примитивный способ, имеющий ряд серьезных ограничений. Защита слишком заметна и легко перехватывается. Действительно достаточно перехватить запись в порт 0x70, чтобы обнаружить защиту. Однако операционная система (наподобие win nt) не позволит напрямую обращаться к портам непривилегированным пользователям. Кроме того, CMOS не видна по сети. Наконец, зарезервированные поля могут быть использованы в новых версиях, что приведет к конфликтам и, возможно, к серьезным последствиям.

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

На самом деле несложно было найти по процедуре системного времени защитный механизм. Однако к системным часам существует множество способов доступа. Чтобы выяснить, какой именно использует приложение, необходимо ознакомиться с таблицой импорта. Так, например, crack07.exe импортирует только одну функцию, непосредственно связанную с опросом времени - GetTickCount. .text:00401109 call j_?GetTickCount!!AMPER!!CTime! !AMPER!!!!AMPER!!SG?AV1!!AMPER!!XZ;

Сейчас в eax адрес двойного слова, содержащего упакованную дату и время. .text:0040110E mov edx, [eax]

Загружаем упакованную дату\время в edx. .text:00401110 mov edi, ds:printf .text:00401116 sar edx, 0Fh

Избавляемся от часов, минут, секунд. .text:00401119 mov esi, 7000h

Упакованная дата окончания использования. .text:0040111E sub esi, edx

Вычисляем, сколько осталось времени для использования приложения. .text:0040112B test esi, esi .text:0040112D pop esi .text:0040112E jle short loc_0_401140 ^^^^^^^^^^^^^^^^^^^^^^^^^^^

Срок истек (нуль или отрицательное число). .text:00401130 push offset aWorking___

Ветка нормального исполнения программы. .text:00401135 call edi .text:00401137 add esp, 4 .text:0040113A mov eax, ebx .text:0040113C pop edi .text:0040113D pop ebx .text:0040113E pop ecx .text:0040113F retn

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

Старые приложения, выполняемые в среде MS-DOS не могут быть взломаны подобным образом, т.к. они не импортируют никаких функций и найти защитный механизм в дизассемблере может быть непростой задачей. Рассмотрим, например, crack07.exe, скомпилированный Турбо-Паскалем под MS-DOS. IDA 3.8 уверенно распознает стандартные функции, среди которых нетрудно найти GetDate, но что делать, если она недоступна?

На самом деле приложения под MS-DOS могут получить системную дату двумя способами - функцией операционной системы f.0x2A (int 0x21) или BIOS f.04 (int 0x1A). Практически не встречается считывание счетчика дней, прошедших с момента 10/1/86 (f.0Ah int 0x1A) или непосредственным чтением регистров CMOS. Перехватить функции указанных прерываний позволит практически любой отладчик, например soft-ice.

Поскольку сегодня приложения под MS-DOS медленно, но верно вымирают, мы не будем останавливаться на этом подробно.

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

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

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

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

Постоянное изменение счетчика позволяет, сравнив протоколы разных запусков, найти различия, которых обычно бывает немного. Один из них и будет искомым счетчиком.

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

Выяснить истину нам поможет отладчик или дизассемблер. В сегменте данных найдем строку: .data:00403050 aCount1 db 'Count1',0 ; DATA XREF: _main+CB .data:00403050 ; _main+122o ...

Перекрестные ссылки помогут нам выяснить, какой код читает или устанавливает значение этого раздела реестра. Я не буду приводить здесь его целиком, отмечу только ключевой фрагмент: .text:0040120F mov eax, [esp+5Ch+var_54] ; Count2 .text:00401213 mov edx, [esp+5Ch+var_4C] ; Count1 .text:00401217 xor eax, edx ^^^^^^^^^^^^^^^^

Расшифровываем значение счетчика. Count1 на самом деле ключ, а Count2 - зашифрованный счетчик. Такой примем позволит надежно скрыть защитный механизм от неквалифицированного пользователя, вооруженного редактором реестра. .text:00401219 dec eax

Уменьшаем значение счетчика на единицу. .text:0040122D test eax, eax .text:0040122F jz short loc_0_401296

Очередной промах компилятора. На самом деле инструкция test eax,eax не нужна, т.к. флаг нуля устанавливается инструкцией dec eax. Как нетрудно догадаться, это и есть тот самый условный переход, который по истечении отведенных запусков приложения прекращает его работу. В качестве тренировки читателю рекомендуется самостоятельно модифицировать его так, чтобы программа работала вечно. Разумеется, можно поступить иначе и удалить инструкцию dec, - кому как нравится. .text:00401231 push 0 .text:00401233 call ds:time .text:00401239 push eax .text:0040123A call ds:sran .text:00401240 add esp, 8 .text:00401243 call ds:rand

Генерируем случайное число - меняем ключ шифрования после каждого запуска. .text:00401249 mov edi, [esp+5Ch+var_54] ; Key .text:0040124D mov ecx, [esp+5Ch+var_50] ; Real Count .text:0040125B xor edi, eax

Зашифровываем новое значение счетчика. Для большей ясности я приведу фрагмент исходного текста, иллюстрирующий вышеизложенное: res=4; RegQueryValueEx(hKey,"Count1",0,&TYPE,(LPBYTE) &Count1,&res); RegQueryValueEx(hKey,"Count2",0,&TYPE,(LPBYTE) &Count2,&res); Count2 = Count2 ^ Count1; Count2--; printf("Count %x \n",Count2); if (!Count2) return 0; srand((unsigned)time( NULL ) ); Count1 = (unsigned) rand(); Count2 = Count2 ^ Count1; RegSetValueEx(hKey,"Count1",0,REG_DWORD,(CONST BYTE *) &Count1,4); RegSetValueEx(hKey,"Count2",0,REG_DWORD,(CONST BYTE *) &Count2,4);

Однако программисты в своем большинстве достаточно ленивы и заняты, чтобы активно использовать подобные приемы. Чаще всего счетчики записаны в реестре "AS IS" и легко могут быть изменены редактором реестра на любое другое значение, ограниченное совестью взломщика.

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

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

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

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

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

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

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

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

Рассмотрим типичный пример защиты file://CD/SRC/CRACK0A/Crack0A.exe. Если запустить этот примитивнейший текстовый редактор, то работе будет мешать периодически появляющееся диалоговое окно, закрыть которое в течение некоторого времени будет невозможно.

******************** Рисунок 0B *******************

Каким образом его можно убрать? Первое, что приходит в голову, - дизассемблировать приложение, найти код, создающий диалог и нейтрализовать его. Мысль, безусловно, правильная, но не дающая ответа на вопрос, как среди километров листинга дизассемблера найти нужный фрагмент. Установить точку останова на API-функцию создания диалога можно, но бесполезно. Код, вызывающий ее, находится где-то глубоко в недрах MFC42.DLL и совершенно неинтересен. На самом деле диалог запускается на выполнение функцией CDialog::DoModal(). Ее ординал равен 0x9D2. Получить его можно описанным выше способом с помощью dumpbin или IDA. Поскольку последнее подробно еще не рассматривалось, сделаем это сейчас. Загрузим файл в дизассемблер и откроем окно имен (ALT-V\N). Найдем в нем 'DoModal'. ??1CDialog!!AMPER!!!!AMPER!!UAE!!AMPER!!XZ 004020A0 ?DoModal!!AMPER!!CDialog!!AMPER!!!!AMPER!!UAEHXZ 004020A4 ^^^^^^^^ ?Enable3dControls!!AMPER!!CWinApp!!AMPER!!!!AMPER!!IAEHX 004020A8

Справа указан адрес ординала в таблице импорта. Было бы логично переключить дизассемблер в hex-режим (благо IDA это позволяет) и узнать, что по этому адресу находится. К сожалению, по непонятным мне причинам она отказываается это выполнить. Если в используемой вами версии этот недостаток не устранен, то на экране появится следующая белиберда: 004020A0 ?? ?? ?? ?? ?? ?? ?? ??-?? ?? ?? ?? ?? ?? ?? ?? "????????????????" 004020B0 ?? ?? ?? ?? ?? ?? ?? ??-?? ?? ?? ?? ?? ?? ?? ?? "????????????????" 004020C0 ?? ?? ?? ?? ?? ?? ?? ??-?? ?? ?? ?? ?? ?? ?? ?? "????????????????"

Если же заглянуть по этому адресу, скажем в hiew-е, то можно увидеть следующее: .004020A0: 81 02 00 80-D2 09 00 80-3D 0A 00 80-6E 04 00 80 .004020B0: 91 14 00 80-18 11 00 80-F5 12 00 80-86 13 00 80 .004020C0: A4 17 00 80-EF 06 00 80-B2 10 00 80-E7 18 00 80

По адресу 0x4020A4 находится слово 0x9D2 - это и есть ординал CDialog:: DoModal(). Интересно, что некоторые версии Софт-Айса находят его неправильно! Если, исходя из здравого смысла, установить точку останова на MFC42!ORD_09D2, то... можно потратить уйму времени, но так и не выяснить, почему же отладчик не всплывает, хотя диалог все же создается! Однажды я из-за этой удручающей ошибки НуМеги потратил десяток часов в поисках антиотладочного кода, которого на самом деле (словно пресловутой черной кошки в темной комнате) никогда и не было.

На самом деле необходимо дать команду BPX MFC42!09D1 - по совершенно магической причине это сработает.

Первое всплытие мы пропускаем, поскольку в создании главного диалога нет ничего интересного. Выйдем из отладчика и немного подождем. Вызов nag-screen-а защитой вызовет исключение, и мы окажемся в самом начале процедуры DoModal. Выйдем из нее командой p ret и изучим окружающий код. 015F:0040159C 8BF1 MOV ESI,ECX 015F:0040159E 8B4660 MOV EAX,[ESI+60] 015F:004015A1 85C0 TEST EAX,EAX 015F:004015A3 754F JNZ loc_0_4015F4 015F:004015A5 8D4C2404 LEA ECX,[ESP+04] 015F:004015A9 C7466001000000 MOV DWORD PTR [ESI+60],01 015F:004015B0 E89BFBFFFF CALL sub_0_401150 015F:004015B5 8D4C2404 LEA ECX,[ESP+04] 015F:004015B9 C78424B0000000000000MOV DWORD PTR [ESP+000000B0],0 015F:004015C4 E857020000 CALL CDialog::DoModal(void) 015F:004015C9 FF4E60 DEC DWORD PTR [ESI+60]

Необходимо найти причину появления диалога и обезвредить. С другой стороны можно не разбираться в защитном механизме, а удалить процедуру вызова диалога, заменив 'E8 57 02 00 00' в строке 0х04015C4 на '90 90 90 90', - но это самый варварский способ. Посмотрим лучше чуть выше, на бросающуюся в глаза констукцию: 015F:0040159E 8B4660 MOV EAX,[ESI+60] 015F:004015A1 85C0 TEST EAX,EAX 015F:004015A3 754F JNZ loc_0_4015F4

Куда ведет ветка loc_0_4015F4? Прокрутим окно немного вниз: loc_0_4015F4 015F:004015F4 8BCE MOV ECX,ESI 015F:004015F6 E857030000 CALL CWnd::Default(void) 015F:004015FB 8B8C24A8000000 MOV ECX,[ESP+000000A8] 015F:00401602 5E POP ESI 015F:00401603 64890D00000000 MOV FS:[00000000],ECX 015F:0040160A 81C4B0000000 ADD ESP,000000B0 015F:00401610 C20400 RET 0004

Несомненно, этот условный переход вызывает ветку, завершающую процедуру без создания диалогового окна. Если JNZ заменить на безусловный переход, то ветка защиты никогда не получит управления, - следовательно, nag-screen никогда не появится. Работу кракера на этом можно считать завершенной. Программа взломана, клиенты довольны, разве хоть что-то еще осталось? Но хакеры обладают пытливой натурой, поэтому назначение переменной [ESI+60] не может их не заинтересовать. С первого взгдяда все ясно. Это переменная типа BOOL. Если она равна TRUE, то NAG-SCREEN никогда не появится. Можно даже с уверенностью дать ей символьное имя 'Registered'. Однако взглянем на код защиты еще раз: 015F:0040159E 8B4660 MOV EAX,[ESI+60] ^^^^^^^^^^^^ 015F:004015A1 85C0 TEST EAX,EAX 015F:004015A3 754F JNZ loc_0_4015F4 015F:004015A5 8D4C2404 LEA ECX,[ESP+04] 015F:004015A9 C7466001000000 MOV DWORD PTR [ESI+60],00000001 ^^^^^^^^^^^^^^^^^^^^^^^^^^^ 015F:004015B0 E89BFBFFFF CALL sub_0_401150 015F:004015B5 8D4C2404 LEA ECX,[ESP+04] 015F:004015B9 C78424B0000000000000MOV DWORD PTR [ESP+000000B0],0 015F:004015C4 E857020000 CALL CDialog::DoModal(void) 015F:004015C9 FF4E60 DEC DWORD PTR [ESI+60] ^^^^^^^^^^^^^^^^^^ 015F:004015CC 8D4C2468 LEA ECX,[ESP+68] 015F:004015D0 C78424B0000000010000MOV DWORD PTR [ESP+000000B0],1

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

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

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

Продолжим трассировку или сразу выйдем из процедуры, командой p ret. Софт Айс покажет, что мы находимся глубоко внутри процедуры MFC42!ORD_142B (OnWndMsg!!AMPER!!CWnd) или, другими словами, в цикле выборки сообщений. Переданное сообщение находится по адресу ss:[ebp+8]. Легко видеть, что в нашем случае это 0x113, которое более известно как WM_TIMER (это можно выяснить командой WMSG 113).

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

Рассмотрим таблицу импорта crack0b.exe: USER32.dll F0 GetClientRect 252 SetTimer ^^^^^^^^^^^^^ 18C IsIconic 195 KillTimer B7 EnableWindow 146 GetSystemMetrics 19E LoadIconA

Попробуем найти код, который вызыает SetTimer, для чего установим на последнюю точку останова: 015F:004013CD MOV EAX,[ESI+20] 015F:004013D0 PUSH 00 015F:004013D2 PUSH 00002710 015F:004013D7 PUSH 01 015F:004013D9 PUSH EAX 015F:004013DA CALL [USER32!SetTimer] 015F:004013E0 MOV ECX,[ESP+0C] 015F:004013E4 MOV DWORD PTR [ESI+60],00000000

Очевидно, что, удалив CALL [USER32!SetTimer] вместе с заносимыми в стек параметрами, можно полностью парализовать защиту. Ветка вывода диалога попросту никогда не получит сообщения WM_TIMER и, следовательно, управления. Анализ защиты можно считать завершенным. Как видим, существует не один путь ее взлома. И любой способ ничем не хуже другого.

Рассмотрим еще один похожий пример file://CD/SRC/CRACK0C/Crack0C.exe. Первым бросающимся в глаза отличием является NAG-Screen, всплывающий при первом запуске программы. Очевидно, что его появление связано не с таймером, а с процедурой инициализации приложения. Другое (более существенное) отличие можно увидеть, если запустить spyxx или изучить таблицу импорта. Нет сообщения WM_TIMER, и нет в импорте процедуры SetTimer. Очевидно, приложение ухитряется с высокой периодичностью вызывать nag-screen не используя таймера. Самый очевидный способ это сделать - постоянно опрашивать текущее время и через некоторые промежутки передавать управление защите. Разумеется, организовать подобное можно либо непосредственно в цикле выборки сообщений, либо в отдельном потоке. По опыту могу сказать, что разработчики защит чаще всего предпочитают последнее. В чем можно убедиться, запустив 'Process Viewer Application', который входит в поставку MS VC.

************ Рисунок pc ****************

Приложение имеет два потока. Вполне возможно, что один из них целиком принадлежит защите и не делает ничего кроме постоянного опроса времени. Для подтверждения этого нам нужно изучить код потока и проанализировать его. Установим точку останова на CreateThread. Разумеется, приложение не вызывает его непосредственно, а действует через MFC. Но сейчас это для нас не важно. Вспомним прототип функции CreateThread или обратимся к SDK: HANDLE CreateThread( LPSECURITY_ATTRIBUTES lpThreadAttributes,// pointer to security attributes DWORD dwStackSize, // initial thread stack size LPTHREAD_START_ROUTINE lpStartAddress, // pointer to thread function LPVOID lpParameter, // argument for new thread DWORD dwCreationFlags, // creation flags LPDWORD lpThreadId // pointer to receive thread ID );

Таким образом, адрес процедуры потока можно узнать с помощью команды D ss:esp+0C и последующего дизассемблирования. Разумеется, это будет _beginthreadex модуля MSVCRT. Однако, немного потрассировав последнюю, можно добратся и до кода приложения. Здесь не помешает небольшой опыт работы с MFC и ее исследования. MicroSoft предоставляет отладочные версии и даже исходные тексты, поэтому архитектуру MFC изучить нетрудно, но очень и очень полезно. То же самое можно отнести и к другим компиляторам и библиотекам.

Так или иначе, но найти рабочий код приложения в потоке будет нетрудно. Изучать его непосредственно в отладчике затруднительно и неудобно. Дизассемблер в этом отношении окажется более подходящим. Рассмотрим результат его работы. .text:00401740 push ecx

ECX указывает на экземпляр класса, производного от CWinThread. .text:00401741 mov eax, [ecx+6Ch] .text:00401744 push esi .text:00401745 test eax, eax

Очевидно, что [ecx+6Ch] какой-то флаг. Но какой? На данном этапе это еще не ясно. .text:00401747 push edi .text:00401748 jz short loc_0_401750

Если этот флаг равен нулю, происходит выход из потока. Он может быть связан или с какой-то ошибкой, или с регистрацией. Так или иначе, если удалить условный переход JZ, поток никогда не получит управления и NAG-SCREEN не появится. Однако, это не будет полноценным взломом, т.к.при первом запуске программы по-прежнему будет появляться диалог. .text:0040174A pop edi .text:0040174B xor eax, eax .text:0040174D pop esi .text:0040174E pop ecx .text:0040174F retn

Остальное тело потока здесь не приводится для экономии места. Его содержимое не представляет ничего интересного. Поток - "стрелочник". Ему приказали - он выполнил, т.е. вывел диалог. Да, конечно, создание диалога нетрудно и блокировать, но это не даст ответа на вопрос: кто же за всем этим стоит? Убедимся, что этот поток всецело подчинен защите, т.е. ни для чего другого больше не служит.

Собственно говоря, непосредственно к защитному механизму могла относиться только переменная [ecx+6Ch], весь остальной код потока полностью автономен и ничем не управляется.

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

Сегодня программирование уже перестало быть искусством, и только отдельные энтузиасты-одиночки все еще считают его увлекательным хобби, на которое не жалко тратить все свободное время. Если вы принадлежите к их числу, то исследование защиты несомненно стоит продолжать, в противном случае можно удалить вызовы AfxMessageBox, чтобы NAG-SCREEN не появлялся ни при запуске, ни в дальнейшем.

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

Читатель, вероятно, уже запускает монитор. Посмотрим, что нам даст его применение: 43 Crack0c OpenKey HKCU\SOFTWARE\CRACK0C\RegIt NOTFOUND

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

****************** Рисунок pD **************

О чудо! Программа признала нас зарегистрированным пользователем! Исчезли NAG-SCREEN-ы, изменилась строка с "Ждите..." на "больше не появится...". Вот что значит качественный взлом!

Конечно, реальные защиты не только проверяют существование раздела, но и проверяют его значение. Здесь же для упрощения этого не делалось. Работа с реестром уже была рассмотрена выше и никаких трудностей не представляет. Более того, даже облегчает нам распространение "кряков" к программе. Достаточно только экспортировать раздел CRACK0C в файл: запустив его, пользователь импортирует раздел в свой реестр и тем самым регистрирует приложение. Надо ли говорить, что распространение подобного файла никак не противоречит российскому законодательству, а поэтому ограничено быть не может.

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

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

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

Однако чаще всего код все же физически присутствует, но не получает управления. Например, просто заблокированы некоторые пункты меню, как в file://CD/SRC/CRACK0D/Crack0D.exe . Такое действительно встречается очень часто и легко программируется. Все, что нужно сделать программисту, - это пометить в редакторе ресурсов некоторые элементы управления или меню как 'Disabled'. Но что просто делается, так же просто и ломается. Необходимо воспользоваться любым редактором ресурсорв. Я предпочитаю пользоваться 'Symantex ResourceStudio 1.0', однако пригоден и любой другой. Загрузим в него наш файл. Дальнейшие действия зависят от интерфейса выбранной программы и не должны вызвать затруднений, за исключением тех ситуаций, когда выбранный редактор не поддерживает используемого формата ресурсов или некорректно работает с ними. Например, с помощью Borland Resource WorkShop мне так и не удалось выполнить эту операцию. Он необратимо портил ресурс диалога, хотя с разблокированием меню справился отлично.

Чтобы разблокировать элементы управления или меню, необходимо вызвать свойства объекта и снять пометку 'Disabled' или 'Grayed', после чего сохранить изменения. Запустим программу, чтобы проверить нашу работу. Получилось! Не исправив ни одного байта кода и даже не прибегая к помощи дизассемблера и отладчика, мы осуществили взлом!

Удивительно, что такие защиты встречаются до сих пор, и не так уж редко. Психология разработчиков - воистину великая тайна. Очень трудно понять, на что они расчитывают. Однако некоторые уже, видимо, начинают догадываться, что нет ничего проще и приятнее, чем редактировать ресурсы в исполняемом файле, поэтому прибегают к явным вызовам API типа EnableWindow(false). Т.е. блокируют элементы управления непосредственно во время работы. Разумеется, можно перехватить этот вызов отладчиком и удалить защитный код. Именно так поступит любой хакер и даже кракер. Рядовой же пользователь остановит свой выбор на программе, подобной Customizer, которая позволяет "на лету" менять свойства любого окна, а впоследствии делать это и автоматически.

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

Именно так реализована защита, например, в crack0E. Откроем файл редактором ресурсов и убедимся, что все элементы разблокированы. Выключаются они позже, на стадии инициализации диалога, функциями API. Попробуем разблокировать их инструментом типа customizer-а. С первого взгляда кажется, что все в порядке. Но попробуем нажать кнопку "hello". Защита сообщает о незарегистрированной версии и вновь блокирует кнопку. Для простого пользователя такой барьер можно уже считать непреодолимым. Однако тому, кто знаком с ассемблером и отладчиком, не трудно нейтрализовать подобную защиту.

Обратимся к MSDN и введем в строке поиска "Disable Window". Среди полученных функций будет только одна, непосредственно относящаяся к Win32 API, - EnableWindow. Можно загрузить отладчик и установить на последнюю точку останова или поискать перекрестные ссылки на нее же в дизассемблере. Но этому я, надеюсь, уже научил читателя. Давайте усложним себе задачу и попробуем обойтись без этих чудес прогресса. В конечном счете гораздо интереснее работать головой, чем техникой.

Очевидно, что сообщение "Это незарегистрировнная копия" выдается защитным механизмом. Для этого он должен передать процедуре AfxMessageBox смещение этой строки. Разумеется, речь идет о смещении в памяти, а не в файле. Однако для PE файлов его легко узнать, например, с помощью HIEW. Эта утилита - единственная из всех мне известных шестнадцатиричных редкторов, позволяющая просматривать локальные смещения для PE файлов.

Находим строку "Это незарегестрированная копия", не забыв сменить кодировку, и переключаем Hiew в режим отображения локальных смещений. В нашем случае это будет 0х00403030. Не забывая про обратный порядок байтов в слове, ищем последовательность '30 30 40 00'. Если все сделать правильно, то получим только одно вхождение. Дизассемблируем прямо в hiew-е найденный код: .00401547: 8B4660 mov eax,[esi][00060] .0040154A: 85C0 test eax,eax .0040154C: 7516 jne .000401564 -------- (1) .0040154E: 6830304000 push 000403030 ;" !!AMPER!!00" ^^^^^^^^^ .00401553: E8C2020000 call .00040181A -------- (2) .00401558: 6A00 push 000 .0040155A: 8D4E64 lea ecx,[esi][00064] .0040155D: E8B2020000 call .000401814 -------- (3) .00401562: 5E pop esi .00401563: C3 retn

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

Найдем переменную, которая управляет выполнением программы. Очевидно, что это [esi+0x060]. Необходимо найти код, который управляет ее значением. Если его изменить на противоположное, то программа автоматически зарегистрируется.

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

На этот раз нам везет, и hiew выдает следующий любопытный фрагмент: .004013D3: 8B4C240C mov ecx,[esp][0000C] .004013D7: C7466000000000 mov d,[esi][00060],00000 .004013DE: 5F pop edi

Это не что иное, как самое сердце защиты. Обратите внимание на то, что приложение не предусматривает явной регистрации. Переменная инициализируется одним и тем же, ни от чего не зависящим значением. Т.е. демонстрационная и коммерческая версии - это по сути дела разные программы. Но отличающиеся всего одним байтом. Попробуем присвоить этой переменной ненулевое значение .004013D7: C7466000000000 mov d,[esi][00060],00001

и перезапустим программу. Сработало! Нам не пришлось даже анализировать алгоритм защиты. Изменив только один байт (переменную-флаг), остальное мы возложили на саму защиту. Ни в коем случае нельзя сказать, что мы ее нейтрализовали или модифицировали. Защита все еще жива и корректно функционирует.

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

Впрочем, разработчики далеко не всегда ограничиваются одним флагом. Таких переменных может быть несколько, и они необязательно будут связаны друг с другом. Это усложнит задачу взломщика, особенно если защита проверяет, чтобы все флаги были идентичны. Тогда не остается ничего, кроме тщательного анализа. В худщих реализациях бывает, что несоответствие флагов регистрации приводит не к вызову сообщений об ошибках, а к искажению алгоритма работы таким образом, что программа выглядит работающей, но фактически работает неправильно. Приведем пример: return SomeResult*(!FlagReg1 ^ FlagReg2);

Если два флага не равны друг другу, то в результате получится ноль! Функция вернет неверный результат. Если такое, например, случится в программе расчета зарплаты, то последствия не заставят себя ждать. Самое печальное, что флаги регистрации могут одновременно являться и рабочими переменными программы. Обычно при этом флагу выделяют младший бит, а все остальное отводят под нужды какой-нибудь функции. Тогда без тщательного анализа всего кода невозможно быть уверенным, что приложение функционирует корректно.

К счастью, программисты часто оказываются слишком ленивы, чтобы детально проработать эту архитектуру. И рождают перлы типа Crack0F. Рассмотрим этот защитный механизм. Перед нами две заблокированных кнопки. Очевидно, для локализации защиты нужно найти вызовы EnableWindow. j_?EnableWindow!!AMPER!!CWnd!!AMPER!!!!AMPER!!QAEHH!!AMPER!!Z proc near ; CODE XREF: sub_0_401360+D4p ; .text:004015CFp jmp ds:?EnableWindow!!AMPER!!CWnd!!AMPER!!!!AMPER!!QAEHH!!AMPER!!Z j_?EnableWindow!!AMPER!!CWnd!!AMPER!!!!AMPER!!QAEHH!!AMPER!!Z endp

Их всего два. Как раз по числу элементов управления. Пока защита не предвещает ничего необычного и ее код выглядит вполне типично: .text:0040142A mov eax, [esi+68h] .text:0040142D lea ecx, [esi+0ACh] .text:00401433 push eax .text:00401434 call j_?EnableWindow!!AMPER!!CWnd! !AMPER!!!!AMPER!!QAEHH!!AMPER!!Z ;

и, аналогично, другой фрагмент: .text:004015C8 mov eax, [esi+60h] .text:004015CB lea ecx, [esi+6Ch] .text:004015CE push eax .text:004015CF call j_?EnableWindow!!AMPER!!CWnd!!AMPER!! !!AMPER!!QAEHH!!AMPER!!Z ;

Попробуем найти, как уже было показано выше, '46 60', т.е. [esi+60] и '46 68'- [esi+68]. Полученный результат должен выглядеть следующим образом .00401385: C7466001000000 mov d,[esi][00060],000000000

и .004012CC: C7466801000000 mov d,[esi][00068],000000000

Кажется, что защита использует два независимых флага. С первого взгляда их нетрудно изменить на ненулевое значение. Ожидается, что это заставит защиту работать. Ну что ж, попытаемся это сделать.

Как будтобы все работает, не правда ли? Но попробуем нажать на левую кнопку:

************* рисунок pe ***********

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

Однако разработчики защит часто упускают из виду, что компилятор мог расположить все флаги близко от друг друга, значительно облегчая поиск кракеру. В самом деле, в нашем примере фигурируют две переменные типа DWORD - [esi+60] и [esi+68]. Нетрудно заметить, что между ними образовалась "дырка" размером ровно в двойное слово. Может быть, эта переменная - еще один флаг защиты? Попробуем найти '46 64': .004015B3: C7466400000000 mov d,[esi][00064],000000000

Что будет, если ноль заменить на единицу? Попробуем, и... сработало! Ранее пустой диалог теперь приветствует нас "Hello, Sailor!". Защита пала! Очевидно, что разработчик использовал по крайней мере три флага и конструкцию типа: s0.SetAt(0,s0[0]*(!RegFlag_1 ^ RegFlag_3));

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

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

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

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

Рассмотрим достаточно простой пример подобной защиты: fiel: //CD/SRC/CRACK10/Crack10.exe Это простой текстовой редактор, который при попытке сохранения отредактированного файла выводит диалоговое окно, информирующее об отсутствии такой возможности в демо-версии.

Найдем этот вызов и дизассемблируем его: .text:00401440 .text:00401440 NagScreen proc near ; DATA XREF: .rdata:00403648o .text:00401440 push 0 .text:00401442 push 0 .text:00401444 push offset unk_0_404090 .text:00401449 call j_?AfxMessageBox!!AMPER!!!!AMPER! !YGHPBDII!!AMPER!!Z .text:0040144E xor eax, eax .text:00401450 retn 4 .text:0040144E NagScreen endp .text:0040144E

Допустим, можно удалить вызов j_?AfxMessageBox!!AMPER!!!!AMPER!!YGHPBDII!!AMPER!!Z, но чего мы этим добьемся? Нет никаких сомнений в том, что код, обрабатывающий запись файла на диск, отсутствует. Впрочем, есть ненулевая вероятность, что он находится сразу после retn или где-нибудь поблизости. Это бывает при использовании следующих конструкций: BOOL CCRACK10Doc::OnSaveDocument(LPCTSTR lpszPathName) { AfxMessageBox("Это ограниченная версия. Пожалуйста, приобретайте полную"); return 0; return CCRACK10Doc::OnSaveDocument(lpszPathName); }

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

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

Однако гораздо большей проблемой, чем написание своего кода, станет его внедрение в уже откомпилированный exe-файл. Под MS-DOS эта проблема уже была хорошо изучена, но Windows обесценила большую часть прошлого опыта. Слишком велика оказалась разница между старой и новой платформами. С другой стороны, Windows принесла и новые возможности такой модификации. Например, помещение кода в DLL и простой вызов его оттуда. Подробное рассмотрение таких примеров требует целой отдельной книги, поэтому рассматриваемый здесь прием специально упрощен.

Вернемся к защите. Перейдем по единственной перекрестной ссылке, чтобы узнать, кто вызывает этот код. .rdata:00403644 dd offset j_?OnOpenDocument!!AMPER!!CDocument .rdata:00403648 dd offset sub_0_401440 ^^^^^^^^^^^^^^^^^^^ .rdata:0040364C dd offset j_?OnCloseDocument!!AMPER!!CDocument

Что представляют собой перечисленные смещения? Программисты, знакомые с MFC, безошибочно узнают в них экземпляр класса CDocument. Это можно подтвердить, если прокрутить экран немного вверх и, перейдя по одной из двух перекрестных ссылок, посмотреть на следующий фрагмент: 401390 sub_0_401390 proc near 401390 push esi 401391 mov esi, ecx 401393 call j_??0CDocument!!AMPER!!!!AMPER!!QAE!!AMPER!!XZ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 401398 mov dword ptr [esi], offset off_0_4035C8 40139E mov eax, esi 4013A0 pop esi 4013A1 retn 4013A1 sub_0_401390 endp

Становится ясно, что sub_0_401440 - это виртуальная функция CDocument::OnSavеDocument()! Но разработчик не передает управления последней, а выводит диалоговое окно и отказывается от записи.

А что если заменить sub_0_401440 на вызов функции по умолчанию OnSaveDocument? Для этого сначала необходимо узнать, импортируется ли эта функция программой или нет. Воспользуемся для этой цели IDA и изучим секцию rdata. К нашему глубокому сожалению, OnSaveDocument в таблице импорта отстутствует. Можно, конечно, вызвать любую функцию непосредственно из DLL или загрузить ее LoadLibrary. Это, разумеется, потребует немало места для размещения нового кода в файле. Но, к счастью, оно там с избытком имеется. Компилятор выравнивает прологи всех функций по границе 0x10 байт для оптимизации выполнения программы, поэтому остается много "дыр", которые взломщик может использовать для своих целей.

Это действительно очень просто, достаточно иметь минимальные навыки программирования под Windows. Однако, первая же попытка реализации сталкивается с серьезной трудностью. Чтобы вызвать функцию по адресу, необходимо наличие GetProcAddress, а приложение не импортирует ее. Печально на первый взгляд, но легко исправимо. Достаточно лишь слегка изменить таблицу импорта, чтобы включить недостающий вызов.

Обычно компиляторы всегда оставляют в файлах много пустого места, позволяющего немного расширить таблицу импорта. Чтобы это сделать, нужно знать формат PE файла, который описан, например, в MSDN. Покажем это на примере. Скопирум файл crack10.exe в myfile.exe . Теперь запустим HIEW 6.x (не ниже) и перейдем в секцию импорта. В самом ее начале расположен массив IMAGE_IMPORT_DESCRIPOR. Подробности о его структуре можно подчерпнуть в SDK или MSDN. Двойное слово, стоящее в начале, - это RVA (relative virtual address) указатель на структуру IMAGE_THUNK_DATA. Он-то нам и нужен. Преобразовать rva в локальное смещение внутри PE файла можно сложением его с image base, которую можно узнать из заголовка файла.

Что собой предстваляет IMAGE_THUNK_DATA? Это массив указателей на RVAFunctionName. Наглядно его можно представить, если изучать эту структуру в любом подходящем для вас шестнадцатиричном редакторе, например hiew. Что может быть интереснее копания в PE файле вручную, а не с помощью готового инструмента просмотра? Конечно, последнее намного проще и, может быть, приятнее, но не дает никаких полезных навыков. Хакер должен расчитывать не на технику, а только на свои руки и голову. Кракер же может, особо себя не утруждая, воспользоваться готовым редактором для таблиц экспорта\импорта (например PEKPNXE Криса Касперски) и всего лишь отредактировать одну строку, что не требует дополнительных объяснений. Ручная же работа с PE файлами, напротив, пока еще не слишком хорошо описана, а сам формат лишь отрывочно документирован. Единственным маяком в мире Windows был и остается заголовочный файл winnt.h, который содержит все необходимые нам структуры. (но, увы, не содержит комментариев к ним). Поэтому назначение некоторых полей придется выяснить самостоятельно. Для начала загрузим исследуемый файл в hiew. Можно было бы сразу вызвать секцию импорта, но в первый раз попытаемся для интереса найти ее вручную.

Заголовок PE файла начинается не в начале файла. Там расположена DOS-овская заглушка, которая нам совсем не интересна. Сам же PE файл начинается с одноименной сигнатуры. Двенадцатое (считая от нуля) двойное слово - это image base, который нам потребуется для вычислений, связанных с RVA: в нашем случае он равен 0x400000, что типично для Win32 файлов.

Теперь нам необходимо найти адрес таблицы импорта. Он стоит вторым в директории (первый - таблица экспорта). Под директорией здесь понимается структура, расположенная в конце OPTIONAL HEADERа и содержащая необходимую нам информацию. Я не привожу точного описания ее формата, отсылая читателя к MSDN и winnt.h . Настоящая книга не предназначена для пересказа существующей документации, и было бы бессмыслено тратить на это десятки страниц. Замечу, что на стадии подготовки книги это вызвало некоторые возражения у тех людей, которые не владели английским даже на уровне чтения технических текстов со словарем и не позаботились приобрести хотя бы электронную документацию, которая свободно поставляется с любым компилятором и доступна в Интернете на сайтах производителей, в первую очередь, MicroSoft. Увы, тут просто ничего не скажешь.

Итак, предположим, что мы уже выяснили, что таблица импорта располагается по адресу 0x40000+0x3A90=0x43a90. Перейдем к ее рассмотрению, а точнее к рассмотрению IMAGE_THUNK_DATA, которое мы уже затронули выше. Формат его данных очевиден из содержания: .00403E30: F6 3E 00 00-02 3F 00 00-16 3F 00 00-C6 3E 00 00 .00403E40: E6 3E 00 00-26 3F 00 00-44 3F 00 00-BE 3E 00 00 .00403E50: 6A 3F 00 00-A8 3E 00 00-9A 3E 00 00-86 3E 00 00 .00403E60: 36 3F 00 00-56 3F 00 00-D8 3F 00 00-00 00 00 00

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

Заглянув в документацию, мы можем убедиться в правильности нашей догадки. Попытаемся теперь, не обращаясь к документации, угадать - это указатели НА ЧТО? Логично предположить, что на непосредственно импортируемые функции. Все еще не обращаясь к документации, перейдем по одному из указателей: 0403F00: 6D 00 83 00-5F 5F 73 65-74 75 73 65-72 6D 61 74 m Г __setusermat ^ 0403F10: 68 65 72 72-00 00 9D 00-5F 61 64 6A-75 73 74 5F herr Э _adjust_ 0403F20: 66 64 69 76-00 00 6A 00-5F 5F 70 5F-5F 63 6F 6D fdiv j __p__com 0403F30: 6D 6F 64 65-00 00 6F 00-5F 5F 70 5F-5F 66 6D 6F mode o __p__fmo

Это действительно имена функций, а слово, стоящее перед ними, очевидно, ординал! Однако мы едва не упустили одну важную деталь - ведь существуют функции, которые экспортируются только по ординалу и символьная информация попросту не доступна. Неужели тогда ДВОЙНЫЕ СЛОВА-указатели будут расточительно указывать на СЛОВА-ординалы? Разумеется, нет: фирма MicroSoft в стремлении к оптимизации предотвратила такой вариант. В этом случае все элементы IMAGE_THUNK_DATA представляют собой не указатели, а непосредственно ординалы функций. Чтобы загрузчик мог распознать эту ситуацию, старший бит двойного слова равен единице. В результате, получается массив наподобие следующего: .00403B00: B2 10 00 80-86 11 00 80-FA 09 00 80-D0 09 00 80 .00403B10: 63 16 00 80-52 0F 00 80-41 04 00 80-4F 14 00 80 .00403B20: 5C 09 00 80-12 0D 00 80-B4 14 00 80-B6 14 00 80 .00403B30: A5 0A 00 80-EF 0F 00 80-5A 12 00 80-BB 14 00 80 .00403B40: A9 14 00 80-52 16 00 80-A6 0B 00 80-4B 0C 00 80

Любопытно, что в оптимизации Windows NT MicroSoft опередила сама себя, и в системных модулях все элементы вышеуказанного массива являются даже не ординалами, а непосредственными смещениями импортируемых функций. Это блестящее решение MicroSoft заслуживает глубокого уважения. Действительно, загрузчику почти совсем не остается работы, что экономит не одну сотню тактов процессора. Это лишний раз подтвержает, что "решение от MicroSoft" чаще все же ирония, чем горькая правда. И хотя Windows в целом оставляет мрачное впечатление (с точки зрения общего построения системы), в ее недрах спрятано немало интересных "конфеток". И в самом деле - ведь над ней работали весьма неглупые люди.

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

Взглянем на наш файл. RVA адрес первой структуры IMAGE_THUNK_DATA равен 0x3B00. Учитывая, что image base 0x400000, получаем локальное смещение 0x403B00. Как узнать, из какого модуля импортируются эти функции? Для этого заглянем в поле Name IMAGE_IMPORT_DESCRIPTOR, (четвертое двойное слово от начала). В нашем случае оно указывает на стоку 'MFC42.DLL' Именно в эту таблицу мы и должны добавить запись для OnSaveDocument. Разумеется, в таблице не будет свободного места, а за ее концом находится начало следующей. Кажется, ситуация неразрешимая... но подумаем немного. На каждую IMAGE_THUNK_DATA указывает всего одна ссылка. А что будет, если мы переместим одну из них в другое свободное место (которое наверняка найдется) и в освободившееся простанство внесем новую запись?

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

Благодаря выравниванию адресов на границе секций данных и ресурсов практически всегда есть бездна никем не занятого пространства. Переместим выделенную структуру, например, по адресу 0х404110. Для этого нужно скопировать блок с адреса 0х403E24 по 0х403E6B и записать его на новое место. Теперь освободившееся место можно использовать по своему усмотрению. Но прежде необходимо скорректировать ссылку на перемещенный фрагмент. Для этого найдем в IMAGE_IMPORT_DESCRIPTOR прежний RVA адрес и исправим его на новый.

Запустим файл: убедимся, что мы все сделали правильно и он работает. Приступим к ручному импортированию функции из файла. Это достаточно утомительный, но познавательный процесс, вынуждающий заглянуть "под капот" PE файла и понять, как он загружается и работает. Для начала изучим массив импортирумых функций: .00403B00: B2 10 00 80-86 11 00 80-FA 09 00 80-D0 09 00 80 ^^ ^^ ^^ ^^ .00403B10: 63 16 00 80-52 0F 00 80-41 04 00 80-4F 14 00 80 .00403B20: 5C 09 00 80-12 0D 00 80-B4 14 00 80-B6 14 00 80 .00403B30: A5 0A 00 80-EF 0F 00 80-5A 12 00 80-BB 14 00 80 .00403B40: A9 14 00 80-52 16 00 80-A6 0B 00 80-4B 0C 00 80

Видно, что все они импортируются по ординалу. И нам необходимо только добавить еще один. Находим в файле MFC42.map функцию OnSaveDocument и на основе полученного смещения определяем ординал с помошью dumpbin или любой другой аналогичной утилиты: получаем, что ее ординал 0x1359. Дописываем ее в конец таблицы. Запускаем dumpbin, чтобы удостовериться, что он заметил проделанные изменения. Однако это далеко не конец нашей работы, а скорее только ее начало. Что нам даст новая запись в IMAGE_THUNK_DATA? Честно говоря, ничего. Нам нужно узнать адрес функции после загрузки, а как это сделать? Для этого существует еще одно поле в IMAGE_IMPORT_DESCRIPTOR - это пятое двойное слово, указывающее адрес массива, в каждый элемент которого загрузчик операционной системы запишет реальный адрес импортируемой функции. В нашем случае для MFC42.DLL такая структура расположена по адресу 0x40300C. Рассмотрим ее более детально, но сначала обратим внимание на то, что адрес 0x40300C находится за пределами секции импорта и принадлежит уже секции .rdata . Это обстоятельство на самом деле очень важно, т.к. иначе загрузчик просто не смог бы получить доступ к памяти на запись, а следовательно, изменить значение. Таким образом, эта таблица перемещаема только в пределах .rdata . Но что она собой представляет? Гораздо проще и быстрее выяснить это самостоятельно, чем искать в документации среди множества бесполезной для нас сейчас информации. Рассмотрим ее более детально: .00403000: 8C 3F 00 00-78 3F 00 00-00 00 00 00-B2 10 00 80 ^^ .00403010: 86 11 00 80-FA 09 00 80-D0 09 00 80-63 16 00 80 .00403020: 52 0F 00 80-41 04 00 80-4F 14 00 80-5C 09 00 80 .00403030: 12 0D 00 80-B4 14 00 80-B6 14 00 80-A5 0A 00 80 .00403040: EF 0F 00 80-5A 12 00 80-BB 14 00 80-A9 14 00 80

Не правда ли, эти таблицы идентичны? И та и другая перечисляет ординалы. Однако между ними все же есть существенная разница. Первая сохраняется неизменной на всем протяжении работы, а последняя замещается реальными адресами импортируемых функций уже на стадии загрузки. И именно ее приложение использует для вызовов типа CALL DWORD PTR [0x403010].

Очевидно, что в случае импорта по имени все элементы таблицы будут указателями на ASCIIZ-строки с именем и ординалом функции. Заглянув в MSDN, можно с гордостью констатировать тот факт, что мы нигде не ошиблись в наших предположениях. Со временем большинство исследователей недр Windows все реже и реже заглядывают в документацию, поскольку многое и так достаточно очевидно и не требует разъяснения.

Печально, что это служит примером для начинающих и неопытных хакеров, которые отказываются от документации вообще. В результате они тычутся вслепую или начинают задавать глупые вопросы наподобие таких: "какой функцией Windows открывет файл. Я установил на OpenFile точку останова, а она не сработала. Почему?" Действительно, общий объем документации для разработчика Win32 столь велик, что даже беглый просмотр заголовков отнимет не один месяц времени. Это верно. Еще про Windows 3.1 говорили, что нужно не меньше года обучения, чтобы стать полноценным программистом под эту платформу. Насколько же все усложнилось с тех пор! Самое обидное, что на этом фоне делается основной упор на каркасные библиотеки типа MFC, технологии OLE и ActiveX, а системному программированию просто не остается места - ни в умах разработчиков, ни в документации. Лозунг "все, что Вам нужно, уже сделано компанией MicroSoft" сейчас очень популярен, но многих людей (включая и меня) он приводит в ярость. Программисты старшего поколения до сих пор любят все делать своими руками и не передают выполнения своей программы чужому коду, пока его не изучат.

Полноценным системщиком может стать лишь тот, кто откажется от MFC и C++ и попробует написать несколько серьезных приложений на старом добром Си. Даже не на ассемблере, а на простом высокоуровневом языке. Непосредственное общение с Win32 может с первого взгляда показаться пугающим, но только так можно почувствовать архитектуру системы. Без этого говорить о хакерстве просто смешно.

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

Впрочем, если быть до конца честным, большинство компиляторов генерируют хорошо известные: .004018D0: FF25C4314000 jmp MFC42.4612 .004018D6: FF2590314000 jmp MFC42.4610 .004018DC: FF2594314000 jmp MFC42.6375 .004018E2: FF2510304000 jmp MFC42.4486 .004018E8: FF2514304000 jmp MFC42.2554 .004018EE: FF2518304000 jmp MFC42.2512 .004018F4: FF251C304000 jmp MFC42.5731 .004018FA: FF2520304000 jmp MFC42.3922 .00401900: FF2524304000 jmp MFC42.1089

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

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

Поскольку сразу за концом IMAGE_IMPORT_DESCRIPTOR следует IMAGE_THUNK_DATA, то очевидно, что добавить еще одну запись можно только в том случае, если переместь одну из двух на свободное место. Первая несравненно короче, поэтому и найти бесхозное пространство для нее легче. Строго говоря, нам необходимо разместить ее в пределах таблицы импорта, и никто не разрешит перемещать ее в секцию .data - получится перекрывание секций, и последствия не заставят себя ждать... hiew "заругается" на такой файл. И, пожалуй, все. Действительно, если изучить код загрузчика Windows, становится ясно, что ему совершенно все равно, в какой секции расположена таблица импорта, и, более того, совершенно безразличен размер последней, а точнее, его соответствие с реальным. Конец определяется null-записью.

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

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

Скопируем IMAGE_IMPORT_DESCRIPTOR в любое свободное место секции данных и изменим на нее ссылку в Import Directory. Теперь нам необходимо создать в ней новую запись. Начнем с четвертого двойного слова, указывающего на имя функции. Можно сослаться на уже существующую строку 'MFC42.DLL' или создать свою и указать на нее. Последнее дает нам больше свободы и независимости. Поэтому поступим именно так: .004041D0: 4D 46 43 34-32 2E 44 4C-4C 00 00 00-00 00 00 00 MFC42.DLL

Итак, имя экспортируемого модуля мы уже записали. Теперь необходимо создать массив IMAGE_THUNK_DATA ("массив" громко сказано, всего лишь одну запись). .004041E0: 59 13 00 80-00 00 00 00-00 00 00 00-00 00 00 00 Y А

Понятно, что 0x1359 и есть импортируемая функция OnSaveDocument, а старший бит 0x8000 указывает, что последняя импортируется по ординалу. Остается создать таблицу адресов - точнее, таблицу создавать нет никакой необходимости. Несмотря на то что каждый ее элемент должен по теории ссылаться на соответствующую функцию, оптимизация загрузчика привела к тому, что он никак не использует начальные значения таблицы адресов, а вносит записи в том порядке, в котором они перечислены в таблице имен (IMAGE_THUNK_DATA). Поэтому достаточно лишь найти незанятое пространство и установить на него указатель в последнем поле IMAGE_IMPORT_DESCRIPOR.

Однако тут мы наталкиваемся на серьезные ограничения. Загрузчику на запись доступна только .rdata, в которой - скажем так - свободного места не густо. Более того, ни один элемент нельзя перемещать, поскольку ссылки на него разбросаны по всему коду программы. Остается только надеяться, что в результате выравнивания в конце таблицы найдется немножко пространства для наших целей. И действительно, несколько десятков байт свободно. Для нас этого более чем достаточно. 0403FC0: 57 69 6E 64-6F 77 00 00-55 53 45 52-33 32 2E 64 Window USER32.d 0403FD0: 6C 6C 00 00-AA 01 5F 73-65 74 6D 62-63 70 00 00 ll к_setmbcp 0403FE0: 00 00 00 00-00 00 00 00-00 00 00 00-00 00 00 00 0403FF0: 00 00 00 00-00 00 00 00-00 00 00 00-00 00 00 00

Остается только скорректировать IMAGE_THUNK_DATA. Финальный вариант может выглядеть так: 0404160: E0 41 00 00-00 00 00 00-00 00 00 00-D0 41 00 00 рA ¦A 0404170: E0 3F 00 00-00 00 00 00-00 00 00 00-00 00 00 00 р?

Убедимся с помощью dumpbin, что он исправно работает. MFC42.DLL 403FE0 Import Address Table 4041E0 Import Name Table 0 time date stamp 0 Index of first forwarder reference Ordinal 4953

Если заглянуть отладчиком по адресу 0x403FE0, то там мы обнаружим готовый к употреблению адрес функции OnSaveDocument. Проверим, что это действительно так. Дизассемблируем (командой u в soft-ice) этот регион памяти. При этом отладчик должен вывести в прологе ординал функции. Это убеждает нас, что все работает. Остается эту функцию всего лишь вызвать. Для этого вернемся далеко назад, когда мы нашли перекрытую функцию OnSaveDocument. Нам стоит переписать ее. Рассмотрим код еще раз: .00401440: 6A00 push 000 .00401442: 6A00 push 000 .00401444: 6890404000 push 000404090 .00401449: E812070000 call AfxMessageBox .0040144E: 33C0 xor eax,eax .00401450: C20400 retn 00004

Очевидно, что ее нужно переписать, - например, следующим образом: .00401440: FF742404 push d,[esp][00004] .00401444: 90 nop .00401445: 90 nop .00401446: 90 nop .00401447: 90 nop .00401448: 90 nop .00401449: 2EFF15E03F4000 call d,cs:[000403FE0] .00401450: C20400 retn 00004

Для понимания этого обратимся к SDK. Вот какой прототип имеет функция virtual BOOL OnSaveDocument( LPCTSTR lpszPathName );

Отсюда вытекает строка push dword [esp][00004], остается объяснить вызов функции. Как мы помним, загрузчик в ячейку 0x403FE0 записал ее адрес, - он и был использован для вызова. И это все! Мы дописали недостающий код. Этот момент очень важен. Читатель может упрекнуть меня за выбор искусственной ситуации. Действительно, часто ли встречаются подобные примеры в жизни? Даже применительно к MFC используемая функция с большой степенью вероятности может быть перекрыта функцией разработчика. Как быть тогда?

Но не спешите. Пусть функция перекрыта, тогда положение осложняется лишь тем, что хакеру сперва нужно будет понять ее алгоритм, а затем воссоздать недостающий код и... поместить его в собственную DLL, а оттуда уже аналогичым образом сделать вызов. При этом нет надобности изощряться и втискивать код в скудные клочки пустого места, беспорядочно разбросанные по файлу. Можно выбрать любое симпатичное средство разработки (например MS VC) и написать на нем недостающую функцию, используя всю мощь MFC и объективно-ориентированного Си++. Это гораздо легче и, кроме того, по-просту удобно.

Для модификации старых exe для MS-DOS обычно использовался только ассемблер. С одной стороны, это было приятно (разумеется, для поклонников этого языка), а с другой - утомительно. Кроме того, в Windows гораздо легче понять взаимодействие различных фрагментов программы, т.к. здесь очень много избыточной информации, а объективно-ориентированные языки (которые доминируют последнее время) оперируют в основном локальными структурами и переменными. К тому же здесь меньше тех ужасных глобальных объектов общего использования, которые непонятно для кого предназначены и как используются. Особенно если программист в погоне за минимизацией требуемой памяти использует одну и ту же переменную повторно, когда предыдущей процедуре она уже не нужна. Допустим, при старте программы пользователь ввел пароль, который был сравнен с некоторой эталонной строкой. Ясно, что во время работы программы эта область памяти может быть отведена под нужды других процедур, если пароль сравнивается только один раз. Из этого следует, что мы получим множество перекрестных ссылок и долго будем чесать в затылке, размышляя "почему это с паролем-то так интенсивно работают?"

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

Вернемся к нашему примеру. Попробуем его запустить. Появляется другое диалоговое окно, с сообщением об ограниченности версии. Выходит, автор защиты предусмотрел двойную проверку. Выкинул ли он еще кусок кода или только вернул управление? Чтобы это выяснить, необходимо изучить вызывающий это сообщение код. Не будем прибегать к столь мощному инструменту как IDA, а воспользуемся компактным и шустрым hiew-ом. Достаточно лишь найти ссылку на строку, смещение которой можно узнать, заглянув в сегмент данных. После чего нетрудно будет найти следующий фрагмент: .00401410: 8B442404 mov eax,[esp][00004] .00401414: 8B5014 mov edx,[eax][00014] .00401417: F7D2 not edx .00401419: F6C201 test dl,001 .0040141C: 7411 je .00040142F ^^^^^^^^^^^^^^^^^^^ .0040141E: 6A00 push 000 .00401420: 6A00 push 000 .00401422: 6854404000 push 000404054 ; << строка .00401427: E834070000 call AfxMessageBox .0040142C: C20400 retn 00004 ;" .00401430: 8B4130 mov eax,[ecx][00030] .00401433: 8B4808 mov ecx,[eax][00008] .00401436: E81F070000 call Serialize .0040143B: C20400 retn 00004

MFC-программистам нетрудно понять, как он работает. Если происходит запись файла, то edx становится равно единице, если чтение - то нулю. Именно на этом и построена защита. В оригинале это могло выглядеть приблизительно так: void CCRACK10Doc::Serialize(CArchive& ar) { // CEditView contains an edit control which handles all serialization if (ar.IsStoring()) { AfxMessageBox("Это ограниченная версия. Пожалуйста, приобретайте полную"); return; } ((CEditView*)m_viewList.GetHead())->SerializeRaw(ar); }

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

Попробуем убрать ret, заменив его, скажем, на nop. казалось бы это не отразится на работоспособности программы. Однако, запустив программу и попытавшись сохранить файл, мы получаем до боли знакомый GPF - "программа выполнила некорректную операцию и будет завершена". В чем же дело? С первого взгляда этот вопрос нас ставит в тупик, поэтому воспользуемся отладчиком и внимательно потрассируем измененный фрагмент. Причина обнаруживается достаточно быстро. Функция AfxMessageBox не сохраняет регистров eax и ecx, а код, расположенный ниже, их использует, никак не предполагая, что их содержимое было изменено. Следовательно, забота о сохранении, точнее о написании соответствующего кода, ложится на плечи взломщика. Это нетрудно и даже не утомительно - добавить пару команд push и pop, но как-то неаккуратно выглядит. Действительно, между условным переходом и вызовом функции нет свободного пространства. Можно, конечно, сместить всю функцию немного вниз, для чего свободного места предостаточно, но, может быть, можно найти решение с изменением меньшего числа байт? В самом деле, если убрать not, а je заменить на jne, мы получим два байта - как раз столько, чтобы сохранить пару регистров. Однако это потребует коррекции точки перехода, поскольку команды push расположены "выше" ее. В итоге мы получим такой вариант: .00401417: 50 push eax .00401418: 51 push ecx .00401419: F6C201 test dl,001 .0040141C: 750E jne .00040142C .0040141E: 6A00 push 000 .00401420: 6A00 push 000 .00401422: 6854404000 push 000404054 .00401427: E834070000 call .000401B60 .0040142C: 59 pop ecx .0040142D: 90 nop .0040142E: 90 nop .0040142F: 90 nop .00401430: 8B4130 mov eax,[ecx][00030] .00401433: 8B4808 mov ecx,[eax][00008] .00401436: E81F070000 call .000401B5A .0040143B: C20400 retn 00004

Удостовертесь, что он действительно работает! Однако это еще не предел, и существует множество более изящных решений. Попробуйте найти их: и вы получите истинное удовольствие.

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

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

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

Можно рассмотреть два типа атаки. В первом хакер воссоздает ключевой файл, не затрагивая сам защитный механизм, а во втором модифицирует последний так, чтобы удалить проверки наличия\валидности ключевого файла. Обычно хакеры выбирают первое, поскольку это зачастую проще и вызывает меньше конфликтов с законодательством. Теоретически "левый" ключевой файл можно объявить поддельным, но чтобы за это привлечь к уголовной ответственности, формат ключевого файла должен быть защищен авторскими правами, что далеко не всегда возможно. Действительно, сам формат не может быть защищен по нашему законодательству. Можно защитить, скажем, используемую функцию шифровки. Но для этого нужно ее разработать. А авторы защит, напротив, сами склонны "втихаря" применять патентованные алгоритмы, ничего не отчисляя за их использование. Кроме того, не известен еще ни один судебный процесс, который был бы возбужден по этому поводу. Это, конечно, никак не должно толкать на нарушение закона. Впрочем, и законов-то, относящихся к ключевым файлам, нет. В их отсутствии каждый должен руководствоваться собственной моралью и этикой. Другими словами, я призываю читателя использовать свои умения и навыки не для нанесения кому-либо ущерба, а только в образовательных и познавательных целях.

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

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

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

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

Рассмотрим простой пример crack11. Для начала попытаемся определить, предусматривает ли программа регистрацию и если да, то какую. Для этого необходимо найти ссылку на строку "UNREGISTRED" и уточнить источник ее вызова. Если это условный переход, то очевидно разработчиком предусмотрен некий путь регистрации. .004011A5: 7517 jne .0004011BE .004011A7: 6838304000 push 000403038 ; "UNREG..." ^^^^^^^^^ .004011AC: FF1554204000 call printf ;MSVCRT.dll .004011B2: 83C404 add esp,004 ;" .004011B5: 33C0 xor eax,eax .004011B7: 81C444010000 add esp,000000144 ;" D" .004011BD: C3 retn .004011BE: 8D542431 lea edx,[esp][00031] .004011C2: 52 push edx .004011C3: 6820304000 push 000403020 ;"REG..." .004011C8: FF1554204000 call printf ;MSVCRT.dll

Не правда ли, наглядно? Регистрация и в самом деле предусмотрена. Осталось выяснить источник ввода, с которым манипулирует защита. Прокрутим экран немного вверх: .00401120: 51 push ecx .00401121: 684C304000 push 00040304C ; .00401126: FF1500204000 call FindFirstFileA;KERNEL32.dll

Очевидно, что регистрационная информация расположена в каком-то файле. Но в каком? На этот вопрос поможет ответить мониторинг файловых операций. Используем, например, Win95 File Monitor Марка Руссиновича. Его протокол очень интересен: 19 Crack11 FindOpen D:\KPNC\PHCK\SRC\VC\CRACK11\*.* SUCCESS . 20 Crack11 FindNext D:\KPNC\PHCK\SRC\VC\CRACK11\*.* SUCCESS .. 21 Crack11 FindNext D:\KPNC\PHCK\SRC\VC\CRACK11\*.* SUCCESS StdAfx.h 22 Crack11 FindNext D:\KPNC\PHCK\SRC\VC\CRACK11\*.* SUCCESS StdAfx.cpp 23 Crack11 FindNext D:\KPNC\PHCK\SRC\VC\CRACK11\*.* SUCCESS CRACK11.dsw 24 Crack11 FindNext D:\KPNC\PHCK\SRC\VC\CRACK11\*.* SUCCESS CRACK11.cpp

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

Алгоритм защитного механизма немного прояснился. В текущей директории ищется какой-то файл. Вероятно, защита даже не знает, какой точно, поэтому и не задает маски поиска. Похоже, что она проверяет некоторую дополнительную информацию, например время создания, модификации или длину. Все эти поля могут выделять ключевой файл среди остальных. Возможно, меня спросят: как же защита может узнать дату, если в протоколе отсутствует функция чтения времени? На самом деле тот, кто хоть немного сталкивался с программированием под Dos\Windows, должен помнить, что FindFirstFile/FindNextFile возращают не только имя найденного файла, но и целый "букет" его свойств. Заглянем в SDK, чтобы уточнить последнее. В описании FindFirstFile встретится ссылка на следующую структуру: typedef struct _WIN32_FIND_DATA { // wfd DWORD dwFileAttributes; FILETIME ftCreationTime; FILETIME ftLastAccessTime; FILETIME ftLastWriteTime; DWORD nFileSizeHigh; DWORD nFileSizeLow; DWORD dwReserved0; DWORD dwReserved1; TCHAR cFileName[ MAX_PATH ]; TCHAR cAlternateFileName[ 14 ]; } WIN32_FIND_DATA;

Какие поля использует защита, можно выяснить только с помощью дизассемблера\отладчика. Попробуем обойтись без IDA и проанализировать алгоритм с помощью одного hiew. Это совсем не так сложно, как может показаться на первый взгляд.

Начнем изучение логики работы защитного механизма с самого начала. "А где оно, это начало?" могут меня спросить. В самом деле, ответить не так просто. Однако достоверно известно, что FindFirstFileA лежит в непосредственной близости от начала защиты. Вот с этой точки мы и начнем свое путешествие по коду.

Прокрутим экран немного вверх, пока не встретим следующий код: .0040110E: 6850304000 push 000403050 ;

Посмотрим, на что указывает данное смещение: .00403050: 43 52 41 43-4B 20 4D 45-20 30 78 31-31 20 0A 00 CRACK ME 0x11

Это же первая выводимая программой строка! Следовательно, мы находимся в непосредственной близости от точки входа в защитный механизм. .00401113: FF1554204000 call printf ;MSVCRT.dll .00401119: 83C404 add esp,004 ; .0040111C: 8D4C2414 lea ecx,[esp][00014]

Выделяется локальная переменная. По тому, как она будет использована чуть ниже (заслана в стек), можно выяснить по прототипу функции, что это структура WIN32_FIND_DATA. .00401120: 51 push ecx

Вот эта переменная и засылается в стек. .00401121: 684C304000 push 00040304C ; .00403040: 52 45 44 20-43 4F 50 59-20 0A 00 00-2A 2E 2A 00 RED COPY *.* ^^^^^^^^^^^ ^^^

А это, как видно, маска файлов для поиска. .00401126: FF1500204000 call FindFirstFileA;KERNEL32.dll

Нашли первый подходящий файл. С этого момента нужно ожидать вызовов FindNextFile. .0040112C: 8B2D0C204000 mov ebp,[00040200C]

А это что за смещение? Смотрим... .0040200B: 008021000000 add [eax][000000021],al

С первого взгляда непонятно. Но обратим внимание: эта область находится в таблице адресов импорта kernel32.dll. Строго говоря, мы не можем твердо рассчитывать, что 0x2180 действительно указывает на импортируемую функцию. Как было показано в предыдущей главе, загрузчик попросту игнорирует записанное в ней значение и вычисляет адреса функций в том порядке, в каком они перечислены в таблице имен. Однако компиляторы, несмотря ни на что, все же правильно заполняют эту структуру, и если над ней не "поработал" разработчик защиты, то ее значение должно соответствовать истине. Попробуем в этом убедиться: .00402180: 9D 00 46 69-6E 64 4E 65-78 74 46 69-6C 65 41 00 Э FindNextFileA

Выглядит правдоподобно. Таким образом, в ebp помещен адрес импортируемой функции, и все вызовы call ebp следует читать как call FindNextFileA. .00401132: 8BF8 mov edi,eax

В eax, как мы помним, был помещен результат выполнения функции FindFirstFileA. SDK нам скажет, что его ненулевое значение свидетельствует об успехе операции (и наоборот). .00401134: 33F6 xor esi,esi .00401136: 8A4C2440 mov cl,[esp][00040]

Это, очевидно, первый символ строки cFileName. Для того чтобы понять это, придется заглянуть в файлы определений и узнать размер переменной FILETIME. Далее, исходя из того что вся структура расположена в памяти по адресу [esp+0x14], нетрудно будет определить, к какому полю принадлежит [esp+0x40]. Однако обратим внимание на такую деталь: все остальные поля, кроме имен, - двойные слова. Выходит, CL не может быть ничем иным, кроме как именем файла. .0040113A: 32D2 xor dl,dl .0040113C: 84C9 test cl,cl

В свете вышесказанного смысл этой строки очевиден. Строка завершается нулевым символом, и test cl,cl проверяет это. .0040113E: 88542410 mov [esp][00010],dl

Объявление еще одной локальной переменной размером в байт. Начальное значение равно нулю, т.к. перед этим была выполнена операция xor dl,dl. .00401142: B801000000 mov eax,000000001 ;

Очевидно, регистровая переменная с начальным значением 1. .00401147: 741F je .000401168 -------- (1)

Условный переход выполняется только при достижении конца строки. .00401149: 8A4C0440 mov cl,[esp][eax][00040]

Посимвольный разбор имени файла, eax при этом индекс. Обратим внимание: начальное значение eax равно единице, поэтому и разбор строки происходит не с первого символа, а только со второго! Интуиция вам уже подсказывает, что в этом должен быть заключен какой-то определенный смысл. Или... ошибка разработчика. .0040114D: 0FBED9 movsx ebx,cl .00401150: 81E303000080 and ebx,080000003 ; .00401156: 7905 jns .00040115D .00401158: 4B dec ebx .00401159: 83CBFC or ebx,-004 ; .0040115C: 43 inc ebx

Этот очень витиеватый код, возможно, заставит вас призадуматься и окажется увлекательной головоломкой. Это маленькое чудо запрограммировано разработчиками MS VC. Действительно призадумаешься, прежде чем сказать в адрес MicroSoft пару крепких бранных слов. Уж больно этот код хорош. При ближайшем рассмотрении оказывается, что это аналог функции x mod 4, но насколько же хорошо он оптимизирован!

Если это так, то, похоже, защита зачем-то вычисляет хеш-сумму от имени файла. В том, что это хеш, сомнений практически нет. Теперь вспомним, что первый символ имени был пропущен, а сама хеш-сумма вычисляется в байтовой переменной. Не правда ли, это наводит на мысль, что первый символ имени файла и есть контрольная сумма остальных? И весь этот механизм придуман для того, чтобы отличать ключевые файлы от остальных. .0040115D: 02D3 add dl,bl

DL в данном случае выступает в роли накопителя хеш-суммы. .0040115F: 40 inc eax

Перемещает индекс на следующий символ. .00401160: 84C9 test cl,cl .00401162: 75E5 jne .000401149 -------- (1)

Вспомним, что CL представляет собой последний анализируемый символ имени файла. А вся эта конструкция проверяет, не является ли он завершающим нулем. В противном случае продолжается цикл подсчета хеш-суммы. .00401164: 88542410 mov [esp][00010],dl

После выхода из цикла вычисления хеша логично ожидать, что теперь этот хеш будет сравниваться с эталонным, предположительно - с первым символом имени. Однако нам встречается очень странный цикл. .00401168: 48 dec eax

Уменьшаем индекс. Очевидно, разбор имени пошел в обратном направлении. .00401169: 740E je .000401179 -------- (2)

Проверка, что индекс не равен нулю. Иначе говоря, шапка цикла - от текущей позиции и до первого символа. .0040116B: 8A54043F mov dl,[esp][eax][0003F]

Загружаем очередной символ строки. .0040116F: 80F206 xor dl,006 ;

Ксорим его. Правда, еще не понятно зачем. .00401172: 8854043F mov [esp][eax][0003F],dl

И записываем обратно. Похоже, что это цикл расшифровки строки. Вероятно, в ее имени содержится что-то важное. .00401176: 48 dec eax .00401177: 75F2 jne .00040116B -------- (3)

А это уже глюк компилятора. Неоптимально организованный цикл. .00401179: 0FBE542440 movsx edx,b,[esp][00040]

Наконец-то мы обращаемся к первому символу. Заметим, что теперь уже расшифрованному. .0040117E: 8B442410 mov eax,[esp][00010]

А это вычисленная хеш-сумма. Действительно, похоже, что сейчас их будут сравнивать. .00401182: 83EA41 sub edx,041 ;"A"

Вычитаем из символа 'A'. Действительно, чтобы имя было читаемым, разработчику защиты пришлось добавить к хеш-сумме этот символ, иначе не было бы попадания в нужный диапазон. .00401185: 25FF000000 and eax,0000000FF ;" "

Преобразуем результат в байт. .0040118A: 3BC2 cmp eax,edx

Сравниваем значения друг с другом. .0040118C: 7505 jne .000401193 -------- (4)

Если два значения идентичны, то флаг esi устанавливается в единицу. (показан ниже) В противном случае он остается равным нулю. .0040118E: BE01000000 mov esi,000000001

Вот очень хороший кандидат на глобальный флаг регистрации. .00401193: 8D4C2414 lea ecx,[esp][00014] .00401197: 51 push ecx .00401198: 57 push edi .00401199: FFD5 call ebp

Вызыв FindNextFile .0040119B: 85C0 test eax,eax .0040119D: 7597 jne .000401136 -------- (5)

Продолжать цикл до тех пор, пока все файлы в текущем каталоге не будут исчерпаны. .0040119F: 5F pop edi .004011A0: 85F6 test esi,esi

А вот и проверка флажка! .004011A2: 5E pop esi .004011A3: 5D pop ebp .004011A4: 5B pop ebx .004011A5: 7517 jne .0004011BE -------- (2)

Очевидно, что переход осуществляется только когда он равен единце. В противном случае программа считается незарегистрированной. .004011A7: 6838304000 push 000403038 ;

Посмотрим, куда он указыавает: .00403030: 20 25 73 20-0A 00 00 00-55 4E 52 45-47 49 53 54 %s UNREGIST .00403040: 52 45 44 20-43 4F 50 59-20 0A 00 00-2A 2E 2A 00 RED COPY *.*

Точно, это незарегистрированная ветка. .004011AC: FF1554204000 call printf ;MSVCRT.dll .004011B2: 83C404 add esp,004 .004011B5: 33C0 xor eax,eax .004011B7: 81C444010000 add esp,000000144 .004011BD: C3 retn

А вот эта ветка получает управление, только когда esi равен единице. .004011BE: 8D542431 lea edx,[esp][00031]

Постойте-ка, а ведь edx указывает на расшифрованную строку! Очевидно для того, чтобы вывести ее на экран. .004011C2: 52 push edx

Точно, prinft выводит ее! .004011C3: 6820304000 push 000403020 ;" !!AMPER!!0 " .00403020: 52 45 47 49-53 54 45 52-45 44 20 46-4F 52 20 3A REGISTERED FOR :

Так вот что это за строка - это имя пользователя! .004011C8: FF1554204000 call printf ;MSVCRT.dll

Защита оказалась довольно любопытной. Она сохраняла регистрационную информацию непосредственно в имени файла. Не самый плохой вариант, особенно учитывая, что длинные имена win95 позволяют вместить до 260 символов, что приблизительно равно размеру среднего ключевого файла. Любопытно, что некоторые разработчики заполняют такие файлы парой килобайт "мусора" для сбивания с толку. Интересно, насколько же наивным надо быть, чтобы думать, будто такой трюк может кого-то запутать.

Мы разобрали алгоритм защиты. А как ее сломать? Можно, конечно, в строке 0х0401134 установить начальное значение esi в единицу и тогда независимо от наличия ключевого файла окажется зарегистрированной. Но это не лучший путь, т.к. теперь программа выдает вместо имени регистрации откровенный мусор. Чтобы этого избежать, придется либо отключить вывод строки на экран, заменив его, скажем на другую строку с нашими инициалами, либо написать собственный генератор. Последнее, конечно, предпочтительнее.

Еще замечание. На самом деле автор предусмотрел два уровня защиты, последний из которых неплохо замаскирован и с первого взгляда вряд ли будет замечен. Рассмотрим еще раз фрагмент: .0040118A: 3BC2 cmp eax,edx .0040118C: 7505 jne .000401193 -------- (4) .0040118E: BE01000000 mov esi,000000001 .00401193: 8D4C2414 lea ecx,[esp][00014] .00401197: 51 push ecx .00401198: 57 push edi .00401199: FFD5 call ebp .0040119B: 85C0 test eax,eax .0040119D: 7597 jne .000401136 -------- (5) .004011BE: 8D542431 lea edx,[esp][00031]

Подумаем, что будет, если ключевой файл окажется не последним в директории. Тогда edx в строке 0х04011BE будет указывать на имя совсем другого файла, превращенное расшифровкой в мусор, а вовсе не на регистрационную строку. Для чего это сделано? Очевидно, что ключевой файл создается последним в директории и все исправно работает. Но лишь до того момента, пока файлы этого каталога не будут куда-нибудь скопированы так, что ключевой файл окажется не последним в списке. Защита откажет в работе! Не правда ли, оригинальный способ защититься от копирования? Но какому пользователю это понравится? Он хочет получить полноценную копию, не обремененную ограничениями защиты. И мы поможем ему в этом. Но сначала создадим хотя бы один корректный ключевой файл.

Не будем спешить писать свой генератор. Гораздо проще положиться на сам защитный механизм и просто "подсмотреть" правильную строку и контрольную сумму для нашего имени. Создадим файл (или каталог - безразлично, но последнее приятнее) и дадим ему имя 'XKRIS KASPERSKI', где 'X' - зарезервированный символ для будущей контрольной суммы.

Теперь достаточно остановиться в строке 0х040118A и подсмотреть сгенерированную защитой строку. Необходимо воспользоваться отладчиком. Но каким? Ведь кроме Софт-Айса в мире немало и других программ. Например, для этой цели подойдет интегрированный отладчик в MS VC. Конечно, по своим возможностям он уступает Айсу, но прост и удобен в управлении, имеет хорошо спроектированный интерфейс и неплохое ядро, а также великолепную возможность работы с исходными текстами "родных" для него MFC библиотек, которые находятся на компакт-диске, вместе с VC.

Загрузим файл в отладчик и перейдем к строке 0х040118A. Если теперь нажать Ctrl-F10, то программа будет выполнена только до текущей строки, а затем управление вновь получит отладчик. Другими словами, аналог 'here' в SI и TD. Но как мы узнаем, что строка принадлежит именно нашему файлу, а не какому-нибудь другому? Самым простым способом выяснить, какой файл сейчас обрабатывается, это взглянуть на его имя. Как мы помним, оно расположено в стеке по адресу esp+0x40. Вызовем окно отображения дампа и перетащим мышью эту строку в окно или введем ее вручную.

Какая жалость: строка зашифрована и по ней никак не получается узнать имя. Но не будем спешить. Действительно, шифровка искажает имена файлов до неузнаваемости, но длину строки оставляет неизменной. Ключевой файл с именем 'XKRIS KASPERSKI' будет самым длинным из всех присутствующих в каталоге, что не сможет не броситься нам в глаза.

***************** Рисунок pf ***********

Строка '^MTOU&MGUVCTUMO' без первого символа - это и есть запись, соответствующая нашему (а точнее, моему) имени. Переименуем теперь папку 'XKRIS KASPERSKI' в 'XMTOU&MGUVCTUMO' и повторим те же манипуляции с отладчиком, чтобы, во-первых удостовериться, что мы нигде не ошиблись, а во-вторых, узнать контрольную сумму. Не вручную же нам ее вычислять!

***************** Рисунок p10 ***********

Вычисленная контрольная сумма, находящаяся в регистре eax, равна 0x16, тогда 0X16+'A' (0X41) == 0x57 = 'W'. Однако вся строка имени файла (включая и первый символ контрольной суммы) шифруется по xor 6. xor 'W',6 = 'Q': таким образом, ключевой файл должен называться 'QMTOU&MGUVCTUMO'. Переименуем его и запустим программу для проверки. CRACK ME 0x11<R> REGISTERED FOR : KRIS KASPERSKI<R> Press any key to continue

В самом деле, работает. Осталось написать законченный генератор ключевых файлов на основе введенного имени пользователя (как это делается, уже было рассказано) и исправить недочет программы с последней записью в каталоге. .00401179: 0FBE542440 movsx edx,b,[esp][00040] .0040117E: 8B442410 mov eax,[esp][00010] .00401182: 83EA41 sub edx,041 ;"A" .00401185: 25FF000000 and eax,0000000FF ;" " .0040118A: 3BC2 cmp eax,edx .0040118C: 7505 jne .000401193 -------- (2) .0040118E: BE01000000 mov esi,000000001 ;"" .00401193: 8D4C2414 lea ecx,[esp][00014] .00401197: 51 push ecx .00401198: 57 push edi .00401199: FFD5 call ebp .0040119B: 85C0 test eax,eax .0040119D: 7597 jne .000401136 -------- (3) .0040119F: 5F pop edi

Понятно, что между 0х040118E и 0х0401193 необходимо вставить jmp 0x040119F, чтобы при нахождении первого же ключевого файла защита выходила из цикла. Однако нам катастрофически не хватает свободного места. Кажется, что никакими ухищрениями не удасться выгадать хотя бы пару байт свободного пространства. Но как вам нравится такой вариант: .0040118E: 46 inc esi .0040118F: EB0E jmps .00040119F -------- (2) .00401191: 0000 add [eax],al .00401193: 8D4C2414 lea ecx,[esp][00014]

Значение esi по умолчанию равно нулю, следовательно inc esi можно записиать как inc (0) == 1. Эта команда занимает всего один байт, за счет чего в наше распоряжение поступают целых четыре освободившихся байта. Можно сделать SHORT или NEAR прыжок, и при этом в худшем случае по крайне мере один байт остается свободным. Интересно, сможет ли читатель найти другие решения?

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

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

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

Способы затруднения анализа программ

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

Строго говоря, сегодня уже не существует действительно хороших и надежных методов противодействия анализу защиты. Мощь современных технологий превзошла самые смелые ожидания прошлых лет. Эта фраза может показаться напыщенной, но в действительности и она не может дать истинного представления о вооружении хакера.

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

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

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

Наконец, популярные операционные системы дают прикладным приложениям не так много привилегий, чтобы их хватило для противодействия отладчикам, даже не ориентированным на взлом. Еще недавно в MS-DOS любая программа могла исполняться в нулевом кольце и работать с аппаратурой исключительно через порты ввода\вывода, минуя операционную систему и BIOS. В Windows же это невозможно. Даже если приложение и установит свой vxd, это только облегчит задачу взломщика, т.к. взаимодействовать с ним защита сможет только через стандартный Win32 API и для хакера не составит труда перехватить и при желании проэмулировать работу vxd, но уже без электронного ключа или ключевой дискеты. Кроме того, сегодня, когда аппаратное обеспечение постоянно меняется прикладная программа никак не может взаимодействовать с ним через порты без риска натолкнуться на несовместимость. К тому же у сетевых рабочих станций весь обмен идет через сеть. Следовательно, любая прикладная программа должна взаимодействовать только с драйвером, но ни в коем случае не с железом, иначе это вызовет отказ от ее использования и переход на продукцию конкурента, что принесет убыток гораздо больший, нежели взлом.

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

Можно в шутку сказать: не пытайтесь усложнить хакеру жизнь. Вы только напрасно потеряете силы и время, и ничего хорошего из этого не получится. Если это действительно хакер, то все ваши услия не увенчаются успехом. Однако многие соответствующие приемы любопытны сами по себе; кроме того, полезно знать, как с ними можно справиться. Поэтому ниже этот вопрос будет рассмотрен очень подробно. Возможно, большую часть проблем, относящуюся к MS-DOS, читатель уже неплохо знает. Однако я так же рассматриваю технологии противодействия взломщикам под Windows, которые не были ранее широко опубликованы и изучены. Замечу, что системный программист под Windows имеет все шансы написать программу, которую трудно взломать даже опытным хакерам.

Приемы против отладчиков
Самым первым отладчиком под MS-DOS был Debug.com фирмы MicroSoft. Совершенно очевидно, что этот инструмент годился разве что для забавы и изучения ассемблера.

Но рынок не терпит пустого места, и в то время отладчики росли как грибы после дождя. Многие из них не представляли ничего интересного и ушли недалеко от своего прототипа. Это была золотая эра для разработчиков защит. Достаточно было "запереть" клавиатуру, запретить прерывания, сбросить флаг трассировки, чтобы надолго отбить охоту у хакера к копанию в чужом коде.

Появление 80286 процессора принесло с собой не только новые возможности, но и волну шедевров программирования, отчасти не потерявших актуальности и по сей день. Это время навсегда запомнилось такими отладчиками как Afd PRO, написанной в 1987 году 'AdTec GmbH', знаменитым Turbo Debuger, созданным годом позже двумя братьями Chris'ом и Rich'ем Williams'ом, первым эмулирующим отладчиком Сергея Пачковки, написанным, правда, с большим опозданием: в 1991 году.

Так или иначе, разработчикам защит стали наступать на пятки. Особенно подлила масла в огонь NuMega, выпустившая в конце восьмидесятых замечательный отладчик Soft-Ice, до сих пор пользующийся у хакеров огромной популярностью. NuMega была первой фирмой, которая не только поставила хакер-ориентированное программное обеспечение на коммерческие рельсы, но и умело манипулировала ситуацией. С одной стороны, ее продукты не объявлялись как хакерские и официально предназнались для отладки своих программ, однако подавляющее большинство пользователей применяло их иначе.

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

С этого момента воюющие стороны на некоторое время поменялись местами. За разработчиками защит стояли небольшие фирмы - порой из пяти-десяти человек, а то и индивидуальные программисты; за хакерами - вся мощь NuMega. Теперь даже не обладающий глубокими познаниями в области системного программирования взломщик мог успешно пользоваться готовым инструментарием, не особо интересуясь, как последний работает. Численность взломщиков росла, и в хакерское общество вливались многие малоприятные и малокультурные личности, постепено изменивщие значение слова "хакер".

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

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

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

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

Однако судьба преподнесла враждующим сторонам большой сюрприз в лице новой операционной системы Windows. Принципиально новая архитектура сделала бесполезными все существующие отладчики. Разумеется, без отладчика был немыслим ни один серьезный компилятор и его появление было просто неизбежным. Однако MicroSoft отказалась опубликовать информацию, необходимую для его написания, предоставляя программистам только свои продукты. Что это были за отладчики! Медленные, неповоротливые и вовсе не предназначенные для работы с исполняемым кодом.

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

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

Сегодня уже мало кто решается противостоять отладчику и включает в свое приложение антиотладочный код. Мода на это давно прошла. Однако технологии, разработанные нашими предшественниками, от этого отнюдь не становятся неинтересными. Мы рассмотрим их с самого начала. С того самого времени, когда на столе разработчиков гордо красовался новый компьютер XT.

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

Легче всего понять концепцию контрольных точек останова исполнения программы. Для этого используется однобайтовый код 0xCC. Когда процессор встречает его, он вызывает исключительную ситуацию (в просторечии "дергает прерыванием") int 0x3. При этом в стеке запоминается регистр флагов, указатель текущего кодового сегмента (регистр CS), указатель команд (регистр IP), запрещаются прерывания (очищается флаг FI) и сбрасывается флаг трассировки.

Что из этого следует? Отлаживаемая программа полностью, включая все аппаратные прерывания, "замораживается" и поступает в монопольное распоряжение процессора, а значит и отладчика. Последнему доступен весь код программы и операционной системы(!) для чтения, анализа и даже модификации. Отладчик может также изменять значение регистров и указателя команд. При этом доступ к последнему осуществляется через стек. Содержимое стека при входе в int 0x3 будет следующим:

--------------------------¬ <------- SP ¦ IP ¦ +-------------------------+ ¦ CS ¦ +-------------------------+ ¦ регистр флагов ¦ +-------------------------+ ¦ ¦

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

Какими недостатками обладает предложенный метод? Самое неприятное в нем то, что точка останова непосредственно модифицирует код (при этом вынуждая отладчик сохранить измененный байт, чтобы потом была возможность вернуться обратно). Поэтому для отлаживаемой программы не составит труда обнаружить этот факт и удалить точку останова. Для этого можно воспользоваться корректирующими кодами Рида-Соломона, которые позволяют определить местоположение контрольного байта или использовать контрольную сумму кода для расшифровки какого-нибудь фрагмента. Рассмотрим следующий пример crack12.asm: Repeat: PUSH SI XOR AX,AX LEA SI,Start Get_CRC: LODSB ADD AH,AL CMP SI,offset Crypted JB Get_CRC POP SI LODSB XOR AL,AH STOSB CMP SI,offset _end JB Repeat Crypted:

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

Пошаговая трассировка программы никак не блокируется защитой, но очень утомительна. Попробуем установить точку останова. Но где? Ясно, что в строке Crypted ни в коем случае нельзя. Рассмотрим, что при этом произойдет:

****************** Рисунок 11 *************

Обратите внимание на значение регистра AL - оно равно 0xCC. Контрольная точка исказила код, и теперь он уже не может быь корректно расшифрован. Однако установка контрольной точки в пределах расшифровщика исказит вычисляемую контрольную сумму его тела. Это наглядно демонстрирует следующий дамп: cs:0119 81FE5101 cmp si,0151 cs:011D72E6 jb 0105  cs:011F 5E pop si cs:0120 D162FF shl word ptr [bp+si-01],1 cs:0123 D915 fst dword ptr[di] cs:0125 F9 stc cs:0126 1B9B8A99 sbb bx,[bp+di-6676] cs:012A 9B wait cs:012B 93 xchg bx,ax cs:012C F8 clc cs:012D E8A0E9 call EAD0

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

Но представляет интерес решить эту проблему менее мощными средставми. Самое простое - положить кирпич на клавишу F7 и идти пить чай, а тем временем программа будет расшифровываться. Если же говорить серьезно, то необходимо дождаться расшифровки хотя бы первой инструкции. При этом допустима только пошаговая (F7), а не Step Over (F8) трассировка (последняя также устанавливает 0xCC в конце команды). Теперь можно безболезненно установить точку останова, т.к. код уже расшифрован и не модифицируется.

Этому, однако, можно воспрепятствовать, применив многопроходную расшифровку кода. Это, конечно, сложнее программируется, но затраченные усилия с лихвой окупятся, т.к. под обычным отладчиком это уже не будет исполняться. Кроме того, шифровка затрудняет дизассемблирование. Обычные дизассемблеры вообще не смогут обработать код и выдадут что-то наподобие следующего листинга: crack12.lst Sourcer v5.10 9-Mar-99 1:53 pm Page 1 43C7:011F 6C D1 62 FF D9 15 db 6Ch,0D1h, 62h,0FFh,0D9h, 15h 43C7:0125 F9 1B 9B 8A 99 9B db 0F9h, 1Bh, 9Bh, 8Ah, 99h, 9Bh 43C7:012B 93 F8 E8 A0 E9 E9 db 93h,0F8h,0E8h,0A0h,0E9h,0E9h 43C7:0131 F6 F8 db 0F6h,0F8h 43C7:0133 57 38 76 7B 38 78 db 'W8v{8xttx'

Впрочем, это не относится к IDA, которая умеет обрабатывать скрипты. Например, следующий скрипт расшифрует весь необходимый код, позволяя дизассемблировать файл: auto a,sym,ch; sym=0; for (a=0;a<0x1F;a++) sym=sym+Byte(MK_FP(0x1000,0x100+a)); sym = sym & 0xFF; Message("CRC = %x \n",sym); for (a=0x11F;a<0x151;a++) { ch = Byte(MK_FP(0x1000,a)); ch = ch ^ sym; PatchByte(MK_FP(0x1000,a),ch); }

Аналогичным образом можно снять любую мыслимую шифровку. Подробнее об этом уже рассказывалось, и я не буду повторяться. Разве что для разнообразия приведу скрипт для hiew, как для более распространенной утилиты. Для начала нам необходимо узнать контрольную сумму расшифровщика. Подсчитывать ее можно, например, в регистре BL, для чего достаточно одной команды ADD BL,AL

HIEW, к сожалению, не поддерживает полностью автономных скриптов, поэтому необходимо вручную "провести" его от 0x0 до 1x1E. Посмотрим теперь, какое значение имеет регистр BL. Если все сделано правильно, то там находится число 0xB8. Попробуем расшифровать им остальной код: XOR AL,0xB8

В результате получится следующее: 0000001F: B409 mov ah,009 00000021: BA2701 mov dx,00127 00000024: CD21 int 021 00000026: C3 retn 00000020: 09 BA 27 01-CD 21 C3 43-52 41 43 4B-20 30 78 31 ¦' =!+CRACK 0x1 00000030: 31 2E 20 8F-E0 AE A3 E0-A0 AC AC A0-20 A2 EB AF 1. Программа вып 00000040: AE AB AD A5-AD A0 20 E3-E1 AF A5 E8-AD AE 0D 0A олнена успешно 00000050: 24 - - - $

Таким образом, приемы против отладчиков "прозрачны" для дизассемблеров. Конечно, дизассемблеры не эквивалентны отладчикам и даже частично не покрывают возможности последних. Поэтому желательно все же найти способ заставить отладчик работать.

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

Казалось бы, сегодня, в век быстрых процессоров, мы должны были бы давно забыть об этом и устанавливать контрольные точки, где нам вздумается, не беспокоясь, что это может нарушить правильное выполнение программы. Однако ничего не изменилось за прошедший десяток лет. Вот грустный пример программы под Win32. Почему грустный? Да потому, что штатным отладчиком с ним ничего нельзя будет сделать. Рассмотрим его подробнее: int main(int argc, char* argv[]) { char c[]="Hello World! \n"; __asm { LEA ESI,From LEA EDI,To xor eax,eax From: MOV al,CS:[ESI] INC ESI ADD ah,al CMP esi,edi jb From LEA ESI,c MOV EDI,ESI Repeat: LODSB OR AL,AL JZ To XOR AL,AH STOSB JMP Repeat To: } printf(c); return 0; }

Не правда ли, этот пример почти полностью повторяет пример под MS-DOS десятилетней давности? Изменилась только одна команда - в Windows не так-то просто модифицировать исполняемый код, вместо этого программа расшифровывает данные.

Попробуйте установить контрольную точку в границах ассемблерского фрагмента. Используется, к примеру, отладчик для MS VC 6.0 или даже Soft-Ice! И в том, и в другом случае строка 'Hello Word!' будет расшифрована некорректно и вместо ожидаемого приветствия на экране появится бессмысленный мусор. Любопытный факт! На протяжении всей истории существования платформы Intel отладчики повторяют одну и ту же ошибку, которую с успехом используют разработчики защит.

А теперь подумаем, что будет, если мы подсчитаем контрольную сумму всей программы, а не только одного расшифровщика. Правильно! Ни одной точки останова нельзя будет установить! Отладка обычными средствами станет невозможной! Впрочем, Soft-Ice позволяет установить аппаратные точки останова командой bpm (в отличие от bpx), и легко выйти из положения.

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

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

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

Защита может прочитать этот адрес и записать в обработчик немного (или много - кому как понравится) "мусора", убивая при этом отладчик. Разумеется, для этого необходимо выяснить, что это именно отладчик, а не что-то другое. По умолчанию Int 0x3 обрабатывается MS-DOS и обработчик состоит из одной команды IRET (0xCF). Если обнаруживается нечто иное, то с некоторой степенью вероятности это и будет отладчиком. Чтобы нейтрализовать последний, достаточно переустановить int 0x3 (и int 0x1 - но об этом ниже) на свой обработчик, в котором нужно вывести сообщение с просьбой не отлаживать эту программу.

Впрочем, это может и не сработать. "Умные" отладчики могут отслеживать чтение программой таблицы векторов прерываний и "подсовывать" предыдущие значения обработчиков. Сегодня это умеет делать большинство из них, в том числе и Soft-Ice. Отметим также, что в Windows прикладной программе никто попросту не даст манипулировать векторами и это становится уже неактуально.

Отлаживаемая программа может также содержать опкод 0xCC, беспорядочно и в изобилии разбросанный по всему ее телу и при этом делающий что-то полезное. Например, обработчик int 0x3 может расшифровывать код или вызывать int 0x21. В результате произойдет конфликт ресурсов: отладчику и защите понадобится прерывание int 0x3 - каждому для своих нужд. Обычный отладчик при этом будет постоянно "всплывать" на каждом 0xСC, и если последнее установить в глубоко вложенном цикле, то отладка превратится в пытку. Однако у некоторых отладчиков можно отключить всплытие по точке останова. К таким отладчикам относится, например, trsutil Евгения Касперского.

Заметим, что это не в меньшей степени актуально и для платформы Windows. Наглядным примером может служить CRACK13, который при попытке запуска под отладчиком (скажем, интегрированным с MS VC) вызывает останов последнего, командой int 0x3. Забавно, но его разработчики не предусмотрели возможности отключения этого действия.

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

Рассмотрим еще раз поведение процессора при встрече инструкции 0xCC - контрольной точки останова: он заносит в стек флаги и значение указателя команд и вызывает обработчик. Давайте представим, что при исполнении некоторого критического фрагмента кода защита присвоила указателю стека нулевое значение. А стек, как известно, растет вверх, т.е. от старших адресов к младшим. Когда процессор, встретив int 0x3, попытается сохранить (занести) в стек регистр флагов, он не сможет этого сделать, т.к. стек уже исчерпан. Операционной системе ничего не останется сделать, как завершить некорректно работающее приложение, потому что восстановить его работоспособность уже будет невозможно.

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

----T---T---T---T---T---T---T---T---T---T---T-------- ¦ H ¦ E ¦ L ¦ L ¦ O ¦ ¦ W ¦ O ¦ R ¦ L ¦ D ¦ . . . L---+---+---+---+---+---+---+---+---+---+---+--------- ^ ¦ ¦ ¦

esp -- pop -->

Если установить esp на начало строки, а потом последовательно извлекать из стека байт за байтом, то получим некий аналог LODSB, только более мощный, т.к.он может читать в любой регистр и оставляет esi нетронутым под нужды программы. При этом любое вмешательство отладчика в процесс приведет к затиранию читаемых данных и краху программы. Неплохо, очень неплохо, - в этом можно убедиться на примере CRACK13. Попробуйте запустить его под отладчиком! Но лучше не пробуйте, ибо ничего хорошего все равно не получится.

Немного поразмыслив, мы придем к выводу, что в данном случае POP AX можно безболезненно заменить на LODSW. Но нетрудно так спроектировать код, чтобы подобная замена была невозможна. Например, достаточно задействовать все свободные регистры, а также команду PUSH, ближайшим аналогом которой будет DEC EDI\STOSW, несколько большая по длине и к тому же тесно связанная с флагом направления (который защита может умышленно изменить на противоположный) и с регистром AX.

Что предпринять хакеру в такой ситуации? Пожалуй, воспользоваться дизассемблером или эмулирующим отладчиком. Однако последних под Windows, похоже, еще не существует, поэтому остается только первое.

****************** Рисунок 13 *************

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

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

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

Практически все антиотладочные механизмы основаны на оригинальных приемах программирования и не поддерживаются языками высокого уровня. Это вынуждает разработчика прибегнуть к ассемблеру. При этом многие (в том числе и старые) программисты забывают, что архитектура процессоров не стояла на месте и за последние несколько лет представления об оптимизации кардинально изменились. Код, оптимальный для 80486, не будет автоматически оптимальным для Pentium'а. Необходимо позаботиться хотя бы о спаривании команд, без чего ассемблерская "заплатка" будет сильно выделяться на фоне оптимизированного компилятором кода.

Вторым отладочным механизмом 8086 процессора является поддержка трассировки исполнения программы. Трассировка - это пошаговое выполнение инструкций. Во времена 8086 она была единственным средством отладки программ на процессорах того поколения. Организовывалась она с помощью "ловушки". Под ее нужды выделялся один из флагов (TF - trace flag). Если он был установлен, то после выполнения каждой инструкции процессор генерировал исключение 1 (т.е. вызывал int 0x1). При этом он сохранял в стеке регистры флагов и указателя команд и при входе в обработчик автоматически очищал флаг трассировки, чтобы не привести систему к краху.

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

Несовершенство механизма трассировки позволяло отлаживаемой программе не только обнаружить отладчик, но и выйти из-под его контроля. Действительно, регистр флагов мог читаться и модифицироваться отлаживаемой программой. Проверить, установлен ли флаг трассировки, защите ничего не стоило. После чего был резонный повод для отказа в работе. Однако хакер мог нейтрализовать такие проверки. Кроме того, отладчик может анализировать следующую инструкцию и если она воздействует на флаг трассировки, то эмулировать ее выполнение. Поэтому разработчики защит начали использовать весьма изощренные приемы, один из которых показан ниже (CRACK14). PUSHF MOV AX,2577h MOV BP,SP LEA DX,NewInt0x77 INC Byte ptr [BP+1] INT 21h POPF INT 77h NewInt0x77: NOP MOV AL,[BP+1] SHL AL,7 ADD SP,6 LEA SI,Crypted MOV CX, offset _end - offset Crypted Repeat: XOR Byte ptr [SI],AL INC SI LOOP Repeat Crypted: MOV AH,9 LEA DX,About INT 21h RET About DB 'Hello,Sailor!',0Dh,0Ah,'$'

Если этот фрагмент запустить под отладчиком, то расшифрованный код будет выглядеть следующим образом: 2298:0126¦ 34 89 XOR AL,89 2298:0128¦ 3A AE 81 4D CMP CH,Byte ptr [BP]+4D81 2298:012C¦ A1 43 C8 MOV AX,Word ptr [C843] 2298:012F¦ E5 EC IN AX,[EC] 2298:0131¦ EC IN AL,DX 2298:0132¦ EF OUT DX,AX 2298:0133¦ AC LODSB 2298:0134¦ D3 E1 SHL CX,CL 2298:0136¦ E9 EC EF JMP F125 2298:0139¦ F2 REPNE

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

Вся загвоздка именно в "рассеянии" инструкций защиты среди не относящихся к ней команд. Рассмотрим подробнее следующий фрагмент: PUSHF MOV AX,2577h MOV BP,SP LEA DX,NewInt0x77 INC Byte ptr [BP+1] INT 21h POPF

Выделим следующую последовательность инструкций: PUSHF MOV BP,SP INC Byte ptr [BP+1] POPF

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

Кроме того, ни в коем случае не надо делать тривиальную проверку типа TEST [BP+1],1 JNZ under_debuger

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

Как с этим жить хакеру? Ответ все тот же: используйте дизассемблеры и эмулирующие отладчики. И все усилия разработчиков защиты пойдут прахом. Сегодня можно с уверенностью сказать, что все антиотладочные приемы стали неактуальны. И просто непостижимо то упорство, с которыми некоторые разработчики тратят свое время и силы на борьбу с отладчиками реального режима, которые уже давно никто не использует.

Кроме того, нормальная операционная система просто не позволит прикладной программе манипулировать с флагом трассировки. Совсем недавно это было не так. Защита могла не только читать, но и сбрасывать флаг трассировки, тем самым выходя из-под контроля отладчика. Заслуживает уважения, что разработчики без всякой поддержки со стороны процессора все же научились защищаться от агрессивных приложений. Однако все же редкие отладчики позволяют программе трассировать саму себя. А защиты это могут активно использовать. Например, для расшифровщика или интерпретатора. К последнему мы еще вернемся, а пока рассмотрим следующую программу (crack16): Repeat: LODSW MOV [SI-2],BX LOOP Repeat

Не правда ли, малопонятный цикл? На самом деле это часть расшифровщика. А другая его часть хитро спрятана в обработчике Int 0x1: NewInt01h: XOR AX,9FADh MOV BX,AX IRET

Т.е. на самом деле расшифровщик полностью выглядит так: Repeat: XOR AX,9FADh MOV BX,AX LODSW XOR AX,9FADh MOV BX,AX MOV [SI-2],BX XOR AX,9FADh MOV BX,AX LOOP Repeat

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

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

Рассмотрим некоторые из них. Неочевидно с первого взгляда, но практически все отладчики перехватывают часть исключений с целью предотвращения некорректной работы программ. Это в первую очередь int 0x6 (неверный опкод) и int 0x0 (деление на нуль или переполнение). Ничего не стоит построить защиту, активно использующую эти ресурсы. Например, для расширения существующих команд микропроцессора. При этом не будет существовать никакого способа заставить эту защиту работать под отладчиком, который самостоятельно перехватывает эту исключительную ситуацию и блокирует работу.

Однако не стоит принимать вышеизложенное как руководство к написанию защит подобного типа. Напротив, я всячески призываю этого не делать. Ведь не только отладчик перехватывает исключение "неверный опкод", но и менеджеры расширенной памяти (emm386, qemm), операционная система Windows, да мало ли еще кто. В любом случае вашему клиенту не понравится, если программу придется запускать в "голой" MS-DOS или отказыватся от использования Windows и драйверов расширенной памяти.

Гораздо больше подходит на эту роль переполнение при делении. Эта исключительная ситуация признана довольно рядовой, и любая операционная система позволяет приложениям обрабатывать ее самостоятельно. В самом деле, иначе было бы невозможно существование математических программ. Как бы вам понравилось, если бы при попытке деления на ноль в электронной таблице Windows, сообщив о некорректном поведении приложения, закрывала бы его? Вообще непонятно, почему отладчики "взъелись" на это исключение и принялись обрабатывать его независимо от приложений. Самое интересное, что этим грешат даже некоторые эмулирующие отладчики!

Самое сложное в использовании этого приема - найти такой алгоритм, который не позволял бы обойти его непосредственным вызовом int 0. Т.е. если в теле защиты есть некая инструкция DIV, которая вызывает обработчик исключения, то ничего не стоит заменить ее на непосредственный вызов прерывания int xx, предварительно переустановив обработчик на любое свободное значение.

Рассмотрим следующий пример, где подобная замена просто невозможна (взят из реальной защиты): NewInt00h: ADD SI,AX CBW ADD SP,6 Repeat: LODSW DIV AH STOSB LOOP Repeat

Зашифрованный фрагмент содержал помимо всего прочего инструкции декодирования. Изучите защиту внимательно еще раз, и вы поймете, что это так. Внешне расшифровщик очень прост. Пара чисел a и b расшифровывается как целая часть от a/b. Но если b равно нулю, тогда a интерпретируется как указатель на следующую расшифровываемую инструкцию. Т.е. декодер может "прыгать блохой" и одновременно разжимать текст, фактически реализуя LZ-распаковку. Дешифровщик и распаковщик в одном флаконе и в семи ассемблерских командах - не правда ли, результат, которым можно гордиться? (Теперь можно уточнить, что это фрагмент из моей защиты).

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

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

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

Приемы против отладчиков защищенного режима
Позже появился 80286 (с точки зрения хакера мало чем отличавшийся от своего предшественника), а вслед за ним и 80386, принесший принципиально новые возможности отладки. Точнее, "принципиально новые" только для платформы Intel, а еще точнее - для этой линейки микропроцессоров, ибо большая часть из того, что появилось только в 80386, уже было реализовано давным-давно на других платформах, например на PDP. Однако это были большие, серьезные, дорогостоящие машины, которые в восьмидесятые годы никак нельзя было сравнивать с персоналками, коим и применения достойного еще не могли найти. Компания Intel первой решилась усилить возможности отладки на "домашнем компьютере", догадываясь, что пройдет совсем немного времени, прежде чем программное обеспечение усложнится настолько, чтобы реально потребовать той мощи, которая была заложена в 80386 чип.

Новый процессор полностью унаследовал все "прелести" реального режима, включая и автоматическую трассировку. Т.е. сохраняя полную совместимость "сверху-вниз". Полную, да не совсем. Изменилась реакция процессора на модификацию сегментных регистров. 8086 ввел такое интересное понятие, как "потеря трассировочного прерывания", которое возникало всякий раз, как предыдущая команда изменяла любой сегментный регистр. При этом трассировочное прерывание действительно терялось на одну команду. 80386 несколько изменил свое поведение. Теперь потеря происходила только при изменении сегмента стека SS. Впрочем, взгляните сами: PUSH DS POP DS PUSH SS POP SS RET

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

Как можно использовать потерю трассировочного прерывания? Например, можно помешать войти в ключевую процедуру: PUSH SS POP SS > CALL MyGodProc

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

Разумеется, современные отладчики все реже и реже используют автоматическую трассировку: они пользуются аппаратными точками останова. Что это такое? Вообще-то эта книга не "Intel Architecture Software Developer's Manual", и подробного объяснения тут не будет. Любой уважающий себя разработчик должен подробно ознакомиться с документацией Intel в первоисточнике. В частности, процесс отладки описан в главе 14 технического руководства N 24319201, доступного на сайте Intel.ru

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

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

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

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

Иначе говоря, невозможно обнаружить правильно спроектированный отладчик или выйти из-под его контроля. К сожалению, таковых на сегодняшний день выпущено немного; мне достоверно известен только один: cup386, которому не научилась противостоять еще ни одна защита. Soft-Ice, к сожалению, оставляет часть своего кода в общем адресном пространстве с отлаживаемой программой и, что самое неприятное, позволяет ей свободно манипулировать отладочными регистрами, а значит снимать установленные отладчиком контрольные точки.

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

Для написания такой утилиты не потребуется особых знаний и навыков. Обо всем позаботились инженеры Intel, вам остается только воспользоваться предлагаемыми возможностями. Аппаратная отладка базируется на восьми специально выделенных для этой цели регистрах DR0-DR7. Чтение и запись их осуществляется командой MOV. Никаких других команд для манипуляций с этими регистрами не предусмотрено. Доступ к этим регистрам возможен только в следующих случаях:

В противном случае будет сгенерировано исключение GPF (General Protection Fault). Отсюда видно, что управлять отладочными регистрами из прикладной программы Windows (исполняющейся в CPL3) никак не получится.

Поэтому для упрощения изложения материала все нижесказанное будет относиться только к реальному режиму и MS-DOS. Написание приложений, работающих в нулевом кольце операционной системы Windows, выходит за рамки данной книги и поэтому рассматриваться не будет. Интересующихся я отсылаю к MSDN. MicroSoft предоставила достаточно примеров программ, позволяющих если не освоить системное программирование, то по крайней мере использовать эти заготовки для своего кода.

Наиболее важным из всех отладочных регистров является DR7. который задает режимы работы всех четырех контрольных точек (а их может быть, к сожалению, только четыре).

****************** Рисунок p15 *************

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

Назначение флагов LE и GE здесь рассматривать не будем, отметим только, что Intel рекомендует устанавливать оба в единицу, чтобы ваши действия возымели должный эффект.

Пара бит R/W задает следующие условия срабатывания контрольной точки:

a) Если бит DE регистра CR4 установлен:

COLUMNS(2), DIMENSION(IN), COLWIDTHS(1.2042,4.0883), WIDTH(8.1767), ABOVE(.0984), BELOW(.0984), HGUTTER(.0555), VGUTTER(.0433), BOX(Z_DOUBLE), HGRID(Z_SINGLE), VGRID(Z_SINGLE), KEEP(OFF), ALIGN(CT)

ZGT, ZGT

R/W

TTL9, TTL9

00, Прерывание по исполнению

01, Прерывание по записи данных

10, Прерывание по записи\чтению в порт ввода\вывода

11, Прерывание по записи и чтению данных,, но не исполнению

б) Если бит DE регистра CR4 сброшен:

COLUMNS(2), DIMENSION(IN), COLWIDTHS(1.2042,4.0883), WIDTH(8.1767), ABOVE(.0984), BELOW(.0984), HGUTTER(.0555), VGUTTER(.0433), BOX(Z_DOUBLE), HGRID(Z_SINGLE), VGRID(Z_SINGLE), KEEP(OFF), ALIGN(CT)

ZGT, ZGT

R/W

TTL9, TTL9

00, Прерывание по исполнению

01, Прерывание по записи данных

10, Неопределено

11, Прерывание по записи и чтению данных,, но не исполнению

Заметим, что перехват обращения к портам ввода-вывода появился несколько позднее на процессорах Pentium. Ни 80386, ни 80486 еще не имели такой возможности.

Два бита LENn задают длину контрольной точки:

COLUMNS(2), DIMENSION(IN), COLWIDTHS(1.2042,4.0883), WIDTH(8.1767), ABOVE(.0984), BELOW(.0984), HGUTTER(.0555), VGUTTER(.0433), BOX(Z_DOUBLE), HGRID(Z_SINGLE), VGRID(Z_SINGLE), KEEP(OFF), ALIGN(CT)

ZGT, ZGT

LEN

TTL9, TTL9

00, 1 байт

01, 2 байта

10, Неопределено

11, 4 байта

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

Но что если защита сама работает в нулевом кольце? Или в "голой" MS-DOS в реальном режиме? Неужели, как говорят, "сушите весла - приехали"? На самом деле инженеры Intel предусмотрели такой поворот событий и ввели новую возможность - перехват обращений к отладочным регистрам. Если флаг DG установлен, то любое обращение к DR0-DR7 вызовет отладочное исключение, т.е. передаст управление отладчику. Теперь последний может для сокрытия своего присутствия проэмулировать эту инструкцию или просто остановить работу (последнее, впрочем, не очень разумно).

Soft-Ice, однако, не обеспечивает ни того, ни другого. Обидно, честное слово. Самый лучший отладчик имеет такую "дырку", которая позволяет защитам как угодно распоряжаться контрольными точками.

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

Отметим этот очень важный момент. Вопреки распространенному заблуждению, что проэмулировать отладочный регистр нельзя. Все эмуляция сводится лишь к переадресации регистров. Так, например, если отладчик использует DR0 для своих нужд и к нему же обращается защита, то он может заменить DR0 на DR1 (к примеру) и при этом защите никак не удастся обнаружить такую подмену. Однако, когда защита потребует все четыре контрольные точки, отладчику придется отдать все свои ресурсы и самому остаться ни с чем. К счастью, подобных защит немного. Большинство из них просто используют отладочные регистры как регистры общего назначения или периодически заполняют их мусором. Разаработки, наверное, думают, что этим они страшно осложняют отладчику жизнь. На самом деле ничуть! Действительно, проэмулировать запись\чтение в отладочный регистр очень просто. Достаточно отслеживать обращения к нему и перенаправлять все вызовы в отдельную переменную. Защита может записывать, читать, сравнивать - и все это будет работать.

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

Однако мы забегаем вперед. Вернемся к описанию архитектуры отладки. Четыре регистра DR0-DR3 задают линейный физический адрес контрольной точки или порта ввода\вывода. На выбор адреса наложены некоторые ограничения. Так, если размер контрольной точки составляет слово, она должна располагаться по четному адресу. При двойном слове - по адресу, кратному четырем. Смысл этого станет ясен, если учесть, что поле LEN маскирует младшие биты регистра адреса. Это связано с микроархитектурой процессоров Intel и иногда может служить источником неудобств. Так, например, установить контрольную точку останова на слово, расположенное по адресу 0x13, просто невозможно. Адрес будет округлен до 0x10, и мы рискуем как натолкнуться на ложные обращения, так и пропустить искомые.

-----T----T----T----¬ ¦ ¦ ¦ ¦ ¦ Попытка установить сюда L----+----+----+----- ¦ ¦--T----T----T----T----T----T----T----T----T----------------------- ¦ 10 ¦ 11 ¦ 12 ¦ 13 ¦ 14 ¦ 15 ¦ 16 ¦ 17 ¦--+----+----+----+----+----+----+----+----+------------ ¦ ¦ -----T----T----T----¬ ¦ ¦ ¦ ¦ ¦ А получилось сюда! L----+----+----+-----

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

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

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

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

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

Регистр DR6 - это регистр статуса. Четыре младших бита ассоциированы с четырьмя контрольными точками. Как только контольная точка срабатывает, соответствующий бит регистра DR6 устнавливается в единицу. Любопытно, что даже если не установлен бит Gn (Ln) и точка останова не вызвала отладочного исключения, соответствующий бит регистра статуса все же будет установлен в единицу. Подумайте на досуге, как можно использовать эту тонкость архитекуры.

Флаг BD установлен, если следующая инструкция обращается к любому из отладочных регистров. При этом необходимо, чтобы бит GD регистра DR7 был равен единице, иначе ничего не произойдет. Заметим, что эмулятор должен правильно выставлять значение этого бита в соответствии с флагом GD. Обычно отлаживаемая программа сбрасывает последний. При этом BD, равный единице, красноречиво свидетельствует об активном отладчике, на что защита может соответствующе прореагировать. К сожалению, многие распространенные отладчики никак не учитывают этот факт. Но, с другой стороны, не так много разработчиков осведомлены об этой тонкости, и несовершенство отладчиков проблем обычно не вызывает.

Бит BS устанавливается во время пошаговой трассировки программы. Заметим, что если отлаживаемое приложение имеет доступ к DR6, то это еще один способ обнаружить трассировку. Иногда это встречается в различных вирусах и антивирусных мониторах с целью обнаружить и блокировать трассировку. Причем это относится не только к DOS программам, но к Windows-приложениям. Уже существуют вирусы, работающие в нулевом кольце, они могут как угодно манипулировать с отладочными регистрами.

Ненулевое значение Флага BT говорит о том, что последнее исключение вызвано переключением задач. К отладочным регистрам это непосредственно не относится, поэтому и не рассматривается здесь подробно.

<B>Важное замечание:<D> содержимое регистра DR6 никогда не очищается процессором. Поэтому он должен обнуляться отладчиком после считывания результата.

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

Но хакер должен всегда мыслить на самом "низком" уровне - уровне микропроцессора и железа. Точнее, даже на уровне микроархитектуры процессора, а не готовых к применению команд и регистров. В самом деле: почему контрольных точек только четыре? Почему поля LENn маскируют младшие биты адреса контрольной точки? С чем все это связано? К сожалению, на этот вопрос невозможно ответить в рамках данной книги. Для этого нужно подробно изучить архитектуру микропроцессора и взаимодействие отдельных его компонентов. При этом многие вопросы отпадут сами собой и мотивы разработчиков станут понятными.

Для закрепления изложенного материала предлагается рассмотреть пример crack18, который устанавливает точку останова на чтение\запись по адресу 0x9000:0 (точнее, физическому адресу 0x90000). При этом раздастся предупредительный сигнал. Программа не работает в DOS окне в операционной системе Windows. Необходим режим эмуляции MS-DOS без установленных драйверов типа emm386.

Рассмотрим здесь ключевой ее фрагмент:

MOV eax,00000000110000001100000010b ; ¦¦ ¦¦ ¦¦ ¦¦ ; ¦¦ ¦¦ ¦¦ ¦L---> Бит Lx Может быть установлен ; ¦¦ ¦¦ ¦¦ L----> Бит Gx любой из них. ; ¦¦ ¦¦ ¦¦ ; ¦¦ ¦¦ L+-----------> Биты LE & GE. P6 их игнорирует ~~10 ; ¦¦ ¦¦ Поэтому их значение некритично ; ¦¦ ¦¦ ; ¦¦ L+-------------------> R\W На чтение и запись памяти ; ¦¦ ; L+---------------------------> LEN Длина конт. точки. 1 байт

MOV ebx,090000h ; ^^^^^^^- линейный физический адрес точки останова. ; вычисляется как segment*0x10+offset

MOV dr7,eax mov dr0,ebx ; ^ Заносим значения в отладочные регистры. С этого момента любое ; обращение к контольной точки будет приводить к генерации отладо- ; чного исключения Int 0x1.

Микропроцессоры Intel, начиная с Pentium предоставляют возможность установления контрольной точки на порты ввода\вывода. Ранее их можно было перехватывать только в защищенном режиме, опираясь на привилегированность инструкций чтения\записи в порт. Разумеется, это было неудобно и недостаточно: поэтому в Pentium реализован механизм перехвата портов через контрольные точки. При этом сам порт необязательно должен физически существовать. Последнее позволяет не только эмулировать неподключенные устройства, но и писать драйверы под полностью виртуальное аппаратное обеспечение, что иногда реализуется некоторыми системами.

Следующий пример издает короткий звук при обращении к порту 1. Можно считать, что этим мы эмулируем некоторое несуществующее устройство. Или, что ближе к проблеме защиты информации, перехватываем обращение к порту, после чего последний переходит в наше распоряжение. (Например, так можно написать 100% эмулятор FDD и ключевой дискеты к нему, при этом ни одна защита не сможет отличить его от настоящего).

MOV eax,cr4 ; ^ Считываем начальное значение управляющего регистра Cr4

OR eax,01000b ; ^ ; L-------> Флаг DE ; ; Устанавливаем флаг DE (смысл этого был изложен выше).

MOV cr4,eax ; ^ Записываем новое значение

MOV eax,0100000001100000010b ; ^^ ^^ ^^ ; ¦¦ ¦¦ L+--> Флаги Ln,Gn. Любой должен быть взведен ; ¦¦ ¦¦ ; ¦¦ L+----------> Флаги LE,GE.Значение некритично для P6 ; ¦¦ ; L+------------------> Точка останова на I\O

MOV ebx,00001h ; ^^^^^^ ; ¦¦¦¦¦¦ ; L+++++---------------> Адрес порта I\0

MOV dr7,eax mov dr0,ebx

Рекомендую поэкспериментировать с контрольными точками. Нетрудно убедиться, какое это мощное средство и какие перспективы оно открывает. Как уже упоминалось выше, можно перехватывать обращения к портам и замещать существующее оборудование или эмулировать неподключенное. Так, например, можно проэмулировать электронный ключ "ХАСП" или любой другой. Ключевую дискету (вместе с дисководом). Если защита привязывается к серийному номеру IDE винчестера, то нетрудно перехватить его порты и выдавать ложную информацию, вводя защиту в заблуждение. Аналогично решается и привязка к дате создания BIOS (для этого достаточно установить контрольную точку в необходимом месте). При этом нет никаких противоречий с законодательством. Действительно, клиент вправе эмулировать любое оборудование, которое пожелает. Впрочем, тут есть маленькое исключение. Возможен конфликт с патентным правом. Запрещается без соответствующего разрешения каким бы то ни было образом воспроизводить запатентованное аппаратное обеспечение. Однако тут обе стороны наталкиваются на расплывчатость формулировок и законодательных статей. Является ли программная эмуляция воспроизведением аппаратного устройства? С одной стороны да, т.к. прикладная программа его не может отличить. С другой стороны - нет, поскольку эмулятор может быть построен по совершенно другой архитектуре.

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

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

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

Однако оставим патентное право в стороне и перейдем непосредственно к вопросу, как защиты могут противостоять отладке. Первое и самое простое решение (как уже отмечалось выше) - это периодически забивать мусором отладочные регистры. Несмотря на комичность ситуации, Софт-Айс этого не переживет. Впрочем, не переживет этого и разработчик. Программа не будет работать в среде WINDOWS (разве что в нулевом кольце), что не понравится клиенту и он уйдет к другому разработчику. При этом надежность такого подхода сомнительна - ведь можно просто найти все команды обращения в регистрам DR0-DR7 и удалить их. Чтобы этого избежать, разработчик будет вынужден хранить в них полезные данные и при этом задействовать все остальные регистры так, чтобы у хакера не было возможности заменить отладочный регистр на какой-нибудь другой или использовать команды PUSH\POP. Скажу сразу, что построить защиту таким образом не просто трудно, но практически невозможно. Поэтому сегодня крайне редко можно встретить прикладную программу, манипулирующую отладочными регистрами. За исключением, быть может, чтения DR6 и попытки обнаружить активную отладку (или DR0-DR3 с проверкой, не стоит ли контрольная точка в определенном месте). Разумеется, все это наивно: хороший отладчик не должен позволять приложению читать отладочные регистры, это просто смешно. Если защита остерегается контрольной точки в некотором месте, то ее без колебаний можно установить именно туда и тем самым сократить время анализа. Любопытно, но последнее все же встречается. На моей жизни я видел две таких защиты, и обе были созданы одним автором!

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

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

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

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

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

История "запирания" клавиатуры восходит к глубокой древности. Во времена PC\XT достаточно было лишь запретить прерывания, чтобы "убить" консольные отладчики наподобие Debug.com . Сейчас, конечно, это выглядит несколько наивным. Действительно, отладчику, запустившему приложение, скажем, на отдельном V86, совершенно безразлично, как оно манипулирует прерываниями. Однако тут разработчик упустил из виду, несмотря на полную изоляцию отладчика и приложения клавиатура у них осталась общей. Заглянем в руководство по контроллеру клавиатуры - что же может сделать защита с последней? Например, существует любопытная команда 0xAD, блокирующая клавиатуру. При этом нажатие клавиш не будет обрабатываться вплоть до момента разблокировки последней командой 0xAE. Этот пример реализован в crack1A. Попробуйте его пошагово трассировать. Очень много отладчиков, начиная с TurboDebuger, убьются этим приемом. Поэтому рекомендую запускать их из DOS окна в операционной системе Windows, чтобы можно было безболезненно "прибить" зависшее окно мышью. Однако Soft-Ice на это не купится и клавиатуру не заблокирует - это еще раз подтверждает, что ребята из NuMega неплохо соображают.

Но и они кое-что упустили из виду. Действительно, у контроллера клавиатуры используется один и тот же порт как для задания адреса, так и для чтения\записи данных. Это уже любопытно. Получается, что после передачи кода команды контроллер клавиатуры не будет отрабатывать нажатие клавиш, пока не будет передан байт данных. Самое интересное, что отладчик не сможет определить причину "зависания" клавиатуры и не сможет корректно ее разморозить. Попробуйте потрассировать пример crack1B, используя Soft-Ice 2.8 (заметим, что это последняя версия отладчика под MS-DOS). При этом после первой же команды записи в порт клавиатура зависнет! Так приложение может защищать критические куски кода от отладчиков.

Однако версия 3.2, например, превосходно справляется с этой проблемой, поскольку виртуализирует порты. Т.е. защита обращается не к физическому контроллеру клавиатуры, а к мнимому, точнее - к эмулируемому отладчиком. И при этом никак испортить настоящий не может.

Аналогичным разделенным ресурсом является монитор, а точнее видеоконтроллер. Далеко не все отладчики способны корректно восстанавливать нестандартные видеорежимы. Это может быть использовано защитами, хотя, не столь часто. Видеокарточки во многом несовместимы, и в современных операционных системах низкоуровневый доступ к ним очень ограничен. К тому же в Windows, например, монитор разделен между всеми приложениями, а не только между защитой и отладчиком, как это было когда-то в MS-DOS. Поэтому крайне маловероятно, что читатель столкнется с такой защитой на практике. Если же такое вдруг случится, то можно попробовать подключить второй монитор (Soft-Ice это поддерживает) или воспользоваться удаленной отладкой по сети или нуль-модемному кабелю, что поддерживает большинство отладчиков.

Технологии эмуляции процессора

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

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

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

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

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

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

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

Действительно, против эмулятора бессильны любые антиотладочные приемы, и в них широко используется усиление отладочных и отслеживающих механизмов. Эмуляторы позволяют "отвязывать" ПО от аппаратных электронных ключей, реализуя последние на программном уровне. Еще больше распространены (и легки в изготовлении) "виртуальные" ключевые диски, реализуемые в большинстве случаев через программный интерфейс int 0x13 и только в достаточно мощных защитах посредством перехвата обращений к портам.

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

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

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

Рассмотрим подробнее механизмы эмуляторов. Прежде всего, любой программный эмулятор состоит из следующих функциональных частей: модуля лексического анализа, цикла выборки команд, блока декодирования инструкций и эмулятора АЛУ (арифметическо-логического устройства). Задача эмуляции АЛУ упрощается тем, что в большинстве случаев набор арифметических и логических операций может быть выполнен базовым процессором, поэтому большей частью АЛУ представляют собой простые переходники. Блок исполнения микрокода в разных архитектурах может примыкать непосредственно к блоку декодирования инструкций или АЛУ, или даже можеи быть выделен в независимый модуль. На программном уровне эмулятора он представляет собой просто библиотеку функций, реализованную на множестве команд базового процессора. Например, пусть в гипотетической виртуальной машине присутствует инструкция CalculateCRC32. Разумеется, для ее реализации потребуется написать на 80x86 специальную подпрограмму, поскольку непосредственно он не обеспечивает такой возможности. Но почему бы реализацию этой функции не отнести к АЛУ? Действительно, некоторые (не самые лучшие архитектуры) относят эту инструкцию к АЛУ, но это не только не самое лучшее, но и иерархически не правильное решение! АЛУ должно обеспечивать базовый арифметическо-логический набор функций, на котором строится все подмножество команд виртуальной машины. В таком случае любая команда исполняется по цепочке "Базовый CPU->АЛУ->Блок исполнения микрокода". Сам блок исполнения непосредственного доступа к CPU не имеет. Такая архитектура упростит перенос эмулятора на другие платформы, а также облегчит процесс отладки эмулятора.

Рассмотрим представленную архитектуру на примере гипотетического эмулятора 8086 процессора.

----------------------------------------- E2 72 90 90 90 .. .. .. .. .. .. ----------------------------------------- ¦ ¦ ¦ ¦  ¦ ---------------¬¦ ----------------------------¬ ¦ блок выборки ¦¦ ¦ блок исполнения микрокода ¦ L-----------T---¦ L----------------------------     ---------------------¬ -----------¬ ¦ блок декодирования ¦ ¦ АЛУ ¦ L--------------------- L-----------

Пусть указатель команд emIP указывает на начало команды LOOP 0x77. Задачей блока выборки инструкций будет выбрать инструкцию из байта 0xE2. Как известно, в 80x86 процессорах код команды занимает 6 старших битов.Но в нашем случае команда занимает все восемь. Все остальное - это операнды. Теперь мы имеем два варианта программной реализации выборки команды. Можно продолжить анализ операндов или установить регистр emIP на начало первого операнда и поручить оставшуюся работу блоку декодирования. Если блок декодирования не может разобраться в числе и размере операндов команды, то окончательное позиционирование регистра emIP осуществит блок исполнения микрокода. Эти решения называются соответственно Альфа-, Бета- и Гамма- декодерами. С точки зрения канонического ООП каждый объект, представленный в нашем случае командой, должен самостоятельно отвечать за формат операндов. Поручение этого отдельному модулю вызывает необходимость унификации операндов всех команд, что далеко не всегда удобно. Процессоры 80x86 имеют жесткую систему адресации операндов, поэтому лучшим вариантом для них будет Бета-декодер. RISC процессоры оперируют командами фиксированного размера, поэтому всегда реализуются через Альфа-декодеры.

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

На блок исполнения микрокода мы подаем готовый опкод инструкции и сформированный физический адрес. На уровне микрокода команда LOOP представляется как DEC emCX\JNZ addr. Обе эти инструкции относятся к элементарным и вызываются из АЛУ. Многие разработчики допускают очевидную ошибку и вызвают DEC и JNZ базового процессора. Это работает, но часто приводит к трудно обнаруживаемым ошибкам и нарушает всю иерархию команд.

Для реализации АЛУ требуется еще один заключенный, но не показанный в нем модуль. Это, конечно, HAL - модуль абстрагирования от базового оборудования. Необходимо так спланировать эмулятор, чтобы HAL получился по возможности компактным и легко переносимым.

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

Теперь мы создадим свою виртуальную машину и напишем для нее простой пример защиты, который вскроем написанным дизассемблером.

Сначала нужно разработать архитектуру виртуальной машины. Пусть это будет простой RISC процессор с фиксированным набором команд и жесткой адресацией памяти. Если команда не требует операндов, то все равно они должны присутствовать, но их значение игнорируется. Для простоты ограничимся минимальным набором команд. Из арифметических будет достаточно команды ADD, единственной логической конструкции if (a,b) go to Bellow, Eqular, Above, имеющей следующую логику:

если :

a<b go to Bellow

a=b go to Eqular

a>b go to Above

Этих двух команд вполне достаточно для реализации микроядра, но для удобства мы добавим команду безусловного перехода и вызова\возврата процедуры.

Пусть будут два адресных пространства для кода и данных и один-единственный порт ввода-вывода. Он будет предназначен для виртуального телетайпа. Запись в порт приведет к появлению символа на экране, а чтение - к вводу символа с клавиатуры.

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

Тонкости дизассемблирования

Дизассемблирование в уме

- Мне известны политические аргументы. - Но меня интересуют человеческие доводы.

Ф. Херберт. "Мессия Дюны".

Очень часто под рукой не оказывается ни отладчика, ни дизассемблера, ни даже компилятора, чтобы набросать хотя бы примитивный трассировщик. Разумеется, говорить о взломе современных защитных механизмов в таких условиях просто смешно - но что делать, если жизнь заставляет?

Предположим, у нас есть простейший шестнадцатиричный редактор, вроде того, какой встроен в DN, и, если очень повезет, debug.com, входящий ~~11 в поставку Windows и часто остающийся не удаленным владельцами машины. Вот этим-то мы и воспользуемся. Скажу сразу, что придется очень нелегко. Большая часть нижеописанного требует труда и упорства, но дает вам практически неограниченную власть над техникой и владеющими ею людьми.

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

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

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

Структура команд INTEL 80x86
- Потому ты и опасен, что овладел своими страстями.

Ф. Херберт. "Мессия Дюны".

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

Для начала разберемся с форматом инструкций архитектуры Intel:

----------T-------T--------T-----T----------T-------------------¬ ¦ префикс ¦ опкод ¦ ModR/M ¦ SIB ¦ смещение ¦ непосред. операнд ¦ L---------+-------+--T-----+---T-+----------+-------------------- ¦ ¦ ------------ L--------------¬   --------------------T-----¬ --------T-------T-------¬ ¦ Mod ¦ Reg/Opcode ¦ R/M ¦ ¦ Scale ¦ Index ¦ Base ¦ L-------------------+------ L-------+-------+--------

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

Само поле опкода занимает восемь бит и часто (но не всегда) имеет следующий формат:

-----------T-------------T--------¬ ¦ ¦ Направление ¦ Размер ¦ ¦ ---+-------------+--------+ ¦ Опкод ¦ р е г и с т р ¦ ¦ ---+----------------T--------+ ¦ ¦ у с л о в и е ¦ инверс ¦ L----+-------------------+--------- 7 4 3 2 1 0

Поле размера равно нулю, если операнды имеют размер в байт. Единичное значение указывает на слово (двойное слово в 32-режиме или с префиксом 0х66 в 16-разрядном режиме).

Направление обозначает операнд-приемник. Нулевое значение присваивает результат правому операнду, а единица левому. Рассмотрим это на примере инструкции mov bx,dx: 8BDA mov bx,dx ^^ 10001011b 89DA mov dx,bx ^^ 10001001b

Не правда ли, мы можем как по мановению волшебной палочки менять местами операнды, меняя всего один бит? Однако задумаемся, как это поле будет вести себя, когда один из операндов имеет непосредственное значение? Разумеется, оно не может быть приемником и независимо от значения этого поля будет только источником. Инженеры Интел учли такую ситуацию и нашли оригинальное применение, часто экономящее до трех байтов. Рассмотрим ситуацию, когда операнду размером в слово или двойное слово присваивается непосредственное значение, меньшее по модулю 0x100. Ясно, что значащим является только младший байт, а нули, стоящие слева, по правилам математики можно и отбросить. Но попробуйте объяснить это процессору! Нужно пожертвовать хотя бы одним битом, чтобы указать ему на такую ситуацию. Вот тут-то и используется байт направления. Рассмотрим следующую команду:

810623016600 add w,[00123],0066 ^^ ^^^^ ^^^^ ¦¦ *L+----> 10000001

Если теперь флаг направления установить в единицу, то произойдет следующее

8306230166 add w,[00123],0066 ^^ ^^ ^^^^ ¦¦¦¦ *L+----> 10000011

Таким образом, мы экономим один байт в 16-разрядном режиме и целых три в 32-разрядном. Этот факт следует учитывать при написании самомодифицирующегося кода. Большинство ассемблеров генерируют второй (оптимизированный) вариант, и длина команды оказывается меньше ожидаемой. На этом, кстати, основан очень любопытный прием против отладчиков. Посмотрите на следующий пример: 00000100: 810600010200 add w,[00100],00002 00000106: B406 ^^ mov ah,006 00000108: B207 mov dl,007 0000010A: CD21 int 021 0000010C: C3 retn

После выполнения инструкция в строке 0х100 приобретет следующий вид:

00000100: 8206000102 add b,[00100],002 00000105: 00¦B406B2 add [si][0B206],dh ^^¦ L----> ip

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

Совсем другое дело - отладчики, и особенно отладчики-эмуляторы, которые часто вычисляют значение ip _после_ выполнения команды (это легче запрограммировать). В результате наступает крах. Несущественное, казалось бы обстоятельство - до или после выполнения команды вычисляется ip - оказалось роковым. Приведу в подтверждение дамп экрана:

г=[¦]=CPU Pentium Pro============================T=======1=[][]=¬¦ cs:0100 8306000102 add word ptr [0100], ax 0000 ¦c=0¦¦ cs:0105 00B406B2 add [si-4DFA],dh ¦ bx 0000 ¦z=0¦¦ cs:0109 07 pop es - cx 0000 ¦s=0¦¦ cs:010A CD21 int 21 - dx 0000 ¦o=0¦¦ cs:010C C3 ret - si 0000 ¦p=0¦

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

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

Перейдем теперь к рассмотрению префиксов. Они делятся на четыре группы:

1. Префиксы блокировки и повторения:

0xF0 LOCK - префикс.

0хF2 REPNZ (только для строковых инструкций).

0xF3 REP (только для строковых инструкций).

2. Префиксы переопределения сегмента:

0х2E CS:

0х36 SS:

0х3E DS:

0х26 ES:

0х64 FS:

0х65 GS:

3. Префикс переопределения размеров операндов:

0х66

4. Префикс переопределения размеров адреса:

0х67

Если используется более одного префикса из той же самой группы, то действие команды не определено и по-своему реализовано на разных типах процессоров.

Префикс переопределения размера операндов используется в 16-разрядном режиме для манипуляции с 32-битными операндами и наоборот. При этом он может стоять перед любой командой, например 0x66 : CLI будет работать! А почему бы и нет? Интересно, но отладчики этого не учитывают и отказываются работать. То же относится и к дизассемблерам, к примеру IDA: seg000:0100 start proc near seg000:0100 db 66h seg000:0100 cli seg000:0102 db 67h seg000:0102 sti seg000:0104 retn

На этом же основан один очень любопытный прием противостояния отладчикам, в том числе и знаменитому отладчику-эмулятору cup386. Рассмотрим, как работает конструкция 0x66 : RETN. Казалось бы, коль скоро функция retn не имеет операндов, то префикс 0x66 можно просто игнорировать. На самом деле все не так просто. retn работает с неявным операндом - регистром ip/eip. Именно это и изменяет префикс. Разумеется, в реальном и 16-разрядном режиме указатель команд всегда обрезается до 16 бит, и поэтому на первый взгляд возврат сработает корректно. Но стек-то окажется несбалансированным! Из него вместо одного слова взяли целых два! Так нетрудно получить и исключение 0xC - исчерпание стека. Попробуйте отладить чем-нибудь пример crack1E.com - даже cup386 во всех режимах откажется это сделать, а Turbo-Debuger вообще зависнет! IDA не сможет отследить стек и вместе с ним все локальные переменные.

Как видим, прием крайне прост, но и крайне надежен. Впрочем, следует признать, что перехват int 0xC под операционной системой Windows бесполезен и, несмотря на все ухищрения, приложение, породившее такое исключение, будет безжалостно закрыто. Хотя в реальном режиме это работает превосходно. Попробуйте убедиться в этом на примере crack1E.com . Забавно наблюдать реакцию на него эмулирующих отладчиков. Все они либо неправильно работают (снимают одно слово из стека, а не два), либо совершают очень далекий переход по 32-битному eip (в результате чего виснут), либо - чаще всего - просто аварийно прекращают работу по исключению 0xC (так ведет себя cup386).

Еще интереснее получится, если попытаться исполнить в 16-разрядном сегменте команду CALL. Если адрес перехода лежит в пределах сегмента, то ничего необычного ожидать не приходится. Инстурукция работает нормально. Чудеса начинаются, когда адрес выходит за эти границы. В защищенном 16-разрядном режиме при уровне привилегий CL0 с большой вероятностью регистр EIP "обрежется" до шестнадцати бит и инструкция сработает (но похоже, что не на всех процессорах). Если уровнень не CL0, то генерируется исключение защиты 0xD. В реальном же режиме эта инструкция может вести себя непредсказуемо. Хотя в общем случае должно генерироваться прерывание int 0xD. В реальном режиме его нетрудно перехватить и совершить далекий 'far' переход в требуемый сегмент. Так поступает, например, моя собственная операционная система OS\7R, дающая в реальном режиме плоскую память. Разумеется, такой поворот событий не может пережить ни один отладчик. Ни трассировщики реального режима, ни v86, ни protect-mode debuger, ни даже эмуляторы (во всяком случае те, что мне известны), с этим справиться не в состоянии.

Одно плохо - все эти приемы не работают под Windows и другими операционными системами. Это вызвано тем, что обработка прерываний типа исключения общей защиты всецело лежит на ядре операционной системы и оно не позволяет приложениям распоряжаться им по своему усмотрению. Забавно, что в режиме эмуляции MS-DOS некоторые EMS-драйверы ведут себя в этом случае совершенно непредсказуемо. Часто при этом они не генерируют ни исключения 0xC, ни 0xD. Это следует учитывать при разработке защит, основанных на приведенных выше приемах.

Обратим внимание также на последовательности типа 0x66 0x66 [xxx]. Хотя фирма Intel не гарантирует поведение своих процессоров в такой ситуации, но фактически все они правильно ее интерпретируют. Иное дело некоторые отладчики и дизассемблеры, которые спотыкаются и начинают вести себя некорректно.

Есть еще один интересный момент, связанный с работой декодера микропроцессора.

------------T-T-T-T-T-T-T-T-T-T-T-T-T-T-T-T-T------ ------------+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+------ ^ F E D C B A 9 8 7 6 5 4 3 2 1^ ¦ ¦ ¦===============================¦ ¦ ¦ L------¬ --------- ¦ ¦ ¦ ¦

-------------------------¬ ¦ декодер ¦ L-------------------------

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

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

Префиксы перекрытия сегмента могут встречаться перед любой командой, в том числе и не обращающейся к памяти, - например, CS:NOP вполне успешно выполнится. А вот некоторые дизассемблеры сбиться могут. К счастью, IDA к ним не относится. Самое интересное, что комбинация из DS:FS:FG:CS:MOV AX,[100]

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

Вернемся к формату опкода. Выше была описана структура первого байта. Отметим, что это фактически недокументировано и Intel уделяет ему всего два слова. Действительно, формат команд разнится от одной команды к другой. Однако можно выделить и некоторые общие правила. Практически для каждой команды, если регистром-приемником фигурирует AX (AL) существует специальный однобайтовый опкод, который в трех младших битах содержит регистр-источник. Этот факт следует учитывать при оптимизации. Так, среди двух инструкций XCHG AX,BX и XCHG BX, DX следует всегда автоматически выбирать первую, т.к. она на байт короче. (Кстати, инструкция XCHG AX,AX более известна нам как NOP. О достоверности этого факта часто спорят в конференциях, но на странице 340 руководства 24319101 фирмы Intel об этом сказано недвусмысленно. Выходит, никто из многочисленных спорщиков не знаком даже с оригинальным руководством производителя).

Для многих команд (Jx) четыре младшие бита обозначают условие операции. Точнее говоря, условие задается в битах 1-2-3, а младший бит приводит к его инверсии.

COLUMNS(3), DIMENSION(IN), COLWIDTHS(1.4200,1.9392,1.9242), WIDTH(4.2600), ABOVE(.0984), BELOW(.0984), HGUTTER(.0555), VGUTTER(.0433), BOX(Z_DOUBLE), HGRID(Z_SINGLE), VGRID(Z_SINGLE), KEEP(OFF), ALIGN(CT)

ZGT, ZGT, ZGT

Код, Мнемоника, Условие

TTL9, TTL9, TTL9

0000, O, Переполнение

0010, B,, NAE, Меньше

0100, Z, Равно

0110, BE,, NA, Меньше или равно

1000, S, Знак

1010, P,, PE, Четно

1100, L,, NGE, Меньше (знаковое)

1110, LE,, NG, Меньше или равно (знаковое)

Как видим, условий совсем немного, - чтобы никаких проблем их запоминания не возникало. Теперь уже не нужно мучительно вспоминать: 'JZ' - это 0x74 или 0x75. Т.к. младший бит первого равен нулю, то jz это 0x74, а jnz соответственно 0x75.

Далеко не все опкоды смогли поместиться в первый байт. Инженеры Intel задумались о поиске дополнительного места для размещения еще нескольких бит и при этом обратили внимание на байт modR/M. Подробнее он описан ниже, а пока рассмотрим приведенный выше рисунок. Трехбитовое поле reg, содержащее регистр-источник, очевидно не используется, когда вслед за ним идет непосредственный операнд. Так почему бы его не использовать для задания опкода? Однако требуется указать процессору на такую ситуацию. Это делает префикс 0xF, размещенный в первом байте опкода. Да, именно префикс, хотя документация Intel этого прямо и не подтверждает. При этом на не MMX процессорах для его декодирования требуется дополнительный такт. Intel же предпочитает называть первый байт основным, а второй уточняющим опкодом. Заметим, что то же поле используют многие инструкции, оперирующие с одним операндом (jmp,call). Это все очень сильно затрудняет написание собственного ассемблера\дизассемблера, но зато дает простор для самомодифицирующегося кода и, кроме того, вызывает уважение к инженерам Intel, до минимума сократившим размеры команд. Конечно, это далось весьма не просто. И далеко не все дизассемблеры работают правильно. С другой стороны, именно благодаря этому и существуют успешно противостоящие им защиты.

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

К тонкостям кодирования команд мы вернемся ниже, а пока приготовимся к разбору поля modR/M. Два трехбитовых поля могут задавать код регистра общего назначения по следующей таблице:

COLUMNS(5), DIMENSION(IN), COLWIDTHS(1.4200,.9608,1.0208,.9442,.9533), WIDTH(4.7667), ABOVE(.0984), BELOW(.0984), HGUTTER(.0555), VGUTTER(.0433), BOX(Z_DOUBLE), HGRID(Z_SINGLE), VGRID(Z_SINGLE), KEEP(OFF), ALIGN(CT)

ZGT, ZGT, ZGT, ZGT, ZGT

, , 8 бит операнд, 16 бит операнд, 32 бит операнд

TTL9, TTL9, TTL9, TTL9, TTL9

Код, 000, AL, AX, EAX

, 001, CL, CX, ECX

, 010, DL, DX, EDX

, 011, BL, BX, EBX

, 100, AH, SP, ESP

, 101, CH, BP, EBP

, 110, DH, SI, ESI

, 111, BH, DI, EDI

Опять можно восхититься лаконичностью решения инженеров Intel, которые ухитрились закодировать столько регистров всего в трех битах. Отсюда, кстати, становится ясно, почему нельзя выборочно обращаться к старшим и младшим байтам регистров SP,BP,SI,DI и, аналогично, старшему слову всех 32-битных регистрв. Во всем "виновата" оптимизация и архитектура команд. Просто нет свободных полей, в которые можно было бы "вместить" дополнительные регистры. Сегодня мы вынуждены расхлебывать результаты архитектурных решений, выглядевших такими удачными всего лишь десятилетие назад.

Обратите внимание на порядок регистров AX-CX-DX-BX-SP-BP-SI-DI. Алфавитный порядок немного нарушен, верно? Особенно странно в этом отношении выглядит BX. Но если понять причины, то не будет никакой нужды запоминать это исключение, все станет на свои места. BX - это индексный регистр. И стоит первым среди индексных.

Таким образом, мы уже можем "вручную", без дизассемблера, распознавать в шестнадцатиричном дампе регистры-операнды. Очень неплохо для начала! Или писать самомодифицирующийся код. Например: 00000000: 800E070024 or b,[00007],024 ; 00000005: FA cli 00000006: 33C0 xor ax,ax 00000008: FB sti

Он изменит строку 0x6 на xor sp,sp. Это "завесит" многие отладчики и, кроме того, не позволит дизассемблерам отслеживать локальные переменные адресуемые через SP. Хотя IDA позволяет скорректировать стек вручную, но для этого сперва нужно понять, что sp обнулился. В приведенном примере это очевидно (хотя, к стати, и не бросается в глаза), а если это произойдет в многопоточной системе? Тогда изменение кода очень трудно будет отследить, особенно в листинге дизассемблера. Однако нужно помнить, что самомодифицирующийся код все же уходит в историю. Сегодня он встречается все реже и реже.

COLUMNS(2), DIMENSION(IN), COLWIDTHS(1.2042,4.0883), WIDTH(8.1767), ABOVE(.0984), BELOW(.0984), HGUTTER(.0555), VGUTTER(.0433), BOX(Z_DOUBLE), HGRID(Z_SINGLE), VGRID(Z_SINGLE), KEEP(OFF), ALIGN(CT)

ZGT, ZGT

2-битная кодировка, 3-битная кодировка

TTL9, TTL9

00 ES, 000 ES

01 CS, 001 CS

10 SS, 010 SS

11 DS, 011 DS

, 100 FS

, 101 GS

, 110 Reserved*

, 111 Reserved*

Первоначально сегментые регистры кодировались всего двумя битами, и этого хватало, т.к. их было всего четыре. Позже, когда их стало больше, перешли на трехбитную кодировку. При этом два регистра 110b и 111b пока отсутствуют и вряд ли будут добавлены в ближайшем будующем. Но что же будет, если попытаться их использовать? Генерация int 0x6. А вот отладчики - эмуляторы могут вести себя странно. Иные при этом не генерируют прерывания, чем себя и выдают, а другие часто ведут себя непредсказуемо, т.к. требуемый регистр может находиться в области памяти, занятой другой переменной (это происходит, когда ячейка памяти опеределяется по индексу регистра; при этом считываются три бита и суммируются с базой, но никак не проверяются пределы).

Поведение дизассемблеров так же разнообразно. Вот, например, hiew: 00000000: 8E ??? 00000001: F8 clc 00000002: C3 retn qview: 00000000: 8EF8 mov !s,ax 00000002: C3 ret IDA: seg000:0100 start db 8Eh ; seg000:0101 db 0F8h ; seg000:0102 db 0C3h ;

Кстати, IDA вообще отказывается анализировать весь последующий код. Как это можно использовать? Да очень просто - если эмулировать еще два сегментных регистра в обработчике int 0x6, то очень трудна будет как отладка, так и дизассемблирование. Однако это опять-таки не работает под Win32!

Управляющие\отладочные регистры кодируются нижеследующим образом:

COLUMNS(3), DIMENSION(IN), COLWIDTHS(1.4200,1.9392,1.9242), WIDTH(4.2600), ABOVE(.0984), BELOW(.0984), HGUTTER(.0555), VGUTTER(.0433), BOX(Z_DOUBLE), HGRID(Z_SINGLE), VGRID(Z_SINGLE), KEEP(OFF), ALIGN(CT)

ZGT, ZGT, ZGT

, Control Register, Debug Register

TTL9, TTL9, TTL9

000, CR0, DR0

001, Reserved*, DR1

010, CR2, DR2

011, CR3, DR3

100, CR4, Reserved*

101, Reserved*, Reserved*

110, Reserved*, DR6

111, Reserved*, DR7

Заметим, что опкоды операций mov, манипулирующих с ними, различны, поэтому-то и получается кажущееся совпадение имен. С управляющими регистрами связана одна любопытная мелочь. Регистр CR1, как известно, в настоящее время зарезервирован и не используется. Так во всяком случае написано в русскоязычной документации. На самом же деле регистр CR1 просто не существует! И любая попытка обращения к нему вызывает генерацию исключения int 0x6. Например, cup386 в режиме эмуляции процессора этого не учитывает и неверно исполняет программу. А все дизассемблеры, за исключением IDA, неправильно дизассемблируют этот несуществующий регистр:

IDA: seg000:0100 start db 0Fh seg000:0101 db 20h seg000:0102 db 0C8h seg000:0103 db 0C3h

SOURCER: 43C5:0100 start: 43C5:0100 0F 20 C8 mov eax,cr1 43C5:0103 C3 retn

Или: 43C5:0100 start: 43C5:0100 0F 20 F8 mov eax,cr7 43C5:0103 C3 retn

Всех этих команд на самом деле не существует, и они приводят к вызову прерывания int 0x6. Не так очевидно, правда? И еще менее очевидно, что при обращении к регистрам DR4-DR5 исключения не генерируется. Между прочим, IDA 3.84 не дизассемблирует ни один регистр. Зато великолепно ассемблирует все (кстати, ассемблер был добавлен другим разработчиком).

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

Теперь перейдем к описанию режимов адресации микропроцессоров Intel. Тема очень интересная и познавательная - не только для оптимизации кода, но и для борьбы с отладчиками.

Первым ключевым элементом является байт modR/M.

---T--T-T-T-T-T-T-¬ ¦ mod ¦ reg ¦ r/m ¦ L--+--+-----+------

Если mod == 11b, то два следующие поля будут представлять собой регистры.(Это так называемая регистровая адресация.)

Например : 00000000: 33C3 xor ax, bx ^^ ---T--^--T-^--¬ L+----->¦11¦ 000 ¦ 011¦ L--+-----+-----

00000000: 32C3 xor al, bl ^^ ---T--^--T-^--¬ L+----->¦11¦ 000 ¦ 011¦ L--+-----+-----

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

Биты 3-5 могут вместо регистра представлять уточняющий опкод (в случае если один из операндов представлен непосредственным значением). Младшие три бита всегда либо регистр, либо способ адресации. Последнее зависит от значения 'mod'. Отметим, что биты 3-5 никак не зависят от выбранного режима адресации и всегда задают либо регистр, либо непосредственный операнд.

Формат поля R/M, строго говоря, не документирован, однако достаточно очевиден. Во всяком случе понимание этого позволяет избежать утомительного запоминания совершенно нелогичной на первый взгляд таблицы адресаций (см. ниже). R/M ----T---T---¬ ¦ x ¦ x ¦ x ¦ L---+---+---- ^ ^ ^ 0 - нет базирования ¦--- ¦ L--------T- 1 - есть базирование ¦ ¦ 2bit = 0 ¦ '0' - SI, '1' - DI ¦ 2bit = 1 ¦ '0' - BP, '1' - BX ¦ L----T 3bit = 0 ¦ '0' - база BX, '1' - BP 3bit = 1 ¦ '0' - Индексный региср,'1' - базов

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

Действительно, в поле R/M все три бита тесно взаимосвязаны, в отличие от поля mod. Оно фактически задает длину следующего элемента в байтах.

Например:

[Reg+Reg] ---------¬ -----T-----T-----¬ ¦ опкод ¦ ¦ 00 ¦ reg ¦ Mem ¦ L--------- L----+-----+------ 7 0 7 0

[Reg+Reg+Offset8] ---------¬ -----T-----T-----¬ ------------¬ ¦ опкод ¦ ¦ 01 ¦ reg ¦ Mem ¦ ¦ offset 8 ¦ L--------- L----+-----+------ L------------ 7 0 7 0 7 0

[Reg+Reg+Offset16] ---------¬ -----T-----T-----¬ ------------¬ ¦ опкод ¦ ¦ 10 ¦ reg ¦ Mem ¦ ¦ offset 16 ¦ L--------- L----+-----+------ L------------ 7 0 7 0 15 0

Разумеется, не может быть смещения 'offset 14', (т.к. процессор не оперирует с полуторными словами) и комбинация '11' указывает на регистровую адресацию.

Может возникнуть вопрос: как складывать с 16-битным регистром 8 битное смещение? Разумеется, непосредственному сложению мешает несовместимость типов, поэтому процессор сначала расширяет 8 бит до слова с учетом знака. Таким образом, диапазон возможных значений младшего байта от -127 до 127 (или от -0x7F до 0x7F).

Все вышесказанное проиллюстрировано в таблице, расположенной ниже. Обратим внимание на любопытный момент: адресация [BP] отсутствует. Ближайшим эквивалентом этого является [BP+0]. Отсюда следует, что для экономии следует избегать непосредственного использования BP в качестве индексного регистра. BP может быть только базой. И хотя mov ax,[bp] и воспринимается любым ассемблером, но ассемблируется в mov ax,[bp+0], что на байт длиннее.

Исследовав приведенную ниже таблицу, можно прийти к выводу, что все виды адресации 8086 процессора были несколько неудобны. Сильнее всего сказывалось ограничение, позволяющее использовать в качестве индекса только три регистра (BX,SI,DI). Между тем гораздо чаще требовалось использовать для этого CX (например, в цикле) и AX (как возвратное значение функции).

Поэтому начиная с процессора 80386 (для 32-режима) концепция адресаций была пересмотрена. Поле R/M стало всегда выражать регистр, независимо от способа его использования. Последним же управляло поле 'mod', задающее (кроме регистровой) три вида адресации:

COLUMNS(2), DIMENSION(IN), COLWIDTHS(1.2042,4.0883), WIDTH(8.1767), ABOVE(.0984), BELOW(.0984), HGUTTER(.0555), VGUTTER(.0433), BOX(Z_DOUBLE), HGRID(Z_SINGLE), VGRID(Z_SINGLE), KEEP(OFF), ALIGN(CT)

ZGT, ZGT

mod, Адрес

TTL9, TTL9

00,[Reg]

01, [Reg+08]

10, [Reg+32]

11, Reg

Видно, что поле mod по-прежнему выражает длину следующего поля - смещения, разве что с учетом 32-режима, где все слова расширяются до 32 бит.

Напомним, что с помощью префикса 0x67 можно и в 16-режиме использовать 32-режимы адресации, и наоборот. Однако при этом мы сталкиваемся с интересным моментом. Разрядность индексных регистров остается 32-битной и в 16-режиме!

В реальном режиме, где нет понятия границ сегментов, это действительно будет работать так, как выглядит, и мы сможем адресовать первые 4 гигабайта памяти (32 бита), что позволит преодолеть печально известное 64-килобайтовое ограничение 8086 процессоров. Но такие приложения окажутся нежизнеспособными в защищенном или V86 режиме. Попытка вылезти за границу 64-килобайтового сегмента вызовет исключение 0xD, что приведет к автоматическому закрытию приложения, - скажем, под управлением Windows. Аналогично поступают и отладчики (в том числе и многие эмуляторы, включая cup386).

Сегодня актуальность этого приема, конечно, значительно снизилась, поскольку "голый DOS" практически уже не встречается, а режим его эмуляции Windows'ом крайне неудобен для пользователей.

16 - режим 32 - режим

COLUMNS(6), DIMENSION(IN), COLWIDTHS(.8350,.9608,1.0208,.8633,.7808,.8342), WIDTH(5.0050), ABOVE(.0984), BELOW(.0984), HGUTTER(.0555), VGUTTER(.0433), BOX(Z_DOUBLE), HGRID(Z_SINGLE), VGRID(Z_SINGLE), KEEP(OFF), ALIGN(CT)

ZGT, ZGT, ZGT, ZGT, ZGT, ZGT

Адрес, Mod, R/M, Адрес, Mod, R/M

TTL9, TTL9, TTL9, TTL9, TTL9, TTL9

[BX+SI], 00, 000, [EAX], 00 000

[BX+DI], 00, 001, [ECX], 00 001

[BP+SI], 00, 010, [EDX], 00 010

[BP+DI], 00, 011, [EBX], 00 011

[SI], 00, 100, [--][--], 00 100

[DI], 00, 101, смещ32, 00 101

смещ16 ^1, 00, 110, [ESI], 00 110

[BX], 00, 111, [EDI], 00 111

[BX+SI]+смещ8, 01, 000, смещ8[EAX], 01 000

[BX+DI]+смещ8, 01, 001, смещ8[ECX], 01 001

[BP+SI]+смещ8, 01, 010, смещ8[EDX], 01 010

[BP+DI]+смещ8, 01, 011, смещ8[EBX], 01 011

[SI]+смещ8, 01, 100, смещ8[--][--], 01 100

[DI]+смещ8, 01, 101, смещ8[ebp], 01 101

[BP]+смещ8, 01, 110, смещ8[ESI], 01 110

[BX]+смещ8, 01, 111, смещ8[EDI], 01 111

[BX+SI]+смещ16, 10, 000, смещ32[EAX], 10 000

[BX+DI]+смещ16, 10, 001, смещ32[ECX], 10 001

[BP+SI]+смещ16, 10, 010, смещ32[EDX], 10 010

[BP+DI]+смещ16, 10, 011, смещ32[EBX], 10 011

[SI]+смещ16, 10, 100, смещ32[--][--], 10 100

[DI]+смещ16, 10, 101, смещ8[ebp], 10 101

[BP]+смещ16, 10, 110, смещ8[ESI], 10 110

[BX]+смещ16, 10, 111, смещ8[EDI], 10 111

Изучив эту таблицу, можно решить, что система адресации 32-режима крайне скудная и ни на что серьезное ее не хватит. Однако это не так. В 386+ появился новый байт SIB (Scale-Index Base).

Процессор будет ждать его вслед за R/M взякий раз, когда последний равен 100b. Эти поля отмечены в таблице как '[--]'. SIB хорошо задокументирован, и назначения его полей показаны на рисунке ниже. Нет совершенно никакой нужды зазубривать таблицу адресаций.

-----------T------------T-----------¬ ¦ Scale ¦ Index ¦ Base ¦ L----------+------------+------------ 7 6 3 0

'Base' - это базовый регистр, Index - индексный, а два байта Scale - это степень двойки для масштабирования. Поясним введенные термины. Что такое индексный регистр, понятно всем. Например [SI]. Теперь же можно выбирать любой регистр в качестве индексного. Правда, за ислючением SP (впрочем, можно выбирать и его, но об этом позже).

Базовый регистр - это тот, который суммировался с индексным, например, [BP+SI]. Точно так же теперь можно выбрать любой регистр в качестве базового. При этом есть возможность выбрать SP. Заметим, что если мы выберем последний в качестве индексного, то получим вместо 'SP' - "никакой". В этом случае адресацией будет управлять только базовый регистр.

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

COLUMNS(11), DIMENSION(IN), COLWIDTHS(.8350,.9608,1.0208,.8633,.7808,.8342,.8342,.8342,.8342,.8342,.8342), WIDTH(5.0050), ABOVE(.0984), BELOW(.0984), HGUTTER(.0555), VGUTTER(.0433), BOX(Z_DOUBLE), HGRID(Z_SINGLE), VGRID(Z_SINGLE), KEEP(OFF), ALIGN(CT)

TTL9, TTL9, TTL9, TTL9, TTL9, TTL9, TTL9, TTL9, TTL9, TTL9, TTL9

Base, , , EAX, ECX, EDX, EBX, ESP, , ESI, EDI

, , , 000, 001, 010, 011, 100, [*], 110, 111

Index, , S, Шестнадцатиричные значения SIB, +, +, +, +, +, +, +

[EAX], 000, 00, 00, 08, 10, 18, 20, 28, 30, 38

[ECX], 001, , 01, 09, 11, 19, 21, 29, 31, 39

[EDX], 010, , 02, 0A, 12, 1A, 22, 2A, 32, 3A

[EBX], 011, , 03, 0B, 13, 1B, 23, 2B, 33, 3B

Отсутствует, 100, , 04, 0C, 14, 1C, 24, 2C, 34, 3C

[EBP], 101, , 05, 0D, 15, 1D, 25, 2D, 35, 3D

[ESI], 110, , 06, 0E, 16, 1E, 26, 2E, 36, 3E

[EDI], 111, , 07, 0F, 17, 1F, 27, 2F, 37, 3F

[EAX*2], 000, , 40, 48, 50, 58, 60, 68, 70, 78

[ECX*2], 001, , 41, 49, 51, 59, 61, 69, 71, 79

[EDX*2], 010, , 42, 4A, 52, 5A, 62, 6A, 72, 7A

[EBX*2], 011, , 43, 4B, 53, 5B, 63, 6B, 73, 7B

Отсутствует, 100, , 01, 44, 4C, 54, 5C, 64, 6C, 74, 7C

[EBP*2], 101, , 45, 4D, 55, 5D, 65, 6D, 75, 7D

[ESI*2], 110, , 46, 4E, 56, 5E, 66, 6E, 76, 7E

[EDI*2], 111, , 47, 4F, 57, 5F, 67, 6F, 77, 7F

[EAX*4], 000, , 80, 88, 90, 98, A0, A8, B0, B8

[ECX*4], 001, , 81, 89, 91, 99, A1, A9, B1, B9

[EDX*4], 010, , 82, 8A, 92, 9A, A2, AA, B2, BA

[EBX*4], 011, , 83, 8B, 93, 9B, A3, AB, B3, BB

Отсутствует, 100, 10, 84, 8C, 94, 9C, A4, AC, B4, BC

[EBP*4], 101, , 85, 8D, 95, 9D, A5, AD, B5, BD

[ESI*4], 110, , 86, 8E, 96, 9E, A6, AE, B6, BE

[EDI*4], 111, , 87, 8F, 97, 9F, A7, AF, 77, BF

[EAX*8], 000, , C0, C8, D0, D8, E0, E8, F0, F8

[ECX*8], 001, , C1, C9, D1, D9, E1, E9, F1, F9

[EDX*8], 010, , C2, CA, D2, DA, E2, EA, F2, FA

[EBX*8], 011, , C3, CB, D3, DB, E3, EB, F3, FB

Отсутствует, 100, 11, C4, CC, D4, DC, E4, EC, F4, FC

[EBP*8], 101, , C5, CD, D5, DD, E5, ED, F5, FD

[ESI*8], 110, , C6, CE, D6, DE, E6, EE, F6, FE

[EDI*8], 111, , C7, CF, D7, DF, E7, EF, F7, FF

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

COLUMNS(2), DIMENSION(IN), COLWIDTHS(1.2042,4.0883), WIDTH(8.1767), ABOVE(.0984), BELOW(.0984), HGUTTER(.0555), VGUTTER(.0433), BOX(Z_DOUBLE), HGRID(Z_SINGLE), VGRID(Z_SINGLE), KEEP(OFF), ALIGN(CT)

ZGT, ZGT

mod, Действие

TTL9, TTL9

00, Смещение32[index]

01, Смещение8 [EBP] [index]

10, Смещение32[EBP] [index]

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

За это время, надеюсь, у вас разовьются достаточные навыки для ассемблирования\дизассемблирования в уме. Впрочем, есть множество эффективных приемов, позволяющих облегчить сей труд. Ниже я покажу некоторые из них. Попробуем без дизассемблера взломать crackme01.com. Для этого даже не обязательно помнить опкоды всех команд!

00000000:B4 09 BA 77 01 CD 21 FE C4 BA 56 01 CD 21 8A 0E ¦ +.¦w.=!.-¦V.=!К.00000010:56 01 87 F2 AC 02 E0 E2 FB BE 3B 01 30 24 46 81 ¦ V.З.м.рт.-;.0$FБ00000020:FE 56 01 72 F7 4E 02 0C 81 FE 3B 01 73 F7 80 F9 ¦ .V.rўN..Б.;.s.А.00000030:C3 74 08 B4 09 BA BE 01 CD 21 C3 B0 94 29 9A 64 ¦ +t.+.¦-.=!+-Ф)Ъd00000040:21 ED 01 E3 2D 2A 70 41 53 53 57 4F 52 44 00 6F ¦ !э.у-*pASSWORD.o00000050:6B 01 20 2A 04 B0 20 00 00 00 00 00 00 00 00 00 ¦ k..*.-..........00000060:00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ¦ ................00000070:00 00 00 00 00 00 00 43 72 61 63 6B 20 4D 65 20 ¦ .......Crack Me00000080:20 30 78 30 20 3A 20 54 72 79 20 74 6F 20 66 6F ¦ 0x0 : Try to fo00000090:75 6E 64 20 76 61 6C 69 64 20 70 61 73 73 77 6F ¦ und valid passwo000000A0:72 64 20 28 63 29 20 4B 50 4E 43 0D 0A 54 79 70 ¦ rd (c) KPNC Typ000000B0:65 20 70 61 73 73 77 6F 72 64 20 3A 20 24 0D 0A ¦ e password : $000000C0:50 61 73 73 77 6F 72 64 20 66 61 69 6C 2E 2E 2E ¦ Password fail...000000D0:20 74 72 79 20 61 67 61 69 6E 0D 0A 24 ¦ try again$

Итак, для начала поищем, кто выводит текст 'Crack me..Type password'. В самом файле начало текста расположено со смещением 0x77. Следовательно, если учесть, что com файлы загружаются начиная со смещения 0x100, эффективное смещение равняется 0x100+0x77==0x177. Учитывая обратное расположение старших и младших байт ищем в файле последовательность 0x77 0x01.

00000000: B4 09 BA 77 01 CD 21 ^^^^^

Вот она! Но что представляет собой опкод 0xBA? Попробуем определить это по трем младшим битам. Они принадлежат регистру DL(DX). А 0xB4 0x9 - это mov AH,9. Теперь нетрудно догадаться, что оригинальный код выглядел следующим образом: MOV AH,9 MOV DX,0x177

И это при том, что не требуется помнить опкод команды MOV! (Хотя это очень распространенная команда, и ее опкод запомнить все же не помешает).

Вызов 21-го прерывания 0xCD 0x21 легко отыскать, если запомнить его символьное представление '=!' в правом окне дампа. Как нетрудно видеть, следующий вызов int 21 лежит чуть правее по адресу 0xC. При этом DX указывает на 0x156. Это соответствует смещению 0x56 в файле. Наверняка эта функция читает пароль. Что ж, уже теплее. Остается выяснить, кто и как к нему обращается. Ждать придется недолго. --- чтение строки ¦00000000: B4 09 BA 77 01 CD 21 FE C4 BA 56 01 CD 21 8A 0E <-- 00001110

00000010: 56 01 87 F2 AC 02 E0 E2 FB BE 3B 01 30 24 46 81 ^ ^ ^^ ^ ^ ^ ¦ LT-LT- ¦ ¦ смещение16 <------- ¦ ¦ L-T-- CL(CX)<-- L-> BP L-------- смещение пароля

При разборе байта 0xE не забудьте, что адресации [BP] не существует в природе. Вместо этого мы получим [offset16]. На размер регистра и приемник результата указывают два младших бита байта 0x8A. Они равны 10b. Следовательно, мы имеем дело с регистром CL, в который записывается содержимое ячейки [0x156].

Все, кто знаком с ассемблером, усмотрят в этом действии загрузку длины пароля (первый байт строки) в счетчик. Неплохо для начала? Мы уже дизассемблировали часть файла и при этом нам не потребовалось знание ни одного опкода операции, за исключением, быть может 0xCD == INT. Продолжим в том же духе.

Вряд ли мы скажем, о чем говорит опкод 0x87. (Впрочем, обращая внимание на его близость к операции NOP = xchg ax,ax, можно догадаться, что 0x87 - это опкод операции xchg). Обратим внимание на связанный с ним байт 0xF2:

F2 == 11110010 ^^^ ^^ ^ ¦¦¦ ¦¦ ¦Reg/Reg-+-LT-L-+--> (DX)

(SI)

Как нетрудно догадаться, эта команда заносит в SI смещение пароля, содержащееся в DX. Этот вывод мы делаем только исходя из смыслового значения регистров, полностью игнорируя опкод команды. К сожалению, этого нельзя сказать о следующем байте - 0xAC. Это опкод операции LODSB, и его просто придется запомнить.

0x02 - это опкод ADD, а следующий за ним байт - это AH,AL (не буду больше повторяться).

0xE2 это опкод операции LOOP, а следующий за ним байт - это знаковое относительное смещение перехода.

00000010: 56 01 87 F2 AC 02 E0 E2 FB BE 3B 01 30 24 46 81 ^ ¦ L------5--------

Чтобы превратить его в знаковое целое, необходимо дополнить его до нуля (операция NEG, которую большинство калькуляторов не поддерживают). Тот же результат мы получим, если отнимем от 0x100 указанное значение (если разговор идет о байте). В нашем примере оно равно пяти. Отсчитаем пять байт влево от начала СЛЕДУЮЩЕЙ КОМАНДЫ. Если все сделать правильно, то вычисленный переход должен указывать на байт 0xAC == LODSB; впрочем, последнее было ясно и без вычислений, ибо других вариантов, по-видимому, не существует.

Почему? Да просто данная процедура подсчета контрольной суммы (или точнее, хеш-суммы) очень типична. Конечно, не стоит всегда полагаться на свою интуицию и "угадывать" код, хотя это все же сильно ускоряет анализ.

С другой стороны, хакер без интуиции - это не хакер. Давайте применим нашу интуицию и "вычислим", что представляет опкод следующей команды. Вспомним, что 0xB4 (== 10110100) это MOV AH,imm8

0xBE очень близко к этому значению, следовательно, это операция MOV. Осталось определить регистр-приемник. Рассмотрим обе команды в двоичном виде:

MOV AH, imm8 MOV ??, ???

* * 10110100 10111110 ^ ^^^ ^ ^ ^^^ ^ ¦ ¦¦¦ ¦ ¦ ¦¦¦ ¦ 'mov'<-+---¦L-+->AH (SP) 'mov'<+---¦L-+--> DH (SI) ¦ ¦ L--------> size <---------

Как уже говорилось выше, младшие три бита - это код регистра. Однако его невозможно однозначно определить без уточнения размера операнда. Обратим внимание на третий (считая от нуля) бит. Он равен нулю для AH и единице в нашем случае. Рискнем предположить, что это и есть бит размера операнда: хотя этого явно и не утверждается Intel, но вытекает из самой архитектуры команд и устройства декодера микропроцессора.

Заметим, что это, строго говоря, частный случай, - могло бы оказаться и иначе. Так, например, четверый справа бит по аналогии должен быть флагом направления или знакового расширения, но, увы, - таковым в данном случае не является. Четыре левые бита - это код операции 'mov reg,imm'. Запомнить его легко - это "13" в восьмеричном представлении.

Итак, 0xBE 0x3B 0x01 - это MOV SI,0x13B. Скорее всего 0x13B - это смещение, и за этой командой последует расшифровщик очередного фрагмента кода. А может быть и нет - это действительно смелое предположение. Однако 0x30 0x24 это подтверждают. Хакеры обычно настолько часто сталкиваются с функций xor, что чисто механически запоминают ее опкод.

Нетрудно установить, что эта последовательность дизассемблируется как XOR [SI],AH Следующий байт 0x46 уже нетрудно "угадать" - INC SI. Кстати, рассмотрим, что же интересного в этом опкоде:

_ 1000110 ^^ ^ ¦¦ ¦ ¦L-+-- AH\SI ¦

Третий бит равен нулю! Выходит, команда должна выглядеть как INC AH! (Что, кстати, выглядит непротиворечиво в смысле дешифовщика.) Однако все же это inc si. Почему мы решили, что третий бит - флаг размера? Ведь Intel этого никак не гарантировала! И INC byte вообще выражается через дополнительный код, что на байт длиннее.

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

81 ¦ V.З.м.рт.-;.0$FБ00000020:FE 56 01 72 F7 4E 02 0C 81 FE 3B 01 73 F7 80 F9 ¦ .V.rўN..Б.;.s.А.

Но продолжим наш анализ. 0x81 это, если можно так выразиться, "гейт". Наконец-то мы с ним столкнулись. Он открывает доступ к целому семейству команд, манипулирующих регистром-непосредственным значеним. "Гейтов" не так много, и всех их нужно знать наизусть. Они только указывают на группу команд, а конкретный код задается в поле reg следующего байта.

0x81: 10000001 -- 0xFE: 11111110 ^ ^^^ ^^ ^ ¦ ¦¦¦ ¦¦ ¦ size------- reg\imm<-+-¦ ¦L-+--> AH\SI LT- ¦

CMP

Таким образом, достаточно помнить, что 111b - обозначают CMP. Хотя об этом косвенно свидетельтсвует и сам операнд. Взгляните:

_____00000020:FE 56 01 72 F7 4E 02 0C 81 FE 3B 01 73 F7 80 F9 ¦ .V.rўN..Б.;.s.А. ^^^^^

Мы должны помнить, что это смещение нам уже встречалось в качестве буфера для вводимого пароля. Можно предположить, что расшифровщик проверяет границы и прекращает шифровку в этом месте. Ясно видно, что зашифрованный блок лежит между расшифровщиком и самим паролем. И тогда команда cmp SI, offset psw логична и уместна.

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

00000020:FE 56 01 72 F7 4E 02 0C 81 FE 3B 01 73 F7 80 F9 ¦ .V.rўN..Б.;.s.А. ^^^^^

Команды из серии 7x известны, наверное, каждому, кто хотя бы раз "правил байтики" в чужой программе. Конечно же, это условный переход! А как узнать само условие? Для этого необходимо вспомнить, о чем я говорил выше, и рассмотреть эту команду более детально:

1110010 ^^^^ ^^ ¦ ¦¦ ¦¦ Jx<-+--LT-L--> флаг '!(условие)'. ¦ условие

Само условие (упакованный регистр флагов) есть проверка флага переноса или, в более понятной мнемонике, JB. Самый правый бит - логическое 'NOT' равен нулю; следовательно, это "прямое" условие, т.е. JB xxx так и есть. Адрес перехода 0xF7 можно угадать не вычисляя. Но на всякий случай проверим:

--->------9-------->-¬00000010:56 01 87 F2 AC^02 E0 E2 FB BE 3B 01L30 24 46 81 ¦ V.З.м.рт.-;.0$FБ

00000020:FE 56 01 72 F7L4E 02 0C 81 FE 3B 01 73 F7 80 F9 ¦ .V.rўN..Б.;.s.А.

Оно равно -9 и переходит на операцию 'XOR'. Последующий код очень близок к 0x46 - INC SI. Особенно это хорошо заметно в двоичном представлении:

0x46 : 1000110 ¦¦¦¦ норма <---L++----> SI инверс <--¬-TT----> SI ¦¦¦¦ 0x4E : 1001110

Тут мы сталкиваемся еще с одиним недокументированным полем, которое характерно для некоторых команд. А именно с полем знака. Во втором случае он отрицательный. Следовательно, INC сменяется на DEC. Конечно, не зная самой команды, нельзя предугадать значение третьего бита, но тут есть одна хитрость. Дело в том, что однотипные команды объединены в группы, в которых действуют свои локальные условия. Отбросив три бита, мы получим, что 0x46 и 0x4E отличаются друг от друга всего на единицу, а значит "территориально" очень близки.

То же можно сказать и про опкод 0x2. Наверно, большинство знает, что 0x0 0x0 0x0... это сложение чего-то с чем-то, не так ли? Теперь можно установить, что 02 или 10 в двоичном представлении - это сложение одного операнда размером в байт с другим. Что это за операнды - покажет следующее поле 0xC:

00001100 ^^^^^^^^ ¦¦¦ ¦¦¦¦ r,m8 <---+-¦ ¦¦¦¦ CL <-----+--¦¦¦ базирование <---------¦¦ индексный р <----------¦ регистр SI <-----------

Таким обазом, получается CL -> [SI]. Но, вспоминая обратный бит направления в предыдущем байте, меняем операнды местами. И у нас получается ADD CL,[SI]. Похоже, считается контрольная сумма расшифрованного фрагмента. Очевидно, что следующей командой будет CMP SI,0x13B.

-----<-------9------<------¬00000020:FE 56 01 72 F7L4E 02 0C 81 FE 3B 01 73 F7-80 F9 ¦ .V.rўN..Б.;.s.А. ^^ ^^^^^

Действительно, это смещение наблюдается в дампе, равно как и 0x81. Следовательно, 0xFE в таком случае будет cmp si,offset 16. И это вряд ли нужно проверять.

А теперь обратим внимание на следующий код (0x73 0xF7 по понятным причинам мы опускаем):

_____ 80 F9 ¦ .V.rўN..Б.;.s.А.

Нам он уже встречался. Нетрудно вспомнить, что это cmp cl,imm8. Само непосредственное значение можно найти в следующем байте:

00000030:C3 74 08 B4 09 BA BE 01 CD 21 C3 B0 94 29 9A 64 ¦ +t.+.¦-.=!+-Ф)Ъd ^^

Оно равно 0xC3. Теперь следующая команда передает управление расшифрованному коду. Чтобы это действительно произошло, необходимо ввести правильный пароль. Как его найти, было подробно рассказано выше; кроме того, этот пример был разобран "по косточкам" и успешно взломан. Поэтому не будем на этом останавливаться.

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

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

Ф. Херберт "Еретики Дюны".

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

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

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

Однако этот способ несколько утомителен. Куда проще запомнить (или вычислить в уме) ряд квадратов:

1 2 4 8 16 32 64 128

Ясно, что любое число от нуля до 255 представляет собой их сумму. Причем каждая степень может встречаться только один раз. Покажем это на примере. Допустим, нам необходимо узнать двоичное представление числа 99. Начиинаем с конца. Число нечетное, значит, в сумме фигурирует единица, т.е. младший бит равен единице. Отнимаем от 99 один и получем 98. Если отнять еще и двойку, то получим 96, а 96 == 32 + 64 как легко можно видеть. Итого в двоичном виде это - 1100011. Конечно, это потребует определенных навыков устного счета, но все же достаточно просто, чтобы не обращаться каждый раз к калькулятору.

Аналогично можно любое число из двоичного перевести в десятичное. Например:

1001b == 1+2*0+4*0+8*1 == 1+8 == 9

Все вышесказанное не в меньшей мере применимо и к шестнадцатиричным числам:

0x1 0x2 0x4 0x8 0x10 0x20 0x40 0x80

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

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

Или более правдоподобная ситуация: вы в поезде (трамвае, автобусе) изучаете распечатку программы, а калькулятор, как назло, забыли взять. Как ни редко, но все же и такое случается!

Ассемблирование в уме
Ничто не превосходит по сложности человеческий ум.

Ф. Херберт "Еретики Дюны".

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

Другими словами, и нужно ли хоть кому-нибудь то, чем мы тут занимаемся? И если нужно, то при каких обстоятельствах? Нужно: во-первых тогда, когда иного выбора просто нет. С другой стороны, тренированный взгляд даже в километровом дампе (при беглом просмотре последнего) может заметить последовательности, характерные для защитного механизма... или для вируса.

Кстати, это типичный случай - когда необходимо удостовериться в наличии вируса в полученном файле. Достаточно дизассемблировать всего несколько десятков команд, чтобы все стало ясно - вирус это или нет. Конечно, не обязательно бывает именно так, но очень и очень часто. При этом посмотреть файл по "F3" гораздо быстрее, чем искать дизассемблер.

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

Кажется, что в такой ситуации ничего сделать невозможно. И администраторы подобных систем уверены, что они на 100% защищены от злоумышленников. Однако это лишь распространенное заблуждение. В MS-DOS есть возможность создавать бинарные файлы с помощью клавиши Alt и вспомогательной цифровой клавитуры. Когда-то это входило практически во все руководства по IBM XT\AT, а сейчас уже никем и нигде не упоминается.

Давайте воскресим этот древний "обряд" и создадим маленький бинарный файл, который ничего не делает, а только возвращает управление MS-DOS. Для этого дадим команду:

copy con test.com

Она вызовет примитивнейщий текстовой редактор системы, но его возможностей для нас в данный момент будет предостаточно. Убедившись, что индикатор "Num Lock" горит, нажмем ALt и, не отпуская ее, на цифровой клавитуаре наберем 195. Отпустим Alt и нажмем Ctrl-Z для закрытия файла и выхода из редактора.

Запустим полученный файл. Он ничего не делает, но и не зависает. Дизассемблировав его, мы поймем, что он состоит всего из одной команды RETN (0xC3 == 195). Конечно, это довольно незатейливый пример, и реализацию можно улучшить, если ввести "магическую" последовательность, показанную ниже.

Alt-180 Alt-09 Alt-186 Alt-09 Alt-01 Alt-205 ! 195 Alt-32 Hello,Sailor!$ Ctrl-Z

Как нетрудно догадаться, мы получим com-файл, выводящий указанную фразу на экран. Действительно, он это и делает. Но обратите особое внимание на то, что мы его создали используя только штатные средсва MS-DOS, которые есть на любой машине, где есть MS-DOS (или Windows).

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

Дизассемблируем только что полученный файл и обратим внимание на один ключевой момент: seg000:0100 start proc near seg000:0100 mov ah, 9 seg000:0102 mov dx, offset aHelloSailor ; "Hello,Sailor!$" seg000:0105 int 21h ; DOS - PRINT STRING seg000:0107 retn seg000:0107 start endp seg000:0108 db 20h ; ^^^^^^^ seg000:0109 aHelloSailor db 'Hello,Sailor!$' ; DATA XREF: start+2o

Зачем в этом месте стоит незначащий символ? Не лучше ли было избавиться от него? Увы, это никак не получится: 00000000: B409 mov ah,009 ; 00000002: BA0901 mov dx,00109 ^^ 00000005: CD21 int 021

Дело в том, что указанным способом невозможно ввести с клавиатуры символ #8. А смещение строки как раз и есть 0x108. Что бы избавиться от восьмерки, можно было бы, конечно, исполнить следующую последовательность команд: MOV DX,0x109 DEC DX

Кстати, 'DEC DX' это однобайтовая команда, и оба варианта эквивалентны по длине и выбор того или иного полностью зависит от вашего вкуса.

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

COLUMNS(4), DIMENSION(IN), COLWIDTHS(1.4200,.9608,1.0208,1.8825), WIDTH(7.5300), ABOVE(.0984), BELOW(.0984), HGUTTER(.0555), VGUTTER(.0433), BOX(Z_DOUBLE), HGRID(Z_SINGLE), VGRID(Z_SINGLE), KEEP(OFF), ALIGN(CT)

TTL9, TTL9, TTL9, TTL9

0, 3, 6, 8,

16 (0x10), 19 (0x13), 27 (0x1B) 255 (0xFF)

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

Теперь перейдем собственно к теме главы - как ассемблировать "на лету" программы в таких "спартанских" условиях. Самый банальный ответ - предварительно составить программу на машине, где выбор инструментария побогаче, а потом распечатать на бумаге полученный листинг, который запомнить или вводить в компьютер прямо с распечатки. Несмотря на все недостатки этого способа, он требует лишь минимального умственного напряжения и часто оказывается полезным во многих ситуациях. Однако он не имеет никакого отношения к теме главы. Поэтому вернемся к ситуации, когда мы сидим перед "голой" машиной и у нас под рукой только блокнот и остро заточенный карандаш. Нет даже такой необходимой вещи, как калькулятора. Впрочем, учитывая, что в Windows 95 принудительно входит Браузер, поддерживающий VBasic Script, то можно сказать, что довольно мощный "вычислитель" у нас всегда под рукой. Можно даже написать простейший шестнадцатиричный редактор, но это уже другой разговор. ~~12

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

Другим решением может быть объявление всех переменных до их использования. Например: MOV AH,9 JMP SHORT $+20 DB 'Hello,Sailor!'$xxxxx 1234567890123 456789 1111 111111 MOV DX,0x100+2+2 ; 0x100-адрес загрузки, 2 длина MOV AH,9, 2 длина jmp

JMP SHORT $+20 - резервирует 20 символов для строки. Предполагается, что этого окажется достаточно (даже не уточняя, сколько символов в строке). Тот же прием может быть применен и по отношению к меткам, направленным вперед. Иными словами, опытный специалист может написать довольно сложную программу даже без карандаша и блокнота. Это действительно "высший пилотаж", требующий изрядного таланта и опыта, но, с другой стороны, он многократно повышает вашу власть над машиной. Насколько это нужно - решать вам. Сегодня, когда уже и структурные языки выходят из моды, такие трудоемкие "ручные" способы программирования вряд ли найдут много приверженцев.

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

Но перейдем к делу. Для начала освоим самый простой метод: компиляцию и перенос программы на бумагу, откуда не составит труда ввести ее на любой машине. 00000000: B80103 mov ax,00301 ; Чтение одного секктора 00000003: B90100 mov cx,00001 ; сектор - 1, цилиндр - 0 00000006: BA8000 mov dx,00080 ; Головка - 0, 1-й HDD 00000009: CD13 int 013 ; Вызов дискового сервиса 0000000B: C3 retn ; Выход

Отметим, что в полученном дампе два раза встретился "запрещенный" символ #0, один раз #3 и один раз #19. Изменим программу так, чтобы избежать этого: 00000000: B80102 mov ax,00201 ; 00000003: FEC4 inc ah 00000005: B90101 mov cx,00101 ; 00000008: FECD dec ch 0000000A: 8AFE mov bh,dh 0000000C: B280 mov dl,080 ; 0000000E: BB0401 mov bx,00104 ; 00000011: FE4711 inc b,[bx][00011] 00000014: CD12 int 012 00000016: C3 retn

Теперь полученный дамп нужно перевести в десятиричное исчисление. Для этого лучше всего воспользоваться специально написанной программой, которую нетрудно будет написать за несколько минут на любом подходящем языке. #184 #001 #002 #254 #196 #185 #001 #001 #254 #205 #138 #254 #178 #128 #187 #004 #001 #254 'G' #017 #205 #018 #195

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

К счастью, в BIOS-ах предусмотрен специальный сторож, который сигнализирует о попытке модификации главного загрузочного сектора. Точно так же может стоять специальное ПО, перехватывающее и блокирующее запись. Например, Win98 поступает именно так, и запуск данной программы под ее управлением не возымеет ожидаемого эффекта. Однако ничто не помешает нам в DOS-окне обратиться непосредственно к портам ввода-вывода и на низком уровне управлять контроллером винчестера. Вообще-то программирование контроллеров к теме этой книги не относится (ему посвящены специальные руководства), но все же я привожу ниже пример процедуры, записывающей главный загрузочный сектор через порты ввода-вывода: MOV DX, 1F2h MOV AL, 1 OUT DX, AL INC DX OUT DX, AL INC DX XOR AX, AX OUT DX, AL INC DX OUT DX, AL MOV AL, 10100000B INC DX OUT DX, AL INC DX MOV AL, 30h OUT DX, AL LEA SI, Buffer MOV DX, 1F0h MOV CX, 513 REP OUTSW

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

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

HIEW

"Ничто не может возникнуть из ничего".

Ф. Херберт. "Дюна".

Сказанное ниже является моим личным мнением о hiew 6.03. Оно не во всем совпадает с мнением автора hiew. После попытки настоять на исправлении ряда моментов я пришел к выводу, что легче написать собственный *view с нуля, чем вступать в перепалку с автором.

Говоря конкретно, он наотрез отказался поддержать хотя бы интерпретируемый язык скриптов или предоставить мне API (компилятор я и сам мог бы написать), добавить поддержку двоичного ввода в калькулятор, поддержать редактирование заголовков PE\LE\LX файлов.

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

Десятилетиями в этих целях традиционно использовались hex-редакторы, которые концептуально мало отличались друг от друга. Менялся интерфейс и предоставляемый сервис - только и всего. HexWorkShop под Windows 95 и hexed под Агат-9 (может быть, кто-нибудь помнит такую машину) имеют больше ходств, чем различий. Евгений Сусликов был первым, кто догадался прикрутить в шестнадцатиричный редактор дизассемблер. Это породило продукт с совершенно новыми качествами. Вы пробовали когда-нибудь загружать в IDA или SOURCER исполняемый файл мегабайт эдак под двадцать? Десятки часов жужжания винта и сотни метров свопа - явление, хорошо знакомое каждому хакеру. А сам дизассемблер? Сколько дискет потребуется, чтобы его разместить, если предстоит работа "на выезде"?

Всех этих недостатков лишен hiew. Шустрый, компактный, проворный, в умелых руках он способен творить чудеса, при этом ограничиваясь чисто "формальными" требованиями к аппаратуре.

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

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

Перечисленные возможности позволяют полностью отказаться от остальных инструментов и проводить анализ программ не используя ничего кроме hiew. При этом задача взломщика ненамного усложнится, а то и наоборот. HIEW относится к ИНТЕРАКТИВНЫМ дизассемблерам, и его мощь в некотором отношении сравнима лишь с мощью IDA.

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

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

Кроме того, HIEW позволяет с легкостью и комфортом прогуляться по LE/PE/NE/LX/NLM файлам, исследовать формат и перекроить, к примеру, таблицы импорта на свой вкус. При этом hiew вообще является единственным шестнадцатиричным редактором, поддерживающим таблицы импорта вышеуказанных файлов:

Взгляните на следующий фрагмент: .00401145: 83EC18 sub esp,018 ;"" .00401148: 57 push edi .00401149: 33FF xor edi,edi .0040114B: 57 push edi .0040114C: FF1500204000 call GetCommandLineA ;KERNEL32.dll ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .00401152: 50 push eax .00401153: 57 push edi .00401154: 57 push edi .00401155: FF1504204000 call GetModuleHandleA;KERNEL32.dll ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .0040115B: 50 push eax

И сравните его, например с qview: 00001145: 83EC18 sub esp,00000018 00001148: 57 push edi 00001149: 33FF xor edi,edi 0000114B: 57 push edi 0000114C: FF1500204000 call dword ptr [00402000] ^^^^^^^^^^^^^^^^^^^^^^^^^^ 00001152: 50 push eax 00001153: 57 push edi 00001154: 57 push edi 00001155: FF1504204000 call dword ptr [00402004] ^^^^^^^^^^^^^^^^^^^^^^^^^^^ Не правда ли, потрясающие возможности для шестнадцатиричного редактора? HIEW можно считать полноценным Win32\Dos\Os/2 дизассемблером, поддерживающим не только 32-битные инструкции, но и форматы исполняемых файлов популярных операционных систем.

Заметим, что не так уж много популярных дизассемблеров поддерживают LE-формат. Но hiew - поддерживает, оставаясь незаменимым помощником при путешествиях в дебрях VxD. Использовать для этой цели IDA не всегда удобно - часто заранее неизвестно, в какой именно файл разработчик поместил защитный механизм и требуется окинуть беглым взглядом далеко не один драйвер виртуального устройства. IDA тратит больше времени на загрузку, чем я на анализ. Самое обидное, что анализ-то в большинстве случаев и не требуется: например, в этом случае сразу видно, что защита тут и не ночевала: .00000007: B800000000 mov eax,000000000 ; .0000000C: B94A000000 mov ecx,00000004A ; .00000011: C7400400000000 mov d,[eax][00004],000000000 .00000018: CD2014000A00 VxDcall VDD.Get_DISPLAYINFO ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .0000001E: 66F705020000000008 test w,[000000002],00800 ; .00000027: 0F85790D0000 jne .000000DA6 -------- (1)

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

Сказанное не должно восприниматься как наезд или повод для отказа от IDA. Это уникальный в своей категории инструмент, аналогов которому не существует и в обозримом будущем не предвидится. Но IDA - это все же тяжелое стратегическое оружие, а общение c hiew-ом больше напоминает работу разведчика.

Бытует мнение, что анализ программ непосредственно в hiew-е более сложен, чем в отладчике или полноценном дизассемблере. Некоторые это считают "высшим пилотажем". На мой взгляд, это лишь распространенное заблуждение. За исключением, может быть IDA, hiew обеспечивает весь сервис, предоставляемый другими "полноценными" дизассемблерами, при этом обладая и присущими IDA возможностями - например интерактивностью. Когда SOURCER может быть легко сбит с толку хитрым приемом разработчика защиты, с hiew-ом такого не произойдет, поскольку он работает в тесной связке с человеком. Нетрудно обмануть машину, но человек человека перехитрить не может (разве что ввести во временное заблуждение).

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

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

FILES.LST - файл описания (увы,плохо совместим с DN и др. оболочками)

HIEW.EXE - собственно сам HIEW (одновременно для DOS и OS\2)

HIEW.INI - конфигурационный файл

HIEW.HLP - файл помощи

HIEW.ORD - символьная информация ординалов распространенных файлов

HIEW.VMM - VMMcall/VxDcall для LE файлов

HIEW.XLT - файл перекодировок (Windows-1251\KOI-8R)

HIEWR.nnn - краткое описание на русском языке

HIEWE.nnn - краткое описание на английском языке

NEDUMP.EXE - утилита для исследования NE-файлов (не актуальна сегодня)

LXDUMP.EXE - утилита для исследования LX\LE-файлов

DEXEM.EXE - утилита для работы с Dual-EXE (NE/LE/LX/PE) файлами

SEN.ASC - публичный PGP ключ автора

HIEW.exe очень тяжел. Целых 284,855 байт, что отнимает много места, например, на спасательной дискете (а он у меня всегда на ней: мало ли с какими вирусами воевать придется). На самом деле это DUAL-exe файл, т.е. два файла для MS-DOS и OS\2 одновременно. Это оригинально, но слишком расточительно (заметим, что такие программы могут существовать и под Windows).

Первое, что необходимо сделать, - "разрезать" файл на две половинки и взять "родной" для вашей операционной системы. Для этого предназначена утилита dexem.exe.

Подробнее она будет рассмотрена ниже, а пока просто запустим ее следующим образом: dexem.exe /S hiew.exe При этом DUAL-exe будет расщеплен на два файла hiew.mz (MS-DOS) и hiew.ne (OS\2) по 102 и 183 килобайта соответственно. Отметим, что 102 много меньше 285, и, учитывая, что OS\2 в жизни многих пользователей, может быть, никогда и не встретится, хорошим решением будет удалить оригинальный hiew.exe и переименовать в последний hiew.mz.

Независимо от того, сделали вы это или нет, попробуем запустить hiew.exe без параметров. Кто знаком с ранними версиями hiew, тот помнит, что при этом программа просто не запускалась, ссылаясь на отсуствие файла в командной строке.

Версия 6.03 поддерживает встроенную систему навигации по файлам и каталогам, которая активируется всякий раз, когда hiew запускается без явного указания файла в командной строке. Логично было бы предположить, что то же произойдет при задании маски (например hiew.exe *.exe). Но автор мыслил иначе. При этом hiew просто найдет первый попавшийся файл, а если таковых не окажется, то с грустью сообщит "File(s) not found" и закончит работу. Печально.

Но вернемся к навигатору.

г==================D:\KPNC\HIEW===================¬ ¦ .. ¦>UP--DIR<¦Attr¦---Date---¦--Time-- ¦ ¦ PROHACK ¦>SUB-DIR<¦....¦15-03-1999¦14:06:06 ¦ ¦ CRACKME.EXE ¦ 182455¦.a..¦15-03-1999¦21:07:18 ¦ ¦ DEXEM.EXE ¦ 11408¦.a..¦22-10-1998¦11:32:26 ¦ ¦ ¦ L========================*========================- ^ маска отображ. файлов ---

1Help 2Hidden 3Name 4Exten 5Time 6Size 7Unsort 8Revers 9FilHis10Filter

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

Alt-F1 (Drive): смена текущего дисковода. Замечу, что hiew не совсем корректно обрабатывает список существующих устройств. Так, например, у меня он обраружил 'B', хотя тут 'B' отродясь не было. Попытка чтения с него привела к переадресации на 'A', что прошло не без "возмущений" со стороны Windows.

F2 (Hidden) - отображение скрытых и системных файлов. Кнопка действует как триггер.

F3 (Name) - сортировка по именам файлов.

F4 (Exten) - сортировки по расширениям.

F5 (Time) - сортировка по времени создания.

F6 (Size) - сортировка по размерам.

F7 (Unsort) - располагать файлы в том порядке, в каком их находит FindNextFile.

F8 (Revers) - обратить условные сортировки. Т.е. по умолчанию (за исключением даты) принята сортировка по возрастанию параметра. Реверс приводит к сортировке по убыванию. Действует как триггер.

F10 (Filter) - задать маску отображаемых файлов. К сожалению, не позволяет задавать более одной маски, что может вызвать неудобства. Маленький баг - если удалить маску, то hiew ее не восстановит по умолчанию. Для этого необходимо будет задать явно '*.*', что лично мне, например, просто неудобно.

При этом существует возможность быстрого поиска необходимого файла. HIEW вобрал в себя все лучшие решения от DN и NC и реализовал очень неплохие для простого встроенного менеджера возможности.

Нажатие любой символьной клавиши приводит к появлению консольной строки, в которой можно ввести имя файла (при этом курсор будет перемещаться по списку синхронно с вводом).

Есть и чисто юниксовская возможность дополнения введенного имени до полного, при условии что последнее однозначно определяет файл. Возможно, что это определение покажется витиеватым, поэтому приведу пример. Допустим, нам нужно найти файл crackme.exe. Если в текущей директории на 'c' есть только один файл, то логично, что он может однозначно быть определен заданием всего одной буквы. Вводим 'c' и нажимаем <tab>. Hiew, догадываясь, что мы хотим открыть crackme.exe, выводит его имя (между прочим, без расширения). А что будет, если у нас есть два файла crackme1 и crackme2? Тогда hiew, сердито пискнув, напишет только 'crackme' и остановится, ожидая уточнения - какой именно из двух файлов нам требуется открыть.

Кому-то это может показаться неудобным. В этом случае можно воспользоваться '*' - непосредственным аналогом Ctrl-Enter в DN и NC - последовательному перебору подходящих файлов.

Имеется и очень ценная недокументированная возможность задания списка в квадратных скобках. Например, [cr,h]ack.exe найдет все crack и hack. Если запятую опустить, то hiew будет интерпретировать строку как [c,r,h]. Т.е. *.[ch] он найдет все файлы c,cpp,h и др. Это очень полезная и вообще уникальная для платформы MS-DOS возможность, которая не существует ни в одной другой аналогичной программе.

Жаль, конечно, что эти возможности большей частью остаются невостребованными - hiew все же не файловая оболочка и чаще всего редактируемый файл непосредстенно задается в строке, хотя бы по чистой привычке, оставшейся от старых версий. (Я думаю, что если бы автор предусмотрел еще и запуск из Файлового Навигатора, то многие, и в первую очередь я, использовали бы его как оболочку, которая была бы особенно удобной на "спасательных" дискетах.)

Если hiew запущен с именем несуществующего файла, то он предложит создать его. Альтернативным вариантом является клавиша <Ins> в Навигаторе. Последняя возможность просто незаменима, когда новые файлы приходится создавать и открывать непосредственно во время работы. К примеру, может потребоваться сделать некоторые заметки по ходу работы, скопировать фрагмент в новый файл и при этом тут же открыть его и, скажем, расшифровать (отметим, что навигатор можно вызвать в любой момент работы клавишей F9).

Ctrl - '\' обеспечивает быстрый переход в корневую директорию текущего диска, а F10 - в материнскую директорию (ту, из которой был запущен hiew). При этом существует полезная возможность быстрого переключения между четырьмя произвольно выбранными директориями. Для этого существуют клавищи Ctrl-F1, Ctrl-F3, Ctrl-F5,Ctrl-F7, которые запоминают текущую директорию и Ctrl-F2, Ctrl-F4, Ctrl-F6,Ctrl-F8, которые, соответственно переходят в записанную. При этом есть возможность сохранения текущего состояния в файл и его последующего использования во всех сеансах. Впрочем, эта возможность реализована не самым лучшим образом. Нет никакой возможности сохранить состояние непосредственно из навигатора, поэтому приходится открывать файл только для того, чтобы получить доступ к клавише 'Ctrl-F10' - 'SaveSatus'. К ней мы еще вернемся, а пока отметим такую приятную особенность, как ведение истории просматриваемых файлов (F9).

г=Mode Offset Name===============================================¬ ¦ Hex ¦0000163C¦D:\KPNC\HIEW\HIEWR.602 ¦ ¦ Text¦00000452¦D:\KPNC\HIEW\DEXEM.EXE ¦ L==================================================================-

При этом кроме собственно имен сохранятся текущий режим и позиция курсора (что особенно приятно). Последнее позволяет использовать hiew для чтения больших текстовых файлов (электронных книг, документации): никогда не придется запоминать, на каком месте вы находились перед выходом. (Впрочем, чтобы быть до конца честными, отметим, что эта возможность присуща сегодня практически всем современным вьюверам - qview by AGC, UniversalViewer и MessageViewer by KPNC, да и многим другим.) Позволю себе так же отметить, что в этом UniversalViewer обогнал других. Тогда как hiew и qview привязываются к имени файла, UV - к хеш-сумме заголовка и окрестностей текущей позиции курсора. Имя файла при этом игнорируется. Последнее вызывает меньше конфликтов, хотя работает немного медленнее. Будем надеяться, что SEN в ближайших версиях реализует нечто похожее. ~~13

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

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

Параметр /SAV задает имя SAV-файла, который автоматически будет загружен. По умолчанию принимается hiew.sav, но последнее может быть изменено в hiew.ini: ; StartUp Savefile = "hiew.sav"

sav-файл полностью сохраняет текущее состояние hiew-а, включая текущую позицию, все закладки и т.д. Обычно, чтобы воспользоваться sav-файлом, нужно запустить hiew без параметров. Заметим, что 'hiew.exe MyFile.exe' не приведет к должному результату. Неудобно, конечно, но приходится мириться: хозяин (SEN) - барин.

Интересная особенность: конфигурационный файл можно также указывать в командной строке после ключа /INI. Это особенно удобно для "корпоративного" использования hiew сразу несколькими людьми. Каждому - настройки на свой вкус.

Если же требуется показать содержимое вложенных директорий, то можно использовать ключ /S с указанием пути и маски. При этом hiew /s C:\*.* с большой вероятностью после продолжительного шуршания диском завершит свою работу сообщением:

"No free memory".

Это будет зависеть от количества имеющихся у вас на диске файлов. Если их относительно немного, то есть шанс, что hiew запустится и можно будет выбрать любой понравившийся файл, нажав Ctrl-F9.

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

После выбора файла hiew автоматически показывает его в текстовом режиме. Не слишком удачный выбор для хакеров, поэтому они обычно первым делом редактируют следующую строку hiew.ini StartMode = Text ; Text | Hex | Code

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

ОСНОВНОЙ ЭКРАН:

- TRIAL.COM R NE 0000007B a16 -------- 823 ¦ Hiew 6.03 (c)SEN^ ^ ^^ ^ ^ ^ ^ ^¦ ¦ ¦¦ ¦ ¦ ¦ ¦ ¦¦ L-имя файла ¦¦ L тип ¦ ¦ ¦ L--длина файла (dec)¦ ¦¦ ¦ ¦ ¦¦ направление --L- состояние ¦ ¦ L- закладки¦ ¦ ¦L-лифт|% текущее смещение (hex) -- L-режим 16/32 разрядный

Вообще же строка статуса может меняться в зависимости от режима, но это не должно вызвать каких-то проблем понимания. Рассмотрим подробнее некоторые элементы.

Левосторонний лифт может показаться непривычным и действительно не очень удобен. Поэтому автор предусмотрел возможность настроить последний по вкусу пользователя, а то и вовсе отключить его. Для этого необходимо отредактировать hiew.ini. Если комментариев в файле окажется недостаточно, то обратитесь к главе "КОНФИГУРИРОВАНИЕ HIEW" настоящего руководства.

Направление поиска (прямое или обратное) задается клавшей Alt-7 в любой момент или непосредственно во время вызова окна поиска клавишей F2. При этом индикатор направления будет обновлен только после завершения поиска. Не нравится мне это. Неплохо бы перенести управление с F2 на ALt-F7 и при этом обновлять индикатор. Но не будем строги к автору - эта возможность появилась только в версии 6.03 и, конечно, до конца еще не отлажена.

Состояние файла может быть следующим:

(R)ead - открыт по чтению.

(W)rite - открыт по записи.

(U)pdate - изменен.

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

Первый же вызов редактора (F3) приводит к автоматическому переоткрытию файла в режиме полного доступа (чтения и записи). Этот режим сохраняется и после выхода из редактора. Т.е. автоматического переоткрытия "Только на чтение" не происходит. А жаль. Индикация просто теряет смысл.

Hiew автоматически распознает следующие типы файлов DOS EXE, NE, PE, LE,LX,NLM, но при этом отображет в стороке статуса только пять последних из них. DOS-EXE hiew, строго говоря, не поддерживает (за исключением заголовка). Да, собственно, там и поддерживать особо нечего. Можно, конечно, правильно настроить регистр DS, но это было бы слишком для шестнадцатиричного редактора - все же hiew изначально отнюдь не планировался как дизассемблер. Впрочем, если будет встроенный язык (а он все равно будет) - пользователи могут решать эти вопросы на месте, не дожидаясь новой версии. То же относится и к нестандартным бинарным файлам, например, разным BIOS-ам или дампам памяти. Для всего этого SEN написал замечательную утилиту StructLook, версия 4.20 которой выложена на ftp автора (ftp.kemsc.ru/pub/sen). Она содержит интерпретируемый препроцессор и очень удобна при "низкоуровневой" работе с различными форматами файлов. Но это совсем другая история.

Режим 16/32 определяется автоматически для поддерживаемых типов файлов. Это отличает его от qview, где режимы приходится переключать вручную, в противном же случае код дизассемблируется неправильно, что может приводить к печальным результатам. В режиме 'text', где понятие 16\32 разрядного кода как таковое отсутствует, это поле выражает номер самой левой отображаемой колонки, считая от нуля.

Очень неплохо продумана работа с закладками. Впрочем, удобно еще не значит привычно. Фирма Borland установила стандарт де-факто: Ctrk-K-n запомнить состояние, Atl-Q-n восстановить его. Эта точка зрения не была поддержана SEN, и он задействовал совсем другие "горячие" клавиши. Grey-'+' запомнить текущее состояние. Этот факт мгновенно отражается в индикаторе. Изображение '-' изменяется на порядковый номер закладки (считая с единицы?!). При этом hiew может запомнить до восьми закладок. Большего обычно и не требуется.

Восстановить текущую закладку (которую индикатор отмечает '') можно нажатием Gray-'-'. Выбрать любую другую закладку поможет Alt-'1-8'. При этом последняя автоматически помечается как текущая. Если ее потребуется удалить, то можно нажать Alt-'-'. А 'Alt-0' - удаляет сразу все закладки без предупреждения. Так что будьте осторожны с этой комбинацией!

В режиме редактора '<Editor>' закладки, к сожалению, становятся недоступны по причине того, что последний ограничен всего одним окном. Удивительно, но этот недостаток присущ лишь hiew-у, а конкуренты давно реализовали соответствующую возможность достойным образом. Самое интересное: непонятно, какие затруднения может испытывать автор с последним... Тем более что это действительно жесткое ограничение, которое особенно дает о себе знать при расшифровке небольших файлов. Поэтому все больше и больше людей сколоняются к мысли, что эту операцию лучше делать в qview, где нет таких ограничений. Остается только надеяться, что автор под мощным натиском общественного движения (ау! хакеры!) хотя бы через несколько версий реализует то, что конкуренты имели от рождения.

Длина файла отображается исключительно в неродном для хакеров десятичном исчислении. Вкупе с шестнадцатиричным смещением это особенно неприятно. Неплохой идеей, думается мне, был бы переход полностью на шестнадцатиричный режим в decode режиме и соответственно - на десятичный в текстовом. При этом было бы полезно отделять точкой каждые три знака, что улучшает читабельность больших чисел. Так что поле для работы у автора в следующих версиях еще есть, а это значит, что они будут выходить, выходить и еще раз выходить (правда, при том условии, если SEN-у все это не надоест и он не забросит свое творение в самый пыльный угол винчестера, как это произошло с ДеГлюкером, Cup-ом, InSight-ом... перечислять можно долго). Я как-то писал в одной своей утилите, что, пожалев сейчас 1$, через некоторое время можно потерять в сотни раз больше из-за отсутствия утилиты, которая не была написана именно по причине экономии этого самого доллара. Увы, российские пользователи привыкли, что лучшие программисты страны должны работать "просто так" для их собственного удовольствия).

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

Ф.Херберт. "Дюна".

Перейдем к непосредственному описанию возможносей hiew-а. Я долго колебался, что писать: "руководство по ключам" или "описание возможностей". В конце концов мой выбор остановился на последнем. По моему глубокому убеждению, описание на уровне клавиатуры не нужно тем пользователям, которые читают это руководство. HIEW все же хакерский продукт. "А зачем хакеру хелп?" - улыбается Сусликов. Но о возможностях, приемах работы и маленьких секретах читатель узнает с большим удовольствием. И может быть, тогда начинающие прекратят задавать глупые вопросы: "я знаю, какие байтики подправить, а в hiew-е их найти никак не могу",

Встроенный ассемблер может быть полезен для многих вещей. Так, например, небольшие файлы удобно набивать сразу в hiew-е, а не пользоваться MASM\TASM-ом, которые работают на порядок медленнее. И не понимают многих "извратов". Так, например, когда мне потребовалоь для хитрой защиты ассемблировать смесь шестнадцати- и тридцатидвухразрядного кода со множеством команд Pentuim Pro, никто кроме hiew-а и моих собственных ручек не смог этого сделать.

Кроме того, все команды сразу показываются и в hex-кодах, а не после редактирования\ассемблирования\линковки\дизассемблирования (при работе со стандарными средствами), что открывает свободу для экспериментирования и страхует от ошибок. Так, например, тот же TASM частенько даже при задании директивы USE32 почему-то вставляет в ненужных местах ненужные префиксы или (что не лучше) опускает с целью оптимизации расставленные мной. Хотите пример? Попробуйте указать префикс DS для данных. TASM его проигнорирует (разве что пальцем у виска не покрутит). А теперь представим, что в самомодифицирующемся коде я пытаюсь менять префикс, которой был опущен.

Так же незаменим встроенный ассемблер, если в ломаемой программе нужно не просто поменять 7x на EB, а дописать десяток-другой строк кода (а такое случается достаточно часто).

К сожалению, встроенный ассемблер содержит много ограничений, о которых будет рассказано ниже. Самое обидное, что в этом режиме hiew еще не поддерживает локальных смещений и все вычисления адресов приходится проводить вручную. К моему удивлению, не все знают, как это делается. На самом деле все очень просто, достаточно знать формат редактирумого файла. Покажем это на примере самого, по-видимому, распространенного PE-формата файлов, поддерживаемых платформой Win32 (мы пишем все же руководство по hiew, а не по взлому). Сначала рассмотрим, как происходит загрузка PE файлов. MicroSoft неплохо оптимизировала этот процесс, и PE целиком проецируются в память, включая и DOS-секцию. При этом один селектор выделяется для кода и данных. А это означает, что перевод глобальных смещений в локальные осуществляется тривиальным добавлением адреса загрузки, который можно узнать из заголовка PE файла. Поскольку hiew отображает последний в удобочитаемом виде, то наша задача упрощается еще больше, - достаточно заглянуть в поле Image base. В большинстве случаев там содержатся круглые значения, например 0x400000,0x010000. Не сложно выполнить все вычисления и в уме, однако к чему напрягаться? Я так и не смог выяснить с какой версии hiew поддерживает базирование, но это нам не помешает им с успехом воспользоваться. Перейдем в начало файла и нажмем Ctrl-F5, после чего введем значение Image base (в моем случае 400000). Посмотрим, что получилось:

COLUMNS(3), DIMENSION(IN), COLWIDTHS(1.4200,1.9392,1.9242), WIDTH(4.2600), ABOVE(.0984), BELOW(.0984), HGUTTER(.0555), VGUTTER(.0433), BOX(Z_DOUBLE), HGRID(Z_SINGLE), VGRID(Z_SINGLE), KEEP(OFF), ALIGN(CT)

TTL9, TTL9, TTL9

, Дизассемблер, Ассемблер

Без базирования:, .0040119A: call .000401298, 0000119A: call 000001298

С базированием:, .0040119A: call .000401298, 0040119A: call 000401298

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

Очень удобно, что hiew при ассемблировании не пытается оптимизировать то, что его не просят. Например, если вы укажете адресацию через DS. то перед командой появится соответствующий префикс 0x3E. Сравните: 00000000: 3EA16606 mov ax,ds:[00666] ^^ 00000004: A16606 mov ax,[00666]

Любой другой привычный нам ассемблер (tasm, masm) выдал бы идентичные результаты, что не вызывает у меня восторга. За это и любим hiew: он послушно делает то, что ему говорят.

Ассемблирование вообще процесс довольно творческий. Одна и та же мнемоническая инструкция может быть ассемблирована по-разному. Такова уж специфика архитектуры линейки 80x86 микропроцессоров от Intel. Микрокод первых процессоров разрабатывался в то далекое время, когда экономить приходилось каждый байт, и поэтому инженеры Intel обратили внимание на ситуацию, когда регистр размером в слово манипулирует непосредственным значением меньшим 0x100. При этом старший байт равен нулю, т.е. теоритически может быть ужат до одного бита, а этот самый бит можно разместить в пустующем поле приемника (ясно, что приемником непосредственный операнд быть никак не может). В результате этих ухищрений экономится один байт. Итак, такую команду можно записать двумя способами: 00000007: 83C266 add dx,066 ;"f" 0000000A: 81C26600 add dx,00066 ;" f"

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

Диаметрально противоположна ситуация при генерации переходов. По умолчанию hiew всегда генерирует близкий (near) переход 0xE9. Если необходимо задать близкий переход, то заставить hiew это сделать поможет команда jmps (jmp short действует аналогично). Позволю себе слегка покритиковать автора и заметить, что кракеры чаще всего заменяют условный переход на безусловный. При этом jmp near откровенно портит код. Обидно. ~~14

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

В остальном же ассемблер ведет себя корректно и безглючно. При этом имеет встроенный калькулятор. Т.е. он прекрасно переваривает конструкции типа mov ax,[66+11] и даже mov ax,[-77]. Последнее, правда, без учета режима (еще один баг - в копилку!). Отрицательные числа адреса всегда дополняются до слова. При этом в 32-разрядном режиме забавно выглядит попытка ассемблировать 'jmp -1' 00000003: E9F7FFFFFF jmp ^^^^^^

Заметим, что только извращенцу придет в голову пользоваться последним, однако в защитах это встречается и работает следующим образом. Пусть, например, есть код: 00000000: E9FAFFFFFF jmp -1 00000005: 90 nop 00000006: 90 nop

При этом jmp смещает указатель команд на единицу, и получается: 00000000: FF9090909090 call d,[eax][090909090]

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

Впрочем, это все мелочи, которые в ближайших версиях обоих продуктов будут исправлены.

Еще одним недостатком, приходящим на ум, является упорное нежелание hiew-a 'переваривать' директиву <ptr>, поэтому выражение mov Word ptr CS: [077],66

не может быть обработано синтаксическим анализатором. Приходится его сокращать до mov Word CS:[077],66

Ужасно неудобно менять свои привычки, но что поделаешь - приходится. Последнее частично скрашивается приятной возможностью сокращать byte/word/dword/pword/qword/tbyte до b/w/d/p/q/t, как показано ниже:

г= Pentium(R) Pro Assembler =====================================¬¦ mov w,[0077],0022--------------------------------------- ¦L================================================================-

Все числа считаются шестнадцатиричными по умолчанию, но никто не будет против буковки 'h'. А вот с 'x' анализатор не знает что делать и незамедлительно начинает ругаться. Это выглядит особенно странно на фоне калькулятора, который такую нотацию как раз понимает, но в свою очередь не переваривает 'h'.

Чем больше я изучаю hiew, тем больше у меня складывается впечатление, что его писали два разных человека. Это, конечно, больше шутка, чем правда. Однако не мешало бы попинать автора, чтобы привести анализатор в порядок, а то работать порой бывает весьма дискомфортно.

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

Дизассемблер
Дизассемблер в hiew великая вещь. Фактически это основной режим работы хакера. Не то чтобы некоторые ленились дизассемблировать в уме hex-коды, (что, скажем, частенько приходится делать при работе со встроеным вьювером в DN), но ассемблерский листинг все же привычнее для глаза и, кроме того, имеет множество дополнительных возможностей (таких, как поиск ассемблерских команд по маске), которые заметно ускоряют анализ программ.

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

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

Но это когда еще будет (и будет ли вообще), а hiew уже есть и поддерживает инструкции вплоть до Pentium Pro. А точнее, до P6-kernel, которое используется и в Celeron-ах.

Типичный вид дизассемблированного текста таков: .00401207: 50 push eax .00401208: 52 push edx .00401209: E8D2FEFFFF call .0004010E0 -------- (1) .0040120E: 83C404 add esp,004 ;"" .00401211: 50 push eax .00401212: E8A9FEFFFF call .0004010C0 -------- (2) .00401217: 83C408 add esp,008 ;"" .0040121A: 663DF801 cmp ax,001F8 ;"°" .0040121E: 7404 je .000401224 -------- (3) .00401220: 6A03 push 003 .00401222: EB02 jmps .000401226 -------- (4) .00401224: 6A04 push 004

Hiew позволяет "путешествовать" по файлу, входя в процедуры и выполняя условные\безусловные переходы. Для этого нужно нажать цифру, которая показана в круглых скобках слева. Посмею высказать свое (возможно предвзятое) мнение, что переходы в IDA реализованы несколько лучше. Переход осуществляется по адресу, на котором находится курсор. Это действительно удобнее, т.к. позволяет "гулять" и по смещениям, передаваемым через стек или регистры. Например: .004012B9: 6840314000 push 000403140 ;" !!AMPER!!1!!AMPER!!" .004012BE: 6844314000 push 000403144 ;" !!AMPER!!1D" .004012C3: FF74240C push d,[esp][0000C] .004012C7: E834010000 call .000401400 -------- (2)

Hiew не распознал смещения 0х00403140 и 0х00403144. Конечно, можно перейти по ним вручную (F5), но это не очень приятно. Впрочем, мое мнение может не совпадать с мнением автора.

При этом поддерживается многоуровневый откат, который по умолчанию находится на '0' (как это изменить, рассказано в описании файла hiew.ini). К сожалению, буфер отката кольцевой, что не вызывает восторга. Т.к. чтобы вернуться в начальную позицию, надо держать в голове глубину вложенности (а это весьма проблематично). Было бы гораздо лучше если бы при исчерпании стека hiew пищал хотя бы...

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

При этом hiew корректно обрабатывает относительные переходы. Например, можно задать +77 или -66 и курсор переместится на 77h байт вперед или 0x66 назад относительно текущей позиции. Печально, но при этом откат невозможен. Хотя поддержка его была бы не лишней и великолепно вписывающейся в общий антураж программы. Еще один довод в пользу того, что встроенный язык избавил бы его от подобных приставаний.

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

Поэтому выбор многих останавливается на hiew-е. Даже когда он не мог делать это автоматически, ручной поиск занимал все же меньше времени, чем загрузка файлов в IDA. К тому же в большинстве случаев ссылки на сегмент данных в PE-файлах (например, поиск кода, выводящего строку 'key not found') с легкостью обнаруживались "прямым" поиском локальных смещений (с учетом обратного порядка байтов в двойном слове).

Однако поиск относительных смещений таким образом был уже невозможен. С другой стороны, требуемое нам смещение лежит "прямым текстом" в дизассемблированном листинге. Остается лишь просканировать последний. Не могу удержаться и не заметить, насколько логична в этом случае IDA, которая поддерживает "медленный" поиск подстроки именно в тексте дизассемблера. Это действительно медленно, но на все 100% надежно. hiew же просто дизассемблирует код на лету с шагом в одну команду (или даже байт) и сравнивает непосредстенный операнд с текущим смещением, при этом косвенная адресация игнорируется и значения сегментых регистров не отслеживаются. Поэтому такой поиск хорошо работает только на односегментных моделях памяти. Во всех других случаях появятся проблеммы (ссылки не будут найдены или найдены неверно).

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

По умолчанию при дизассемблировании hiew анализирует текст с шагом в одну команду, что многократно ускоряет работу, но недостаточно надежно. Разберем следующий пример: retn DB 0x66 call 0x0666

Для com-файлов это достаточно типичный случай. Как вы думаете дизассемблирует его hiew? Разумеется hiew не догадается, что '0x66' переменная, и выдаст следующий результат: 00000000: C3 retn 00000001: 66E86106 call 000000668 ^^^^^^^^^

Обратите внимание, что теперь переход вычислен неправильно и весь анализ программы летит к черту. А если переменная будет равна нулю (что чаще всего и бывает), на экране появится следующий мусор: 00000000: C3 retn 00000001: 00E8 add al,ch ^^^^ 00000003: 61 popa 00000004: 06 push es

Это происходит потому, что hiew неправильно определил границы команд, в результате чего не смог их правильно дизассемблировать. Ни в коем случае не стоит считать последнее "глюком" или недостатком. Это следствие самой концепции элементарного дизассемблера. IDA справляется с этой ситуацией ценой больших затрат времени на анализ программы. Для файлов в сотни килобайт это не вызывает проблем на современных быстродействующих процессорах, но даже мощности Pentuim-a II и Celeron-a начинает не хватать, когда счет идет на мегабайты или даже десятки мегабайт (между прочим, размер типичного исполняемого файла под Windows).

Ситуацию может спасти разве что перекладывание части работы на человека. В приведенном примере ясно, что код после ret, собственно говоря, не является обязательно кодом. С таким же успехом это могут быть данные. Чтобы разобраться, небходимо найти ссылки на эту область памяти. Устанавливаем курсор на первый байт, переключаем (на всякий случай) шаг сканирования на единицу (alt-F6) и нажимаем F6. Допустим, hiew нашел следующий код: MOV AL,[01]

(Впрочем, не факт, что версия 6.03 его найдет, но для простоты будем считать, что hiew к моменту чтения этого опуса уже научился поддерживать и такую адресацию). Ясно, что 0x01 - это переменная размером в байт. Отмечаем это (карандашом в блокноте, т.к. hiew все еще не поддерживает комментариев) и переходим к ячейке 0x2. Вновь нажимаем f6 и изучаем код, манипулирующий с этим адресом. Пусть он выглядит следующим образом: 00000000: C3 retn 00000001: 00E8 add al,ch 00000003: 61 popa 00000004: 06 push es 00000005: A00100 mov al,[00001] 00000008: E8F7FF call 000000002 -------- (1) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Чтобы привести код в удобочитаемое состояние, достаточно перейти по адресу 0x2, для чего можно нажать '1' (переход по ссылке) или даже <F5> "02". 00000002: E86106 call 000000666 00000005: A00100 mov al,[00001] 00000008: E8F7FF call 000000002 -------- (1)

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

Если приведенный пример действительно является com-файлом, то скорее всего hiew не сможет правильно найти ссылки, потому что неправильно вычислит адреса. Это неудивительно, если вспомнить, что com-файлы загружаются в память со смещения 0x100 в текущем сегменте. Пусть оригинальный файл выглядел так: 0x100: A10401 mov ax,[00104h] -¬ 0x103: C3 retn ¦ 0x104: 1234 DB 3412h <----

Hiew же дизассемблирует его как: 00000000: A10401 mov ax,[00104] --¬ 00000003: C3 retn ¦ 00000004: 1234 adc dh,[si] ¦ ........ ¦ 00000104: xxxx <----

Разница в том, что ссылка во втором случае указывает "в космос", но никак на переменную 0x4. Это можно исправить, указав hiew-у вручную начальное смещения файла. Выше мы уже сталкивались с этим при анализе PE-файлов. В терминологии SEN-а это называется базированием и может быть задано в любой момент анализа (а не только до загрузки, как во многих других дизассемблерах).

Нажимаем Ctrl-F5 и вводим число 0x100. Теперь код выглядит следующим образом: 00000100: A10401 mov ax,[00104] -----¬ 00000103: C3 retn ¦ 00000104: 1234 adc dh,[si] <----- 00000106: 0100 add [bx][si],ax 00000108: E8F7FF call 000000102 -------- (1)

И все ссылки при этом работают правильно. Заметим, что базирование никак не влияет на вызовы процедур и переходы, поскольку в архитектуре процессоров intel они относительные.

Рассмотрим более сложный пример, в котором код и данные размещены в разных сегментах. hiew ничего не знает о последних и поэтому неверно вычисляет все ссылки. Рассмотрим, например, любую программу на Паскале. Загрузим ее в hiew и перейдем в точку входа (F8, F4, но об этом чуть позже). 000000CE: 2E9A00007100 call 00071:00000 -------- (1) 000000D4: 9A0D000F00 call 0000F:0000D -------- (2) 000000D9: 55 push bp

Поскольку после загрузки файла регистр DS указывает на сегмент PSP, то приложение должно настроить его самостоятельно. В программах, компилированных turbo-pascal, это происходит в модуле SYSTEM (самый первый вызов). Входим в него нажатием '1' и изучаем следующий код: 000007A0: BA0D01 mov dx,0010D ;"" 000007A3: 8EDA mov ds,dx

Что такое 0x10D? Это смещение в памяти, которое отличается от смещения в файле только длиной заголовка. На деле, в отличие от PE файлов, загрузчик DOS-EXE пропускает заголовок, тем самым экономя немного памяти. Длину заголовка узнать несложно (значение поля Paragraphs in header нужно умножить на 0x10). Аналогично поступим и со значением DS. Итого 0x10D0+ 0x90 == 0x1160 смещение сегмента данных в файле.

Смотрим на код, стоящий ниже: 000007A0: BA0D01 mov dx,0010D ;"" 000007A3: 8EDA mov ds,dx 000007A5: 8C063800 mov [00038],es

Чтобы узнать, на какую ячейку ссылается [0x038], надо к последней добавить 0x1160. Не правда ли, утомительно? Было бы гораздо лучше, если бы hiew выполнял такой пересчет автоматически. Попробуем для этой цели использовать базирование. Установим курсор на адрес 0x1160 и нажмем Alt-F5. Теперь надо добиться, чтобы текущее смещение равнялось нулю. Очевидно, для этого необходимо задать базирование, равное по модулю, но противоположное по знаку. Т.е. '-0x1160'. Однако hiew поддерживает и относительные смещения, отмеченные префиксом '*'. Это действительно удобно и избавляет от лишних математических вычислений. При этом Ctrl-F5,Ctrl-F5 действует аналогично '*0'.

Мы добились того, что смещения в сегменте данных начинаются с нуля, но... Маленькое, но грустное "но". Взгляните на сегмент кода: FFFFEF6F: 2E9A00007100 call 00071:00000 -------- (2) FFFFEF75: 9A0D000F00 call 0000F:0000D -------- (3) FFFFEF7A: 55 push bp FFFFEF7B: 89E5 mov bp,sp FFFFEF7D: 31C0 xor ax,ax

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

Так-то оно так, но Турбо-Паскаль имеет странную привычку располагать некоторые данные в кодовом сегменте. Взгляните: FFFFEF31: 10 82 A2 A5-A4 A8 E2 A5-20 AF A0 70-AE AB EC 3A Введите паpоль: FFFFEF41: 20 04 59 75-4B 69 12 8F-A0 70 AE AB-EC 20 AD A5 YuKiПаpоль не

И вот обращение к этой строке: FFFFEF88: BF6602 mov di,00266 ;"f" FFFFEF8B: 1E push ds FFFFEF8C: 57 push di FFFFEF8D: BF0000 mov di,00000 ;" " ^^^^^^^^^^^^^^^^^^ FFFFEF90: 0E push cs FFFFEF91: 57 push di FFFFEF92: 31C0 xor ax,ax FFFFEF94: 50 push ax FFFFEF95: 9A70067100 call 00071:00670 --------(5)

Кто бы мог подумать, что в кодовом сегменте эта строка располагается с нулевым смещением! Однако это действительно так, в чем можно убедиться: F5,90\Ctrl-F5,Ctrl-F5: 00000000: 10 82 A2 A5-A4 A8 E2 A5-20 AF A0 70-AE AB EC 3A Введите паpоль: 00000010: 20 04 59 75-4B 69 12 8F-A0 70 AE AB-EC 20 AD A5 YuKiПаpоль не

Но теперь "уползли" все смещения в сегменте данных. И необходимо при первом же обращении к нему вернуть все на место. Как-то неаккуратно получается. К тому же маловероятно, чтобы это было как-то исправлено в последующих версиях. Автор hiew-а изначально рассчитывал на поддержку только одного сегмента. Теперь же, когда DOS файлы уходят в прошлое, это ограничение выгядит уже не таким существенным.

Поиск
К полной луне призывы... Шаи-Хулуд поднялся, чтобы ее увидеть; Красная ночь, сумеречное небо, Кровавая смерть - ты погиб. Возносим молитвы луне: она круглая... Счастье всегда будет с нами, Потому, что мы нашли то, что искали, В стране с твердой землей.

Ф. Херберт. "Дюна".

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

Это позволяет искать любые команды и их сочетания в ассемблерском тексте. Мощность и возможности этого механизма не имеют равных ни среди отчественных, ни среди зарубежных утилит. Даже IDA не представляет такого сервиса.

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

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

Поэтому взломщики используют различные хитрости, которые при всем их разнообразии сводятся к одному: поиску уникальной последовательности команд, которая предположительно присутствует в защитном механизме. Например, типичной будет конструкция : if (!IsValidUser()) abort();

или if (IsValidUser()) { ; // нормальное выполнение программы } else abort();

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

г=[Forward /Full ]===================================================¬ ¦ ASCII: -------------------- ¦ ¦ ¦ ¦ Hex: ------------------------------------------------------------¦

г= Pentium(R) Pro Assembler =====================================¬ ¦ call ?;or ax,ax;j?-------------------------------------------- ¦ L================================================================-

Hiew найдет приблизительно следующий код: .0001140C: 9A3E172114 call 001:0173E -------- (4) .00011411: 0BC0 or ax,ax .00011413: 7405 je .00001141A -------- (5)

Разумеется, возможно, что call 001:0173E на самом деле не имеет никакого отношения к защите (так скорее всего и будет), но тем не менее ожидается, что в программе не так уж много вышеприведенных комбинаций, и в любом случе это сужает поиск.

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

г=[Forward /Full ]===================================================¬ ¦ ASCII: -------------------- ¦ ¦ ¦ ¦ Hex: ------------------------------------------------------------¦

г= Pentium(R) Pro Assembler =====================================¬ ¦ call ?;or eax,eax;j?;;? 66664000------------------------------ ¦ L================================================================-

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

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

FindNext сбрасывается при перемещении курсора в начало файла по Ctrl-Home, (или в конец по Ctrl-End). Так же разумеется FindFirst (F7) и непонятно откуда взявшееся Goto (F5). Последнее иногда вынуждает на неприятные "путешествия" по файлу "вручную" (кирпич на Page Down).

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

К сожалению, в строке поиска нельзя задавать логические конструкции типа 'AND', 'OR' и другие. Между тем эта возможность очень полезна и в последнее время поддерживается многими популярными системами.

Манипуляции с блоками
"Соединение невежества и знания, соединение дикости и культуры - не начинается ли оно с того чувства достоинства, с которым мы относимся к своей смерти?"

Ф. Херберт "Дюна".

Выше мы затронули способность hiew-а работать с выделенными фрагментами текста (блоками). Рассмотрим теперь это подробнее. Для того чтобы выделить фрагмент, необходимо однократно нажать Gray-'*'. Теперь перемещения по файлу с помощью клавишей управления курсором будут приводить к выделению фрагмента и заливке его по умолчанию бордовым цветом. Повторное нажатие Gray-'*' прекращает выделение и активирут клавиши управления блоком.

Это Запись\Чтение блока в файл, а также заливка его некоторым значением. Рассмотрим окно записи:

г===================== Write block to file ======================¬ ¦ Block: 000001FF-000002EF length:000000F1/241 ¦ ¦ File: ------------------------------------------------------ ¦ ¦ Offset: ........ ( Hexadecimal ) ¦ ¦ Table: As Is г====== Select table =======¬ ¦ L=================¦ As Is ¦==================- ¦ Windows-1251 ¦ ¦ Koi-8 ¦ L===========================-

Приятной особенностью является возможность записи в файл с произвольным смещением. Это, дейстительно, насущная потребность любого кодокопателя. Скажем, вы решили заменить шрифты в файле или для какой-то цели дописать в его конец маленькую программу (скажем, вирус). При этом можно выбрать любую кодировку. Да, я не оговорился: "любую", и это следует понимать буквально вплоть до кодировки племен индейцев Северной Америки. Ниже описывается структура файла hiew.xlt, который позволяет это делать.

Замечательно, что все смещения и длина блока выражены шестнадцатиричными цифрами. Это попросту удобно и практично, поскольку большинству хакеров десятичная система нравится куда меньше.

То же самое наблюдается и в окне чтения блока. Взгляните:

г===================== Read block from file =====================¬ ¦ Block: 000001FF-000002EF length:000000F1/241 ¦ ¦ File: ------------------------------------------------------ ¦ ¦ Offset: ........ ( Hexadecimal ) ¦ ¦ Table: As Is ¦ L================================================================-

Загрузка блока - процесс, обратный записи. И на редкость бесполезный. За мою сознательную жизнь я эту возможность ни разу не использовал. Не то чтобы мой опыт был каким-либо важным показателем, но я действительно не могу придумать ситуацию, в которой эта возможность была бы необходима, поэтому не будем на ней останавливаться, а перейдем сразу к "заливке":

г====================================================================¬ ¦ ASCII: -------------------- ¦ ¦ ¦ ¦ Hex: ------------------------------------------------------------¦ L====================================================================-

Это окно вызывается по Alt-F3. Никаких проблем управление им вызвать не должно. Заметим только, что все операции с блоком являются необратимыми и лучше семь раз подумать, чем нажать на кнопку. Резервной копии ведь за вас никто не сделает.

Поддержка LE/PE/NE/LX/NLM форматов
Понятие прогресса служит защитным механизмом, отгораживающим нас от ужасов будущего.

Ф.Херберт. "Дюна".

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

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

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

Неудивительно, что вся поддержка DOS-EXE свелась к простому отображению заголовка в удобочитаемом виде.

г========[ MZ-header ]=========¬ ¦ Signature 4D5A ¦ ¦ Bytes on last page 01C0 ¦ ¦ Pages in file 0009 ¦ ¦ Relocations 001D ¦ ¦ Paragraphs in header 0009 ¦ ¦ Minimum memory 0436 ¦ ¦ Maximum memory A436 ¦ ¦ SS:SP 0149:4000 ¦ ¦ Checksum 0000 ¦ ¦ CS:IP 0000:003F ¦ ¦ Relocation table adress 001C ¦ ¦ Overlay number 0000 ¦ ¦ Overlay length 00000B1F ¦ ¦ NewExe offset 00000000 ¦ ¦ >Entry point 000000CF ¦ L==============================-

Пояснять значения полей здесь нет смысла - это гораздо лучше расписано в руководстве программиста для MS-DOS. Ознакомившись с последним, можно попытаться отредактировать поля, когда в этом возникнет необходимость. hiew позволяет сделать это с комфортом, избавляя от некоторых рутиных вычислений. Так, например, F2 автоматически вычисляет значения полей Pages in file и Bytes on last page (это бывает необходимо при манипуляции с размером файла, чаще всего "отрезании" того мусора, который так любят оставлять в конце некоторые распаковщики). При этом hiew никак не учитывает значения поля Overlay length, что может привести к некоторым проблемам и является досадным багом, который автор ухитрился до сих пор не исправить (вероятнее всего потому, что никто из пользователей hiew этого и не заметил; а если кто и заметил, так не имел Интернета, чтобы ему об этом сообщить).

Другим приятным сервисом является возможность быстрого перехода в точку входа (F5) и в начало кодового сегмента (F4). Заметим на всякий случай, что это не взаимозаменяемые понятия и редкие exe-файлы начинают выполнение с нуля.

Клавиша F2 поможет быстро перейти в начало оверлея. Это, пожалуй, все, что можно сказать про поддержку old-dos форматов. Предлагаемый набор сервиса типичен для современных hex-редакторов и скопирован практически всеми конкурентами.

Диаметрально противоположно обстоит дело с поддержкой PE-файлов. Выше мы уже неоднократно сталкивались с этим, а сейчас рассмотрим подробнее:

г===========================[ PE-header ]============================¬ ¦ Count of sections 4 ¦ Machine(014C) intel386 ¦ ¦ Symbol table 00000000[00000000] ¦ TimeStamp 36CC1C56 ¦ ¦ Size of optional header 00E0 ¦ Magic optional header 010B ¦ ¦ Linker version 6.00 ¦ OS version 4.00 ¦ ¦ Image version 0.00 ¦ Subsystem version 4.00 ¦ ¦ Entrypoint RVA 00001390 ¦ Size of code 00001000 ¦ ¦ Size of init data 00003000 ¦ Size of uninit data 00000000 ¦ ¦ Size of image 00005000 ¦ Size of headers 00001000 ¦ ¦ Base of code 00001000 ¦ Base of data 00002000 ¦ ¦ Image base 00400000 ¦ Subsystem(0003) Windows char ¦ ¦ Section alignment 00001000 ¦ File alignment 00001000 ¦ ¦ Stack 00100000/00001000 ¦ Heap 00100000/00001000 ¦ ¦ Checksum 00000000 ¦ Number of directories 16 ¦ L====================================================================-

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

Когда же сведений, описываемых документацией, начинает не хватать, хакеры обычно обращаются к winnt.h - настоящей сокровищнице, где все структуры хоть и бедно комментированы, но все же хоть как-то расписаны.

Неприятным моментом будет отсутствие возможности редактирования заголовка непосредственно из этого экрана. То ли SEN посчитал, что среднему хакеру это не нужно, то ли просто поленился (кто знает...), - но если дело до этого дойдет, то придется, вооружившись SDK и MSDN, орудовать в hex-редакторе вручную. Или писать свою собствнную утилиту, поскольку такая необходимость возникает достаточно часто.

Аналогично поступил автор и с показом флагов (F2). Смотреть можно сколько угодно, а редактировать - нет. Печально.

г===================================================================¬ ¦ Characteristics 010F ¦ DLL flag 0000 ¦ ¦ 0: Relocations stripped :Yes ¦ 0: Process initialization :No ¦ ¦ 1: Executable image :Yes ¦ 1: Process termination :No ¦ ¦ 2: Line numbers stripped :Yes ¦ 2: Thread initialization :No ¦ ¦ 3: Local symbols stripped :Yes ¦ 3: Thread termination :No ¦ ¦ 4: reserved :No ¦ ¦ ¦ 5: reserved :No ¦ Loader flag 00000000 ¦ ¦ 6: 16 bit machine :No ¦ 0: Break on load :No ¦ ¦ 7: Bytes reversed lo :No ¦ 1: Debug on load :No ¦ ¦ 8: 32 bit machine :Yes ¦ ¦ ¦ 9: Debug stripped :No ¦ ¦ ¦ 10: Patch :No ¦ ¦ ¦ 11: reserved :No ¦ ¦ ¦ 12: System file :No ¦ ¦ ¦ 13: File is DLL :No ¦ ¦ ¦ 14: reserved :No ¦ ¦ ¦ 15: Bytes reversed hi :No ¦ ¦ L===================================================================-

На этом фоне довольно качественной выглядит навигация по секциям PE-файла (F6) или, в терминологии hiew, по таблице объектов. При этом выдается дополнительная информация о каждой секции. При этом, к сожалению, вновь отсутствует возможность редактирования и все флаги показаны не в бинарном, а в шестнадцатиричном виде. Последнее вынуждает выполнять все расчеты в уме. Действительно, какие атрибуты имеет секция .text? 0х60000020 можно разложить на 0x20+0x40000000+0x20000000. (Если читатель не понял, откуда взялись эти цифры, то пусть переведет 0x60000020 в двоичное представление, оно будет таким: 00000110000000000000000000100000b. Теперь уже нетрудно вычислить, что 0100000b == 0x20; 010000000000000000000000000b == 0x20000000 и 0100000000000000000000000000b == 0х40000000. Достаточно тривиальные вычисления, которые любой хакер производит в уме даже без помощи калькулятора). ~~15 Получается, что секция .text имеет следующие атрибуты - Code | Can be discarded | Not cachable. Было бы куда нагляднее, конечно, представить всю эту информацию сразу в удобочитаемом виде. Но, автор признается, что не любит плодить монстров, да и не так уже трудно все эти вычисления выполнить в уме, который стремительно усыхает в наш бурный век думающих машин и автоматических калькуляторов.

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

г===========================[ PE-header ]============================¬¦ ¦¦ г=Number Name VirtSize RVA PhysSize Offset Flag===¬ ¦¦ ¦ 1 .text 000004FA 00001000 00001000 00001000 60000020 ¦ ¦¦ ¦ 2 .rdata 0000051A 00002000 00001000 00002000 40000040 ¦ ¦¦ ¦ 3 .data 00000168 00003000 00001000 00003000 C0000040 ¦ ¦¦ ¦ 4 .rsrc 000000F0 00004000 00001000 00004000 40000040 ¦ ¦¦ ¦ г========================[PE Object flags]=========================¬L ¦ ¦ -------------------- Flag for object ------------------------- ¦ L= C¦ 0x00000004 Used for 16-bit offset code. ¦ ¦ 0x00000020 Code. ¦ ¦ 0x00000040 Initialized data. ¦

Кроме отображения секций, hiew еще умеет читать IMAGE_DATA_DIRECTORY и представлять ее в удобочитаемом виде. Это действительно наиболее важный элемент структуры PE-файла, необходимый для поиска таблиц экспорта\импорта и ресурсов. Оставим временно в стороне ресурсы и обратим внимание на таблицу импортируемых функций. Фактически изучение любого приложения начинается с ее анализа. Какие функции вызывает программа? Какие загружает DLL? Ставить ли точку останова на GetWindowTextA или GetDlgItemTextA? На все эти вопросы можно найти ответ, просмотрев список импортируемых функций.

Естественно, что при изучении DLL нас в первую очередь будет интересовать, наоборот, экспорт и соответствующая ему секция. Конечно, мне могут возразить, что для этого существуют специальные программы наподобие dumpbin, которые делают всю работу за нас, генерируя удобный список, а в hiew-е еще придется полазить по каждой секции вручную. И уж совсем, казалось бы, не к месту разговор о ресурсах, в которых с первого взгляда ни одному смертному разобраться не дано. К тому же существуют великолепные визуальные редакторы ресурсов наподобие популярного Borland ResourceWorkShop.

Так-то оно так, да только на первый взгляд. "Популярные и великолепные" редакторы оказываются неспособными поддерждать новые элементы Win98 (например календарь) и при этом просто необратимо портят ресурс (особенно это относится к Борландовскому редактору).

Относительно же таблиц экспорта\импорта разница между "структурированным" листингом и "живым" предствлением AS IS не так уж и велика. Действительно, взгляните на рисунок. Не нужно большой сноровки, что бы бегло пробежаться глазами по знакомым функциям MSVCRT. Впрочем, для "гурманов" Сусликов включил в пакет несколько специальных программ *dump, позволяющих более детально исследовать формат файла.

г= Name RVA Size =¬ ¦ Export 00000000 00000000 ¦ ¦ Import 000020E0 00000064 ¦ ¦ Resource 00004000 00000010 ¦ ¦ Exception 00000000 00000000 ¦ ¦ Security 00000000 00000000 ¦ ¦ Fixups 00000000 00000000 ¦ ¦ Debug 00000000 00000000 ¦ ¦ Description 00000000 00000000 ¦ ¦ GlobalPtr 00000000 00000000 ¦ ¦ TLS 00000000 00000000 ¦ ¦ Load config 00000000 00000000 ¦.004029F0: 00 00 00¦ (reserved) 00000000 00000000 ¦49 00 MFC42.DLL I.00402A00: 5F 5F 43¦ (reserved) 00002000 000000A4 ¦6C 65 __CxxFrameHandle.00402A10: 72 00 B2¦ (reserved) 00000000 00000000 ¦5F 5F r -sprintf U __.00402A20: 64 6C 6C¦ (reserved) 00000000 00000000 ¦6E 65 dllonexit Ж_one.00402A30: 78 69 74¦ (reserved) 00000000 00000000 ¦00 00 xit MSVCRT.dll.00402A40: D3 00 5FL================================-74 46 L _exit H _XcptF

Однако если бы hiew только и мог, что отображать некоторые структуры PE файлов, то эта возможность скорее всего осталась бы так и не замеченной на фоне таких конкурентов, как dumpbin, делающий, кстати, это значительно лучше hiew-а.

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

Чтобы понять, насколько это удобно, нужно поработать с hiew-ом хотя бы пару часов. Сравните qview и hiew. Не правда ли второй значительно информативнее и просто практичнее? Смысл же команд первого совершенно непонятен и загадочен. Даже если перейти по косвенному адресу, все равно там мы ничего не увидим, ибо содержимое этой секции не определено до загрузки файла.

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

В результате имя вызываемой функции по-прежнему остается загадкой. Тут на помощь приходит недокументированная особенность, заключающася в том, что секция адресов во многих случаях совпадает с секцией имен. Несмотря на то что она в любом случае будет затерта загрузчиком, это дает нам возможность определить по косвенному вызову ординал и имя функции. Как это сделать, подробно рассказывают другие книги, здесь я лишь посоветую обратится по RVA адресу (если он не равен нулю). Например, в нашем случае он равен 0х02B56 и по нему располагается строка 'AppendMenuA', таким образом mov ebx, [0004021E8] следует читать как mov ebx,AppendMenuA. Если же секция адресов пуста (или там содержится мусор), то необходимо вычислить индексы элемента от ее начала и обратиться к секции имен или просто скопировать ее самостоятельно поверх первой. Как уже отмечалось, ее содежание некритично и лишь помогает при анализе программы в дизассемблере. Qview: 00001730: FF250C214000 jmp d,[00040210C] 000019AE: FF25D8214000 jmp d,[0004021D8] 00001372: 8B1DE8214000 mov ebx,[0004021E8] Hiew: .00401730: FF250C214000 jmp MFC42.4673 .004019AE: FF25D8214000 jmp __CxxFrameHandler ;MSVCRT.dll .00401372: 8B1DE8214000 mov ebx,[0004021E8] ^^^^^^^^^^^

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

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

Однако vxd полезны не только как собственно драйверы устройств. Код, размещенный в них, работает в нулевом кольце защиты и практически без ограничений. Это открывает широкое поле для фантазии разработчиков. Действительно, трудно представить себе мало-мальски серьезный защитный механизм, не выходящий за пределы третьего кольца.

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

Как видно из рисунка, LE-заголовок ненамного сложнее своего PE-собрата и не должен вызвать особых трудностей. Назначение большинства полей понятно из их названия, и только несколько моментов могут потребовать уточнения в SDK. Между прочим, MicroSoft не очень-то открыто предоставляет сей формат на общественное растерзание. Описания довольно скудные и отрывочные. Несколько статей по этому поводу в MSDN только возбуждают аппетит и разжигают любопытство, но никак не проясняют ситуацию.

Фактически анализ формата приходится выполнять каждому кодокопателю самостоятельно. И в этом значительную помощь могут оказать hiew и прилагаемая к нему программ ledump. К последней мы еще вернемся, а возможности hiew-а опишем прямо сейчас:

г============================[ LE-header ]=============================¬ ¦ Object table count 2 ¦ Page count 4 ¦ ¦ Starting EIP 00000000:00000000 ¦ Page size 00001000 ¦ ¦ Starting ESP 00000000:00000000 ¦ Bytes in last page 00000490 ¦ ¦ Fixup section size 000002D8 ¦ Loader section size 00000053 ¦ ¦ Object table 000000C4/00000144 ¦ Object page 000000F4/00000174 ¦ ¦ Object iterat 00000000/00000000 ¦ Resident name 00000104/00000184 ¦ ¦ Resource 00000000/00000000 ¦ Number of resource 0 ¦ ¦ Directives 00000000/00000000 ¦ Number of directive 0 ¦ ¦ Fixup page 00000117/00000197 ¦ Fixup records 0000012B/000001AB ¦ ¦ Import module 000003EF/0000046F ¦ Number of module 0 ¦ ¦ Import proc 000003EF/0000046F ¦ Entry table 0000010D/0000018D ¦ ¦ Data pages offset 00000600 ¦ Number preload pages 3 ¦ ¦ Non-resident name 00003A90 ¦ Bytes in non-resident 51 ¦ ¦ Instance in preload 00000000 ¦ Instance in demand 00000000 ¦ ¦ Auto data 00000000 ¦ Heap/Stack 00000000/00000000 ¦ ¦ Debug info 00000000/00000000 ¦ Debug length 00000000 ¦ L======================================================================-

Заглянув в помощь, просто поражаешься возможностям навигации. Можно переходить к LE заголовку, точке входа, таблице объектов и импорта, DDB секции, таблице страниц и ресурсов, кроме того, резидентных и нерезидентных имен, да всего и не перечислишь!

Немного печально, однако, что Hiew показывает все вышеперечисленное "AS IS" и не делает никаких попыток улучшить читабельность. Можно, конечно, воспользоваться для этой цели соответствующими утилитами, но в однозадачной MS-DOS это крайне неудобно. Легче уж смириться с тем, что есть, и разбирать все структуры вручную, надеясь, что автор все же пойдет когда-нибудь навстречу своим клиентам и реализует недостающий сервис.

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

Также hiew правильно дизассемблирует все вызовы VMM, что, между прочим, является не такой уж тривиальной задачей. Qview, например, это делает совершенно неправильно, чем и вводит пользователя в заблуждение.

Сравните, как дизассемблировали hiew и qview один и тот же фргамент. Увы, но этот пример не в пользу qview-а, который я все же люблю несмотря ни на что, хотя и должен признать, что он безнадежно проигрывает hiew-у в этом отношении и вряд ли в ближайшее время ситуация изменится в лучшую сторону. Hiew: .000002E3: CD208D000100 VMMcall Save_Client_State .000002E9: 5F pop edi Qview: 000046E3: CD20 int 20 000046E5: 8D00 lea eax,dword ptr [eax] 000046E7: 0100 add dword ptr [eax],eax 000046E9: 5F pop edi

К сожалению, SEN допустил досадную ошибку (с каким программистом этого не случается!), и его творение спотыкается при попытке ассемблирования VMM-вызова. Взгляните на экран, показанный ниже, - он наглядно иллюстрирует мои слова. Hiew "съедает" аргумент и отказывается анализировать любой введенный вручную. Как символьный, так и цифровой.

г= Pentium(R) Pro Assembler =====================================¬ ¦ VMMcall------------------------------------------------------- ¦ L================================================================-

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

Заглянем в MSDN: там по этому поводу содержится много полезных статей и информации. Но для начала обратим внимание на базовую функцию VMMCall: VMMCall Service, Parameters

Она ассемблируется следующим образом: INT 0x20 DW Parameters DW Service

Рассмотрим это на примере, показанном ниже. Все настолько просто, что никаких проблем вызвать не должно. Если вы чего-то не понимаете, то обратитесь к MSDN. Там есть множество примеров, подробно комментирующих вышесказанное. .00002665: CD2066661234 VxDcall 3412.6666 ^^^^"""" """" ^^^^

Заслуга автора hiew в том, что его продукт представляет это в удобочитаемом виде. Откуда же он берет символьную информацию? Ведь ее явным образом не содержится в файле! Верно, не содержится, поэтому-то автор и создал файл hiew.vmm приблизительно следующего содержания: [048B.VCACHE] ^^^^ Get_Version Register GetSize

Формат его очевиден и не требует особых комментариев, достаточно взглянуть на расположенную ниже строку: .00002665: CD2000008B04 VxDcall VCACHE.Get_Version ^^^^

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

При этом vxd с ординалом сервиса 0x01 иначе еще называется (в терминологии архитектуры Win32) - VMM "Virtual-Memory Manager". На самом деле это все тот же vxd, и вызов его происходит аналогично: .00012161: CD2001000100 VMMcall Get_Cur_VM_Handle ^^^^ .00002665: CD2000008B04 VxDcall VCACHE.Get_Version ^^^^

Доказательством этого служит следующая строка из hiew.vmm "[0001.VMM]". Вообще-то с названием файла, автор, похоже, допустил еще одну досадную ошибку, немного сбивающую с толку. Все же это не 'hiew.vmm', а 'hiew.vxd'. Надеюсь, что в последующих версиях это будет исправлено.

Мы еще не упомянули о таком понятии, как VxDjmp. Он вызывается аналогично, с маленьким исключением - старший бит параметра в этом случае равен единице, а не нулю. Вот, взгляните сами: .00005040: CD201C801700 VxDjmp SHELL.Update_User_Activity ^ .00005048: CD2048810100 VMMjmp RegOpenKey ^ Hiew правильно интерпретирует значение этого бита, в противном случае он неверно дизассемблировал бы вызов и не смог определить функцию. Это еще раз подчеркивает, что автор проделал большую работу, и в благодарность ему можно простить мелкие ошибки. Любопытно, что вместо этого их склонны упорно не замечать, - иначе трудно объяснить, почему они продержались вплоть до 6.03 версии. Пассивный нынче народ стал, однако...

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

Калькулятор
"Врагу, которым восхищаешься, легче вселить в тебя ужас".

Ф.Херберт. "Дюна".

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

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

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

К счастью, во всем остальном калькулятор hiew-а ни в чем не уступает своим собратьям и поддерживает все типовые логические и математические операции, которые подробно будут описаны ниже:

г================ Calculator ================¬ ¦ (0xFF ^ 0x80 | 128) > 0------------------- ¦ ¦ Signed: 1 ¦ ¦ Unsigned: 1 . . ¦ ¦ Binary: 00000000000000000000000000000001 ¦ ¦ Hex: 00000001 " " ¦ L============================================-

СКОБКИ: hiew поддерживает круглые скобки. Если их не ставить, то операции будут выполняться в порядке старшинства. Однако я рекомендую не полагаться на это, а все же не полениться и расставить побольше скобок. Они еще никогда никому не помешали.

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

'+' Сложение, а также положительное число. Забавно, что hiew переваривает такие конструкции, как '+-+-++' и подобные им. При этом '-' изменяет значение всех знаков, стоящих правее в цепочке, на противоположное. Математически это верно, но ценность этого сомнительна.

г================ Calculator ================¬ ¦ +1++2+-+-3----4--------------------------- ¦ ¦ Signed: 10 ¦

'*' Умножение. Поддерживает знаковые числа. Конструкции типа '**' не воспринимаются синтаксическим анализатором. (Возведение в степень, увы, отсутствует).

'/' Целочисленное деление.

'%' Взятие остатка. ВНИМАНИЕ! HIEW содержит большой и очень-очень неприятный баг - операция 'X % 0' приводит к аварийному завершению и выходу в DOS без сохранения проделанной работы.

'^' Битовая операция XOR (т.е. ИЛИ-исключающее-И)

'&' Битовая операция AND (т.е. И)

'|' Битовая операция OR (т.е. ИЛИ)

'!' Логическое (не битовое) отрицание. !0 == 1 !X == 0, где X!=0. Вообще непонятно, а почему автор ввел логическое, а не битовое отрицание. На редкость бесполезная операция, и к тому же отсутствует логическое NOT, что мне категорически не нравится.

'>>' Циклический сдвиг вправо. Имеет самый низший приоритет, поэтому 0xFF>>1+1 == 0x3F == 0xFF>>2. Не забывайте расставлять скобки. (Заметим, что A >>-x == 0, что является еще одним багом, идущим вразрез с математикой).

'<<' Циклический сдвиг влево.

'>' Логическое "больше". A >B == 1, если A >B и A >B == 0, если это условие ложно. Например, (1>2)+3 == 3: (10>0)+1==2; но 1>2+3==0. Обратите внимание на последний баг. Он может служить источником трудноуловимых ошибок, а вообще, если честно эта операция

'<' Логическое "меньше".

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

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

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

Ф. Херберт. "Дюна".

Уникальность hiew-а прежде всего в том, что SEN впервые в мире реализовал в нем удобную интерпретируемую крипт-систему. Раньше этого попросту не было. Если требовалось расшифровать файл или его фрагмент, то необходимо было писать собственную программу на ассемблере или любом другом языке высокого уровня.

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

Внешне экран встроенного редактора скриптов показан ниже. Конечно, это не привычный нам полноэкранный редактор, а подстрочечник (наподобие того, который был в ZX-SPECTRUM-48), но пользоваться им достаточно удобно. К тому же типичный скрипт занимает всего несколько строк, ради которых интегрировать в hiew полноэкранный редактор было бы расточительством.

000A0: 65 77 20 г=[Byte/Forward ]==============¬ 29 ew release V 000B0: 53 45 4E ¦ 1>mov bx,77 ¦ AX=0000 ¦ 39 SEN, Kemerovo 000C0: 39 31 2D ¦ 2 rol al,1 ¦ BX=0000 ¦ 00 91-1999. 000D0: 00 00 00 ¦ 3 xor al,bl ¦ CX=0000 ¦ 00 000E0: 00 00 00 ¦ 4 rol al,7 ¦ DX=0000 ¦ 00 000F0: 00 00 00 ¦ 5 ¦ SI=0000 ¦ 00 00100: 00 00 00 ¦ 6 ¦ DI=0000 ¦ 00

г= Pentium(R) Pro Assembler =====================================¬ ¦ loop 2-------------------------------------------------------- ¦ L================================================================-

00160: 00 00 00 ¦ 12 ¦ ¦ 00 00170: 00 00 00 ¦ 13 ¦ ¦ 00 00180: 00 00 00 ¦ 14 ¦ ¦ 00 00190: 00 00 00 ¦ 15 ¦ ¦ 00 001A0: 00 00 00 ¦ 16 ¦ ¦ 00 001B0: 00 00 00 ¦ 17 ¦ ¦ 00 001C0: 00 00 00 L==============================- 00 001D0: 00 00 00 00

Хотя в заголовке строки ввода гордо красуется 'Pentium Pro Assembler', hiew понимает только ограниченный набор команд, который даже не покрывает 8086. Было бы неплохо, если бы автор изменил заголовок и не вводил пользователей в заблуждение.

Очень большим ограничением является отстутствие операндов в памяти. Из этого следует, что сколь-нибудь серьезный скрипт написать не удастся. Все, что доступно хакеру это 32 байта памяти в виде регистров EAX,EBX,ECX, EDX,EBP,ESI,EDI,ESP. Да, в последнем случае я не оговорился - в криптосистеме hiew регистр esp - "общего" назначения, и ничто не помешает использовать его для своих нужд.

При этом скорее забавно, чем грустно, выглядит отсутствие условных переходов. Pentium Pro Assembler? Ну-ну... С другой стороны, программирование в таких "жестких" рамках само по себе представляет увлекательнейшую головоломку и действительно иногда напоминает "высший пилотаж". 32 байта памяти - примерно столько было в первых программируемых микрокалькуляторах. Но даже там были условные переходы и прямая адресация памяти. Впрочем, черт с ними, с условными переходами их бы можно было реализовать исходя из принципа эквивалентности с помощью всего двух команд NOR и OR. Но в hiew-е вообще нет переходов! Нет регистра-указателя команд!

Печально все это... А ведь в qview-е таких ограничений попросту нет. Там управление передается полноценной ассемблерской программе, которая может делать все что ей заблагорассудится. Однако, qview проигрывает в интерактивности. За все нужно платить: мне по душе интерактивность, поэтому я выбираю hiew. Тем более что для ряда случаев его все же хватает.

Итак, поддерживаются следующие команды:

Набор регистров:

AL,AH,AX,EAX,BL,BH,BX,EBX.CL,CH,CX,ECX,DL,DH,DX,EDX,SI,ESI,DI,EDI,BP,EBP, SP,ESP

Команды пересылки: MOV регистр, регистр MOV регистр, непосредственный операнд

Арифметические команды:

AND, NOT, NEG, OR, XOR, SUB, ADD, ROL, ROR. MUL, DIV.

SHL и SHR не поддерживаются.

Передача параметров:

На входе AX

На выходе AX

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

Самое интересное, что последний работает не только с байтами\словами\двойными словами, но и со строками любой длины (точнее до 20 символов). Для задания xor-маски нужно нажать Ctrl-F8, но это окно вызывается и при нажатии F8, если маска еще пуста.

г========================== Enter XOR mask ==========================¬ ¦ ASCII: -------------------- ¦ ¦ ¦ ¦ Hex: ------------------------------------------------------------¦ L====================================================================-

Очень часто в программировании используется шифр Вернама, сводящийся к ксорению кода некой строкой, допустим 'KPNC++'. (Подробнее о нем читайте в главе, посвященной криптографии). Предыдущие версии hiew не имели такой возможности и не поддерживали строки. Разве что из четырех байт (двойного слова).

К сожалению, остальные команды до сих пор не могут работать со строками, и это сильно удручает. Но вернемся, собственно, к интерпретатору скриптов. Рассмотрим простейший пример. Для вызова редактора нажмем Ctrl-F7 и введем следующую последовательность команд:

г=[Byte/Forward ]==============¬ ¦ 1>xor ax,1234 ¦ AX=0000 ¦ ¦ 2 loop 1 ¦ BX=0000 ¦

Ожидается, что она должна расшифровывать текст по xor word,0х1234. Однако этого не произойдет. hiew автоматически не может определить размера операндов и поэтому по умолчанию работает только с байтами (см. строку статуса в заголовке). При этом он старший байт регистра AX действительно будет корректен, но инкремент все же будет равен единице, а не двойке, как следовало бы ожидать по логике вещей.

Чтобы изменить шаг, необходимо нажать F2; при этом в строке статуса 'Byte' сменится на 'Word'. Также можно изменить и направление (т.е. поменять инкремент на декремент, для чего служит клавиша F3), но в нашем примере мы этого делать не будем.

Команда 'loop' на самом деле никакой не 'loop', а настоящий 'jmp', причем направленный только назад. Если вы попытаетесь сделать прыжок "вниз", то произойдет приблизительно следующее:

г=[Byte/Forward ]==============¬ ¦ 1 xor ax,1234 ¦ AX=1263 ¦ ¦ 2> ¦ BX=0000 ¦ ¦ 3 xor bl,al ¦ CX=0000 ¦ г= Pentium(R) Pro Assembler =====¬ ¦ loop 3-------г==================·Hiew·==================¬ L===============¦ Jump out of range ¦ L==========================================-

Ну никак не хочет hiew понимать таких конструкций. Впрочем, так и должно быть. Команда 'loop' последняя в скрипте, и все ее назначение - зациклить программу. При нажатии на F7 (Crypt) hiew шифрует слово\байт и, встретив 'loop', останавливается, ожидая следующего нажатия F7, после чего переходит на указанную метку.

При этом 'loop 1' можно опустить. Обычно так и поступают. Но иногда необходимо выполнить только один раз некий инициализационный код, как например, показано ниже. При этом строка '1' выполняется только один раз, а все остальное время hiew крутится в цикле 2-4.

г=[Byte/Forward ]==============¬ ¦ 1>mov bl,66 ¦ AX=0000 ¦ ¦ 2 xor al,bl ¦ BX=0000 ¦ ¦ 3 sub bl,7 ¦ CX=0000 ¦ ¦ 4 loop 2 ¦ DX=0000 ¦

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

Пользователю приходится действовать вручную, нажимая Ctrl-F7 (Crypt-Set), F5 (ClearReg). При этом всплывает еще один баг автора - "теневой" регистр указателя команды будет также сброшен, но "визуальный" указатель '>' останется неизменным, вводя пользователя в некоторое заблуждение и заставляя искать еще одну клавишу сброса этого регистра. Но ее нет: достаточно однократного нажатия F5, а на знак '>' попытайтесь просто не обращать внимания.

Использование Crypt в качестве калькулятора - довольно любопытный прием, облегчающий хакерам жизнь и страхующий от многих ошибок. Большая часть вычислений так или иначе связана с анализируемым файлом. При этом утомительно переносить необходимые значения в калькулятор (еще и не ошибившись в порядке следования старших и младших байт), - можно просто указать hiew-у последние курсором. Ведь крипт принимает входные данные, и, если сохранить то же самое значение регистра AX на выходе скрипта, шифровка окажется "прозрачной", т.е. не изменяющей значение под курсором.

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

г=[Byte/Forward ]==============¬ ¦ 1>add bx,ax ¦ AX=0000 ¦

"Прогоняем" расшифровщик по выбранному фргаменту, теперь вызываем редактор и смотрим на значение регистра BX:

г=[Byte/Forward ]==============¬ ¦ 1>add bx,ax ¦ AX=0121 ¦ ¦ 2 ¦ BX=7534 ¦

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

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

Однако несмотря на вышесказанное, встроенный интерпретатор hiew-а многими горячо любим и интенсивно используется. Быть может, потому что он был первым, или потому что сделан с любовью. Так или иначе он нужен. И очень большое недовольство вызвало отсутствие крипта в 6.0 версии. К счастью, автор быстро одумался и тут же вернул его на место.

Жаль, конечно, что за всю историю существования hiew-а крипт претерпел наименьшие изменения. Появилась разве что возможность записи скриптов во внешний файл (F10). Это конечно, удобно, но практически этим редко пользуются. А зачем? Обычно скрипты состоят из нескольких строк и не так уж трудно вновь "набить" их вручную.

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

Описание файла HIEW.INI
- Осторожность - важное качество для человека, который будет вождем.

Ф. Херберт. "Дюна".

HIEW хранит часть настроек в ini файле, который немного напоминает одноименные Windows-файлы. Их легко редактировать вручную, структура достаточно проста и не нуждается в описании. Однако я все же решился подробно рассказать о ней. Кто знает, какие вопросы могут возникнуть у читателя?

Первая строка непременно должна быть [HiewIni 5.03] независимо от текущеей версии. Непонятно, почему так? Если для совместимости "сверху-вниз", то почему бы просто не искать строку 'HiewIni'? Тем более что ini от разных версий меж собой не совместимы, т.к. автор частенько удалял один ключи и добавлял другие.

Пустые строки и строки, начинающиеся с ';', игнорируются. Последнее, как нетрудно догадаться, предназначено для комментариев.

Hiew.ini не является обязательным файлом. При его отсутствии будут браться параметры по умолчанию, которые совпадают с содержащимися в "дистрибутивном" варианте.

; стартовый режим

[StartMode] может принимать следующие значения Text, Hex, Code. Выбранное значение будет автоматически установлено при открытии файла. Рекомендую установить 'Hex'- а впрочем, воля ваша.

[Beep] Включить\выключить (on\off) звуковой сигнал при ошибочных и нештатных ситуациях. По умолчанию включен, и я не вижу смысла изменять это значение.

[Bar] Лифт или другими словами индикатор прогресса. По умолчанию расположен слева (Left), несмотря на то что пользователь приучен интерфейсом Windows видеть эту полоску справа (Right). Впрочем, в текстовом режиме попытки изобразить что-либо похожее всегда неудачны и на мой взгляд более подходящим является числовое представление в процентах (Percent), которое к тому же высвобождает одну колонку, что особенно актуально для просмотра текстовых документов, отформатированных по 80 символов в строке.

[Wrap] Перенос длинных строк. Может принимать значения 'on' или 'off', но намного более удобен автоматический (Auto) режим, устанавливаемый, кстати, по умолчанию. При этом для двоичных файлов всегда выполняется перенос строк, а для текстовых - нет.

[Tab] Поддержка табуляции. Если установлено значение 'On', то hiew корректно обрабатывает символы табуляции, встретившиеся в просматриваемом тексте. Если же установть 'Off', то все символы табуляции будут проигнорированы. Аналогично вышеупомянутому [warp] существует и автоматический режим, который устананавливается по умолчанию и на мой взгляд очень удобен.

[StepCtrlRight] Задает число столбцов, на которые смещается текст при нажатии Ctrl-Left \ Ctrl-Right. По умолчанию равен 20. Очень удобная возможность для просмотра текстовых файлов, отформатированных более чем с 80 символами в строке и притом так, что перенос строк не представляется возможным. Например, при просмотре таблиц, диаграмм и т.д. Может принимать значения от 1 до 128. Или, другими словами, signed byte.

[DisableMouse] По идее должен прятать\не прятать мышиный курсор. Однако независимо от установленного значения курсор все равно не отображается.

[JumpTable] Задает таблицу переходов по call/jmp в дизассемблере. По умолчанию она выглядит следующим образом : "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ". При этом первый символ - это клавиша отката, т.е. возвращения назад. А.Куленцов нашел довольно оригинальное (хотя, на мой взгляд, слишком очевидное) решение представить эту строку в виде "0123456789QWERTYUIOPASDFGHJKLZXCVBNM" для удобства работы с клавиатурой. А по мне хватает и девяти цифровых символов. Заметим: если на экране появится больше ссылок, то остальные просто не будут отображены.

[Linefeed] Для текстоых файлов задает символ переноса строки. Из-за того что в разных системах он разный, могут возникнуть некоторые проблеммы с его интерпретацией. Большинство просмотрщиков автоматически определяют используемый формат. Точно так же поступает и hiew (Auto). Однако если нужно, то можно перевести его и на ручной режим управления. Он понимает следующие значения - LF (0xA), CR (0xD), CRLF (0x0D0A), но, к сожалению, неправильно интерпретирует (0xA 0xD) и (0xA 0xA). Впрочем, последнее достаточно редко встречается, чтобы вызвать какие-то проблемы. Отметим, что эта возможность впервые появилась только в версии 5.10.

[AutoCodeSize] Автоматически определяет разрядность кода (16\32) в LX файлах (префикс 'a' в строке статуса говорит, что активирован автоматический режим определения). Не вижу никакого смысла выключать эту возможность, хотя это включение по каким-то загадочным причинам все же предусмотрено автором.

[KbdFlush] Управляет очисткой клавиатурного буфера перед вводом. По умолчанию включено. В противном случае в окнах ввода информации частенько бы появлялся мусор, оставшийся от предыдущих нажатий клавиш. Поэтому я никак не могу представить себе ситуацию, в которой это было бы полезно. Словом, отключать эту возможность можно разве что ради эксперимента. ; Маска для показа смещения при поиске с шаблоном и поиске ссылок RunningOffsetMask = 0xFF ; 0 - 0xFFFFFFFF v5.53

[XlatTableIndex] Задает индекс таблицы перекодировки в файле hiew.xlt, выбираемый по умолчанию. При этом '0' трактуется как отсутствие таблицы перекодировки, или, в терминологии hiew-а, 'AS IS'.

[FlistSort] Задает критерий сортировки файлов в Навигаторе по умолчанию.

[FlistSortReverse] Инвертировать критерий сортировки. Т.е. по умолчанию он задается по возрастанию параметра сортировки. Если это кажется неудобным, то процесс можно и обратить.

[FlistShowHidden] Показывать или нет скрытые файлы в Навигаторе. По умолчанию такие файлы не отображаются. Странно, однако. Рекомендую установить этот параметр в 'On'.

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

[SaveFileAtExit] Записывать состояние файла по выходу. По умолчанию выключено (?!), что мне абсолютно непонятно, - если учесть, что выход происходит без всякого предупреждения пользователя и на Esc и F10, так что ложные нажатия происходят довольно часто. Непременно включить этот механизм.

[ScanStep] Шаг по умолчанию при поиске ссылки (F6 Reference) или команд (F7,F7). По умолчанию имеет значение 'Command', но лучше проиграть в скорости, чем в надежности, и (как было сказано выше) я предпочитаю устанавливать шаг поиска в байт ('Byte').

[Savefile] Задает имя и путь к файлу сохранения состояния. По умолчанию hiew создает файл 'hiew.sav' в текущей директории, но это можно изменить. Это бывает полезно, например, когда на текущий диск записать нельзя (ключевая дискета, защищенная от записи, CD-ROM)...

Цвета
Наконец-то hiew стал поддерживать цветовую раскладку! Теперь каждый может настроить ее под свой вкус. Я, допустим, большой поклонник "зеленым по черному" и все используемые программы настраиваю именно так.

Подробно описывать кодировку цветов не имеет смысла, она тривиальна. К тому же это гораздо удобнее делать специальной утилитой, (например, похожей на qview) чем вручную.

Структура этой секции hiew.ini ясна из приведенного фрагмента:

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  ColorMain = 0x1B ; основной цвет  ColorCurrent = 0x71 ; текущий байт - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 

HIEW.XLT
Этот файл служит для перекодировки символов. Может содержать до 15 кодировок, но в распространяемом автором варианте содержит только две - Win1251 и KOI8R. Имеет довольно витиеватую структуру, и для его создания\просмотра\редактирования не помешает написать хорошую утилиту. Автор в сопутствующей документации описывает структуру как: typedef struct{ BYTE sign[ 9 ], // "HiewXlat",0 unused[ 5 ], versionMajor, // 0x05 versionMinor; // 0x40 }XLAT_HEADER; typedef struct{ BYTE title[ 16 ], // заголовок tableOut[ 256 ], // для вывода tableIn[ 256 ], // для ввода tableUpper[ 256 ]; // для игнорирования регистров в поиске }XLAT;

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

Жаль только, что в комплекте с hiew-ом нет утилиты для работы с этим файлом. Откомпилировать его можно - положим, препроцессором, но вот декомпилировать... для декомпиляции потребуется написать специальную программу. Если вы, допустим, решитесь добавить поддержку ISO, то потребуется сначала декомпилировать существующий файл, внести исправления и повторить опять.

Заметим, что hiew.xlt может отсутствовать. В этом случае поддерживается единственная кодировка по умолчанию DOS, или, в терминологии hiew-а, 'AS IS'.

BoundsCheck OverView

"Покажите мне пример совершенно гладко идущей рутины и я найду вам того, кто прикрывает ошибки."

Ф. Херберт. "Дом Глав Дюны".

BoundsChecker фирмы NuMega (далее просто BC) - это инструмент, прежде всего нацеленный на поиск ошибок в вашем собственном программном обеспечении. Однако он подходит и для исследования взаимодействия приложений с операционной системой или другими приложениями, поступившими в ваше распоряжение без исходных кодов.

Что представляет собой BC? Отладчик или дизассемблер? Ни то ни другое. Это Win32-шпион. Обычные шпионы (типа Spyxx из MS VC) перехватывают только сообщения, которыми приложение обменивается с окном. Учитывая, что архитектура Windows фактически полностью построена на сообщениях, перехват последних несет достаточно полную информацию о происходящих событиях. Полную, но увы, не исчерпывающую. Множество функций вообще не генерируют сообщений (например, выделение памяти, чтение из файла или реестра).

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

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

BC же - это десятки мегабайт сложного кода, который ненамного хитрее созданного Питтреком; он разве что покрывает больший круг задач. По крайней мере я, поработав с версией 5.0, пришел к выводу, что нет ничего лучше собственных шпионов, специализированных применительно к интересующему вас кругу задач. И все же BC не совсем то, что нужно хакеру. Очень многого в нем недостает: в частности, нет никакой системы навигации по его протоколам. Нет и никакого фильтра функций, так что вам предстоит тяжелая археологическая работа: отыскивать нужные имена среди бесконечных вызовов GetMessage\TranslateMessage\DispathMessage и им подобных. Так же обстоит дело и с сообщениями - километрами листингов, в основном не представляющих для хакера в данный момент ничего интересного. Spyxx имеет гибкую систему фильтров, которая сокращает протоколы и заметно ускоряет анализ.

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

К недостаткам BC можно отнести также невозможность анализа самозагружаемых модулей, крайнюю неустойчивость к приложениям, пытающимся "прибить" BC, а так же отсутствие поддержки 16-приложений и DLL. Последнее особенно неприятно. Но ничего не поделаешь. Пишите собственных шпионов или довольствуйтесь тем, что есть сегодня на рынке.

Вышесказанное не означает, что я призываю отказаться от BC. Вовсе нет! Это действительно очень мощный инструмент, в настоящее время не имеющий аналогов. Но нужно четко представлять, для решения каких задач он полезен. Часто более быстрый путь - использование дизассемблера или отладчика.

Не стоит пытаться "перетянуть" одеяло и решать BC-ом те задачи, для которых он, мягко выражаясь, не совсем предназначен. Строго говоря, BC даже и не может работать в одиночку. Его назначение - показать вызовы API функций и сообщений в изучаемом приложении. Для чего может понадобиться эта информация?

Рассмотрим случай, когда программа, считывающая строку из окна редактирования, не вызывает ни GetWindowText, ни GetDlgItemText, и даже не посылает сообщения WM_GETTEXT. Чтобы понять, как текст все же попадает в буфер, надо проанализировать манипулирующий с ним код. Т.е. пройтись дизассемблером по нужному участку кода. Но тут вся проблема в том, что мы не знаем, какой код "нужный", и в поисках последнего рискуем потерять уйму времени.

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

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

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

Быстрый старт
"- Существуют во Вселенной вопросы, на которые нет ответов."

Ф. Херберт. "Мессия Дюны".

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

Воистину непостижим такой подход на фоне Soft-Ice, который содержит весьма привлекательный для профессионала интерфейс. Но почему-то применительно к BC фирма-разработчик пошла другим путем, чем явно огорчила многих своих поклонников.

Остается использовать то, что дают. Запустим BC и попробуем открыть какой-нибудь файл для анализа. Напомню, что поддерживаются только 32-разрядные Win32 файлы. Даже сегодня во время победного шествия Win32 API это ограничение все еще существенно, и существует немало 16-разрядных приложений даже среди новых разработок.

Чтобы согласовать наши действия, предлагаю вам остановить свой выбор на примере BUG1, находящемся в комплекте поставки BC. При этом последний автоматически откроет окно "Program Transcript". В нем на протяжении всего сеанса работы будут отображаться такие события, как загрузка различных DLL, а также выводиться все отладочные сообщения. Это нас будет интересовать в последнюю очередь.

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

Это кстати, самая распространенная ошибка начинающих пользователей BC. За неудачной попыткой следует разочарование или вопрос - а где же обещанные вызовы API?

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

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

Действительно, NuMega предусмотрела такую возможность. Конечно, среднестатическому пользователю она не нужна и поэтому по умолчанию выключена. Чтобы исправить это, заглянем в установки программы (Program\Setting). Выберем закладку "Event Reporting". Появится выбор из следующих пунктов:

Collect and report program event data

Report messages

Report pointer data for API

Report Hooks

Но последние три пункта будут недоступны, пока не установлена галочка "Collect and report program event data". Выбор остальных зависит от выбранной вами цели и исследуемой программы. Для перехвата функций API нужно установить "Report pointer data for API", а также остальные - для перехвата сообщений или хуков.

Если теперь снова перейти на первую закладку "Error Detection", то будет возможность сохранить текущие настройки значениями по умолчанию. Это и в самом деле приятная возможность, особенно если планируется использовать BC в основном для анализа чужих программ, а не по прямому назначению.

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

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

Винчестер продолжает интенсивно шуршать, и мы, заботясь о свободном пространстве на своем диске, поспешим прервать выполнение контрольного приложения нажатием кнопки "stop" или закрытием его окна.

И где же ожидаемый нами протокол? Почему он по-прежнему выглядит пустым? Куда подевались все функции API, ведь наши ушки явно слышали, что в протокол записывалось что-то "тяжелое"! Не будем волноваться. На самом деле все сработало успешно, просто BC по умолчанию не показывает в окне событий ничего, кроме ошибок и потоков.

Нажатием правой кнопки мыши, вызовем контекстное меню и установим галочку над пунктом "Show All Evetns". А затем "Expand All". Если все было сделано правильно, то в окне появится длинный перечень вызываемых функций.

+----------------------------T--------------¦ ¦ окно отображения событий ¦ окно стека ¦ ¦ ¦ ¦ ¦ ¦ ¦ ¦ ¦ ¦ ¦ ¦ ¦ ¦ ¦ ¦ +----------------------------+--------------+ ¦ Окно исходного текста ¦ ¦ ¦ ¦ ¦ L--------------------------------------------

Прокрутим его немного вниз, пока не встретим вызов функции CreateWindowExA. Щелкнув по нему, мы увидим в окне стека, расположенном справа, все передаваемые параметры в удобочитаемом виде. Из этой информации можно узнать много интересного. Но важно уже то, что мы знаем, какой именно функцией было создано окно.

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

Подключение нестандартных DLL
...использовать грубую силу - значит оказаться во власти гораздо более могущественных сил.

BC реализует далеко не лучшую схему перехвата вызовов API и DLL-функций. Это отличает его, например от продвинутых шпионов, перехватывающих на лету все функции загруженных приложением модулей. Причем последние даже не обязательно должны быть DLL-модулями. Например, IDA поддерживает плагины, которые представляют собой PE или LE файлы, экспортирующие определенные функции. А IDA в свою очередь предоставляет им собственный API.

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

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

Если бы научить BC "видеть" эту DLL, то никаких проблем бы не было и мы получили бы интересный протокол, раскрывающий все секреты защиты. К счастью, NuMega предусмотрела этот момент и снабдила пользователей инструментарием для поддержки "их собственных DLL". Понятно, что BC не может с уверенностью сказать, ваша это DLL или еще чья-то. При перехвате BC никак не изменит исследуемую DLL, а поэтому это действие не противоречит нашему законодательству.

Чтобы понять, как происходит подключение нестандартных DLL, нужно представить себе механизм перехвата экспортируемых ими функций. К сожалению, это очень емкая тема, для которой требуется отдельная книга; кроме того, она уже многократно и исчерпывающе освещалась. Поэтому ограничусь двумя словами. Для каждой перехватываемой DLL нужно построить определенную заглушку. BC подменяет вызовы оригинальной DLL на "свои". При этом он полностью контролирует управление и передаваемые параметры. Разумеется, для этого должна существовать специальная база данных, которая содержит типы и допустимые параметры. Впрочем, последнее нас интересует меньше всего, ибо нам необходимо отметить сам факт вызова какой-то функции, и только. Это облегчит нам задачу написания "заглушки".

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

Однако написание заглушки - очень трудоемкий процесс, требующий определенных навыков и знаний. Впрочем, специалисты (а именно такой специалист подразумевается под словом "хакер") не боятся этих трудностей. Была бы только документация и хотя бы краткое описание. В том-то и беда, что его нет! Веселенькое начало нам обеспечила NuMega! Как же быть? Остается использовать мастера, который сгенерирует за вас весь код автоматически, на C++.

Работу мастера описывать я не буду. Скажу только, что она проста, и этот мастер, как любой другой, потребует от вас минимальных умственных и физических усилий. Разумеется, возможности выбора окажутся очень ограниченными, но, как бы то ни было, на финальном пункте появятся три файла - *.cpp, *.h и *.mak При этом откомпилировать это MS VC можно только с ручной правкой некоторых мест, что не приносит абсолютно никакого удовольствия и тормозит весь процесс.

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

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

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

Ф. Херберт. "Мессия Дюны".

Я уже предупреждал, что BC имеет крайне скудный для профессионала интерфейс. Если вы хотите всерьез заняться изучением программ с помощью перехвата API функций, то лучше все же не пожалеть времени и написать необходимый инструментарий самостоятельно. Однако большинство программистов слишком лениво или занято, и поэтому BC стал стандартом "де-факто".

Кому-то моя критика может показаться несправедливой. Быть может, я слишком суров по отношению к этой великолепной утилите? Кто знает... когда конкуренты спят и не предлагают ничего лучшего, то на безрыбье и рак рыба. Хотя есть еще и WinScope, написанный фирмой Periscope, но он почему-то не получил широкого распространения.

Меню "файл" типично для Windows-приложений. Пунткы Open\Close\Save As коментариев не требуют. Заострим внимание только на одном моменте: "сохранить" подразумевает сохранить текущее состояние и перехваченные вызовы в файл. К сожалению, используется не текстовой, а внутренний формат BC, а это означает, что написание собственного навигатора усложняется. Для этого потребуется проделать кропотливую работу по изучению сложной, а местами и запутанной структуры файла. Быть может, я опишу ее подробно (или хотя бы кратко) или даже предоставлю собственную DLL, работающую с файлами, созданными BC 5.0, и, возможно, более поздних версий (до сих пор не было возможности это проверить).

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

"Print" и "Print PreView" позволят распечатать или хотя бы показать на экране протокол в более удобочитаемом виде. К сожалению, для его распечатывания потребуется тонны бумаги (соответственно, литры чернил в случае струйного принтера), что вряд ли оправдано. Особенно если вспомнить про сегодняшний кризис и стоимость расходных материалов... Непосредственная печать в файл невозможна, поскольку BC представляет все данные в графическом, а не текстовом формате.

Меню "Edit" содержит традиционную команду "Find" (Ctrl-F). Вызов ее приводит к появлению стандартного диалога поиска, которое вроде бы не поддерживает ни шаблонного, ни мультистрокового поиска. Однако при близком рассмотрении справа от окна ввода обнаруживается маленькая прямоугольная кнопочка со стрелкой, обращенной вправо. Нажатие ее раскрывает подменю поиска, которое притягивает к себе с первого взгляда.

Error

Resource, memory or interface leak

API or OLE method call

API or OLE method return

Windows or Dialog Message

Hook

Comment

Thread start of switch

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

Поиск вызовов API был бы полезен, если бы была хоть какая-то возможность указывать сложную маску. Фактически их так много, что следующий вызов нетрудно найти и ручным поиском. Более того, он скорее всего, окажется в пределах окна событий, так что не потребуется даже прокрутки последнего.

Фактически единственным используемым пунктом будет 'Windows of Dialog Message' - т.к. не всегда легко найти сообщения, относящиеся к окну, (диалогу) среди множества малоинформативных вызовов функций API.

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

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

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

Меню VIEW начинается с пункта 'Event Summary'. Это своего рода и "легенда" всех условных обозначений, но в то же время - статистика вызовов функций API, оконных сообщений и всего остального в виде сводной таблицы. Эта бесполезная (но очень любопытная) информация не несет в себе ничего, что можно было бы использовать для анализа программы. Ну какой мне прок знать, что приложение вызвало 2562 функций API и приняло\передало 115 сообщений?

Следующие три пункта:

Show Error and leak only (показывать только ошибки и утечки)

Show All Events (показывать все события)

Show Error and Specific event (показывать ошибки и выбранные события)

Specific event

Нам уже должны быть хорошо знакомы по "быстрому старту". Обычно используется "Show All Events" или "Show Error and Specific event". При этом можно выбрать произвольный набор отображаемых событий. К несчастью для хакера, крайне грубый и непрактичный.

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

Установка флажка "Arguments" приводит к тому, что в круглых скобках будут указываться аргументы каждой из отображамых функций. Это в самом деле очень удобно. При этом BC распознает строки и представляет их в символьном виде. Поэтому мы имеем все шансы встретить введенный пароль и найти функции, манипулирующие им. То же верно и по отношению к другим типам переменных. Если при этом последняя представляет собой какую-то сложную структуру, то она будет отображена в развернутом виде в окне стека, расположенном справа.

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

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

Следующие несколько пунктов меню управления позволяют свернуть или развернуть все отображаемые ветви. Учитывая, что с последним неплохо справляется и мышь, трудно понять, чем вызвано дублирование их в меню, причем без ассоциации с "горячими клавишами". Местами интерфейс BC и вправду плохо продуман.

Меню "run" содержит только один интересный пункт "Setting", который и будет подробно рассмотрен ниже. Если кликнуть по нему мышкой, то появится окно с несколькими закладками.

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

Гораздо более интересна следующая закладка - 'Events reporting', уже рассмотренная выше. Я думаю, мне уже нечего добавить, разве лишь напомнить еще раз о необходимости взвести нужные флажки: в противном случае просто ничего не будет работать.

"Program Info" позволяет задавать командную строку приложения (что уже давно не актуально для Windows приложений, но, может быть, когда-нибудь и пригодится) и рабочую директорию (по умолчанию выбирается текущая); а также указать путь к исходным текстам, которых скорее всего просто в нашем распоряжении не окажется. (а потому никакого пути и указывать не придется).

А вот 'Error Supressions'- очень полезная и актуальная функция. Помните, я говорил, что нет никакой возможности задать фильтр для произвольных и мешающих функций? Так вот: я вас обманул. Эта возможность есть, правда, в очень грубом варианте ее реализации. Можно вообще отключить перехват не интересующих нас в данный момент функций. Но это не самый лучший выход. Если в процессе изучения рапорта потребуется информация о "заблокированных" функциях, она уже никак не может быть получена, разве что полным ре-анализом, что удовольствия не приносит.

Но, к сожалению, выбирать не приходится. Обычно мы вынуждены действовать по следующему алгоритму. Первым делом вносятся в "черный список" все функции, работающие с выборкой сообщений из цикла. Запускаем приложение и смотрим в протокол. Если последний по-прежнему забит ненужными и малоинформативными функциями, то добавляем их к "черному списку" и повторяем анализ вновь. И так до тех пор, пока рапорт не окажется хотя бы минимально читабельным.

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

Как сломать TRIAL.COM

- Я говорю людям: смотрите! У меня есть руки! Но мне говорят: "А что такое руки?"

Ф. Херберт. "Дюна".

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

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

Я попытался (и не без успеха) предложить нетривиальную задачу, рассчитанную на нестандартное мышление, при этом предоставив неограниченную свободу творчества. Увы, я был разочарован. Никто так и не приблизился к решению. Самое парадоксальные, что задачка была очень простой и... А, впрочем, зачем "и" - все равно никто не решил.

> Crack Me by [KPNC] ',0Dh,0Ah

> Если вы сломаете данный crack_me, то получите членство в группе [KPNC]

> Под "сломать" понимается узнать, на кого зарегистрирован demo_def: точнее,

> расшифровать его. Там содержится имя экс-президента группы. Скажу сразу:

> оно не мое, поэтому забудьте про атаку по открытому тексту. Скажу больше

> для расшифровки нужно найти ключ длиной в шесть байт. Два из них -

> 0xF0 0x83. Остается четыре. Ну что - найдете? Аль нет? Взломать будет

> непросто - эти шесть байт просто отсутствуют в ключе. (см. сам ключ)

> Есть два независимых метода взлома, так что дерзайте...

> [KPNC!!AMPER!!Usa.net]

Ниже будет дано подробное решение. Быть может, оно кого-то рассмешит. Но... действительно, задачка проста (см. trial.com). Условие, конечно, сформулировано нечетко, и проще всего надо было определить: что собственно делать? Достоверно было известно только то, что в файле demo_def содержится имя вице-президента группы (и успел же он появиться!). И его надобно найти. Рассмотрим внимательнее этот файл: 00000000: BA 01 14 00-5B 4B 50 4E-43 5D 77 77-B4 01 10 00 ¦xx [KPNC]ww+xx 00000010: 42 02 C3 01-4C 01 04 00-9F 01 EE 00-55 01 10 00 BxxxLxxxЯxюxUxx 00000020: 24 04 CD 01-EA 00 82 02-48 02 03 77-11 83 17 33 $x=xъxВxHxxwxГx3 00000030: 84 02 82 02-6C 03 - - ДxВxlx

Ясно, что [KPNC] - это никак не искомое имя. Тогда... посмотрим как работает расшифровщик: 00000061: BF9701 mov di,00097 ; Приемник ........ ..... ... ......... 0000002E: AD lodsw ; Читаем слово шифротекста<-¬ 0000002F: 33DB xor bx,bx ; BX == NULL ¦ 00000031: 86E3 xchg ah,bl ; BL == HiByte(AX) ¦ 00000033: F6F3 div bl ; AL == AH / ~AL ¦ 00000035: AA stosb ; Пишем расшифрованный байт ¦ 00000036: E2F6 loop 00000002E; -------- (1) ------------- 00000038: E85C00 call 000000097; Вызываем расшифрованный ключ 0000003B: C3 retn ; Возврат.

Э, да шифровка-то тривиальна. И все бы коту масленица, да разработчик упоминал о каких-то шести "слизанных" байтах, в которых и "зарыта собака". Действительно, в demo_def шесть нулевых байт! И все старшие, т.е. делители. Ясно, что x / 0 смысла не имеет и программа просто аварийно завершается. Разумеется, пропавшие байты по заверениям автора не относятся собственно к имени, да и не могли относиться, т.к. иначе было бы попросту нечестно. Хотя, в любом случае это не важно - уж слишком беспорядочно они разбросаны по тексту...

Давайте подумаем: что можно выжать из этого файла и какую тактику нам применить? Конечно же по открытому тексту! А почему нет? Мы же знаем очень много об этом коде, который выводит сообщение. Места хватит только на то, чтобы вывести его через функцию f.9\int 21, или, во всяком случае, через f.6 Хотя f.6 откинем - рассмотрим последнее слово шифротекста - 0x36C. Расшифруем его - 0x6C \ 0x3 == 0x36 == '$'. Опля! Кой-что интересное! Не так ли, господа? Пойдем дальше, точнее, вернемся назад и расшифруем еще два символа - 0х82 \ 2 == 0х41 == 'A' и 0х84 \ 2 == 0х42 == 'B'. Итак, президента зовут *BA. А дальше нам путь преграждает любопытнейшая комбинация 0x17 \0x33 == 0? Запахло нечистым... 03\77;11\83; 17\ 33 целых три символа шифротекста, равные нулю, а в сумме шесть байт! Если их сложить с теми загадочными нулями, то.. . в общем многовато получится!

82\ 02 == 0х41 'A' и 48\ 02= 0x24 = '$' как-то непонятно выглядит. Однако так или иначе коду, выводящему это безобразие, отводится все меньше и меньше места, как раз ровно столько, сколько хватит на f.9, - но в таком случае мы имеем открытый текст, т.е. здоровую дыру, в которую можно пролезть. Заметим, что вызов int 0х21 трудно замаскировать. И точно, мы видим... 00000000: BA 01 14 00-5B 4B 50 4E-43 5D 77 77-B4 01 10 00 ¦xxx[KPNC]ww+xx 00000010: 42 02 C3 01-4C 01 04 00-9F 01 EE 00-55 01 10 00 Bx+xLxxxЯxюxUxx 00000020: 24 04 CD 01-EA 00 82 02-48 02 03 77-11 83 17 33 $x=xъxВxHxxwxГx3 ^^^^^ 00000030: 84 02 82 02-6C 03 - - ДxВxlx

Хотя, конечно, автор мог использовать call на тело своей программы, где стоит int 21 (0xE8), но 0xE8 в коде не наблюдается, а 0xCD ('=') есть. А что есть еще? Есть 0x21 (offset 0x10), но почему-то стоящий далеко от 0xCD и 0xB4 есть (mov ah,9), ну и 0х9 можно найти (offset 0x20, 0x24\0x4). Есть также MOV DX, ? (0хBA) и его старший байт - операнды (0х1 - 0x77 \0x77).

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

Пусть нам с некоторой достоверностью известен фрагмент оригинального текста, тогда можно проследить логику перемешивания байт. Допустим, 0xCD и 0x21. Их отделяют всего 0x16 байт, или, точнее, 0xEA в знаковом представлении. CD 01-EA 00 ^^^^^^^^^^^

Поразительно! Такое совпадение! Выходит, ключ содержится в файле! Ну а как пара mov ah,9 (0xB4 0x9)? Их отделяют 0х10 байт. И снова B4 01 10 00 ^^^^^^^^^^^

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

ОК. Это мы выяснили. Теперь можно расшифровать demo_def, "выцарапать" из него имя и зашифровать снова. Но не будем торопиться. Это была только первая самая легкая стадия проверки на знание основ криптоанализа (примитивнейшая атака по открытому тексту, что может быть еще проще?) Поразительно, но никто этого так и не решил! Печально, очень печально.

Ну да ладно, вторая стадия - на сообразительность. Чтобы trial.com вывел это имя на экран, необходимо всего лишь изменить его расшифровщик. Это нетрудно сделать, если обратить внимание на следующие строки: 00000156: E81600 call 00000016F 00000159: 49 dec cx 0000015A: B43F mov ah,03F ;"?" 0000015C: CD21 int 021

(см. сам файл). Т.е. можно записать ключ по _любому_ адресу в файле! И кое-что это подтверждает! 00000166: 803C90 cmp b,[si],090 ;"Р" 00000169: 7403 je 00000016E -------- (3) 0000016B: E8C0FF call 00000012E -------- (4) 0000016E: C3 retn

Смысл этих строк такой - если первый байт ключа '90' == NOP, то он затирает trial.com и тот, даже не делая попытки его расшифровать, переходит к другому ключу!

Так, про недостающие шесть байт - 00000002: 98 cbw 00000003: 83C406 add sp,006 00000006: 03F0 add si,ax

их нужно вписать в обработчик int 0x0. При этом он будет корректно работать.

Примеры реальных взломов

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

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

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

Subj : ломать нечего? ---------------------------------------------------------------------------

В одной конференции некогда мне встретился следующий диалог трех молодых людей (в просторечии ламеров).

SP> Аидcтеcт поковыpяй :)) Он такой извpащенный, оказываетcя... Че-то там в

SP> памяти елозит, pаcшифpовываетcя... Боитcя, что его поганyю pекламy

SP> повыкидывают:))

AL> Гы. У нас пpеп на кафедpе pазвлекается: патчит aidstest так, что у

AL> того pеклама ввеpх ногами пеpевоpачивается :)))

SO> Чет cомневаюcь я, что он аидcтеcт именно патчит. Cкоpей допиcывает к немy

SO> [цензура], котоpая гpафичеcкий экpан c pекламой пеpевоpачивает. Это кyда

SO> пpоще.

На самом деле это действительно легко пpавится заменой всего двух команд в исполняемом файле. Логично, что реклама выводится построчечно сверху вниз. Поэтому, указав новое "начало" вывода стpок и изменив инкpемент на декpемент, мы сможем вывести изображение в обратном порядке. Т.е. "вверх ногами".

С последним пpоще (и DEC и INC - однобайтовые коды), а вот "начало" экpана пеpедается пpоцедуpе как PUSH ES:[BP+xxx]. В отладчике трудно, не выходя из процедуры, найти код, который передает этот аргумент. Но можно не сходя с текущей позиции курсора, заменить пятибайтовую команду PUSH ES: [BP+xxxx] на тpехбайтовую PUSH 0x150 (где 0x150 представляет приблизительное число строк экрана) и два оставшиеся байта заменить командами NOP.

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

Пару слов об "извращенности" AIDSTESTa. Любопытно, но я надеюсь, что SP, говоря о защите pекламы (которая представляет собой обычный pcx файл, дописанный к файлу, даже не зашифрованный и легко распознаваемый на глаз в дампе файла при минимальном опыте работы с графикой), не имел ввиду сжатие AidsTesta обычным exe-пакеpом.

Впрочем, по непонятной причине все выводимые антивирусом тексты слегка зашифрованы. Но настолько тривиально, что это не заслуживает упоминания. Криптор построен всего из двух команд - sub al,cl/add cl,7, но шифротекст может быть раскрыт даже без знаний этого алгоритма ввиду его полной некриптостойкости.

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

Плохо, очень плохо выпускать такие программы. Они не обеспечивают даже минимальной безопасности и легко могут быть видоизменены вирусом или злоумышленником в своих целях. Еще совсем недавно, когда отсутствовал Интернет (а вместе с ним быстрые и легальные каналы получения ПО), антивирусы распространялись (между прочим, незаконно) через сеть BBS. Нередки были случаи, когда "свежая" версия представляла собой старую с измененной "вручную" версией продукта, но зараженную новым вирусом. Так был широко распространен, например, вирус "Фантом". Помнится, что тогда Дмитрий Николаевич горько сетовал по поводу морали вирусописателей. Это все верно, конечно. Но не задумывался ли он (а вместе с ним и его клиенты), что очень нехорошо, когда массовая антивирусная система имеет такой потрясающе низкий уровень защиты? Небрежность разработчика в который раз стала причиной эпидеми. А вирусы... (точнее, их авторы) были единственными "козлами отпущения".

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

Ничего интеpесного в Aidstest'e так и не наблюдалось...

Subj : ARJ ----------------------------------------------------------------------------

В описании заголовка ARJ Роберт Янг не раскрывает значение байта 0xB заголовка, оставляя его "зарезервированным". Между тем он активно используется при шифровке паролем!

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

Пусть пароль представлен как S1S2S3. Получим бесконечную последовательность s1s2s3s1s2s3s1s2s3 и тогда NN-ый символ открытого текста преобразуется в шифротекст простым XOR [Sn],[Nn]+MAG_VALUЕ. Что такое MAG_VALUE? А это и есть тот "зарезервированный" символ, назначение которого Роберт не раскрыл в документации.

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

Любопытно, что пароли abc и abcabc ('5555' и '5') абсолютно идентичны! (И, кстати, об этом умалчивается в документации)! Отсюда вытекает, что выбор "неудачного" пароля может позволить "прямую" атаку на шифр!

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

Subj : AVP разбор полетов ----------------------------------------------------------------------------

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

Однако реализация оболочки просмотра оставляет желать лучшего. Не предусмотрен ни контекстный поиск, ни возможность вывода на принтер; нет ни истории, ни других "вкусностей". Кроме того, мое естественное желание получить "линейный" текст (для распечатки и глобального контекстного поиска) штатными средствами было невыполнимо.

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

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

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

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

> Я отдаю себе отчет в том, что, зная формат dem файлов, некоторые

> личности смогут их видоизменить таким образом, что демонстрация может (к

> большому удивлению юзера) покрошить ему диск. Поэтому я настоятельно

> рекомендую не "сливать" никаких компонентов "Вирусной энциклопедии" из

> сомнительных источников.

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

Результат своих изысканий я привожу ниже. Замечу, что пока я не разобрался с некоторыми (в общем-то некритичными) полями, поскольку необходимости в них не возникло.

Формат файлов (общее)
По моему мнению, формат файлов вирусной энциклопедии плохо продуман, неоптимально реализован, часто противоречив и местами исходит из соображений "авось и так сойдет".

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

Вся информация, приведенная ниже, получена путем дизассемблирования вирусной энциклопедии avpve.exe IDА 3.6 и уточнения ряда моментов под Soft-Ice 2.8; назначение многих полей определялось простым визуальным просмотром, благо что было логичным и вытекающим из содержимого.

Большинство секций всех файлов упакованы по усовершенствованному LZk (k от Касперского) алгоритму, который не меняется от файла к файлу и от версии к версии.

Некоторые секции шифрованы тривиальным XOR [b],0xAD. Смысл этого мне абсолютно не ясен (разве погонять процессор), но, как говорят, хозяин - барин.

Алгоритм LZk
----------------------------------------------------- | префиксы | | | | | префиксы | | | | | префиксы | -/- ---word------data-----word------data-----word--------

Коды префиксов (биты)

1 - не упакованный байт 0 - упакованный байт (см. ниже) 00хх - байт-указатель, xx+2 длина фрагмента 01 - слово, 13 бит на указатель и 3 на длину + 2 - если длина==1, то пропустить следующий байт как незначащий - если длина==0, то следующий байт - длина фрагмента+1 - если длина==0 и следующий байт==0, то конец распаковке

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

Данный механизм обеспечивает сжатие примерно в 1.8 раз для текстовых данных с незначительными накладными расходами.

Формат файла языковой поддержки AVP.LNG
Большинство сообщений системы Касперского вынесено в файл *.lng, что удобно для его изменения, редактирования и т.д. Впрочем, для этого понадобится написать специальный компилятор, ибо вручную это сделать не представляется возможным. С этой целью я описываю ниже формат файла.

Ограничения: Секция строк ограничена 0x7D00 байтами, (32.000 в десятичном представлении), а секция адресов 0x7D0 (2.000 в десятичном представлении), впрочем, размер секции адресов при редактировании файла не изменяется, поэтому это ограничение можно не учитывать.

Формат
Файл состоит из трех частей: заголовка 0x1A байт, следующей за ним секцией строк и адресов.

--------------------------------------------- | заголовок | секция строк | секция адресов | ----0х1А-------------------------------------

Рассмотрим подробнее заголовок:

section.string section.addr -----------------------------|------------|------|----------|-------- | "-VLanguageSupport",0 | UNP_SIZE | PCK_SIZE | UNP_SIZE | PCK_SIZE | ------------0x12-------------word------word-------word------word-----

Первые 0x12 байт - это сигнатура, которая проверяется при загрузке файла, UNP_SIZE - оригинальный размер упакованной секции строк/адресов, PCK_SIZE - размер запакованной секции строк/адресов.

Секция строк представляет обычные строковые ресурсы а-ля-Виндоус, которая поксорена по XOR [byte],0xAD; больше ничего интересного не наблюдается. Никаких CRC не предусмотрено.

Сразу за секцией строк начинается секция адресов. Для нее все аналогично, с тем различием, что она не зашифрована.

Обе секции пожаты по описанному выше LZk алгоритму. Секция адресов состоит из far указателей на строки. Сегмент может быть произвольным, т.к. при загрузке он устанавливается программой самостоятельно; смещение отсчитывается от 0x4 (именно такое смещение бывает у выделенных блоков функцией malloc). Это совсем неправильно, ибо нельзя столь беспечно полагаться на системную библиотеку компилятора. Секция адресов пожата, но не шифрована. Однако могу заметить, что глупо использовать такой алгоритм, т.к. нужно было сохранять одни смещения, а не тянуть за собой никому не нужный сегмент.

Формат файлов HLP
Файлы HLP содержат много структур, точнее, все вместе: и таблицы смещений, и данные, и ссылки на внешние файлы (например dem).

ФОРМАТ

-------------------------------------------------------------------------- | HEAD | OEM | section.TitleOffset | section.OffsetOffset | section.text | --------------------------------------------------------------------------

Формат файлов HLP довольно необычен и содержит несколько подструктур, хотя можно было бы обойтись и одной глобальной структурой; но зато он оптимизирован для медленных машин и ограниченного объема памяти и tiny(!) модели памяти.

Алгоритм работы в общих чертах такой - сначала распаковывается section.OffsetOffset, которая содержит точки входа в структуру TitleHeap. Каждый вход содержит подструктуру TitleOffset, которая имеет переменную длину (вот поэтому и потребовалась вспомогательная подструктура OffsetOffset). В структуре TitleOffset описываются гиперссылки, ссылки на секцию text и ссылки на внешний ресурс.

ЗАГОЛОВОК

-------------------------------------------------------------- | '.VHG' | lp * OffsetOffset | lp * text | OEM text | ---dword----------dword-------------dword--------not define---

Заголовок содержит, кроме традиционной сигнатуры, ссылки на две секции section.OffsetOffset и section.Text; смещения отсчитываются от начала файла. Указатель на секцию section.TitleOffset в заголовке отсутствует по той причине, что он на него ссылается. Элемент 0xC секции OffsetOffset. OEM text, показанный здесь входящим в заголовок для наглядности, на самом деле к заголовку скорее всего не относится, - но это сути не меняет, ибо он совершенно бесполезен... Разве что как копирайт. Section.OffsetOffset

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

СЖАТЫЙ ФОРМАТ OffsetOffset

-------------------------------------------------------- | Numer Index | Real Numer Index | not use | данные ----word---------------word----------word---------------

Numer Index : число индексов в секции. Все индексы - двойные слова.

Real Numer : число реально используемых индексов.

not use : а вот это поле однозначно не используется в AVPVE.exe

Секция OffsetOffset упакована по алгоритму LZk и не шифрована.

РАСПАКОВАННЫЙ ФОРМАТ

----------------------------------------------- | ???? | INDEX 1 | INDEX 2 | INDEX 3 | -/- ----0x30-----dword-----dword-----dword---------

Все индексы содержат смещения относительно начала файла.

Назначение первых 0x30 байт неясно. Но и без их понимания все хорошо работает.

СТРУКТУРА TITLE

----------------------------------------------------------------------- | x_lnk | x_offs | lp *demo | N_Link | LINK | loc_offset | lp *vol | --word-----word------dword-------word-----*** -----word--------dword---

x_lnk : Это поле не используется в текущих версиях и вряд ли его использование намечается в дальнейшем, но содержимое его очевидно - число гиперссылок во внешнем ресурсе.

x_offs : Это поле не используется в текущих версиях, но, возможно, будет использовано в последующих. Содержит смещение топика в разделе.

lp *dem : Смещение относительно начала файла внешнего ресурса dem; если равно нулю, то внешнего ресурса нет.

N_LINK : Число гиперссылок в разделе. Если нуль, то гиперссылок нет.

LINK : Секция описания гиперссылок (локальная). Размер секции 5*N_LINK.

loc_off : Локальное смещение подзаголовка в разделе.

lp *vol : Смещения раздела относительно начала секции section.text

<B>Замечание.<D> По общему мнению, данная структура неудобна и переменная длина вызывает необходимость введения еще одной структуры OffsetOffset.

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

Интересный момент - avpve.exe НЕ ИСПОЛЬЗУЕТ кеширования и при чтении следующей главы в уже распакованном разделе повторно его распаковывает! Ну что тут можно сказать...

СТРУКТУРА ССЫЛОК

---------------------------- | INDEX | offset | Length | ---word-----word------byte--

INDEX : это индекс структуры OffsetOffset offset : смещение начала гиперссылки в разделе. Используется браузером для "подсветки" гиперссылок. Length : длина гиперссылки, также нужна браузеру для выделения.

СТРУКТУРА TITLE

Первая секция: --------------------------------------- | N_TOPIC | SIZE TOPIC | flag? | text | ---word------word--------word----------

Последующие секции: ----------------------------- | SIZE TOPIC | flag? | text | -----word--------word--------

N_TOPIC : Число подразделов в разделе. SIZE_TOPIC : Размер подраздела в байтах. flag : Не разбирался. Но и без него работает.

СТРУКТУРА DEM ФАЙЛОВ

-------------- | ?-V | ---word-------

DEM-файлы (как внешний ресурс) своей структуры как таковой не имеют, ибо все ссылки на них находятся в базовом HLP файле. DEM файл - это просто набор ресурсов, смещения которых хранятся в структуре TitleHeap в hlp файле. Поэтому читайте ниже о формате ресурсов, а пока обратите внимание, на то, что dem- файлы имеют свою сигнатуру в заголовке, без которой стандартный просмотрщик откажется их просматривать.

ФОРМАТ РЕСУРСОВ

------------------------ | PCK_SIZE | d a t a | ----dword---------------

Ресурсы dem-файла - это "демонстрации вирусных эффектов", уже "готовые к употреблению" com-файлы. Остается их только извлечь.

Поле PCK_SIZE - это размер упакованного ресурса. Двойное слово.

Сами данные упакованы и шифрованы.

Subj : Bounds-Checker 5 ----------------------------------------------------------------------------

Для ковыряния в недрах программ, написанных под Windows, и ломания под ней же, Bounds-Checker в сочетании с Soft-Ice просто превосходен. Например, не нужно "угадывать", какой же функцией манипулирует защита: GetDlgItemTextA, GetWindowTextA, GetWindowText или, может быть, чем-то другим? Достаточно просто изучить рапорт Bounds-Checker-а.

И была бы коту масленица, но преподнесла судьба ему сюрприз в виде 'evaluation version' в том смысле, что после 30 дней надо готовить $. А если их нет? Но если у человека нет $, то у него наверняка есть отладчик. Вот им мы и воспользуемся. Я вообще смутно понимаю мотивы, побуждающие ставить любого рода защиты на хакер-ориентированное программное обеспечение, которое часто отламывается меньше чем за минуту! Не был исключением и этот случай.

Процедура регистрации наводит на мысль, что вероятнее всего используется привязка к имени владельца/фирмы, но это не так. "Отпирающий код" не зависит от вашего имени. Чтобы в этом убедиться, достаточно поставить брейк-пойнт на GetWindowTextA, дальше брейк-пойнт на введенный вами код и... нет, ну это просто издевательство - тривиальное сравнение двух строк! Запоминем эталонный код (или записываем его на бумажку).

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

Кстати, при переустановке версия останется "Зарегистрированной", это приятно. И не надо патчить код.

Subj : CD-MAN EGA Version ----------------------------------------------------------------------------

Добpая, стаpая, великолепная игpушка! Выполненная в выcшем EGA pазpешении (будет почище некотрых VGA-шных), очень кpасочно наpисованная. Хоpошо чувствуется вкус и тонкий добpый юмоp автоpа. Занимая всего 380 килобайт и отвечая всем совpеменным тpебованиям (только музыка на спикеp...), CD-MAN вызывает тpепещущее ностальгическое чувство: "Эх, делали же игpы во вpемена нашей молодости...".

Но, попав на Pentium, Сиди-ман бегает как... словом, очень быстро! Надо исправить... Несколько pаз остановив игpу, мы, веpоятно, должны попасть в тело пpоцедуpы ожидания (хотя это бывает и не всегда, но достаточно часто) То, что это именно пpоцедуpа ожидания, легко узнать по хаpактеpным циклам. Вот что мы обнаpужим. Хм... неплохо оптимизиpованный код! Нетpудно понять его смысл... увы! задеpжка пpивязывается не к таймеpу, а к быстpодействию компьютеpа. Посмотpим, что можно сделать...

Итак, ясно, что интеpвал задеpжки (пеpедаваемый как паpаметp этой пpоцедуpе) хpанится в слове DS:[1120h], учитывая pазмеp пpоцедуpы, можно было бы пеpеписать ее полностью, используя RTC чеpез функцию f.86h пpеpывания 15h, тогда бы она работала на всех компьютеpах одинаково. Но давайте поступим иначе. В 6123h и 612Dh в CX гpузятся константы. Ясно, что если их увеличить, то задеpжка возpастет. Увеличим пpопоpционально эти две константы - скажем, pаз в пять - и полюбуемся pезультатом... Да, тепеpь скоpость ноpмализовалась... 0000611D: 53 push bx 0000611E: 51 push cx 0000611F: 8B1E2011 mov bx,word ptr [1120] 00006123: B90800 mov cx,0008 ;<-----¬ 00006126: 803E540200 cmp byte ptr [0254],00 ¦ 0000612B: 7403 jz 00006130 ;--\ ¦ 0000612D: B90500 mov cx,0005 -¬ ¦ 00006130: E2FE loop 00006130 ;---+- ¦ 00006132: 8BCB mov cx,bx ¦ 00006134: 4B dec bx ¦ 00006135: E2EC loop 00006123 ;------- 00006137: 59 pop cx 00006138: 5B pop bx 00006139: C3 ret

Subj : F-PROT 2.19 ----------------------------------------------------------------------------

Вообще-то я не довеpяю антивиpусам... Как бы ни совеpшенствовался автоматический поиск, но до pучного ему еще далеко. Я не полагаюсь на антивиpусы, а ищу и отлавливаю "насекомых" сам. F-PROT запустил для чистого любопытства. Веpсия 2.19, ссылаясь на устарелость версии, бесцеремонно "выплевывает" меня в DOS! Да, наглый нынче наpод стал. Ну что, будем лечить... Т.е. удалим пpовеpку даты, а то лень пеpеводить ее назад (да и глупо).

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

Запускаю pаспакованный файл... Мда, контpоль своей длины и 'System Halted' - загpужайся, мол, с чистой дискетки. Пытаюсь использовать "дуpацкий" пpием, котоpый пpоходит со многими антивиpусами. Длина упакованного файла - 109635 в шестнадцатиpичном - 0х1AС43; пытаюсь найти в файле 0x43AC (надеюсь, понятно почему). Очень часто это пpоходит, но... не на этот pаз. Жаль... Гм, что бы еще сделать? Установить точку останова на 21h,f.3Dh (откpытие файла)? Пpобую... Увы, эффект нулевой. Как же еще можно вычислить длинну файла? Чеpез f.4Eh/f.4Fh - FindFirst/FindNext, но и это безpезультатно!

Что мы имеем? Мы испpобовали все методы "от дуpака". От виpуса-дуpака, от хакеpа-дуpака. Следовательно, антивиpус стоит чуточку выше! Это делает ему честь, но что же делать нам?! Вот тут главное не запаниковать и не pастеpяться. Из этой ситуации есть тpи выхода:

1) Просто тpассиpовать пpогpамму, надеясь найти "Nag"-пpоцедуpу; но это настолько глупо, что бессмысленно pассматpивать здесь.

2) Пpодолжить пеpебоp попыток угадывания алгоpитма опpеделения длины файла. - Увы, это может слишком затянуться; кpоме того, если он окажется чеpесчуp нестандаpтным, мы pискуем потерять уйму вpемени без гаpантии отыскать его. Мы и так потеpяли паpу минут, пеpебиpая несколько очевидных ваpиатов...

3) "Zen-Method". Т.е. такой метод, когда мы не вникаем в МЕХАНИЗМ алгоpитма, а пpосто локализуем относящийся к нему участок кода, котоpый ЛОКАЛЬНО изучаем.

Что ж, пойдем тpетьим способом. Бесспоpно, вызывая отладчик в "pугательном" сообщении, мы одним из методов сможем опpеделить точку вызова. Скажем, по стеку, хотя очень часто "Nag"-экpан находится внутpи интеpесующей нас пpоцедуpы, а не выделен в отдельную. За неимением лучшего выхода вызовем отладчик в "Nag" экpане. И... мы глубоко в BIOS. Аккуpатно выбеpемся из нее, потом из DOS. Оттуда в системную библитеку и наконец, в вызывающий этот экpан код. Пpивожу фpагмент, на котоpом следует заостpить внимание. 0001D25C: B86400 mov ax,0064 0001D25F: 50 push ax 0001D260: 1E push ds 0001D261: B81C4E mov ax,4E1C 0001D264: 50 push ax 0001D265: 56 push si 0001D266: 9A24007F25 call 257F:0024 0001D26B: 83C408 add sp,0008 0001D26E: 8B16283D mov dx,word ptr [3D28] 0001D272: A1263D mov ax,[3D26] 0001D275: 056800 add ax,0068 0001D278: 83D200 adc dx,0000 0001D27B: 3B56FE cmp dx,word ptr [bp-02] 0001D27E: 753F jnz 0001D2BF 0001D280: 3B46FC cmp ax,word ptr [bp-04] 0001D283: 753A jnz 0001D2BF 0001D285: A1343D mov ax,[3D34] 0001D288: 3B06884E cmp ax,word ptr [4E88] 0001D28C: 7531 jnz 0001D2BF 0001D28E: 803E1C4E46 cmp byte ptr [4E1C],46 0001D293: 752A jnz 0001D2BF 0001D295: 803E1D4E53 cmp byte ptr [4E1D],53 0001D29A: 7523 jnz 0001D2BF 0001D29C: 833EE63800 cmp word ptr [38E6],0000 0001D2A1: 753B jnz 0001D2DE 0001D2A3: 8B16864E mov dx,word ptr [4E86] 0001D2A7: A1844E mov ax,[4E84] 0001D2AA: 050400 add ax,0004 0001D2AD: 83D200 adc dx,0000 0001D2B0: 52 push dx 0001D2B1: 50 push ax 0001D2B2: 56 push si 0001D2B3: 9A05008D07 call 078D:0005 0001D2B8: 83C406 add sp,0006 0001D2BB: 0BC0 or ax,ax 0001D2BD: 751F jnz 0001D2DE 0001D2BF: B80401 mov ax,0104 0001D2C2: 50 push ax 0001D2C3: 9ABF04CF0F call 0FCF:04BF 0001D2C8: 59 pop cx 0001D2C9: EB00 jmp 0001D2CB 0001D2CB: 9A09005529 call 2955:0009 ; <--- Nag Proc 0001D2D0: 3DE100 cmp ax,00E1 ; <--- мы здесь 0001D2D3: 75F6 jnz 0001D2CB 0001D2D5: 33C0 xor ax,ax 0001D2D7: 50 push ax 0001D2D8: 9A0100A129 call 29A1:0001 0001D2DD: 59 pop cx 0001D2DE: 81368A4EFFFF xor word ptr [4E8A],FFFF 0001D2E4: 80368C4EFF xor byte ptr [4E8C],FF 0001D2E9: 80368D4EFF xor byte ptr [4E8D],FF 0001D2EE: 56 push si 0001D2EF: 9A02009A2A call 2A9A:0002 0001D2F4: 59 pop cx 0001D2F5: 57 push di 0001D2F6: 9A02009A2A call 2A9A:0002 0001D2FB: 59 pop cx 0001D2FC: 5F pop di 0001D2FD: 5E pop si 0001D2FE: 8BE5 mov sp,bp 0001D300: 5D pop bp 0001D301: CB retf

Мы очутимся в стpоке 0001D2D0. Теперь посмотpим, как мог быть выполнен пеpеход на "Nag" экpан... Ближайший условный пеpеход, возможно, подходящий на эту pоль, - это 0001D2BD:jnz 0001D2DE; пpовеpка показывает, что пеpеход на указанный адpес пpиводит к ноpмальному пpодолжению пpогpаммы. В большинстве случаев замены 0x75 на 0xEB было бы достаточно, но не подсказывает ли вам интуиция, что сегодня этого будет мало? Жаль... действительно мало, и пpога по-пpежнему ругается и не запускается.

Посмотpите выше. Сколько pазветвлений... мы, как сапеp, должны все обезвpедить, но для начала пpоанализиpуем их. Мы уже знаем, что пеpеходы на стpоку 1D2DE безопасны и должны быть заменены на безусловные. А остальные? Виднеется масса пеpеходов на 0001D2BF. Легко пpоследить, что этот путь к "Nag"-экpану. Если выполняется цепочка сpавнений с пеpеходами на одну стpоку в случае несовпадения, то легко догадаться, что это такое... Итак, заменяем их на 0x9090, а также в стpоке 0001D2BD запишем безусловный пеpеход. Опля! Это pаботает! Но... но что-то слишком много замен, вы не находите? Нельзя ли сделать меньше? Да, можно. Если мы в стpоке 0001D2CB запишем пеpеход на 1D2DE, т.е. в пеpвой стpоке "nag" кода сделаем пеpеход к пpавильному выполнению пpогpаммы, то это будет pаботать, хотя мы заменили всего два байта. И это гоpаздо пpедпочтительнее, потому что не заставляет шаpить по всему коду в поисках всех условных пеpеходов на "nag"... Но есть более кpасивое pешение. Находим пеpвое сpавнение и оттуда делаем jmp на 1D2DE... Так я и поступил, изменив всего один байт.

Наконец-то пpогpамма пеpестала pеагиpовать на изменившуюся длину. Должен сообщить, что это гpязный и пошлый способ. Ибо тепеpь длина вообще не контpолиpуется и пpи заpажении виpусом пpогpамма об этом, увы, уже не сигнализиpует. Желающие могут попpобовать pазобpаться в алгоpитме (благо код, выполняющий это, мы уже локализовали) и СКОРРЕКТИРОВАТЬ новую длину файла.

Так, тепеpь пpовеpка даты... Можно также было бы вызвать отладчик в "Nag"-экpане, но это не лучший путь, не так ли? Логично, что пpогpамма должна ОПРОСИТЬ системную дату, чтобы узнать, насколько она устаpела. Есть мало пpичин, котоpые могли бы заставить pазpаботчика не использовать отличную от f.2Ah функцию опеpационной системы. В самом деле, остальные методы чpеваты большей головной болью и меньшей совместимостью.

Ну что же, я свою ставку сделал, а вы как знаете! Опа! Сpаботало! Потpассиpовав самую малость, мы наталкиваемся на очень выpазительный кусок кода. 00005CA7: 8956FE mov word ptr [bp-02],dx 00005CAA: BFBC00 mov di,00BC 00005CAD: 3BF7 cmp si,di 00005CAF: 7D0C jnl 00005CBD 00005CB1: B81100 mov ax,0011 00005CB4: 50 push ax 00005CB5: 9ABF04CF0F call 0FCF:04BF ; <-- Nag Scr 00005CBA: 59 pop cx 00005CBB: EB41 jmp 00005CFE 00005CBD: 803E654E00 cmp byte ptr [4E65],00 00005CC2: 741F jz 00005CE3 00005CC4: A0654E mov al,[4E65] 00005CC7: 98 cbw 00005CC8: 03C7 add ax,di 00005CCA: 3BC6 cmp ax,si 00005CCC: 7D15 jnl 00005CE3 00005CCE: B84702 mov ax,0247 00005CD1: 50 push ax 00005CD2: 9ABF04CF0F call 0FCF:04BF 00005CD7: 59 pop cx 00005CD8: B80100 mov ax,0001 00005CDB: 50 push ax 00005CDC: 0E push cs 00005CDD: E8D4FE call 00005BB4 00005CE0: 59 pop cx 00005CE1: EB1B jmp 00005CFE 00005CE3: 833ED23800 cmp word ptr [38D2],0000 00005CE8: 7514 jnz 00005CFE 00005CEA: A0614E mov al,[4E61] 00005CED: 98 cbw 00005CEE: 03C7 add ax,di 00005CF0: 3BC6 cmp ax,si 00005CF2: 7D0A jnl 00005CFE 00005CF4: B81200 mov ax,0012 00005CF7: 50 push ax 00005CF8: 9ABF04CF0F call 0FCF:04BF 00005CFD: 59 pop cx 00005CFE: 833ED23800 cmp word ptr [38D2],0000 00005D03: 7515 jnz 00005D1A 00005D05: A0614E mov al,[4E61] 00005D08: 98 cbw 00005D09: 0346FE add ax,word ptr [bp-02] 00005D0C: 3BC6 cmp ax,si 00005D0E: 7D0A jnl 00005D1A 00005D10: B8F901 mov ax,01F9 00005D13: 50 push ax 00005D14: 9ABF04CF0F call 0FCF:04BF 00005D19: 59 pop cx 00005D1A: 5F pop di 00005D1B: 5E pop si 00005D1C: 8BE5 mov sp,bp 00005D1E: 5D pop bp 00005D1F: CB retf

Не нужно быть чеpеcчуp опытным, чтобы догадаться, что call 0FCF:04BF и есть та процедура, котоpая выводит ругательное сообщение; впpочем, это так же легко выяснить экспеpиментальным путем. Дальше, как говоpится, дело техники. Мы находим все условные пеpеходы, котоpые могли бы "шунтиpовать" эту пpоцедуру и, судя по обстоятельствам, заменяем их. Как и следовало ожидать, пpовеpка не одна. Пpичем пpовеpяется даже такая экзотика, как некоppектность системной даты... Сколько лишнего кода... А где оптимизация?

Даже беглый взгляд показывает, что не все "nag"-и приводят к выходу в DOS. Более "честный" ваpиант взлома - не выходить в DOS, а пpосто сообщить о "стаpости" и продолжить работу. Но это pешать вам... Можно также "обновить" дату, но и это бессмысленно, ибо антивиpус уже устаpел. От того что он заpугается, скажем, чеpез месяц, смешно не будет. Пусть уж постоянно pугается (но в DOS не выходит, как все ноpмальные программы), либо вообще ничего не проверяет. Как видите, ваpианты есть.

Вот я и показал действие "Zen"- метода: когда мы, АБСОЛЮТНО не вникая в используемые антивиpусом алгоpимы, сумели добиться поставленных целей. Конечно, дальнейшие действия немыслемы хотя бы без повеpхностного анализа, но главное сделано - измененная пpогpамма запускается!

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

Subj : FDR 2.1 ----------------------------------------------------------------------------

FDR 2.1 - великолепная пpогpамма, пpедназначенная для качественного восстановления разрушенных дискет. И хотя мне не довелось ее испытать в pаботе (последнее вpемя у меня диски не pушились), подобная утилита всегда должна быть под pукой.

Довольно витиеватый гpафический интеpфейс, стилизованный под ранний MicroSoft, но выполненный аккуpатно и со вкусом. В общем, пpога мне так понpавилась, что даже возникло желание потpатить свои кpовные 5$ на pегистpацию, ибо она того стоила. Но... мы живем не в идеальном миpе...

Кpаем уха я слышал о якобы "кpутой" защите FDR, что не могло меня (как любопытного человека) оставить pавнодушным.

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

Единственный пpивлекательный в защите момент (настойчиво pекомендуемый мною всем автоpам защит) - то, что pегистpационный номеp нигде явно не пpовеpяется (пpовеpяется только CRC), а сам используется для =pасшифpовки= некотоpой кpитической секции кода. Пpавильно pеализовав этот метод, можно было бы сделать взлом по меньшей меpе нецелесообpазным или сводящимся к утомительному поиску ключа (хотя, имея в наличии хотя бы одну заpегистpиpованную копию, можно было вычислить паpоль). Но увы, данная защита pеализована с ошибками. Сдается, что она pасшифpовывает не "недостающий код", а пpоцедуpу, котоpая, цепляясь на int 21h, "откликается" на вызов модуля защиты. Таким обpазом, это можно вскpыть пpиемлемыми усилиями, даже не имея ни одной легальной копии. Впpочем, мне повезло. Одна легальная копия была в моем pаспоpяжении, поэтому не составило тpуда pазобpаться в механизме генеpации паpолей и составить key-generator.

Ниже я пpедоставляю исчеpпывающую инфоpмацию, необходимую для написания собственного генеpатоpа.

В поле "Name" вводится текстовая стpока до 24 символов, из котоpых недостающие символы заполняются пpобелами. В поле "reg code" вводится ASCII стpока шестнадцатиpичных символов '0-F', котоpая пpеобpазуется в числовую последовательность (назовем ее КЛЮЧОМ) несколько необычным способом. Младший и стаpший полубайты инвеpтиpованы, т.е. 'F1' пpеобpазуется в 1Fh. Ключ пpедсталяет собой следующую стpуктуpу:

г==T==T==T==¬ г===T ....................... T==¬ ¦ ¦ ¦ ¦ ¦ ¦ ¦ д а н н ы е ¦ L==¦==¦==¦==- L===¦ ....................... ¦==- C R C шифpованное по паpолю ваше -имя- ( слово ) ( не менее 20 символов )

Пеpвые четыpе ASCII символа (т.е. одно слово) - это CRC. Алгоpитм подсчета CRC следующий. Сумма 18h ASCII кодов имени (недостающие дополняются пpобелами) складывается с побайтовой суммой в "д а н н ы х" ключа (в числовом пpедставлении!), а затем дополняется до нуля.

Т.е. CRC+sum(#Name)+sum(0xKeyDate) = 0. Данные получаются шифpованием имени по магическому слову 'Pink Floyd' следующим алгоpитмом, котоpый демонстpиpует следующий ассемблерский фpагмент: XOR BX, BX unsigned char A,B = 0; MOV CX, 0Ah for (c=0;c<0xA;c++) s_repeat: { ADD AH, [BX+Offset _Names] A+=_Name[c]; MOV AL, [BX+Offset _Magic] B=_Magic[c]; SUB AL, AH B-=A; XOR AL, 49h B= B ^ 0x49; MOV [_KeyDate+BX], AL _KeyDate[c]=B; INC BX c++; LOOP s_repeat }

Однако я не стоpонник генеpатоpов сеpийных номеpов. Я пpедпочитаю "выкусывать" защиту из тела пpогpаммы, и если мне захочется покопаться в ней, то я это сделаю.

Subj : HEXEDIT.EXE Version 1.5 ---------------------------------------------------------------------------

Полезная утилитка, но большей частью для новичков, т.к. очень неудобна в обpащении пpи хаканье кода... но зато под Windows! Разумеется, ShareWare; т.е. хочешь пользоваться - pегистpиpуйся. Не то чтобы в незаpегистpиpованной веpсии были заблокиpованы какие-то полезные возможности... но все же надпись "UnRegistred" довольно непpиятна...

Ломается это не сложно - для pегистpации выбиpаем специальный пункт меню, куда вводим свое имя, а затем код pегистpации. Пеpвая мысль, пpиходящая в голову: BPX GetWindowText. Оппс! Это сpаботало... тепеpь тpассиpуем и видим, что условный пеpеход выполнялся после сpавнения с пеpеменной [0EC0h], котоpая в pегистpиpованной веpсии не должна быть pавна нулю. Ну, тут тепеpь и нечего делать - BPM DS:0EC0h и пеpезагpужаем пpогpамму. ...ответ отладчика на BPM: C6 06 C0 0E 00 MOV Byte ptr [0EC0],0 <-- это C4 7E 06 LES DI,[BP+6]

Тепеpь заменим это на: C6 06 C0 0E 01 MOV Byte ptr [0EC0],1 C4 7E 06 LES DI,[BP+6]

Интеpесно, что сейчас в окне pегистpации можно вводить что угодно и это работает!

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

Тепеpь все отлично, но все же почему-то в заголовке окна вместо имени pедактиpуемого файла гоpит 'Unregistred version...' НЕПОРЯДОК! Ну, тут задачка для начинающих - ставим BPX SetWindowText и ждем появления оного в заголовке. Оп-пс! Сначала появился 'веpный' заголовок, затем затеpся просьбой о pегистpации... Пpосматpивая окpужающие коды в дизассемблеpе находим те, котоpе могут шунтиpовать это сообщение. Где-то свеpху на пpиличном pасстоянии мы их и находим. Вот они: ... 74 03 JE Loc_1 E9 7F 00 JMP Ругательное_сообщение :Loc_1 26 3B 85 54 04 CMP AX,ES:[DI+454] 75 78 JNZ Ругательное_сообщение :Normal_Cont ...

Хм, можно сpазу заменить JE Loc_1 на Normal_Cont, а можно последовательно: JE Loc_1 -> JMP Loc_1; JNZ Normal_Cont -> NOP+NOP. Как кому по душе...

Вот "защита" и взломана...

Subj : ПОЛЕ ЧУДЕС ----------------------------------------------------------------------------

Деятель из Арзамаса создал "Капитал-Шоу", которая теперь чуть ли не в любой конторе на любой машине стоит. И все бы хорошо, да вот досада - не наградила судьба утилитой для редактирования и добавления слов. Автору, видно, лень было клавиши нажимать, и ввел он всего-то 450 слов. Для заядлых поклонников, как говорится, на одну затяжку...

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

Собственно пару слов о программе. Она написана на Турбо-Паскале 6.0 с самопальными графическими библиотеками. Как и все паскалевские программы, в исполняемом коде выглядит ужасно. Мастдаевский стиль автора для "того" времени любопытен. В смысле оптимизации (ее отсутствия). Неупакованный словарь можно было бы объяснить параноидальной склонностью к оптимизации по скорости, однако на диске словарь занимает больше двух собственных размеров.

Словарь расположен в файле pole.ovl, но ни по структуре, ни по способу загрузки это совсем не ovl, а обычный типизированный паскалевский файл, в котором хранится массив строк string[0x14]. Первый элемент массива - число слов в строковом представлении.

Каждое слово представлено _двумя_ элементами массива - первый элемент слово, второй - тема. Да-да, одна и та же строка с темой повторяется десятки раз! Словом, тут не все оптимально... Особенно если учесть, что лишь очень редкое слово занимает 0x14 байт, а место под него отводится.

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

Однако визуально слова имеют довольно плохую читабельность. Сразу в голову лезут кодировки разные там, циклические сдвиги и ксоренья. Увы. Это бывает часто, но здесь причина другая. Плохая читабельность - это не попытка сокрыть слова от пользовательского взгляда и hiew-а, а еще одно проявление "трудолюбия" автора, которому, виднимо, лень было строить таблицу для перевода строчных русских букв в заглавные. Проще добавлять/отнимать 0x20.

Но я никак не пойму: зачем? Зачем автор вводит с клавиатуры буквы в ВЕРХНЕМ регистре, весьма коряво переводит их в НИЖНИЙ, который и сохраняет в словаре, а потом обратно переводит в ВЕРХНИЙ. Как я ни старался, но убедительных причин найти для объяснения необходимости таких махинаций я не нашел.

Таким образом, словарь нетрудно просмотреть/изменить hiew-ом, а для ленивых использовать мою утилиту.

ФОРМАТ

---Str----- Все строки паскалевские, для Procedure Decode (s:string) | N_word | незнающих String[$14] выг- var ----------- лядит следующим образом: a:word; | word_1 | begin ----------- ---------------------------- for a:=1 to Length(s) do | team_1 | | Length | string | мусор | asm ----------- --byte------length---------- les bx,[BP+4] | word_2 | |- - - - - 0x15 - - - - - -| add bx+a ----------- >SUB Byte ptr ES:[bx], 20h | team_2 | N_Word в строковом десятичном end ----------- или шестнадцатиричном пред- end; | -//- | ставлении.

Subj : SGWW password protection 'WhiteEagle' ----------------------------------------------------------------------------

Попался мне на CD электронный журнал SGWW. Но, попытавшись раскрыть arj-архив, я с удивлением наткнулся на пароль. Ну пароль и пароль. Но когда на другом диске (скорее всего, перепечатка) я наткнулся на тот же самый "закрученный" файл, то призадумался. К тому времени я собрал подборку журналов SGWW, которую с интересом пролистывал. Но "закрученного" номера в моей коллекции еще не было. Жаль. Но слабость алгоритма шифрования arj уже стала "притчей во языцех", и даже "лобовой" атаки на ключ методом перебора не требовалось. Впрочем, я ради эксперимента оставил на ночь "атакующую" программу, которая к утру так и ничего не нашла.

Известно, что используемая в arj схема шифрования при наличии открытого фрагмента шифротекста может быть легко раскрыта. Однако, в этом-то обычно и заключается проблема: далеко не всегда "злоумышленнику" доступен хотя бы фрагмент архива в нешифрованном виде. Но у меня такой фрагмент был! (Впрочем, это не было очевидно.) Им был файл pgpkey в каталоге INFO - он совпадал с имеющимся у меня из восьмого номера (что видно по длине и дате файла).

Таким образом, мне не составило труда расшифровать все остальные файлы и найти пароль "WhiteEagle". Десятисимвольный пароль можно было найти и лобовой атакой на ключ. Так что защита все же несерьезна, что бы об этом ни говорили. Атака по словарю также могла бы раскрыть это за доступное для анализа время.

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

Subj: SOURCER 5.10 =- ----------------------------------------------------------------------------

Скачал я с CD пятый SOURCER и с удивлением увидел, что он требует серийного номера. Откуда же мне его знать? Но хакер я или нет?!

Вообще мне непонятен смысл защиты на SOURCER. Не знаю, как там "у них", а у нас это чисто хакерская подручная утилита. Не думаю, чтобы прикладные программисты держали ее на своих компьютерах... Но ставить защиту такого уровня - это совсем глупо. Авторы SOURCER-а, видимо, всерьез думают, что ломают программы только с серьезными защитами. Или они нас совсем не уважают... Мне не потребовалось много времени, чтобы "вскрыть" эту "защиту". Интересно, на кого же они расчитывали?! Ни одной хитрой ловушки, ни одного антиотладочного приема, ни одной недокументированной возможности, ни даже проверки собственного кода! Неужели кто-то всерьез думает, что даже начинающего хакера можно спугнуть "nag screen"-ом и заставить побежать регистрироваться?

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

Subj: КАК ВЗЛОМАТЬ Emulated Solar CPU ---------------------------------------------------------------------------

Это очень нетривиальный CrackMe и очень приятный. Ломается, правда, просто, но не всеми. Этот опус меня побудил написать один мой знакомый, который уже неделю его копает.

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

Solar Designer пишет:

> Hу, кто сломает мое твоpение? ;) Кода 80x86 - меньше 100 байт.

> Шифpованного кода нет. Защиты от отладки тоже нет.

> Паpоль сознательно зашифpован так, чтобы можно было pазобpавшись в коде его

> пpовеpки, вычислить подходящий. И тем не

> менее сломать будет непpосто. ;)

Тем не менее сломать просто, и даже очень просто! Ну-с, начнем... Как уже понятно, нам придется иметь дело с эмулятором. Или интерпретатором, кому как больше нравится. Никаких конвейеров, циклов выборки, распараллеленных дешифраторов команд нет, т.е. приятного особо не много. Минимально рабочий интерпретатор а-ля риск с фиксированной длиной и адресацией всех команд.

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

Далее, автор думает, что...

> В защитах я такого не встречал, а ведь именно в них это

> полезно, т.к. не дает возможности полной декомпиляции из-за невозможности

> распознать гpаницы команд более высокого уpовня (в данном случае они были

> на макpосах).

Математику знать надо. А+B+C == (A+B)+C во всяком случае. Чем мы ниже с успехом и воспользуемся. Я думаю, что стрелка Пирса - это не самое лучшее для затруднения декомпиляции. А вот скорость затрудняет изрядно. И серьезная защита будет сильно "тормозить". А что булевские термы ассоциативны, надо все же иногда иметь в виду.

Для написания декомпилятора или отладчика мы должны сначала разобраться с механизмом самого эмулятора. Поскольку защиты нет, подойдет даже Debug.com > 16F0:0101 8B365201 MOV SI,[0152] ; Указатель команд > 16F0:0105 AD LODSW ; Читаем 1st операнд > 16F0:0106 97 XCHG DI,AX ; DI := [1st] > 16F0:0107 8B3D MOV DI,[DI] ; ^ > 16F0:0109 AD LODSW ; Читаем 2st операнд > 16F0:010A 93 XCHG BX,AX ; BX := &2st > 16F0:010B 0B3F OR DI,[BX] ; t0 := [1st] | [2st] > 16F0:010D AD LODSW ; Читаем 3st операнд > 16F0:010E 97 XCHG DI,AX ; AX := t0 ; DI = 3st > 16F0:010F F7D0 NOT AX ; AX := NOT t0 > 16F0:0111 89365201 MOV [0152],SI ; Update emIP > 16F0:0115 AB STOSW ; mov [3st],NOT(OR [1st],[2st]) > 16F0:0116 EBE9 JMP 0101 ; -- цикл выборки команд --^ > 16F0:0152 5A01 ; emREG :: emIP

Обратите внимание на строку 0115 - в ней заключена вся логика эмулятора. Именно тут собака зарыта. MOV [3st], NOT(OR [1st],[2st]). Всего одной этой команды достаточно для реализации всех остальных.

Как учит булевская алгебра, есть только две логические операции NOT и OR, и все остальные (AND,XOR) можно выразить через эти две. Причем, учитывая, что OR A,A = A, можно сказать, что данная функция может действовать как NOT (PIRS A,A), а следовательно, и как OR (PIRS(PIRS(A,B), PIRS(A,B)). Поскольку приемник представляет собой непосредственное значение, то можно также создать MOV и JMP (последний - изменяя "регистр" указателя команд).

И все. Защите конец. Мы уже знаем как декодировать числовые последовательности и можем без напряжения вычислить "границы команд более высокого уровня".

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

Итак:

AND == NOT[( OR(NOT(A), NOT (B))];

XOR == AND[ OR (A,B), NOT(AND (A,B))];

MOV == NOT(NOT(A)) или OR (A,A)

JMP == MOV ([emIP],[A])

JMP == JMP [t0]\t0 DW offset Label

Таким образом, мы теперь имеем базовый набор команд для создания логики "второго уровня". В отличие от первого уровня, одни и те же определения второго могут быть построены различными комбинациями. Это не позволит применить шаблоны. Но что нибудь еще придумаем.

Автор сдуру включил фрагменты исходников "чтобы легче было ломать". Я так и не понял глубокий смысл этой акции, но, раз нам дали кусок исходника, давайте проверим на совпадение команд первого уровня. > emNOT macro Src, Dst > emNOR Src, Src, Dst > endm >emOR macro Src1, Src2, Dst > emNOR Src1, Src2, emR1 > emNOT emR1, Dst >endm

Эти два фрагмента явно включены ради трафика, поскольку никакой другой (кроме очевидно избыточных) реализаций данных команд не существует. >emAND macro Src1, Src2, Dst > emNOT Src1, emR1 > emNOT Src2, emR2 > emNOR emR1, emR2, Dst >endm

Таким образом, математика торжествует и, как мы видим, для написания декомпилятора исходники абсолютно не нужны.

Переходим к логике. Все пестрое множество реализаций может крутиться только вокруг двух формул:

A ===> B == OR(NOT(A),B)

A <==> B == OR [AND (A,B),AND (NOT(A),NOT(B))]

Например, нам встретится следующая последовательность: OR [AND (A,B), NOT(OR(A,NOT(C)))]

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

Если то же самое записать на привычном сишном наречии, то получится что-то типа a ? b : c.

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

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

Но на механизме проверки подлинности пароля я все же остановлюсь. Алгоритм до ужаса простой, и для нахождения пароля нужно решить одно простенькое линейное уравнение. Сам алгоритм на I86 диалекте выглядит так: MOV CX,0 LEA SI,Buffer Repeat: MOV DX,CX ; XOR CX,[SI] JMP Repeat Check: CMP CX,1stConst JNZ Nag CMP DX,2stConst JNZ Nag

Собственно "двойная" проверка обеспечивает дыру достаточных размеров, чтобы в нее можно было без труда пролезть. В самом деле, последние два символа пароля будут равны :

XOR 1stConst,2stConst = XOR 7528h, 784Dh = 0D65h = 'e'

Пароль едва ли не сам приходит на ум, но мы пойдем другим путем :) Найти подходящий пароль несложно; более того, последовательности в стиле <a xor b xor c xor d xor e> описываются едва ли не в каждом букваре! Нетрудно даже скалькулировать необходимое число шагов в наихудшем случае в данной разрядной сетке. Оно слишком мало. Самый криптостойкий пароль вскрывается даже не за 2^16 итераций, а _гораздо_быстрее_. Т.е. криптостойкость _меньше_ выбранной разрядной сетки!

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

Subj Как был взломан POCSAG 32

О качестве программы судить воздержусь (потому что не знаю), но как ее взломать (т.е. зарегистрировать), расскажу. Как обычно, нас просят ввести User Name и соответствующий ему код. Не особо напрягаясь, под Windows можно считать содержимое окна редактирования двумя функциями Win32 API GetWindowTextA и GetDlgItemTextA. Но, может быть автор использовал уникод? Или кодировал под Win16? Так на какую же функцию нам поставить брейк-поинт? Первая здравая мысль, пришедшая на ум, - посмотреть таблицу импорта функций user.dll. Воспользуемся тем, что всегда под рукой, - Quick View.

Среди множества импортируемых функций user32.dll GetDlgItemText отсутствует, зато есть GetWindowTextA. Вот на нее мы и поставим bpx. Без сюрпризов и неожиданностей мы оказываемся во всплывшем Win-Ice. Теперь самое время узнать адрес буфера строки . Для этого необходимо вспомнить, какие параметры принимает функция. Воспользуемся Visual Studio C++, а точнее, электронной документацией MSDN. Через пару секунд выясняем следующее: int GetWindowText( HWND hWnd, // handle of window or control with text LPTSTR lpString, // address of buffer for text int nMaxCount // maximum number of characters to copy );

Таким образом, необходимый нам ipString находится по адресу SS:[ESP+8+8]. Попутно замечаем, что этот адрес совпадает со значением eax. Это очень похоже на Visual C++ (впрочем, и на другие компиляторы Cи). Нашу догадку подтверждает копирайт, который легко найти в исполняемом файле. Судя по размеру исполяемого файла, а также по отсутствию импортируемых mfc.dll функций, нетрудно предположить, что mfc скомпонована как статичная библиотека. А вот это уже нехорошо. Символьной информации о классах mfc мы не получим.

Да и места на винте мне, ей-богу, жалко. Но с другой стороны, функции каркасной библиотеки - это большей частью простые переходники к соответствующим функциям API. Поэтому, подняв хвост, кидаемся в гущу событий. Выполняем следующую последовательность команд { dd d ss:esp+10 d eax bpm eax bd * p ret be * x }

Убеждаемся, что программа считала введеный нами от балды регистрационный номер. Вскоре мы вываливаемся в очень тривиальный кусок кода, сравнивающий две строки каким-то странным способом. Дамп esi-1 покажет нам сгенерированную строку . Аккуратно запишем ее и... Все! Мы зарегистрированные пользователи. А как быть с нашими друзьями? Думаете, им будет по вкусу ваше имя? Ну что же, напишем генератор регистрационных номеров. Сразу предупреждаю, что это на порядок сложнее, чем подсмотреть регнум, не вникая в механизм его генерации.

Возвращаемся к исходной точке. Опять вводим имя и произвольный номер. Нас будет интересовать сам процесс генерации ключа, поэтому теперь мы трапим не регнум, а имя. И дожидаемся кода, который его читает. Им оказывается movsx ebp, byte ptr [esi+eax]. Сразу видно, что сделано со вкусом и размахом. Судя по тому, что идущие подряд команды модифицируют один регистр, оптимизацией тут и не пахнет.

Теперь нам необходимо вникнуть в суть данного фрагмента кода и написать аналогичный для своего генератора. Обратите внимание на цикл в строке 0x40E2EB. Даже беглого взгляда на код достаточно, чтобы понять, что это простая и нестандартная хеш-функция, возвращающая сразу две свертки! (Видно, стандартное CRC32 автору реализовать было лень, впрочем, в данном случае это не страшно). Пишем собственный код генератора: void KeyGen::GenPoly0(CString Name) { unsigned long int poli0 = 0xADACAFFE; unsigned long int poli1 = 0xAFFEADAC; unsigned long int Len = Name.GetLength(); unsigned long int temp0; unsigned long int temp1; unsigned char transmit; for (unsigned int a=0;a<Len;a++) { transmit=Name.GetAt(a); temp=transmit; temp0=(temp0 << ( a & 0x7)); poli0 = (poli0 ^ temp0); transmit=Name.GetAt(Len-a-1); temp1=transmit; temp1=(temp1 << (a & 0xF )); poli1=(poli1 ^ temp1); } TRACE("%x poly1 = %x \n",poli0,poli1); }

Просматривая код дальше, мы видим два вызова функции 0x41D150, принимающей полученные свертки. Тут надо полагать, что свертки превратятся в развертки, ибо 64 бита ключа автору защиты показалось мало, и он ухитрился обеспечить работой и себя, и нас. Заходим внутрь функции развертки. Автор делит свертку на 0x24 и к остатку прибавляет 0x30, если тот меньше 0х9, и 0х57 в противном случае. И так до тех пор, пока в результате не получится нуль. После чего, преследуя одному Богу понятную цель, переворачивает строку?! Пишем еще немного кода. CString KeyGen::GenSeq(unsigned long int poly) { CString s0=""; unsigned char zzz; while (true) { zzz = (unsigned char) (poly % 0x24); if (zzz<=9) zzz=zzz+0x30; else zzz=zzz+0x57; s0=s0+(char) zzz; if (!(poly = poly / 0x24)) break; } for (int a=0;a<(s0.GetLength()/2);a++) { char c=s0[a]; s0.SetAt(a,s0.GetAt(s0.GetLength()-a-1)); s0.SetAt(s0.GetLength()-a-1,c); } TRACE0(s0); return s0; }

Осталось превратить это в готовый продукт. О вкусах и привычках не спорят, поэтому примите Visual C++ не как руководство к действию, а как возможный вариант. Ничем ни хуже будет сделать то же на ассемблере, Дельфи и даже на Visual Basic.

seo & website usability inet html os faq hardware faq memory video cpu hdd mainboard faq printer & scaner modem mobiles

Windows 10 | Registry Windows 10 | Windows7: Общие настройки | Windows7: Реестр | Windows7: Реестр faq | Windows7: Настроки сети | Windows7: Безопасность | Windows7: Брандмауэр | Windows7: Режим совместимости | Windows7: Пароль администратора |  |  |  |  | Память | SDRAM | DDR2 | DDR3 | Quad Band Memory (QBM) | SRAM | FeRAM | Словарь терминов | Video | nVIDIA faq | ATI faq  | Интегрированное видео faq | TV tuners faq | Терминология | Форматы графических файлов | Работа с цифровым видео(faq) | Кодеки faq | DVD faq | DigitalVideo faq | Video faq (Архив) | CPU | HDD & Flash faq | Как уберечь винчестер | HDD faq | Cable faq | SCSI адаптеры & faq | SSD | Mainboard faq | Printer & Scaner | Благотворительность

На главную | Cookie policy | Sitemap

 ©  2004