
Доказательство концепции для CVE-2024-54756, уязвимости, которую я обнаружил в ZScript scripting engine в GZDoom.
Доказательство концепции уязвимости произвольного выполнения кода, которую я нашёл в функциональности ZScript в GZDoom (https://github.com/zdoom/gzdoom). Злоумышленник может распространить PK3-файл, содержащий вредоносный исходный файл ZScript, и получить доступ к ПК жертвы.
Большое спасибо Rachael и Agent Ash из команды разработчиков GZDoom за оперативные ответы, а также им и другим разработчикам GZDoom за быстрое устранение проблемы!
Подтверждена работа для 4.13.0 и 4.13.1, и, вероятно, работает и для более ранних версий. Остерегайтесь тех, кто советует вам откатиться до версии 4.13.1 или ниже, чтобы иметь возможность играть в их WAD.
Это PoC работает только в Linux, но уязвимость, скорее всего, существует и в Windows. Не проверялось на ZDoom или LZDoom, но уязвимость может существовать и там.
Уязвимость была раскрыта разработчикам до публикации этого PoC, и она больше не должна присутствовать в версии 4.13.2. Насколько мне известно, эта версия не содержит каких-либо существенных критических изменений.
Это PoC создано и опубликовано в образовательных целях, чтобы разработчики игровых/скриптовых движков могли понять, как могут возникать уязвимости, и чтобы игроки могли понять, как может выглядеть вредоносная модификация игры. Я не несу ответственности за любое неправомерное использование этого PoC. Пожалуйста, не используйте это для взлома компьютеров ваших товарищей по игре; это незаконно (вам не нужно, чтобы я вам это говорил), и это особенно подлый поступок — захватывать чей-то компьютер через видеоигру.
Чтобы использовать это PoC, скачайте этот репозиторий и создайте PK3-файл (который на самом деле является zip-файлом с расширением .pk3), содержащий zscript.zs и MAPINFO:
git clone https://github.com/Chainmanner/GZDoom-Arbitrary-Code-Execution-via-ZScript-PoC
cd GZDoom-Arbitrary-Code-Execution-via-ZScript-PoC
zip PoC.pk3 zscript.zs MAPINFO
Полезная нагрузка по умолчанию — запуск обратной оболочки на localhost через порт 1337. Запустите прослушиватель:
nc -nvlp 1337
Запустите PoC следующим образом:
gzdoom -iwad <your-doom-or-freedoom-wad> -file PoC.pk3
Если всё сработало, вы должны получить обратную оболочку к самому себе.
Это PoC только для Linux. Может не сработать с первой попытки; просто пробуйте снова, пока не получится.
ПРИМЕЧАНИЕ: Это моя первая статья об эксплуатации, и я всё ещё совершенствую свои навыки написания материалов низкого уровня. Кроме того, я проводил большую часть отладки с помощью GDB, и, к сожалению, не додумался сохранить некоторые дампы памяти, чтобы лучше проиллюстрировать свои объяснения. Извините! Моя следующая статья будет лучше, обещаю.
GZDoom — это порт исходного кода Doom, предназначенный для производительности и расширяемости. Благодаря его мощным функциям было создано множество замечательных WAD, модов и даже коммерческих полных переработок. К сожалению, там, где есть сложность, есть и возможность появления уязвимостей, и в данном случае в скриптовом движке ZScript присутствовали две уязвимости, которые позволили реализовать полную цепочку эксплуатации.
Эта атака обходит ASLR и обходит необходимость обхода стековых канареек. Я не думаю, что CFI от Clang или теневые стеки могли бы здесь помочь.
Первая и наиболее важная уязвимость заключалась в обработке огромных массивов. Если выделить достаточно маленький массив, выделенная область памяти обычно заполняется нулями и должным образом отделена от других объектов; чтение неинициализированной памяти не даёт никакой информации, и никакие объекты не перекрываются с массивом. Однако, если выделить огромный массив — скажем, 1073741823 32-битных слова или больше — вы сможете читать и записывать до 4 ГиБ потенциально неинициализированной памяти, начиная с точки начала массива, что позволяет атакующему напрямую изменять другие объекты и обходить ASLR, находя адреса с известными смещениями. Кроме того, любые другие массивы, созданные после этого места, будут перекрываться с огромным массивом.
Вторая уязвимость была в разрешениях страниц памяти. Для повышения производительности код ZScript компилируется JIT в байт-код x86 или x86-64, когда это возможно. Для этого код должен быть записан в область памяти, и эта область памяти должна быть исполняемой. Однако правило W^X гласит, что область должна быть либо записываемой, либо исполняемой, но не одновременно. Если применяются оба разрешения одновременно (вместо того, чтобы сделать область записываемой, записать код, а затем сделать область исполняемой и незаписываемой), то атакующий, имеющий примитив произвольной записи, сможет повысить его до произвольного выполнения кода; он может записать шелл-код и перейти на него, например, изменив адрес возврата в стеке (при условии, что у атакующего нет примитива произвольного выполнения). Если посмотреть на отображение памяти GZDoom во время работы, можно увидеть несколько областей RWX:
7fcd19700000-7fcd19800000 rwxp 00000000 00:00 0
7fcd1a100000-7fcd1a200000 rwxp 00000000 00:00 0
7fcd1eb00000-7fcd1ec00000 rwxp 00000000 00:00 0
Таким образом, если доступны примитивы произвольной записи и произвольного выполнения, и атакующий знает, где находится любая область RWX, он может записать произвольный шелл-код и выполнить его. Сделать эти области RW- при записи JIT-скомпилированного кода, а затем R-X, когда они готовы к выполнению, остановило бы это PoC, но не помешало бы атакующему получить выполнение кода, например, изменяя данные в стеке (ROP) или куче.
Кроме того, есть полезный гаджет. Помните, что при выделении огромного массива любые другие массивы, созданные после него, будут перекрываться? Это касается и массивов указателей на объекты. Подобно объектам C++, объекты ZScript могут содержать переменные и указатели на функции. Предположим, у нас есть такой объект:
class WeirdObject
{
uint one;
uint two;
uint three;
uint four;
Function<clearscope void()> funcptr;
}
Если мы создадим массив, содержащий указатель на экземпляр WeirdObject, то атакующий может изменить указатель куда угодно с помощью огромного массива и изменить данные по этому адресу, обращаясь к полям объекта, что даёт нам примитив произвольного чтения/записи, выходящий за пределы кучи. Указатели в ZScript проверяются на неравенство нулю, но не на то, что они корректны.
Наличие указателя на функцию также даёт нам примитив произвольного выполнения; однако это немного менее прямолинейно, требуя создания поддельного VMFunction, удовлетворяющего виртуальной машине. Как только в коде эксплойта появляется вызов функции ZScript, этот код больше не компилируется JIT. Всё ещё работает, но отладка и эксплуатация становятся немного более запутанными. Возможно, есть лучший способ выполнить эту часть, но я недостаточно хорошо изучил внутренности GZDoom, чтобы знать о нём.
Стоит отметить: у WeirdObject есть унаследованные переменные-члены, поэтому первый член начинается со смещения 0x28.
Итак, теперь у нас есть следующие инструменты:
Как объединить их для создания эксплойта?