Hybrid Fileless (Netlink)

It is the third post in the series and, in my opinion, the most intriguing one compared to the previous two, focusing on extracting symbol addresses in Linux. This one tries to minimize the use of regular files, shared objects, and file operations and minuscules the attention that is caused by the existence of files. I really love it.

Netlink Solution

Netlink is a communication mechanism(socket-family) used in the Linux kernel for inter-process communication(IPC) between the kernel and user-space processes, as well as between different user-space processes. It was designed to replace the older ioctl interface, providing a more flexible and extensible way to exchange information. For user-space processes, Netlink offers a standard socket-based interface.For kernel modules, it provides a specialized kernel API. Netlink uses the AF_NETLINK socket family. This family supports various protocols, each identified by a unique number, such as NETLINK_ROUTE for routing information and NETLINK_AUDIT for auditing.

Cool! This mechanism can implement the new idea. Well, I assume you grasped the new idea upon reading the post title, but let’s think about the new idea:

Kernel-space program(say kernel module) waits to handle the message that is sent by the user-space program. The user-space program reads the address from /proc/kallsyms and sends it to the kernel module through the Netlink socket. From here, the kernel module can use the symbol address that was sent from the user-space program.

Good, Let’s see what will happen in each space. First, I will explain the kernel-space code, because virtually it should load first.

What happens in the kernel space?

Generally, this kernel module sets up a Netlink socket, defines a callback function to handle incoming messages, and ensures proper initialization and cleanup of the socket. This allows for efficient communication between user-space processes and the kernel.

After including the necessary headers, a constant NETLINK_USER with the value 31 is defined. It is the protocol number for the Netlink socket:

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/netlink.h>
#include <net/sock.h>

#define NETLINK_USER 31
C

Then, a pointer nl_sk to a sock structure, which will represent the Netlink socket is declared:

struct sock * nl_sk = NULL;
C

A function nl_recv_msg that will be called when a message is received on the Netlink socket is defined. It extracts the Netlink message header(nlmsghdr) from the socket buffer(skb), retrieves the address from the message, and prints it to the kernel log:

static void nl_recv_msg(struct sk_buff *skb) {
    struct nlmsghdr *nlh;
    unsigned long addr;
    nlh = (struct nlmsghdr *)skb->data;
    addr = ((unsigned long)nlmsg_data(nlh));
    printk(KERN_INFO "Received address from user space: 0x%lx\n", addr);
}
C

Following line sets up a netlink_kernel_cfg structure with the nl_recv_msg function as the input callback:

struct netlink_kernel_cfg cfg = { .input = nl_recv_msg, };
C

Then, the Netlink socket with netlink_kernel_create is created:

nl_sk = netlink_kernel_create(&init_net, NETLINK_USER, &cfg);
C

That’s it! Now the kernel module is ready to receive the symbol address from user-space. The kernel module code is here.

What happens in the user space?

Generally, this user-space program sets up a Netlink socket, prepares a message containing an address, and sends this message to the kernel. It ensures proper initialization and cleanup of the socket, facilitating communication between user-space and kernel-space.

After including the necessary headers, a constant NETLINK_USER with the value 31 is defined. It is the protocol number for the Netlink socket:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <linux/netlink.h>
#include <ctype.h>

#define NETLINK_USER 31
C

In the following line, a Netlink socket is created. It specifies PF_NETLINK as the protocol family, SOCK_RAW as the socket type, and NETLINK_USER as the protocol number. If the socket creation fails, it prints an error message and exits:

sock_fd = socket(PF_NETLINK, SOCK_RAW, NETLINK_USER);
C

This block initializes the destination address structure(dest_addr) to zero. It sets the address family to AF_NETLINK, the destination pid to 0(indicating the kernel), and the groups to 0(indicating unicast communication):

nlh = (struct nlmsghdr *)malloc(NLMSG_SPACE(sizeof(unsigned long)));
memset(nlh, 0, NLMSG_SPACE(sizeof(unsigned long)));
nlh->nlmsg_len = NLMSG_SPACE(sizeof(unsigned long));
nlh->nlmsg_pid = getpid();
nlh->nlmsg_flags = 0;
C

And, copy the address data into the Netlink message payload:

memcpy(NLMSG_DATA(nlh), &addr, sizeof(unsigned long));
C

This block sets up the iovec structure and the msghdr structure for sending the message. iov_base points to the Netlink message, and iov_len is the length of the message. msg_name points to the destination address, and msg_namelen is its length. msg_iov points to the iovec structure, and msg_iovlen is set to 1:

iov.iov_base = (void *)nlh;
iov.iov_len = nlh->nlmsg_len;
msg.msg_name = (void *)&dest_addr;
msg.msg_namelen = sizeof(dest_addr);
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
C

Finally, It sends the message using sendmsg:

sendmsg(sock_fd, &msg, 0);
C

The user-space code is here.

Test it!

To make the test simple, I implement the kernel code in the form of a kernel module. Like before, we need to compile the source codes. The kernel code compilation is done with make. After that, we need to load the kernel module:

# make
# insmod netlnk_lkm.ko
Bash

Now, compile the user-space code with:

# gcc -o userspace userspace.c
Bash

And, run the userspace binary:

# ./userspace
Bash

Now, if you look at the kernel logs(dmesg), you will find something like this:

Address read from shared memory: ffffffffbb99f2e0
Bash

Note that, you can run the user-space program automatically with the call_usermodehelper() as I said here. But this is up to you 😀

2 Comments Hybrid Fileless (Netlink)

  1. Marmo

    This method was super fascinating! I read the first two articles and loved how your ideas evolved, but this one really stood out. do you think there’s a way to completely get rid of the need for a user-level program, or at least make it so it doesn’t need a file to run? like, could it be executed as a real-time payload in memory instead of having a binary file?

    Reply
    1. Avatar photoAmirReza

      Dear Marmo, Thank you and, this method relies on extracting the symbol address from /proc/kallsyms. So, abstractly it can be done on kernel level, but reading /proc/* by a kernel module is challenging and may need to bypass some security mechanisms. Actually, I’m trying to find a solution for that but now, I focus on minimizing the disk-related operations.

      Reply

Leave a Reply

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

twelve − 11 =