如何写入 Linux 内核中的受保护页面?

How to write to protected pages in the Linux kernel?

我正在尝试在模块中添加系统调用。我的理由是:

  1. 这是一个研究项目,所以具体的实现并不重要。
  2. 在内核核心中添加系统调用需要很长时间才能重新编译。我可以通过扩展的系统调用 table 编译一次,但不是每次都这样。即使使用增量编译,链接和归档最终的二进制文件也需要很长时间。
  3. 由于项目对时间敏感,使用 kprobes 拦截系统调用处理程序会减慢系统调用处理程序的速度。

我仍然对添加系统调用的其他方法持开放态度,但出于上述原因,我认为写入模块中的 sys_call_table 是完成我想做的事情的最干净的方法。

我已经从 System.map 获得了系统调用 table 的地址,禁用了 kaslr,我正在尝试清除页面保护,但一些写保护仍然阻碍我.

// following https://web.iiit.ac.in/~arjun.nath/random_notes/modifying_sys_call.html

// clear cr0 write protection
write_cr0 (read_cr0 () & (~ 0x10000));

// clear page write protection
sys_call_table_page = virt_to_page(&sys_call_table[__NR_execves]);
set_pages_rw(sys_call_table_page, 1);

// do write
sys_call_table[__NR_execves] = sys_execves;

但是,我仍然遇到权限错误,但我不知道它的强制执行机制:

[   11.145647] ------------[ cut here ]------------
[   11.148893] CR0 WP bit went missing!?
[   11.151539] WARNING: CPU: 0 PID: 749 at arch/x86/kernel/cpu/common.c:386 native_write_cr0+0x3e/0x70
...
Here was a call trace pointing to the write of sys_call_table
...
[   11.332825] ---[ end trace c20c95651874c08b ]---
[   11.336056] CPA  protect  Rodata RO: 0xffff888002804000 - 0xffff888002804fff PFN 2804 req 8000000000000063 prevent 0000000000000002
[   11.343934] CPA  protect  Rodata RO: 0xffffffff82804000 - 0xffffffff82804fff PFN 2804 req 8000000000000163 prevent 0000000000000002
[   11.351720] BUG: unable to handle page fault for address: ffffffff828040e0
[   11.356418] #PF: supervisor write access in kernel mode
[   11.359908] #PF: error_code(0x0003) - permissions violation
[   11.363665] PGD 3010067 P4D 3010067 PUD 3011063 PMD 31e29063 PTE 8000000002804161
[   11.368701] Oops: 0003 [#1] SMP KASAN PTI

full dmesg

关于如何禁用它的任何猜测?

内核有防止此类操作的代码。

首先,内核在默认情况下不允许您从cr0 寄存器中删除写保护。它检查 arch/x86/kernel/cpu/common.c:native_write_cr0

if (static_branch_likely(&cr_pinning)) {
    if (unlikely((val & X86_CR0_WP) != X86_CR0_WP)) {
        bits_missing = X86_CR0_WP;
        val |= bits_missing;
        goto set_register;
    }
    /* Warn after we've set the missing bits. */
    WARN_ONCE(bits_missing, "CR0 WP bit went missing!?\n");
}

其次,页面table不允许你将一个应该只读的页面设置为可读写。它会检查 arch/x86/mm/pageattr.c:static_protections

/* Check the PFN directly */
res = protect_rodata(pfn, pfn + npg - 1);
check_conflict(warnlvl, prot, res, start, end, pfn, "Rodata RO");
forbidden |= res;

如果您通过删除两个 blob 来禁用这两个检查,则更改页面的代码table 有效。

可以使用 set_memory_rw 函数将 sys_call_table 重新映射为读写,因此可以在不禁用整个内核的写保护的情况下对其进行写入。自己在aarch64上用过这个方法,不知道在x86上能不能用。

有一种方法不需要重新编译内核。由于内核会在write_cr0中检测wp位是否被修改,你可以提供一个自定义函数来绕过它。

inline void mywrite_cr0(unsigned long cr0) {
  asm volatile("mov %0,%%cr0" : "+r"(cr0), "+m"(__force_order));
}

这里是enables/disables写保护的功能。我们用 mywrite_cr0 而不是 write_cr0

void enable_write_protection(void) {
  unsigned long cr0 = read_cr0();
  set_bit(16, &cr0);
  mywrite_cr0(cr0);
}

void disable_write_protection(void) {
  unsigned long cr0 = read_cr0();
  clear_bit(16, &cr0);
  mywrite_cr0(cr0);
}

在你的 mod_init 函数中,你可以使用 kallsyms_lookup_name("sys_call_table") 在运行时计算出 sys_call_table 的地址,而不是在编译时。幸运的是,我们现在可以直接写入 sys_call_table 而无需处理 pageattr.

下面的代码是在 Linux 内核 5.1.4

上测试的
inline void mywrite_cr0(unsigned long cr0) {
  asm volatile("mov %0,%%cr0" : "+r"(cr0), "+m"(__force_order));
}

void enable_write_protection(void) {
  unsigned long cr0 = read_cr0();
  set_bit(16, &cr0);
  mywrite_cr0(cr0);
}

void disable_write_protection(void) {
  unsigned long cr0 = read_cr0();
  clear_bit(16, &cr0);
  mywrite_cr0(cr0);
}

static struct {
  void **sys_call_table;
  void *orig_fn;
} tinfo;

static int __init mod_init(void) {
  printk(KERN_INFO "Init syscall hook\n");
  tinfo.sys_call_table = (void **)kallsyms_lookup_name("sys_call_table");
  tinfo.orig_fn = tinfo.sys_call_table[your_syscall_num];
  disable_write_protection();
  // modify sys_call_table directly
  tinfo.sys_call_table[your_syscall_num] = sys_yourcall;
  enable_write_protection();
  return 0;
}

static void __exit mod_cleanup(void) {
  printk(KERN_INFO "Cleaning up syscall hook.\n");
  // backup syscall
  disable_write_protection();
  tinfo.sys_call_table[your_syscall_num] = tinfo.orig_fn;
  enable_write_protection();
  printk(KERN_INFO "Cleaned up syscall hook.\n");
}

module_init(mod_init);
module_exit(mod_cleanup);