网络与安全工具¶
仅限授权测试
伪造源 IP、任意发包等能力仅可用于授权的测试环境,禁止对非授权目标使用。
🛡️iptables 防止端口扫描¶
SET 模块 · ipset · --packets-gt · conntrack。陷阱端口 + 动态黑名单 + 过期计数 + 延迟反馈。
原理¶
拦截端口扫描的思路是布置陷阱:随机监听一些未使用的端口,若某个 IP 在短时间内连接了好几个,就很可能是扫描者。此时临时屏蔽该 IP 的所有流量,保护尚未被扫到的端口。
| 方案 | 问题 |
|---|---|
| 普通 socket 监听端口 | 扫描者可根据连接成功信息陆续探知陷阱端口号,之后避开即可破解 |
| raw socket / libpcap | 可以接收包,但无法拦截包 |
| 把 IP 交给 iptables/ipset | 需自己实现交互,频繁与内核交互浪费资源 |
更优的方案是让接收和拦截都由 iptables 实现。
动态黑名单¶
iptables 提供了可以在运行时动态增删 ipset 的 SET 模块:
它可将 iptables 匹配到的包的源 IP 直接添加到 ipset,无需自己实现交互,简单高效。
# 创建存放扫描者 IP 的集合
ipset create scanner-ip-set hash:ip
# 保护 12345 端口:把尝试连接非 12345 端口的 IP 都当作扫描者加入黑名单
iptables \
-A INPUT \ # (1)!
-p tcp --syn ! --dport 12345 \ # (2)!
-j SET --add-set scanner-ip-set src # (3)!
- 规则追加到
INPUT链。 - 匹配
SYN包且目的端口 !=12345。 - 命中的源 IP 加入
scanner-ip-set。
验证:每秒列出 scanner-ip-set 表。用另一台设备连接非 12345 端口,该设备 IP 就会出现在黑名单里。
拦截流量¶
需要拦截两种包:
| 时机 | 说明 |
|---|---|
| 加入黑名单之前 | 扫描者连接陷阱端口 |
| 加入黑名单之后 | 扫描者连接任何端口 |
第一种若不拦截,扫描者的 SYN 包可能触发系统回复 RST,在云主机场景下会暴露云防火墙开放的陷阱端口。
此时连接过非 12345 端口的设备,其其他任何端口都无法连接。
清空黑名单
使用 ipset flush scanner-ip-set 可清空黑名单。推荐在本地虚拟机上试验,不要远程试验。
过期与累计¶
即便确定是扫描者,IP 也不能永远拉黑:长时间拉黑意义不大,扫描者可重新拨号换 IP,反而可能屏蔽之后分到这些 IP 的正常用户。因此创建 ipset 时加上过期时间,例如 30 秒:
展示黑名单时每条记录都有 timeout 参数,该值每秒自动 -1,到 0 记录即删除。若 IP 在过期时间内再次出现,默认不重置倒计时;如需重置,可参考 SET 模块的 --exist 选项。
此外,上述实现只要连接一个陷阱端口就拉黑 IP,可能过于苛刻,可加上统计功能。通过 ipset 的 counters 选项实现:
现在每条记录会多出 packets 和 bytes 参数:包匹配成功时,参与过的记录 packets 累加 1,bytes 累加包长度。配合 --packets-gt 即可读取并比较,将它加在第二种拦截中:
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的灵活性更高。
仅拦截 SYN¶
上述两种拦截都只针对 SYN 包,原因如下:
| 拦截 | 原因 |
|---|---|
| 加入黑名单之后 | 只针对 SYN 可以让已建立的 TCP 连接不受影响(可按实际情况调整) |
| 加入黑名单之前 | 若拦截所有包,服务器对外连接可能受影响:如果本地端口正好和陷阱端口相同,对方 IP 就会进黑名单 |
第一种情况仍存在问题:若扫描者不用 SYN 而是用 ACK 探测,系统会回复 RST,导致端口暴露。因此还需通过连接跟踪拦截非 SYN 的异常包:
云防火墙¶
使用云主机时通常会用到厂商防火墙,例如只开放使用的端口、屏蔽其他端口。但这会导致陷阱端口失效,因此需要开放一定数量的端口,且不能太少。
假设开放 100 个随机端口,主机每收到 1 个陷阱包,意味着扫描者其实发送了约 655.36 个,其中绝大部分被云防火墙丢弃。如果没有云防火墙,这个 IP 早已被判定为扫描者;现在只收到 1 个,甚至还没达到拦截计数器阈值,因此部分策略需要调整。
例如可在需要保护的端口前后埋设陷阱:保护 12345 端口时放行 12340 ~ 12350 端口,若扫描器端口号是线性的(无论递增还是递减),都可大概率提前落入陷阱。
陷阱端口必须足够随机
如果攻击者知道陷阱端口号,就可以伪造源 IP 故意踩陷阱,导致正常用户被拉黑无法访问。因此最好通过云防火墙 API 定期修改陷阱端口。(攻击者即使不知道陷阱端口,也可通过发送大量包把正常 IP 拉黑,只是效率较低。)
云防火墙的优点是:被拦截的流量不占带宽,即使有超大扫描流量,服务器也不受影响。
延迟反馈¶
如果要保护 80 端口,而扫描者发的第一个包就是 80,是否就没办法了?如果扫描器在短时间里发送多个包,仍可解决:延迟反馈。
收到 80 端口的 SYN 包时不立即放行到系统,而是先延迟一会;若这段时间内该 IP 命中了其他陷阱端口,就将延迟队列里的该 SYN 删除,从而防止首次猜中的情况。延迟多少取决于安全与性能的平衡;若扫描器发包很慢,该方案未必奏效。
端口集合¶
现实中的公开端口可能不止一个且不断变化,若端口变一次就改一次 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 集合:
完整实现¶
flowchart TD
A[收到 TCP SYN] --> B{目的端口在 pub-port-set?}
B -->|是| C[放行]
B -->|否| D[交给 trap-scan]
D --> E[源 IP 计数 +1]
E --> F{源 IP 已在 scanner-ip-set?}
F -->|否| G[加入黑名单]
F -->|是| H{"packets > PORT_SCAN_MAX?"}
G --> H
H -->|是| I[拒绝该 IP 的所有端口]
H -->|否| J[仅拒绝未开放端口]
#!/usr/bin/env bash
#
# 使用 iptables/ipset 阻止端口扫描
#
# 如果有 IP 连接未开放端口,该 IP 将进入扫描者名单,过期时间 IP_DENY_SECOND 秒。
# 如果该 IP 继续连接未开放端口,过期时间不复位,但包计数器会累计,
# 如果累计超过 PORT_SCAN_MAX,该 IP 将无法连接任何端口,直到过期。
IP_DENY_SECOND=30
PORT_SCAN_MAX=3
# 目标网卡
DEV=eth0
INPUT="-A INPUT"
# INPUT=-t raw -A PREROUTING
# 开放的端口
ipset create pub-port-set bitmap:port range 0-65535
# 如果应用程序端口有变化,需及时更新该 set,否则正常用户会被当成扫描者
ipset add pub-port-set 22
# ipset add pub-port-set 443
# 添加更多端口 ...
# 名单最大条数
# 例如 100Mbps 网络下 IP_DENY_SECOND 秒能收到多少 SYN 包?(SYN 最小 60B)
IP_SET_MAX=$((100 * 1024 * 1024 / 8 / 60 * $IP_DENY_SECOND))
# 扫描者名单
ipset create scanner-ip-set hash:ip \
timeout $IP_DENY_SECOND \
maxelem $IP_SET_MAX \
counters
## function trap-scan ##
iptables \
-N trap-scan
# 更新扫描者 packets/bytes 计数器
iptables \
-A trap-scan \
-m set --match-set scanner-ip-set src \
-j DROP
# 将 IP 加入扫描者名单(仅首次)
# 使用 iptables 动态改变 ipset,避免了额外的交互。
iptables \
-A trap-scan \
-j SET \
--add-set scanner-ip-set src
iptables \
-A trap-scan \
-j DROP
## end function ##
# 连接未开放端口,大概率是扫描者,交给 trap-scan 处理
iptables \
-i $DEV \
$INPUT \
-p tcp --syn \
-m set ! --match-set pub-port-set dst \
-j trap-scan
# 连接未开放端口超过 PORT_SCAN_MAX 次的 IP,禁止访问任何服务!
# 此处不更新计数器
# 已建立的 TCP 不影响,因为此处只针对 --syn
iptables \
-i $DEV \
$INPUT \
-p tcp --syn \
-m set ! --update-counters \
--match-set scanner-ip-set src \
--packets-gt $PORT_SCAN_MAX \
-j DROP
# 屏蔽非 SYN 类型的端口扫描
# 例如扫描者发送 ACK,服务器默认会回复 RST,仍有可能暴露端口
# 因此对于非 SYN 包,如果不匹配已建立的连接,则丢弃
iptables \
-i $DEV \
-A INPUT \
-p tcp ! --syn \
-m conntrack ! --ctstate ESTABLISHED,RELATED \
-j DROP
🎯Scapy 报文构造¶
IP · UDP · TCP · ICMP · send/sr/sr1 · sniff。报文构造与收发。
Scapy- 基于 Python 的交互式报文构造 / 收发库,用于网络协议测试与故障排查。
导入依赖¶
from scapy.all import *
from scapy.layers.inet import *
from scapy.layers.l2 import *
from scapy.packet import ls
from faker import Faker
构造报文¶
# 伪造一个 IPv4 地址
fake = Faker(locale='zh-cn')
ipv4addr = fake.ipv4(network=False, address_class=None, private=None)
# 构造一个 UDP 报文(伪造源 IP)
packet = IP(src=ipv4addr, dst='8.8.8.8', ttl=18) / UDP(sport=8800, dport=80) / Raw(load=b'hellow i am Zorro')
# 构造一个 TCP SYN 报文(随机源端口)
packet2 = IP(dst='10.44.3.3', ttl=18) / TCP(
sport=[random.randint(1, 65535) for _ in range(2)],
dport=80,
flags='S'
)
# 构造一个 ICMP 请求报文
packet3 = IP(dst="8.8.8.8") / ICMP()
发送与校验¶
🎭Faker 伪造数据¶
配合 Scapy 伪造源 IP 等场景,常用方法:
from faker import Faker
fake = Faker(locale='zh-cn')
fake.ipv4() # '160.152.149.78'
fake.ipv4_private() # '10.99.124.57'
fake.ipv6() # 'f392:573f:d60f:...'
fake.mac_address() # '62:67:79:8c:c2:40'
fake.port_number() # 端口
fake.name() / fake.company() # 姓名 / 公司
fake.phone_number() # '18520149907'
fake.ssn() # 身份证
fake.email() # 'na13@ding.cn'
fake.user_agent() # 随机 UA
fake.password() / fake.md5() / fake.sha256()
-
SET模块(ipset目标)由 xtables-addons 提供,配合hash:ip集合可在内核态完成匹配与写入,避免用户态反复交互。 ↩