漏洞简介
CVE-2017-7184是 Kernel版本 < 4.10.6 的 XFRM 模块中存在一段越界写,使得攻击者可以覆盖 cred
中的值从而本地提权。
XFRM
XFRM(transform)是Linux中实现 IPsec
协议的模块
IPsec
IPSec是通过分组加密认证来保护IP协议的一类协议,包含 AH、ESP、IKE协议,提供对数据包的认证和加密功能,能帮助IP层建立安全可信的数据包传输通信:
SA(Security Associstion)
:安全关联,SA由spi、ip、安全协议标识(AH或ESP)这三个参数唯一确定,SA定义了ipsec双方的ip地址、ipsec协议、加密算法、密钥、模式、抗重放窗口等,SA用 xfrm_state
结构体标识
AH(Authentication Header):认证,AH为ip包提供数据完整性校验和身份认证功能,提供抗重放能力,验证算法由SA指定;
ESP(Encapsulating security payload):加密,ESP为ip数据包提供完整性检查,认证和加密
Linux内核的IPSec实现
Linux内核中IPSec实现即是通过xfrm框架,该框架用于内核协议栈收到的IPsec报文需要经过转换才能还原为原始报文),在16.04版本下xfrm代码主要在net/xfrm以及net/ipv4下。下面是主要代码的功能:
- xfrm_state.c 状态管理
- xfrm_policy.c xfrm策略管理
- xfrm_algo.c 算法管理
- xfrm_hash.c 哈希计算函数
- xfrm_input.c 安全路径(sec_path)处理,用于处理进入的ipsec包
- xfrm_user.c netlink接口的SA和SP(安全策略)管理
其中xfrm_user.c
中的代码允许我们想内核发送netlink
消息来调用相关handler
实现对SA和SP的配置,其中涉及处理函数如下:
1 | xfrm_dispatch[XFRM_NR_MSGTYPES] = { |
其中,需要重点关注的几个函数功能如下:
- xfrm_add_sa:创建一个新的SA,并可以指定相关attr,在内核中,使用一个xfrm_state结构来表示一个SA
- xfrm_del_sa:删除一个SA,也即删除一个指定的xfrm_state
- xfrm_new_sa:根据传入参数,更新指定xfrm_state结构的内容
- xfrm_get_ae:根据传入的参数,查询指定xfrm_state结构中的内容(包括attr)
Netlink简介
Netlink是Linux提供的用于内核和用户态进程之间的通信方式,也能用于用户空间的两个进程通信。一般用户空间和内核空间通信有三种方式:proc、ioctl和 Netlink。而前两种都是单向的,但是 Netlink可以实现双工通信。
Netlink是一种特殊的socket,其是Linux所特有的,类似于BSD中的 AF_ROUTE但又远比他的功能强大,目前在Linux内核中使用netlink进行应用与内核通信的应用很多。包括:路由daemon()
Netlink
用户态使用的标准socket API
有 sendto\ recvfrom\ sendmsg\ recvmsg
四种。
用户态数据结构
Netlink通信跟常用的UDP Socket通信类似:
srtuct sockaddr_nl
是 netlink
通信地址跟普通 socket struct sockaddr_in
类似
struct sockaddr_nl:
1 | struct sockaddr_nl { |
struct nlmsghd:
1 | /* struct nlmsghd 是netlink消息头*/ |
nlmsg_len:
整个netlink
消息的长度(包含消息头);nlmsg_type
:消息状态,定义了4种通用的消息类型:
nlmsg_flags
:消息标记,用以表示消息的类型:
nlmsg_seq
:消息序列号,用以将消息排队,类似 TCP协议种的序号,但是 Netlink
的这个字段是可选的,不强制使用
nlmsg_pid
:发送端口的ID号,对于内核该值就是0,对于用户进程来说就是其socket所绑定的 ID 号。
struct msghdr:
1 | struct iovec { /* Scatter/gather array items */ |
漏洞分析
xfrm_state结构体
1 | /* Full description of state of transformer. */ |
其中 xfrm_id
如下,用于标识一个 SA身份,包含 daddr、spi、proto三个参数:
1 | struct xfrm_id { |
xfrm_replay_state_esn
结构体(replay_esn
和 preplay_esn
),bmp
是一个变长的内存区域,是一块bitmap
,用于标识数据包的seq
是否被重放过,其中 bmp_len
表示变长结构体的大小,replay_window
用于 seq
索引的模数,即索引的范围,此结构体在创建 xfrm_state
结构体时根据用户输入参数动态被创建,而程序漏洞存在于对这个结构体的读写过程中。bmp_len
决定整个结构体的具体大小,而 replay_window
则决定了bmp
数组的索引范围:
1 | struct xfrm_replay_state_esn { |
xfrm_add_sa
xfrm_replay_state_esn
结构体由 xfrm_add_sa
函数初始化,具体调用顺序为 xfrm_add_sa()->xfrm_state_construct()->xfrm_alloc_replay_state_esn()/xfrm_update_ae_params()
,除此之外 xfrm_new_ae
函数也会调用 xfrm_update_ae_params()
来更新 xfrm_replay_state_esn
。xfrm_add_sa
函数如下:
1 | static int xfrm_add_sa(struct sk_buff *skb, struct nlmsghdr *nlh, |
verify_newsa_info
函数如下,我们重点关注 verify_replay
函数会对 xfrm_replay_state_esn
结构参数进行检查
1 | static int verify_newsa_info(struct xfrm_usersa_info *p, |
verify_replay
主要对 xfrm_replay_state_esn
进行三项检查:
1 | static inline int verify_replay(struct xfrm_usersa_info *p, |
xfrm_state_construct
会根据用户输入对结构体进行构造:
1 | static struct xfrm_state *xfrm_state_construct(struct net *net, |
而 xfrm_alloc_replay_state_esn()
函数主要是通过 kzalloc
函数分别申请了两块同样大小的内存 replay_esn
和 preplay_esn
,大小为 sizeof(*replay_esn)+replay_esn->bmp_len*sizeof(__u32)
,并将用户数据中 attr[XFRMA_REPLAY_ESN_VAL]
内容复制过去。
1 | static int xfrm_alloc_replay_state_esn(struct xfrm_replay_state_esn **replay_esn, |
在 xfrm_init_replay()
函数中会对申请的结构体 xfrm_state
进行检查,replay_window
不大于定义的 bmp_len
大小,并对 x->repl
进行初始化,该成员是一个函数虚表,作用是在收到 AH
或 ESP
协议数据包时进行数据重放检查
1 | int xfrm_init_replay(struct xfrm_state *x) |
总结
xfrm_add_sa
函数参数:skb
是用户传入的socket buffer
,nlh
是netlink
通信的消息头,attrs
是nlatter
类型,包含nla_len
和nla_type
。首先
xfrm_add_sa
调用verify_newsa_info
函数检查用户输入p
与attrs
:verify_newsa_info
主要检查用户输入的协议族和id
是否正确,随后调用函数 对attrs
中的类型和长度进行检查,最后重点关注 调用了verify_replay
对xfrm_replay_state_esn
结构参数进行了检查verify_replay
主要对xfrm_replay_state_esn
进行了三项检查,一是bmp
的长度bmp_len
是否超过界限,一是 参数长度nla_len
是否超过界限,其次是协议是否为 ESP或 AH ,*未对 replay_window进行检查 *
经过第一步的检查后,
xfrm_add_sa
会调用xfrm_state_construct
对 用户输入的 数据进行重构:- 首先 调用
xfrm_state_alloc
分配xfrm_state
结构,随后调用copy_from_user_state
将 用户数据拷贝到xfrm_state
结构,随后就是将attrs
中的数据类型依次赋给xfrm_state
结构; - 调用
xfrm_mark_get
将attrs
标志位赋给新结构; - 调用
__xfrm_init_state
初始化xfrm_state
- 如果设置了
XFRMA_SEC_CTX
,则调用securiy_xfrm_state_alloc
分配nla_data
- 随后调用
xfrm_alloc_replay_state_esn
函数 会调用kzalloc
重新 分配replay_esn
和preplay_esn
结构体,并重新赋值 - 调用
xfrm_ini_replay
初始化xfrm_state
结构体,检查了 replay_window小于 bmap_len - 最后调用
xfrm_update_ae_params
更新xfrm_state
内容,这里更新时的size
都是 由bmp_len
指定
- 首先 调用
最后完成了 用户输入到
SA
结构xfrm_state
的重构
xfrm_replay_state_esn结构体更新
xfrm_new_ae
函数,作用是修改 replay_esn
成员,即 xfrm_alloc_replay_state_esn
申请的第一个内存块:
1 | static int xfrm_new_ae(struct sk_buff *skb, struct nlmsghdr *nlh, |
xfrm_update_ae_params
函数主要利用 memcpy
函数对 xfrm_state
结构进行更改,其中 计算size
为 xfrm_replay_state_esn
函数如下所示。
1 | /* |
1 | static inline int xfrm_replay_state_esn_len(struct xfrm_replay_state_esn *replay_esn) |
总结
xfrm_new_ae函数先调用
xfrm_mark_get
获得mark随后调用
xfrm_state_lookup
查找hash表,获得需要更新的 xfrm_data结构体随后调用
xfrm_replay_verify_len
对xfrm_data
进行检查:- 主要检查了修改部分的bmp_len长度,该检查是因为replay_esn成员内存是直接进行复制的,不再二次分配。但缺少了对replay_window变量的检测,导致引用replay_window变量进行bitmap读写时造成的数组越界问题
然后调用了
xfrm_update_ae_params
对xfrm_data
进行了更新,这里 可以对 replay_window进行修改,未检查replay_window
。
注意:从 xfrm_data
的生成和更新中,可以看到 对于 replay_window
的检查都是比较宽松,replay_window
表示 bmap
数组的 大小,如果将 replay_window
改大就可以获得 数组越界漏洞
数组越界构造
首先,能够对 xfrm_replay_state_esn
操作的结构体如下,该结构用于重放检测,定义了如下的几个函数:
1 | static struct xfrm_replay xfrm_replay_esn = { |
首先关注 xfrm_replay_advance_esn
函数
1 | static void xfrm_replay_advance_esn(struct xfrm_state *x, __be32 net_seq) |
xfrm_replay_check_esn
函数
1 | static int xfrm_replay_check_esn(struct xfrm_state *x, |
接着,分析 xfrm_replay_esn
结构体如何调用,可以找到在xfrm_init_replay
函数中,会将 xfrm_replay_esn
地址赋值给 x->repl
成员,进而查找 x->repl
在何处调用会调用 advance
和 check
成员函数,最终在 xfrm_input
中可以看到调用了 x->repl->advance(x, seq)
。现在找到一处可以调用 越界数组读写的函数。
但是,还得找到我们能够在用户态就能控制的函数,而 xfrm_input
我们不能直接控制,所以转而继续查找 xfrm_input
的引用函数,最终找到如下调用链:
- xfrm4_rcv_spi 调用
xfrm_input
- xfrm4_rcv 调用 xfrm4_rcv_spi
- xfrm4_ah_rvc调用 xfrm4_rcv
最终可以追溯到 AH
协议的内核协议栈中:
1 | static const struct net_protocol ah4_protocol = { |
也即,可以通过发送 AH
数据包来触发越界读写。
在 xfrm_input
函数如下,
1 | int xfrm_input(struct sk_buff *skb, int nexthdr, __be32 spi, int encap_type) |
总结: 通过用户态空间发送一个 AH
数据包将导致,一个 bit
的内存写,或者一段空间的置0.
漏洞触发
首先需要了解 netlink
与 xfrm
通信的数据包格式,如下:
1 | /* ======================================================================== |
上图中,可以看到发送到内核的数据需要如下形式:
nlmsghdr + Family Header + n * (nla + data)
首先从 xfrm_netlink_rcv
函数会调用 sock_net
获得传入的网络信息,随后调用 netlink_rcv_skb
函数,会检查 nlmsg_type
及 nlmsg_len
范围,并交由 cb
函数处理,其赋值为 xfrm_user_rcv_msg
:
1 | static void xfrm_netlink_rcv(struct sk_buff *skb) |
1 | int netlink_rcv_skb(struct sk_buff *skb, int (*cb)(struct sk_buff *, |
1 | static int xfrm_user_rcv_msg(struct sk_buff *skb, struct nlmsghdr *nlh) |
其中,xfrm_dispatch
结构如下,里面有我们需要的函数,我们只需要将 nlmsg_type
设置为相应的值即可
1 | xfrm_dispatch[XFRM_NR_MSGTYPES] = { |
而 Family Header
需要到对应的处理函数中寻找,以 xfrm_add_sa
为例,其调用 nlmsg_data
函数的赋值变量类型为 xfrm_usersa_info
,即为 Family Header
1 | struct xfrm_usersa_info *p = nlmsg_data(nlh); |
总结
这里解释一下 EXP
中比较关心的几个问题:
- 如何控制
xfrm_replay_state_esn->bmp
的大小
我们必须精确控制 bmp
的大小,才能为其布置堆布局。这里需要利用上述的数据包格式,先构造 xfrm_replay_state_esn
中的 replay_window
和 bmp_len
,其中 bmp_len
确定了 申请的大小;
- 如何修改
replay_window
和seq
等值
直接修改 xfrm_replay_state_esn_rs
中的 replay_window
、bmp_len
和 seq_hi
以及 seq
等值。
漏洞利用
权限限制
在 xfrm_user_rcv_msg
函数中,会对调用权限进行检查,如下所示:
1 | netlink_net_capable(skb, CAP_NET_ADMIN) |
其所需权限为 CAP_NET_ADMIN
权限。在 Linux
系统中存在命名空间的权限隔离机制,在每一个 NET
沙箱中,非 ROOT
进程可以具有 CAP_NET_ADMIN
权限。可以通过如下命令查看命名空间是否开启,若为 y
,则启用了命名空间。
1 | cat /boot/config* | grep COFNIG_USER_NS |
namespace:是Linux内核用来隔离内核资源的方式,通过 namespace
可以让一些进程只能看到与自己相关的一部分资源,而另外一些进程也只能看到与其自己相关的资源,这两个进程资源互不干扰。具体的实现方式是把一个或多个进程的相关资源指定在同一个 namespace
中。
绕过限制的两种方法:一是使用 setcap
命令为 EXP
赋予权限,即执行 sudo setcap cap_net_raw, cap_net_admin=eip ./exp
;二是设置 namespace sandbox
。
注意:而在 ubuntu等发行版中,User namespace是默认开启的,非特权用户可以创建用户命名空间、网络命名空间,在命名空间内部,即可触发漏洞
利用方法
覆写 cred 结构体
第一种提权方法是覆写 cred
结构体。首先 xfrm_replay_state_esn
是一个变长的数据结构,其长度是可以由用户输入的 bmp_len
来指定,并在 xfrm_alloc_replay_state_esn
函数 中通过 kzalloc
申请 bmp_len*4+0x18
大小。
其中,可以调用 xfrm_replay_advance_esn
函数中的越界写,将紧邻着 bmp
的一片区域写为0.
1 | nr = (replay_esn->replay_window - 1) >> 5 |
cred结构体
每一个线程都有自己的线程栈、和一个自己的线程结构体 thread_info
,其中有一个 task_struct
结构体,里面有一个 cred
结构体指定了当前线程的权限。只要能够将 cred
结构中 uid~fsgid
全部覆写为0就可以把这个线程权限提升为 root(root uid 为0)
。
而 cred
结构体是在线程初始化时由 prepare_creds
函数创建,其创建方法是 kmem_cache_alloc
,取出的大小由 cred_jar
所在的 kmem_cache
决定。
1 | new = kmem_cache_alloc(cred_jar, GFP_KERNEL); |
kzalloc
而 kzalloc
函数 与 kmalloc
函数十分相似,其只会对 kmalloc
申请的堆块进行清空操作:
1 | /** |
而 kmalloc
是内核中最常用的一种内存分配方式,通过调用 kmem_cache_alloc
函数来实现。
而 kmem_cache_alloc
是基于 slab
分配器的一种内存分配方式,适用于反复分配释放同一大小内存块的场合,首先用 kmem_cache_create
创建一个高速缓存区域,然后用 kmem_cache_alloc
从高速缓存区域中获取新的内存块。这里 kmem_cache_alloc
会从指定的 kmem_cache
中取,而 cred
就是从 名称为cred_jar
的 kmem_cache
中取出。
而 kmalloc
是会根据 size
获取相应的 kmem_cache
再取出。
本来,kmem_cache_alloc(cred_jar, GFP_KERNEL)
是从 cred_jar
所在 kmem_cache
取出,而 分配 bmp
是由 kmalloc
根据size
取。本应该不会在同一条链上,而这里经过调试发现 cred_jar
就是 kmalloc-192
。
所以:这里能够采用覆写 cred
结构体的方法
对于同一个 kmem_cache
分配出来的内存块有一定概率是相邻的,因为slub
初始化时是直接从伙伴算法中分配地址相邻的 page
到同一个 kmem_cache
中,尽管后续因为分配释放,导致同一个 kmem_cache
中不一定全部相邻,但是有一定的机率分配到相邻的两个堆块。
那么当通过 设置 xfrm_replay_state_esn
结构体设置为 192
以内,就会从 kmalloc-192
中分配出来,并利用 fork
新建大量进程,使申请大量 cred
,这样喷射之后有很大概率使得 xfrm_replay_state_esn
与 cred
相邻。然后就可以利用 越界读写漏洞 将相邻的 cred
置零,这样就会导致某个进程能够提权,并通过 反弹shell
得到一个 root
权限的 shell
。
总体思路即为:
创建 xfrm_replay_state_esn:
调用 xfrm_add_sa
函数,指定 bmp
大小为 192
,并设置好对应 参数。
修改xfrm_replay_state_esn
调用 xfrm_new_ae
对 xfrm_replay_state_esn
中的 seq
,seq_hi
,replay_window
进行设定,replay_window
为即将要置0的长度大小。由于连续申请了两次 xfrm_replay_state_esn
结构体,所以这里需要将 replay_window
设置较大,至少能够越过 相邻的 第2个 xfrm_replay_state_esn
结构体覆写 cred
。而 seq_hi
和 seq
两个数据需要结合之后发送的 ah
数据包中的 seq
参数,引导 xfrm_replay_advance_esn
到达 bmp[0]~bmp[n]
这个分支
AH数据包
AH
数据包的要求即 spi
需要和之前申请的 SA
和 spi
相同用于寻找 xfrm_state
,并且需要满足:
diff >= replay_esn->replay_window
,其中 diff
的数据由 xfrm_repaly_state_esn
中的 seq
、seq_hi
及 AH
的 seq
共同决定,还需再后续单字节写的位置,将 cred
结构体中的 usage
置回原值。
漏洞调试
环境搭建
环境搭建太心累了,前前后后拖了5天,才把内核编译好,主要参考这篇文章:
https://blog.csdn.net/m0_37329910/article/details/97620934
但是就算编译好后,才发现编译的内核没有开启 xfrm
功能,应该是编译前配置文件没有选对。
然后,转而投向双机调试。这个网上现成教程比较多,可以参考这篇文章。但,唯一问题就是特别慢,连接上到能够开始调试大概得几分钟吧。心累~
EXP
1 |
|
总结
第一次接触Linux kernel
的真实漏洞调试,对这个模块也不是很熟悉,对于代码流程也只是懂得了漏洞触发的部分,这里是需要花更多的时间来学习的,希望后续能够再进一步研究吧。
参考
【CVE-2017-7184】Linux xfrm模块越界读写提权漏洞分析
- 本文作者: A1ex
- 本文链接: http://yoursite.com/2021/04/08/CVE-2017-7184-Linux-xfrm模块越界读写提权漏洞分析/
- 版权声明: 本博客所有文章除特别声明外,均采用 MIT 许可协议。转载请注明出处!