【kernel exploit】CVE-2022-32250 nftables错误链表操作导致UAF写的漏洞利用
影响版本:Linux v4.19.37~v5.18.1 v5.18.2 已修补。由syzkaller发现。7.8分。原作者在 Ubuntu 22.04 Kernel 5.15上提权
测试版本:Linux-5.17.12 exploit及测试环境下载地址—https://github.com/bsauce/kernel-exploit-factory
编译选项:
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.12.tar.xz
$ tar -xvf linux-5.17.12.tar.xz
# KASAN: 设置 make menuconfig 设置"Kernel hacking" ->"Memory Debugging" -> "KASan: runtime memory debugger"。
$ make -j32
$ make all
$ make modules
# 编译出的bzImage目录:/arch/x86/boot/bzImage。
漏洞描述:nftables
模块的 net/netfilter/nf_tables_api.c
采用 NFT_MSG_NEWSET
功能来添加 nft_set
时,处理 lookup
和 dynset
expression 时,由于错误的 NFT_EXPR_STATEFUL
检查,nft_expr
对象释放后仍位于nft_set->binding
链表中,新加入 nft_expr
时导致UAF写(触发漏洞需要 CAP_NET_ADMIN
权限)。UAF写会往 kmalloc-64
的偏移 0x18 处写入另一个 kmalloc-64
堆块偏移 0x18 的地址值。
nft_set_elem_expr_alloc() 先调用 nft_expr_init() 初始化 expression,再检查 expression类型是否为 NFT_EXPR_STATEFUL
(不是则直接释放该expression,但是忘记从nft_set->binding
链表解绑,后续链入新的 expression 时触发UAF写)。nft_expr_init() 先调用 nf_tables_expr_parse() ,再分配nft_expr
对象内存,最后调用 nf_tables_newexpr() 初始化 nft_expr
对象(并绑定到 nft_set->binding
链表)。
补丁:patch 先检查 NFT_EXPR_STATEFUL
,再分配 expression 内存。
diff --git a/net/netfilter/nf_tables_api.c b/net/netfilter/nf_tables_api.c
index 12fc9cda4a2cf..f296dfe86b622 100644
--- a/net/netfilter/nf_tables_api.c
+++ b/net/netfilter/nf_tables_api.c
@@ -2873,27 +2873,31 @@ static struct nft_expr *nft_expr_init(const struct nft_ctx *ctx,
err = nf_tables_expr_parse(ctx, nla, &expr_info);
if (err < 0)
- goto err1;
+ goto err_expr_parse;
+
+ err = -EOPNOTSUPP;
+ if (!(expr_info.ops->type->flags & NFT_EXPR_STATEFUL))
+ goto err_expr_stateful;
err = -ENOMEM;
expr = kzalloc(expr_info.ops->size, GFP_KERNEL_ACCOUNT);
if (expr == NULL)
- goto err2;
+ goto err_expr_stateful;
err = nf_tables_newexpr(ctx, &expr_info, expr);
if (err < 0)
- goto err3;
+ goto err_expr_new;
return expr;
-err3:
+err_expr_new:
kfree(expr);
-err2:
+err_expr_stateful:
owner = expr_info.ops->type->owner;
if (expr_info.ops->type->release_ops)
expr_info.ops->type->release_ops(expr_info.ops);
module_put(owner);
-err1:
+err_expr_parse:
return ERR_PTR(err);
}
@@ -5413,9 +5417,6 @@ struct nft_expr *nft_set_elem_expr_alloc(const struct nft_ctx *ctx,
return expr;
err = -EOPNOTSUPP;
- if (!(expr->ops->type->flags & NFT_EXPR_STATEFUL))
- goto err_set_elem_expr;
-
if (expr->ops->type->flags & NFT_EXPR_GC) {
if (set->flags & NFT_SET_TIMEOUT)
goto err_set_elem_expr;
保护机制:KASLR/SMEP/SMAP/KPTI
利用总结:采用 mqueue 中的 msg_msg
来泄露内核基址,因为 mqueue 中的 posix_msg_tree_node->msg_list
偏移为 0x18(且位于kmalloc-64
),恰好是UAF写的偏移;另外,posix_msg_tree_node->msg_list
也能用来构造 Unlink 利用来篡改 modprobe_path
。利用user_key_payload
泄露堆地址(便于构造unlink),老生常谈了。注意,需使用ubuntu21.04 以上的版本的libmnl 或 libnftnl才行。
- (0)初始化
- 设置 namespace 和 CPU 亲属性;
- 打开5个
mqueue
用于泄露内核基址和进行Unlink; - 分配4个 table 和 set,本次利用需触发3次UAF写;
- (1)泄露堆指针:利用
user_key_payload
对象读取堆地址- (1-1)分配漏洞对象
nft_expr1
并释放; - (1-2)喷射50个
user_key_payload
(kmalloc-64
,调用addkey
),占据漏洞对象nft_expr1
; - (1-3)链入新的
nft_expr
触发UAF写,往user_key_payload->data[0]
(偏移0x18)写入新nft_expr+0x18
的地址—kmalloc-64
; - (1-4)读取
user_key_payload
泄露堆地址heap_base
;
- (1-1)分配漏洞对象
- (2)泄露内核基址:利用 mqueue 的
posix_msg_tree_node->msg_list
来读取user_key_payload->rcu.func
内核函数地址- (2-1)堆风水:喷射 1000 个
percpu_ref_data
对象(kmalloc-64
,调用syscall(__NR_io_uring_setup, ...)
)来去碎片化(参考 CVE-2022-34918利用); - (2-2)分配漏洞对象
nft_expr2
并释放; - (2-3)喷射4个
posix_msg_tree_node
(kmalloc-64
,通过mq_send
调用发送消息),占据漏洞对象nft_expr2
; - (2-4)释放2个
percpu_ref_data
对象(没用,可以跳过这一步); - (2-5)链入新的
nft_expr
触发UAF写,往posix_msg_tree_node->msg_list
(偏移0x18)写入新nft_expr+0x18
的地址,释放新nft_expr
; - (2-6)喷射100个
user_key_payload
,占据释放的新nft_expr
并占据其附近堆块,用于伪造posix_msg_tree_node->msg_list
指向的msg_msg
(伪造msg_msg->m_list = heap_base
和msg_msg->m_ts = 0x28
,确保msg_msg->security = NULL
); - (2-7)通过读取
posix_msg_tree_node->msg_list
(调用mq_receive
)来泄露user_key_payload->rcu.func
(偏移0x8处)
- (2-1)堆风水:喷射 1000 个
- (3)unlink篡改
modprobe_path
提权:利用 mqueue 的posix_msg_tree_node->msg_list
构造 Unlink- (3-1)分配漏洞对象
nft_expr3
并释放; - (3-2)同
(2-3)
。喷射4个posix_msg_tree_node
(kmalloc-64
,通过mq_send
调用发送消息),占据漏洞对象nft_expr3
; - (3-3)同
(2-5)
。链入新的nft_expr
触发UAF写,往posix_msg_tree_node->msg_list
(偏移0x18)写入新nft_expr+0x18
的地址,释放新nft_expr
; - (3-4)喷射 1 个
user_key_payload
,占据释放的新nft_expr
,用于伪造posix_msg_tree_node->msg_list
指向的msg_msg
(伪造msg_msg->mlist.next = modprobe_path-7
和msg_msg->mlist.prev = (heap_base&0xffffffff00000000)+0x2f706d74
);(这里喷1个就能占据新nft_expr
,有点神奇) - (3-5)通过读取
posix_msg_tree_node->msg_list
(调用mq_receive
)来释放该msg_msg
,将该msg_msg
从链表中取出时触发 Unlink;
- (3-1)分配漏洞对象
- (4)触发执行
modprobe_path
并提权。
1. 漏洞分析
1-1. 简介
Netfilter:这是Linux内核中通过用户定义handler来实现网络相关任务的框架,Netfilter 提供不同的函数用于包过滤、网络地址转换、端口转换、包日志记录,内核模块可以通过Netfilter提供的一系列 hook,将回调函数注册到内核网络栈。
nftables:属于Netfilter的一个组件,可以根据用户定义的规则来对包进行过滤或路由重定向。nftables支持sets,可以在一个规则中使用多个IP地址、端口号。在定义规则时可以用大括号表示sets(例如{22, 80, 443}),sets类型包括 ipv4_addr
, ipv6_addr
, ether_addr
/ inet_proto
/ inet_service
/ mark
。
nftables 采用 tables
/ chains
/ rules
/ expressions
来存储和处理命令,tables
包含多个 chains
,tables
与特定协议绑定在一起,例如 IP
/ IP6
;chains
包含多个 rules
和待处理网络流量信息的类型;rules
包含多个 expressions
;expressions
评估输入是否满足一系列条件。
关于nftables 代码和结构分析可参见 【kernel exploit】CVE-2022-1015 nftables 栈溢出漏洞分析与利用 / 【kernel exploit】CVE-2022-34918 nftable堆溢出漏洞利用(list_head任意写)。
1-2. 结构关系
结构关系:nft_table -> nft_set -> nft_expr set 之间是双链表链接,expression 之间也是双链表链接。
1-3. 代码分析
调用关系:nf_tables_newset() -> nft_set_elem_expr_alloc() (漏洞函数)-> nft_expr_init() (expression初始化) -> nf_tables_newexpr() -> nft_lookup_init() (lookup
expression) -> nf_tables_bind_set() (将expression绑定到 set->binding
)
(1)入口函数 nf_tables_newset()
static const struct nfnl_callback nf_tables_cb[NFT_MSG_MAX] = {
··· ···
[NFT_MSG_NEWSET] = {
.call = nf_tables_newset, // <---------
.type = NFNL_CB_BATCH,
.attr_count = NFTA_SET_MAX,
.policy = nft_set_policy,
},
··· ···
}
static int nf_tables_newset(struct sk_buff *skb, const struct nfnl_info *info,
const struct nlattr * const nla[])
{
const struct nfgenmsg *nfmsg = nlmsg_data(info->nlh);
u32 ktype, dtype, flags, policy, gc_int, objtype;
struct netlink_ext_ack *extack = info->extack;
u8 genmask = nft_genmask_next(info->net);
int family = nfmsg->nfgen_family;
const struct nft_set_ops *ops;
struct nft_expr *expr = NULL;
struct net *net = info->net;
struct nft_set_desc desc;
struct nft_table *table;
unsigned char *udata;
struct nft_set *set;
struct nft_ctx ctx;
size_t alloc_size;
u64 timeout;
char *name;
int err, i;
u16 udlen;
u64 size;
···
set = nft_set_lookup(table, nla[NFTA_SET_NAME], genmask); // [1] 查找现有的 set
if (IS_ERR(set)) { // 找到则直接返回,一般找不到,则进行下面的初始化
if (PTR_ERR(set) != -ENOENT) {
NL_SET_BAD_ATTR(extack, nla[NFTA_SET_NAME]);
return PTR_ERR(set);
}
} else {
...
}
···
set = kvzalloc(alloc_size, GFP_KERNEL); // [2] 分配 set 空间
···
INIT_LIST_HEAD(&set->bindings); // [3] 初始化 set, 注意 bindings 成员是 list 结构
INIT_LIST_HEAD(&set->catchall_list);
set->table = table;
write_pnet(&set->net, net);
set->ops = ops;
set->ktype = ktype;
set->klen = desc.klen;
set->dtype = dtype;
set->objtype = objtype;
set->dlen = desc.dlen;
set->flags = flags;
set->size = desc.size;
set->policy = policy;
set->udlen = udlen;
set->udata = udata;
set->timeout = timeout;
set->gc_int = gc_int;
set->field_count = desc.field_count;
for (i = 0; i < desc.field_count; i++)
set->field_len[i] = desc.field_len[i];
err = ops->init(set, &desc, nla);
if (err < 0)
goto err_set_init;
if (nla[NFTA_SET_EXPR]) { // [4] 若设置了 NFTA_SET_EXPR 字段, 则调用 nft_set_elem_expr_alloc() 处理 NFTA_SET_EXPR 对应的数据
expr = nft_set_elem_expr_alloc(&ctx, set, nla[NFTA_SET_EXPR]); // <-------------
if (IS_ERR(expr)) {
err = PTR_ERR(expr);
goto err_set_expr_alloc;
}
set->exprs[0] = expr;
set->num_exprs++;
}
...
}
(2)漏洞函数nft_set_elem_expr_alloc()
struct nft_expr *nft_set_elem_expr_alloc(const struct nft_ctx *ctx,
const struct nft_set *set,
const struct nlattr *attr)
{
struct nft_expr *expr;
int err;
expr = nft_expr_init(ctx, attr); // [4-1] 调用 nft_expr_init() 初始化expr
if (IS_ERR(expr))
return expr;
err = -EOPNOTSUPP;
if (!(expr->ops->type->flags & NFT_EXPR_STATEFUL))
goto err_set_elem_expr; // [4-2] 如果不存在 NFT_EXPR_STATEFUL 标识,则释放刚初始化的 expr
...
err_set_elem_expr:
nft_expr_destroy(ctx, expr); // [4-3] 调用 nft_expr_destroy() 释放 expr
return ERR_PTR(err);
}
(3)初始化 expression nft_expr_init() -> nf_tables_newexpr()
struct nft_expr {
const struct nft_expr_ops *ops; // expr 对应的回调函数表
unsigned char data[] // 根据具体 expr 而定
__attribute__((aligned(__alignof__(u64))));
};
static struct nft_expr *nft_expr_init(const struct nft_ctx *ctx,
const struct nlattr *nla)
{
struct nft_expr_info expr_info;
struct nft_expr *expr;
struct module *owner;
int err;
err = nf_tables_expr_parse(ctx, nla, &expr_info); // [4-1-1] 初始化expr_info
if (err < 0)
goto err1;
err = -ENOMEM;
expr = kzalloc(expr_info.ops->size, GFP_KERNEL); // [4-1-2] 申请空间 `8+私有结构体长度`,本exp中是56
if (expr == NULL)
goto err2;
err = nf_tables_newexpr(ctx, &expr_info, expr); // [4-1-3] 初始化expr
if (err < 0)
goto err3;
return expr;
...
}
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) { // 调用 expr 对应的 init 函数进行初始化
err = ops->init(ctx, expr, (const struct nlattr **)expr_info->tb);
if (err < 0)
goto err1;
}
...
}
(4)nft_lookup
漏洞expression
不同类型的 expression 对应的 nft_expr 结构不同(主要是 nft_expr->data
存放的内容不同),例如,如果是lookup
expression,则nft_expr->data
中包含的是 nft_lookup 结构。本漏洞受到影响的 expression 有两种(结构中必须含有 binding
字段,这样才能绑定到 nft_set
结构上):look_up
/ nft_dynset
,分别位于 net\netfilter\nft_lookup.c
和 net\netfilter\nft_dynset.c
。作者对 nft_dynset
(kmalloc-96
)也完成了利用,本文以 look_up
(kmalloc-64
)为例,初始化函数对应为 nft_lookup_init() 。
static const struct nft_expr_ops nft_lookup_ops = {
.type = &nft_lookup_type,
.size = NFT_EXPR_SIZE(sizeof(struct nft_lookup)), // 标识 expr->data 的大小 56
.eval = nft_lookup_eval,
.init = nft_lookup_init,//init 是nft_lookup_init
.activate = nft_lookup_activate,
.deactivate = nft_lookup_deactivate,
.destroy = nft_lookup_destroy,
.dump = nft_lookup_dump,
.validate = nft_lookup_validate,
};
static inline void *nft_expr_priv(const struct nft_expr *expr)
{
return (void *)expr->data;//获取data地址
}
static int nft_lookup_init(const struct nft_ctx *ctx,
const struct nft_expr *expr,
const struct nlattr * const tb[])
{
struct nft_lookup *priv = nft_expr_priv(expr); // [1] 获取 expr 的 data 数据段,这里是 nft_lookup 结构体
u8 genmask = nft_genmask_next(ctx->net);
struct nft_set *set;
u32 flags;
int err;
if (tb[NFTA_LOOKUP_SET] == NULL ||
tb[NFTA_LOOKUP_SREG] == NULL)
return -EINVAL;
set = nft_set_lookup_global(ctx->net, ctx->table, tb[NFTA_LOOKUP_SET],
tb[NFTA_LOOKUP_SET_ID], genmask); // [2] 找到之前创建的set
··· // 各种初始化
priv->binding.flags = set->flags & NFT_SET_MAP;
err = nf_tables_bind_set(ctx, set, &priv->binding); // [3] 调用 nf_tables_bind_set() 进行绑定 <---- 将 expression 绑定到 nft_set
if (err < 0)
return err;
priv->set = set;
return 0;
}
(5)绑定 nft_set->bindings
链表
nf_tables_bind_set():主要是调用 list_add_tail_rcu()
函数将 nft_set->bindings
和 nft_lookup->binding->list
用双向链表链接起来。对应的结构体为 nft_set -> bindings
和 nft_lookup -> nft_set_binding -> list
(结构关系为 nft_expr -> nft_lookup -> nft_set_binding)
int nf_tables_bind_set(const struct nft_ctx *ctx, struct nft_set *set,
struct nft_set_binding *binding)
{
struct nft_set_binding *i;
struct nft_set_iter iter;
if (set->use == UINT_MAX)
return -EOVERFLOW;
if (!list_empty(&set->bindings) && nft_set_is_anonymous(set))
return -EBUSY;
if (binding->flags & NFT_SET_MAP) { // 上层函数设置的,会走入这个分支
/* If the set is already bound to the same chain all
* jumps are already validated for that chain.
*/
list_for_each_entry(i, &set->bindings, list) {
if (i->flags & NFT_SET_MAP &&
i->chain == binding->chain)
goto bind;
}
...
}
bind:
binding->chain = ctx->chain;
list_add_tail_rcu(&binding->list, &set->bindings); // 调用 list_add_tail_rcu() 链接链表, 将 expression 绑定到 set->binding
nft_set_trans_bind(ctx, set);
set->use++;
return 0;
}
EXPORT_SYMBOL_GPL(nf_tables_bind_set);
struct nft_set {
struct list_head list;
struct list_head bindings; // list
struct nft_table *table;
possible_net_t net;
char *name;
...
};
struct nft_lookup {
struct nft_set *set;
u8 sreg;
u8 dreg;
bool invert;
struct nft_set_binding binding; // list
};
struct nft_set_binding {
struct list_head list;
const struct nft_chain *chain;
u32 flags;
};
1-4. 漏洞分析
漏洞分析:nft_set_elem_expr_alloc() 中 [4-1]
中已经分成了空间分配、链接到 set 等操作,但如果在 [4-2]
中不满足条件(nft_expr->ops->type->flags != NFT_EXPR_STATEFUL
),则 [4-3]
调用 nft_expr_destroy() 释放该 nft_expr
结构体,但是却忘记将 nft_expr
从 set->binding
双向链表中取下来,后面再加入新的 nft_expr
时就会导致UAF。
释放路径:nft_expr_destroy() -> nf_tables_expr_destroy() -> nft_lookup_destroy() -> nf_tables_destroy_set()
void nft_expr_destroy(const struct nft_ctx *ctx, struct nft_expr *expr)
{
nf_tables_expr_destroy(ctx, expr); // -> nf_tables_expr_destroy()
kfree(expr);
}
static void nf_tables_expr_destroy(const struct nft_ctx *ctx,
struct nft_expr *expr)
{
const struct nft_expr_type *type = expr->ops->type;
if (expr->ops->destroy) // 调用 lookup 对应的 destory 函数
expr->ops->destroy(ctx, expr);
module_put(type->owner);
}
static void nft_lookup_destroy(const struct nft_ctx *ctx,
const struct nft_expr *expr)
{
struct nft_lookup *priv = nft_expr_priv(expr);
nf_tables_destroy_set(ctx, priv->set); // 啥都没干, nf_tables_destroy_set() 也啥都没干
}
void nf_tables_destroy_set(const struct nft_ctx *ctx, struct nft_set *set)
{
if (list_empty(&set->bindings) && nft_set_is_anonymous(set)) // 不满足条件, 因为 set->bindings 包含之前赋值的 expression,所以不会调用 nft_set_destroy() 释放 set 并将 expr 从 set->binding 取出来
nft_set_destroy(ctx, set);
}
UAF write:再次使用 SET_EXPR
功能,则会在已经释放的nft_expr
后面再链接一个nft_expr
,造成偏移0x18的uaf 写。根据list 操作的代码和本次参与运算的结构体,可以看出,该uaf写会篡改偏移为0x18 和偏移为0x20的两个字段指向另外两个堆地址。我们这里主要关注偏移0x18,会将其指向一个新的nft_expr
(kmalloc-64
)的偏移0x18处。
#define list_add_tail_rcu list_add_tail
static inline void list_add_tail(struct list_head *new, struct list_head *head)
{
__list_add(new, head->prev, head);
}
static inline void __list_add(struct list_head *new,
struct list_head *prev,
struct list_head *next)
{
if (!__list_add_valid(new, prev, next))
return;
next->prev = new;
new->next = next;
new->prev = prev;
WRITE_ONCE(prev->next, new);
}
2. 漏洞利用
问题:参见 nft_expr_init()。v5.18.1
版本中漏洞对象是采用 GFP_KERNEL_ACCOUNT
标识分配的,v5.17.12
版本中漏洞对象是采用 GFP_KERNEL
标识分配的。 msg_msg
是采用 GFP_KERNEL_ACCOUNT
标识分配的(单独存放在 kmalloc-cg-xx
slab 中)。所以 v5.17.12
版本不能使用 msg_msg
对象来利用。并且UAF写会在 kmalloc-64
的偏移 0x18 处写一个堆地址,写偏移和内容不可控。
// v5.18.1
// https://elixir.bootlin.com/linux/v5.18.1/source/net/netfilter/nf_tables_api.c#L2879
expr = kzalloc(expr_info.ops->size, GFP_KERNEL_ACCOUNT);
// v5.17.12
// https://elixir.bootlin.com/linux/v5.17.12/source/net/netfilter/nf_tables_api.c#L2800
expr = kzalloc(expr_info.ops->size, GFP_KERNEL);
2-1. 泄露堆地址-user_key_payload
方法:使用 user_key_payload
来泄露(采用 GFP_KERNEL
申请),data字段用户可控。且data数据偏移正好是0x18,所以UAF写会正好往data数据上写上一个堆地址(恰好写入到 user_key_payload->data[0:8]
),可以泄露出来。
struct user_key_payload {
struct rcu_head rcu; /* RCU destructor */
unsigned short datalen; /* length of this data */
char data[] __aligned(__alignof__(u64)); /* 变长数据区,用户可控数据 */
};
int user_preparse(struct key_preparsed_payload *prep)
{
struct user_key_payload *upayload;
size_t datalen = prep->datalen;
...
upayload = kmalloc(sizeof(*upayload) + datalen, GFP_KERNEL);
if (!upayload)
return -ENOMEM;
...
}
2-2. 泄露内核基址-posix_msg_tree_node
2-2-1. 构造越界读
方法:采用mqueue 的posix消息队列模块,该模块和msg_msg一样是IPC进程间通信的消息队列功能。mqueue 的消息采用 posix_msg_tree_node 结构来存储,采用 GFP_KERNEL
flag 分配。
步骤:利用UAF写篡改 posix_msg_tree_node->msg_list
(msg_msg
链表),指向某个 kmalloc-64
的偏移 0x18 处(某个 nft_expr
对象的0x18偏移处),这样该位置就被当作一个 msg_msg
结构;然后利用 mq_timedreceive
调用来读取消息,就能读到相邻堆块偏移8开始的16字节(0x18+0x30 = 0x48
,从偏移0x18开始,0x30的字节被当做 msg_msg
头,所以可读取的data从相邻堆块的第8字节开始)。因为 copy_to_user()
中有 heap_check,会检查拷贝大小是否超出内存所在slab的大小,所以这里只能读取0x10字节。
struct posix_msg_tree_node {
struct rb_node rb_node; // rb_node 大小为 0x18
struct list_head msg_list; // 偏移0x18,该字段管理了一个 msg_msg 链表,指向首个 msg_msg
int priority;
};
struct rb_node { // 大小为 0x18
unsigned long __rb_parent_color;
struct rb_node *rb_right;
struct rb_node *rb_left;
} __attribute__((aligned(sizeof(long))));
分配路径:SYSCALL-mq_timedsend -> do_mq_timedsend()
static int do_mq_timedsend(mqd_t mqdes, const char __user *u_msg_ptr,
size_t msg_len, unsigned int msg_prio,
struct timespec64 *ts)
{
struct fd f;
struct inode *inode;
struct ext_wait_queue wait;
struct ext_wait_queue *receiver;
struct msg_msg *msg_ptr;
struct mqueue_inode_info *info;
ktime_t expires, *timeout = NULL;
struct posix_msg_tree_node *new_leaf = NULL;
int ret = 0;
DEFINE_WAKE_Q(wake_q);
...
/* First try to allocate memory, before doing anything with
* existing queues. */
msg_ptr = load_msg(u_msg_ptr, msg_len); // [1] 调用 load_msg() 创建消息队列, 从用户空间拷贝消息
if (IS_ERR(msg_ptr)) {
ret = PTR_ERR(msg_ptr);
goto out_fput;
}
msg_ptr->m_ts = msg_len;
msg_ptr->m_type = msg_prio;
/*
* msg_insert really wants us to have a valid, spare node struct so
* it doesn't have to kmalloc a GFP_ATOMIC allocation, but it will
* fall back to that if necessary.
*/
if (!info->node_cache)
new_leaf = kmalloc(sizeof(*new_leaf), GFP_KERNEL); // [2] 申请 posix_msg_tree_node 对象,采用 GFP_KERNEL flag 分配
spin_lock(&info->lock);
if (!info->node_cache && new_leaf) {
/* Save our speculative allocation into the cache */
INIT_LIST_HEAD(&new_leaf->msg_list);
info->node_cache = new_leaf; // [3] 将申请的 posix_msg_tree_node 对象存入 mqueue_inode_info 中
new_leaf = NULL;
} else {
kfree(new_leaf);
}
if (info->attr.mq_curmsgs == info->attr.mq_maxmsg) {
...
} else {
receiver = wq_get_first_waiter(info, RECV);
if (receiver) {
pipelined_send(&wake_q, info, msg_ptr, receiver);
} else {
/* adds message to the queue */
ret = msg_insert(msg_ptr, info); // [4] msg_insert() 将消息插入消息队列
if (ret)
goto out_unlock;
__do_notify(info);
}
inode->i_atime = inode->i_mtime = inode->i_ctime =
current_time(inode);
}
...
}
static int msg_insert(struct msg_msg *msg, struct mqueue_inode_info *info)
{
struct rb_node **p, *parent = NULL;
struct posix_msg_tree_node *leaf;
bool rightmost = true;
··· ···
··· ···
insert_msg:
info->attr.mq_curmsgs++;
info->qsize += msg->m_ts;
list_add_tail(&msg->m_list, &leaf->msg_list); // [4-1] 将 msg_msg 消息添加到 posix_msg_tree_node->msg_list 链表
return 0;
}
读取路径:SYSCALL-mq_timedreceive -> do_mq_timedreceive()
static int do_mq_timedreceive(mqd_t mqdes, char __user *u_msg_ptr,
size_t msg_len, unsigned int __user *u_msg_prio,
struct timespec64 *ts)
{
ssize_t ret;
struct msg_msg *msg_ptr;
struct fd f;
struct inode *inode;
struct mqueue_inode_info *info;
struct ext_wait_queue wait;
ktime_t expires, *timeout = NULL;
struct posix_msg_tree_node *new_leaf = NULL;
...
inode = file_inode(f.file);
if (unlikely(f.file->f_op != &mqueue_file_operations)) {
ret = -EBADF;
goto out_fput;
}
info = MQUEUE_I(inode); // [1] 先根据消息队列的文件描述符获取对应inode, 从 inode 中获取 mqueue_inode_info
audit_file(f.file);
...
if (!info->node_cache)
new_leaf = kmalloc(sizeof(*new_leaf), GFP_KERNEL);
spin_lock(&info->lock);
if (!info->node_cache && new_leaf) {
/* Save our speculative allocation into the cache */
INIT_LIST_HEAD(&new_leaf->msg_list);
info->node_cache = new_leaf; // [2] 获取 posix_msg_tree_node
} else {
kfree(new_leaf);
}
if (info->attr.mq_curmsgs == 0) {
...
} else { // 若消息队列消息数量不为0
DEFINE_WAKE_Q(wake_q);
msg_ptr = msg_get(info); // [3] 从消息队列获取一个消息
...
}
if (ret == 0) {
ret = msg_ptr->m_ts;
if ((u_msg_prio && put_user(msg_ptr->m_type, u_msg_prio)) ||
store_msg(u_msg_ptr, msg_ptr, msg_ptr->m_ts)) { // [4] 将消息发给用户, copy_to_user()
ret = -EFAULT;
}
free_msg(msg_ptr); // [5] 释放消息
}
...
}
static inline struct msg_msg *msg_get(struct mqueue_inode_info *info)
{
...
} else {
msg = list_first_entry(&leaf->msg_list, // [3-1] 获取msg_list中第一个消息
struct msg_msg, m_list);
list_del(&msg->m_list); // [3-2] 从消息队列中删除
if (list_empty(&leaf->msg_list)) {
msg_tree_erase(leaf, info);
}
}
info->attr.mq_curmsgs--; // [3-3] 消息队列数减1
info->qsize -= msg->m_ts;
return msg;
}
void free_msg(struct msg_msg *msg)
{
struct msg_msgseg *seg;
security_msg_msg_free(msg);
seg = msg->next;
kfree(msg);
while (seg != NULL) {
struct msg_msgseg *tmp = seg->next;
cond_resched();
kfree(seg);
seg = tmp;
}
}
void security_msg_msg_free(struct msg_msg *msg)
{
call_void_hook(msg_msg_free_security, msg);
kfree(msg->security); // [5-1] 伪造 msg_msg 时, 需保证 msg->security 为0
msg->security = NULL;
}
2-2-2. 泄露目标对象
泄露对象:需找到一个目标对象,放在 posix_msg_tree_node 对象的后面。
- (1)目标对象需位于
kmalloc-64
,且偏移0x8~0x18
处包含内核地址指针; - (2)由于
mq_timedreceive
调用会触发执行msg_free
来释放msg_msg->security
(恰好与目标对象的前8字节重合),所以目标对象的前8字节需为0
最终还是选择 user_key_payload,前8字节 user_key_payload->rcu->next
正常为0,且第2个8字节为 user_key_payload->rcu->func
内核函数指针,指向 user_free_payload_rcu()
函数。
struct user_key_payload {
struct rcu_head rcu; /* RCU destructor */
unsigned short datalen; /* length of this data */
char data[] __aligned(__alignof__(u64)); // 变长数据区,用户可控
};
struct callback_head {
struct callback_head *next;
void (*func)(struct callback_head *head); // 偏移 0x8 可泄露内核基址
} __attribute__((aligned(sizeof(void *))));
#define rcu_head callback_head
步骤:
(1)分配两个漏洞对象
nft_expr
称为vul1
/vul2
;(2)触发
do_mq_timedsend()
分配一个posix_msg_tree_node
来占据vul1
;(3)触发UAF写漏洞(创建
vul2
并链接到vul1
,vul1->binding->next = vul2->binding
),通过vul1
篡改posix_msg_tree_node
;(4)通过
keyctl
函数在vul2
附近布置user_key_payload
对象。vul 1 (posix_msg_tree_node) vul 2 ------------------------------- ------------------------------------------------------------------------- 0x0 | rb color | rb_right | | * ops | * set | --------------- --------------- --------------------------------- --------------------------------------- 0x10 | rb_left | (vul 2+0x18) | | reg & invert | bingding.list.next (msg_msg->m_list) | --------------- --------------- --------------------------------- --------------------------------------- 0x20 | .... | .... | | X.X.prev (msg_msg->m_list) | chain (msg_msg->m_type) | --------------- --------------- --------------------------------- --------------------------------------- 0x30 | .... | .... | | flags (msg_msg->m_ts) | (msg_msg->next) | --------------- --------------- --------------------------------- --------------------------------------- user_key_payload ------------------------------------------------------------------------- 0x40 | rcu->next (msg_msg->security)| rcu->func ptr (msg_msg->data[0]) | --------------------------------- --------------------------------------- 0x50 | data_len | data[0] | --------------------------------- --------------------------------------- 0x60 | data[1] | data[2] | --------------------------------- --------------------------------------- 0x70 | data[3] | data[4] | --------------------------------- --------------------------------------- vul 1 overlap info: struct nft_expr == struct posix_msg_tree_node vul 2: struct nft_expr (nft_expr+0x18 开始被当做 msg_msg 结构) next to vul 2: struct user_key_payload (前8字节被当做 msg_msg->security)
此时,
posix_msg_tree_node->msg_list->next = vul2 + 0x18
,所以从vul2 + 0x18
开始被当做msg_msg
;但是这个msg_msg
的copy_to_user
只能泄露0x10字节,且msg_msg->security
必须为NULL(vul2
相邻对象的前8字节),所以能泄露vul2
相邻堆块的0x8 ~ 0x18
字节。过去有人用 percpu_ref_data 来泄露内核基址(参见 CVE-2022-34918利用,来自io_uring
),但不满足前8字节为NULL,所以只能再用 user_key_payload 来泄露。
2-3. 覆写 modprobe_path
提权
步骤:构造 msg_msg
的Unlink来篡改 modprobe_path
提权。
(1)利用UAF篡改
posix_msg_tree_node->msg_list
指向某个vul2 + 0x18
处;vul 1 vul 2 ------------------------------- ------------------------------- 0x0 | rb color | rb_right | | rcu->next | rcu->func ptr | --------------- --------------- --------------- --------------- 0x10 | rb_left | (vul 2+0x18) | | data_len | data[0] | --------------- --------------- --------------- --------------- 0x20 | .... | .... | | data[1] | data[2] | --------------- --------------- --------------- --------------- 0x30 | .... | .... | | data[3] | data[4] | --------------- --------------- --------------- --------------- vul 1 overlap info: struct nft_expr == struct posix_msg_tree_node vul 2 overlap info: struct nft_expr == struct user_key_payload == fake msg_msg data[0] = m_list->next / data[1] = m_list->prev / data[2] = m_type / data[3] = m_ts
(2)释放该
nft_expr - vul2
,喷射usr_key_payload
伪造一个假的msg_msg
,msg_msg->m_list.next = &modprobe_path-7
,msg_msg->mlist.prev = 0xffff????2f706d74
;&modprobe_path-7
表示修改modprobe_path
的第 2~9 字节;0xffff????2f706d74
是 physmap 中可写的堆地址(进行 unlink 时必须为可写地址),前面已经泄露过该地址。可以将modprobe_path
字符串从/sbin/modprobe
改为/tmp/????\xff\xffprobe
;
unlink操作:执行
mq_timedreceive
调用,触发执行 msg_get() -> list_del() -> __list_del_entry() -> __list_del() 对msg_msg
对象进行unlink。static inline void list_del(struct list_head *entry) { __list_del_entry(entry); entry->next = LIST_POISON1; entry->prev = LIST_POISON2; } static inline void __list_del_entry(struct list_head *entry) { if (!__list_del_entry_valid(entry)) return; __list_del(entry->prev, entry->next); } static inline void __list_del(struct list_head * prev, struct list_head * next) { next->prev = prev; // <---------- *(entry->next + 8) = entry->prev unlink操作 WRITE_ONCE(prev->next, next); // *(entry->prev) = entry->next }
2-4. 测试结果
问题:每次start.sh
启动后执行 ./exploit
总是报错 -sh: 2: ./exploit: Exec format error
,只能重新传 ./exploit
进去才能执行。scp -P 10021 ./exploit.c ./exploit hi@localhost:/home/hi
3. 补充
3-1. 防护机制
- 对特定对象的cache隔离,防止UAF对象替换:grsecurity’s autoslab / Google’s experimental mitigations;
- CFI 机制防止执行 ROP gadget,目前在x64上没有有效的机制;
- 设置
panic_on_oops=1
来避免 unlink 利用; - 设置
modprobe_path
为只读—CONFIG_STATIC_USERMODHELPER
; - 关闭
unprivileged namespaces
; - 关闭
userland FUSE server
支持。
3-2. 常用命令
exp编译:
$ sudo apt-get install libcap2-bin bzip2 make pkg-config # 安装 setcap/bzip2/make/pkg-config
$ tar -jxvf xx.tar.bz2
$ ./configure --prefix=/usr && make # libmnl / libnftl
$ sudo make install
liburing 安装(本次exp不需要安装liburing)
# 安装 liburing 生成 liburing.a / liburing.so.2.2
$ apt-get install zip g++
$ apt-get update
$ apt-get install gcc-10 g++-10 # 在QEMU中编译liburing总是出错, 可能需要更新 gcc
$ git clone https://github.com/axboe/liburing
$ unzip ./liburing-master.zip
$ cd ./liburing-master
$ make
$ sudo make install
# 另一种方法
wget https://github.com/axboe/liburing/archive/liburing-0.2.zip
unzip liburing-0.2.zip
cd liburing-liburing-0.2/
./configure --libdir=/usr/lib64
make CFLAGS=-std=gnu99 && make install
常用命令:
# ssh连接与测试
$ ssh -p 10021 hi@localhost # password: lol
$ ./exploit
# 编译exp 注意libmnl不支持静态编译,加 -static 就会报错; 加 -lrt 表示实时库
$ gcc ./exploit.c -lmnl -lnftnl -lrt -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
参考
Linux Kernel Exploit (CVE-2022-32250) with mqueue
Settlers of Netlink - Exploiting a limited kernel UAF on Ubuntu 22.04 — 学习使用 CodeQL搜寻目标对象(指定大小,指定偏移处为指针)。介绍了另一种利用方法,基于nft_dynset
漏洞对象,比本文更复杂,要触发4次UAF(用到 cgroup_fs_context->release_agent
来构造任意释放,恰好和 nft_dynset->bindings->prev
重叠)。
[漏洞分析] CVE-2022-32250 netfilter UAF内核提权
nftables-details — 学习nftables的细节
团队介绍:该团队成功利用了 CVE-2022-0185 / CVE-2022-0995 / CVE-2022-32250
文档信息
- 本文作者:bsauce
- 本文链接:https://bsauce.github.io/2022/11/03/CVE-2022-32250/
- 版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)