Статья 'Методика автоматизированного формирования структуры дизассемблированного листинга' - журнал 'Кибернетика и программирование' - NotaBene.ru
по
Journal Menu
> Issues > Rubrics > About journal > Authors > About the Journal > Requirements for publication > Council of Editors > Peer-review process > Policy of publication. Aims & Scope. > Article retraction > Ethics > Online First Pre-Publication > Copyright & Licensing Policy > Digital archiving policy > Open Access Policy > Article Processing Charge > Article Identification Policy > Plagiarism check policy
Journals in science databases
About the Journal

MAIN PAGE > Back to contents
Cybernetics and programming
Reference:

Methods for automated formation of a disassembled listing structure

Revnivykh Aleksandr Vladimirovich

PhD in Technical Science

Associate Professor, Department of Information Security, Novosibirsk State University of Economics and Management 

630099, Russia, Novosibirskaya oblast', g. Novosibirsk, ul. Kamenskaya, 56

al.revnivykh@mail.ru
Other publications by this author
 

 
Velizhanin Anatolii Sergeevich

Specialist, Tyumen Industrial University

625000, Russia, Tyumenskaya oblast', g. Tyumen', ul. Volodarskogo, 38

anatoliy.velizhanin@gmail.com
Other publications by this author
 

 

DOI:

10.25136/2644-5522.2019.2.28272

Received:

05-12-2018


Published:

25-12-2018


Abstract: The subject of the research is the method of splitting a disassembled code into logical blocks in automatic mode, searching for software vulnerabilities without using source code (using a binary file or its equivalent, obtained by reverse engineering).The object of the research is the existing code analyzers and features of their functionality.The aim of the study is to consider the possibility of splitting a disassembled code into logical blocks in automatic mode and some of the possible difficulties associated with this.Formulation of the problem. The complexity of analyzing large software products at the level of machine code necessitates the automation of this process. The research methodology is based on a combination of theoretical and empirical approaches using the methods of static and dynamic analysis, comparison, generalization, algorithmization, modeling, synthesis. Key findings. Splitting the code into blocks by sequential in line-by-line analysis of machine code in some cases can lead to misinterpretation. In addition, the analysis of the code according to the conclusions of the functions also does not guarantee the correctness of the determination of the boundaries of the functions. However, in general, the matrix method can be applied to analyze the dependencies of functions on the blocks of code thus selected.The scientific novelty is connected with the determination of promising vectors for the study of software code for vulnerability, the rationale for the approach (building the transition matrix from integer values), which may be the initial stage of preparation for the automated analysis of the disassembled code.


Keywords:

Information security, Vulnerabilities, Code analyses, Disassembling, FASM compiler, IDA Pro utility, Adjacency matrix, Matrix method, Code blocks, Matrix building algorithm

This article written in Russian. You can find original text of the article here .

Введение

1. Программное обеспечение (ПО), разнообразие аппаратных архитектур и технологий программирования

Различное программное обеспечение стало неотъемлемой частью фактически всех сфер нашей жизни. Это обусловлено широким внедрением различного информационно-вычислительного оборудования практически во все области деятельности человека. Примерами компьютерных систем являются обычные персональные компьютеры, сервера, мобильные телефоны, встраиваемые системы, а также многое другое. Современные микропроцессоры построены на различных принципах. Это архитектуры Intel x86, Intel x64, AMD64, Intel Itanium, ARM и т. п. Каждая из архитектур имеет свои особенности и принципы создания и функционирования программных компонентов в ней. Разнообразие аппаратных архитектур и технологий программирования формирует сложную гетерогенную структуру современных информационно-вычислительных систем, являющихся большими программно-аппаратными комплексами.

2. Обзор тенденций развития технологий разработки программного обеспечения

2.1. Технологии создания программных решений, технологии Rapid Application Development

Следует отметить, что технологий создания программных решений так же имеется множество, что обусловлено различными потребностями конечных пользователей и задачами, которые были поставлены перед программистом. Кроме того, в последнее время все большее распространение получают технологии RAD (Rapid Application Development) — быстрой разработки программных решений. Причиной этого является потребность производителей программного обеспечения в сокращении сроков производства по экономическим причинам. Таким образом, с течением времени, все большее число производителей программных систем стараются как можно дальше абстрагироваться от деталей функционирования программного обеспечения в целевой операционной системе и сделать максимальный упор именно на сокращение сроков производства.

2.2. Краткий обзор истории языков программирования

Бегло рассмотрим историю языков программирования на нескольких примерах.

2.2.1. Assembler

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

2.2.2. Языки программирования С и С++. Трансляция программного кода, написанного на высокоуровневых языках программирования, в машинные инструкции

Значительно позже появился язык программирования C (в конце 1970-х годов), позволяющий не только сократить время производства программных решений по сравнению с Assembler, но и предоставляющий возможность сборки программы без модификации (при условии, что программа не использует код платформенно-зависимых компонентов и т. п.) под другой архитектурой, а так же предлагающий более удобный для понимания человеком синтаксис, абстрагируя программиста от многих деталей функционирования микропроцессора. На тот момент это был своего рода прорыв в развитии языков программирования. Несколько позже появилась идея объектно-ориентированного подхода к разработке программного обеспечения, что привело к созданию языка программирования С++.

Программы, созданные на C/C++, в ряде ситуаций функционируют медленнее, чем аналогичные программы, написанные на Assembler. Связано это в том числе с увеличением фактически исполняемого процессором объема кода. Так, программа, написанная на языке Assembler, в бинарном представлении зачастую значительно меньше, чем аналогичная программа, написанная на языке программирования C/C++. Хотя данное утверждение не всегда справедливо при сравнении отдельных функциональных блоков, например, цикла с приращением переменной по определенному условию. Зачастую больший объем машинного кода после компиляции из высокоуровневых языков программирования связан с включением дополнительных подключаемых файлов, хранящих, например, используемые классы С++, которые зачастую содержат излишний (для данной конкретной задачи) функционал.

Таким образом, трансляция отдельных частей программного кода, написанного на высокоуровневых языках программирования, в машинные инструкции, может приводить к формированию значительно более объемного и, отчасти, избыточного кода, нежели непосредственное написание его на языке Assembler. Однако не во всех случаях код, написанный на C/C++, выполняется дольше [1]. Например, инструкции перехода чаще всего транслируются в соответствующие машинные инструкции и зачастую не формируют излишний машинный код. Кроме того, на скорость выполнения программы влияет и способ выполнения различных задач. В частности, различные алгоритмы могут быть реализованы несколькими способами не только с точки зрения алгоритмического построения, но и с точки зрения применяемых инструкций процессора для организации логики функционирования высокоуровневого программного кода.

2.2.3. Языки программирования Java, C#, JavaScript, Python, C# 4

Языки программирования Java, C# и т. п. в еще большей степени абстрагировали программиста даже от механизмов управления памятью, предложив «автоматическую сборку мусора», когда виртуальная машина самостоятельно решает какая память должна продолжать быть выделенной, а какую можно освободить. Кроме того, множество языков наподобие JavaScript, Python, C# 4 (в диалект именно этой версии добавлено ключевое слово dynamic) предлагают динамическую типизацию, что в еще большей степени абстрагирует даже от технологий межпроцессного взаимодействия, упрощает, например, управление OLE объектами и т. п.

Следует отметить, что каждое последующее поколение языков программирования все в большей степени удаляет программиста от любых деталей функционирования компьютерных систем, создавая своеобразную «песочницу», в которой компилятор и среда выполнения следит за правильностью функционирования большого числа низкоуровневых операций [2]. Данный факт, несомненно, положительно сказывается на сроках производства программных решений не только из-за сокращения времени, затраченного на разработку, но и за счет сокращения числа трудно обнаруживаемых ошибок в разрабатываемых программных компонентах (а значит и сокращения времени на отладку) благодаря жесткому контролю со стороны компилятора и особенностям синтаксиса самих языков программирования. С другой стороны, программное обеспечение (далее – ПО), разработанное на подобных языках программирования, во многих случаях функционирует значительно медленнее аналогичных программ, созданных на C/C++.

2.3. Уязвимости

В случае наличия уязвимости в какой-либо из динамических библиотек исполняющей среды или платформы, возникает вероятность эксплуатации данной уязвимости посредством программного обеспечения, функционирующего в уязвимой системе. Учитывая массовость распространения некоторых сред исполнения и платформ (например, NET Framework входит в стандартную поставку современных версий Microsoft Windows), данный факт может стать критичным [3]. Кроме того, дополнительный слой в виде виртуальной машины так же повышает общую сложность функционирования информационно-вычислительной системы, что может привести к дополнительным проблемам интеграции в и без того сложную систему [4].

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

2.4. Техника защиты интеллектуальной собственности на программное обеспечение, написанное на языках программирования с Just In Time-компиляцией

Отдельный вопрос представляет собой и техника защиты интеллектуальной собственности на программное обеспечение, написанное на языках программирования с JIT (Just In Time — точно в срок) компиляцией. Так, программы, написанные, например, на C# можно не только декомпилировать обратно в высокоуровневый исходный код, но и собрать в исполняемый файл снова, благодаря метаинформации, сохраняемой в исполняемых файлах компилятором. Программы, написанные на C++, декомпилировать в функционирующий и способный в дальнейшем быть скомпилированным каким-либо компилятором языка C/C++ высокоуровневый код до сих пор не удавалось, поскольку фактически вся информация о структуре программы в ходе сборки теряется [6].

2.5. Преобразование дизассемблированных листингов и машинного кода. Native-программные компоненты

Ведутся разработки, направленные на преобразование дизассемблированных листингов и машинного кода в некое подобие высокоуровневого кода на C/C++ (например, дизассемблер IDA Pro содержит в себе подобный функционал, предоставляя псевдокод, напоминающий по синтаксису код на языке программирования C/C++).

Не поддаются сборке и дизассемблированные коды без значительной предварительной модификации. Следует отметить, что не только программы, написанные на языке программирования C/C++, не поддаются такому декомпилированию в соответствующий высокоуровневый код. Например, рассматривая операционную систему Microsoft Windows, любое программное обеспечение, скомпилированное в режиме Native (непосредственно в машинные коды) не сможет быть качественно преобразовано в соответствующий высокоуровневый код, а лишь возможно получение дизассемблированных листингов и элементов псевдокода, предложенных, например, дизассемблером IDA Pro. В то же время, продолжая пример с Microsoft Windows, программы, написанные под платформу. NET Framework, например, на языке программирования C#, легко декомпилируются в соответствующий высокоуровневый код, например, с помощью утилиты .Net Reflector.

Для Native программных компонентов в большинстве случаев объем необходимой для выполнения компиляции дизассемблированных листингов модификации чрезвычайно велик. Однако, даже в случае ручного формирования кода, способного быть собранным в исполняемый файл из предварительно значительно обработанных дизассемблированных листингов, нельзя гарантировать правильного функционирования результирующего исполняемого файла, а значит и большой вероятности совершения ошибки при ручном исправлении кода [3, 7]. Кроме того, в ходе сборки, настройки компилятора и компоновщика могут привести к формированию несколько отличного файла. Также, сложно утверждать, что дизассемблер абсолютно безошибочно обработал двоичный файл, размеры которого могут быть не одну сотню мегабайт.

2.6. Производительность, ядро операционной системы Linux

Несмотря на широкое распространение языков программирования C#, Java, Python, JavaScript, предоставляющих множество различных преимуществ для программиста, некоторый функционал невозможно или неэффективно решать с их использованием. Причинами этому могут быть, например, вопросы производительности. Ядро операционной системы Linux, а также многих других операционных систем, написано на языке программирования С. Для его реализации, например, на Java потребовалось бы создание виртуальной машины, способной работать без операционной системы и самой, в каком-то смысле, являться своеобразной операционной системой. Реализовать такую виртуальную машину было бы возможно только на языках программирования, результатом обработки которых компилятором, является бинарный код с машинными инструкциями для данного микропроцессора. Исключением могут быть только случаи, если микропроцессор, например, во встроенной памяти имел бы небольшую виртуальную машину. Однако в таком случае, данная виртуальная машина будет своеобразной микрооперационной системой, реализующей базовый ограниченный функционал.

Таким образом, несмотря на нарастающую тенденцию в использовании языков программирования наподобие Java, C#, Python и т. п., мы имеем множество программных решений, разработанных и функционирующих с использованием программных модулей, написанных на языках C/C++, Assembler и т. п., формирующих машинный код в процессе сборки решения.

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

3.1. Ориентация на бинарный файл или его эквивалент, полученный реверс-инжинирингом. Fuzzing

Важным является поиск уязвимостей в программном обеспечении, ориентируясь не на исходные коды данного программного продукта, а именно на сам бинарный файл или его эквивалент, полученный реверс-инжинирингом. Кроме того, для анализа современных программных решений необходим ряд средств автоматизации этого процесса. На сегодняшний день, одним из распространенных методик поиска уязвимостей в ПО без использования исходного кода является Fuzzing [8]. Данная методика основана на формировании вариантов входных данных, а затем внедрении их в качестве входных данных в исполняемый файл, в ожидании на возникновение исключения в программном модуле [9].

Данная методика дала значительные результаты согласно источникам [8] и [10]. Однако для реализации данной методики необходимо знать точки входа информационных потоков в процесс. Подобного рода точек входа может быть весьма значительное количество. Это и обычные входные параметры, передаваемые через командную строку, и используемые переменные окружения, и межпроцессное взаимодействие, а также множество других вариантов внедрения информации в исполняющийся процесс. Помимо прочего, данный подход не гарантирует полнообъемности тестирования. Т. е. вполне возможно и наличие не протестированных веток потока выполнения. Данный вопрос рассмотрен в работе [10], где предложены некоторые варианты решения данной проблемы.

Так же важно отметить, что Fuzzing сам по себе является видом динамического анализа, а значит программный модуль, подверженный тестированию, запускается в определенной среде, которая может оказать непосредственное влияние на ход выполнения тестируемой программы, а, следовательно, и на результаты исследования [11]. Данный фактор может негативно сказаться в случаях, когда количество определенных точек входа в исследуемый процесс меньше, чем их реальное число.

3.2. Критерии выбора вектора исследования дизассемблированного листинга

3.2.1. Предварительный статический анализ.

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

Под вектором исследования дизассемблированного листинга будем понимать направление и последовательность переходов, потенциально образующих интересующий нас поток выполнения. На этапе статического анализа мы не можем гарантировать, что выполнение какой-либо отдельной ветки в коде по направлению данного вектора исследования в условиях какой-либо конкретной реальной системы будет выполнено [13]. В зависимости от цели исследования мы можем иметь различные вектора исследования даже для одного и того же программного модуля. Так, если мы пытаемся обнаружить уязвимости на срыв стека, то целесообразнее выбрать вектор исследования, который приведет к проходу по большему числу потенциально опасных точек в программе (среди таких точек могут быть вызовы функций стандартной библиотеки языка программирования С/С++, например, такие, как strcpy и т. п.) [14]. Для определения возможных векторов исследования на этапе статического анализа необходимо предварительно построить карту вызовов исследуемого модуля. Под картой вызовов будем понимать представление (текстовое, графическое или любое другое), отражающее последовательность вызовов функций. Подобные карты вызовов строят некоторые дизассемблеры, например, IDA Pro (графический пример отрывка карты вызовов изображен на рис. 1).

Рис. 1.Пример отрывка карты вызовов, построенный программой IDA Pro

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

3.2.2. Использование математических матриц

Одним из вариантов является использование математических матриц. Опираясь на то, что карта вызовов является графом, строится матрица смежности. Если A — матрица смежности графа G, то матрица Am обладает следующим свойством: элемент в i-й строке, j-м столбце равен числу путей из i-й вершины в j-ю, состоящих из ровно m ребер. Построив на основе данного матрицу достижимости, мы имеем информацию о достижимости вершин графа, а значит и о вызовах функций программы, согласно нашей задаче. В работе [10] данная методика была описана для определения конечной матрицы достижимости, которая отражает возможность перехода из заданной функции в некоторую другую. Но результирующая матрица не отражает промежуточных путей, а отражает лишь потенциальную возможность перехода и показывает только количественную оценку, насколько много вызовов других функций имеется у данной функции [15]. Кроме того, построение матрицы смежности, согласно источнику [10] предполагается во время динамического анализа.

Таким образом, одним из основных критериев выбора вектора исследования программного обеспечения предполагалось считать именно количество вызываемых функций. Однако исследование программных компонентов на уязвимости может более специфичным [16, 17]. Так, например, некоторые исследования могут быть направлены на поиск уязвимостей на срыв стека, а некоторые на поиск утечек памяти и т. п. [18]. Поиск определенного типа уязвимостей, вероятно, потребует более специфичных методик, а также наиболее перспективными могут оказаться несколько другие векторы исследования, нежели тестирование всех функций по очереди в зависимости от количества дочерних вызовов предложенное в источнике [10]. Так, например, наиболее интересными векторами исследования для обнаружения уязвимостей на срыв стека могут быть ветки кода, ведущие к большему количеству вызовов функций стандартной библиотеки C/C++ работы со строками. А для выявления ошибок управления памятью — функции работы с динамической памятью, указателями и т. п. [19] Таким образом, мы имеем необходимость хранения расширенной и структурированной информации о внутреннем устройстве программы. Характер хранимой дополнительной информации зависит от того, какой именно тип уязвимостей мы пытаемся обнаружить.

3.3. Способы хранения дополнительной информации

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

3.4. Определение степени опасности участка кода, использование для построения матриц непосредственно блоков кода

Для определения степени опасности участка кода в рамках определенного критерия предполагается использование различных правил, являющихся алгоритмами поиска. Например, в случае обнаружения кода, соответствующему определенному правилу, предлагается инкремент значения текущего элемента матрицы AIcurrJcurr [20].

В работе [10] элементами матрицы смежности в основном рассматривались отдельные функции, однако структура ассемблерного кода может быть чрезвычайно запутана и не во всех случаях можно четко и однозначно верно выделить границы функции. Проведем эксперимент (Листинг 1.1.). Основываясь на том, что по сути своей инструкции call и jmp в архитектуре Intel x86, а так же в Intel x86_64 по сути своей являются безусловными переходами, сформируем не сложный экспериментальный код на языке Assembler в программе-ассемблере fasm (flat assembler).

Листинг 1.

format PE64 GUI 5.0

entry start

section '.text' code readable executable

start:

sub rsp,8*5

mov r9d,0

lea r8,[_caption]

lea rdx,[_message]

mov rcx,0

call [MessageBoxA]

call proc_1;

after_proc_1_call:

call proc_2;

after_proc_2_call:

end_start_proc:

mov r9d,0

lea r8,[_caption]

lea rdx,[_message_end_start_proc]

mov rcx,0

call [MessageBoxA]

mov ecx,eax

call [ExitProcess]

proc_1:

sub rsp,8*5

mov r9d,0

lea r8,[_caption]

lea rdx,[_message_1]

mov rcx,0

call [MessageBoxA]

add rsp,8*5

jmp proc_2

proc_2:

sub rsp,8*5

mov r9d,0

lea r8,[_caption]

lea rdx,[_message_2]

mov rcx,0

call [MessageBoxA]

add rsp,8*5

ret

section '.data' data readable writeable

_caption db 'Win64 assembly program',0

_message db 'Hello World start_proc!',0

_message_1 db 'proc_1!',0

_message_2 db 'proc_2!',0

_message_end_start_proc db 'Good by start_proc!',0

section '.idata' import data readable writeable

dd 0,0,0,RVA kernel_name,RVA kernel_table

dd 0,0,0,RVA user_name,RVA user_table

dd 0,0,0,0,0

kernel_table:

ExitProcess dq RVA _ExitProcess

dq 0

user_table:

MessageBoxA dq RVA _MessageBoxA

dq 0

kernel_name db 'KERNEL32.DLL',0

user_name db 'USER32.DLL',0

_ExitProcess dw 0

db 'ExitProcess',0

_MessageBoxA dw 0

db 'MessageBoxA',0

4. Результаты

4.1. Результаты анализа кода, анализа перекрёстных ссылок IDA Pro 6.1

В данном листинге мы видим, что переход на метку «proc_1» и «proc_2» из кода в блоке «start» осуществляется командой «call». В то же время переход на метку «proc_2» осуществляется из блока кода от метки «proc_1» с помощью инструкции jmp. Данный код является несколько не типичным. Дизассемблер IDA Pro 6.1 проанализировав бинарный файл сообщает, что мы имеем 3 функции: «start», «sub_401058», «sub_401083». При этом в функции «sub_401058» в графическом представлении, открытом по умолчанию, можно увидеть сообщение «sub_401058 endp ; sp-analysis failed» на красном фоне (рис. 2, 3).

Рис. 2. Результат анализа кода в IDA Pro 6.1

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

Рис. 3. Результат анализа перекрестных ссылок IDA Pro 6.1

С точки зрения анализатора кода, встроенного в дизассемблер в данной программе, имеется 3 функции. Однако, это недостаточно точно отражает сути происходящего. Несмотря на то, что мы имеем инструкции перехода «call» на функции «proc_1» и «proc_2» из функции «start», на самом деле мы имеем дело и с промежуточным переходом их блока кода функции «proc_1» в блок кода функции «proc_2». Таким образом, анализатор кода, ограничив функцию «proc_1» адресом первой инструкции блока кода «proc_2», несколько неточно отразил происходящее, поскольку с позиции функции «proc_1» функция «proc_2» является всего лишь продолжением функции «proc_1». На рис. 1.2. изображен результат графического представления графа вызовов. Кроме того, следует отметить, что в самом языке программирования Assembler нет понятия функции или процедуры в привычном для высокоуровневых языков программирования смысле. Имеются лишь инструкции переходов. Так, инструкция «call» выполняет всего лишь безусловный переход, но в отличие от инструкции «jmp», помещает в стек адрес возврата. Инструкция «ret» производит обратный переход по адресу из стека и т. п. Все это несколько отличается от представления механизма работы сущности «функция» в высокоуровневых языках программирования.

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

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

4.2. Вариант реализации предложенного алгоритма

Вкратце рассмотрим один из вариантов реализации данного алгоритма.

1. На первом шаге предлагается разбить дизассемблированный код на блоки, а не на функции. Для разбиения мы последовательно перебираем инструкции дизассемблированного листинга и, когда встречаем инструкцию, которая может повлиять на ход выполнения программы (для архитектуры x86 и унаследованных это инструкции условного, безусловного переходов, «call», «ret»), закрываем текущий блок. Следующий блок начинается со следующей инструкции. Таким образом, в конце цикла мы получаем набор блоков, по конечным адресам которых находятся инструкции переходов.

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

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

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

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

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

Б) внешняя система хранения дополнительной информации используется тогда, когда мы сохраняем её в отдельных структурах данных. Например, в случаях, когда мы используем многокритериальный анализ веса элементов.

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

Заключение

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

References
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
Link to this article

You can simply select and copy link from below text field.


Other our sites:
Official Website of NOTA BENE / Aurora Group s.r.o.