[toc]
背景 已知我们可以编译模块,问题是,如何才能通过内核模块来对系统的const变量(常量)进行修改?对计算机系统来说,数据都在内存里。有内存就有地址,有地址就有页表,页表里面是页表项,页表项控制读写权限。内核常量往往会被编译到系统的只读区域,只要能将该区域对应的页表项的控制标志上加上读标志,就可以实现对目标变量的编辑了。
思路
第一步,找,通过地址找对应的pte,不会找就看看page_fault自己是怎么找的,然后抄
第二步,改,看看对应平台怎么改,加上读标志就行了
第三步,写,用指针去修改目的常量所在地址
aarch64 找pte 通过分析aarch64的page_fault流程,发现其处理内核地址的page_fault时,会进入__do_kernel_fault函数,进而有个show_pte
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 void show_pte (unsigned long addr) { struct mm_struct *mm ; pgd_t *pgdp; pgd_t pgd; if (addr < TASK_SIZE) { mm = current->active_mm; if (mm == &init_mm) { pr_alert("[%016lx] user address but active_mm is swapper\n" , addr); return ; } } else if (addr >= VA_START) { mm = &init_mm; } else { pr_alert("[%016lx] address between user and kernel address ranges\n" , addr); return ; } pr_alert("%s pgtable: %luk pages, %u-bit VAs, pgdp = %p\n" , mm == &init_mm ? "swapper" : "user" , PAGE_SIZE / SZ_1K, mm == &init_mm ? VA_BITS : (int ) vabits_user, mm->pgd); pgdp = pgd_offset(mm, addr); pgd = READ_ONCE(*pgdp); pr_alert("[%016lx] pgd=%016llx" , addr, pgd_val(pgd)); do { pud_t *pudp, pud; pmd_t *pmdp, pmd; pte_t *ptep, pte; if (pgd_none(pgd) || pgd_bad(pgd)) break ; pudp = pud_offset(pgdp, addr); pud = READ_ONCE(*pudp); pr_cont(", pud=%016llx" , pud_val(pud)); if (pud_none(pud) || pud_bad(pud)) break ; pmdp = pmd_offset(pudp, addr); pmd = READ_ONCE(*pmdp); pr_cont(", pmd=%016llx" , pmd_val(pmd)); if (pmd_none(pmd) || pmd_bad(pmd)) break ; ptep = pte_offset_map(pmdp, addr); pte = READ_ONCE(*ptep); pr_cont(", pte=%016llx" , pte_val(pte)); pte_unmap(ptep); } while (0 ); pr_cont("\n" ); }
修改权限 在asm/pgtable.h 中有如下定义:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 static inline void set_pte (pte_t *ptep, pte_t pte) { WRITE_ONCE(*ptep, pte); if (pte_valid_not_user(pte)) dsb(ishst); } static inline pte_t pte_mkwrite (pte_t pte) { pte = set_pte_bit(pte, __pgprot(PTE_WRITE)); pte = clear_pte_bit(pte, __pgprot(PTE_RDONLY)); return pte; }
一句话就可以修改
1 set_pte(ptep, pte_mkwrite(*ptep));
x86-64 找pte 从page_fault找过来的话,可以看到vmalloc_fault有走page walk的流程,按照里面的思路可以实现对ptep的查找。实际上,x86已经有一个现成的函数用来找内核虚拟地址对应的ptep了。
1 2 3 4 5 pte_t *lookup_address (unsigned long address, unsigned int *level) { return lookup_address_in_pgd(pgd_offset_k(address), address, level); } EXPORT_SYMBOL_GPL(lookup_address);
在内核代码中有如下定义,可以看到内核虚拟地址的页表也是从init_mm开始找的
1 #define pgd_offset_k(address) pgd_offset(&init_mm, (address))
再来看lookup_address_in_pgd函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 pte_t *lookup_address_in_pgd (pgd_t *pgd, unsigned long address, unsigned int *level) { p4d_t *p4d; pud_t *pud; pmd_t *pmd; *level = PG_LEVEL_NONE; if (pgd_none(*pgd)) return NULL ; p4d = p4d_offset(pgd, address); if (p4d_none(*p4d)) return NULL ; *level = PG_LEVEL_512G; if (p4d_large(*p4d) || !p4d_present(*p4d)) return (pte_t *)p4d; pud = pud_offset(p4d, address); if (pud_none(*pud)) return NULL ; *level = PG_LEVEL_1G; if (pud_large(*pud) || !pud_present(*pud)) return (pte_t *)pud; pmd = pmd_offset(pud, address); if (pmd_none(*pmd)) return NULL ; *level = PG_LEVEL_2M; if (pmd_large(*pmd) || !pmd_present(*pmd)) return (pte_t *)pmd; *level = PG_LEVEL_4K; return pte_offset_kernel(pmd, address); }
修改权限 和aarch64保持一致。
其他需要的内容 x86-64的lookup_address直接返回ptep,不需要修改这个函数,可以直接调用。而aarch64需要手动将找ptep的流程剥离出来,写进模块代码里。由于默认init_mm 不导出,所以需要通过符号查找的方法找到init_mm 的地址
1 2 #include <linux/kallsyms.h> struct mm_struct init_mm = kallsyms_lookup_name("init_mm" );