堆溢位的unlink利用方法

wyzsk發表於2020-08-19
作者: 藏在灌木叢中 · 2015/07/22 14:18

0x00 背景


本文寫給對堆溢位無的放矢的童鞋,分為如下幾部分:

一.經典的unlink利用方法簡介
二.在當今glibc的保護下如何繞過進行unlink利用

建議閱讀本文之前先對glibc的malloc.c有所瞭解

你可以在這裡線上看到所有的malloc.c的原始碼

0x01 第一部分


首先簡要介紹一下堆chunk的結構

我們可以在malloc.c中找到關於堆chunk結構的程式碼

#!c
struct malloc_chunk {

      INTERNAL_SIZE_T      prev_size;  /* Size of previous chunk (if free).  */
      INTERNAL_SIZE_T      size;       /* Size in bytes, including overhead. */

      struct malloc_chunk* fd;         /* double links -- used only if free. */
      struct malloc_chunk* bk;

      /* Only used for large blocks: pointer to next larger size.  */
      struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */
      struct malloc_chunk* bk_nextsize;
    };

這指明瞭一個heap chunk是如下的結構

#!c
+-----------+---------+------+------+-------------+
|           |         |      |      |             |
|           |         |      |      |             |
| prev_size |size&Flag|  fd  |  bk  |             |
|           |         |      |      |             |
|           |         |      |      |             |
+-----------+---------+------+------+-------------+

如果本chunk前面的chunk是空閒的,那麼第一部分prev_size會記錄前面一個chunk的大小,第二部分是本chunksize,因為它的大小需要8位元組對齊,所以size的低三位一定會空閒出來,這時候這三個位置就用作三個Flag(最低位:指示前一個chunk是否正在使用;倒數第二位:指示這個chunk是否是透過mmap方式產生的;倒數第三位:這個chunk是否屬於一個執行緒的arena)。之後的FD和BK部分在此chunk是空閒狀態時會發揮作用。FD指向下一個空閒的chunk,BK指向前一個空閒的chunk,由此串聯成為一個空閒chunk的雙向連結串列。如果不是空閒的。那麼從fd開始,就是使用者資料了。(詳細資訊請參考glibcmalloc.c部分,在此不再多做解釋。)

首先,為了方便,我直接引用一位外國博主的漏洞示例程式,以便繼續解釋

#!c
/* 
 Heap overflow vulnerable program. 
 */
#include <stdlib.h>
#include <string.h>

int main( int argc, char * argv[] )
{
        char * first, * second;

/*[1]*/ first = malloc( 666 );
/*[2]*/ second = malloc( 12 );
        if(argc!=1)
/*[3]*/         strcpy( first, argv[1] );
/*[4]*/ free( first );
/*[5]*/ free( second );
/*[6]*/ return( 0 );
}

這個程式在[3]處有很明顯的堆溢位漏洞,argv[1]中的內容若過長則會越界覆蓋到second部分。

簡單給出此程式的堆結構

#!c
+---------------------+   <--first chunk ptr
|     prev_size       |
+---------------------+
|     size=0x201      |          
+---------------------+   <--first                  
|                     |
|     allocated       |         
|      chunk          |      
+---------------------+   <--second chunk ptr                
|    prev_size        |         
+---------------------+                     
|    size=0x11        |         
+---------------------+   <--second                  
|     Allocated       |         
|       chunk         |     
+---------------------+   <-- top                  
|     prev_size       |            
+---------------------+                     
|    size=0x205d1     |           
+---------------------+                      
|                     |
|                     |
|                     |
|        TOP          |   
|                     |
|       CHUNK         |    
|                     |
+---------------------+

此處不贅餘介紹exploit具體程式碼,只介紹利用方法.

只要我們透過溢位構造,使得second chunk

#!c
prev_size=任意值
size=-4(因為最低位的flag沒有設定,所以prev_size是什麼值是無所謂了)
[email protected]([email protected]定技術”)
bk=shellcode地址

在我們的payload將指定位置的數值改好後。下面介紹在[4][5]行程式碼執行時發生的詳細情況。

第四行執行free(first)發生如下操作

1).檢查是否可以向後合併

首先需要檢查previous chunk是否是空閒的(透過當前chunk size部分中的flag最低位去判斷),當然在這個例子中,前一個chunk是正在使用的,不滿足向後合併的條件。

2).檢查是否可以向前合併

在這裡需要檢查next chunk是否是空閒的(透過下下個chunk的flag的最低位去判斷),在找下下個chunk(這裡的下、包括下下都是相對於chunk first而言的)的過程中,首先當前chunk+當前size可以引導到下個chunk,然後從下個chunk的開頭加上下個chunksize就可以引導到下下個chunk。但是我們已經把下個chunksize覆蓋為了-4,那麼它會認為下個chunkprev_size開始就是下下個chunk了,既然已經找到了下下個chunk,那就就要去看看size的最低位以確定下個chunk是否在使用,當然這個size-4,所以它指示下個chunk是空閒的。

在這個時候,就要發生向前合併了。即first chunk會和 first chunk的下個chunk(即second chunk)發生合併。在此時會觸發unlink(second)宏,想將second從它所在的bin list中解引用。

具體如下

#!c
BK=second->bk(在例子中bk實際上是shellcode的地址)
FD=second->fd ([email protected] - 12)
FD->bk=BK
/*shellcode的地址被寫進了FD+12的位置,[email protected][email protected]*/
BK->fd=FD 

執行unlink宏之後,再呼叫free其實就是呼叫shellcode,這時就可以執行任意命令了。

但是,在現如今,glibc已經不這麼簡單了,為了使堆溢位不那麼容易就被利用,它加入了許多新的保護措施,如何繞過也就是要在第二部分中討論的內容。

0x02 第二部分


以glibc中的程式碼作為示例,首先拿出最新版本的unlink宏。

#!c
1413    /* Take a chunk off a bin list */
1414    #define unlink(AV, P, BK, FD) {                                            
1415        FD = P->fd;                                                                      
1416        BK = P->bk;                                                                      
1417        if (__builtin_expect (FD->bk != P || BK->fd != P, 0))                      
1418          malloc_printerr (check_action, "corrupted double-linked list", P, AV);  
1419        else {                                                                      
1420            FD->bk = BK;                                                              
1421            BK->fd = FD;                                                              
1422            if (!in_smallbin_range (P->size)                                      
1423                && __builtin_expect (P->fd_nextsize != NULL, 0)) {                      
1424                if (__builtin_expect (P->fd_nextsize->bk_nextsize != P, 0)              
1425                    || __builtin_expect (P->bk_nextsize->fd_nextsize != P, 0))    
1426                  malloc_printerr (check_action,                                      
1427                                   "corrupted double-linked list (not small)",    
1428                                   P, AV);                                              
1429                if (FD->fd_nextsize == NULL) {                                      
1430                    if (P->fd_nextsize == P)                                      
1431                      FD->fd_nextsize = FD->bk_nextsize = FD;                      
1432                    else {                                                              
1433                        FD->fd_nextsize = P->fd_nextsize;                              
1434                        FD->bk_nextsize = P->bk_nextsize;                              
1435                        P->fd_nextsize->bk_nextsize = FD;                              
1436                        P->bk_nextsize->fd_nextsize = FD;                              
1437                      }                                                              
1438                  } else {                                                              
1439                    P->fd_nextsize->bk_nextsize = P->bk_nextsize;                      
1440                    P->bk_nextsize->fd_nextsize = P->fd_nextsize;                      
1441                  }                                                                      
1442              }                                                                      
1443          }                                                                              
1444    }
1445    
1446    /*

我們可以看到我們最大的阻礙是下面的這部分程式碼

#!c
if (__builtin_expect (FD->bk != P || BK->fd != P, 0))                     
      malloc_printerr (check_action, "corrupted double-linked list", P);

這段程式碼被新增到了unlink宏中,所以現在再呼叫unlink宏的時候,chunk指標P->fd->bk(即程式碼中的大寫FD->bk)應該還是p指標自己。對於BK->fd != p這部分也是同樣的道理。

在第一部分的利用方法中,我們修改了

#!c
p->[email protected]
p->bk=shellcode_adress

我們在此記FD=p->fd , BK=p->bk,再去看FD->bk已經是[email protected]了,這部分是不能滿足要求的。再看BK->fd已經是shellcode+16了,所以如上文的利用方法已經不能成功了。之所以還加以介紹,是因為這會使我們理解第二部分變得又快又好。

如果繞過還是要根據這段保護程式碼來談。我們勢必需要構造合適的條件的來過掉這行程式碼,那麼就要找一個指向p的的已知的地址,然後根據這個地址去設定偽造的fd和bk指標就能改掉原p指標。

以64bit為例,假設找到了一個已知地址的ptr是指向p(p指向堆上的某個地方)的,透過堆溢位,我們可以做如下的修改。

#!c
p->fd=ptr-0x18
p->bk=ptr-0x10

佈置好如此結構後,再觸發unlink宏,會發生如下情況。

#!c
1.FD=p->fd(實際是ptr-0x18)
2.BK=p->bk(實際是ptr-0x10)
3.檢查是否滿足上文所示的限制,由於FD->bk和BK->fd均為*ptr(即p),由此可以過掉這個限制
4.FD->bk=BK
5.BK->fd=FD(p=ptr-0x18)

這時候再對p進行寫入,可以覆蓋掉p原來的值,例如我們用合適的payload[email protected]寫入。p就變成了[email protected],那麼再改一次p,把[email protected]改為shellcode的地址或者說system的地址都可以。之後再呼叫free功能,就可以任意命令執行。

為了方便,在這邊拿出一個最近的wargame出現的一個邏輯非常簡單的程式作為漏洞示例程式,可以在此下載

首先簡單介紹這個Binary的功能以及基本情況

開啟的保護
RELRO    STACK CANARY    NX          PIE     RPATH    RUNPATH    FILE
No RELRO No canary found NX enabled  No PIE  No RPATH No RUNPATH shellman
基本功能
1.顯示已經建立的堆塊中儲存的內容
2.建立一個新的堆塊,大小和內容又使用者決定
3.對一個已經分配的堆塊做編輯,這個地方沒有限制大小,若太長可造成堆溢位
4.釋放一個已經分配的堆塊
存放的堆塊的基本邏輯結構
.bss:00000000006016C0 ; __int64 usingFLAG[]
.bss:00000000006016C0 usingFLAG       dq ?                    ; DATA XREF: main+38o
.bss:00000000006016C0                                         ; .text:0000000000400A90o ...
.bss:00000000006016C8 ; __int64 LEN[]
.bss:00000000006016C8 LEN             dq ?                    ; DATA XREF: new+B5w
.bss:00000000006016C8                                         ; delete+79w
.bss:00000000006016D0 ; __int64 content[]
.bss:00000000006016D0 content         dq ?                    ; DATA XREF: new+BCw

程式有一個全域性陣列會儲存好每一個經過malloc分配的堆塊返回的指標。以及在全域性陣列中儲存長度以及本塊是否正在使用的標誌。

如何利用

按照前文所介紹的,我們希望使用Unlink的方法去利用這個堆溢位漏洞。首先,我們要找一個指向堆上某處的指標。因為儲存malloc返回指標的全域性陣列的存在,這讓我們的利用變得異常的簡單。因為bss段的地址也是固定的,我們可以知道,從而設定滿足需要的bk和fd指標,下面介紹具體步驟。

1.我們可以首先分配兩個長度合適的堆塊。(如下圖所示)

chunk0                malloc返回的ptr        chunk1        malloc返回的ptr
|                     |                     |             |
+-----------+---------+---+---+-------------+------+------+----+----+------+
|           |         |   |   |             |      |      |    |    |      |
|           |         |   |   |             | prev | size&|    |    |      |
| prev_size |size&Flag|   |   |             | size | flag |    |    |      |
|           |         |   |   |             |      |      |    |    |      |
|           |         |   |   |             |      |      |    |    |      |
+-----------+---------+---+---+-------------+------+------+----+----+------+

這時候這兩塊的fd和bk區域其實都是空的,因為他們都是正在使用的

2.對第一塊進行編輯,編輯的過程中設定好第零塊的bk和fd指標並溢位第一塊,改好第一塊的chunk頭的控制資訊(如下圖所示)

chunk0                malloc返回的ptr           chunk1        malloc返回的pt
|                     |                        |             |
+-----------+---------+----+----+----+----+----+------+------+----+----+------+
|           |         |fake|fake|fake|fake| D  | fake | fake |    |    |      |
|           |         |prev|size| FD | BK | A  | prev | size&|    |    |      |
| prev_size |size&Flag|size|    |    |    | T  | size | flag |    |    |      |
|           |         |    |    |    |    | A  |      |      |    |    |      |
|           |         |    |    |    |    |    |      |      |    |    |      |
+-----------+---------+----+----+----+----+----+------+------+----+----+------+
                      |--------new_size--------|

我們為了欺騙glibc,讓它以為堆塊零malloc返回的指標(我們後文中簡記為p)出就是chunk0指標,所以我們偽造了prev_size和size的部分,然後溢位堆塊1,改掉第1個堆塊的prev_size,數值應該是上圖所示new_size的大小;另外第1塊的size部分還要把prev_inuse的flag給去掉。如此就做好了unlink觸發之前的準備工作

3.刪掉chunk1,觸發unlink(p),將p給改寫。

在刪除堆塊1時,glib會檢查一下自己的size部分的prev_inuse FLAG,發現到到比較早的一個chunk是空閒的(實際是我們偽造的),glibc希望將即將出現的兩個空閒塊合併。glibc會先將chunk0從它的Binlist中解引用,所以觸發unlink(p)。

1).FD=p->fd(實際是0x6016D0-0x18,因為全域性陣列裡面指向p的那個指標就是0x6016D0)
2).BK=p->bk(實際是6016D0-0x10)
3).檢查是否滿足上文所示的限制,由於FD->bk和BK->fd均為*6016D0(即p),由此可以過掉這個限制
4).FD->bk=BK
5).BK->fd=FD(p=0x6016D0-0x18)

4.對p再次寫入,[email protected]

[email protected][email protected]實地址,進而算出libc的基址來過掉ASLR。

6.根據已經算出的libc基址再次算出system函式的真實[email protected] (如果沒有libc,可以考慮簡歷多個chunk,[email protected]面的函式,這樣在list時,我們可以得到兩個libc函式的真實地址,根據其偏移,便可以找出伺服器上的libc,若保護再夠複雜無法改got,我們還可以構造ropchain,同樣利用這樣的方式,把ropchain丟進全域性陣列中)

7.因為free已經變成了system,只要再建立一個內容為/bin/sh的塊,再刪掉,就可以得到shell,由此全部利用完成。

本文章來源於烏雲知識庫,此映象為了方便大家學習研究,文章版權歸烏雲知識庫!

相關文章