影响版本:Linux v5.18.10。 v5.18.11 已修补。
测试版本:Linux-v5.18.10 失败,改用 v5.17.15 exploit及测试环境下载地址—https://github.com/bsauce/kernel-exploit-factory 原作者在 Ubuntu 22.04(v5.15.0)上成功提权。
编译选项:
CONFIG_NF_TABLES=y
CONFIG_NETFILTER_NETLINK=y
CONFIG_BINFMT_MISC=y (否则启动VM时报错)
CONFIG_USER_NS=y
在编译时将.config
中的CONFIG_E1000
和CONFIG_E1000E
,变更为=y。参考
$ wget https://mirrors.tuna.tsinghua.edu.cn/kernel/v5.x/linux-5.17.15.tar.xz
$ tar -xvf linux-5.17.15.tar.xz
# KASAN: 设置 make menuconfig 设置"Kernel hacking" ->"Memory Debugging" -> "KASan: runtime memory debugger"。
$ make -j32
$ make all
$ make modules
# 编译出的bzImage目录:/arch/x86/boot/bzImage。
漏洞描述:nft_set_elem_init() 函数存在堆溢出,溢出长度可达 64-16=48
字节,漏洞对象可以位于 kmalloc-{64,96,128,192}
(本文利用时选取 kmalloc-64 漏洞对象)。漏洞利用——首先构造堆布局 vul_obj -> user_key_payload -> percpu_ref_data
,溢出篡改 user_key_payload->datalen
为 0xffff,以泄露出 percpu_ref_data->release
内核基址和 percpu_ref_data->ref
physmap基址;然后构造堆布局 vul_obj -> simple_xattr
,溢出篡改 simple_xattr->list
链表,利用这个有限制的任意写将 modprobe_path
从 /sbin/modprobe
修改为 /tmp/xxxxprobe
来提权(从链表中移除xattr时触发该任意写)。该任意写的前提条件是需要泄露 physmap 地址,percpu_ref_data
/ shm_file_data
都既包含内核基址又包含physmap地址。
补丁:patch
diff --git a/net/netfilter/nf_tables_api.c b/net/netfilter/nf_tables_api.c
index 51144fc66889b..d6b59beab3a98 100644
--- a/net/netfilter/nf_tables_api.c
+++ b/net/netfilter/nf_tables_api.c
@@ -5213,13 +5213,20 @@ static int nft_setelem_parse_data(struct nft_ctx *ctx, struct nft_set *set,
struct nft_data *data,
struct nlattr *attr)
{
+ u32 dtype;
int err;
err = nft_data_init(ctx, data, NFT_DATA_VALUE_MAXLEN, desc, attr);
if (err < 0)
return err;
- if (desc->type != NFT_DATA_VERDICT && desc->len != set->dlen) {
+ if (set->dtype == NFT_DATA_VERDICT)
+ dtype = NFT_DATA_VERDICT;
+ else
+ dtype = NFT_DATA_VALUE;
+
+ if (dtype != desc->type ||
+ set->dlen != desc->len) {
nft_data_release(data, desc->type);
return -EINVAL;
}
保护机制:KASLR / SMEP / SMAP
利用总结:
/tmp/get_root
提权程序,该程序会被赋予root权限;CAP_NET_ADMIN
权限);modprobe_path
;user_key_payload
对象(最好位于漏洞对象后面);user_key_payload->datalen
为 0xffff;percpu_ref_data
对象(最好位于 user_key_payload
后面);percpu_ref_data->release
内核基址和 percpu_ref_data->ref
physmap基址(只要读取长度为 0xffff 则表示 user_key_payload->datalen
被成功覆写);modprobe_path
simple_xattr
(最好位于漏洞对象后面);simple_xattr->list
;
simple_xattr->list.next = (physmap_base + 0x2f706d74)
simple_xattr->list.prev = (kaslr_base + MODPROBE_PATH_BASE + 1)
simple_xattr
,simple_xattr->name
长度为0x100,最低字节覆盖为 0xe5 = 229
,这样name 恰好指向attribute结尾的某个特定字符串(例如 "security.Iwanttoberoot"
),这样只要能成功移除该字符串对应的xattr,则表示该标签已被覆盖unlinking attack
,将 /sbin/modprobe
修改为 /tmp/xxxxprobe
;netfilter是一个开源项目,用于执行数据包过滤,也就是Linux防火墙。这个项目经常被提到iptables,它是用于配置防火墙的用户级应用程序。2014年,netfilter 防火墙添加了一个新子系统,称为nftables,可以通过nftables用户级应用程序进行配置。
nftables取代了流行的{ip,ip6,arp,eb}tables。该软件提供了一个新的内核数据包分类框架,该框架基于特定于网络的虚拟机 (VM) 和新的nft用户空间命令行工具。nftables重用了现有的netfilter子系统,例如现有的钩子基础设施、连接跟踪系统、NAT、用户空间队列和日志子系统。对于nftables,只需要扩展expression即可,用户自行编写expression,然后让nftables虚拟机执行它。nftables框架的数据结构如下所示:
Table{
Chain[
Rule
(expression1,expression2,expression3,...)
| | |--> expression_action
| |--> expression_action
|-->expression_action
Rule
(expression,expression,expression,...)
...
],
Chain[
...
],
...
}
Table为chain的容器,chain为rule的容器,rule为expression的容器,expression响应action。构造成由 table->chain->rule->expression
四级组成的数据结构。
nfnetlink初始化:参见 nfnetlink_net_init() 函数,其中定义了 netlink_kernel_cfg
对象,后续如果收到 netfilter 的消息后会调用 netlink_kernel_cfg->input
函数,也即 nfnetlink_rcv() 函数,其中规定了需具备 CAP_NET_ADMIN
权限,只要支持 namespace 那么普通用户也可以访问(编译时勾选 CONFIG_USER_NS
)。
static int __net_init nfnetlink_net_init(struct net *net)
{
struct nfnl_net *nfnlnet = nfnl_pernet(net);
struct netlink_kernel_cfg cfg = {
.groups = NFNLGRP_MAX,
.input = nfnetlink_rcv, // <===== 当收到 netfilter netlink 的消息后会调用这个 input 函数
#ifdef CONFIG_MODULES
.bind = nfnetlink_bind,
#endif
};
nfnlnet->nfnl = netlink_kernel_create(net, NETLINK_NETFILTER, &cfg);
if (!nfnlnet->nfnl)
return -ENOMEM;
return 0;
}
整体调用trace:通过发送netlink消息数据包来操作nftables,从netlink到nftables的调用过程如下所示:
__sys_sendto() -> sock_sendmsg() -> sock_sendmsg_nosec() -> netlink_sendmsg -> netlink_unicast() -> netlink_unicast_kernel() -> nfnetlink_rcv() -> nfnetlink_rcv_skb_batch() -> nfnetlink_rcv_batch()
在 nfnetlink_rcv_batch() 函数中对netlink消息进行操作。从netlink消息中剥离出nftable载荷,并依次进行对应处理。进入 nfnetlink_rcv_batch() 函数,首先根据 subsys_id
(取值如下所示)获得 nfnetlink_subsystem。
#define NFNL_SUBSYS_NONE 0
#define NFNL_SUBSYS_CTNETLINK 1
#define NFNL_SUBSYS_CTNETLINK_EXP 2
#define NFNL_SUBSYS_QUEUE 3
#define NFNL_SUBSYS_ULOG 4
#define NFNL_SUBSYS_OSF 5
#define NFNL_SUBSYS_IPSET 6
#define NFNL_SUBSYS_ACCT 7
#define NFNL_SUBSYS_CTNETLINK_TIMEOUT 8
#define NFNL_SUBSYS_CTHELPER 9
#define NFNL_SUBSYS_NFTABLES 10 // <------------ nftables
#define NFNL_SUBSYS_NFT_COMPAT 11
#define NFNL_SUBSYS_HOOK 12
#define NFNL_SUBSYS_COUNT 13
这里nftables类型为0xa。获得subsystem后,然后拿到子系统对应的回调客户端(nfnl_callback)。通过 nfnetlink_find_client() 实现该功能。
static inline const struct nfnl_callback *
nfnetlink_find_client(u16 type, const struct nfnetlink_subsystem *ss)
{
u8 cb_id = NFNL_MSG_TYPE(type);
if (cb_id >= ss->cb_count)
return NULL;
return &ss->cb[cb_id];
}
对应nftables回调客户端,在\net\netfilter\nf_tables_api.c直接找到定义——nf_tables_subsys:
static const struct nfnetlink_subsystem nf_tables_subsys = {
.name = "nf_tables",
.subsys_id = NFNL_SUBSYS_NFTABLES,
.cb_count = NFT_MSG_MAX,
.cb = nf_tables_cb,
.commit = nf_tables_commit,
.abort = nf_tables_abort,
.cleanup = nf_tables_cleanup,
.valid_genid = nf_tables_valid_genid,
.owner = THIS_MODULE,
};
函数入口:.cb
数据域便是回调客户端——nf_tables_cb。可以看到针对不同的nftables操作,定义了多个回调客户端,例如table的增删改查操作。
static const struct nfnl_callback nf_tables_cb[NFT_MSG_MAX] = {
[NFT_MSG_NEWTABLE] = {
.call = nf_tables_newtable,
.type = NFNL_CB_BATCH,
.attr_count = NFTA_TABLE_MAX,
.policy = nft_table_policy,
},
[NFT_MSG_GETTABLE] = {
.call = nf_tables_gettable,
.type = NFNL_CB_RCU,
.attr_count = NFTA_TABLE_MAX,
.policy = nft_table_policy,
},
[NFT_MSG_DELTABLE] = {
.call = nf_tables_deltable,
.type = NFNL_CB_BATCH,
.attr_count = NFTA_TABLE_MAX,
.policy = nft_table_policy,
},
[NFT_MSG_NEWCHAIN] = {
.call = nf_tables_newchain,
.type = NFNL_CB_BATCH,
.attr_count = NFTA_CHAIN_MAX,
.policy = nft_chain_policy,
},
...
}
nf_tables_newtable()
nf_tables_gettable()
nf_tables_deltable()
nf_tables_newchain()
nf_tables_getchain()
nf_tables_delchain()
nf_tables_newrule()
nf_tables_getrule()
nf_tables_delrule()
nf_tables_newset()
nf_tables_getset()
nf_tables_delset()
nf_tables_newsetelem()
nf_tables_getsetelem()
nf_tables_delsetelem()
nf_tables_getgen()
nf_tables_newobj()
nf_tables_getobj()
nf_tables_delobj()
nf_tables_getobj()
nf_tables_newflowtable()
nf_tables_getflowtable()
nf_tables_delflowtable()
然后再从netlink消息中剥离出netlink载荷,根据不同的消息类型(nf_tables_msg_types)进行不同的分发处理,消息类型如下所示:
enum nf_tables_msg_types {
NFT_MSG_NEWTABLE,
NFT_MSG_GETTABLE,
NFT_MSG_DELTABLE,
NFT_MSG_NEWCHAIN,
NFT_MSG_GETCHAIN,
NFT_MSG_DELCHAIN,
NFT_MSG_NEWRULE,
NFT_MSG_GETRULE,
NFT_MSG_DELRULE,
NFT_MSG_NEWSET,
NFT_MSG_GETSET,
NFT_MSG_DELSET,
NFT_MSG_NEWSETELEM,
NFT_MSG_GETSETELEM,
NFT_MSG_DELSETELEM,
NFT_MSG_NEWGEN,
NFT_MSG_GETGEN,
NFT_MSG_TRACE,
NFT_MSG_NEWOBJ,
NFT_MSG_GETOBJ,
NFT_MSG_DELOBJ,
NFT_MSG_GETOBJ_RESET,
NFT_MSG_NEWFLOWTABLE,
NFT_MSG_GETFLOWTABLE,
NFT_MSG_DELFLOWTABLE,
NFT_MSG_MAX,
};
依次调用 nc->call()
进一步处理。
static void nfnetlink_rcv_batch(struct sk_buff *skb, struct nlmsghdr *nlh,
u16 subsys_id, u32 genid)
{
struct sk_buff *oskb = skb;
struct net *net = sock_net(skb->sk);
const struct nfnetlink_subsystem *ss;
const struct nfnl_callback *nc;
struct netlink_ext_ack extack;
LIST_HEAD(err_list);
u32 status;
int err;
...
ss = nfnl_dereference_protected(subsys_id); // 根据 subsys_id 获得 nfnetlink_subsystem, 例如 nf_tables_subsys
...
while (skb->len >= nlmsg_total_size(0)) {
int msglen, type;
...
type = nlh->nlmsg_type; // <--
...
nc = nfnetlink_find_client(type, ss); // 拿到子系统对应的回调客户端,
if (!nc) {
err = -EINVAL;
goto ack;
}
...
{
int min_len = nlmsg_total_size(sizeof(struct nfgenmsg));
struct nfnl_net *nfnlnet = nfnl_pernet(net);
struct nlattr *cda[NFNL_MAX_ATTR_COUNT + 1];
struct nlattr *attr = (void *)nlh + min_len;
u8 cb_id = NFNL_MSG_TYPE(nlh->nlmsg_type);
int attrlen = nlh->nlmsg_len - min_len;
struct nfnl_info info = {
.net = net,
.sk = nfnlnet->nfnl,
.nlh = nlh,
.nfmsg = nlmsg_data(nlh),
.extack = &extack,
};
/* Sanity-check NFTA_MAX_ATTR */
if (ss->cb[cb_id].attr_count > NFNL_MAX_ATTR_COUNT) {
err = -ENOMEM;
goto ack;
}
err = nla_parse_deprecated(cda,
ss->cb[cb_id].attr_count,
attr, attrlen,
ss->cb[cb_id].policy, NULL);
if (err < 0)
goto ack;
err = nc->call(skb, &info, (const struct nlattr **)cda);
...
}
...
}
}
开始剥洋葱式分析,第一层操作创建一个table,响应函数为 nf_tables_newtable() 。
static int nf_tables_newtable(struct sk_buff *skb, const struct nfnl_info *info,
const struct nlattr * const nla[])
{
struct nftables_pernet *nft_net = nft_pernet(info->net);
struct netlink_ext_ack *extack = info->extack;
u8 genmask = nft_genmask_next(info->net);
u8 family = info->nfmsg->nfgen_family;
struct net *net = info->net;
const struct nlattr *attr;
struct nft_table *table;
struct nft_ctx ctx;
u32 flags = 0;
int err;
... ...
lockdep_assert_held(&nft_net->commit_mutex);
attr = nla[NFTA_TABLE_NAME]; // [1] 查找是否存在该table
table = nft_table_lookup(net, attr, family, genmask,
NETLINK_CB(skb).portid);
if (IS_ERR(table)) {
if (PTR_ERR(table) != -ENOENT)
return PTR_ERR(table);
} else {
...
return nf_tables_updtable(&ctx); // [2] 如果存在该table, 调用 nf_tables_updtable() 进行更新
}
...
table = kzalloc(sizeof(*table), GFP_KERNEL_ACCOUNT); // [3] 如果不存在就创建该表, 并进行初始化
if (table == NULL)
goto err_kzalloc;
table->name = nla_strdup(attr, GFP_KERNEL_ACCOUNT);
if (table->name == NULL)
goto err_strdup;
if (nla[NFTA_TABLE_USERDATA]) {
table->udata = nla_memdup(nla[NFTA_TABLE_USERDATA], GFP_KERNEL_ACCOUNT);
if (table->udata == NULL)
goto err_table_udata;
table->udlen = nla_len(nla[NFTA_TABLE_USERDATA]);
}
err = rhltable_init(&table->chains_ht, &nft_chain_ht_params);
if (err)
goto err_chain_ht;
INIT_LIST_HEAD(&table->chains); // [4] 初始化4个链表
INIT_LIST_HEAD(&table->sets);
INIT_LIST_HEAD(&table->objects);
INIT_LIST_HEAD(&table->flowtables);
table->family = family;
table->flags = flags;
table->handle = ++table_handle;
if (table->flags & NFT_TABLE_F_OWNER)
table->nlpid = NETLINK_CB(skb).portid;
nft_ctx_init(&ctx, net, skb, info->nlh, family, table, NULL, nla); // [5] 将table加到nftbales上下文中
err = nft_trans_table_add(&ctx, NFT_MSG_NEWTABLE);
if (err < 0)
goto err_trans;
list_add_tail_rcu(&table->list, &nft_net->tables); // [6] 将table链到net->nft.tables中
...
}
第二步操作创建一个chain,响应函数为 nf_tables_newchain()。
static int nf_tables_newchain(struct sk_buff *skb, const struct nfnl_info *info,
const struct nlattr * const nla[])
{
struct nftables_pernet *nft_net = nft_pernet(info->net);
struct netlink_ext_ack *extack = info->extack;
u8 genmask = nft_genmask_next(info->net);
u8 family = info->nfmsg->nfgen_family;
struct nft_chain *chain = NULL;
struct net *net = info->net;
const struct nlattr *attr;
struct nft_table *table;
u8 policy = NF_ACCEPT;
struct nft_ctx ctx;
u64 handle = 0;
u32 flags = 0;
lockdep_assert_held(&nft_net->commit_mutex);
table = nft_table_lookup(net, nla[NFTA_CHAIN_TABLE], family, genmask, // [1] 首先先找table, 无table直接退出
NETLINK_CB(skb).portid);
if (IS_ERR(table)) {
NL_SET_BAD_ATTR(extack, nla[NFTA_CHAIN_TABLE]);
return PTR_ERR(table);
}
chain = NULL;
attr = nla[NFTA_CHAIN_NAME]; // [2] 找chain是否存在, 存在进入update, 不存在则添加一个新chain
if (nla[NFTA_CHAIN_HANDLE]) { // [2-1] 两种方式寻找chain, 一是通过 nla[NFTA_CHAIN_HANDLE]
handle = be64_to_cpu(nla_get_be64(nla[NFTA_CHAIN_HANDLE]));
chain = nft_chain_lookup_byhandle(table, handle, genmask);
if (IS_ERR(chain)) {
NL_SET_BAD_ATTR(extack, nla[NFTA_CHAIN_HANDLE]);
return PTR_ERR(chain);
}
attr = nla[NFTA_CHAIN_HANDLE];
} else if (nla[NFTA_CHAIN_NAME]) { // [2-2] 二是通过 nla[NFTA_CHAIN_NAME]
chain = nft_chain_lookup(net, table, attr, genmask);
if (IS_ERR(chain)) {
if (PTR_ERR(chain) != -ENOENT) {
NL_SET_BAD_ATTR(extack, attr);
return PTR_ERR(chain);
}
chain = NULL;
}
} else if (!nla[NFTA_CHAIN_ID]) {
return -EINVAL;
}
...
if (chain != NULL) {
if (info->nlh->nlmsg_flags & NLM_F_EXCL) {
NL_SET_BAD_ATTR(extack, attr);
return -EEXIST;
}
if (info->nlh->nlmsg_flags & NLM_F_REPLACE)
return -EOPNOTSUPP;
flags |= chain->flags & NFT_CHAIN_BASE;
return nf_tables_updchain(&ctx, genmask, policy, flags, attr, // [3] 找到 chain 则更新
extack);
}
return nf_tables_addchain(&ctx, family, genmask, policy, flags, extack);// [4] 未找到就调用nf_tables_addchain()创建
}
创建chain的过程具体看 nf_tables_addchain() 函数实现,首先分配一个chain,然后初始化chain->rules链表,并设置chain->hanle和chain->table。随后进行初始化chain->name等操作,并将chain链到table->chains中。
static int nf_tables_addchain(struct nft_ctx *ctx, u8 family, u8 genmask,
u8 policy, u32 flags,
struct netlink_ext_ack *extack)
{
const struct nlattr * const *nla = ctx->nla;
struct nft_table *table = ctx->table;
struct nft_base_chain *basechain;
struct nft_stats __percpu *stats;
struct net *net = ctx->net;
char name[NFT_NAME_MAXLEN];
struct nft_rule_blob *blob;
struct nft_trans *trans;
struct nft_chain *chain;
unsigned int data_size;
int err;
...
if (nla[NFTA_CHAIN_HOOK]) {
...
basechain = kzalloc(sizeof(*basechain), GFP_KERNEL_ACCOUNT); // [1] 分配 nft_base_chain
...
} else {
if (flags & NFT_CHAIN_BASE)
return -EINVAL;
if (flags & NFT_CHAIN_HW_OFFLOAD)
return -EOPNOTSUPP;
chain = kzalloc(sizeof(*chain), GFP_KERNEL_ACCOUNT); // [2] 分配 nft_chain
if (chain == NULL)
return -ENOMEM;
chain->flags = flags;
}
ctx->chain = chain;
INIT_LIST_HEAD(&chain->rules); // [3] 初始化chain->rules链表
chain->handle = nf_tables_alloc_handle(table);
chain->table = table;
if (nla[NFTA_CHAIN_NAME]) { // [4] 初始化chain->name
chain->name = nla_strdup(nla[NFTA_CHAIN_NAME], GFP_KERNEL_ACCOUNT);
} ...
data_size = offsetof(struct nft_rule_dp, data); /* last rule */
blob = nf_tables_chain_alloc_rules(data_size);
if (!blob) {
err = -ENOMEM;
goto err_destroy_chain;
}
RCU_INIT_POINTER(chain->blob_gen_0, blob);
RCU_INIT_POINTER(chain->blob_gen_1, blob);
err = nf_tables_register_hook(net, table, chain);
if (err < 0)
goto err_destroy_chain;
trans = nft_trans_chain_add(ctx, NFT_MSG_NEWCHAIN);
if (IS_ERR(trans)) {
err = PTR_ERR(trans);
goto err_unregister_hook;
}
nft_trans_chain_policy(trans) = NFT_CHAIN_POLICY_UNSET;
if (nft_is_base_chain(chain))
nft_trans_chain_policy(trans) = policy;
err = nft_chain_add(table, chain); // [5] 将chain链到table->chains
...
}
第三步操作创建一个rule,响应函数为 nf_tables_newrule()。
static int nf_tables_newrule(struct sk_buff *skb, const struct nfnl_info *info,
const struct nlattr * const nla[])
{
struct nftables_pernet *nft_net = nft_pernet(info->net);
struct netlink_ext_ack *extack = info->extack;
unsigned int size, i, n, ulen = 0, usize = 0;
u8 genmask = nft_genmask_next(info->net);
struct nft_rule *rule, *old_rule = NULL;
struct nft_expr_info *expr_info = NULL;
u8 family = info->nfmsg->nfgen_family;
struct nft_flow_rule *flow = NULL;
struct net *net = info->net;
struct nft_userdata *udata;
struct nft_table *table;
struct nft_chain *chain;
struct nft_trans *trans;
u64 handle, pos_handle;
struct nft_expr *expr;
struct nft_ctx ctx;
struct nlattr *tmp;
int err, rem;
lockdep_assert_held(&nft_net->commit_mutex);
table = nft_table_lookup(net, nla[NFTA_RULE_TABLE], family, genmask, // [1] 获取table
NETLINK_CB(skb).portid);
if (IS_ERR(table)) {
NL_SET_BAD_ATTR(extack, nla[NFTA_RULE_TABLE]);
return PTR_ERR(table);
}
if (nla[NFTA_RULE_CHAIN]) { // [2] 获取chain
chain = nft_chain_lookup(net, table, nla[NFTA_RULE_CHAIN],
genmask);
if (IS_ERR(chain)) {
NL_SET_BAD_ATTR(extack, nla[NFTA_RULE_CHAIN]);
return PTR_ERR(chain);
}
if (nft_chain_is_bound(chain))
return -EOPNOTSUPP;
} else if (nla[NFTA_RULE_CHAIN_ID]) {
chain = nft_chain_lookup_byid(net, nla[NFTA_RULE_CHAIN_ID]);
if (IS_ERR(chain)) {
NL_SET_BAD_ATTR(extack, nla[NFTA_RULE_CHAIN_ID]);
return PTR_ERR(chain);
}
} else {
return -EINVAL;
}
...
nft_ctx_init(&ctx, net, skb, info->nlh, family, table, chain, nla);
n = 0;
size = 0;
if (nla[NFTA_RULE_EXPRESSIONS]) { // [3] 若设置了nla[NFTA_RULE_EXPRESSIONS], 会先把所有的expression遍历出来. 计算其总值放在size中
expr_info = kvmalloc_array(NFT_RULE_MAXEXPRS,
sizeof(struct nft_expr_info),
GFP_KERNEL);
if (!expr_info)
return -ENOMEM;
nla_for_each_nested(tmp, nla[NFTA_RULE_EXPRESSIONS], rem) {
err = -EINVAL;
if (nla_type(tmp) != NFTA_LIST_ELEM)
goto err_release_expr;
if (n == NFT_RULE_MAXEXPRS)
goto err_release_expr;
err = nf_tables_expr_parse(&ctx, tmp, &expr_info[n]);
if (err < 0) {
NL_SET_BAD_ATTR(extack, tmp);
goto err_release_expr;
}
size += expr_info[n].ops->size;
n++;
}
}
/* Check for overflow of dlen field */
err = -EFBIG;
if (size >= 1 << 12)
goto err_release_expr;
if (nla[NFTA_RULE_USERDATA]) { // [4] 若设置了nla[NFTA_RULE_USERDATA],获取userdata的大小放在usize中
ulen = nla_len(nla[NFTA_RULE_USERDATA]);
if (ulen > 0)
usize = sizeof(struct nft_userdata) + ulen;
}
err = -ENOMEM;
rule = kzalloc(sizeof(*rule) + size + usize, GFP_KERNEL_ACCOUNT); // [5] 分配内存,创建一个rule,并初始化相关数据域
if (rule == NULL)
goto err_release_expr;
nft_activate_next(net, rule);
rule->handle = handle;
rule->dlen = size;
rule->udata = ulen ? 1 : 0;
if (ulen) {
udata = nft_userdata(rule);
udata->len = ulen - 1;
nla_memcpy(udata->data, nla[NFTA_RULE_USERDATA], ulen);
}
...
}
第四步操作创建expression,其实这一步和创建rule是连在一起的。都在 nf_tables_newrule() 函数中实现。expresssion总共有如下多种类型(参见 nft_basic_types)。
static struct nft_expr_type *nft_basic_types[] = {
&nft_imm_type,
&nft_cmp_type,
&nft_lookup_type,
&nft_bitwise_type,
&nft_byteorder_type,
&nft_payload_type,
&nft_dynset_type,
&nft_range_type,
&nft_meta_type,
&nft_rt_type,
&nft_exthdr_type,
&nft_last_type,
&nft_counter_type,
};
nf_tables_newrule() -> nf_tables_newexpr()
static int nf_tables_newrule(struct sk_buff *skb, const struct nfnl_info *info,
const struct nlattr * const nla[])
{
...
expr = nft_expr_first(rule); // [6] 将用户层传进来的expression剥离出来后,依次放在rule中
for (i = 0; i < n; i++) {
err = nf_tables_newexpr(&ctx, &expr_info[i], expr); // [6-1] nf_tables_newexpr() 函数,会根据expression类型对其进行初始化
if (err < 0) {
NL_SET_BAD_ATTR(extack, expr_info[i].attr);
goto err_release_rule;
}
if (expr_info[i].ops->validate)
nft_validate_state_update(net, NFT_VALIDATE_NEED);
expr_info[i].ops = NULL;
expr = nft_expr_next(expr);
}
if (chain->flags & NFT_CHAIN_HW_OFFLOAD) {
flow = nft_flow_rule_create(net, rule);
if (IS_ERR(flow)) {
err = PTR_ERR(flow);
goto err_release_rule;
}
}
...
}
static int nf_tables_newexpr(const struct nft_ctx *ctx,
const struct nft_expr_info *expr_info,
struct nft_expr *expr)
{
const struct nft_expr_ops *ops = expr_info->ops;
int err;
expr->ops = ops;
if (ops->init) {
err = ops->init(ctx, expr, (const struct nlattr **)expr_info->tb);
if (err < 0)
goto err1;
}
return 0;
err1:
expr->ops = NULL;
return err;
}
以上就是一个完整的table->chain->rule->expression的创建过程。
漏洞触发trace:nf_tables_newsetelem() -> nft_add_set_elem() -> nft_set_elem_init()
nft_set 结构中表示长度的成员 udlen/klen/dlen
可能可以用来构造写原语,构造更好的溢出。
struct nft_set {
struct list_head list;
struct list_head bindings;
struct nft_table *table;
possible_net_t net;
char *name;
u64 handle;
u32 ktype;
u32 dtype;
u32 objtype;
u32 size;
u8 field_len[NFT_REG32_COUNT];
u8 field_count;
u32 use;
atomic_t nelems;
u32 ndeact;
u64 timeout;
u32 gc_int;
u16 policy;
u16 udlen;
unsigned char *udata;
/* runtime data below here */
const struct nft_set_ops *ops ____cacheline_aligned;
u16 flags:14,
genmask:2;
u8 klen;
u8 dlen;
u8 num_exprs;
struct nft_expr *exprs[NFT_SET_EXPR_MAX];
struct list_head catchall_list;
unsigned char data[]
__attribute__((aligned(__alignof__(u64))));
};
通过观察对dlen
成员的访问代码,nft_set_elem_init() 函数中 [1]
处调用了 memcpy()
,并且拷贝长度用到了 set->dlen
。
这个memcpy()
调用用到了两个不同的对象,目的地址位于 nft_set_ext 对象,拷贝长度却来自 nft_set 结构。 nft_set_ext 对象是在 [0]
处分配的,分配长度用到了 tmpl->len
,作者想检查前面是不是用到了 set->dlen
来计算 tmpl->len
。
void *nft_set_elem_init(const struct nft_set *set,
const struct nft_set_ext_tmpl *tmpl,
const u32 *key, const u32 *key_end,
const u32 *data, u64 timeout, u64 expiration, gfp_t gfp)
{
struct nft_set_ext *ext;
void *elem;
elem = kzalloc(set->ops->elemsize + tmpl->len, gfp); // [0] 调用kzalloc()函数分配内存,大小为 elemsize+tmpl->len,这里的 tmpl->len 已经包括了 desc.dlen。正常情况下desc.dlen应该是等于set->dlen的
if (elem == NULL)
return NULL;
ext = nft_set_elem_ext(set, elem);
nft_set_ext_init(ext, tmpl);
if (nft_set_ext_exists(ext, NFT_SET_EXT_KEY))
memcpy(nft_set_ext_key(ext), key, set->klen);
if (nft_set_ext_exists(ext, NFT_SET_EXT_KEY_END))
memcpy(nft_set_ext_key_end(ext), key_end, set->klen);
if (nft_set_ext_exists(ext, NFT_SET_EXT_DATA))
memcpy(nft_set_ext_data(ext), data, set->dlen); // [1] 可疑拷贝点, 如果 set->dlen 不等于 desc.dlen, 则有可能发生溢出
...
return elem;
}
tmpl->len
初始化目标:往上看看哪里调用了漏洞函数,看看是否用到了 set->dlen
来计算 tmpl->len
。
[3]
初始化 desc
[4]
采用用 desc->len
来计算 tmpl->len
,所以 tmpl->len
与 set->dlen
无关(set->dlen
和 id NFT_SET_EXT_DATA
对应的数据无关) ,从 [7]
中的检查可以看出二者可以不相等。[5]
漏洞函数。漏洞:[7]
处的检查可以绕过。用户可控制 set->dlen
,但是限制是 set->dlen
必须小于 64 字节,且 data type 不等于 NFT_DATA_VERDICT
(如果 desc->type == NFT_DATA_VERDICT
,则不会判断 desc->len
和 set->dlen
是否相同,并成功返回)。
如果往 NFT_DATA_VERDICT
中添加一个 data type 为 NFT_DATA_VALUE
的成员,就可以使 desc->len != set->dlen
(desc->len = 16
/ set->dlen = 64
),这样就可以在 nft_set_elem_init() 函数中 [1]
处触发OOB,溢出长度可以达到48字节。
static int nft_add_set_elem(struct nft_ctx *ctx, struct nft_set *set,
const struct nlattr *attr, u32 nlmsg_flags)
{
struct nlattr *nla[NFTA_SET_ELEM_MAX + 1];
struct nft_set_ext_tmpl tmpl;
struct nft_set_elem elem; // [2]
struct nft_data_desc desc;
...
if (nla[NFTA_SET_ELEM_DATA] != NULL) { // 处理 nla[NFTA_SET_ELEM_DATA] 对应的数据
err = nft_setelem_parse_data(ctx, set, &desc, &elem.data.val, // [3] 初始化 desc
nla[NFTA_SET_ELEM_DATA]);
if (err < 0)
goto err_parse_key_end;
dreg = nft_type_to_reg(set->dtype);
list_for_each_entry(binding, &set->bindings, list) {
...
}
nft_set_ext_add_length(&tmpl, NFT_SET_EXT_DATA, desc.len); // [4] 采用 desc.len 来设置 tmpl.len, 所以 tmpl.len 与 set->dlen 无关 (从 [7] 中可以看出二者可以不相等)
}
...
err = -ENOMEM;
elem.priv = nft_set_elem_init(set, &tmpl, elem.key.val.data, // [5] nft_set_elem_init() 漏洞函数,进行初始化
elem.key_end.val.data, elem.data.val.data,
timeout, expiration, GFP_KERNEL);
if (elem.priv == NULL)
goto err_parse_data;
...
}
static int nft_setelem_parse_data(struct nft_ctx *ctx, struct nft_set *set,
struct nft_data_desc *desc,
struct nft_data *data,
struct nlattr *attr)
{
int err;
err = nft_data_init(ctx, data, NFT_DATA_VALUE_MAXLEN, desc, attr); // [6] 调用 nft_data_init(), 根据用户的输入 attr 来填充 data / desc
if (err < 0)
return err;
if (desc->type != NFT_DATA_VERDICT && desc->len != set->dlen) { // [7] 检查了 desc->len 和 set->dlen 是否相等, 但是只要 desc->type == NFT_DATA_VERDICT, 这个检查就形同虚设, desc->len 和 set->dlen 可以不相等
nft_data_release(data, desc->type);
return -EINVAL;
}
return 0;
}
对比:本漏洞的最上层函数是 nf_tables_newsetelem(),但是在 nf_tables_newset() 函数中,是正确将 desc->dlen
赋值给了 set->dlen
,没有这个漏洞。
目标:用户如何控制溢出的数据呢,这里溢出时会拷贝栈上的随机数据。
源地址来源:从[5]
处 nft_add_set_elem() -> nft_set_elem_init() 可以看出,源地址 data 来自局部变量 elem
,也即 nft_set_elem 结构——elem.data.val.data
。在创建新成员时,该结构用于存储新成员的信息。以下可以看到,elem.data
只有16字节可控,所以溢出时会拷贝栈上的随机字节(elem.data.val.data
后面的数据)。
#define NFT_DATA_VALUE_MAXLEN 64
struct nft_verdict {
u32 code;
struct nft_chain *chain;
};
struct nft_data {
union {
u32 data[4]; // <------------ 16字节数据可控
struct nft_verdict verdict;
};
} __attribute__((aligned(__alignof__(u64))));
struct nft_set_elem {
union {
u32 buf[NFT_DATA_VALUE_MAXLEN / sizeof(u32)];
struct nft_data val;
} key;
union {
u32 buf[NFT_DATA_VALUE_MAXLEN / sizeof(u32)];
struct nft_data val;
} key_end;
union {
u32 buf[NFT_DATA_VALUE_MAXLEN / sizeof(u32)]; // NFT_DATA_VALUE_MAXLEN = 64, 所以最长为64字节
struct nft_data val; // <------------
} data;
void *priv;
};
栈未初始化使用:[2]
处的 elem.data
没有进行初始化,现在看看上层调用中是否可以控制 elem.data
,nf_tables_newsetelem() -> nft_add_set_elem(),对每个新添加的成员调用 nft_add_set_elem()。
static int nf_tables_newsetelem(struct sk_buff *skb,
const struct nfnl_info *info,
const struct nlattr * const nla[])
{
...
// 遍历用户输入的 attribute, 所以用户可以控制遍历次数。由于每次调用 nft_add_set_elem() 都是紧挨着的,所以尽管 elem.data 没有初始化,所以很有可能用的就是上一次调用时的成员数据。可以忽略栈结构的随机化,所以控制溢出要取决于内核的编译。
nla_for_each_nested(attr, nla[NFTA_SET_ELEM_LIST_ELEMENTS], rem) {
err = nft_add_set_elem(&ctx, set, attr, info->nlh->nlmsg_flags);// 调用 nft_add_set_elem()
if (err < 0) {
NL_SET_BAD_ATTR(extack, attr);
return err;
}
}
}
控制溢出数据:下图总结了不同阶段时,栈上的elem.data
数据的变化。最开始,栈上存着随机数据,然后添加一个新的成员 NFT_DATA_VALUE
布置栈数据,最后添加第2个 NFT_DATA_VERDICT
来触发堆溢出(复用 NFT_DATA_VALUE
的数据),这样就能控制溢出数据。
最后,看看漏洞对象所属的cache([0]
处分配的 elem
)。漏洞对象的大小取决于用户的选项( nft_add_set_elem() 函数)。这里有几个选项可以增大该对象的大小,例如 NFT_SET_ELEM_KEY
/ NFT_SET_ELEM_KEY_END
,可以在 elem
中增大最多64字节的缓冲区,总的来说,漏洞对象可以位于 kmalloc-{64,96,128,192}
,注意,分配时的flag为 GFP_KERNEL
。
如何构造 elem
以获得最佳溢出效果呢?如下图所示,构造 elem
使之位于 kmalloc-64:
NFT_SET_ELEM_KEY
选项来填充 28 字节;NFT_DATA_VERDICT
会有 16字节来存成员数据。泄露内核基址:由于漏洞对象位于 kmaloc-x
,而msg_msg
位于 kmalloc-cg-x
,所以不能用 msg_msg
来泄露了。可以采用 user_key_payload 来泄露(),该对象也包含长度变量和用户数据,在 user_preparse() 函数中分配,该对象 header 占24字节,大小可以位于 kmalloc-32
到 kmalloc-8k
。
目标是溢出覆盖 user_key_payload->datalen
,然后获取更多的数据。
struct user_key_payload {
struct rcu_head rcu; /* RCU destructor */
unsigned short datalen; /* length of this data */
char data[] __aligned(__alignof__(u64)); /* actual data */
};
int user_preparse(struct key_preparsed_payload *prep)
{
struct user_key_payload *upayload;
size_t datalen = prep->datalen;
if (datalen <= 0 || datalen > 32767 || !prep->data)
return -EINVAL;
upayload = kmalloc(sizeof(*upayload) + datalen, GFP_KERNEL); // [6] 用户提供长度
if (!upayload)
return -ENOMEM;
/* attach the data */
prep->quotalen = datalen;
prep->payload.data[0] = upayload;
upayload->datalen = datalen;
memcpy(upayload->data, prep->data, datalen); // [7] 将用户数据拷贝进去
return 0;
}
EXPORT_SYMBOL_GPL(user_preparse);
限制:本方法有个缺点,就是 user_key_payload 对象的分配数量有限,sysctl
变量 kernel.keys.maxkeys
限制了key的最大分配个数,kernel.keys.maxbytes
限制了key的总长度。Ubuntu 22.04 的默认值如下:
kernel.keys.maxbytes = 20000
kernel.keys.maxkeys = 200
泄露对象:kmalloc-64
中有哪些数据可以泄露呢。percpu_ref_data 对象也位于 kmalloc-64
,其中包含两种有用的指针,release
/ confirm_switch
可以泄露内核基址(io_uring
已经整合到了内核中,io_ring_ctx_ref_free() 地址可以用于计算内核基址),ref
可以泄露 physmap 基址。
该对象在 io_ring_ctx_alloc() -> percpu_ref_init() 函数中分配,用户可以使用 io_uring_setup
syscall 来触发执行该函数(在初始化 io_ring_ctx 对象时调用该函数),调用 close
可以释放该对象。
作者测试时,发现还泄露了些意料之外的 percpu_ref_data 对象,其中 percpu_ref_data->resease
指向 io_rsrc_node_ref_zero() 函数。研究后发现,这些对象也来自 io_uring_setup
syscall。这个副作用也有利于内核基址泄露。所以 percpu_ref_data->release
的值有两种可能。
struct percpu_ref_data {
atomic_long_t count;
percpu_ref_func_t *release; // 内核基址
percpu_ref_func_t *confirm_switch;// 内核基址
bool force_atomic:1;
bool allow_reinit:1;
struct rcu_head rcu;
struct percpu_ref *ref; // physmap 地址
};
int percpu_ref_init(struct percpu_ref *ref, percpu_ref_func_t *release,
unsigned int flags, gfp_t gfp)
{
struct percpu_ref_data *data;
...
data = kzalloc(sizeof(*ref->data), gfp); // alloc
...
data->release = release;
data->confirm_switch = NULL;
data->ref = ref;
ref->data = data;
return 0;
}
static __cold struct io_ring_ctx *io_ring_ctx_alloc(struct io_uring_params *p)
{
struct io_ring_ctx *ctx;
...
if (percpu_ref_init(&ctx->refs, io_ring_ctx_ref_free, // <-----------
PERCPU_REF_ALLOW_REINIT, GFP_KERNEL))
goto err;
...
}
任意写新方法:参考了文章 io_uring - new code, new bugs, and a new exploit technique 对 CVE-2021-41073 漏洞新的利用方法,叫做 unlinking attack
,基于 list_del
操作。伪造两个地址来替代 list_head
,这样其中一个地址就会被写到另一个地址上。
simple_xattr
对象simple_xattr 对象介绍:该结构常用于存储 in-memory filesystems (例如tmpfs)的扩展属性(xattrs - extended attribute),每个文件的 simple_xattr 以 list_head
链表存起来。分配函数是 simple_xattr_alloc(),用户可控 simple_xattr->value
,分配大小是 kmalloc-32 到很大。
缺点:simple_xattr 不能修改,当对它进行编辑时,会把旧的 simple_xattr
从链表unlink,然后分配新的 simple_xattr
并链接上去。所以通过伪造size和 next指针,无法构造越界写或任意地址写。还有个问题就是非特权用户无法设置 simple_xattr
,但是只要系统支持 user namespace 即可。
struct simple_xattr {
struct list_head list;
char *name;
size_t size;
char value[];
};
struct list_head {
struct list_head *next, *prev;
};
任意写:__list_del() 代码如下,如果我们能够控制 prev
/ next
指针,可以把 next
指针设置为 modprobe_path
,这样就会在 [1]
处将 prev
值写入 next
指向的内存偏移8字节处。
问题:[2]
处,next
会写往 prev
,这意味着 prev
也必须是一个有效的指针,这限制了我们能写往 next
的值。解决办法是,利用 physmap 提供一个有效的 prev
值。
static inline void __list_del(struct list_head * prev, struct list_head * next)
{
next->prev = prev; // [1]
WRITE_ONCE(prev->next, next); // [2]
}
physmap:physmap 是一块内核虚拟内存,物理内存页连续映射到该处。例如,如果机器有4G内存(2^32 字节),需用32 bit来索引物理内存;假设 physmap 起始地址是 0xffffffff00000000,则 0xffffffff00000000~0xffffffffffffffff
范围内的值都有效。因此,若系统有4G内存,攻击者可以控制 prev
的低4字节,只要高4字节表示physmap地址即可。
由于我们目标是修改 modprobe_path
,可以构造 prev = 0xffffxxxx2f706d74
,若 next = modprobe_path+1
,利用 2
将 modprobe_path
覆写为 /tmp/xxxxprobe
(其中 xxxx
是 prev
的高4字节)。后面即可提权。
触发分配 kmalloc-32 的 simple_xattr,然后采用 unlinking attack,就能将一个溢出或错误释放转化为有限制的任意写,尽管只能写4个可控的字节和4个不可控的字节,但足以提权。
优点与限制:和 msg_msg
相比的优点是,list_head
前面没有metadata 不担心破坏。本技术需要提前泄露physmap 中的一个地址,有些结构,例如 shm_file_data 既包含text段指针又包含 physmap 地址,所以这不是问题。还要注意,所选的physmap地址必须是可写的,该地址处的数据会被覆写。
目标:该技术需要知道哪个 simple_xattr 对象被覆盖了,否则随意移除item会导致遍历 list 时报错,可通过 name 来确定list中的item。
为了识别被覆盖的对象,可以分配长度 256 的name,这样最低字节为NULL,这样我们伪造 list_head 的同时可以覆盖 name 指针的最低字节,这样就能识别出被覆盖的对象。唯一的要求就是所有的name结尾相同。下图总结了如何构造 simple_xattr->name
。
提权:采用这个写原语来篡改 modprobe_path
提权。
利用问题:从 v5.18.1 开始,就把漏洞对象放入 kmalloc-cg-*
中了(nft_add_set_elem() -> nft_set_elem_init() 分配),而弹性对象 user_key_payload
(user_preparse() 分配)和 percpu_ref_data
( io_ring_ctx_alloc() -> percpu_ref_init() 分配)都是采用 GFP_KERNEL
flag 分配的,导致一直不能使3个对象相邻。所以 v5.18.1 以上版本的内核无法完成利用(可能可以利用 msg_msg
对象来越界读和任意写)。
# ftrace 调试 v5.18.1 发现问题
exploit-308 [000] ..... 42.319031: kmalloc: call_site=strndup_user+0x46/0x60 ptr=ffff888106856f90 bytes_req=16 bytes_alloc=16 gfp_flags=GFP_USER|__GFP_NOWARN
exploit-308 [000] ..... 42.319033: kmalloc_node: call_site=kvmalloc_node+0x26/0xf0 ptr=ffff888106856c40 bytes_req=15 bytes_alloc=16 gfp_flags=GFP_KERNEL node=-1
exploit-308 [000] ..... 42.319037: kmalloc: call_site=user_preparse+0x36/0x70 ptr=ffff8881061b2c40 bytes_req=39 bytes_alloc=64 gfp_flags=GFP_KERNEL
...
exploit-308 [000] ..... 42.319186: kmalloc: call_site=nft_set_elem_init+0x46/0x290 ptr=ffff888101b56ae0 bytes_req=82 bytes_alloc=96 gfp_flags=GFP_KERNEL_ACCOUNT|__GFP_ZERO
exploit-308 [000] ..... 42.319190: kmalloc: call_site=nft_trans_alloc_gfp+0x22/0x60 ptr=ffff888100834000 bytes_req=288 bytes_alloc=512 gfp_flags=GFP_KERNEL|__GFP_ZERO
exploit-308 [000] ..... 42.319205: kmalloc: call_site=nft_set_elem_init+0x46/0x290 ptr=ffff8881017c92c0 bytes_req=64 bytes_alloc=64 gfp_flags=GFP_KERNEL_ACCOUNT|__GFP_ZERO
...
exploit-308 [000] ..... 42.355076: kmalloc: call_site=percpu_ref_init+0x6f/0x130 ptr=ffff8881061b2b40 bytes_req=56 bytes_alloc=64 gfp_flags=GFP_KERNEL|__GFP_ZERO
v5.18.1 中分配漏洞对象:
static int nft_add_set_elem(struct nft_ctx *ctx, struct nft_set *set,
const struct nlattr *attr, u32 nlmsg_flags)
{
...
elem.priv = nft_set_elem_init(set, &tmpl, elem.key.val.data,
elem.key_end.val.data, elem.data.val.data,
timeout, expiration, GFP_KERNEL_ACCOUNT);
...
}
v5.17.15 中分配漏洞对象:
static int nft_add_set_elem(struct nft_ctx *ctx, struct nft_set *set,
const struct nlattr *attr, u32 nlmsg_flags)
{
...
elem.priv = nft_set_elem_init(set, &tmpl, elem.key.val.data,
elem.key_end.val.data, elem.data.val.data,
timeout, expiration, GFP_KERNEL);
...
}
# 安装 liburing 生成 liburing.a / liburing.so.2.2
$ make
$ sudo make install
常用命令:
# ssh连接与测试
$ ssh -p 10021 hi@localhost # password: lol
$ ./exploit
# 编译exp
$ make CFLAGS="-I /home/hi/lib/libnftnl-1.2.2/include"
$ gcc -static ./get_root.c -o ./get_root
$ gcc -no-pie -static -pthread ./exploit.c -o ./exploit
# scp 传文件
$ scp -P 10021 ./exploit hi@localhost:/home/hi # 传文件
$ scp -P 10021 hi@localhost:/home/hi/trace.txt ./ # 下载文件
$ scp -P 10021 ./exploit.c ./get_root.c ./exploit ./get_root hi@localhost:/home/hi
问题:原来的 ext4文件系统空间太小,很多包无法安装,现在换syzkaller中的 stretch.img
试试。
# 服务端添加用户
$ useradd hi && echo lol | passwd --stdin hi
# ssh连接
$ sudo chmod 0600 ./stretch.id_rsa
$ ssh -i stretch.id_rsa -p 10021 -o "StrictHostKeyChecking no" root@localhost
$ ssh -p 10021 hi@localhost
# 问题: Host key verification failed.
# 删除ip对应的相关rsa信息即可登录 $ sudo nano ~/.ssh/known_hosts
# https://blog.csdn.net/ouyang_peng/article/details/83115290
ftrace调试:注意,QEMU启动时需加上 no_hash_pointers
启动选项,否则打印出来的堆地址是hash之后的值。trace中只要用 %p
打印出来的数据都会被hash,所以可以修改 TP_printk() 处输出时的格式符,%p
-> %lx
。
# host端, 需具备root权限
cd /sys/kernel/debug/tracing
echo 1 > events/kmem/kmalloc/enable
echo 1 > events/kmem/kmalloc_node/enable
echo 1 > events/kmem/kfree/enable
# ssh 连进去执行 exploit
cat /sys/kernel/debug/tracing/trace > /home/hi/trace.txt
# 下载 trace
scp -P 10021 hi@localhost:/home/hi/trace.txt ./ # 下载文件
[CVE-2022-34918] A crack in the Linux firewall
https://www.openwall.com/lists/oss-security/2022/07/02/3
Linux内核nftables子系统研究与漏洞分析 —— 总结 nftables 漏洞
io_uring - new code, new bugs, and a new exploit technique —— CVE-2021-41073 利用新方法
https://github.com/star-sg/CVE/ —— CVE-2021-41073 利用新方法