unlink漏洞(small bin)
當堆塊free時,會檢查相鄰的後面的堆塊(地址更小的),或者前面的堆塊(地址更大的),是否空閒,如果空閒,那麼需要進行堆塊合併操作。
-
空閒的堆塊一般以雙向鏈表的形式組織(fast bin是單向鏈表,此攻擊不適用),如果剛剛釋放的堆塊要與前面或者後面空閒的堆塊進行合併操作,那麼需要將前或後的堆塊從雙向鏈表中摘下來,合併成更大的堆塊插入到unsort bin鏈表中。空閒堆塊從(small bin)雙向鏈表中摘下來的操作就是unlink。
-
這個漏洞能夠寫任何內存,爲什麼呢?
漏洞利用
首先需要兩個相鄰的堆塊,其中一個堆塊空閒,一個堆塊佔用,釋放佔用的堆塊,引發兩個堆塊合併。正常的空閒堆塊鏈接在空閒鏈表中,我們無法控制其中的fd和bk指針,所以方法是僞造一個空閒的堆塊。libc判斷相鄰堆塊空閒的方法是通過本堆塊的size字段。
每個堆塊大小是8的倍數,所以size字段最後3位是0,被libc作爲標誌位。其中最後一位如果爲0,說明後面的相鄰的堆塊(地址更小的)是free的,爲1說明正在使用。pre_size字段指明後一個堆塊的起始位置。這兩個字段可以判斷相鄰的後面堆塊:是否分配和堆塊的位置。那麼unlink操作就根據這兩個信息來發生。如果系統還會判斷這個相鄰的堆塊是否在某個未分配的鏈表中,那麼unlink攻擊便實現不了,因爲如果相鄰堆塊在某個空閒鏈表中,那麼如何修改其中的bk和fd指針呢?答案是不可行。所以我們需要一個分配的堆塊,來構造一個free堆塊和一個已分配堆塊。
具體操作
-
分配兩個堆塊,但不要過小,大於80字節就好。小於80字節應該是fastbin
-
前面分配的堆塊用來僞造需要unlink的空閒堆塊,那麼需要設置堆塊頭和兩個指針fd和bk。
-
僞造後面分配的堆塊的頭部,即pre_size和size字段,pre_size是整個堆塊的大小(包含用戶分配的大小和堆塊頭部),那麼pre_size需要設置成前面分配的堆塊的用戶區大小,並且設置size字段最後一位爲0。表示後面的僞造堆塊是空閒的。free第二個分配的堆塊時,系統檢查發現相鄰的後面的僞空閒堆塊是空閒的,那麼需要進行合併。
-
僞空閒堆塊需要從空閒鏈表中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
-
如何僞造僞空閒堆塊上的fd和bk處的值才能繞過檢查呢?fd = &p - 3*size(int); bk = &p - 2*size(int) 這樣可以保證檢查沒有問題,否則提示double link錯誤。
-
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做了一些防禦。
參考連接: