前言

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(...); 

补丁分析

image-20200529140546788

补丁commit: 677e806da4d916052585301785d847c3b3e6186a

上述补丁是在更新xfrm_state结构时,在xfrm_replay_verify_len中添加对用户更新的up->replay_window字段进行检查,必须小于bmp数组的大小,防止越界。

image-20200529142603806

补丁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

commit: f843ee6dd019bcece3e74e76ad9df0155655d0df