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

cha*_*umQ 3 paging system-calls linux-kernel

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

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

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

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

// 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;
Run Code Online (Sandbox Code Playgroud)

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

[   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
Run Code Online (Sandbox Code Playgroud)

完整的消息

关于如何禁用它有什么猜测吗?

小智 5

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

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

这是启用/禁用写保护的功能。我们使用 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);
}
Run Code Online (Sandbox Code Playgroud)

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

下面的代码在Linux Kernel 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);
Run Code Online (Sandbox Code Playgroud)