强网杯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> |