типах* • Гораздо сложнее локализовать различную «метаинформацию» (например, обработчики исключений) • В исполняемых файлах возможно применение обфускации и антиотладочных приёмов, затрудняющих анализ • Высокая семантическая нагрузка отдельных ассемблерных инструкций (особенно на CISC архитектурах) *Классы легко распознаются благодаря наличию виртуальных таблиц, но что делать с элементарными типами?
происходит • Динамический анализ • Анализ по одной трассе исполнения • Комбинированный анализ Технологии, применяемые в анализе: • Символьное исполнение (symbolic execution) • Как правило, используется в статическом анализе • Анализ помеченных данных (taint analysis) • Как правило, применяется при динамическом анализе • Fuzzing • Ожидаемые входные данные подменяются случайными или специально сформированными • И многие другие Методики анализа бинарного кода
происходит • Динамический анализ • Анализ по одной трассе исполнения • Комбинированный анализ Технологии, применяемые в анализе: • Символьное исполнение (symbolic execution) • Как правило, используется в статическом анализе • Анализ помеченных данных (taint analysis) • Как правило, применяется при динамическом анализе • Fuzzing • Ожидаемые входные данные подменяются случайными или специально сформированными Методики анализа бинарного кода На практике инструменты анализа комбинируют в себе различные типы и технологии из-за ограничений, существующих в них. Согласованное применение различных подходов позволяет преодолевать эти ограничения полностью или частично
run-time информации: карты памяти процесса, адресов неявных вызовов и др. • Явное исполнение программы может требовать специфического окружения • Не всегда возможно воспроизвести результаты анализа Статический анализ • Как правило, работает быстрее • Один анализ покрывает потенциально бесконечное число путей исполнения • Работоспособен при отсутствии части исходников / библиотек • Пасует перед обфускацией и шифрованием • Отсутствие информации о неявных вызовах
(аргументов функции) на символьные • Символ представляет множество всех возможных значений переменной • Вместо конкретных значений программа будет обрабатывать символьные выражения • Символьное исполнение способно покрывать все возможные пути в программе • Каждый путь – это «состояние» программы, в котором хранятся условия прохождения по этому пути (path constraints) и набор ограничений на значения символьных данных (value constraints) • SMT решатель (solver) – инструмент, определяющий совместность (разрешимость) условий для прохождения по заданному пути Символьное исполнение
void test(int x, int y) { z = twice(y); if (x == z) { if (x > y + 10) ERROR; } } int main() { x = read(); y = read(); test(x,y); } Символьное исполнение: пример При помощи символьного исполнения найдем значения x и y, при которых исполнение попадет в ERROR Пример взят из http://www.srl.inf.ethz.ch/pa2015/Lecture8.pdf
void test(int x, int y) { z = twice(y); if (x == z) { if (x > y + 10) ERROR; } } int main() { x = read(); y = read(); test(x,y); } Символьное исполнение: пример Value constraints: X->x0 Y->y0 Path constraints: True
void test(int x, int y) { z = twice(y); if (x == z) { if (x > y + 10) ERROR; } } int main() { x = read(); y = read(); test(x,y); } Символьное исполнение: пример Value constraints: X->x0 Y->y0 Z->2*y0 Path constraints: True Символьно исполняем вызов функции
void test(int x, int y) { z = twice(y); if (x == z) { if (x > y + 10) ERROR; } } int main() { x = read(); y = read(); test(x,y); } Символьное исполнение: пример Value constraints: X->x0 Y->y0 Z->2*y0 Path constraints: x0 = 2y0 Value constraints: X->x0 Y->y0 Z->2*y0 Path constraints: x0 != 2y0 Два различных состояния после условного перехода if (x==z)
Набор инструкций IR Пул состояний (по одному для каждого пути исполнения) State №1 State №2 State №… State №500 Каждое состояние хранит следующие данные: • Текущий IP (instruction pointer) • Символьный контекст (регистры, ячейки памяти, символьные ресурсы) • Constraints Executor (director) – занимается обработкой конкретного состояния X86: mov eax, ecx ___________________ IR: STR R_ECX:32, , V_00:32 STR V_00:32, , R_EAX:32 Интерпретатор – содержит обработчики для каждой инструкции IR Трансляция Инструкция перехода по условию X (branch) Если достигнута контрольная точка программы, проверяем её достижимость: извлекаем path constraints, решаем SMT- задачу SMT-Solvers: Z3, STP, Boolector New state a: Constraints += X New state b: Constraints += ~X Добавляем новые состояния в пул Searcher – выбирает состояние из пула
число состояний?) • Cycle-unrolling (что делать с циклами, условие остановки которых зависит от символьной переменной?) • Symbolic pointers (что делать с операциями load и store, адрес которых тоже символический?) • Constraint difficulty (не все SMT-solver’ы справятся с нахождением решения) • External resources (что делать с файлами, хэндлерами и другими внешними объектами?)
– мёрджить (объединять) несколько состояний в одно (но как и когда это делать?) • Path explosion – распараллелить обработку различных состояний • Cycle unrolling, symbolic pointers – применять специальные логики, созданные для верифицирования программ (но насколько это эффективно?) • External resources – создать DSL для описания внешних вызовов в терминах executor’а или SMT-solver’а (насколько это быстро и реализуемо?)
of High-Coverage Tests for Complex Systems Programs (C. Cadar, D. Dunbar, D. Engler) • Unleashing MAYHEM on Binary Code (S. Cha, T. Avgerinos, A. Rebert and D. Brumley) • S2E: A Platform for In-Vivo Multi-Path Analysis of Software Systems (V. Chipounov, V. Kuznetsov, G. Candea)
программы с данными, которые обрабатывались в ней в процессе этого исполнения • Помогает дать ответ на вопрос о том, как именно программа обрабатывала те или иные входные данные Анализ помеченных данных (taint analysis)
mov ebp, esp lea eax, [esp+8] … ret Анализ исполняемых инструкций во время исполнения add eax, [esp+8] Instruction handler: Синтаксический парсинг инструкции на операнды, разрешение адресов у memory операндов Taint context EBX: not tainted Taint propagation ECX: tainted … EDI: tainted EAX: not tainted SHADOW MEMORY Операнды: dest - eax, src: eax, 0x7f2300 Чтение контекста: eax – not tainted 0x7f2300 - tainted Запись контекста: eax – tainted
о возможности перехвата управления (например, в результате stack\heap overflow) • Tainted arguments в некоторых функциях (например, форматная строка в printf или строка команды в system) говорят о возможной уязвимости • Tainted resources (например, хэндлеры, мьютексы и пр., которые не зависят напрямую от пользовательского ввода) говорят о возможной ошибке в программе Недостатки: • По своей природе требует детального анализа каждой исполняемой инструкции, что может быть очень тяжело для набора x86 • Идеальный taint analysis должен отслеживать и инструментировать весь код, исполняемый операционной системой (как в режиме пользователя, так и в режиме ядра) Чревато низкой производительностью анализа
Know About Dynamic Taint Analysis and Forward Symbolic Execution (but might have been afraid to ask) E. Schwartz, T. Avgerinos, D. Brumley • Dynamic taint analysis: Automatic detection, analysis, and signature generation of exploit attacks on commodity software (J. Newsome , D. Song , J. Newsome, D. Song) • Program slicing (M. Weiser)
concolic: • Для некоторых символьных переменных используются их «конкретные» значения при символьном исполнении Применение: • На контрольных точках создаём снимок всего процесса • Инструментируем конкретную трассу: делаем taint-analysis и одновременно набираем очередь символьных условий (constraints) для каждой инструкции перехода на пути • После завершения анализа текущей трассы откатываем процесс к контрольной точке, выбираем символьное условие из очереди, решаем для него SMT-задачу, полученное решение (регистры и участки памяти) подставляем в память и контекст процесса • Инструментируем новую трассу Concolic execution – метод, применяемый для покрытия максимального количества кода
void test(int x, int y) { z = twice(y); if (x == z) { if (x > y + 10) ERROR; } } int main() { x = read(); y = read(); test(x,y); } Комбинированный анализ: пример Теперь воспользуемся техникой concolic execution и найдем значения x и y, при которых исполнение попадет в ERROR
void test(int x, int y) { z = twice(y); if (x == z) { if (x > y + 10) ERROR; } } int main() { x = read(); y = read(); test(x,y); } Комбинированный анализ: пример Предположим, что функция read вернула «конкретные» значения X=22 Y=7
void test(int x, int y) { z = twice(y); if (x == z) { if (x > y + 10) ERROR; } } int main() { x = read(); y = read(); test(x,y); } Комбинированный анализ: пример «Конкретные» значения: X=22 Y=7 Делаем снимок процесса в точке входа в функцию test Value constraints: X->x0 Y->y0 Path constraints: True
void test(int x, int y) { z = twice(y); if (x == z) { if (x > y + 10) ERROR; } } int main() { x = read(); y = read(); test(x,y); } Комбинированный анализ: пример «Конкретные» значения: X=22 Y=7 Z=14 Value constraints: X->x0 Y->y0 Z->2*y0 Path constraints: True Исполняем вызов функции
void test(int x, int y) { z = twice(y); if (x == z) { if (x > y + 10) ERROR; } } int main() { x = read(); y = read(); test(x,y); } Комбинированный анализ: пример «Конкретные» значения: X=22 Y=7 Z=14 Value constraints: X->x0 Y->y0 Z->2*y0 Path constraints: X0 != 2*y0 Заталкиваем X0 == 2*y0 в пул собранных условий «Конкретное» исполнение пойдет по ветке else
void test(int x, int y) { z = twice(y); if (x == z) { if (x > y + 10) ERROR; } } int main() { x = read(); y = read(); test(x,y); } Комбинированный анализ: пример «Конкретные» значения: X=2 Y=1 Z=2 Value constraints: X->x0 Y->y0 Z->2*y0 Path constraints: x0 = 2*y0 Символьное исполнение вычислит новые x и y, чтобы пойти по ветке true, и «конкретное» исполнение будет перезапущено с точки входа в test
void test(int x, int y) { z = twice(y); if (x == z) { if (x > y + 10) ERROR; } } int main() { x = read(); y = read(); test(x,y); } Комбинированный анализ: пример «Конкретные» значения: X=2 Y=1 Z=2 Value constraints: X->x0 Y->y0 Z->2*y0 Path constraints: x0 = 2*y0 ^ x0 <= y0 + 10 Однако «конкретное» исполнение опять не дойдёт до error
void test(int x, int y) { z = twice(y); if (x == z) { if (x > y + 10) ERROR; } } int main() { x = read(); y = read(); test(x,y); } Комбинированный анализ: пример «Конкретные» значения: X=30 Y=15 Z=30 Value constraints: X->x0 Y->y0 Z->2*y0 Path constraints: x0 = 2*y0 ^ x0 > y0 + 10 Символьное исполнение вычислит новые значения x и y для нужных path constraints, откатимся к снимку и подменим x и y, исходя из новых условий.
IR • Использует символьное исполнение • Автоматическая генерация тестов (максимальное покрытие исходного кода) • Имеет несколько стратегий выбора состояний в процессе symbolic execution KLEE используется в S2E – платформе для анализа исполнения приложений в «реальной» среде
execution • Переводит инструкции непосредственно в выражения solver’а (Z3), минуя внутреннее представление Другие: FuzzBall, BitBlaze, Avalanche и прочие • Как правило, нет инструментов надлежащего продуктового качества • Каждый инструмент «заточен» под решение некоторой своей специфичной задачи
и автоматической генерации exploit’ов • Есть продвижение в работе с символьными адресами • Победитель конкурса DARPA в 2016 CodeSurfer, VeraCode • Платные инструменты бинарного анализа • Очень мало информации о деталях их работы
в глубоких исследованиях • В данный момент не существует универсального инструмента бинарного анализа • Каждый инструмент решает какую-либо конкретную задачу, обходя известные ограничения за счет качества анализа • Positive Technologies работает над своим инструментом – STAY TUNED!
Набор инструкций IR Пул состояний (по одному для каждого пути исполнения) State №1 State №2 State №… State №500 Searcher – выбирает состояние из пула. Возможные стратегии выбора: • DPS BPS • Random choice • Best coverage state Каждое состояние хранит следующие данные: • Текущий IP (instruction pointer) • Символьный контекст (регистры, ячейки памяти, символьные ресурсы) • Path constraints Executor (director) – занимается обработкой конкретного состояния mov eax, ecx ___________________ STR R_ECX:32, , V_00:32 STR V_00:32, , R_EAX:32 Интерпретатор – содержит обработчики для каждой инструкции IR Трансляция Логическая или арифметическая микроинструкция: xor, and, or, bvadd, bvsub и пр. – изменить символьный контекст обрабатываемого состояния Обработка внешнего вызова: изменить символьный контекст в соответствии с семантикой, приписанной (в DSL) конкретной сторонней функции База с семантикой внешних функций Микроинструкции аллокации памяти / работы с памятью: создание новой или изменение существующей символьной ячейки памяти для обрабатываемого состояния Микроинструкции передачи управления по условию X Если достигнута контрольная точка программы, проверяем её достижимость: извлекаем path constraints, решаем SMT-задачу SMT-Solvers: Z3, STP, Boolector New state a: Constraints += X New state b: Constraints += ~X Добавляем новые состояния в пул