威胁简报
恶意软件
漏洞攻击
我们很高兴地宣布,我们之前向Linux内核报告的漏洞CVE-2026-43456已经修复,现在可以公开披露。我们将在本博客上分享详细信息。
该漏洞最引人注目的特点是,其底层代码于 2007 年被纳入 Linux 内核,并且在近 19 年的时间里一直未被发现,作为一个可利用的漏洞也未被检测到。
此外,由于该漏洞的特性,利用该漏洞的攻击可以在一秒内可靠地执行,成功率超过 99%。
鉴于此,我们向谷歌的漏洞赏金活动 kernelCTF提交了该漏洞,并获得了超过 8 万美元的奖励。

篇博文解释了 CVE-2026-43456 的根本原因以及如何实现权限提升。
影响的条件和范围
受影响版本:Linux 2.6.24 至 6.12.77
子系统:net/bonding
原因:类型混淆
介绍提交:1284cd3a2b740d0118458d2ea470a1e5bc19b187
修复提交:950803f7254721c1c15858fbbfae3deaaeeecb11
CAP_NET_ADMIN需要触发条件
从受影响的 Linux 内核版本可以看出,该漏洞的影响范围非常广泛。
漏洞详情
先决知识
在详细解释漏洞之前,让我们先了解一些必要的基础知识。
skb
在 Linux 内核的网络协议栈中,数据包struct sk_buff被视为(以下简称 skb)。
skb->head skb->data skb->tail skb->end, skb_shinfo(skb)| || | v v v v +----------------+-----------------+------------------+------------------+| headroom | packet data | tail room | skb_shared_info | +----------------+-----------------+------------------+------------------+skb->head`<headroom>` 指的是已分配缓冲区的开头,`<headroom>`指的skb->data是当前数据包的开头,`<headroom>`skb->tail指的是当前数据包的结尾。
skb->data - skb->head
此外,在 skb 缓冲区末尾struct skb_shared_info还会放置一个名为 `.src` 的文件。
struct skb_shared_info::flags该文件存储有关 skb 状态的信息(例如是否启用了零拷贝)。
Bonding
绑定(Bonding)是 Linux 的一项网络功能,它允许将多个网络接口视为一个单一接口。创建的接口称为绑定设备(bond device),属于该绑定设备的从属接口称为从属设备(slave devices)。
根本原因
这种漏洞被称为类型混淆。
以下是实际创建绑定设备的代码,而漏洞仅由一行代码引起,该代码以 ★ 标记:
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; ★ 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;header_ops这是一个函数指针表,其中包含一组用于处理某些协议数据包头部的函数。Bond
设备被设计和实现为能够透明地处理底层从设备,因此“Bond 对数据包头部执行的处理 = 底层设备对数据包头部执行的处理”,乍一看,直接重用这些函数似乎没有问题。
然而,其中一些功能需要引用和修改设备的内存区域。
由于绑定设备的内存区域类型不同且与从设备的内存区域不兼容,因此仅仅这一行代码就会导致类型混淆。
让我们实际检查一下代码,看看是否存在不兼容的情况,以及是否会出现问题。
首先,绑定设备sizeof(struct bonding)分配一个字节区域并将dev->priv其分配给(前面提到的内存区域)。
structrtnl_link_opsbond_link_ops __read_mostly = { .kind = "bond", .priv_size = sizeof(struct bonding), .setup = bond_setup, .maxtype = IFLA_BOND_MAX,dev = kvzalloc(struct_size(dev, priv, sizeof_priv), GFP_KERNEL_ACCOUNT | __GFP_RETRY_MAYFAIL);另一方面,例如,该漏洞利用中使用的 GRE 协议dev->priv采用了以下技术:
staticinlinevoid *netdev_priv(const struct net_device *dev){return (void *)dev->priv;}structip_tunnel *t = netdev_priv(dev);struct bonding这struct ip_tunnel是一个如下所示的结构,显然是不兼容的:
structbonding {structnet_device *dev;/* first - useful for panic debug */structslave __rcu *curr_active_slave;structslave __rcu *current_arp_slave;structslave __rcu *primary_slave;structbond_up_slave __rcu *usable_slaves;structbond_up_slave __rcu *all_slaves;...structip_tunnel {structip_tunnel __rcu *next;structhlist_nodehash_node;structnet_device *dev; netdevice_tracker dev_tracker;structnet *net;/* netns for packet i/o */unsignedlong err_time; /* Time when the last ICMP error * arrived */...因此,如果 GRE 作为从设备连接,并且在绑定设备上执行 GRE 标头处理,则可能导致内存损坏和其他问题。
利用漏洞进行权限提升
在 kernelCTF 中,需要公开漏洞利用方法的全部细节以供将来参考,而这次提交的漏洞利用方法也是公开的。
因此,虽然我不会透露所有细节,但我想在此简要说明一下。
问题的根源在于一个简单的单行语句,但要利用它创建一个稳定的漏洞利用程序则需要复杂的过程,如下所述,并且有很多因素需要考虑。
此外,这个看似简单却极其危险的漏洞为何在长达19年的时间里一直未被发现,原因可以从以下几点来解释。
步骤 1:KASLR 泄漏
与用户空间一样,Linux 内核也具有地址随机化功能(KASLR)。
因此,为了可靠地执行漏洞利用,必须识别这些随机化的地址。这个过程称为地址泄漏。
这次,我们使用了一种名为 IP6GRE 的协议来进行泄露。
如前所述,此漏洞dev->priv会导致类型混淆。对于 bond 协议dev->priv,它会变成struct bondingIP6GREdev->priv协议。struct ip6_tnl
struct bonding::recv_probe它位于bonding结构的偏移位置。另一方面,从IP6GRE端来看,它也是从偏移位置读取的。该值包含在接收到的数据包中,因为它是存储IPv6源地址的字段。0x38 struct ip6_tnl::parms.laddr0x38
/* 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];/* total size (bytes): 16 */ } in6_u;/* total size (bytes): 16 */ } laddr;/* offset | size */type = struct bonding {/* 0x0000 | 0x0008 */struct net_device *dev;.../* 0x0038 | 0x0008 */int (*recv_probe)(conststruct sk_buff *, struct bonding *, struct slave *);如上所述,recv_probe`f` 是一个函数指针,从以下代码bond_rcv_validate可以看出,它实际上包含的是以下函数的地址:
if (bond->params.arp_interval) { queue_delayed_work(bond->wq, &bond->arp_work, 0); bond->recv_probe = bond_rcv_validate;}bond_rcv_validate由于这是一个内核中的函数,因此
可以通过泄露内核基地址来计算内核基地址。
步骤 2:执行任意代码
步骤 1 允许我们绕过 KASLR,因此接下来我们的目标是破坏内存并将指令指针设置为任意值
(即执行任意代码)。
步骤 2.1:重写标志
总之,对于任意代码执行,我们现在将使用 GRE(IPv4 版本)而不是 IP6GRE。
具体来说,uarg->callback通过将以下代码中显示的值更改为任意值,您可以导致向该函数调用无效地址。
staticinlinestruct 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;}staticinlinevoid 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);}skb_shinfo(skb)->flags通过非法修改 的值,uarg我们可以导致在应该返回 NULL 值时返回非 NULL 值,从而实现无效的函数调用。
这看起来像是一种自上而下的方法,但这种skb_shinfo(skb)->flags重写是通过header_ops在以下 GRE 函数中greh->flags造成类型混淆来实现的:
staticintipgre_header(struct sk_buff *skb, struct net_device *dev,unsignedshort type,constvoid *daddr, constvoid *saddr, unsignedint len){structip_tunnel *t = netdev_priv(dev);structgre_base_hdr *greh;structiphdr *iph; ... iph = skb_push(skb, t->hlen + sizeof(*iph)); greh = (struct gre_base_hdr *)(iph + 1); greh->flags = gre_tnl_flags_to_gre_flags(t->parms.o_flags);当发生类型混淆时,t它struct bonding指的是 `a`。在这种情况下,
在 GRE 中t->hlen >= sizeof(*greh)应该是 `a` 的地方,struct bonding它却是 `a` t->hlen == 0。
结果,原本应该只返回(减去)`a` 的skb_push()地方,却只返回了`a` 。之后,由于`a`被设置为 `a`,` a` 又恢复到其原始值。换句话说,此时发生了缓冲区溢出。sizeof(struct iphdr) + sizeof(struct gre_base_hdr)skb->datasizeof(struct iphdr)skb->data
grehiph + 1grehskb->data
如先决条件部分所述,skb->data最初,它指的是 skb 中数据包的开头。
但是,在某些情况下,skb_shared_info开头会与此重叠(具体来说,当没有更多未使用的缓冲区时)。
greh因为它们skb_shared_info各自具有以下结构,在这些条件下,greh->flags写给skb_shared_info->flags……就变成了写给……
/* offset | size */type = struct gre_base_hdr {/* 0x0000 | 0x0002 */ __be16 flags;.../* offset | size */type = struct skb_shared_info {/* 0x0000 | 0x0001 */ __u8 flags;...顺便一提,greh->flags用于写入的值gre_tnl_flags_to_gre_flags(t->parms.o_flags)始终固定为 0x7ff(因此,这不会影响稳定性)。
greh->flagst->parms.o_flags由于类型混淆,用于写入的值bonding也是从读取的。
t->parms.o_flags是偏移量0x6e,struct bonding::bond_list.next对应于的第 6 个字节。
由于这是一个内核指针,这两个字节0xff 0xff始终为 。
因此,GRE 标志转换后的值如下:
gre_tnl_flags_to_gre_flags(0xffff) = 0x07ff此时,该标志被设置,表明此 skb 是零拷贝。
SKBFL_ZEROCOPY_ENABLE = BIT(0)换句话说,原本不是零拷贝的 SKB 文件struct skb_shared_info::flags被非法覆盖,在后续的复制过程中,它会被当作零拷贝 SKB 文件来处理。
步骤 2.2:skb->data调整
我之前提到过,在特定条件下,skb->data字符的开头部分会重叠。由于漏洞利用了这些条件下发生的内存损坏,因此必须找到满足这些条件的方法。skb_shared_info
由于 skb 缓冲区是按页面大小对齐分配的,struct skb_shared_info因此它始终出现在页面的末尾。
以下代码为 skb 分配缓冲区。
hlen = LL_RESERVED_SPACE(dev);tlen = dev->needed_tailroom;linear = __virtio16_to_cpu(vio_le(), vnet_hdr.hdr_len);linear = max(linear, min_t(int, len, dev->hard_header_len));skb = packet_alloc_skb(sk, hlen + tlen, hlen, len, linear, msg->msg_flags & MSG_DONTWAIT, &err);例如LL_RESERVED_SPACE(dev),0x3ec0当 时,skb 缓冲区0x4000正好是 ,struct skb_shared_info偏移量为0x3ec0。
skb_shinfo(skb) = skb->head + (0x4000 - sizeof(struct skb_shared_info)) = skb->head + 0x3ec0此外,len == 0发送一个数据包skb->data(由于没有缓冲区,该数据包会与另一个数据包重叠skb_shared_info)后,它也会0x3ec0到达偏移量处。
因此,为了满足条件,可以将其重新表述为需要创建一个绑定设备,使其LL_RESERVED_SPACE(dev)能够发送数据包。0x3ec0len == 0
LL_RESERVED_SPACE它的定义如下:
#define LL_RESERVED_SPACE(dev) \ ((((dev)->hard_header_len + READ_ONCE((dev)->needed_headroom)) \ & ~(HH_DATA_MOD - 1)) + HH_DATA_MOD)在这里,我们调整了设置,bond->needed_headroom目的是增加 GRE 设备的数量。具体来说,我们创建了 329 个 GRE 设备,并按如下方式将它们链接起来(GRE 中支持设备链接)。LL_RESERVED_SPACE
if0 <- if1 <- if2 <- ... <- if328前 8 个 GRE 将采用 FOU 封装,其余的则为普通 GRE。
当 GRE 串联时ip_tunnel_bind_dev(),当前 GREtunnel->hlen和下方设备的tdev->hard_header_lenGRE的值tdev->needed_headroom将相加。
int hlen = LL_MAX_HEADER;int t_hlen = tunnel->hlen + sizeof(struct iphdr);if (tdev) hlen = tdev->hard_header_len + tdev->needed_headroom;dev->needed_headroom = t_hlen + hlen;所需尺寸如下。
plain GRE: tunnel->hlen = 0x4, t_hlen = 0x18, dev->hard_header_len = 0x18FOU GRE: tunnel->hlen = 0xc, t_hlen = 0x20, dev->hard_header_len = 0x20LL_MAX_HEADER = 0x80前 8 个 FOU GRE0x260给出了该值。if8然后needed_headroom,当连接剩余的 320 个普通 GRE 时,它就0x298变成了。
N328needed_headroom0x3e98
当你把最后一个 GRE 成绩奴役到 Bond 上时,Bond 会复制它的价值。
bond->needed_headroom = 0x3e98bond->hard_header_len = 0x18按照上述步骤操作,债券将按LL_RESERVED_SPACE预期发挥作用0x3ec0。
LL_RESERVED_SPACE = align_down(0x3eb0, 0x10) + 0x10 = 0x3ec0这就是为什么这个漏洞长达19年都没被发现的原因。
由于设备链设计得非常巧妙,LL_RESERVED_SPACE上述情况(即内存缓冲区重叠)只有在被设置为一个非常精确的特定值时才会发生skb->data。skb_shared_info即使
它们不重叠,缓冲区溢出仍然会发生,但由于skb与页面大小对齐,因此只会导致写入未使用的内存区域,几乎没有任何副作用,因此不会被KASAN等内存损坏检测机制检测到,更不用说导致程序崩溃了。
事实上,我们是偶然触发 Syzkaller 崩溃并进行彻底分析后才发现这个漏洞的。这也是为什么我们的部分解释听起来有些牵强的原因。
综上所述
这篇博文详细介绍了我们发现的漏洞。希望您能看出其看似简单的外表下隐藏的复杂性,这也解释了为什么它长达19年都未被发现。
正如前文所述,我们使用syzkaller发现了这个漏洞,虽然我们对一些设置进行了调整,但它无需任何特殊代码修改就能发现如此深藏的漏洞,这着实令我们惊叹。
此外,正如上文所述,崩溃代码(或其调用)与导致根本问题的代码callback之间存在显著差异,这使得根本原因分析(RCA)极其困难。
事实上,人工智能在RCA中发挥了重要作用。
当时是2025年上半年,即便如此,对于Linux内核等开源软件的前沿模型,我们的认知水平也相当惊人。回首往事,我认为我们在利用人工智能发现漏洞方面是先驱,而如今这种做法已司空见惯。
然而,即便到了2026年,人工智能取得了显著进步并拥有了令人难以置信的能力,我仍然对仅凭人工智能能否识别出这种漏洞持怀疑态度。
我希望未来人工智能的进一步发展能够实现对这类复杂漏洞的自动检测,
在此之前,我们人类将继续努力发现并解决零日漏洞。
提交:1284cd3a2b740d0118458d2ea470a1e5bc19b187 ↩︎
这是谷歌漏洞赏金计划 (Google VRP) 的一部分,该计划旨在奖励那些能够利用 Linux 内核漏洞进行权限提升的参与者。参与者将根据漏洞利用的难度获得奖励。在本案例中,该漏洞在高度稳定性、零日漏洞以及定制缓解措施的条件下成功演示。谷歌开展这项计划的目的是观察顶级研究人员使用哪些类型的漏洞和攻击手段来攻击目标,并利用这些信息来研究如何提高漏洞利用的难度。
参考:https://google.github.io/security-research/kernelctf/rules.html
END

公众号内容都来自国外平台-所有文章可通过点击阅读原文到达原文地址或参考地址
排版 编辑 | Ots 小安
采集 翻译 | Ots Ai牛马
公众号 |AnQuan7 (Ots安全)