缓存时间:
2026/07/07 10:15
# 潜伏19年,奖金8万美元:向Google kernelCTF报告Linux内核零日漏洞CVE-2026-43456 | 安全博客 | 漏洞诊断(安全诊断)的GMO网络安全 by伊埃拉
来源:https://gmo-cybersecurity.com/blog/19-years-hidden-80000-rewarded-reporting-a-linux-kernel-zero-day-for-google-kernelctf/
潜伏19年,奖金8万美元:向Google kernelCTF报告Linux内核零日漏洞CVE-2026-43456
发布日期:2026.07.07 更新日期:2026.07.07
我们是高级执行董事/CTO小池优纪,以及高级分析部门兼职成员户田耕大。
我们两人向Linux内核报告的漏洞CVE-2026-43456现已修复并准备公开披露。因此,我们想在这篇博文中介绍详细信息。
该漏洞最显著的特点是,根本原因代码于2007年合并到Linux内核中1(https://gmo-cybersecurity.com/blog/19-years-hidden-80000-rewarded-reporting-a-linux-kernel-zero-day-for-google-kernelctf/#14b04b21-3e6d-4084-9553-3631be7b9d89),并且在近19年的时间里一直未被发现作为一个可利用的漏洞。
此外,由于该漏洞的特性,利用它的漏洞利用可以在1秒内以超过99%的成功率可靠完成。
鉴于此,我们将此漏洞提交给了Google的漏洞奖励活动kernelCTF2(https://gmo-cybersecurity.com/blog/19-years-hidden-80000-rewarded-reporting-a-linux-kernel-zero-day-for-google-kernelctf/#ced0678e-5dad-42dd-8f18-3e5da5f29b1d),并获得了超过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的网络功能中,禁用此功能可消除影响。 - 在大多数发行版中,bonding作为模块提供,因此可以使用`rmmod`移除。默认启用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`是头部空间。
此外,`struct skb_shared_info`位于skb缓冲区的末尾。`struct skb_shared_info::flags`存储skb的状态,例如是否启用了零拷贝。
### Bonding
Bonding是Linux的一项网络功能,可以将多个网络接口视为单个接口。创建的接口称为bond设备,属于该bond的底层接口称为从设备。
## 根本原因
此漏洞是通常所说的类型混淆漏洞。以下是实际创建bond设备的代码,漏洞由标记为★的单行代码引起:
```c
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对头部执行的处理 == 底层设备对头部执行的处理”,乍一看,直接重用这些函数似乎是合理的。
然而,其中一些函数会通过引用和修改设备的私有存储区域来运行。bond设备持有的存储区域与从设备持有的存储区域类型不同,且不兼容。结果,仅这一行代码本身就已经造成了可能发生类型混淆的状况。
我们也在代码中确认这些区域实际上是不兼容的,可能会引起问题。
首先,在bond设备一侧,分配了`sizeof(struct bonding)`字节的区域并赋值给`dev->priv`,即上面提到的存储区域。
```c
struct rtnl_link_ops bond_link_ops __read_mostly = {
.kind = "bond",
.priv_size = sizeof(struct bonding),
.setup = bond_setup,
.maxtype = IFLA_BOND_MAX,
```
```c
dev = kvzalloc(struct_size(dev, priv, sizeof_priv),
GFP_KERNEL_ACCOUNT | __GFP_RETRY_MAYFAIL);
```
另一方面,在GRE(我们漏洞利用中使用的协议)中,`dev->priv`的使用方式如下:
```c
static inline void *netdev_priv(const struct net_device *dev)
{
return (void *)dev->priv;
}
```
```c
struct ip_tunnel *t = netdev_priv(dev);
```
`struct bonding`和`struct ip_tunnel`的结构如下,显然它们是不兼容的:
```c
struct bonding {
struct net_device *dev; /* 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;
...
```
```c
struct ip_tunnel {
struct ip_tunnel __rcu *next;
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 the last ICMP error
* arrived */
...
```
结果,当一个GRE设备作为从设备连接,并且针对bond设备执行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的角度来看,`struct ip6_tnl::parms.laddr`也从偏移`0x38`处读取。由于这是一个存储IPv6源地址的字段,因此该值会包含在接收到的数据包中。
```
/* 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)(const struct sk_buff *, struct bonding *, struct slave *);
```
如上所示,`recv_probe`是一个函数指针,从以下代码可以看出,它实际上包含了`bond_rcv_validate`的地址:
```c
if (bond->params.arp_interval) {
queue_delayed_work(bond->wq, &bond->arp_work, 0);
bond->recv_probe = bond_rcv_validate;
}
```
由于`bond_rcv_validate`是内核中存在的一个函数,泄漏它允许我们计算内核基址。
### 第二步:任意代码执行
由于第一步允许我们绕过KASLR,下一个目标是破坏内存并将指令指针设置为任意值,即实现任意代码执行。
#### 步骤2.1:重写标志
先说结论,为了实现任意代码执行,我们使用IPv4版本的GRE,而不是IP6GRE。
具体来说,通过将以下代码中显示的`uarg->callback`设置为任意值,我们导致对无效地址的函数调用。
```c
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) // 如果非NULL,则调用callback
uarg->callback(skb, uarg, success);
}
```
通过不当重写`skb_shinfo(skb)->flags`的值,我们使得原本应该返回NULL的`uarg`返回了非NULL值,从而实现了无效函数调用。
这听起来可能有点突然,但重写`skb_shinfo(skb)->flags`是通过在以下GRE的`header_ops`函数中,利用`greh->flags`的类型混淆来实现的:
```c
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 + 1`,`greh`变成了`skb->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->flags`的值`t->parms.o_flags`同样由于类型混淆而从`struct bonding`中读取。`t->parms.o_flags`位于偏移`0x6e`处,对应`struct bonding::bond_list.next`的第六个字节。由于这是一个内核指针,这两个字节始终是`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缓冲区。
```c
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`。
```c
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设备。