在本系列文章中,我们将为读者深入讲解三星手机的内核防护技术。在上一篇文章中,我们为读者详细介绍了页表的处理过程,并对前面的内容进行了阶段性的总结。在本文中,将继续为读者呈现更多精彩内容!
(接上文)
RKP/KDP命令
我们在前面的文章中已经介绍了RKP是如何完全控制内核页表的。现在我们将考察如何通过将关键的内核数据分配到只读内存页上来保护这些数据。
保护内核数据
全局变量
在内核中,所有需要被RKP保护的全局变量都用__rkp_ro或__kdp_ro进行标注。这些变量被移到了.rkp_ro区段,它属于内核的.rodata区段。
// from include/asm-generic/vmlinux.lds.h
#define RO_DATA_SECTION(align)
// ...
.rkp_ro: AT(ADDR(.rkp_ro) - LOAD_OFFSET) {\
VMLINUX_SYMBOL(__start_rkp_ro) = .;\
*(.rkp_ro)\
VMLINUX_SYMBOL(__stop_rkp_ro) = .;\
VMLINUX_SYMBOL(__start_kdp_ro) = .;\
*(.kdp_ro)\
VMLINUX_SYMBOL(__stop_kdp_ro) = .;\
VMLINUX_SYMBOL(__start_rkp_ro_pgt) = .;\
RKP_RO_PGT\
VMLINUX_SYMBOL(__stop_rkp_ro_pgt) = .;\
}\
// from include/linux/linkage.h
#ifdef CONFIG_UH_RKP
#define __page_aligned_rkp_bss__section(.rkp_bss.page_aligned) __aligned(PAGE_SIZE)
#define __rkp_ro__section(.rkp_ro)
// ...
#endif
#ifdef CONFIG_RKP_KDP
#define __kdp_ro__section(.kdp_ro)
#define __lsm_ro_after_init_kdp __section(.kdp_ro)
// ...
#endif
// from arch/arm64/mm/mmu.c
unsigned long empty_zero_page[PAGE_SIZE / sizeof(unsigned long)] __page_aligned_rkp_bss;
// ...
static pte_t bm_pte[PTRS_PER_PTE] __page_aligned_rkp_bss;
static pmd_t bm_pmd[PTRS_PER_PMD] __page_aligned_rkp_bss __maybe_unused;
static pud_t bm_pud[PTRS_PER_PUD] __page_aligned_rkp_bss __maybe_unused;
// from fs/namespace.c
struct super_block *rootfs_sb __kdp_ro = NULL;
struct super_block *sys_sb __kdp_ro = NULL;
struct super_block *odm_sb __kdp_ro = NULL;
struct super_block *vendor_sb __kdp_ro = NULL;
struct super_block *art_sb __kdp_ro = NULL;
// from init/main.c
int is_recovery __kdp_ro = 0;
// ...
rkp_init_t rkp_init_data __rkp_ro = { /* ... */ };
sparse_bitmap_for_kernel_t* rkp_s_bitmap_ro __rkp_ro = 0;
sparse_bitmap_for_kernel_t* rkp_s_bitmap_dbl __rkp_ro = 0;
sparse_bitmap_for_kernel_t* rkp_s_bitmap_buffer __rkp_ro = 0;
// ...
int __check_verifiedboot __kdp_ro = 0;
// ...
extern int ss_initialized __kdp_ro;
// from kernel/cred.c
int rkp_cred_enable __kdp_ro = 0;
// ...
struct cred init_cred __kdp_ro = { /* ... */ };
// from security/selinux/hooks.c
struct task_security_struct init_sec __kdp_ro;
// ...
//int selinux_enforcing __kdp_ro;
// ...
int selinux_enabled __kdp_ro = 1;
// ...
static struct security_hook_list selinux_hooks[] __lsm_ro_after_init_kdp = { /* ... */ };
// from security/selinux/ss/services.c
int ss_initialized __kdp_ro;
动态分配
除了保护全局变量外,RKP还会保护SLUB分配器的3个缓存,用于保护RKP返回的只读页面,而不是内存页分配器返回的页面。这3个缓存是:
- cred_jar_ro 用于为struct cred分配内存。
- tsec_jar 用于为struct task_security_struct分配内存。
- vfsmnt_cache 用于为struct vfsmount分配内存。
// from include/linux/rkp.h
#define CRED_JAR_RO"cred_jar_ro"
#define TSEC_JAR"tsec_jar"
#define VFSMNT_JAR"vfsmnt_cache"
typedef struct ns_param {
u32 ns_buff_size;
u32 ns_size;
u32 bp_offset;
u32 sb_offset;
u32 flag_offset;
u32 data_offset;
}ns_param_t;
#define rkp_ns_fill_params(nsparam,buff_size,size,bp,sb,flag,data)\
do {\
nsparam.ns_buff_size = (u64)buff_size;\
nsparam.ns_size= (u64)size;\
nsparam.bp_offset = (u64)bp;\
nsparam.sb_offset = (u64)sb;\
nsparam.flag_offset = (u64)flag;\
nsparam.data_offset = (u64)data;\
} while(0)
static inline void dmap_prot(u64 addr,u64 order,u64 val)
{
if(rkp_cred_enable)
uh_call(UH_APP_RKP, RKP_KDP_X4A, order, val, 0, 0);
}
static inline void *rkp_ro_alloc(void){
u64 addr = (u64)uh_call_static(UH_APP_RKP, RKP_RKP_ROBUFFER_ALLOC, 0);
if(!addr)
return 0;
return (void *)__phys_to_virt(addr);
}
static inline void rkp_ro_free(void *free_addr){
uh_call_static(UH_APP_RKP, RKP_RKP_ROBUFFER_FREE, (u64)free_addr);
}
// from mm/slub.c
static inline void set_freepointer(struct kmem_cache *s, void *object, void *fp)
{
// ...
if (rkp_cred_enable && s->name &&
(!strcmp(s->name, CRED_JAR_RO)|| !strcmp(s->name, TSEC_JAR) ||
!strcmp(s->name, VFSMNT_JAR))) {
uh_call(UH_APP_RKP, RKP_KDP_X44, (u64)object, (u64)s->offset,
(u64)freelist_ptr(s, fp, freeptr_addr), 0);
}
// ...
}
static struct page *allocate_slab(struct kmem_cache *s, gfp_t flags, int node)
{
// ...
if (s->name &&
(!strcmp(s->name, CRED_JAR_RO) ||
!strcmp(s->name, TSEC_JAR)||
!strcmp(s->name, VFSMNT_JAR))) {
virt_page = rkp_ro_alloc();
if(!virt_page)
goto def_alloc;
page = virt_to_page(virt_page);
oo = s->min;
} else {
// ...
/*
* We modify the following so that slab alloc for protected data
* types are allocated from our own pool.
*/
if (s->name){
u64 sc,va_page;
va_page = (u64)__va(page_to_phys(page));
if(!strcmp(s->name, CRED_JAR_RO)){
for(sc = 0; sc < (1 << oo_order(oo)) ; sc++) {
uh_call(UH_APP_RKP, RKP_KDP_X50, va_page, 0, 0, 0);
va_page += PAGE_SIZE;
}
}
if(!strcmp(s->name, TSEC_JAR)){
for(sc = 0; sc < (1 << oo_order(oo)) ; sc++) {
uh_call(UH_APP_RKP, RKP_KDP_X4E, va_page, 0, 0, 0);
va_page += PAGE_SIZE;
}
}
if(!strcmp(s->name, VFSMNT_JAR)){
for(sc = 0; sc < (1 << oo_order(oo)) ; sc++) {
uh_call(UH_APP_RKP, RKP_KDP_X4F, va_page, 0, 0, 0);
va_page += PAGE_SIZE;
}
}
}
// ...
dmap_prot((u64)page_to_phys(page),(u64)compound_order(page),1);
// ...
}
static void free_ro_pages(struct kmem_cache *s,struct page *page, int order)
{
unsigned long flags;
unsigned long long sc,va_page;
sc = 0;
va_page = (unsigned long long)__va(page_to_phys(page));
if(is_rkp_ro_page(va_page)){
for(sc = 0; sc < (1 << order); sc++) {
uh_call(UH_APP_RKP, RKP_KDP_X48, va_page, 0, 0, 0);
rkp_ro_free((void *)va_page);
va_page += PAGE_SIZE;
}
return;
}
spin_lock_irqsave(&ro_pages_lock,flags);
for(sc = 0; sc < (1 << order); sc++) {
uh_call(UH_APP_RKP, RKP_KDP_X48, va_page, 0, 0, 0);
va_page += PAGE_SIZE;
}
memcg_uncharge_slab(page, order, s);
__free_pages(page, order);
spin_unlock_irqrestore(&ro_pages_lock,flags);
}
static void __free_slab(struct kmem_cache *s, struct page *page)
{
// ...
dmap_prot((u64)page_to_phys(page),(u64)compound_order(page),0);
// ...
/* We free the protected pages here. */
if (s->name && (!strcmp(s->name, CRED_JAR_RO) ||
!strcmp(s->name, TSEC_JAR) ||
!strcmp(s->name, VFSMNT_JAR))){
free_ro_pages(s,page, order);
return;
}
// ...
}
// from kernel/cred.c
void __init cred_init(void)
{
/* allocate a slab in which we can store credentials */
cred_jar = kmem_cache_create("cred_jar", sizeof(struct cred), 0,
SLAB_HWCACHE_ALIGN|SLAB_PANIC|SLAB_ACCOUNT, NULL);
#ifdefCONFIG_RKP_KDP
if(rkp_cred_enable) {
cred_jar_ro = kmem_cache_create("cred_jar_ro", sizeof(struct cred),
0, SLAB_HWCACHE_ALIGN|SLAB_PANIC|SLAB_ACCOUNT, cred_ctor);
if(!cred_jar_ro) {
panic("Unable to create RO Cred cache\n");
}
tsec_jar = kmem_cache_create("tsec_jar", rkp_get_task_sec_size(),
0, SLAB_HWCACHE_ALIGN|SLAB_PANIC|SLAB_ACCOUNT, sec_ctor);
if(!tsec_jar) {
panic("Unable to create RO security cache\n");
}
usecnt_jar = kmem_cache_create("usecnt_jar", sizeof(atomic_t) + sizeof(struct ro_rcu_head),
0, SLAB_HWCACHE_ALIGN|SLAB_PANIC|SLAB_ACCOUNT, usecnt_ctor);
if(!usecnt_jar) {
panic("Unable to create use count jar\n");
}
uh_call(UH_APP_RKP, RKP_KDP_X42, (u64)cred_jar_ro->size, (u64)tsec_jar->size, 0, 0);
}
#endif/* CONFIG_RKP_KDP */
}
// from fs/namespace.c
void __init mnt_init(void)
{
// ...
vfsmnt_cache = kmem_cache_create("vfsmnt_cache", sizeof(struct vfsmount),
0, SLAB_HWCACHE_ALIGN | SLAB_PANIC, cred_ctor_vfsmount);
if(!vfsmnt_cache)
panic("Failed to allocate vfsmnt_cache \n");
rkp_ns_fill_params(nsparam,vfsmnt_cache->size,sizeof(struct vfsmount),(u64)offsetof(struct vfsmount,bp_mount),
(u64)offsetof(struct vfsmount,mnt_sb),(u64)offsetof(struct vfsmount,mnt_flags),
(u64)offsetof(struct vfsmount,data));
uh_call(UH_APP_RKP, RKP_KDP_X41, (u64)&nsparam, 0, 0, 0);
// ...
}
// from fs/dcache.c
void __init vfs_caches_init(void)
{
// ...
mnt_init();
// ...
}
// from init/main.c
asmlinkage __visible void __init start_kernel(void)
{
// ...
if (rkp_cred_enable)
kdp_init();
cred_init();
// ...
vfs_caches_init();
// ...
}
下面,我们来总结一下SLUB分配器使用了哪些命令。
命令 函数 描述
RKP_RKP_ROBUFFER_ALLOC rkp_cmd_rkp_robuffer_alloc 分配一个只读内存页。
RKP_RKP_ROBUFFER_FREE rkp_cmd_rkp_robuffer_free 释放一个只读内存页。
RKP_KDP_X50 rkp_cmd_set_pages_ro_cred_jar 将一个slab标记为cred_jar。
RKP_KDP_X4E rkp_cmd_set_pages_ro_tsec_jar 将一个slab标记为tsec_jar。
RKP_KDP_X4F rkp_cmd_set_pages_ro_vfsmnt_jar 将一个slab标记为vfsmnt_jar。
RKP_KDP_X48 rkp_cmd_ro_free_pages 解除一个slab的标记。
RKP_KDP_X42 rkp_cmd_assign_cred_size 通知cred对象大小。
RKP_KDP_X41 rkp_cmd_assign_ns_size 通知ns对象的大小。
RKP_KDP_X44 rkp_cmd_cred_set_fp 设置对象内部的freelist指针。
RKP_KDP_X4A rkp_cmd_prot_dble_map 防止双重映射。
int64_t rkp_cmd_rkp_robuffer_alloc(saved_regs_t* regs) {
// ...
page = page_allocator_alloc_page();
ret_p = regs->x2;
if ((ret_p & 1) != 0) {
if (ha1 != 0 || ha2 != 0)
rkp_policy_violation("Setting ha1 or ha2 should be done once");
ret_p &= 0xFFFFFFFFFFFFFFFE;
ha1 = page;
ha2 = page + 8;
}
if (ret_p) {
if (!page)
uh_log('L', "rkp.c", 270, "RKP_8f7b0e12");
*virt_to_phys_el1(ret_p) = page;
}
regs->x0 = page;
return 0;
}
rkp_cmd_rkp_robuffer_alloc只是通过内存页分配器(它使用我们前面看到的“robuf”区域)分配一个内存页。而ha1/ha2仅供RKP测试模块使用。
int64_t rkp_cmd_rkp_robuffer_free(saved_regs_t* regs) {
// ...
if (!regs->x2)
uh_log('D', "rkp.c", 286, "Robuffer Free wrong address");
page = rkp_get_pa(regs->x2);
page_allocator_free_page(page);
return 0;
}
rkp_cmd_rkp_robuffer_alloc只是简单的将内存页释放给内存页分配器。
rkp_cmd_set_pages_ro_cred_jar、rkp_cmd_set_pages_ro_tsec_jar和rkp_cmd_set_pages_ro_tsec_jar将用不同的类型来调用rkp_set_pages_ro。
uint8_t* rkp_set_pages_ro(saved_regs_t* regs, int64_t type) {
// ...
if ((regs->x2 & 0xFFF) != 0)
return uh_log('L', "rkp_kdp.c", 803, "Page not aligned in set_page_ro %lx", regs->x2);
page = rkp_get_pa(regs->x2);
rkp_phys_map_lock(page);
if (rkp_s2_page_change_permission(page, 0x80, 0, 0) == -1) {
uh_log('L', "rkp_kdp.c", 813, "Cred: Unable to set permission %lx %lx %lx", regs->x2, page, 0);
} else {
memset(page, 0xFF, 0x1000);
switch (type) {
case 0:
type = CRED;
break;
case 1:
type = SEC_PTR;
break;
case 2:
type = NS;
break;
}
rkp_phys_map_set(page, type);
return rkp_phys_map_unlock(page);
}
return rkp_phys_map_unlock(page);
}
函数rkp_set_pages_ro将执行以下操作:
- 使用rkp_get_pa将PA转换为VA。
- 使用rkp_s2_page_change_permission将第2阶段的内存页转换为RO。
- 将内存页清零。
- 在physmap中将该页标记为CRED、SEC_PTR或NS。
之后,函数rkp_cmd_ro_free_pages将会调用函数rkp_ro_free_pages。
uint8_t* rkp_ro_free_pages(saved_regs_t* regs) {
// ...
if ((regs->x2 & 0xFFF) != 0)
return uh_log('L', "rkp_kdp.c", 843, "Page not aligned in set_page_ro %lx", regs->x2);
page = rkp_get_pa(regs->x2);
rkp_phys_map_lock(page);
if (!is_phys_map_cred(page) && !is_phys_map_ns(page) && !is_phys_map_sec_ptr(page)) {
uh_log('L', "rkp_kdp.c", 854, "rkp_ro_free_pages : physmap_entry_invalid %lx %lx ", regs->x2, page);
return rkp_phys_map_unlock(page);
}
if (rkp_s2_page_change_permission(page, 0, 1, 0) < 0) {
uh_log('L', "rkp_kdp.c", 862, "rkp_ro_free_pages: Unable to set permission %lx %lx %lx", regs->x2, page);
return rkp_phys_map_unlock(page);
}
memset(page, 0, 0x1000);
rkp_phys_map_set(page, FREE);
return rkp_phys_map_unlock(page);
}
函数rkp_cmd_ro_free_pages将执行以下操作:
- 使用rkp_get_pa将PA转换为VA;
- 验证该页面在physmap中是否标记为CRED、SEC_PTR或NS;
- 在第2阶段中使用rkp_s2_page_change_permission将内存页转换为RWX;
- 将内存页清零;
- 在physmap中将该内存页标记为FREE。
int64_t rkp_cmd_assign_cred_size(saved_regs_t* regs) {
rkp_assign_cred_size(regs);
return 0;
}
函数rkp_cmd_assign_cred_size将会调用rkp_assign_cred_size函数。
int64_t rkp_assign_cred_size(saved_regs_t* regs) {
// ...
cred_jar_size = regs->x2;
rkp_cred->CRED_BUFF_SIZE = cred_jar_size;
tsec_jar_size = regs->x3;
rkp_cred->SP_BUFF_SIZE = tsec_jar_size;
return uh_log('L', "rkp_kdp.c", 1033, "BUFF SIZE %lx %lx %lx", cred_jar_size, tsec_jar_size, 0);
}
函数rkp_assign_cred_size的作用如下所示:
- 将struct cred的大小保存在CRED_BUFF_SIZE中。
- 将struct task_security_struct的大小保存在SP_BUFF_SIZE中。
int64_t rkp_cmd_assign_ns_size(saved_regs_t* regs) {
rkp_assign_ns_size(regs);
return 0;
}
之后,函数rkp_cmd_assign_ns_size将会调用函数rkp_assign_ns_size。
int64_t rkp_assign_ns_size(saved_regs_t* regs) {
// ...
if (!rkp_cred)
return uh_log('W', "rkp_kdp.c", 1041, "RKP_ae6cae81");
nsparam_user = rkp_get_pa(regs->x2);
if (!nsparam_user)
return uh_log('L', "rkp_kdp.c", 1048, "NULL Data: rkp assign_ns_size");
memcpy(&nsparam, nsparam_user, sizeof(nsparam));
ns_buff_size = nsparam.ns_buff_size;
ns_size = nsparam.ns_size;
rkp_cred->NS_BUFF_SIZE = ns_buff_size;
rkp_cred->NS_SIZE = ns_size;
if (nsparam.bp_offset > ns_size)
return uh_log('L', "rkp_kdp.c", 1061, "RKP_9a19e9ca");
sb_offset = nsparam.sb_offset;
if (nsparam.sb_offset > ns_size)
return uh_log('L', "rkp_kdp.c", 1061, "RKP_9a19e9ca");
flag_offset = nsparam.flag_offset;
if (nsparam.flag_offset > ns_size)
return uh_log('L', "rkp_kdp.c", 1061, "RKP_9a19e9ca");
data_offset = nsparam.data_offset;
if (nsparam.data_offset > ns_size)
return uh_log('L', "rkp_kdp.c", 1061, "RKP_9a19e9ca");
rkp_cred->BPMNT_VFSMNT_OFFSET = nsparam.bp_offset >> 3;
rkp_cred->SB_VFSMNT_OFFSET = sb_offset >> 3;
rkp_cred->FLAGS_VFSMNT_OFFSET = flag_offset >> 2;
rkp_cred->DATA_VFSMNT_OFFSET = data_offset >> 3;
uh_log('L', "rkp_kdp.c", 1070, "NS Protection ActivatedBuff_size = %lx ns size = %lx", ns_buff_size, ns_size);
return uh_log('L', "rkp_kdp.c", 1071, "NS %lx %lx %lx %lx", rkp_cred->BPMNT_VFSMNT_OFFSET, rkp_cred->SB_VFSMNT_OFFSET,
rkp_cred->FLAGS_VFSMNT_OFFSET, rkp_cred->DATA_VFSMNT_OFFSET);
}
函数rkp_assign_ns_size的作用如下所示:
- 对struct ns_param的偏移量进行同样的基本安全检查。
- 将各个偏移量保存到全局变量中。
之后,函数rkp_cmd_cred_set_fp将会调用函数rkp_cred_set_fp。
int64_t invalid_cred_fp(int64_t object_pa, uint64_t object_va, int64_t offset) {
rkp_phys_map_lock(object_pa);
if (!is_phys_map_cred(object_pa) ||
object_va && object_va == object_va / rkp_cred->CRED_BUFF_SIZE * rkp_cred->CRED_BUFF_SIZE &&
rkp_cred->CRED_SIZE == offset) {
rkp_phys_map_unlock(object_pa);
return 0;
} else {
rkp_phys_map_unlock(object_pa);
return 1;
}
}
int64_t invalid_sec_ptr_fp(int64_t object_pa, uint64_t object_va, int64_t offset) {
rkp_phys_map_lock(object_pa);
if (!is_phys_map_sec_ptr(object_pa) || object_va &&
object_va == object_va / rkp_cred->SP_BUFF_SIZE * rkp_cred->SP_BUFF_SIZE &&
rkp_cred->SP_SIZE == offset) {
rkp_phys_map_unlock(object_pa);
return 0;
} else {
rkp_phys_map_unlock(object_pa);
return 1;
}
}
int64_t invalid_ns_fp(int64_t object_pa, uint64_t object_va, int64_t offset) {
rkp_phys_map_lock(object_pa);
if (!is_phys_map_ns(object_pa) || object_va &&
object_va == object_va / rkp_cred->NS_BUFF_SIZE * rkp_cred->NS_BUFF_SIZE &&
rkp_cred->NS_SIZE == offset) {
rkp_phys_map_unlock(object_pa);
return 0;
} else {
rkp_phys_map_unlock(object_pa);
return 1;
}
}
void rkp_cred_set_fp(saved_regs_t* regs) {
// ...
object_pa = rkp_get_pa(regs->x2);
offset = regs->x3;
freelist_ptr = regs->x4;
rkp_phys_map_lock(object_pa);
if (!is_phys_map_cred(object_pa) && !is_phys_map_sec_ptr(object_pa) && !is_phys_map_ns(object_pa)) {
uh_log('L', "rkp_kdp.c", 242, "Neither Cred nor Secptr %lx %lx %lx", regs->x2, regs->x3, regs->x4);
is_cred = is_phys_map_cred(object_pa);
is_sec_ptr = is_phys_map_sec_ptr(object_pa);
rkp_policy_violation("Data Protection Violation %lx %lx %lx", is_cred, is_sec_ptr, regs->x4);
rkp_phys_map_unlock(object_pa);
}
rkp_phys_map_unlock(object_pa);
if (freelist_ptr) {
freelist_ptr_pa = rkp_get_pa(freelist_ptr);
rkp_phys_map_lock(freelist_ptr_pa);
if (!is_phys_map_cred(freelist_ptr_pa) && !is_phys_map_sec_ptr(freelist_ptr_pa) &&
!is_phys_map_ns(freelist_ptr_pa)) {
uh_log('L', "rkp_kdp.c", 259, "Invalid Free Pointer %lx %lx %lx", regs->x2, regs->x3, regs->x4);
is_cred = is_phys_map_cred(freelist_ptr_pa);
is_sec_ptr = is_phys_map_sec_ptr(freelist_ptr_pa);
rkp_policy_violation("Data Protection Violation %lx %lx %lx", is_cred, is_sec_ptr, regs->x4);
rkp_phys_map_unlock(vafreelist_ptr_par14);
}
rkp_phys_map_unlock(freelist_ptr_pa);
}
if (invalid_cred_fp(object_pa, regs->x2, offset)) {
uh_log('L', "rkp_kdp.c", 267, "Invalid cred pointer_fp!! %lx %lx %lx", regs->x2, regs->x3, regs->x4);
rkp_policy_violation("Data Protection Violation %lx %lx %lx", regs->x2, regs->x3, regs->x4);
} else if (invalid_sec_ptr_fp(object_pa, regs->x2, offset)) {
uh_log('L', "rkp_kdp.c", 272, "Invalid Security pointer_fp 111 %lx %lx %lx", regs->x2, regs->x3, regs->x4);
is_sec_ptr = is_phys_map_sec_ptr(object_pa);
uh_log('L', "rkp_kdp.c", 273, "Invalid Security pointer_fp 222 %lx %lx %lx %lx %lx", is_sec_ptr, regs->x2,
regs->x2 - regs->x2 / rkp_cred->SP_BUFF_SIZE * rkp_cred->SP_BUFF_SIZE, offset, rkp_cred->SP_SIZE);
rkp_policy_violation("Data Protection Violation %lx %lx %lx", regs->x2, regs->x3, regs->x4);
} else if (invalid_ns_fp(object_pa, regs->x2, offset)) {
uh_log('L', "rkp_kdp.c", 278, "Invalid Namespace pointer_fp!! %lx %lx %lx", regs->x2, regs->x3, regs->x4);
rkp_policy_violation("Data Protection Violation %lx %lx %lx", regs->x2, regs->x3, regs->x4);
} else {
*(offset + object_pa) = freelist_ptr;
}
}
rkp_cred_set_fp的作用是:
- 检查对象在physmap中是否被标记为CRED, SEC_PTR或NS;
- 如果不是就会触发违规。
- 对freelist指针进行同样的检查。
- 检查对象是否按照其类型的预期大小对齐。
- 检查freelist指针的偏移量是否等于预期值。
- 如果这些检查中的任何一项失败,就会触发违规。
- 最后,设置对象内部的freelist指针。
之后,函数rkp_cmd_prot_dble_map将调用函数rkp_prot_dble_map。
saved_regs_t* rkp_prot_dble_map(saved_regs_t* regs) {
// ...
address = regs->x2 & 0xFFFFFFFFF000;
if (!address)
return 0;
val = regs->x4;
if (val > 1) {
uh_log('L', "rkp_kdp.c", 1163, "Invalid op val %lx ", val);
return 0;
}
order = regs->x3;
if (order <= 19) {
offset = 0;
size = 0x1000 << order;
do {
res = rkp_set_map_bitmap(address + offset, val);
if (!res) {
uh_log('L', "rkp_kdp.c", 1169, "Page has no bitmap %lx %lx %lx ", address + offset, val, offset);
}
offset += 0x1000;
} while (offset < size);
}
}
函数rkp_prot_dble_map的作用如下所示:
- 在区域的每一页上调用rkp_set_map_bitmap函数(使用dbl_bitmap)。
- 细心的读者会注意到,内核函数dmap_prot在调用rkp_prot_dble_map时出现了一点问题:由于没有提供addr参数,所以参数都乱了。
修改页表
如果内核需要修改它的页表项,该咋办?在内核端,在arch/arm64/include/asm/pgtable.h的set_pud、set_pmd和set_pte中的每个级别都会出现这种情况。
static inline void set_pud(pud_t *pudp, pud_t pud)
{
#ifdef CONFIG_UH_RKP
if (rkp_is_pg_protected((u64)pudp)) {
uh_call(UH_APP_RKP, RKP_WRITE_PGT1, (u64)pudp, pud_val(pud), 0, 0);
} else {
asm volatile("mov x1, %0\n"
"mov x2, %1\n"
"str x2, [x1]\n"
:
: "r" (pudp), "r" (pud)
: "x1", "x2", "memory");
}
#else
*pudp = pud;
#endif
dsb(ishst);
isb();
}
static inline void set_pmd(pmd_t *pmdp, pmd_t pmd)
{
#ifdef CONFIG_UH_RKP
if (rkp_is_pg_protected((u64)pmdp)) {
uh_call(UH_APP_RKP, RKP_WRITE_PGT2, (u64)pmdp, pmd_val(pmd), 0, 0);
} else {
asm volatile("mov x1, %0\n"
"mov x2, %1\n"
"str x2, [x1]\n"
:
: "r" (pmdp), "r" (pmd)
: "x1", "x2", "memory");
}
#else
*pmdp = pmd;
#endif
dsb(ishst);
isb();
}
static inline void set_pte(pte_t *ptep, pte_t pte)
{
#ifdef CONFIG_UH_RKP
/* bug on double mapping */
BUG_ON(pte_val(pte) && rkp_is_pg_dbl_mapped(pte_val(pte)));
if (rkp_is_pg_protected((u64)ptep)) {
uh_call(UH_APP_RKP, RKP_WRITE_PGT3, (u64)ptep, pte_val(pte), 0, 0);
} else {
asm volatile("mov x1, %0\n"
"mov x2, %1\n"
"str x2, [x1]\n"
:
: "r" (ptep), "r" (pte)
: "x1", "x2", "memory");
}
#else
*ptep = pte;
#endif
/*
* Only if the new pte is valid and kernel, otherwise TLB maintenance
* or update_mmu_cache() have the necessary barriers.
*/
if (pte_valid_not_user(pte)) {
dsb(ishst);
isb();
}
}
这些函数已经被修改。对于所有3个级别,它们会检查它们试图写入的条目是否已经被RKP设置为只读(使用我们之前看到的ro_bitmap),如果是这样的话,它们会使用uh_call函数调用管理程序。每个级别都会使用的命令分别是:
- RKP_WRITE_PGT1
- RKP_WRITE_PGT2
- RKP_WRITE_PGT3
还需要注意的是,set_pte会额外检查(中间)物理地址是否已经使用dbl_bitmap进行过映射,以防止双重映射(将同一个PA映射到两个或多个VA)。
在管理程序端,rkp_cmd_write_pgtx会在递增计数器后调用rkp_lxpgt_write函数。
第一级
uint8_t* rkp_l1pgt_write(uint64_t pudp, int64_t pud_new) {
// ...
pudp_pa = rkp_get_pa(pudp);
pud_old = *pudp_pa;
rkp_phys_map_lock(pudp_pa);
if (!is_phys_map_l1(pudp_pa)) {
if (!rkp_deferred_inited) {
set_entry_of_pgt((int64_t*)pudp_pa, pud_new);
return rkp_phys_map_unlock(pudp_pa);
}
rkp_policy_violation("L1 write wrong page, %lx, %lx", pudp_pa, pud_new);
}
is_kernel = is_phys_map_kernel(pudp_pa);
if (pud_old) {
if ((pud_old & 3) != 3)
rkp_policy_violation("l1_pgt write cannot handle blocks - for old entry, %lx", pudp_pa);
res = rkp_l2pgt_process_table(pud_old & 0xFFFFFFFFF000, (pudp_pa << 27) & 0x7FC0000000, 0);
}
start_addr = 0xFFFFFF8000000000;
if (!is_kernel)
start_addr = 0;
if (pud_new) {
addr = start_addr | (pudp_pa << 27) & 0x7FC0000000;
if ((pud_new & 3) != 3)
rkp_policy_violation("l1_pgt write cannot handle blocks - for new entry, %lx", pud_new);
res = rkp_l2pgt_process_table(pud_new & 0xFFFFFFFFF000, addr, 1);
if (!is_kernel)
set_pxn_bit_of_desc(&pud_new, 1);
if ((pud_new & 3) != 0 && (pud_new & 0xFFFFFFFFF000) == 0)
uh_log('L', "rkp_l1pgt.c", 309, "l1 write zero");
}
if (res) {
uh_log('L', "rkp_l1pgt.c", 316, "L1 write failed, %lx, %lx", pudp_pa, pud_new);
return rkp_phys_map_unlock(pudp_pa);
}
set_entry_of_pgt(pudp_pa, pud_new);
return rkp_phys_map_unlock(pudp_pa);
}
函数rkp_l1pgt_write的作用如下所示:
- 检查该条目是否属于physmap中的L1表。
- 如果不属于,并且我们是defer inited的,就会触发违规。
- 如果不属于,并且我们不是defer inited的,就会触发违规;如果不属于,并且我们没有被defer inited,就会进行相应的修改并返回。
- 如果旧条目不是无效描述符,则返回。
- 如果旧的条目是一个块描述符,则触发违规。
- 否则,调用rkp_l2pgt_process_table函数来处理旧的L2表。
- 调用时,is_alloc=0表示删除。
- 如果新条目不是无效描述符。
- 如果新条目是块描述符,则会触发违规。
- 否则,调用rkp_l2pgt_process_table函数来处理新的L2表。
- 如果新描述符映射到用户空间,则设置其PXN位。
- 最后,它进行相应的修改并返回。
第二级
uint8_t* rkp_l2pgt_write(int64_t pmdp, int64_t pmd_new) {
// ...
pmdp_pa = rkp_get_pa(pmdp);
pmd_old = *pmdp_pa;
rkp_phys_map_lock(pmdp_pa);
if (!is_phys_map_l2(pmdp_pa)) {
if (rkp_deferred_inited) {
uh_log('D', "rkp_l2pgt.c", 236, "l2 is not marked as L2 Type in Physmap, trying to fix it, %lx", pmdp_pa);
} else {
set_entry_of_pgt(pmdp_pa, pmd_new);
return rkp_phys_map_unlock(pmdp_pa);
}
}
is_flag3 = is_phys_map_flag3(pmdp_pa);
is_kernel = is_phys_map_kernel(pmdp_pa);
start_addr = 0xFFFFFF8000000000;
if (!is_kernel)
start_addr = 0;
addr = (pmdp_pa << 18) & 0x3FE00000 | ((is_flag3 & 0x1FF) << 30) | start_addr;
if (pmd_old) {
res = check_single_l2e(pmdp_pa, addr, 0);
if (res < 0) {
uh_log('L', "rkp_l2pgt.c", 254, "Failed in freeing entries under the l2e %lx %lx", pmdp_pa, pmd_new);
uh_log('L', "rkp_l2pgt.c", 276, "l2 write failed, %lx, %lx", pmdp_pa, pmd_new);
return rkp_phys_map_unlock(pmdp_pa);
}
}
if (pmd_new) {
res = check_single_l2e(&pmd_new, addr, 1);
if (res < 0) {
uh_log('L', "rkp_l2pgt.c", 276, "l2 write failed, %lx, %lx", pmdp_pa, pmd_new);
return rkp_phys_map_unlock(pmdp_pa);
}
if ((pmd_new & 3) != 0 && (pmd_new & 0xFFFFFFFFF000) == 0)
uh_log('L', "rkp_l2pgt.c", 269, "l2 write zero, %lx", pmdp_pa);
}
set_entry_of_pgt(pmdp_pa, pmd_new);
return rkp_phys_map_unlock(pmdp_pa);
}
rkp_l2pgt_write函数的作用如下所示:
- 它检查该条目是否属于physmap中的L2表。
- 如果不属于,并且我们是defer inited的,它仍然继续。
- 如果不属于,并且我们不是defer inited的,则进行相应的修改并返回。
- 如果旧的条目不是一个无效的描述符,则返回。
- 调用check_single_l2e来处理旧条目。
- 调用过程中,is_alloc = 0表示删除。
- 如果函数失败,则返回,不进行任何修改。
- 如果新条目不是一个无效的描述符。
- 它调用rkp_l2pgt_process_table对新条目进行处理。
- 如果函数失败,则返回且不做任何修改。
- 最后,进行相应的修改并返回。
第三级
int64_t* rkp_l3pgt_write(uint64_t ptep, int64_t pte_val) {
// ...
ptep_pa = rkp_get_pa(ptep);
rkp_phys_map_lock(ptep_pa);
if (is_phys_map_l3(ptep_pa) || is_phys_map_free(ptep_pa)) {
if ((pte_val & 3) != 3 || get_pxn_bit_of_desc(pte_val, 3))
allowed = 1;
else
allowed = rkp_deferred_inited == 0;
} else {
allowed = 0;
}
rkp_phys_map_unlock(ptep_pa);
cs_enter(&l3pgt_lock);
if (stext_ptep && ptep_pa < stext_ptep && (ptep_pa ^ stext_ptep) <= 0xFFF) {
if (pte_val)
pte_val |= 0x20000000000000;
cs_exit(&l3pgt_lock);
return set_entry_of_pgt(ptep_pa, pte_val);
}
cs_exit(&l3pgt_lock);
if (!allowed) {
pxn_bit = get_pxn_bit_of_desc(pte_val, 3);
return rkp_policy_violation("Write L3 to wrong page type, %lx, %lx, %x", ptep_pa, pte_val, pxn_bit);
}
return set_entry_of_pgt(ptep_pa, pte_val);
}
rkp_l3pgt_write的作用如下所示:
- 检查该条目是否属于L2表或在physmap中是否为空闲的。
- 如果新的描述符是无效的。
- 或者如果我们不是defer inited的。
- 或者如果新描述符的PXN位被设置。
- 那么它就会将allowed设置为1。
- 在所有其他情况下,allowed仍将为0。
- 如果该条目与内核的text中的一个条目在同一个表中。
- 如果设置了描述符的PXN位。
- 则会进行修改并返回。
- 如果allowed为1,则进行修改并返回。
- 否则,就会触发违规。
分配/释放PGD
内核可能需要完成的另一个操作是分配或释放页表目录。在内核端,这是由arch/arm64/mm/pgd.c中的pgd_alloc和pgd_free函数来完成的。
pgd_t *pgd_alloc(struct mm_struct *mm)
{
// ...
pgd_t *ret = NULL;
ret = (pgd_t *) rkp_ro_alloc();
if (!ret) {
if (PGD_SIZE == PAGE_SIZE)
ret = (pgd_t *)__get_free_page(PGALLOC_GFP);
else
ret = kmem_cache_alloc(pgd_cache, PGALLOC_GFP);
}
if(unlikely(!ret)) {
pr_warn("%s: pgd alloc is failed\n", __func__);
return ret;
}
uh_call(UH_APP_RKP, RKP_NEW_PGD, (u64)ret, 0, 0, 0);
return ret;
// ...
}
void pgd_free(struct mm_struct *mm, pgd_t *pgd)
{
// ...
uh_call(UH_APP_RKP, RKP_FREE_PGD, (u64)pgd, 0, 0, 0);
/* if pgd memory come from read only buffer, the put it back */
/*TODO: use a macro*/
if (is_rkp_ro_page((u64)pgd))
rkp_ro_free((void *)pgd);
else {
if (PGD_SIZE == PAGE_SIZE)
free_page((unsigned long)pgd);
else
kmem_cache_free(pgd_cache, pgd);
}
// ...
}
这些函数再次被修改。第一个修改是,它们将使用RKP返回的只读内存页,而不是使用内核的内存页分配器的内存页。这些内存页可以使用rkp_ro_alloc函数进行分配,使用rkp_ro_free函数进行释放。
第二个修改是,它们将通过分别使用RKP_NEW_PGD和RKP_FREE_PGD这两个命令调用uh_call函数,通知RKP新分配或释放的页表目录。
在管理程序端,rkp_cmd_new_pgd和rkp_cmd_free_pgd函数在递增计数器之后分别会调用rkp_l1pgt_new_pgd和rkp_l1pgt_free_pgd函数。
void rkp_l1pgt_new_pgd(saved_regs_t* regs) {
// ...
pgdp = rkp_get_pa(regs->x2) & 0xFFFFFFFFFFFFF000;
if (pgdp == INIT_MM_PGD || pgdp == ID_MAP_PGD || TRAMP_PGD && pgdp == TRAMP_PGD) {
rkp_policy_violation("PGD new value not allowed, pgdp : %lx", pgdp);
} else if (rkp_inited) {
if (rkp_l1pgt_process_table(pgdp, 0, 1) < 0)
uh_log('L', "rkp_l1pgt.c", 383, "l1pgt processing is failed, pgdp : %lx", pgdp);
}
}
rkp_l1pgt_new_pgd的作用如下所示:
- 它检查PGD是否为swapper_pg_dir, idmap_pg_dir或tramp_pg_dir。
- 如果是它们中的任何一个,就会触发违规。
- 否则,如果我们被inited,它就会调用rkp_l1pgt_process_table函数。
void rkp_l1pgt_free_pgd(saved_regs_t* regs) {
// ...
pgd_pa = rkp_get_pa(regs->x2);
pgdp = pgd_pa & 0xFFFFFFFFFFFFF000;
if (pgdp == INIT_MM_PGD || pgdp == ID_MAP_PGD || (TRAMP_PGD && pgdp == TRAMP_PGD)) {
uh_log('E', "rkp_l1pgt.c", 345, "PGD free value not allowed, pgdp=%lx k_pgd=%lx k_id_pgd=%lx", pgdp, INIT_MM_PGD,
ID_MAP_PGD);
rkp_policy_violation("PGD free value not allowed, pgdp=%p k_pgd=%p k_id_pgd=%p", pgdp, INIT_MM_PGD, ID_MAP_PGD);
} else if (rkp_inited) {
if ((get_ttbr0_el1() & 0xFFFFFFFFFFFF) == (pgd_pa & 0xFFFFFFFFF000) ||
(get_ttbr1_el1() & 0xFFFFFFFFFFFF) == (pgd_pa & 0xFFFFFFFFF000)) {
uh_log('E', "rkp_l1pgt.c", 354, "PGD free value not allowed, pgdp=%lx ttbr0_el1=%lx ttbr1_el1=%lx", pgdp,
get_ttbr0_el1(), get_ttbr1_el1());
}
if (rkp_l1pgt_process_table(pgdp, 0, 0) < 0)
uh_log('L', "rkp_l1pgt.c", 363, "l1pgt processing is failed, pgdp : %lx", pgdp);
}
}
rkp_l1pgt_free_pgd的作用如下所示:
- 它检查PGD是否为swapper_pg_dir, idmap_pg_dir或tramp_pg_dir。
- 如果是它们中的任何一个,它就会触发违规。
- 如果我们没有被inited,它就直接返回并且不做任何事情。
- 否则,检查PGD是否与TTBR0_EL1或TTBR1_EL1在同一页面。
- 如果是的话,它回记录一个错误,但仍然继续。
- 它将调用rkp_l1pgt_process_table函数来处理删除事宜。
小结
在本系列文章中,我们将为读者深入讲解三星手机的内核防护技术。在本文中,我们为读者介绍了与RKP/KDP相关的一些命令,在后续的文章中,会有更多精彩内容呈现给大家,敬请期待!
(未完待续)