文章目录
参考
地址相关
每个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)来标记内存地址信息
c
#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映射内存地址,直接操作内存
c
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访问
c
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
c
#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 device
int 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会被设置成偏移
c
#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到文件系统
bash
gcc -static exp.c -o exp
find . | cpio -o --format=newc > ../rootfs.img
启动qemu
bash
./launch.sh
bash
touch .gdbinit
往里面写入断点
b*d3dev_pmio_write
b*d3dev_pmio_read
b*d3dev_mmio_write
b*d3dev_mmio_read
bash
ps -ef | grep qemu
一般是最下面的那个进程
bash
sudo gdb attach -p 进程号
在qemu中
bash
./exp
此时gdb会断在断点上,然后开始调试
成功attach之后即可下断点调试,开启了pie保护,且没有符号表可以用pwndbg的$rebase()根据ida中的地址确定进程中的真实地址