How to write to protected pages in the Linux kernel?

Viewed 3617

I am trying to add a syscall in a module. My rationale is:

  1. This is for a research project, so the exact implementation does not matter.
  2. Adding syscalls in the kernel-core takes a prohibitively long time to re-compile. I can suck up compiling once with an expanded syscall table, but not every time. Even with incremental compiling, linking and archiving the final binary takes a long time.
  3. Since the project is timing sensitive, using kprobes to intercept the syscall handler would slow down the syscall handler.

I am still open to other means of adding a syscall, but for the above reasons, I think that writing to the sys_call_table in a module is the cleanest way to do what I am trying to do.

I've gotten the address of the syscall table from the System.map, disabled kaslr, and I am trying to clear the page protections, but some write-protection is still holding me back.

// 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;

However, I'm still getting a permission error, but I don't know the mechanism by which it is enforced:

[   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

Any guesses on how to disable it?

3 Answers

There is a way that does not need to recompile the kernel. Since the kernel will detect whether the wp bit has been modified in write_cr0, you can provide a custom function to bypass it.

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

Here is the function that enables/disables write protection. We use mywrite_cr0 instead of 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);
}

In your mod_init function, you can use kallsyms_lookup_name("sys_call_table") to figure out the address of sys_call_table at runtime, instead of compile time. Fortunately, we can now directly write to sys_call_table without dealing with pageattr.

The code below is tested on 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);

The kernel has code to protect against this sort of action.

First, the kernel by default does not allow you to remove write protection from the cr0 register. It checks that in 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");
}

Second, the page table does not allow you to set a page that should be read-only to read-write. It does that check 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;

If you disable these two checks by removing both blobs, the code to change the pagetable works.

It is possible to just remap the sys_call_table as read-write using the set_memory_rw function, so it's possible to write to it without disabling write protection for the whole kernel. Used this method myself on aarch64, not sure if it works on x86.

Related