QEMU:CVE-2015-5165
Qemu笔记
计划
CVE-2015-5165+CVE-2015-7504
CVE-2019-6778+CVE-2019-14378
Qemu相关
简介
QEMU是一种通用的开源计算机仿真器和虚拟器。
当用作机器仿真器(machine emulator,)时,QEMU可以在另一台机器(例如您自己的PC)上运行为一台机器(例如ARM板)制作的OS和程序。通过使用动态翻译,它可以获得非常好的性能。
当用作虚拟器(virtualizer)时,QEMU通过直接在主机CPU上执行来宾代码来达到近乎本机的性能。在Xen虚拟机管理程序下执行或在Linux中使用KVM内核模块时,QEMU支持虚拟化。使用KVM时,QEMU可以虚拟化x86,服务器和嵌入式PowerPC,64位POWER,S390、32位和64位ARM以及MIPS guest虚拟机。
关于emulator和virtualizer的区别可以看这篇博客,简单来说emulator是使用软件仿真完整的硬件,可以模拟各种CPU架构或者多个系统,而virtualizer一般不模拟硬件,而是将模拟计算机的部分经过虚拟化,大多数程序依然直接运行在硬件上。(关于虚拟化最典型的例子比如页表?)
可在此处下载QEMU=>www.qemu.org/download/
CVE-2015-5165
Cve-2015-5165是一个信息泄露漏洞,能够让攻击者获取到qemu程序的基地址以及qemu为虚拟机分配的内存地址。
漏洞问题是出在Qemu模拟的 RTL8139 网卡(qemu/hw/net/rtl8139.c),漏洞原因是在C+模式下对数据包解析的时候没有对数据包长度进行检测,导致了溢出。
环境搭建
在qemu的git平台找到CVE-2015-5165漏洞修复的commit
安装一些依赖
1 | sudo apt -f install |
编译qemu,记得加参数–enable-debug,可以gdb源码调试。
1 | git clone git://git.qemu-project.org/qemu.git && cd qemu |
编译好的程序在**/bin/debug/naive/x86_64-softmmu/qemu-system-x86_64**,检查一下qemu版本。
1 | $ ./qemu-system-x86_64 --version |
编译出*/usr/bin/ld: qga/commands-posix.o: in function `dev_major_minor’:*问题
只需要在commands-posix.c文件中加上头文件<sys/sysmacros.h>重新编译即可
gdb基础操作命令
通过gdb对qemu进行源代码调试
完整名称 | 短称 | 功能介绍 | 使用示例 |
---|---|---|---|
continue | c | 继续执行 | c |
list | l | 查看 c 源码 | l vga_mem_write |
help | h | 帮助说明 | h list |
break | b | 下断点 | b vga.c:45 |
next | n | 步过 | n5 |
step | s | 步入 | s |
p | 输出 | print /x var | |
x | x | 输出 | x/2wx pmem |
backtrace | bt | 堆栈回溯 | bt |
finish | fin | 执行到函数返回 | finish |
制作系统镜像
制作一个镜像
1 | qemu-img create -f qcow2 ubuntu.img 20G |
将ubuntu镜像拷贝到空镜像中,如果运行qemu提示通过vnc连接,应该是系统缺少SDL库(configure时显示SDL support no),装一个就行了,否则可以装一个Remmina在本地进行VNC连接。之后只需要一步步执行安装过程即可。 或者可以直接去镜像网站下载一个qcow2(账号和密码都是root)的镜像(但不推荐这个镜像,内核版本太老有很多问题,但我也没有找到更好的)。
1 | ./x86_64-softmmu/qemu-system-x86_64 -m 1G -hda ubuntu.img -cdrom ../../../ubuntu-14.04.6-desktop-i386.iso -enable-kvm |
或者自己做一个rootfs.img的镜像
1 | !/bin/sh |
编译内核
1 | sudo apt install libelf-dev |
使用launch.sh脚本起启动,ssh只需要连接10021端口
1 | cat launch.sh |
make defconfig时候出现/bin/sh: 1: flex: not found
相关问题只需要安装一下flex(一个快速的词法分析生成器)
运行系统
使用下面的命令来运行系统
1 | qemu-system-x86_64 -hda ubuntu.img -nographic |
漏洞在RTL18139网卡上,所以启动时候把网卡也启动一下。
1 | -netdev user,id=t0, -device rtl8139,netdev=t0,id=nic0 -net user,hostfwd=tcp::22222-:22 -net nic |
虚拟机内部换一下源,方便安装一些工具。
1 | deb http://mirrors.aliyun.com/debian/ buster main non-free contrib |
问题解决:
- failed to load ldlinux.c32 : 换个版本,一开始我用ubuntu16.04总是有这个问题。
rtl8139网卡
本次漏洞的成因位于rtl8139的虚拟化实现(准确的来说是**RTL-8139/8139C/8139C+*),实现文件在/hw/net/rtl8139.c*中。RTL8139State结构体内部大多为RTL8139的寄存器实现,可以参考官网的REALTEK文档.
1 | typedef struct RTL8139State { |
关于C+ Mode:
支持两种缓冲管理模式。第一种是C模式,是RTL8139系列产品默认使用的缓冲区管理算法。第二种是C +模式(仅通过软件设置为相对的C +模式寄存器和描述符),这是基于描述符(Tx desciptor)的增强设计,特别适用于服务器应用程序。 可以通过软件进行配置,以应用新的缓冲区管理算法,即基于描述符的增强型缓冲区管理体系结构,这是现代网络服务器卡的基本设计。
RTL8139 网卡在 C+ 模式下的寄存器结构:
1 | +---------------------------+----------------------------+ |
各个部分对应功能和实现:
- MAC0 :存储mac地址
uint8_t phys[8];
(uint8_t aka. char) - MAR0 : 组播掩码数组
uint8_t mult[8];
- TxStatus0 : 在C模式下是TxStatus0,在C+模式下为
DTCCR[0] and DTCCR[1]
- TxAddr0 :Tx descriptiors table 相关的物理内存地址
uint32_t TxAddr[4];
- 0x20 ~ 0x27:Transmit Normal Priority Descriptors Start Address
- 0x28 ~ 0x2F:Transmit High Priority Descriptors Start Address
- RxBuf :接收数据的缓冲区
uint32_t RxBuf;
- TxConfig :发送数据相关的配置参数
uint32_t TxConfig
- RxConfig :接收数据相关的配置参数
uint32_t RxConfig
- RxRingAddrLO :Rx descriptors table 物理内存地址低 32 位
- RxRingAddrHI :Rx descriptors table 物理内存地址高 32 位
- TxPoll :让网卡检查 Tx descriptors
Tx desciptor的结构和实现如下
取自REALTEK文档.9.2.1 Transmit
1 | struct rtl8139_desc { |
我们关注一下16~31bit的标志位实现,网卡中的路径走向是由desciptor结构中的标志位确定的,这部分在后面理解漏洞部分的触发有一些帮助。具体作用还是参考REALTEK文档,具体我注释在代码中。
1 | /*26~31bit位实现了desciptor的*/ |
漏洞分析
根据修复漏洞时的**diff文件,我们可以找到漏洞产生的函数rtl8139_cplus_transmit_one**
漏洞出现在rtl8139_cplus_transmit_one函数处对IP包头部和IP总长度计算时产生的溢出。
1 | static int rtl8139_cplus_transmit_one(RTL8139State *s) |
漏洞成因:当ip_len(ip总长度)小于hlen(ip头长度,一般等于20),调用 be16_to_cpu(ip->ip_len) - hlen 会返回一个小于0的数据,ip_data_len是无符号整型,所以会导致ip_data_len变成一个很大的数。
注:be16_to_cpu:将网络字节序转化为无符号短整形,与htons函数功能正好相反。
接下来继续追踪ip_data_len。接下来是要发送网络帧的代码,ip_data_len赋值给了tcp_data_len,如果tcp_data_len过长(大于 1500-ip头-tcp头),对tcp_data_len进行切片并且调用rtl8139_transfer_frame进行发送。这样的结果就是会读取超长的一段数据发送出去。
1 | if (ip) |
设置回环网卡(TxLoopBack),这样网卡会接收自己发送的数据,就能够实现泄露信息读取。
具体实现让我们继续看rtl8139_transfer_frame,当TxConfig标志位被设置为TxLoopBack,会调用rtl8139_do_receive将刚才网卡发送的数据接收回来,保存在缓冲区中。
1 | static void rtl8139_transfer_frame(RTL8139State *s, uint8_t *buf, int size, |
漏洞触发
我们需要找到如何触发函数rtl8139_cplus_transmit_one调用。
首先看一下rtl8139的realize(实现)函数,使用MemoryRegion初始化了PMIO和MMIO。(memory_region_init_io函数具体相关可以去看qemu内存模型相关博客)
在计算机中,内存映射I/O(MMIO)和端口映射I/O(PMIO)是两种互为补充的I/O方法,在CPU和外部设备之间。另一种方法是使用专用的I/O处理器,通常为大型机上的通道,它们执行自己特有的指令。
- 在MMIO中,IO设备和内存共享同一个地址总线,因此它们的地址空间是相同的; 而在PMIO中,IO设备和内存的地址空间是隔离的。
- 在MMIO中,无论是访问内存还是访问IO设备,都使用相同的指令; 而在PMIO中,CPU使用特殊的指令访问IO设备,在Intel微处理器中,使用的指令是IN和OUT。
1 | static void pci_rtl8139_realize(PCIDevice *dev, Error **errp) |
分析一下PMIO部分(MMIO基本类似,不需要重复分析),MMIO和PMIO的写操作都会调用rtl8139_io_writeb这个函数,这个函数当val 包含 (1 << 6)时就会进入分支,调用包含漏洞的函数rtl8139_cplus_transmit。
1 | // 初始化结构体 |
再深挖一下PMIO是如何初始化和被触发的,看memory.c中初始化函数memory_region_init_io是如何实现的。可以参考这篇博客在qemu中增加pci设备并用linux驱动验证
1 | void memory_region_init_io(MemoryRegion *mr, |
小结
函数调用栈以及进入分支的条件
1 | rtl8139_ioport_write |
漏洞分析完毕,然后就可以开始写poc了,不过在写之前,先了解一些网卡相关的开发基础。
一些相关的开发基础
操作网卡相关:
cat /proc/ioports 查看端口
lshw -short 查看设备信息
1 | root@debian-i386:~# lshw -short |
- Lshw -C network 查看网卡信息,网卡版本为RTL-8139/8139C/8139C+,IO端口地址0xc000~0xc0ff.
1 | root@debian-i386:~# lshw -C network |
- lspci -v 同样可以查看IO地址,可以看到PMIO的端口在0xc000
1 | # lspci -v |
io写端口操作函数
outb() I/O 上写入 8 位数据 ( 1 字节 )
outw() I/O 上写入 16 位数据 ( 2 字节 )
outl () I/O 上写入 32 位数据 ( 4 字节)
1
2
3
4
void outb ( unsigned char data , unsigned short port);
void outw ( unsigned short data , unsigned short port);
void outl ( unsigned long data , unsigned short port);
一般使用out*函数向端口写数据,一般格式是 data和port+偏移值。于是,通过PMIO端口向网卡写数据可以通过下面三个函数实现。
1 | /*通过lspci -v 获取IO端口*/ |
io读端口操作函数
inb() I/O上读取8位数据
inw() I/O上读取16位数据
inl() I/O上读取32位数据
1
2
3byte inb(word port);
word inw(word port);
longword inw(word port);
读函数的实现
1 | uint32_t pmio_readb(uint32_t addr){ |
POC
静态编译为32位的程序(gcc -m32 -static poc.c -o poc -std=c99),然后通过scp拷贝到虚拟机中。
1 | sudo apt-get install build-essential module-assistant |
- FATAL: kernel too old ,内核版本太低,两种方案,一种是更新一下虚拟机的内核,第二种是降低编译阶段的内核版本,参考链接。(第三种,也可以直接在虚拟机下装一下编译工具链,本地编译)
接下来编写poc,相关功能都注释在代码中。
poc.c
1 |
|
CVE-2015-7504
cve-2015-7504漏洞存在于hw/net/pcnet.c的**pcnet_receive()**函数中,漏洞出现在对于数据包的crc校验计算中。
pcnet网卡
网卡有16位(默认)和32位两种模式,这取决于DWIO(存储在网卡上的变量)的实际值,16位模式是网卡重启后的默认模式。网卡有两种内部寄存器:CSR(控制和状态寄存器)和BCR(总线控制寄存器)。两种寄存器都需要通过设置对应的我们要访问的RAP(寄存器地址端口)寄存器来实现对相应CSR或BCR寄存器的访问。
pcnet_receive函数
当size值等于s->buffer的大小时,最后会越界四个字节。
1 | ssize_t pcnet_receive(NetClientState *nc, const uint8_t *buf, size_t size_) |
看一下PCNetState_st结构体。溢出s->buffer会覆盖到后面的qemu_irq irq变量
1 | typedef struct PCNetState_st PCNetState; |
找一下qemu_irq的定义(在irq.h中),是一个指向IRQState结构体的指针(4字节)。
1 | // irq.h |
触发漏洞
追溯调用栈,pcnet_transmit函数在标志位BCR_SWSTYLE为1时会触发漏洞函数 pcnet_receive。
1 | static void pcnet_transmit(PCNetState *s) |
而pcnet_ioport_writew调用pcnet_csr_writew最终会调用pcnet_transmit。
1 | void pcnet_ioport_writew(void *opaque, uint32_t addr, uint32_t val) |