2018 Google CTF Just in Time
折磨了一周了, 终于搞出来了. 貌似还是版本有点问题, 官方的和网上找的 exp 都打不通. 不管了, 本地通了就是通了.
环境搭建
原题 build 的是 Chrome, 版本号为 70.0.3538.9. 在 OmahaProxy 中可以查询 Chrome 对应的 v8 版本. 这里就直接 build v8 来做题了. 找到 v8 的 commit 是 e0a58f83255d1dae907e2ba4564ad8928a7dedf4.
附件有两个 patch, nosandbox
是 chrome 的, 不用管它 把 addition-reducer.patch
打到 v8 上. 之后编译需要加上 v8_untrusted_code_mitigations = false
选项.
题目分析
添加了两个文件 src/compiler/duplicate-addition-reducer.cc
和 src/compiler/duplicate-addition-reducer.h
. patch 最后可以看到, 在 typed lowering 阶段添加了 duplicate addition reduce 优化过程.
|
|
duplicate-addition-reducer.cc
如下:
|
|
typed lowings 阶段遍历到每一个节点时都会尝试这些 reducer, 对于新添加的这个 DuplicateAdditionReducer::Reduce()
, 可以看到如果节点是 IrOpcode::kNumberAdd
(NumberAdd), 则进入 DuplicateAdditionReducer::ReduceAddition()
进行处理, 否则什么也不做. DuplicateAdditionReducer::ReduceAddition()
中有如下检查:
|
|
第一个 if 是判断该节点的第一个输入节点的类型要和当前节点一致, 也就是 IrOpcode::kNumberAdd
, 第二个 if 判断该节点的第二个输入是 IrOpcode::kNumberConstant
, 也就是 Number 常数. 第三个 if 判断第一个输入节点的第二个输入节点是 IrOpcode::kNumberConstant
. 画图如下:
graph TD; parent_left\nNoMatterWhat-->left\nNumberAdd; parent_right\nNumberConstant-->left\nNumberAdd; left\nNumberAdd-->node\nNumberAdd; right\nNumberConstant-->node\nNumberAdd;
之后新建一个节点, 值为两个常数之和, 并且把当前节点的输入改成了 parent_left
和新节点:
|
|
即从上图变为了下图:
graph TD; parent_left\nNoMatterWhat-->node\nNumberAdd; new_const\nNumberConstant\n-->node\nNumberAdd;
乍一看没什么不对的地方, 只不过是一个结合律罢了. 不过计算机不是数学, 浮点数运算就不满足结合律. 浮点数能够具体表示的有理数是有限的, 当数字较大时, 整数是不连续的. 比如 9007199254740992 和 9007199254740994 能够被精确表示, 但是 9007199254740993 不能. v8 的 Number 类型包含了浮点数, 在代码里也能够看到, 就是取的浮点数进行运算. 漏洞出现在这里.
Number.MAX_SAFE_INTEGER
常量, 其值为 $2^{53} - 1$.PoC
起个 python 验证一下浮点数:
>>> 9007199254740992.0
9007199254740992.0
>>> 9007199254740993.0
9007199254740992.0
>>> 9007199254740994.0
9007199254740994.0
>>> 9007199254740992.0 + 1.0 + 1.0
9007199254740992.0
>>> 9007199254740992.0 + (1.0 + 1.0)
9007199254740994.0
加入的 DuplicateAdditionReducer 不过只是影响一点点精度问题, 正常使用倒也没有影响, 那这里为什么会是一个可以利用的漏洞呢?
原因在于 DuplicateAdditionReducer 在 typed lowing 阶段, 之前的 typer 阶段确定了节点的输出值的类型. 如果写一个这样的代码, 那么在 typer 阶段, 节点的类型应该是 Range(9007199254740992, 9007199254740992). 而 Duplicate Addition Reducer 在替换完节点以后没有修改类型, typed lowing 阶段实际上的值变成了 9007199254740994, 超出了 Range 的范围. 之后的 check bounds 是根据节点的 Range 来判断的, 后续 simplified lowering 会消除 check bounds, 所以这里可以造成越界.
将如下的代码用 TurboFan 优化, 并在 Turbolizer 中查看:
|
|
typer 阶段和 typed lowering 阶段的图如下:
可以看到, 在 typed lowering 阶段已经把 + 1 + 1
优化成了 + 2
. 同时还可以发现, 节点的 Range 依旧是 Range(9007199254740991, 9007199254740992).
x
从 9007199254740989 和 9007199254740992 中选一个, 这样在 sea of nodes 中会变成一个 phi 节点. 如果直接用常数的话, 会被 typed lowering 阶段的 ConstantFoldingReducer 中和两个 + 1
化简成一个常数.接下来验证一下是否能够把 check bounds 优化掉. 代码如下:
|
|
观察 escape analysis 和 simplified lowering 两个阶段, 可以看到, simplified lowering 已经没有 check bounds 了.
之后执行一个 poc(1)
, 成功越界读.
let
修饰. 只有局部变量才会被 LoadEliminationPhase 优化 LoadField 节点.内存布局
上面的 PoC 已经能够越界一位读写了, 现在来分析一下布局内存, 看能够搞乱什么东西 利用乘法, 可以控制下标越界更多, 于是想能不能访问到其他变量, 比如 object 数组, 这样就可以很方便 addrof 和 fakeobj. 试一下:
|
|
输出如下:
DebugPrint: 0x30f87d09c7b1: [JSArray]
- map: 0x016bb9302931 <Map(PACKED_DOUBLE_ELEMENTS)> [FastProperties]
- prototype: 0x2e86add86919 <JSArray[0]>
- elements: 0x30f87d09c789 <FixedDoubleArray[3]> [PACKED_DOUBLE_ELEMENTS]
- length: 3
- properties: 0x3af438d02d29 <FixedArray[0]> {
#length: 0x1cbc57d1c111 <AccessorInfo> (const accessor descriptor)
}
- elements: 0x30f87d09c789 <FixedDoubleArray[3]> {
0: 0
1: 1.1
2: 2.2
}
0x16bb9302931: [Map]
...
DebugPrint: 0x30f87d09c8a1: [JSArray]
- map: 0x016bb93029d1 <Map(PACKED_ELEMENTS)> [FastProperties]
- prototype: 0x2e86add86919 <JSArray[0]>
- elements: 0x30f87d09c879 <FixedArray[3]> [PACKED_ELEMENTS]
- length: 3
- properties: 0x3af438d02d29 <FixedArray[0]> {
#length: 0x1cbc57d1c111 <AccessorInfo> (const accessor descriptor)
}
- elements: 0x30f87d09c879 <FixedArray[3]> {
0: 0x30f87d09c7d1 <Object map = 0x16bb9302521>
1: 0x30f87d09c809 <Object map = 0x16bb9302521>
2: 0x30f87d09c841 <Object map = 0x16bb9302521>
}
0x16bb93029d1: [Map]
...
可以发现, double_arr
是可以越界访问到 obj_arr
的 elements 元素的.
%DebugPrint
貌似还会扰乱栈布局, 对后续利用可能有影响. 不用 %DebugPrint
的话, 可以用 %DisassembleFunction
打印编译后的指令地址, 然后下断点传统手艺 gdb 看内存.
这里有 %DebugPrint
的话, elements 在 JSObject 的前面 (elements 地址更低), 没有的话它们貌似不挨着…
不过 double_arr
的 elements 始终在 obj_arr
的 elements 前面.
原语构造
接下来就是调一下下标, addrof 直接读 obj_arr
中的元素, fakeof 写 obj_arr
, 然后返回. 代码如下:
|
|
obj_arr[0]
那么很可能就直接被 TurboFan 优化掉了, 导致压根就不会有这个 JSObject. 经过猜测和调试, 这里让一个参数影响 obj_arr
, 这样就不会被优化掉了.接着还需要泄漏一下 double array 的 map, 以构造任意地址读写的 fake obj.
泄漏不了一点. elements 附近只有其他的 elements. 换个方法.
目标是任意地址读写, 那么在栈上放一个 ArrayBuffer 试试.
|
|
断点下在函数入口, 一路 si 往下, 找到往 double_arr
的 elements 写数据的地方, 就找到了 elements 的地址. 然后 si, 再看 elements 之后的内存:
pwndbg> telescope 0x1e0209b17740
00:0000│ 0x1e0209b17740 —▸ 0x80502c83539 ◂— 0x80502c822
01:0008│ 0x1e0209b17748 ◂— 0x900000000
02:0010│ 0x1e0209b17750 ◂— 0x0
03:0018│ 0x1e0209b17758 ◂— 0x3ff199999999999a
04:0020│ 0x1e0209b17760 ◂— 0x400199999999999a
05:0028│ 0x1e0209b17768 ◂— 0x400a666666666666 ('ffffff\n@')
06:0030│ 0x1e0209b17770 ◂— 0x401199999999999a
07:0038│ 0x1e0209b17778 ◂— 0x4016000000000000
08:0040│ 0x1e0209b17780 ◂— 0x401a666666666666
09:0048│ 0x1e0209b17788 ◂— 0x401ecccccccccccd
0a:0050│ 0x1e0209b17790 ◂— 0x402199999999999a
0b:0058│ 0x1e0209b17798 —▸ 0x3b385fd84c81 ◂— 0x90000080502c822
0c:0060│ 0x1e0209b177a0 —▸ 0x80502c82d29 ◂— 0x80502c828
0d:0068│ 0x1e0209b177a8 —▸ 0x1e0209b177e1 ◂— 0x80502c845
0e:0070│ 0x1e0209b177b0 —▸ 0x114cbd20dca9 ◂— 0x2900003b385fd84a
0f:0078│ 0x1e0209b177b8 ◂— 0x0
10:0080│ 0x1e0209b177c0 ◂— 0x10000000000
11:0088│ 0x1e0209b177c8 ◂— 0x2000000000
12:0090│ 0x1e0209b177d0 ◂— 0x0
13:0098│ 0x1e0209b177d8 ◂— 0x0
14:00a0│ 0x1e0209b177e0 —▸ 0x80502c845c9 ◂— 0x80502c822
15:00a8│ 0x1e0209b177e8 ◂— 0x2000000000
16:00b0│ 0x1e0209b177f0 ◂— 0x0
17:00b8│ 0x1e0209b177f8 —▸ 0x55d2981c82e0 ◂— 0x0
pwndbg> job 0x1e0209b17799
0x1e0209b17799: [JSTypedArray]
- map: 0x3b385fd84c81 <Map(FLOAT64_ELEMENTS)> [FastProperties]
- prototype: 0x21cf25814559 <Object map = 0x3b385fd84cd1>
- elements: 0x1e0209b177e1 <FixedFloat64Array[32]> [FLOAT64_ELEMENTS]
- embedder fields: 2
- buffer: 0x114cbd20dca9 <ArrayBuffer map = 0x3b385fd84aa1>
- byte_offset: 0
- byte_length: 256
- length: 32
- properties: 0x080502c82d29 <FixedArray[0]> {}
- elements: 0x1e0209b177e1 <FixedFloat64Array[32]> {
0-31: 0
}
- embedder fields = {
(nil)
(nil)
}
可以看到在 double_arr
elements 之后便是 Float64Array
的 JSTypedArray, 这个 JSObject 的 elements 在内存中和数组的类似, 如下:
14:00a0│ 0x1e0209b177e0 —▸ 0x80502c845c9 ◂— 0x80502c822
15:00a8│ 0x1e0209b177e8 ◂— 0x2000000000
16:00b0│ 0x1e0209b177f0 ◂— 0x0
17:00b8│ 0x1e0209b177f8 —▸ 0x55d2981c82e0 ◂— 0x0
不过这里并不是存储真实元素的地方, 而是最后的这个 0x55d2981c82e0 地址. 所以利用溢出修改这个指针, 就能够获得任意位置读写的功能了.
利用
依旧利用 WASM 所用的 rwx 内存. 我编译出来的这个版本下地址相对 WasmInstanceObject 的偏移是 0xe8. 上一题已经说过了, 不再赘述.
完整 exp:
|
|