原理和實現
如何攔截端口掃描?其實有個簡單的思路:布置陷阱。我們隨機監聽一些未使用的端口,假如有 IP 在短時間內前來連接好幾個,那么很可能就是掃描者。於是可臨時屏蔽該 IP 所有流量,保護那些還未被掃到的端口。
這個思路很簡單,但如何讓實現也簡單?如果使用普通 socket 監聽端口,那么掃描者可根據連接成功信息,陸續獲得陷阱端口號,之后就可以避開這些端口,一段時間后即可完全破解。因此,陷阱需要在底層實現。
使用 raw socket 或 libpcap 倒是可以,不過需要寫點代碼。況且這兩者只能接收包,無法攔截包。要簡單的實現攔截,還得把 IP 扔給 iptables/ipset 去處理。這需要頻繁和內核交互,浪費資源。
那么,能不能讓接收和攔截都由 iptables 實現?
動態黑名單
事實上 iptables 非常強大,除了常用的功能,還有豐富的 擴展模塊。
這次要介紹的,是一個可以在運行時動態增刪 ipset 的模塊:SET
。它的用法很簡單,例如:
iptables 匹配條件 -j SET --add-set ip-set-xxx src
通過它,即可將 iptables 匹配到的包的源 IP 直接添加到 ipset,無需自己實現交互,簡單並且高效!
我們來嘗試一下。首先創建一個名為 scanner-ip-set
的集合,用於存放掃描者的 IP:
ipset create scanner-ip-set hash:ip
我們假設保護 12345 端口,因此對任何嘗試連接非 12345 的 IP 都當做掃描者,添加到黑名單里:
iptables \
-A INPUT \
-p tcp --syn ! --dport 12345 \
-j SET --add-set scanner-ip-set src
我們來驗證下,每隔 1 秒列出 scanner-ip-set
表:
watch -n1 \
ipset list scanner-ip-set
當我們使用另一台設備連接非 12345 端口時,該設備的 IP 出現在黑名單里了!
接下來,我們實現攔截的功能。
攔截流量
需要注意,我們得攔截兩種包:
-
攔截掃描者連接 陷阱 端口(IP 添加到黑名單之前)
-
攔截掃描者連接 任何 端口(IP 添加到黑名單之后)
如果不攔截第 1 種,掃描者的 SYN 包可能會觸發系統回復 RST 包,這在使用雲主機的場合下,會暴露雲防火牆開放的陷阱端口,細節后面討論。
至於實現是非常簡單的,這里大致表示下。攔截第 1 種:
iptables \
-A INPUT \
-p tcp --syn ! --dport 12345 \
-j DROP
攔截第 2 種:
iptables \
-A INPUT \
-p tcp --syn \
-m set --match-set scanner-ip-set src \
-j DROP
現在,當我們連接非 12345 端口之后,該設備其他任何端口都無法連接了!
使用
ipset flush scanner-ip-set
可清空黑名單。推薦在本地虛擬機上試驗,不要遠程試驗~
過期和累計
即便確定是掃描者,IP 也不能永遠拉黑。長時間拉黑意義並不大,掃描者可以重新撥號不斷換 IP,反而我們可能將之后分到這些 IP 的正常用戶給屏蔽了。
因此我們在創建 ipset 時需要加上過期時間,例如 30 秒:
ipset create scanner-ip-set hash:ip timeout 30
現在展示黑名單時,每條記錄都有 timeout
參數。該值每秒自動 -1,到 0 記錄就刪除了。
如果某 IP 在過期時間內又出現,那么是重置定時器,還是保持原先倒計時?
默認是不重置的。如果希望重置,可參考 SET
模塊的 --exist
選項。
此外,在本文開頭也提到「IP 短時間內前來連接好幾個」,而在上述實現中,只要連接一個陷阱端口 IP 就拉黑了,這也許太過苛刻。
因此,我們需要加上統計功能。可以通過 ipset 的 counters
選項實現:
ipset create scanner-ip-set hash:ip timeout 30 counters
現在展示黑名單時,每條記錄又多了 packets
和 bytes
參數。每當包匹配成功時,參與過的 set 記錄的 packets
累加 1,bytes
累加包長度。
那么,怎樣才能讀取 packets
然后做比較?這需要另一個選項 --packets-gt
。
我們將該條件加在上述第 2 種攔截(攔截所有端口)中,例如:
iptables \
-A INPUT \
-p tcp --syn \
-m set --match-set scanner-ip-set src \
--packets-gt 5 \
-j DROP
這樣,只有當某 IP 在過期時間內訪問 5 次以上陷阱端口時,才會進行攔截。策略相對寬松了一些。
這個值可根據當前風險狀況進行調整。例如在被很多 IP 掃描時,可以降低一些。
上述提到的這些功能,事實上用
-m recent
也能實現,甚至更簡單。當然本文主要介紹的-j SET
靈活性更高一些。
TCP 狀態
在上述兩種攔截中,我們都只針對 SYN 包,為什么不是所有包?
在第 2 種攔截(所有端口)中,只針對 SYN 可以讓已建立的 TCP 連接不受影響。當然這個策略可根據實際情況調整。
在第 1 種攔截(陷阱端口)中,如果攔截所有包,那么服務器對外的連接可能會受到影響,如果本地端口正好和陷阱端口相同,對方 IP 就進黑名單了。
不過第 1 種情況仍存在問題:假如掃描者不用 SYN 而是用 ACK 進行探測,那么系統會回復 RST 包,導致端口暴露。
因此我們還得通過連接跟蹤,將非 SYN 的異常包進行攔截:
iptables \
-A INPUT \
-p tcp ! --syn \
-m conntrack ! --ctstate ESTABLISHED \
-j DROP
雲防火牆
使用雲主機時,通常會用到廠商提供的防火牆,例如只開放使用的端口,其他端口都屏蔽。
然而這會導致陷阱端口失效,因此我們需要開放一定數量的端口。這個數量不能太少,否則安全性會降低。
假如開放 100 個隨機端口,那么主機每收到 1 個陷阱包,意味着掃描者其實發送了 655.36 個,其中絕大部分都被雲防火牆丟棄了。如果沒有雲防火牆,這個 IP 早被判定為掃描者了;但現在只收到 1 個,甚至還沒達到攔截計數器的閾值。因此有些策略需要進行調整。
例如,可以在需要保護的端口前后埋設陷阱。假設保護 12345 端口,那么我們可放行 12340 - 12350 端口,如果掃描器發送的端口號是線性的話,無論遞增還是遞減,都可以大概率提前落入陷阱。
需要注意的是,陷阱端口必須足夠隨機,以防被猜中。如果攻擊者知道陷阱端口號,就可以將源 IP 偽造成正常用戶故意踩陷阱,從而導致正常用戶被拉黑無法訪問。 因此,最好通過雲防火牆 API 定期修改陷阱端口。(當然攻擊者即使不知道陷阱端口,也可以通過發送很多包,把正常 IP 拉黑,只是效率較低。這個問題之后再寫文討論)
雲防火牆雖然用起來比較麻煩,每個廠商的 API 也都不同,但它有個很大的優點:被攔截的流量不占帶寬。這對安全防護很有用。即使有超大的掃描流量,我們的服務器也不受影響。
延遲反饋
假如我們要保護 80 端口,但掃描者發的第一個包就是 80,那么是不是就沒辦法了?
如果掃描器在短時間里發送了多個包,這種情況仍有解決方案:延遲反饋。
當我們收到 80 端口的 SYN 包時,不立即放行到系統,而是先延遲一會。假如在這段時間內,該 IP 命中了其他的陷阱端口,那么將延遲隊列里的 80 SYN 刪除,這樣就可以防止首次猜中的情況!
至於延遲多少,取決於安全和性能的平衡。當然,如果掃描器發包很慢的話,這種方案未必奏效。
至於實現,目前暫未研究。iptables 似乎沒有延遲的功能,而 tc 命令只能延遲發送的包,看來需要一些奇技淫巧才能簡單實現~
端口集合
前面為了簡單表示,我們使用 12345 端口,但現實中端口也許不止一個,並且可能不斷變化。
如果端口變一次 iptables 就得改一次,顯然很累贅。因此不妨將端口也放在 ipset 里:
ipset create pub-port-set bitmap:port range 0-65535
iptables \
-A INPUT \
-p tcp --syn \
-m set ! --match-set pub-port-set dst \
-j SET --add-set scanner-ip-set src
之后我們只需操作 pub-port-set
集合即可:
ipset add pub-port-set 12345
完整實現
綜上所述,整理了一個相對完整的版本:
https://github.com/EtherDream/anti-portscan/blob/master/install.sh
由於寫的比較倉促,如果存在問題請及時指出,有更好的思路也可以交流~
在線演示
可以試試多久找出這個 HTTP 端口?