[kernel pwn] CVE-2017-7184复现

环境配置

使用 Linux 4.4.0-21-generic 版本,在这里下载

编译Linux需要启用一些配置信息

cp /boot/config-$(uname -r) .config
make menuconfig
Kernel hacking
--> Compile-time checks and compiler options
--> [X] Compile the kernel with debug info
--> [ ] Strip assembler-generated symbols during link 
[X] Kernel debugging

针对CVE-2017-7184需要启用一些CONFIG项

CONFIG_INET_AH=y
CONFIG_USER_NS=y
CONFIG_INET_XFRM_MODE_TRANSPORT=y
make -j8

Image我放弃了busybox转而选择syzkaller

wget https://raw.githubusercontent.com/google/syzkaller/master/tools/create-image.sh -O create-image.sh
chmod +x create-image.sh
./create-image.sh

最后启动qemu

qemu-system-x86_64 \
  -m 128M \
  -smp 2 -gdb tcp::1234 \
  -net nic,model=e1000 \
  -net user,host=10.0.2.10,hostfwd=tcp::1569-:22 \
  -display none -serial stdio -no-reboot \
  -hda ~/project/code/pwn/cve-2017-7184/img/2881fc2/stretch.img \
  -kernel /home/etenal/project/code/pwn/cve-2017-7184/linux-4.4.20/arch/x86_64/boot/bzImage \
  -append "console=ttyS0 net.ifnames=0 root=/dev/sda"

漏洞原理

XFRM模块中有一个名叫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代表bmp数组的长度,这个结构体在xfrm_add_sa函数中被创建并赋值。具体的调用顺序是 xfrm_add_sa->xfrm_state_construct->xfrm_update_ae_params。

static int xfrm_add_sa(struct sk_buff *skb, struct nlmsghdr *nlh,
		struct nlattr **attrs)
{
	struct net *net = sock_net(skb->sk);
	struct xfrm_usersa_info *p = nlmsg_data(nlh);
	struct xfrm_state *x;
	int err;
	struct km_event c;

	err = verify_newsa_info(p, attrs);
	if (err)
		return err;

	x = xfrm_state_construct(net, p, attrs, &err);
	if (!x)
		return err;

	xfrm_state_hold(x);
	if (nlh->nlmsg_type == XFRM_MSG_NEWSA)
		err = xfrm_state_add(x);
	else
		err = xfrm_state_update(x);

	xfrm_audit_state_add(x, err ? 0 : 1, true);

	if (err < 0) {
		x->km.state = XFRM_STATE_DEAD;
		__xfrm_state_put(x);
		goto out;
	}

	c.seq = nlh->nlmsg_seq;
	c.portid = nlh->nlmsg_pid;
	c.event = nlh->nlmsg_type;

	km_state_notify(x, &c);
out:
	xfrm_state_put(x);
	return err;
}

来看看赋值函数xfrm_update_ae_params的代码,在re分支中,通过memcpy把用户输入复制给xfrm_replay_state_esn结构体,即replay_esn。在代码中可以看到复制了两次,这是因为其外层结构体有两个xfrm_replay_state_esn成员变量,即replay_esn和preplay_esn,我们主要关注replay_esn。

static void xfrm_update_ae_params(struct xfrm_state *x, struct nlattr **attrs,
				  int update_esn)
{
	struct nlattr *rp = attrs[XFRMA_REPLAY_VAL];
	struct nlattr *re = update_esn ? attrs[XFRMA_REPLAY_ESN_VAL] : NULL;
	struct nlattr *lt = attrs[XFRMA_LTIME_VAL];
	struct nlattr *et = attrs[XFRMA_ETIMER_THRESH];
	struct nlattr *rt = attrs[XFRMA_REPLAY_THRESH];

	if (re) {
		struct xfrm_replay_state_esn *replay_esn;
		replay_esn = nla_data(re);
		memcpy(x->replay_esn, replay_esn,
		       xfrm_replay_state_esn_len(replay_esn));
		memcpy(x->preplay_esn, replay_esn,
		       xfrm_replay_state_esn_len(replay_esn));
	}

	if (rp) {
		struct xfrm_replay_state *replay;
		replay = nla_data(rp);
		memcpy(&x->replay, replay, sizeof(*replay));
		memcpy(&x->preplay, replay, sizeof(*replay));
	}

	if (lt) {
		struct xfrm_lifetime_cur *ltime;
		ltime = nla_data(lt);
		x->curlft.bytes = ltime->bytes;
		x->curlft.packets = ltime->packets;
		x->curlft.add_time = ltime->add_time;
		x->curlft.use_time = ltime->use_time;
	}

	if (et)
		x->replay_maxage = nla_get_u32(et);

	if (rt)
		x->replay_maxdiff = nla_get_u32(rt);
}

可知xfrm_add_sa函数执行后会将xfrm_replay_state_esn结构体初始化。接下来介绍xfrm_new_ae函数

static int xfrm_new_ae(struct sk_buff *skb, struct nlmsghdr *nlh,
		struct nlattr **attrs)
{
	struct net *net = sock_net(skb->sk);
	struct xfrm_state *x;
	struct km_event c;
	int err = -EINVAL;
	u32 mark = 0;
	struct xfrm_mark m;
	struct xfrm_aevent_id *p = nlmsg_data(nlh);
	struct nlattr *rp = attrs[XFRMA_REPLAY_VAL];
	struct nlattr *re = attrs[XFRMA_REPLAY_ESN_VAL];
	struct nlattr *lt = attrs[XFRMA_LTIME_VAL];
	struct nlattr *et = attrs[XFRMA_ETIMER_THRESH];
	struct nlattr *rt = attrs[XFRMA_REPLAY_THRESH];

	if (!lt && !rp && !re && !et && !rt)
		return err;

	/* pedantic mode - thou shalt sayeth replaceth */
	if (!(nlh->nlmsg_flags&NLM_F_REPLACE))
		return err;

	mark = xfrm_mark_get(attrs, &m);

	x = xfrm_state_lookup(net, mark, &p->sa_id.daddr, p->sa_id.spi, p->sa_id.proto, p->sa_id.family);
	if (x == NULL)
		return -ESRCH;

	if (x->km.state != XFRM_STATE_VALID)
		goto out;

	err = xfrm_replay_verify_len(x->replay_esn, re);
	if (err)
		goto out;

	spin_lock_bh(&x->lock);
	xfrm_update_ae_params(x, attrs, 1);
	spin_unlock_bh(&x->lock);

	c.event = nlh->nlmsg_type;
	c.seq = nlh->nlmsg_seq;
	c.portid = nlh->nlmsg_pid;
	c.data.aevent = XFRM_AE_CU;
	km_state_notify(x, &c);
	err = 0;
out:
	xfrm_state_put(x);
	return err;
}

这个函数会将新的
xfrm_replay_state_esn 结构体覆盖原有的结构体,使用xfrm_replay_verify_len校验replay_esn的合法性,接下来使用xfrm_update_ae_params更新
xfrm_replay_state_esn 结构体,但是在验证函数
xfrm_replay_verify_len 中只比较了两个
xfrm_replay_state_esn 结构体的长度相同,即sizeof(*replay_esn) + replay_esn->bmp_len * sizeof(__u32)相同,而忽略了replay_window的比较

static inline int xfrm_replay_verify_len(struct xfrm_replay_state_esn *replay_esn,
					 struct nlattr *rp)
{
	struct xfrm_replay_state_esn *up;
	int ulen;

	if (!replay_esn || !rp)
		return 0;

	up = nla_data(rp);
	ulen = xfrm_replay_state_esn_len(up);

	if (nla_len(rp) < ulen || xfrm_replay_state_esn_len(replay_esn) != ulen)
		return -EINVAL;

	return 0;
}

并且replay_window会被用于更新bitmap,在xfrm_replay_advance_esn函数中,replay_esn->bmp[0 至 (replay_window-1) >> 5] 会被置零,因此我们使用一个replay_window就可能覆盖超过结构体边界的数据造成overflow

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);
	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;
				replay_esn->bmp[nr] &=  ~(1U << bitnr);
			}
		} else {
			nr = (replay_esn->replay_window - 1) >> 5;
			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;
	replay_esn->bmp[nr] |= (1U << bitnr);

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

漏洞利用

目前我们拥有一段区间置零的能力,并不能造成特别的危害。我这里使用CVE-2016-0728的方法通过置零来劫持控制流。

在Linux中有一个struct key,这个结构体被用来保存密钥

struct key {
	atomic_t		usage;		/* number of references */
	key_serial_t		serial;		/* key serial number */
	union {
		struct list_head graveyard_link;
		struct rb_node	serial_node;
	};
	struct rw_semaphore	sem;		/* change vs change sem */
	struct key_user		*user;		/* owner of this key */
	void			*security;	/* security data for this key */
	union {
		time_t		expiry;		/* time at which key expires (or 0) */
		time_t		revoked_at;	/* time at which key was revoked */
	};
	time_t			last_used_at;	/* last time used for LRU keyring discard */
	kuid_t			uid;
	kgid_t			gid;
	key_perm_t		perm;		/* access permissions */
	unsigned short		quotalen;	/* length added to quota */
	unsigned short		datalen;	/* payload data length
						 * - may not match RCU dereferenced payload
						 * - payload should contain own length
						 */

#ifdef KEY_DEBUGGING
	unsigned		magic;
#define KEY_DEBUG_MAGIC		0x18273645u
#define KEY_DEBUG_MAGIC_X	0xf8e9dacbu
#endif

	unsigned long		flags;	

	/* the key type and key description string
	 * - the desc is used to match a key against search criteria
	 * - it should be a printable string
	 * - eg: for krb5 AFS, this might be "[email protected]"
	 */
	union {
		struct keyring_index_key index_key;
		struct {
			struct key_type	*type;		/* type of key */
			char		*description;
		};
	};

	/* key data
	 * - this is used to hold the data actually used in cryptography or
	 *   whatever
	 */
	union {
		union key_payload payload;
		struct {
			/* Keyring bits */
			struct list_head name_link;
			struct assoc_array keys;
		};
		int reject_error;
	};
};

我们关心的是它的两个成员变量,usage和type。usage标记了当前key被引用了多少次,如果usage为0,在下一次操作这个key的时候这个key会被free掉。而type成员变量指向了一个结构体key_type

struct key_type {
    char * name;
    size_t datalen;
    void * vet_description;
    void * preparse;
    void * free_preparse;
    void * instantiate;
    void * update;
    void * match_preparse;
    void * match_free;
    void * revoke;
    void * destroy;
};

这个结构体内有一个函数指针revoke,当执行revoke操作时就会调用该函数指针。因此我们可以使用UAF的方式利用该漏洞劫持控制流

首先运用一段空间置零的能力将这个key的usage置零,使其释放,再向其中写入新的数据将type->revoke指向可控地址从而控制rip。

讨论一些细节:

  • 在堆上喷射大量数据使
    xfrm_replay_state_esn 结构体紧邻key结构体
  • 精确控制replay_window大小,将key的usage置零
  • 将key释放掉后再次向堆上喷射数据,使原来的key被覆盖,revoke指向可控地址
  • key生成时会同时生成一个cred结构体,堆喷后需要释放两块地址。
  • 覆盖key的数据需要遵循一些规则,key->flag=9 key->expiry=0 key->security.sid=1 key->perm=0x3f3f3f

最后exp:https://github.com/FlyRabbit/pwnable/blob/master/CVE-2017-7184/exp2.c

文章没有详细介绍原理成因,如有意或可以参见这篇文章

引用:

【1】http://p4nda.top/2019/02/16/CVE-2017-7184/

【2】
https://perception-point.io/resources/research/analysis-and-exploitation-of-a-linux-kernel-vulnerability/

1 Comment

Join the discussion and tell us your opinion.

浪迹天涯reply
2019年4月30日 at am10:21

原来大佬都是随时建虚拟机的。。

Leave a reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.