本文主要是介绍CTF-pwn-虚拟化-qemu前置知识,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
文章目录
- 参考
- 地址相关
- 交互相关
- 配置相关
- 调试
- 待完善(以后做题用到啥再加吧)
参考
https://xz.aliyun.com/t/6562?time__1311=n4%2bxnD0DRDBAi=GkDgiDlhjmYh2xuCllx7whD&alichlgref=https://www.bing.com/#toc-3
地址相关
每个qemu虚拟机都是宿主机上的一个进程,在进程中用mmap分配出大小为0x40000000字节的宿主机的虚拟内存来作为虚拟机的物理内存
GVA:guest virtual address(虚拟机中的虚拟地址)
GPA:guest physical address(虚拟机中的物理地址)
HVA:host virtual address(宿主机中的虚拟地址)
HPA: host physical address(宿主机中的物理地址)
GVA通过客户机的页表得到GPA,GPA实际是由宿主机进程mmap出来的空间内的偏移。再加上起始地址就是HVA(mmap分配出相应虚拟机申请大小的内存,用于给该虚拟机当作物理内存)
- 设备可以申请两类空间,memory mapped I/O(MMIO)和port mapped I/O(PMIO),并在配置空间中用Base Address Registers(BAR)来标记内存地址信息
#include <stdio.h>
#include <string.h>
#include <stdint.h>
#include <stdlib.h>
#include <fcntl.h>
#include <assert.h>
#include <inttypes.h>#define PAGE_SHIFT 12
#define PAGE_SIZE (1 << PAGE_SHIFT)
#define PFN_PRESENT (1ull << 63)
#define PFN_PFN ((1ull << 55) - 1)int fd;uint32_t page_offset(uint32_t addr)
{return addr & ((1 << PAGE_SHIFT) - 1);//虚拟地址addr在其所在页面内的偏移量
}uint64_t gva_to_gfn(void *addr)
{uint64_t pme, gfn;size_t offset;offset = ((uintptr_t)addr >> 9) & ~7;//页表条目相对于文件开始的偏移量(GFN, Guest Physical Frame Number)lseek(fd, offset, SEEK_SET);//移动文件指针到计算出的偏移量处,准备读取PTE。read(fd, &pme, 8); //从文件中读取8字节到pme变量即物理块起始地址if (!(pme & PFN_PRESENT))return -1;gfn = pme & PFN_PFN;return gfn;
}uint64_t gva_to_gpa(void *addr)
{uint64_t gfn = gva_to_gfn(addr);assert(gfn != -1);return (gfn << PAGE_SHIFT) | page_offset((uint64_t)addr);
}int main()
{uint8_t *ptr;uint64_t ptr_mem;// uint64_t ptr_malloc;fd = open("/proc/self/pagemap", O_RDONLY);if (fd < 0) {perror("open");exit(1);}ptr = malloc(256);strcpy(ptr, "Where am I?");printf("%s\n", ptr);ptr_mem = gva_to_gpa(ptr);printf("Your physical address is at 0x%"PRIx64"\n", ptr_mem);// ptr_malloc = gva_to_gpa(&malloc);// printf("malloc physical address 0x%"PRIx64" \n",ptr_malloc);getchar();return 0;
}
gdb attach后vmmap可以查看到启动脚本中的-M指定的分配大小
交互相关
每个PCI设备有一个总线号、一个设备号、一个功能标识,存在PCI域,PCI域最多可以承载256条总线, 每条总线最多可以有32个设备,每个设备最多可以有8个功能
lspci 可以查看设备所在的域、总线号、设备号、功能号,不同版本lspci 显示的内容不一样
其中对于形如0000:00:03.0的数据,0000是域,00是总线号,03是设备号,0是功能号
resource中的pmio端口和mmio起始地址是固定的
mmio:将设置的寄存器映射到端口,访问端口来访问寄存器
pmio:将设置的寄存器映射到内存, 访问内存来访问寄存器
- hexdump /sys/devices/pci0000:00/0000:00:03.0/config查看配置信息如deviceid和vendorid
- 访问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);// iopl(3) 表示打开对全部IO端口的访问权限
inb(port); //从端口读一个字节
outb(val,port); //往端口写一个字节val
设备会注册自己的MMIO和PMIO读写函数,当检测到访问其所在的内存或端口时调用注册的读写函数,实现对设备内存的读写功能
当对设备其所在的内存访问时,会调用注册的读mmio函数d3dev_mmio_read,读的位置是基地址+0x128,是4字节。被d3dev_mmio_read识别为了rsi为0x128(偏移),rdx为4
当对设备其所在的内存写时,会调用注册的写mmio函数d3dev_mmio_write
#include <assert.h>
#include <fcntl.h>
#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <unistd.h>
#include<sys/io.h>unsigned char* mmio_mem;void die(const char* msg)
{perror(msg);exit(-1);
}void mmio_write(uint32_t addr, uint32_t value)
{*((uint32_t*)(mmio_mem + addr)) = value;
}uint32_t mmio_read(uint32_t addr)
{return *((uint32_t*)(mmio_mem + addr));
}int main(int argc, char *argv[])
{// Open and map I/O memory for the strng deviceint mmio_fd = open("/sys/devices/pci0000:00/0000:00:03.0/resource0", O_RDWR | O_SYNC);//O_SYNC标志表示同步I/O操作,确保对文件的写操作在返回前完成if (mmio_fd == -1)die("mmio_fd open failed");mmio_mem = mmap(0, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED, mmio_fd, 0);//PROT_READ | PROT_WRITE:指定映射区域的访问权限,这里允许读写。//MAP_SHARED:映射类型,共享映射意味着对映射区域的修改会反映到设备上,其他映射同一资源的进程也能看到修改。//mmio_fd:之前通过open获得的文件描述符。//0:文件偏移量,从资源的起始位置开始映射。if (mmio_mem == MAP_FAILED)die("mmap mmio_mem failed");printf("mmio_mem @ %p\n", mmio_mem);mmio_read(0x128);mmio_write(0x128, 1337);mmio_read(0x128);}
可以发现rsi会被设置成偏移
#include <assert.h>
#include <fcntl.h>
#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <unistd.h>
#include<sys/io.h>void die(const char* msg)
{perror(msg);exit(-1);
}uint32_t pmio_base=0xc040;uint32_t pmio_write(uint32_t addr, uint32_t value)
{outl(value,addr);
}uint32_t pmio_read(uint32_t addr)
{return (uint32_t)inl(addr);
}int main(int argc, char *argv[])
{if (iopl(3) !=0 )die("I/O permission is not enough");pmio_read(pmio_base+4);pmio_write(pmio_base+4,1);pmio_read(pmio_base+4);}
- qemu中执行其中的程序时,当涉及到设备读写此时进行的操作会是qemu进程执行的,不是虚拟中的,就是对qemu进程上的虚拟地址相关进行操作,不是mmap出来作为宿主机的那部分(就和平常用户态一样了)
- 注册的相关读写函数中的addr是偏移,mmio范围是0x800,pmio是0x20
配置相关
PCI设备有其配置空间来保存设备信息,头部最开始的数据为Device id和Vendor id
在初始化设备时会初始化四个比较重要的结构体:TypeInfo -> TypeImpl -> ObjectClass -> Object,每个 Object对应一个具体的device,其构造函数在qemu启动用-device参数加载设备时调用
设备读写操作函数的第一个参数是这个Object类的指针
调试
打包exp到文件系统
gcc -static exp.c -o exp
find . | cpio -o --format=newc > ../rootfs.img
启动qemu
./launch.sh
touch .gdbinit
往里面写入断点
b*d3dev_pmio_write
b*d3dev_pmio_read
b*d3dev_mmio_write
b*d3dev_mmio_read
ps -ef | grep qemu
一般是最下面的那个进程
sudo gdb attach -p 进程号
在qemu中
./exp
此时gdb会断在断点上,然后开始调试
成功attach之后即可下断点调试,开启了pie保护,且没有符号表可以用pwndbg的$rebase()根据ida中的地址确定进程中的真实地址
待完善(以后做题用到啥再加吧)
这篇关于CTF-pwn-虚拟化-qemu前置知识的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!