深入剖析SVE-2020-18610漏洞(上)

阅读量    207767 |

分享到: QQ空间 新浪微博 微信 QQ facebook twitter

 

简介

关于Samsung Galaxy的NPU漏洞,谷歌安全团队Project Zero曾经专门撰文加以介绍;但是,在本文中,我们将为读者介绍另外一种不同的漏洞利用方法。由于该漏洞本身非常简单,因此,我们将重点放在如何获得AAR/AAW,以及如何绕过Samsung Galaxy的缓解措施(如SELinux和KNOX)方面。我们的测试工作是在Samsung Galaxy S10上完成的,对于Samsung Galaxy S20,这里介绍的方法应该同样有效。

在介绍具体的漏洞利用技术之前,我们需要先了解一下Samsung Galaxy内核方面的基本防御机制。

 

Samsung Galaxy的内核缓解措施

实际上,Samsung Galaxy是在Android生态系统的基础之上实现的自己安全机制。

下面,让我们来简单了解一下漏洞利用之旅中最大的拦路虎——KNOX和SELinux。

KNOX

KNOX是Samsung Galaxy实现的一种安全机制,它引入了许多缓解措施,如DM-verify、KAP、PKM等,以防御安卓内核的本地提权攻击。

在KNOX机制中,最重要的缓解措施为RKP(Real-time Kernel Protection)和DFI(Data Flow Integrity)。

其中,RKP缓解措施是在安全环境中实现的,比如TrustZone或Hypervisor。

同时,RKP提供了许多功能,如防止运行来自非信任源的未经授权的特权代码,阻止来自用户空间的直接访问,以及验证重要内核数据的完整性,等等。

而缓解措施DFI则用于保护与root权限相关的数据,如init_cred、页表条目等。其工作原理是,将这些对象分配在受RKP保护的只读内存区中,这样一来,即使攻击者获得了AAW(Arbitrary Address Write,AAW)原语,也无法修改这些数据。

struct cred init_cred __kdp_ro = {
	.usage			= ATOMIC_INIT(4),
#ifdef CONFIG_DEBUG_CREDENTIALS
	.subscribers		= ATOMIC_INIT(2),
	.magic			= CRED_MAGIC,
#endif
	.uid			= GLOBAL_ROOT_UID,
	.gid			= GLOBAL_ROOT_GID,
	.suid			= GLOBAL_ROOT_UID,
  
  ...

那么,需要修改这些数据的时候该怎么办呢?为此,RKP专门提供了一个名为rkp_call/uh_call的特殊函数,专门用于修改这些受保护的数据。

当然,攻击者可以设法滥用该函数来达到他们的目标。问题是,这可能吗?

答案是,要想利用这个函数非常困难,因为目前所有的RKP函数都会在内部进行数据完整性检查。

// kernel/cred.c
void __put_cred(struct cred *cred)
{
	kdebug("__put_cred(%p{%d,%d})", cred,
	       atomic_read(&cred->usage),
	       read_cred_subscribers(cred));

#ifdef CONFIG_RKP_KDP
	if (rkp_ro_page((unsigned long)cred))
		BUG_ON((rocred_uc_read(cred)) != 0);
	else
#endif /*CONFIG_RKP_KDP*/
    
...

// fs/exec.c
#define RKP_CRED_SYS_ID 1000

static int is_rkp_priv_task(void)
{
	struct cred *cred = (struct cred *)current_cred();

	if(cred->uid.val <= (uid_t)RKP_CRED_SYS_ID || cred->euid.val <= (uid_t)RKP_CRED_SYS_ID ||
		cred->gid.val <= (gid_t)RKP_CRED_SYS_ID || cred->egid.val <= (gid_t)RKP_CRED_SYS_ID ){
		return 1;
	}
	return 0;
}

如上面的代码所示,当通过调用commit_creds()函数安装新的凭证时,会在内部调用__put_cred()函数。当__put_cred()函数调用一些rkp_call/uh_call时,HyperVisor/TrustZone不仅会检查进程凭证是否位于受RKP保护的只读内存区中,同时,还会检查进程id是否大于1000。

所以,现在已经无法伪造task_struct->cred member了。

另外,通常情况下,linux内核攻击者会滥用ptmx_fops来获取任意的函数调用原语,因为他们可以覆盖ptmx_fops。因此,ptmx_fops是实现任意函数调用原语的绝佳目标。

但是,由于RKP防御机制的缘故,Samsung Galaxy内核中包括ptmx_fops在内的所有fops结构都驻留在只读内存区中,这导致之前的攻击方法都行不通了,所以,他们必须寻找其他方法来获取可靠的任意函数调用原语。

SELinux

在Android 4.3之前,谷歌使用应用沙箱作为Android系统的安全模型。但是,在Android 5.0之后,SELinux已经上升为Android系统中主要的安全机制,并在默认情况下强制执行。

在谷歌的NEXUS和PIXEL系列设备上,SELinux策略是由内核空间中的一个名为selinux_enforcing的全局变量控制的,需要注意的是,这是一个可写的变量。因此,如果selinux_enforcing的值为false的话,SELinux就无法在这些安卓系统上正常工作。

然而,Samsung Galaxy的SELinux策略并不依赖于selinux_enforcing,而是通过定制SELinux策略来加固SELinux的薄弱之处。入下面的代码所示,Samsung Galaxy在原有SELinux权限管理的基础上,对几乎所有的系统调用接口都添加了相应的完整性检查。

struct cred {
  ...
#ifdef CONFIG_RKP_KDP
	atomic_t *use_cnt;
	struct task_struct *bp_task;
	void *bp_pgd;
	unsigned long long type;
#endif /*CONFIG_RKP_KDP*/
} __randomize_layout;

首先,在cred结构体中,提供了诸如bp_task和bp_pgd等成员,用于SELinux的security_integrity_current函数。当新的凭证在安全环境中被提交或覆盖时,RKP会在bp_task中记录其所有者信息,在bp_pgd中记录PGD信息。

// security/security.c
#define call_void_hook(FUNC, ...)				\
	do {							\
		struct security_hook_list *P;			\
								\
		if(security_integrity_current()) break;	\
		list_for_each_entry(P, &security_hook_heads.FUNC, list)	\
			P->hook.FUNC(__VA_ARGS__);		\
	} while (0)

#define call_int_hook(FUNC, IRC, ...) ({			\
	int RC = IRC;						\
	do {							\
		struct security_hook_list *P;			\
								\
		RC = security_integrity_current();		\
		if (RC != 0)							\
			break;								\
		list_for_each_entry(P, &security_hook_heads.FUNC, list) { \
			RC = P->hook.FUNC(__VA_ARGS__);		\
			if (RC != 0)				\
				break;				\
		}						\
	} while (0);						\
	RC;							\
})

...
  
// security/selinux/hooks.c
int security_integrity_current(void)
{
	rcu_read_lock();
	if ( rkp_cred_enable && 
		(rkp_is_valid_cred_sp((u64)current_cred(),(u64)current_cred()->security)||
		cmp_sec_integrity(current_cred(),current->mm)||
		cmp_ns_integrity())) {
		rkp_print_debug();
		rcu_read_unlock();
		panic("RKP CRED PROTECTION VIOLATION\n");
	}
	rcu_read_unlock();
	return 0;
}

如果CONFIG_RKP_KDP被启用,security_integrity_current函数就会发挥作用:验证一个进程的cred安全上下文。简单的说,它会完成下列检测:

  1. 进程描述符中的cred和security是否分配在受RKP保护的只读内存区中。
  2. bp_cred和cred是否一致,防止被篡改。
  3. bp_task是否是进程。
  4. mm->pgd和cred->bp_pgd是否一致。
  5. current->nsproxy->mnt_ns->root和current->nsproxy->mnt_ns->root->mnt->bp_mount是否一致。

此外,Samsung还会将与SELinux相关的数据如cred->security、task_security_struct和selinux_ops等保存在受RKP保护的只读内存区中,以防止攻击者通过AAW原语伪造数据。

上面就是对于Samsung定制的SELinux的简要概述。

除了需要了解SELinux的行为之外,还需要知道的一点是:SELinux也是漏洞市场的一个重要衡量标准,因为它被用来评估android系统漏洞的价值。

例如,如果某个漏洞可以在“isolated_app”上下文中被触发,那么它通常比只能在“untrusted_app”上下文中被触发的漏洞更有价值。

我们可以通过以下命令来检查这些信息。

adb pull /sys/fs/selinux/policy

sesearch --allow policy |grep -v "magisk" |grep "isolated_app"

 

 

Samsung Galaxy之前曝出的安全漏洞

关于KNOX的绕过漏洞,请参阅x82在POC 2019上发表的相关介绍,具体地址为http://powerofcommunity.net/poc2019/x82.pdf。

除此之外,网上还有几篇已经发表的相关文章,让我们来快速浏览一下。

KNOX 2.6 (Samsung Galaxy S7)

KeenLab在blackhat USA 2017大会上(https://www.blackhat.com/docs/us-17/thursday/us-17-Shen-Defeating-Samsung-KNOX-With-Zero-Privilege-wp.pdf),发布了一种绕过DFI和SELinux的新型方法。

首先,他们用一些诡异的方式(我不知道这个诡异的方式是什么)调用rkp_override_creds来覆盖自己的cred,即使RKP会在rkp_override_creds内部进行uid_checking。然后,他们通过以经过修改的poweroff_cmd为参数的orderly_poweroff函数调用usermodehelper来创建特权进程。所以,在获取拥有完全root权限的特权进程后,就可以通过调用rkp_override_creds来修改其cred信息。

即使新创建的进程具有完整的root权限,但是,由于SELinux的原因,它对整个文件系统的访问也是受限的。

KNOX 2.8 (Samsung Galaxy S8)

static int kdp_check_sb_mismatch(struct super_block *sb) 
{	
	if(is_recovery || __check_verifiedboot) {
		return 0;
	}
	if((sb != rootfs_sb) && (sb != sys_sb)
		&& (sb != odm_sb) && (sb != vendor_sb) && (sb != art_sb)) {
		return 1;
	}
	return 0;
}

static int invalid_drive(struct linux_binprm * bprm) 
{
	struct super_block *sb =  NULL;
	struct vfsmount *vfsmnt = NULL;
	
	vfsmnt = bprm->file->f_path.mnt;
	if(!vfsmnt || 
		!rkp_ro_page((unsigned long)vfsmnt)) {
		printk("\nInvalid Drive #%s# #%p#\n",bprm->filename, vfsmnt);
		return 1;
	} 
	sb = vfsmnt->mnt_sb;

	if(kdp_check_sb_mismatch(sb)) {
		printk("\nSuperblock Mismatch #%s# vfsmnt #%p#sb #%p:%p:%p:%p:%p:%p#\n",
					bprm->filename, vfsmnt, sb, rootfs_sb, sys_sb, odm_sb, vendor_sb, art_sb);
		return 1;
	}

	return 0;
}

#define RKP_CRED_SYS_ID 1000
static int is_rkp_priv_task(void)
{
	struct cred *cred = (struct cred *)current_cred();

	if(cred->uid.val <= (uid_t)RKP_CRED_SYS_ID || cred->euid.val <= (uid_t)RKP_CRED_SYS_ID ||
		cred->gid.val <= (gid_t)RKP_CRED_SYS_ID || cred->egid.val <= (gid_t)RKP_CRED_SYS_ID ){
		return 1;
	}
	return 0;
}
#endif

int flush_old_exec(struct linux_binprm * bprm)
{
	...
#ifdef CONFIG_RKP_NS_PROT
	if(rkp_cred_enable &&
		is_rkp_priv_task() && 
		invalid_drive(bprm)) {
		panic("\n KDP_NS_PROT: Illegal Execution of file #%s#\n", bprm->filename);
	}
#endif /*CONFIG_RKP_NS_PROT*/
  ...

如您所见,如果调用方的uid小于1000,它将检查挂载点是否位于受RKP保护的空间中。但是,由于这些验证没有添加到load_script()函数中,因此,攻击者仍然可以运行任意根脚本,而不是二进制文件。

以上漏洞现在都已经得到了修复,所以,攻击者仍然需要寻找新的方法来绕过Samsung Galaxy的自定义SELinux和KNOX机制。

NPU驱动程序

实际上,NPU是从exynos 9820系列开始提供的,也就是说,exynos 9820之后的Samsung Galaxy设备都提供了相应的NPU内核驱动程序。

在这个漏洞被修复前,这个驱动程序允许通过untrusted_app(Chromium浏览器、普通app,……)进行访问,但是在2020年11月Samsung发布安全更新之后,untrutsted_app也被新的selinux策略所禁用了。

实际上,用户只要打开/dev/vertex10就可以使用NPU驱动程序,它为用户提供了多种ioctl命令。

long vertex_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
	int ret = 0;
	struct vision_device *vdev = vision_devdata(file);
	const struct vertex_ioctl_ops *ops = vdev->ioctl_ops;

	/* temp var to support each ioctl */
	union {
		struct vs4l_graph vsg;
		struct vs4l_format_list vsf;
		struct vs4l_param_list vsp;
		struct vs4l_ctrl vsc;
		struct vs4l_container_list vscl;
	} vs4l_kvar;

	switch (cmd) {
	case VS4L_VERTEXIOC_S_GRAPH:
		ret = get_vs4l_graph64(&vs4l_kvar.vsg,
				(struct vs4l_graph __user *)arg);
		if (ret) {
			vision_err("get_vs4l_graph64 (%d)\n", ret);
			break;
		}

		ret = ops->vertexioc_s_graph(file, &vs4l_kvar.vsg);
		if (ret)
			vision_err("vertexioc_s_graph is fail(%d)\n", ret);

		put_vs4l_graph64(&vs4l_kvar.vsg,
				(struct vs4l_graph __user *)arg);
		break;

	case VS4L_VERTEXIOC_S_FORMAT:
		ret = get_vs4l_format64(&vs4l_kvar.vsf,
				(struct vs4l_format_list __user *)arg);
		if (ret) {
			vision_err("get_vs4l_format64 (%d)\n", ret);
			break;
		}

		ret = ops->vertexioc_s_format(file, &vs4l_kvar.vsf);
		if (ret)
			vision_err("vertexioc_s_format (%d)\n", ret);

		put_vs4l_format64(&vs4l_kvar.vsf,
				(struct vs4l_format_list __user *)arg);
		break;
      
...

虽然NPU提供的ioctl命令比较多,但我们只需关注VS4L_VERTEXIOC_S_GRAPH和VS4L_VERTEXIOC_S_FORMAT这两个命令即可,因为我们使用的漏洞位于VS4L_VERTEXIOC_S_GRAPH命令中,而VS4L_VERTEXIOC_S_FORMAT命令是用来进行越界读写的。

const struct vertex_ioctl_ops npu_vertex_ioctl_ops = {
	.vertexioc_s_graph      = npu_vertex_s_graph,
	.vertexioc_s_format     = npu_vertex_s_format,
	.vertexioc_s_param      = npu_vertex_s_param,
	.vertexioc_s_ctrl       = npu_vertex_s_ctrl,
	.vertexioc_qbuf         = npu_vertex_qbuf,
	.vertexioc_dqbuf        = npu_vertex_dqbuf,
	.vertexioc_prepare      = npu_vertex_prepare,
	.vertexioc_unprepare    = npu_vertex_unprepare,
	.vertexioc_streamon     = npu_vertex_streamon,
	.vertexioc_streamoff    = npu_vertex_streamoff
};

实际上,像get_vs4l_graph64这样的函数,其实只是copy_from_user函数的封装器,所以,我们只需要关注vertexioc_s_graph和vertexioc_s_format就行了。这些函数的具体定义,请参阅drivers/vision/npu/npu-vertex.c。

int npu_session_s_graph(struct npu_session *session, struct vs4l_graph *info)
{
	int ret = 0;
	BUG_ON(!session);
	BUG_ON(!info);
	ret = __get_session_info(session, info);
	if (unlikely(ret)) {
		npu_uerr("invalid in __get_session_info\n", session);
		goto p_err;
	}
	ret = __config_session_info(session);
	if (unlikely(ret)) {
		npu_uerr("invalid in __config_session_info\n", session);
		goto p_err;
	}
	return ret;
p_err:
	npu_uerr("Clean-up buffers for graph\n", session);
	return ret;
}

首先,函数npu_session_s_graph调用__get_session_info函数来映射对应的ION fd。正如下面的代码所示,在这个映射操作中使用了vmalloc——大家一定记住这一点。

void *ion_heap_map_kernel(struct ion_heap *heap,
			  struct ion_buffer *buffer)
{
	...
  
	int npages = PAGE_ALIGN(buffer->size) / PAGE_SIZE;
	struct page **pages = vmalloc(sizeof(struct page *) * npages);

  ...

	return vaddr;
}

然后,将调用__config_session_info函数,通过解析用户提供的数据来配置npu_session。

int __config_session_info(struct npu_session *session)
{
	...

	ret = __pilot_parsing_ncp(session, &temp_IFM_cnt, &temp_OFM_cnt, &temp_IMB_cnt, &WGT_cnt);

  ...
    
	ret = __second_parsing_ncp(session, &temp_IFM_av, &temp_OFM_av, &temp_IMB_av, &WGT_av);

在这里,struct npu_session由各种成员组成,其中最重要的成员是ncp_mem_buf。

struct npu_memory_buffer {
	struct list_head		list;
	struct dma_buf			*dma_buf;
	struct dma_buf_attachment	*attachment;
	struct sg_table			*sgt;
	dma_addr_t			daddr;
	void				*vaddr;
	size_t				size;
	int				fd;
};

...

struct npu_session {
	...
	struct npu_memory_buffer *ncp_mem_buf;
	...
};

其中,ncp_mem_buf->vaddr是由函数ion_heap_map_kernel返回的、由vmalloc分配的内存区,用户可以通过映射ION的DMA文件描述符将数据插入该区域。所以,temp_IFM_cnt、tmp_IFM_av等参数都是由用户数据进行初始化的。

 

小结

关于Samsung Galaxy的NPU漏洞,谷歌安全团队Project Zero曾经专门撰文加以介绍;但是,在本文中,我们将为读者介绍另外一种不同的漏洞利用方法。由于该漏洞本身非常简单,因此,我们将重点放在如何获得AAR/AAW,以及如何绕过Samsung Galaxy的缓解措施(如SELinux和KNOX)方面。我们的测试工作是在Samsung Galaxy S10上完成的,对于Samsung Galaxy S20,这里介绍的方法应该同样有效。

(未完待续)

分享到: QQ空间 新浪微博 微信 QQ facebook twitter
|推荐阅读
|发表评论
|评论列表
加载更多