三星RKP内核完整性保护程序分析

· 2024-03-06 11:46 · 2 阅读

wzt 2024-03-06 11:46 上海

本文主要以三星s6与s20二进制为样本对内核完整性保护程序RKP进行分析。

1 功能分析

  本文主要以三星s6s20二进制为样本进行分析。S62016年发布,这个版本由于有符号存在,可以大大降低逆向工程分析的难度,对于s20二进制只做部分参考分析。

1.1 Cred保护

  内核Struct cred数据结构保存了进程的权限凭证如uid/gidcapability等,是内核漏洞攻击程序进行权限提升必须要更改的数据结构,因此对cred数据结构的保护至关重要。三星rkpel2限制了credel1为只读,当内核对cred进行正常写操作时,通过rkp接口调用el2层函数对其进行写操作。但rkp除了对cred做只读保护外, 还在cred数据结构引入了两个字段bp_taskbp_pgdcred做完整性校验以及struct task_security_struct结构加入bp_cred字段,防止被其他进程篡改。

struct cred {        atomic_t        usage;        kuid_t          uid;            /* real UID of the task */        kgid_t          gid;            /* real GID of the task */        kuid_t          suid;           /* saved UID of the task */...        union {                int non_rcu;                    /* Can we skip RCU deletion? */                struct rcu_head rcu;            /* RCU deletion hook */        };} __randomize_layout;


1.1.1 反向task_struct指针保护

  当内核需要对cred进行创建和更新时,bp_taskbp_pgd就需要同步更新。

  对于bp_task,内核在prepare_ro_creds函数里通过调用如下rkp接口:

drivers/uh/kdp.cstruct cred *prepare_ro_creds(struct cred *old, int kdp_cmd, u64 p){        memset((void *)&param_data, 0, sizeof(struct cred_param));        param_data.cred = &temp_old;        param_data.cred_ro = new_ro;        param_data.use_cnt_ptr = use_cnt_ptr;        param_data.sec_ptr = tsec;        param_data.type = kdp_cmd;        param_data.use_cnt = (u64)p;
uh_call(UH_APP_KDP, PREPARE_RO_CRED, (u64)&param_data, (u64)current, (u64)&init_cred, (u64)&init_cred_kdp);}

  对应的el2层函数操作为:

通过rkp_assign_credscred结构体的CRED_BP_TASK_OFFSET偏移进行赋值。

1.1.2 反向pgd指针保护

  对于bp_pgd,内核提供kdp_assign_pgd进行操作。

void kdp_assign_pgd(struct task_struct *p){        u64 pgd = (u64)(p->mm ? p->mm->pgd : swapper_pg_dir);
uh_call(UH_APP_KDP, SET_CRED_PGD, (u64)p->cred, (u64)pgd, 0, 0);}

  对应的el2层函数操作为:

通过rkp_pgd_assigncred结构体的CRED_BP_PGD_OFFSET偏移进行赋值。

1.1.3 task_security_struct指针保护

security/selinux/hooks.cstatic void cred_init_security(void){        struct cred *cred = (struct cred *) current->real_cred;        struct task_security_struct *tsec;
#ifdef CONFIG_KDP_CRED tsec = &init_sec; tsec->bp_cred = cred; // is not support 5.4 upper version, so we added cred->security = tsec;#else tsec = selinux_cred(cred);#endif tsec->osid = tsec->sid = SECINITSID_KERNEL;}

  内核调用cred_init_securityinit进程进行初始化,后续子进程将会继承struct task_security_struct指针。当cred需要更改时同样使用prepare_ro_creds进行处理。

drivers/uh/kdp.cstruct cred *prepare_ro_creds(struct cred *old, int kdp_cmd, u64 p){        memset((void *)&param_data, 0, sizeof(struct cred_param));        param_data.cred = &temp_old;        param_data.cred_ro = new_ro;        param_data.use_cnt_ptr = use_cnt_ptr;        param_data.sec_ptr = tsec;        param_data.type = kdp_cmd;        param_data.use_cnt = (u64)p;
uh_call(UH_APP_KDP, PREPARE_RO_CRED, (u64)&param_data, (u64)current, (u64)&init_cred, (u64)&init_cred_kdp);}

 对应的el2函数接口为:

通过rkp_assign_secptrcred结构体的CRED_SECURITY_OFFSET偏移进行赋值。

Rkp加入这三个指针保护的目的是做完整性检查,在LSM框架里调用hook钩子之前加入判断语句:

security/security.c#define call_void_hook(FUNC, ...)                               \        do {                                                    \                struct security_hook_list *P;                   \                                                                \                if(security_integrity_current()) break;         \                hlist_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; \ hlist_for_each_entry(P, &security_hook_heads.FUNC, list) { \ RC = P->hook.FUNC(__VA_ARGS__); \ if (RC != 0) \ break; \ } \ } while (0); \ RC; \})
drivers/uh/kdp.cint security_integrity_current(void){ const struct cred *cur_cred = current_cred();
rcu_read_lock(); if (kdp_enable && (is_kdp_invalid_cred_sp((u64)cur_cred, (u64)cur_cred->security) || cmp_sec_integrity(cur_cred, current->mm)#ifdef CONFIG_KDP_NS || cmp_ns_integrity())) {#else )) {#endif rcu_read_unlock(); panic("KDP CRED PROTECTION VIOLATION\n"); } rcu_read_unlock(); return 0;}
static inline bool is_kdp_invalid_cred_sp(u64 cred, u64 sec_ptr){ if ((u64)tsec->bp_cred != cred) { printk(KERN_ERR, "[KDP] %s: tesc->bp_cred: %lx, cred: %lx\n", __func__, (u64)tsec->bp_cred, cred); return true; }
return false;}

 cmp_sec_integrity用来验证cred数据结构的bp_taskbp_pgd指针是否被篡改。

1.2 Namespace保护

   RkpNamespace的保护目前仅局限于mount namespace,对其保护的方式为验证nsproxy->mnt_ns->root字段是否被篡改,同时还对mount挂载点进行了只读保护,不能挂载新的分区以及二进制程序必须从可信的mount点启动。

 首先在vfsmountmount数据结构中都加入了互相指向的backup指针:

include/linux/mount.hstruct vfsmount {        struct dentry *mnt_root;        /* root of the mounted tree */        struct super_block *mnt_sb;     /* pointer to superblock */        int mnt_flags;        ANDROID_KABI_RESERVE(1);        ANDROID_KABI_RESERVE(2);        ANDROID_KABI_RESERVE(3);        ANDROID_KABI_RESERVE(4);        void *data;} __randomize_layout;
#ifdef CONFIG_KDP_NSstruct kdp_vfsmount { struct vfsmount mnt; struct mount *bp_mount; /* pointer to mount*/};#endif
fs/mount.hstruct mount { struct hlist_node mnt_hash; struct hlist_head mnt_stuck_children;} __randomize_layout;
#ifdef CONFIG_KDP_NSstruct kdp_mount { struct mount mount; struct vfsmount *mnt;};#endif

  内核通过调用kdp_mnt_alloc_vfsmount请求el2进行指针设置。

int kdp_mnt_alloc_vfsmount(struct mount *mnt){        uh_call(UH_APP_KDP, ALLOC_VFSMOUNT, (u64)vfsmnt, (u64)mnt, 0, 0);
return 0;}

  对应的el2函数操作为:

El2通过rkp_init_nsmount结构的BPMNT_VFSMNT_OFFSET偏移进行赋值。

LSM框架的hook钩子执行时,会调用cmp_ns_integrity进行指针完整性检查:

static unsigned int cmp_ns_integrity(void){        root = (struct kdp_mount *)current->nsproxy->mnt_ns->root;        if (root != (struct kdp_mount *)((struct kdp_vfsmount *)root->mnt)->bp_mount) {                printk(KERN_ERR "[KDP] NameSpace Mismatch %lx != %lx\n nsp: 0x%lx, mnt_ns: 0x%lx\n",                                root, ((struct kdp_vfsmount *)root->mnt)->bp_mount, nsp, nsp->mnt_ns);                return 1;        }
return 0;}

  当内核挂载一个新的文件系统时,调用kdp_do_new_mount->kdp_populate_sb来对指定的白名单分区做只读保护:

static void kdp_populate_sb(char *mount_point, struct vfsmount *mnt){
if (!odm_sb && !strncmp(mount_point, KDP_MOUNT_PRODUCT, KDP_MOUNT_PRODUCT_LEN)) uh_call(UH_APP_KDP, SET_NS_SB_VFSMOUNT, (u64)&odm_sb, (u64)mnt, KDP_SB_ODM, 0); }

    Rkp对以下的super_block结构体做了只读保护:

static struct super_block *rootfs_sb __kdp_ro = NULL;static struct super_block *sys_sb __kdp_ro = NULL;static struct super_block *odm_sb __kdp_ro = NULL;static struct super_block *vendor_sb __kdp_ro = NULL;static struct super_block *art_sb __kdp_ro = NULL;static struct super_block *crypt_sb     __kdp_ro = NULL;static struct super_block *dex2oat_sb   __kdp_ro = NULL;static struct super_block *adbd_sb              __kdp_ro = NULL;

 对应的分区名白名单为:

#define KDP_MOUNT_SYSTEM "/system"#define KDP_MOUNT_SYSTEM2 "/first_stage_ramdisk/system"#define KDP_MOUNT_PRODUCT "/product"#define KDP_MOUNT_VENDOR "/vendor"#define KDP_MOUNT_ART "/com.android.runtime"#define KDP_MOUNT_CRYPT "/com.android.conscrypt"#define KDP_MOUNT_DEX2OAT "/com.android.art"#define KDP_MOUNT_ADBD "/com.android.adbd"

  当内核通过execve执行一个新的二进制程序时,将会调用invalid_drive函数来判断二进制程序是否从以上白名单分区中启动:

int invalid_drive(struct linux_binprm *bprm){        if (!kdp_check_path_mismatch((struct kdp_vfsmount *)vfsmnt)) {                return 0;        }
sb = vfsmnt->mnt_sb;
if (kdp_check_sb_mismatch(sb)) { return 1; }
return 0;}

    kdp_check_path_mismatch忽略了以下白名单程序:

/com.android.runtime/com.android.conscrypt/com.android.art/com.android.adbd

    kdp_check_sb_mismatch检查是否来自以上白名单分区。

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

1.3 二进制程序启动权限限制

    Rkp在内核execve执行一个二进制程序时,对每个二进制在el2层做了一个标记,用于后续进行权限检查。

SYSCALL_DEFINE3(execve,                const char __user *, filename,                const char __user *const __user *, argv,                const char __user *const __user *, envp){#ifdef CONFIG_KDP_CRED                uh_call(UH_APP_KDP, MARK_PPT, (u64)path->name, (u64)current, 0, 0);                if (current->cred->uid.val == 0 || current->cred->gid.val == 0 ||                        current->cred->euid.val == 0 || current->cred->egid.val == 0 ||                        current->cred->suid.val == 0 || current->cred->sgid.val == 0) {                        if (kdp_restrict_fork(path)) {                                pr_warn("RKP_KDP Restricted making process. PID = %d(%s) PPID = %d(%s)\n",                                                current->pid, current->comm,                                                current->parent->pid, current->parent->comm);                                putname(path);                                return -EACCES;                        }                }        }        putname(path);#endif        return do_execve(getname(filename), argv, envp);}

    EL2对应的MARK_PPT操作为:

__int64 __fastcall rkp_mark_ppt(__int64 a1){  if ( !(unsigned int)sub_8000A578(v8, &unk_80020F0C)// /system/bin/    || !(unsigned int)sub_8000A578(v8, aSystemBinAppPr)// /system/bin/app_process32    || !(unsigned int)sub_8000A578(v8, aSystemBinAppPr_0) )// /system/bin/app_process64  {    v10 = 4LL;    goto LABEL_17;  }  if ( !(unsigned int)sub_8000A578(v8, aSystemBinNst) )// /system/bin/nst  {    v10 = 8LL;LABEL_17:    *(_QWORD *)(v6 + 8LL * *(_QWORD *)(CRED_FLAGS_OFFSET + 104)) |= v10;  }
inline int kdp_restrict_fork(struct filename *path){ struct cred *shellcred; const struct cred_kdp *cred_kdp = (const struct cred_kdp *)(current->cred);
if (!strcmp(path->name, "/system/bin/patchoat") || !strcmp(path->name, "/system/bin/idmap2")) { return 0; }
if ((cred_kdp->type) >> 1 & 1) { shellcred = prepare_creds(); if (!shellcred) return 1;
shellcred->uid.val = 2000; shellcred->gid.val = 2000; shellcred->euid.val = 2000; shellcred->egid.val = 2000;
commit_creds(shellcred); } return 0;}

    kdp_restrict_fork先判断二进制是否在白名单内:

/system/bin/patchoat/system/bin/idmap2

  如果cred_kdp->type的第2bit被置位, 则强制二进制程序的uid2000

2 自身防护差距分析

    Rkp在自身防护一些优势,可以大大增加逆向工程分析的难度。

2.1 字符串混淆

    Rkp对输出的字符串进行了哈希计算,大大增加了逆向过程的难度。

if ( !(unsigned int)sub_80001400(v6) || !(unsigned int)sub_80001400(v6 + *(unsigned int *)CRED_FLAGS_OFFSET - 1) )  {    debug_log(76LL, aRkpD5edfb8f, 140LL, "RKP_934c3492 %lx %lx %lx", v6, v6, v6);    sub_80000E9C(v6);    return debug_log(76LL, aRkpD5edfb8f, 536LL, "RKP_b1c3f061 %lx %lx %lx", v6, 0LL, 0LL);  }

2.2 rkp cmd限定使用次数

   Rkp对特定的cmd使用次数进行了限制,比如一些初始化函数只需执行一次, 这样可以防止rop/jop对其进行后续重用。

_int64 __fastcall rkp_main(unsigned __int64 a1, unsigned __int8 *a2){if ( byte_422A8 )                            [1]                            rkp_policy_violation(2u, 0LL, 0LL, 0LL);
if ( (unsigned int)((__int64 (__fastcall *)(__int64))rkp_paging_init)(v21) ) [2] { byte_422A8 = 1;}

    [2]处在rkp_main执行完后对byte_422A8变量设置为1, 当下次rkp_main再次被调用时在[1]处会被执行检查, rkp_policy_violation函数打印Multiple INIT callspanic系统。

2.3 stack canary栈溢出保护

  三星s20使用的高通qhee平台使用了stack canary栈溢出保护机制。

  在一些关键函数的开头插入如下代码:

qword_80094A48地址保存的是rkp启动时产生的一个随机值,用作stack canary,保存在x8寄存器中。

  在函数的末尾插入如下代码:

 重新加载qword_80094A48值到x9寄存器,然后与之前保存的x8寄存器值进行比较,如果发生改变则跳转到loc_8000442DC去执行。

2.4 gadget去除

  三星、苹果在发布的binary程序中,将黑客经常用到的gadget优化掉, 防止rop/jop利用。

阅读原文

跳转微信打开