Дизассемблеры, декомпиляторы и отладчики
Подготовка к анализу загрузочного файла – тема отдельного разговора. Отладчики – это программные средства, предназначенные для контроля выполнения программ. Отладчики позволяют приостановить выполнение программы в некоторой точке, изменить значение переменных и даже, в некоторых случаях, внести изменения в машинный код программы на лету в процессе ее выполнения. К сожалению, возможность выполнения отладчиком подобных действий зависит от включения в выполнимый код отладочной информации, прежде всего таблицы соответствия символов (для большинства загрузочных программ это не выполняется). Если отладочной информации в выполнимом коде нет, то отладчик может выполнить некоторые функции, хотя большую часть работы по отладке программ приходится выполнять вручную, например при указании точек прерывания вместо имен приходится задавать адреса памяти.
Декомпилятор (или дизассемблер) – программа, которая преобразует двоичный код программ в исходный текст, написанный на одном из языков программирования, чаще всего – ассемблере. Некоторые дизассемблеры могут представить исходный текст на простом языке C. В процессе трансляции большая часть информации об исходном тексте программы теряется, например имена переменных, поэтому декомпилятор пытается восстановить исходный текст программы настолько, насколько это возможно. Если при декомпиляции таблица соответствия имен была не найдена, то зачастую декомпилятор присваивает переменным имена, составленные из плохо воспринимаемой последовательности цифр и букв.
Проблема несколько упрощается, если исследователь в состоянии разобраться с ассемблерным кодом, генерируемым декомпилятором. В этом случае декомпилятор особенно полезен. Рассмотрим пример результатов работы декомпилятора.
Среди коммерческих декомпиляторов для Windows хорошая репутация у IDA Pro компании DataRescue (пример работы декомпилятора показан на рис. 4.1). IDA Pro может декомпилировать программный код многих процессоров, включая виртуальную машину Java.

Рис. 4.1. Пример работы IDA Pro
На рисунке показан пример применения декомпилятора IDA Pro для дизассемблирования программы pbrush.exe (Paintbrush). IDA Pro нашел секцию внешних функций, используемых программой pbrush.exe. Если программа выполняется под управлением операционной системы, которая поддерживает разделяемые библиотеки (например, под управлением операционных систем Windows или UNIX), то она содержит список необходимых ей библиотек. Обычно этот список представлен в удобочитаемом виде, который легко обнаружить при экспертизе выполняемого кода. Для выполнения программ операционной системе также требуется этот список, поэтому она загружает его в память. В большинстве случаев это позволяет декомпилятору вставить список в двоичный код программы, сделав его более понятным.
Чаще всего таблица соответствия имен pbrush.exe недоступна, поэтому в большей части сгенерированного декомпилятором ассемблерного кода отсутствуют имена.
Оценочную версию IDA Pro, пригодную для первоначального знакомства с программой, можно загрузить с www.datarescue.com/idabase/ida.htm. SoftICE компании Numega – другой популярный отладчик. Дополнительные сведения о нем можно найти по адресу www.compuware.com/products/numega/drivercentral/.
Для сравнения была написана небольшая программа на языке C (классическая небольшая программа, выводящая строку «Hello World»). Для отладки использовался отладчик GNU (GDB). Код программы представлен ниже:
printf (“Hello World ”);
Программа была скомпилирована с включением отладочной информации (был включен переключатель —g):
[elliptic@]$ gcc -g hello.c -o hello
Пример протокола отладки под управлением GDB показан ниже:
[elliptic@ellipse]$ gdb hello
GNU gdb 19991004
Copyright 1998 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public
License, and you are welcome to change it and/or
distribute copies of it under certain conditions.
Type “show copying” to see the conditions.
There is absolutely no warranty for GDB. Type “show
warranty” for details.
This GDB was configured as “i386-redhat-linux”.
Была установлена точка прерывания при входе в функцию main. При ее достижении выполнение программы было приостановлено для выполнения программистом действий по отладке программы. Контрольная точка была установлена до выдачи команды run.
Breakpoint 1 at 0x80483d3: file hello.c, line 5.
Команда run начинает выполнение программы под управлением отладчика.
Starting program: /home/ryan/hello
Breakpoint 1, main () at hello.c:5
5 printf (“Hello World ”);
После того достижения программой точки прерывания и начала сессии отладки была выдана команда disassemble, позволяющая вывести дополнительную отладочную информацию о программе.
Dump of assembler code for function main:
0x80483d0 : push %ebp
0x80483d1 : mov %esp,%ebp
0x80483d3 : push $0x8048440
0x80483d8 : call 0x8048308
0x80483dd : add $0x4,%esp
0x80483e0 : xor %eax,%eax
0x80483e2 : jmp 0x80483e4
End of assembler dump.
Протокол отображает ассемблерный код программы «hello world», соответствующий ассемблеру x86 Linux. Исследование собственных программ в отладчике – хороший способ изучения листингов ассемблерного кода.
printf (format=0x8048440 “Hello World ”) at printf.c:30
printf.c: No such file or directory.
После задания командой s («step») режима пошагового выполнения программы в отладчике выполняется переход к вызову функции printf. GDB сообщает о невозможности дальнейшей детализации функции printf из-за отсутствия в распоряжении отладчика исходного кода функции printf.
Далее выполняется несколько шагов реализации программы внутри функции printf и программа завершается. По команде c («continue») программа будет выполнена до следующей точки прерывания или будет завершена.
Program exited normally.
В число аналогичных программ входят программы nm и objdump пакета GNU. Программа objdump позволяет управлять объектными файлами. Она применяется для отображения символов объектного файла, его заголовка и даже дизассемблирования. Nm выполняет аналогичные действия, что и objdump, позволяя просматривать символьные ссылки на объектные файлы.
Инструментарий и ловушки
Инструментарий никогда не заменит знаний
Некоторые из средств дизассемблирования и отладки предлагают фантастические возможности. Но они, как и любой другой инструментарий, несовершенны. Особенно это проявляется при анализе злонамеренного кода (вирусов, червей, Троянских коней) или специально разработанных программ. Обычно авторы подобных поделок делают все возможное для затруднения их анализа и снижения эффекта от применения отладчиков, дизассемблеров и других подобных инструментальных средств. Например, вирус RST для Linux в случае обнаружения, что он работает под управлением отладчика, завершает свою работу. Этот же вирус при инфицировании исполняемых и компонуемых файлов ELF (Executable and Linkable Format – формат исполняемых и компонуемых модулей) модифицирует их заголовки таким образом, что некоторые дизассемблеры не могут дизассемблировать двоичный код вируса (обычно это достигается путем размещения кода вируса в необъявленном программном сегменте). Часто злоумышленный код шифруется или сжимается, что защищает его от экспертизы. Черви Code Red распространялись половинками своих программ, маскируясь под строки переполнения, и, таким образом, формат их двоичных данных не подходил ни под один из стандартных заголовков файла.
Поэтому при анализе двоичных данных нужно знать не только о том, какие инструментальные средства и как применять, но и как обойтись без них. Анализируя заголовок файла, возможно, потребуется определить, какие данные в файле модифицированы, как и с какой целью. Вероятно, потребуется проанализировать зашифрованные данные и процедуру их расшифровки, восстановить алгоритм работы программы и выяснить результаты ее работы.
Необходимо знать ассемблер не только в объеме, достаточном для чтения ассемблерных программ, но и для написания программ расшифровки или восстановления данных. Писать на ассемблере намного сложнее, чем читать программы, написанные на этом языке.
Из этого не следует, что инструментальные средства бесполезны. Далеко не так. Нужно только выявить часть программы, оказавшуюся не по зубам инструментальным средствам, и проанализировать ее самостоятельно, а остальную часть программы – при помощи инструментальных средств. Кроме того, иногда для лучшего понимания логики работы программы следует воспользоваться инструментарием.
Данный текст является ознакомительным фрагментом.
Чем отличается декомпиляция от дизассемблирования
Аннотация. В статье даётся краткое введение в проблематику задачи декомпиляции программ как одной из задач обратной инженерии. Рассматриваются возможности и недостатки существующих инструментальных средств декомпиляции программ.
1. Введение
В настоящее время в комплексном программном обеспечении широко применяются программные приложения, разработанные сторонними производителями. В ряде случаев такие приложения предоставляются без исходного кода на языке высокого уровня, необходимого для их аудита с точки зрения информационной безопасности их использования. Несмотря на это, такие приложения обязательно должны быть исследованы для оценки рисков их использования. Ни бинарный код, ни ассемблерный листинг, полученный в результате дизассемблирования, не позволяют с приемлемыми трудозатратами оценить взаимосвязь элементов программы, а также идентифицировать в программе стандартные алгоритмические конструкции. Восстановление программы на языке высокого уровня дает возможность преодолеть указанные выше трудности. Программные приложения, представленные в виде исполняемых файлов или на языке ассемблера, сложны для анализа их специалистами в области информационной безопасности, криптографии и т.д. и должны быть предоставлены им для анализа на более высоком уровне представления. В качестве одного из инструментальных средств повышения уровня абстракции представления программы может использоваться декомпилятор.
Под декомпилятором мы будем понимать инструментальное средство, получающее на вход программу на языке ассемблера и выдающее на выход эквивалентную ей программу на некотором языке высокого уровня.
Задача декомпиляции была поставлена в 60-е годы XX века сразу же, когда стали широко применяться компиляторы с языков высокого уровня, но не утратила своей актуальности и по сей день [2]. Эта задача не решена в полной мере из-за наличия ряда трудностей принципиального характера. В частности, при компиляции программы из языка высокого уровня в язык ассемблера характерно отображение «многие к одному» концепций языка высокого уровня в концепции языка ассемблера, и, как следствие, однозначное восстановление программы на языке высокого уровня становится зачастую невозможным.
В силу указанных выше причин полностью автоматический декомпилятор реализовать принципиально невозможно. Поэтому системы декомпиляции программ должны работать во взаимодействии с аналитиком, который (зачастую методом проб и ошибок) управляет процессом декомпиляции. В ходе декомпиляции программы решаются следующие задачи: выделение структурных единиц программы, в частности, подпрограмм в однородном ассемблерном листинге, выявление параметров подпрограмм и возвращаемых ими значений, структурный анализ, то есть восстановление операторов циклов, ветвлений и т. п., восстановление типов данных, как базовых, так и производных и другие. Поскольку все эти задачи достаточно трудоемки и алгоритмически неразрешимы, на сегодняшний день нет известных декомпиляторов, восстанавливающих программы в какой-либо язык высокого уровня, которые качественно справлялись бы со всеми перечисленными выше задачами. Для решения задач посредством использования декомпиляторов требуется хорошо представлять возможности используемого инструмента, и для достижения наилучшего результата, возможно, потребуется использовать набор декомпиляторов в некоторой композиции. В данной работе предлагается обзор наиболее известных декомпиляторов в язык Си из бинарных файлов, рассматривается набор тестов, на основе которого можно сделать сравнительный анализ работоспособности декомпиляторов, и выполняется этот анализ.
В данной работе в качестве процессорной архитектуры, с которой ведётся декомпиляция, выбрана архитектура Intel i386, наиболее распространённая в настоящее время. В листингах фрагментов программ на языке ассемблера используется синтаксис AT&T [3].
Предлагаемая работа имеет следующую структуру. Поскольку и в литературе, и на практике зачастую смешиваются понятия дизассемблирования программы и декомпиляции программы, уместно рассмотреть различия этих задач. Этому посвящен второй раздел статьи. В третьем разделе статьи дается описание основных подзадач декомпиляции с описанием возникающих трудностей при их решении. В четвертом разделе приводится обзор языка Си с точки зрения обратной инженерии. Пятый раздел посвящен описанию существующих декомпиляторов для языка Си. В пятом разделе представлены результаты сравнительного тестирования декомпиляторов на разработанном наборе тестовых примеров. В заключении сформулированы выводы работы и направления дальнейших исследований.
2. Декомпиляция и дизассемблирование
Рассмотрим независимо друг от друга задачу дизассемблирования и задачу декомпиляции программ. Под декомпиляцией понимается построение программы на языке высокого уровня, эквивалентной исходной программе на языке низкого уровня (языке ассемблера). Под дизассемблированием понимается построение программы на языке ассемблера, эквивалентной исходной программе в машинном коде. Программа в машинном коде представляется либо в виде исполняемого модуля в стандартном для целевой операционной системы формате (например, для Win32 в формате PE [16], а для Linux – в формате ELF [15]), либо в виде дампа содержимого памяти, либо в виде трассы исполнения программы.
Традиционно декомпиляция рассматривается в более широком смысле, а именно, как построение программы на языке высокого уровня по программе в машинном коде. Очевидно, что в такой постановке задача декомпиляции поглощает задачу дизассемблирования. Такое «широкое» понимание декомпиляции излишне, поскольку дизассемблирование и декомпиляция решают разные по сути задачи, хотя и используют схожие методы (в частности, построение графа потока управления и исполняемого покрытия программы). Так, при дизассемблировании выполняется трансляция исполняемого файла, представляемого в виде набора машинных команд, в программу на языке ассемблера. При декомпиляции программа с представления низкого уровня транслируется в представление высокого уровня. Дальнейшим этапом повышения уровня абстракции программы может быть рефакторинг, посредством которого из программы на языке Си можно, например, получить программу на языке Си++.
Рассмотрим разбиение задач декомпиляции и дизассемблирования на подзадачи. Так, при дизассемблировании требуется решать следующие основные задачи:
- Разделение кода и данных . Для каждой ячейки программы (или ячейки памяти дампа) должно быть установлено, хранит ли ячейка исполняемые инструкции или данные. Задача эта сама по себе алгоритмически неразрешима [5] и не всегда может быть решена однозначно (например, в случае самомодифицирующегося кода, динамически подгружаемого кода и т. п.).
- Замена абсолютных адресов на символические.
При декомпиляции должны быть решены следующие основные задачи:
- Выделение функций в потоке инструкций.
- Выявление параметров и возвращаемых значений.
- Восстановление структурных конструкций языка высокого уровня.
- Замена всех обращений к памяти на конструкции языка высокого уровня (в частности, сюда входит идентификация обращения к локальным переменным и параметрам и их замена на символические имена, идентификация обращений к массивам и их замена на операции с массивами и т. д.).
- Восстановление типов объектов языка высокого уровня, выявленных на предыдущем шаге.
В дальнейшем мы будем рассматривать задачу декомпиляции в узкой постановке, то есть как задачу трансляции программы, представленной на языке низкого уровня, в частности, на языке ассемблера, в программу на языке высокого уровня, в частности, на Си.
3. Обзор основных подзадач декомпиляции
Рассмотрим основные задачи декомпиляции и подходы к их решению.
3.1. Выделение функций
Одной из основных структурных единиц программ на языке Си являются функции, которые могут принимать параметры и возвращать значения. Откомпилированная программа, однако, состоит из потока инструкций, функции в котором никак структурно не выделяются. Как правило, компиляторы генерируют код с одной точкой входа в функцию и одной точкой выхода из функции. При этом в начало кода, генерируемого для функции, помещается последовательность машинных инструкций, называемая прологом функции, а в конец кода – эпилог функции. И прологи, и эпилоги функций, как правило, стандартны для каждой архитектуры и лишь незначительно варьируются. Например, стандартный пролог и эпилог функции для архитектуры i386 показаны ниже:
pushl %ebp movl %esp, %ebp
movl %ebp, %esp popl %ebp ret
Прологи и эпилоги функций могут быть легко выделены в потоке инструкций. Кроме того, при работе с трассами можно считать, что инструкции, на которые управление передается с помощью инструкции call , являются точками входа в функции, а инструкции ret завершают функции. Возникает соблазн считать инструкции, расположенные между прологом и эпилогом, или между точками входа и выходом, телом функции, однако в этом случае можно натолкнуться на ряд сложностей. Во-первых, при компиляции программы могут быть указаны опции, влияющие на форму пролога и эпилога функции. Например, опция компилятора GCC –fomit-frame-pointer подавляет использование регистра %ebp в качестве указателя на текущий стековый кадр, когда это возможно. В этом случае пролог и эпилог функции будут, как таковые, отсутствовать. Во-вторых, отдельные оптимизационные преобразования могут разрушать исходную структуру функций программы. Очевидным примером такого оптимизационного преобразования является встраивание тела функции в точку вызова. Встроенная функция не существует как отдельная структурная единица программы, и ее автоматическое выделение представляется затруднительным.
Существуют оптимизирующие преобразования, которые приводят к появлению в машинном коде конструкций, принципиально невозможных в языках высокого уровня. Таким оптимизирующим преобразованием является, например, sibling call optimization. Если список параметров двух функций идентичен, и первая функция вызывает вторую с этими параметрами, то инструкция вызова подпрограммы call может быть преобразована в инструкцию безусловного перехода jmp в середину тела второй функции. Результатом такого рода «неструктурных» оптимизаций будет появление переходов из одной функции в другую, появление функций с несколькими точками входа или несколькими точками выхода. Другим источником «неструктурных» конструкций в машинной программе являются операторы обработки исключений в таких языках, как Си++.
Таким образом, хотя в типичном случае компилятор генерирует хорошо структурированный код, поддающийся разбиению на функции, достаточно легко может быть получен и «неструктурированный» код. Следует отметить, что в этом случае влияние программиста, пишущего программу на языке Си, на структуру генерируемого кода ограничено возможностями языка Си, не позволяющего бесконтрольной передачи управления между функциями и не поддерживающего механизм исключений. Поэтому можно предполагать, что если восстанавливается программа с языка ассемблера, полученная в резу-льтате компиляции программы на языке Си, то она не содержит «неструк-турных» особенностей, описанных выше, и может быть разбита на функции.
3.2. Выявление параметров и возвращаемых значений
В языках высокого уровня, в частности, Си поддерживается передача параметров в функции и возврат значений. В языке Си существует только передача параметров по значению, в других языках могут поддерживаться и другие механизмы. Заметим, что здесь мы рассматриваем только механизмы передачи параметров, отображаемые в генерируемый машинный код. Передача параметров по имени, передача параметров в шаблоны и другие механизмы периода компиляции программы здесь не рассматриваются.
Способы передачи параметров и возврата значений для каждой платформы специфицированы и являются составной частью так называемого ABI (application binary interface). Под платформой здесь понимается, как обычно, тип процессора и тип операционной системы, например, Win32/i386 или Linux/x86_64. Одной из задач ABI является обеспечение совместимости по вызовам приложений и библиотек, скомпилированных разными компиляторами одного языка или написанных на разных языках.
Так, для платформы win32/i386 используется несколько соглашений о передаче параметров. Соглашение о передаче параметров _cdecl используется по умолчанию в программах на Си и Си++ и имеет следующие особенности [9]:
- Параметры передаются в стеке и заносятся в стек справа налево (то есть первый в списке параметр заносится в стек последним).
- Параметры выравниваются в стеке по границе 4 байт, и адреса всех параметров кратны 4. То есть параметры типа char и short передаются как int , но и дополнительное выравнивание для размещения, например, double не производится.
- Очистку стека производит вызывающая функция.
- Регистры %eax, %ecx, %edx и %st(0) – %st(7) могут свободно использоваться (не должны сохраняться при входе в функцию и восстанавливаться при выходе из нее).
- Регистры %ebx, %esi, %edi, %ebp не должны модифицироваться в результате работы функции.
- Значения целых типов, размер которых не превосходит 32 бит, возвращаются в регистре %eax, 64-битных целых типов – в регистрах %eax и %edx, вещественных типов – в регистре %st(0).
- Если функция возвращает результат структурного типа, то место под возвращаемое значение должно быть зарезервировано вызывающей функцией. Адрес этой области памяти передается как (скрытый) первый параметр.
Отметим, что этот набор правил – это именно соглашения, которые «добровольно» выполняются в сгенерированном коде. Пока речь не заходит об интерфейсе с независимо скомпилированными сторонними модулями, программист может в определенной мере модифицировать эти правила, существенно затрудняя задачу автоматического восстановления функций.
Опять же можно предполагать, что если программа декомпилируется из автоматически полученного ассемблерного кода (либо компилятором, либо дизассемблером), то в ней используются только соглашения о передаче параметров из некоторого предопределенного множества. Причем в одной программе для разных функций не могут использоваться разные соглашения о передаче параметров.
На первом этапе решения задачи выявления параметров функций следует определить следующие особенности вызова функций:
- Используемое соглашение о передаче параметров. Требуется определить, какое соглашение из набора предопределенных соглашений используется в программе.
- Размер области параметров функции. Почти все соглашения о передаче параметров могут быть достаточно надежно идентифицированы по используемым инструкциям. Так, соглашение о передаче параметров stdcall требует, чтобы параметры из стека удалялись вызываемой функцией. Для этого может использоваться единственная инструкция системы команд i386 – ret N , где N – размер удаляемых из стека параметров. Таким образом, использование этой инструкции для возврата из функции указывает как на соглашение о передаче параметров, так и на размер параметров функции.
В случае вызова функции по указателю при статическом анализе нам может быть неизвестен адрес вызываемой функции. В этом случае не представляется возможным отследить, как возвращается управление из вызываемой функции. Определение соглашения о вызовах тогда должно быть отложено на фазы последующего анализа.
Итак, на фазе выявления параметров и возвращаемых значений определяется размер передаваемых в функцию параметров и способ возврата значения из функции. В дальнейшем эта информация используется как начальная при восстановлении символических имен и восстановлении типов.
3.3. Структурный анализ
Одним из результатов предыдущих фаз анализа ассемблерного листинга программы является разбиение потока инструкций ассемблерного листинга на отдельные функции и выявление точек входа в функции и возврата из функций.
Инструкции ассемблерной программы в функции могут рассматриваться как представление нижнего уровня (Low-level intermediate representation) [12]. В частности, представление низкого уровня отличается от представления высокого уровня (программы на языке Си) отсутствием структурных управляющих конструкций ( if , for и т. п.).
Для восстановления управляющих конструкций сначала строится граф потока управления программы. По графу потока управления строится дерево доминаторов, затем дуги графа потока управления классифицируются на «прямые», «обратные» и «косые».
На основании этой информации уже можно выполнять непосредственно структурный анализ, то есть восстановление высокоуровневых управляющих конструкций [6]. Поиском в глубину в графе выделяются шаблоны основных структурных конструкций, которые затем организуются в иерархическую структуру.
3.4. Восстановление типов
Задача автоматического восстановления типов данных на настоящее время – одна из задач в области декомпиляции, наименее проработанных с теоретической точки зрения. Ее можно условно разделить на подзадачу восстановления базовых типов данных языка, таких как char , unsigned long и т. п., и на подзадачу восстановления производных типов, таких как типы структур, массивов и указателей. В работе [13] рассматривается восстановление как базовых, так и производных типов при декомпиляции, однако этот подход имеет ряд существенных недостатков, и отсутствует его практическая реализация. В работе [4] описан подход к автоматическому восстановлению производных типов языка по исполняемому файлу. Такой подход используется для анализа на уязвимость программ в виде исполняемых файлов и поэтому не применим напрямую к задаче восстановления типов при декомпиляции.
На практике же все декомпиляторы, кроме Hex-Rays, вообще не восстанавливают даже базовые типы переменных, а в выражениях используют явное приведение типов, что делает восстановленные выражения сложными для понимания и модификации.
4. Языки высокого уровня с точки зрения обратной инженерии
Языки высокого уровня позволяют повысить уровень абстракции представления реализуемого алгоритма, избавляя программиста от необходимости заботиться о низкоуровневых деталях. Эти языки соперничают друг с другом по простоте использования и гибкости, а разработчики компиляторов соперничают по производительности сгенерированного ими кода. Следовательно, имеется большое количество разнообразных языков высокого уровня, и для каждого из них существует множество компиляторов.
При восстановлении программ по программе на языке низкого уровня, имея широкое представление о языке высокого уровня, нужно с достаточной точностью восстановить то, что было написано на языке высокого уровня в исходном тексте программы. Точность и трудозатраты восстановления программы сильно зависят от языка высокого уровня, на котором была написана исходная программа.
Язык Си формально считается языком высокого уровня, однако в нем присутствует много черт языка низкого уровня. В частности, в языке Си поддерживается прямой доступ к памяти и работа с указателями. При обращении к элементам массива не контролируется выход за его пределы, то есть возможен доступ к областям памяти, не имеющим никакого отношения к массиву. С другой стороны, в языке Си поддерживаются такие высокоуровневые конструкции, как производные типы данных: массивы, структуры, объединения, а также условные операторы, циклы и т. д.
На практике особую значимость имеют декомпиляторы, транслирующие ассемблерный листинг в язык Си. Во-первых, восстанавливать программы, написанные изначально на языке Си, удобно, потому что это процедурный язык и у него много низкоуровневых особенностей. Во-вторых, язык Си широко применяется в промышленном программировании, и большое количество системных приложений написано именно на языке Си. С другой стороны, восстанавливать программу из ассемблера в объектно-ориентированный язык принципиально сложнее, да и к тому же программа, реализованная на основе процедурной парадигмы программирования, может быть переведена в объектно-ориентированную программу посредством рефакторинга ее кода. Следовательно, в данной работе ограничим множество рассматриваемых декомпиляторов теми, которые восстанавливают на языке Си программы, представленные либо на языке ассемблера, либо в виде исполняемых файлов.
5. Декомпиляторы в язык Си
В данном разделе дается краткое описание существующих на сегодняшний момент декомпилятров в язык Си. Это – декомпиляторы Boomerang [5], DCC [8], REC [14] и плагин Hex-Rays [10] к дизассемблеру IdaPro [11]. Все рассматриваемые декомпиляторы, кроме плагина Hex-Rays, на вход принимают исполняемый файл, и выдают программу на языке Си. В том случае, когда декомпилятор оказывается не в состоянии восстановить некоторый фрагмент исходной программы на языке Си, этот фрагмент сохраняется в виде ассемблерной вставки. Надо заметить, что даже небольшие исходные программы после декомпиляции зачастую содержат очень много ассемблерных вставок, что практически сводит на нет эффект от декомпиляции.
В отличие от этого, плагин Hex-Rays принимает на вход программу, являющуюся результатом работы дизассемблера Ida Pro, то есть схему программы на ассемблеро-подобном языке программирования. В качестве результата Hex-Rays выдает восстановленную программу в виде схемы на Си-подобном языке программирования. Тем не менее, для простоты мы в дальнейшем объединим процесс дизассемблирования с использованием Ida Pro и последующей декомпиляции.
5.1. Boomerang
Декомпилятор Boomerang [5] является программным обеспечением с открытым исходным кодом (open source). Разработка этого декомпилятора активно началась в 2002 году, но сейчас проект развивается достаточно вяло. Изначально задачей проекта была разработка такого декомпилятора, который восстанавливает исходный код из исполняемых файлов, вне зависимости от того, с использованием какого компилятора и с какими опциями исполняемый файл был получен. Для этого в качестве внутреннего представления было решено использовать представление программы со статическими одиночными присваиваниями (SSA). Однако, несмотря на поставленную цель, в результате декомпилятор не сильно адаптирован под различные компиляторы и чувствителен к применению различных опций, в частности, опций оптимизации. Еще одной особенностью, затрудняющей использование декомпилятора Boomerang, является то, что в нем не поддерживается распознавание стандартных функций библиотеки Си.
5.2. DCC
Проект по разработке этого декомпилятора [8] был открыт в 1991 году и закрыт в 1994 году с получением главным разработчиком степени PhD. В качестве входных данных декомпилятор DCC принимает 16-битные исполняемые файлы в формате DOS EXE. Алгоритмы декомпиляции, реализованные в этом декомпиляторе, основаны на теории графов (анализ потока данных и потока управления). Для распознавания библиотечных функций используется сигнатурный поиск, для которого была разработана библиотека сигнатур. Однако надо заметить, что, несмотря на это, декомпилятор плохо справляется с выявлением функций стандартной библиотеки.
Реверс-инжиниринг: правовое регулирование
При разработке нового программного обеспечения (далее – «ПО») и, в частности, мобильных приложений иногда требуется прибегать к таким техническим приемам, как декомпиляция и дизассемблирование существующих программ. Эти приемы представляют собой преобразование объектного кода уже существующих программ в понятный для разработчика язык (исходный текст) с целью последующего его изучения. Но насколько правомерны такие действия? Какие условия необходимо соблюсти, чтобы изучить программу для ЭВМ, не нарушая прав правообладателя?
Основные термины статьи:
Реверс-инжиниринг (реинжиниринг; англ. reverse engineering) — исследование некоторого устройства или программы, а также документации на них с целью понять принцип его (ее) работы и, чаще всего, воспроизвести устройство, программу или иной объект с аналогичными функциями, но без копирования как такового. Процесс реверс-инжиниринга включает в себя декомпиляцию, дизассемблирование и др. [1]
Декомпиляция – технический прием, посредством которого происходит преобразование объектного кода программы в исходный текст.
Законодательное регулирование реверс-инжиниринга
Критерии правомерности осуществления декомпиляции содержатся в ст. 1280 Гражданского Кодекса РФ. Согласно данной норме проведение реинжиниринга возможно при соблюдении следующих условий:
1) Лицо, осуществляющее декомпиляцию, должно владеть ПО на законных основаниях.
Приобрести ПО, получить разрешение на его использование – это первый шаг к правомерному реверс-инжинирингу. Приобретение «пиратской» версии ПО и последующее его декомпилирование является правонарушением согласно ст. 7.12 Кодекса об административных правонарушениях РФ (КоАП РФ). Также правонарушением является использование для декомпиляции взломанных или иных испорченных версий ПО. Это подтверждает Постановление Пленума Верховного Суда РФ N 5, Пленума ВАС РФ N 29 от 26.03.2009: «Судам следует учитывать, что право совершения в отношении программы для ЭВМ или базы данных действий, предусмотренных статьей 1280 ГК РФ, принадлежит только лицу, правомерно владеющему экземпляром такой программы для ЭВМ или базы данных (пользователю)».
Правомерность владения экземпляром ПО определяется наличием у лица оснований владения, установленных договором (например, приобретение экземпляра ПО по договору купли-продажи).
2) Единственно возможная легальная цель декомпиляции – достижение способности к взаимодействию независимо разработанной лицом программы для ЭВМ с другими программами, которые, в том числе, могут взаимодействовать с декомпилируемой программой.
Изучение программы конкурента с целью создания собственной аналогичной программы, по общему правилу, будет считаться незаконным. Также попытка устранить найденные программные ошибки самостоятельно, то есть без привлечения правообладателя, может привести к появлению ответственности
Постановление Федерального Арбитражного Суда Северо-Западного округа от 07 июня 2013 года по делу N А13-6254/2012): «Исследовав и оценив представленные лицами, участвующими в деле, доказательства по правилам статей 65 и 71 АПК РФ, в том числе заключенные Обществом в 2010-2011 годах договоры на оказание услуг по сопровождению программного обеспечения , судебные инстанции установили, что внесение изменений в программное обеспечение, исправление выявленных ошибок в программном обеспечении без согласия правообладателя приведет к нарушению исключительного права на результат интеллектуальной деятельности».
3) Информация, необходимая для достижения способности программы для ЭВМ к взаимодействию, ранее не была доступна лицу, осуществляющему декомпиляцию, из других источников.
Данные об исходном тексте программы могут находиться в открытом доступе и в этом случае нет необходимости в проведении декомпиляции. Под «другими источниками» понимаются официальный интернет-сайт компании-разработчика или руководство к пользованию программой и прочее.
4) Декомпиляция не должна противоречить обычному использованию программы для ЭВМ или базы данных и не должна ущемлять необоснованным образом законные интересы автора или иного правообладателя.
На практике факт ущемления законных интересов автора или иного правообладателя устанавливается с учетом таких факторов как цель декомпиляции, конкурирование лица, осуществляющего декомпиляцию, с правообладателем, доля дохода, которая может быть утрачена правообладателем из-за действий пользователя и т.п.
Считается, что вред законным интересам правообладателя достигает необоснованного уровня, если декомпиляция его программы причиняет или имеет возможность причинить неразумные потери доходам правообладателя.
Разрешенная декомпиляция на практике
В пользовательских соглашениях / договорах разработчики ПО часто устанавливают запрет на реверс-инжиниринг. Многие правообладатели хотят ограничить действия пользователя в части исследования полученной по лицензии программы, чтобы избежать копирования кода. Приведем типичный пример подобного запрета, взятый из реального лицензионного соглашения: «4.1. За исключением использования в объёмах и способами, прямо предусмотренными настоящей Лицензией или законодательством РФ, Пользователь не имеет права изменять, декомпилировать, дизассемблировать, дешифровать и производить иные действия с объектным кодом и исходным текстом Программы, имеющие целью получение информации о реализации алгоритмов, используемых в Программе, создавать производные произведения с использованием Программы, а также осуществлять (разрешать осуществлять) иное использование Программы, любых компонентов Программы, хранимых Программой на мобильном устройстве Пользователя картографических материалов, иных изображений и прочих данных, без письменного согласия Правообладателя».
Такие формулировки о запрете реверс-инжиниринга – это юридическая защита правообладателя от действий пользователя по исследованию программного продукта с закрытым исходным кодом с целью его копирования, то есть с целью нарушения прав правообладателя. Гарантией соблюдения пользователем такого запрета является перспектива наступления для пользователя различных негативных последствий в случае его несоблюдения (начиная от досрочного прекращения лицензии и заканчивая взысканием с пользователя понесенных правообладателем убытков, включая упущенную выгоду, или денежной компенсации за нарушение авторских прав).
Проведем анализ судебной практики с целью получения ответа на вопрос: возможна ли декомпиляция в рамках, установленных статьей 1280 ГК РФ, в случаях, когда лицензионное соглашение запрещает реверс-инжиниринг вообще и декомпиляцию, в частности?
Постановление Одиннадцатого арбитражного апелляционного суда от 25 октября 2012 года по делу № А55-13189/2012: «Заключение лицензионного договора означает, что пользователь программы вправе совершить в отношении нее действия, предусмотренные ст. 1280 ГК РФ, а также иные действия, обусловленные договором и связанные с эксплуатацией программы. На этот договор в отличие от иных лицензионных соглашений не распространяются правила, установленные пунктами 2-6 ст. 1235 ГК РФ».
Лицензионным договором (пользовательским соглашением) нельзя ограничить лицо в праве осуществлять те действия в отношении ПО, которые предусмотрены статьей 1280 ГК РФ (в том числе и декомпиляцию). Следовательно, положения лицензионных договоров, ограничивающие права лиц на реверс-инжиниринг, не должны признаваться законными.
Нормы законодательства не дают точный ответ на вопрос, должно ли лицо, имеющее интерес в декомпиляции ПО, осуществлять такую декомпиляцию только лично или для этого можно привлечь третьих лиц?
Прямого запрета на осуществление реинжиниринга одним лицом в интересах другого не содержится в нормах Гражданского Кодекса. По смыслу ст. 1274 ГК РФ и п. 2 ст. 1280 ГК РФ исследование программы для ЭВМ, как и любого другого объекта исключительных прав, может быть проведено как пользователем самостоятельно, так и любым иным лицом, обладающим специальными знаниями, но в интересах пользователя, с его ведома и по его согласию.[2] Это подтверждает Решение Арбитражного суда города Москвы от 29 мая 2013 г. по делу № А40-10750/2013: «По смыслу ст. 1274 ГК РФ и п. 2 ст. 1280 ГК РФ исследование программы для ЭВМ, как и любого другого объекта исключительных прав, может быть проведено как пользователем самостоятельно, так и любым иным лицом, обладающим специальными знаниями, но в интересах пользователя, с его ведома и по его согласию. Это объясняется тем, что само по себе исследование в силу ст. 1270 ГК РФ не указано в качестве способа использования объекта исключительных прав и не предполагает его возмездное отчуждение или иное введение в оборот».
На настоящий момент судебная практика по вопросу реверс-инжиниринга в России не так обширна, как в странах Запада, где есть громкие и известные судебные прецеденты (например, дело Blizzard v. BnetD [3] и др.). Суды США в большинстве судебных решений отмечали, что реверс-инжиниринг был добросовестным и не нарушающим права правообладателя ПО. Например, в деле Sega Enterprises v. Accolade [4] компания Sega подала иск против издателя игр (Accolade) после того, как издатель произвел реверс-инжиниринг консоли Sega в целях создания совместимых с нею игр. Благодаря реверс-инжинирингу Accolade получила информацию об объектном коде, которая позже была собрана в письменное руководство, которое использовали программисты для создания своих игр. Код самой компании Sega при этом не был скопирован в игры от Accolade — код Accolade был полностью оригинальным. Такое «промежуточное» копирование было признано судом законным.
- Реверс-инжиниринг будет неправомерен в случае, если владение программой/мобильным приложением, которые подвергаются реверс-инжинирингу, незаконно. Данный вывод подтверждает практика Верховного Суда РФ. За нарушение прав правообладателя ПО возможно привлечение к административной ответственности.
- Декомпиляция ПО, проводимая в соответствии с правилами, предусмотренными ст. 1280 ГК РФ, правомерна, даже если это нарушает установленный лицензионным соглашением с правообладателем такого ПО запрет на осуществление декомпиляции. Свободный реинжиниринг должен соответствовать одной из целей его проведения, разрешенных статьей 1280 ГК РФ, например, цели достижения способности взаимодействия ПО, разработанного одним лицом, с ПО других лиц.
- Неправомерно копирование исходного кода декомпилируемой программы в создаваемую (новую) программу, поскольку такое копирование нарушает авторские права правообладателя декомпилируемой программы. Отметим, что копирование может включать в себя одновременно как имитацию нефункциональных элементов, так и буквальное копирование.
- Декомпиляция в законных целях может проводиться как самим лицом, которому необходима такая декомпиляция, так и привлеченными им специалистами, если они действуют исключительно в интересах привлекшего их лица.
- Российское законодательство и российская судебная практика по вопросу реверс-инжиниринга не дают ответы на все вопросы, которые возникают у лиц, имеющих интерес в его проведении.
Александр Нечаев, Анна Андрусова
Практическая работа №4 Защита программ от дизассемблирования.
Дизассе́мблер — транслятор, преобразующий машинный код, объектный файл или библиотечные модули в текст программы на языке ассемблера.
По режиму работы с пользователем делятся на
Примером автоматических дизассемблеров может служить Sourcer. Такие дизассемблеры генерируют готовый листинг, который можно затем править в текстовом редакторе. Пример интерактивного — IDA. Он позволяет изменять правила дизассемблирования и является весьма удобным инструментом для исследования программ.
Дизассемблеры бывают однопроходные и многопроходные. Основная трудность при работе дизассемблера — отличить данные от машинного кода, поэтому на первых проходах автоматически или интерактивно собирается информация о границах процедур и функций, а на последнем проходе формируется итоговый листинг. Интерактивность позволяет улучшить этот процесс, так как просматривая дамп дизассемблируемой области памяти, программист может сразу выделить строковые константы, дать содержательные имена известным точкам входа, прокомментировать разобранные им фрагменты программы.
Чаще всего дизассемблер используют для анализа программы (или ее части), исходный текст которой неизвестен — с целью модификации, копирования или взлома. Реже — для поиска ошибок (багов) в программах и компиляторах, а также для анализа оптимизации создаваемого компилятором машинного кода. Обычно однопроходный дизассемблер (как и построчный ассемблер) является составной частью отладчика.
Защита от дизассемблирования
Первое направление защиты, как правило, реализуется значительно легче, чем второе, поэтому будет приведен лишь краткий обзор данного направления. При реализации защиты программ от дизассемблирования можно применять различные приемы.
Среди них наиболее часто используемым и эффективным приемом является зашифровка и \ или запаковка отдельных участков исходного кода или всего кода целиком, при этом необходимо позаботиться о распаковке \ расшифровке программы на точке входа. Таким образом, при просмотре исполняемого машинного кода исполняемого файла вместо рабочего кода программы будет отображен лишь бессмысленный набор операций. При реализации защиты от дизассемблирования используется также множество приемов, которые реализуются с целью запутать потенциального взломщика. Можно навести несколько примеров такого вида приемов:
— увеличение исходного кода программы добавлением множества «бессмысленных» операций, а рабочий участок программы записать в определенное место этого множества;
— замена местами адресов обработчиков (векторов) прерываний, например, поменять местами вектор прерывания видео сервиса (INT 10h) с вектором прерывания сервиса DOS (INT 21h), после такой замены для вызова из программы какой-либо функции прерывания INT 21h необходимо пользоваться вызовом прерывания INT 10h.
Для достижения наиболее надежной и эффективной защиты используется комбинация нескольких приемов.
Защита от отладки
Для защиты программы от трассировки отладчиком также существует несколько способов. Наиболее распространенными являются два из них.
При трассировке программы команды выполняются по команде человека, поэтому длительность выполнения операций(время от начала одной операции до начала следующей) изменяется. Поэтому в программу можно включать точки для проверки времени выполнения одинаковых участков кода программы. Если время выполнения выполнения одинаковых участков различна, то это означает, что программа трассируется в данный момент, необходимо выйти из программы, иначе — продолжить выполнение.
Алгоритм реализации:
1. Запомнить текущее время;
2. Выполнить контрольный участок кода;
3. Запомнить текущее время и разность текущего и предыдущего запомненного времени;
4. Выполнить контрольный участок кода повторно;
5. Сравнить разность текущего времени и предыдущего запомненного текущего времени с предыдущей запомненной разностью;
6. Если разности совпадают, продолжить выполнение, иначе – выйти из программы.
— метаморфическое преобразование кода программы, позволяющее защитить программу от дизассемблирования и модификации;
— защита ключом отдельных участков кода программы (поддерживается только в зарегистрированной версии);
— полное разрушение логики защищенных фрагментов кода, не позволяющее анализировать программу с помощью дизассемблера или отладчика;
— обнаружение и противодействие отладчикам SoftIce, NtIce, TD и др.;
— защита точки входа;
— защита от модификации кода;
— защищенная работа с реестром, не позволяющая программам вроде RegMon определить, к какому ключу реестра обращается твоя программа;
— технология «динамического импорта», которая разрушает имена всех импортируемых функций, а также не использует функцию GetProcAddress;
— сжатие ресурсов и исполнимого кода приложения;
— поддержка коротких серийных номеров (12 символов);
— поддержка внешнего генератора серийных номеров с OLE/DLL-интерфейсом;
— технология OneTouch Trial (о ней читай ниже).
Самое главное, что нас интересует – это метаморфическое преобразование кода программы и поддержка серийных номеров. Метаморфическое кодирование позволяет изменить код программы до неузнаваемости и запутать отладчик и человека, который запустил этот отладчик.
Он переводит двоичный код в символьный на языке команд какого-нибудь языка. Например, диассемблеры, деклиппер и многие другие. Эти средства появились раньше отладчиков, т.к. вначале не было архитектуры со встроенными средствами отлаживания программ. С помощью декомпиляторов можно изменять исходный код программы. Допустим необходимо внести крупные изменения в код программы. Прямая вставка двоичных кодов не помогает, т.к. нарушается расположение меток перехода и процедур. Программа – это линейка кода, по которой нужно перемещаться нелинейно, переходить с определенным смещением. Если линейка удлиняется из-за добавления чего-то в середине, все смещения будут показывать не туда куда нужно. Повторная перекомпиляция вписывает новые смещения.
Среди декомпиляторов можно выделить: Hacker-VIEW (HVIEW), IDA (интерактивный дизассемблер).
С помощью Hacker-VIEW можно посмотреть любой исполняемый файл по любому смещению. Можно выполнить какую-то часть программы. Это позволяет расшифровывать программы и обходить защиту от дизассемблирования. Этот декомпилятор «понимает» как старые форматы исполняемых файлов DOS-COM и DOS-EXE, так и форматы исполняемых файлов Windows.
IDA очень мощное средство работы с ассемблерными текстами программ. Обладает широким спектром возможностей, имеет более удобный интерфейс, чем Hacker-VIEW. Очень хорошо предусмотрена архитектура работы программ в Windows (такие вещи, как DLL, расширенный режим работы с памятью и т.д.).
Декомпиляторы программ занимают свое место в инструментарии взломщика. В основном это совместное использование с отладчиками.
Второе средство – отладчики.
Отладчики позволяют запускать отдельные части программы и следить за изменениями, которые она производит, за результатами ее работы.
Защите от отладки не стоит уделять много времени, т.к. все возможные хитрости и приемы уже известны и взломщикам и программистам. Так же и шифрование. Любой хакер, если получает заказ на взлом, имеет доступ к нормальной копии программы. То есть он ее либо может купить, либо попользоваться ею на компьютере покупателя.
Среди отладчиков выделим: SOFTICE и WINICE.
С появлением Windows отладка программ стала на порядок проще и намного удобнее дизассемблирования. Принципиально изменился стиль некоторых атак на защиту программ. Теперь не надо шаг за шагом смотреть на ассемблерный код, «продираться» сквозь дебри незначащих кодов и защит. Теперь надо отловить нужное событие и понять как на него реагирует программа. Это, конечно, не всегда бывает так просто, как выглядит на словах. Как и ранее, отладка требует знание архитектуры операционной системы.
Неважно насколько сложным был бы механизм защиты, все сводится к простейшей проверке или дешифровке. И взлом, в случае с проверкой, можно разбить на два этапа: установка «брейков» на «подозрительные» флаги, обнаруженные в процедуре защиты; анализ обращений к флагам. По реакции программы можно судить флаг это или просто переменная.
Практическая часть.
Изучить теоретическую часть. Сделать записи в тетради.
Провести сравнение декомпилятора и отладчика. По данным составить таблицу сравнений.
Ответить на контрольные вопросы
Контрольные вопросы:
Что такое дизассемблер?
Как происходит защита программ от дизассемблирования?
Как происходит защита программ от отладки?
Какие виды отладчиков вы знаете?
Что такое декомпилятор?
Какие он функции выполняет?
Что такое трассировка?
Какие виды дизассемблеров вам известны?
Какие приемы дизассемблирования вам известны?