跳转至

网络与安全工具

仅限授权测试

伪造源 IP、任意发包等能力仅可用于授权的测试环境,禁止对非授权目标使用。


🛡️iptables 防止端口扫描

SET 模块 · ipset · --packets-gt · conntrack。陷阱端口 + 动态黑名单 + 过期计数 + 延迟反馈。

原理

拦截端口扫描的思路是布置陷阱:随机监听一些未使用的端口,若某个 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,无需自己实现交互,简单高效。

# 创建存放扫描者 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)!
  1. 规则追加到 INPUT 链。
  2. 匹配 SYN 包且目的端口 != 12345
  3. 命中的源 IP 加入 scanner-ip-set

验证:每秒列出 scanner-ip-set 表。用另一台设备连接非 12345 端口,该设备 IP 就会出现在黑名单里。

watch -n1 ipset list scanner-ip-set

拦截流量

需要拦截两种包:

时机 说明
加入黑名单之前 扫描者连接陷阱端口
加入黑名单之后 扫描者连接任何端口

第一种若不拦截,扫描者的 SYN 包可能触发系统回复 RST,在云主机场景下会暴露云防火墙开放的陷阱端口。

iptables \
  -A INPUT \
  -p tcp --syn ! --dport 12345 \
  -j DROP
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,可能过于苛刻,可加上统计功能。通过 ipset 的 counters 选项实现:

ipset create scanner-ip-set hash:ip timeout 30 counters

现在每条记录会多出 packetsbytes 参数:包匹配成功时,参与过的记录 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 的异常包:

iptables \
  -A INPUT \
  -p tcp ! --syn \
  -m conntrack ! --ctstate ESTABLISHED \
  -j DROP

云防火墙

使用云主机时通常会用到厂商防火墙,例如只开放使用的端口、屏蔽其他端口。但这会导致陷阱端口失效,因此需要开放一定数量的端口,且不能太少。

假设开放 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 集合:

ipset add pub-port-set 12345

完整实现

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[仅拒绝未开放端口]
install.sh
#!/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()

发送与校验

# 指定接口发送并记录第一个响应报文(一直等待响应)
rec = sr1(packet3, iface="MIBO")

# 打印响应报文的目的地址
ipdst = rec[IP].dst
print(f"收到的响应报文的目的地址是 => {ipdst}")

# 打印响应报文的全部细节
ls(rec)

# 保存收到的报文
wrpcap("rec.cap", rec)

# 之后可以直接把保存的包导入
pkts = rdpcap("rec.cap")
pkts = sniff(offline="rec.cap")
# 发送报文并检查响应
ans, unans = sr(packet2, iface="ens3")
print('响应报文')
ans.show()

print('其他报文')
unans.summary()
# 只发送,不接收;注意源地址可被随意伪造,仅限授权测试
send(packet, verbose=0, iface="MIBO")
pkg = sniff(count=4, filter="host 10.44.3.3", iface='eth0')
pkg.show()
sendp(pkg)
pkg = sniff(offline='rec.cap', count=10)
pkg.show()
sendp(pkg)

🎭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()

  1. SET 模块(ipset 目标)由 xtables-addons 提供,配合 hash:ip 集合可在内核态完成匹配与写入,避免用户态反复交互。