转载

IE漏洞攻防编年简史

本文对历史上的微软IE 浏览器的影响较大0day做了梳理,讨论了IE漏洞在攻击防御技术上的进化,以及记录了此类漏洞前人们在历史上遗留下来的对抗经验和足迹。

在IE浏览器攻防已经白热化的进入到第三个阶段的时候笔者才进入到IE浏览器攻防方面的研究学习。此时代号’Project Spartan’的微软的Edge浏览器从IE11手中接过windows默认浏览器的重担,使得服役将近20年的IE浏览器定格11这个历史的大版本号上面,自觉对IE浏览器漏洞的历史研究应有一篇简记,可供后来的初入行的安全研究者有所参考,故成此文,疏漏之处再所难免,敬请指正。

0×01 浏览器漏洞研究的前置背景

最近几年,网络安全研究的部分重心开始有由PC端向移动端倾斜的趋势。但是在PC端的安全研究依然以浏览器IE/Chrome/Firefox/Spartan,Adobe的Reader/Acrobat/Flash系列作为技术研究深度的展现。当然还有部分用于特定地区定向攻击的文件格式漏洞也属于一些黑客着重关注的领域,比如日本比较流行的字处理软件ichiaro和在韩国地区比较流行的Hancom Office等字处理软件系统以及WPS等字处理软件方面的文件格式漏洞。

浏览器从诞生之初主要提供简单的文档阅读功能.很少构成网络安全威胁,但随着互联网的高速发展,越来越多的功能集被加入到浏览器中。浏览器不仅需要像操作系统那样,为阅读文档、观看电影、欣赏音乐等传统计算机应用提供基础,也需要为社交网络、网络购物等新兴互联网应用提供支持。浏览器在增加功能集的同时,也就带来了更多的安全问题。而集成捆绑于系统中的IE浏览器可以占据市场较大份额,自然也成了众矢之的。针对IE浏览器的攻防军备竞赛也就是在这种情况下拉开了帷幕。

0×02 IE浏览器漏洞攻防的几个时代

1.1 缓冲区溢出和ActiveX控件时代(03年-08年)

03年-08年这是一个阶段,这个阶段时候的IE漏洞基本是以ActiveX控件造成的漏洞居多以及栈溢出漏洞还有一些简单的堆溢出漏洞。比如IFRAME标签的单个超长SRC属性导致的缓冲区溢出以及类似的栈溢出漏洞。

早一些的阶段,常规的Fuzz 方法,无论是基于变形的还是基于生成的,比较适合应用于二进制格式的流数据,特别是那些包含大量C 语言结构类型的文件或网络协议格式。由于格式解析代码经常不加检查的使用数据流数据作为内存操作的参数,单点的畸形往往就足以触发解析代码中可能存在的处理漏洞:超长数据导致的缓冲区溢出、畸形数值导致的整数上溢和下溢、畸形索引值导致数组的越界访问、畸形记数导致过量的内存读写操作。其中对于超长数据导致的缓冲区溢出,样本构造起来相对简单,传入超长的值。所以这种类型的漏洞由于发掘起来相对简单。

对早期的IE 0day 漏洞的历史简单做一下梳理,只包含了影响比较大的例子(图1.1.1,图1.1.2),有些漏洞可能并不是IE 本身的问题,但是以IE 为最主要的利用渠道。

IE漏洞攻防编年简史

图1.1.1

IE漏洞攻防编年简史

图1.1.2

1.2 时代关键字

防护方: DEP /ASLR / Stack Cookie

攻击方: 栈溢出/简单堆溢出/ROP/HeapSpray

A 缓冲区溢出

关于缓冲区溢出也简单给出一个例子方便理解:

IE漏洞攻防编年简史

图1.2.1

我们在VC6.0的编译器编译上述程序,执行后结果如下:

IE漏洞攻防编年简史

图1.2.2

可以看到main函数中只调用了foo这个函数。但是实际运行中,bar函数也被调用了。

实际上因为foo函数处理不当,并且外部输入超长,造成了缓冲区溢出后修改了foo()函数的返回地址从而导致程序执行本不应该执行的bar()函数。而如果被覆盖的返回地址是一串经过精心编码具有后门功能的shellcode,此时计算机即可被恶意攻击者控制。

这只是一个简单的C程序的范例,表现在浏览器当中形式上多少有所不同,比方说IE浏览器支持的IFRAME标签,IFRAME标签的单个超长SRC属性构造超长数据可能就会导致的缓冲区溢出从而浏览器崩溃。

DEP和Security Cookie

从上面的缓冲区溢出我们可以看到,在特定的环境下只要控制输入的数据超长然后覆盖掉返回地址,只要此时程序崩溃就很容易感知目标程序存在缓冲区溢出漏洞。然后在修改这串超长数据糅合上恰当的shellcode精确覆盖返回地址就可以执行我们的恶意代码。只是留给攻击者的这样的大好时光极为短暂。微软从Windows XP SP2开始提供DEP的支持。DEP全称是Data Execution Prevention,可以分为硬件的DEP和软件的DEP。但是目的都是一致的。阻止数据页上代码执行。(图1.2.3)

IE漏洞攻防编年简史

图1.2.3

由于数据所在的内存页被标识为不可执行,即使程序溢出成功转入shellcode的执行,这个时候CPU就会抛出异常从而阻止恶意shellcode的执行。

从这里也可以看到这一阶段的攻击者只是去覆盖栈上的返回地址,试图从栈上将恶意shellcode执行起来。

考虑到攻击是因为覆盖返回地址产生的,微软在VS2008和之后的编译器加入了一个编译选项GS。也就是Security Cookie也可以称为Stack Cookie。

IE漏洞攻防编年简史

图1.2.4

可以看到在开始的时候会将一个security_cookie提前写入到栈中。而在函数返回之前会检查这个security_cookie是否被篡改。

IE漏洞攻防编年简史

图1.2.5

一旦被篡改便会跳转到异常执行的流程:

IE漏洞攻防编年简史

(图1.2.6)

当然这里说的是栈中的情况。在堆溢出中也有相似的防护措施如Header Cookie。

盯上SEH

当攻击者发现覆盖4字节返回地址(32位系统)去执行shellcode这种攻击方法的门槛被抬高之后便又想出了新的方案。覆盖SEH的Exception handler

其实在二进制的攻防当中所有的努力都是为了获得哪怕一次控制EIP(RIP)的机会。而SEH恰好符合这个要求可以给攻击者提供这个机会。SEH存放在栈内,故超长的数据就可能覆盖掉SEH  ,其中将异常处理函数的入口地址更改为shellcode的起始地址。由于溢出后产生的错误数据往往会触发异常,而此时shellcode恰好可以得到一次被EIP指向的机会。

只是留给攻击者的好时光依然极为短暂。在VS2003(.net)当中支持了/SafeSEH的选项,用于应对针对S E H的攻击。后来又进一步推出了SEHOP。当然这些措施需要XP SP2操作系统以及更新的操作系统还有DEP的配合。这两种防护措施详细展开又要占很多篇幅。感兴趣的读者可以自行学习。

需要说明的是64位的windows系统SEH已经不是放在栈中了。想要通过栈溢出覆盖异常处理例程来实现漏洞利用已经是不可能的了。

ROP ASLR和HeapSpray技术

前面说到DEP技术即使返回地址被shellcode覆盖,DEP也会去阻止shellcode的执行。但是如果执行的代码是操作系统的库本身提供的函数如直接使用libc库中提供的system()函数来覆盖程序函数调用的返回地址。然后传递重新设定好的参数使其能够按照预期执行。这种绕过DEP的攻击方式称为return to libc。

Return-to-libc 攻击用库函数的地址来覆盖程序函数调用的返回地址,这样在程序返回时就可以调用库函数从而使攻击得以成功实施。但是由于攻击者可用的指令序列只能为应用程序中已存在的函数,所以这种攻击方式的攻击能力有限。并且攻击只能在 x86 的 CPU 平台中实施而对 x86_64 的 CPU 平台中无效。这是因为x86_64CPU 平台中程序执行时参数不是通过栈传递的而是通过寄存器,而 return-into-libc 需要将参数通过栈来传递。

由于这种 return-to-libc 攻击方式的局限性,返回导向编程(Return-Oriented Programming, ROP)被提出,并成为一种有效的 return-to-libc 攻击手段。返回导向编程攻击的方式不再局限于将漏洞程序的控制流跳转到库函数中,而是可以利用程序和库函数中识别并选取的一组指令序列。攻击者将这些指令序列串连起来,形成攻击所需要的 shellcode 来从事后续的攻击行为。因此这种方式仍然不需要注入新的指令到漏洞程序就可以完成任意的操作。同时,它不利用完整的库函数,因此也不依赖于函数调用时通过堆栈传递参数。

一般我们通过immunitydbg配合mona的脚本插件提取rop的指令序列构造rop chain。

Rop chain 展示:

IE漏洞攻防编年简史

图1.2.7

Rop的出现一度使得在XP时代的攻击者占据上风。由于dll加载地址的固定。针对不同的IE版本然后提取rop chain的工作虽然让人感觉无趣但是达成的攻击效果的确不错。但是作为防御一方的微软的脚步并没有停止。Vista系统的臃肿繁杂为人所诟病并且市场份额也并不高。但是从Vista系统引入的由Win7沿袭的ASLR机制却结结实实的又一次提高了攻击的门槛。

在Rop攻击中,攻击者可以事先预知特定的函数如system或者VirtualProtect的入口地址。这是因为在XP以及2000的操作系统上面,由于kernel32.dll这些动态链接库加载地址是固定的,所以导致相关的函数入口地址也是固定的即攻击者可以事先确定这些函数的入口地址。

当然Rop这一技术有一个弊端就是针对不同的操作系统要编写提取不同的rop chain。这使得兼容性并不是很好。

ASLR全称Address space layout randomization,是系统级别的特性,率先在Vista操作系统中得到支持。它的原

理就是在 当一个应 用程序或动态链接库 ,如 kernel32.dll,被加载时,如果其选择了被ASLR保护 ,那么 系统就会将其加载的基址随机 设定 。这样 ,攻击者就无法事先预知动态库,如 kernel32.dll的基址,也就 无法事先确定特定 函数,如 VirtualProtect的入 口地址 了。

如果感兴趣,可以自己写一段简单的C代码打印出VC运行库的加载地址。会发现每次重启之后win7下面VC运行库加载地址是变化的,但是XP系统VC的运行库加载地址就是固定的。

Heap Spray

ASLR在新系统上面的应用又使得相当长的一段时间在缓冲区溢出利用时代攻击方陷入了弱势。但是攻击者发现之前很早就被提出的(2001年左右)heap spray正好可以解决这个问题。基本在2005年之后IE漏洞的利用很多都用到了Heap Spray的技术。

在缓冲区溢出的时候,我们能够劫持覆盖一个地址,从而使得程序崩溃,但是只使得程序崩溃这样是没有价值的接下来如何将程序的执行流程交接到shellcode的手中,这就变成了一个问题。如果覆盖到一个固定的地址比0x0C0C0C0C,0x0A0A0A0A,0x0D0D0D0D而恰好从这个地址开始布满了我们的shellcode。这样触发漏洞的时候就转入了我们的shellcode进行了恶意代码的执行。

实际应用当中shellcode前面都是要加上一些slidecode的(滑板指令)。为什么要加入滑板指令而不是全用shellcode去填充呢。因为如果要想shellcode执行成功,必须要准确命中shellcode的第一条指令,如果整个进程空间都是shellcode,反而精确命中shellcode的概率大大降低了,因为必须要命中第一条指令,加上slidecode之后,现在只要命中slidecode就可以保证shellcode执行成功了,一般shellcode的指令的总长度在50-100个字节左右,而slidecode的长度则大约是100万字节(按每块分配1M计算),那么现在命中的概率就接近99.99%了。因为现在命中的是slidecode,那么执行slidecode的结果也不能影响和干扰shellcode。但是如果单纯使用0×90(NOP)指令进行填充,因为现在使用较多的攻击场景是覆盖虚函数指针(这是一个多级指针),这种情况下如果你使用0×90来做slidecode,而用0x0C0C0C0C去覆盖虚函数指针,那么现在的虚表(假虚表)里面全是0×90909090,程序跑到0×90909090(内核空间)去执行,直接就crash了。根据这个流程,你可以看到,我们的slidecode也选取0x0C0C0C0C就可以了。

IE漏洞攻防编年简史

(图1.2.8)

大概大量分配内存之后分别覆盖到的地址是这样的:

  0x0A0A0A0A(160M),   0x0C0C0C0C(192M),   0x0D0D0D0D(208M)

网马里面进行堆喷时,申请的内存大小一般都是200M的原因,主要是为了保证能覆盖到0x0C0C0C0C地址。(图1.2.9)

IE漏洞攻防编年简史

2.1 UAF时代

由于缓冲区类漏洞由于发掘起来相对简单,在攻防对抗的时间长河中这类漏洞资源很快耗尽。08年之后释放重利用这样漏洞利用方式变成了IE漏洞的主流。逐渐在这几年达到了高峰。对象畸形操作类的漏洞一般来说触发漏洞需要一系列的操作。单个的操作,比方说对象的创建使用删除都是正常的。导致问题的是对于对象操作的畸形的组合。由于没有标准的章法可供参考,基于传统的溢出类漏洞的发掘手段已经不甚适用。

2.2 对象操作类漏洞原理

跟面向过程的编程语言不同,c++支持多态和继承。支持这些机制的核心就是虚表。C++的(虚)函数指针位于一个全局数组中,形成虚表。而指向这个虚表的指针(VSTR)一般位于对象实例在内存中开始的4个字节(32位系统).

之后才是类中声明的数据成员,一般按照声明的先后顺序排列。对于存在多态行为的类,子类的所有实例共享相同的虚表,但区别于父类的虚表。对于某个对象,其调用存在多态行为的某个函数时,会先通过虚表指针得到虚表.再根据函数在虚表中的偏移来得到相应的函数指针,最后才会调用函数。

另外,对象所在的地址一般通过ecx等寄存器传递。因此.C++中调用存在多态行为的函数的反汇编代码类似于如下序列:

IE漏洞攻防编年简史

(图2.2.1)

我们以stackexchange上面Polynomial 给出的示范代码对UAF做一下简介。

如下例的这样的一段C++代码:

IE漏洞攻防编年简史

图2.2.2

可以衍生为:

IE漏洞攻防编年简史

注意,当执行到Account_GetBalance的时候,由于Account_Destroy 函数的执行myAccount指针指向的内存已经是一个不确定的状态。如果此时能够可靠的触发Account_Destroy函数。并且填充一块精心构造的内容到myAccount指针指向的内存,时机在Account_Destroy执行后Account_GetBalance执行之前。很多情况下这是可能实现的。

Account_Create函数执行之后分配了8个字节的内存。Balance和transactionCount分别占据4个字节,并且返回一个指向他们的指针。这个指针储存在myAccount变量当中。Account_Destroy释放了这块内存,但是myAccount变量依然指向那个8字节的内存。我们将39 05 00 00 01 00 00 00 这8字节内容可靠的进行内存分配。由于旧的8字节内存块已经被标记释放,所以内存管理器有很大可能会用新分配的内存去覆盖掉旧的内存块到那8个字节已经被释放的内存。这个时候Account_GetBalance函数被调用了,他会去读取那8个字节的内存块,但是实际上那8字节的内存块已经被我们覆盖成了

Balance    39 05 00 00   (1337)    transactionCount    01 00 00 00 (1)

所以我们已经越权执行到了下一个函数。

当然具体到IE当中,由于对象繁杂,UAF就更为错综复杂。

浏览器中跟对象操作类漏洞相关的对象有DOM ,BOM ,JavaScript对象。我们以DOM对象的分配过程为例。

DOM(文档对象模型)提供了操作HTML/XML文档的接口。IE浏览器中跟DOM实现相关的代码主要在mshtml dll中。mshtml中的CMarkup类负责构造整个htmI树结构,其成员函数CreateElement会调用全局的CreateElement函数束构造不同标签对应的元素。比如<object>标签.会构造对应的CObjectElement元素:<area>标签.会构造对应的CAreaElement元素。这些Element类都是CElement类的子类。接下来的逆向工作主要基于IE8  8.00.7601.17537版本的mshtml dll。

对于每个不同的标签,IE测览器内部有不同的CTagDesc结构。这些CTagDesc结构中的其中一项就是对应元素的CreateElement函数指针。因此,全局的CreateElement函数,会根据不同的标签柬获得对应的CTagDesc结构,然后再从里面取得对应该标签的CreateElement函数指针然后call过去进行调用。具体可参看全局CreateElement函数的反汇编代码,如图2.2.6所示。

IE漏洞攻防编年简史

IE漏洞攻防编年简史

这里有一些小细节,有的时候直接用IDA反汇编如mshtml dll这样的dll文件的时候没有找到对应的符号表,可以先使用Symbol Type Viewer这样的小工具将符号表下载下来放到跟dll同目录然后再使用IDA对相关的dll文件进行反汇编。

IE漏洞攻防编年简史

接下来,以CObjectElement为例,介绍其创建过程,其他Elenlent的创建过程类似。CObjectElement的初始化是在成员函数CrcateElement函数中完成的。创建过程如下:先分配内存,然后调用构造函数,最后将返回的对象指针保存在传入的CElemen**参数中。反汇编代码如图。

IE漏洞攻防编年简史

图2.2.9

HeapAlloc进行堆内存分配,高版本的一些mshtml dll中可能是由ProcessHeapAllocClear这个函数进行内存的分配。传给HeapAlloc的字节数是0E0h可知,当前IE浏览器版本中的CObjectElement大小为E0h。

接下来调用CObjectEtemem的构造函数完成CObjectElement对象的初始化,构造函数会自动调用父类的构造函数。调用完构造函数后.会将新建的CObjcctElemenl对象指针保存在传入的参数CElemen**中。这是通过代码

mov ecx,[ebp+arg_8] mov[ecx],eax

完成的。

IE浏览器采用引用计数束跟踪DOM对象的生命周期。引用计数(Reference Counting)算法对每个对象计算指向它的指针的数量。当有一个指针指向该对象时计数值加1 ,当删除一个指向酸对象的指针时,计数值减l。如果计数值减为0,说明不存在指向该对象的指针.此时就可以安全的销毁泼对象。垃圾回收过程就是回收引用计数为0的对象。引用计数算法的优点是算法实现简单,并且进行垃圾回收时无需挂起应用程序,回收速度快。

缺点是出于每一次对对象的指针操作都要对对象的引用计数进行更新,因此会减缓系统的整体运行速度。另外,使用引用计数算法的每个对象都需要额外的空问存储计数值。除此之外,引用计数算法的最大缺点是无法处理循环引用。循环引用指的是两个对象互相指向对方。此时两个对象的引用计数都依赖于对方.因此始终无法减至0。

IE浏览器实现引用计数的核心就是IUnknown接口。该接口提供了两个非常重要的特性:生存期控制与接口查询。对象内部通过引用计数来实现对象的生存期控制。调用程序不甩在意对象的内部实现细节.通过接口查询即可获得指向对象的指针。IE浏览器中的很多类都继承于IUnknown。IUnknown有三个方法。

IE漏洞攻防编年简史

图2.2.10

以上节介绍的<obiect>标签的内部实现CObjectElement类为例.该类最终继承于CElement。而CElement继承于CBase,CBase则实现了IUnknown接口。用户要查询<object>标签对应的CObjectElement对象,需要调用CObjectElement::QueryInterface函数。而CObjectElement的QueryIntefface函数最终会调用到CElement的QueryInterface接口.CElement的QueryInterface接口最终会调用PrivateQueryInterface 来获得对象指针。

PrivateQueryInterface会先调用CElement::CreateTearOffThunk函数退回对象包装后的指针.然后在接下来调用CCaret::AddRef函数(call eCX)增加引用计数。

而CElement::CreateTearOffThunk函数仅仅是简单的调用全局的CreateTearOflThunk函数。全局的CreateTearOflThunk函数反汇编部分代码如图

IE漏洞攻防编年简史

图 2.2.11

再来看看释放引用时所做的工作。对于CElement,用户不再需要其引用时,调用CElement::Release即可。CElement::Release是对CElement::PrivateRelease的封装,而CElemem::PrivateRelease主要的工作是调用父类CBase的PrivateRelease函数。CBase::PrivateRelease负责减少引用计数。

实际上IE当中这种对对象的创建和销毁的场景比比皆是,这也是在缓冲区漏洞在IE上面几近绝迹后UAF中兴的基础。

2.3 时代关键字

Deferred/Delayed Free Control Flow Guard Isolated Heap

上文已经简单的给出一个例子帮助理解UAF的成因和触发了。但是由于IE中对象众多调用关系复杂,微软作为防守的一方并不能像挖掘缓冲区溢出漏洞一样容易的穷举并修复所有潜在的漏洞。但是微软分别以发布补丁的方式在14年的6月和7月分别引入了隔离堆和延迟释放的漏洞利用缓解措施。并且在Win8.1Update3和Win10中引入了新的机制Control Flow Guard。我们简单记录说明一下这些机制。

UAF的触发和利用依赖于被释放的对象的重用。利用的过程必须依赖非法IE对象被确定的分配和释放。而隔离堆和延迟释放分别在对象的分配和释放的时候加入了保护。

在IE中CVE-2014-0282修补前CTextArea::CreateElement分配内存的时候有这样的代码

IE漏洞攻防编年简史

图2.3.1

漏洞修补之后代码是这样的。

IE漏洞攻防编年简史

图2.3.2

可以比较明显的看到存在UAF隐患的对象的内存分配已经单独使用了隔离堆进行内存分配。

而延迟释放是这样的。正常的对象释放使用HeapFree就立即释放了,而加入延迟释放之后需要被释放的对象会被统一记录然后根据规则再进行延迟释放。

再说一下CFG(Control Flow Guard)这个机制。CFG的机制是基于控制流完整性Control-Flow Integrity的设想。这里通过对二进制可执行文件的改写,对jmp的目的地址前插入一个在改写时约定好的校验ID,在jmp的时候看目的地址前的数据是不是我们约定好的校验ID,如果不是则进入错误处理流程。

IE漏洞攻防编年简史

图2.3.3

在Call的过程中会引入一个CFG的校验函数。CFG需要编译器和操作系统的双重配合。当这个校验函数在不支持的操作系统上运行的时候直接就return了。当在被支持的操作系统(win10和win8.1 update3)的时候就会跳转到一个ntdll里面的一个检测函数。检测的机制我们不在详细展开。

由于在溢出漏洞和UAF的大部分利用当中都依赖于覆盖某个地址然后劫持程序的EIP跳转到我们的恶意代码的地址进行执行。CFG在控制非法地址跳转方面直接斩断了大部分漏洞利用的可能。

3.1 后UAF时代

就目前来看,14年之后由于新的缓解措施的加入使得攻防双方的优势几乎一边的倒向了微软为首的防守者的阵营。

浏览器的漏洞利用已经没有固定的套路。如浏览器内部的脚本引擎的设计错误导致从脚本层面突破IE而进行漏洞的相关利用(CVE-2014-6332),对浏览器中flash插件的漏洞发掘利用得到ring3权限,然后配合对较老字体解析引擎代码发掘出来的提权漏洞再进行提权拿到系统权限(Hacking Team相关利用)。漏洞利用方式不一而足,有机会在修订简史的时候一并补充。

0×03 IE漏洞防护措施关键时点

2015年7月 CFG编译器支持 VS2015 RTM版本引入/guard开关对Control Flow Guard特性提供编译器支持。

2014年11月 CFG系统级别支持 Windows8.1 update3 对Control Flow Guard提供系统层面的支持,之后的windows系统均在操作系统层面支持该特性。

2014年7月 MS14-037补丁发布引入Delayed  Free特性。

2014年6月 MS14-035补丁发布引入Isolated  Heap特性。

2008年1月 SEHOP系统级别支持 发布vista Service Pack 1补丁包,引入对SEHOP特性的操作系统支持。自vista sp1后的windows系统均在操作系统层面支持该特性。

2007年1月 ASLR系统级别支持 windows vista系统引入对ASLR特性操作系统级别的支持。自vista后的windows系统均在操作系统层面支持该特性。

2006年年初 safeseh/stack cookie/aslr/dep编译器支持 VS2005引入/safeseh编译开关缓和溢出漏洞对seh的攻击,引入/GS编译开关插入Stack Cookie缓和对返回地址的攻击,加入/dynmicbase编译开关引入对ASLR特性的编译器支持,加入/NXCOMPAT编译开关引入对DE特性的编译器支持。自VS2005之后的编译器均支持上述编译开关。

2004年8月 DEP系统级别支持。微软推出XP Service Pack 2补丁包引入对DEP特性的操作系统支持,自XP SP2后的windows系统均在操作系统层面支持该特性。

* 作者:阿尔法实验室,转载请注明来自FreeBuf黑客与极客(FreeBuf.COM)

原文  http://www.freebuf.com/articles/system/93598.html
正文到此结束
Loading...