CVE-2021-38001 :V8 IC 中的类型混淆漏洞
信息收集
该 CVE 就是 TFC 上昆仑打的,issue 页面暂未公开,patch 如下:

可以看出来漏洞应该和 CVE-2021-30517 十分类似,为 receiver 和 holder 的类型混淆,更确切来说是把 holder 所对应的 handle 给了 receiver 而导致的类型混淆。
在 [github](https://github.com/vngkv123/articles/blob/main/CVE-2021-38001.md) 上可以找到验证的 poc ,分为两个文件,首先是 1.mjs :
1 | export let x = {}; |
随后是 2.mjs :
1 | import * as module from "1.mjs"; |
当运行 2.mjs 的时候会发生以下错误:
1 | # |
可以从错误信息中得知本来是需要一个 module 对象的,结果却得到了一个 Smi 的值。
调用栈如下:
1 | #3 0x00007f707760a1bb in v8::internal::CheckObjectType (raw_value=0x30a242424242, raw_type=0xda, raw_location=0x30a20800bded) at ../../src/objects/object-type.cc:45 |
漏洞分析
该漏洞与 CVE-2021-30517 十分类似,故不重复分析之前分析过的内容。我们直接从 LdaNamedPropertyFromSuper 开始看:
1 | // LdaNamedPropertyFromSuper <receiver> <name_index> <slot> |
其调用了 kLoadSuperIC
1 | void AccessorAssembler::LoadSuperIC(const LoadICParameters* p) { |
第一次会没有 feedback ,随后走到 LoadSuperIC_NoFeedback :
1 | void AccessorAssembler::i(const LoadICParameters* p) { |
随后到 miss 并走到 kLoadWithReceiverNoFeedbackIC_Miss :
1 | RUNTIME_FUNCTION(Runtime_LoadWithReceiverNoFeedbackIC_Miss) { |
然后是 load :
1 | MaybeHandle<Object> LoadIC::Load(Handle<Object> object, Handle<Name> name, |
调试一下可以发现此时的 receiver 和 lookup_start_object (也就是 object )分别为 c 和 zz ,key ( 也就是 name )为 y :
1 | gef➤ jh receiver |
随后会走到 UpdateCaches
1 | void LoadIC::UpdateCaches(LookupIterator* lookup) { |
最关键的来了,它会走到 ComputeHandler :
1 | Handle<Object> LoadIC::ComputeHandler(LookupIterator* lookup) { |
分别查看 receiver,lookup_start_object 和 holder ,可以知道他们分别为 c,zz,module:
1 | gef➤ jh receiver |
最终会用 holder 来生成对应的 handle
当有了该 handle 的时候,接下来 LoadSuperIC 就会走到有 handle 的地方去使用 handle :
1 | BIND(&if_handler); |
接下来依次 HandleLoadICHandlerCase → HandleLoadICSmiHandlerCase → HandleLoadICSmiHandlerLoadNamedCase ,并最终走到这个分支:
1 | BIND(&module_export); |
可以看到 module 是直接使用 p->receiver 来加上 kModuleOffset 进行获取的,而并非 holder ,这样便会造成一个类型混淆的问题
漏洞利用
和 CVE-2021-30517 一样,我们可以得知 value 是通过以下过程获得
1 | // module |
然而由于是 ObjectHashTable , y 的位置也不是固定的,只要我们填满这块区域就能绕过该限制。
但是经过尝试,在不知道地址的情况下很难构造假的对象,其中的一种方法是堆喷出来固定的 map 等值伪造一个对象( Basic maps 都用有静态的低 32 位值,由于指针压缩的原因使得它更不随机了。不过该值因机器、版本等因素而异)。
另外也可以参考 p4nda 师兄在第五届看雪安全开发者峰会的演讲:

使用字符串进行没有泄漏的类型混淆利用,并通过修改 string 的 length 来进行地址泄漏:

首先我们先得到伪造的 double array ,并使其 element 指针指向 oobStr 并修改其长度:
1 | var oobStr = "Ver"; |
随后使用 fakeArr 和 oobStr 构造一系列的原语:
1 | for(let i = 0; i < 0x8000; i++){ |
这里手动用 ArrayBuffer 调了一下堆布局,这里似乎写代码会一定程度影响堆布局,同时用的 1.8457939563e-314 而非 i2f(0xdeadbeef) 是因为如果用后者的话 lastIndexOf 就会找到别的地方去(猜测是因为我们自己也使用了一个相同的 SMI )
最后写 shellcode 并执行:
1 | var wasmCode = new Uint8Array([0,97,115,109,1,0,0,0,1,133,128,128,128,0,1,96,0,1,127,3,130,128,128,128,0,1,0,4,132,128,128,128,0,1,112,0,0,5,131,128,128,128,0,1,0,1,6,129,128,128,128,0,0,7,145,128,128,128,0,2,6,109,101,109,111,114,121,2,0,4,109,97,105,110,0,0,10,138,128,128,128,0,1,132,128,128,128,0,0,65,42,11]); |
如果用 arbWrite 方法一个字节一个字节写会走到 ic 的地方然后 crash 掉,可能和我们前面的操作有关,暂时还没弄清楚具体的原因,不过十分神奇2333
EXP
1 | /* 1.mjs |
Reference
https://github.com/Peterpan0927/TFC-Chrome-v8-bug-CVE-2021-38001-poc
https://github.com/vngkv123/aSiagaming/tree/master/Chrome-v8-1260577
https://github.com/vngkv123/articles/blob/main/CVE-2021-38001.md
