Поиск одного из паролей в CMU Binary Bomb

Поиск одного из паролей в CMU Binary Bomb

Некоторое время назад я запланировал изучить тему символического выполнения, и вот теперь мы рассмотрим, как применить эту технику на примере поиска пароля в CMU Binary Bomb.

Автор: Cory Duplantis

Некоторое время назад я запланировал изучить тему символического выполнения, и вот теперь мы рассмотрим, как применить эту технику на примере поиска пароля в CMU Binary Bomb.

Все утилиты, описанные в этой статье, находятся в репозитории Vagrant CTF VM.

Что такое символическое выполнение

По мере изложения материала я буду кратко описывать отдельные элементы этой технологии. Однако если вы впервые слышите словосочетание «символическое выполнение», рекомендую ознакомиться с лекцией Массачусетского технологического института, иначе вы ничего не поймете.  

Символическое выполнение позволяет нам, реверс-инженерам, найти при наличии каких входных данных программа проходит путь от Адреса A к Адресу B. Эта задача эквивалентна булевой проблеме выполнимости (Boolean Satisfiability Problem), где мы можем использовать различные библиотеки для поиска корректных входных данных.

CMU Binary Bomb

Мы будем работать с приложением CMU Binary Bomb, при помощи которого можно размять свои мозги посредством решения задач из области реверс-инжиниринга (аналог CrackMe). Чтобы перейти на следующий уровень, нужно найти пароль от предыдущего. Мы начнем со второго пароля, поскольку первый легко найти, если поискать строки внутри бинарного файла.

Radare

При помощи radare2 быстро находим нужную нам функцию.

Начинаем анализ с команды aaa.

radare1

Рисунок 1: Подгружаем исследуемый файл в анализатор

Первым делом необходимо найти нужную функцию при помощи команды afl. В командной строке вводим оператор ~ и кусок фразы, которую содержит имя функции. В полученном перечне видим функцию sym.phase_2.

radare2

Рисунок 2: Перечень функций, в имени которых встречается слово phase

Чтобы посмотреть содержимое интересующей нас функции, используем команду pdf ([p]rint [d]issembly of [f]unction).

radare2

Рисунок 3: Содержимое функции sym.phase_2

Вышеуказанный метод представления удобен для маленьких функций. Для просмотра больших функций меняем режим отображения на визуальный (Visual Mode), в котором наглядно показываются условные переходы. Используем команду VV.

radare2

Рисунок 4: Наглядное отображение условных переходов функции sym.phase_2

Перед началом символического выполнения необходимо выяснить, какая функция принимает входные данные. На рисунке выше замечаем интересную функцию read_six_numbers. Находясь все там же в режиме визуального просмотра, вводим команду ga и переходим к функции read_six_numbers. В составной команде ga символ «g» соответствует команде goto symbol (перейти к символу) и «а» – собственно, сам символ, находящийся по адресу 0x400f05 (см. рисунок выше).

radare2

Рисунок 5: Содержимое функции read_six_numbers

Внутри функции read_six_numbers замечаем функцию scanf, которая на входе считывает шесть 32-битных целочисленных значений. Чтобы подтвердить наши догадки, запускаем отладчик pwndbg, устанавливаем точку останова сразу же после вызова функции read_six_numbers и вводим шесть целых чисел: 1 2 3 4 5 6.

radare2

Рисунок 6: Устанавливаем точку останова по адресу 0x400f0a

Как только сработала точка останова, смотрим содержимое стека при помощи команды hexdump $rsp.

radare2

Рисунок 7: Содержимое стека после ввода 6 целых чисел

Наше предположение оказалась верным. В стеке находятся шесть целых 32-битных чисел. В начале нашего будущего скрипта мы будем создавать шесть символических значений.

Последнее, что нужно сделать, определиться с тем, где нужно начать и закончить выполнение, и какую часть кода выполнять не следует. В анализаторе radare бегло проглядываем полное дерево функции (переход между режимами осуществляется при помощи команды p). Выясняем, что функция explode_bomb вызывается по двум адресам: 0x400f10 и 0x400f20.

radare2

Рисунок 8: Место вызова функции sym.explode_bomb

Глядя внутрь sym.explode_bomb, видим, что функция печатает несколько строк, выводит сообщение о взрыве бомбы и завершает выполнение.

radare2

Рисунок 9: Содержимое функции sym.explode_bomb

Очевидно, что вызова этой функции нужно избежать.

Теперь мы знаем, куда попадают введенные данные, и чтобы успешно решить задачу, нам нужно вернуться из функции sym.phase_2 и избежать выполнения функции sym.bomb_explode.

В итоге мы имеет полный маршрут, необходимый для символического выполнения.

radare2

Рисунок 10: Маршрутная карта, необходимая для успешного выполнения

Символическое выполнение

Имея на руках отдельные куски золотого кубка, приступаем к составлению скрипта для решения нашей задачи.

Мы будем использовать фреймворк angr, разработанный несколькими исследователями из организации Computer Security Lab, находящейся в Санта-Барбаре. Более подробную информацию об angr можно узнать из документации.

Вначале передаем наш бинарный файл в angr.

proj = angr.Project('bomb', load_options={'auto_load_libs':False})

Нам не нужно запускать весь бинарный файл, чтобы добраться до функции phase_2. Чтобы сразу же начать с phase_2, воспользуемся функцией blank_state. Начинаем выполнение с инструкции, идущей сразу же после вызова функции read_six_numbers.

state = proj.factory.blank_state(addr=0x400f0a)

Поскольку мы начинаем выполнение с середины бинарного фала и после вызова функции scanf, нам нужно сконструировать входные данные. Мы создадим шесть 32-битных символических значения и разместим их в стеке. Здесь мы моделируем состояние бинарного файла после чтения входных данных.

for i in xrange(6):
    state.stack_push(state.se.BVS('int{}'.format(i), 4*8))

В качестве объекта символической переменной мы используем битовый векторный символ (Bit Vector Symbol, BVS). Битовый векторный символ используется в angr для внутренних нужд при создании уравнения, которое затем будет решаться.

Теперь, когда инициализировано первоначальное состояние, создаем объект типа Explorer. Этому объекту мы указываем, где начать и закончить выполнение и каких вызовов нужно избежать. Далее будет предпринята попытка найти путь, удовлетворяющий заданным условиям.

# ID from Radare
bomb_explode = 0x40143a
path = proj.factory.path(state=state)
ex = proj.surveyors.Explorer(start=path, find=(0x400f3c,),
                             avoid=(bomb_explode,), enable_veritesting=True)
ex.run()

Если путь найден, мы пытаемся определить набор входных данных, который поможет нам добраться до конца пути. Мы будем извлекать значения из стека в конце пути и в то же время попытаемся найти корректные целочисленные значения. Поскольку наш бинарный файл – 64-битный, каждое вынутое значение также будет 64-битным. Мы должны будем извлечь два 32-битных значения.

if ex.found:
    found = ex.found[0].state
 
    answer = []
 
    for x in xrange(3):
        curr_int = found.se.any_int(found.stack_pop())
 
        # We are popping off 8 bytes at a time
        # 0x0000000200000001
 
        # This is just one way to extract the individual numbers from this popped value
        answer.append(str(curr_int & 0xffffffff))
        answer.append(str(curr_int>>32 & 0xffffffff))
 
    return ' '.join(answer)

Мы считываем конечное состояние правильного пути от начала до конца, избегая вызова функции bomb_explode. Мы извлекаем содержимое из стека и пытаемся найти корректные целочисленные значения (found.se.any_int(found.stack_pop())). Из найденного целого числа мы извлекаем два 32-битных значения, чтобы первую часть пароля. Повторяем процесс до тех пор, пока не получим весь пароль.

Конечный скрипт

## Binary found here: http://csapp.cs.cmu.edu/3e/bomb.tar
 
import angr, logging
from subprocess import Popen, PIPE
from itertools import product
import struct
 
def main():
    proj = angr.Project('bomb', load_options={'auto_load_libs':False})
 
    logging.basicConfig()
    logging.getLogger('angr.surveyors.explorer').setLevel(logging.DEBUG)
 
    def nop(state):
        return
 
    bomb_explode = 0x40143a
 
    # Start analysis at the phase_2 function after the sscanf
    state = proj.factory.blank_state(addr=0x400f0a)
 
    # Sscanf is looking for '%d %d %d %d %d %d' which ends up dropping 6 ints onto the stack
    # We will create 6 symbolic values onto the stack to mimic this
    for i in xrange(6):
        state.stack_push(state.se.BVS('int{}'.format(i), 4*8))
 
    # Attempt to find a path to the end of the phase_2 function while avoiding the bomb_explode
    path = proj.factory.path(state=state)
    ex = proj.surveyors.Explorer(start=path, find=(0x400f3c,),
                                 avoid=(bomb_explode, 0x400f10, 0x400f20,),
                                 enable_veritesting=True)
    ex.run()
    if ex.found:
        found = ex.found[0].state
 
        answer = []
 
        for x in xrange(3):
            curr_int = found.se.any_int(found.stack_pop())
 
            # We are popping off 8 bytes at a time
            # 0x0000000200000001
 
            # This is just one way to extract the individual numbers from this popped value
            answer.append(str(curr_int & 0xffffffff))
            answer.append(str(curr_int>>32 & 0xffffffff))
 
        return ' '.join(answer)
 
def test():
    assert main() == '1 2 4 8 16 32'
 
if __name__ == '__main__':
    print(main())
 

Код, приведенный выше, можно найти здесь: git clone https://github.com/thebarbershopper/ctf-writeups


Ваша приватность умирает красиво, но мы можем спасти её.

Присоединяйтесь к нам!