【CVE-2026-43456】潜伏19年的Linux内核0day漏洞,最终收获超8万美元奖励

admin 2026-07-10 05:53:13 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 该文档披露了Linux内核中一个潜伏19年的高危漏洞CVE-2026-43456,位于net/bonding子系统,属于类型混淆漏洞。攻击者需具备CAPNETADMIN权限,可在1秒内以超过99%的成功率实现权限提升。漏洞影响Linux2.6.24至6.12.77版本,已在2026年3月修复。建议用户更新内核或通过禁用bonding功能、限制非特权用户命名空间进行缓解。 综合评分: 89 文章分类: 漏洞分析,威胁情报,安全工具,红队,其他


cover_image

【CVE-2026-43456】潜伏19年的Linux内核0day漏洞,最终收获超 8 万美元奖励

原创

骨哥说事 骨哥说事

骨哥说事

2026年7月7日 08:44 上海

在小说阅读器读本章

去阅读

| | | — | | 声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由用户承担全部法律及连带责任,文章作者不承担任何法律及连带责任。 |

#

#

防走失:https://gugesay.com/

不想错过任何消息?设置星标↓ ↓ ↓

#

我们两位——GMO网安高级董事兼首席技术官小池祐树,以及高级分析部兼职工员户田康太——向Linux内核报告的漏洞CVE-2026-43456现已被修复,并准备公开披露。因此,我们希望在这篇博客中介绍一下漏洞详情。

这个漏洞最引人注目的特点是,其根本原因的代码在2007年就被合并进了Linux内核,且在近19年的时间里都没有被发现是一个可利用的漏洞。

此外,由于这个漏洞的特性,利用它的攻击可以在1秒内可靠地完成,成功率超过99%。

鉴于此,我们将此漏洞提交给了Google的漏洞悬赏活动kernelCTF,并获得了超过8万美元的奖励。

在这篇博客中,我们将解释CVE-2026-43456的根源以及我们是如何实现权限提升的。

影响范围和条件

  • 受影响版本:Linux 2.6.24 至 6.12.77
  • 子系统net/bonding
  • 原因:类型混淆
  • 引入(问题)提交1284cd3a2b740d0118458d2ea470a1e5bc19b187
  • 修复提交950803f7254721c1c15858fbbfae3deaaeeecb11
  • 触发要求:需要具备 CAP_NET_ADMIN 权限

从受影响的Linux内核版本可以看出,这个漏洞的影响范围极其广泛。

缓解措施

该漏洞已在2026年3月修复,预计各大主流Linux发行版的最新版本都已包含修复。因此,最有效的缓解方法是更新到最新版本

然而,某些发行版或版本可能尚未修复。如果您正在使用这样的环境,或者因某些原因无法更新,可以通过以下任一缓解措施来禁用该漏洞:

  • 将 /proc/sys/kernel/unprivileged_userns_clone 设置为 0

  • 这将防止非特权用户获取 CAP_NET_ADMIN。作为代价,像无根(Rootless)Docker等功能可能将无法使用。

  • 禁用 bonding 功能

  • 在大多数发行版中,bonding是作为模块提供的,因此可以用 rmmod 移除。本身默认启用bonding的环境就很少见。

  • 由于漏洞存在于名为bonding的网络功能中,禁用此功能即可消除影响。

换句话说,可以像处理CopyFail漏洞一样,使用以下命令作为缓解措施:

echo "install bonding /bin/false" > /etc/modprobe.d/disable-bonding.conf
rmmod bonding 2>/dev/null

漏洞详情

背景知识

在详细解释漏洞之前,我们先介绍一些背景知识。

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 指向分配缓冲区的开始,skb->data 指向当前数据包数据的开始,而 skb->tail 指向当前数据包数据的结束。skb->data - skb->head 就是头区(headroom)的大小。

此外,struct skb_shared_info 被放置在skb缓冲区的末尾。struct skb_shared_info::flags 存储着skb的状态,例如是否启用了零拷贝(zerocopy)。

Bonding

Bonding是Linux的一项网络功能,它可以将多个网络接口视为一个单一的接口来对待。创建出来的接口称为绑定设备(bond device),而归属于该绑定的底层接口则称为从属设备(slave device)。

根本原因

这个漏洞就是通常所说的类型混淆(type confusion)漏洞。 以下是实际创建绑定设备的代码,标记为★的这一行导致了漏洞:

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)端,会分配一个大小为 sizeof(struct bonding) 字节的区域,并分配给 dev->priv,也就是上面提到的存储区域。

struct rtnl_link_ops bond_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 的使用方式如下。

static inline void *netdev_priv(const struct net_device *dev)
{
    return (void *)dev->priv;
}
struct ip_tunnel *t = netdev_priv(dev);

struct bonding 和 struct ip_tunnel 的结构如下所示,显然它们不兼容:

struct bonding {
    struct   net_device *dev; /* 第一个成员 - 对内核panic调试有用 */
    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;
...
struct ip_tunnel {
    struct ip_tunnel __rcu  *next;
    struct hlist_node hash_node;

    struct net_device   *dev;
    netdevice_tracker   dev_tracker;

    struct net      *net;   /* 供数据包I/O使用的网络命名空间 */

    unsigned long   err_time;   /* 最后一条ICMP错误消息
                     * 到达的时间 */
...

因此,当一个GRE设备作为从属设备连接,并对绑定设备执行GRE头部处理时,就可能导致内存损坏等问题。

利用漏洞实现权限提升

在kernelCTF中,出于教育目的,参与者需要披露其利用方法的所有细节,我们所提交的利用方法也因此成为了公开信息。

鉴于此,虽然我们将避免在此处透露完整的细节,但我们仍想简要解释一下其内容。

尽管根本原因很简单,只是一行代码的问题,但要滥用它来构建一个稳定的利用程序,就需要如下所述的一系列复杂步骤,并且有很多细节需要仔细考虑。 同时,通过查看以下内容,我们也能理解为何这样一个看似非常简单且非常危险的漏洞能够隐藏19年之久。

第一步:KASLR信息泄露

与用户空间一样,Linux内核也有一个称为KASLR的功能,用于随机化地址。 因此,为了稳定地执行利用,需要识别随机化后的地址。这个过程称为信息泄露。

这次,我们使用了一种名为IP6GRE的协议来进行泄露。 如前所述,此漏洞会导致 dev->priv 的类型混淆。对于bond设备,dev->priv 是 struct bonding;对于IP6GRE设备,dev->priv 是 struct ip6_tnl

struct bonding::recv_probe 位于 bonding 结构体的偏移量 0x38 处。 另一方面,从IP6GRE的角度,也会从其结构体偏移量 0x38 处读取 struct ip6_tnl::parms.laddr。 由于这个字段存储的是IPv6源地址,这个值会被包含在接收到的数据包中。

/* 偏移量      |    大小 */  类型 = 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];
                                           /* 总大小(字节):   16 */
                                       } in6_u;
                                       /* 总大小(字节):   16 */
                                   } laddr;
/* 偏移量      |    大小 */  类型 = struct bonding {
/* 0x0000      |  0x0008 */    struct net_device *dev;
...
/* 0x0038      |  0x0008 */    int (*recv_probe)(const struct sk_buff *, struct bonding *, struct slave *);

如上所示,recv_probe 是一个函数指针,从以下代码可以看出,它实际上包含了 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 是内核中的一个函数,泄露它就能让我们计算出内核的基地址。

第二步:任意代码执行

由于第一步可以让我们绕开KASLR,接下来的目标就是破坏内存并将指令指针(Instruction Pointer)设置为任意值,也就是实现任意代码执行。

第二步.1:覆写Flags

首先直接给出结论,为了实现任意代码执行,我们使用IPv4版本的GRE,而不是IP6GRE。

具体来说,通过将下面代码中显示的 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;
}

static inline void skb_zcopy_clear(struct sk_buff *skb, bool success)
{
    struct ubuf_info *uarg = skb_zcopy(skb);

    if (uarg) // 如果非空,则调用其回调函数
        uarg->callback(skb, uarg, success);
}

通过不适当地改写 skb_shinfo(skb)->flags 的值,我们在 uarg 本应返回NULL的时候让它返回了一个非NULL值,从而实现一次无效的函数调用。

这听起来可能有些突然,但改写 skb_shinfo(skb)->flags 之所以可能,是因为在以下GRE header_ops 函数中,通过 greh->flags 产生了类型混淆:

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);
    struct gre_base_hdr *greh;
    struct iphdr *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。 此时,在GRE中,t->hlen 应该大于等于 sizeof(*greh),但在 struct bonding 中,t->hlen 等于 0。 结果就是,skb_push() 本应将 skb->data 向后移动 sizeof(struct iphdr) + sizeof(struct gre_base_hdr),但它实际上只移动了 sizeof(struct iphdr) 的距离。 之后,由于 greh 被设置为 iph + 1greh 就变成了 skb->data 原始值的位置。换句话说,此时发生了缓冲区溢出。

正如背景部分提到的,skb->data 通常指向skb数据包数据的开头。 然而,在特定条件下,skb_shared_info 的开头会与这个位置重叠。具体来说,当没有剩余未使用的缓冲区时,这种情况就会发生。

借助 greh 和 skb_shared_info 具有以下结构的事实,在这种条件下,对 greh->flags 的写入就会变成对 skb_shared_info->flags 的写入。

/* 偏移量      |    大小 */  类型 = struct gre_base_hdr {
/* 0x0000      |  0x0002 */    __be16 flags; /* 请注意是2字节 */
...
/* 偏移量      |    大小 */  类型 = struct skb_shared_info {
/* 0x0000      |  0x0001 */    __u8 flags; /* 这是1字节 */
...

需要指出的是,原文描述 skb_shared_info->flags 大小为1字节,而 gre_base_hdr->flags 为2字节,这种操作是低字节覆盖。在汉语网络安全表达中一般保留术语”覆写”或”写入”,但需注意字节大小差异的实际情况。

顺便提一句,用于写入 greh->flags 的值,即 gre_tnl_flags_to_gre_flags(t->parms.o_flags),总是固定为 0x7ff。因此,这对利用的稳定性没有影响。

用于写入 greh->flags 的 t->parms.o_flags 值,也因类型混淆而从 bonding 结构中读取。t->parms.o_flags 位于偏移量 0x6e 处,这对应于 struct bonding::bond_list.next 的第六个字节。 由于这是一个内核指针,那两个字节总是 0xff 0xff(在64位小端序机器上,这是一个指针的低16位,通常是非零值)。

结果,经过GRE标志转换后的值如下:

gre_tnl_flags_to_gre_flags(0xffff) = 0x07ff

在这里,用于指示该skb启用了零拷贝的标志被设置了。

SKBFL_ZEROCOPY_ENABLE = BIT(0) /* 位0 */

换句话说,一个原本不是零拷贝的skb的 struct skb_shared_info::flags 被不适当地改写,并在后续的执行路径中被当作零拷贝skb来处理。

第二步.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缓冲区正好是 0x4000struct skb_shared_info 的偏移量就是 0x3ec0

skb_shinfo(skb) = skb->head + (0x4000 - sizeof(struct skb_shared_info))
                = skb->head + 0x3ec0

同时,在发送了一个 len == 0 的数据包之后,skb->data 也会处于偏移量 0x3ec0 的位置,因为没有数据包缓冲区,所以它与 skb_shared_info 重叠。 因此,满足条件可以重新表述为:需要创建一个 LL_RESERVED_SPACE(dev) 为 0x3ec0 的bond设备,然后发送一个长度为 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 来调整 LL_RESERVED_SPACE。 具体来说,我们创建329个GRE设备,并按以下方式将它们链接起来。GRE允许设备进行级联链接。

if0 <- if1 <- if2 <- ... <- if328

前八个是带有FOU封装的GRE,其余的是普通GRE。 当GRE设备链式连接时,ip_tunnel_bind_dev() 会加上当前的 tunnel->hlen 和底层设备的 tdev->hard_header_len / tdev->needed_headroom

int hlen = LL_MAX_HEADER;
int t_hlen = tunnel->hlen + sizeof(struct iphdr);

if&nbsp;(tdev)
&nbsp; &nbsp; 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 = 0x18
FOU GRE: &nbsp;tunnel->hlen = 0xc, t_hlen = 0x20, dev->hard_header_len = 0x20
LL_MAX_HEADER = 0x80

前八个FOU GRE设备将值提高到 0x260if8 的 needed_headroom 变成了 0x298。 之后,当剩余的320个普通GRE设备被连接时,if328(此处原文写为N328,统一为索引编号if328) 的 needed_headroom 变成了 0x3e98

当最终的GRE设备被设置为bond的从属设备时,bond设备直接复制了这个值。

bond->needed_headroom = 0x3e98
bond->hard_header_len = 0x18

通过上述操作,bond设备的 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过程中,人工智能(AI)给予了我们巨大的帮助。 那是在2025年上半年,但即使在那时,前沿模型关于Linux内核这类开源软件的知识已经非常出色了。回顾起来,我们认为这如今日益普遍的“与AI共同发现漏洞”模式的一个早期例子。

话虽如此,如果问今天(乃至到了2026年,我们目睹了其显著的进化和强大能力之后)的AI是否能完全靠自身识别出这个漏洞,我们仍然持保留态度。

虽然我们期望未来AI的进步能够带来一个可以自动发现此类复杂漏洞的世界,但在那个未来到来之前,我们人类打算继续致力于发现和消除零日漏洞。

原文:https://gmo-cybersecurity.com/blog/19-years-hidden-80000-rewarded-reporting-a-linux-kernel-zero-day-for-google-kernelctf/

  • END –

感谢阅读,如果觉得还不错的话,动动手指给个三连吧~


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:骨哥说事 骨哥说事 骨哥说事《【CVE-2026-43456】潜伏19年的Linux内核0day漏洞,最终收获超 8 万美元奖励》

评论:0   参与:  0