Creating a Linux Security Module with Kprobes: Blocking network of Targeted Processes

Emanuele Santini
8 min readFeb 2, 2024
Tux linux penguin mascot
Tux could serve as an efficient penguin-themed network firewall.

The Linux Security Module (LSM) is a framework in the Linux kernel that provides mechanisms for implementing various security controls through kernel extensions. Traditionally, these extensions known as modules, are integrated at compile-time and can be customized at boot with the “security=…” kernel command line option. On the current Linux kernel versions the LSM as loadable modules are not permit (except if you want to start to use BPF hooks). However, with the dynamic capabilities of kprobes — special kernel functions that enable runtime interception of other kernel functions — we can develop LSM programs that are loadable at runtime, as a traditional kernel module.

The Linux Security Module (LSM)

An example of a Linux Security Module is SELinux — a security module integrated into the Linux kernel that provides a mechanism for supporting access control security policies — or AppArmor. The Linux Kernel source code includes a directory named ‘security’ that contains all the Linux Security Module (LSM) code. Within this directory, there’s a file named ‘security.c’, which contains various kernel hooks (callback called by the kernel calls). An LSM module can register themselves with these hooks to intercept and make decisions on underlying kernel calls, thereby controlling and enforcing mandatory access control.

We aim to develop a module designed for blocking the network of a target process, identified by its executable. To achieve this, we plan to implement mandatory access control on key network operations such as connect, accept, recvfrom, sendto, recv, and send. These operations are fundamental in both connection-oriented and connectionless networking scenarios.

Opening the security.c kernel source file, we can looking for this four hooks:

int security_socket_accept(struct socket *sock, struct socket *newsock)
{
return call_int_hook(socket_accept, 0, sock, newsock);
}
...
int security_socket_connect(struct socket *sock,
struct sockaddr *address, int addrlen)
{
return call_int_hook(socket_connect, 0, sock, address, addrlen);
}
...
int security_socket_recvmsg(struct socket *sock, struct msghdr *msg,
int size, int flags)
{
return call_int_hook(socket_recvmsg, 0, sock, msg, size, flags);
}
...
int security_socket_sendmsg(struct socket *sock, struct msghdr *msg, int size)
{
return call_int_hook(socket_sendmsg, 0, sock, msg, size);
}

Each of these callbacks is called from the internal kernel calls. An LSM module can hook into these callbacks to implement mandatory access control. If the callback returns, for example, a -EPERM value, access to that resource is denied.
We can check, for example, the recvfrom and recv system call (from net/socket.c)

SYSCALL_DEFINE6(recvfrom, int, fd, void __user *, ubuf, size_t, size,
unsigned int, flags, struct sockaddr __user *, addr,
int __user *, addr_len)
{
return __sys_recvfrom(fd, ubuf, size, flags, addr, addr_len);
}

SYSCALL_DEFINE4(recv, int, fd, void __user *, ubuf, size_t, size,
unsigned int, flags)
{
return __sys_recvfrom(fd, ubuf, size, flags, NULL, NULL);
}

Both of the system calls performing the kernel function __sys_recvfrom , that internally calls the sock_recvmsg:

int sock_recvmsg(struct socket *sock, struct msghdr *msg, int flags)
{
int err = security_socket_recvmsg(sock, msg, msg_data_left(msg), flags);

return err ?: sock_recvmsg_nosec(sock, msg, flags);
}
EXPORT_SYMBOL(sock_recvmsg);

Within the sock_recvmsg function, security_socket_recvmsg is called. As we have seen, this latter function performs the hook to execute all loaded LSM modules. An LSM can return a security error to deny user access to this resource, thereby preventing the process from receiving data from the network.

Kprobes

As we mentioned, LSM programs are not loadable and are statically linked to the kernel image. However, we can use Kprobes to hook into the security callbacks.

Kprobes in the Linux kernel is a tool used for dynamic tracing of kernel functions. It allows developers to dynamically break into any kernel routine and collect debugging and performance information. You can think of it as a way to set breakpoints in kernel code. Kprobes comes with Kretprobes, which helps to set breakpoints at the start (entry point) and end (exit point) of any kernel function, specified by its symbol. It is most used for kernel live-patching (more information about livepatch here: https://docs.kernel.org/livepatch/livepatch.html), and in our example, we will use it to attach code that executes when the security hooks are performed.

The kernel calls that we want to control will be the four security function we have explored above, so we have to declare the strings with its symbols in our code:

/* The kernel functions we want to hooks: */
const char *sendmsg_hook_name = "security_socket_sendmsg";
const char *recvmsg_hook_name = "security_socket_recvmsg";
const char *connect_hook_name = "security_socket_connect";
const char *accept_hook_name = "security_socket_accept";

To set a kretprobe into the kernel (for example the security_socket_sendmsg probe) , we can doing something like this:

struct kretprobe sendmsg_probe = {
.handler = security_hook_exit,
.entry_handler = security_hook_entry,
.data_size = 0, // We don't need data to exchange beetwen entry and exit callbacks
.maxactive = NR_CPUS, // Number of consecutive probe execution
};

Where, handler and entry_handler are two functions that we must to implement, to manage the operation between and after the execution of the security_socket_sendmsg. They are the type of:

// Exit callback from the probed functions
static int security_hook_exit(struct kretprobe_instance *ri, struct pt_regs *regs);
// Entry callback from the probed functions
static int security_hook_entry(struct kretprobe_instance *ri, struct pt_regs *regs);

We need an instance of kretprobe for each function we want to hook, so we have implemented a macro on our code to make it easy: declare_kretprobe.

The entry callback we have declared, security_hook_entry, can return 1 or 0. This depends on whether you wish to invoke the exit handler or not after the completion of the probed kernel call. In our code, we will call the exit handler only if the current process executing the function is the one we aim to block from accessing the network. So, for default the handler returns 1 (do not call the exit handler).

int security_hook_entry(struct kretprobe_instance *ri, struct pt_regs *regs)
{
/* TODO:
* If current is the process to block:
* return 0 (will execute security_hook_exit at the end of the probed kernel function)
*/

// Current is not the process to block: return 1
return 1;
}

If we wish to modify the return value of a function, we need to alter the RAX register on the stack. We can access the pt_regs structure in both the entry and exit callbacks. The exit callback, if invoked, will place the EACCES error into the RAX register. This action will force the security function invoked to fail.

int security_hook_exit(struct kretprobe_instance *ri, struct pt_regs *regs)
{
// rax contains the exit value of the probed function
regs->ax = -EACCES;
return 0;
}

Getting the current process executable

Our goal is to block network access for a target process. We aim to identify this process by using the path of its executable file. For example, we intend to block network access for a process whose executable is located at the following path:/usr/bin/wget.

We are introducing a new function, to get the executable file object from the task_struct of the current process:

static struct file* my_get_task_exe_file(struct task_struct *ctx)
{
struct file *exe_file = NULL;
struct mm_struct *mm;

if(unlikely(!ctx))
return NULL;

task_lock(ctx);
mm = ctx->mm;

if(mm && !(ctx->flags & PF_KTHREAD))
{
rcu_read_lock();

exe_file = rcu_dereference(mm->exe_file);
if(exe_file && !get_file_rcu(exe_file))
exe_file = NULL;

rcu_read_unlock();
}

task_unlock(ctx);

return exe_file;
}

In this function, we are simply retrieving the file object pointer stored in the mm_struct of the task_struct. To access this data, we need to acquire the task_lock, which is necessary when accessing the mm_struct of the process. Before retrieving the file pointer, we must ensure that the current process is not a kernel thread. We do this by checking if mm pointer is not null and ctx->flags is not masked with PF_KTHREAD.

The struct file on the kernel contains the struct path object field, that represent the path structure on the file system. For retrieving the string path we can use the d_path kernel function.

So, we can now include the executable path check in our security_hook_entry callback, and return 0 if the check is true. By returning 0, the exit handler will be executed, which will force the security hook to return EACCES error by directly substituting the RAX register.

The code completed

// SPDX-License-Identifier: GPL-2.0
/*
* Copyright (C) 2023 - 2024 Emanuele Santini <emanuele.santini.88@gmail.com>
*/

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/kprobes.h>
#include <linux/file.h>

// Put here the process you want to block
const char *executable_path = "/usr/bin/wget";

// Exit callback from the probed functions
static int security_hook_exit(struct kretprobe_instance *ri, struct pt_regs *regs);
// Entry callback from the probed functions
static int security_hook_entry(struct kretprobe_instance *ri, struct pt_regs *regs);

/* The kernel functions we want to hooks: */
const char *sendmsg_hook_name = "security_socket_sendmsg";
const char *recvmsg_hook_name = "security_socket_recvmsg";
const char *connect_hook_name = "security_socket_connect";
const char *accept_hook_name = "security_socket_accept";

// Utility function to initialize a kretprobe data
#define declare_kretprobe(NAME, ENTRY_CALLBACK, EXIT_CALLBACK, DATA_SIZE) \
static struct kretprobe NAME = { \
.handler = EXIT_CALLBACK, \
.entry_handler = ENTRY_CALLBACK, \
.data_size = DATA_SIZE, \
.maxactive = NR_CPUS, \
};

// Utility function to register a kretprobe with error handling
#define set_kretprobe(KPROBE) \
do { \
if(register_kretprobe(KPROBE)) { \
pr_err("MB EDR drv - unable to register a probe\n"); \
return -EINVAL; \
} \
} while(0)

declare_kretprobe(sendmsg_probe, security_hook_entry, security_hook_exit, 0);
declare_kretprobe(recvmsg_probe, security_hook_entry, security_hook_exit, 0);
declare_kretprobe(connect_probe, security_hook_entry, security_hook_exit, 0);
declare_kretprobe(accept_probe, security_hook_entry, security_hook_exit, 0);

static int __init process_network_blocker_init(void)
{
sendmsg_probe.kp.symbol_name = sendmsg_hook_name;
recvmsg_probe.kp.symbol_name = recvmsg_hook_name;
connect_probe.kp.symbol_name = connect_hook_name;
accept_probe.kp.symbol_name = accept_hook_name;

set_kretprobe(&sendmsg_probe);
set_kretprobe(&recvmsg_probe);
set_kretprobe(&connect_probe);
set_kretprobe(&accept_probe);

return 0;
}

static void __exit process_network_blocker_exit(void)
{
unregister_kretprobe(&sendmsg_probe);
unregister_kretprobe(&recvmsg_probe);
unregister_kretprobe(&connect_probe);
unregister_kretprobe(&accept_probe);
}

/* Returns the file pointer of the executable of a task_struct.
* The file pointer returned must to be released with fput(file)
*/
static struct file* my_get_task_exe_file(struct task_struct *ctx)
{
struct file *exe_file = NULL;
struct mm_struct *mm;

if(unlikely(!ctx))
return NULL;

task_lock(ctx);
mm = ctx->mm;

if(mm && !(ctx->flags & PF_KTHREAD))
{
rcu_read_lock();

exe_file = rcu_dereference(mm->exe_file);
if(exe_file && !get_file_rcu(exe_file))
exe_file = NULL;

rcu_read_unlock();
}

task_unlock(ctx);

return exe_file;
}

int security_hook_entry(struct kretprobe_instance *ri, struct pt_regs *regs)
{
struct file *fp_executable;
char *res;
char exe_path[256];

memset(exe_path, 0x0, 256);

// Get the current task executable file pointer
fp_executable = my_get_task_exe_file(get_current());
if(fp_executable == NULL)
return 1; // Do not call exit handler

// Gets the path of the fp_executable
if(IS_ERR(res = d_path(&fp_executable->f_path, exe_path, 256)))
return 1;

/* If the process executable is the same of executable_path (the one we want to block):
* 0 is returned: The exit callback is executed
*/
if(!strncmp(res, executable_path, 256))
{
printk("Blocking %s\n", res);
return 0;
}

fput(fp_executable);

// Retrun 1: Do not execute the exit callback (security_hook_exit)
return 1;
}

/*
* Exit callback:
* Executed when the probed function is eneded
*/
int security_hook_exit(struct kretprobe_instance *ri, struct pt_regs *regs)
{
// rax contains the exit value of the probed function
regs->ax = -EACCES;
return 0;
}

module_init(process_network_blocker_init);
module_exit(process_network_blocker_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Emanuele Santini <emanuele.santini.88@gmail.com>");

You can find the complete code and Makefile here: https://github.com/emalele1688/linux-kernel-examples/tree/main/process_network_blocker

Compile the module, insert with the insmod commnad, and try to download something with wget.

--

--

Emanuele Santini

Hello! I'm a Linux Kernel developer and an enthusiast in the world of hacking and cybersecurity. My job are Linux systems' security and performance.