将一个指针 free 两次之后会发生什么?
0x00 簡介
在入門 c 語言時我們都知道一個常識:通過 malloc() 動態申請的內存在使用完之后需要通過 free() 釋放;那么如果因為程序設計不當,導致這塊堆內存釋放之后,再釋放一次會發生什么呢?看起來這個操作似乎很愚蠢,但是 double free 的確是現代軟件中十分常見的一種二進制漏洞。
我將通過一個例子來說明 double free 可能造成的危害。這個例子是曾經的一道 0ctf 賽題。ctf 比賽通過簡單演示常見計算機漏洞向參與者普及安全技術,是入門安全的較好方法之一。
環境:ubuntu 16.04 x86_64
工具:ida, pwntools, pwndbg
逆向之后還原的代碼如下,如果不想看全部可以先注意虛線處的 bug(reversed by
@愛發呆的sakura ):#include <stdio.h> #include <stdlib.h> #include <unistd.h>typedef struct note {long int flag;//是否存在筆記long int length;//筆記內容的長度char *content;//筆記內容 } note; typedef struct notes {long int max;long int length;note notes256[256]; } notes;notes *base;void allocate_space() {base = (notes *) malloc(sizeof(notes));for (int i = 0; i < 256; i++) {base->notes256[i].flag = 0;base->notes256[i].length = 0;base->notes256[i].content = NULL;} }int read_choice() {int choice;puts("== 0ops Free Note ==");puts("1. List Note");puts("2. New Note");puts("3. Edit Note");puts("4. Delete Note");puts("5. Exit");puts("====================");printf("Your choice: ");scanf("%d", &choice);return choice; }void list() {for (int i = 0;; i++) {if (i >= 256) {break;}if (base->notes256[i].flag == 1) {printf("%d. %s\n", i, base->notes256[i].content);}} }void read_content(char *temp, int str_len) {int i;int read_num;for (i = 0; i < str_len; i += read_num) {read_num = read(0, (void *) (temp + i), str_len - i);if (read_num <= 0) {break;}} }void new_note() {int str_len;//字符串長度char *temp;void *str;if (base->length < base->max) {for (int i = 0;; i++) {if (i >= base->max) {break;}if (!base->notes256[i].flag) {printf("Length of new note: ");scanf("%d", &str_len);if (str_len > 0) {if (str_len > 4096) {str_len = 4096;}printf("Enter your note: ");temp = (char *) malloc((128 - str_len % 128) % 128 + str_len);read_content(temp, str_len);base->notes256[i].flag = 1;base->notes256[i].length = str_len;base->notes256[i].content = temp;base->length++;puts("Done.");break;}}}} }void edit_note() {printf("Note number: ");int num;scanf("%d",&num);int length;scanf("%d",&length);if(length!=base->notes256[num].length){base->notes256[num].content=realloc(base->notes256[num].content,(128 - length % 128) % 128 + length);base->notes256[num].length=length;}printf("Enter your note: ");read_content(base->notes256[num].content,length);puts("Done."); }void delete_note() {int index;printf("Note number: ");scanf("%d", &index);base->length--;base->notes256[index].flag = 0;base->notes256[index].length = 0;/*------------------------------------------------------------------*/ /*-------------------------- bug is here ---------------------------*/free(base->notes256[index].content);/*-------------------------------------------------------------------*/ /*-------------------------------------------------------------------*/puts("Done."); }int main() {allocate_space();base->max = 256;while (1) {switch (read_choice()) {case 1:list();break;case 2:new_note();break;case 3:edit_note();break;case 4:delete_note();break;case 5:puts("Bye");return 0;default:puts("Invalid!");break;}} }可以看到在
free(base->notes256[index].content);之后該指針并未置空,在隨后的執行流程中可以再次 free 該指針造成 double free 漏洞。如果賦值為 NULL 則不會出現這個問題,釋放空指針是安全的行為。
并且注意到在
base->notes256[i].content固定儲存著 note0 字符串的地址,當然,在利用這個指針的時候還需要知道它的地址。然后通過觸發 double free,更改存 note0 字符串地址的地方,覆蓋 got 表,改變程序執行流程。
下面講具體實現過程。
0x01 Info Leak
根據源代碼可以看出 list 功能中存在一個疏漏可以導致泄漏未初始化的堆中的數據,如果
泄漏地址需要用到兩個 chunk,防止合并需要兩個,所以首先添加 4 個 note。
這時候的堆布局:
將 0 和 2 釋放之后,note0 chunk 中的 BK 將指向 note2 chunk:
這時候添加一個長度小于等于 8 的 note,又將被分配到 note0 的地址,然后在打印其內容的時候將上次 free 后保存的 BK 指針一起打印出來。能這樣做是因為,malloc chunk 是空間復用的,每一個 chunk 都只是一段連續內存,根據不同的情況,一個地址的數據可以被解釋為用戶數據,也可以被解釋為堆指針。將這個泄漏的地址減去 1940h 就得到了 heap base。 知道 heap base 之后就可以計算出 base->notes256[i].content.
泄漏地址之后,就可以釋放所有 chunk。
實現如下:
for i in range(4):newnote('a') delnote(0) delnote(2)newnote('murasaki')s = getnote(0)[8:] heap_addr = u64((s.ljust(8, "\x00"))[:8]) heap_base = heap_addr - 0x1940 print "heap base is at %s" % hex(heap_base)delnote(0) delnote(1) delnote(3)0x02 unlink()
unlink 在空閑 chunk 合并時觸發。在這里,因為不是 fastbin,在 free 時如果前后有空閑 chunk 就會觸發 unlink:更改相鄰 chunk 相關參數、指針,實現向前或者向后合并。我們可以通過觸發 unlink(p, BK, FD), 造成 p = &p - 3,將不可寫的地址轉化為可寫。
為什么呢?我們來回顧一下 unlink(p, BK, FD)的行為:
在二進制可執行文件的層次不存在結構體的概念,只有一段連續的內存,通過偏移量來訪問。所以我們可以布置偽造的 chunk,將我們提供的指針被解釋為 BK 和 FD,最終實現 p = &p - 3,如果還有不懂的可以查找 unlink 有關資料進一步學習。
接下來是最有意思的部分—— free 偽造堆塊實現任意位置讀寫。堆布局的方法比較靈活,只要能成功利用怎么玩都可以。我的思路是這樣的:
實現如下:
經過調試你會發現這個時候就實現了 p = &p - 3,也就是原來儲存 note0 地址的地方變了,現在修改 note0 的用戶數據就是修改 note0 的地址,之后再編輯 note0 就是改變這個地址的內容。
0x03 覆蓋 GOT
因為這個程序的 got 是可寫的,所以在實現任意寫之后可以將 free@got 覆寫成 system() 地址:
現在調用 free() 就是調用 system() :
綜上,將一個指針釋放兩次確實是非常危險的行為,它可以造成任意代碼執行。希望廣大開發者和想從事安全行業的新手們可以從中得到一點點啟發。
0x04 完整 EXP
鏈接:http://pan.baidu.com/s/1jIotmGQ 密碼:pdvw
https://zhuanlan.zhihu.com/p/30513886
總結
以上是生活随笔為你收集整理的将一个指针 free 两次之后会发生什么?的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Double Free浅析(泄露堆地址的
- 下一篇: 来自智能合约中的威胁:去中心化应用安全威