cve-2017-8890 root case analysis

Introduction

学习 linux 内核漏洞利用,对 cve-2017-8890 进行分析与调试,该漏洞存在于 linux 内核的 net 模块中的 double free 漏洞, 取名 Phoenix Talon,影响几乎 Linux Kernel 2.5.69 ~ 4.10.15 范围的内核版本,可同时对 Android 进行提权。

System Setup

在开始分析漏洞成因之前,我们先搭建调试环境,可以选择 qemu 模拟运行,也可虚拟机双机调试,用虚拟机调试,更贴近原本的场景, 所以这里用双机调试。

kernel.org 下载合适的版本,理论上 4.10.15 之前的版本 都可是可行的,这里选择的是 4.10.15 版本,编译之前需要安装好依赖。

wget https://mirrors.edge.kernel.org/pub/linux/kernel/v4.x/linux-4.10.15.tar.gz
tar -zxf linux-4.10.15.tar.gz
sudo apt update
sudo apt upgrade
sudo apt install make libncurses5-dev libssl-dev build-essential

之后开始编译安装

make menuconfig
make
make modules
sudo make modules_install
sudo make install
sudo update-grub

make menuconfig 的作用是生成 .config 配置文件,直接默认就好。安装完成之后配置虚拟机,把打印机删除,添加串口。

客户端作为调试者对服务端进行调试,这时对虚拟机克隆,对克隆出来的虚拟机设置为服务端,然后编辑一下启动项。

让服务端启动的时候进入 kgdb 的调试状态,编辑 grub,增加启动时的引导选项

sudo vim /etc/grub.d/40_custom

先从 /boot/grub/grub.cfg 中复制一个菜单项过来,在内核命令行中增加 kgdb 选项:

kgdboc=ttyS0,115200 nokaslr

然后输入一下命令

sudo echo g > /proc/sysrq-trigger

这时我们在客户端用 gdb 连接

sudo gdb ./vmlinux

...

gef➤  target remote /dev/ttyS0

现在我们可以对其进行调试了。

Root Cause

对漏洞进行分析我们可以从两个方面入手,一个是 poc, 另一个是 patchbug tracker

patch 中可以看到,补丁对 inet_sk() 函数返回的结果的 mc_list 成员的值进行清空, 结合漏洞类型为 double free 可以得知是释放过程中对 mc_list 对象处理不当,导致漏洞产生。 我们分析一下漏洞补丁函数 inet_csk_clone_lock,用 understand 进行源码阅读,事半功倍, 可以生成函数调用关系图与结构体成员变量图,省去很多功夫,我们查看一下该函数的被调用关系:

对函数调用进行回溯发现漏洞函数是发生在 tcp 三次握手之后,创建 socket 的过程中。

net/ipv4/tcp_ipv4.c # 1266

/*
 * The three way handshake has completed - we got a valid synack -
 * now create the new socket.
 */
struct sock *tcp_v4_syn_recv_sock(const struct sock *sk, struct sk_buff *skb,
				  struct request_sock *req,
				  struct dst_entry *dst,
				  struct request_sock *req_unhash,
				  bool *own_req)

创建新的 socket 对象时,复制了一个副本,

net/ipv4/inet_connection_sock.c # 644

/**
 *	inet_csk_clone_lock - clone an inet socket, and lock its clone
 *	@sk: the socket to clone
 *	@req: request_sock
 *	@priority: for allocation (%GFP_KERNEL, %GFP_ATOMIC, etc)
 *
 *	Caller must unlock socket even in error path (bh_unlock_sock(newsk))
 */
struct sock *inet_csk_clone_lock(const struct sock *sk,
				 const struct request_sock *req,
				 const gfp_t priority)
{
	struct sock *newsk = sk_clone_lock(sk, priority);

	if (newsk) {

		...

		// inet_sk(newsk)->mc_list = NULL;

		...
	}

...
}

接着是 sk_clone_lock 函数又调用了 sock_copy 函数进行复制,把原来 socket 对象所有的值赋值给了新的克隆对象。

net/core/sock.c

/**
 *	sk_clone_lock - clone a socket, and lock its clone
 *	@sk: the socket to clone
 *	@priority: for allocation (%GFP_KERNEL, %GFP_ATOMIC, etc)
 *
 *	Caller must unlock socket even in error path (bh_unlock_sock(newsk))
 */
struct sock *sk_clone_lock(const struct sock *sk, const gfp_t priority)
{
	struct sock *newsk;
	bool is_charged = true;

	newsk = sk_prot_alloc(sk->sk_prot, priority, sk->sk_family);
	if (newsk != NULL) {
		struct sk_filter *filter;

		sock_copy(newsk, sk);

...
}

...

/*
 * Copy all fields from osk to nsk but nsk->sk_refcnt must not change yet,
 * even temporarly, because of RCU lookups. sk_node should also be left as is.
 * We must not copy fields between sk_dontcopy_begin and sk_dontcopy_end
 */
static void sock_copy(struct sock *nsk, const struct sock *osk)
{
#ifdef CONFIG_SECURITY_NETWORK
	void *sptr = nsk->sk_security;
#endif
	memcpy(nsk, osk, offsetof(struct sock, sk_dontcopy_begin));

	memcpy(&nsk->sk_dontcopy_end, &osk->sk_dontcopy_end,
	       osk->sk_prot->obj_size - offsetof(struct sock, sk_dontcopy_end));

#ifdef CONFIG_SECURITY_NETWORK
	nsk->sk_security = sptr;
	security_sk_clone(osk, nsk);
#endif
}

但是在后面初始化的过程中没有对 mc_list 对象初始化,而在释放过程中原 socketmc_list 成员被多次引用, 则会对 mc_list 对象多次释放。我们来看看该对象从创建到释放的过程。

通过源码分析与搜索引擎,得知 mc_list 对象代表的是组播列表,其结构体如下:

/* ip_mc_socklist is real list now. Speed is not argument;
   this list never used in fast path code
 */

struct ip_mc_socklist {
	struct ip_mc_socklist __rcu *next_rcu;
	struct ip_mreqn		multi;
	unsigned int		sfmode;		/* MCAST_{INCLUDE,EXCLUDE} */
	struct ip_sf_socklist __rcu	*sflist;
	struct rcu_head		rcu;
};

通过分析源码,查看其相关引用,可能在创建组播与加入组播时会创建 mc_list 对象,断在 ip_mc_join_group 看看

那么调用链为:

用户态:

setsockopt(MCAST_JOIN_GROUP)

内核态:

SyS_setsockopt() -> sock_common_setsockopt() -> ip_setsockopt() -> do_ip_setsockopt() -> ip_mc_join_group()

释放流程也可通过源码分析得出

用户态:

close(sockfd)

内核态:

sock_release() ->  inet_release() -> tcp_close() -> ip_mc_drop_socket()

知道了漏洞产生的原因,漏洞在哪里产生,那么我们可以构造如下 poc

poc 伪代码

sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_IP);
setsockopt(server_sockfd, SOL_IP, MCAST_JOIN_GROUP, &group, sizeof(group);
bind(sockfd, xxx, xxx);
listen(sockfd, xxx);
accept_sockfd = accept(sockfd, (struct sockaddr*)&accept1_si, sizeof(accept1_si));
close(accept_sockfd1);	// first free
sleep(xxx);
close(accept_sockfd2);	// second free

Summary

网上暂时没有搜到完全有效的 exploit,但有一些讲解利用思路的文章以及在保护机制开启不完全下利用的文章, 经过一番尝试,发现该漏洞最终无法在 linux 上单独完成提权,缺少 kalsr 的绕过。

总的思路就是要在两次释放中间堆喷覆盖 ip_mc_list 结构体,该结构体中包含一个 rcu_head 对象, 而该对象刚好包含一个函数指针,从而劫持eip,之后可以直接 rop 提权,也可以先绕过 smep 执行 shellcode。

Reference