CVE-2023-4427复现
后记
文章发布于看雪论坛,被看雪论坛微信公众号转发,但后来因为奇怪的原因被微信官方删除😭
前言
首发于个人博客,感谢网络上分析过这个问题的师傅@Tokameine@XiaozaYa(排名不分先后)
v8学了一段时间,查阅了很多资料,同时收获很多,因此记录一下。
以下内容可能会存在一些错误,如果有问题,恳请各位大佬指正
环境搭建
编译debug版本,is_debug=true
1 | |
编译release版本,is_debug=false
1 | |
diff.patch的内容
1 | |
漏洞分析
推荐去看issue页面的description.pdf,讲的很清晰。下面的内容也是由这个pdf展开
poc验证
在release版本下验证,debug版本有检测,会导致直接carsh
精简了一下poc
1 | |
看到这样的输出,就说明环境是没有问题的

poc分析
首先可以看这样一段代码
1 | |
不难从下图看出,enum cache所在的位置,object -> map -> DescriptorArray -> enum_cache

这张图更形象

接着看这一段代码,介绍一下transition chain
1 | |
如下输出
1 | |
其中这里描述了obj1和obj2的transitions的相关信息
1 | |
第一个说明当前map添加了一个b属性,然后向下转化为新的map 0x235e000d9cdd,也就是obj2的map
第二个说明当前map添加了一个c属性,然后向下转化为新的map 0x235e000d9d05,也就是obj3的map
下面是更为详细的图

三个对象共享一个DescriptorArray,然后其中的enum cacahe也是一样的
下面初始化一下enum cache
1 | |
输出
1 | |
V8 将 for…in 循环转换为常规 for 循环,并使用三个关键操作来执行:ForInEnumerate、ForInPrepare 和 ForInNext。ForInEnumerate 和 ForInPrepare 共同收集目标对象的所有可枚举属性名,并将它们存储到一个固定数组(fixed array)中,同时设置适当的上限(即属性的数量),作为隐式循环变量的上界。这个隐式变量还充当该固定数组的索引,所以在每次迭代时,ForInNext 都会从当前索引处加载键(key),然后将其赋值给用户可见的变量。

poc的触发函数
1 | |
接着去vscode全局搜索Reduction JSNativeContextSpecialization::ReduceJSLoadPropertyWithEnumeratedKey,查看问题代码
1 | |
注释中将优化的过程讲的很清楚,首先receiver会被JSToObject转化为对象,然后调用ForInNext加载key,接着通过JSLoadProperty去加载value
优化完毕之后,就会走第二条路径,从receiver到JSLoadProperty,但此时的JSLoadProperty会变成map check,也就是说如果map没发生变化,那么就会继续执行后面的流程,也就是从enum cache中调用,但是如果map发生了变化,那么就会重新进行优化。
接着是trigger的调用
1 | |
这里先通过obj3的赋值,导致了enum cache的消失,此时的obj1和obj2的enum cache就会变成invaild,但是还是存在于内存里。然后obj3会创建新的map,此时的三个obj共享一个新的descriptor array

1 | |
然后去初始化obj1的enum cache,但此时的obj2和obj3的enum cache都为invalid,这里之后会进入trigger函数的函数体,执行的是遍历obj2,此时会去检查obj2的map,发现其实没有变化,然后会载入enum cache的长度,这个长度是根据map来确定的,因此length本应该是1,但是这里载入了原本map上的enum cache的length,这样就造成了溢出
下图就是攻击的原理图

调试相关
release版本没有job的显示,只有debug版本有,所以只能release和debug对着调
调试一下上面涉及到的原理
调试的poc
1 | |
在执行输出trigger时,下断点b Builtins_ForInEnumerate,然后连续3次c,接着finish执行完Builtins_ForInEnumerate,然后会发现返回值会将obj2的map赋值给rcx

接着就是对于下面指令的解释
1 | |

先取描述符数组,接着取enum cache,然后取enum_cache.key,最后然后取对应的enum_cache的length(这个length根据map来确定,因此造成了oob

map、enum_cache.key、length放到栈上
接着map的检查,然后取key[0]

接着取存在栈上的map,然后检查map是否发生变化
依次分别取出描述符数组、enum cache、enum_cache.indices

取出了enum_cache.indices[0],也就是对应的key,接着通过[r8 + r11*2 + 0xb]取到value

这里还是check map,但是这里变成了-0x38,原因是因为前面push了两个值

取key[1]
下面的流程其实就已经开始重复了,因为这是一个循环

这里是一个越界,因为原本的obj1对应的enum cache的size就是1,所以这里二就已经是越界了,取出了一个0x6a5的值,这就对应着新的idx的值,然后越界出了0x6a5,因为是smi,所以会右移1位,也就是/2

这里在取值,也就是说,通过这个map去向后索引这么多[r8 + r12*2 + 0xb],结合前面的/2,其实也就是obj2的map+0x6a4+0x8,这个的结果是让这个地址成为一个对象。
所以利用思路也就出来了,这里设obj2的地址为A,这里使得A+0x6a5+0x8的值落在一个可控的范围内,其实不难想到伪造对象,因为他是解引,所以这里需要伪造一个被指向的地址的区域是一个obj,稳定可控的话,可以想到适用通用对象的堆喷
这里的思路借鉴了@XiaozaYa师傅,非常巧妙,且成功率高
调试的时候遇到一些问题
- debug和release版本的堆布局不一样,而且差距会很大
- TurboFan优化之后,堆布局会发生变化
需要通过调整对象的分配大小,最后才能成功触发poc,并得到fakeobj
exp
1 | |

参考文章
https://bugs.chromium.org/p/chromium/issues/detail?id=1470668
https://blog.csdn.net/qq_61670993/article/details/137133853
https://bbs.kanxue.com/thread-280786.htm
https://paper.seebug.org/3081/
https://cwresearchlab.co.kr/entry/CVE-2023-4427-PoC-Out-of-bounds-memory-access-in-V8