又到了研究漏洞的时候了
先前有探讨过kernel
中的同步机制,一个临界区
的访问应当是被保护的,但是如果没有考虑到这一层因素的话,会发生什么呢?自然是会产生竞争并引发超乎设想以外的情况发生,以一个用户态程序的代码为例子:
int count = 0;
void *IncreaseCount(void *args)
{
count += 1;
printf("count1 = %d\n", count);
printf("count2 = %d\n", count);
}
int main(int argc, char *argv[])
{
pthread_t p;
printf("start:\n");
for ( int i = 0; i < 10; i ++ ) {
pthread_create(&p, NULL, IncreaseCount, NULL);
}
sleep(30);
return 0;
}
一个很简单的逻辑,但是其输出却比不如我所想:
理想 | 现实 |
---|---|
start: count1 = 1 count2 = 1 count1 = 2 count2 = 2 count1 = 3 count2 = 3 count1 = 4 count2 = 4 count1 = 5 count2 = 5 count1 = 6 count2 = 6 count1 = 7 count2 = 7 count1 = 8 count2 = 8 count1 = 9 count2 = 9 count1 = 10 count2 = 10 |
start: count1 = 1 count2 = 1 count1 = 2 count2 = 3 count1 = 3 count1 = 4 count2 = 4 count2 = 4 count1 = 5 count2 = 5 count1 = 6 count2 = 6 count1 = 7 count2 = 8 count1 = 8 count2 = 9 count1 = 9 count2 = 9 count1 = 10 count2 = 10 |
并且真实的输出并非一定,但是可以清晰的看到count2 = 2
消失了,可以这样理解,就是当前一个线程的count2
去取count
的值的时候,正好被另外线程的count += 1
给刷成了新的数字,可以通过加入sleep
来放大这种影响:
printf("count1 = %d\n", count);
sleep(2);
printf("count2 = %d\n", count);
这样执行后就能发现所有的count2 = 10
,这是因为当count2
去访问count
的时候已经被最后一个线程的count += 1
刷成了10
。
简单来说就是一个临界资源
在程序逻辑执行间隙中间倘若被恶意篡改的话,那篡改者就很有可能对程序造成影响甚至是完成恶意利用,这个换个形容就是Double Fetch
,那转换到kernel
中的话这个double fetch
情况会如何呢?
在正常情况下,内核态
去处理用户态
程序的数据时候往往是用的类似于copy_from_user
将数据拷贝到内核缓冲区
中,后续使用到的数据都是这部分数据,因为处于内核态
因此基本没有什么风险,但是存在特殊情况在于当要处理的数据十分的动态或者复杂的时候,程序逻辑往往会采用引用指针的方式在内核态直接去访问到用户态的数据,那当出现double fetch
漏洞的时候就会对整个内核产生影响。例如造成数据越界,缓冲区溢出等各种问题。当然还有情况是内核本身的逻辑会多次从用户态取数据,并且中间存在可利用的间隙,这也是一种Double Fetch
情况。
这个情况其实非常的少,因为现代内核往往是开启了
SMAP/SMEP
来防止内核访问或执行内核态的数据和代码
依旧是照例子找出曾经出现过的例子来作demo
-- CVE-2016-6516
,为了这个我自己还重装了一个4.5.1
的内核。
这个漏洞出现在ioctl()
系统调用里,换算到代码上来说就从fs/ioctl.c#L579
的地方说起
if (get_user(count, &argp->dest_count)) {
ret = -EFAULT;
goto out;
}
利用get_user()
将用户空间中dest_count
的值赋值到内核空间的count
,这个值接下来被用作设置成员指示器
以便计算偏移
size = offsetof(struct file_dedupe_range __user, info[count]); //计算info[count]在file_dedupe_range __user里面的偏移量
分配size
大小的内核内存并从用户空间拷贝size
大小的数据
same = memdup_user(argp, size);
而接下来的函的取值对象则是这个same
的内存区域
ret = vfs_dedupe_file_range(file, same);
进入到vfs_dedupe_file_range
中可以直接看到dest_count
再一次被取值并用作后续使用当中
u16 count = same->dest_count;
这儿就出现了一个问题,就是count
的来源第一次来源于用户内存
,而第二次则来源于拷贝自用户内存
的内核内存
中,程序设计的本意在于这两个值虽然取的地方不同但是应该完全相同,然而问题在于用户内存
在相当程度上来说都是可控的,即倘若在调用memdup_user
以前就针篡改了dest_count
的值的话会造成什么影响呢?
问题需要一个一个地解决掉
上面的代码是ioctl_file_dedepe_range()
函数的逻辑,其主要功能在于合并映射多个文件中相同的部分来节省物理内存,其对应的实际功能是ioctl_fideduperange
,简单来说就是多个文件共享一份数据。
#include <sys/ioctl.h>
#include <linux/fs.h>
int ioctl(int src_fd, FIDEDUPERANGE, struct file_dedupe_range *arg);
src_fd
是源文件,而file_dedupe_range
则代表了要共享的数据
struct file_dedupe_range {
__u64 src_offset;
__u64 src_length;
__u16 dest_count;
__u16 reserved1;
__u32 reserved2;
struct file_dedupe_range_info info[0];
};
其中的dest_count
就是漏洞的利用点也是用户可控的位置
逻辑中并没有类似cond_resched
这种退让函数,因此在用户态
下利用只能强插入,采用竞争的方式修改掉这部分数据,原理就是在相同的进程下启动两个线程,一个负责正常的逻辑调用另一个则负责篡改数据,而成功的关键在于篡改数据的时机正好落在race
里。而这点可以利用flag
预先设置两个线程的启动时间,然后再篡改线程中加入时间控制,一点点延缓执行时间直到落入race
。
给个demo,通过双向循环等待来控制两个线程的执行:
int finish = 0;
int main_flag = 0;
int exp_flag = 0;
int time = 0;
void exp_thread()
{
while(!flinsh) {
exp_flag = 1;
while(!main_flag) {};
usleep(time);
time ++;
......
exp_flag = 0;
}
}
void main()
{
pthread_create(p1, NULL, exp_thread, NULL);
for ( i = 0; i < try; i ++) {
while(!exp_flag) {}
main_flag = 1;
......
main_flag = 0;
}
finish = 1;
pthread_join(p1, NULL);
return;
}
- kernel_Double_Fetch详解
- Double Fetch
- 以CVE-2016-6516为例深入分析内核Double Fetch型漏洞利用方法
- Linux Kernel API
- ioctl_fideduperange(2) — Linux manual page