如何写入 Linux 内核中的受保护页面?
How to write to protected pages in the Linux kernel?
我正在尝试在模块中添加系统调用。我的理由是:
- 这是一个研究项目,所以具体的实现并不重要。
- 在内核核心中添加系统调用需要很长时间才能重新编译。我可以通过扩展的系统调用 table 编译一次,但不是每次都这样。即使使用增量编译,链接和归档最终的二进制文件也需要很长时间。
- 由于项目对时间敏感,使用
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
关于如何禁用它的任何猜测?
内核有防止此类操作的代码。
首先,内核在默认情况下不允许您从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);
我正在尝试在模块中添加系统调用。我的理由是:
- 这是一个研究项目,所以具体的实现并不重要。
- 在内核核心中添加系统调用需要很长时间才能重新编译。我可以通过扩展的系统调用 table 编译一次,但不是每次都这样。即使使用增量编译,链接和归档最终的二进制文件也需要很长时间。
- 由于项目对时间敏感,使用
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
关于如何禁用它的任何猜测?
内核有防止此类操作的代码。
首先,内核在默认情况下不允许您从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);