Shiva modules for advancing threat detection in Linux

Do you or your organization need state of the art Linux threat detection and exploit mitigations? Shiva is a specialized ELF interpreter that is capable of dynamic linking ELF microprograms for security hardening of the process image.

Very recently I illustrated how Shiva can be used to design exploit mitigations such as gASLR (Granular ASLR) for x86_64 Linux. If you haven't already seen this paper take a gander at the paper Fine grained ASLR for X86_64 Linux in the latest tmp.0ut issue to see the full implementation of gASLR and familiarize with how Shiva modules work.

In this blog-post we will use an anti-forensics tool called Saruman found here to stealthly inject a remote backdoor into the X11 server process running "/usr/bin/Xwayland". Next we will show how a Shiva module can be rapidly developed for detecting and preventing process infections of this nature.

What is Shiva?

If you are not familiar with Shiva yet, take a peek at the main Shiva page and the Shiva GitHub repo.

Shiva is a specialized ELF interpreter that treats ELF relocatable objects as first-class modules for load-time binary patching, security hardening, and more.

What is Saruman

Saruman is an anti-forensics executable technique for running native ELF programs in the context of a remote process with an injected thread of execution. This technique is powerful for hiding an executable program from being listed by the tool ps. It also hides the program from init task list, and /proc in general.

Saruman uses PTRACE trickery to remotely execute libc.so:dlopen() and a custom create_thread() routine that effectively inject the executable into the remote process.

Just last week I released a new version of Saruman after many years of it sitting in a mostly broken state on my github repo. Freshly tested on Ubuntu 22 and 24.

Build and Install Saruman

$ git clone git@github.com:elfmaster/saruman.git
$ cd saruman
$ make

This will build the tool Saruman and the executable "backdoor" that we want to inject into X11 process.

Infecting the X11 server with a remote backdoor

Pretend we are an attacker who just gained root and we want to maintain a stealth presence. Saruman will force a remote process to load our backdoor and inject a thread of execution to run it. From the outside observer the "backdoor" executable is listed nowhere in the ps aux output and is hidden as a thread within the X11 servers thread group. Despite the jenky and simple backdoor code it has become a fully Advanced Persistent Threat with the help of Saruman disguising it in an existing process image.

SECCOMP filtering

Some processes such as sshd will prevent Saruman from working due to SECCOMP filters that restrict which syscalls can be executed. The sshd sandbox filters the clone syscall for example, preventing Saruman from being able to inject a thread.

Shiva allows developers to build generic security modules for hardening the process image in similar ways for threat detection, exploit mitigation, and process hardening.

We are going to test our Shiva module with Xwayland which does not have any existing hardening or protection built into it.

X11 Server binary: Xwayland

The ELF binary /usr/bin/Xwayland is the X11 server program. I am in Ubuntu 24 and Xwayland runs by default. We want to inject a simple backdoor that binds to port 31337 and then forks() for new incoming connections to receive and execute shell commands.

Injecting ./backdoor into the Xwayland process

Within the saruman directory there exists a source file backdoor.c which contains the code for the backdoor we want to inject into the Xwayland process. The ./backdoor executable should already be compiled from the make command in the saruman directory.

In this illustration we will use the Saruman tool to inject the backdoor into the Xwayland pid, and then verify that the backdoor is active by telneting to localhost 31337 and logging in to the backdoor shell.

Illusration 1.0: Saruman injecting ./backdoor into x11 process

x11_injection

Detecting and stopping Saruman

Saruman is tricky to detect by hand unless you are an advanced system administrator with reverse engineering and Linux internals knowledge. It scrubs the ELF headers of the injected executable, and runs the code with a thread created by SYS_clone.

Shiva can load custom ELF microprograms which can optionally begin executing before control is transferred to ld-linux.so, or after ld-linux.so has finished depending on what the developers needs are.

For detecting an APT (Advanced Persistent Threat) tool such as Saruman we could build a Shiva module that sets a SECCOMP filter on the clone syscall and uses a listener thread with heuristics to detect whether calls to SYS_clone show characteristics of a regular thread such as with pthread_create() or if it's something suspicious like what Saruman does.

The following function create_thread() is from the Saruman source code and it gets executed in the remote process in order to create a thread that runs the injected executable (In our case the backdoor). We can see that it is quite a bit different from libc's pthread_create and it doesn't even setup TLS or a TCB (Thread control block). We could view this as anomalous behavior.

How the thread spawning in Saruman works: create_thread() function

__PAYLOAD_KEYWORDS__ int create_thread(void (*fn)(void *), void *data,
    unsigned long stack, int main_argc, char **main_argv)
{
        long retval;
        void **newstack = (void **)stack;

        *--newstack = data;
        newstack = (void **)((unsigned long)newstack & ~0xfUL);

        __asm__ __volatile__(
                "xor %%rdx, %%rdx\n\t"
                "xor %%r10, %%r10\n\t"
                "xor %%r8,  %%r8\n\t"
                "syscall\n\t"
                "test %%rax, %%rax\n\t"
                "jne 1f\n\t"
                "mov %[argc], %%rdi\n\t"
                "mov %[argv], %%rsi\n\t"
                "xor %%rdx, %%rdx\n\t"
                "call *%[fn]\n\t"

                "xor %%rdi, %%rdi\n\t"
                "mov %[exitnr], %%eax\n\t"
                "syscall\n"
                "1:\n"
                : "=a"(retval)
                : "0"((long)__NR_clone),
                  "D"((long)(CLONE_VM | CLONE_FS | CLONE_FILES |
                             CLONE_SIGHAND | CLONE_THREAD | CLONE_SYSVSEM )), /*SIGCHLD)), */
                  "S"(newstack),
                  [fn] "r"(fn),
                  [argc] "r"((long)main_argc),
                  [argv] "r"(main_argv),
                  [exitnr] "i"(__NR_exit)
                : "rcx", "r11", "rdx", "r10", "r8", "memory"
        );

        if (retval < 0) {
                retval = -1;
                __RETURN_VALUE__(retval);
        }
        __BREAKPOINT__;
        return (int)retval;
}

Detecting Saruman is as easy as filtering the clone syscall and detecting whether or not the thread that was created uses the flags expected by a real pthread_create() vs. the style of thread created by Saruman.

Designing a Shiva module to detect Saruman

The following Shiva module was coded and tested in a single day. All Shiva modules begin executing at the entry point function int shiva_init(shiva_ctx_t *ctx) The context handle gives access to all of Shiva's internal internal structures for linking, ELF binary parsing, program disassembly, and more.

SECCOMP filter on clone syscall

Our design is simple. The Shiva module spawns a listener thread to monitor the clone and clone3 syscalls. Since we are in userland we can't simply trap the breakpoints or hook the sys_call_table so we use the same techniques that sshd and other heavily sandboxed applications use, SECCOMP + BPF.

Source code for detect_saruman.c

/*
 * Arcana Research, 2026
 * Shiva module that runs directly after ld-linux.so finishes
 * Spawning a thread which detects and prevents potentially malicious thread injection
 * (i.e. Saruman)
 * Elfmaster [at] Arcana-Research.io
 */

#define _GNU_SOURCE
#include "shiva_module.h"
#include "shiva.h"
#include "libelfmaster.h"

#include <errno.h>
#include <linux/audit.h>
#include <linux/filter.h>
#include <linux/seccomp.h>
#include <linux/unistd.h>
#include <sched.h>
#include <stdarg.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/prctl.h>
#include <sys/syscall.h>
#include <unistd.h>
#include <stddef.h>

#ifndef SECCOMP_IOCTL_NOTIF_RECV
#define SECCOMP_IOCTL_NOTIF_RECV SECCOMP_IOWR(0, struct seccomp_notif)
#define SECCOMP_IOCTL_NOTIF_SEND SECCOMP_IOWR(1, struct seccomp_notif_resp)
#endif
#ifndef SECCOMP_USER_NOTIF_FLAG_CONTINUE
#define SECCOMP_USER_NOTIF_FLAG_CONTINUE (1U << 0)
#endif

#define SARUMAN_CORE (CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND)

#define PTHREAD_HINT \
    (CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND | \
     CLONE_THREAD | CLONE_SYSVSEM | CLONE_SETTLS | \
     CLONE_PARENT_SETTID | CLONE_CHILD_CLEARTID)

pid_t listener_tid;
struct shiva_ctx *g_ctx;
volatile int notify_fd = -1;

#ifndef __NR_clone3
#define __NR_clone3 435
#endif

struct clone_args {
    uint64_t flags;
    uint64_t pidfd;
    uint64_t child_tid;
    uint64_t parent_tid;
    uint64_t exit_signal;
    uint64_t stack;
    uint64_t stack_size;
    uint64_t tls;
};

#include <stdint.h>
#include <linux/prctl.h>

#define __NR_prctl 157

#define __NR_seccomp 317

#ifndef SECCOMP_SET_MODE_FILTER
#define SECCOMP_SET_MODE_FILTER 1
#endif
#ifndef SECCOMP_FILTER_FLAG_NEW_LISTENER
#define SECCOMP_FILTER_FLAG_NEW_LISTENER (1UL << 3)
#endif

#define __NR_process_vm_readv 310
#define __NR_exit         60

struct iovec_raw {
        void  *iov_base;
        unsigned long iov_len;
};


static long
write_raw(int fd, const void *buf, unsigned long n)
{
    long r;

    asm volatile("syscall"
        : "=a"(r)
        : "a"(1L), "D"((long)fd), "S"((long)buf), "d"(n)
        : "rcx", "r11", "memory");
    return (r);
}

void
print_msg(const char *fmt, ...)
{
    char buf[512];
    va_list ap;
    int n;

    va_start(ap, fmt);
    n = vsnprintf(buf, sizeof(buf), fmt, ap);
    va_end(ap);
    if (n > (int)sizeof(buf) - 1)
        n = (int)sizeof(buf) - 1;
    if (n > 0)
        write_raw(2, buf, (unsigned long)n);
}

void
debug_msg(const char *fmt, ...)
{
    char buf[512];
    va_list ap;
    int n;

#if DEBUG
    va_start(ap, fmt);
    n = vsnprintf(buf, sizeof(buf), fmt, ap);
    va_end(ap);
    if (n > (int)sizeof(buf) - 1)
        n = (int)sizeof(buf) - 1;
    if (n > 0)
        write_raw(2, buf, (unsigned long)n);
#endif
    return;
}

long
process_vm_readv_raw(long pid,
             const struct iovec_raw *local, unsigned long liovcnt,
             const struct iovec_raw *remote, unsigned long riovcnt,
             unsigned long flags)
{
    long ret;
    register long r10 asm("r10") = (long)remote;
    register long r8  asm("r8")  = (long)riovcnt;
    register long r9  asm("r9")  = (long)flags;

    asm volatile(
    "syscall"
    : "=a"(ret)
    : "a"((long)__NR_process_vm_readv),
      "D"(pid),
      "S"((long)local),
      "d"(liovcnt),
      "r"(r10), "r"(r8), "r"(r9)
    : "rcx", "r11", "memory"
    );
    return ret;
}

long
seccomp_raw(unsigned int op, unsigned int flags, void *args)
{
    long ret;
    asm volatile("syscall"
        : "=a"(ret)
        : "a"(317L), "D"((long)op), "S"((long)flags), "d"((long)args)
        : "rcx", "r11", "r8", "r9", "r10", "memory");
    return ret;
}

pid_t
gettid_raw(void)
{
    return (pid_t)syscall(SYS_gettid);
}

static int
install_filter(void)
{
    static struct sock_filter filt[] = {
        BPF_STMT(BPF_LD  | BPF_W   | BPF_ABS,
         offsetof(struct seccomp_data, arch)),
        BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, AUDIT_ARCH_X86_64, 1, 0),
        BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),

        BPF_STMT(BPF_LD  | BPF_W   | BPF_ABS,
         offsetof(struct seccomp_data, nr)),
        BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, 56,  2, 0),   /* clone */
        BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, 435, 1, 0),   /* clone3 */
        BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
        BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_USER_NOTIF),
    };
    static struct sock_fprog prog;
    long r;

    prog.len = (unsigned short)(sizeof(filt) / sizeof(filt[0]));
    prog.filter = filt;

    r = prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
    if (r < 0) {
        fprintf(stderr, "prctl() failed\n");
        return -1;
    }

    r = seccomp_raw(1,
        SECCOMP_FILTER_FLAG_NEW_LISTENER,
        &prog);

    if (r <= 0) {
        fprintf(stderr, "seccomp() failed\n");
        return -1;
    }

    notify_fd = (int)r;
    return 0;
}

bool
read_clone3_args(pid_t pid, uint64_t uaddr, size_t sz, struct clone_args *out)
{
    struct iovec local = { .iov_base = out, .iov_len = sizeof(*out) };
    struct iovec remote = { .iov_base = (void *)uaddr,
        .iov_len = sz < sizeof(*out) ? sz : sizeof(*out) };
    ssize_t n;

    memset(out, 0, sizeof(*out));
    n = process_vm_readv_raw(pid, (void *)&local, 1, (void *)&remote, 1, 0);
    return n == (ssize_t)remote.iov_len;
}


#define SARUMAN_FLAGS \
    (CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND | \
     CLONE_THREAD | CLONE_SYSVSEM)

bool
classify_clone(uint64_t flags, uint64_t stack, uint64_t tls)
{
    if (stack == 0) {
        debug_msg("stack is NULL in clone\n");
        return false;
    }

    if ((flags & PTHREAD_HINT) == PTHREAD_HINT) {
        debug_msg("PTHREAD_HINT found in clone\n");
        return false;
    }

    /*
     * Does this call to clone use the flags that Saruman uses?
     */
    if ((flags & SARUMAN_FLAGS) == SARUMAN_FLAGS) {
        debug_msg("SARUMAN detected\n");
        return true;
    }

    (void)tls;
    debug_msg("return false\n");
    return false;
}

/*
 * handle_notification runs heuristics to detect whether or not the
 * clone is suspicious.
 */
void
handle_notification(struct seccomp_notif *req, struct seccomp_notif_resp *resp)
{
    uint64_t flags = 0, stack = 0, tls = 0;
    struct clone_args clone_args;
    bool detected = false;

    resp->id = req->id;
    resp->val = 0;
    resp->error = 0;
    resp->flags = SECCOMP_USER_NOTIF_FLAG_CONTINUE;

    if (req->pid == (uint32_t)listener_tid)
        return;

    if (req->data.nr == __NR_clone) {
        flags = req->data.args[0];
        stack = req->data.args[1];
        tls   = req->data.args[4];
        detected = classify_clone(flags, stack, tls);
    } else if (req->data.nr == __NR_clone3) {
        if (read_clone3_args((pid_t)req->pid,
            req->data.args[0],
            (size_t)req->data.args[1],
            &clone_args)) {
            flags = clone_args.flags;
            stack = clone_args.stack;
            tls   = clone_args.tls;
            detected = classify_clone(flags, stack, tls);
        }
    }
    if (detected == false) {
        debug_msg("detected = %d allowing thread\n", detected);
    }
    if (detected == true) {
        print_msg("Detected and prevented suspicious thread injection!"
            " pid=%d nr=%d flags=0x%lx stack=0x%lx tls=0x%lx\n",
        (int)req->pid, req->data.nr,
        (unsigned long)flags,
        (unsigned long)stack,
        (unsigned long)tls);
        resp->error = -EPERM;
        resp->flags = 0;
    }
    return;
}

long
ioctl_raw(int fd, unsigned long cmd, void *arg)
{
    long r;
    asm volatile("syscall"
    : "=a"(r)
    : "a"(16L), "D"((long)fd), "S"(cmd), "d"((long)arg)
    : "rcx", "r11", "memory");
    return r;
}

volatile int listener_ready = 0;

int
listener_loop(void *arg)
{
        struct seccomp_notif_sizes sz;
        unsigned long cmd_recv, cmd_send;
        void *req, *resp;
        long rec, snd;

        (void)arg;

        while (notify_fd <= 0)
                ;

        memset(&sz, 0, sizeof(sz));
        if (seccomp_raw(3u, 0, &sz) < 0 || sz.seccomp_notif == 0) {
                sz.seccomp_notif = 256;
                sz.seccomp_notif_resp = 128;
        }

        cmd_recv = _IOC(_IOC_READ | _IOC_WRITE, '!', 0, sz.seccomp_notif);
        cmd_send = _IOC(_IOC_READ | _IOC_WRITE, '!', 1, sz.seccomp_notif_resp);

        req = mmap(NULL, sz.seccomp_notif, PROT_READ | PROT_WRITE,
            MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
        resp = mmap(NULL, sz.seccomp_notif_resp, PROT_READ | PROT_WRITE,
            MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
        if (req == MAP_FAILED || resp == MAP_FAILED)
                return (-1);

        listener_ready = 1;

        for (;;) {
                memset(req, 0, sz.seccomp_notif);
                rec = ioctl_raw(notify_fd, cmd_recv, req);
                if (rec < 0) {
                        print_msg("recv errno=%d\n", (int)(-rec));
                        continue;
                }
                handle_notification(req, resp);
                snd = ioctl_raw(notify_fd, cmd_send, resp);
                if (snd < 0)
                        print_msg("send=%ld\n", snd);
        }
}
#define STACK_SIZE 4096 * 10

int
shiva_init(struct shiva_ctx *ctx)
{
    long child;
    uint8_t *stack;
    g_ctx = ctx;

    stack = (uint8_t *)mmap(0, STACK_SIZE,
        PROT_READ | PROT_WRITE,
        MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    if ((long)stack <= 0) {
        fprintf(stderr, "mmap failed to allocate stack\n");
        return -1;
    }

    int ptid = 0;

    debug_msg("Calling clone\n");

    child = clone(listener_loop,
        stack + STACK_SIZE,
        CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND |
        CLONE_THREAD | CLONE_SYSVSEM, NULL);

    if (child <= 0) {
        fprintf(stderr, "clone failed, child: %ld\n", child);
        return -1;
    }

    debug_msg("Installing filter\n");
    if (install_filter() < 0) {
        fprintf(stderr, "install_filter failed!\n");
        return -1;
    }

    debug_msg("listener_ready == 0)\n");
    while (listener_ready == 0)
        ;

    debug_msg("returning 0\n");
    return 0;
}

Building and Installing the detect_saruman.c Shiva module

First I assume you have Shiva already built for x86_64 Linux. See the Shiva README.md

The Saruman detection engine livesin the Shiva source code base here: ~/shiva/modules/x86_64_modules/detect_saruman

Run the following five commands from the detect_saruman directory

-= 1. Build the Shiva module

make

-= 2. Prelink the Shiva module to the Xwayland executable and copy it into place at /usr/bin/Xwayland

cp /usr/bin/Xwayland .
shiva-ld -i /lib/shiva -s /opt/shiva/modules -p detect_saruman.o -e Xwayland -o Xwayland.secure

-= 3. Verify that /lib/shiva is now set as the primary ELF interpreter

readelf -l Xwayland.secure | grep interpreter

-= 4. Copy the Xwayland.secure executable into place at /usr/bin/Xwayland.

The new Xwayland binary is nearly identical to the original except that it has /lib/shiva set as the primary ELF interpreter and there are several extra ELF sections and ELF dynamic tags that are necessary for Shiva to know to load /opt/shiva/modules/detect_saruman.o at program load-time.

-= 5. Copy the detect_saruman.o module into place at /opt/shiva/modules-- The module search path set by our shiva-ld command

sudo cp detect_saruman.o /opt/shiva/modules

Illustration 1.1: Building and installing our detect_saruman.o module

build_saruman_module

Restart Xwayland to see changes

Now restart Xwayland or restart your system and future versions of Xwayland will be protected from Saruman's thread injection.

When Xwayland begins running it will clone a thread that monitors itself, waiting for any notifications on filtered clone syscalls that have the characteristics of a Saruman's thread injection.

Attempt injecting ./backdoor into the hardened Xwayland

Now that the Xwayland binary is prelinked to our Shiva module detect_saruman.o it should prevent any attempts to thread-inject a backdoor into the process.

Illustration 1.2: Try Saruman against the hardened Xwayland process

build_saruman_module

As you can see in the terminal output above the Saruman thread injection failed, as our sexy threat detection agent was able to heuristically identify that the clone syscall being used was abnormal and prevented its execution with a -EPERM.

Closing thoughts

We live in an age where vulnerabilities and attacks are extrapolating and evolving at an all time high due to the advent of AI. Shiva modules allow developers to rapidly design security modules in a pragmatic and efficient way. DevSecOps has never been so easy as it is with Shiva-- in past days one must be plugged into the glibc, gcc, linux and binutils development teams to cross-coordinate and build such features. Now with Shiva most userland security hardening features could be implemented as a Shiva module.

This blog-post demonstrates this with a Shiva module detect_saruman.o that I developed in one day. Please note that Shiva has many more capabilities than those outlined in todays blog-post, including powerful binary patching and program transformation capabilities that go way beyond todays blog-post.

I am waiting on DARPA approval to release my upcoming 65 page blog-post on using X86_64 Shiva for binary patching. This blog-post will be a powerful introduction to using Shiva hands-on. Meanwhile...

Workshop trainings on Shiva

The ElfMaster "Ryan O'Neill of Arcana Research Inc. is available to train organizations on using Shiva for binary patching and for building custom security modules.

Custom threat detection and exploit mitigations

If you or your organization needs help directly designing and deploying custom security mitigations for Linux, please let me know. This is one of our specialties...

Recently at Arcana Research we have designed a powerful prototype for gASLR (Granular ASLR) as mentioned earlier, and we are working on a novel security hardening feature called "Syscall Authentication" which prevents un-authenticated syscalls from being invoked.

At Arcana Research we use state-of-the-art tooling and innovative techniques to help mitigate and harden against modern hacking and exploitation techniques.

Contact Ryan:

Email

elfmaster [at] arcana-research.io

Twitter

@ryan_elfmaster