本文最后更新于 2025-02-11T23:51:39+08:00
论文原文:https://inai.de/documents/Chaostables.pdf
iptables无法实现复杂检测规则,iptables编写的防火墙有较大概率假阳或假阴,不建议在生产环境中使用
隐蔽扫描 标准的TCP连接以SYN包开始,其他一切都会被视为异常,隐蔽扫描就是通过构造异常的数据包来进行扫描
隐蔽扫描非常容易被阻止,只需要简单的丢弃所有异常数据包就可以(一点也不隐蔽,为什么要起这个名字,雾)
隐蔽扫描同时也非常不可靠,可能在路由上就被丢弃导致无法到达目标,目标也可能全部不响应或全部RST导致误判
NULL扫描 NULL扫描的原理是发送一个没有任何标志位的TCP包(RFC-793标准规定:开放端口应忽略没有标志位的数据包)
NULL扫描无法判断出端口是开放还是被过滤(因为都没有响应)
端口开放,目标主机不会回复 端口关闭,目标回复RST-ACK
如果所有标志位都为0,数据包是无效的,路由器、防火墙、操作系统可能会直接丢弃这种数据包,导致NULL扫描无法正常工作
在我测试过程中,NULL包确实不能到达阿里云上的服务器,只能对内网设备进行扫描
以下是一个NULL扫描器示例代码,扫描内网的设备
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 from scapy.all import * SYN = 0x02 RST = 0x04 ACK = 0x10 SYN_ACK = 0x12 RST_ACK = 0x14 def null_scan (ip:str , port:int , timeout:int = 5 ) -> bool : ip_pkt = IP(dst = ip) tcp_pkt = TCP(sport = RandShort()._fix(), dport = port, flags = 0 ) response = sr1(ip_pkt / tcp_pkt, timeout = timeout, verbose = 0 ) if response is None : return True elif response.haslayer(TCP): if response.getlayer(TCP).flags == RST_ACK: return False else : raise RuntimeError(f"未知flags:{flags} " )for i in [21 ,22 ,80 ,443 ,8889 ,39001 ]: try : if null_scan("192.168.0.1" , i): print (f"{i} 开放/过滤" ) else : print (f"{i} 关闭" ) except RuntimeError as e: print (e)
iptables阻止NULL扫描:
既然NULL数据包是无效的,那直接全部丢弃就行,也不用怕丢错导致网络出现问题
1 -A INPUT -p tcp --tcp-flags ALL NONE -j DROP
FIN扫描、XMAS扫描和ACK扫描 FIN扫描原理和NULL扫描原理类似,但是只设置FIN标志位
XMAS扫描同理,设置FIN、URG和PSH标志位
ACK扫描同理,只设置ACK标志位
此处不再提供示例代码
iptables阻止隐蔽扫描 1 2 3 4 5 6 7 8 9 10 11 12 # tcp_inval链 -N tcp_inval;# -A tcp_inval -p tcp --tcp-flags SYN,FIN,RST,ACK RST,ACK -j RETURN;# -A tcp_inval -j LOG --log-prefix "[STEALTH] ";# -A tcp_inval -j DROP;# 匹配大多数隐蔽扫描数据包(NULL,FIN,Xmas) # 但是不检测ACK扫描,因为这可能导致误判 -A INPUT -p tcp ! --syn -m conntrack --ctstate INVALID -j tcp_inval;
半开扫描 半开扫描,基本思想是不完成完整的三次握手,以规避防火墙的检测
隐蔽扫描构造异常数据包,半开扫描构造的是合法数据包
SYN扫描 SYN扫描利用TCP三次握手机制,TCP三次握手此处不再赘述
RFC-793标准规定:当尝试连接到一个关闭的端口时,目标主机应返回一个RST响应,表示该端口不可达
扫描器向目标端口发送SYN 端口开放,目标回复SYN-ACK 端口关闭,目标回复RST-ACK
以下是一个SYN扫描器示例代码
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 from scapy.all import * SYN = 0x02 RST = 0x04 ACK = 0x10 SYN_ACK = 0x12 RST_ACK = 0x14 def syn_scan (ip:str , port:int , rst:bool = True , timeout:int = 5 ) -> bool : ip_pkt = IP(dst = ip) tcp_pkt = TCP(sport = RandShort()._fix(), dport = port, flags = "S" ) response = sr1(ip_pkt / tcp_pkt, timeout = timeout, verbose = 0 ) if response and response.haslayer(TCP): flags = response.getlayer(TCP).flags if flags == SYN_ACK: if rst: send(ip_pkt / TCP(sport = tcp_pkt.sport, dport = port, flags = "RA" , seq = response.ack, ack = response.seq + 1 ), verbose = 0 ) return True elif flags == RST_ACK: return False else : raise RuntimeError(f"{port} 未知flags:{flags} " ) else : raise RuntimeError(f"{port} 服务器无响应" )for i in [21 ,22 ,80 ,443 ,8889 ,39001 ]: try : if syn_scan("目标IP" , i): print (f"{i} 开放" ) else : print (f"{i} 关闭" ) except RuntimeError as e: print (e)
完全阻止SYN扫描是不可能的,因为无法判断客户端发送的SYN是真实连接还是扫描尝试 唯一能做的,就是标记可能的SYN扫描,阻止进一步的扫描
注意到,SYN扫描特征:
多次尝试连接不同的端口,且不完成完整的三次握手
扫描器发送RST终止三次握手
扫描器不发送RST,此时服务器多次重发SYN+ACK直到超时才终止三次握手
iptables阻止SYN扫描:
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 # 定义四个状态 SYN=401 CLOSED=402 SYNSCAN=403 ESTAB=404# 标记关闭的连接 -N mark_closed -A mark_closed -j CONNMARK --set-mark $CLOSED# 标记已建立的连接 -N mark_estab -A mark_estab -j CONNMARK --set-mark $ESTAB# tcp_new1链用于处理新TCP连接 -N tcp_new1# # -A tcp_new1 -i lo -p tcp --tcp-flags ALL SYN,ACK -j RETURN# -A tcp_new1 -i lo -p tcp --tcp-flags ALL RST,ACK -g mark_closed# # -A tcp_new1 -p tcp --tcp-flags ALL ACK -g mark_estab# -A tcp_new1 -j CONNMARK --set-mark $SYNSCAN# 匹配标记为SYN的连接,并将其交给tcp_new1链处理 -A INPUT -m connmark --mark $SYN -j tcp_new1# 如果收到一个新的SYN包(即新连接),将其标记为SYN状态 -A INPUT -p tcp --syn -m conntrack --ctstate NEW -j CONNMARK --set-mark $SYN# 处理SYN扫描的逻辑 # -A tcp_new1 -j handle_evil# -A INPUT -m connmark --mark $SYNSCAN -j handle_evil# handle_evil链逻辑,比如禁止IP访问等 # ...
全开扫描 全开扫描经过三次握手建立完整的TCP连接,
半开扫描使用raw socket通常需要root权限,全开扫描主要用于无权限情况
Connect扫描 以下是一个Connect扫描器示例代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import socketdef connect_scan (ip:str , port:int , timeout:int = 5 ) -> bool : with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: sock.settimeout(timeout) try : sock.connect((ip, port)) except (socket.timeout, socket.error): return False return True for i in [21 ,22 ,80 ,443 ,8889 ,39001 ]: try : if connect_scan("121.40.34.51" , i): print (f"{i} 开放" ) else : print (f"{i} 关闭" ) except RuntimeError as e: print (e)
注意到,连接扫描特征:
多次尝试连接不同的端口,连接建立后不发送任何数据就终止(RST或FIN)
以下规则检测刚建立连接就立即断开的异常流量
1 2 3 4 5 6 7 8 9 10 11 12 CNSCAN=406; VALID=408; -N mark_cnscan; -A mark_cnscan -j CONNMARK --set-mark $CNSCAN; -N tcp_new3; -A tcp_new3 -p tcp --tcp-flags SYN,FIN,RST RST -g mark_cnscan; -A tcp_new3 -p tcp --tcp-flags SYN,FIN,RST FIN -g mark_cnscan; -A tcp_new3 -j CONNMARK --set-mark $VALID; -A INPUT -m connmark --mark $ESTAB -j tcp_new3;
Grab扫描 Grab扫描不仅检测端口是否开放,还会获取服务器返回的Banner信息以判断出服务器上运行的服务及其版本
连接建立后服务器返回Banner信息,客户端响应Banner的ACK后就断开连接
以下规则检测建立连接后发送一个ACK包就断开的异常流量
1 2 3 4 5 6 -N tcp_new4; -A tcp_new4 -p tcp --tcp-flags SYN,FIN,RST,ACK ACK -m length --length 52 -j RETURN; -A tcp_new4 -p tcp --tcp-flags SYN,FIN,RST RST -g mark_grscan; -A tcp_new4 -p tcp --tcp-flags SYN,FIN,RST FIN -g mark_grscan; -A tcp_new4 -j CONNMARK --set-mark $VALID;
真正的”暴力检测” 依我看,要检测端口扫描哪有那么复杂,我连完整的连接都不跟踪了,那样只会复杂化算法,直接暴力判断某个IP是否多次发送不同目标端口的数据包就行
简单有效易维护,符合KISS原则(喜
正常一个网站阈值为4非常合理,普通用户最多访问80和443两个端口(笑
nmap扫一下,后台哐哐报
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 from scapy.all import *import ipaddress PORT_SCAN_THRESHOLD = 4 record = {}def packet_callback (packet ): if packet.haslayer(IP) and packet.haslayer(TCP): src_ip = packet[IP].src dst_port = packet[TCP].dport if ipaddress.IPv4Address(src_ip).is_global: if src_ip not in record: record[src_ip] = [] if dst_port not in record[src_ip]: record[src_ip].append(dst_port) if len (record[src_ip]) >= PORT_SCAN_THRESHOLD: print (f"{src_ip} 尝试端口扫描(IP可伪造仅供参考)" ) del record[src_ip] sniff(iface="eth0" , prn=packet_callback, filter ="tcp" )