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_slow
nf_hook_slow is the core scheduling function of Netfilter, responsible for:
- Iterating through all registered callback functions at specific hook points
- Executing these callbacks in order of priority
- Determining the fate of packets based on return values:
NF_ACCEPT (1)– Continue processingNF_DROP (0)– Drop the packetNF_STOLEN (2)– The packet has been consumedNF_QUEUE (3)– Send to user space queueNF_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
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
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
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