三星RKP内核完整性保护程序分析
wzt 2024-03-06 11:46 上海
本文主要以三星s6与s20二进制为样本对内核完整性保护程序RKP进行分析。
1 功能分析
本文主要以三星s6与s20二进制为样本进行分析。S6在2016年发布,这个版本由于有符号存在,可以大大降低逆向工程分析的难度,对于s20二进制只做部分参考分析。
1.1 Cred保护
内核Struct cred数据结构保存了进程的权限凭证如uid/gid、capability等,是内核漏洞攻击程序进行权限提升必须要更改的数据结构,因此对cred数据结构的保护至关重要。三星rkp在el2限制了cred在el1为只读,当内核对cred进行正常写操作时,通过rkp接口调用el2层函数对其进行写操作。但rkp除了对cred做只读保护外, 还在cred数据结构引入了两个字段bp_task和bp_pgd对cred做完整性校验以及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_task和bp_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 *)¶m_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)¶m_data, (u64)current, (u64)&init_cred, (u64)&init_cred_kdp);}
对应的el2层函数操作为:

通过rkp_assign_creds对cred结构体的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_assign对cred结构体的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;tsec = &init_sec;tsec->bp_cred = cred;// is not support 5.4 upper version, so we addedcred->security = tsec;tsec = selinux_cred(cred);tsec->osid = tsec->sid = SECINITSID_KERNEL;}
内核调用cred_init_security对init进程进行初始化,后续子进程将会继承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 *)¶m_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)¶m_data, (u64)current, (u64)&init_cred, (u64)&init_cred_kdp);}
对应的el2函数接口为:

通过rkp_assign_secptr对cred结构体的CRED_SECURITY_OFFSET偏移进行赋值。
Rkp加入这三个指针保护的目的是做完整性检查,在LSM框架里调用hook钩子之前加入判断语句:
security/security.cdo { \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)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)|| cmp_ns_integrity())) {)) {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_task和bp_pgd指针是否被篡改。
1.2 Namespace保护
Rkp对Namespace的保护目前仅局限于mount namespace,对其保护的方式为验证nsproxy->mnt_ns->root字段是否被篡改,同时还对mount挂载点进行了只读保护,不能挂载新的分区以及二进制程序必须从可信的mount点启动。
首先在vfsmount和mount数据结构中都加入了互相指向的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;struct kdp_vfsmount {struct vfsmount mnt;struct mount *bp_mount; /* pointer to mount*/};fs/mount.hstruct mount {struct hlist_node mnt_hash;struct hlist_head mnt_stuck_children;} __randomize_layout;struct kdp_mount {struct mount mount;struct vfsmount *mnt;};
内核通过调用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_ns对mount结构的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;
对应的分区名白名单为:
当内核通过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_CREDuh_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);#endifreturn 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的第2个bit被置位, 则强制二进制程序的uid为2000。
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 calls”并panic系统。
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利用。