无恒实验室:基于Qemu/kvm硬件加速的下一代安全对抗平台

robots

 

安全的本质永远绕不开“对抗”这个主题,基于对日常攻防对抗的思考和作为研究员不断探索的天性。

2021看雪第五届安全开发者峰会上,字节跳动无恒实验室云安全专家蒋浩天带来了《基于Qemu/kvm硬件加速的下一代安全对抗平台》的议题,虚拟化等技术带给传统攻防全新的思考维度,让我们看到了站在上帝模式下的降维打击。随着后续行为监控和武器库的完善,相信此工具可以帮广大安全人员解决安全对抗的疑难杂症。

下面就让我们来回顾下《基于Qemu/kvm硬件加速的下一代安全对抗平台》的精彩内容。

议题分为6个部分,分别是:

  1. 安全对抗思考
  2. 虚拟化技术
  3. Ark工具设计思路
  4. 调试器设计思路
  5. 平台介绍(KVM-Based security platform)
  6. 后续展望

 

01 安全对抗思考

首先,先来说一下:安全对抗思考。我们在安全对抗中会经常遇到那些棘手的问题呢?

由于很多黑产模块商业化。导致对抗异常激烈,商业化模块具备各种对抗功能,当恶意样本,黑产工具集成了商业化模块后,会具备很强的对抗能力,例如说:

1、反调试,公开插件已经无法绕过。

2、反沙箱,反ark工具,一旦检测到沙箱环境,行为监控工具,ark工具等常用的安全工具,样本停止工作。

3、机器封禁,当它检测到你在分析它,将会进行机器封禁,导致此机器无法运行此样本。

4、样本很紧急,老板让你今晚出报告。

目前来说,业界有哪些优秀的解决方案?

第一个解决方案,也是目前比较流行的解决方案,就是模拟执行,例如unicorn。模拟执行具备一定的优势,例如它可以模拟执行一段shellcode且是轻量级的。也有一些劣势,例如需要模拟一个可以执行的环境,这个环境想要模拟非常全面,工程量也是很大的。它的致命缺点是效率比较低,且环境模拟不全。无法模拟D3D,一些依赖于D3D的软件无法正常工作运行。例如说:游戏无法运行,游戏外挂就不能正常工作,你就无法分析它。

第二个比较主流的解决方案就是硬件调试器,此方案优点比较多:效率高,支持d3d。劣势是:第一,需要额外购买硬件并且价格比较高;第二,携带不方便,不方便给小伙伴展示高端调试技巧;第三,对于用户态环境识别不是非常好。

这两种解决方案为什么比较好用?

当前的这些问题,其实我综合分析一下,站在我的角度来看:windbg依赖于windows的调试子系统,gdb依赖unix的ptrace,所以就会存在很多可检测特征。

我理解来说:硬件调试器和模拟执行这两类工具抽象一点来看,更像一个站在CPU内部的工具,可以精准的控制每一行指令的执行,监控或修改指令执行结果。它不依赖于系统的调试机制,所以很多反调试方法对其无效。

通俗一点说:站在了一个至高点,实现了降维打击。

我们继续思考:模拟执行这项技术最开始用于虚拟机,来虚拟一个操作系统,虚拟机本身具备模拟执行的能力。

Unicorn和虚拟机有什么关联?经过查阅资料,unicorn的代码大部分是移植的QEMU中的代码。由于虚拟机模拟执行操作系统时,性能非常低,所以模拟执行逐渐被抛弃。硬件虚拟化成为了主流。启用硬件虚拟化的虚拟机,不再需要模拟执行了,效率得到了非常大的提升。

虚拟机一般都具备一个virtualJTAG接口,这个接口在模拟执行和硬件虚拟化两种模式中,实现原理不同。VirtualJTAG接口不能区分进程上下文,不能调试用户态程序,而且不能识别windows内核。

这时突发奇想,我们是否可以基于虚拟化来开发一套类似的工具?

是否可以对虚拟机进行二次开发,开发一个支持windows系统,支持用户态和内核态的调试器呢?并且将各种安全工具,ark工具,调试,都集成到一起,形成平台化。

这样,我们也是站在了一个制高点,形成一个降维打击。以后做安全对抗就非常方便了。我们的目标是:将安全工具运行在物理机,对虚拟机进行控制。在开发之前,就可以想到一些优略势,首先说优势:
1、不依赖于操作系统机制。例如调试机制。
2、不需要在虚拟机中安装模块,安全工具无法被检测。
3、直接在物理机对虚拟机进行环境扫描,权限更高且不会被检测。
4、可以监控虚拟机中敏感内存的读写执行。

但同时也会存在一些劣势:1、数据解析工作量大,适配繁琐。2、不能调用 api。一些常规操作变得异常复杂,例如锁,同步。

 

02虚拟化技术

接下来介绍一下虚拟化技术,先来介绍几个名词:对应的翻译在后面都列出来了,大家可以先看一下,因为后续讲解过程中可能会用到。

由于模拟执行,性能非常低,随着技术的发展,硬件虚拟化成为了主流。所以我们需要看一下硬件虚拟化的原理:

如图我们调用vmxon指令后,cpu进入虚拟化模式。分别为guest模式和host模式。通过VMExit和VMEntry事件来进行驱动。

例如说执行VMLAUNCH指令后,此时就会有一个VMEntry事件,虚拟机从host模式,进入到guest模式,此时虚拟机就开始执行了。

当虚拟机运行过程中,遇到了一些特殊事件比方说执行了in out 指令,就会产生一个VMExit 事件,此时CPU将会从guest模式切换到host模式。

在host模式下,对in out指令进行虚拟化处理。处理完成之后,在返回到guest,让虚拟机继续执行。

接下来需要介绍qemu kvm的架构图:

首先最底层,是硬件层,例如说CPU,GPU, 内存,磁盘,网卡等硬件设备。再硬件层之上,是linux内核层,例如说vmlinux,设备驱动,kvm等,其中kvm就处于这一层。再上一层,就是linux用户层,在这一层,运行了很多linux程序,服务,守护进程等相关的东西。Qemu就处于这一层,qemu以一个进程的形式存在。

Qemu基于kvm开启硬件虚拟化。所以说图中的两个qemu虚拟机都运行在kvm之上。在qemu虚拟机内部,运行着虚拟机的操作系统,分为虚拟机的内核层,和虚拟机的用户层。

根据上面的架构图得知,虚拟机是运行在qemu进程的上下文的。那么qemu进程,是否包含虚拟机的内存数据呢?

我们在创建虚拟机时,需要设置虚拟机内存的大小。虚拟机在启动时,首先要为虚拟机申请物理内存,代码如下。

通过此代码反映出一个问题。虚拟机的内存,其实就是对应这个mmap的内存,在qemu进程的上下文中。大家可以从代码中看到,这个mmap函数的调用。

如上图可以看到,虚拟机启动后,用户态会存在一个qemu的进程。我们的问题是:如何对这块内存进行识别,解析出虚拟机中操作系统的数据?想要完成这项工作,我们必须要完全了解虚拟机中是如何实现内存的虚拟化的。

我们先看一下虚拟化模式和非虚拟化模式的内存地址翻译流程区别:

对比发现,多了一层地址翻译,需要把虚拟机中的物理地址,再次进行翻译,翻译成物理机的物理地址。也就是GPA 翻译成HPA。

我们首先需要简单回顾一下X86架构下内存翻译流程:

逻辑地址到线性地址的翻译,我们在保护模式下,地址访问都是段寄存器加逻辑地址来进行访问的。

例如说,TI位为0。我们需要在gdt中寻址。根据段寄存器的index字段,来访问gdt表。找到对应的项,例如说,cs段寄存器的index 为2,对应GDT的第3项,获取到对应的段描述符之后,P位为1表示有效。

判断段描述符的DPL和段寄存器的RPL是否满足一系列的权限检查。如果检查全部通过。将得到段对应的base。大家可以看到,这些段的base都是0。拿到段的base之后,用base 加上逻辑地址偏移,既可以计算出对应的线性地址。

接下来我们说一下从线性地址到物理地址的转换,线性地址到物理地址的转换需要通过页表来完成。由这张图,我们可以找到每一级索引的位置:

先说一下4K页面的转换,CR3寄存器是页表寄存器。存放了页表的首地址,首先需要对线性地址进行拆分,把每一级的索引都划分出来,偏移也要划分出来。最高级索引,索引CR3指向的目录,拿到对应的项之后,这里面存放的也是一个物理地址。然后用第二级索引进行索引,以此类推,直到索引完成,最后拿索引到的物理地址,加上offset,即可完成线性地址到物理地址的转换。

接下来看一下2M页面的转换,他跟4K页面的转换基本原理一致,只是说索引级别少了。Offset变长了。

再看一下从GPA到HPA的翻译过程,先将GPA 进行拆分,划分好每一级的索引,和offset。根据eptp指向的页表,进行一级一级的索引,跟前面的页表翻译原理类似。

接着看一下虚拟机的内存虚拟化实现的整体架构,如图:

1、在虚拟机中,GVA先通过页表,翻译成GPA。
2、通过EPT,将GPA翻译成HPA.

Host模式下:1、在宿主机中,HVA通过宿主机的页表翻译成HPA。2、Qemu通过memory slot机制,管理这个GPA 到HPA的映射。

如图,黄色的方块,就代表虚拟机的一块内存,这块内存在guest中,访问对应的GVA可以访问到。同时,在host中,访问对应的HVA也可以访问到,因为都是对应的同一块物理内存。

前面我们介绍了X86架构下的地址翻译过程,我们还需要了解一下qemu是如何管理内存的。

由于qemu在启动时,mmap了一块内存,这块内存用于虚拟机的物理内存。由于内存的惰性分配,和效率优化,虚拟机需要对他进行管理。

Qemu有两种内存管理方式,第一种是树形管理,具备一个更好的管理视图。第二种是平坦类型管理,用于和kvm交互。

我们先说一下树形管理:Qemu的内存,最顶端是一个address space 结构体,不同类型的内存由不同的address space来表示。所有的address space通过链表,连接在一起。

在address space结构体内部,有一个root字段,指向一个memory region结构体,这个字段是内存树的根节点。

Memoryregion下面会有子节点,有一些子节点的ram_block是为空的,他表示一个别名的引用。其中alias offset 就是指向引用的memory region对应的这个虚拟机物理内存的偏移。有一些memory region的ram_block是有值的,如果不为空,里面会有一个host字段,这里指向的是虚拟机物理内存对应的hva。

虽然说qemu对虚拟机的内存有自己的管理体系。但是想要实现内存虚拟化,必须按照cpu的规则来进行ept映射。才能够生效。所以qemu还有一套平坦内存管理视图,用于和kvm进行交互。实现基于硬件的内存虚拟化。

如图所示,每一个memory region经过generatememory topology函数调用后,都会生成一个Flatview结构体,这个结构体会插入到一个flat_views的一个hashmap中,方便下次快速查找。

这个FlatView内部,存在一个ranges字段,这个字段是一个指针,指向Flat range的数组。每一个flat range 结构体管理着一块虚拟机的物理内存,标志着kvm中EPT如何对虚拟机物理内存进行虚拟化。

例如说flat range里有一个addr字段,Addr是一个addrrange 类型,里面包含一个start,和size。这里面的start存放的是一个gpa。Qemu通过调用kvm的接口来完成ept映射。kvm会根据调用参数,和上述的X86架构的规则来完成ept的映射。建立GPA和HPA的映射关系。从而实现了内存的虚拟化。到目前位置,qemukvm的内存虚拟化,我们大概介绍完成。

 

03 ark工具设计

有了前面的知识体系,我们想实现ark的功能,已经有了一些希望。我们就有了初步的想法。虚拟机中的物理内存是qemu启动中mmap出来的一块内存。所以我们读写这块内存,其实就是在读写虚拟机的物理内存。

所以第一步,我们要先完成HVP ßà GPA转换。我们可以加载一个内核模块,从而获取到EPT的页表。拿到页表后,我们就可以将GPA翻译成HPA。然后再从HPA转换成HVA。

但是这样有一个缺点,想要从HPA 翻译成GPA 就比较麻烦了,需要遍历ept的表,效率非常低。

KVM为了完成这个GPA到HVA的互相翻译,实现了一个remap机制。我们通过解析kvm的remap 可以完成这个步骤,也可以通过qemu的两种内存管理模型来完成这项工作。

我们完成了上述功能后,我们就具备了读取任意GPA的能力了。但是现在的操作系统都是分页的,就跟前面所讲述的一样,通过页表进行了映射。

所以说我们具备了GPA的任意地址读写能力还是不够的。我们还需要能够读取任意地址GVA才行。我们要想具备GVA的读写能力,我们就需要一个页表。有了页表,我们才能够自己翻译内存,找到GVA和GPA的对应关系。

现在的操作系统,每一个进程的用户态内存都是隔离的。内核态的内存是相同的。每一个进程都有一个独立的的页表,存放在进程控制块中。所以说我们要想能够读取任意进程的任意GVA,我们就需要先拿到所有进程的页表。但是我们想要拿到所有进程的页表,我们就需要拿到所有进程控制块信息。

由于进程控制块是存放在内核地址空间的,进程的内核态地址空间都是相同的,所以说,我们只要拿到任意一个进程的cr3,即可通过解析页表,来实现一个读写内核GVA的能力。有了这个能力,我们才能进行下一步操作。

问题来了,我们如何获得虚拟机中任意进程的cr3寄存器?

我们如何获取一个cr3寄存器的值呢?我们这里尝试了几种方法:
1、通过vmread指令,读取vmcs中的guest cr3。

2、通过对vmexit handler进行hook。

3、Kvm运行时,会把cr3 保存在某个位置。

如图,只要设置kvm_valid_regs的值, kvm在运行过程中就会自动把所有寄存器保存下来。经过调试发现,默认情况下,这个字段为0。所以我们需要自己将他置为1,之后我们就可以去读到cr3寄存器了。

有了cr3之后,我们自己写一套页表翻译的代码,将GVA转换成GPA。我们就可以具备读写内核地址空间GVA能力了。我们需要适配各个模式,例如32位模式,PAE模式,64位模式,4k页面模式,2M页面,1G页面等多种情况。

现在,我们只具备了读写内核地址空间任意GVA,但是我们还不能读写任意进程用户态的任意内存。

如前面所述,每一个进程都拥有一个自己的页表。而这个页表存放在进程控制块中。而进程控制块在内核地址空间中。

所以接下来我们应该找到所有的进程控制块。进程控制块由操作系统内核进行管理,例如说windows平台下,在ntoskrnl模块中,我们可以通过解析PspCidTable, PsActiveProcessHead等来识别出所有的进程控制块。

但是我们目前无法知道这些数据结构所在的地址。所以我们当务之急,我们需要找到ntoskrnl模块的首地址在哪里?

如何找到它,其实方法很多,可以通过IDT中的中断向量的handler来进行定位,或者msr寄存器等等诸多方法。

例如说:我们首先在物理机读取到虚拟机的IDT表。获取异常向量的handler。我们知道,异常向量的handler,肯定是位于ntoskrnl模块中。

第一种方法采用暴力搜索,向上搜做mz文件头,肯定能搜到。还是不够优雅。

第二种方法采用符号解析,我们通过解析内核符号,来获取handler在模块中的位置,Ntoskrnl首地址 =handler 地址 – 偏移;这样我们就能够获取到内核模块首地址了。

我们既然要用到符号信息,那么我们就得具备解析符号的能力。我们的代码运行在linux平台,我们如何解决符号解析的问题?

对于linux虚拟机,我们解析elf文件的debug info信息,这里面是一个dwarf结构,开源,第三方库比较多。所以很好解决。

但是windows pdb符号如何解析呢。我们知道在windows平台下解析pdb,调用com接口即可,非常简单。但是我们运行在linux平台上,是无法调用windows提供的api的。

微软最近几年公开了pdb相关的格式和代码。但是非常复杂,如果我们自己去解析,将耗费非常多的时间。于是我们想到了其他方法:

1、硬编码?放弃,维护很困难。
2、Llvmpdbutil,解析成功,但是对于win10 的符号信息解析崩溃,不知道现在llvm有没有修复这个问题。
3、Wine?我们最终采用了wine的方式,开发一个windows平台的符号解析程序,然后运行在wine环境下,来完成pdb解析。

这样,我们就解决了符号解析的问题。

我们现在具备了符号解析的能力,知道内核模块的首地址,并且具备对内核地址空间任意地址读写的能力。我们就可以基于这些能力实现一个ark的工具,例如说:

解析PspCidTable,PsActiveProcessHead读取所有的进程信息;解析PsLoadedModuleList读取所有的内核模块信息等等,方法非常多,可以实现的功能也非常多,而且老前辈写过很多文章,这里就不一一讲解了。

当我们解析出所有的进程控制块的时候,我们就可以拿到进程对应的页表。通过解析进程对应的页表,我们就具备读写任意进程任意空间GVA的能力了。

此时有一个潜在的问题,由于内存分页的机制问题,会把不常用的内存交换到磁盘上。

这种情况我们就无法读取这块内存了,这个问题我们后续进行讲解。

经过我们不懈努力,我们已经具备一个ark初级的功能,例如:可以读取到虚拟机中的所有进程;每一个进程中,加载的模块信息;可以读取到虚拟机中所有的内核模块等等。这些都是调试器不可缺少的能力。

 

04 调试器设计

接下来我们介绍一下调试器的设计。

传统的虚拟机,在未开启硬件虚拟化加速时,采用的模拟执行。实现一个调试接口比较简单。采用了硬件虚拟化后,虚拟机不再需要模拟执行,具备了更高的性能。

虚拟机本身提供了一个virtual JTAG调试接口。只是在模拟执行和硬件虚拟化两种模式下原理不同。并且不支持windows虚拟机,不能区分用户态和内核态。

如果我们对virtual JTAG调试接口进行改造,二次开发。我们是否可以能实现windows 内核态,用户态调试呢?

我们首先需要深入分析一下virtual JTAG的实现原理。我们以软件断点为例,会调用kvmarch insert sw breakpoint 函数来插入一个断点,我们看这个代码实现,我们发现,就是保存了原来的位置的字节码,然后替换成了cc。

把内存改写成一个0xcc,就能实现断点功能?看似平平无奇,其实在kvm_update_guest_debug中存在着奥秘。

我们首先得介绍一下intel硬件虚拟化中的一个特性,exception bitmat特性。

他是一个32位的字段,每一位对应一个异常。当对应的位为1时,虚拟机中,产生对应的异常,就会产生vmexit事件。从而host可以进行捕获,并进行虚拟化。

kvm_update_guest_debug 函数,看名字就可以猜到,会和kvm进行交互。我们分析到最后,到最关键的位置,发现:最终通过一个vmcs_write32函数,更新了vmcs的ExceptionBitmap,使得vcpu对guest中int3 异常具备拦截能力。

虚拟机中执行代码,如果遇到int3指令,会产生vmexit,进入host模式,首先由宿主机接管,宿主机将int3指令封装成一个事件,投递给qemu中内置的gdbserver来处理。

接着我们整体梳理一下virtualJTAG流程:

1、Vcpu遇到int 3,会产生异常,如果exception bitmap中第四位为1,则产生vmexit事件,并切换到host模式。
2、执行kvm中注册的vmexit handler。
3、Handler将此次的这个vmexit的事件封装成一个结构体,投递给qemu。4、Qemu把此事件交给内置的gdb server。
5、Gdb server 会和gdb client进行交互。
6、Gdb server 收到请求后,返回到vmexit handler中。
7、Vmexit handler中调用vmresume指令,产生vmentry事件,虚拟机恢复执行。
这个virtual jtag方案大概是这个原理。

我们通过分析发现,virtualJTAG之所以不能调试用户态内存,不支持windows,根本原因在于这个gdbserver不能识别windows 的结构体,

无法解析虚拟机中的所有进程,所有模块等等系统信息。

所以我们要做的事情,就是帮它来完成这个功能。于是我们就有了实现一个基于virtualJTAG的调试接口的思路的。

我们通过我们自己的ark,来识别所有的进程,进程中所有的模块。我们的ark,具备对虚拟机任意进程,任意内存的读写能力,我们对想调试的位置,把内存改写成int3。通过ExceptionBitmap 的能力,拦截int3异常。后续的处理流程交给qemu内置的gdbserver。

我们对内置的gdb server进行了修改,来完成了这个功能。有个问题是:是否可以采用硬件断点?其实采用硬件断点也是可以的,我们需要在kvm中对dr寄存器操作进行拦截,由于精力有限,所以并未实现。

其实不止如此,还有非常多的细节问题,例如说:

我们采用软件断点的方法,最终还是改写了内存,容易被检测到,怎么办?

用户态的内存,可能会被交换到磁盘,如何解决?
对于系统动态库,copy_on_write 如何解决?
Cpu 缓存问题?等等

我们需要把重点放在第一个问题上。用调试器下断点时还是把内存改写成了int 3,如何才能不被检测到?

我们要做的一件事情就是隐藏int3断点。保证不被检测。

通过ept特性,对虚拟机的内存进行hook,来欺骗虚拟机,使其无法感知到内存被修改。

如图所示,左边这部分时正常状态下的内存翻译流程,和权限,例如说,GPAàHPA的翻译,此页面具备读写执行三个权限。

右边这部分时我们hook之后的状态。我们对原始内存页面进行复制,将权限进行拆分,分为读写权限对应原始内存,和执行权限对应复制的内存,对复制的内存改写int3。

当guest 进程读写这块内存的时,读取到的是原始页面。当guest 进程执行这块内存的时候,将翻译成另一个已修改的另一个页面,执行的是修改成int 3的内存页面。

通过此方法,我们欺骗进程,让其无法检测到我们修改了内存。

我们通过eptMTF 等特性,进行了一些其他功能的扩展,例如说指令trace,内存读写监控。

 

05 平台介绍(KVM-Based security platform)

通过上述介绍,我们的安全对抗平台,大概原理想必大家都已经理解。

现在我们介绍一下我们安全对抗平台的整体架构:

1、首先我们具备一个linux内核模块,用于patchkvm,读写kvm中的关键数据,hookkvm 的关键函数。

2、我们需要对qemu进行二次编译,将我们自己实现的调试相关的代码移植到qemu去。使其具备调试能力。

3、Manager模块,分为几个子模块。虚拟机管理功能;符号解析功能,支持windows,支持linux;鼠标键盘模拟功能,虚拟机屏幕图片查找功能;Hypervisorsdk,用于和内核模块,qemu调试模块交互。

5、Python引擎,用于将目前的大部分功能通过python接口的形式导出,降低开发门槛。方便安全人员参与开发

6、还开发了一个简易的ui。

Python插件,来实现一些强力的功能,例如antirootkit,武器库,调试器等功能。

这是我们目前导出的部分python接口。例如说,获取所有进程,获取所有内核模块。获取符号信息。读写GPA,读写GVA,EPT hook, 内存读写执行监控等。

开发者,知道内核模块地址,进程控制块信息,然后通过解析符号的方法,可以轻松的开发其他的功能。

例如上面代码,大家可以通过非常简单的几行代码,就可以获取到虚拟机中的所有进程,所有内核模块等信息。

只需要在python中,importhypervisor_engine,即可。

接下来是视频演示:通过视频可以观察,我们的工具在运行过程中,不会出现虚拟机卡顿情况,而且不需要在虚拟机安装任何程序。

如果采用传统方法,对working set 进行对抗,比较麻烦。通过我们平台提供的能力,几行代码就可以搞定。

如图,非常简单,我们直接定位到内核的PsWatchEnabled,直接进行修改,关闭working set的信息收集。

调试能力展示,大家可以看到,我们在linux宿主机,直接调试虚拟机中的进程。

右边是被调试程序的ida截图,大家可以看一下汇编指令,和地址。左边是在物理机通过gdb,调试虚拟机中的这个demo程序。可以观察,汇编指令,和地址都是对应的。

调试能力展示,大家可以看到,我们在linux宿主机,直接调试虚拟机中的进程。

 

06 后续展望

1、武器库完善,后续希望把 ark 中的常用功能全部移植,实现一个完整的 ark。

2、通过 hook 虚拟机内核,完善行为监控工具,将沙箱检测能力放在host中。

3、后续尝试支持 arm 架构,通过 arm 服务器,运行安卓系统,完成对安卓系统的支持。

4、希望此工具可以帮广大安全人员解决安全对抗的疑难杂症

谢谢大家!

 

关于无恒实验室:

无恒实验室是由字节跳动资深安全研究人员组成的专业攻防研究实验室,实验室成员具备极强的实战攻防能力,通过渗透入侵演练,业务蓝军演练,漏洞挖掘、黑产打击、漏洞应急、APT应急等手段,不断提升公司基础安全、数据安全、业务安全水位,极力降低安全事件对业务和公司的影响程度。同时为公司和各大产品提供定期的渗透测试服务,产出渗透测试报告。全力确保字节跳动用户在使用旗下产品与服务时的安全。

无恒实验室持续招聘中,欢迎投递简历。

(完)