GRKERNSEC_KERN_LOCKOUT Active Kernel Exploit Response
This is a brand new feature of “Address Space Protection” that grsecurity offers. Its configuration option is very clear and it is implemented by adding just two new routines in the existing patch.
config GRKERNSEC_KERN_LOCKOUT bool "Active kernel exploit response" depends on X86 help If you say Y here, when a PaX alert is triggered due to suspicious activity in the kernel (from KERNEXEC/UDEREF/USERCOPY) or an OOPs occurs due to bad memory accesses, instead of just terminating the offending process (and potentially allowing a subsequent exploit from the same user), we will take one of two actions: If the user was root, we will panic the system If the user was non-root, we will log the attempt, terminate all processes owned by the user, then prevent them from creating any new processes until the system is restarted This deters repeated kernel exploitation/bruteforcing attempts and is useful for later forensics.
First of all, the ‘user_struct’ at include/linux/sched.h was updated to include two new members that will be used to keep track of the banned users. Here is the code snippet that shows the newly added members.
/* * Some day this will be a full-fledged user tracking system.. */ struct user_struct { ... struct key *session_keyring; /* UID's default session keyring */ #endif #if defined(CONFIG_GRKERNSEC_KERN_LOCKOUT) || defined(CONFIG_GRKERNSEC_BRUTE) unsigned int banned; unsigned long ban_expires; #endif ... };
Next we can have a look at grsecurity/grsec_sig.c to see the first function which is responsible for handling the banned users.
int gr_process_user_ban(void) { #if defined(CONFIG_GRKERNSEC_KERN_LOCKOUT) || defined(CONFIG_GRKERNSEC_BRUTE) if (unlikely(current->cred->user->banned)) { struct user_struct *user = current->cred->user; if (user->ban_expires != ~0UL && time_after_eq(get_seconds(), user->ban_expires)) { user->banned = 0; user->ban_expires = 0; free_uid(user); } else return -EPERM; } #endif return 0; }
What it does is checking if the user is banned and if this is the case, wait for ‘user->ban_expires’ to reset its status. Of course, this does not apply to users with values of ‘~0UL’ in ‘ban_expires’ variable. Those users will be banned until the system is restarted.
The next routine also located in the same source code file is this one.
void gr_handle_kernel_exploit(void) { #ifdef CONFIG_GRKERNSEC_KERN_LOCKOUT const struct cred *cred; struct task_struct *tsk, *tsk2; struct user_struct *user; uid_t uid; if (in_irq() || in_serving_softirq() || in_nmi()) panic("grsec: halting the system due to suspicious kernel crash caused in interrupt context"); uid = current_uid(); if (uid == 0) panic("grsec: halting the system due to suspicious kernel crash caused by root"); else { /* kill all the processes of this user, hold a reference to their creds struct, and prevent them from creating another process until system reset */ printk(KERN_ALERT "grsec: banning user with uid %u until system restart for suspicious kernel crash\n", uid); /* we intentionally leak this ref */ user = get_uid(current->cred->user); if (user) { user->banned = 1; user->ban_expires = ~0UL; } read_lock(&tasklist_lock); do_each_thread(tsk2, tsk) { cred = __task_cred(tsk); if (cred->uid == uid) gr_fake_force_sig(SIGKILL, tsk); } while_each_thread(tsk2, tsk); read_unlock(&tasklist_lock); } #endif }
So, if this is called in the context of an interrupt (IRQ, SoftIRQ or NMI) or the current user is root, it will immediately invoke panic() to halt the system and avoid any possible further exploitation of a kernel vulnerability. In any other case it will log the event and ban that user by updating the ‘user->banned’ and ‘user->ban_expires’ members of the ‘user_struct’ structure. The final ‘while_each_thread’ loop will use gr_fake_force_sig() which is shown below to terminate (by sending kill signal) every task owned by the user who triggered the event.
#ifdef CONFIG_GRKERNSEC extern int specific_send_sig_info(int sig, struct siginfo *info, struct task_struct *t); int gr_fake_force_sig(int sig, struct task_struct *t) { unsigned long int flags; int ret, blocked, ignored; struct k_sigaction *action; spin_lock_irqsave(&t->sighand->siglock, flags); action = &t->sighand->action[sig-1]; ignored = action->sa.sa_handler == SIG_IGN; blocked = sigismember(&t->blocked, sig); if (blocked || ignored) { action->sa.sa_handler = SIG_DFL; if (blocked) { sigdelset(&t->blocked, sig); recalc_sigpending_and_wake(t); } } if (action->sa.sa_handler == SIG_DFL) t->signal->flags &= ~SIGNAL_UNKILLABLE; ret = specific_send_sig_info(sig, SEND_SIG_PRIV, t); spin_unlock_irqrestore(&t->sighand->siglock, flags); return ret; } #endif
This routine will send the requested signal to the process.
So, now to the actual patching, the first patched code is the __kprobes oops_end() routine located at arch/x86/kernel/dumpstack.c file.
void __kprobes oops_end(unsigned long flags, struct pt_regs *regs, int signr) { ... if (panic_on_oops) panic("Fatal exception"); gr_handle_kernel_exploit(); do_group_exit(signr); }
This is triggered at the last step of a kernel OOPS. Consequently, it’s an ideal location to place this protection. Next we have the ‘execve’ routines that are invoked for spawning new processes. Specifically, the compat_do_execve() you see here from fs/compat.c file.
/* * compat_do_execve() is mostly a copy of do_execve(), with the exception * that it processes 32 bit argv and envp pointers. */ int compat_do_execve(char * filename, compat_uptr_t __user *argv, compat_uptr_t __user *envp, struct pt_regs * regs) { ... bprm->interp = filename; if (gr_process_user_ban()) { retval = -EPERM; goto out_file; } ... out_ret: return retval; }
Which is where it checks if the user is banned. Of course, similar check is also included in the do_execve() system call from fs/exec.c.
/* * sys_execve() executes a new program. */ int do_execve(const char * filename, const char __user *const __user *argv, const char __user *const __user *envp, struct pt_regs * regs) { ... bprm->interp = filename; if (gr_process_user_ban()) { retval = -EPERM; goto out_file; } ... out_ret: return retval; }
Finally, the pax_report_usercopy() is updated to handle the possible attacks using the new locking-out feature.
void pax_report_usercopy(const void *ptr, unsigned long len, bool to, const char *type) { if (current->signal->curr_ip) printk(KERN_ERR "PAX: From %pI4: kernel memory %s attempt detected %s %p (%s) (%lu bytes)\n", ¤t->signal->curr_ip, to ? "leak" : "overwrite", to ? "from" : "to", ptr, type ? : "unknown", len); else printk(KERN_ERR "PAX: kernel memory %s attempt detected %s %p (%s) (%lu bytes)\n", to ? "leak" : "overwrite", to ? "from" : "to", ptr, type ? : "unknown", len); dump_stack(); gr_handle_kernel_exploit(); do_group_exit(SIGKILL); }
Leave a comment