Table of Contents
1. Samsung KNOX Protect and Bypass
1.1. KASLR
debug 接口泄漏内核地址
1.2. Real-time kernel protection (RKP)
Linux kernel and secure world 的接口,通过 rpk_call() 调用
- 阻止未经授权的特权代码运行
- 阻止用户进程直接访问内核数据
监控一些危险内核数据结构并检测他们是不是被利用或攻击了
敏感操作全部由 rpk_call 调用实现,
- cred 操作: (copy_creds, commit_creds, rkp_override_creds…)
- 内存操作: (exec_mmap, allocate_slab, …)
- 执行操作: (execve…)
- 页表操作: (rkp_assign_pgd, set_pte, set_pmd, set_pud, …)
- mount 操作: (do_new_mount, …)
1.2.1. Kernel code protection
内核在编译的时候都开启了"KERNEL_TEXT_RDONLY", ".text"节区就不可写了, ".data"节区不可执行. 用户代码不能在内核模式下运行(PXN).
1.2.2. Kernel page and page table protection
RKP 为敏感的内核数据和对象提供只读内核页面,只能通过 secure world 来申请,释放,来操作他们的内核页面。这些页的表项应该不会受到页属性篡改的影响。
当内核必须访问受到保护的 PGD/PTE/PMD/PUD,相关函数时,将使用 rpk_call()函数来进入 secure world 来完成
static inline void set_pte(pte_t *ptep, pte_t pte) { #ifdef CONFIG_TIMA_RKP if (pte && rkp_is_pg_dbl_mapped((u64)(pte)) ) { panic("TIMA RKP : Double mapping Detected pte = 0x%llx ptep = %p",(u64)pte, ptep); return; } if (rkp_is_pg_protected((u64)ptep)) { rkp_call(RKP_PTE_SET, (unsigned long)ptep, pte_val(pte), 0, 0, 0); } else { asm volatile("mov x1, %0\n" "mov x2, %1\n" "str x2, [x1]\n" : : "r" (ptep), "r" (pte) : "x1", "x2", "memory" ); if (pte_valid_not_user(pte)) { dsb(ishst); isb(); } } #else *ptep = pte; #endif /* CONFIG_TIMA_RKP */ }
1.2.3. Kernel data protection
内核数据保护基于页保护, 许多危险的全局变量都会放在".rkp.prot.page"节里, 这个节的页面在内核初始化之后是不能被覆盖的.
#define RKP_RO_AREA __attribute__((section (".rkp.prot.page")))
android_kernel_samsung_g9550\fs\namespace.c: 73: RKP_RO_AREA struct super_block *sys_sb; 74: RKP_RO_AREA struct super_block *rootfs_sb; android_kernel_samsung_g9550\include\linux\security.h: 85: #define RKP_RO_AREA __attribute__((section (".rkp.prot.page"))) android_kernel_samsung_g9550\init\main.c: 253: RKP_RO_AREA unsigned int is_boot_recovery = 0; android_kernel_samsung_g9550\kernel\cred.c: 43: RKP_RO_AREA int rkp_cred_enable = 0; 44: RKP_RO_AREA int is_recovery = 0; ... 85 * The initial credentials for the initial task 86 */ 87: RKP_RO_AREA struct cred init_cred = { 88 .usage = ATOMIC_INIT(4), 89 #ifdef CONFIG_DEBUG_CREDENTIALS android_kernel_samsung_g9550\security\selinux\hooks.c: 110 #include<linux/rkp_entry.h> 111 112: RKP_RO_AREA struct task_security_struct init_sec; ... 181 182 #ifdef CONFIG_SECURITY_SELINUX_DEVELOP 183: RKP_RO_AREA int selinux_enforcing; 184 185 static int __init enforcing_setup(char *str) ... 200 201 #ifdef CONFIG_SECURITY_SELINUX_BOOTPARAM 202: RKP_RO_AREA int selinux_enabled = CONFIG_SECURITY_SELINUX_BOOTPARAM_VALUE; 203 204 static int __init selinux_enabled_setup(char *str) ... 217 __setup("selinux=", selinux_enabled_setup); 218 #else 219: RKP_RO_AREA int selinux_enabled = 1; 220 #endif 221 ... 6950 #endif 6951 6952: RKP_RO_AREA static struct security_hook_list selinux_hooks[] = { 6953 LSM_HOOK_INIT(binder_set_context_mgr, selinux_binder_set_context_mgr), 6954 LSM_HOOK_INIT(binder_transaction, selinux_binder_transaction),
1.2.4. Kernel object protection
内核堆上的对象受到 RKP 保护
#define CRED_JAR_RO "cred_jar_ro" // credential of processes #define TSEC_JAR "tsec_jar" // security context #define VFSMNT_JAR "vfsmnt_cache" // struct vfsmount – mount namespace static struct page *allocate_slab(struct kmem_cache *s, gfp_t flags, int node) { // ... #ifdef CONFIG_RKP_KDP if (s->name && (!strcmp(s->name, CRED_JAR_RO) || !strcmp(s->name, TSEC_JAR)|| !strcmp(s->name, VFSMNT_JAR))) { } virt_page = rkp_ro_alloc(); // ... #endif // ... }
这些内核对象在内核或用户模式下都是只读的, 申请, 释放和覆盖都是在 secure world 内完成的.
/** * override_creds - Override the current process's subjective credentials * @new: The credentials to be assigned * * Install a set of temporary override subjective credentials on the current * process, returning the old set for later reversion. */ const struct cred *rkp_override_creds(struct cred **cnew) { const struct cred *old = current->cred; struct cred *new = *cnew; struct cred *new_ro; volatile unsigned int rkp_use_count = rkp_get_usecount(new); void *use_cnt_ptr = NULL; void *tsec = NULL; kdebug("override_creds(%p{%d,%d})", new, atomic_read(&new->usage), read_cred_subscribers(new)); validate_creds(old); validate_creds(new); if(rkp_cred_enable) { cred_param_t cred_param; new_ro = kmem_cache_alloc(cred_jar_ro, GFP_KERNEL); if (!new_ro) panic("override_creds(): kmem_cache_alloc() failed"); use_cnt_ptr = kmem_cache_alloc(usecnt_jar,GFP_KERNEL); if(!use_cnt_ptr) panic("override_creds() : Unable to allocate usage pointer\n"); tsec = kmem_cache_alloc(tsec_jar, GFP_KERNEL); if(!tsec) panic("override_creds() : Unable to allocate security pointer\n"); rkp_cred_fill_params(new,new_ro,use_cnt_ptr,tsec,RKP_CMD_OVRD_CREDS,rkp_use_count); rkp_call(RKP_CMDID(0x46),(unsigned long long)&cred_param,0,0,0,0); rocred_uc_set(new_ro,2); rcu_assign_pointer(current->cred, new_ro); if(!rkp_ro_page((unsigned long)new)){ if(atomic_read(&new->usage) == 1) { rkp_free_security((unsigned long)new->security); kmem_cache_free(cred_jar, (void *)(*cnew)); *cnew = new_ro; } } } else { get_cred(new); alter_cred_subscribers(new, 1); rcu_assign_pointer(current->cred, new); } alter_cred_subscribers(old, -1); kdebug("override_creds() = %p{%d,%d}", old, atomic_read(&old->usage), read_cred_subscribers(old)); return old; }
Credential verifying in secure world:
在 Galaxy S6 时,攻击者可以简单的调用 rkp_override_creds()来绕过内核对象保护并提升权限,但在 S7 或以后版本不再有效了。
RKP 添加了另一种检测方式,来验证提交的新证书(credential)是否合法。
rkp_call(RKP_CMDID(0x46),(unsigned long long)&cred_param,0,0,0,0); rkp 实现位于 hyp.mbn
1.2.5. Uid_checking()
在 adb 和 zygote 启动前,uid_checking 总是返回 ALLOW;
如果大于 1000 则不能将新创建的 credential 修改为小于 1000 的值
在这之后无特权的进程(uid>1000)不能覆盖高权限的证书(credential). 也就是说如果 uid 大于 1000 则不能将新创建的 credential 修改为小于 1000 的值。
这也是 S6 无法直接使用 override_creds 的原因。但是在 S7 上是可以修改内核的 capabilities,但是不能修改 uid.
__int64 __fastcall uid_checking(__int64 new_cred, __int64 old_cred) { __int64 _new; // x20@1 __int64 old; // x19@1 int v4; // w0@1 BOOL v5; // w2@1 int v6; // w0@2 int v7; // w0@3 _new = new_cred; old = old_cred; v4 = check_cred_id_priv(*(_DWORD *)(new_cred + 4LL * uid_offset), *(_DWORD *)(old_cred + 4LL * uid_offset)); v5 = 1; if ( !v4 ) { v6 = check_cred_id_priv(*(_DWORD *)(_new + 4LL * gid_offset), *(_DWORD *)(old + 4LL * gid_offset)); v5 = 1; if ( !v6 ) { v7 = check_cred_id_priv(*(_DWORD *)(_new + 4LL * suid_offset), *(_DWORD *)(old + 4LL * suid_offset)); v5 = 1; if ( !v7 ) v5 = (unsigned __int64)check_cred_id_priv( *(_DWORD *)(_new + 4LL * sgid_offset), *(_DWORD *)(old + 4LL * sgid_offset)) != 0; } } return (unsigned int)v5; } __int64 __fastcall override_priv_uid(unsigned int new_uid, unsigned int old_uid) { return (old_uid > 1000) & (unsigned __int8)(new_uid <= 1000); }
1.2.6. sid_checking()
如果原来 sid > 20 && new_sid <= 19 操作会被禁止
__int64 __fastcall sid_checking(__int64 new_cred, __int64 new_cred_copy, __int64 old_cred, __int64 new_cred_copy_buf) { // ... copy_security_buf = *(_QWORD *)(new_cred_copy_buf + 0x18); if ( copy_security_buf ) { copy_security_pa = virt_to_phys(copy_security_buf); new_cred_pa = virt_to_phys(*(_QWORD *)(v4 + 8 * security_offset)); old_sid = virt_to_phys(*(_QWORD *)(v6 + 8 * security_offset)); if ( !security_pointer_valid(copy_security_pa) && new_cred_pa && old_sid ) { LODWORD(old_sid) = *(_DWORD *)(old_sid + 4); new_sid = *(_DWORD *)(new_cred_pa + 4); if ( byte_8580A2A1[0] && (old_sid = (unsigned int)old_sid, v14 = new_sid, (unsigned int)old_sid > 0x14uLL) && new_sid <= 0x13uLL ) { rkp_debug_log((unsigned __int16 *)"Selinux Priv Escalation !! [assign_creds]", new_sid, old_sid, 0LL); result = err_func(0x14u, v14, old_sid, 0LL); } // ... } // ... } // ... return result; }
- Integrity_checking()
检查当前的 credential 是否属于当前进程,当前的 security context 是否属于当前 credential
int security_integrity_current(void) { 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(); panic("RKP CRED PROTECTION VIOLATION\n"); } return 0; }
signed __int64 intergrity_check() { __int64 caller_thread_info; // x0@1 __int64 caller_ti; // x23@1 signed __int64 caller_thread_info_pa; // x0@1 __int64 caller_ti_pa; // x22@1 __int64 caller_cred; // x20@1 signed __int64 caller_cred_pa; // x0@1 __int64 _cred; // x19@1 signed __int64 result; // x0@2 __int64 security_pa; // x21@3 __int64 bp_cred; // x1@6 caller_thread_info = get_caller_thread_info(); caller_ti = caller_thread_info; caller_thread_info_pa = get_physical_addr(caller_thread_info); caller_ti_pa = caller_thread_info_pa; caller_cred = *(_QWORD *)(caller_thread_info_pa + 8LL * cred_offset); caller_cred_pa = get_physical_addr(*(_QWORD *)(caller_thread_info_pa + 8LL * cred_offset)); _cred = caller_cred_pa; if ( caller_cred_pa ) { security_pa = get_physical_addr(*(_QWORD *)(caller_cred_pa + 8LL * security_offset)); if ( security_pa || !byte_8580A2A1[0] ) { bp_cred = *(_QWORD *)(security_pa + 8LL * *(_QWORD *)&bp_cred_offset); if ( bp_cred == caller_cred && *(_QWORD *)(_cred + 8LL * bp_task_offset) == caller_ti ) { result = 1LL; } // ... } // ... } // ... }
- Data Flow Integrity (DFI)
该方法防止了 init_cred 重用的技巧,current->cred = init_cred;
struct cred { // ... #ifdef CONFIG_RKP_KDP atomic_t *use_cnt; struct task_struct *bp_task; // cred's owner void *bp_pgd; // process's pgd unsigned long long type; #endif /*CONFIG_RKP_KDP*/ };
每次 committing/overriding 新的 credential, secure world RKP 会记录 bp_task
同时也会在 task_security_struct 记录 credential
struct task_security_struct { u32 osid; /* SID prior to last execve */ u32 sid; /* current SID */ u32 exec_sid; /* exec SID */ u32 create_sid; /* fscreate SID */ u32 keycreate_sid; /* keycreate SID */ u32 sockcreate_sid; /* fscreate SID */ #ifdef CONFIG_RKP_KDP void *bp_cred; #endif }; 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))|| (!rkp_ro_page(sp)|| !rkp_ro_page(sp+sizeof(struct task_security_struct)))) { return 1; } if((u64)tsec->bp_cred != cred) { return 1; } return 0; }
- Summary of RKP and DFI
在 RKP 开启后,即使我们可以实现任意内核内存的覆盖,但是我们也不能做到如下三点:
- 在内核模式下操作证书(credentials)和安全上下文(security context)
- 当前证书指向 init_cred
- 调用 rkp_override_creds()通知 secure world 帮助我们覆盖 uid 0~1000 进程的证书。
但是我们仍然可以做到:
- 从用户模式下调用内核函数进行劫持 ptmx_fops->check_flags(intflag).
- 覆盖所有内核的证书(cred->cap_**)
- 覆盖其他未被保护的内核数据。
1.2.7. SELinux enhancement
1.2.8. Removed selinux_enforcing
selinux_enforcing = 0 即可关闭 selinux,但是 samsung 通过禁用编译选项"CONFIG_SECURITY_SELINUX_DEVELOP"将其删除
#ifdef CONFIG_SECURITY_SELINUX_DEVELOP extern int selinux_enforcing; #else #define selinux_enforcing 1 #endif
- Disability of policy reloading
系统初始化后, init 进程不再 reload SELinux policy, 这就意味着系统启动后,将当前进程的 domain 改为 init 重新加载 selinux policy 的方法将失效。
- Removed support of permissive domain
如果可以重新 reload selinux policy,并将当前进程改为 permissive domain,即可绕过 selinux
删除了 permissive domain, 如果进程的 domain 是 permissive, 直接返回-EACCES
static noinline int avc_denied(u32 ssid, u32 tsid, u16 tclass, u32 requested, u8 driver, u8 xperm, unsigned flags, struct av_decision *avd) { if (flags & AVC_STRICT) return -EACCES; // ... #ifdef CONFIG_ALWAYS_ENFORCE if (!(avd->flags & AVD_FLAGS_PERMISSIVE)) #else if (selinux_enforcing && !(avd->flags & AVD_FLAGS_PERMISSIVE)) #endif // ] SEC_SELINUX_PORTING_COMMON return -EACCES; // ... }
1.3. Bypassing techniques
KASLR -> kernel rwcap -> overwrite ptmx_fops -> overwrite addr_limit -> DFI -> SELINUX -> root
1.3.1. KASLR bypassing
1.3.2. DFI bypassing
让内核创建一个线程,符合 RKP 和 DFI 的所有规则
ptmx_fops->check_flags(x0):
char poweroff_cmd[POWEROFF_CMD_PATH_LEN] = "/sbin/poweroff"; static const char reboot_cmd[] = "/sbin/reboot"; static int run_cmd(const char *cmd) { char **argv; static char *envp[] = { "HOME=/", "PATH=/sbin:/bin:/usr/sbin:/usr/bin", NULL }; int ret; argv = argv_split(GFP_KERNEL, cmd, NULL); if (argv) { ret = call_usermodehelper(argv[0], argv, envp, UMH_WAIT_EXEC); argv_free(argv); } else { ret = -ENOMEM; } return ret; } static int __orderly_poweroff(bool force) { int ret; ret = run_cmd(poweroff_cmd); // ... return ret; } static bool poweroff_force; static void poweroff_work_func(struct work_struct *work) { __orderly_poweroff(poweroff_force); } static DECLARE_WORK(poweroff_work, poweroff_work_func); /** * orderly_poweroff - Trigger an orderly system poweroff * @force: force poweroff if command execution fails * * This may be called from any context to trigger a system shutdown. * If the orderly shutdown fails, it will force an immediate shutdown. */ void orderly_poweroff(bool force) { if (force) /* do not override the pending "true" */ poweroff_force = true; schedule_work(&poweroff_work); }
- Call rpk_override_creds() via ptmx_fops->check_flags() to override own cred to gain full kernel capabilities
- Overwrite poweroff_cmd with "/data/data/***/expl"
- Call orderly_poweroff() via ptmx_fops->check_flags()
- Modify expl thread_info->address_limit
expl call rpk_override_creds() to change its context from u:r:kernel:s0 to u:r:init:s0
but….
poweroff 调用流程:
/* orderly_poweroff(force) -> poweroff_work_func(...) -> __orderly_poweroff(force) -> run_cmd(cmd) -> call_usermodehelper(...) -> call_usermodehelper_setup(...) -> call_usermodehelper_exec_work(...) -> call_usermodehelper_exec_async(...) -> do_execve(...) -> do_execveat_common(...) -> exec_binprm(bprm) -> search_binary_handler(...) -> load_elf_binary(fmt->load_binary(bprm)) -> flush_old_exec(bprm)-> */ int flush_old_exec(struct linux_binprm * bprm) { // ... #ifdef CONFIG_RKP_NS_PROT if(rkp_cred_enable && is_rkp_priv_task() && invalid_drive(bprm)) { panic("\n Illegal Execution file_name #%s#\n",bprm->filename); } #endif /*CONFIG_RKP_NS_PROT*/ // ... } #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; } 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((!is_boot_recovery) && sb != rootfs_sb && sb != sys_sb) { printk("\n Superblock Mismatch #%s# vfsmnt #%p#sb #%p:%p:%p#\n", bprm->filename,vfsmnt,sb,rootfs_sb,sys_sb); return 1; } return 0; }
必须是 rootfs(/) 和 system(/system) 目录下文件才可以
- SELinux bypassing
- patch security_hook_heads
security_hook_heads 可读写
ROM:0000000000251E60 security_transfer_creds ; CODE XREF: key_change_session_keyring+134p ROM:0000000000251E60 var_s0 = 0 ROM:0000000000251E60 var_s10 = 0x10 ROM:0000000000251E60 var_s20 = 0x20 ROM:0000000000251E60 ROM:0000000000251E60 STP X29, X30, [SP,#-0x30+var_s0]! ROM:0000000000251E64 MOV X29, SP ROM:0000000000251E68 STP X21, X22, [SP,#var_s20] ROM:0000000000251E6C MOV X21, X0 ROM:0000000000251E70 ADRP X0, #0x17C9000 ROM:0000000000251E74 ADD X0, X0, #0x428 ROM:0000000000251E78 MOV X22, X1 ROM:0000000000251E7C STP X19, X20, [SP,#var_s10] ROM:0000000000251E80 ADD X19, X0, #0x4E0 ROM:0000000000251E84 LDR X20, [X0,#0x4E0] ROM:0000000000251E88 ROM:0000000000251E88 loc_251E88 ; CODE XREF: security_transfer_creds+44j ROM:0000000000251E88 CMP X20, X19 ROM:0000000000251E8C B.EQ loc_251EA8 ROM:0000000000251E90 LDR X2, [X20,#0x18] ROM:0000000000251E94 MOV X0, X21 ROM:0000000000251E98 MOV X1, X22 ROM:0000000000251E9C BL jopp_springboard_blr_x2 ROM:0000000000251EA0 LDR X20, [X20] ROM:0000000000251EA4 B loc_251E88 ROM:0000000000251EA8 ; --------------------------------------------------------------------------- ROM:0000000000251EA8 ROM:0000000000251EA8 loc_251EA8 ; CODE XREF: security_transfer_creds+2Cj ROM:0000000000251EA8 LDP X19, X20, [SP,#var_s10] ROM:0000000000251EAC LDP X21, X22, [SP,#var_s20] ROM:0000000000251EB0 LDP X29, X30, [SP+var_s0],#0x30 ROM:0000000000251EB4 RET ROM:0000000000251EB4 ; End of function security_transfer_creds
if (*(&security_hook_heads[x]) == &security_hook_heads[x] ) { bypass }
重定向 crrrent_cred()到可读写内存中,然后修改
void key_change_session_keyring(struct callback_head *twork) { const struct cred *old = current_cred(); struct cred *new = container_of(twork, struct cred, rcu); if (unlikely(current->flags & PF_EXITING)) { put_cred(new); return; } new->uid = old->uid; new->euid = old->euid; new->suid = old->suid; new->fsuid = old->fsuid; new->gid = old->gid; new->egid = old->egid; new->sgid = old->sgid; new->fsgid = old->fsgid; new->user = get_uid(old->user); new->user_ns = get_user_ns(old->user_ns); new->group_info = get_group_info(old->group_info); new->securebits = old->securebits; new->cap_inheritable = old->cap_inheritable; new->cap_permitted = old->cap_permitted; new->cap_effective = old->cap_effective; new->cap_ambient = old->cap_ambient; new->cap_bset = old->cap_bset; new->jit_keyring = old->jit_keyring; new->thread_keyring = key_get(old->thread_keyring); new->process_keyring = key_get(old->process_keyring); security_transfer_creds(new, old); // will bypass check commit_creds(new); }
1.3.3. rkp_override_creds
rkp_is_protected 检测 cred 是否在 rkp 保护内存中,否则返回 0
const struct cred *rkp_override_creds(struct cred **cnew) #else const struct cred *override_creds(const struct cred *new) #endif /* CONFIG_RKP_KDP */ { const struct cred *old = current->cred; #ifdef CONFIG_RKP_KDP struct cred *new = *cnew; struct cred *new_ro; volatile unsigned int rkp_use_count = rkp_get_usecount(new); void *use_cnt_ptr = NULL; void *tsec = NULL; #endif /* CONFIG_RKP_KDP */ // ... } unsigned int rkp_get_usecount(struct cred *cred) { if (rkp_ro_page((unsigned long )cred)) return (unsigned int)rocred_uc_read(cred); else return atomic_read(&cred->usage); } /*Check whether the address belong to Cred Area*/ static inline u8 rkp_ro_page(unsigned long addr) { if(!rkp_cred_enable) return (u8)0; if((addr == ((unsigned long)&init_cred)) || (addr == ((unsigned long)&init_sec))) return (u8)1; else return rkp_is_pg_protected(addr); } #define rkp_is_pg_protected(va) rkp_is_protected(va,__pa(va),(u64 *)rkp_pgt_bitmap,1) static inline u8 rkp_is_protected(u64 va, u64 pa, u64 *base_addr,u64 type) { u64 phys_addr = pa & (RKP_PHYS_ADDR_MASK); u64 index = rkp_get_sys_index((phys_addr>>PAGE_SHIFT)); u64 *p = base_addr; u64 tmp = (index>>6); u64 rindex; u8 val; /* We are going to ignore if input address NOT belong to DRAM area */ if((phys_addr < PHYS_OFFSET) || (index ==(~0ULL)) || (phys_addr > RKP_PHYS_OFFSET_MAX)) { return 0; } p += (tmp); rindex = index % 64; val = (((*p) & (1ULL<<rindex))?1:0); return val; }
Created: 2018-07-17 Tue 15:51
没有评论:
发表评论