УДК 004.021
МЕТОДЫ ВОССТАНОВЛЕНИЯ ОБЪЕКТОВ ДАННЫХ ИЗ БИНАРНОГО КОДА
М.А.Климушенкова
Институт электронных и информационных систем НовГУ, maria.klimushenkova@ispras.ru
Дан обзор основных подходов к восстановлению объектов данных, используемых в современных инструментах анализа бинарного кода. Описаны четыре основные проблемы при реализации инструментов: восстановление переменных, представление типов внутри алгоритма, выявление источников получения типов и распространение типов.
Ключевые слова: восстановление переменных, динамический анализ, статический анализ, бинарный код
The article reviews the general approaches to the recovery of data objects used in the modern tools of binary analysis. It describes the four main problems in the implementation of tools: variables recovery, types' representation, "type sinks" and types' propagation.
Keywords: variables recovery, dynamic analysis, static analysis, binary code
Введение
Одним из важных шагов при обратной инженерии бинарного кода программы является восстановление высокоуровневых объектов данных, таких как буферы, массивы, структуры, объединения, указатели, глобальные и локальные переменные, а также их типов. Информация о них позволяет лучше понять особенности обработки данных и логику работы программы. В процессе компиляции все переменные удаляются, а код транслируется в операции над регистрами и ячейками памяти в одном глобальном адресном пространстве, все это осложняет понимание функциональности программы. В этой статье делается обзор и анализ основных методов восстановления объектов данных, применяемых в су-
ществующих инструментах исследования исполняемых файлов.
Восстановление объектов данных включает в себя два этапа. Первый — восстановление переменных, т. е. обнаружение высокоуровневых переменных в низкоуровневом коде. Второй этап — восстановление типов. Его целью является определение высокоуровневого типа для каждой переменной. Решение этой задачи является более сложным, так как в откомпилированном коде отсутствует информация о типах. Все данные хранятся в регистрах или ячейках памяти без указания типов, к которым они относятся. Например, если переменная расположена на регистре eax, это значит, что она имеет 32-битный тип. Однако трудно определить, какой именно высокоуровневый тип она имеет: знаковое ли целое это, указатель или объединение.
Современные способы решения задачи восстановления переменных берут за основу методы статического или динамического анализа бинарного кода программы. Каждый из них имеет ряд преимуществ и недостатков.
При статическом анализе в качестве входных данных используется дисассемблированный код программы. Такой анализ дает представление о структуре программы и ее функциональности, однако различные методы обфускации или упаковки исполняемого кода могут существенно усложнить его.
Динамический анализ проводится по трассе программы, полученной при выполнении ее внутри виртуальной машины, при этом анализируется только реально выполнившийся код и информация о значениях регистров на каждом шаге. В этом случае работа ведется не со всем кодом программы, как при статическом анализе, а только с конкретной трассой исполнения программы. Это приводит к тому, что недостаточное покрытие кода препятствует полному анализу функциональности. Для достижения наиболее полного покрытия кода трассами необходима возможность контроля потока управления программы.
Существует несколько программных инструментов, восстанавливающих объекты данных из бинарного кода. Такие системы, как CodeSurfer [1], Ty-Dec [2], основаны на статическом анализе, система REWARDS [3] использует методы динамического анализа. Процесс анализа происходит параллельно выполнению программы на симуляторе.
Инструмент TIE [4] может контролировать поток управления и позволяет применять и статический и динамический анализ. При статическом анализе дисассемблируется бинарный код и идентифицируются функции, используя существующие эвристики. При динамическом анализе программа запускается внутри виртуальной машины, и на выходе мы получаем список инструкций, которые выполнились. В обоих случаях в результате получаем ассемблерный код: для статического — программа, для динамического — один путь выполнения программы. Далее используется общий алгоритм восстановления объектов данных.
Восстановление переменных
Языки высокого уровня, как правило, позволяют использовать произвольное количество переменных, каждая из которых при компиляции располагается на фиксированном множестве регистров и ячеек памяти.
В различных точках программы один и тот же регистр может представлять разные переменные, а также одна и та же переменная может располагаться на разных элементах памяти. Выделение переменных является первым шагом при восстановлении объектов данных.
Чаще всего используются три основных подхода к выделению переменных в коде.
1. Использование SSA (Static Single Assignment) — форма промежуточного представления, при использовании которой для каждого присваивания
нового значения элементу памяти создается новая переменная. Недостатком этой формы представления является создание чрезмерного количества переменных [4].
2. Использование a-loc объектов — по сути они эквивалентны понятию переменной в языке С. Этот метод не подразумевает перемещения переменной по нескольким элементам памяти. Для каждой высокоуровневой переменной — один регистр/ячейка памяти [1].
3. Использование web объектов — представление переменных в виде компонент связности defini-tion-use (определение-использование) графа. Этот метод позволяет создавать переменные, основываясь на их семантике, а не на низкоуровневом представлении [2].
Представление типов
После выявления переменных нужно восстановить их типы. В разных системах сами типы внутри алгоритма представлены по-разному.
В системе REWARDS каждый элемент памяти, используемый в программе, помечается меткой-атрибутом, обозначающей его тип (CHAR, UNSIGNED INT и т.д.) [3]. При таком подходе снижается точность восстановления типов, так как некоторые инструкции могут нести в себе только часть информации, например, что переменная имеет размер 8 бит, но ничего не говорить о ее знаковости. С этой точки зрения представления типов, предполагающие постепенное уточнение информации в процессе анализа, позволяют получить более точную информацию.
В системе TIE существует иерархия типов [4]. Более общие типы включают в себя более конкретные. Так например, тип reg32_t является обобщением для типов num32_t, ptr, code_t. Изначально каждая переменная имеет тип, соответствующий ее размеру (EAX - reg32_t, AX - reg16_t и т.д.). При появлении новой информации от инструкций тип уточняется, спускаясь ниже по иерархии (см. рис.).
Схема иерархии типов
Другим вариантом представления типа является совокупность трех характеристик: ядро (целое число, указатель, вещественное число), размер (1, 2,
4, 8) и знак (знаковый, беззнаковый). Этот способ позволяет восстанавливать все характеристики независимо друг от друга [2].
Источники типов
Из некоторых инструкций программы можно получить информацию о типе переменных. Эти инструкции называют источниками типов. Источники типов — это точки в программе, где можно однозначно определить тип одной или более переменных [3]. Можно разделить все источники типов на три категории.
1. Системные вызовы. Большинство программ вызывает сервисы операционной системы через системные вызовы. Соглашения о системных вызовах и прототипы функций заранее известны, по ним можно однозначно определить типы аргументов и тип возвращаемого значения.
2. Вызовы функций стандартной библиотеки. Информацию о типах также можно получить из вызовов функций стандартных библиотек. Например, оба аргумента strcpy должны иметь тип char*.
3. Определяющие тип инструкции. Некоторые инструкции требуют определенного типа для аргументов. Например, для архитектуры x86 можно выделить три типа таких инструкций:
— инструкции работы с массивами, выполняющие операции пересылки-сохранения (MOVS/B/D/W, STOS/B/D/W), загрузки (LOADS/B/D/W), сравнения (CMPS/B/D/W) и сканирования (SCAS/B/D/W);
— инструкции работы с вещественными числами, выполняющие арифметические операции с числами с плавающей точкой (FADD, FABS, FST);
— инструкции с косвенно адресованными операндами.
Распространение типов
Знания о типах, полученные из их источников, далее распространяются по всей программе.
В системе REWARDS существует два способа продвижения типов [3]:
1. On-line процедура распространения типов. Во время выполнения программы в симуляторе каждый раз при получении новой информации о типах она распространяется вдоль потока данных в обе стороны.
2. Off-line процедура распространения, дополняющая on-line процедуру, уточняет результаты уже после завершения выполнения исследуемой программы. Типы большинства переменных удается установить во время выполнения программы, однако остаются и те, для определения типов которых необходима информация, доступная только после завершения on-line анализа.
Распространение типов в TyDec основано на решении системы уравнений итеративным алгоритмом, основанным на продвижении значений. Для каждой характеристики типа строится отдельная система, и решаются они независимо друг от друга [2].
В системе TIE для вывода и распространения типов используются ограничения, накладываемые программой. Каждая инструкция программы ограни-
чивает диапазон типов своих операндов, так инструкция goto говорит, что ее аргумент — указатель на код. После формирования множества ограничений алгоритм последовательно обрабатывает каждое ограничение и удаляет его из множества, действуя так до тех пор, пока оно не окажется пустым. В результате для каждой переменной выводится пара ограничивающих типов, которые являются диапазоном возможных значений [4].
Заключение
Проанализировав существующие подходы, можно сделать вывод, что наилучшим методом будет являться динамический анализ трассы исполнения программы с возможностью анализа нескольких трасс для обеспечения наилучшего покрытия кода. Для понимания логики обработки данных анализ должен быть консервативным, т. е. будет использоваться только та информация, которая следует из бинарного кода без угадываний.
Методом выделения переменных, наиболее полно соответствующим высокоуровневым переменным, является нахождение компонент связности в графе «определение-использование». Этот метод позволяет избежать создания большого количества переменных.
Для наиболее точного восстановления типов необходимо использовать все возможные источники типов, так как из них можно точно узнать тип объекта.
Представление типов внутри алгоритма должно позволять хранить отдельные его характеристики, а не только тип целиком. Это избавит от угадывания недостающей информации в случае, когда мы знаем только размер типа или его знаковость. При недостатке информации более консервативным будет восстановление общего типа для переменной.
Основной принцип распространения типов является общим для всех инструментов. Он основан на создании множества ограничений, накладываемых программой, и итеративном выводе типов переменных, удовлетворяющих этим ограничениям.
Осуществленный нами анализ позволяет разработать алгоритм, реализующий выше описанные принципы, который будет консервативно восстанавливать объекты данных.
1. Balakrishnan G. WYSINWYX: What You See Is Not What You eXecute // PhD thesis, Computer Science Department, University of Wisconsin at Madison. Aug. 2007. P.20-97
2. Dolgova E. and Chernov A. Automatic reconstruction of data types in the decompilation problem // Programming and Computer Software. Mar. 2009. V.35. P. 105-119.
3. Lin Z., Zhang X., and Xu D. Automatic reverse engineering of data structures from binary execution // Proc. of the Network and Distributed System Security Symposium, 2010. P. 1-18.
4. Lee Jong Hyup, Avgerinos Thanassis and Brumley David. TIE: Principled Reverse Engineering of Types in Binary Programs // Proc. of the Distributed System Security Symposium. 2011. P.1-18.