Most userspace system tools just parse and manipulate data from one or more files and present them nicely to STDOUT. We’ve already seen this with processes (see Part 7), but this time we’re going to do the same thing with open ports. By the end, we’ll be able to open a listener on port 8080 (any port would do) without it showing up in things like netstat.

Assuming that a file is being read from, we need to try to find out which one. By looking at the output of strace -e openat netstat -tunelp, we can see that /proc/net/tcp and /proc/net/tcp6 are both read by netstat. A sensible guess is that tcp is going to be for IPv4 connections, and tcp6 is for IPv6 ones. This makes /proc/net/tcp our target because, if we can control it, then we can control the output produced by netstat (and others like it).

If you read my container escape writeup, then you’ll know that files under /proc aren’t really files, but are defined by functions within the kernel that are assigned to different IO operations (open/close/read/write/etc). If we want to control the “contents” of /proc/net/tcp, then we need to find the function that is called when a process tries to read it. If we grep for the top line of the output of /proc/net/tcp, we get a hit: net/ipv4/tcp_ipv4.c, specifically the function tcp4_seq_show(). Finally, we need to check that this function is exported - otherwise we can’t hook it!

$ sudo cat /proc/kallsyms | grep tcp4_seq_show
ffffffffbad03500 t tcp4_seq_show

All good! We’ll be able to hook tcp4_seq_show() using our normal Ftrace method (see Part 2) without any issues.

The eagle-eyed reader will have spotted that tcp4_seq_show() can’t possibly be the function called by sys_read() for /proc/net/tcp - it’s arguments are all wrong! This is true, but whatever function is really called, ends up using tcp4_seq_show() to fill the buffer that’s handed back to the user anyway.

Network Socket Structs

The first thing we see when we look at tcp4_seq_show() is:

static int tcp4_seq_show(struct seq_file *seq, void *v)
{
    struct tcp_iter_stat *st;
    struct sock *sk = v;

That argument v is casted to a new variable sk as a sock struct. We’d better take a look at this struct - it can be found in include/net/sock.h. The source code tells us that this particular struct is the “network layer representation of sockets” - this sounds like what we’re after! There’s bound to be a field in this struct that contains the listening port.

Okay, this struct has a lot of fields. The first one is another struct, this time sock_common, followed by a bunch of #defines into it.

struct sock {
    struct sock_common __sk_common;
#define sk_node         __sk_common.skc_node
    /* etc */
};

This is kinda like cheating - it lets us refer directly to sock->sk_node without having to go through sock_common first. A little odd, but whatever - let’s take a look at sock_common in the same file.

struct sock_common {
    /* redacted for clarity */

    /* skc_dport && skc_num must be grouped as well */
    union {
        __portpair      skc_portpair;
        struct {
            __be16      skc_dport;
            __u16       skc_num;
        };
    };

    /* redacted for clarity */
};

Aha! The skc_dport and skc_num fields are unioned together into __portpair. Looks like we’ve found our target! Looking back up at the definition of the sock struct, we see on line 365:

#define sk_num  __sk_common.skc_num

This means that we should be able to just dereference the sk_num field of a pointer to a sock struct and get the local port that’s being listened on! Earlier, we saw that tcp4_seq_show() casts one of it’s arguments to a sock struct and calls it sk (line 2603). In particular, note that it looks like a single socket is passed at a time to tcp4_seq_show(), so we should expect it to be called many times as it goes through all the open connections.

To help cement this, we can take a look at the “contents” of /proc/net/tcp again:

$ cat /proc/net/tcp
  sl  local_address rem_address   st tx_queue rx_queue tr tm->when retrnsmt   uid  timeout inode  
  0:  00000000:E115 00000000:0000 0A 00000000:00000000 00:00000000 00000000   1000 0 864656 1 00000000f6208100 99 0 0 10 0
  # etc

The first entry under local_address is 00000000:E115. Anyone who’s had the pleasure of writing a reverse shell in assembly before will probably recognise that this is an IP/port combo in hex. Indeed, 0xe115 = 57621, and the output of netstat confirms that a process is listening on 0.0.0.0:57621 (in this case, the culprit was Spotify).

When we write our hook, we will cast v to a sock struct too (calling it sk for simplicity) and just dereference sk->sk_num to get the listening port number for each entry!

Writing the hook

Believe it or not, understanding all of that was the hard part! The actual hook itself is pretty simple, as you’ll soon see. The point of going through all of the above was to illustrate the research into the kernel source code that is required to know how to build these modules. That is usually the longest part of the process!

So, we know that tcp4_seq_show() is going to get called repeatedly when we read from /proc/net/tcp, and that a pointer to a sock struct is passed to it in the second argument. All our hook needs to do is check if the listening port equals the one we want to hide, return 0 if it does, or return the real tcp4_seq_show() if it doesn’t. The only caveat is that sometimes v isn’t initialized (like if we’re just printing the top line of the table). In this case, v = 0x1, so we need to check that this is not the case before trying to dereference it.

/*
 * Usual function declaration for the real tcp4_seq_show
 */
static asmlinkage long (*orig_tcp4_seq_show)(struct seq_file *seq, void *v);

/*
 * Function hook for tcp4_seq_show()
 */
static asmlinkage long hook_tcp4_seq_show(struct seq_file *seq, void *v)
{
    struct sock *sk = v;

    /*
     * Check if sk_num is 8080
     * (0x1f90 = 8080 in hex)
     * If sk doesn't point to anything, then it points to 0x1
     */
    if (sk != 0x1 && sk->sk_num == 0x1f90)
        return 0;

    /*
     * Otherwise, just return with the real tcp4_seq_show()
     */
    return orig_tcp4_seq_show(seq, v);
}

Note that, just like in Part 4, we don’t have to worry about multiple versions of the hook involving pt_regs because tcp4_seq_show() is not a syscall!

Pretty simple, right? This is probably the shortest function hook yet, but it took me longer than any of the others to write! There were many rabbit-holes that I got lost down before I settled on the method outlined above.

Putting it all together

Now that we’ve written the hook, go ahead and finish up all the Ftrace parts (Part 2), or grab the source from the repo.

Once you’ve built the module, go ahead and load it with insmod. In another terminal, create a netcat listener on port 8080 with nc -lvnp 8080. Now run the usual netstat -tunelp and you’ll see that port 8080 doesn’t show up! Unloading the rootkit with rmmod and trying netstat again will show you that it’s really there afterall.

hiding open ports

Note that we aren’t actually touching the internal socket table within the kernel, so the functionality of the connection is totally unimpaired!