漏洞简介
漏洞分析
成因
使用 NFTA_SET_ELEM_OBJREF 请求可以让 set 保存指向一个位于其他 table 中的 object,如果 set 和 object 不在同一张表中,并且释放 object 所在的表,会把 object 也释放掉,但是没有清理 set 中保存的 object 指针,导致后面内核通过 set 来引用 object 指针时会触发 UAF.
触发 UAF 的步骤:
-
首先创建两个 table (NFT_MSG_NEWTABLE)
-
使用 NFT_MSG_NEWOBJ 分配一个 object 并将其链接到 table_1->objects.
-
使用 NFT_MSG_NEWSET 分配一个 set 并将其链接到 table_2->sets
-
使用 NFT_MSG_NEWSETELEM 和 NFTNL_SET_ELEM_OBJREF 让 set 中保存对 obj 的指针.
-
使用 NFT_MSG_DELTABLE 释放 table_1 ,table_1 和 obj 都会被释放,此时 set 中依然保存着 obj 的指针.
相关代码分析
创建 table
用户态使用 NFT_MSG_NEWTABLE 请求可以创建 table
[NFT_MSG_NEWTABLE] = {
.call_batch = nf_tables_newtable,
.attr_count = NFTA_TABLE_MAX,
.policy = nft_table_policy,
},
nft_table_policy 函数的关键代码如下
static int nf_tables_newtable(struct net *net, struct sock *nlsk,
struct sk_buff *skb, const struct nlmsghdr *nlh,
const struct nlattr * const nla[],
struct netlink_ext_ack *extack)
{
attr = nla[NFTA_TABLE_NAME];
table = nft_table_lookup(net, attr, family, genmask);
if (IS_ERR(table)) {
if (PTR_ERR(table) != -ENOENT)
return PTR_ERR(table);
} else {
// 表已经存在, 更新表
}
// 新建表
table = kzalloc(sizeof(*table), GFP_KERNEL);
// 初始化 table
table->name = nla_strdup(attr, GFP_KERNEL);
if (nla[NFTA_OBJ_USERDATA]) {
// memdup 原语
obj->udata = nla_memdup(nla[NFTA_OBJ_USERDATA], GFP_KERNEL);
obj->udlen = nla_len(nla[NFTA_OBJ_USERDATA]);
}
err = nft_trans_table_add(&ctx, NFT_MSG_NEWTABLE);
// 将表挂在 net->nft.tables 链表中
list_add_tail_rcu(&table->list, &net->nft.tables);
return 0;
}
创建 object
相关代码
static int nf_tables_newobj(struct net *net, struct sock *nlsk,
struct sk_buff *skb, const struct nlmsghdr *nlh,
const struct nlattr * const nla[],
struct netlink_ext_ack *extack)
{
// 找到要链接的表
table = nft_table_lookup(net, nla[NFTA_OBJ_TABLE], family, genmask);
// 分配 object
obj = nft_obj_init(&ctx, type, nla[NFTA_OBJ_DATA]);
obj->key.table = table;
obj->handle = nf_tables_alloc_handle(table);
// 分配 obj->key.name
obj->key.name = nla_strdup(nla[NFTA_OBJ_NAME], GFP_KERNEL);
if (nla[NFTA_OBJ_USERDATA]) {
// memdup 原语
obj->udata = nla_memdup(nla[NFTA_OBJ_USERDATA], GFP_KERNEL);
obj->udlen = nla_len(nla[NFTA_OBJ_USERDATA]);
}
// 将 object 插入到 hash 表和 table->objects
err = rhltable_insert(&nft_objname_ht, &obj->rhlhead,
nft_objname_ht_params);
list_add_tail_rcu(&obj->list, &table->objects);
return 0;
}
分配 set
static int nf_tables_newset(struct net *net, struct sock *nlsk,
struct sk_buff *skb, const struct nlmsghdr *nlh,
const struct nlattr * const nla[],
struct netlink_ext_ack *extack)
{
// 找表
table = nft_table_lookup(net, nla[NFTA_SET_TABLE], family, genmask);
ops = nft_select_set_ops(&ctx, nla, &desc, policy);
if (IS_ERR(ops))
return PTR_ERR(ops);
udlen = 0;
if (nla[NFTA_SET_USERDATA])
udlen = nla_len(nla[NFTA_SET_USERDATA]);
// 分配 set
set = kvzalloc(sizeof(*set) + size + udlen, GFP_KERNEL);
if (!set)
return -ENOMEM;
// 申请 set 中的 expr
if (nla[NFTA_SET_EXPR]) {
} else if (nla[NFTA_SET_EXPRESSIONS]) {
}
udata = NULL;
if (udlen) {
udata = set->data + size;
nla_memcpy(udata, nla[NFTA_SET_USERDATA], udlen);
}
// 初始化 set
// 将 set 链接到 table->sets
list_add_tail_rcu(&set->list, &table->sets);
return 0;
}
往 set 中插入元素
首先会进入 nf_tables_newsetelem 找到要操作的 set,然后一个一个往里面增加 elem
static int nf_tables_newsetelem(struct net *net, struct sock *nlsk,
struct sk_buff *skb, const struct nlmsghdr *nlh,
const struct nlattr * const nla[],
struct netlink_ext_ack *extack)
{
// 找到需要操作的 set
set = nft_set_lookup_global(net, ctx.table, nla[NFTA_SET_ELEM_LIST_SET],
nla[NFTA_SET_ELEM_LIST_SET_ID], genmask);
// 一个一个加元素
nla_for_each_nested(attr, nla[NFTA_SET_ELEM_LIST_ELEMENTS], rem) {
err = nft_add_set_elem(&ctx, set, attr, nlh->nlmsg_flags);
if (err < 0)
return err;
}
}
nft_add_set_elem 漏洞相关代码如下
static int nft_add_set_elem(struct nft_ctx *ctx, struct nft_set *set,
const struct nlattr *attr, u32 nlmsg_flags)
{
if (nla[NFTA_SET_ELEM_OBJREF] != NULL) {
// 找到 object
obj = nft_obj_lookup(ctx->net, ctx->table,
nla[NFTA_SET_ELEM_OBJREF],
set->objtype, genmask);
nft_set_ext_add(&tmpl, NFT_SET_EXT_OBJREF);
}
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);
ext = nft_set_elem_ext(set, elem.priv);
if (obj) {
*nft_set_ext_obj(ext) = obj; // [0] ext 里面保存了 object 的指针
obj->use++;
}
// [1] 将 elem 插入到 set 中
err = set->ops->insert(ctx->net, set, &elem, &ext2);
return 0;
}
如果请求中包含了 NFTA_SET_ELEM_OBJREF,在 [0]
处会将 object 指针保存到 ext (通过 set 和 elem 可以获取 )里面,然后 [1]
处会将 elem 插入到 set 中.
释放 table
nf_tables_deltable 会调用 nft_flush_table 释放 table 和 table 中的相关元素.
static int nf_tables_deltable(struct net *net, struct sock *nlsk,
struct sk_buff *skb, const struct nlmsghdr *nlh,
const struct nlattr * const nla[],
struct netlink_ext_ack *extack)
{
// 找到 table
table = nft_table_lookup(net, attr, family, genmask);
ctx.family = family;
ctx.table = table;
return nft_flush_table(&ctx);
}
static int nft_flush_table(struct nft_ctx *ctx)
{
list_for_each_entry_safe(set, ns, &ctx->table->sets, list) {
// 删除 table 中的 set
err = nft_delset(ctx, set);
}
list_for_each_entry_safe(obj, ne, &ctx->table->objects, list) {
// 删除 table 中的 object
err = nft_delobj(ctx, obj);
}
// 释放 table
err = nft_deltable(ctx);
}
释放 object
nft_delobj 用于删除 object,函数就是通过 nft_trans_obj_add 往 net->nft.commit_list 里面插入了一个节点,消息类型为 NFT_MSG_DELOBJ.
static int nft_delobj(struct nft_ctx *ctx, struct nft_object *obj)
{
int err;
err = nft_trans_obj_add(ctx, NFT_MSG_DELOBJ, obj);
return err;
}
static int nft_trans_obj_add(struct nft_ctx *ctx, int msg_type,
struct nft_object *obj)
{
struct nft_trans *trans;
trans = nft_trans_alloc(ctx, msg_type, sizeof(struct nft_trans_obj));
nft_trans_obj(trans) = obj;
list_add_tail(&trans->list, &ctx->net->nft.commit_list);
return 0;
}
消息处理函数为 nf_tables_commit
nfnetlink_rcv_batch --> ss->commit --> nf_tables_commit
static int nf_tables_commit(struct net *net, struct sk_buff *skb)
{
list_for_each_entry_safe(trans, next, &net->nft.commit_list, list) {
switch (trans->msg_type) {
case NFT_MSG_DELOBJ:
nft_obj_del(nft_trans_obj(trans)); // unlink object
nf_tables_obj_notify(&trans->ctx, nft_trans_obj(trans),
NFT_MSG_DELOBJ);
break;
}
nf_tables_commit_release(net);
return 0;
}
static void nft_obj_del(struct nft_object *obj)
{
rhltable_remove(&nft_objname_ht, &obj->rhlhead, nft_objname_ht_params);
list_del_rcu(&obj->list);
}
nf_tables_commit 首先调用 nft_obj_del 把 object 从 obj->list 中摘下来,然后再通过 nf_tables_commit_release 完成具体的释放
static void nf_tables_commit_release(struct net *net)
{
struct nft_trans *trans;
// 将这次的 commit_list 放到 nf_tables_destroy_list
spin_lock(&nf_tables_destroy_list_lock);
list_splice_tail_init(&net->nft.commit_list, &nf_tables_destroy_list);
spin_unlock(&nf_tables_destroy_list_lock);
// 执行内核 work 完成具体的释放,处理函数 nf_tables_trans_destroy_work
schedule_work(&trans_destroy_work);
}
在内核 work 里面会 nf_tables_trans_destroy_work –> nft_commit_release –> nft_obj_destroy 完成对 obj 的释放.
nf_tables_trans_destroy_work --> nft_commit_release
static void nft_commit_release(struct nft_trans *trans)
{
switch (trans->msg_type) {
case NFT_MSG_DELOBJ:
nft_obj_destroy(&trans->ctx, nft_trans_obj(trans));
break;
}
static void nft_obj_destroy(const struct nft_ctx *ctx, struct nft_object *obj)
{
if (obj->ops->destroy)
obj->ops->destroy(ctx, obj);
module_put(obj->ops->type->owner);
kfree(obj->key.name);
kfree(obj->udata);
kfree(obj);
}
获取 ext 中 obj 的 name
static int nf_tables_fill_setelem(struct sk_buff *skb,
const struct nft_set *set,
const struct nft_set_elem *elem)
{
const struct nft_set_ext *ext = nft_set_elem_ext(set, elem->priv);
unsigned char *b = skb_tail_pointer(skb);
struct nlattr *nest;
if (nft_set_ext_exists(ext, NFT_SET_EXT_OBJREF) &&
nla_put_string(skb, NFTA_SET_ELEM_OBJREF,
(*nft_set_ext_obj(ext))->key.name) < 0) // []
goto nla_put_failure;
利用分析
-
触发 UAF 后 obj 和 obj->key.name 会被释放,但是 obj 所在内存没有被清零.
-
因此当 obj 被释放后,用户态通过 NFT_MSG_GETSETELEM 获取 obj->key.name 时依然会拿到已经释放的 obj->key.name 指针,且 obj->key.name 的内存大小可控.(利用释放内存中的残留数据)
-
让 obj->key.name 大小为 0x20,触发 UAF 后堆喷 seq_operations ,占位 name 所在的内存空间,然后读取 obj->key.name 拿到其中的函数指针,泄露内核基地址.
-
控制 obj->key.name 大小,触发 UAF ,然后堆喷 nft_object 让其落入 UAF 的内存,然后泄露 obj->list.next ,得到 ctx->table 的地址.
-
触发 UAF ,然后堆喷内存,占位 object 所在的内存空间,修改 object->key.name 指针为 table 的地址,泄露 object 堆地址
-
释放堆喷的 object,然后利用 memdup 原语喷 nft_object_ops,就可以在泄露的地址处布置函数指针
-
劫持函数指针,ROP 修改 modprobe_path 提权.
补丁分析
object 不能被其他 table 对象引用.
--- a/net/netfilter/nf_tables_api.c
+++ b/net/netfilter/nf_tables_api.c
@@ -3842,6 +3842,7 @@ static struct nft_set *nft_set_lookup_byhandle(const struct nft_table *table,
}
static struct nft_set *nft_set_lookup_byid(const struct net *net,
+ const struct nft_table *table,
const struct nlattr *nla, u8 genmask)
{
struct nftables_pernet *nft_net = nft_pernet(net);
@@ -3853,6 +3854,7 @@ static struct nft_set *nft_set_lookup_byid(const struct net *net,
struct nft_set *set = nft_trans_set(trans);
if (id == nft_trans_set_id(trans) &&
+ set->table == table &&
nft_active_genmask(set, genmask))
return set;
}
@@ -3873,7 +3875,7 @@ struct nft_set *nft_set_lookup_global(const struct net *net,
if (!nla_set_id)
return set;
- set = nft_set_lookup_byid(net, nla_set_id, genmask);
+ set = nft_set_lookup_byid(net, table, nla_set_id, genmask);
}
return set;
}
参考
https://www.openwall.com/lists/oss-security/2022/08/29/5
本文仅代表作者本人观点,用于技术探讨和交流,如有谬误,欢迎指正!
原文始发于微信公众号(华为安全应急响应中心):CVE-2022-2586 nf_tables UAF 漏洞分析