强网杯2020决赛-GooExec题解
前言
最近在学习v8相关的Issue和漏洞利用的知识,想起来去年决赛附件都没打开的GooExec,今年尝试跟着网上其他师傅的一些分析解决这道题目。
环境搭建
题目给了编译好的chrome和v8,包含debug版本和release版本,但是为了分析漏洞,我还是决定自己编译一下对应版本的d8方便看源码调试。
1 | git checkout 8.7.74 |
其中为了方便release里也可以带符号调试,配置文件args.gn修改如下。
1 | is_debug = false |
漏洞分析
这是一个比较老的issue,通过搜索diff中的关键patch可以搜索到issue-799263,看下git log发现我们手里的v8是2020年的版本,应该是当时最新的代码加了patch来的,所以直接拿issue里的poc测会有一些问题,不过漏洞原理是相同的,所以我们的分析对应回18年的那个版本也是没问题的。这里比较好的一种方式是编译两版d8,在18年那版测PoC调试看日志把漏洞原理搞清楚,再回到题目的环境尝试触发漏洞,这里我比较懒就只用题目这个环境了。因为PoC有点问题,我们直接来看源码。
之前分析CVE-2020-16009
时我们介绍了map transition
的概念,大概就是说有了新的属性根据Hidden class
的思想生成新的map
,这里的ElementsTransition
有两种模式,fast mode
对应in-place
的更新,slow mode
不能通过仅更新map的representation来更新成新的map,而是需要分配一个新的map instance,再将旧的map的不变的属性拷贝过去,再更新某些field。
source_map
和target_map
顾名思义为transition
的源map和目标map,AbstractState
可以抽象地认为是对象的状态,而maps
就是用来描述状态的一个属性,effect
会对节点的状态造成影响,因此这里判断了effect
的状态,如果为null
则无须改变节点。
AliasStateInfo
我们看下其定义,结合注释可以得知它是用于保存object aliasing
的信息的,也就是说同一个object可能在不同的操作中生成不同节点,但是它们可能需要保存同一个map
副本,所谓的AliasStateInfo
就是保存了同一对象的不同状态信息。
ZoneHandleSet
是保存对象的map
的一个集合,集合中包含有该对象可能存在的map属性,在处理TransitionElementsKind
节点时首先查询target_map
是否包含有当前处理的object
的object_map
,如果有的话则说明这个节点是一个冗余节点,调用Replace(effect)
消除这个节点。
如果object_maps
包含source_map
则将source_map
从object_maps
中移除,将target_maps
插入其中,在节点的state
中保存这个对象和object_maps
的信息。这里注释掉的部分是删除掉alias object
保存的source_map
的信息。
1 | // A descriptor for elements kind transitions. |
我们具体来看下LoadElimination::AbstractState::KillMaps
函数,调用了LoadElimination::AbstractMaps::Kill
函数,MayAlias
用来判断两个Node
是否可能指向同一个object
,如果一定不
指向同一个object
则返回false
。AbstractMaps
表示在某个effect
状态时所有Node
所有可能的maps
,ZoneVector<AbstractState const*> info_for_node_
变量用来存储相关的信息,也就是说Kill
函数主要是将不会alias
的node->maps
映射存储在集合中,假如node某个可能的maps没有放进去则可能存在类型混淆
。
1 | //src/compiler/load-elimination.cc:492 |
PoC
我们手里有issue
的PoC,结合源码分析一下PoC
构造出的效果,先调用两次opt
函数让其收集信息,调用优化后的函数opt
,arr2[0]=0
触发map transition
生成TransitionElementsKind
节点。后面发现这个PoC并不能触发到LoadElimination::ReduceTransitionElementsKind
,再对比一下TurboFan
的IR
图发现并没有生成这个节点,我们调整测试这份PoC,最终得到下面的PoC
1 | function opt(a, b) { |
最终在调用opt
前输出arr2[0]
的值为1.1而调用后输出object Object
,成功构造出类型混淆,这里我们传入的a、b
指向同一个对象但是在TurboFan
中开始无法识别出来的时候会将其放在两个Parameter
节点中,当我们进行ReduceTransitionElementsKind
时会对a
对象做transition
,将其field representation
向更为通用的方向进行转换,最开始作为浮点数数组,其map类型为Map(PACKED_DOUBLE_ELEMENTS)
,在类型转换之后参数2
节点可能的map类型中移除了Map(PACKED_DOUBLE_ELEMENTS)
,而参数3
节点没有调用KillMap
把这个信息也删除掉,造成保存了错误信息,进而产生类型混淆。
具体地,在处理CheckMaps
节点时,因为object_maps
中保存了错误的关于对象的maps的信息,这里的maps.contains(object_maps)
返回真,最终调用了Replace(effect)
将CheckMaps
节点消除掉,以浮点数数组的类型执行b[0] = 9.431092e-317
,然而在之前的a[0] = temp
中数组的类型已经由浮点数数组转换成了对象数组,产生了类型混淆。
1 | //src/compiler/load-elimination.cc:752 |
1 | //before opt |
1 | function opt(a, b) { |
在此之后有两种利用的思路,我们来看比较好理解和构造的一种方式,即构造出OOB
数组。这里需要用到压缩指针的知识,我们在之前的分析中提到压缩指针开启的情况下使用32
位保存指针,64
位保存double
数据。我们在将对象数组类型混淆为浮点数数组后实际可以操作的内存区域扩大了一倍,因此对属性赋值可以达到越界写的效果,我们在目标数组后布置oob数组,修改其length处的值即可得到OOB
数组。这里有个坑是我拿全浮点数的数组搞后面无法越界,后面都是HeapNumber
,混淆后的空间并没有被回收,我们又无法在opt
函数中调用gc,最后把数组元素换成Smi
即可。
再往后的利用思路主要参考raycp
师傅的方法,我们在oob
数组后布置字典对象,因为现在有越界读写,可以根据字典对象的属性标识找到obj
属性的地址,得到访问该obj
所需的索引。同理在oob
数组后布置BigUint64Array
对象,根据数组的元素找到该对象的base_pointer
和external_pointer
以及array_len_idx
。这里base_pointer
保存了一个相对地址,external_pointer
保存了一个同heap_base
相关的地址,根据它可以泄露出压缩指针机制下的heap基址。劫持这两个指针就可以实现任意地址读写。另外为了得到这俩指针我的exp里加了padding以使得oob_arr的elements和这俩地址是8字节对齐的,否则比较麻烦。通过覆写字典对象的obj
可以实现addressOf
原语,因为wasm
被禁了,我们先找到这个flag
,将其从0改为1再走wasm那一套流程。
这里chrome
里是通过libv8.so
来调用v8
的,我们在gdb调试中如果直接搜这个FLAG_expose_wasm
字符串,找到的并不是这个flag,因为它的值为0/1
,一种比较好的方式是我们自己编译带符号的chromium
,p & v8::internal::FLAG_expose_wasm
打印其地址,寻找相对引用,在libv8.so
找到可以定位该flag的函数,再到我们手里的版本里去查找,这里其位于chrome的地址为0x0EED418
。
另外介绍一下chrome调试的方法(感谢P4nda
和Keenan
手把手教学),首先打开chrome,每一个标签对应一个进程,我们可以通过shift+esc
来查看当前的chrome进程号及信息,首先拿./chrome --js-flags="--noexpose_wasm --allow-natives-syntax" --no-sandbox
启动,这样可以让我们在console输出debug信息,在我们希望调试的地方加个alert
断下来,比如alert前放DebugPrint,在终端里看到其地址之后再gdb attach到该进程中调试,因为没有debug信息我们只能按照d8调试中看到的布局对应过去。
expose.html
对象布局还是不太稳定,这里基本上试个十次能成功一次
1 | <script> |
exp.html
1 | <script> |