Хардкорный олдскул: QEMU и реверс образа флоппика

a0e1d128aba04e2aaf1e25a1c97a941c.png
В преддверии «очной ставки» NeoQUEST-2015, которая состоится уже завтра, 2 июля в 11:00, в Санкт-Петербурге, публикуем write-up последнего неразобранного задания online-этапа!

Напоминаем, что вход на мероприятие свободный, и мы ждём всех, кто интересуется информационной безопасностью! NeoQUEST — это шанс узнать что-то новое, усовершенствовать свои «хакерские» навыки, пообщаться с коллегами, понаблюдать за решающим соревнованием лучших хакеров, и просто отлично провести время!

Подробнее про место проведения и темы докладов NeoQUEST-2015 можно прочитать тут и там.

Задание online-этапа, оставленное «на десерт», было достаточно олдскульным: достаточно уже того, что речь шла о дампе всеми давно забытого флоппи-диска! О том, как участникам квеста пришлось повозиться с реверсом и с QEMU — под катом!

Что делать с исходными данными к заданию?


В задании в качестве исходных данных выступает файл task.bin. Судя по легенде, это должен быть образ загрузочной дискеты. Попробуем скормить его утилите file.

89f1f723760641119da4960b38ab7543.png

Предположение оказалось верным, это дискета. Что же, попробуем с нее загрузиться. В качестве виртуальной машины используем QEMU. Выполним

qemu –fda task.bin


и …

894c6f605a8340dcab679049f6a78fc8.png

… и ничего. По каким-то причинам загрузиться не получилось — QEMU написала «Loading» и зависла. Для начала попробуем посмотреть, что же происходит внутри виртуальной машины, подключив к ней gdb в качестве отладчика. Для подключения gdb важно знать режим работы виртуальной машины, так как это влияет на формат данных, передаваемых от приложения к отладчику.

Виртуальная машина: взгляд изнутри


Перейдем в окно QEMU и нажмем Ctrl + Alt + 2, чтобы открыть консоль команд. Выполним в ней «info registers» и проскроллим вверх комбинацией Ctrl + Up.

a14d480ca2a848c1a083b104e6c044e7.png

На рисунке выше обведены поля, на которые стоит обратить внимание — CR0 и атрибуты дескриптора, на который указывает CS. Из значения CR0 и CS.ATTR следует, что включен защищенный режим без виртуальной памяти и выполняется 32х битный код. Для нас это означает, что в gdb нужно переключать режим командой

set architecture i386

В случае, если gdb 32 битный, эта архитектура выставлена по умолчанию.

Запустим QEMU с опцией »–s»(эта опция позволяет подключать отладчик) и подключим gdb, выполнив в нем команду «target remote localhost:1234». Выведем несколько инструкций вокруг EIP, и увидим, что виртуальная машина находится в HALT, при этом в стеке нули. Совершенно непонятно, откуда мы сюда попали? Похоже, придется дизассемблировать.

acac6239dc5242cfaeea798f49970b88.png

Дизассемблирование и отладка кода


Попробуем разобраться, где происходит прыжок в HALT, последовательно дизассемблируя и отлаживая код! Начнем с первого сектора дискеты. При загрузке с флоппи в legacy режиме (а по другому, наверное, и не выйдет), BIOS читает первый сектор и грузит его по адресу 0×7c00. Чаще всего, задача кода из первого сектора — загрузить «продолжение» с диска и перейти в защищенный режим. Посмотрим, что же за код там находится, посредством утилит dd и objdump.

6caaeaea74004f5ebc401b979ce53cef.png

Немного пролистав код от начала, можно заметить переход в защищенный режим. Инструкция ljmp здесь используется для изменения селектора кода, в качестве адреса перехода стоит 0×7c61. Так как при дизассемблировании я не указывал базу, равную 0×7c00, то в моем листинге адресу 0×7c61 соответствует 0×61.

Обычно это делается для того, чтобы начать выполнять 32 битный код. Дополнительно в этом можно убедиться, найдя структуру gdt, адрес которой находится в регистре gdtr, значение которого лежит по адресу 0×7d95 и загружается инструкцией lgdtw по адресу 0×7c4d (0×4d в нашем листинге).

В gdt нужно посмотреть на тип дескриптора со смещением в 8, это первый аргумент инструкции ljmp. Это значит, что код по адресу 0×7c61 32 битный, а значит, дизассемблировать objdump«м его нужно с другими параметрами. Выделим из task.bin по смещению 0×61 интересующий нас код и дизассемблируем его как 32 битный.

051676c5e942407aa6441ced959c79b0.png

В получившемся коде в селекторы загружаются новые значения, и происходит прыжок на адрес 0×80000. Запустим виртуальную машину и поставим брейкпоинт на этот адрес. Для этого QEMU запускается с командой

qemu –s –S –fda task.bin

gdb подключается так, как это было сделано раньше. Установить брейкпоинт на адрес — «b *0×80000» в gdb, продолжить — «c». После срабатывания брейкпоинта выведем несколько инструкций.

55082f3fde3d41368febfbee481f05d0.png

Выполним первый jmp командой «si» и снова выведем код к выполнению.

3a505dcc08d14cf8b01168cae927f7de.png

Код до первого ret небогат на ветвления, и есть только один call, в котором что-то может происходить. Сдампим 4Кб памяти по адресу 0×82961, и посмотрим, что за код там выполняется. Дамп памяти можно получить из gdb следующей командой:

d0d5cef1bcf5454da6d796e502c7fe02.png

Дизассемблируем получившийся дамп командой

objdump –D –b binary –m i386 ./eip_dump.bin > eip.txt

Функция по адресу 0×82961 содержит довольно много call«ов, но сама представляет из себя последовательный кусок кода с одним ret«ом в конце. Нас интересует, где мы попадаем в halt, и так как в видимом коде halt«а нет — поставим брейкпоинты на все call«ы и ret в конце функции.

Вот список интересующих нас адресов: 0×82970, 0×82aef, 0×82A5B, 0×82A7D, 0×82A91, 0×82AB0, 0×82AEF, 0×82B10, 0×82B32, 0×82B46, 0×82B65, 0×82C53, 0×82CAD. Далее будем продолжать выполнение, последовательно вываливаясь на каждом поставленном брейкпоинте. Нас интересует брейкпоинт, после которого произойдет зависание. Им оказывается брейкпоинт, установленный на ret — в конце исследуемой функции. Это неожиданно, но если обратить внимание на push перед ret, то становится ясно, что это не возврат в точку вызова, а передача управления на новый код. Выполним si и попадаем на адрес 0×4000020.

c0c9b52e3b16448287b9b0e2bcd59318.png

Ура, наконец-то мы запустили задание!


Как мы помним, инструкция halt находится по адресу 0×4000260, что уже значительно ближе к нынешнему eip. Что бы снова не искать call«ы и не ставить брейкпоинты руками, сделаем следующим образом — напишем простой скрипт, который будет в цикле исполнять одну инструкцию, печатать следующую и проверять, что eip!= 0×4000260. Скрипт выглядит следующим образом:

b *0x4000020
commands 1
        while $pc != 0x4000260
                x /1i $pc
                si
        end
        x /1i $pc
end

c

Поместим скрипт в файл script.txt и выполним его в gdb командой source. После выполнения получим следующее:

0089875d7c3e4d6b878d1d5d388c3912.png

В коде бросаются в глаза два вызова cpuid, после которых происходит зависание. Похоже, что это какие-то проверки. Разберемся что же они проверяют. Первый вызов выполняется с параметром eax = 0×80000000, в качестве результата в eax содержится максимальное значения параметра, которое можно передавать инструкции cpuid. Далее значение сравнивается с 0×80000001, это проверка на возможность выполнения следующего вызова. Второй вызов выполняется с параметром eax = 0×800000001, а проверяется 29-й бит edx, который установлен в 1, если поддерживается Long Mode.

Похоже, что виртуальная машина зависает потому, что QEMU, которое я запускаю, не поддерживает Long Mode. Запустим виртуальную машину следующим образом:

1965c2db74254f19aace86b5ad8d1ba3.png

Ура, нам удалось запустить задание! Дело осталось за небольшим — выполнить его! Вообще, описанной выше проблемы с зависанием не случилось бы, если linux, в котором мы работаем, был 64х битным. В данном случае нам просто не повезло с разрядностью системы.

Подбор пароля


Добравшись до самого задания, становится ясно, что требуется сделать. Судя по всему, нужно подобрать «password», который бы удовлетворял алгоритм проверки. Для этого нужно найти место, в котором происходит проверка введенного пароля.

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

c203813382364cadb5db0e209e693c44.png

Как же определить, где поставить брейкпоинт в коде, для того, чтобы попасть в нужное место? Печать символа на экран возможна двумя способами:

  1. Простой способ — запись символа в видеопамять по адресу 0xb8000 в текстовом режиме, который включен по умолчанию при старте.
  2. Сложный способ — написание драйвера, который бы настраивал видеокарту, и предоставлял функцию рисования точки на экране, а затем, используя шрифты, по точкам нарисовать символ. Вместо драйвера можно использовать BIOS VBE, как это сделано здесь.

Предположим, что использовался простой способ. Тогда мы можем поставить брейкпоинт на доступ к видеопамяти, а именно, к первому символу четвертой строки. Видеопамять начинается с адреса 0xb8000, размер строки 80 символов, на каждый символ приходится 2 байта (символ + цвет), искомый адрес равен 0xb8000 + 80×2 * 3 = 0xb81e0. Команда на установку брейкпоинта на запись в память в gdb будет выглядеть так:

watch *0xb81e0

643fd4a351e24e14a385c22b3245b6d3.png

Предположение было верно, мы вывалились сразу после записи символа в память. Брейкпоинт больше не нужен, можно его удалить. Сделаем еще одно предположение — допустим, что есть функция, в которой последовательно вызывается код печати и проверки пароля. Тогда исходный код должен выглядеть следующим образом:

c4fc0e820ce6474185023da5b09654a5.png

Наша цель — найти функцию CheckPass (). Для этого будем ставить брейкпоинты на адреса возврата из функций, вложенных в PrintPass (), и продолжать выполнение. Если вывалились в только что установленный брейкпоинт, а сообщение » Password incorrect.» еще не напечатано, то ставим новый и продолжаем.

Если напечатано, то предпоследний поставленный и был нам нужен — он стоял сразу после вызова PrintPass () в теле task (). Разберемся, как достать адрес возврата. Если код компилировался без специфичных флагов, то в начале функции парой инструкций «push $rbp; mov $rsp, $rbp» формируется новый фрейм стека. В таком случае, по адресу $rbp+8 хранится адрес возврата. Это легко проверить:

f12ca01b38ae48408ca371f4a40a5123.png

Действительно, перед адресом 0xfffff8000020e5b5 находится инструкция call. Теперь мы можем осуществить задуманное.

Пишем скрипт


Так как неизвестно, какой глубины стек, напишем небольшой скрипт для gdb, который будет подниматься вверх по стеку, пока QEMU не начнет печатать на экран «Password incorrect».

set confirm off

# save start values of first 4 chars from 5th row of screen
set $start_vmem_val = *(unsigned long long*)(0xb8280)
set $curr_vmem_val = $start_vmem_val

# if nothing changed in 5th row of screen, we continue
while $start_vmem_val == $curr_vmem_val
        # delete all old breakpoints
        d
        
        # get return addres from stack and set breakpoint on it. Then, continue.
        set $ret_addr = *(unsigned long long*)($rbp + 8)
        b *$ret_addr
        c
        
        set $curr_vmem_val = *(unsigned long long*)(0xb8280)
end

Сохраним скрипт в файл и выполним командой source, как уже делали до этого. Получим следующее:

fc76ec29ae08498ebb8247d92ab0b0c3.png

Скрипт завис, так и не попав в брейкпоинт по адресу 0×20069c, но сообщение «Password incorrect» напечатано. Это значит, что предположение о существовании функции, которую мы назвали task (), верно. Зависание говорит о том, что функция task () никогда не возвращается после того, как сообщение «Password incorrect» печатается на экран. Впрочем, это не важно, главное, что мы теперь знаем адрес возврата из функции PrintPass (), на который мы поставили предпоследний, тринадцатый брейкпоинт.

Поиски продолжаются…


Продолжим поиск процедуры проверки пароля от только что полученного адреса 0xfffff80000205808: запустим QEMU, поставим брейкпоинт на этот адрес и введем произвольный пароль. Снимем дамп кода, отступив несколько байт назад от RIP, чтобы узнать адрес функции, из которой мы только что вышли.

40d4fe6a97e94ee1b7e9dfe7c242ab72.png

Дизассемблируем полученный дамп командой «objdump –D –b binary –m i386: x86–64 –adjust-vma=0xfffff800002057fc task.bin > task.txt».

Обратим внимание на то, что мы только что вышли из функции по адресу 0xfffff80000203358, и этот адрес еще несколько раз встречается в полученном дампе.

fffff800002057fc <.data>:
fffff800002057e3: 48 8d 85 60 ff ff ff lea -0xa0(%rbp),%rax
fffff800002057ea: 48 89 c6 mov %rax,%rsi
fffff800002057ed: 48 bf 56 15 21 00 00 movabs $0xfffff80000211556,%rdi
fffff800002057f4: f8 ff ff
fffff800002057f7: b8 00 00 00 00 mov $0×0,%eax
fffff800002057fc: 48 ba 58 33 20 00 00 movabs $0xfffff80000203358,%rdx
fffff80000205803: f8 ff ff
fffff80000205806: ff d2 callq *%rdx
rip => fffff80000205808: 48 b8 08 15 21 00 00 movabs $0xfffff80000211508,%rax
fffff8000020580f: f8 ff ff
….
fffff80000205930: 48 bf 65 15 21 00 00 movabs $0xfffff80000211565,%rdi
fffff80000205937: f8 ff ff
fffff8000020593a: b8 00 00 00 00 mov $0×0,%eax
fffff8000020593f: 48 ba 58 33 20 00 00 movabs $0xfffff80000203358,%rdx
fffff80000205946: f8 ff ff
fffff80000205949: ff d2 callq *%rdx
….
fffff8000020594d: 48 bf 78 15 21 00 00 movabs $0xfffff80000211578,%rdi
fffff80000205954: f8 ff ff
fffff80000205957: b8 00 00 00 00 mov $0×0,%eax
fffff8000020595c: 48 ba 58 33 20 00 00 movabs $0xfffff80000203358,%rdx
fffff80000205963: f8 ff ff
fffff80000205966: ff d2 callq *%rdx

Рассматриваемый код 64-битный и есть две основные конвенции вызовов, применяемых в 64х битном коде:

  1. «Microsoft x64 calling convention»
  2. «System V ABI»


В данном случае используется System V, так как аргументы для call передаются через регистры RDI, RSI, RDX и т.д. Мы только что вышли из функции, которая, как минимум, вывела на экран текст, и эта функция вызывается несколько раз. В первый раз она вызывается с аргументами 0xfffff80000211556 и -0xa0(%rbp), во второй раз — с 0xfffff80000211565, в третий раз — с 0xfffff80000211578. Посмотрим, что находится по этим адресам.

21dc74d0d8d34e7784fc18758da285d8.png

Функция 0xfffff80000203358 — это printf, и, в зависимости от результата проверки, она выводит разные сообщения. Строка »123» — это введенный пароль. Посмотрим, в зависимости от чего выводятся сообщения.

fffff8000020591c: movabs $0xfffff800002114c0,%rax
fffff80000205926: mov 0×38(%rax),%rax
fffff8000020592a: cmp $0×1,%rax if (g_struct.res == 1)
,====< fffff8000020592e: jne 0xfffff8000020594d {
| fffff80000205930: movabs $0xfffff80000211565,%rdi
| fffff8000020593a: mov $0×0,%eax
| fffff8000020593f: movabs $0xfffff80000203358,%rdx
| fffff80000205949: callq *%rdx printf («Password correct!»);
| ,==< fffff8000020594b: jmp 0xfffff80000205968 }
`====> fffff8000020594d: movabs $0xfffff80000211578,%rdi else
| fffff80000205957: mov $0×0,%eax {
| fffff8000020595c: movabs $0xfffff80000203358,%rdx
| fffff80000205966: callq *%rdx printf («Password incorrect.»);
`==> fffff80000205968: movabs $0xfffff80000204b83,%rax }
fffff80000205972: callq *%rax some_func ();
fffff80000205974: leaveq
fffff80000205975: retq

Результат проверки хранится в структуре по адресу 0xfffff800002114c0 со смещением 0×38. Посмотрим, есть ли обращения к этой структуре в рассматриваемой функции.

fffff8000020587c: mov $0×48,%edx
fffff80000205881: mov $0×0,%esi
fffff80000205886: movabs $0xfffff800002114c0,%rdi
fffff80000205890: movabs $0xfffff80000203d40,%rax
fffff8000020589a: callq *%rax memset (&g_struct, 0, 0×48);
fffff8000020589c: lea -0xa0(%rbp),%rdx
fffff800002058a3: movabs $0xfffff800002114c0,%rax
fffff800002058ad: mov %rdx,(%rax) *(u64*)& g_struct = password;
fffff800002058b0: lea -0xa0(%rbp),%rdx
fffff800002058b7: movabs $0xfffff800002114c0,%rax
fffff800002058c1: mov %rdx,0×20(%rax) *((u64*)& g_struct + 4) = password;

Выше по коду есть вызов функции с тремя аргументами, один из которых является указателем на нашу структуру. Если обратиться к коду этой функции, то станет ясно, что это memset. В структуру дважды записывается указатель на строку с введенным паролем, по смещению 0 и 32(0×20). Видимо, это инициализация. Если посмотреть на код между инициализацией и проверкой результата, то мы увидим следующее:

; выше находится инициализация структуры g_struct
fffff800002058c5: movzbl -0×1(%rbp),%eax l_var1 = -0×1(%rbp);
fffff800002058c9: mov %rax,%rdi
fffff800002058cc: movabs $0xfffff80000203e94,%rax
fffff800002058d6: callq *%rax if (func1(l_var1))
fffff800002058d8: test %rax,%rax {
fffff800002058db: sete %al
fffff800002058de: test %al,%al
,====< fffff800002058e0: je 0xfffff80000205909
| fffff800002058e2: movabs $0xfffff80000211508,%rax asm (
| fffff800002058ec: mov (%rax),%rax push *0xfffff80000211508
| fffff800002058ef: mov %rax,%rdx retq
| fffff800002058f2: push %rdx);
| fffff800002058f3: retq
| fffff800002058f4: movzbl -0×1(%rbp),%eax
| fffff800002058f8: mov %rax,%rdi
| fffff800002058fb: movabs $0xfffff800002040b2,%rax
| fffff80000205905: callq *%rax func2(l_var1);
| ,==< fffff80000205907: jmp fffff8000020591c }
`====> fffff80000205909: movzbl -0×1(%rbp),%eax else
| fffff8000020590d: mov %rax,%rdi {
| fffff80000205910: movabs $0xfffff800002040b2,%rax
| fffff8000020591a: callq *%rax func2(l_var1);
`==> fffff8000020591c: movabs $0xfffff800002114c0,%rax }
; ниже находится проверка и вывод результата

Желтым цветом выделены ветвления в коде, в которых может находиться код проверки пароля. Несколько странно выглядит конструкция push/ret посреди кода, так как не ясно, как будет продолжаться выполнение после нее. Мы по-прежнему ищем функцию проверки пароля.

Функции по адресам 0xfffff800002040b2 и 0xfffff80000203e94 не используют введенный пароль и не обращаются к найденной структуре. Интерес представляет пара инструкций push, retq, посредством которых происходит прыжок на адрес 0xfffff80000600000, но если попытаться посмотреть, что за код находится по этому адресу, то мы увидим следующее:

8b4be7642c064a6daf67405489a68382.png

При попытке его выполнить, происходит переход на адрес 0xfffff80000209ac5. Почему так происходит? Сообщение об ошибке доступа к памяти наталкивает на мысль, что виртуальная память по этому аддресу не доступна. Это можно проверить, выполнив «info mem» в консоли QEMU.

01485aefcd4140bbbd0688095c4a8ad7.png

И действительно, 2х мегабайтный диапазон с адреса 0xf80000600000 не замаплен. Пусть вас не смущает, что верхние 4 цифры равны нулю, а не f — при трансляции виртуальных адресов в 64х битном режиме верхние 16 бит не используются, и адрес 0×0 равен адресу 0xfffff00000000000. При обращении по незамапленному адресу происходит #PF (page fault), проблемный адрес записывается в CR2, управление передается соответствующему обработчику исключений, который в нашем случае находится по адресу 0xfffff80000209ac5. В верности этого предположения можно еще раз убедиться, посмотрев значение регистра CR2 в консоли QEMU — оно равно 0xfffff80000600000.

Внимательно смотрим на код


В обработчике прерывания в начале сохраняется состояние, и первый код на С появляется по адресу 0xfffff8000020da3c. В нем есть интересное место:
….
0xfffff8000020da5b: cmp $0xe,%rax
0xfffff8000020da5f: jne 0xfffff8000020da95
0xfffff8000020da61: mov -0×18(%rbp),%rax
0xfffff8000020da65: mov 0xb8(%rax),%rdx
0xfffff8000020da6c: movabs $0xfffff80000211508,%rax
0xfffff8000020da76: mov (%rax),%rax
0xfffff8000020da79: cmp %rax,%rdx
0xfffff8000020da7c: jb 0xfffff8000020da95
0xfffff8000020da7e: mov -0×18(%rbp),%rax
0xfffff8000020da82: mov %rax,%rdi
0xfffff8000020da85: movabs $0xfffff80000204df8,%rax
0xfffff8000020da8f: callq *%rax
….
Сравнение с 0xe (#PF) очень похоже на проверку причины возникшего исключения, а по адресу 0xfffff80000211508 лежит значение 0xfffff80000600000, с которым происходит еще 1 сравнение. Если оба условия выполняются, то происходит call на адрес 0xfffff80000204df8. Там мы можем увидеть следующий код:
….
0xfffff80000204e19: movabs $0xfffff8000020fda0,%rax
0xfffff80000204e23: lea (%rdx,%rax,1),%rax
0xfffff80000204e27: mov (%rax),%rdx
0xfffff80000204e2a: mov %rdx,-0×50(%rbp)
0xfffff80000204e2e: mov 0×8(%rax),%rdx
0xfffff80000204e32: mov %rdx,-0×48(%rbp)
0xfffff80000204e36: mov 0×10(%rax),%rax
0xfffff80000204e3a: mov %rax,-0×40(%rbp)
0xfffff80000204e3e: mov -0×50(%rbp),%rax
0xfffff80000204e42: cmp $0×726574,%rax
0xfffff80000204e48: je 0xfffff80000205418
0xfffff80000204e4e: cmp $0×726574,%rax
0xfffff80000204e54: ja 0xfffff80000204ea4
0xfffff80000204e56: cmp $0×69667a,%rax
0xfffff80000204e5c: je 0xfffff80000205067
0xfffff80000204e62: cmp $0×69667a,%rax
0xfffff80000204e68: ja 0xfffff80000204e87
0xfffff80000204e6a: cmp $0×616464,%rax
0xfffff80000204e70: je 0xfffff80000205225
….
Значительную часть функции занимает пара инструкций cmp/je, обилие которых наталкивает на мысль о том, что в к коде на Си здесь был длинный switch/case, где каждому значению соответствовал свой обработчик. В качестве сравниваемого выступает 8 ми байтное значение, читаемое по адресу 0xfffff8000020fda0 с некоторым смещением.

Если поставить брейк-поинт на данный код, то можно увидеть, что он выполняется многократно и смещение всегда кратно 24-м. Это похоже на виртуальную машину с длиной инструкции в 24 байта, где первые 8 байт — это сигнатура инструкции, а оставшиеся 16 — параметры. Сделаем дамп памяти по адресу 0xfffff8000020fda0 командой «dump memory vmcode.bin 0xfffff8000020fda0 0xfffff80000210da0» и откроем его в хекс эдиторе (у меня под рукой оказался Okteta).

3ee8086f91384661a138fa05496c5ba1.png

В правой части рисунка отчетливо видно, что сигнатуры инструкций закодированы в виде комбинаций ASCII символов. Среди инструкций есть такие, как llac, tixe, bus, что наводит на мысль о том, что мы видим их перевернутыми. Это связано с тем, что в коде на Си они были записаны, как значение в одинарных кавычках, хранимое в little-endian. В качестве параметров инструкций выступают или значения r0, r1, … и т. д., или числа.

Добрались до кода виртуальной машины!


В принципе, добравшись до кода виртуальной машины, разобрать его — дело техники. Сигнатуры инструкций недвусмысленно намекают на то, как они работают, а если что-то все же нужно уточнить, то достаточно найти соответствующий обработчик в функции по адресу 0xfffff80000204df8. В псевдокоде код VM выглядит примерно так:

r3 = 5381;
while (1)
{
   r1 = *(u8*)r0;
   if (r1 == 0)
      break;

   r3 = r3 * 33 + r1;
}
// check DJB hash
if (r3 != 0x40e1baa8ff648029)
{
   return 0;
}
r5 << 64 + r3 = (u128)hexstr2val(r4)
if (r5 - r3 != 0x2a60386296a57940)
{
   return 0;
}
r2 = r3 >> 32;
r1 = (r3 << 32) >> 32;
if (r2 - r1 != 0x3394749a)
{
   return 0;
}
r2 = (r3 << 32) >> 48;
r1 = (r3 << 48) >> 48;
if (r2 - r1 != 0x465e)
{
   return 0;
}
return 1;       // success

После проверки DJB хэша от введенной строки идут дополнительные условия, которым должен удовлетворять ключ.

Пример программы для подбора ключа


Ключ не присутствует в проверках в явном виде, но на базе 4х условий в псевдокоде его можно быстро подобрать. Ниже представлен пример программы, подбирающей верный ключ.

int main(int argc, char **argv)
{
    unsigned long long d64 = 0x2a60386296a57940;
    unsigned long long d32 = 0x3394749a;
    unsigned long long d16 = 0x465e;
    unsigned long long step = 0x1000100010001;

    unsigned long long key_l = ((d32 << 32) + (d16 << 48)) + ((d16 << 16) + 0);
    unsigned long long key_h = key_l + d64;

    std::stringstream key_ss;
    unsigned int i = 0;
    while (1)
    {
        key_ss << std::hex << key_h;
        key_ss << std::hex << std::setw(16) << std::setfill('0') << key_l;

        unsigned long long key_str_hash = djb2_hash(key_ss.str().c_str());
        if (key_str_hash == 0x40e1baa8ff648029)
        {
            std::cout << "Success! " << i << "\n";
            std::cout << "res_key = '" << key_ss.str() << "'\n";
        }
        key_h += step;
        key_l += step;

        i++;
        key_ss.str("");
    }
    return 0;
}

Pадание пройдено! Чтобы внести дополнительную ясность в то, как используются исключения в процессе проверки пароля, ниже изображена общая схема вызовов.

626fa0c503a748ef9f139b02f0701c4c.png

Цикл выполнения инструкции VM начинается с генерации #PF, затем в обработчике исключения происходит выполнение одной инструкции VM, если эта инструкция была инструкцией exit, то выполняется longjmp, который возвращает нас в момент до выполнения первого #PF. Далее печатается результат проверки ключа. Если выполнялась любая другая инструкция VM, то обработчик исключения восстанавливает состояние перед возникновением #PF, и цикл начинается заново.

Вопрос к Хабровчанам


Небольшое отступление: для анализа полученного дампа было бы очень удобно использовать дизассемблер, подобный IDA, так как в него можно было бы загрузить дамп памяти (сдампив, предварительно, побольше), указать, где код, данные, дать имена функциям и переменным. Однако демо- и триальные версии IDA не работают с 64 битным кодом.

Я пробовал приспособить для этих целей бесплатный radare2, который умеет все, что нам нужно, но столкнулся с тем, что rasm некорректно разбирает инструкции вида «movabs $0xfffff800002114c0,%rax», и в некоторых версиях не получается установить адрес, по которому загружается код (bin.laddr), выше 4Gb. Если кто-то из читателей Хабра опишет, как в radare2 загрузить и наглядно разобрать такой дамп, то я буду благодарен. В разборе задания я все же ограничился objdump, gdb и $EDITOR.

Чего ждать участникам NeoQUEST-2015?


Участников ожидает продолжение масонской легенды и 7 заданий, на которые есть 8 часов (начало соревнования для участников — в 10:00)! Задания будут касаться различных аспектов информационной безопасности, поэтому каждый сможет найти задание на свой вкус, в соответствии с имеющимися навыками. В 18:00 мы подведем итоги, и победитель получит главный приз — поездку на одну из международных конференций, «серебряному» и «золотому» участникам также достанутся крутые призы. До главного кибербезопасного Питерского события осталось всего ничего!

© Habrahabr.ru