Обезвреживаем бомбу с Radare2
Доброго времени суток, %username%! Сегодня мы отправимся изучать бесчисленные возможности фреймворка для реверсера — radare2. В виде подопытного я взял первую попавшую бомбу, она оказалась с сайта Университета Карнеги Меллон.
Т.к. бинарник может поменяться оставлю на всякий случай хэшсуммы:
md5sum: 1f38d04188a1d08f95d8c302f5361e9f
sha1sum: 31022a4baa524f6275209f7424c616226bc9814b
sha256sum: 8849e033691d51426c0c91a76eeb0c346eddd37e8fdf21cd93acd16669f1b461
О чем вообще речь?
Radare2(aka r2) — опенсорсный кроссплатформенный фреймворк, для исследования бинарных файлов (изначально, к слову, hex-редактор). Главным конкурентом оного является небезызвестная IDA Ильфака, но, увы, для студента она дороговата, а бесплатная версия с x86–64 не дружит. А еще радар вроде как круче.
Бинарная бомба или просто бомба — исполняемый файл для обучения, который получает некое количество строк, и, в случае, если все строки проходят проверки, поздравляет с этим юного аналитика. Это так называемые уровни или фазы, у нас их целых 6.
Еще немного про фреймворк
Radare2, как уже было сказано, именно фреймворк, а не просто дизассемблер. Он включает кучу различных тулз вроде дебаггера, hex-редактора, компилятора, поиска ROP-гаджетов и много чего другого. Для не любителей консоли у него также имеется два сыроватых фронтенда это WebUI ($ r2 -c "=H" file
) и Bokken.
Мануал как обычно есть в man, а также для каждой команды путем добавления после нее »?». Например, «pd?» выдаст описания команд, начинающихся на pd.
Немного ссылок по теме:
Here we go!
Помимо самого исполняемого файла нам любезно предоставили файл с исходными кодами. Однако все, что там есть — инициализация ввода, вызов фаз, и забавные комментарии. Остальные функции вытягиваются из соответствующих хэдеров, которых у нас нет.
/***************************************************************************
* Dr. Evil's Insidious Bomb, Version 1.1
* Copyright 2011, Dr. Evil Incorporated. All rights reserved.
*
* LICENSE:
*
* Dr. Evil Incorporated (the PERPETRATOR) hereby grants you (the
* VICTIM) explicit permission to use this bomb (the BOMB). This is a
* time limited license, which expires on the death of the VICTIM.
* The PERPETRATOR takes no responsibility for damage, frustration,
* insanity, bug-eyes, carpal-tunnel syndrome, loss of sleep, or other
* harm to the VICTIM. Unless the PERPETRATOR wants to take credit,
* that is. The VICTIM may not distribute this bomb source code to
* any enemies of the PERPETRATOR. No VICTIM may debug,
* reverse-engineer, run "strings" on, decompile, decrypt, or use any
* other technique to gain knowledge of and defuse the BOMB. BOMB
* proof clothing may not be worn when handling this program. The
* PERPETRATOR will not apologize for the PERPETRATOR's poor sense of
* humor. This license is null and void where the BOMB is prohibited
* by law.
***************************************************************************/
#include
#include
#include "support.h"
#include "phases.h"
/*
* Note to self: Remember to erase this file so my victims will have no
* idea what is going on, and so they will all blow up in a
* spectaculary fiendish explosion. -- Dr. Evil
*/
FILE *infile;
int main(int argc, char *argv[])
{
char *input;
/* Note to self: remember to port this bomb to Windows and put a
* fantastic GUI on it. */
/* When run with no arguments, the bomb reads its input lines
* from standard input. */
if (argc == 1) {
infile = stdin;
}
/* When run with one argument , the bomb reads from
* until EOF, and then switches to standard input. Thus, as you
* defuse each phase, you can add its defusing string to and
* avoid having to retype it. */
else if (argc == 2) {
if (!(infile = fopen(argv[1], "r"))) {
printf("%s: Error: Couldn't open %s\n", argv[0], argv[1]);
exit(8);
}
}
/* You can't call the bomb with more than 1 command line argument. */
else {
printf("Usage: %s []\n", argv[0]);
exit(8);
}
/* Do all sorts of secret stuff that makes the bomb harder to defuse. */
initialize_bomb();
printf("Welcome to my fiendish little bomb. You have 6 phases with\n");
printf("which to blow yourself up. Have a nice day!\n");
/* Hmm... Six phases must be more secure than one phase! */
input = read_line(); /* Get input */
phase_1(input); /* Run the phase */
phase_defused(); /* Drat! They figured it out!
* Let me know how they did it. */
printf("Phase 1 defused. How about the next one?\n");
/* The second phase is harder. No one will ever figure out
* how to defuse this... */
input = read_line();
phase_2(input);
phase_defused();
printf("That's number 2. Keep going!\n");
/* I guess this is too easy so far. Some more complex code will
* confuse people. */
input = read_line();
phase_3(input);
phase_defused();
printf("Halfway there!\n");
/* Oh yeah? Well, how good is your math? Try on this saucy problem! */
input = read_line();
phase_4(input);
phase_defused();
printf("So you got that one. Try this one.\n");
/* Round and 'round in memory we go, where we stop, the bomb blows! */
input = read_line();
phase_5(input);
phase_defused();
printf("Good work! On to the next...\n");
/* This phase will never be used, since no one will get past the
* earlier ones. But just in case, make this one extra hard. */
input = read_line();
phase_6(input);
phase_defused();
/* Wow, they got it! But isn't something... missing? Perhaps
* something they overlooked? Mua ha ha ha ha! */
return 0;
}
После запуска r2 он встречает нас случайной фразой. Затем он ставит текущий указатель на entry-point и ждет команды. Флажок -А при открытии файла сразу же его анализирует.
Тоже самое можно сделать блоком команд начинающихся, на a), к примеру afl — достает из бинарника список функций. ~ — аналог grep-a (фильтра). Поищем у него наши функции.
$ r2 -A bomb
-- In soviet Afghanistan, you debug radare2!
[0x00400c90]> afl~phase
0x00400ee0 28 3 sym.phase_1
0x004015c4 149 8 sym.phase_defused
0x00400efc 71 8 sym.phase_2
0x00400f43 139 8 sym.phase_3
0x0040100c 86 7 sym.phase_4
0x00401062 146 9 sym.phase_5
0x004010f4 272 26 sym.phase_6
0x00401242 81 5 sym.secret_phase
Level 1
Чудесно, все функции из исходника на месте, а заодно еще и нашли секретную фазу. Давайте наконец посмотрим содержимое первого уровня. Сделать это можно, сместив указатель на определенный адрес функции, а затем вывести нужное количество опкодов для дизассемблирования.
[0x00400c90]> s 0x00400ee0 # В данном случае 's' - не обязательно
[0x00400ee0]> pd 8 # дизассемблировать 8 опкодов от текущего смещения
Т.к. вывод r2 из коробки просто прекрасен, то для наглядности ради буду ассемблерные листинги постить в виде картинок.
Обратите внимание, что радар снабдил нас XREF-ами (то откуда может быть передано управление) с мнемониками прыжка. Кроме того, подставил строку по указанному адресу и самое главное — показал в виде ascii-стрелок переходы в блоке.
0000000000400ee0 :
400ee0: 48 83 ec 08 sub rsp,0x8
400ee4: be 00 24 40 00 mov esi,0x402400
400ee9: e8 4a 04 00 00 call 401338
400eee: 85 c0 test eax,eax
400ef0: 74 05 je 400ef7
400ef2: e8 43 05 00 00 call 40143a
400ef7: 48 83 c4 08 add rsp,0x8
400efb: c3 ret retq
Даже не вникая, становится ясно, что первая необходимая нам строка — «Border relations with Canada have never been better.». И само собой, при скармливании ее бомбе, она, пускает нас на вторую фазу.
$ ./bomb
Welcome to my fiendish little bomb. You have 6 phases with
which to blow yourself up. Have a nice day!
Border relations with Canada have never been better.
Phase 1 defused. How about the next one?
Level 2
Второй вариант вывода дизассемблированной функции — с помощью абсолютной адресации/меток. На примере второй фазы — это будет выглядеть так:
[0x00400ee0]> pdf @ 0x00400efc
либо, если адрес не известен:
[0x00400ee0]> pdf @ sym.phase_2
Конечная цель у нас — не взорвать бомбу, т.е. не делать вызовов call sym.explode_bomb, так что от этого и будем отталкиваться. Следовательно, у нас всегда должны срабатывать оба прыжка je.
Первое, что стоит у нас на пути — вызов call sym.read_six_numbers. Соответственно после этого вызова на верху стека должна быть единица. Посмотрим, что происходит в этой функции.
Раньше r2 разбирал файлы на функции основываясь на опкоде ret, что нередко приводило к выводу нескольких функций (например, при наличии системных вызовов exit ()). В подобных случаях, когда радар неправильно определяет функции можно сделать это вручную.
[0x00400ee0]> s 0x0040149e # двигаем указатель на опкод после конца
[0x0040149e]> af+ sym.read_six_numbers `?vi $$-sym.read_six_numbers` rsn # Определение функции rsn, начинающейся с метки sym.read_six_numbers до текущего указателя.
Кстати, в этом примере использовалась другая команда как аргумент первой с помощью парных скобок``.
В самой функции не происходит ничего интересного, она из считанной строки достает 6 чисел и записывает их по порядку в переданный указатель. Затем убеждается, что чисел больше 5.
В Си бы это выглядело примерно так:
void read_six_numbers(char *str, long long *p) {
if (sscanf(str, "%d %d %d %d %d %d", p, p+1, p+2, p+3, p+4, p+5) <= 5)
explode_bomb();
}
Вернемся к нашей функции phase_2. Указатель на массив передается указатель на вершину стека (mov rsi, rsp). Следовательно, первое число в строке должно быть — 1.
Как вы могли заметить там достаточно много переходов. Пользователь IDA скорей всего нажал бы пробел и посмотрел граф переходов. Вы не поверите, но тут они тоже есть. Подобно vim тут есть visual-mode (команда V) и в нем присутствует тот самый граф переходов, тоже по команде V(либо сразу VV). Выход из каждого мода — q
Отображается он прекрасно, с минимум пересечений (по сравнению с прошлыми версиями). Подвигать это чудо можно стрелками, либо vim-like 'hjkl'. Если же вам не нравится расположение блоков их также можно двигать хоткеями Shift+'hjkl'. При этом двигается выбранный блок (синий), выбрать его можно Tab/Shift-Tab.
В первом блоке в rbx записывается указатель на второе число, а в rbp — на конец массива чисел. И управление перекидывается на цикл, в котором попарно сравниваются соседние числа.
mov eax, dword [rbx - 4] ; Положить предыдущее число в eax
add eax, eax ; Удвоить его
cmp dword [rbx], eax ; Сравнить с текущим
je 0x400f25 ; Продолжить цикл, если равны
call sym.explode_bomb ; Взорвать бомбу, если не равны
add rbx, 4 ; Сдвинуть указатель на следующее число
cmp rbx, rbp ; Проверить, в конце ли мы массива
jne 0x400f17 ; Если нет, то вернутся в начало
jmp 0x400f3c ; Завершить цикл
Получается, что нам нужно ввести последовательность степеней двойки от 1 до 32. Отлично, но все-таки проверим, что это то, что нужно.
$ ./bomb
...
Phase 1 defused. How about the next one?
1 2 4 8 16 32
That's number 2. Keep going!
Level 3
Наверно сейчас человек, далекий от ассемблера, в эпилептических припадках пытается попасть по крестику на краю окна. На самом деле бояться тут нечего, просто так выглядит самый обычный switch-case блок.
Числа считываются аналогично предыдущему случаю, только без вызова функции и всего два. Хранятся они в [rsp+8] и [rsp+0xc] соответственно.
Затем идут проверки, что оба числа считаны успешно, а также первый аргумент не больше 7. После чего и происходит тот самый switch переход по адресу 0×402470 со смещением (введенное число)*8.
Не трудно догадаться, что там лежат адреса case-меток. Дабы не быть голословным, посмотрим, что там действительно лежит. Сделать это можно с помощью групп команд px. В данном случае нас интересуют 8-байтные слова (Quad-word).
[0x0040149e]> pxQ 72 @ 0x402470
0x00402470 0x0000000000400f7c sym.phase_3+57
0x00402478 0x0000000000400fb9 sym.phase_3+118
0x00402480 0x0000000000400f83 sym.phase_3+64
0x00402488 0x0000000000400f8a sym.phase_3+71
0x00402490 0x0000000000400f91 sym.phase_3+78
0x00402498 0x0000000000400f98 sym.phase_3+85
0x004024a0 0x0000000000400f9f sym.phase_3+92
0x004024a8 0x0000000000400fa6 sym.phase_3+99
Собственно, что и ожидалось, хотя не совсем последовательно. Дальше идет проверка второго нашего числа с тем магическим, что было записано в eax при свитче. Так как ввод в десятичном основании придется переконвертировать из hex-а. Посчитать можно это с помощью rax2(аналог калькулятора) через вызов шелла, а также напрямую, через встроенный калькулятор (хоткей — ? ), не создавая новых программ. А, дабы не жмакать постоянно enter можно группировать команды прям как в баше.
[0x0040149e]> !rax2 0xcf
207
[0x0040149e]> ?vi 0x2c3; ?vi 0x100; ?vi 0x185; ?vi 0xce; ?vi 0x2aa; ?vi 0x147; ?vi 0x137
707
256
389
206
682
327
311
Итого возможные решения будут:
- 0 207
- 1 311
- 2 707
- 3 256
- 4 389
- 5 206
- 6 682
- 7 327
И лишний раз убедимся, на произвольном варианте, что все работает:
$ ./bomb
...
That's number 2. Keep going!
4 389
Halfway there!
Level 4
На этом уровне появляется рекурсия. Помимо встроенных ASCII-графов также есть возможность получить графы в виде файлов для dot утилиты, а затем, например, переконвертить в png.
[0x0040149e]> ag sym.func4 > func4.dot
[0x0040149e]> dot -Tpng -o func4.png func4.dot
Если все это перевести в Си, то будет выглядеть почти не так страшно.
void phase_4(char *str) {
int x, y;
if (sscanf(str, "%d %d", &x, &y) != 2 ||
x > 14 ||
func4(x, 0, 14) ||
y != 0)
explode_bomb();
}
int func4(int x, int y, int z) {
unsigned diff = (z - y)/2;
int p = y + diff;
if (p > x) {
func4(x, y, p-1);
return diff * 2;
} else if (p < x) {
func4(x, p + 1, z);
return diff * 2 + 1;
}
return 0;
}
Самое простое и очевидное решение — функция func4 возвращает 0, когда она не заходит внутрь else-if. Ввод пользователя управляет только x, а p=7 при первом заходе. Соответственно при x=7 функция просто вернет 0, без рекуррентных вызовов. Вторая же переменная строго задана нулем. Убедимся в этом.
$ ./bomb in.tmp
...
Halfway there!
7 0
So you got that one. Try this one.
Level 5
С этим будет сложнее, тут нагромождено очень много кода.
В 0×00401073 записывается канарейка на стек. Подобную информацию о бинарнике можно достать с помощью команды i. К примеру, i~canary вернет в данном случае true.
После этого возвращаемое значение string_length сравнивается с 6 и начинается трудно анализируемый спагетти-код. Разбираться без дебагера с этим долго и сложно, поэтому будет грех им не воспользоваться. Для этого надо открыть файл с флагом дебага либо при запуске:
$ r2 -Ad bomb
либо просто переоткрыв файл:
[0x0040149e]> ood
Адрес указателя при этом автоматически поменяется на первый опкод в entry-point, который уже подгружен в память. Как обычно ставим breakpoints и продолжаем выполнение до них.
[0x7f2960b99d80]> db sym.phase_5 # либо s sym.phase_5; db $$
[0x7f2960b99d80]> dc # продолжить выполнение до 1 брейкпоинта
Дальше есть 2 способа анализа: первый — использовать блок команд d/db, второй — переключиться в Visual-mode. Первый способ не такой наглядный, поэтому остановимся на втором.
Как и раньше переходим Visual-mode, а затем выбираем необходимый debug-layout для отладки хоткеями p/P.
Перемешаться по код можно с помощью n/N — следующая/предыдущая функция; j/k — следующий предыдущий опкод.
И самое интересное: b/F2 — поставить брейкпоинт, s/F7 — шаг в 1 опкод, S/F8 — шаг в 1 опкод не заходя в call, F9 — продолжить до брейкпоинта.
Итого поскармливав дебаггеру несколько строк, не трудно догадаться, что там происходит.
Для каждого символа из нашей строки по модулю 16 берется соответствующий символ из строки char *s = "maduiersnfotvbyl"
и полученная строка сравнивается «flyers».
Собственно наша задача и найти такие символы, индексы которых в str дают искомую строку.
Строку flyers получить можно единственным образом: {s[0×9], s[0xe], s[0xf], s[0×5], s[0×6], s[0×7]}. Думаю, ASCII таблицу в голове хранит не каждый, посему можно снова обратиться к утилите rax2 за помощью. С ключом -s он конвертит из hex в string. Т.к. символы берутся по модулю 16, то для эстетичности можно подобрать печатаемые значения.
$ rax2 -s 49 4e 4f 45 46 47
IONEFG
$ ./bomb
...
So you got that one. Try this one.
IONEFG
Good work! On to the next...
Я очень долго думал и все еще думаю, что там может получиться какое-нибудь осмысленное слово. Так что если вдруг кто-то отыщет буду крайне признателен :)
The Last One
Так как статья вышла великовата все фазы я рассматривать не буду. В частности, 6 фазу я опущу, ибо там достаточно много кропотливого вникания без непосредственного участия фреймворка. Пусть это останется домашним заданием самым любознательным.
В секретную фазу попасть не так просто, посему упростим себе жизнь пропатчив бинарник. Открыть файл нам придется заново, выдав права на запись флажком -w, либо переоткрыть с помощью oo+ не выходя из r2.
$ r2 -Aw bomb
-- Did you ever ordered a pizza using radare2?
[0x00400c90]> s 0x00400ec6 # call sym.phase_6
[0x00400ec6]> wa call sym.secret_phase # запись нужных опкодов
[0x00400ec6]> pdf @ sym.secret_phase; pdf @ sym.fun7
Опять появляется рекурсивная функция и на этот раз ее так просто не обойти, ибо без рекурсивных вызовов нужное значение так просто, как было в прошлый раз, не получить. ASCII-граф снова может упростить жизнь.
Анализ дает примерно такой аналог на Си:
void secret_phase() {
long num = strtol(read_line()) - 1;
if (num > 0x3e8 ||
fun7((long*)(0x6030f0), num) != 2)
explode_bomb();
puts("Wow! You've defused the secret stage!");
phase_defused();
}
int fun7(long *array, int num) {
if (array == 0)
return -1;
if (*array <= num) {
if (*array == num)
return 0; // 1
else {
return 2 * fun7(array + 1, num); // 2
}
} else {
return 2 * fun7(array + 2, num) + 1; // 3
}
}
Итого, чтобы получить на выходе fun7 именно двойку, нам надо вначале вызвать ret на 2 строке, затем на 3 и наконец на 1. Остается лишь одна загадка — что хранится по адресу 0×6030f0.
[0x00401204]> px 480 @ 0x6030f0
- offset - 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
0x006030f0 2400 0000 0000 0000 1031 6000 0000 0000 $........1`.....
0x00603100 3031 6000 0000 0000 0000 0000 0000 0000 01`.............
0x00603110 0800 0000 0000 0000 9031 6000 0000 0000 .........1`.....
0x00603120 5031 6000 0000 0000 0000 0000 0000 0000 P1`.............
0x00603130 3200 0000 0000 0000 7031 6000 0000 0000 2.......p1`.....
0x00603140 b031 6000 0000 0000 0000 0000 0000 0000 .1`.............
0x00603150 1600 0000 0000 0000 7032 6000 0000 0000 ........p2`.....
0x00603160 3032 6000 0000 0000 0000 0000 0000 0000 02`.............
0x00603170 2d00 0000 0000 0000 d031 6000 0000 0000 -........1`.....
0x00603180 9032 6000 0000 0000 0000 0000 0000 0000 .2`.............
0x00603190 0600 0000 0000 0000 f031 6000 0000 0000 .........1`.....
0x006031a0 5032 6000 0000 0000 0000 0000 0000 0000 P2`.............
0x006031b0 6b00 0000 0000 0000 1032 6000 0000 0000 k........2`.....
0x006031c0 b032 6000 0000 0000 0000 0000 0000 0000 .2`.............
0x006031d0 2800 0000 0000 0000 0000 0000 0000 0000 (...............
0x006031e0 0000 0000 0000 0000 0000 0000 0000 0000 ................
0x006031f0 0100 0000 0000 0000 0000 0000 0000 0000 ................
0x00603200 0000 0000 0000 0000 0000 0000 0000 0000 ................
0x00603210 6300 0000 0000 0000 0000 0000 0000 0000 c...............
0x00603220 0000 0000 0000 0000 0000 0000 0000 0000 ................
0x00603230 2300 0000 0000 0000 0000 0000 0000 0000 #...............
0x00603240 0000 0000 0000 0000 0000 0000 0000 0000 ................
0x00603250 0700 0000 0000 0000 0000 0000 0000 0000 ................
0x00603260 0000 0000 0000 0000 0000 0000 0000 0000 ................
0x00603270 1400 0000 0000 0000 0000 0000 0000 0000 ................
0x00603280 0000 0000 0000 0000 0000 0000 0000 0000 ................
0x00603290 2f00 0000 0000 0000 0000 0000 0000 0000 /...............
0x006032a0 0000 0000 0000 0000 0000 0000 0000 0000 ................
0x006032b0 e903 0000 0000 0000 0000 0000 0000 0000 ................
0x006032c0 0000 0000 0000 0000 0000 0000 0000 0000 ................
Тут хранятся 8-байтные переменные блоками по 4. Первая используется для сравнения с нашим числом; второе и третье — указатели на следующие символы для сравнения; четвертое — просто паддинг, не используется.
Теперь можно разложить все условия и найти искомую переменную.
- 2 ret: num < 0x24
- 3 ret: num > 0×08
- 1 ret: num == 0×16
$ ./bombSec in.tmp
...
Good work! On to the next...
22
Wow! You've defused the secret stage!
Congratulations! You've defused the bomb!
Еху, мы сделали это! Человечество может жить спокойно. Не смотря на то, что статья вышла немного великовата, достаточно много фич фреймворка не были освещены, так что интересующиеся еще могут очень много для себя открыть. Также, буду рад выслушать идеи для возможного продолжения.