文章首发于安全客:CVE-2019-15666 xfrm UAF 8字节写NULL提权分析
CVE-2019-15666是个被低估的漏洞,NVD给的评分只有4.4分。但是2020年5月份,据外媒报道,英国、德国、瑞士和西班牙等国超级计算机中心纷纷报告被加密货币恶意软件感染,导致多个高性能计算集群关闭。根据英国网络安全公司Cado Secutiry调查分析,黑客很有可能是通过利用非法得来的SSH凭证获得了超级计算机集群的权限,然后利用CVE-2019-15666漏洞进行了提权,然后部署了恶意挖矿程序。这次受影响的超级计算机有不少都是用于新冠肺炎研究的,因此这个时候尝试利用这些超级计算机来挖矿真的是非常不道德的。
影响版本:Linux v5.0.19 以前。v5.0.19已修补,v5.0.18未修补。 4.4分。
测试版本:Ubuntu 18.04(4.15.0-20-generic #21) exploit及测试环境下载地址—https://github.com/bsauce/kernel-exploit-factory
编译选项: CONFIG_USER_NS=y CONFIG_SLAB=y 所有含NETLINK
和XFRM
的配置全部选上。
General setup
---> Choose SLAB allocator (SLUB (Unqueued Allocator))
---> SLAB
在编译时将.config
中的CONFIG_E1000
和CONFIG_E1000E
,变更为=y。参考
CONFIG_USER_NS=y
CONFIG_USERFAULTFD=y
CONFIG_DEBUG_INFO=y
// debug on running kernel with gdb
CONFIG_GDB_SCRIPTS=y
CONFIG_FRAME_POINTER=y
CONFIG_KGDB=y
CONFIG_KGDB_SERIAL_CONSOLE=y
CONFIG_KDB_KEYBOARD=y
漏洞描述:net/xfrm/xfrm_user.c
中的 verify_newpolicy_info() 错误处理了dir
验证,导致__xfrm_policy_unlink()中出现越界访问。dir的范围本来是0~2,但从index计算dir时错误(policy2),导致dir超过3,后续将所有policy重新加入bydst
list时漏掉该policy2,释放bydst
后但该policy2上的pprev
指针仍指向释放后的policy1,将该policy2进行unlink时会导致空闲块8字节写null(*policy1->next = 0
),见[12]
。
如果不构造policy1,直接对policy2进行unlink,只会导致越界减1,见[8]
。
补丁:patch 这里检查过后,后续还会通过p->index
来计算p->dir
(p->dir = xfrm_policy_id2dir(p->index)
-> p->dir = p->index & 7
),p->dir
前后不一致,导致未正确处理。
diff --git a/net/xfrm/xfrm_user.c b/net/xfrm/xfrm_user.c
index a131f9ff979e1..8d4d52fd457b2 100644
--- a/net/xfrm/xfrm_user.c
+++ b/net/xfrm/xfrm_user.c
@@ -1424,7 +1424,7 @@ static int verify_newpolicy_info(struct xfrm_userpolicy_info *p)
ret = verify_policy_dir(p->dir);
if (ret)
return ret;
- if (p->index && ((p->index & XFRM_POLICY_MAX) != p->dir)) // p->index=4, p->dir=0即可绕过该检查。
+ if (p->index && (xfrm_policy_id2dir(p->index) != p->dir))
return -EINVAL;
return 0;
保护机制:开启KASLR/SMEP/SMAP。
利用总结:漏洞原本是个越界减1,可以利用别的路径来构造UAF,可以往空闲块上的8字节写null。漏洞对象xfrm_policy
位于kmalloc-1024
,cred结构位于kmalloc-192
。首先利用setxattr+userfaultfd
在policy0
周围都喷射kmalloc-1024
堆块,释放policy0
后同时释放喷射块,促使该slab释放后被c子进程的red复用,然后触发UAF 空闲块8字节NULL写来修改cred中的gid/suid
,再将当前用户添加到sudoers,即可提权。
1. 漏洞分析
1.1 越界访问漏洞
创建xfrm_policy
:用户采用特定的index和timer set来创建policy,参数是XFRM_MSG_NEWSA
。
漏洞链:xfrm_add_policy() -> verify_newpolicy_info()
xfrm_policy
创建链:xfrm_add_policy() -> xfrm_policy_construct() -> xfrm_policy_alloc()
static int xfrm_add_policy(struct sk_buff *skb, struct nlmsghdr *nlh,
struct nlattr **attrs)
{
struct net *net = sock_net(skb->sk);
struct xfrm_userpolicy_info *p = nlmsg_data(nlh);
struct xfrm_policy *xp;
struct km_event c;
int err;
int excl;
err = verify_newpolicy_info(p); // [1] 检查用户参数—xfrm_userpolicy_info结构
if (err)
return err;
err = verify_sec_ctx_len(attrs);
if (err)
return err;
xp = xfrm_policy_construct(net, p, attrs, &err);
if (!xp)
return err;
/* shouldn't excl be based on nlh flags??
* Aha! this is anti-netlink really i.e more pfkey derived
* in netlink excl is a flag and you wouldnt need
* a type XFRM_MSG_UPDPOLICY - JHS */
excl = nlh->nlmsg_type == XFRM_MSG_NEWPOLICY;
err = xfrm_policy_insert(p->dir, xp, excl); // [2] 检查通过则将新的policy对象插入到全局policy list中
xfrm_audit_policy_add(xp, err ? 0 : 1, true);
... ...
return 0;
}
// [1] verify_newpolicy_info() 参数检查
static int verify_newpolicy_info(struct xfrm_userpolicy_info *p)
{
... ...
ret = verify_policy_dir(p->dir); // [3] 检查 p->dir,保证其值只能是0,1,2
if (ret)
return ret;
if (p->index && ((p->index & XFRM_POLICY_MAX) != p->dir)) // [4] XFRM_POLICY_MAX=3,设置p->index=4 p->dir=0即可绕过本限制
return -EINVAL;
return 0;
}
如果policy插入时包含timer set,则触发OOB后、超时后会执行以下timer函数:
xfrm_policy_timer() -> xfrm_policy_delete() -> __xfrm_policy_unlink()
static void xfrm_policy_timer(struct timer_list *t)
{
struct xfrm_policy *xp = from_timer(xp, t, timer);
time64_t now = ktime_get_real_seconds();
time64_t next = TIME64_MAX;
int warn = 0;
int dir;
read_lock(&xp->lock);
if (unlikely(xp->walk.dead))
goto out;
dir = xfrm_policy_id2dir(xp->index); // [5] 根据用户提供的index重新计算: return index & 7 -> dir=4&7=4 dir之前是用 XFRM_POLICY_MAX 来限制的,本应该小于3,现在却用7来计算,导致取值超过3。
... ...
expired:
read_unlock(&xp->lock);
if (!xfrm_policy_delete(xp, dir)) // [6]
km_policy_expired(xp, dir, 1, 0);
xfrm_pol_put(xp);
}
// [6] xfrm_policy_delete()
int xfrm_policy_delete(struct xfrm_policy *pol, int dir)
{
struct net *net = xp_net(pol);
spin_lock_bh(&net->xfrm.xfrm_policy_lock);
pol = __xfrm_policy_unlink(pol, dir); // [7]
spin_unlock_bh(&net->xfrm.xfrm_policy_lock);
if (pol) {
xfrm_policy_kill(pol);
return 0;
}
return -ENOENT;
}
EXPORT_SYMBOL(xfrm_policy_delete);
// [7] __xfrm_policy_unlink()
static struct xfrm_policy *__xfrm_policy_unlink(struct xfrm_policy *pol,
int dir)
{
struct net *net = xp_net(pol);
if (list_empty(&pol->walk.all))
return NULL;
/* Socket policies are not hashed. */
if (!hlist_unhashed(&pol->bydst)) {
hlist_del_rcu(&pol->bydst);
hlist_del_init(&pol->bydst_inexact_list);
hlist_del(&pol->byidx);
}
list_del_init(&pol->walk.all);
net->xfrm.policy_count[dir]--; // [8] dir=4, 导致越界减一
return pol;
}
1.2 构造UAF 空闲块8字节写NULL
步骤:
- (1)插入
policy1
对象,index=0, direction=0, priority=0 - (2)插入
policy2
对象,index=4, direction=0, priority=1 (>0), a timer set - (3)请求
XFRM_SPD_IPV4_HTHRESH
,触发 policy rehashing - (4)请求
XFRM_FLUSH_POLICY
,触发释放policy1
- (5)
policy2
超时,触发policy1
的UAF
第(1)(2)步后,两个policy对象被插入到同一list(direction 0),第(3)步执行以下函数,将已有的policy逆序重新插入 bydst
list。xfrm_hash_rebuild()
static void xfrm_hash_rebuild(struct work_struct *work)
{
... ...
/* re-insert all policies by order of creation */
list_for_each_entry_reverse(policy, &net->xfrm.policy_all, walk.all) {
if (policy->walk.dead)
continue;
dir = xfrm_policy_id2dir(policy->index); // [9] 将 policy1 重新插入到 bydst list,由于 policy2 的index>=3,所以跳过不插入。
if (dir >= XFRM_POLICY_MAX) {
/* skip socket policies */
continue;
}
newpos = NULL;
chain = policy_hash_bysel(net, &policy->selector,
policy->family, dir);
hlist_del_rcu(&policy->bydst);
if (!chain) {
void *p = xfrm_policy_inexact_insert(policy, dir, 0);
WARN_ONCE(IS_ERR(p), "reinsert: %ld\n", PTR_ERR(p));
continue;
}
hlist_for_each_entry(pol, chain, bydst) {
if (policy->priority >= pol->priority)
newpos = &pol->bydst;
else
break;
}
if (newpos)
hlist_add_behind_rcu(&policy->bydst, newpos);
else
hlist_add_head_rcu(&policy->bydst, chain);
}
... ...
}
第(3)步,[9]
之后,两个policy不再连贯,但policy2
仍然指向policy1
第(4)步,释放[9]
中bydst
的policy1
,由于policy2
不在bydst
中,所以没有被释放,且policy2
的pprev指针仍然指向释放后的 policy1
。
xfrm_policy_flush()
int xfrm_policy_flush(struct net *net, u8 type, bool task_valid)
{
int dir, err = 0, cnt = 0;
struct xfrm_policy *pol;
... ...
again:
list_for_each_entry(pol, &net->xfrm.policy_all, walk.all) {
dir = xfrm_policy_id2dir(pol->index);
if (pol->walk.dead ||
dir >= XFRM_POLICY_MAX ||
pol->type != type)
continue;
__xfrm_policy_unlink(pol, dir);
spin_unlock_bh(&net->xfrm.xfrm_policy_lock);
cnt++;
xfrm_audit_policy_delete(pol, 1, task_valid);
xfrm_policy_kill(pol); // [10] 释放
spin_lock_bh(&net->xfrm.xfrm_policy_lock);
goto again;
}
... ...
}
EXPORT_SYMBOL(xfrm_policy_flush);
static void xfrm_policy_kill(struct xfrm_policy *policy)
{
policy->walk.dead = 1;
atomic_inc(&policy->genid);
if (del_timer(&policy->polq.hold_timer))
xfrm_pol_put(policy);
skb_queue_purge(&policy->polq.hold_queue);
if (del_timer(&policy->timer))
xfrm_pol_put(policy);
xfrm_pol_put(policy);
}
第(5)步,当policy2
超时后,将对policy2
进行unlink操作:__xfrm_policy_unlink() -> __hlist_del()
static struct xfrm_policy *__xfrm_policy_unlink(struct xfrm_policy *pol,
int dir)
{
struct net *net = xp_net(pol);
if (list_empty(&pol->walk.all))
return NULL;
/* Socket policies are not hashed. */
if (!hlist_unhashed(&pol->bydst)) {
hlist_del_rcu(&pol->bydst); // [11] 对bydst list上 policy2 调用 __hlist_del()
hlist_del_init(&pol->bydst_inexact_list);
hlist_del(&pol->byidx);
}
list_del_init(&pol->walk.all);
net->xfrm.policy_count[dir]--; // [8] dir=4, 导致越界减一
return pol;
}
// [11] __hlist_del()
static inline void __hlist_del(struct hlist_node *n)
{
struct hlist_node *next = n->next;
struct hlist_node **pprev = n->pprev;
WRITE_ONCE(*pprev, next); // [12] policy2 的pprev指针仍然指向释放后的 policy1,这样导致 policy1 的next指针被写为null。导致8字节写
if (next)
next->pprev = pprev;
}
1-3 unlink过程分析
插入到bydst后,效果如图。总的来说,最后能往空闲块pol1(xfrm_policy 结构)中的xfrm_policy->bydst.next
写NULL,也就是空闲块的第2个8字节。
// 删除 pol1
next = pol1->next = NULL;
pprev = pol1->pprev = pol2;
*pprev = next ==> pol2->next = NULL;
next->pprev = pprev // 没有操作 最终 pol2->pprev = pol1。pol2还引用这释放后的pol1值。可堆喷站位pol1。
// 删除 pol2
next = pol2->next =NULL
pprev = pol2->pprev =pol1
pprev = next ==> pol1->next = NULL
next->pprev = pprev // 没有操作
xfrm_policy 结构如下,大小为0x358(v4.10.7版本),cred结构大小为0xa8。能将cred结构中的gid/suid
覆盖为NULL,拥有suid = 0的进程就可以成功的利用seteuid, setresuid提权成功。
// 能将空闲块 xfrm_policy 结构的第2个8字节写NULL
struct xfrm_policy {
possible_net_t xp_net; // 8 bytes
struct hlist_node bydst;
struct hlist_node byidx;
...
}
typedef struct { // possible_net_t
#ifdef CONFIG_NET_NS
struct net *net;
#endif
} possible_net_t;
struct hlist_node { // hlist_node
struct hlist_node *next, **pprev;
};
// 对比 cred 结构, 能将对应的 gid/suid 覆盖为0
struct cred {
atomic_t usage; // 4 bytes
uid_t uid; /* real UID of the task */ // 4 bytes
gid_t gid; /* real GID of the task */ // 4 bytes
uid_t suid; /* saved UID of the task */ // 4 bytes
gid_t sgid; /* saved GID of the task */
uid_t euid; /* effective UID of the task */
gid_t egid; /* effective GID of the task */
uid_t fsuid; /* UID for VFS ops */
gid_t fsgid; /* GID for VFS ops */
unsigned securebits; /* SUID-less security management */
kernel_cap_t cap_inheritable; /* caps our children can inherit */
kernel_cap_t cap_permitted; /* caps we're permitted */
kernel_cap_t cap_effective; /* caps we can actually use */
kernel_cap_t cap_bset; /* capability bounding set */
......
};
2. 漏洞利用
堆喷:堆喷用到了setxattr+userfaultfd堆喷。setxattr往内核拷贝的是用户栈上未初始化的0x1000字节,所以利用的关键并非布置数据,而是用作堆占位。那么kmalloc-1024
(xfrm_policy
对象)是如何覆盖kmalloc-192
(cred
对象,位于 dedicated cache-cred_jar)堆块的呢?
cred_jar
是一个 kmem_cache
,每次 释放一个无用的 cred 的时候不会直接释放占用的内存 而是放入 cred_jar
,高频使用的数据结构都有这样一个缓存机制。
cache转化:SLAB/SLUB分配器采用slabs来管理物理内存页,slabs有三种状态,分别是empty(所有对象都空闲)、partial(包含使用的和空闲的对象)、full(所有对象都被使用)。当Linux内核打算释放 empty slab
时,相应的物理页返回到可用的页池中,所以某些情况下,这些页可用从一种cache转化为另一种cache。
利用思路:这里我们按照exp中的说法,将这两个policy称为policy0
和policy1
。最开始policy0
和policy1
都位于kmalloc-1024
,当policy0
被释放后,相应的内存被另一种cache kmalloc-192
也即cred结构占用,再利用policy1
对空闲块进行8字节NULL写。详细EXP的步骤如下图所示:
(1) 父进程—触发漏洞 | (2) Process 0~249—堆喷射 | (3) Process 250~299—堆喷射 | (4) Process 300~1999—提权 |
---|---|---|---|
sleep(2) | 设置userfaultfd() | 设置userfaultfd() | sleep(8) 等待policy0 被释放 |
sleep(2) | pthread_create(handler) 错误处理子线程 |
pthread_create() 错误处理子线程 |
|
setxattr() 喷射 1024 字节 |
读wait_for_pol0[] 时阻塞,等(1) |
||
handler() 读wait_for_bug[] 时阻塞,等(1) |
|||
创建policy0 :__xfrm_add_policy0() |
|||
sleep(1) | |||
close(wait_for_pol0[]) 解除阻塞 |
|||
setxattr() 喷射 1024 字节 |
|||
handler() 读wait_for_bug[] 时阻塞,等(1) |
|||
policy0 被 kmalloc-1024 包围 |
|||
创建policy1 :__xfrm_add_policy1() timer = 7 -> sleep(7) |
|||
__xfrm_hash_rebuild() |
|||
sleep(1) | |||
__xfrm_flush_policy0() 释放policy0 |
|||
close(wait_for_bug[]) 解除阻塞 |
|||
handler() 拷贝数据,释放kmalloc-1024 |
handler() 拷贝数据,释放kmalloc-1024 |
||
子进程调用setgid() 促使内核分配cred |
|||
sleep(5) | |||
policy1 超时触发漏洞,policy0->next 被写NULL |
|||
检查cred是否被覆盖为NULL | |||
如果为root则将当前用户添加到sudo组 | |||
exit(1) 通知父进程,提权成功! |
利用步骤:
-
- 父进程创建
policy0
,用setattr()
在policy0
之前和之后喷射kmalloc-1024
,这样就能保证policy0
和policy1
相距很远,且不在同一slab中(便于之后policy0
所在的slab被释放,接着被cred使用);
- 父进程创建
-
- 父进程创建
policy1
,并调用__xfrm_hash_rebuild()
和__xfrm_flush_policy0()
,等待policy1
超时触发漏洞;
- 父进程创建
-
-
setxattr()
所喷射的kmalloc-1024
对象被释放,很有可能与policy0
相邻,这样相应的slab为empty也被释放;
-
-
- 子进程300~1999调用
setgid()
促使分配子进程的cred结构,很有可能复用了policy0
的内存。调用链——setgid() -> prepare_creds()
- 子进程300~1999调用
-
- 如果子进程的
gid
和suid
被覆盖为NULL(调用seteuid(0)
来检测),则有了root权限,可以更新/etc/sudoers
,添加当前用户为sudoer,使当前用户具备持久的root权限。
- 如果子进程的
成功提权:
参考
CVE-2019-15666 Ubuntu / CentOS / RHEL Linux Kernel 4.4 - 4.18 privilege escalation
[原创]cve-2019-15666 xfrm_policy 提权漏洞
Exploiting CVE-2019-15666 by reversing the binary PoC
Linux kernel (3.x-5.x) use-after-free in the XFRM subsystem
CVE-2019-15666-Exploit
信号量释放和等待函数sem_post()和sem_wait()
The Slab Allocator in the Linux kernel
因黑客尝试利用超算来挖矿,部分超算被逼下线
欧洲多国超级计算机被挖矿软件“团灭”