I wrote an example of system call hooking from our Linux Kernel module.
Updated open system call in system call table to use my entry point instead of the default.
#include <linux/module.h>
#include <linux/kallsyms.h>
MODULE_LICENSE("GPL");
char *sym_name = "sys_call_table";
typedef asmlinkage long (*sys_call_ptr_t)(const struct pt_regs *);
static sys_call_ptr_t *sys_call_table;
typedef asmlinkage long (*custom_open) (const char __user *filename, int flags, umode_t mode);
custom_open old_open;
static asmlinkage long my_open(const char __user *filename, int flags, umode_t mode)
{
char user_msg[256];
pr_info("%s\n",__func__);
memset(user_msg, 0, sizeof(user_msg));
long copied = strncpy_from_user(user_msg, filename, sizeof(user_msg));
pr_info("copied:%ld\n", copied);
pr_info("%s\n",user_msg);
return old_open(filename, flags, mode);
}
static int __init hello_init(void)
{
sys_call_table = (sys_call_ptr_t *)kallsyms_lookup_name(sym_name);
old_open = (custom_open)sys_call_table[__NR_open];
// Temporarily disable write protection
write_cr0(read_cr0() & (~0x10000));
sys_call_table[__NR_open] = (sys_call_ptr_t)my_open;
// Re-enable write protection
write_cr0(read_cr0() | 0x10000);
return 0;
}
static void __exit hello_exit(void)
{
// Temporarily disable write protection
write_cr0(read_cr0() & (~0x10000));
sys_call_table[__NR_open] = (sys_call_ptr_t)old_open;
// Re-enable write protection
write_cr0(read_cr0() | 0x10000);
}
module_init(hello_init);
module_exit(hello_exit);
I wrote a simple user program to verify.
#define _GNU_SOURCE
#include <sys/syscall.h>
#include <sys/time.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <fcntl.h>
int main(int argc, char *argv[])
{
int fd = syscall(__NR_open, "hello.txt", O_RDWR|O_CREAT, 0777);
exit(EXIT_SUCCESS);
}
File gets created in my folder, but strncpy_user fails with bad address
[ 927.415905] my_open
[ 927.415906] copied:-14
What is the mistake in the above code?
OP is probably using a kernel/architecture that uses "syscall wrappers" where the system call table contains a wrapper function that calls the real syscall function (possibly as an inline function call). The x86_64 architecture has used syscall wrappers since kernel version 4.17.
For x86_64 on kernel 4.17 or later, sys_call_table[__NR_open]
points to __x64_sys_open
(with prototype asmlinkage long __x64_sys_open(const struct pt_regs *regs)
), which calls static
function __se_sys_open
(with prototype static long __se_sys_open(const __user *filename, int flags, umode_t mode)
), which calls inline function __do_sys_open
(with prototype static inline long __do_sys_open(const __user *filename, int flags, umode_t mode)
. Those will all be defined by the SYSCALL_DEFINE3(open, const char __user *, filename, int, flags, umode_t, mode)
macro call in "fs/open.c" and the function body that follows the macro call.
SYSCALL_DEFINE3
is defined in "include/linux/syscalls.h" and uses the SYSCALL_DEFINEx
macro in the same file, which uses the __SYSCALL_DEFINEx
macro. Since x86_64 defines CONFIG_ARCH_HAS_SYSCALL_WRAPPER
, the __SYSCALL_DEFINEx
macro is defined by #include <asm/syscall_wrapper.h>
, which maps to "arch/x86/include/asm/syscall_wrapper.h".
For background on this change, see
It seems the motivation is to only pass a pointer to pt_regs
, instead of having a bunch of user-space values in registers down the call chain. (Perhaps to increase resistance to Spectre attacks by making gadgets less useful?)
Why open
still worked, even though the wrapper didn't:
If OP is indeed using x86_64 kernel 4.17 or later, and replacing the sys_call_table[__NR_open]
entry with a pointer to a function that uses a different prototype and calls the original function (pointed to by old_open
) with the same parameters, that explains why the call to strncpy_from_user(user_msg, filename, sizeof(user_msg))
failed. Although declared as const char * __user filename
, the filename
pointer is actually pointing to the original struct pt_regs
in kernel space.
In the subsequent call to old_open(filename, flags, mode)
, the first parameter filename
is still pointing to the original struct pt_regs
so the old function (which expects a single parameter of type struct pt_regs *
) still works as expected.
i.e. the function passed on its first pointer arg unchanged, despite calling it a different type.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With