标题 简介 类型 公开时间
关联规则 关联知识 关联工具 关联文档 关联抓包
参考1(官网)
参考2
参考3
详情
[SAFE-ID: JIWO-2024-3002]   作者: 大猪 发表于: [2022-02-25]

本文共 [236] 位读者顶过

这是“横扫江湖的一个字体漏洞”系列文章中的第二篇。在第一篇文章即“BLEND漏洞入门”中,我们论述了最近四十年数字印刷领域如何发展出了当今应用广泛的丰富字体格式,并讲解了两种最常见的PostScript格式(Type 1和OpenType),还阐述了Windows NT OpenType/Type 1 字体驱动ATMFD.DLL(许多产品都会使用到这个驱动)的构造情况,最后介绍了“blend”字体漏洞。 [出自:jiwo.org]

今天,我们关注的是如何为这个安全漏洞开发一个可靠利用方式,如何成功在Renderer进程中通过它执行任意的代码,并为彻底征服整个计算机系统打好基础。好了,现在就下手吧!

Adobe Reader的漏洞利用方法

我们的总体目标是,构造一个PDF文件,让32位和64位的Windows 8.1 Update 1系统上的Adobe Reader 11.0.10(受影响的最新版本)在打开它时弹出calc.exe。对于这个过程,我们的要求是,对特定软件版本应该稳定可靠,并能够将计算器进程的权限提升至NT AUTHORITY/SYSTEM水平,同时还能与用户态和核心态中启用的漏洞利用缓解技术相安无事。 为此,第一阶段需要一个专门针对Adobe Reader的漏洞利用,第二阶段需要两个分别用于32位和64位Windows内核的漏洞利用。

虽然原则上讲,这个漏洞利用起来并非难事,因为它看起来简单而粗暴,然而魔鬼总是藏在细节里的,为了构造前面讲到的PDF漏洞利用,我们还有许多具体的事情要做。首先,虽然我们可以令op_sp指针指向本地的op_stk数组之外的内存,并使这些内存中的字符串作为指令执行,但是,这种方法并非对所有的指令操作符都行之有效。 具体地说,所有需要前移栈指针(即压入更多数据)的指令操作符,都会检查指针是否发生越界。 这给我们造成了极大的困难,因为这样一来,许多非常有用的数据写入指令我们就都无法访问了,最基本的一种就是常规的数值型指令,还有dup、pop、callgsubr、random等等。下面我以random指令为例来说明边界检查,具体如下所示:

1
2
3
4
5
6
Case RANDOM :
 if (op_sp >= &op_stk_end) {
   AtmfdDbgPrint("windows\\core\\ntgdi\\fondrv\\otfd\\bc\\t1interp.c",
                 6015, "stack overflow - otherRANDOM", "false");
   goto label_error;
 }

然而,在各种受到支持的操作符中,有一些操作符还是可以对栈进行写操作的,但是不会令栈指针增加,因为它们还会从中加载更多或等量的数据。对于这类操作符来说,它们会忽略检查边界,因为这是一种有效的优化措施:既然每次op_sp增加都进行了彻底地“消毒”,那么对于那些不会令op_sp增长的指令来说,就可以安全地假定,任何时刻该指针都是合法有效的。 有意思的是,正是由于缺乏了这张安全网,才使得这里的漏洞得以有效利用。

在考察了所有已经实现的指令之后,我们发现以下涉及栈的写操作的指令仍然可用来实现op_sp的越界:

NOT(逐位取反)

NEG(取反)

ABS(绝对值)

SQRT(平方根)

INDEX(从栈中按索引取值)

EXCH(交换栈最上面的值)

DIV(除法)

ADD(加法)

SUB(减法)

MUL(乘法)

GET(获取瞬态数组值)

令人遗憾的是,这些指令当中,没有一个可以直接对栈指针控制范围内的数据进行写操作。此外,虽然算术和逻辑运算指令多少还涉及一点栈指针控制之下的数据,但是我们明显无法控制那些需要覆盖的内存(而这些内存才是我们的关键所在)。 同样的,我们也无法利用index指令,因为它会用栈顶部下面第x个元素取代栈顶元素,同样,这里的x我们照样无法控制。

这样的话,所剩下的指令就只有get了,它用瞬态数组中16位的值替换op_sp对应的16位的索引。 由于索引值的位宽是有限制的,所以,我们最初的想法是,(通过/lenBuildCharArray 字段)构造一个由65535个元素组成的瞬态数组,并将所有的元素设为我们想要的数值,这样,无论原来的值(被解释为索引)是什么,该指令总能把我们想要的数值写到栈中。虽然这个办法行得通,却存在明显的缺点,即开销太大了,每插入一个值都要向文件中存入65000条指令;同时,索引号被解释为带符号整数,而get的实现会自动拒绝负数(当然,这个问题可以通过abs指令来解决)。

这里的主要问题在于,对于栈中将要被覆盖的初始值,我们一无所知。不过,如果我们再仔细看看上面所允许的操作符的话,就会发现有个操作符每次运行都能有效减少最上面的值,这个指令就是sqrt。 该指令将32位的操作数视为16.16的固定值,并用其平方根的近似值来替换它。这里的重点在于,我们可以通过get指令使用的16比特的索引来覆盖这个值的16比特的整数部分。 由于上述原因,如果我们连续执行5次sqrt指令,那么我们就可以肯定,op_sp对应的16比特数值必定为下面两者之一:

0:如果这个值的初始值为0的话。

1,在其他情况下。

因此,我们只需要执行5个指令,就能把get的参数的可能的取值从65536个减小到2个。总的说来,我们是能够在这个线程的栈的任意位置插入任意值的:首先,利用put指令将其放入下标为0和1的瞬态数组元素中,然后,利用有漏洞的blend操作符让op_sp指向预定位置,并在get后面执行5次sqrt指令。

下面的动画生动演示了将数字31337写到栈中op_stk数组的前400字节中的完整过程:

http://p1.qhimg.com/t0178c91aa173675a9e.gif

虽然向栈内写入数据是一种重要能力,但是从中读出数据也是同样重要的能力。这主要是为了绕过ASLR,它要求我们能够暴露模块在虚拟地址空间的基址,并用之计算出ROP组件的位置(以便绕过DEP)。 类似的,我们也可以通过put指令(与get指令相当),把数据从栈加载到瞬态数组,也可以达到上述目的。 如果我们将put操作放到执行5次sqrt之前完成,那么栈顶的第二个值就会被插入到下标为0或1的瞬态数组单元中。 为了能够有把握地读取到这个值并进行相应的处理,必须首先用0来提前初始化这两个单元,并在处理完成后将这些值相加以得到最终结果。

对栈的某处执行完读写操作之后,最好复位op_sp,让它重新指向op_stk[0],以便处理最新获得的数据,或者为写入其他代码段设置运行上下文,这样无需担心遇到非法指令或者破坏精心构造好的ROP链了。为此,我们可以借助setcurrentpoint指令来避免所有这类副作用,因为它会令操作数栈指针重新指向局部堆栈数组的起始位置。

下面的动画,生动展示了利用上面介绍的技术从栈中读取函数指针,并用它计算可执行映像的基址的过程:

http://p0.qhimg.com/t01392befd77dfcc265.gif

现在,我们已经能够利用这种字符串式的程序代码向栈中的任意地点肆意进行读写操作了,也就是说,我们已经为可靠地创建ROP链并在沙箱进程执行任意代码打好了坚实的基础。根据KISS原则,最简单和最优雅的方法就是调用LoadLibrary函数,只要在参数中提供精心构造的PDF文件的路径即可。 理论上来说,利用混合了PE和PDF的文件是可以达到我们的目的的,不过,由于“%PDF”这个魔幻字节未必出现在文件的起始位置,所以我们需要在同一个文件中同时提供Adobe Reader漏洞利用和用C/C+编写的第二阶段DLL(second-stage DLL )。对于这种混合使用PE/PDF的做法,Ange Albertini已于2012年在他的概念验证代码CorkaMIX中证明是可行的。 令人遗憾的是,这种思路还是存在一些问题:

为了向LoadLibrary传递带有PDF文件路径的字符串,要求被利用的线程的栈中的某个地方必须存在字符串或者指向这个字符串的指针,但是根据我们的经验,事情并不是这样的。

即使不存在上面所说的问题,但是,现在Adobe Reader已经开始拒绝以可执行标志MZ开头的PDF文件了。这方面的点变化具体我们还不太清楚,但是在防止将我们的混合文件作为文档来打开方面却是毋庸置疑的,这就将我们的攻击完全阻止了。

因此,我们必须选用一种更加优雅(却同样可靠)的解决方案,好在借助标准的ROP链,我们可以利用VirtualProtect调用将攻击者控制之下的内存空间标记为可执行的,然后将控制权传递给这些代码从实现执行任意代码的目的。对于Adobe Reader来说,这需要我们首先使用CoolType内部实现的GetProcAddress例程(第一个小工具)来解析VirtualProtectEx函数的地址,然后以PAGE_EXECUTE_READWRITE和一个指向(用第二个小工具)建立的第1阶段有效载荷所在栈的内存地址的指针作为参数调用VirtualProtectEx函数,从而返回可执行的shellcode。 对于这个漏洞利用的概念验证来说,所用的ROP链的最终结构如图1所示。

http://p6.qhimg.com/t01221e89ee785cab08.png

图1 在Adobe Reader的渲染进程中执行任意代码的ROP链带有由单个int3指令组成的有效载荷打造的ROP链的使用效果如图2所示,我们已经可靠地在Adobe Reader中实现远程代码执行了!

http://p1.qhimg.com/t0120607c8e15c1eef5.png

图2 在Adobe Reader稳定可靠地运行第1阶段有效载荷

尽管我们前面使用的是汇编级别的代码,但是为简便起见,后面我们会使用C/C++代码来发动攻击——当然,利用汇编语言来开发涉及win32k.sys漏洞的第2阶段字体漏洞利用也是完全有可能的,但是这样做一点都不好玩。如果我们可以通过LoadLibrary调用加载一个可控的DLL的话,那就再好不过了,尽管这是由第1阶段的有效载荷“迂回”调用的,而非由ROP链直接调用的。    

 这样做有两个好处:首先,Renderer进程可以在利用本漏洞的同时利用有效的HANDLE(具有读权限)来利用PDF文件。其次,虽然文件系统的访问具有大量的限制,尤其是在写入操作方面,但是这个渲染进程仍然对%APPDATA%\Adobe\Acrobat\11.0这个临时目录具有写权限。

为了充分利用这些有利条件,我们可以把第2阶段的DLL放入上面提到过的PE/PDF混合文件中,为此,使用Visual Studio编译这个动态连接库时,需要令链接器选项/STUB指向的PDF文件带有一个前置的有效DOS头部。这样的话,就会使该PDF文件被嵌入到一个PE文件中,更利于Adobe Reader正确打开这个文件。 考虑到这个程序的行为特点,我们必须使用其他的字节诸如“mz”来替换“MZ”标志。 准备好上述文件之后,我们就可以通过汇编语言有效载荷来完成下列活动,从而调用这个DLL模块的DllMain函数:

遍历合理范围内的所有可能的HANDLE值,例如范围(0,0x1000,4)。

通过上面遍历的每一个值来调用GetFinalPathNameByHandle API,以取得相应的文件路径。

如果碰到以.pdf结束的路径,说明我们已经找到了这个漏洞利用文件,然后把这个文件复制到临时目录%APPDATA%\Adobe\Acrobat\11.0下面。

恢复原来的“MZ”标志,让这个文件再次成为一个有效的PE文件。

通过这个新文件引用LoadLibrary,从而让我们的C++函数DllMain被调用。

这样一来,我们就能够使用高级程序设计语言来完成剩下的攻击活动。图3为我们展示的是DllMain函数中的一个消息对话框。

http://p3.qhimg.com/t01984a8d3630f4eb51.png

图3 在第2阶段DLL的DllMain函数中使用任意C++程序代码

至此,我们建立的PDF文件已经能够在Adobe Reader 11.0.10的上下文中可靠地执行任意C++程序代码了。接下来,我们开始着手开发实现沙箱逃逸的内核攻击代码,然而就本例而言,我们必须区分x86和x64平台,因为不同的平台必须使用不同的漏洞利用代码。 通过调用IsWow64Process API,我们可以获得系统位数方面的必要信息。

在下一篇文章中,我们会讨论如何从受限的Adobe Reader 进程上下文中进入ATMFD.DLL有漏洞的代码,通过利用BLEND漏洞将我们的权限提高到系统级别。

评论

暂无
发表评论
 返回顶部 
热度(236)
 关注微信