Welcome to Netfilter!

In this blog we’ll be exploring Netfilter, what it is, how it works, why we’d want to use it, and what we can use it for.

Netfilter… What’s that?

Recently I’ve been exploring Netfilter a little bit.
Before I start explaining why Netfilter is so interesting, let’s start by getting to know it a bit. In the immortal words of The netfilter.org Project:

The netfilter project is a community-driven collaborative FOSS project that provides packet filtering software for the Linux 2.4.x and later kernel series. The netfilter project enables packet filtering, network address [and port] translation (NA[P]T), packet logging, userspace packet queueing and other packet mangling. The netfilter hooks are a framework inside the Linux kernel that allows kernel modules to register callback functions at different locations of the Linux network stack. The registered callback function is then called back for every packet that traverses the respective hook within the Linux network stack.

I know, super descriptive. By the end, hopefully we’ll understand all that.

For reference, here’s a picture of the path a packet takes through the kernel. Everything colorful is something related to Netfilter Netfilter image

Let’s start with a super simple definition of what Netfilter is:

Netfilter allows using hooks in the kernel to handle packets passing through the system.

In order to understand that, let’s talk about IPTables.

IPTables

IPTables is a binary which allows a system administrator to set up, maintain, and inspect “tables” of IP packet filter rules in the Linux Kernel (This uses the Netfilter kernel module).
Each table contains a number of built-in chains, and can also contain user-defined chains.
IPTables contains a few default tables:

  • Filter - Used for filtering packets.
  • NAT - Used for implementing NAT rules. Changing src/dst address, etc.
  • Mangle - Used for altering IP headers. For example, changing the TTL of a packet.
  • Raw - Used for marking packets, as a way to opt out of connection tracking.
  • Security - Used for SELinux purposes.

Each table allows setting rules in specific “chains”:

  • Prerouting - Allows adding rules before packet routing decision is made.
  • Input - Allows adding rules after packet is destined to local system, before being recieved by user-mode.
  • Forward - Allows adding rules for packets destined to another host.
  • Output - Allows adding rules for locally generated packets
  • Postrouting - Allows adding rules for packets leaving the system (Either local, or routed)

Each chain is a list of rules which can match a set of packets. Each rule specifies what to do with a packet that matches. This is called a “target”, which may be one of the following:

  • User-defined chain - We’ll explain this later, but chains can call each other
  • ACCEPT - Allow the packet. Stop processing more rules.
  • DROP - Drop the packet. Stop processing more rules.
  • RETURN - Return the packet to the previous chain. If first chain, use default.
  • QUEUE - Pass packet to userspace. Queue is not really relevant to this post.

IPTables is extensible, and thus can (and does) have extra targets not listed here.

Let’s check out what that means, by adding a rule with IPTables, which will drop all incoming tcp packets on port 1337.

iptables -t filter -A INPUT -p tcp --dport 1337 -j DROP
# -t [TABLE] - Specifies the table to add the rule to.  (Default is filter)
# -A [CHAIN] - Adds a rule to the specific chain (Use -D to delete).
# -p [protocol] - Allows specifying a protocol.  Only works for known protocols.
# --dport [port] - A protocol specific flag, allows filtering by destination port.
# -j [TARGET] - Specifies the action.

If you now attempt to communicate on port 1337, you will see that it doesn’t work. (You won’t recieve a response to the SYN.)

# To delete the rule:
iptables -D INPUT -p tcp --dport 1337 -j DROP

Sounds cool, what can we do with this?

Now I’m going to show how to do things that are a wee bit more advanced, so you can understand the power of Netfilter (Via IPTables).

Firewalls

As you might already know, iptables is often used for firewalls. Let’s show you an example:
I rent a VPS, and I want my VPS to be as safe from attack as possible. For that reason, I want to block as many things as possible. This is what I would do:

# This command dumps all the rules from the filter table, from the INPUT chain.
# Each line dumped this way is a valid iptables command.
➜  ~ iptables -t filter -S INPUT
# -P sets the default action.  In this case, by default I would drop all packets.
-P INPUT DROP
# -m [module] Specifies which modules to use.  In this case, I want to use a conntrack flag (--ctstate), so I need to specify it.
# --ctstate [STATE] only matches packets in that state.  In this case, I accept all packets which are RELATED or ESTABLISHED. (AKA - Already in progress)
-A INPUT -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
# -i [interface] Only matches packets sent to that interface.
-A INPUT -i lo -j ACCEPT
# Accepting all packets destined for SSH (Listening on 11195)
-A INPUT -p tcp -m tcp --dport 11195 -j ACCEPT
# Accepting all packets destined for Wireguard (Listening on UDP:11196)
-A INPUT -p udp -m udp --dport 11196 -j ACCEPT

In 5 lines of IPTables, I have a secure, simple, easy firewall. You can also limit similarly in the OUTPUT chain.

Turn into a Router that performs NAT

Let’s assume my server is running Wireguard (A VPN software).
I want all clients which connect to me, to be able to access the internet using my servers IP address1. In that case, I would do the following:

# I'm assuming wg0 is the name of my Wireguard interface.
# Because Wireguard is a VPN, I want to pass all packets going to/from the VPN.
# This rule allows all packets that came from the Wireguard interface (From clients)
➜  ~ iptables -A FORWARD -o wg0 -j ACCEPT
# This rule allows all packets destined to the Wireguard interface.  (Returning to clients)
➜  ~ iptables -A FORWARD -i wg0 -j ACCEPT
# This rule applies MASQUERADE to all packets leaving my server to the internet (After Routing it, before sending).  
# Masquerade changes the IP address to be the eth0 IP address.
# Upon recieveing a response, the NAT Netfilter module knows how to convert the packet back into the original IP address.
➜  ~ iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE

My server can now route all packets to/from the internet, with a NAT setup.

Proxy server

This one is a bit different. Let’s assume I wanted to use my server as a proxy to 1.1.1.1, but I didn’t want to install any software.1 This is how I would go about doing that:

# -j DNAT - Destination nat, then specify destination.  Here I specify that if there is a packet destined to me on port 443, I will redirect it to 1.1.1.1:443
➜  ~ iptables -t nat -I PREROUTING -d {my_ip} -p tcp --dport 443 -j DNAT --to-destination 1.1.1.1:443
➜  ~ iptables -t nat -I POSTROUTING -p tcp --dport 443 -d 1.1.1.1 -j MASQUERADE
# Now if I were to run, my traffic would be rerouted to 1.1.1.1
➜  ~ curl -k https://{my_ip}

A list of tables/chains: TableChains

A bit of Netfilter internals

As you can see, there are many things to do with IPTables, and we’ve only explored a very small part of it. Before we continue, let’s look at how this works in the kernel. We’ll be looking at Netfilter, because IPTables is only a “proxy” to Netfilter.

Netfilter has 5 hooks we can register with. As packets are handled, they will trigger kernel modules that are registered. The hooks that are triggered depend on whether the packet is coming in, going out, where it is going, etc. The 5 hooks are as follows:

  • NF_IP_PRE_ROUTING - This hook will be triggered by any incoming traffic after entering the network stack. This hook is processed before any routing decisions have been made.
  • NF_IP_LOCAL_IN -This hook is triggered after an incoming packet has been routed if the packet is destined for the local system.
  • NF_IP_FORWARD - This hook is triggered after an incoming packet has been routed if the packet is to be forwarded to another host.
  • NF_IP_LOCAL_OUT - This hook is triggered by any locally created outbound traffic as soon it hits the network stack.
  • NF_IP_POST_ROUTING - This hook is triggered by any outgoing or forwarded traffic after routing has taken place.

When a kernel module registers with these hooks, it must provide a priority, which determines the order in which it will be called when the hook is triggered. This allows multiple hooks to be triggered in a deterministic order. Each module is called in order and returns a decision to Netfilter.

The chains that IPTables uses, mirror the 5 hooks:

  • PREROUTING - NF_IP_PRE_ROUTING
  • INPUT - NF_IP_LOCAL_IN
  • FORWARD - NF_IP_FORWARD
  • OUTPUT - NF_IP_LOCAL_OUT
  • POSTROUTING - NF_IP_POST_ROUTING

One more illustration to show how the tables and chains interact:



                                   Netfilter hooks

                                  +-----------> local +-----------+
                                  |             process           |
                                  |                               |
                                  |                               |
                                  |                               |
                                  |                               v
        MANGLE      +-------------+--------+               +----------------------+    RAW
        FILTER      |                      |               |                      |    conntrack
        SECURITY    |        input         |               |      output          |    MANGLE
        SNAT        |                      |               |                      |    DNAT
                    +------+---------------+               +-------+--------------+    routing
                           ^                                       |                   FILTER
                           |                                       |                   SECURITY
                           |                                       |                 
                           |                                       |  
                           |            +---------------------+    |         +-------------+
     +-----------+                      |                     |    +-------> |             |
+--> |pre routing+----  route    -----> |      forward        |              |post routing +---->
     |           |      lookup          |                     +------------> |             |
     +-----------+                      +---------------------+              +-------------+
     
     RAW                                       MANGLE                         MANGLE
     conntrack                                 FILTER                         SNAT
     MANGLE                                    SECURITY
     DNAT
     routing

One more thing about IPTables: When you add an IPTables rule, it adds it to the global struct: xt, which is allocated upon module insertion.
As you can see in the source code (For example, xt_register_targets), every time a new chain/match/target is inserted, it updates the global xt. When IPTables wants to show rules, it iterates over xt, and lists all the rules. This means, if we add our own rule, not via xt, IPTables won’t show us that rule, which is useful if we want to hide rules.

Writing Kernel modules

Let’s show an example of what we can do with a basic Netfilter hook. I’ll include the code, and comment along the way.

After compiling this (Using this) makefile.

➜  ~ make -s
➜  ~ insmod LKM-simple-nf.ko 
➜  ~ dmesg | tail
[  848.453051] Sending packet!
[  848.453082] Sending packet!
[  848.453086] Sending packet!
[  848.453161] Sending packet!
[  849.194945] Sending packet!
[  849.195127] Sending packet!
[  849.197056] Sending packet!
[  849.197070] Sending packet!
[  849.197604] Sending packet!
[  849.197612] Sending packet!
➜  ~ rmmod LKM-simple-nf.ko

As you can see, we’re on to something.
Let’s try something a little more useful:

➜ ~ make -s
➜ ~ insmod LKM-simple-nf.ko
➜ ~ dmesg | tail           
[ 1320.922381] Packet sent 172.19.241.12:22 -> 172.19.240.1:64689
[ 1321.014828] Packet sent 127.0.0.1:56976 -> 127.0.0.1:37295
[ 1321.016172] Packet sent 127.0.0.1:37295 -> 127.0.0.1:56976
[ 1321.016184] Packet sent 127.0.0.1:56976 -> 127.0.0.1:37295
[ 1321.016286] Packet sent 172.19.241.12:22 -> 172.19.240.1:64689
[ 1321.016321] Packet sent 127.0.0.1:37295 -> 127.0.0.1:56976
[ 1321.016325] Packet sent 127.0.0.1:56976 -> 127.0.0.1:37295
[ 1321.016409] Packet sent 172.19.241.12:22 -> 172.19.240.1:64689
[ 1321.017337] Packet sent 127.0.0.1:37295 -> 127.0.0.1:56976
[ 1321.017348] Packet sent 127.0.0.1:56976 -> 127.0.0.1:37295
➜ ~ rmmod LKM-simple-nf.ko

Those are a few funky things to do with Netfilter (Of course, both examples could be done in a safe and secure way via eBPF). The main reason I might not want to use BPF, is that BPF requires a process to be running.

Sneaky Netfilter

Okay, so we’ve learned a bit about how to write a kernel module that interacts with Netfilter, what else can we build with that? A few things that come to mind:

  • Kernel Firewall - Basically IPTables, but with more control, and hidden
  • Packet sniffer
  • Packet Proxy
  • Advanced packet logging
  • Kernel-Based Malware with hidden networking
  • Packet injector/Editor

We’ve already seen how we can implement a TCP Forwarder via IPTables. It’d be pretty easy to implement that here, so let’s look at something a bit different. We’re going to write a kernel module that uses Netfilter to intercept ICMP (meaning usermode won’t see it), and execute commands sent within, assuming we send it with a special code.
I decided to use ICMP and command execution for simplicity, but for all purposes it could be TCP/UDP, encrypted, and with many more features.

As for the code to execute commands:

What we’re doing here is capturing specific ICMP packets, with a made up ICMP code (so only our special packet will trigger this), extracting the data from the packet, and running that command (Using a small trick to send the output back to me instead of having to implement packet sending in the kernel).
It wouldn’t be too difficult to implement upload/download, port forwarding, or any other malware feature, and all of it would be unblockable, since we will always hook first. This is much better than anything we’d have in usermode.

So what are our solutions?

Now that we’ve explored why Netfilter can be dangerous, let’s try to figure out how to solve this.

  1. If we were to capture traffic with libpcap, we’d still see the traffic - This is pretty easily solvable, as hooking the function libpcap uses is pretty easy, but we cannot prevent sniffing on the network along the way, and this type of traffic is hard to hide.
  2. We could find the global list of nf_hook_ops, kept by Netfilter, and scan each of them to search for hooks. This would be harder to block, but still possible.
  3. Monitor kernel modules loaded. This becomes a game of cat and mouse (I can hide the fact that I’m loaded, but you can hook into that, but I can hook into that, etc)
  4. In the Netfilter hook, we used the priority: NF_IP_PRI_FIRST. This means we will always be hooked first. What happens if two modules use that? Well the answer is first come first serve. So if you write your own module which hooks packets coming in, make sure it is loaded before any other module, and compare them with actually accepted packets in usermode, you could possibly identify packets gone missing. Another option could be to recompile IPTables to use the NF_IP_PRI_FIRST priority, so your drop rules will be caught before the attackers rules.
  5. Aside from that, you don’t have many more options.

So… What now?

Well I guess it depends on whether you’re looking to attack, or defend.
If you’re looking to attack, then you’ve learned a great way of keeping yourself stealthy and activating upon recieving packets, without using threads orusermode programs, in a hidden way.

If you’re looking to defend, you’ve learned about another something you need to defend from, and it’s not an easy game of cat and mouse to win, but there are ways to fight (As outlined in solutions)

  1. This only works if you enable packet forwarding on linux, you can check that by running cat /proc/sys/net/ipv4/ip_forward and enable it by running echo 1 > /proc/sys/net/ipv4/ip_forward  2