2. Заголовок
• Константин Панарин, Positive Technologies,
kpanarin@ptsecurity.com
• Разработчик группы анализа низкоуровневых приложений
• Интересы: x86-64 reverse-engineering, C++ Template
Metaprogramming
#whoami
3. Заголовок
• Цели анализа бинарного кода
• Некоторые методики анализа
• Обзор проблем
• Обзор современных средств бинарного анализа
Содержание
4. Заголовок
• Поиск ошибок
• Поиск уязвимостей
• Поиск недекларированных возможностей (НДВ)
• Восстановление логики работы программы (RE)
• Построение тестов
Задачи анализа бинарного кода
5. ЗаголовокОсобенности бинарного анализа
• Почти полное отсутствие информации о типах*
• Гораздо сложнее локализовать различную «метаинформацию»
(например, обработчики исключений)
• В исполняемых файлах возможно применение обфускации
и антиотладочных приёмов, затрудняющих анализ
• Высокая семантическая нагрузка отдельных ассемблерных инструкций
(особенно на CISC архитектурах)
*Классы легко распознаются благодаря
наличию виртуальных таблиц, но что делать
с элементарными типами?
6. Заголовок
Типы анализа:
• Статический анализ
• Исполнения программы не происходит
• Динамический анализ
• Анализ по одной трассе исполнения
• Комбинированный анализ
Технологии, применяемые в анализе:
• Символьное исполнение (symbolic execution)
• Как правило, используется в статическом анализе
• Анализ помеченных данных (taint analysis)
• Как правило, применяется при динамическом анализе
• Fuzzing
• Ожидаемые входные данные подменяются случайными
или специально сформированными
• И многие другие
Методики анализа бинарного кода
7. Заголовок
Типы анализа:
• Статический анализ
• Исполнения программы не происходит
• Динамический анализ
• Анализ по одной трассе исполнения
• Комбинированный анализ
Технологии, применяемые в анализе:
• Символьное исполнение (symbolic execution)
• Как правило, используется в статическом анализе
• Анализ помеченных данных (taint analysis)
• Как правило, применяется при динамическом анализе
• Fuzzing
• Ожидаемые входные данные подменяются случайными или специально
сформированными
Методики анализа бинарного кода
На практике инструменты анализа комбинируют в себе различные типы и технологии
из-за ограничений, существующих в них. Согласованное применение различных подходов
позволяет преодолевать эти ограничения полностью или частично
8. ЗаголовокСтатический анализ vs динамический анализ
Динамический анализ
• Наличие run-time информации:
карты памяти процесса, адресов
неявных вызовов и др.
• Явное исполнение программы
может требовать специфического
окружения
• Не всегда возможно
воспроизвести результаты
анализа
Статический анализ
• Как правило, работает быстрее
• Один анализ покрывает
потенциально бесконечное число
путей исполнения
• Работоспособен при отсутствии
части исходников / библиотек
• Пасует перед обфускацией
и шифрованием
• Отсутствие информации
о неявных вызовах
9. Заголовок
• Основная идея – замена конкретных входных данных
программы (аргументов функции) на символьные
• Символ представляет множество всех возможных
значений переменной
• Вместо конкретных значений программа будет обрабатывать
символьные выражения
• Символьное исполнение способно покрывать все возможные пути
в программе
• Каждый путь – это «состояние» программы, в котором хранятся условия
прохождения по этому пути (path constraints) и набор ограничений
на значения символьных данных (value constraints)
• SMT решатель (solver) – инструмент, определяющий совместность
(разрешимость) условий для прохождения по заданному пути
Символьное исполнение
10. Заголовок
int twice(int v) {
return 2 * v;
}
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
11. Заголовок
int twice(int v) {
return 2 * v;
}
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
12. Заголовок
int twice(int v) {
return 2 * v;
}
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
Символьно исполняем вызов функции
13. Заголовок
int twice(int v) {
return 2 * v;
}
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)
14. Заголовок
int twice(int v) {
return 2 * v;
}
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 ^ x0 > y0+10
Value constraints:
X->x0
Y->y0
Z->2*y0
Path constraints:
x0 =2y0 ^ x0 <= y0+10
Исследуем условие x==z
15. Заголовок
int twice(int v) {
return 2 * v;
}
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 ^ x0 > y0+10
Условие достижимости ERROR:
16. Заголовок
int twice(int v) {
return 2 * v;
}
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 ^ x0 > y0+10
Условие достижимости ERROR:
SMT Solver выдает решение:
x0 = 40, y0 = 20
17. ЗаголовокСимвольное исполнение: общая схема
Транслятор
в IR
Ассемблерная
инструкция
Набор
инструкций 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 – выбирает
состояние из пула
18. ЗаголовокSymbolic execution: проблемы
• Path explosion (как генерировать меньшее число состояний?)
• Cycle-unrolling (что делать с циклами, условие остановки
которых зависит от символьной переменной?)
• Symbolic pointers (что делать с операциями load и store, адрес
которых тоже символический?)
• Constraint difficulty (не все SMT-solver’ы справятся
с нахождением решения)
• External resources (что делать с файлами, хэндлерами
и другими внешними объектами?)
19. ЗаголовокSymbolic execution: возможные пути решения проблем
• Path explosion – мёрджить (объединять) несколько состояний
в одно (но как и когда это делать?)
• Path explosion – распараллелить обработку различных
состояний
• Cycle unrolling, symbolic pointers – применять специальные
логики, созданные для верифицирования программ
(но насколько это эффективно?)
• External resources – создать DSL для описания внешних вызовов
в терминах executor’а или SMT-solver’а (насколько это быстро
и реализуемо?)
20. ЗаголовокSymbolic execution: ссылки
• KLEE: Unassisted and Automatic Generation 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)
21. Заголовок
• Исключительно динамический метод анализа
• Связывает трассу исполнения программы с данными, которые
обрабатывались в ней в процессе этого исполнения
• Помогает дать ответ на вопрос о том, как именно программа
обрабатывала те или иные входные данные
Анализ помеченных данных (taint analysis)
23. Заголовок
mov eax, tainted_input
xor eax, eax ; eax is UNTAINTED
-----------------------------------------
push tainted_input
pop eax ; eax is TAINTED, dword[esp + 4] is
TAINTED
-----------------------------------------------------------------
xor eax, eax
cmp eax, tainted_input ; AF, CF, OF, PF, SF, ZF
is TAINTED
Taint propagation: примеры
mov eax, tainted _input
mov ecx, untainted_input
add ecx, eax ; ecx is TAINTED
-----------------------------------------
mov eax, tainted_input
mov ecx, untainted_input
mov ax, cx ; ax is UNTAINTED, eax is TAINTED
-----------------------------------------------------------------
Пример взят из http://defcon.org.ua/data/1/4_Oleksyk_Code_Analysis.pdf
24. ЗаголовокTaint analysis: общая схема
Program code:
________________
push ebp
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
25. ЗаголовокTaint analysis
Чем полезен taint-analysis:
• Tainted EIP говорит о возможности перехвата управления
(например, в результате stackheap overflow)
• Tainted arguments в некоторых функциях (например, форматная строка
в printf или строка команды в system) говорят о возможной уязвимости
• Tainted resources (например, хэндлеры, мьютексы и пр., которые не зависят
напрямую от пользовательского ввода) говорят о возможной ошибке
в программе
Недостатки:
• По своей природе требует детального анализа каждой исполняемой
инструкции, что может быть очень тяжело для набора x86
• Идеальный taint analysis должен отслеживать и инструментировать весь код,
исполняемый операционной системой (как в режиме пользователя,
так и в режиме ядра) Чревато низкой производительностью анализа
26. ЗаголовокTaint analysis: ссылки
• All You Ever Wanted to 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)
27. ЗаголовокКомбинированный анализ – concolic execution
Concrete + symbolic = concolic:
• Для некоторых символьных переменных используются их «конкретные» значения
при символьном исполнении
Применение:
• На контрольных точках создаём снимок всего процесса
• Инструментируем конкретную трассу: делаем taint-analysis и одновременно набираем
очередь символьных условий (constraints) для каждой инструкции перехода на пути
• После завершения анализа текущей трассы откатываем процесс к контрольной точке,
выбираем символьное условие из очереди, решаем для него SMT-задачу, полученное
решение (регистры и участки памяти) подставляем в память и контекст процесса
• Инструментируем новую трассу
Concolic execution – метод, применяемый для покрытия
максимального количества кода
28. Заголовок
int twice(int v) {
return 2 * v;
}
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
29. Заголовок
int twice(int v) {
return 2 * v;
}
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
30. Заголовок
int twice(int v) {
return 2 * v;
}
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
31. Заголовок
int twice(int v) {
return 2 * v;
}
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
Исполняем вызов функции
32. Заголовок
int twice(int v) {
return 2 * v;
}
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
33. Заголовок
int twice(int v) {
return 2 * v;
}
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
34. Заголовок
int twice(int v) {
return 2 * v;
}
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
35. Заголовок
int twice(int v) {
return 2 * v;
}
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, исходя
из новых условий.
36. ЗаголовокСуществующие инструменты (Open Source)
KLEE
• Базируется на llvm IR
• Использует символьное исполнение
• Автоматическая генерация тестов
(максимальное покрытие исходного кода)
• Имеет несколько стратегий выбора состояний
в процессе symbolic execution
KLEE используется в S2E – платформе для анализа исполнения
приложений в «реальной» среде
37. ЗаголовокСуществующие инструменты (Open Source)
Triton
• Реализует схему concolic execution
• Переводит инструкции непосредственно в выражения solver’а
(Z3), минуя внутреннее представление
Другие: FuzzBall, BitBlaze, Avalanche и прочие
• Как правило, нет инструментов надлежащего продуктового
качества
• Каждый инструмент «заточен» под решение некоторой своей
специфичной задачи
38. ЗаголовокСуществующие инструменты (Closed Source)
MAYHEM
• Создан для поиска и автоматической генерации exploit’ов
• Есть продвижение в работе с символьными адресами
• Победитель конкурса DARPA в 2016
CodeSurfer, VeraCode
• Платные инструменты бинарного анализа
• Очень мало информации о деталях их работы
39. ЗаголовокОбщий вывод
• Методики бинарного анализа все еще нуждаются в глубоких
исследованиях
• В данный момент не существует универсального инструмента
бинарного анализа
• Каждый инструмент решает какую-либо конкретную задачу,
обходя известные ограничения за счет качества анализа
• Positive Technologies работает над своим инструментом – STAY
TUNED!
41. ЗаголовокСимвольное исполнение: общая схема
Транслятор
в IR
Ассемблерная
инструкция
Набор
инструкций 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
Добавляем
новые
состояния
в пул