Arthur Mongodin 13 min

This year, I have done my end-of-study internship at Randorisec, the original purpose of my internship was to study different ways of doing variant analysis on open-sources project but also on closed-source software solutions. The Linux kernel is an attractive target, and so it was chosen as the target of my project. I started scouting for publicly disclosed bugs which could lead to interesting variant patterns. Since the beginning of the year, several bugs have been found and fixed within the kernel component netfilter, such has CVE 2022-1015. In this article, I am going to talk about two bugs I’ve found inside the netfilter kernel section. One of them lead to information leakage, and potentially to a fully functional local privilege escalation (LPE) exploit.

A brief introduction to netfilter

Netfilter is an open-source project that is used to perform packet filtering, aka the linux firewall.

This project is often mentioned iptables which is the userland application used to configure your firewall. In 2014, a new subsystem was added to the netfilter firewall, called nftable, which is now configured through the nftables userland application.

Schema of the netfilter organization

The core logic of netfilter is implemented within the kernel (/net/netfilter), and the userland tools communicate with it via netlink messages.

A VERY basic usage

The main purpose of netfilter is the table object. This object will contain all the information about a filter. The following nftables command creates a new table that is named my-table that could be used as a filter on the IP protocol.

nft> add table ip my-table

A table can contain different object such as sets, that is used to store data. The following command create a new set associated to the table my-table named my-set that stores IPv4 addresses.

nft> add set ip my-table my-set {type: ipv4_addr;}

Finally, we can create chains of rules that would be applied on the received packet.

Starting point

On April 2022, David Bouman released an article about vulnerabilities within the Linux kernel. I focused my interest on the CVE-2022-1015, as it is the target of my variant analysis. This vulnerability is very interesting because some of the out-of-bound vulnerabilities happen because the user input checks are not well implemented. Moreover, this kind of bugs can be found both in the Linux kernel and in userland applications. The bug associated to this vulnerability is an integer overflow located into the function nft_validate_register_load (/net/netfilter/nf_tables_api.c) at (0).

static int nft_validate_register_load(enum nft_registers reg, unsigned int len)
{
    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))           <===== (0)
        return -ERANGE;

    return 0;
}

After reading and understanding how this vulnerability works, I decided to write a CodeQL query able to find such a vulnerability. The analysis of the different results of the query led to several candidates for CVE-2022-1015 variant.

CVE-2022-1015 variant

The bug

One of these candidates hold my attention, it is located inside the function nft_set_desc_concat_parse (/net/netfilter/nf_tables_api.c).

static int nft_set_desc_concat_parse(const struct nlattr *attr,
                     struct nft_set_desc *desc)
{
    struct nlattr *tb[NFTA_SET_FIELD_MAX + 1];
    u32 len;
    int err;

    err = nla_parse_nested_deprecated(tb, NFTA_SET_FIELD_MAX, attr,
                      nft_concat_policy, NULL);
    if (err < 0)
        return err;

    if (!tb[NFTA_SET_FIELD_LEN])
        return -EINVAL;

    len = ntohl(nla_get_be32(tb[NFTA_SET_FIELD_LEN]));

    if (len * BITS_PER_BYTE / 32 > NFT_REG32_COUNT)             <===== (1)
        return -E2BIG;

    desc->field_len[desc->field_count++] = len;                 <===== (2)

    return 0;
}

At (1), we discern the pattern which matched the query.

Let’s focus on the left part of the comparison len * BITS_PER_BYTE / 32. len is a 32 bits integer extracted from a netlink message with nla_get_be32, so we can assume that this is user controlable. The macro BITS_PER_BYTE is defined in /include/linux/bits.h.

#define BITS_PER_BYTE       8

The multiplication len * BITS_PER_BYTE will result in left shift of three bits. The next arithmetic operation is the division with 32, which will result in a right shift of five bits. We understand that five bits of len are ignored, the three most significant and the two less significant.

Now, let’s have look to the right side of the comparison. The macro NFT_REG32_COUNT is defined in the file /include/uapi/linux/netfilter/nf_tables.h.

enum nft_registers {

    ...

    NFT_REG32_00    = 8,
    NFT_REG32_01,
    NFT_REG32_02,
    NFT_REG32_03,
    NFT_REG32_04,
    NFT_REG32_05,
    NFT_REG32_06,
    NFT_REG32_07,
    NFT_REG32_08,
    NFT_REG32_09,
    NFT_REG32_10,
    NFT_REG32_11,
    NFT_REG32_12,
    NFT_REG32_13,
    NFT_REG32_14,
    NFT_REG32_15,
};
...
#define NFT_REG32_COUNT (NFT_REG32_15 - NFT_REG32_00 + 1)

We have NFT_REG32_COUNT equal to 23 - 8 + 1 = 16.

If we invert the condition at (1) in order to get a condition on len, we get len < 64. Nevertheless, in this comparison five bits are ignored, so this five bits could be used to store an unwanted value at (2).

The next step is to find a way for a random unprivileged user to reach this buggy code.

Unprivileged access

The kernel netfilter module can be accessed from a netlink defined in /net/netfilter/nfnetlink.c.

static int __net_init nfnetlink_net_init(struct net *net)
{
    struct nfnl_net *nfnlnet = nfnl_pernet(net);
    struct netlink_kernel_cfg cfg = {
        .groups = NFNLGRP_MAX,
        .input  = nfnetlink_rcv,    <===== (3)
#ifdef CONFIG_MODULES
        .bind   = nfnetlink_bind,
#endif
    };

    nfnlnet->nfnl = netlink_kernel_create(net, NETLINK_NETFILTER, &cfg);
    if (!nfnlnet->nfnl)
        return -ENOMEM;
    return 0;
}

The input field of the structure netlink_kernel_cfg at (3) gives us the function that will be called when a message is received via the netfilter netlink. Let’s have a look to this handler.

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)) {                         <===== (4)
        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);
}

We see a capability check at (4) done by the function netlink_net_capable, it’s mandatory that the user have the CAP_NET_ADMIN capabilities to use netfilter. Luckily for us, this capability could be acquired with user namespaces (CONFIG_USER_NS). User namespace have been a game changer those past few years in Linux kernel exploitation, they’ve opened a new set of attack surfaces. This is kind of funny, because they are also used to sandbox applications, a good example of double-edged sword.

Let’s get back to our topic, we can map the call path from nfnetlink_rcv to nft_set_desc_concat_parse as the following.

nfnetlink_rcv
    nfnetlink_rcv_skb_batch
        nfnetlink_rcv_batch
            nf_tables_newset
                nf_tables_set_desc_parse
                    nft_set_desc_concat
                        nft_set_desc_concat_parse

There is not any relevant information within the intermediate functions, so we are going to ignore them for the moment.

If the kernel is compiled with CONFIG_USER_NS enabled, we will be able to reach our vulnerable code path.

Integer overflow verification into GDB

Well, what can we do?

Sadly, it seems that this bug didn’t have any impact on the linux security.

Indeed, the assignment at (2) is hiding a cast from 32 bits unsigned integer into a 8 bits one, because the field field_len of the nft_set_desc structure is of type u8.

struct nft_set_desc {
    unsigned int        klen;
    unsigned int        dlen;
    unsigned int        size;
    u8          field_len[NFT_REG32_COUNT];
    u8          field_count;
    bool            expr;
};

It reduces a lot the power of this overflow as the two less significant bits can’t be used.

A strange neighbor

However, this is not the end of the adventure.

When I was looking to exploit the previous bug, something halted me: there was no checks on the index field_count before the assignment at (2).

The parent function of nft_set_desc_concat_parse is nft_set_desc_concat (/net/netfilter/nf_tables_api.c).

static int nft_set_desc_concat(struct nft_set_desc *desc,
                   const struct nlattr *nla)
{
    struct nlattr *attr;
    int rem, err;

    nla_for_each_nested(attr, nla, rem) {                                   <===== (5)
        if (nla_type(attr) != NFTA_LIST_ELEM)
            return -EINVAL;

        err = nft_set_desc_concat_parse(attr, desc);                        <===== (6)
        if (err < 0)
            return err;
    }

    return 0;
}

The call to nft_set_desc_concat_parse is done at (6) but the most interesting thing is that the call is located in a loop that starts at (5), and there is not any verification in the body loop on the value of desc->field_count. nla_for_each_nested will loop over all the netlink attributes provided by the user, if the user provides more than NFT_REG32_COUNT, a buffer overflow will happen.

Gimme gimme gimme an infoleak …

We will see how to turn this buffer overflow into a kernel infoleak. The first byte that will be overflown is the field field_count that stored the number of elements stored into the field field_len. We will use this field to reach our goal.

Netfilter sets

We will use the netfilter sets, represented by the structure nft_set (/include/net/netfilter/nf_tables.h).

struct nft_set {
    struct list_head        list;
    struct list_head        bindings;
    struct nft_table        *table;
    possible_net_t          net;
    char                *name;
    u64             handle;
    u32             ktype;
    u32             dtype;
    u32             objtype;
    u32             size;
    u8              field_len[NFT_REG32_COUNT];
    u8              field_count;
    u32             use;
    atomic_t            nelems;
    u32             ndeact;
    u64             timeout;
    u32             gc_int;
    u16             policy;
    u16             udlen;
    unsigned char           *udata;
    /* runtime data below here */
    const struct nft_set_ops    *ops ____cacheline_aligned;
    u16             flags:14,
                    genmask:2;
    u8              klen;
    u8              dlen;
    u8              num_exprs;
    struct nft_expr         *exprs[NFT_SET_EXPR_MAX];
    struct list_head        catchall_list;
    unsigned char           data[]
        __attribute__((aligned(__alignof__(u64))));
};

This sets are allocated into the function nf_tables_newset (/net/netfilter/nf_tables_api.c) that is a parent of the vulnerable function nft_set_desc_concat_parse.

static int nf_tables_newset(struct sk_buff *skb, const struct nfnl_info *info,
                const struct nlattr * const nla[])
{
    const struct nft_set_ops *ops;
    struct nft_set_desc desc;
    struct nft_set *set;
    size_t alloc_size;
    u64 size;
    ...

    memset(&desc, 0, sizeof(desc));             <===== (7)
    
    ...
    
    if (nla[NFTA_SET_DESC] != NULL) {
        err = nf_tables_set_desc_parse(&desc, nla[NFTA_SET_DESC]);              <===== (8)
        if (err < 0)
            return err;
    }
    
    ...
    
    size = 0;
    if (ops->privsize != NULL)
        size = ops->privsize(nla, &desc);
    alloc_size = sizeof(*set) + size + udlen;
    if (alloc_size < size || alloc_size > INT_MAX)
        return -ENOMEM;
    set = kvzalloc(alloc_size, GFP_KERNEL);                                     <===== (9)
    if (!set)
        return -ENOMEM;

    ...

    set->field_count = desc.field_count;
    for (i = 0; i < desc.field_count; i++)                                      <===== (10)
        set->field_len[i] = desc.field_len[i];
        
    ...
    
}

We retrieve at (8) the call that leads to the vulnerable function. Its first argument is the address of the local variable desc, that is initialized at (7). This is the object that will be modified into the function nft_set_desc_concat_parse. We can see that its fields field_count and field_len are used at (10) and they are stored into the freshly allocated nft_set (9). So, if the out-of-bound has been used to override solely the field field_len of desc with a value bigger than 20 (end of the structure + alignment), some data from the stack will be copied into the nft_set structure at (10). It will also, in this case, produce an out-of-bound into the nft_set structure, but it won’t be useful for our leak.

Back to the userland

The next step is to find a way to send back this information to userland. In order to do so, we will use the function nf_tables_getset (/net/netfilter/nf_tables_api.c) that can be reached in a similar way of nf_tables_newset. nf_tables_getset can be used to retrieve information about a registered set.

tatic int nf_tables_getset(struct sk_buff *skb, const struct nfnl_info *info,
                const struct nlattr * const nla[])
{
    const struct nft_set *set;
    struct sk_buff *skb2;
    struct nft_ctx ctx;
    int err;

    ...
    
    skb2 = alloc_skb(NLMSG_GOODSIZE, GFP_ATOMIC);
    if (skb2 == NULL)
        return -ENOMEM;

    err = nf_tables_fill_set(skb2, &ctx, set, NFT_MSG_NEWSET, 0);           <===== (11)
    if (err < 0)
        goto err_fill_set_info;

    return nfnetlink_unicast(skb2, net, NETLINK_CB(skb).portid);            <===== (12)

}

This function will send data to userland through the netfilter netlink thanks to nfnetlink_unicast at (12) and this data will be setup into the call to nf_tables_fill_set (/net/netfilter/nf_tables_api.c).

static int nf_tables_fill_set(struct sk_buff *skb, const struct nft_ctx *ctx,
                  const struct nft_set *set, u16 event, u16 flags)
{
    struct nlmsghdr *nlh;
    u32 portid = ctx->portid;
    struct nlattr *nest;
    u32 seq = ctx->seq;
    int i;

    event = nfnl_msg_type(NFNL_SUBSYS_NFTABLES, event);
    nlh = nfnl_msg_put(skb, portid, seq, event, flags, ctx->family,
               NFNETLINK_V0, nft_base_seq(ctx->net));
               
    ...
 
    if (set->udata &&
        nla_put(skb, NFTA_SET_USERDATA, set->udlen, set->udata))
        goto nla_put_failure;
    
    ...
 
    if (set->field_count > 1 &&
        nf_tables_fill_set_concat(skb, set))                <===== (13)
        goto nla_put_failure;
        
    ...
    
    nlmsg_end(skb, nlh);
    return 0;
    
nla_put_failure:
    nlmsg_trim(skb, nlh);
    return -1;
}

At (13), an other auxiliary function is used to manage data concerning the fields field_len and field_count of the nft_set structure.

static int nf_tables_fill_set_concat(struct sk_buff *skb,
                     const struct nft_set *set)
{
    struct nlattr *concat, *field;
    int i;

    concat = nla_nest_start_noflag(skb, NFTA_SET_DESC_CONCAT);
    if (!concat)
        return -ENOMEM;

    for (i = 0; i < set->field_count; i++) {                <===== (14)
        field = nla_nest_start_noflag(skb, NFTA_LIST_ELEM);
        if (!field)
            return -ENOMEM;

        if (nla_put_be32(skb, NFTA_SET_FIELD_LEN,           <===== (15)
                 htonl(set->field_len[i])))
            return -ENOMEM;

        nla_nest_end(skb, field);
    }

    nla_nest_end(skb, concat);

    return 0;
}

As we can see at (14), we will iterate set->field_count times, and at each iteration, one element of the set->field_len buffer will be added to the socket buffer at (15). So if the off-by-one has been produced at (2) as explained previously, information stored into the stack will be copied into the heap at (10) and finally, the user will be able to access them thanks to sockets.

A leak example

This leak is the result of my PoC. We can observe that it allows, in these specific circumstances, to leak a stack canary and an address of the data section.

Limitations

We have seen that the out-of-bound can lead to an infoleak, however this leak is not so powerful because it is restricted to 28 bytes due to the organization of the nft_set structure. Indeed, if we want a bigger leak, it requires to overwrite the least significant bytes of the field udata, and it can lead to segmentation fault in the nf_tables_fill_set function. Moreover, the leak is dependent on the stack organization which is itself dependent on the compilation, so this leak can provide interesting information on a specific kernel but could also be useless on the next version.

Remediation

I didn’t manage to turn this out-of-bound into a fully functional LPE exploit before it was publicly disclosed and fixed. It has been fixed on the 31st may 2022 by the commit fecf31ee395b0295f2d7260aa29946b7605f7c85. This commit is fixing both the integer overflow and the out-of-bound. The patch of the integer overflow allows to have a length up to 255, that will had facilitated the exploitation of the vulnerability.

This vulnerability has been officially disclosed on oss-security, and the CVE-2022-1972 has been assigned to this vulnerability.

Say LPE again! I dare you!

The nft_set structure stores several pointers, making it a good place to start. The field udata stores the address at the end of the structure, address of the field data. If some data are stored into the field data, the number of stored bytes into this field is stored in the udlen field. The write primitive allows to overwrite this field, which could lead to an heap infoleak. From this leak, we can imagine being able to get some pointers from different caches.

We can also try to have a random free primitive with an overflow that reaches the field num_exprs of the structure nft_set and set a bigger value than 5 (NFT_SET_EXPR_MAX = 2). As a result, a part of the field data will also be considered as part as the field exprs, and we are going to obtain a full control over the stored exprs. We just have to find a way to call nft_expr_destroy. So as to do that, we may try to produce an error inside the management of the expression, just after the out-of-bound inside the nft_set structure.

By combining these techniques, we should be able to get a random free primitive and try to produce use-after-free.

All of this is a hypothesis as it has not been tested, so it may or may not work.

Conclusion

To sum-up, I found two bugs within the kernel portion of netfilter which lead to a 28 bytes infoleak. These bugs could not be exploited to obtain a privilege escalation due to a lack of time and weak write primitive (bytes should be lower than 67). All the sources of my poc can be found on our GitHub.