УДК 004.431.2
© А.А. Михайлов
ПРОМЕЖУТОЧНОЕ ПРЕДСТАВЛЕНИЕ ПОДПРОГРАММ В ЗАДАЧЕ ДЕКОМПИЛЯЦИИ ОБЪЕКТНЫХ ФАЙЛОВ DCUIL
В работе рассматривается одна из задач декомииляции - генерация промежуточного представления программы. Описаны некоторые оптимизации над представлением, нацеленные на улучшение качества генерируемого кода. Также приведена архитектура разработанного декомпилятора DCUIL2PAS.
Ключевые слова: декомпиляция, CIL, dcuil.
© A.A. Mikhailov
THE INTERMEDIATE REPRESENTATION OF SUBROUTINES IN THE TASK OF DECOMPILATION OF THE OBJECT FILES DCUIL
In paper considered one of decompiling tasks - generation intermediate representation of the program. Some are described optimization over this representation, the qualities of a generated code aimed at improving. The architecture of the developed DCUIL2PAS decompiler is also given.
Keywords: reverse engineering, CIL, dcuil.
Введение
Для разработки большинства сложных программных систем в среде Delphi часто используются сторонние компоненты, предоставляемые в виде скомпилированных модулей. Такой подход существенно сокращает время и стоимость разработки программного обеспечения. С другой стороны, наличие сторонних модулей уменьшает надежность программного обеспечения с точки зрения информационной безопасности из-за возможного наличия уязвимостей. Кроме того сторонние компоненты могут содержать ошибки, исправление которых может оказаться затруднительным из-за невозможности связаться с разработчиком, отсутствием исходных кодов у разработчика (в связи с их утратой), и т. д. Также, в некоторых случаях может потребоваться доработка сторонних модулей, без возможности использования исходных кодов.
Сторонние модули чаще всего распространяются в закрытом формате dcu (Delphi compiled unit). В ИДСТУ СО РАН Хмельновым А.Е. разработан инструмент dcu32int [1] для разбора модулей Delphi. Данный инструмент позволяет получить исходный код на языке ассемблера и интерфейсную часть модуля. Результат разбора в таком виде в отличие от программы на языке высокого уровня не предоставляет необходимого уровня абстракции для того, чтобы за приемлемое время и трудозатраты идентифицировать алгоритмические конструкции, а также отследить способы взаимодействия элементов программы.
1. Виды промежуточных представлений
Наиболее часто используемыми формами промежуточными представлениями являются ориентированный граф, трехадресный код, префиксная и постфиксная запись [2].
Одной из наиболее распространённых форм промежуточного представления является форма статического одиночного присваивания (Static Single Assignment, SSA). Применению данной формы в задаче декомпиляции посвящена докторская диссертация [3] Майка Ван Еммерика, результатом которой является декомпилятор Boomerang [4] использующий форму S SA. Также данная форма представления используется в таких декомпиляторах, как dcc [5], REC [6], SmartDec [7], Hex-Rays [8] и других.
Основной особенность программы представленной в форме SSA является то, что любая переменная может быть определена только один раз. Для комбинации двух определений в местах схождения потока данных применяется соглашение о записи, называемое ф-функцией. Ниже приведен пример перевода программы в SSA форму (слева исходная программа, справа, в форме SSA).
if a > b then
Result:= a else
Result:= b;
if a > b then
Result1:= a else
Result2:= b; Result3:= 9(Result1, Result2);
Другой формой промежуточного представления является синтаксическое дерево программы. Ту же самую информацию, но в более компактном виде дает представление в виде ориентированного ациклического графа. Здесь общие подвыражения объединены в одну вершину.
На рис 1. приведены представления для выражения a:= b + c * b в виде синтаксического дерева (рис 1. а) и ориентированного ациклического графа (рис 1. б).
Рис. 1. Виды промежуточного представления.
2. Формат объектных файлов dcuil
В объектных файлах Delphi в отличие от исполняемого файла в формате PE программа оказывается более структурированной, например, выделены блоки памяти соответствующие коду каждой процедуры; имеется информация о типах данных; может присутствовать отладочная информация. Такая информация отсутствует в обычных исполняемых файлах. В общем виде формат файла скомпилированных юнитов Delphi выглядит следующим образом: сначала идет небольшой заголовок, в котором содержится общая информация о файле, такая как размер, время компиляции и т. д. После заголовка следует поток теговой информации. Для обобщения теги можно разделить на следующие группы:
• Описания включаемых модулей и объектных файлов.
• Импортируемых из этих модулей определений (типов данных, процедур, и т. д.).
• Описания определений (типов данных, процедур и функций, и т. д.) из данного модуля.
• Блок памяти, составленный из блоков для процедур и функций, образов констант, и т. д.
• Информация для редактора связей (в какие места блока памяти необходимо занести адреса, получаемые из других модулей).
• Отладочная информация.
Блоки кода состоят из наборов CIL инструкций, предназначенных для выполнения виртуальной машиной .NET. Байт код CIL имеет следующие преимущества по сравнению с машинным кодом:
• код отделен от данных;
• машина является стековой, причем стек жестко типизирован;
• стек используется, как правило, только для хранения промежуточных результатов;
• большинство команд CIL получают свои аргументы на стеке, удаляют их со стека и помещают вместо них результат(ы) вычисления;
• машина является объектно-ориентированной: структура CIL отражает разбиение кода на классы, методы и т. д.
3. Промежуточное представление
Промежуточное представление CILIR (CIL Intermediate Representation) разработанное в декомпиляторе DCUIL2PAS реализовано в виде иерархии классов и строится для каждого базового блока графа потоков управления. Сначала определяется начальное состояние каждого блока, которое задаёт значения параметров и аргументов функции, а также состояние стека. Затем каждой инструкции CIL путём последовательного обхода линейного участка кода ставится в соответствие выражение (экземпляр класса), реализующий семантику «опкода».
Поскольку переменная на линейном участке кода может иметь несколько вхождений в выражения, то при генерации новых выражений для экономии памяти каждая переменная снабжается счетчиком ссылок. Вначале счетчики всех переменных устанавливаются равными 1. При каждом вхождении переменной в выражение счетчик увеличивается на единицу. При переопределении переменной ей присваивается ссылка на новый объект, счетчик ссылок при этом снова устанавливается равным 1. В дальнейшем для вычисления результатов выражений используются ссылки на существующие объекты.
Иерархия классов, реализующая промежуточное представление изображена на рис 2.
Базовый класс TCILExpr реализует логику работы со счетчиками ссылок и задает набор обязательных методов:
• Eval— вычисляет значение выражения;
• Eq(E: TCILExpr) — возвращает true еслипереданное выражение эквивалентно текущему ;
• AsString(BrRq: boolean) — возвращает текстовое представление выражения;
• Show — выводит на печать текстовое представление выражения ;
Рис. 2. Иерархия классов промежуточного представления
Наследники класса TCILUnOp описывают все «опкоды» аргументом которых является один единственный операнд. Аналогично все наследники TCILBinOP описывают семантику всех бинарных операций. Аргументы и локальные переменные наследуются от класса TCILArgs. Для описания оставшихся неописанных инструкций в качестве базового используется класс
TCILSemOp.
Все выделяемые регионы в процессе анализа потоков управления, также являются наследниками базового класса TCILExpr. Каждому из выделяемых регионов соответствует свой класс, реализующий его семантику. Список классов реализующих регионы в программе DCUIL2PAS: TCILIfThenElse, TCILWhile, TCILRepeat, TCILCaseSt, TCILBasicBlack.
Все условные и безусловные переходы в процессе разбора преобразуются в выражение вида -
CILCond (Next, Trg; Cond), где Next и Trg - это блоки назначения перехода, Cond -условие перехода.
Для логических выражений, включающих логическое сложение, умножение компилятор может генерировать код для операторов условного перехода двумя разными способами:
1. Условие будет преобразовано в последовательность инструкций, результат будет помещен на стек, для последующего извлечения в качестве аргумента для «опкода» условного перехода. То есть вычислять их в процессе выполнения программы.
2. С помощью условных выражений, основанных на следующих правилах: A and B => if A then B else False, A or B => if A then True else B.
В первом случае результатом вычисления выражения извлеченного со стека в качестве операнда условного перехода будет исходное логическое условие. Во втором случае объединение сложных выражений происходит в процессе их вычисления по заранее определённому набору правил. Например, выражение IfThenElse (Cond, IfThenElse(Cond1, lblTrue1, IblFalse), IblFalse) будет приведено к виду IfThenElse (Cond and Cond1, True1, False). В случае если условное IfThenElse (Cond, IfThenElse (True, (Cond1, True, False) ) , False) выражение содержится в ветке False, тогда выражение примет следующий вид: IfThenElse (Cond or Cond1, True, False) . Аналогичным образом объединение происходит для циклов. Выражение вида WhileSt (Cond, WhileSt (Cond1,Trg) ) будетприведено к виду WhileSt(Cond and Cond1, Trg). Например, промежуточное представление для кода:
function IfSt1 (a: Integer; b: Integer; c: Integer): Integer; var
Result: Integer; begin
if (a > b) then
if (a > c) then if (b > c) then Result := 1
else
Result := 0
else
Result := 0
else
Result := 0; end;
будет выглядеть следующим образом:
IfThenElse(Cond, IfThenElse(Cond1, IfThenElse(Cond2, True, False), False), False)
После применения оптимизации объединения условий код примет следующий вид:
function IfSt1 (a: Integer; b: Integer; c: Integer): Integer; var
Result: Integer;
begin
if (a > b) and (a > c) and (b > c) then Result := 1;
else
Result := 0; end;
4. Архитектура декомпилятора
Структурный анализ
Входные
данные -> CILSeq
Граф потока управления
Генерация промежуточного представления
Оптимизации
1
CILIR
Графический интерфейс
Исходный код Delphi
Рис. 3. Архитектура декомпилятора DCUIL2PAS
На рис. 3 многоугольниками представлены компоненты декомпилятора, а стрелками отображается поток данных между ними.
В архитектуре DCUIL2PAS можно выделить следующие составные блоки (рис 3):
• CILSeq - это внутренне представление входной программы в виде последовательности CIL инструкций;
• Модуль CILCtrlflowGraph выполняет построение графа потоков управления;
• Модуль структурного анализа восстанавливает высокоуровневые конструкции языка посредством модификации графа потоков управления;
• Модуль промежуточного представления отвечает за генерацию промежуточного кода;
• Модуль оптимизаций, производит некоторые изменения промежуточного представления для улучшения качества генерируемого кода. Одной из важных оптимизаций является объединение условий операторов ветвления;
• Модуль GUI реализует графический интерфейс пользователя с поддержкой подсветки синтаксиса.
На вход декомпилятору подаются файлы в формате dcuil. С помощью функционала реализованного в программе dcu32int[1] выделяется блоки кода для процедур и функций. С помощью реализованного на основе библиотеки Mono [9] дизассемблера генерируется байт-код CIL соответствующий подпрограммам. Далее строится граф потока управления путем выделения базовых блоков и связей между ними. Для каждого базового блока строится промежуточное представление. Каждой инструкции ставится в соответствие выражение, полностью описывающее семантику кода. При этом производятся все необходимые операции со стеком, локальными переменными и параметрами подпрограммы. Далее с помощью методов структурного анализа и дерева доминирующих вершин производится структурный анализ. Полученное промежуточное представление подвергается некоторым оптимизациям, нацеленным на улучшения качества производимого кода. На выходе декомпилятор производит код на языке Delphi семантически эквивалентный исходной программе.
5. Пример декомпиляции файла в формате dcuil
Для демонстрации результатов работы декомпилятора DCUIL2PAS рассмотрим следующий небольшой пример. В листинге 1 представлен результат разбора функции, полученного с помощью программы dcu32int:
function CMpIf (n: Integer; b: Integer): Integer; var
Result: Integer; i: Integer;
+ .
begin
00 01 02
03
04 06
07
08 0A 0B 0D 0E 0F 10 11
13
14
15
16
17
18 1A 1C 1D 1E 1F 20 22
23
24
25
26 27 29 2B 2C 2D 2E 2F
31
32
33
34
35
36 end;
[Flags:3013,MaxStack:3,CodeSz:37,LocalVarSigTok:0]
+.
16 | ldc_i4_0
0A | stloc 0
03 | ldarg_1
1B | ldc_i4_5
ю. FE 02| cgt
02 | ldarg_0
1D | ldc i4 7
ю. FE 02 cgt
a 61 xor
,.| 2C 06 brfalse
06 ldloc 0"
1B ldc_i4_
X 58 add
0A stloc 0
+.
|2B |06 |18 |58 |0A |03 |1F |2F |06 |1B |58 |0A |2B |06 18 58 0A 03
1F 3C 2F 06 06 1B 58 0A 2B 06 18 58 0A 06 2A
04
0F 06
04
04
$13
15
br_s $17
ldloc_0 ldc_i4_2 add
stloc_0 ldarg_1
ldc_i4_s bge_s $22
ldloc_0
ldc_i4_5
add
stloc_0 br_s $2 6 ldloc_0 ldc_i4_2 add
stloc_0
ldarg_1
ldc_i4_s 60
bge_s $31
ldloc_0
ldc_i4_5
add
stloc_0 br_s $35 ldloc_0 ldc_i4_2 add
stloc_0 ldloc_0 ret
Листинг 1. Функция на CIL.
Результат разбора в таком виде не предоставляет необходимого уровня абстракции для его анализа специалистом за приемлемое время и трудозатраты. Для того чтобы провести анализ такого кода, необходимо знать состояние стека, значения переменных и аргументов функции в каждый момент выполнения программы. Также необходимо знать семантику всех представленных «опкодов».
В листинге 2 приведен результат декомпиляции функции из листинга 1. Результат разбора в таком виде семантически эквивалентен исходному коду, не содержит операторов неструктурных переходов и представляет собой код на языке высокого уровня Delphi.
function CMpIf (n: Integer; b: Integer): Integer; var
Result: Integer; i: Integer; begin
Result := 0;
if (((b > 5) mod (n > 7)) <> 0) then
Result := Result + 5 else
Result := Result + 2; if (b < 15) then
Result := Result + 5
else
Result := Result + 2; if (b < 60) then
Result := Result + 5
else
Result := Result + 2;
end;
Листинг 2. Результат декомпиляции функции из листинга 1.
Заключение
В работе рассмотрена задача декомпиляции на примере объектных файлов dcuil. Предложено и реализовано промежуточное представление, позволяющее генерировать семантически эквивалентный код на языке высокого уровня Delphi.
Высокий уровень абстракции байт-кода CIL с информацией о типах данных и именах переменных содержащейся в объектных файлах dcuil позволяет добиться высокого качества генерируемого кода.
Литература
1. http://hmelnov.icc.ru/DCU/index.ru.html // DCU32INT - Программа для разбора юнитов Delphi.
2. Ахо А., Лам М., Сети Р., Ульман Д.Д. Компиляторы: принципы, технологии и инструменты. 2-е изд. - Вильяме, 2008. - 1184 с.
3. Michael James Van Emmerik. Static Single Assignment for Decompilation.
4. http://boomerang.sourceforge.net // A general, open source, retargetable decompiler of machine code programs.
5. http://www.itee.uq.edu.au/cristina/d-cc.htmltfthesis // Cifuentes C. Reverse compilation techniques. — 1994.
6. http://www.backerstreet.com/rec/rec.htm // REC Studio 4 - Reverse Engineering Compiler
7. http://smartdec.ru // SmartDec PUSHING NATIVE CODE DECOMPILATION TO THE NEXT LEVEL
8. https://www.hex-rays.com/index.shtml // Hex-Rays
9. http://www.mono-project.com/Main_Page // cross platform, open source .NET development framework.
Михайлов Андрей Анатольевич, младший научный сотрудник Института динамики систем и теории управления Сибирского отделения Российской академии наук, e-mail: [email protected]
Mikhailov Andrey Anatolievich, junior researcher, Institute of system dynamics and control theory SB RAS.