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


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

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

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

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


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


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



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

    qemu –fda task.bin
    

    и …



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

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


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



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

    set architecture i386
    


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

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



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


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



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

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

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



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

    qemu –s –S –fda task.bin
    


    gdb подключается так, как это было сделано раньше. Установить брейкпоинт на адрес – “b *0x80000” в gdb, продолжить – “c”. После срабатывания брейкпоинта выведем несколько инструкций.



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



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



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

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


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

    Вот список интересующих нас адресов: 0x82970, 0x82aef, 0x82A5B, 0x82A7D, 0x82A91, 0x82AB0, 0x82AEF, 0x82B10, 0x82B32, 0x82B46, 0x82B65, 0x82C53, 0x82CAD. Далее будем продолжать выполнение, последовательно вываливаясь на каждом поставленном брейкпоинте. Нас интересует брейкпоинт, после которого произойдет зависание. Им оказывается брейкпоинт, установленный на ret – в конце исследуемой функции. Это неожиданно, но если обратить внимание на push перед ret, то становится ясно, что это не возврат в точку вызова, а передача управления на новый код. Выполним si и попадаем на адрес 0x4000020.



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


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

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


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



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

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



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

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


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

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



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

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


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

    watch *0xb81e0
    




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



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

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



    Действительно, перед адресом 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, как уже делали до этого. Получим следующее:



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

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


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



    Дизассемблируем полученный дамп командой “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 $0x0,%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 $0x0,%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 $0x0,%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. Посмотрим, что находится по этим адресам.



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

    fffff8000020591c: movabs $0xfffff800002114c0,%rax
    fffff80000205926: mov 0x38(%rax),%rax
    fffff8000020592a: cmp $0x1,%rax if (g_struct.res == 1)
    ,====< fffff8000020592e: jne 0xfffff8000020594d {
    | fffff80000205930: movabs $0xfffff80000211565,%rdi
    | fffff8000020593a: mov $0x0,%eax
    | fffff8000020593f: movabs $0xfffff80000203358,%rdx
    | fffff80000205949: callq *%rdx printf(“Password correct!”);
    | ,==< fffff8000020594b: jmp 0xfffff80000205968 }
    `====> fffff8000020594d: movabs $0xfffff80000211578,%rdi else
    | fffff80000205957: mov $0x0,%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 со смещением 0x38. Посмотрим, есть ли обращения к этой структуре в рассматриваемой функции.

    fffff8000020587c: mov $0x48,%edx
    fffff80000205881: mov $0x0,%esi
    fffff80000205886: movabs $0xfffff800002114c0,%rdi
    fffff80000205890: movabs $0xfffff80000203d40,%rax
    fffff8000020589a: callq *%rax memset(&g_struct, 0, 0x48);
    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,0x20(%rax) *((u64*)& g_struct + 4) = password;

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

    ; выше находится инициализация структуры g_struct
    fffff800002058c5: movzbl -0x1(%rbp),%eax l_var1 = -0x1(%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 -0x1(%rbp),%eax
    | fffff800002058f8: mov %rax,%rdi
    | fffff800002058fb: movabs $0xfffff800002040b2,%rax
    | fffff80000205905: callq *%rax func2(l_var1);
    | ,==< fffff80000205907: jmp fffff8000020591c }
    `====> fffff80000205909: movzbl -0x1(%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, но если попытаться посмотреть, что за код находится по этому адресу, то мы увидим следующее:



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



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

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


    В обработчике прерывания в начале сохраняется состояние, и первый код на С появляется по адресу 0xfffff8000020da3c. В нем есть интересное место:
    ….
    0xfffff8000020da5b: cmp $0xe,%rax
    0xfffff8000020da5f: jne 0xfffff8000020da95
    0xfffff8000020da61: mov -0x18(%rbp),%rax
    0xfffff8000020da65: mov 0xb8(%rax),%rdx
    0xfffff8000020da6c: movabs $0xfffff80000211508,%rax
    0xfffff8000020da76: mov (%rax),%rax
    0xfffff8000020da79: cmp %rax,%rdx
    0xfffff8000020da7c: jb 0xfffff8000020da95
    0xfffff8000020da7e: mov -0x18(%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,-0x50(%rbp)
    0xfffff80000204e2e: mov 0x8(%rax),%rdx
    0xfffff80000204e32: mov %rdx,-0x48(%rbp)
    0xfffff80000204e36: mov 0x10(%rax),%rax
    0xfffff80000204e3a: mov %rax,-0x40(%rbp)
    0xfffff80000204e3e: mov -0x50(%rbp),%rax
    0xfffff80000204e42: cmp $0x726574,%rax
    0xfffff80000204e48: je 0xfffff80000205418
    0xfffff80000204e4e: cmp $0x726574,%rax
    0xfffff80000204e54: ja 0xfffff80000204ea4
    0xfffff80000204e56: cmp $0x69667a,%rax
    0xfffff80000204e5c: je 0xfffff80000205067
    0xfffff80000204e62: cmp $0x69667a,%rax
    0xfffff80000204e68: ja 0xfffff80000204e87
    0xfffff80000204e6a: cmp $0x616464,%rax
    0xfffff80000204e70: je 0xfffff80000205225
    ….
    Значительную часть функции занимает пара инструкций cmp/je, обилие которых наталкивает на мысль о том, что в к коде на Си здесь был длинный switch/case, где каждому значению соответствовал свой обработчик. В качестве сравниваемого выступает 8ми байтное значение, читаемое по адресу 0xfffff8000020fda0 с некоторым смещением.

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



    В правой части рисунка отчетливо видно, что сигнатуры инструкций закодированы в виде комбинаций 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адание пройдено! Чтобы внести дополнительную ясность в то, как используются исключения в процессе проверки пароля, ниже изображена общая схема вызовов.



    Цикл выполнения инструкции 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 мы подведем итоги, и победитель получит главный приз — поездку на одну из международных конференций, «серебряному» и «золотому» участникам также достанутся крутые призы. До главного кибербезопасного Питерского события осталось всего ничего!
    НеоБИТ
    85,00
    Компания
    Поделиться публикацией

    Комментарии 13

      +2
      Я пошёл путём статического анализа, после того, как сдампил расшифрованный кусок.
      Ну и изредка gdb для проверок того, что всё верно понял (автоматом архитектура не менялась и сыпались ошибки, поэтому через несколько бряков пришлось работать):
      Скрипт GDB
      target remote localhost:1234
      display/3i $pc
      hbreak *0x04000148
      break *0x200318
      set arch i386:x86-64
      break *0xFFFFF80000204E19 if $rdx == 12*0x18

      Мнемоники инструкций ВМ даже не проверял, работу смотрел непосредственно в хэндлерах. Набросал скрипт который дизассемблит байт-код, даже интерпретатор влепил на всякий случай.
      Скрипт дизассемблера
      <?php
      
      $code =
      [
          [0x633272, '0x7233', '0x1505'],
      ...
          [0x726574, 0, 0]
      ];
      
      $regs = [];
      
      $regMap =
      [
          0x7230 => 'r0',   // +0x0  PASSWORD
          0x7231 => 'r1',   // +0x8
          0x7232 => 'r2',   // +0x10
          0x7233 => 'r3',   // +0x18
          0x7234 => 'r4',   // +0x20 PASSWORD
          0x7235 => 'r5',   // +0x28
          0 => 'pc',        // +0x30
          0x726573 => 'r6', // +0x38
          0x727362 => 'r7'  // +0x40
      ];
      
      function getByte($ptr)
      {
          return 0;
      }
      
      function getVMRegister($reg)
      {
          global $regMap;
          return $regMap[$reg];
      }
      
      function setVMRegister($reg, $value)
      {
          global $regMap;
          $regMap[$reg] = $value;
      }
      
      $opcodes =
      [
          0x726574 =>
          [
              'pc = r7',
              function ($arg1, $arg2) use ($regs)
              {
                  $regs['pc'] = $regs['r7'];
                  return 0;
              }
          ],
          0x69667A =>
          [
              'pc += (reg[$arg1] != 0) ? 1 : $arg2',
              function ($arg1, $arg2) use ($regs)
              {
                  $reg = getVMRegister($arg1);
                  $regs['pc'] += $reg ? 0x18 : $arg2 * 0x18;
                  return 0;
              }
          ],
          0x616464 =>
          [
              'reg[$arg1] += reg[$arg2]',
              function ($arg1, $arg2) use ($regs)
              {
                  setVMRegister($arg1, getVMRegister($arg1) + getVMRegister($arg2));
                  $regs['pc'] += 0x18;
                  return 0;
              }
          ],
          0x633272 =>
          [
              'reg[$arg1] = $arg2',
              function ($arg1, $arg2) use ($regs)
              {
                  setVMRegister($arg1, $arg2);
                  $regs['pc'] += 0x18;
                  return 0;
              }
          ],
          0x6A6D70 =>
          [
              'pc += $arg1',
              function ($arg1, $arg2) use ($regs)
              {
                  $regs['pc'] += $arg1 * 0x18;
                  return 0;
              }
          ],
          0x723272 =>
          [
              'reg[$arg1] = reg[$arg2]',
              function ($arg1, $arg2) use ($regs)
              {
                  setVMRegister($arg1, getVMRegister($arg2));
                  $regs['pc'] += 0x18;
                  return 0;
              }
          ],
          0x65786974 =>
          [
              'r6 = $arg1, exit from VM',
              function ($arg1, $arg2) use ($regs)
              {
                  setVMRegister(0x726573, $arg1);
                  return 0;
              }
          ],
          0x737562 =>
          [
              'reg[$arg1] -= reg[$arg2]',
              function ($arg1, $arg2) use ($regs)
              {
                  setVMRegister($arg1, getVMRegister($arg1) - getVMRegister($arg2));
                  $regs['pc'] += 0x18;
                  return 0;
              }
          ],
          0x63616C6C =>
          [
              'r7 = pc + 1, pc += $arg1',
              function ($arg1, $arg2) use ($regs)
              {
                  $regs['r7'] = $regs['pc'] + 0x18;
                  $regs['pc'] += $arg1 * 0x18;
                  return 0;
              }
          ],
          0x6D623272 =>
          [
              'reg[$arg1] = *(byte *)reg[$arg2]',
              function ($arg1, $arg2) use ($regs)
              {
                  setVMRegister($arg1, getByte(getVMRegister($arg2)));
                  $regs['pc'] += 0x18;
                  return 0;
              }
          ],
          0x73686674 =>
          [
              'reg[$arg1] = $arg2 > 0 ? reg[$arg1] >> $arg2 : reg[$arg1] << -$arg2',
              function ($arg1, $arg2) use ($regs)
              {
                  $reg = getVMRegister($arg1);
                  setVMRegister($arg1, ($arg2 >= 0) ? $reg >> $arg2 : $reg << -$arg2);
                  $regs['pc'] += 0x18;
                  return 0;
              }
          ],
      ];
      
      $pc = 0;
      foreach ($code as $command)
      {
          if (!isset($opcodes[$command[0]]))
          {
              echo "Unknown opcode, PC = $pc\n";
              break;
          }
          $cmd = $opcodes[$command[0]][0];
          $arg1 = $command[1];
          $arg2 = $command[2];
      
          if (isset($regMap[hexdec(substr($arg1, 2))]))
              $cmd = str_replace('reg[$arg1]', $regMap[hexdec(substr($arg1, 2))], $cmd);
          if (isset($regMap[hexdec(substr($arg2, 2))]))
              $cmd = str_replace('reg[$arg2]', $regMap[hexdec(substr($arg2, 2))], $cmd);
      
          if (preg_match('/0x0FFFFFFFFFFFFFF([0-9A-F]{1,2})/', $arg1, $matches))
              $arg1 = -(pow(16, strlen($matches[1])) - hexdec($matches[1]));
          if (preg_match('/0x0FFFFFFFFFFFFFF([0-9A-F]{1,2})/', $arg2, $matches))
              $arg2 = -(pow(16, strlen($matches[1])) - hexdec($matches[1]));
          $cmd = str_replace('$arg1', $arg1, $cmd);
          $cmd = str_replace('$arg2', $arg2, $cmd);
      
          echo '[' . str_pad($pc, 3, '0', STR_PAD_LEFT) . '] ' . $cmd . "\n";
          $pc++;
      }
      


      Результат:
      Байткод
      [000] r3 = 0x1505
      [001] r1 = *(byte *)r0
      [002] pc += (r1 != 0) ? 1 : 8
      [003] r2 = r3
      [004] r3 = -5 > 0 ? r3 >> -5 : r3 << --5
      [005] r3 += r2
      [006] r3 += r1
      [007] r2 = 1
      [008] r0 += r2
      [009] pc += -8
      [010] r2 = 0x40E1BAA8FF648029
      [011] r3 -= r2
      [012] pc += (r3 != 0) ? 1 : 2
      [013] r6 = 0, exit from VM
      [014] r7 = pc + 1, pc += 0x1F
      [015] r5 = r3
      [016] r7 = pc + 1, pc += 0x1D
      [017] r2 = r5
      [018] r2 -= r3
      [019] r1 = 0x2A60386296A57940
      [020] r2 -= r1
      [021] pc += (r2 != 0) ? 1 : 2
      [022] r6 = 0, exit from VM
      [023] r2 = r3
      [024] r2 = 0x20 > 0 ? r2 >> 0x20 : r2 << -0x20
      [025] r1 = r3
      [026] r1 = -32 > 0 ? r1 >> -32 : r1 << --32
      [027] r1 = 0x20 > 0 ? r1 >> 0x20 : r1 << -0x20
      [028] r2 -= r1
      [029] r0 = 0x3394749A
      [030] r2 -= r0
      [031] pc += (r2 != 0) ? 1 : 2
      [032] r6 = 0, exit from VM
      [033] r2 = r3
      [034] r2 = -32 > 0 ? r2 >> -32 : r2 << --32
      [035] r2 = 0x30 > 0 ? r2 >> 0x30 : r2 << -0x30
      [036] r1 = r3
      [037] r1 = -48 > 0 ? r1 >> -48 : r1 << --48
      [038] r1 = 0x30 > 0 ? r1 >> 0x30 : r1 << -0x30
      [039] r2 -= r1
      [040] r0 = 0x465E
      [041] r2 -= r0
      [042] pc += (r2 != 0) ? 1 : 2
      [043] r6 = 0, exit from VM
      [044] r6 = 1, exit from VM
      [045] r3 = 0
      [046] r0 = 0
      [047] r2 = 0x10
      [048] r0 -= r2
      [049] pc += (r0 != 0) ? 1 : 2
      [050] pc += 2
      [051] pc = r7
      [052] r0 += r2
      [053] r1 = *(byte *)r4
      [054] r2 = 0x61
      [055] r1 -= r2
      [056] pc += (r1 != 0) ? 1 : 2
      [057] pc += 8
      [058] r3 = -4 > 0 ? r3 >> -4 : r3 << --4
      [059] r2 = 0x0A
      [060] r3 += r2
      [061] r2 = 1
      [062] r4 += r2
      [063] r0 += r2
      [064] pc += -17
      [065] r1 = *(byte *)r4
      [066] r2 = 0x62
      [067] r1 -= r2
      [068] pc += (r1 != 0) ? 1 : 2
      [069] pc += 8
      [070] r3 = -4 > 0 ? r3 >> -4 : r3 << --4
      [071] r2 = 0x0B
      [072] r3 += r2
      [073] r2 = 1
      [074] r4 += r2
      [075] r0 += r2
      [076] pc += -12
      [077] r1 = *(byte *)r4
      [078] r2 = 0x63
      [079] r1 -= r2
      [080] pc += (r1 != 0) ? 1 : 2
      [081] pc += 8
      [082] r3 = -4 > 0 ? r3 >> -4 : r3 << --4
      [083] r2 = 0x0C
      [084] r3 += r2
      [085] r2 = 1
      [086] r4 += r2
      [087] r0 += r2
      [088] pc += -12
      [089] r1 = *(byte *)r4
      [090] r2 = 0x64
      [091] r1 -= r2
      [092] pc += (r1 != 0) ? 1 : 2
      [093] pc += 8
      [094] r3 = -4 > 0 ? r3 >> -4 : r3 << --4
      [095] r2 = 0x0D
      [096] r3 += r2
      [097] r2 = 1
      [098] r4 += r2
      [099] r0 += r2
      [100] pc += -12
      [101] r1 = *(byte *)r4
      [102] r2 = 0x65
      [103] r1 -= r2
      [104] pc += (r1 != 0) ? 1 : 2
      [105] pc += 8
      [106] r3 = -4 > 0 ? r3 >> -4 : r3 << --4
      [107] r2 = 0x0E
      [108] r3 += r2
      [109] r2 = 1
      [110] r4 += r2
      [111] r0 += r2
      [112] pc += -12
      [113] r1 = *(byte *)r4
      [114] r2 = 0x66
      [115] r1 -= r2
      [116] pc += (r1 != 0) ? 1 : 2
      [117] pc += 8
      [118] r3 = -4 > 0 ? r3 >> -4 : r3 << --4
      [119] r2 = 0x0F
      [120] r3 += r2
      [121] r2 = 1
      [122] r4 += r2
      [123] r0 += r2
      [124] pc += -12
      [125] r1 = *(byte *)r4
      [126] r2 = 0x30
      [127] r1 -= r2
      [128] pc += (r1 != 0) ? 1 : 2
      [129] pc += 8
      [130] r3 = -4 > 0 ? r3 >> -4 : r3 << --4
      [131] r2 = 0
      [132] r3 += r2
      [133] r2 = 1
      [134] r4 += r2
      [135] r0 += r2
      [136] pc += -12
      [137] r1 = *(byte *)r4
      [138] r2 = 0x31
      [139] r1 -= r2
      [140] pc += (r1 != 0) ? 1 : 2
      [141] pc += 8
      [142] r3 = -4 > 0 ? r3 >> -4 : r3 << --4
      [143] r2 = 1
      [144] r3 += r2
      [145] r2 = 1
      [146] r4 += r2
      [147] r0 += r2
      [148] pc += -12
      [149] r1 = *(byte *)r4
      [150] r2 = 0x32
      [151] r1 -= r2
      [152] pc += (r1 != 0) ? 1 : 2
      [153] pc += 8
      [154] r3 = -4 > 0 ? r3 >> -4 : r3 << --4
      [155] r2 = 2
      [156] r3 += r2
      [157] r2 = 1
      [158] r4 += r2
      [159] r0 += r2
      [160] pc += -12
      [161] r1 = *(byte *)r4
      [162] r2 = 0x33
      [163] r1 -= r2
      [164] pc += (r1 != 0) ? 1 : 2
      [165] pc += 8
      [166] r3 = -4 > 0 ? r3 >> -4 : r3 << --4
      [167] r2 = 3
      [168] r3 += r2
      [169] r2 = 1
      [170] r4 += r2
      [171] r0 += r2
      [172] pc += -12
      [173] r1 = *(byte *)r4
      [174] r2 = 0x34
      [175] r1 -= r2
      [176] pc += (r1 != 0) ? 1 : 2
      [177] pc += 8
      [178] r3 = -4 > 0 ? r3 >> -4 : r3 << --4
      [179] r2 = 4
      [180] r3 += r2
      [181] r2 = 1
      [182] r4 += r2
      [183] r0 += r2
      [184] pc += -12
      [185] r1 = *(byte *)r4
      [186] r2 = 0x35
      [187] r1 -= r2
      [188] pc += (r1 != 0) ? 1 : 2
      [189] pc += 8
      [190] r3 = -4 > 0 ? r3 >> -4 : r3 << --4
      [191] r2 = 5
      [192] r3 += r2
      [193] r2 = 1
      [194] r4 += r2
      [195] r0 += r2
      [196] pc += -12
      [197] r1 = *(byte *)r4
      [198] r2 = 0x36
      [199] r1 -= r2
      [200] pc += (r1 != 0) ? 1 : 2
      [201] pc += 8
      [202] r3 = -4 > 0 ? r3 >> -4 : r3 << --4
      [203] r2 = 6
      [204] r3 += r2
      [205] r2 = 1
      [206] r4 += r2
      [207] r0 += r2
      [208] pc += -12
      [209] r1 = *(byte *)r4
      [210] r2 = 0x37
      [211] r1 -= r2
      [212] pc += (r1 != 0) ? 1 : 2
      [213] pc += 8
      [214] r3 = -4 > 0 ? r3 >> -4 : r3 << --4
      [215] r2 = 7
      [216] r3 += r2
      [217] r2 = 1
      [218] r4 += r2
      [219] r0 += r2
      [220] pc += -12
      [221] r1 = *(byte *)r4
      [222] r2 = 0x38
      [223] r1 -= r2
      [224] pc += (r1 != 0) ? 1 : 2
      [225] pc += 8
      [226] r3 = -4 > 0 ? r3 >> -4 : r3 << --4
      [227] r2 = 8
      [228] r3 += r2
      [229] r2 = 1
      [230] r4 += r2
      [231] r0 += r2
      [232] pc += -12
      [233] r1 = *(byte *)r4
      [234] r2 = 0x39
      [235] r1 -= r2
      [236] pc += (r1 != 0) ? 1 : 2
      [237] pc += 8
      [238] r3 = -4 > 0 ? r3 >> -4 : r3 << --4
      [239] r2 = 9
      [240] r3 += r2
      [241] r2 = 1
      [242] r4 += r2
      [243] r0 += r2
      [244] pc += -12
      [245] pc = r7 
      



      Причёсанный код:
      Вменяемый байт-код
      r0 = r4 = password
      r3 = 0x1505
      while (r1 = *(byte *)r0)
        r3 = (r3 << 5) + r3 + r1
        r0++;
      if (r3 != 0x40E1BAA8FF648029) return 0;
      
      function 45
      r5 = r3
      function 45
      if (r5 - r3 != 0x2A60386296A57940) return 0;
      r2 = (r3 >> 32) - (r3 << 32) >> 32  # r2 = r3 [4..7] - r3 [0..3]
      if (r2 != 0x3394749A) return 0;
      r2 = (r3 << 32) >> 48 # r2 = r3 [2..3]
      r1 = (r3 << 48) >> 48 # r1 = r3 [0..1]
      return (r2 - r1 == 0x465E)
      
      function 45:
        r3 = 0, r0 = 0
        while (r0 != 0x10)
          r1 = *(byte *)r4
          if (r1 >= 0x61 && t1 <= 0x66)
            r3 = r3 << 4 + (0x0A + (r1 - 0x61))
            r4++, r0++
            continue;
          if (r1 >= 0x30 && r1 <= 0x39)
            r3 = r3 << 4 + (r1 - 0x30)
            r4++, r0++
            continue
      



      Кейген:
      Скрытый текст
      for i in range(0, 65536):
        word2 = (0x465E + i) & 0xFFFF
        dword1 = (word2 << 16) + i
        dword2 = (dword1 + 0x3394749A) & 0xFFFFFFFF
        qword2 = (dword2 << 32) + dword1
        qword1 = (qword2 + 0x2A60386296A57940) & 0xFFFFFFFFFFFFFFFF
        str = format(qword1, 'x') + format(qword2, 'x')
      #  print hex(i) + ': ' + str
        r3 = 0x1505
        for c in str:
          r3 = ((r3 << 5) + r3 + ord(c)) & 0xFFFFFFFFFFFFFFFF
        if r3 == 0x40E1BAA8FF648029:
          print str
          break
      

        –2
        Однако демо- и триальные версии IDA не работают с 64битным кодом.

        Работает бесплатная 5.0 https://www.hex-rays.com/products/ida/support/download_freeware.shtml
          0
          Спасибо! Будем знать!
            +1
            А как? У меня отказывается. Говорит, надо открывать в 64-битной версии Иды, а самой 64-битной версии там нет.
              0
              Ну ок, нельзя.
                0
                Ну вот… А я уж было возрадовался, что просто чего-то не заметил… :-(
            +3
            >> для анализа полученного дампа было бы очень удобно использовать дизассемблер, подобный IDA
            www.hopperapp.com
              0
              Спасибо!
              0
              Извините, где само задание-то? Интересно потренироваться.
                0
                  0
                  Спасибо! Все задание — это сам файл, без пояснений?
                    +1
                    Затаив дыхание, я поднимался по чугунной лестнице. Принято считать, что если, поднимаясь по лестнице Ротонды, закрыть глаза, то никогда не дойдешь до конца… Всё это, конечно, сказки, но атмосфера загадочности здесь действительно будто бы витала в воздухе. Я любил подняться до ротондовского тупичка, своего рода, лестничной площадки, и сидеть там, поворачиваясь так и сяк – вдруг и правда удастся увидеть тень от несуществующей, седьмой колонны?

                    Похоже, не так давно кто-то уже успел здесь побывать… На площадке лежала дискета. Не валялась, а именно лежала, чистая – ни пылинки! Как будто кто-то незадолго до моего прихода ее здесь бережно положил. Почему-то я сразу вспомнил незнакомца в черном. Брать чужие вещи, конечно, нельзя, но я как чувствовал: мне надо знать, что на этой дискете!

                    Дома, едва скинув куртку и ботинки, я уселся за компьютер. Скачивание и настройка эмулятора флоппи-дисковода заняли минут 15, и вот передо мной уже образ дискеты…
                      0
                      Весьма благодарен.

              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

              Самое читаемое