CVE-2017-7184 xfrm_user分析
前言
IPSEC是一个协议组合,用于提供IP层的安全性。它包含AH、ESP、IKE协议,IPSec提供了两种安全机制:认证(采用ipsec的AH)和加密(采用ipsec的ESP)。
(1) SA(Security Associstion)
SA由spi、ip、安全协议标识(AH或ESP)这三个参数唯一确定。SA定义了ipsec双方的ip地址、ipsec协议、加密算法、密钥、模式、抗重放窗口等。
(2) AH(Authentication Header)
AH为ip包提供数据完整性校验和身份认证功能,提供抗重放能力,验证算法由SA指定。
(3) ESP(Encapsulating security payload)
ESP为ip数据包提供完整性检查、认证和加密
xfrm_user.c中的代码允许我们向内核发送netlink消息来调用相关handler实现对SA和SP的配置,其中涉及处理函数如下:
xfrm_dispatch[XFRM_NR_MSGTYPES] = {
[XFRM_MSG_NEWSA - XFRM_MSG_BASE] = { .doit = xfrm_add_sa },
[XFRM_MSG_DELSA - XFRM_MSG_BASE] = { .doit = xfrm_del_sa },
[XFRM_MSG_GETSA - XFRM_MSG_BASE] = { .doit = xfrm_get_sa,
.dump = xfrm_dump_sa,
.done = xfrm_dump_sa_done },
[XFRM_MSG_NEWPOLICY - XFRM_MSG_BASE] = { .doit = xfrm_add_policy },
[XFRM_MSG_DELPOLICY - XFRM_MSG_BASE] = { .doit = xfrm_get_policy },
[XFRM_MSG_GETPOLICY - XFRM_MSG_BASE] = { .doit = xfrm_get_policy,
.dump = xfrm_dump_policy,
.done = xfrm_dump_policy_done },
[XFRM_MSG_ALLOCSPI - XFRM_MSG_BASE] = { .doit = xfrm_alloc_userspi },
[XFRM_MSG_ACQUIRE - XFRM_MSG_BASE] = { .doit = xfrm_add_acquire },
[XFRM_MSG_EXPIRE - XFRM_MSG_BASE] = { .doit = xfrm_add_sa_expire },
[XFRM_MSG_UPDPOLICY - XFRM_MSG_BASE] = { .doit = xfrm_add_policy },
[XFRM_MSG_UPDSA - XFRM_MSG_BASE] = { .doit = xfrm_add_sa },
[XFRM_MSG_POLEXPIRE - XFRM_MSG_BASE] = { .doit = xfrm_add_pol_expire},
[XFRM_MSG_FLUSHSA - XFRM_MSG_BASE] = { .doit = xfrm_flush_sa },
[XFRM_MSG_FLUSHPOLICY - XFRM_MSG_BASE] = { .doit = xfrm_flush_policy },
[XFRM_MSG_NEWAE - XFRM_MSG_BASE] = { .doit = xfrm_new_ae },
[XFRM_MSG_GETAE - XFRM_MSG_BASE] = { .doit = xfrm_get_ae },
[XFRM_MSG_MIGRATE - XFRM_MSG_BASE] = { .doit = xfrm_do_migrate },
[XFRM_MSG_GETSADINFO - XFRM_MSG_BASE] = { .doit = xfrm_get_sadinfo },
[XFRM_MSG_NEWSPDINFO - XFRM_MSG_BASE] = { .doit = xfrm_set_spdinfo,
.nla_pol = xfrma_spd_policy,
.nla_max = XFRMA_SPD_MAX },
[XFRM_MSG_GETSPDINFO - XFRM_MSG_BASE] = { .doit = xfrm_get_spdinfo },
};
其中几个函数的功能:
xfrm_add_sa
创建一个新的SA,并可以指定相关attr,在内核中,是用一个xfrm_state结构来表示一个SA的。
xfrm_del_sa
删除一个SA,也即删除一个指定的xfrm_state。
xfrm_new_ae
根据传入参数,更新指定xfrm_state结构中的内容。
xfrm_get_ae
根据传入参数,查询指定xfrm_state结构中的内容(包括attr)。
漏洞相关的重要数据结构:
xfrm_state数据结构,由于表示SA:
struct xfrm_state {
possible_net_t xs_net;
union {
struct hlist_node gclist;
struct hlist_node bydst;
};
struct hlist_node bysrc;
struct hlist_node byspi;
atomic_t refcnt;
spinlock_t lock;
struct xfrm_id id; // 用于识别一个SA身份,包含daddr、spi、proto三个参数
struct xfrm_selector sel;
struct xfrm_mark mark;
u32 tfcpad;
u32 genid;
/* Key manager bits */
struct xfrm_state_walk km;
/* Parameters of this state. */
struct {
u32 reqid;
u8 mode;
u8 replay_window;
u8 aalgo, ealgo, calgo;
u8 flags;
u16 family;
xfrm_address_t saddr;
int header_len;
int trailer_len;
u32 extra_flags;
} props;
struct xfrm_lifetime_cfg lft;
/* Data for transformer */
struct xfrm_algo_auth *aalg;
struct xfrm_algo *ealg;
struct xfrm_algo *calg;
struct xfrm_algo_aead *aead;
const char *geniv;
/* Data for encapsulator */
struct xfrm_encap_tmpl *encap;
/* Data for care-of address */
xfrm_address_t *coaddr;
/* IPComp needs an IPIP tunnel for handling uncompressed packets */
struct xfrm_state *tunnel;
/* If a tunnel, number of users + 1 */
atomic_t tunnel_users;
/* State for replay detection */
struct xfrm_replay_state replay;
struct xfrm_replay_state_esn *replay_esn; // ***
/* Replay detection state at the time we sent the last notification */
struct xfrm_replay_state preplay;
struct xfrm_replay_state_esn *preplay_esn; // ***
/* The functions for replay detection. */
const struct xfrm_replay *repl;
/* internal flag that only holds state for delayed aevent at the
* moment
*/
u32 xflags;
/* Replay detection notification settings */
u32 replay_maxage;
u32 replay_maxdiff;
/* Replay detection notification timer */
struct timer_list rtimer;
/* Statistics */
struct xfrm_stats stats;
struct xfrm_lifetime_cur curlft;
struct tasklet_hrtimer mtimer;
/* used to fix curlft->add_time when changing date */
long saved_tmo;
/* Last used time */
unsigned long lastused;
/* Reference to data common to all the instances of this
* transformer. */
const struct xfrm_type *type;
struct xfrm_mode *inner_mode;
struct xfrm_mode *inner_mode_iaf;
struct xfrm_mode *outer_mode;
/* Security context */
struct xfrm_sec_ctx *security;
/* Private data of this transformer, format is opaque,
* interpreted by xfrm_type methods. */
void *data;
};
struct xfrm_replay_state_esn {
unsigned int bmp_len;//表示bmp数组的大小,最大值为4096/4/8
__u32 oseq;
__u32 seq;
__u32 oseq_hi;
__u32 seq_hi;
__u32 replay_window;//seq的模数,表示索引的范围,以bit为单位的大小
__u32 bmp[0];
};
bmp_len决定整个结构体的具体大小. 而replay_window则决定了bmp数组的索引范围(以bit为单位)。
bmp_len由用户输入控制,通过kzalloc申请6*4 + bmp_len *4 大小的内存块。
cred结构:
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
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 */
kuid_t fsuid; /* UID for VFS ops */
kgid_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 */
kernel_cap_t cap_ambient; /* Ambient capability set */
#ifdef CONFIG_KEYS
unsigned char jit_keyring; /* default keyring to attach requested
* keys to */
struct key __rcu *session_keyring; /* keyring inherited over fork */
struct key *process_keyring; /* keyring private to this process */
struct key *thread_keyring; /* keyring private to this thread */
struct key *request_key_auth; /* assumed request_key authority */
#endif
#ifdef CONFIG_SECURITY
void *security; /* subjective LSM security */
#endif
struct user_struct *user; /* real user ID subscription */
struct user_namespace *user_ns; /* user_ns the caps and keyrings are relative to. */
struct group_info *group_info; /* supplementary groups for euid/fsgid */
struct rcu_head rcu; /* RCU deletion hook */
};
漏洞分析
申请一个xfrm_state结构(SA)流程:
xfrm_add_sa:
->verify_newsa_info
->verify_replay // 检验bmp_len是否小于最大值
->xfrm_state_construct
->xfrm_state_alloc // 申请xfrm_state结构
->copy_from_user_state
->xfrm_alloc_replay_state_esn(&x->replay_esn, &x->preplay_esn,
attrs[XFRMA_REPLAY_ESN_VAL]))//申请xfrm_replay_state_esn结构
->xfrm_replay_state_esn_len //获取申请空间的大小
->xfrm_init_replay(x) //检验xfrm_replay_state_esn结构中的数据
static inline int xfrm_replay_state_esn_len(struct xfrm_replay_state_esn *replay_esn)
{
return sizeof(*replay_esn) + replay_esn->bmp_len * sizeof(__u32);
}
更新一个xfrm_state结构(SA)流程:
xfrm_add_ae:
->xfrm_state_lookup //循环查找hash表,得到xfrm_state结构体
->xfrm_replay_verify_len // 检查整个结构体的大小,但没有检查replay_window变量,导致越界
->ulen = xfrm_replay_state_esn_len(up);
->if (nla_len(rp) < ulen || xfrm_replay_state_esn_len(replay_esn) != ulen)
->xfrm_update_ae_params // memcpy复制更新的数据
replay_esn为之前的结构体,up为用户更新的结构体,检查两个结构体大小要相同,即只需要提供的bmp_len与xfrm_state中原来的bmp_len一致就可以通过检查。
所以漏洞成因在于更新xfrm_state时,没有检查x->replay_esn.replay_window,导致引用replay_window变量进行bitmap读写时会造成越界。
xfrm_replay_verify_len代码:
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进行访问的函数:
static const struct xfrm_replay xfrm_replay_esn = {
.advance = xfrm_replay_advance_esn, <------
.check = xfrm_replay_check_esn, <------
.recheck = xfrm_replay_recheck_esn,
.notify = xfrm_replay_notify_esn,
.overflow = xfrm_replay_overflow_esn,
};
通过AH数据包触发越界读写,漏洞触发链:
xfrm4_ah_rcv -> xfrm4_rcv -> xfrm4_rcv_spi->
xfrm_input:
->xfrm_state_lookup //找到对应的xfrm_state结构
->if (x->repl->check(x, skb, seq)) //x->repl 在xfrm_init_replay被赋值,此处调用xfrm_replay_check_esn
->if (replay_esn->bmp[nr] & (1U << bitnr)) // 检查AH中的seq是否被处理过
->if (async && x->repl->recheck(x, skb, seq))//调用xfrm_replay_recheck_esn
->x->repl->advance(x, seq); //调用xfrm_replay_advance_esn
xfrm_replay_check_esn 中只是对bmp中的值进行判断,没有读写操作,而xfrm_replay_advance_esn函数中有三处对bmp进行写操作。
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;
[1] replay_esn->bmp[nr] &= ~(1U << bitnr);//某一个bit执行置零操作
}
} else {
nr = (replay_esn->replay_window - 1) >> 5;
for (i = 0; i <= nr; i++)
[2] replay_esn->bmp[i] = 0; // 对从bmp[0]到bmp[(replay_esn->replay_window - 1) >> 5]块内存均置零
}
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;
[3] replay_esn->bmp[nr] |= (1U << bitnr); //对某一个bit写1
if (xfrm_aevent_is_on(xs_net(x)))
x->repl->notify(x, XFRM_REPLAY_UPDATE);
}
综上所述,通过用户态空间发送一个AH数据包可以造成越界读写,可以每次写1bit大小的数据,同时也可以将(replay_windows -1)»5比特大小的内存块清空。
漏洞利用
将cred结构堆喷到xfrm_replay_state_esn结构体后,通过越界置零操作,将cred的uid和gid写0,进行提权。
cred结构体的申请是通过prepare_creds中的new = kmem_cache_alloc(cred_jar, GFP_KERNEL);得到的。
cred结构体由kmalloc-192分配,所以要控制bmp_len大小,保证申请的xfrm_replay_state_esn结构大小是由kmalloc-192分配,并需要满足128 < bmp_len * 4 +0x18 < 192。根据slub分配机制,这样申请完xfrm_replay_state_esn,fork出进程的cred结构有很大概率会分配在相邻位置,就可以对cred结构进行越界写0。
replay_esn->replay_window决定要置零的内存大小。
xfrm_alloc_replay_state_esn函数中会申请两个紧邻的xfrm_replay_state_esn结构体分别存放replay_esn和preplay_esn,所以利用时的内存布局如下:
内存低地址 | xfrm_replay_state_esn 结构0 | xfrm_replay_state_esn 结构1 | cred 结构 | 高地址
那么cred结构的相对于esn->bmp偏移为0xc0-0x18+0xc0,对应32位数据的数组索引,cred->usage的nr值为 (0xc0-0x18+0xc0)/4。
在xfrm_user_rcv_msg函数中,需要CAP_NET_ADMIN权限进行漏洞利用:
/* All operations require privileges, even GET */
if (!netlink_net_capable(skb, CAP_NET_ADMIN))
return -EPERM;
也可以通过利用下面方法运行exp,该方法应该是偷懒了,需要sudo权限。非特权用户可以创建用户命名空间、网络命名空间。在命名空间内部,我们就可以有相应的capability来触发漏洞。
sudo setcap cap_net_raw,cap_net_admin=eip ./exp
如果不能使用,需要安装sudo apt-get install libcap2-bin
漏洞利用伪代码:
/* 用于向内核发送信息, 然后生成/更新xfrm_state结构体的套接字 */
xfrm_state_fd = socket(PF_NETLINK, SOCK_RAW, NETLINK_XFRM); // init_xfrm_socket
/* 用于接收IPPROTO_AH信息, 触发xfrm_input的接收套接字 */
recvfd = socket(AF_INET, SOCK_RAW, IPPROTO_AH); //init_recvfd
/* 用于发送自定义数据包的发送套接字 */
sendfd = socket(AF_INET, SOCK_RAW, IPPROTO_AH);// init_sendfd
// fork_spary_n
alloc_xfrm_state(...); // xfrm_add_sa
update_esn(...); //xfrm_new_sa
trigger_oob(...);
补丁分析
补丁commit: 677e806da4d916052585301785d847c3b3e6186a
上述补丁是在更新xfrm_state结构时,在xfrm_replay_verify_len中添加对用户更新的up->replay_window字段进行检查,必须小于bmp数组的大小,防止越界。
补丁commit:f843ee6dd019bcece3e74e76ad9df0155655d0df
该补丁添加了前xfrm_state结构中bmp_len和用户更新的bmp_len要相等的验证,一般来说,xfrm_replay_state_esn_len(replay_esn) 和ulen相等说明整个结构体大小相等,扣掉固定字段大小,bmp数组大小是相等的,这里应该是防止未知的越界访问手段。
参考链接
https://github.com/snorez/blog/blob/master/cve-2017-7184%20(%E9%95%BF%E4%BA%AD%E5%9C%A8Pwn2Own%E4%B8%8A%E7%94%A8%E4%BA%8E%E6%8F%90%E6%9D%83Ubuntu%E7%9A%84%E6%BC%8F%E6%B4%9E)%20%E7%9A%84%E5%88%86%E6%9E%90%E5%88%A9%E7%94%A8.md
https://bbs.pediy.com/thread-250749.htm
https://xz.aliyun.com/t/4133
poc地址:https://github.com/snorez/exploits/blob/master/cve-2017-7184/exp.c
Netlink 内核实现分析(一):创建 :https://blog.csdn.net/luckyapple1028/article/details/50839395
Netlink+内核实现分析(二):通信:https://blog.csdn.net/sinat_32343037/article/details/78487445
补丁链接:https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=677e806da4d916052585301785d847c3b3e6186a
commit: 677e806da4d916052585301785d847c3b3e6186a
https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=f843ee6dd019bcece3e74e76ad9df0155655d0df