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