Exploring Multi-Version Iptables Bypass in the Linux Kernel (Part Two)

Continuing from the previous article. In the last article, we pointed out the simplest bypass method, NF_STOP, but unfortunately, while NF_STOP was still usable in version 4.9.337, it was removed in 4.10-rc1. Therefore, our current requirement is to find a Gadget that can be utilized after 4.10-rc1. Here we introduce nf_hook_slowExploring Multi-Version Iptables Bypass in the Linux Kernel (Part Two)

nf_hook_slow is the core scheduling function of Netfilter, responsible for:

  1. Iterating through all registered callback functions at specific hook points
  2. Executing these callbacks in order of priority
  3. Determining the fate of packets based on return values:
  • NF_ACCEPT (1) – Continue processing
  • NF_DROP (0) – Drop the packet
  • NF_STOLEN (2) – The packet has been consumed
  • NF_QUEUE (3) – Send to user space queue
  • NF_REPEAT (4) – Call the current hook again

Our thought process is clear here: Hook nf_hook_slow, intercept at the scheduler level, and return NF_ACCEPT before any Netfilter callback is executed, directly skipping all subsequent processing.Next, we will detail the issues encountered in the practical environment.

Evolution of Function Signatures

Version 1: Linux 3.19 – 4.0 (7 parameters)

int nf_hook_slow(u_int8_t pf,              // Protocol family (PF_INET)                 unsigned int hook,         // Hook point number                 struct sk_buff *skb,       // Packet buffer                 struct net_device *indev,  // Input device                 struct net_device *outdev, // Output device                 struct list_head *head,    // Hook list head                 int thresh);               // Priority threshold

Exploring Multi-Version Iptables Bypass in the Linux Kernel (Part Two)The earliest version used a linked list to store hook callbacks, with numerous parameters.

Version 2: Linux 4.1 – 4.13 (2 parameters)

int nf_hook_slow(struct sk_buff *skb,                 struct nf_hook_state *state);  // Encapsulates all context

Exploring Multi-Version Iptables Bypass in the Linux Kernel (Part Two)Introduced nf_hook_state: Submitted on 2015-04-03 (commit 082a7a04b9d6), encapsulating all hook context:

struct nf_hook_state {    unsigned int hook;           // Hook point    u_int8_t pf;                 // Protocol family    struct net_device *in;       // Input device    struct net_device *out;      // Output device    struct sock *sk;             // Related socket    struct net *net;             // Network namespace    int (*okfn)(struct net *, struct sock *, struct sk_buff *);  // Callback to continue processing};

Version 3: Linux 4.14 – 6.8+ (4 parameters)

int nf_hook_slow(struct sk_buff *skb,                 struct nf_hook_state *state,                 const struct nf_hook_entries *e,  // Hook callback array                 unsigned int s);                  // Starting index

Exploring Multi-Version Iptables Bypass in the Linux Kernel (Part Two)Submitted on 2017-08-23 (commit 960632ece6597), changing the hook list to an array (nf_hook_entries):

struct nf_hook_entries {    u16 num_hook_entries;              // Number of callbacks    struct nf_hook_entry hooks[];      // Callback array (variable length)};struct nf_hook_entry {    nf_hookfn *hook;                   // Hook function pointer    void *priv;                        // Private data};

Note: There are actually more versions; the author has tested versions from Ubuntu 12.04 to 24.04 and CentOS 7-10, but has not tested other minor kernel versions. However, while reading the source code, it was found that there is a version with 3 parameters, so the above conclusions may not be entirely accurate. If incompatible versions are encountered, the same approach should be taken.The specific Hook strategy will not be elaborated here, along with the execution flow.

1. The CPU executes to the entry of the nf_hook_slow function

2. The Ftrace mechanism is triggered, calling our ftrace_callback

3. ftrace_callback modifies the IP register to point to hook_nf_hook_slow

4. The CPU jumps to hook_nf_hook_slow for execution

5. hook_nf_hook_slow can choose:

– Directly return (bypass the original function)

– Call orig_nf_hook_slow (normal flow)

Next, we will explain a practical pitfall.

Hook ipt_do_table (Kernel 3.10-3.18)

Issue 1: FTRACE_OPS_FL_IPMODIFY Crash

On kernels 3.10-3.18, using ftrace hook nf_hook_slow triggers serious issues.Some guesses regarding this issue: Netfilter itself is a network hook framework, while Ftrace is also a hook framework (the specifics of the hook framework will not be elaborated here). In kernels 3.10-3.18, using Ftrace to hook Netfilter triggers some unknown issues (it may also be due to the author’s lack of technical proficiency; readers can test it themselves), so we can only abandon Ftrace and turn to Kprobes.

Issue 2: Kprobes Conflict

Some distributions (such as RHEL7) have registered system-level kprobes on nf_hook_slow (for auditing/monitoring), which conflicts with our ftrace hook.For this issue, we can only switch to Kretprobe.

Evolution of ipt_do_table Function Signatures

Original API (3.10-3.12 vanilla)

unsigned int ipt_do_table(struct sk_buff *skb,                          unsigned int hook,                          const struct net_device *in,                          const struct net_device *out,                          struct xt_table *table);

New API (3.13+ and RHEL7 3.10)

unsigned int ipt_do_table(struct sk_buff *skb,                          const struct nf_hook_state *state,                          struct xt_table *table);

RHEL7 backported nf_hook_state in 3.10.

Kretprobe Hook

Kretprobe = Kprobe + Return Probe, divided into two stages:

Entry Handler:

  • Executed at the entry of the target function
  • Can access function parameters (via registers)
  • Can save data to ri->data

Return Handler:

  • Executed before the target function returns
  • Can access return values (regs->ax)
  • Can modify return values
  • Can read data saved by the entry handler

Mechanism:

1. When registering a kretprobe, the kernel places a breakpoint (int3) at the function entry

2. The CPU executes to the breakpoint, triggering an exception

3. The Kprobe exception handler takes over:

– Saves the return address

– Replaces the return address with the trampoline address

– Calls the entry_handler

4. The target function executes normally

5. When the function returns, it jumps to the trampoline (instead of the actual caller)

6. The trampoline calls the return_handler

7. The trampoline restores the actual return address and continues execution

Specific implementation omitted.

Note: There are various pitfalls in the specific implementation, most of which have been noted by the author in the text. If readers encounter issues during debugging, they can refer back to this article or consult the Linux kernel source code. This approach has been successfully tested on Ubuntu 12.04 – 24.04 and CentOS 7 – 10.

Appendix:

Online view of the Linux kernel source code:https://elixir.bootlin.com/

The bypass content comes to a temporary conclusion. If there are technical questions, please contact the author. Thank you for reading.

Leave a Comment