CVE-2021-22555 内核堆上 off-by-null 溢出漏洞分析与利用
字数 2382 2025-08-25 22:58:56

CVE-2021-22555 Linux内核堆off-by-null漏洞分析与利用

漏洞概述

CVE-2021-22555是Linux Netfilter模块中的一个堆溢出漏洞,CVSS评分为7.8。该漏洞影响64位系统上为32位进程处理setsockopt时的场景,当指定optname为IPT_SO_SET_REPLACE(或IP6T_SO_SET_REPLACE)且开启内核选项CONFIG_USER_NS、CONFIG_NET_NS时,在内核结构转换过程中由于错误计算转换大小会导致内核堆上的越界写入0字节。

影响范围

  • 引入版本:v2.6.19-rc1 (9fa492cdc160cd27ce1046cb36f47d3b2b1efa21)
  • 修复版本:
    • 5.12 (b29c457a6511435960115c0f548c4360d5f4801d)
    • 5.10.31
    • 5.4.113
    • 4.19.188
    • 4.14.231
    • 4.9.267
    • 4.4.267

前置知识

内核编译选项

需要开启以下选项:

CONFIG_IP_NF_IPTABLES=y
CONFIG_IP_NF_MATCH_AH=y
CONFIG_IP_NF_MATCH_ECN=y
CONFIG_IP_NF_MATCH_RPFILTER=y
CONFIG_IP_NF_MATCH_TTL=y
CONFIG_IP_NF_FILTER=y
CONFIG_IP_NF_TARGET_REJECT=y
CONFIG_IP_NF_TARGET_SYNPROXY=y
CONFIG_IP_NF_NAT=y
CONFIG_IP_NF_TARGET_MASQUERADE=y
CONFIG_IP_NF_TARGET_NETMAP=y
CONFIG_IP_NF_TARGET_REDIRECT=y
CONFIG_IP_NF_MANGLE=y
CONFIG_IP_NF_TARGET_CLUSTERIP=y
CONFIG_IP_NF_TARGET_ECN=y
CONFIG_IP_NF_TARGET_TTL=y
CONFIG_IP_NF_RAW=y
CONFIG_IP_NF_SECURITY=y
CONFIG_IP_NF_ARPTABLES=y
CONFIG_IP_NF_ARPFILTER=y
CONFIG_IP_NF_ARP_MANGLE=y

CONFIG_NETFILTER=y
CONFIG_NETFILTER_ADVANCED=y
CONFIG_NETFILTER_INGRESS=y
CONFIG_NETFILTER_NETLINK=y
CONFIG_NETFILTER_FAMILY_BRIDGE=y
CONFIG_NETFILTER_FAMILY_ARP=y
CONFIG_NETFILTER_NETLINK_ACCT=y
CONFIG_NETFILTER_NETLINK_QUEUE=y
CONFIG_NETFILTER_NETLINK_LOG=y
CONFIG_NETFILTER_NETLINK_OSF=y
CONFIG_NETFILTER_CONNCOUNT=y
CONFIG_NETFILTER_NETLINK_GLUE_CT=y
CONFIG_NETFILTER_SYNPROXY=y
CONFIG_NETFILTER_XTABLES=y
CONFIG_NETFILTER_XT_MARK=y
CONFIG_NETFILTER_XT_CONNMARK=y
CONFIG_NETFILTER_XT_SET=y
CONFIG_NETFILTER_XT_MATCH_U32=y

CONFIG_USER_NS=y
CONFIG_NET_NS=y
CONFIG_COMPAT=y

Netfilter架构

Netfilter是Linux内核中的一个子模块,提供数据包过滤、网络地址转换、端口转换等功能。主要结构包括:

  1. xt_table:存储不同功能的配置信息
struct xt_table {
    struct list_head list;
    unsigned int valid_hooks;
    struct xt_table_info *private;
    struct module *me;
    u_int8_t af;
    int priority;
    int (*table_init)(struct net *net);
    const char name[XT_TABLE_MAXNAMELEN];
};
  1. xt_table_info:xt_table的核心结构
struct xt_table_info {
    unsigned int size;
    unsigned int number;
    unsigned int initial_entries;
    unsigned int hook_entry[NF_INET_NUMHOOKS];
    unsigned int underflow[NF_INET_NUMHOOKS];
    unsigned int stacksize;
    void ***jumpstack;
    unsigned char entries[] __aligned(8);
};
  1. ipt_entry:表示防火墙规则
struct ipt_entry {
    struct ipt_ip ip;
    unsigned int nfcache;
    __u16 target_offset;
    __u16 next_offset;
    unsigned int comefrom;
    struct xt_counters counters;
    unsigned char elems[0];
};
  1. xt_entry_matchxt_entry_target:表示匹配规则和执行动作
struct xt_entry_match {
    union {
        struct {
            __u16 match_size;
            char name[XT_EXTENSION_MAXNAMELEN];
            __u8 revision;
        } user;
        struct {
            __u16 match_size;
            struct xt_match *match;
        } kernel;
        __u16 match_size;
    } u;
    unsigned char data[0];
};

struct xt_entry_target {
    union {
        struct {
            __u16 target_size;
            char name[XT_EXTENSION_MAXNAMELEN];
            __u8 revision;
        } user;
        struct {
            __u16 target_size;
            struct xt_target *target;
        } kernel;
        __u16 target_size;
    } u;
    unsigned char data[0];
};

32位下的setsockopt系统调用路径

32位程序通过COMPAT_SYSCALL_DEFINE宏定义的兼容系统调用完成操作,调用链如下:

  1. __compat_sys_setsockopt()
  2. sock->ops->compat_setsockoptsock->ops->setsockopt
  3. 对于AF_INET SOCK_STREAM socket,最终调用inet_stream_ops.compat_setsockopt
  4. compat_sock_common_setsockopt()
  5. sk->sk_prot->compat_setsockopt (对于TCP是tcp_prot.compat_setsockopt)
  6. compat_tcp_setsockopt()
  7. inet_csk_compat_setsockopt()
  8. icsk->icsk_af_ops->compat_setsockopt (对于IPv4是ipv4_specific.compat_setsockopt)
  9. compat_ip_setsockopt()
  10. compat_nf_setsockopt()
  11. compat_nf_sockopt()
  12. ops->compat_set (对于iptables是ipt_sockopts.compat_set)
  13. compat_do_ipt_set_ctl()
  14. compat_do_replace()
  15. translate_compat_table()
  16. compat_copy_entry_from_user()
  17. xt_compat_match_from_user()xt_compat_target_from_user()

漏洞分析

漏洞存在于xt_compat_match_from_user()xt_compat_target_from_user()函数中,核心问题在于:

void xt_compat_target_from_user(struct xt_entry_target *t, void **dstptr,
                unsigned int *size)
{
    // ...
    pad = XT_ALIGN(target->targetsize) - target->targetsize;
    if (pad > 0)
        memset(t->data + target->targetsize, 0, pad); // 漏洞点
    // ...
}

问题在于:

  1. t->data不一定是8字节对齐的
  2. 计算pad时假设t->data是8字节对齐的
  3. t->data非8字节对齐时,会导致越界写入0字节到相邻object

漏洞利用

利用思路

  1. 使用msg_msg构造UAF
  2. 使用sk_buff写入object
  3. 使用pipe_buffer劫持RIP

详细利用步骤

Step 0: 准备工作

if (unshare(CLONE_NEWUSER) < 0)
    errExit("failed to unshare(CLONE_NEWUSER)");
if (unshare(CLONE_NEWNET) < 0)
    errExit("failed to unshare(CLONE_NEWNET)");

CPU_ZERO(&cpu_set);
CPU_SET(0, &cpu_set);
sched_setaffinity(getpid(), sizeof(cpu_set), &cpu_set);

Step 1: 堆喷msg_msg构造重叠对象

  1. 创建多个消息队列,每个队列发送两条消息:
    • 主消息:大小0x1000
    • 辅助消息:大小0x400
  2. 释放部分主消息创建空洞
  3. 触发off-by-null漏洞覆盖相邻主消息的next指针
  4. 导致两个消息队列指向同一个辅助消息

Step 2: 构造UAF

  1. 释放辅助消息
  2. 通过一个消息队列仍能访问该辅助消息
  3. 实际该object已在freelist上

Step 3: 堆喷sk_buff伪造辅助消息泄露地址

  1. 使用sk_buff伪造msg_msg结构
  2. 设置m_ts为大值进行越界读取
  3. 泄露堆地址(主消息和msg_queue地址)
  4. 计算UAF对象地址

Step 4: 堆喷pipe_buffer泄露内核基址

  1. 使用sk_buff修复辅助消息
  2. 喷射pipe_buffer结构体数组
  3. 读取pipe_buf_operations指针
  4. 计算内核基址

Step 5: 伪造pipe_buffer劫持RIP

  1. 伪造pipe_buffer->ops指向可控区域
  2. 构造ROP链:
    • prepare_kernel_cred(0)
    • commit_creds()
    • 返回用户态
  3. 关闭管道触发release函数指针执行ROP

Step 6: 容器逃逸(可选)

在内核中执行:

switch_task_namespaces(find_task_by_vpid(1), init_nsproxy);

将进程命名空间切换为初始全局命名空间。

漏洞修复

内核主线通过取消对pad置0的操作修复该漏洞,改为在translate_compat_table()中预先置0:

+   memset(newinfo->entries, 0, size);
-   pad = XT_ALIGN(match->matchsize) - match->matchsize;
-   if (pad > 0)
-       memset(m->data + match->matchsize, 0, pad);

参考

  1. CVE-2021-22555: Turning \x00\x00 into 10000$
  2. 内核修复commit: b29c457a6511435960115c0f548c4360d5f4801d
CVE-2021-22555 Linux内核堆off-by-null漏洞分析与利用 漏洞概述 CVE-2021-22555是Linux Netfilter模块中的一个堆溢出漏洞,CVSS评分为7.8。该漏洞影响64位系统上为32位进程处理setsockopt时的场景,当指定optname为IPT_ SO_ SET_ REPLACE(或IP6T_ SO_ SET_ REPLACE)且开启内核选项CONFIG_ USER_ NS、CONFIG_ NET_ NS时,在内核结构转换过程中由于错误计算转换大小会导致内核堆上的越界写入0字节。 影响范围 引入版本:v2.6.19-rc1 (9fa492cdc160cd27ce1046cb36f47d3b2b1efa21) 修复版本: 5.12 (b29c457a6511435960115c0f548c4360d5f4801d) 5.10.31 5.4.113 4.19.188 4.14.231 4.9.267 4.4.267 前置知识 内核编译选项 需要开启以下选项: Netfilter架构 Netfilter是Linux内核中的一个子模块,提供数据包过滤、网络地址转换、端口转换等功能。主要结构包括: xt_ table :存储不同功能的配置信息 xt_ table_ info :xt_ table的核心结构 ipt_ entry :表示防火墙规则 xt_ entry_ match 和 xt_ entry_ target :表示匹配规则和执行动作 32位下的setsockopt系统调用路径 32位程序通过COMPAT_ SYSCALL_ DEFINE宏定义的兼容系统调用完成操作,调用链如下: __compat_sys_setsockopt() sock->ops->compat_setsockopt 或 sock->ops->setsockopt 对于AF_ INET SOCK_ STREAM socket,最终调用 inet_stream_ops.compat_setsockopt compat_sock_common_setsockopt() sk->sk_prot->compat_setsockopt (对于TCP是 tcp_prot.compat_setsockopt ) compat_tcp_setsockopt() inet_csk_compat_setsockopt() icsk->icsk_af_ops->compat_setsockopt (对于IPv4是 ipv4_specific.compat_setsockopt ) compat_ip_setsockopt() compat_nf_setsockopt() compat_nf_sockopt() ops->compat_set (对于iptables是 ipt_sockopts.compat_set ) compat_do_ipt_set_ctl() compat_do_replace() translate_compat_table() compat_copy_entry_from_user() xt_compat_match_from_user() 和 xt_compat_target_from_user() 漏洞分析 漏洞存在于 xt_compat_match_from_user() 和 xt_compat_target_from_user() 函数中,核心问题在于: 问题在于: t->data 不一定是8字节对齐的 计算pad时假设 t->data 是8字节对齐的 当 t->data 非8字节对齐时,会导致越界写入0字节到相邻object 漏洞利用 利用思路 使用msg_ msg构造UAF 使用sk_ buff写入object 使用pipe_ buffer劫持RIP 详细利用步骤 Step 0: 准备工作 Step 1: 堆喷msg_ msg构造重叠对象 创建多个消息队列,每个队列发送两条消息: 主消息:大小0x1000 辅助消息:大小0x400 释放部分主消息创建空洞 触发off-by-null漏洞覆盖相邻主消息的next指针 导致两个消息队列指向同一个辅助消息 Step 2: 构造UAF 释放辅助消息 通过一个消息队列仍能访问该辅助消息 实际该object已在freelist上 Step 3: 堆喷sk_ buff伪造辅助消息泄露地址 使用sk_ buff伪造msg_ msg结构 设置m_ ts为大值进行越界读取 泄露堆地址(主消息和msg_ queue地址) 计算UAF对象地址 Step 4: 堆喷pipe_ buffer泄露内核基址 使用sk_ buff修复辅助消息 喷射pipe_ buffer结构体数组 读取pipe_ buf_ operations指针 计算内核基址 Step 5: 伪造pipe_ buffer劫持RIP 伪造pipe_ buffer->ops指向可控区域 构造ROP链: prepare_ kernel_ cred(0) commit_ creds() 返回用户态 关闭管道触发release函数指针执行ROP Step 6: 容器逃逸(可选) 在内核中执行: 将进程命名空间切换为初始全局命名空间。 漏洞修复 内核主线通过取消对pad置0的操作修复该漏洞,改为在translate_ compat_ table()中预先置0: 参考 CVE-2021-22555: Turning \x00\x00 into 10000$ 内核修复commit: b29c457a6511435960115c0f548c4360d5f4801d