В 2019 году на конференции BSidesCBR в рамках соревнования CyBearsCTF была представлена интересная задача под названием Block Dude. На первый взгляд, это была простая игра, выполненная с использованием библиотеки pygame, где игрок управляет персонажем, двигаясь по платформам, подбирая и устанавливая блоки для преодоления препятствий на пути к выходу. Однако настоящий вызов заключался не столько в прохождении игры, сколько в исследовании её внутренней структуры и поиске значительно более глубоких уязвимостей, которые позволяли вывести игру за рамки обычного геймплея и получить хищнический флаг победителя CTF.В архиве с игрой содержалось несколько файлов: графические ресурсы (тайлы), исходный код на языке C, скрипт на Python, а также скомпилированная библиотека libblock_dude.so.
Инструкция предполагала запуск игры через команду python3 BlockDude.py с указанием удалённого адреса и порта. Это означало, что игра была сетевой и имела серверную часть, которая выполняла ключевые операции по хранению состояния и обработке команд игрока.Первоочередным шагом стало изучение исходников. Скрипт на Python, судя по всему, был лишь интерфейсом для пользовательского взаимодействия и отображения игры, он просто передавал команды пользователя на сервер и обновлял визуальный вид.
Ключевой интерес вызвал заголовочный файл blockdude.h, где определялся глобальный массив карты и структура состояния игры State. Эта структура содержала несколько байтовых полей, интересных с точки зрения потенциальных уязвимостей: количество блоков, текущий выбранный блок, флаг того, несет ли персонаж блок, координаты игрока, направление, команда, успех выполнения и массив из восьми блоков с их координатами.Интересно, что массив блоков ограничивался восемью элементами, и при этом не было гарантий, что программа должным образом следит за выходом за пределы этого массива. Это сразу навело на мысль об опасности переполнения буфера — особенно с типами unsigned char (u8), которые могут «прокручиваться» по границам и отрицательные значения могли использоваться для выхода за пределы.
Одним из важнейших открытий стала функция update_south, в которой реализован механизм подбора и сброса блоков. Забрав найденные условия, автор заметил, что при выполнении определенных действий (например, стоя рядом с блоком и нажав вниз) персонаж может поднять новый блок, при этом поле num_blocks инкрементируется без проверок на максимум. Это позволило создавать массив блоков вне заявленных восьми. На практике, при создании блоков с индексами выше восьми, программа начинала читать и использовать память за пределами выделенного массива, фактически обращаясь к информации, находящейся в стеке — в частности, к возвратному адресу функции main_loop.Понимание того, что расположение блока 9 и 10 на игровом поле на самом деле отражало байты возвратного адреса, было настоящим прорывом.
Возвратный адрес main_loop находился очень близко к функции debug_flag, которая и содержала чтение и вывод флага из файла на сервере. Функция debug_flag в исходном коде не вызывалась напрямую, но если переписать возвращаемый адрес функции main_loop на адрес debug_flag, то при завершении main_loop управление направится к желаемой функции, которая выведет флаг.Важной составляющей эксплойта стало понимание именно структуры стека в вызывающей функции client_game. Там выделялось 1024 байта стека под какие-то данные, которые в ходе работы не использовались явно, что давало дополнительное пространство для манипуляций с блоками без риска аварийного завершения программы из-за повреждения критичных данных.В процессе атаки автору удалось с помощью грамотного управления поднятыми блоками изменить координаты блоков 9 и 10, тем самым переписав частично возвратный адрес main_loop и направив программу к функции debug_flag.
Для этого использовалось смещение блока на девять позиций влево по игровому полю, что отзывалось на значениях нужных байтов, ведь x и y координаты блоков напрямую отображались в последовательные байты в памяти рядом с возвратным адресом.В решении задачи не обязательно было писать скрипт и автоматизировать процесс. Игрок мог вручную проделывать нужные ходы: «прогулка» назад и вперед по игровому полю, подбирая и сбрасывая блоки в правильных местах, строя «лестницы» для преодоления стен, чтобы достать до нужных координат и наконец вывести игру к двери, завершая уровень.Технически этот пример хорошо демонстрирует важность контроля границ массивов и проверки пользовательского ввода в программах, а особенно в тех, что работают с памятью на низком уровне. Использование вложенных структур, отсутствие проверок и особенности работы с памятью призвели к возникновению критической уязвимости, позволяющей изменить ключевые данные процесса.