Данный текст всего лишь небольшой writeup трех реверсерских историй вокруг маскота конференции OffZone 2024. Тот самый символ конференции – загадочный куб (таинственный предмет с глазом). Как объясняют организаторы про кубоглазы – «идея была в том, что не только люди тянутся к новым технологиям и их безграничному потенциалу, но и технологические артефакты тянутся к людям в ответ, пытаясь понять иррациональную природу человека». Как раз к «новым технологиям» можно было прикоснуться, решив три задачки на реверс.
Сами задания не зубодробительные, без нудного брутфорса, логичные, компактные, с приятными мелочами, за что респект их автору @revker. Рассмотрим их в порядке возрастания сложности. И, да, картинок будет много. Бинари можно взять с облака.
CUB_3_3 = Зеленый куб (стартовая цена 400)
Легенда и данные задачи на рисунке
А Вы, уважаемый читатель, когда-нибудь пробовали отмерить что-либо без линейки? Так скажем сантиметров 60. Сложно, но можно. А такие люди живут среди нас. Удивительные товарищи: прикинут на глаз, раз и готово. Проверяешь с линейкой, и точно тот самый размер. И всё благодаря опыту («сын ошибок трудных»). В обратной разработке такой метод уважаемые люди называют «глазным реверсом». В этом случае не требуется сложных инструментов, достаточно любого hex-редактора (в моем случае WinHex). Поехали!
Рассмотрим файлы green_dump и commands.txt. Глянув на содержимое команд, уверенно предполагаем, что green_dump – это дамп сессии процесса python.exe. Повторим, первые пять команд создания случайного ключа на компе (Crypto – это модуль известного пакета pycryptodome, который должен быть установлен для питона заранее).
Последняя введенная команда на картинке выше – «key, iv» выводит содержимое сгенерированного ключа key и инициализирующего вектора iv в консоль в формате «(b’…’, b’…’)». Созданный сгенерированный ключ будет отличаться от ключа в дампе, поэтому ищем содержимое ключа по формату (первых символов «(b’» хватит) в green_dump.
Находим два вхождения. Содержимое key и iv и там и там одинаковое, но обратим внимание, что во втором вхождении перед выделенным блоком есть структура: некий адрес (не очень важно на что он указывает) и размер 63h, совпадающий с длиной нашего результата. Итак, ключ и вектор получили:
И этот ключ используется для шифрования некоего файла green_cube.png по алгоритму AES CBC.
Чем хорош png-файл? Тем, что первые 16 байт – это константа. Ну, почти.
Поиск в дампе этой константы не привел к успеху. Тогда зашифровываем первые 16 байт png заголовка с найденным ключом и пробуем найти зашифрованный заголовок в дампе.
Результат найден с похожей структурой: адрес (и снова не очень важно на что он указывает) + размер (150560h). Осталось только расшифровать эти данные и получить флаг.
Задача оказалась столь популярной и на второй день конференции организаторы перестали за неё выдавать offcoin (местная чеканная монета).
Мораль. Тренируйте «глазной реверс».
CUB_3_4 = Синий куб (стартовая цена 700)
Легенда и данные задачи на рисунке
В книге «Искусстве ясно мыслить» Рольфа Добелли есть пассаж про альтернативные пути на примере двух людей. Один рискнул жизнью и выиграл в русскую рулетку 10 лимонов, а другой усердно работал и заработал те же 10 лимонов только за 20 лет. И человек со стороны, глядя на этих уже богатых людей, не знает в чём принципиальная разница между ними: в уровне риска при обретении денег. «Что такое альтернативные пути? Всё, что могло бы произойти, но не произошло. Альтернативные пути невидимы для окружающих, оттого мы так редко о них задумываемся». Уязвимости – это альтернативные пути в цифровом мире. И эта задачка хороший пример этому.
Удобный 64-битный бинарь на STL C++. Почему удобный? Есть отладочные символы, нет антиотладки, обфускаций и пр.
Что же делает программа, кроме того, что при старте просит ввести имя. Позволяет ходить по семи комнатам CubeRoom(в отладочных символах имя «кубокомната» применяется, а в консоли пишется как «комната»). При запуске программы оказываемся в одной из них (точнее в одной и той же – этот момент захардкожен), поэтому перемещаться можно только в шесть других. Каждая комната имеет номер (9-тизначный, чтоб лучше запоминалось ?), указав который происходит переход в следующую. И еще. Перед тем как сменить комнату просят ввести сообщение на случай возвращения. Т.е. при возврате в посещенную комнату будет отображаться оставленное сообщение.
Большинство уязвимостей крутится вокруг непроверенного ввода пользователя. На что влияет пользователь тут: номер, имя, сообщение. Проще всего с номером. Номер сравнивается со списком номеров комнат, который хранится внутри каждого CubeRoom: есть совпадение – перемещаемся в следующую комнату, нет совпадений – остаемся в прежней. Тут нет никакого обмана. С именем будет поинтереснее.
В функции CubeRoom::GetLastVisitorMessage (показать сообщение в посещенной комнате) обнаружилось, что имя никак не проверяется и может стать причиной уязвимости Path Traversal. Функция sprintf обрезает итоговый буфер на 255 символах. Т.е. весь буфер можно заполнить очень длинным вводом в формате «/../../../../..///////имяфайла» и вычитать файл.
Но есть нюанс. Эта ветка кода с Path Traversal не выполнится НИКОГДА! Потому что пользователя постоянно просят ввести сообщение (поле message на рисунке ниже) при смене комнаты. Пустой ввод не прокатит, он всегда будет дополняться строкой «None». Одним словом, поле message всегда будет ненулевым, а ветка кода с уязвимостью как раз требует пустого сообщения.
Хороший вывод. Уязвимость есть, но не доступна. Тогда посмотрим на поле message. И тут находим вторую уязвимость. Под сообщение отводится 256 байт и читается не более этого. Так вот вопрос: если сообщение будет ровно 256 байт, тогда конец строки (нулик) запишется в 257-й символ и что же перепишется в этом случае? В этом случае надо смотреть на структуру объекта CubeRoom, где и находится поле message.
Все семь объектов CubeRoom храним в контейнере std::map<long,CubeRoom> (STL всё-таки), где ключ – это номер комнаты, значение – объект CubeRoom (на рисунке ниже). Поле message находится перед массивом connections, в котором храним номера комнат для перемещения. И если в message запишем 256 символов, то 257-м окажется первый байт первого элемента массива connections. А именно номер кубокомнаты 148395987 (8D857D3h). И в результате перезаписи D3 57 D8 08 нуликом получим 00 57 D8 08 = 8D85700h (148395776). Номер несуществующей комнаты. А вот это уже сюрприз, так сюрприз.
Вторая уязвимость позволяет изменить номер комнаты и нарушить логику программы, которая приведет к созданию несуществующей комнаты. В этом заключается путь решения задачи. Дело в том, что для выбора комнаты используется метод std::map<long,CubeRoom>::operator[](gCube, &number). Его штатное поведение в STL имеет такой момент, если объект не найден по ключу (а в нашем случае ключ – это номер комнаты), то std::map добавит новый элемент «ключ = объект CubeRoom» в свою коллекцию. А у нового CubeRoom согласно конструктора все поля будут ПУСТЫЕ (на рисунке ниже), кроме name. После выбора комнаты выполняется функция CubeRoom::GetLastVisitorMessage, и с пустым сообщением сможем вычитать файл flag.txt.
Наблюдательный читатель обратит внимание на функцию CubeRoom::IsVisited. Созданный CubeRoom не может быть уже посещенным, если флажок is_visited в объекте равен нулю. Просто CubeRoom::IsVisited принимает решение не только на основании флажка, но и на наличии поля name (в новом объекте оно проинициализировано).
Алгоритм решения:
Запускаем blue_code
Указываем своё имя «../../../../../..////////////////////////////////flag.txt» (254 символа)
Оказываемся в комнате 987133987 (это захардкоженный номер, всегда начинаем тут)
Вводим номер следующей комнаты 148395987
Оставляем сообщение для комнаты 987133987 – «AAA…AAA» (256 символов), тем самым изменяем первый номер в списке номеров кубокомнат (148395987 станет 148395776)
Оказываемся в комнате 148395987
Вводим номер следующей комнаты 987133987 (возвращаемся обратно)
Оставляем сообщение для комнаты 148395987 – любое
Оказываемся в комнате 987133987 (и вот тут список номеров на экране консоли начинается с 148395776)
Вводим номер следующей комнаты 148395776 (она создастся с пустыми полями)
Оставляем сообщение для комнаты 987133987 – тут уже указываем любое
Оказываемся в комнате 000 (в комнате 148395776 список номеров не инициализирован) и читаем файл flag.txt
Конечно, flag.txt лежал на сервере организаторов. Откуда и надо было его утянуть, решив попутно POW (Proof-of-Work). На второй день конференции задача стала хорошей добычей. Ибо при получении мерча в местной лавке правил девиз сталкеров: «Есть хабар — пойдёт базар. Нет хабара — нет базара».
Мораль: ищите альтернативные пути, реверсерам/пентестерам за это деньги платят.
CUB_3_2 = Красный куб (стартовая цена 2000)
Легенда и данные задачи на рисунке
Финалочка. Богатая на монеты задача. Но цена не всегда предполагает сложность. Задача решалась после завершения конференции в спокойной домашней обстановке и показалась в меру простой.
Первые испытания встретились с элементарной загрузки бинаря в Иду. Вот так выглядит точка входа с адресом 404670h в программу:
Нетипичные инструкции для пролога функции, особенно на старте. Неужели какой-либо упаковщик. Прям не очень приятная штука, словно манускрипт Войнича. Одна из причин почему задачка решалась после мероприятия.
В итоге, ничего страшного не обнаружилось. Это так называемое «противоИдие». Средство для запутывания пользователей, работающих с Идой, можно сказать фича для конкретного дизассемблера при обработке ELF-файлов. Всё дело в обработке таблиц PHT (Program Headers Table) и SHT (Section Headers Table). В операционной системе загрузчик исполняемых файлов всегда ориентируется на таблицу заголовков программы PHT, при этом игнорируя таблицу заголовков секций SHT. Ида же пытается усидеть на двух стульях, анализируя и PHT, и SHT при создании сегментов. Больше уделяя внимания SHT. И вот тут специально подготовленные SHT приводят к забавным артефактам.
В нашем случае PHTEntry 1 – сегмент кода с виртуального адреса 401000, и SHTEntry 1 тоже указывает на сегмент кода, но с виртуального адреса 4010BF. В результате получим два сегмента: 401000 (Ида обрежит его до размера BFh) и 4010BF (размером 1784DEh, как в SHTEntry 1), а данные в них будут проецироваться из одного и тоже места, начиная с файлового смещения 1000h. Получается первые BFh байты будут совпадать в обоих сегментах, а точка входа 404670hокажется сдвинутой ровно на BFhбайт и попадает в середину некой функции. Сегмент данных на самом деле должен начинаться с адреса 57A000 (это PHTEntry 2), но SHTEntry 2 вообще указывает на заголовок файла 400000h (это PHTEntry 0). Так выглядит картина в дизасме версии 7.7 / 8.4. А в версии 6.95 Ида в первую очередь ориентируется только на SHT при создании сегментов, не обращая внимания на остальные PHTEntry.
Выход тут один – патчить SHT Entry для корректной загрузки. Еще одна картинка.
С загрузкой в Иду разобрались.
Вернемся к задаче. Бинарь без отладочной информации, STL C++ в комплекте вместе со статической сборкой (чем и объясняется большой размер исполняемого файла). При старте программы просят ввести пароль длиной 18 символов. Также при старте создаётся объект, состоящий из 6 матриц размером 4х4 – из 6 квадратов. Квадраты заполняются числами при старте. Специально пометим нули на рисунке, их окажется ровно 18 по числу символов пароля. В эти позиции будут устанавливаться значения на основании символов пароля.
Так зачем нам эти квадраты. Их значение увидим в финальной проверке перед тем, как отдать флаг, находящаяся в функции 404C7Ch. Сама проверка подтверждает, что квадрат является магическим.
Магическим квадратам более 4000 лет. Древняя штука: от ветхого Китая, Индии до средневековья и наших дней (подробнее тут). Кубы OffZone тоже магия, их и сюда добавили. В нашем случае квадрат 4х4.
Решим магический квадрат на примере самого первого из шести. Нулевые элементы (сверху вниз) обозначим как x, y, z. Вокруг таблицы будут суммы строк, колонок и двух диагоналей.
Сумма по всем направлениям должна быть одинакова и равна 8С00h для данного квадрата. Легко посчитать, что x=2000h, y=3000h, z=1000h. В таком же стиле найдем все остальные значения. Хотя для ленивых напишем скрипт на примере второго квадрата (на базе библиотеки sympy).
from sympy import symbols, Eq, solve
x, y, z = symbols('x,y,z')
M = [
0x27F6, 0x33F3, x, 0x0BFD,
0x43EF, y, 0x23F7, 0x37F2,
0x13FB, 0x4FEC, 0x2BF5, 0x1FF8,
0x2FF4, z, 0x17FA, 0x4BED
]
equations = [
Eq(M[0]+M[5]+M[10]+M[15], M[3]+M[6]+M[9]+M[12]), #sum diag1 == sum diag2
Eq(M[0]+M[5]+M[10]+M[15], M[0]+M[1]+M[2]+M[3]), #sum diag1 == sum row 0
Eq(M[0]+M[1]+M[2]+M[3], M[0]+M[4]+M[8]+M[12]), #sum row 0 == sum col 0
Eq(M[0]+M[4]+M[8]+M[12], M[2]+M[6]+M[10]+M[14]), #sum col 0 == sum col 2
Eq(M[2]+M[6]+M[10]+M[14], M[3]+M[7]+M[11]+M[15]), #sum col 2 == sum col 3
Eq(M[0]+M[4]+M[8]+M[12], M[1]+M[5]+M[9]+M[13]), #sum col 0 == sum col 1
Eq(M[0]+M[1]+M[2]+M[3], M[8]+M[9]+M[10]+M[11]), #sum row 0 == sum row 2
Eq(M[4]+M[5]+M[6]+M[7], M[12]+M[13]+M[14]+M[15]), #sum row 1 == sum row 3
Eq(M[0]+M[1]+M[2]+M[3], M[4]+M[5]+M[6]+M[7]), #sum row 0 == sum row 1
]
solution = solve(equations)
print("x = %Xh (%d)" % (solution[x], solution[x]))
print("y = %Xh (%d)" % (solution[y], solution[y]))
print("z = %Xh (%d)" % (solution[z], solution[z]))
Так получим все значения преобразованных символов пароля (на рисунке ниже). Основная часть задачи решена. Осталось малость: разобрать способ преобразования символов.
Схема преобразования символов пароля на рисунке ниже. Вся сила в псевдослучайном генераторе, для которого необходимо узнать начальное значение (seed). Реализацию генератора можно найти в freebsd libc исходнике «random.c» (например, тут).
Найденный пароль – «JfivkLA49f0LawFp1C». Содержимое flag.txt не получилось прочитать, конкурс-то уже закончился.
Мораль: противоИдие (дизассемблер может стать врагом твоим, усложнив задачу)
Эпилог
Перефразируя главного героя из «Форрест Гамп» можно сказать: «Задачки на реверс как коробка шоколадных конфет: никогда не знаешь, какая начинка тебе попадётся». Испытание, суета, игра разума, потраченное время, с пользой проведенное время, что-то новое, что-то забытое старое, понять сокрытое, сокрыть понятное, брутфорсное уныние, просто уцуцуга. И всё равно ждём новых задач на следующей конференции.
Доклад окончен.
P.S. Крайний эпиграф для тех мастеров, к которым прислушивались и общались при написании этого текста, особенно когда молчали.
Как-то один прохожий пришел к Будде и спросил: «Можете ли вы поведать мне об истине, не используя слов, но и не отбрасывая слова?»
Будда остался в молчании.
Человек поклонился и поблагодарил Будду: «Благодаря вашему высочайшему милосердию я избавился от всех пут и иллюзий и вступил на Путь».
Когда человек ушел, ближайший ученик Ананда спросил у Будды:
- Так почему же он прозрел?
- Хорошая лошадь пускается вскачь лишь при виде тени от плетки! – ответил Будда.
Самое сокровенное и тонкое всегда передается вне слов, жестов и трактатов! Но значит ли это, что достаточно побыть рядом с мастером, посмотреть, как он многозначительно молчит, - и тотчас прозреть всю глубину бытия? Конечно, нет. Важно, чтобы сознание человека само было подготовлено к этому, а мастер лишь дает небольшой толчок.
Из книги «Диалоги китайских мудрецов» Алексея Маслова
4uneral
Привет! Я овнер этой зоны на конференции)
Спасибо за райтап :) Мы с автором тасков очень рады, что задания так вам понравились, что вдохновили на написание райтапа.
Только небольшое уточнение. Согласно нашей легенде CUB_3 — это основной маскот, но у него нет глаз, только лопасти на гранях. А вот его дочерние объекты с кодовыми номерами имеют глаза)
Если вам будет интересно, с историей можно ознакомиться здесь, если вы еще не https://offzone.moscow/cub-3/