[漏洞分析] CVE-2022-32250 netfilter UAF内核提权
文章目录
漏洞简介
漏洞编号: CVE-2022-32250
漏洞产品: linux kernel - netfilter
影响范围: ~ linux kernel 5.19
利用条件: CAP_NET_ADMIN
利用效果: 本地提权
环境搭建
调试只需要 CONFIG_NF_TABLES=y就行了
但exp中使用了NFT_SET_EXPR,必须要使用ubuntu21.04 以上的版本的libmnl 或 libnftnl才行。
利用效果:
漏洞原理
漏洞触发
漏洞发生在netfilter 模块的NFT_MSG_NEWSET 功能中,在特定报文(有特定成员的结构体)处理上会出现UAF问题。
按顺序分析,首先来看NFT_MSG_NEWSET 的入口函数:
net\netfilter\nf_tables_api.c : 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;
if (nla[NFTA_SET_TABLE] == NULL || //[1]一些先决条件
nla[NFTA_SET_NAME] == NULL ||
nla[NFTA_SET_KEY_LEN] == NULL ||
nla[NFTA_SET_ID] == NULL)
return -EINVAL;
··· ···
··· ···//一顿处理
set = nft_set_lookup(table, nla[NFTA_SET_NAME], genmask);//[2]寻找已经存在的set
if (IS_ERR(set)) {//一般是找不到,直接跳过这里,下面初始化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);//[3]准备初始化set
··· ···
INIT_LIST_HEAD(&set->bindings);//初始化set,注意这里的bindings字段
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 情况下的处理
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++;
}
··· ···
··· ···
}
[1] 首先是一些需要注意的字段,都要设置了。
[2] 如果已经建立了set,则查找已经存在的并返回,但这里第一次是找不到的,会走到下面进行初始化set。
[3] 申请空间&初始化set 的各个部分,注意这里的bindings 成员,是一个列表结构,在后面看具体信息。
[4] 如果设置了NFTA_SET_EXPR 字段,则进入到NFTA_SET_EXPR 的处理函数nft_set_elem_expr_alloc:
net\netfilter\nf_tables_api.c : 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); //[1]初始化expr
if (IS_ERR(expr))
return expr;
err = -EOPNOTSUPP;
if (!(expr->ops->type->flags & NFT_EXPR_STATEFUL))
goto err_set_elem_expr;//[2]如果不存在NFT_EXPR_STATEFUL flag,则失败,销毁刚初始化的expr
··· ···
err_set_elem_expr:
nft_expr_destroy(ctx, expr);//销毁expr 函数
return ERR_PTR(err);
}
[1] 首先进行expr 的初始化调用nft_expr_init 函数,下文分析
[2] 然后如果expr 没有NFT_EXPR_STATEFUL flag的话,则会被销毁,调用nft_expr_destroy,下文分析。
先分析初始化expr 的nft_expr_init 函数:
net\netfilter\nf_tables_api.c : nft_expr_init
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);//初始化expr_info
if (err < 0)
goto err1;
err = -ENOMEM;
expr = kzalloc(expr_info.ops->size, GFP_KERNEL);//申请空间 8+私有结构体长度,在该次利用是56
if (expr == NULL)
goto err2;
err = nf_tables_newexpr(ctx, &expr_info, expr);//初始化expr
if (err < 0)
goto err3;
return expr;
··· ···
··· ···
}
申请的大小是56,实际申请的属于kmalloc-64,之后相当于直接调用了nf_tables_newexpr 进行struct nft_expr
结构体的初始化:
net\netfilter\nf_tables_api.c : nf_tables_newexpr
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;
}
··· ···
}
实际收到影响的expr 只有look_up 和dynset 两个,分别位于net\netfilter\nft_lookup.c 和 net\netfilter\nft_dynset.c(其实是结构体中带有binding字段的),这里以look_up为例:
net\netfilter\nft_lookup.c : 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);//获取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);//找到之前创建的set
··· ···//各种初始化
··· ···
priv->binding.flags = set->flags & NFT_SET_MAP;
err = nf_tables_bind_set(ctx, set, &priv->binding);//调用nf_tables_bind_set进行绑定
if (err < 0)
return err;
priv->set = set;
return 0;
}
先找到expr 结构体中的私有数据指针,对于lookup来说,私有结构是struct nft_lookup
。然后找到lookup 报文中对应的搜索set,这里我们设置成我们刚刚创建的set,一顿初始化之后,最后调用nf_tables_bind_set 将lookup 结构和set 绑定到一起:
net\netfilter\nf_tables_api.c : nf_tables_bind_set
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 链接链表
nft_set_trans_bind(ctx, set);
set->use++;
return 0;
}
nf_tables_bind_set 中主要是调用list_add_tail_rcu 函数将nft_set->bindings 和 nft_lookup->binding->list 用双向链表链接起来。也就是说,是将下面两个结构体的binding(s)字段通过双向链表相连:
struct nft_set {
struct list_head list;
struct list_head bindings;//列表
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;//列表
};
struct nft_set_binding {
struct list_head list;
const struct nft_chain *chain;
u32 flags;
};
整个过程没什么问题,但回看申请expr的函数:
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); //[1]初始化expr
if (IS_ERR(expr))
return expr;
err = -EOPNOTSUPP;
if (!(expr->ops->type->flags & NFT_EXPR_STATEFUL))
goto err_set_elem_expr;//[2]如果不存在NFT_EXPR_STATEFUL flag,则失败,销毁刚初始化的expr
··· ···
err_set_elem_expr:
nft_expr_destroy(ctx, expr);//销毁expr 函数
return ERR_PTR(err);
}
在[1] 中完成了空间分配、链接到set 等操作,但如果在[2]中不满足,则会调用nft_expr_destroy 去销毁这个expr:
net\netfilter\nf_tables_api.c & net\netfilter\nft_lookup.c
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);//基本什么也没干,调用这个函数也没啥可干的
}
void nf_tables_destroy_set(const struct nft_ctx *ctx, struct nft_set *set)
{
if (list_empty(&set->bindings) && nft_set_is_anonymous(set))//不满足条件
nft_set_destroy(ctx, set);
}
可以看到整个destroy 调用栈除了free 了expr 结构体之外就没干啥事。最主要的是忘记将expr 从set 的双向链表中卸下来了,导致后面的uaf。
UAF写
如果再次使用SET_EXPR功能,则会在已经释放的堆块后面再链接一个堆块,造成偏移0x18的uaf 写:
#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);
}
根据list 操作的代码和本次参与运算的结构体,可以看出,该uaf写实篡改偏移为0x18 和偏移为0x20的两个字段指向另外两个堆地址。我们这里主要关注偏移0x18,会将其指向一个新的expr(kmalloc-64)的偏移0x18处。
漏洞利用
限制
首先漏洞所在的堆是用GFP_KERNEL 申请的,与常用的堆利用原语如msg_msg等(使用GFP_KERNEL_ACCOUNT
申请)不是在同slab中。
expr = kzalloc(expr_info.ops->size, GFP_KERNEL);//申请空间
其次,uaf 写的限制比较明显,在0x18的地方写一个堆地址,写的偏移和内容我们不可控。
然后,漏洞所在结构体属于kmalloc-64
泄露堆地址
由于不能使用msg_msg,这里采取的是使用usr_key_payload来利用,user_key_payload 同样是可以自定义大小的内核结构体,但是用GFP_KERNEL申请,可以跟漏洞结构体申请到同slab。并且data字段是用户可控内容
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;
··· ···
}
而且user_key_payload 的data 数据偏移正好是0x18,也就是说如果我们在上面expr 结构体释放之后使用usr_key_payload 占领空位,然后使用uaf ,则会改变data数据段,那么我们读取该key 就可以读到一个堆地址(用来干什么后文描述)。
泄露内核地址
posix 消息队列
泄露linux内核地址这里采用的是mqueue 的posix消息队列模块,该模块和msg_msg一样是IPC进程间通信的消息队列功能。我们这里使用的posix_msg_tree_node结构体内容如下:
struct posix_msg_tree_node {
struct rb_node rb_node;
struct list_head msg_list;//偏移0x18,该字段管理了一个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))));
该结构体的初始化与使用主要是在do_mq_timedsend函数中:
ipc\mqueue.c : do_mq_timedsend
//[1]属于mq_timedsend系统调用
SYSCALL_DEFINE5(mq_timedsend, mqd_t, mqdes, const char __user *, u_msg_ptr,
size_t, msg_len, unsigned int, msg_prio,
const struct __kernel_timespec __user *, u_abs_timeout)
{
struct timespec64 ts, *p = NULL;
if (u_abs_timeout) {
int res = prepare_timeout(u_abs_timeout, &ts);
if (res)
return res;
p = &ts;
}
return do_mq_timedsend(mqdes, u_msg_ptr, msg_len, msg_prio, p);
}
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);//[2] 从用户空间获得消息
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);//[3]申请posix_msg_tree_node结构体
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;//将申请的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]将消息插入消息队列
if (ret)
goto out_unlock;
__do_notify(info);
}
inode->i_atime = inode->i_mtime = inode->i_ctime =
current_time(inode);
}
··· ···
}
[1] 该操作的主要流程比较简单,属于mq_timedsend 系统调用,并且主要逻辑发生在do_mq_timedsend 函数之中
[2] 首先该系统调用会创建一个消息队列,消息和msg_msg 一样,这里调用load_msg 函数获取用户构造的消息,关于load_msg 可以查看[kernel exploit] 消息队列msg系列在内核漏洞利用中的应用文章
[3] 然后会为struct posix_msg_tree_node
结构体申请空间,使用GFP_KERNEL
flag,这样可以和漏洞结构所在同一个slab,并且大小相同。之后会将申请的struct posix_msg_tree_node
结构体存入mqueue_inode_info中,mqueue_inode_info会记录在inode中用于后续查找
[4] 最后调用msg_insert 函数将消息添加到消息队列:
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); //将消息添加到msg_list
return 0;
}
在msg_insert 函数中将用户传入的msg_msg 添加到posix_msg_tree_node->msg_list 链表。
可以使用do_mq_timedreceive 函数读取posix 消息队列中的消息:
SYSCALL_DEFINE5(mq_timedreceive, mqd_t, mqdes, char __user *, u_msg_ptr,//[1]属于mq_timedreceive系统调用
size_t, msg_len, unsigned int __user *, u_msg_prio,
const struct __kernel_timespec __user *, u_abs_timeout)
{
··· ···
return do_mq_timedreceive(mqdes, u_msg_ptr, msg_len, u_msg_prio, p);
}
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);//[2]从inode中获取mqueue_inode_info
audit_file(f.file);
··· ···
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;//获取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]将消息发送到用户层
ret = -EFAULT;
}
free_msg(msg_ptr);//[5]释放消息
}
out_fput:
fdput(f);
out:
return ret;
}
[1] 从posix消息队列接收消息属于mq_timedreceive系统调用
[2] 首先根据消息队列的文件描述符获取对应inode再获取struct posix_msg_tree_node
结构
[3] 消息队列中消息数量不为0,则获取第一个消息出来:
static inline struct msg_msg *msg_get(struct mqueue_inode_info *info)
{
··· ···
} else {
msg = list_first_entry(&leaf->msg_list,//获取msg_list中第一个消息
struct msg_msg, m_list);
list_del(&msg->m_list);//然后从消息队列中删除
if (list_empty(&leaf->msg_list)) {
msg_tree_erase(leaf, info);
}
}
info->attr.mq_curmsgs--;//消息队列数量减少
info->qsize -= msg->m_ts;
return msg;
}
[4] 调用store_msg 会将消息使用copy_to_user发送给用户层,具体参考[kernel exploit] 消息队列msg系列在内核漏洞利用中的应用文章
[5] 调用free_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);
msg->security = NULL;
}
这里会释放msg->security 字段,所以非法释放的时候必须要保证msg->security 为0。
泄露
也就是说,我们uaf写如果写到struct posix_msg_tree_node
的偏移0x18处,会改写msg_list ,而msg_list 是msg_msg链表,会改写它指向一个kmalloc-64的偏移0x18处。所以我们使用如下堆布局:
-
首先申请一个look_up的
struct nft_expr
结构,并对其进行UAF,先free -
使用
struct posix_msg_tree_node
作为被uaf目标,占领刚free的堆地址 -
开始UAF,会将posix_msg_tree_node->msg_list字段改写指向下一个
struct nft_expr
的偏移0x18处 -
而msg_list 字段原本是指向一个
struct msg_msg
结构体,所以会将下一个struct nft_expr
的偏移0x18处开始认为成一个msg_msg。 -
利用mq_timedreceive 读取消息,就可以读到下一个堆块的第二个字段16个字节。这是由于copy_to_user 中有heap_check,会检查拷贝大小是否超出内存所在slab 的大小,所以这里我们最多就读0x10字节。
所以这里的坑是:
- 由于copy_to_user 的限制,只能读到下一个堆的8字节偏移开始0x10字节长度,所以需要选择第二第三个字段有内核地址指针的结构,并且属于kmalloc-64
- 在mq_timedreceive 最后还会调用msg_free释放msg_msg结构,而msg_free 中会释放msg_msg->security 指针,必须要保证第一个字段为0 才行,否则会崩溃
所以这里还是选择user_key_payload:
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);
} __attribute__((aligned(sizeof(void *))));
#define rcu_head callback_head
user_key_payload 前0x10是struct callback_head,他的第一个字段是next指针,正常情况下就是0,满足msg_free 释放msg_msg->security 指针的绕过条件,并且第二个字段是一个函数指针func,指向user_free_payload_rcu函数。正好可以泄露user_free_payload_rcu 的地址计算出kernel 的基地址。
改写modprobe_path
接下来利用unlink 来复写modprobe_path,使用如下方式:
- 构造跟上一段结尾相同的堆布局的堆布局,用posix_msg_tree_node来uaf并篡改msg_list 指向下一个nft_expr
- 然后释放该expr,用usr_key_payload 占领,data段正好覆盖该expr 相对于msg_msg的list 头部分
- 使用usr_key_payload 的data段覆盖msg_msg 的mlist.next与mlist.prev为&modprobe_path-7 和 0xffff???2f706d74
- &modprobe_path-7 就是modprobe_path 的地址减7,这样后面unlink就可以篡改modprobe_path 的第二字节到第九字节这8个字节
- 0xffff???2f706d74 是一个堆地址,其中后四字节是"tmp/“字符,前面0xffff???是堆地址的范围,其中问号表示地址随机的部分,之前我们已经泄露过堆地址了,所以是知道问号部分的。之所以覆盖为这个,为了将modprobe_path 的第二字节到第九字节 篡改为0xffff???2f706d74,这样它就可以变成字符串:”/tmp/???\xff\xffprobe"。并且unlink 利用的限制0xffff???2f706d74 也是一个可以被写入的地址才行,而这属于堆地址空间,可以被写入。
- 然后使用mq_timedreceive 接收消息之后的msg_get函数中的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;//unlink 写
WRITE_ONCE(prev->next, next);//unlink 写
}
参考
https://blog.theori.io/research/CVE-2022-32250-linux-kernel-lpe-2022/
https://www.openwall.com/lists/oss-security/2022/05/31/1
https://github.com/theori-io/CVE-2022-32250-exploit