在本系列文章中,我们将为读者深入讲解三星手机的内核防护技术。在上一篇文章中,我们为读者介绍了与RKP/KDP相关的一些命令,在本文中,将继续为读者呈现更多精彩内容!
凭证的保护机制
内核中的相关结构体
我们已经看到,struct cred和struct task_security_struct现在被分配在只读页中,因此,内核也无法对其进行修改。同时,这些结构体还增加了新的字段,这些字段将被用于数据流完整性保护。同时,每个结构体都有一个“反向指针(back-pointer)”,也就是指向所属结构体的指针:
- struct task_struct对应于struct cred;
- struct cred对应于struct task_security_struct。
实际上,struct cred也具有一个指向所属任务的PGD的反向指针,以及一个“使用计数器”,用于防止重复使用另一个task_struct的struct cred(尤其是init的凭证)。
// from include/linux/cred.h
struct cred {
// ...
atomic_t *use_cnt;
struct task_struct *bp_task;
void *bp_pgd;
unsigned long long type;
} __randomize_layout;
// from security/selinux/include/objsec.h
struct task_security_struct {
// ...
void *bp_cred;
};
对于这些值来说,都应该通过调用security_integrity_current对每个SELinux hook进行相应的验证,但在我们的研究设备中并没有实现这一功能,也许是他们忘了。因此,我们只好从另一个提供该功能的设备中获取了相关的源代码。
// from 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;\
})
// from security/selinux/hooks.c
static inline unsigned int cmp_sec_integrity(const struct cred *cred,struct mm_struct *mm)
{
return ((cred->bp_task != current) ||
(mm && (!( in_interrupt() || in_softirq())) &&
(cred->bp_pgd != swapper_pg_dir) &&
(mm->pgd != cred->bp_pgd)));
}
extern struct cred init_cred;
static inline unsigned int rkp_is_valid_cred_sp(u64 cred,u64 sp)
{
struct task_security_struct *tsec = (struct task_security_struct *)sp;
if((cred == (u64)&init_cred) &&
( sp == (u64)&init_sec)){
return 0;
}
if(!rkp_ro_page(cred)|| !rkp_ro_page(cred+sizeof(struct cred)-1)||
(!rkp_ro_page(sp)|| !rkp_ro_page(sp+sizeof(struct task_security_struct)-1))) {
return 1;
}
if((u64)tsec->bp_cred != cred) {
return 1;
}
return 0;
}
inline void rkp_print_debug(void)
{
u64 pgd;
struct cred *cred;
pgd = (u64)(current->mm?current->mm->pgd:swapper_pg_dir);
cred = (struct cred *)current_cred();
printk(KERN_ERR"\n RKP44 cred = %p bp_task = %p bp_pgd = %p pgd = %llx stat = #%d# task = %p mm = %p \n",cred,cred->bp_task,cred->bp_pgd,pgd,(int)rkp_ro_page((unsigned long long)cred),current,current->mm);
//printk(KERN_ERR"\n RKP44_1 uid = %d gid = %d euid = %degid = %d \n",(u32)cred->uid,(u32)cred->gid,(u32)cred->euid,(u32)cred->egid);
printk(KERN_ERR"\n RKP44_2 Cred %llx #%d# #%d# Sec ptr %llx #%d# #%d#\n",(u64)cred,rkp_ro_page((u64)cred),rkp_ro_page((u64)cred+sizeof(struct cred)),(u64)cred->security, rkp_ro_page((u64)cred->security),rkp_ro_page((u64)cred->security+sizeof(struct task_security_struct)));
}
/* Main function to verify cred security context of a process */
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;
}
unsigned int rkp_get_task_sec_size(void)
{
return sizeof(struct task_security_struct);
}
unsigned int rkp_get_offset_bp_cred(void)
{
return offsetof(struct task_security_struct,bp_cred);
}
我们可以看到,security_integrity_current将执行下列操作:
- 检查当前的cred和task_security_struct是否属于init。
- 检查当前cred和task_security_struct的起始/结束地址是否位于RO页中。
- 检查task_security_struct的反向指针是否指向cred。
- 检查cred的反向指针是否指向当前的task_struct。
- 检查cred的PGD反向指针是不是swapper_pg_dir。
- 检查cred的PGD bp是否是当前mm_struct的PDG;
- 对挂载命名空间进行相应的检查,这一点我们将在后面看到。
修改struct cred
如前所述,内核每次要修改进程的struct cred时,都需要调用RKP。在执行内核操作之前,RKP会做一些必要的检查。而为了完成这些检查,它需要知道task_struct和cred结构体的各个字段的偏移量。
在内核端,该命令是由init/main.c中的kdp_init函数执行的。
// from init/main.c
void kdp_init(void)
{
kdp_init_t cred;
cred.credSize= sizeof(struct cred);
cred.sp_size= rkp_get_task_sec_size();
cred.pgd_mm= offsetof(struct mm_struct,pgd);
cred.uid_cred= offsetof(struct cred,uid);
cred.euid_cred= offsetof(struct cred,euid);
cred.gid_cred= offsetof(struct cred,gid);
cred.egid_cred= offsetof(struct cred,egid);
cred.bp_pgd_cred= offsetof(struct cred,bp_pgd);
cred.bp_task_cred= offsetof(struct cred,bp_task);
cred.type_cred= offsetof(struct cred,type);
cred.security_cred= offsetof(struct cred,security);
cred.usage_cred= offsetof(struct cred,use_cnt);
cred.cred_task= offsetof(struct task_struct,cred);
cred.mm_task= offsetof(struct task_struct,mm);
cred.pid_task= offsetof(struct task_struct,pid);
cred.rp_task= offsetof(struct task_struct,real_parent);
cred.comm_task= offsetof(struct task_struct,comm);
cred.bp_cred_secptr= rkp_get_offset_bp_cred();
cred.verifiedbootstate = (u64)verifiedbootstate;
#ifdef CONFIG_SAMSUNG_PRODUCT_SHIP
cred.selinux.ss_initialized_va= (u64)&ss_initialized;
#endif
uh_call(UH_APP_RKP, RKP_KDP_X40, (u64)&cred, 0, 0, 0);
}
asmlinkage __visible void __init start_kernel(void)
{
// ...
if (rkp_cred_enable)
kdp_init();
cred_init();
// ...
}
在管理程序端,该命令在rkp_cmd_cred_init函数中进行处理的,该函数将调用rkp_cred_init。
int64_t rkp_cmd_cred_init(saved_regs_t* regs) {
uh_log('L', "rkp.c", 399, "KDP_KDP About to call cred_init");
rkp_cred_init(regs);
return 0;
}
void rkp_cred_init(saved_regs_t* regs) {
// ...
rkp_cred = malloc(0xF0, 0);
cred = rkp_get_pa(regs->x2);
if (cred_inited == 1) {
uh_log('L', "rkp_kdp.c", 1083, "Cannot initialized for Second Time\n");
return;
}
cred_inited = 1;
credSize = cred->credSize;
sp_size = cred->sp_size;
uid_cred = cred->uid_cred;
euid_cred = cred->euid_cred;
gid_cred = cred->gid_cred;
egid_cred = cred->egid_cred;
usage_cred = cred->usage_cred;
bp_pgd_cred = cred->bp_pgd_cred;
bp_task_cred = cred->bp_task_cred;
type_cred = cred->type_cred;
security_cred = cred->security_cred;
bp_cred_secptr = cred->bp_cred_secptr;
if (uid_cred > credSize || euid_cred > credSize || gid_cred > credSize || egid_cred > credSize ||
usage_cred > credSize || bp_pgd_cred > credSize || bp_task_cred > credSize || type_cred > credSize ||
security_cred > credSize || bp_cred_secptr > sp_size) {
uh_log('L', "rkp_kdp.c", 1102, "RKP_9a19e9ca");
return;
}
rkp_cred->CRED_SIZE = cred->credSize;
rkp_cred->SP_SIZE = sp_size;
rkp_cred->CRED_UID_OFFSET = uid_cred >> 2;
rkp_cred->CRED_EUID_OFFSET = euid_cred >> 2;
rkp_cred->CRED_GID_OFFSET = gid_cred >> 2;
rkp_cred->CRED_EGID_OFFSET = egid_cred >> 2;
rkp_cred->TASK_PID_OFFSET = cred->pid_task >> 2;
rkp_cred->TASK_CRED_OFFSET = cred->cred_task >> 3;
rkp_cred->TASK_MM_OFFSET = cred->mm_task >> 3;
rkp_cred->TASK_PARENT_OFFSET = cred->rp_task >> 3;
rkp_cred->TASK_COMM_OFFSET = cred->comm_task >> 3;
rkp_cred->CRED_SECURITY_OFFSET = security_cred >> 3;
rkp_cred->CRED_BP_PGD_OFFSET = bp_pgd_cred >> 3;
rkp_cred->CRED_BP_TASK_OFFSET = bp_task_cred >> 3;
rkp_cred->CRED_FLAGS_OFFSET = type_cred >> 3;
rkp_cred->SEC_BP_CRED_OFFSET = bp_cred_secptr >> 3;
rkp_cred->MM_PGD_OFFSET = cred->pgd_mm >> 3;
rkp_cred->CRED_USE_CNT = usage_cred >> 3;
rkp_cred->VERIFIED_BOOT_STATE = 0;
vbs_va = cred->verifiedbootstate;
if (vbs_va) {
vbs_pa = check_and_convert_kernel_input(vbs_va);
if (vbs_pa != 0) {
rkp_cred->VERIFIED_BOOT_STATE = strcmp(vbs_pa, "orange") == 0;
}
}
rkp_cred->SELINUX = rkp_get_pa(&cred->selinux);
rkp_cred->SS_INITIALIZED_VA = rkp_get_pa(cred->selinux.ss_initialized_va);
uh_log('L', "rkp_kdp.c", 1147, "RKP_4bfa8993 %lx %lx %lx %lx");
}
函数rkp_cred_init将执行下列任务:
- 为包含所有偏移量的结构体rkp_cred分配内存空间。
- 对不同的偏移量进行一些基本的安全检查。
- 将各种偏移量存储到这个rkp_cred结构体中。
- 检查设备是否被解锁(即verifiedbootstate是否为“orange”)。
- 保存ss_initialized的地址(它是由SELinux初始化的)。
修改PGD
当内核要更新struct task_struct的PGD时,会调用RKP来同时更新struct cred中的反向指针。在内核端,这主要发生在进程被复制的时候。
// from fs/exec.c
static int exec_mmap(struct mm_struct *mm)
{
// ...
if(rkp_cred_enable){
uh_call(UH_APP_RKP, RKP_KDP_X43,(u64)current_cred(), (u64)mm->pgd, 0, 0);
}
// ...
}
// from kernel/fork.c
void rkp_assign_pgd(struct task_struct *p)
{
u64 pgd;
pgd = (u64)(p->mm ? p->mm->pgd :swapper_pg_dir);
uh_call(UH_APP_RKP, RKP_KDP_X43, (u64)p->cred, (u64)pgd, 0, 0);
}
static __latent_entropy struct task_struct *copy_process(
unsigned long clone_flags,
unsigned long stack_start,
unsigned long stack_size,
int __user *child_tidptr,
struct pid *pid,
int trace,
unsigned long tls,
int node)
{
// ...
if(rkp_cred_enable)
rkp_assign_pgd(p);
// ...
}
函数rkp_cmd_pgd_assign将会调用rkp_pgd_assign函数。
int64_t rkp_cmd_pgd_assign(saved_regs_t* regs) {
rkp_pgd_assign(regs);
return 0;
}
int64_t rkp_phys_map_verify_cred(uint64_t cred) {
// ...
if (!cred)
return 1;
if (cred != cred / CRED_BUFF_SIZE * CRED_BUFF_SIZE)
return 1;
rkp_phys_map_lock(cred);
if (is_phys_map_cred(cred)) {
uh_log('L', "rkp_kdp.c", 127, "physmap verification failed !!!!! %lx %lx %lx", cred, cred, cred);
rkp_phys_map_unlock(cred);
return 1;
}
rkp_phys_map_unlock(cred);
return 0;
}
void rkp_pgd_assign(saved_regs_t* regs) {
// ...
cred = rkp_get_pa(regs->x2);
pgd = regs->x3;
if (rkp_phys_map_verify_cred(cred)) {
uh_log('L', "rkp_kdp.c", 146, "rkp_pgd_assign !!%lx %lx %lx", cred, regs->x2, pgd);
return;
}
*(cred + 8 * rkp_cred->CRED_BP_PGD_OFFSET) = pgd;
}
函数rkp_pgd_assign在更新struct cred的bp_pgd字段之前,会检查是否需要将physmap中的struct cred标记为CRED。
修改security字段
与上一条命令类似,当内核需要修改struct cred的security字段时,它也会调用RKP。这发生在struct cred被释放的时候。
// from security/selinux/hooks.c
int rkp_from_tsec_jar(unsigned long addr)
{
static void *objp;
static struct kmem_cache *s;
static struct page *page;
objp = (void *)addr;
if(!objp)
return 0;
page = virt_to_head_page(objp);
s = page->slab_cache;
if(s && s->name) {
if(!strcmp(s->name,"tsec_jar")) {
return 1;
}
}
return 0;
}
int chk_invalid_kern_ptr(u64 tsec)
{
return (((u64)tsec >> 36) != (u64)0xFFFFFFC);
}
void rkp_free_security(unsigned long tsec)
{
if(!tsec ||
chk_invalid_kern_ptr(tsec))
return;
if(rkp_ro_page(tsec) &&
rkp_from_tsec_jar(tsec)){
kmem_cache_free(tsec_jar,(void *)tsec);
}
else {
kfree((void *)tsec);
}
}
static void selinux_cred_free(struct cred *cred)
{
// ...
if (rkp_ro_page((unsigned long)cred)) {
uh_call(UH_APP_RKP, RKP_KDP_X45, (u64) &cred->security, 7, 0, 0);
}
// ...
rkp_free_security((unsigned long)tsec);
// ...
}
之后,函数rkp_cmd_cred_set_security将调用rkp_cred_set_security函数。
int64_t rkp_cmd_cred_set_security(saved_regs_t* regs) {
rkp_cred_set_security(regs);
return 0;
}
int64_t* rkp_cred_set_security(saved_regs_t* regs) {
// ...
cred = rkp_get_pa(regs->x2 - 8 * rkp_cred->CRED_SECURITY_OFFSET);
if (is_phys_map_cred(cred))
return uh_log('L', "rkp_kdp.c", 146, "invalidate_security: invalid cred !!!!! %lx %lx %lx", regs->x2,
regs->x2 - 8 * CRED_SECURITY_OFFSET, CRED_SECURITY_OFFSET);
security = rkp_get_pa(regs->x2);
*security = 7;
return security;
}
rkp_cred_set_security函数在将struct cred的security字段设置为7(毒药值)之前,会检查传给它的地址是否确实是struct cred(在physmap中标记为CRED)。
进程的标记
当进程进行execve系统调用时,将执行额外的检查。
// from fs/exec.c
#define rkp_is_nonroot(x) ((x->cred->type)>>1 & 1)
#define rkp_is_lod(x) ((x->cred->type)>>3 & 1)
#define CHECK_ROOT_UID(x) (x->cred->uid.val == 0 || x->cred->gid.val == 0 || \
x->cred->euid.val == 0 || x->cred->egid.val == 0 || \
x->cred->suid.val == 0 || x->cred->sgid.val == 0)
static int rkp_restrict_fork(struct filename *path)
{
struct cred *shellcred;
if (!strcmp(path->name, "/system/bin/patchoat") ||
!strcmp(path->name, "/system/bin/idmap2")) {
return 0;
}
/* If the Process is from Linux on Dex,
then no need to reduce privilege */
#ifdef CONFIG_LOD_SEC
if(rkp_is_lod(current)){
return 0;
}
#endif
if(rkp_is_nonroot(current)){
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;
}
SYSCALL_DEFINE3(execve,
const char __user *, filename,
const char __user *const __user *, argv,
const char __user *const __user *, envp)
{
struct filename *path = getname(filename);
int error = PTR_ERR(path);
if(IS_ERR(path))
return error;
if(rkp_cred_enable){
uh_call(UH_APP_RKP, RKP_KDP_X4B, (u64)path->name, 0, 0, 0);
}
if(CHECK_ROOT_UID(current) && rkp_cred_enable) {
if(rkp_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);
return do_execve(getname(filename), argv, envp);
}
之后,内核会调用RKP,将可执行文件的路径传给它,并通过设置struct cred的type字段来标记该进程。如果当前任务以root身份执行,那么内核也会调用rkp_restrict_fork函数。
rkp_restrict_fork函数将执行以下操作:
- 如果运行的二进制文件是patchoat或idmap2,则放行。
- 如果进程来自DeX上的Linux,则放行。
- 如果当前进程被标记为非root进程,则为其分配shell用户的凭证。
在管理程序端,rkp_cmd_mark_ppt函数将调用rkp_mark_ppt函数。
int64_t rkp_cmd_mark_ppt(saved_regs_t* regs) {
rkp_mark_ppt(regs);
return 0;
}
void rkp_mark_ppt(saved_regs_t* regs) {
// ...
current_va = rkp_ns_get_current();
current_pa = rkp_get_pa(current_va);
current_cred = rkp_get_pa(*(current_pa + 8 * rkp_cred->TASK_CRED_OFFSET));
name_va = regs->x2;
name_pa = rkp_get_pa(name_va);
if (!current_cred || !name_pa || rkp_phys_map_verify_cred(current_cred)) {
uh_log('L', "rkp_kdp.c", 551, "rkp_mark_ppt NULL Cred OR filename %lx %lx %lx", current_cred, 0, 0);
}
if (!strcmp(name_pa, "/system/bin/adbd") || !strcmp(name_pa, "/system/bin/app_process32") ||
!strcmp(name_pa, "/system/bin/app_process64")) {
*(current_cred + 8 * rkp_cred->CRED_FLAGS_OFFSET) |= CRED_FLAG_MARK_PPT;
}
if (!strcmp(name_pa, "/system/bin/nst"))
*(current_cred + 8 * rkp_cred->CRED_FLAGS_OFFSET) |= CRED_FLAG_LOD;
if (!strcmp(name_pa, "/system/bin/idmap2"))
*(current_cred + 8 * rkp_cred->CRED_FLAGS_OFFSET) &= ~CRED_FLAG_CHILD_PPT;
if (!strcmp(name_pa, "/system/bin/patchoat"))
*(current_cred + 8 * rkp_cred->CRED_FLAGS_OFFSET) &= ~CRED_FLAG_CHILD_PPT;
}
函数rkp_mark_ppt将执行以下操作:
- 对参数进行合理性检查。
- 如果可执行文件是adbd、app_process32或app_process64;
- 则设置标志CRED_FLAG_MARK_PPT (4)。
- 如果可执行文件是nst;
- 则设置标志CRED_FLAG_LOD(8,由rkp_is_lod进行相关的检查)。
- 如果可执行文件是idmap2或patchoat;
- 则取消标志CRED_FLAG_CHILD_PPT(2,由rkp_is_nonroot进行相关的检查)。
凭证的分配
该命令是KDP的核心。当内核需要给struct task_struct分配一个struct cred并进行大量检查以检测特权提升时,将调用该方法。
在内核端,copy_creds、commit_creds和override_creds将调用prepare_ro_creds进行相应的处理,而不是直接设置struct task_struct的cred字段。
函数prepare_ro_creds将执行以下操作:
- 为一个只读类型的struct cred分配内存空间;
- 为一个供“使用计数器”使用的结构体分配内存空间;
- 为一个只读类型的struct task_security_struct分配内存空间;
- 创建内核要分配给RKP的上述3个结构体以及一个读写类型的结构体;
- RKP将验证相关的值,并将数据从RW struct cred复制到RO struct cred;
- prepare_ro_creds返回赋值后的RO struct cred。
// from include/linux/cred.h
typedef struct cred_param{
struct cred *cred;
struct cred *cred_ro;
void *use_cnt_ptr;
void *sec_ptr;
unsigned long type;
union {
void *task_ptr;
u64 use_cnt;
};
}cred_param_t;
enum {
RKP_CMD_COPY_CREDS = 0,
RKP_CMD_CMMIT_CREDS,
RKP_CMD_OVRD_CREDS,
};
#define override_creds(x) rkp_override_creds(&x)
#define rkp_cred_fill_params(crd,crd_ro,uptr,tsec,rkp_cmd_type,rkp_use_cnt) \
do {\
cred_param.cred = crd;\
cred_param.cred_ro = crd_ro;\
cred_param.use_cnt_ptr = uptr;\
cred_param.sec_ptr= tsec;\
cred_param.type = rkp_cmd_type;\
cred_param.use_cnt = (u64)rkp_use_cnt;\
} while(0)
// from kernel/cred.c
static struct cred *prepare_ro_creds(struct cred *old, int kdp_cmd, u64 p)
{
u64 pgd =(u64)(current->mm?current->mm->pgd:swapper_pg_dir);
struct cred *new_ro;
void *use_cnt_ptr = NULL;
void *rcu_ptr = NULL;
void *tsec = NULL;
cred_param_t cred_param;
new_ro = kmem_cache_alloc(cred_jar_ro, GFP_KERNEL);
if (!new_ro)
panic("[%d] : kmem_cache_alloc() failed", kdp_cmd);
use_cnt_ptr = kmem_cache_alloc(usecnt_jar,GFP_KERNEL);
if (!use_cnt_ptr)
panic("[%d] : Unable to allocate usage pointer\n", kdp_cmd);
rcu_ptr = get_usecnt_rcu(use_cnt_ptr);
((struct ro_rcu_head*)rcu_ptr)->bp_cred = (void *)new_ro;
tsec = kmem_cache_alloc(tsec_jar, GFP_KERNEL);
if (!tsec)
panic("[%d] : Unable to allocate security pointer\n", kdp_cmd);
rkp_cred_fill_params(old,new_ro,use_cnt_ptr,tsec,kdp_cmd,p);
uh_call(UH_APP_RKP, RKP_KDP_X46, (u64)&cred_param, 0, 0, 0);
if (kdp_cmd == RKP_CMD_COPY_CREDS) {
if ((new_ro->bp_task != (void *)p)
|| new_ro->security != tsec
|| new_ro->use_cnt != use_cnt_ptr) {
panic("[%d]: RKP Call failed task=#%p:%p#, sec=#%p:%p#, usecnt=#%p:%p#", kdp_cmd, new_ro->bp_task,(void *)p,new_ro->security,tsec,new_ro->use_cnt,use_cnt_ptr);
}
}
else {
if ((new_ro->bp_task != current)||
(current->mm
&& new_ro->bp_pgd != (void *)pgd) ||
(new_ro->security != tsec) ||
(new_ro->use_cnt != use_cnt_ptr)) {
panic("[%d]: RKP Call failed task=#%p:%p#, sec=#%p:%p#, usecnt=#%p:%p#, pgd=#%p:%p#", kdp_cmd, new_ro->bp_task,current,new_ro->security,tsec,new_ro->use_cnt,use_cnt_ptr,new_ro->bp_pgd,(void *)pgd);
}
}
rocred_uc_set(new_ro, 2);
set_cred_subscribers(new_ro, 0);
get_group_info(new_ro->group_info);
get_uid(new_ro->user);
get_user_ns(new_ro->user_ns);
#ifdef CONFIG_KEYS
key_get(new_ro->session_keyring);
key_get(new_ro->process_keyring);
key_get(new_ro->thread_keyring);
key_get(new_ro->request_key_auth);
#endif
validate_creds(new_ro);
return new_ro;
}
int copy_creds(struct task_struct *p, unsigned long clone_flags)
{
// ...
/*
* Disabling cred sharing among the same thread group. This
* is needed because we only added one back pointer in cred.
*
* This should NOT in any way change kernel logic, if we think about what
* happens when a thread needs to change its credentials: it will just
* create a new one, while all other threads in the same thread group still
* reference the old one, whose reference counter decreases by 2.
*/
if(!rkp_cred_enable){
// ...
}
// ...
if(rkp_cred_enable){
p->cred = p->real_cred = prepare_ro_creds(new, RKP_CMD_COPY_CREDS, (u64)p);
put_cred(new);
}
else {
p->cred = p->real_cred = get_cred(new);
alter_cred_subscribers(new, 2);
validate_creds(new);
}
// ...
}
int commit_creds(struct cred *new)
{
if (rkp_ro_page((unsigned long)new))
BUG_ON((rocred_uc_read(new)) < 1);
else
// ...
if(rkp_cred_enable) {
struct cred *new_ro;
new_ro = prepare_ro_creds(new, RKP_CMD_CMMIT_CREDS, 0);
rcu_assign_pointer(task->real_cred, new_ro);
rcu_assign_pointer(task->cred, new_ro);
}
else {
rcu_assign_pointer(task->real_cred, new);
rcu_assign_pointer(task->cred, new);
}
// ...
if (rkp_cred_enable){
put_cred(new);
put_cred(new);
}
// ...
}
const struct cred *rkp_override_creds(struct cred **cnew)
{
// ...
struct cred *new = *cnew;
// ...
if(rkp_cred_enable) {
volatile unsigned int rkp_use_count = rkp_get_usecount(new);
struct cred *new_ro;
new_ro = prepare_ro_creds(new, RKP_CMD_OVRD_CREDS, rkp_use_count);
*cnew = new_ro;
rcu_assign_pointer(current->cred, new_ro);
put_cred(new);
}
else {
get_cred(new);
alter_cred_subscribers(new, 1);
rcu_assign_pointer(current->cred, new);
}
// ...
}
在管理程序端,rkp_cmd_assign_creds函数将调用rkp_assign_creds函数。
int64_t rkp_cmd_assign_creds(saved_regs_t* regs) {
rkp_assign_creds(regs);
return 0;
}
uint64_t rkp_ns_get_current() {
if (get_sp_sel())
return get_sp_el0();
return get_sp();
}
int64_t check_pe_id(uint32_t targ_id, uint32_t curr_id) {
return curr_id > 1000 && targ_id <= 1000;
}
bool rkp_check_pe(int64_t targ_cred, int64_t curr_cred) {
// ..
curr_uid = *(curr_cred + 4 * rkp_cred->CRED_UID_OFFSET);
targ_uid = *(targ_cred + 4 * rkp_cred->CRED_UID_OFFSET);
if (check_pe_id(targ_uid, curr_uid))
return 1;
curr_gid = *(curr_cred + 4 * rkp_cred->CRED_GID_OFFSET);
targ_gid = *(targ_cred + 4 * rkp_cred->CRED_GID_OFFSET);
if (check_pe_id(targ_gid, curr_gid))
return 1;
curr_ueid = *(curr_cred + 4 * rkp_cred->CRED_EUID_OFFSET);
targ_euid = *(targ_cred + 4 * rkp_cred->CRED_EUID_OFFSET);
if (targ_euid < curr_uid && check_pe_id(targ_euid, curr_euid))
return 1;
curr_egid = *(curr_cred + 4 * rkp_cred->CRED_EGID_OFFSET);
targ_egid = *(targ_cred + 4 * rkp_cred->CRED_EGID_OFFSET);
if (targ_egid < curr_gid && check_pe_id(targ_egid, curr_egid))
return 1;
return 0;
}
int64_t from_zyg_adbd(int64_t curr_task, int64_t curr_cred) {
// ...
curr_flags = *(curr_cred + 8 * rkp_cred->CRED_FLAGS_OFFSET);
if ((curr_flags & 2) != 0)
return 1;
task = curr_task;
while (1) {
task_pid = *(task + 4 * rkp_cred->TASK_PID_OFFSET);
if (!task_pid)
return 0;
task_comm = task + 8 * rkp_cred->TASK_COMM_OFFSET;
memcpy(comm, task_comm, sizeof(comm));
if (!strcmp(comm, "zygote") || !strcmp(comm, "zygote64") || !strcmp(comm, "adbd"))
return 1;
parent_va = *(task + 8 * rkp_cred->TASK_PARENT_OFFSET);
task = parent_pa = rkp_get_pa(parent_va);
}
}
int64_t rkp_privilege_escalation(int64_t targ_cred, int64_t curr_cred, int64_t flag) {
uh_log('L', "rkp_kdp.c", 461, "Priv Escalation - Current %lx %lx %lx", *(curr_cred + 4 * rkp_cred->CRED_UID_OFFSET),
*(curr_cred + 4 * rkp_cred->CRED_GID_OFFSET), *(curr_cred + 4 * rkp_cred->CRED_EGID_OFFSET));
uh_log('L', "rkp_kdp.c", 462, "Priv Escalation - Passed %lx %lx %lx", *(targ_cred + 4 * rkp_cred->CRED_UID_OFFSET),
*(targ_cred + 4 * rkp_cred->CRED_GID_OFFSET), *(targ_cred + 4 * rkp_cred->CRED_EGID_OFFSET));
return rkp_policy_violation("KDP Privilege Escalation %lx %lx %lx", targ_cred, curr_cred, flag);
}
bool check_privilege_escalation(int32_t targ_id, int32_t curr_id) {
return ((curr_id - 0x61A80000) <= 0xFFFF && (targ_id - 0x61A80000) > 0xFFFF && targ_id != -1);
}
int64_t chk_invalid_sec_ptr(uint64_t sec_ptr) {
rkp_phys_map_lock(sec_ptr);
if (!sec_ptr || !is_phys_map_sec_ptr(sec_ptr) || !is_phys_map_sec_ptr(sec_ptr + rkp_cred->SP_SIZE - 1) ||
sec_ptr != sec_ptr / rkp_cred->SP_BUFF_SIZE * rkp_cred->SP_BUFF_SIZE) {
uh_log('L', "rkp_kdp.c", 186, "Invalid Sec Pointer %lx %lx %lx", is_phys_map_sec_ptr(sec_ptr), sec_ptr,
sec_ptr - sec_ptr / rkp_cred->SP_BUFF_SIZE * rkp_cred->SP_BUFF_SIZE);
rkp_phys_map_unlock(sec_ptr);
return 1;
}
rkp_phys_map_unlock(sec_ptr);
return 0;
}
void rkp_assign_creds(saved_regs_t* regs) {
// ...
cred_param = rkp_get_pa(regs->x2);
if (!cred_param) {
uh_log('L', "rkp_kdp.c", 662, "NULL pData");
return;
}
curr_task_va = rkp_ns_get_current();
curr_task = rkp_get_pa(curr_task_va);
curr_cred_va = *(curr_task + 8 * rkp_cred->TASK_CRED_OFFSET);
curr_cred = rkp_get_pa(curr_cred_va);
targ_cred = rkp_get_pa(cred_param->cred);
targ_cred_ro = rkp_get_pa(cred_param->cred_ro);
curr_secptr_va = *(curr_cred + 8 * rkp_cred->CRED_SECURITY_OFFSET);
curr_secptr = rkp_get_pa(curr_secptr_va);
if (!curr_cred) {
uh_log('L', "rkp_kdp.c", 489, "\nCurrent Cred is NULL %lx %lx %lx\n ", curr_task, curr_task_va, 0);
return rkp_policy_violation("Data Protection Violation %lx %lx %lx", curr_task_va, curr_task, 0);
}
if (!curr_secptr && rkp_deferred_inited) {
uh_log('L', "rkp_kdp.c", 495, "\nCurrent sec_ptr is NULL%lx %lx %lx\n ", curr_task, curr_task_va, curr_cred);
return rkp_policy_violation("Data Protection Violation %lx %lx %lx", curr_task_va, curr_cred, 0);
}
bp_cred_va = *(curr_secptr + 8 * rkp_cred->SEC_BP_CRED_OFFSET);
bp_task_va = *(curr_cred + 8 * rkp_cred->CRED_BP_TASK_OFFSET);
if (bp_cred_va != curr_cred_va || bp_task_va != curr_task_va) {
uh_log('L', "rkp_kdp.c", 502, "\n Integrity Check failed_1%lx %lx %lx\n ", bp_cred_va, curr_cred_va, curr_cred);
uh_log('L', "rkp_kdp.c", 503, "\n Integrity Check failed_2 %lx %lx %lx\n ", bp_task_va, curr_task_va, curr_task);
rkp_policy_violation("KDP Privilege Escalation %lx %lx %lx", bp_cred_va, curr_cred_va, curr_secptr);
return;
}
if (!targ_cred || !targ_cred_ro || rkp_phys_map_verify_cred(targ_cred_ro)) {
uh_log('L', "rkp_kdp.c", 699, "rkp_assign_creds !! %lx %lx", targ_cred_ro, targ_cred);
return;
}
skip_checks = 0;
curr_flags = *(curr_cred + 8 * rkp_cred->CRED_FLAGS_OFFSET);
if ((curr_flags & 8) == 0) {
curr_uid = *(curr_cred + 4 * rkp_cred->CRED_UID_OFFSET);
curr_euid = *(curr_cred + 4 * rkp_cred->CRED_EUID_OFFSET);
curr_gid = *(curr_cred + 4 * rkp_cred->CRED_GID_OFFSET);
curr_egid = *(curr_cred + 4 * rkp_cred->CRED_EGID_OFFSET);
if ((curr_uid & 0xFFFF0000) != 0x61A80000 && (curr_euid & 0xFFFF0000) != 0x61A80000 &&
(curr_gid & 0xFFFF0000) != 0x61A80000 && (curr_egid & 0xFFFF0000) != 0x61A80000) {
if (!rkp_cred->VERIFIED_BOOT_STATE) {
if (rkp_check_pe(targ_cred, curr_cred) && from_zyg_adbd(curr_task, curr_cred)) {
uh_log('L', "rkp_kdp.c", 717, "Priv Escalation! %lx %lx %lx", targ_cred,
*(targ_cred + 8 * rkp_cred->CRED_EUID_OFFSET), *(curr_cred + 8 * rkp_cred->CRED_EUID_OFFSET));
return rkp_privilege_escalation(targ_cred, cred_pa, 1);
}
}
skip_checks = 1;
} else {
*(curr_cred + 8 * rkp_cred->CRED_FLAGS_OFFSET) = curr_flags | CRED_FLAG_LOD;
}
}
if (!skip_checks) {
targ_uid = *(targ_cred + rkp_cred->CRED_UID_OFFSET);
priv_esc = 0;
if (targ_uid != 3003) {
curr_uid = *(cred_pa + 4 * rkp_cred->CRED_UID_OFFSET);
priv_esc = 0;
if (check_privilege_escalation(targ_uid, curr_uid)) {
uh_log('L', "rkp_kdp.c", 382, "\n LOD: uid privilege escalation curr_uid = %ld targ_uid = %ld \n", curr_uid,
targ_uid);
priv_esc = 1;
}
}
targ_euid = *(targ_cred + rkp_cred->CRED_EUID_OFFSET);
if (targ_euid != 3003) {
curr_euid = *(cred_pa + 4 * rkp_cred->CRED_EUID_OFFSET);
if (check_privilege_escalation(targ_euid, curr_euid)) {
uh_log('L', "rkp_kdp.c", 387, "\n LOD: euid privilege escalation curr_euid = %ld targ_euid = %ld \n", curr_euid,
targ_euid);
priv_esc = 1;
}
}
targ_gid = *(targ_cred + rkp_cred->CRED_GID_OFFSET);
if (targ_gid != 3003) {
curr_gid = *(cred_pa + 4 * rkp_cred->CRED_GID_OFFSET);
if (check_privilege_escalation(targ_gid, curr_gid)) {
uh_log('L', "rkp_kdp.c", 392, "\n LOD: Gid privilege escalation curr_gid = %ld targ_gid = %ld \n", curr_gid,
targ_gid);
priv_esc = 1;
}
}
targ_egid = *(targ_cred + rkp_cred->CRED_EGID_OFFSET);
if (targ_egid != 3003) {
curr_egid = *(cred_pa + 4 * rkp_cred->CRED_EGID_OFFSET);
if (check_privilege_escalation(targ_egid, curr_egid)) {
uh_log('L', "rkp_kdp.c", 397, "\n LOD: egid privilege escalation curr_egid = %ld targ_egid = %ld \n", curr_egid,
targ_egid);
priv_esc = 1;
}
}
if (priv_esc) {
uh_log('L', "rkp_kdp.c", 705, "Linux on Dex Priv Escalation! %lx", targ_cred);
if (curr_task) {
curr_comm = curr_task + 8 * rkp_cred->TASK_COMM_OFFSET;
uh_log('L', "rkp_kdp.c", 707, curr_comm);
}
return rkp_privilege_escalation(param_cred_pa, cred_pa, 1);
}
}
memcpy(targ_cred_ro, targ_cred, rkp_cred->CRED_SIZE);
cmd_type = cred_param->type;
*(targ_cred_ro + 8 * rkp_cred->CRED_USE_CNT) = cred_param->use_cnt_ptr;
if (cmd_type != RKP_CMD_COPY_CREDS) {
curr_mm_va = *(current_pa + 8 * rkp_cred->TASK_MM_OFFSET);
if (curr_mm_va) {
curr_mm = rkp_get_pa(curr_mm_va);
curr_pgd_va = *(curr_mm + 8 * rkp_cred->MM_PGD_OFFSET);
} else {
curr_pgd_va = rkp_get_va(get_ttbr1_el1() & 0xFFFFFFFFC000);
}
*(targ_cred_ro + 8 * rkp_cred->CRED_BP_PGD_OFFSET) = curr_pgd_va;
*(targ_cred_ro + 8 * rkp_cred->CRED_BP_TASK_OFFSET) = curr_task_va;
if (cmd_type == RKP_CMD_OVRD_CREDS) {
if (cred_param->use_cnt <= 1)
*(targ_cred_ro + 8 * rkp_cred->CRED_FLAGS_OFFSET) &= ~CRED_FLAG_ORPHAN;
else
*(targ_cred_ro + 8 * rkp_cred->CRED_FLAGS_OFFSET) |= CRED_FLAG_ORPHAN;
}
} else {
*(targ_cred_ro + 8 * rkp_cred->CRED_BP_TASK_OFFSET) = cred_param->task_ptr;
}
sec_ptr_va = cred_param->sec_ptr;
cred_ro_va = cred_param->cred_ro;
if (sec_ptr_va) {
sec_ptr = rkp_get_pa(sec_ptr_va);
targ_secptr_va = *(targ_cred + 8 * rkp_cred->CRED_SECURITY_OFFSET);
targ_secptr = rkp_get_pa(targ_secptr);
if (chk_invalid_sec_ptr(sec_ptr) || !targ_secptr || !curr_secptr) {
uh_log('L', "rkp_kdp.c", 594, "Invalid sec pointer [assign_secptr] %lx %lx %lx", sec_ptr_va, sec_ptr,
targ_secptr);
rkp_policy_violation("Data Protection Violation %lx %lx %lx", sec_ptr_va, targ_secptr, curr_secptr);
} else {
curr_sid = *(curr_secptr + 4);
targ_sid = *(targ_secptr + 4);
if (rkp_deferred_inited && targ_sid <= 19 && curr_sid >= 21) {
uh_log('L', "rkp_kdp.c", 607, "Selinux Priv Escalation !! [assign_secptr] %lx %lx ", targ_sid, curr_sid);
rkp_policy_violation("Data Protection Violation %lx %lx %lx", targ_sid, curr_sid, 0);
} else {
memcpy(sec_ptr, targ_secptr, rkp_cred->SP_SIZE);
*(targ_cred_ro + 8 * rkp_cred->CRED_SECURITY_OFFSET) = sec_ptr_va;
*(sec_ptr + 8 * rkp_cred->SEC_BP_CRED_OFFSET) = cred_ro_va;
}
}
} else {
uh_log('L', "rkp_kdp.c", 583, "Security Pointer is NULL [assign_secptr] %lx", 0);
rkp_policy_violation("Data Protection Violation", 0, 0, 0);
}
if (rkp_cred->VERIFIED_BOOT_STATE)
return;
targ_flags = *(targ_cred_ro + 8 * rkp_creds->CRED_FLAGS_OFFSET);
if ((targ_flags & CRED_FLAG_MARK_PPT) != 0) {
parent_task_va = *(curr_task + 8 * rkp_cred->TASK_PARENT_OFFSET);
parent_task = rkp_get_pa(parent_task_va);
parent_cred_va = *(parent_task + 8 * rkp_cred->TASK_CRED_OFFSET);
parent_cred = rkp_get_pa(parent_cred_va);
parent_flags = *(parent_cred + 8 * rkp_cred->CRED_FLAGS_OFFSET);
if ((parent_flags & CRED_FLAG_MARK_PPT) != 0)
*(targ_cred_ro + 8 * rkp_cred->CRED_FLAGS_OFFSET) |= CRED_FLAG_CHILD_PPT;
}
}
函数rkp_assign_creds将完成下列任务:
- 通过SP_EL0获取当前的task_struct。
- 这就是内核在使用current时的做法。
- (参见arch/arm64/include/asm/current.h)。
- 对参数进行一些安全检查。
- 检查当前struct task_struct的反向指针是否正确。
- 检查当前struct task_security_struct的反向指针是否正确。
- 检查目标RO struct cred在physmap中是否被标记为CRED。
- 如果当前struct cred没有设置任何标志。
- 如果当前UID/EUID/GID/EGID中的任何一个是0x61A8xxxx;
- 则设置标志CRED_FLAG_LOD(8)。
- 如果VERIFIED_BOOT_STATE为0(设备解锁),则跳过下一步检查。
- 否则调用rkp_check_pe和from_zyg_adbd。
- rkp_check_pe(检查权限提升)检查当前ID是否>AID_SYSTEM,目标ID是否<=AID_SYSTEM。同时,它还会检查UID、GID、EUID和EGID。
- 如果当前任务被标记,或者它的父任务是zygote、zygote64或adbd(意味着它是这些任务的子任务),那么from_zyg_adbd将会返回。
- 如果这两个函数都返回true,则调用rkp_privilege_escalation来触发违规。
- 否则,在设备未锁定的情况下,它将跳过下一步检查。
- 它开始对UID/EUID/GID/EGID进行一系列检查。
- 对于每一对ID(当前/目标ID),调用check_privilege_escalation;
- 如果当前ID属于LOD,而目标ID不属于LOD,该函数将返回。
- 如果任何一个调用返回true,则调用rkp_privilege_escalation。
- 现在检查已经完成,它将RW struct cred复制到RO struct cred中。
- 它将RO struct cred的use_cnt字段设置为参数给定的值,如果命令类型是RKP_privilege_escalation,则调用rkp_privilege_escalation。
- 如果命令类型是RKP_CMD_CREDS(即从copy_creds中调用的),则设置RO结构cred的后指针。
- 则将RO struct cred的反向指针设置为struct task_struct。
- 否则,如果命令类型是RKP_CMD_CMMIT_CREDS(即从commit_creds中调用)或RKP_CMD_OVRD_CREDS(即从override_creds中调用);
- 获取当前struct task_struct的mm_struct的PGD,如果是NULL,则从TTBR1_EL1获取PGD。
- 如果是NULL,则从TTBR1_EL1中获取PGD。
- 设置RO struct cred的PGD反向指针。
- 将RO struct cred的PGD反向指针设置为当前任务的反向指针。
- 如果命令类型为RKP_CMD_OVRD_CREDS。
- 如果“使用计数器”的值为0或1,则取消标志CRED_FLAG_ORPHAN(1)。
- 否则,设置标志CRED_FLAG_ORPHAN(1)。
- 调用chk_invalid_sec_ptr对以参数形式给出的struct task_security_struct进行检查。
- 这个函数检查其地址是否在physmap中被标记为SEC_PTR。
- 以及它是否具有预期的大小(SP_BUFF_SIZE的倍数)。
- 检查安全上下文ID是否允许权限升级。
- 如果我们是defer inited的,并且当前SID >= SECINITSID_SYSCTL_NET_UNIX 而目标SID <= SECINITSID_SYSCTL_KERNEL,那么这就是权限升级。
- (SECINITSID_xxx是在security/selinux/include/flask.h中定义的)。
- 它将目标struct task_security_struct复制到对应的RO结构体中。
- 它将RO struct cred的security字段设置为RO的。
- 它将RO struct cred的bp_cred字段设置为RO的。
- 如果设备被解锁,则提前返回。
- 否则,如果目标没有标志CRED_FLAG_MARK_PPT。
- 当前任务的父任务的标志是否为CRED_FLAG_MARK_PPT:
- 是否将目标标记为CRED_FLAG_CHILD_PPT。
小结
在本系列文章中,我们将为读者深入讲解三星手机的内核防护技术。在本文中,我们为读者详细介绍了RKP内核保护机制是如何处理安全凭证的,在后续的文章中,会有更多精彩内容呈现给大家,敬请期待!
(未完待续)