[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 Comment
Join the discussion and tell us your opinion.
原来大佬都是随时建虚拟机的。。