简介
关于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安全上下文。简单的说,它会完成下列检测:
- 进程描述符中的cred和security是否分配在受RKP保护的只读内存区中。
- bp_cred和cred是否一致,防止被篡改。
- bp_task是否是进程。
- mm->pgd和cred->bp_pgd是否一致。
- 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,这里介绍的方法应该同样有效。
(未完待续)

评论