Технические переводы

«КНИГА об «IDA PRO»
Неофициальное руководство по самому популярному дизассемблеру в мире».

THE IDA PRO BOOK
The unofficial guide to the world's most popular disassembler

КНИГА об
«IDA PRO»
Неофициальное руководство
ПО САМОМУ ПОПУЛЯРНОМУ ДИЗАССЕМБЛЕРУ В МИРЕ


Содержание главы I
«Введение в дизассемблирование»

ЧТО ВЫ НАЙДЕТЕ В ЭТОЙ КНИГЕ
К ВОПРОСУ ДИЗАССЕМБЛИРОВАНИЯ
Языки первого поколения
Языки второго поколения
Языки третьего поколения
Языки четвертого поколения
ЧТО ПОНИМАЕТСЯ ПОД ДИЗАССЕМБЛИРОВАНИЕМ
Компиляция происходит с потерями
Компиляция имеет много решений
Декомпиляторы крайне зависимы от языка программирования и библиотек
Для точной декомпиляции необходимо наиболее корректное дизассемблирование
ЗАЧЕМ ПРОВОДИТЬ ДИЗАССЕМБЛИРОВАНИЕ
Анализ вредоносных программ
Анализ уязвимостей
Анализ функциональной совместимости программ
Проверка правильности работы компилятора
Отображение информации при отладке
КАК ПРОИСХОДИТ ПРОЦЕСС ДИЗАССЕМБЛИРОВАНИЯ
Базовый алгоритм дизассемблирования
Шаг 1
Шаг 2
Шаг 3
Шаг 4
Дизассемблирование методом линейной развертки
Дизассемблирование методом рекурсивного спуска
Последовательный поток команд
Команды условного ветвления
Команды безусловного ветвления
Команды вызова функции
Команды возврата
ЗАКЛЮЧЕНИЕ
ПРИМЕЧАНИЯ ПЕРЕВОДЧИКА



ВВЕДЕНИЕ В ДИЗАССЕМБЛИРОВАНИЕ

Что Вы найдете в этой книге
Читатель справедливо может задать вопрос: «Что следует ожидать от книги, посвященной «IDA Pro»? Естественно предположить, что в ней содержится «Руководство пользователя» для данной программы, но в действительности это не совсем так. Мы предлагаем рассматривать «IDA Pro», как основной инструмент, иллюстрирующий методы обратного проектирования [«Пр. переводчика» - 1] («reverse engineering»), которые применяются для анализа самых различных программ, начиная с прикладных и заканчивая вредоносными («malware»). В книге вы найдете пошаговые инструкции для решения описываемых задач . Такой практический подход позволяет сформировать наиболее широкое представление о возможностях «IDA Pro». Материал в книге построен от простого к сложному. Сначала рассматриваются базовые задачи, которые выполняются при первичной проверке файла и, затем уже более трудоемкие приемы в т.ч. настройка инструмента для решения основных проблем обратного проектирования. К сожалению, не удастся в этой книге рассмотреть все возможности «IDA Pro». Однако, здесь представлены именно те особенности, которые окажутся наиболее полезны для читателя при решении проблем обратного проектирования. Это позволит рассматривать данный дизассемблер, как наиболее эффективное оружие в Вашем арсенале инструментальных средств.

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

К вопросу дизассемблирования

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

Языки первого поколения
Эти языки представляют собой наиболее простую форму и в целом состоят из единиц и нулей или некоторой системы условных обозначений, например, такой как шестнадцатеричный код, читаемый в основном лишь супер–людьми, вроде Скэйпа (Skape)[«Пр. переводчика» - 2]. На этом уровне все представляется весьма запутанным, потому что часто бывает трудно отличить данные от команд, поскольку они выглядят практически одинаково. Под языками первого поколения по сути понимаются машиннные языки, в некоторых случаях называемые байт-кодом (byte code), а сами программы, представленные на машинном языке часто упоминаются как двоичные (binaries), бинарные или исполняемые.

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

Иногда мнемоники действительно помогают программистам запомнить команды, с которыми они ассоциированы.

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

Языки третьего поколения
Эти языки представляют собой другую ступень приближения к возможностям выражения естественных языков путем введения ключевых слов и конструкций, которые используются программистами как строительные блоки для своих программ. Языки третьего поколения, как правило, являются платформонезависимыми, хотя написанные на них программы могут зависеть от платформы из-за использования возможностей, характерных для специфической операционной системы. Часто встречаемые примеры включают «ФОРТРАН» («FORTRAN»), «КОБОЛ» («COBOL»), «Си» (C) и «Джава» («Java»). В большинстве случаев программисты пользуются компиляторами для перевода своих программ на язык ассемблера или сразу в машинный язык (или же некий его приблизительный эквивалент, такой, как байт-код).

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

Что понимается под дизассемблированием

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

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

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

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

Декомпиляторы крайне зависимы от языка программирования и библиотек
Обработка исполняемого файла, созданного компилятором «Delphi», при помощи декомпилятора, ориентированного на генерацию кода языка «С», может привести к очень необычным результатам. Точно так же предоставление исполняемого файла, откомпилированного для «Windows»-систем, декомпилятору, не способному распознавать Windows API[«Пр. переводчика» - 3], вряд ли приведет к чему-нибудь полезному.

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

Однако, медленно но верно осуществляется прогресс в области декомпиляции. В Главе 23 будет приведено описание «Hex-Rays», – наиболее совершенного декомпилятора на сегодняшнем рынке программного обеспечения.

Зачем проводить дизассемблирование

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

  • 1) анализ вредоносных программ;
  • 2) анализ уязвимостей программного обеспечения, распространяемого без исходных текстов;
  • 3) анализ функциональной совместимости программного обеспечения распространяемого без исходных текстов;
  • 4) анализ сгенерированного компилятором кода для проверки полноты и корректности результата;
  • 5) отображение программных инструкций во время отладки.

В последующих разделах каждая ситуация будет рассмотрена более подробно.

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

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

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

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

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

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

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

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

Как происходит процесс дизассемблирования

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

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

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

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

Шаг 1
Первым шагом в данном процессе является идентификация области кода, который необходимо дизассемблировать. Это не всегда так просто, как может показаться на первый взгляд. Обычно команды перемешаны с данными, и важно их отличить друг от друга. В наиболее распространенном варианте дизассемблированный файл будет соответствовать общим форматам исполняемых файлов, таким, как Portable Executable (PE) формат, используемом в «Windows», или Executable and Linking Format (ELF) [см. раздел «Примечание переводчика» – 5], используемом во многих системах на базе «Unix». Для этих форматов является типичным содержание механизмов (часто в форме иерархических файловых заголовков) для определения участков файла, содержащих код и точку входа в этот код.

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

Шаг 3
Как только машинная команда определена, и все необходимые ей операнды распознаны, выявляется ее эквивалент на языке ассемблера и выводится как часть ассемблерного текста. При этом, возможно придется выбирать из более, чем одного варианта синтаксиса для выводимого языка. Например, существуют два формата, преобладающих над остальными для ассемблера x86-архитектуры и ими являются форматы Intel и AT&T.

АССЕМБЛЕРНЫЙ СИНТАКСИС ДЛЯ Х86: AT&T ПРОТИВ INTEL

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

AT&T синтаксис ассемблера характеризуется использованием символа % в префиксе всех наименований регистров, использованием символа $ в качестве префикса для литерных констант (также называемых непосредственными операндами), и расстановкой операндов, при которой источник находится слева, а принимающий операнд располагается справа. При использовании AT&T синтаксиса команда, сложения числа четыре с содержимым регистра EAX выглядит следующим образом: add $0x4, %eax. GNU ассемблер («Gas») и множество других свободно распространяемых инструментальных средств, включающих компилятор «gcc» и отладчик «gdb», используют AT&T синтаксис.

Синтаксис Intel отличается от AT&T тем, что он не требует никаких префиксов для регистров или литерных констант, и расстановка операндов изменена таким образом, что операнд-источник располагается справа, а принимающий операнд - слева. Та же команда сложения при использовании синтаксиса Intel читалась бы так: add eax, 0x4. К ассемблерам, использующим синтаксис Intel, относятся следующие: «Microsoft Assembler» (MASM), «Borland's Turbo Assembler» (TASM), и «Netwide Assembler» (NASM).

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

В процессе разбора приходится решать различные задачи, в частности, такие как:

  • 1) определение участка файла, с которого необходимо начать процесс дизассемблирования;
  • 2) выбор следующей инструкции, которая должна быть дизассемблирована;
  • 3) отличие кода от данных;
  • 4) определение момента окончания дизассемблирования последней команды и др.

Далее мы рассмотрим два широко распространенных подхода к дизассемблированию – метод линейной развертки (linear sweep) и метод рекурсивного спуска (recursive descent).

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

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

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


Рисунок 1 – Демонстрация некорректной обработки кода, методом линейной развертки

Если мы посмотрим на последовательно расположенные 4-х байтные группы в прямом порядке следования байтов (от младшего к старшему), начинающиеся с адреса, отмеченного меткой , то мы увидим, что каждый из них представляет собой указатель на близлежащий адрес, который является фактически пунктом назначения для одного из различных переходов (собственно из кода, показанного на рисунке 1, можно увидеть указатели: 004012e0, 0040128b, 00401290 и т.д.). Таким образом, код операции, интерпретированный, как команда «loopne», вообще не является командой. Это показывает невозможность должным образом отличить внедренные данные от кода при использовании алгоритма линейной развертки.

Линейная развертка используется механизмами дизассемблирования, содержащимися в свободно распространяемом отладчике «gdb», в отладчике фирмы Microsoft «WinDbg», а также в утилите «objdump».

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

Последовательный поток команд
Последовательный поток команд передает выполнение команде, которая непосредственно следует за текущей. В качестве примера можно привести простые арифметические инструкции, такие, как «add» и «sub»; команды передачи данных между регистрами и памятью, например «mov» и «lea»; и операции по работе со стеком такие как «push» и «pop». Для подобных команд процесс дизассемблирования происходит так же, как и при использовании метода линейной развертки.

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

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

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

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

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

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


Рисунок 1 – Пример некорректного листинга, полученного методом рекурсивного спуска

В результате, во время работы программы управление на самом деле не передается команде «add», следующей после вызова функции «foo» ( команда отмечена на рисунке 2 значком ).

Дизассемблирование, выполненное должным образом, показано ниже:


Рисунок 3 – Корректное дизассемблирование кода

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

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

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


Рисунок 4 – Дизассемблированный текст, полученный методом рекурсивного спуска

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

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

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

ПРИМЕЧАНИЯ ПЕРЕВОДЧИКА

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

2) «Skape» - это псевдоним Матта Миллера (Matt Miller).
Целью данного проекта «Metasploit Project» является предоставление полезной информации людям, которые выполняют тестирование систем защиты, разработкой сигнатур для систем обнаружения атак и исследования уязвимостей программного обеспечения. В поддержку проекта был создан сайт http://www.markkit.net/untrusted/www.metasploit.com_.html предназначенный для заполнения дыр в публично доступной информации о различных возможностях эксплуатации тех и для создания полезных ресурсов для разработчиков средств, демонстрирующих уязвимость программных продуктов. Инструменты и информация на этом сайте предоставляются для легальных исследователей безопасности и исключительно в тестовых целях.

3) Windows API (сокр. от application programming interface) - программный интерфейс приложения для ОС семейства «Windows».

4) «Эксплойт (exploit) - [оригинальный жаргон взломщиков]:
1. Некоторая уязвимость в программном обеспечении, которая может быть использована для преодоления системы защиты или, в другом случае, для атаки Интернет-узла через сеть. Известным эксплойтом является «Ping O' Death»

2. С точки зрения грамматики более верно следующее определение – программа, которая использует эксплойт, описанный первой формулировкой». [«The Jargon File (version 4.4.7)»]

«Ping O' Death» («Отправитель пакетов смерти», если «PING» рассматривали авторы, как «Packet Internet Groper» (Отправитель Интернет-пакетов ) или «Свист смерти» при значении слова общепринятом смысле) – «Один печально известный эксплойт, который (когда впервые обнаружен) мог быть легко использован для краха самых разнообразных вычислительных машин, посредством выхода за границы их TCP/IP стека. Впервые был обнаружен в конце 1996 года. Unix-сообщество открытых проектов исправило эти системы , потратив на исключение уязвимости несколько дней или недель, в то время как производителям операционных систем с закрытыми исходными текстами понадобились месяцы. Пока различия во времени реакции повторялись по подобной схеме и с другими инцидентами безопасности, сопутствующее освещение ситуации, подогреваемое публикациями в интернете, оказалось необычно смущающим для производителей операционных систем, а также возникли различные истории и мифы. Термин в настоящее время используется для указания на любые перемещения сетевых пакетов, фрагментированных на небольшие части, которые являются причиной плохих вещей, способных случиться в системе, выбранную для «толчка локтем». Полную же историю про данный эксплойт можно прочитать здесь http://insecure.org/sploits/ping-o-death.html » [«The Jargon File (version 4.4.7) »]

5) Portable Executable File Format – переносимый и выполняемый формат файла
Executable and Linking File Format – исполняемый и скомпонованный формат файла.