Содержание статьи
Много кто помнит черно-белый интерфейс SoftICE с просмотром дизассемблерного листинга и содержимого регистров. Еще больше человек знают об IDA Pro (декомпилятор не рассматриваем) и бессменном objdump (в случае линуксоидов). Однако времена ручного разбора ассемблера прошли. Сейчас большую популярность приобрели инструменты преобразования нативного кода в некое абстрактное представление для упрощения анализа. Подобные инструменты используются повсеместно — от набора компиляторов на основе LLVM до декомпилятора HexRays. В данной статье я попытаюсь объяснить основы этого метода на примере ESIL от проекта radare2.
Введение
Для начала разберем, что же такое IL/IR (Intermediate Language / Intermediate Representation). Промежуточное представление повсеместно используется как для задачи компиляции, так и для обратной задачи. Поскольку в мире очень много различных кросс-платформенных компиляторов, число IL/IR для них поистине огромно. Так что имеет смысл остановиться лишь на наиболее известных, ключевых и выделяющихся представителях и только из мира декомпиляции. Основная идея IL/IR — возможность преобразования машинно зависимого кода в абстрактное представление, достаточное для анализа.
Ввиду разных задач и большого количества и разнообразия поддерживаемых архитектур ключевые особенности разных промежуточных представлений могут различаться очень существенно. При этом необходимо помнить, что количество поддерживаемых операций должно быть достаточно мало, чтобы облегчить алгоритмы последующего анализа. Кроме непосредственно лифтинга промежуточного представления в SSA (Static single assignment) форму и далее свертки в некое конечное представление с генерацией кода, IL/IR может применяться и для более специфического анализа: решения SMT (Satisfiability modulo theories) условий на его базе, автоматического поиска уязвимостей (AEG) и так далее.
Я специально не буду упоминать здесь LLVM IR: информации о нем очень много, сам он документирован, и существует большое количество утилит, его использующих. Плюс к этому так как он был создан для нужд компиляции, то не всегда может конкурировать с другими представителями. Давай познакомимся с наиболее яркими и известными из них.
REIL
Впервые этот язык появился в утилитах BinNavi и BinDiff небезызвестной компании Zynamics. Обе эти утилиты были созданы как плагины для IDA Pro и использовались вместе с ней. Один из первых языков промежуточного представления, получивших широкую известность в мире реверс-инжиниринга. Поддерживает архитектуры x86, ARM, PowerPC. Представляет собой абстрактную машину с бесконечной памятью и бесконечным количеством регистров. Достаточно небольшое количество инструкций — всего семнадцать, таким образом последующий анализ затруднен из-за потери большого количества информации об оригинальной структуре программы. Оригинальные утилиты были проприетарными (до этого года), что осложняло взаимодействие с ними.
BAP
BAP — это собирательное имя для целого фреймворка. Само же промежуточное представление носит имя BIL и поддерживает две платформы: x86 и ARM. На базе BIL построена как сама Binary Analysis Platform, так и другие утилиты: TEMU, Qira, плагины к IDA Pro и Immunity. Также существует возможность конвертации его в язык VEX (наиболее известен как часть Valgrind). Большой плюс BAP — в открытости кода и процесса разработки. Однако написан он на языке OCaml, что существенно поднимает планку для желающих участвовать в проекте.
BitBlaze, VEX, Vine
BitBlaze — платформа, аналогичная BAP, однако имеет два уровня и два промежуточных представления. Низкоуровневый язык — Vine IL, более высокоуровневый — VEX. В Vine IL используется прямое указание всех side-эффектов инструкций, для обеспечения точности «перевода» и выполнения (например, для поиска утечек памяти с помощью Valgrind). Однако подобная точность трансляции налагает ограничения в глубине анализа. Поэтому платформа предлагает также и высокоуровневое представление VEX, позволяющее скрыть неважные для анализа данные. VEX, как и REIL, представляет собой абстрактную машину с бесконечной памятью и бесконечным количеством регистров. Для упрощения последующего анализа VEX имеет поддержку типов и области действия переменных. Так же как и в BAP, большая часть кода написана на OCaml.
RREIL, OpenREIL, MAIL
RREIL был создан под влиянием REIL, и в то же время все концепции, его составляющие, пересмотрены полностью, с нуля. Наиболее выделяются две вещи: поддержка типов на базовом уровне и концепция «доменов».
MAIL — промежуточный язык «специального назначения». Его создатели ставили перед собой главную цель — упростить массированный анализ вредоносных программ (отсюда и название — Malware Analysis Intermediate Language). Этот язык также содержит в себе ряд интересных идей, главное же отличие от всех остальных — возможность трансляции самомодифицирующихся программ на уровне промежуточного представления.
OpenREIL — «перезагрузка» REIL, представляет собой написанный на Python фреймворк, использующий в своей основе libasmir (взят из BAP, основан на libVEX) для трансляции бинарного кода, с последующей конвертацией его в язык, созданный по подобию REIL, однако отличающийся от него. Главной целью фреймворка поставлена возможность использования его для последующего анализа, например генерации SMT-представления с дальнейшим его решением. Также включает в себя плагины для интеграции с GDB, WinDbg и IDA Pro. OpenREIL поддерживает x86 и ARM (включая Thumb).
ESIL
Ввиду того что многие промежуточные языки и утилиты, их использующие, как правило, создавались только для архитектур x86 и ARM, команда radare2 решила создать собственное промежуточное представление «низкого уровня». На тот момент фреймворк уже поддерживал большое количество архитектур, от 8-битных микроконтроллеров до 48/96-битных DSP. Поэтому при создании языка учитывались особенности, необходимые для как можно большей его универсальности.
Аббревиатура ESIL расшифровывается как Evaluable Strings Intermediate Language, что сразу дает понять главное отличие от других подобных языков — текстовое представление и есть само содержание (во многих других языках текстовое представление — это своеобразная «расшифровка» байт-кода). Более того, чтобы ускорить парсинг и облегчить написание сторонних утилит, ESIL использует обратную польскую нотацию записи опкодов. Как и упомянутый выше VEX, ESIL предназначен в первую очередь для «точной» трансляции в более абстрактное представление или же эмуляции. Это делает обязательным прямое указание «побочных» эффектов для каждой инструкции. Рассмотрим поближе, что же собой представляет ESIL. На рис. 1 приведена таблица с примерами некоторых опкодов (полный список доступных опкодов можно посмотреть здесь).
Как и все описанные выше языки промежуточного представления, ESIL не имеет (пока) поддержки операций с плавающей точкой, представляет собой абстрактную виртуальную машину с бесконечной памятью и бесконечным количеством регистров. Кроме того, он позволяет использовать «алиасы» для регистров, привычные для выбранной архитектуры (например, алиас EAX для регистра R0). В самой реализации виртуальной машины есть возможность добавлять собственные операнды и устанавливать хуки на любую инструкцию. Плюс возможность перенаправлять часть кода в нативное исполнение (например, syscall’ы).
Практическое применение
Поскольку ESIL — детище проекта radare2, то и практические примеры мы будем рассматривать с помощью этого фреймворка. Некоторые основы работы с самим фреймворком можно посмотреть в статье «Основы работы с фреймворком radare». Главное — помнить, что большинство команд radare2 по сути аббревиатуры того действия, которое надо выполнить (например, pae
— print analysis esil или aes
— analysis esil step). Это поможет легко разобраться с любыми новыми командами.
Базовым методом применения ESIL был и остается ручной механизм запуска виртуальной машины с указанием необходимых параметров. Для этого нам понадобятся следующие команды:
ae*
— набор инструкций;aei
— инициализация ESIL VM;aeim
— инициализация стека/памяти VM;aeip
— установка IP (Instruction Pointer);aes
— step в режиме эмуляции ESIL;aec[u]
— continue [until];aef
— эмуляция функции.
Подробнее можно посмотреть в записи asciinema.
Для разнообразия попробуем этот метод не на x86-архитектуре, а на эмуляции прошивки микроконтроллера 8051:
r2 -a 8051 ite_it8502.rom
;[0x00000000]> . ite_it8502.r2
;[0x00000000]> e io.cache=true
для использования кеширования IO;- запустим
aei
; - запустим
aeim
; - запустим
aeip
для старта с момента указания команды; aecu [addr]
для эмуляции, пока не достигнем IP = [addr].
Второй наиболее распространенный режим работы с ESIL — эмуляция на лету. По сути, это эмуляция того кода, который мы видим в визуальном режиме. Для этого нам достаточно лишь выставить переменную e asm.emu=true
.
Как мы видим, в этом режиме добавляются не только комментарии, показывающие значения регистров во время эмуляции, но и вероятность того или иного перехода (likely/unlikely). Есть и третий режим работы, активизируется он переменной e asm.esil=true
. Он заменяет вывод дизассемблерного листинга на вывод ESIL.
Обособленно стоят команды работы с эмуляцией ESIL, повторяющей интерфейс обычного отладчика. За это отвечают команды de
:
[0x100404a90]> de?
Usage: de[-sc] [rwx] [rm] [expr]
Examples:
> de # list esil watchpoints
> de-* # delete all esil watchpoints
> de r r rip # stop when reads rip
> de rw m ADDR # stop when read or write in ADDR
> de w r rdx # stop when rdx register is modified
> de x m FROM..TO # stop when rip in range
> dec # continue execution until matching expression
> des [num] # step-in N instructions with esildebug
> desu [addr] # esildebug until specific address
TODO: Add support for conditionals in expressions like rcx == 4 or rcx<10
TODO: Turn on/off debugger trace of esil debugging
Еще один вариант работы с ESIL — конвертация его в другие языки промежуточного представления, например REIL (а точнее, диалект OpenREIL). Конвертация ESIL -> REIL уже включена в базовый набор radare2 и выполняется с помощью команды aetr
:
r2 -a 8051 ite_it8502.rom
[0x00000000]> . ite_it8502.r2
Запустим pae 36
для показа ESIL представления функции set_SMBus_frequency
. Запустим aetr pae 36
для конвертации строки ESIL в REIL. Используя перенаправление >
, мы можем сохранить вывод в файл и передать управление в OpenREIL. Можно проделать все это с помощью скрипта r2pipe.
Radeco — проект декомпилятора на основе ESIL
Однако, как я уже упоминал, одно из основных предназначений промежуточных языков — конвертация бинарного кода для последующей декомпиляции (в контексте реверс-инжиниринга). В начале 2015 года проект radare2 запустил GSoC/RSoC, основным заданием которого было создание декомпилятора или базы для него. Вместе с двумя нашими студентами — Шушантом Динешем (Sushant Dinesh) и Даниэлем Креутером (Daniel Kreuter) — мы изучили большое количество доступных материалов по декомпиляции, методам анализа графов CFG, промежуточным языкам и исходным кодам подобных воплощений.
В процессе такой систематизации родилось понимание, что ESIL в таком виде, как он есть, не подходит для задач декомпиляции. Поэтому было решено сделать еще один, на этот раз высокоуровневый язык radeco IL, по аналогии с Vine IL. Главным его отличием от ESIL является непланарность — по сути, это исключительно графовое представление программы. В качестве исходных данных для получения radeco IL декомпилятор берет ESIL из radare2. Поскольку создание подобных алгоритмов представляет непростую задачу (ввиду используемых абстракций) и в то же время может требовать большого количества вычислений, было решено использовать Rust для написания всех уровней выше ESIL, включая саму конвертацию ESIL -> radeco IL.
Конвертация ESIL -> radeco IL происходит одновременно с преобразованием кода в SSA-представление (а точнее, его свертку). В дальнейшем могут быть применены стадии constant propagation, values propagation, variable propagation, DCE (Dead Code Elimination), избавление от goto (решейпинг получаемого графа). Взаимодействие с radare2 осуществляется через интерфейс r2pipe. Сам radeco разбит на две части: библиотеку и базовое приложение, по образу и подобию radare2, где вся функциональность доступна в виде разделяемой библиотеки, что позволяет использовать фреймворк в сторонних продуктах, как свободных, так и проприетарных.
Сборка radeco элементарна:
git clone https://github.com/radare/radeco
cd radeco
cargo build
Исполняемый файл radeco будет лежать в каталоге radeco/target/debug
. На данный момент radeco не умеет выдавать псевдо си представление программы, однако умеет генерировать dot-файлы с CFG (Control Flow Graph) после прохождения SSA и DCE. Запустим его с помощью r2pipe.rs
из текущей сессии radare2:
[0x00000000]> #!pipe <path/to/radeco> -p r2,esil,cfg,ssa,const,dce,svg
Как мы видим, radeco в данном случае делает следующие шаги:
- читает ESIL из текущей сессии r2;
- преобразовывает ESIL в представление radeco IL;
- создает CFG (Control Flow Graph);
- создает дерево SSA;
- запускает поиск констант (Constant Propagation);
- запускает DCE (Dead Code Elimination);
- создает SVG-файл с помощью утилиты graphviz и промежуточного dot-файла.
Возьмем простейшую программу:
global _main
section .text
main:
mov rax, 2048
cmp rax, 2048
je equal
add rax, 1
equal:
mov rbx, rax
ret
После анализа и преобразования ее ESIL выглядит следующим образом:
2048,rax,=
2048,rax,==,%z,zf,=,%b64,cf,=,%p,pf,=,%s,sf,=
zf,?{,408,rip,=,}
1,rax,+=
rax,rbx,=
rsp,[8],rip,=,8,rsp,+=
После запуска radeco поверх этой программы мы получаем представленный на рис. 6 граф (поскольку изображение очень велико, приведена лишь его основная часть).
Добавление поддержки ESIL в плагин анализа архитектуры
Трудно ли добавить поддержку ESIL в свою любимую архитектуру? Давай посмотрим. Во-первых, заглянем в документацию по добавлению своего плагина для анализа. В этой статье в качестве примера указан плагин для анализа SNES:
/* radare - LGPL - Copyright 2015 - condret */
#include <string.h>
#include <r_types.h>
#include <r_lib.h>
#include <r_asm.h>
#include <r_anal.h>
#include "snes_op_table.h"
static int snes_anop(RAnal *anal, RAnalOp *op, ut64 addr, const ut8 *data, int len) {
memset (op, '\0', sizeof (RAnalOp));
op->size = snes_op[data[0]].len;
op->addr = addr;
op->type = R_ANAL_OP_TYPE_UNK;
switch (data[0]) {
case 0xea:
op->type = R_ANAL_OP_TYPE_NOP;
break;
}
return op->size;
}
struct r_anal_plugin_t r_anal_plugin_snes = {
.name = "snes",
.desc = "SNES analysis plugin",
.license = "LGPL3",
.arch = R_SYS_ARCH_NONE,
.bits = 16,
.init = NULL,
.fini = NULL,
.op = &snes_anop,
.set_reg_profile = NULL,
.fingerprint_bb = NULL,
.fingerprint_fcn = NULL,
.diff_bb = NULL,
.diff_fcn = NULL,
.diff_eval = NULL
};
#ifndef CORELIB
struct r_lib_struct_t radare_plugin = {
.type = R_LIB_TYPE_ANAL,
.data = &r_anal_plugin_snes,
.version = R2_VERSION
};
#endif
Для полноценной поддержки ESIL нам требуется добавить:
- преобразование опкодов архитектуры в ESIL;
- регистровый профиль, для эмуляции.
В качестве простейшего примера возьмем опкод JMP. Для этого в функцию snes_anop()
добавим следующие строчки (внутрь switch):
case 0x4c: // jmp $ffff
op->cycles = 3;
op->type = R_ANAL_OP_TYPE_JMP;
op->jump = data[1] | data[2] << 8;
r_strbuf_setf (&op->esil, "0x%04x,pc,=", op->jump);
break;
Как мы видим, основную роль в генерации ESIL играет генерация соответствующей строки (0x[addr],pc,=
). Все очень просто. Вторым ингредиентом будет добавление register profile:
static int set_reg_profile(RAnal *anal) {
char *p =
"=PC pc\n"
"=SP sp\n"
"gpr a .8 0 0\n"
"gpr x .8 1 0\n"
"gpr y .8 2 0\n"
"gpr flags .8 3 0\n"
"gpr C .1 .24 0\n"
"gpr Z .1 .25 0\n"
"gpr I .1 .26 0\n"
"gpr D .1 .27 0\n"
// bit 4 (.28) is NOT a real flag.
// "gpr B .1 .28 0\n"
// bit 5 (.29) is not used
"gpr V .1 .30 0\n"
"gpr N .1 .31 0\n"
"gpr sp .8 4 0\n"
"gpr pc .16 5 0\n";
return r_reg_set_profile_string (anal->reg, p);
}
static int esil_snes_init (RAnalEsil *esil) {
if (esil->anal && esil->anal->reg) { // initial values
r_reg_set_value (esil->anal->reg, r_reg_get (esil->anal->reg, "pc", -1), 0x0000);
r_reg_set_value (esil->anal->reg, r_reg_get (esil->anal->reg, "sp", -1), 0xff);
r_reg_set_value (esil->anal->reg, r_reg_get (esil->anal->reg, "a", -1), 0x00);
r_reg_set_value (esil->anal->reg, r_reg_get (esil->anal->reg, "x", -1), 0x00);
r_reg_set_value (esil->anal->reg, r_reg_get (esil->anal->reg, "y", -1), 0x00);
r_reg_set_value (esil->anal->reg, r_reg_get (esil->anal->reg, "flags", -1), 0x00);
}
return true;
}
static int esil_snes_fini (RAnalEsil *esil) {
return true;
}
Как видишь, все довольно просто: сам профиль (set_reg_profile()
) и две функции — инициализация esil_snes_init()
и деинициализация esil_snes_fini()
. Теперь нам осталось добавить их в структуру, описывающую плагин (struct r_lib_struct_t r_anal_plugin_snes
):
.set_reg_profile = &set_reg_profile,
.esil = true,
.esil_init = esil_snes_init,
.esil_fini = esil_snes_fini,
Тут важно обратить внимание, что SNES базируется на микропроцессоре 65802, который может работать в режиме как 8 бит, так и 16 бит. Мы для простоты рассмотрели случай только 8-битного режима, так как всегда можно посмотреть исходные коды подобного модуля для процессора 6502 (libr/anal/p/anal_6502.c
).
Заключение
К сожалению, отведенный под статью объем подходит к концу, поэтому пора закругляться. Все приведенные примеры использования — лишь капля в море для подобных инструментов. На основе ESIL (а особенно radeco IL) можно (и планируется) реализовать множество различных утилит — от генерации SMT до автовыведения типов, от автоматической деобфускации до автоматического поиска уязвимостей.
Проект планирует проводить Radare Summer of Code этим летом и опять будет подавать заявку на Google Summer of Code. Поэтому приглашаем всех желающих вносить свой вклад в развитие подобных инструментов: участвуй в проектах radare2/radeco и используй их в своих, более высокоуровневых утилитах и программных комплексах.