CVE-2018-19134:通过Ghostscript中的类型混淆来远程执行代码

 

一、前言

在这篇文章中,我将展示如何利用CVE-2018-19134漏洞远程执行任意代码。该漏洞是由类型混淆引起的。2018年11月,该漏洞被发现。如果你想了解更多关于QL技术相关文章,可以翻看我以前的博客

 

二、漏洞

我们先回顾一下这个漏洞,首先,PostScript对象的类型为结构体ref_s(或者为ref,ref是ref_s的别名),如下图所示:

图1

该结构大小为16字节,其中tas_s类型占据前8个字节,包含的字段为类型信息以及数组,字符串,字典的大小。如下图所示:

图2

我发现该漏洞触发的原因是zsetcolor函数中缺少类型检查:在将其解释为gs_pattern_instance_t之前未进行pPatInst类型检查。如下图所示:

图3

如下图所示,r_ptr是一个宏,被定义在iref.h文件中:

图4

pstruct的值源自PostScript,因此由用户控制。例如,setpattern的输入将造成pPatInst.value.pstruct的值为0x41。如下所示:

图5

将代码放入pattern_instance_uses_base_space后,我看到我控制了一个指针对象,代码将其解释为gs_pattern_instance_t指针,如下所示:

图6

所以我可以控制许多函数指针:get_pattern,uses_base_space和pinst。

创建一个伪造的对象

PostScript类型数组非常重要,因为它的值指向ref数组开头的ref指针。并允许我创建一个缓冲区进行存储,我可以控制缓冲区内容,如下所示:

图7

如上图,灰色部分表示我可以控制部分数据(我无法完全控制type_attrs和pad),绿色部分是我可以完全控制的数据。其中关键点是,refs中的值和gs_pattern_instance_t中的type都是8个字节,意味着pinst-> type-> procs中的procs将是部分被我控制的底层PostScript数组。事实证明,我确实可以通过使用嵌套数组来控制函数指针get_pattern和uses_base_space:

图8

[16#41 [16#51 16#52]]的结果如下所示:

图9

这表明我确实可以控制uses_base_space和get_pattern,下一步,如何使用任意函数指针来实现代码执行。

我决定先获取一些有效的函数指针,在Ghostscript中,内置的PostScript运算符由t_operator类型表示,ref的值是一个op_proc_t,它是一个函数指针。可以通过下面的命令获得:

图10

让我们尝试在我们伪造的数组中加入一些内置函数,如下所示:

图11

我将使用以下指令:systemdict <foo> get <arr> exch <idx> exch put。该指令可以从systemdict中获取foo,并将其存储在索引为idx的数组中。

实际上,我现在可以直接调用zget函数和zput函数来替代uses_base_space和get_pattern,如下所示:

图12

我现在可以调用PostScript中的函数并控制函数的参数,当从PostScript调用底层C函数时,执行上下文作为参数传递给C函数,该上下文类型为i_ctx_t(gs_context_state_s的别名),包含许多无法通过PostScript控制的信息,其中包括重要的安全设置,如LockFilePermissions,如下所示:

图13

当从PostScript调用操作符时,传递的参数存储在op_stack中,通过直接调用这些函数并控制参数i_ctx_p。

我们尝试创建一个PostScript数组来伪造i_ctx_t对象,PostScript函数参数存储在i_ctx_p-> op_stack.stack.p中,这是一个指向参数的ref指针。为了使用伪造的上下文调用PostScript函数,我需要控制p。从p到i_ctx_p的偏移量实际上与op_stack的偏移量相同,即0x270。由于每个ref的大小为0x10,所以对应伪造数组的第39个元素。

图14

从上图可以看出,这种对齐并不理想,op_stack.stack.p对应数组的tas部分,我无法完全控制,如果只有op_stack对应的ref值,我便能成功控制。另外,tas存储了ref的元数据,即使我完全控制,也无法在不知道其地址的情况下将其设置为任意对象地址。因此,任何漏洞利用很可能只会在此时崩溃在Ghostscript中。

获取任意的读写原语

我想找到一个符合下列条件的PostScript函数:

1.引用osp(op_stack.stack.p)指针
2.使用osp指针做一些事情
3.可在SAFER模式下使用

我想到了pop运算,如下所示:

图15

它使用check_op检查堆栈指针的值,并将osp与指针osbot进行比较,如果大于osbot,则减小osp的值,这是一个简单的函数,并且不会取消引用osp。我们先仔细研究一下ref和op_stack的结构,如下图所示:

图16

回顾我们之前提到的东西,在我们伪造的对象中,op_stack是ref数组的第39个元素,如上图,字段tas对应p,而value对应osbot。type_attrs,_padd和rsize三个字段组合在一起形成op_stack中的指针p。如之前提到的,type_attrs指定ref对象的类型及其可访问性。通过pop函数,我可以修改选择对象的类型和可访问性。但问题在于,pop只有在p大于osbot时才有效,osbot是ref对象的地址。所以我篡改的对象需要是一个足够大的字符串,数组或字典。以便于rsize字段大于大多数ref对象的指针地址。我无法仅修改systemdict等内置只读对象的可访问性以获得写权限。但是,我可以做以下事情:

1.我可以使用将数组转换为字符串的方法,将内部引用数组视为字节数组,因为PostScript中的字符串不是由空字符终止,而是由rsize指定的长度。因此任何字节都可以从缓冲区读/写。值得注意的是,这样做并不会产生任何越界读/写,因为结果字符串具有与原始数组相同的长度,生成的字节缓冲区仅覆盖ref数组分配缓冲区的1/16。

2.我也可以反过来做,将一个字符串转换成一个相同长度的数组。如上所述,生成的ref数组将比原始字符串数组大16倍,这样我可以进行OOB读写,但我没有这么做。

我还需要克服另一个技术难题,伪造对象,pinst实际上调用了两个函数,如下所示,一个函数输出到另一个:

图17

如上所示,use_base_space的返回值是pinst-> type-> procs.get_pattern(pinst)。现在,我们把zpop(pinst)作为输入。当我使用任何内置的PostScript运算符替代uses_base_space时,可能导致产生空指针。使zpop返回0。我需要找到一个不使用上下文指针i_ctx_p的操作。

下面是我进行的查找代码:

图18

我的QL查询使用了一些启发式方法来识别PostScript运算符,它们的名称通常以z开头,并在psi目录中定义。它们也采用i_ctx_t 类型的参数,然后,我查找不会取消引用参数的函数,该函数不会调用自身且不包含i_ctx_t 类型的变量。

您可以在LGTM.com上的130,000+的GitHub,Bitbucket和GitLab项目上运行自己的QL查询,您可以使用在线查询控制台,也可以安装QL for Eclipse插件并在本地快照上运行查询语句。Ghostscript不是在GitHub,Bitbucket或GitLab上开发的,因此LGTM.com尚未对其进行分析。 但是您可以在此处下载Ghostscript代码快照

查询后返回了6个结果:

图18.1

如上图所示,ucache函数是我们所需要的。首先我们伪造pinst对象,如下所示:

图19

现在我们需要创建一个大型数组对象并将其存储在pinst的第39个元素中。它的元数据tas将被解释为堆栈指针地址osp。我将使用PostScript中的put运算作为第一个元素,然后使用pop将类型更改为字符串并读取zput函数的地址。

图20

不幸的是,数组的type_attrs值是0x4,而string的值是0x12。我必须使ushort下溢以从数组转换为字符串。所以我进行impl setpattern操作1291次。

图21

从上面的截图可以看出,伪造的数组被转换为字符串,我得到了zput的地址。我可以使用该方法将字节写入arr中的任何位置。

现在我可以从任意PostScript对象读取和写入任意字节。我最初的计划是覆盖LockFilePermissions参数,然后调用file,允许任意命令执行,如同CVE-2018-19475中的操作。然而,事实证明,我还需要伪造许多对象。这使工作变得复杂。相反,我仅仅想要调用一个简单但功能强大的函数,forceput操作能够满足我的需求。

总结一下,我需要做的事情:

1.使用forceput提供的参数创建一个伪造的操作数堆栈。
2.覆盖pinst中的地址,并将操作数堆栈指针的地址存储到我上面创建的堆栈中。
3.获取forceput的地址,并替换pinst-> type.procs.getpattern的值。

为了实现1,我只需要用我的参数创建一个数组。如下所示:

图22

然后我可以将它存储在arr中以检索此数组的地址。重用pinst并将其放在第31个元素中:

图23

使用上一节的技巧,我现在可以读取这个伪造堆栈指针的地址并将其写入pinst中的适当位置。我可以简单获取zput地址,并将其偏移量添加到zforceput以获取zforceput的地址(因为此偏移量不是随机的)。在使用commit 81f3d1e编译的调试二进制文件中,此偏移量为0x437,在9.25版本中,偏移量为0x4B0。执行此操作后,我可以将LockFilePermissions参数写入当前设备,然后运行任意shell命令。如下所示,是从Ghostscript中启动计算器的示意图。

图24

通过覆盖userparams中的其他字段,如PermitFileReading和PermitFileWriting,还可以获得任意文件访问。至此,漏洞利用结束。

(完)