Восстановление типов данных в задаче декомпилирования в язык c

m 6(24) 2009

Е. Н. Трошина, А. В. Чернов

Восстановление типов данных в задаче декомпилирования в язык С

Декомпиляция — одна из сложнейших задач обратной инженерии. Одной из подзадач декомпиляции является задача восстановления типов данных. В работе подробно рассматриваются методы восстановления типов данных языка С, как базовых, так и производных.

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

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

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

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

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

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

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

В настоящее время из широко используемых компилируемых языков программирования высокого уровня распространены языки C и C++, поскольку именно они наиболее часто используются при разработке прикладного и системного программного обеспечения для платформ Windows, MacOS и Unix. Поэтому декомпиляторы с этих языков имеют наиболь-

99

№ 6(24) 2009

Vj *

3

Si

«о

IS

is

«о

о §

§

с £

а

а

т «о

в %

§

«о

о §

S

U

a

is §

о §

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

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

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

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

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

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

Можно сказать, что задача автоматического восстановления типов данных на настоящее время — одна из наименее проработанных с теоретической точки зрения задач в области декомпиляции. Полнота восстановления типов данных существенно повышает полноту декомпиляции. Восстановление программы из низкоуровневого представления без восстановления типов данных не сильно повышает уровень представления программы, и может сводить на нет эффект от декомпиляции из-за трудности понимания восстановленного кода. Данная работа посвящена одной из задач декомпиляции — восстановлению типов данных, другая задача декомпиляции — восстановление структурных конструкций языков высокого уровня — рассматривалась в работе [1], которая была опубликована в №4(22). Комплексное решение задачи декомпиляции в язык C представлено в диссертации на соискание ученой степени кандидата физико-математических наук по специальности 05.13.11 — «Математическое и программное обеспечение вычислительных машин и компьютерных сетей» [2].

Задачу восстановления типов данных условно можно разделить на подзадачи восстановления:

1) базовых типов данных языка, таких как char, unsigned long и т. п.;

2) производных типов данных, таких как типы структур, массивов и указателей.

Изложение материала в данной работе выполнено в несколько упрощенной форме. Более строгое описание представленных моделей восстановления базовых данных представлено в работах [2], [3] и производных типов —

100

№ 6(24) 2009

в работах[2], [4]. Описание инструментальной среды по декомпиляции программ представлено в работах [2], [5] и [6].

Данная статья имеет следующую структуру. В первом разделе представлено описание методов восстановления типов данных. Во втором — описание метода восстановления базовых типов данных, далее — описание восстановления производных типов данных. Следующий раздел посвящен экспериментальным проверкам и описанию работы модуля восстановления типов данных декомпилятора TyDec, который разрабатывается авторами.

Обзор работ по восстановлению типов данных

Один из подходов к восстановлению типов заключается в использовании методов математической логики. Подход впервые был предложен А. Майкрофтом (A. Mycroft) [7].

Программа рассматривается как константное выражение над термами типов данных. Компилятор в процессе семантического анализа программы вычисляет это выражение.

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

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

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

метод неприменим для автоматического восстановления типов данных.

Другой подход к задаче анализа бинарной программы представлен в работах Г. Балак-ришнана (G. Balacrishnan) [8] и [9], которые посвящены методам статического выявления значений переменных в исполняемых файлах.

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

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

Кроме того, в методе не предусмотрено восстановление базовых типов полей структур и элементов массивов.

Еще один подход к восстановлению типов данных в задаче декомпиляции предложен в работах М. Ю. Гусенко [10]. В качестве операций над типизированными данными рассматриваются инструкции процессора, а в качестве значений типизированных данных — значения примитивов, которыми они оперируют. Каждый тип характеризуется именем и длиной. В работе предложены методы восстановления следующих типов данных: числовой, указатель, структура struct, объединение union и массив.

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

«о

о %

а.

si %

а 3 о

101

№ 6(24) 2009

Vj

iE

s Si

«о

IS iS

«о

о §

§

с £

а

а

т «о äs

о %

iS

«о

о §

S

U

a

is is

о §

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

Следует также отметить, что одной из фундаментальных работ по декомпиляции в язык C считается диссертационная работа К. Цифу-ентес (C. Cuffuentes). Однако в ее работах [11], [12] восстановление типов переменных практически не рассматривается.

На практике все декомпиляторы, кроме расширения Hex-Rays [13] к интерактивному дизассемблеру Ida Pro [14], вообще не восстанавливают даже базовые типы переменных, а в выражениях используют явное приведение типов, что делает декомпилированные программы сложными для понимания и модификации.

Восстановление базовых типов данных

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

Представление типов данных. Ограничим множество автоматически выводимых типов данных языка C только базовыми типами и указателями на базовые типы. Пусть множество T0 {unsigned char, char, unsigned short, short, unsigned int, int, unsigned long, long, unsigned long long, long long, float, double, long double, void} — это множество базовых скалярных типов и «псевдо» тип void. Будем рассматривать ключевое слово void в качестве некоторого специального типа данных, об использовании которого будет сказано ниже.

Пусть множество T = {т*|т€ T0} — это множество, состоящее из указателей на базовые скалярные типы и обобщенного указателя void*. Тогда все множество автоматически

выводимых типов данных

это T = UT-.

Представим каждый тип данных из множества T в виде совокупности трех характеристик: ядро core €{int, pointer,float};

размер size €{1,2,4,8};

знак sign €{signed, unsigned}.

Специальный тип void будем представлять как

void =

voidcore = 0 void size ={1,2,4}. void sign =0

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

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

Будем считать, что работа с указательным типом ведется как с беззнаковым 32-хбитным целым числом. Тогда, если размер типа меньше 4-х байтов, то тип не может быть указательным. Для типа данных «указатель» размер и знаковость относятся к тому типу данных, на который он указывает.

Например, если тип

T — {unsigned short*} — [unsigned short*],

то его представление в виде трех составляющих следующее:

core —{pointer}

T=

= {2} .

={unsined}

102

0

№ 6(24) 2009

И, наоборот, если, например, имеем представление некоторого типа,

т core ={pointer} T = - тsize ={1} ,

т sign ={unsined}

то тип T = [unsigned char].

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

1) определение знаковости типа, например short и unsigned short;

2) определение того, является ли тип указательным или целым, например unsigned int и char*;

3) определение размера типа, например, short и int.

Объекты. В работе [9] предложено понятие a-loc абстракции, которое, по сути, эквивалентно понятию переменной в языке С. Однако a-loc абстракция не дает возможности отслеживать перемещения переменных по регистрам и не позволяет разделять несвязанные использования одной переменной. Поэтому в данной работе объектами для анализа по восстановлению типа являются web [15]. Web представляет собой компонент связности двудольного графа «определения переменной — использование переменной». Отличие web от переменных, регистров и других подобных конструкций в том, что web учитывает особенность повторного использования ячеек памяти и регистров независимым образом, так как одна и та же ячейка памяти или регистр часто используются в различных фрагментах программы независимо. Таким образом, в ассемблерной программе web строятся по:

1) регистрам процессора, например, %eax;

2) ячейкам памяти по фиксированным адресам (это были глобальные переменные в исходной программе на языке C);

3) ячейкам памяти по фиксированным смещениям в текущем стековом кадре, например, -2(%ebp) — в исходной программе на языке C это локальные переменные функций и их параметры;

4) ячейкам памяти, адресуемым по смещениям регистра %esp, и ячейкам памяти, неявно адресуемым при занесении значений в стек, например при использовании команды push.

В дальнейшем под объектом будем понимать web, построенный по описанным выше компонентам ассемблерной программы. Каждый объект obj, характеризуется некоторым типом данных T¡ (т,) из рассматриваемого множества T, т. е. T, € T. Тип T, назовем «идеальным типом». Это именно тот тип, который изначально был у переменной, отображенной в объект obji, в исходной программе на языке C. Как было сказано выше, при корректной декомпиляции восстановленный тип может отличаться от идеального типа. Поэтому для объекта obj, будем искать тип T,, т. е. obj, :T,, где T, € T, который назовем «искомым типом». Искомый тип в декомпилируемой программе должен совпадать с идеальным типом синтаксически или быть семантически эквивалентным ему.

Например, пусть параметр <Type> в функции func (см. листинг) принимает значение {unsigned int}. Тогда «идеальный тип» для всех объектов этой функции — это {unsigned int}. А «искомый тип» — это любой тип из множества {int, unsigned int, long, unsigned long}.

Искать «искомый тип» T¡ € T будем через последовательное приближение. Каждый тип данных можно представить в виде 3-х составляющих. Будем считать, что каждый объект

obj, имеет тип obj, :T¡, T¡ € T

где Ti

«о

о %

а.

sí %

а 3 о

Тогда будем восстанавливать тип T € T посредством последовательного приближения 3-х его составляющих характеристик

core ^ size ^ sign ^

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

103

№ 6(24) 2009

Vj *

S

Si «0

IS

is «0

о §

§

с £

a

a

m «о is

o %

iS

«о

о §

s

QJ

а

is is

о

iS

у которого «узел» есть операция, полученная на основе ассемблерной инструкции, или объект, «лист» — это объект, а «дуга» — это зависимость использования узлов. Назовем узел, построенный на основе ассемблерной инструкции, «узел-инструкция», а все остальные узлы назовем «узел-объект». Назовем родительский узел-объект узла-инструкции «результат операции». Узлы-потомки узла-инструкции назовем «операндами».

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

Дерево состоит из узлов-инструкций 5 типов:

1) следование, такие узлы будем обозначать «FN»; их потомки не имеют локальной зависимости по использованию и должны соответствовать в декомпилированной программе в язык C отдельному оператору;

2) вычисление выражений, этим узлам соответствуют операции вычисления выражения, например операция сложения двух объектов;

3) копирования, такие узлы будем обозначать «CN»; им соответствует операции передачи данных в ассемблерном листинге между объектами;

4) вызов подпрограмм, обозначим такие узлы «CALL»; этот узел соответствует вызову пользовательской или библиотечной функции;

5) проверка условия, этот узел соответствует инструкциям проверки условий при условном переходе.

Для каждого узла-инструкции дерева зависимостей типов выпишем уравнение вида

obji :tnл <op>objj:tn,2 =>objk :tn,3 где obji, objj, objk — это узлы-объекты;

tn,i, tn 2, tn 3 — это типы соответствующих объектов в n-м уравнении; <op > — это операция, соответствующая узлу-инструкции дерева зависимостей типов;

obji и objj — это операнды узла-инструкции, соответствующего инструкции <op>; objk — это результат операции этого же узла-инструкции.

Совокупность уравнений для всех узлов объединим в систему уравнений. Каждый тип tn,, соответствует некоторому типу Tj, и, следовательно, его также можно разложить на 3 составляющие: тc°re —ядро типа, т— его размер и т— знак типа данных. Таким образом, между полученной системой и построенным деревом зависимостей использования типов есть взаимно однозначное соответствие. Так как каждый тип tn,, соответствует некоторому типу Tj, то для каждого типа Tj можно выписать все типы объекта objj из уравнений. Тогда можно сказать, что тип Tj представлен типом 11ьk 1 в уравнении i1, типом t,2,k2 в уравнении i2 и т. д. Обозначим всех представителей типа Tj во всех уравнениях множеством {ti 1,k 1 ,..., timkm }, т. е. Tj ={t,1,k 1 ,..., timkm }.

Рассмотрим отдельно правую и левую части уравнений. Тогда можно сказать, что множество {j11,...,tjm,im } — это множество представителей некоторого типа Ti по всем левым частям уравнений системы и назовем множество {tj 1,11,...,tjm,im } представителями типа T по левой части системы. Аналогично для правой части системы. Тогда тип Tj =t, 1k 1,...,timkm — это все представители типа Tj по всей системе,

Tleft {tn ......... tnm ,1 m } и T/-ht {tj ......... tjmJm } —

это все представители типа Tj по левой и правой частям системы, соответствено.

Очевидно, что для любого типа верно, что

T _T left ^ T right

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

1. Creg|= — регистровое ограничение, оно влияет на составляющую «ядро», т. е. core и «размер», т. е. size.

104

№ 6(24) 2009

Например, если некоторый объект obj:T построен по регистру %dx, то можно сказать, что «тип» этого объекта int, а «размер» у него 1 байт или 2 байта, т. е. T = {тcore, тsize, тsign }, то тcore ={ int}, а тsize ={1,2}.

2. Ccmd|= — командное ограничение, оно влияет на составляющую «ядро», т. е. type, «размер»,т. е. size и «знак», т. е. sign.

Например, если имеем команду movswl obji :Т objj :Tj, то можно сказать, что, если Т, ={т,Core , тsize , тsign }, то тsign ={signed}, а, следовательно, т (Core ={int}.

3. Cfiags|= — флаговое ограничение, оно влияет на составляющую типа «знак», т. е. sign.

Например, если имеем следующий фрагмент кода на ассемблере

cmpl -12(%ebp), %eax

jae L3

то, пусть объектobj:T построен по регистру %eax, тогда можно сказать, что тsign = unsigned.

4. Cenv|= — ограничение окружения, оно влияет на все три составляющие типа. Это ограничение, которое возникает в результате использования библиотечных функций, так как прототип библиотечных функций считается известным.

Функция слияния. При решении уравнений потребуется использовать функцию слияния. Использования простого пересечения двух множеств не достаточно, так как в случае пустого пересечения множеств слияемых объектов не сохранятся их значения. А так как здесь рассматривается итеративный алгоритм, то, как раз в случае пустого пересечения слияемых множеств их значения надо объединять, так как на следующих итерациях алгоритма будет получено множество, пересечение с которым будет непустое. В противном случае мы имеет конфликт, для разрешения которого все равно требуется полная история возможных значений слияемых множеств. Следовательно, назовем множество S результатом слияния множеств S1 и S2, и обозначим S = {S1,S2}, если

S=

S1 П S2 ,если S1 П S2 S1 U S2 ,если S1 П S2

Пустьt1 и Г2 —это представители некоторого типа Тп €Т, следовательно, как сказано в пункте 3.1, их можно разложить на три составляющие, т.е. ^ ={т,с°ге, т,3"е, т,з1дп } и

core size sign ¡2 = {т2 , т2 , т2 }.

Пусть t — это представитель некоторого типа Тп € Т, t1 и ^ — это представители того же типа Тп € Т. НазовемТрезультатом слияния t1 и и обозначимt={t1,t2}, если

t ={т

1

где тc

тs

_core _type.

- I 1 , I 2 '

core 1

sign 1

,т ,2

core i 1, j 1 size i 1, j 1 ' sign i 1, j 1

W t,„.j„ =

core in , jn size in , jn ' sign in , jn

то

«0

О %

a.

si %

a 3 о

Тогда, если Тк ,...,t¡n,¡п }, выполним

слияние по всем множествам трех составляющих для соответствующих трех составляющих типа Тк, т. е. если

т core _ Гт core т core \

1 ^ L 1 ¡1,j1 ,..., 1 in , jn У

~ sign — J"T sign т sign \

1 k 1 1 ii,ji ,..., 1 in , jn Г

После того, как слияние выполнено, переопределим все составляющие подтипов типа Tk значением функции слияния, т.е. теперь Tk — Tk ,•••, tin, jn:— Tk}. Будем называть

левой функцией слияния слияние представителей части уравнения, т. е.

Tjleft = {tni,11 ,•, tnm ,}. ^_^

Аналогично T.right — {tni>,1 ,•..,tnm,tm } — правая функция.

Правила обхода дерева. Для дерева зависимостей использования типов определим 2 типа обхода:

1) обход снизу вверх, или восходящий обход;

2) обход сверху вниз, или нисходящий обход.

105

а

и

№ 6(24) 2009

Vj *

3

Si

«о

IS

is

«о

о §

§

с £

а

а

т «о

о %

iS

«о

о §

S

U

a

is is

о

iS

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

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

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

1) бинарная;

2) унарная;

3) копирования;

4) обращение к памяти.

Узел следования не распространяет непосредственно информации о типах данных между своими потомками.

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

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

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

movzbw

fedx,

то для узла копирования, соответствующего команде movzbw, первым операндом будет объект, соответствующий обращению к памяти по адресу, записанному в регистре %еах, вторым операндом — объект, соответствующий регистру %dx, а результатом операции — объект, соответствующий регистру %edx.

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

Узел проверки условий является узлом, непосредственно распространяющим информацию. Этот узел соответствует унарной операции.

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

Рассмотрим правила распространения информации о типах при обходе дерева снизу вверх.

Бинарную операцию рассмотрим на примере вычитания. Пусть уравнение имеет вид

е1 1 е2:^е 2 ^е ге5 ,

te res _{- c

={т

_{ — core _ size _ sign

' e i ' e i ' e 1

следовательно, можно условно обозначить,

ЧТО core _ — core _ _ core и ^sign _ — sign _— sign

res e i e 2 ' res e i e 2

Определим правила распространения информации об использовании типов данных для составляющей типа «ядро», т.е. для тcore, следующим образом:

106

а

t

ei

№ 6(24) 2009

core e 1

e 1

core

e 1

core

e 1

core

pointer—тc

! :integer =

5:pointer;

pointer —т core : float ^ e 2

float—т core e 2

float—т core e 2

float—т core e 2

integer —тc

: pointer ^т CeSre: error; :float^тceosre :float; :integer ^т ceore :error;

:integer

5: integer;

integer —т core :pointer =

Рассмотрим операцию над множествами «пересечение сверху»: S — S1Д52,т. е.

S — {s| s—max(s1, s2), s1 € S1, s2 € S2},

где (s1, s2) € S1 x S2, т. е. (s1, s2) — это все члены декартового произведения множеств S1 и S2.

Для составляющей типа «размер», т. е. для тsize, определим правила распространения информации о типах так:

size size size

1 res - 1 е 1 И 1 е 2 .

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

sign

e 1

unsigned—т s

1:unsigned ^ т

sign res

unsigned;

unsigned —т^ g :signed-signed—т : signed =

sign

rres : unsigned; rseisgn: signed.

Для унарной операции правила распространения информации такое: если унарная операция имеет вид

e,:ie, <op>=>eres:teге5,тоteres :=te Пусть операция копирования имеет вид:

el :te 1 >e2:te 2 >e res :t res,

тогда правило распространения информации для нее такое: teres :=te 1,te2. Для операции обращения к памяти правило распространения информации определим таким образом: пусть есть некоторое уравнение

el:te 1  e2:te 2 =>e res :t res,

операндов, выполняется обращение к памяти, например, в операндеe,:ie 1,то

:=т„

5 U{pointer},

Ит-size--т- size

res

107

где  — это либо бинарная операция, либо * операция копирования. £

Если по адресу, записанному в одном из

3 о

Рассмотрим правила распространения информации о типах при обходе дерева сверху вниз.

При обходе дерева зависимостей типов сверху вниз для таких составляющих типа данных, как «ядро», т. е. core, и «знак», т. е. sign может возникать альтернативная информация. Если в логике предикатов рассматривать уравнение вида A — B VC, то, зная, что A — true, можно точно сказать, что либо B — true, либо C — true, либо и B — true, и C — true. Таким образом, можно сказать, что значение «true» является альтернативным для переменных B и C, потому что они одновременно не могут принимать значение «false». По аналогии рассмотрим альтернативное значение для составляющих типа данных. Обозначим альтернативное значение как

т mlass (?М

где class€{core, size, sign}.

Альтернативное значение соответствует значению некоторой составляющей типа данных. Альтернативные элементы должны быть парными. Это означает, что если во множестве некоторой составляющей типа m есть альтернативный элемент тmlass (?)[n], то во множестве той же самой характеристики типа n есть альтернативный элемент тclass (?)[m]. Следовательно, определим альтернативный элемент как такой элемент, что при слиянии, либо и тmlass(?)[n], и тclass(?)[m] будут принадлежать результирующему множеству, либо обязательно один из них, т. е. либо тclass(?)[m]€ тclass либо тmlass(?)[n]€ тmlass, либо и тclass(?)[m]€ тclass, и тmlass (?)[п]€тmlass после выполнения слияния. Если в результате слияния оба из альтернативных элементов пары пропадают из соот-

e1

№ 6(24) 2009

ветствующих множеств, то их надо явно добавить во множество т „ass и во множество т class.

Рассмотрим правила распространения информации для бинарной операции.

Пусть i-e уравнение системы имеет вид

el :te 1  e2 :te2 _>eres :teres ,

где  — некоторая бинарная операция, тогда для характеристики «ядро», т. е. для тcore применяется правило: если pointer€Tce°sre,то

core ._гг core

Т uui у т

Т c°re := re и { pointer(re) (?)[e, ]}, если pointer^TCe°sre,то

тсог^_тсоге тсоге тсог^_тсоге тсоге

1 е 1 1 е 1 , 1 , 1 е 2 1 е 2 , 1 е '

Для характеристики типа «размер», т.е. для тз1ге, правила такое т|:|2е:=т^ек2 Пт,

Пт!

- size ._-size

а т size :=т

e 2

- size

e 2 '

О

iE

s

«о

is is

«о

о §

§

с £

а

а

т «о äs

о %

5

«о

о §

S a

is is

о

is

Для характеристики типа «знак», т. е. для тsign, правила распространения информации аналогичны правилам для характеристики типа «ядро».

Есл и {unsigned} € т^^, то

тsign :_тsign и {unsigned£}(?)[e2 ]},

тesign:_тesign U{unsigned£)(?)[e1 ]}, если {unsigned}, то

sign ._ sign sign sig^_ sign sign

I e i .— ei , 1 res ' 1 e2 1 e2 ' 1 res '

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

e,:ie1 < op > = >e res.te res'то te ,.= te ^ .

Для операции копирования правило распространения информации определено так: если операция копирования имеет вид

el :te 1 >e2:te 2 >e res :t res,

то

te 1 :=te 1 U te res и te 2.= te 2 U te

Для операции обращения к памяти используется правила, аналогичные для бинарной операции, в случае если {pointer} €TCe°sre.

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

Алгоритм нахождения типов для объектов по построенной системе уравнений. Пусть исходная программа имеет N объектов. Тогда множество OBJ мощности N — это множество, которое содержит все объекты исходной программы и только их, т. е.

OBJ={obj1,..., objn}, где obj, — это объект исходной программы.

Для каждого объекта obj, е OBJ существует «идеальный тип» T] е T, а также существует «искомый тип» T, е T. Будем считать, что «искомый тип» T е T для объекта obj, е OBJ найден корректно, если «идеальный тип» T е T является его надмножеством, т.е., если T, ={int}, а T, = {int, unsigned int}, то T еT найден корректно. Также будем считать, что «идеальный тип» найден точно, если имеется полное совпадение, т.е. если T, ={int} и T, ={int}, то T е T найдет точно.

Следовательно, определим множество Type = {T,...,Tn}, мощности N — это множество, содержащее все искомые типы объектов исходной программы и только их. Пусть функция Tree make_tree (program) — это функция, которая по программе на ассемблере строит дерево зависимостей типов. Пусть построенное дерево отображается в систему, описанную там же и состоящую из M уравнений, функцией Equationmake_equation(Tree). Определим процедуру init(Equation), которая инициализирует все типы объектов в системе уравнений начальным значением void, т. е. для всех типов t в системе уравнений. Затем на полученную систему уравнений Equation накладываются ограничения, это выполняет процедура set_constraint(Equation).

108

№ 6(24) 2009

Определим процедуры

spread_information_lr

и

spread_information_rl(Equation), которые по уравнению выполняет распространение информации об использовании типов данных в соответствии с правилами: первая — слева направо, а вторая — справа налево соответственно. Функция Type(obj) возвращает множество всех представителей типов объекта obj € OBJ по всем уравнениям из множества Equation. Процедуры

boolean make_left_join(Type(obj)),

boolean make_right_join

и

boolean make_full_join(Type(obj)) выполняют соответственно левое, правое и полное слияние типа объекта obj, € OBJ и возвращают «true», если в результате слияния T; € T изменилось, и «false» — в противном случае.

После того, как для всех объектов obj € OBJ множества представителей типа стали неподвижными в результате последовательного распространения информации о ти-

Tree:= make_tree(program);

Equation:= make_ _equation(Tree);

init (Equation);

set_constraint(Equation) ;

flag:=true;

while (flag) do begin

flag:=false;

for all equation: Equation do

spread_information_lr(equation);

for all obj:OBJ do

flag | |= make_left_join(Type(obj));

for all equation: Equation do

spread_information_rl(equation);

for all obj:OBJ do

flag ||= make_right_join(Type(obj));

for all obj:OBJ do

flag ||= make_full_join(Type(obj));

end

for all obj:OBJ do

obj:=match_type((Type(obj)) ;

пах по уравнениям и слияния, выполняется под- * борка типа из множества всех рассматриваемых £ типов T. Это выполняется процедурой T match_type(Type(obj)). ^

Ниже представлен алгоритм вычисления а типов для программы program.

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

Восстановление производных типов данных

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

109

№ 6(24) 2009

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

Поскольку метод является консервативным, для некоторых пар обращений к памяти ^ отождествление типов может быть не установ-| лено, хотя, на самом деле, типы были тождест- венны. Тогда в декомпилированной программе ме возникнут клоны одного и того же струк-§ турного типа, возможно, с различным распо-§ знанным набором полей. | Очевидно, что гарантировать восстановле-| ние типов всех полей структуры при декомпи-ляции в общем случае невозможно, так как де-,§ компилятор не имеет информации о полях, 2 к которым не было обращений вдекомпили-g руемом фрагменте кода. Такие поля представ-| ляются так же, как и память, зарезервирован-^ ная под выравнивание, в виде массивов типа | char необходимого размера. S Поля структуры могут быть произвольного | типа: базового, указательного, массивового, § структурного. Однако, случай, когда один

о

§ структурный тип вложен в другой структур-

§ ный тип, может оказаться неотличим от слу-

вд чая, когда поля вложенной структуры непо-

средственно находятся в объемлющем структурном типе. Например,

struct t1 struct t

{ {

int t1; int t1;

int t2; int t2;

}; int t3;

struct t2 };

{

struct t1 tt;

int t3;

};

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

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

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

Массивы. Массив — это совокупность однотипных элементов, размещаемая в памяти последовательно. В отличие от структур, доступ к элементам которых ведется с помощью константных смещений относительно начала области памяти, доступ к элементам массива производится с помощью индексных выражений. Например, для массива int m[32] и операции доступа m[j] будет сгенерировано адресное выражение, подобное m + j * 4. Операция умножения в адресных выражениях — отли-

№ 6(24) 2009

чительная особенность обращений к массивам.

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

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

Некоторые сложности могут возникать, если в индексном выражении в программе на языке высокого уровня уже используется умножение, например, m[2*j]. Тогда константа 2 в результате окажется в константном множителе адресного выражения, что дало бы нам неправильный размер массива. Чтобы избавиться от пользовательских мультипликаторов, необходимо взять наибольший общий делитель от всех мультипликативных констант, найденных во всех адресных выражениях, относящихся к одному и тому же массиву.

Если же все обращения к массиву используют один и тот же множитель, т. е., например, m[2*j], то с точки зрения алгоритма восстановления производных типов, элемент массива окажется структурой из двух полей, причем тип первого поля будет восстановлен, а второе поле, поскольку к нему отсутствуют обращения, будет представлено как массив типа char требуемого размера.

Операции доступа к элементам массива с константным индексом, например, m[5], транслируются в ассемблерной программе в доступ по константному смещению относи-

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

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

Формальное описание алгоритма. Допустим, что все обращения к памяти можно представить в виде:

п

Ь+о +Е с]х] 1 = 0

где Ь (базовый адрес) — это изначально регистр, а впоследствии — объект; о — смещение; С1 — это константы.

Такое предположение справедливо в поставленных выше ограничениях на использование языка С в исходной программе. Без ограничения общности можно считать, что С1 <С1+1. Пусть т=ттп=0 С1 =С0 и М=ттп=0 С1 =Сп.

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

«о

о %

а.

si %

а 3 о

111

№ 6(24) 2009

Vj

iE

s Si

«о

IS

ig «о

о §

§

с £

а

а

т «о äs

о %

§

«о

о §

S

U

a

is §

о §

(obj + offset). При этом obj может быть не тем же самым объектом, что b, и даже может иметь другой базовый адрес. Такие ситуации могут возникнуть при восстановлении вложенных структурных типов. offset также может не совпадать со смещением o в исходном адресном выражении.

Присваивание меток Assign_labels. Пусть l — это метка, выбранная из перечислимого множества меток. Будем присваивать метки следующим элементам ассемблерной программы:

1) доступам к памяти: l, :(b, ,o, ,C0 ,...,Cn);

2) возвращаемым значениям подпрограмм (содержимое регистра %eax).

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

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

Построение множества меток в регистрах (Build_reg_label_sets). Пусть LSin (reg, n) — это множество меток, соответствующее тем значениям, которые могут находится в регистре reg в точке, непосредственно предшествующей n-й строке ассемблерной программы. Аналогично, пусть LSout (reg, n) — это множество меток, соответствующее тем значениям, которые могут находиться в регистре в точке, непосредственно следующей за n-й строкой ассемблерной программы. Для простоты обозначения будем опускать квалификатор in для множества меток LS.

Анализ достижимости меток (label-set analysis) строит множество меток LS для каждого регистра и для каждой строки ассемблерной программы. Это прямой итеративный анализ потока данных, в котором в качестве функции слияния используется объединение множеств. Так как множество присваиваемых

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

Построение множеств эквивалентных меток (Build_label_equiv_sets). Основываясь на вычисленном множестве достижимых меток, между метками устанавливается отношение эквивалентности следующим образом:

3/,, /2, reg, n:

li = I2

Ii: (bi,oi,Ci,0 ,...,Ci,n ),I2: (b2 ,o2 ,C2,o C2,n ),bi = b2

Ii =l2

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

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

Две метки /п и /2 имеют общую базу, если /1 :(Ь, ,о1,...), /2 :(Ь2 ,о2,...) и Ь1 =Ь2.

Построение агрегированных множеств (Build_aggreg_sets). Пусть t¡ это — представитель ¡-го класса эквивалентности. Определим отношения агрегирования в массив для двух меток, обозначенное как АА(/,, /2), следующим правилом вывода:

/1 :(t 1 ,о 1 ,С1, о //С1,п ),/2 : (t 1 ,о2 ,С2, о ''С2,п )<|° 1 2 |<С1,о

АА

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

112

№ 6(24) 2009

struct si

{

struct s2 {

int fi; int f2; } a[1];

struct s3 {

int f3; int f4; } b[1];

Операция доступа к полю fi имеет вид (b,0,8), где b — это некоторый базовый адрес структуры, 0 — смещение поля fi относительно начала структуры, 8 — размер структуры struct s2, так как поле fi расположено в массиве структур этого типа. Аналогично, операция доступа к полю f2 имеет вид (b,4,8), к полю f3 — (b,8,8), к полю f4 — (b, 12,8). Согласно правилу вывода 3 все они окажутся связанными отношением AA.

Отношение AA назовем конфликтным, если для него нарушена транзитивность, т. е., если существуют две метки l1 и l2, попавшие в один класс транзитивного замыкания отношения AA, что |о1 —о2 |>Сho.

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

1) в один подкласс попали только метки, для которых |o1 —o2 |<С1уo;

2) для любых двух подклассов K1 и K2 |minKl(o) — minK2(o)|>С1о, т. е. разность минимальных смещений полей двух подклассов должна быть не меньше константы С1о (размера массива), индуцировавшей данное разбиение.

Отношение AA с дополнительным разбиением устранения конфликтов разбивает множество всех меток на множества агрегации. Пусть множество

K = {l1,l2,...,lm} — это какое-либо множество агрегации.

Пусть o=minm=1 о,.

Обозначим t'=(b,О как метку для массивов вложенных структур, где (b,o) означает

вычисление смещения от базы, но без разыменования.

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

I,: (Ь, о,, Со.....Ст) е К, I,' = (:', о, — о, С ¡о.....С,, т).

Кроме того, поскольку агрегация в массив структур выполнена, константа Со удаляется. В результате множество К' состоит из меток I,' = (:',о/ — о,С,,о,...,С,,т). Агрегирование не изменяет базу меток, поэтому если они до агрегирования имели общую базу, то и после него они также будут иметь общую базу.

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

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

Восстановление структур (Иесопя^ис^ я^и^я). В итоге на основании вычисленной информации восстанавливаются структурные типы. Пусть Б — это множество всех меток. Оно разбивается на классы эквивалентности Б,,Б2,...,Бт, индуцируемые отношением «иметь общую базу», т. е. в одном множестве находятся все метки, имеющие одну базу. Пусть О, — это множество смещений, непосредственно относящихся к общей базе для элементов множества Б,. Это множество смещений рассматривается как множество смещений полей нового структурного типа , соответствующего множеству меток Б,.

Обновление множества объектов (Update_ object_sets). Для всех полей нового типа создаются объекты и добавляются в множество рабочих объектов алгоритма восстановления базовых типов. Теперь задача алгоритма восстановления базовых типов состоит в отображении типов полученных объектов в базовые типы языка С.

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

«о

о %

а.

si %

а 3 о

113

№ 6(24) 2009

«о §

«о

0 §

§

с

1

def сотроБ^е_гесопя^ис^оп

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

Предлагаемый алгоритм восстановления производных типов данных языка С имеет ряд недостатков, как-то:

1) не обрабатываются объединения и операции приведения типов указателей;

2) не во всех случаях имеется возможность восстановить размер массива;

3) вложенные структуры могут быть восстановлены только в том случае, если они представимы в виде массива.

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

Реализация и экспериментальная проверка

На рис. 1 представлена схема работы модуля восстановления типов данных.

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

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

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

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

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

Рис. 1. Организация работы модуля восстановления типов данных

114

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

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

^ № 6(24) 2009

«о

ресечение множеств троек атрибутов. В табл.1 * приведены результаты ручной проверки каче- £ ства восстановления типов данных. Колонка «CLOC» содержит количество строк кода в ис- ^ ходном коде на языке C, колонка «ALOC» со- а держит количество строк кода в соответст-  вующем ассемблерном коде. ^

Результаты статического восстановления uj типов данных представлены в табл. 2. Детальное восстановление типов данных на примере функции isnow представлено в табл. 3 (с. 116). Так как переменная char * endp используется только как аргумент функции getfield и никогда не разыменовывается, в результате только статического анализа она восстановлена как 4-хбайтная переменная типа int. Однако по результатам динамического анализа тип может быть восстановлен как указатель void*.

Колонка «ВТ» табл. 3 показывает количество переменных базового типа данных на стеке; переменные, расположенные на регистрах, не учитываются. Колонка «ExactBT» показывает количество переменных, тип которых был восстановлен точно, а колонка «CorrBT» показывает количество переменных, тип которых был восстановлен корректно. Колонка «FailBT» показывает количество переменных,

Таблица 1

Пример ручной проверки восстановления типов данных

Пример CLOC ALOC Описание

35_wc 107 245 cnt

36_cat 27 104 raw_cat

37_execute 486 1167 execute

38_day 173 346 isnow

Таблица 2

Статистика ручной проверки восстановления типов данных

Пример BT Exact BT Corr BT Fail BT PTRs Exact PTRs Corr PTRs Fail PTRs

35_wc 7 1 6 0 2 1 1 0

36_cat 7 4 3 0 1 0 0 0

37_execute 1 0 1 0 0 0 0 0

38_day 9 8 1 0 4 2 1 1

115

№ 6(24) 2009 ^

Таблица 3

Детальное восстановление типов функции ¡зпош

Адрес объекта Восстановленный тип Исходный тип

[ebp + 8] [unsigned] int, void* (profile-based) char *endp

[ebp + 12] int* int *monthp

[ebp + 16] int* int *dayp

[ebp + 20] [unsigned] int* int *varp

[ebp - 28] [unsigned] int* int flags

[ebp - 24] int int day

[ebp - 20] int int month

[ebp - 16] int int v1

[ebp - 12] int int v2

о *

5

Si

«о

IS

is

«о

о

6

§

с

»о

а »о а

m «о

о %

§

»о

«о

о §

S

QJ

a

is is

о g

тип которых не удалось восстановить ни точно, ни корректно. Колонка «PTR» содержит количество переменных типа «указатель на переменную базового типа». Колонки «EcaxtPTR», «CorrPTR», «FailPTR» показывают количество переменных указательного типа, тип которых удалось восстановить точно, корректно и не удалось восстановить ни точно, ни корректно соответственно.

На рис. 2 и рис. 3 показано графическое представление восстановления производных типов данных. На рис. 2 представлено восстановление структуры sturct shorts программы

typedef struct shorts { struct shorts *next; short value; } shorts;

short *lookaheads;

Рис. 2.

lalr.c. На рис. 3 представлено восстановление структуры struct tm из файла <time.h> программы day.c. Здесь следует заметить, что некоторые поля, например, поле tm_sec и другие, восстановлены как «pad», т. е. в виде заглушек, потому что в программе к ним не было обращений.

На рис. 4 представлен результат работы декомпилятора TyDec по восстановлению типов данных для примеров 59_lalr и 38_day. Представлено две программы. Слева расположено окно, отображающее входную программу,

0: struct* 0: struct*

4:uint16

Пример восстановления структурного типа программы lalr.c

struct tm {

int tm_sec, tm_min, tm_hour;

int tm_mday, tm_mon, tm_year;

int tm_wday, tm_yday, tm_isdst;

long tm_gmtoff;

char *tm zone;

pad

pad

pad

12: int32

16: int32

20: int32

24: int32

28: int32

Рис. 3. Пример восстановления структурного типа программы day.c

116

№ 6(24) 2009

з:

в §

3

о

Рис. 4. Результат работы компоненты восстановления типов данных

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

СПИСОК ЛИТЕРАТУРЫ

1. Деревенец Е. О., Трошина Е. Н. Структурный анализ в задаче декомпиляции II Прикладная информатика. № 4. С. 87-99.

2. Трошина Е. Н. Диссертация на соискание ученой степени кандидата физико-математических наук по специальности 05.i3.ii — Математическое и программное обеспечение вычислительных машин, комплексов и компьютерных сетей II Научная библиотека МГУ им. М. В. Ломоносова. 20.ii.2009.

3. Dolgova K., Chernov A. Automatic Type Reconstruction in Disassembled C Programs II Proceedings of IEEE i5th Working Conference on Reverse Engineering 2008. Antwerp, Belgium. October 2008. P. 202-206.

4. Troshina K. and Chernov A. High-Level Composite Type Reconstruction During Decompilation from Assembly Programs I Proceedings of 7th Perspectives of System Informatics. Akademgorodok. Novosibirsk. Russia. i5-i9 June 2009. P. 292-299.

5. Troshina K., Chernov A., DerevenetsY. C Decompilation: Is It Possible? II Proceedings of International

Workshop on Program Understanding. Altai Mountains, Russia. i9-23 June 2009. P. i8-27.

6. Долгова К. Н., Чернов А. В. Автоматическое восстановление типов в задаче декомпиляции II Программирование [журнал Российской академии наук]. № 2.2009. Март—апрель. С. 63-80.

7. MycroftA. Type-Based Decompilation II European Symp.on Programming. i999. Pp. 208-223.

8. Balacrishnan G., RepsT. DIVINE: Discovering variables in executables II Verification, Model Checking, and Abstract Interpretation. 2007. P. i-28.

9. Balakrishnan G., Ganai M. PED: Proof-guided Error Diagnosis by Triangulation of Program Error Causes II Proc. of Software Engineering and Formal Methods (SEFM), 2008.

10. ГусенкоМ. Ю. Декомпиляция типов данных исполняемых программ II Безопасность информационных технологий. i998. С. 83-88.

11. Cifuentes C., FrabouletA. Assembly to high-level language translation II Int. Conf.on Softw. Maint., i998. P. 223-237.

12. Cifuentes C., EmmerikM., Lewis B., Ramsey N. Experience in the Design, Implementation and Use of a Retargetable Static Binary Translation Framework I Technical Report, 2002.

13. Декомпилятор Hex-Rays II Hex-Rays Decompiler SDK. URL: http:Zwww.hex-rays.com.

14. Интерактивный дизассемблер Ida Pro. URL: http:Zwww.idapro.ru.

15. Muchnick S. Advanced Compiler Design and Implementation II Morgan Kaufmann Publishers, i997.

V 117