资讯动态

用Scapy手写Ping扫描器:从造包到并发探测的实战指南

发布时间:2026/10/15 9:51:35 来源:尧图企业网站定制
1. 从一个“造包”的视角重新认识Ping扫描很多人第一次接触网络探测脑子里蹦出来的第一个工具就是系统自带的ping命令。敲一下回车看到几行回显知道对面通不通任务就算完成了。但如果你手里有几十上百个地址要摸一遍或者你想搞清楚“通”和“不通”背后到底发生了什么光靠系统命令就不够用了。这时候我会把目光转向Scapy——一个能让你从数据包层面“手工造物”的 Python 库。这篇内容要聊的就是怎么用 Scapy 写一个属于自己的 ping 扫描器。它解决的核心问题很直接批量判断一批目标地址是否在线并且让你完全掌控发包的每一个字段。系统 ping 是别人给你造好的轮子Scapy 则是把扳手、螺丝刀、钢板全摊在你面前你想怎么拧就怎么拧。适合谁来参考有一定 Python 基础、想入门网络编程或者安全测试方向的朋友也适合那些用惯了现成工具、想往下再挖一层、看看 ICMP 报文长什么样的从业者。我先把结论摆在这儿用 Scapy 做 ping 扫描代码量不大核心逻辑也就几十行但里面藏着不少门道——超时怎么设、并发怎么控、回包怎么匹配、权限不够怎么办。这些细节才是决定你这个扫描器“能不能用、好不好用”的关键。下面我按自己的实操思路一层一层拆开讲。2. 为什么不用系统 ping非要自己造轮子2.1 系统 ping 的三个硬伤系统自带的 ping 命令在单点测试时确实方便但一旦进入批量场景它的短板就暴露得很明显。第一个硬伤是解析成本高。系统 ping 的输出是给人看的自然语言你要从里面提取“是否在线”“延迟多少”就得靠字符串匹配或者正则去抠。不同操作系统、不同语言环境下的输出格式还不一样写出来的解析代码又脆又难维护。我早期偷懒用过subprocess调系统 ping结果换了一台机器输出格式变了脚本直接趴窝。第二个硬伤是并发控制弱。系统 ping 默认是串行的一个地址等一秒一百个地址就是一百秒。虽然可以用多进程或者-c参数配合 shell 循环去凑并发但进程开销大控制粒度也粗想精确限制“同时最多发 50 个包”这种需求用 shell 拼起来非常别扭。第三个硬伤是不可控。你没法指定 ICMP 报文的标识符、序列号没法自定义 payload更没法在收到回包时拿到原始字节去做进一步分析。对于做协议研究或者需要精细探测的场景系统 ping 就是个黑盒。2.2 Scapy 带来的三个自由度换成 Scapy 之后上面三个问题基本都迎刃而解。自由度一报文完全自定义。你可以用IP(dstx.x.x.x)/ICMP()这样一层一层叠出一个包每个字段想改就改。想给 ICMP 加一段自定义 payload直接/Raw(loadbhello)接上去就行。这种“搭积木”式的构造方式是理解网络协议最直观的途径。自由度二收发一体结果结构化。Scapy 的sr()、sr1()、srp()这些函数把“发包”和“收包”打包成了一个动作返回的是解析好的报文对象不是文本。你直接访问resp[IP].src、resp[ICMP].type就能拿到字段值不用再做字符串解析。自由度三并发和超时随手可调。sr()函数自带timeout和inter参数配合多线程或者 Scapy 自己的异步发送机制可以很轻松地控制发包节奏和并发规模。提示Scapy 构造和发送原始 ICMP 包需要较高权限。在 Linux/macOS 上通常需要sudo在 Windows 上需要以管理员身份运行并且要装好 Npcap 驱动。这是绕不过去的门槛后面会专门讲怎么处理。3. 动手之前环境与权限这两道坎3.1 Scapy 的安装与版本选择安装本身没什么难度一条命令的事pip install scapy但版本上我建议用2.5.0 及以上。早期版本在 Windows 上的收发稳定性和 IPv6 支持都有些问题新版本对 Python 3.10 的兼容性也更好。装完之后可以跑一句python -c from scapy.all import *; print(conf.version)确认版本。如果你用的是虚拟环境记得在虚拟环境里装别装到系统 Python 里去了。我见过有人系统里装了一份、虚拟环境里没装跑脚本时一直报ModuleNotFoundError查了半天才发现是环境搞混了。3.2 权限问题的三种应对姿势权限是 Scapy 做原始套接字操作时最常撞的墙。原始套接字raw socket允许你直接读写网络层的数据操作系统出于安全考虑默认只给高权限进程开放这个能力。姿势一直接提权。Linux/macOS 下sudo python your_script.pyWindows 下用管理员权限打开终端。这是最省事的办法但缺点是整个脚本都以高权限运行如果你脚本里还干了别的事风险面就大了。姿势二只给 Python 解释器赋权。Linux 下可以用setcap给 Python 二进制文件加上网络原始套接字的权限sudo setcap cap_net_raweip $(readlink -f $(which python3))这样之后普通用户跑脚本也能发包。但要注意这等于给这台机器上所有用这个 Python 的脚本都开了口子安全性上要自己权衡。姿势三Windows 下装 Npcap。Windows 没有setcap这种机制Scapy 依赖 Npcap 驱动来收发原始包。安装 Npcap 时记得勾选“WinPcap API 兼容模式”否则 Scapy 可能找不到网卡。装完之后仍然需要管理员权限运行脚本。注意不管用哪种方式都别把提权后的脚本随便丢到不受信任的环境里跑。原始套接字能做的事很多权限给出去容易收回来难。4. 核心原理一个 ICMP 回显包的完整生命周期4.1 ICMP 回显请求与应答的字段拆解要写好 ping 扫描得先搞清楚一个 ICMP 回显包Echo Request里到底有什么。用 Scapy 打印一下最清楚from scapy.all import * pkt IP(dst192.168.1.1)/ICMP() pkt.show()输出会告诉你IP 层有源地址、目的地址、TTL、协议号ICMP 是 1ICMP 层有 type请求是 8应答是 0、code通常为 0、id标识符、seq序列号、chksum校验和。其中id 和 seq 是匹配请求与应答的关键。当你发出一个 type8 的请求目标如果在线且允许 ICMP就会回一个 type0 的应答并且把请求里的 id 和 seq 原样带回来。扫描器要做的就是发出去一批请求然后看哪些请求收到了对应的应答。4.2 为什么 id 和 seq 的匹配如此重要在单目标单包的情况下你发一个收一个不太需要担心匹配问题。但批量扫描时你会在短时间内发出大量请求回包也是乱序到达的。如果不靠 id 和 seq 去对应你根本不知道哪个应答是回应哪个请求的。Scapy 的sr()函数在底层帮你做了这件事它维护了一个发送记录收到回包时根据 IP 层和 ICMP 层的字段去匹配。但如果你自己用send()和sniff()分开写就得手动处理这个匹配逻辑。我个人的建议是除非有特殊需求否则优先用sr()系列函数省心且不容易出错。4.3 超时机制等多久才算“不通”超时设置是 ping 扫描里最需要拿捏的参数。设太短网络稍微抖一下就把在线主机判成离线设太长扫一批地址要等到天荒地老。我的经验值是局域网内 1 到 2 秒足够跨公网建议 2 到 3 秒。这个数字不是拍脑袋来的。局域网 RTT 通常在 1 毫秒以内留 1 秒是给极端情况兜底公网 RTT 可能到几十甚至几百毫秒加上中间可能丢包重传2 到 3 秒是比较稳妥的窗口。Scapy 的sr()里timeout参数控制的是“等待回包的总时长”不是单个包的超时。也就是说如果你一次发 100 个包timeout2那么这 2 秒内收到的所有回包都会被收集2 秒一到就停止等待。这个语义和很多人直觉里的“每个包等 2 秒”不一样用的时候要心里有数。5. 从零写一个可用的 Ping 扫描器5.1 最小可用版本单线程逐个扫先上一个最朴素的版本把逻辑跑通from scapy.all import IP, ICMP, sr1 import sys def ping_one(host, timeout2): pkt IP(dsthost)/ICMP() resp sr1(pkt, timeouttimeout, verbose0) if resp is None: return False, None return True, resp[IP].src if __name__ __main__: targets [192.168.1.1, 192.168.1.2, 192.168.1.3] for t in targets: alive, ip ping_one(t) print(f{t} - {在线 if alive else 离线})这段代码的逻辑很直白对每个目标构造一个 ICMP 请求用sr1()发出去并等第一个回包收到就判在线超时就判离线。verbose0是为了关掉 Scapy 默认的发送日志不然屏幕会被刷屏。这个版本能跑但问题也很明显串行执行效率低。三个地址还好三百个地址就得等好几分钟。而且它没有做任何异常处理遇到网络不可达或者权限报错会直接崩。5.2 进阶版本多线程并发扫描要提速最直接的办法是把发包动作丢到线程池里。下面这个版本用concurrent.futures做并发控制from scapy.all import IP, ICMP, sr1 from concurrent.futures import ThreadPoolExecutor, as_completed def ping_one(host, timeout2): try: pkt IP(dsthost)/ICMP() resp sr1(pkt, timeouttimeout, verbose0) if resp is None: return host, False return host, True except Exception as e: return host, False def scan(targets, max_workers50, timeout2): results {} with ThreadPoolExecutor(max_workersmax_workers) as executor: futures {executor.submit(ping_one, t, timeout): t for t in targets} for future in as_completed(futures): host, alive future.result() results[host] alive return results这里有几个参数需要解释。max_workers50表示同时最多有 50 个线程在发包。这个数字不是越大越好——线程太多会导致本机网络栈排队反而增加丢包率。我实测下来普通办公网络环境下 50 到 100 是比较舒服的区间再往上收益递减明显。as_completed的作用是“谁先完成就先处理谁”不用等所有任务按顺序结束结果收集更及时。实操心得多线程 Scapy 的sr1()组合在 Linux 上表现很稳但在 Windows 上偶尔会遇到 Npcap 驱动的并发瓶颈表现为部分线程莫名超时。如果遇到这种情况把max_workers降到 20 左右通常能缓解。5.3 更优雅的方案用 sr() 批量发包其实 Scapy 的sr()函数本身就支持一次发多个包内部做了批量处理比手动开线程更高效from scapy.all import IP, ICMP, sr def batch_scan(targets, timeout2): pkts [IP(dstt)/ICMP() for t in targets] answered, unanswered sr(pkts, timeouttimeout, verbose0) alive [resp[IP].src for _, resp in answered] dead [pkt[IP].dst for pkt in unanswered] return alive, deadsr()返回两个列表answered里是“请求-应答”对unanswered里是没收到回包的请求。这种写法代码更短而且 Scapy 内部对批量发包做了优化性能通常比手动开线程还好一些。但要注意sr()是一次性把所有包都发出去如果你的目标列表有几千个瞬间的流量峰值可能触发某些网络设备的限速或者告警。这时候可以配合inter参数控制发包间隔answered, unanswered sr(pkts, timeouttimeout, inter0.01, verbose0)inter0.01表示每发一个包间隔 10 毫秒这样能把流量摊平降低被限速的概率。6. 结果解析与输出让数据说话6.1 从回包里提取有用信息收到应答只是第一步真正有价值的是从应答里挖出更多信息。除了“在线/离线”你还可以拿到响应时间Scapy 的sr()返回的answered列表里每个元素是一个元组(sent_pkt, recv_pkt)而recv_pkt上带有time属性可以算出 RTT。TTL 值resp[IP].ttl能反映目标的操作系统类型和经过的路由跳数。TTL 接近 64 通常是 Linux/Unix接近 128 通常是 Windows。ICMP 类型正常情况下应答是 type0但如果收到 type3目的不可达之类的说明目标主机可能在线但端口或协议被拒。下面这段代码把 RTT 和 TTL 一起打出来import time from scapy.all import IP, ICMP, sr def scan_with_detail(targets, timeout2): pkts [IP(dstt)/ICMP() for t in targets] start time.time() answered, unanswered sr(pkts, timeouttimeout, verbose0) for sent, recv in answered: rtt (recv.time - sent.sent_time) * 1000 print(f{recv[IP].src} 在线 | RTT{rtt:.1f}ms | TTL{recv[IP].ttl}) for sent in unanswered: print(f{sent[IP].dst} 无响应)sent.sent_time是 Scapy 在发包时自动打的时间戳recv.time是收包时间两者相减就是 RTT。这个精度比系统 ping 给出的还要细。6.2 输出格式的选择扫描结果怎么呈现取决于你的使用场景。如果只是自己看直接 print 就行如果要喂给别的程序建议输出成 JSON 或者 CSV。import json def scan_to_json(targets, timeout2): pkts [IP(dstt)/ICMP() for t in targets] answered, unanswered sr(pkts, timeouttimeout, verbose0) result {alive: [], dead: []} for sent, recv in answered: result[alive].append({ ip: recv[IP].src, ttl: recv[IP].ttl, rtt_ms: round((recv.time - sent.sent_time) * 1000, 2) }) for sent in unanswered: result[dead].append(sent[IP].dst) return json.dumps(result, ensure_asciiFalse, indent2)JSON 的好处是结构清晰后续不管是入库还是做可视化都方便。CSV 则更适合丢进表格软件里做统计。7. 踩坑实录那些文档里不会写的问题7.1 常见问题速查表问题现象可能原因解决思路报错PermissionError权限不足提权运行或给 Python 赋 raw socket 权限所有目标都判离线防火墙拦截 ICMP换用 TCP SYN 扫描或确认目标是否禁 ping部分目标结果不稳定超时设太短或并发过高调大 timeout降低 max_workersWindows 下找不到网卡Npcap 未装或未勾选兼容模式重装 Npcap 并勾选 WinPcap 兼容回包匹配错乱手动 send/sniff 未做 id 匹配改用 sr() 系列函数扫描速度极慢串行发包改用多线程或 sr() 批量发送7.2 三个我亲自踩过的坑坑一以为所有在线主机都会回 ICMP。这是新手最容易犯的错。很多服务器出于安全策略会直接丢弃 ICMP 请求你扫出来是“离线”实际上它的 Web 服务跑得好好的。所以 ping 扫描的结果只能作为参考不能当作“主机是否存活”的唯一依据。要更准确得结合 TCP 端口扫描一起看。坑二在虚拟机里跑扫描结果全是超时。虚拟机的网络模式如果是 NATICMP 包可能被宿主机的虚拟网络层拦掉。我当初在虚拟机里调了半天代码最后发现是网络模式的问题换成桥接模式就正常了。这个坑排查起来很费时间因为代码本身没错错在环境。坑三目标列表里有重复地址结果对不上。如果你的目标列表里有重复 IPsr()返回的answered里会出现多条相同 IP 的记录统计在线数量时就会重复计数。解决办法是在扫描前用set()去重或者在结果收集时用字典按 IP 聚合。提示扫描前先对目标列表做一次清洗——去重、剔除明显不合法的地址、确认网段范围。这一步花不了几秒钟但能省掉后面很多莫名其妙的调试时间。8. 性能调优让扫描又快又稳8.1 超时、并发、间隔的三角平衡这三个参数是相互制约的。超时短、并发高、间隔小扫描就快但丢包和误判的概率上升反过来则稳但慢。我的调参思路是先定超时。局域网 1 秒公网 2 到 3 秒这是底线再短就容易误判。再定并发。从 50 开始试如果发现丢包率明显上升比如同一批地址扫两次结果差异很大就往下调。最后定间隔。如果目标网段有流量监控或者限速设备把inter设成 0.01 到 0.05 秒把流量摊开。8.2 分批次扫描大网段如果你要扫一个 /16 的网段那就是六万多个地址一次性全发出去不现实。合理的做法是分批比如每批 256 个地址扫完一批再扫下一批def scan_subnet(base, start1, end254, batch_size64, timeout2): all_alive [] for i in range(start, end 1, batch_size): batch [f{base}.{j} for j in range(i, min(i batch_size, end 1))] pkts [IP(dstt)/ICMP() for t in batch] answered, _ sr(pkts, timeouttimeout, verbose0) all_alive.extend(resp[IP].src for _, resp in answered) return all_alive分批的好处是内存占用可控而且每批之间可以加个短暂休眠进一步降低对网络的冲击。8.3 用异步发送替代多线程Scapy 从 2.4 版本开始提供了AsyncSniffer和异步发送的能力可以在单线程内实现高并发收发避免了多线程的 GIL 和上下文切换开销。不过异步模式的代码复杂度更高回调逻辑要自己理清楚。如果你的扫描规模在几百个地址以内多线程方案完全够用上千个地址再考虑上异步。9. 从 Ping 扫描延伸出去的可能性Ping 扫描本身只是个起点。把这套“构造报文-发送-解析回包”的框架掌握之后你能做的事远不止 ICMP。比如把ICMP()换成TCP(dport80, flagsS)就变成了 TCP SYN 扫描能探测端口开放情况换成ARP(pdst192.168.1.1)配合srp()就变成了 ARP 扫描在局域网内比 ICMP 更可靠因为很多主机不响应 ICMP 但一定会响应 ARP。再进一步你可以把扫描结果和 TTL 分析结合起来做操作系统指纹识别或者把 RTT 数据收集起来做网络质量监控。这些延伸方向的核心逻辑都是一样的用 Scapy 精确控制每一个字节然后从回包里读出你想要的信息。我个人在实际操作中的体会是Scapy 最大的价值不在于它帮你省了多少代码而在于它逼着你去理解协议本身。当你亲手拼出一个 ICMP 包、亲眼看到回包里的每一个字段时那些原本停留在书本上的协议格式突然就变得具体了。这种“看得见摸得着”的感觉是调用现成工具永远给不了的。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑