声明: 我nice值至少为0,利用Linux kernel 漏洞不是为了做坏事,在这里分享如何利用Linux kernel漏洞,在整个利用过程中掌握linux kernel的运行机制和原理
本篇为本系列文章第一篇之背景介绍。
一共四大篇,参考国外一个系列博文改编而成(增加了更多的背景知识和O2优化 :-),这第一篇一整篇整下来实在是太长了,本篇先介绍背景。
本文利用的漏洞是 CVE-2017-11176, 此漏洞就是在mq_notify系统调用中在满足一定条件下调用了两次sock_put,从而造成了use-after-free(以下简称UAF).
首先看内核中针对这个漏洞打的patch:
commit: f991af3daabaecff34684fd51fac80319d1baad1
mqueue: fix a use-after-free in sys_mq_notify()
1) The sock refcnt is already released when retry is needed
2) The fd is controllable by user-space because we already
release the file refcnt
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;
这个patch本身非常简单,仅仅只加了一行代码:sock = NULL;
但是这个patch告诉了我们几点信息:
这个漏洞发生在mq_notify系统调用
在retry代码标签中有逻辑错误
由于sock变量的引用计数问题导致了 use-after-free
使用close(fd)可以出现race condition,进而达到攻击的目的,控制系统(看到后面就明白了)
刚开始看到这个patch,肯定会有点懵,接下来慢慢介绍背景知识。
首先介绍一下实验的环境:
由于这个漏洞是在linux 4.11.9上发现的(推荐使用环境在4.x以下),但是我做的实验在linux 5.2.2 ,源代码只改了一行:
ret = netlink_attachskb(sock, nc, &timeo, NULL);
if (ret == 1) {
// sock = NULL;
goto retry;
}
必须运行在"amd64"(x86_64)环境下
使能SMEP
关掉 KASLR 和SMAP
为了实验方便,KASLR(地址空间随机化)必须关闭,SMAP(内核空间不能访问用户空间内存)也必须关闭,启动内核时传递内核参数nosmap nokaslr。smep(内核空间不能执行用户空间的代码)打开,因为利用漏洞的目的就是要绕过这个限制,为了实验更加方便,可以使用虚拟环境,比如qemu, 要打开smep,在启动qemu时,可以加上参数 -cpu kvm64,+smep
qemu-system-x86_64 \
-cpu kvm64,+smep \
-smp cores=1,threads=1 \
-nographic \
-m 512M \
-kernel ./linux-5.2.2/arch/x86/boot/bzImage \
-hda ./ramdisk \
-append "root=/dev/sda rw init=/linuxrc noibrs noibpb nopti nospectre_v2 nospectre_v1 l1tf=off nospec_store_bypass_disable no_stf_barrier mds=off mitigations=off loglevel=8 console=ttyS0"
SMEP和KASLR很好理解,但是如果有SMAP(正常使用的发行版,这个都是打开的),那内核层怎样和用户层交换数据呢?原来copy_from_user和copy_to_user这样的API是和架构相关的,它们的代码放在一个特殊的区域,当访问用户态内存发生page_fault时,会识别出发生在这个区域,从而饶它一命,让它顺利通行。
由于之后会大量使用kprobe,现在着重介绍kprobe的原理:(有的人喜欢使用systemtap工具,但是安装环境太麻烦,舍弃)
kprobe是一个动态收集调试和性能信息的工具,他的基本工作机制是:用户指定一个探测点,并把一个用户定义的处理函数关联到探测点,当内核执行到该探测点时,相应的关联函数被执行,然后继续执行正常的代码路径。
kprobe实现了三种类型的探测点:Kprobe,jprobe和kretprobe(返回探测点)。kprobe是可以被插入到内核中的任何指令位置的探测点(除了一些规定的黑名单函数),jprobe则只能被插入到一个内核函数的入口(jprobe API在2017年被废止了,意味着在linux5.2.2不能使用jprobe),而kretprobe则是在指定的内核函数返回时才被执行。
当安装一个kprobes探测点时,kprobe首先备份被探测的指令,然后使用断点指令(即在i386和x86_64的int3指令)来取代被探测指令的头一个或几个字节。当CPU执行到探测点时,将因运行断点指令而执行trap操作,此时会保存CPU的寄存器,调用相应的trap处理函数,而trap处理函数将调用相应的notifier_call_chain(内核中一种异步工作机制)中注册的所有notifier函数,kprobe正是通过向trap对应的notifier_call_chain注册关联到探测点的处理函数来实现探测处理的。当kprobe注册的notifier被执行时,它首先执行关联到探测点的pre_handler函数,并把相应的kprobe 结构和保存的寄存器作为该函数的参数,接着,kprobe单步执行被探测指令的备份,最后,kprobe执行post_handler。等所有这些运行完毕后,紧跟在被探测指令后的指令流将被正常执行。
jprobe通过注册kprobe在被探测函数入口的来实现,它能无缝地访问被探测函数的参数。jprobe处理函数应当和被探测函数有同样的原型,而且该处理函数在函数末必须调用kprobe提供的函数jprobe_return()。当执行到该探测点时,kprobe备份CPU寄存器和栈的一些部分,然后修改指令寄存器指向jprobe处理函数,当执行该jprobe处理函数时,寄存器和栈内容与执行真正的被探测函数一模一样,因此它不需要任何特别的处理就能访问函数参数, 在该处理函数执行到最后时,它调用jprobe_return(),那导致寄存器和栈恢复到执行探测点时的状态,因此被探测函数能被正常运行。需要注意,被探测函数的参数可能通过栈传递,也可能通过寄存器传递,但是jprobe对于两种情况都能工作,因为它既备份了栈,又备份了寄存器,当然,前提是jprobe处理函数原型必须与被探测函数完全一样。jprobe在2017年被废止,现在可以忘掉它了,但是它的实现原理还是有参考价值,下面的实验中也会利用kprobe达到jprobe相同的效果。
kretprobe也使用了kprobe来实现,当用户调用register_kretprobe()时,kprobe在被探测函数的入口建立了一个探测点,当执行到探测点时,kprobe保存了被探测函数的返回地址并取代返回地址为一个trampoline的地址,kprobe在初始化时定义了该trampoline并且为该trampoline注册了一个kprobe,当被探测函数执行它的返回指令时,控制传递到该trampoline,因此kprobe已经注册的对应于trampoline的处理函数将被执行,而该处理函数会调用用户关联到该kretprobe上的处理函数,处理完毕后,设置指令寄存器指向已经备份的函数返回地址,因而原来的函数返回被正常执行。
这个漏洞还涉及到linux中 Socket, Sock 和 SKB
都知道在linux的世界,万事万物都是一个文件,每一个文件由一个文件描述符来表示,文件描述符中有很多的方法(函数)对应于socket,
// [net/socket.c]
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()系统调用,会在内核中生成对应的struct file结构并绑定上socket_file_ops.
struct file结构中的变量private_data指向struct socket,也就是此文件的私有数据。而socket结构又有一个file指针指向struct file.这种结构之间互相引用的例子在内核中也是非常常见的。
socket结构实现了BSD socket API(connect(), bind(), accept(), listen(). 每一种类型的socket(比如AF_INET,AF_NETLINK)实现它们自己的proto_ops.
// [include/linux/net.h]
struct socket {
struct file *file;
struct sock *sk;
const struct proto_ops *ops;
// ...
};
// [include/linux/net.h]
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);
// ...
}
比如当调用bind这样的api时,
从文件描述符表中获取到struct file结构
从struct file中获得struct socket结构
调用对应proto_ops中的回调函数(比如sock->ops->bind())
由于每一种协议(比如sending/receiving data)可能需要调用到网络栈的更底层,struct socket有一个指针指向struct sock。总的来说,socket就相当于struct file和struct sock之间的纽带。
// [include/linux/net.h]
struct socket {
struct file *file;
struct sock *sk;
const struct proto_ops *ops;
// ...
};
// [net/netlink/af_netlink.c]
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,
// ...
}
Netlink Socket
netlink socket是一种socket(AF_NETLINK),它可以用来进行用户层和内核层的通讯,常见的有通过netlink更改路由表.
// [include/net/netlink_sock.h]
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的一种继承,在linux内核中其实有大量这样的例子,比如常见的struct kobject等。struct sock更像一种抽象类,struct netlink_sock又相当于对 struct sock的实例化。
一张图来汇总:
大部分背景知识介绍就到这里,在下一文中使用kprobe和用户层代码触发这个漏洞。然后在接下面的几篇中会一步步在应用层触发利用这个漏洞达到控制整个系统的目的。
读者如果感兴趣可以直接读一个国外黑客写的四篇系列文章,没有一定耐性,真的会看得死的心都有,我已经死了几遍了:)
https://blog.lexfo.fr/cve-2017-11176-linux-kernel-exploitation-part1.html
觉得好看或者期待后文,可以关注公众号:相遇linux