堆漏洞之簡單的unlink利用

unlink漏洞(small bin)

       當堆塊free時,會檢查相鄰的後面的堆塊(地址更小的),或者前面的堆塊(地址更大的),是否空閒,如果空閒,那麼需要進行堆塊合併操作。

  1. 空閒的堆塊一般以雙向鏈表的形式組織(fast bin是單向鏈表,此攻擊不適用),如果剛剛釋放的堆塊要與前面或者後面空閒的堆塊進行合併操作,那麼需要將前或後的堆塊從雙向鏈表中摘下來,合併成更大的堆塊插入到unsort bin鏈表中。空閒堆塊從(small bin)雙向鏈表中摘下來的操作就是unlink。

  2. 這個漏洞能夠寫任何內存,爲什麼呢?

漏洞利用

        首先需要兩個相鄰的堆塊,其中一個堆塊空閒,一個堆塊佔用,釋放佔用的堆塊,引發兩個堆塊合併。正常的空閒堆塊鏈接在空閒鏈表中,我們無法控制其中的fd和bk指針,所以方法是僞造一個空閒的堆塊。libc判斷相鄰堆塊空閒的方法是通過本堆塊的size字段。

        每個堆塊大小是8的倍數,所以size字段最後3位是0,被libc作爲標誌位。其中最後一位如果爲0,說明後面的相鄰的堆塊(地址更小的)是free的,爲1說明正在使用。pre_size字段指明後一個堆塊的起始位置。這兩個字段可以判斷相鄰的後面堆塊:是否分配和堆塊的位置。那麼unlink操作就根據這兩個信息來發生。如果系統還會判斷這個相鄰的堆塊是否在某個未分配的鏈表中,那麼unlink攻擊便實現不了,因爲如果相鄰堆塊在某個空閒鏈表中,那麼如何修改其中的bk和fd指針呢?答案是不可行。所以我們需要一個分配的堆塊,來構造一個free堆塊和一個已分配堆塊。

具體操作

  1.  分配兩個堆塊,但不要過小,大於80字節就好。小於80字節應該是fastbin

  2. 前面分配的堆塊用來僞造需要unlink的空閒堆塊,那麼需要設置堆塊頭和兩個指針fd和bk。

  3. 僞造後面分配的堆塊的頭部,即pre_size和size字段,pre_size是整個堆塊的大小(包含用戶分配的大小和堆塊頭部),那麼pre_size需要設置成前面分配的堆塊的用戶區大小,並且設置size字段最後一位爲0。表示後面的僞造堆塊是空閒的。free第二個分配的堆塊時,系統檢查發現相鄰的後面的僞空閒堆塊是空閒的,那麼需要進行合併。

  4. 僞空閒堆塊需要從空閒鏈表中unlink,實際上這個僞空閒堆塊並不存在於任何空閒鏈表中。unlink之前需要進行一些簡單的檢查,這個檢查是可以欺騙的:首先需要搞清楚"->"操作,"->"操作符左邊的是指針,這個指針存放了某個內存的地址,操作符右邊的是這個指針指向地址的某個偏移位置。合起來就是取指針指向地址的某個偏移處的內存。fd的偏移是3個機器位數,bk的偏移是4個機器位數。即在64位機器上,fd是8*3=24字節,bk是8*4=32字節;32位機器上,fd是4*3=12字節,bk是4*4=16字節。設僞空閒堆塊的堆塊頭指針是p,那麼需要檢查:p->bk->fd==p && p->fd->bk==p

  5. 如何僞造僞空閒堆塊上的fd和bk處的值才能繞過檢查呢?fd = &p - 3*size(int); bk = &p - 2*size(int) 這樣可以保證檢查沒有問題,否則提示double link錯誤。

  6. unlink發生:FD->bk 和 BK->fd是p那個內存,設置成FD後,那麼p = &p - 3*size(int)

    FD = p->fd;
    BK = p->bk;
    FD->bk = BK;
    BK->fd = FD;

        由上圖所示,當ptr[0] = system_addr後,free函數的got表被改寫成system函數的真實地址。只要之後再次調用free函數就會執行system函數,但是system函數需要參數"/bin/sh"才能彈出shell,可以再次申請空間,並且寫入"/bin/sh"字符串,然後free就行了。

    ptr2 = alloc(0x80)

    ptr2 = "/bin/sh"
    
    free(ptr2) //free在got表的地址已經變成了system的地址,而ptr2指向"/bin/sh"字符串

 還有問題是:如何知道system函數的地址?

    上圖中ptr[3]=&free_got後,應該有打印函數可以打印ptr指向的值,即free_got處的值,既然leak到了free在libc上的值,那麼system函數的值可以通過相對地址獲得。

示例程序如下

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

int main(){
	int malloc_size = 0x80;
	uint64_t* ptr0 = (uint64_t*)malloc(malloc_size);
	uint64_t* ptr1 = (uint64_t*)malloc(malloc_size);
	ptr0[2] = (uint64_t)&ptr0 - 3*sizeof(uint64_t);
	ptr0[3] = (uint64_t)&ptr0 - 2*sizeof(uint64_t);

	uint64_t* ptr1_head = (uint64_t)ptr1 - 2*sizeof(uint64_t);
	ptr1_head[0] = malloc_size;
	ptr1_head[1] &= ~1;
	free(ptr1);
	char victim[10] = "hello";
	ptr0[3]=(uint64_t)victim;
	ptr0[0] = 0x4141414141;
	printf("%s\n",victim);
	return 0;

}

    注意這個示例在ubuntu16.04 64位上通過,在ubuntu18.04 64位上測試失敗,應該是libc做了一些防禦。 

參考連接:

  1. http://www.cnblogs.com/shangye/tag/ctf/
  2. https://github.com/shellphish/how2heap/blob/master/glibc_2.26/unsafe_unlink.c            
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章