首页
社区
课程
招聘
[原创] D3CTF-2021 d3dev 漏洞分析及复现
发表于: 2021-3-10 02:46 11218

[原创] D3CTF-2021 d3dev 漏洞分析及复现

2021-3-10 02:46
11218

比赛的时间是3.5到3.7(也就是前几天),当时看了一下pwn第一题,记得题目描述里出现了三连——easy-signin-baby,感觉应该是pwn里面最简单的一题了,准备开搞~
题目描述
解压出来是一个docker环境,将rootfs用binwalk解压,看一下启动脚本,给的root权限,应该是虚拟机逃逸题了

这题当时比赛的时没有做出来(主要是去玩某个新玩具了,也没做过这类题目,所以就没继续做下去)

最后在github上找到的一个6小时前创建的叫d3ctf-2021-pwn-d3dev的仓库,据Readme来看应该是出题人发的,简单的讲了一下解题思路,丢了个exp,于是我也跟着分析、复现了一次,顺便也写个文章记录一下~

qemu的基本就是模拟了CPU、内存、I/O设备以及其他设备,如果开启了kvm,kvm会实现CPU以及内存的虚拟

CTF的qemu逃逸类题目基本上都是直接修改了qemu的源码,在这题里面,出题人在qemu里添加了一个pci设备,解题思路是通过设备中的漏洞以此获取host机上的flag

qemu的详细实现原理因为有大佬详细讲过了,想要了解的可以在文末找到相关主题链接自行阅读

与qemu的虚拟设备进行I/O交互通常有以下两种方式,分别是MMIO和PMIO,区别在于是否与设备共享内存,在这题里面我们两种都有用到

这种方法简单来讲就是直接操作I/O设备的共享内存空间,以此来交互,实现方法就是直接调用mmap映射内存,然后直接通过指针读写
mmap的fd参数为open以下两个文件之一,flags参数需要传递MAP_SHARED属性

不共享内存空间,需要调用inx和outx函数来进行交互(要先调用iopl(3)来提权

直接把qemu丢进IDA分析,然后看一下qemu的启动脚本,可以看到有个device参数后面跟了个d3dev,这应该就是漏洞所在的设备名

因为qemu二进制文件里有DWARF(调试信息),所以直接通过搜索函数名来定位相关函数是可以的,这里还有一种方法是从_start开始逐步跟下去找到初始化表,然后定位pci设备的注册表
具体流程: _libc_csu_init -> _frame_dummy_init_array_entry -> do_qemu_init_pci_d3dev_register_types

找到虚拟设备的info表后,我们可以定位到设备的初始化函数d3dev_class_init
在函数d3dev_class_init里,我们可以找到设备的vendor_id和device_id,这两个值在后面查询pci设备的时候会用到,这里我们先记下来

跟进pci_d3dev_realize函数里,这里分别定义了设备的两种I/O交互操作函数(即mmio和pmio)以及共享区域的大小(mmio为0x800),以便qemu检查是否越界

在d3dev_mmio_ops和d3dev_pmio_ops两个结构体里面,可以找到对应的read、write函数: d3dev_mmio_read、d3dev_mmio_write和d3dev_pmio_read、d3dev_pmio_write 这四个

逐个函数分析,我们可以看到d3dev_mmio_write函数里面有一个任意写

通过查看结构体我们可以发现blocks的大小刚好是0x800,也就是我们共享内存的区域,在这里我们有val、addr可控,但实际上不能通过直接控制addr来溢出,因为PCI设备在内部会检查这个地址是否越界

这里其实seek的值也是可控的,具体在d3dev_pmio_write函数里,控制seek我们就可以利用这个任意写漏洞(注意这里是通过index的方式访问内存,数组元素大小为8字节

这里我们可以看到val值可以是0-0x100之间的任意值,相当于可以溢出控制0x800大小的内存

继续分析其他函数,我们可以看到d3dev_mmio_read函数里其实还有任意读漏洞,分析到这里我们就有了任意读写d3devState这个结构体附近的内存

现在我们接着分析,看看有什么地方可以利用来执行system("sh")

还是d3dev_pmio_write这个函数里(前文对这个函数的这部分进行了省略),通过rand_r指针调用了函数,函数的首个参数是r_seed,r_seed这个值我们可以直接通过val控制(这里直接写字符串"sh"即可),而rand_r的值需要我们用任意写来修改(改成system的地址),这样我们就成功获取了宿主机的shell

前面在讲任意读、写漏洞的时候我们省略了加解密的过程,这里简单的说一下,我们先分析d3dev_mmio_read函数

我们读出数据的时候数据被进行了tea解密处理,其中16*low这里算一下相当于左移4位(直接看汇编也可以)
需要注意的是这里的key,实际上是可控的,通过调用d3dev_pmio_write函数可以直接清零整个key

在这里我们可以看到ida的反汇编结果有一个类型强转,0是64位,而key则是有4个32位元素的数组,这两行操作相当于清零了整个key数组

至于这个函数里对数据的解密实际上只是加密的逆操作(就是F5出来难看了点),不详细讨论

由于我们需要把rand_r的地址覆盖成system的地址,接下来我们需要计算共享内存开始到rand_r的偏移

从前面任意写漏洞我们可以知道blocks即使我们共享内存的区域,从blocks到rand_r的偏移是0x818,blocks是8字节数组,计算0x818/8=0x103也就是数组的index值,我们可以直接把seek的值设置成0x100,然后将addr往后偏移3*8=24个字节即可对rand_r进行修改

设备的pci地址我们可以直接通过执行指令lspci来查看

通过开头记下的vendor_id和device_id我们可以看出00:03.0对应的就是d3dev设备pci,然后通过cat /sys/devices/pci0000:00/0000:00:03.0/resource可以找到mmio和pmio的基址

febf1000即为mmio基址,c040即为pmio基址

这题由于可以直接通过静态分析的结果写出exp,故省略gdb调试qemu环节(其实主要因为我不会调试docker里的qemu,有大佬知道可以留言)

exp可以通过两种方法传到客户机,分别是直接通过python脚本压缩然后b64上传(远程),或者直接修改rootfs然后重新打包回去

这里介绍第二种方法,为了方便测试我们可以直接写一个Makefile

之后我们直接cd到rootfs然后make即可,记得也要修改一下launch.sh,将rootfs.img改为rootfs.cpio
然后根据题目readme重新打包docker镜像、运行即可

至于第一种方法,基本上脚本都一样的写法,没什么好说的

脚本可以在文末我的Github仓库里下载

不出意外执行结果是这样的,我们成功获取到了host的shell
运行结果

[原创]QEMU逃逸初探-二进制漏洞 - 看雪论坛
strng2 湖湘杯 2019 - 安全客
出题人写的github仓库(不知道为什么404了)

Github(什么时候有文件大小限制的...)
题目和idb(提取码: qjrg)

PS: 萌新第一次发帖,编辑器用的不习惯,排版比较丑,请见谅
PS: 本人接触CTF不到两年,经验不太丰富,有什么地方讲错,还请指出
PS: 转载请注明出处

#!/bin/sh
 
mkdir /tmp
mount -t tmpfs none /tmp
mount -t proc none /proc
mount -t sysfs none /sys
mount -t devtmpfs devtmpfs /dev
 
exec 0</dev/console
exec 1>/dev/console
exec 2>/dev/console
 
mdev -s
 
echo -e "\nBoot took $(cut -d' ' -f1 /proc/uptime) seconds\n"
echo "Interactive mode\n"
setsid /bin/cttyhack setuidgid 0 /bin/sh # 0 即为root
umount /proc
umount /sys
poweroff -d 0 -f
#!/bin/sh
 
mkdir /tmp
mount -t tmpfs none /tmp
mount -t proc none /proc
mount -t sysfs none /sys
mount -t devtmpfs devtmpfs /dev
 
exec 0</dev/console
exec 1>/dev/console
exec 2>/dev/console
 
mdev -s
 
echo -e "\nBoot took $(cut -d' ' -f1 /proc/uptime) seconds\n"
echo "Interactive mode\n"
setsid /bin/cttyhack setuidgid 0 /bin/sh # 0 即为root
umount /proc
umount /sys
poweroff -d 0 -f
 
 
 
#!/bin/sh
./qemu-system-x86_64 \
-L pc-bios/ \
-m 128M \
-kernel vmlinuz \
-initrd rootfs.img \
-smp 1 \
-append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 nokaslr quiet" \
-device d3dev \
-netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \
-nographic \
#!/bin/sh
./qemu-system-x86_64 \
-L pc-bios/ \
-m 128M \
-kernel vmlinuz \
-initrd rootfs.img \
-smp 1 \
-append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 nokaslr quiet" \
-device d3dev \
-netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \
-nographic \
 
void __fastcall d3dev_class_init(ObjectClass_0 *a1, void *data)
{
  PCIDeviceClass_0 *v2; // rax
  v2 = (PCIDeviceClass_0 *)object_class_dynamic_cast_assert(
                             a1,
                             (const char *)&env.tlb_table[1][115]._anon_0.dummy[31],
                             "/home/eqqie/CTF/qemu-escape/qemu-source/qemu-3.1.0/hw/misc/d3dev.c",
                             229,
                             "d3dev_class_init");
  v2->realize = (void (*)(PCIDevice_0 *, Error_0 **))pci_d3dev_realize;
  v2->exit = 0LL;
  *(_DWORD *)&v2->vendor_id = 0x11E82333;       // vendor=2333 device=11E8
  v2->revision = 0x10;
  v2->class_id = 0xFF;
}
void __fastcall d3dev_class_init(ObjectClass_0 *a1, void *data)
{
  PCIDeviceClass_0 *v2; // rax
  v2 = (PCIDeviceClass_0 *)object_class_dynamic_cast_assert(
                             a1,
                             (const char *)&env.tlb_table[1][115]._anon_0.dummy[31],
                             "/home/eqqie/CTF/qemu-escape/qemu-source/qemu-3.1.0/hw/misc/d3dev.c",
                             229,
                             "d3dev_class_init");
  v2->realize = (void (*)(PCIDevice_0 *, Error_0 **))pci_d3dev_realize;
  v2->exit = 0LL;
  *(_DWORD *)&v2->vendor_id = 0x11E82333;       // vendor=2333 device=11E8
  v2->revision = 0x10;
  v2->class_id = 0xFF;
}
void __fastcall pci_d3dev_realize(d3devState *pdev, Error_0 **errp)
{
  memory_region_init_io(&pdev->mmio, &pdev->pdev.qdev.parent_obj, &d3dev_mmio_ops, pdev, "d3dev-mmio", 0x800uLL);
  pci_register_bar(&pdev->pdev, 0, 0, &pdev->mmio);
  memory_region_init_io(&pdev->pmio, &pdev->pdev.qdev.parent_obj, &d3dev_pmio_ops, pdev, "d3dev-pmio", 0x20uLL);
  pci_register_bar(&pdev->pdev, 1, 1u, &pdev->pmio);
}
void __fastcall pci_d3dev_realize(d3devState *pdev, Error_0 **errp)
{
  memory_region_init_io(&pdev->mmio, &pdev->pdev.qdev.parent_obj, &d3dev_mmio_ops, pdev, "d3dev-mmio", 0x800uLL);
  pci_register_bar(&pdev->pdev, 0, 0, &pdev->mmio);
  memory_region_init_io(&pdev->pmio, &pdev->pdev.qdev.parent_obj, &d3dev_pmio_ops, pdev, "d3dev-pmio", 0x20uLL);
  pci_register_bar(&pdev->pdev, 1, 1u, &pdev->pmio);
}
.data.rel.ro:0000000000B78980 d3dev_mmio_ops  dq offset d3dev_mmio_read; read
.data.rel.ro:0000000000B78980                 dq offset d3dev_mmio_write; write
...
.data.rel.ro:0000000000B78920 d3dev_pmio_ops  dq offset d3dev_pmio_read; read
.data.rel.ro:0000000000B78920                 dq offset d3dev_pmio_write; write
.data.rel.ro:0000000000B78980 d3dev_mmio_ops  dq offset d3dev_mmio_read; read
.data.rel.ro:0000000000B78980                 dq offset d3dev_mmio_write; write
...
.data.rel.ro:0000000000B78920 d3dev_pmio_ops  dq offset d3dev_pmio_read; read
.data.rel.ro:0000000000B78920                 dq offset d3dev_pmio_write; write
void __fastcall d3dev_mmio_write(d3devState *opaque, hwaddr addr, uint64_t val, unsigned int size)
{
  ...
  if ( size == 4 )
  {
    offset = opaque->seek + (unsigned int)(addr >> 3);
    if ( opaque->mmio_write_part )
    {
      ... // 这部分后文会细讲
    }
    else
    {
      opaque->mmio_write_part = 1;
      opaque->blocks[offset] = (unsigned int)val;// 任意写
    }
  }
}
void __fastcall d3dev_mmio_write(d3devState *opaque, hwaddr addr, uint64_t val, unsigned int size)
{
  ...
  if ( size == 4 )
  {
    offset = opaque->seek + (unsigned int)(addr >> 3);
    if ( opaque->mmio_write_part )
    {
      ... // 这部分后文会细讲
    }
    else
    {
      opaque->mmio_write_part = 1;
      opaque->blocks[offset] = (unsigned int)val;// 任意写
    }
  }
}
 
void __fastcall d3dev_pmio_write(d3devState *opaque, hwaddr addr, uint64_t val, unsigned int size)
{
  uint32_t *key; // rbp
  if ( addr == 8 )
  {
    if ( val <= 0x100 )
      opaque->seek = val;                       // 控制seek
  }
  ...
}
void __fastcall d3dev_pmio_write(d3devState *opaque, hwaddr addr, uint64_t val, unsigned int size)
{
  uint32_t *key; // rbp
  if ( addr == 8 )
  {
    if ( val <= 0x100 )
      opaque->seek = val;                       // 控制seek
  }
  ...
}
uint64_t __fastcall d3dev_mmio_read(d3devState *opaque, hwaddr addr, unsigned int size)
{
  ...
  data = opaque->blocks[opaque->seek + (unsigned int)(addr >> 3)];// 任意读
  low = data;
  high = HIDWORD(data);
  ... // 这里做了异或加密,后面会提到,这里省略
  return high;
}
uint64_t __fastcall d3dev_mmio_read(d3devState *opaque, hwaddr addr, unsigned int size)
{
  ...
  data = opaque->blocks[opaque->seek + (unsigned int)(addr >> 3)];// 任意读
  low = data;
  high = HIDWORD(data);
  ... // 这里做了异或加密,后面会提到,这里省略
  return high;
}
 
void __fastcall d3dev_pmio_write(d3devState *opaque, hwaddr addr, uint64_t val, unsigned int size)
{
  uint32_t *key; // rbp
  if ( addr == 8 )
  {
    ...
  }
  else if ( addr > 8 )
  {
    if ( addr == 28 )
    {
      opaque->r_seed = val;                     // "sh"
      key = opaque->key;
      do
        *key++ = ((__int64 (__fastcall *)(uint32_t *, __int64, uint64_t, _QWORD))opaque->rand_r)(// system
                   &opaque->r_seed,
                   28LL,
                   val,
                   *(_QWORD *)&size);
      while ( key != (uint32_t *)&opaque->rand_r );
    }
  }
  ...
}
void __fastcall d3dev_pmio_write(d3devState *opaque, hwaddr addr, uint64_t val, unsigned int size)
{
  uint32_t *key; // rbp
  if ( addr == 8 )
  {
    ...
  }
  else if ( addr > 8 )
  {
    if ( addr == 28 )
    {
      opaque->r_seed = val;                     // "sh"
      key = opaque->key;
      do
        *key++ = ((__int64 (__fastcall *)(uint32_t *, __int64, uint64_t, _QWORD))opaque->rand_r)(// system
                   &opaque->r_seed,
                   28LL,
                   val,
                   *(_QWORD *)&size);
      while ( key != (uint32_t *)&opaque->rand_r );
    }
  }
  ...
}
uint64_t __fastcall d3dev_mmio_read(d3devState *opaque, hwaddr addr, unsigned int size)
{
  uint64_t data; // rax
  unsigned int i; // esi
  unsigned int low; // ecx
  uint64_t high; // rax
  ...
  i = 0xC6EF3720;
  low = data; // data为seek和addr控制的指针指向的8字节数据
  high = HIDWORD(data);
  do
  {
    LODWORD(high) = high - ((low + i) ^ (opaque->key[3] + (low >> 5)) ^ (opaque->key[2] + 16 * low));// low << 4 <=> 16 * low
    low -= (high + i) ^ (opaque->key[1] + ((unsigned int)high >> 5)) ^ (opaque->key[0] + 16 * high);
    i += 0x61C88647;
  }
  while ( i );
  ...
  return high;
}
uint64_t __fastcall d3dev_mmio_read(d3devState *opaque, hwaddr addr, unsigned int size)
{
  uint64_t data; // rax
  unsigned int i; // esi
  unsigned int low; // ecx
  uint64_t high; // rax
  ...
  i = 0xC6EF3720;
  low = data; // data为seek和addr控制的指针指向的8字节数据
  high = HIDWORD(data);
  do
  {
    LODWORD(high) = high - ((low + i) ^ (opaque->key[3] + (low >> 5)) ^ (opaque->key[2] + 16 * low));// low << 4 <=> 16 * low
    low -= (high + i) ^ (opaque->key[1] + ((unsigned int)high >> 5)) ^ (opaque->key[0] + 16 * high);
    i += 0x61C88647;
  }
  while ( i );
  ...
  return high;
}
void __fastcall d3dev_pmio_write(d3devState *opaque, hwaddr addr, uint64_t val, unsigned int size)
{
  uint32_t *key; // rbp
  if ( addr == 8 )
  {
    ...
  }
  else if ( addr > 8 )
  {
    ...
  }
  else if ( addr )
  {
    if ( addr == 4 )
    {
      *(_QWORD *)opaque->key = 0LL;             // key[0] = key[1] = 0
      *(_QWORD *)&opaque->key[2] = 0LL;         // key[2] = key[3] = 0
    }
  }
  else
  {
    ...
  }
}
void __fastcall d3dev_pmio_write(d3devState *opaque, hwaddr addr, uint64_t val, unsigned int size)
{
  uint32_t *key; // rbp
  if ( addr == 8 )

[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

最后于 2022-9-28 11:59 被lakwsh编辑 ,原因: 奇奇怪怪的格式问题
收藏
免费 1
支持
分享
最新回复 (6)
雪    币: 4468
活跃值: (1869)
能力值: ( LV12,RANK:209 )
在线值:
发帖
回帖
粉丝
2
mark
2021-3-10 11:32
0
雪    币: 20
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
3
az,仓库忘记设私有被你看到了...
2021-3-10 20:02
0
雪    币: 36
活跃值: (332)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
4
赤道企鹅 az,仓库忘记设私有被你看到了...
哈哈,当时抱着github会不会有wp的想法搜了一下,结果还刚好搜到了、、学到了很多
说起来第一题monitor的非预期解是怎么样的……
2021-3-11 00:22
0
雪    币: 20
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
5
lakwsh 哈哈,当时抱着github会不会有wp的想法搜了一下,结果还刚好搜到了、、学到了很多 说起来第一题monitor的非预期解是怎么样的……
ctrl a+c进了monitro就可以为所欲为了
2021-3-11 12:45
0
雪    币: 36
活跃值: (332)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
6
赤道企鹅 ctrl a+c进了monitro就可以为所欲为了
az,那确实可以叫签到了……
2021-3-11 15:25
0
雪    币: 2225
活跃值: (1154)
能力值: ( LV6,RANK:80 )
在线值:
发帖
回帖
粉丝
7
大爷爷写的太详细了,我直接膜拜
2021-3-11 16:52
0
游客
登录 | 注册 方可回帖
返回
//