Skip to content

Latest commit

 

History

History
189 lines (165 loc) · 7.11 KB

cve-2017-7184 (长亭在Pwn2Own上用于提权Ubuntu的漏洞) 的分析利用.md

File metadata and controls

189 lines (165 loc) · 7.11 KB

漏洞信息

ubuntu的公告中, 在xfrm_user模块中的xfrm_replay_verify_len函数, 由于条件检测不充分, 导致堆越界访问, 可以用来提权. (05-19更新)其中需要CAP_NET_RAW/CAP_NET_ADMIN权限, 在配置了CONFIG_USER_NS的系统上, 可以使用namespace来绕过这些检查. 长亭blog 通过upstream的两个补丁补丁0 补丁1来看这个漏洞, 补丁增加了对replay_window和bmp_len的检测, 用以确保后续的更新操作不会更改bmp_len成员以及replay_window的值. 那么这两个值是什么作用?

漏洞分析

补丁增加检测的两个成员为replay_window和bmp_len, 包含它的结构体为xfrm_replay_state_esn.

struct xfrm_replay_state_esn {
	unsigned int	bmp_len;
	__u32		oseq;
	__u32		seq;
	__u32		oseq_hi;
	__u32		seq_hi;
	__u32		replay_window;
	__u32		bmp[0];
};

bmp_len决定整个结构体的具体大小. 而replay_window则决定了bmp数组的索引范围. 这个结构在xfrm_alloc_replay_state_esn函数中生成, 后者由xfrm_state_construct也即xfrm_add_sa(XFRM_MSG_NEWSA)调用.

static int xfrm_alloc_replay_state_esn(struct xfrm_replay_state_esn **replay_esn,
				       struct xfrm_replay_state_esn **preplay_esn,
				       struct nlattr *rta)
{
	struct xfrm_replay_state_esn *p, *pp, *up;
	int klen, ulen;
        /* ... */

	up = nla_data(rta);
	klen = xfrm_replay_state_esn_len(up); /* 大小由bmp_len决定 */
	ulen = nla_len(rta) >= klen ? klen : sizeof(*up);

	p = kzalloc(klen, GFP_KERNEL);
        /* ... */
	pp = kzalloc(klen, GFP_KERNEL);
        /* 函数在这里申请了两个xfrm_replay_state_esn结构 */
	return 0;
}

然后xfrm_state_construct函数调用**xfrm_init_replay**

int xfrm_init_replay(struct xfrm_state *x)
{
	struct xfrm_replay_state_esn *replay_esn = x->replay_esn;

	if (replay_esn) {
                /* 在这里首先就检测了replay_window与bmp_len的大小关系 */
		if (replay_esn->replay_window >
		    replay_esn->bmp_len * sizeof(__u32) * 8)
			return -EINVAL;

		/* ... */
	} else
		x->repl = &xfrm_replay_legacy;

	return 0;
}

可以看出, 在xfrm_add_sa的流程中, 对新生成的xfrm_replay_state_esn结构是检测了replay_window与bmp_len的大小关系的. 在xfrm_new_ae(XFRM_MSG_NEWAE)中, 只调用了xfrm_replay_verify_len后便调用xfrm_update_ae_params更新xfrm_replay_state_esn结构体.

触发越界的函数有两个, xfrm_replay_check和xfrm_replay_advance中, 均在net/xfrm/xfrm_replay.c中实现. 其中, 后者advance*有越界写操作.

static void xfrm_replay_advance_esn(struct xfrm_state *x, __be32 net_seq)
{
	unsigned int bitnr, nr, i;
	int wrap;
	u32 diff, pos, seq, seq_hi;
	struct xfrm_replay_state_esn *replay_esn = x->replay_esn;

	if (!replay_esn->replay_window)
		return;

	seq = ntohl(net_seq); /* 网络发送过来的ip_auth_ah.seq_no值 */
	pos = (replay_esn->seq - 1) % replay_esn->replay_window;
	seq_hi = xfrm_replay_seqhi(x, net_seq);
	wrap = seq_hi - replay_esn->seq_hi;

	if ((!wrap && seq > replay_esn->seq) || wrap > 0) {
		if (likely(!wrap))
			diff = seq - replay_esn->seq;
		else
			diff = ~replay_esn->seq + seq + 1;

		if (diff < replay_esn->replay_window) {
			for (i = 1; i < diff; i++) {
				bitnr = (pos + i) % replay_esn->replay_window;
				nr = bitnr >> 5;
				bitnr = bitnr & 0x1F;
                                /* 对某32位数据执行 某位清0 */
				replay_esn->bmp[nr] &=  ~(1U << bitnr);
			}
		} else {
			nr = (replay_esn->replay_window - 1) >> 5;
                        /* 对一片区域清0 */
			for (i = 0; i <= nr; i++)
				replay_esn->bmp[i] = 0;
		}

		bitnr = (pos + diff) % replay_esn->replay_window;
		replay_esn->seq = seq;

		if (unlikely(wrap > 0))
			replay_esn->seq_hi++;
	} else {
		diff = replay_esn->seq - seq;

		if (pos >= diff)
			bitnr = (pos - diff) % replay_esn->replay_window;
		else
			bitnr = replay_esn->replay_window - (diff - pos);
	}

	nr = bitnr >> 5;
	bitnr = bitnr & 0x1F;
        /* 对某32位数据执行 某位置1 操作 */
	replay_esn->bmp[nr] |= (1U << bitnr);

	if (xfrm_aevent_is_on(xs_net(x)))
		x->repl->notify(x, XFRM_REPLAY_UPDATE);
}

可以看出, 通过一系列的操作, 我们是可以指定esn结构体后面的对象的值的. 有点需要注意的是, 在调用advance函数之前, 会调用check函数来检查某32位数据的某位是否置1了

static int xfrm_replay_check_esn(struct xfrm_state *x,
				 struct sk_buff *skb, __be32 net_seq)
{
	/* ... */
	if (replay_esn->bmp[nr] & (1U << bitnr))
		goto err_replay;

	return 0;

err_replay:
	x->stats.replay++;
err:
	xfrm_audit_state_replay(x, skb, net_seq);
	return -EINVAL;
}

advance函数和check函数都在xfrm_input函数中被调用. 如果采用IPPROTO_AH协议的话, 调用关系如下

xfrm4_ah_rcv -> xfrm4_rcv -> xfrm4_rcv_spi -> xfrm_input

漏洞利用

通过上述分析, 我们是可以设置esn->bmp后面数据的值的. 由于我们可以控制xfrm_replay_state_esn对象的大小(由bmp_len决定), 那么可以选取的目标对象是比较多的. 看看cred对象

struct cred {
	atomic_t	usage;
        /* CONFIG_DEBUG_CREDENTIALS */
	kuid_t		uid;		/* real UID of the task */
	kgid_t		gid;		/* real GID of the task */
	kuid_t		suid;		/* saved UID of the task */
	kgid_t		sgid;		/* saved GID of the task */
	kuid_t		euid;		/* effective UID of the task */
	kgid_t		egid;		/* effective GID of the task */

结合前面advance函数的一片区域清0的操作, 我们很容易就将cred结构体的前7个32位数据清0.

在advance中, 先清0, 后续的还有一个置位操作, 所以构造好 replay_window seq seq_no三个值, 将cred结构体清0之后, 再将cred->usage值还原(调试这个值为2).

这样我们只需要一个advance调用即可完成提权, 并且恢复系统.

利用伪代码:

/* 用于向内核发送信息, 然后生成/更新xfrm_state结构体的套接字 */
xfrm_state_fd = socket(PF_NETLINK, SOCK_RAW, NETLINK_XFRM);
/* 用于接收IPPROTO_AH信息, 触发xfrm_input的接收套接字 */
recvfd = socket(AF_INET, SOCK_RAW, IPPROTO_AH);
/* 用于发送自定义数据包的发送套接字 */
sendfd = socket(AF_INET, SOCK_RAW, IPPROTO_AH);

alloc_xfrm_state(...);
update_esn(...);
trigger_oob(...);

需要注意的地方就是, 在前面提到xfrm_alloc_replay_state_esn函数中, 连续申请了两个esn 也就是说, 在alloc_xfrm_state函数之后, 内存布局是这样的

内存低地址 | xfrm_replay_state_esn 结构0 | xfrm_replay_state_esn 结构1 | cred 结构 | 高地址

在测试系统中, cred对象的大小为0xa8, 合并在kmalloc-192的slab块中, 也即每个对象大小为0xc0. 那么cred结构的相对于esn->bmp偏移为0xc0-0x18+0xc0, 对应32位数据的数组索引, cred->usage的nr值为 (0xc0-0x18+0xc0)/4. 查看POC点这里