Pull to refresh

Обезвреживаем бомбу с Radare2

Reading time 12 min
Views 60K

Доброго времени суток, %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 <stdio.h>
#include <stdlib.h>
#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 <file>, the bomb reads from <file> 
     * until EOF, and then switches to standard input. Thus, as you 
     * defuse each phase, you can add its defusing string to <file> 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 [<input_file>]\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-стрелок переходы в блоке.
Для сравнения вывод с objdump
0000000000400ee0 <phase_1>:
  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 <strings_not_equal>
  400eee:   85 c0                   test   eax,eax
  400ef0:   74 05                   je     400ef7 <phase_1+0x17>
  400ef2:   e8 43 05 00 00          call   40143a <explode_bomb>
  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
На выводе будет вот такой милый ASCII граф


Отображается он прекрасно, с минимум пересечений(по сравнению с прошлыми версиями). Подвигать это чудо можно стрелками, либо 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 переход по адресу 0x402470 со смещением (введенное число)*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


sym.phase_4


sym.func4


На этом уровне появляется рекурсия. Помимо встроенных 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



С этим будет сложнее, тут нагромождено очень много кода.
В 0x00401073 записывается канарейка на стек. Подобную информацию о бинарнике можно достать с помощью команды 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[0x9], s[0xe], s[0xf], s[0x5], s[0x6], s[0x7]}. Думаю, ASCII таблицу в голове хранит не каждый, посему можно снова обратиться к утилите rax2 за помощью. С ключом -s он конвертит из hex в string. Т.к. символы берутся по модулю 16, то для эстетичности можно подобрать печатаемые значения.
$ rax2 -s 49 4e 4f 45 46 47
INOEFG

$ ./bomb
...
So you got that one.  Try this one.
INOEFG
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

secret_phase


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. Остается лишь одна загадка — что хранится по адресу 0x6030f0.
480 байт
[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 > 0x08
  • 1 ret: num == 0x16

$ ./bombSec in.tmp 
...
Good work!  On to the next...
22
Wow! You've defused the secret stage!
Congratulations! You've defused the bomb!


Еху, мы сделали это! Человечество может жить спокойно. Не смотря на то, что статья вышла немного великовата, достаточно много фич фреймворка не были освещены, так что интересующиеся еще могут очень много для себя открыть. Также, буду рад выслушать идеи для возможного продолжения.
Tags:
Hubs:
+49
Comments 28
Comments Comments 28

Articles