别再下那些来路不明的绿色版了,我用 Python 手搓了一个端口扫描与安全监控工具。
0x00 引子:新机初启,百“孔”难安
故事发生在 2026 年 8 月 4 日的清晨。
新配的台式机到了,Windows 10 系统清爽得像一张白纸。作为一个手痒难耐的安全爱好者,我习惯性地敲下了那条刻在 Windows 管理员 DNA 里的命令:
netstat -ano

屏幕瞬间滚出密密麻麻的列表。一个刚装好、连额外软件都没装的系统,居然挂着二十多个监听端口:
TCP 0.0.0.0:135 0.0.0.0:0 LISTENING 1348
TCP 0.0.0.0:445 0.0.0.0:0 LISTENING 4
TCP 0.0.0.0:3389 0.0.0.0:0 LISTENING 1504
…………
…………
…………
135(RPC)——冲击波蠕虫的老巢;445(SMB)——WannaCry 勒索病毒就是从这杀进来的;3389(RDP)——BlueKeep 漏洞能让攻击者不认证就执行任意代码。新机器像没锁门的仓库,门外就是整个互联网。
怎么办?
-
用 Nmap?参数比我头发还多,查手册翻到手抽筋。
-
用 TCPView?Sysinternals 出品虽好,但闭源、无风险评级、无漏洞库。
-
用 CurrPorts?缺日志、缺知识库、缺本地化中文。
-
去网上下个绿色版?谁知道是哪年的版本?有没有被植入后门?
一个念头冒了出来:与其用别人的刀,不如自己铸一把。
不就是端口扫描加连接监控嘛——Windows 底层 API 摆在那里,Python 的 ctypes 能调,PyQt5 能做界面,我决定自己写一个。
这个项目的名字就是 PortSentinel ,目前v0.08版本已开源在 GitHub(github.com/donoot/PortSentinel)。
0x01 顶层设计:三层架构与核心模块
动工之前先画蓝图。我需要一个清晰的分层结构,让代码可维护、可测试、可替换。
架构总览
我采用了经典的三层架构:

| GUI 层 | 主窗口、工作线程、右键菜单 | PyQt5(信号槽驱动) |
| 核心业务层 | 端口扫描器、连接管理器、OS 指纹、漏洞库、日志系统 | Python 纯逻辑封装 |
| 系统接口层 | Windows API 调用、Socket、进程管理 | ctypes + iphlpapi.dll + psutil |
依赖方向自上而下:GUI 层依赖业务层,业务层依赖系统接口层。即使将来把 PyQt5 换成 PySide6,核心业务层可以纹丝不动。
模块清单
-
PortScanner:多线程 TCP Connect 扫描器,支持协作式取消
-
ConnectionManager:通过 iphlpapi.dll 枚举/关闭 TCP/UDP 连接
-
PortDatabase:107+ 端口的 JSON 知识库,四级风险分类
-
OSFingerprint:基于开放端口组合的加权评分 OS 推断
-
VulnerabilityDatabase:20+ CVE 漏洞库(永恒之蓝、BlueKeep、SMBGhost 等)
-
AppLogger:结构化日志,格式 netinfo_YYYYMMDD_HHMMSS_主机名.log
-
MainWindow:双标签页 GUI(扫描结果 + 活动连接),19 项右键功能
0x02 攻坚难点一:用 ctypes 驯服 Windows IP Helper API
为什么不用 subprocess 调 netstat?因为解析文本脆弱得跟玻璃一样——换个 Windows 版本,输出格式一变,程序就崩了。
底层硬桥硬马,才够稳健。
IP Helper API 三件套
Windows 的 iphlpapi.dll 提供了三个关键函数:
GetExtendedTcpTable:获取 TCP 连接表(含进程 PID)
GetExtendedUdpTable:获取 UDP 连接表
SetTcpEntry:强制关闭 TCP 连接(需管理员权限)
两次调用法(避坑指南)
变长结构体是第一个大坑。连接数量动态变化,不知道缓冲区该开多大。解决方法是两次调用:
iphlpapi = ctypes.WinDLL('iphlpapi.dll')
AF_INET = 2
TCP_TABLE_OWNER_PID_ALL = 5
# 第一次调用:传空指针,获取所需缓冲区大小
size = ctypes.wintypes.DWORD(0)
iphlpapi.GetExtendedTcpTable(
None, ctypes.byref(size), False,
AF_INET, TCP_TABLE_OWNER_PID_ALL, 0
)
# 第二次调用:按大小分配缓冲区,真正取数据
buf = (ctypes.c_byte * size.value)()
iphlpapi.GetExtendedTcpTable(
buf, ctypes.byref(size), False,
AF_INET, TCP_TABLE_OWNER_PID_ALL, 0
)
指针算术破解零长度数组
C 结构体里的柔性数组(table[0])在 ctypes 里没法直接声明。我用指针偏移的方式绕过:
table = ctypes.cast(buf, ctypes.POINTER(MIB_TCPTABLE_OWNER_PID)).contents
num = table.dwNumEntries
row_ptr = ctypes.pointer(table.table[0]) # 拿首行地址当基址
for i in range(num):
row = row_ptr[i] # 指针算术,按 sizeof(row) 步进
local_ip = socket.inet_ntoa(struct.pack('<I', row.dwLocalAddr))
local_port = socket.ntohs(row.dwLocalPort & 0xFFFF)
# …
字节序的那点事儿
Windows API 里 IP 地址是主机字节序的 DWORD,端口号却是网络字节序存于低 16 位。转换函数如下:
def _dw_to_ip(dw_addr):
return socket.inet_ntoa(struct.pack('<I', dw_addr))
def _dw_to_port(dw_port):
return socket.ntohs(dw_port & 0xFFFF)
def _port_to_dw(port):
return socket.htons(port) # 用于 SetTcpEntry
跑起来的那一刻,屏幕上刷出了和 netstat -ano 一模一样的数据——但这次,数据是我从底层 API 里亲手“捞”出来的。
0x03 攻坚难点二:PyQt5 多线程与 UI 安全更新
有了数据,还得有个漂亮的“窗口”让用户看。
信号槽是跨线程通信的命脉
Qt 强制要求所有 UI 控件操作必须在主线程执行,否则随时段错误。所以我把耗时任务扔进
QThread 子类,通过 pyqtSignal 把结果安全“发射”回主线程:
class ScanWorker(QThread):
result_found = pyqtSignal(dict) # 发现一个开放端口
progress_updated = pyqtSignal(int, int) # 进度更新
scan_finished = pyqtSignal(list) # 扫描完成
scan_error = pyqtSignal(str) # 出错啦
# 工作线程里
def run(self):
for port in ports:
if self.scanner.is_open(port):
self.result_found.emit({"port": port, …})
# 主线程里
worker.result_found.connect(self._on_scan_result)
信号跨线程传递时默认采用队列模式(QueuedConnection),绝对线程安全。
表格排序的小技巧
端口号存入 QTableWidgetItem 时用 setData(Qt.DisplayRole, int):
port_item = QTableWidgetItem()
port_item.setData(Qt.DisplayRole, result["port"]) # 存整数
这样点击表头排序时是按数值大小排,而不是按字符串排——否则 “9” 会排在 “80” 后面,那画面太美我不敢看。
0x04 攻坚难点三:I/O 密集型任务的并发与协作取消
端口扫描是典型的 I/O 密集型任务,socket.connect_ex 大部分时间在等待响应或超时。
ThreadPoolExecutor 批量提交
with ThreadPoolExecutor(max_workers=self.threads) as executor:
future_to_port = {
executor.submit(self.scan_port, port): port
for port in range(self.start_port, self.end_port + 1)
}
for future in as_completed(future_to_port):
if self._cancelled: # 协作式取消检查
break
port = future_to_port[future]
is_open = future.result()
if is_open:
# 回调通知 UI
Python 的 GIL 在 I/O 阻塞时会释放,所以多线程对扫描吞吐量的提升是实打实的。
协作式取消:不杀线程,只停脚步
用户点了“取消”怎么办?暴力杀线程会留下各种不干净的状态。我采用协作式取消:
def cancel(self):
self._cancelled = True # 设置标志
# 在 as_completed 循环中每个 Future 完成时检查
for future in as_completed(future_to_port):
if self._cancelled:
break
# …
不强行中断正在执行的 connect_ex,只是不再处理新的结果。干净、安全、优雅。
实测吞吐量
在 300 线程、0.5 秒超时下,对本机 1–65535 全端口扫描:
-
耗时:109.83 秒
-
吞吐量:约 597 端口/秒
| 100 | ≈ 180 | ≈ 364 |
| 300 | 109.83 | ≈ 597 |
| 500 | ≈ 95 | ≈ 690 |
| 1000 | ≈ 90 | ≈ 728 |
从 500 到 1000 收益递减,说明本机扫描瓶颈已从并发度转向系统 socket 处理能力。
0x05 知识工程:端口风险库与 OS 指纹算法
扫出端口只是第一步,让用户看懂才是本事。
端口知识库(107+ 条目)
JSON 格式,结构清晰:
{
"445": {
"service": "SMB",
"protocol": "tcp",
"risk": "danger",
"description": "Windows文件共享",
"hazard": "勒索病毒(WannaCry)主要传播通道,永恒之蓝(MS17-010)利用此端口"
},
"22": {
"service": "SSH",
"protocol": "tcp",
"risk": "safe",
"description": "安全Shell远程登录",
"hazard": "若使用弱密码或旧版本,可能被暴力破解"
}
}
四级风险分类 + 对应颜色:
| safe | 正常 | #27ae60 | 22/SSH, 443/HTTPS |
| warning | 警告 | #f39c12 | 21/FTP, 25/SMTP |
| danger | 危险 | #e74c3c | 445/SMB, 3389/RDP |
| unknown | 未知 | #95a5a6 | 49152+ 动态端口 |
OS 指纹识别:加权评分算法
不同操作系统的典型端口组合不同:
-
Windows:135, 139, 445, 3389
-
Linux:22, 80, 443
-
macOS:22, 548, 445
评分规则:
-
端口匹配:+10 分/个
-
服务名匹配:+15 分/个
-
置信度 = 最高分 / (开放端口数 × 15) × 100,上限 100%
def _is_windows(self, open_ports):
return any(p in open_ports for p in [135, 139, 445, 3389, 1433, 5985, 5986])
# 若检测到 Windows 典型端口,置信度下限直接拉到 70%
实测扫到 [135, 139, 445, 3389] 时,准确识别为 Windows 10。
漏洞匹配库(20+ CVE)
内置三组漏洞:
-
Windows 组:永恒之蓝(CVE-2017-0144)、SMBGhost(CVE-2020-0796)、BlueKeep(CVE-2019-0708)
-
Linux 组:SSH、MySQL、Redis 相关
-
通用组:FTP、Telnet、SNMP
匹配逻辑:端口 → 服务类型 → 漏洞条目 → 结合 OS 类型过滤。对 445 + 3389 的 Windows 主机,一次性命中 4 个 Critical 级漏洞。
0x06 实战验收:109 秒扫描 6.5 万端口
测试环境
| 操作系统 | Windows 10 19045 |
| CPU | Intel i7-10700K(8 核 16 线程) |
| 内存 | 31.72 GB |
| Python | 3.11.3 |
| PyQt5 | 5.15.11 |
全端口扫描结果
2026-08-04 20:22:07 [INFO] 【开始端口扫描】 目标: 127.0.0.1 范围: 1-65535
2026-08-04 20:22:08 [INFO] [发现] 135/tcp MS-RPC 危险
2026-08-04 20:22:08 [INFO] [发现] 445/tcp SMB 危险
2026-08-04 20:22:12 [INFO] [发现] 3389/tcp RDP 危险
…
2026-08-04 20:23:57 [INFO] 【扫描完成】 耗时: 109.83秒 开放端口: 24个
2026-08-04 20:23:57 [WARNING] ⚠ 检测到 3 个高危端口!

活动连接监控
程序启动后自动拉取本机连接表,100 条连接稳定解析:
TCP 192.168.6.125:56146 -> 27.222.17.33:443 [ESTABLISHED] PID:14632 (Trae CN.exe)
TCP 192.168.6.125:57956 -> 52.242.103.142:443 [ESTABLISHED] PID:11304 (msedge.exe)
UDP 192.168.6.125:137 -> *:0 [UDP] PID:4 (System)

PID 到进程名的映射准确无误——PID 4 是 “System”,PID 11304 是 “msedge.exe (11304)”,与任务管理器完全一致。
右键菜单功能矩阵
两个表格共集成了 19 项右键功能:

| 复制端口 / 复制完整信息 | 端口扫描结果 |
| 查找该端口的服务项(本机) | 定位占用进程 |
| 通过防火墙关闭/打开该端口 | 本机快速封禁 |
| 查看已知漏洞 / 在线搜索 CVE | 安全评估 |
| 全网络检索 / 大模型分析 | 知识扩展 |
| 关闭该连接(TCP) | 紧急断连(需管理员) |
| 结束进程 / 查看进程详情 | 异常进程处置 |
| WHOIS / IP 归属地查询 | 溯源分析 |
0x07 踩坑记:那些年我掉进去的坑
ctypes.Structure 的字节对齐 Windows API 结构体默认 #pragma pack(8),ctypes 默认是 4 字节对齐。我用 _pack_ = 1 或 _pack_ = 8 强制匹配,否则偏移量全错。
UDP 连接的 remote_addr 解析 UDP 是无连接的,dwRemoteAddr 可能为 0x00000000,dwRemotePort 为 0。inet_ntoa 不能处理全零地址,得特判。
SetTcpEntry 返回 ERROR_ACCESS_DENIED 非管理员权限下关闭连接必失败。我在 UI 上明确提示“请以管理员身份运行”,并把错误码映射成中文提示。
进程名缓存过期 ConnectionManager 启动时用 psutil 一次性缓存所有进程名。但扫描过程中新进程启动不会刷新。我的对策是:缓存命中直接返回,未命中时单次查询并补充入缓存——“预取 + 懒加载”策略。
已知端口扫描的端口列表来源 如果 port_database.json 加载失败,get_all_known_ports() 返回空列表。我在初始化时加了 fallback 空数据库,并打印警告。
0x08 项目开源与后续展望
PortSentinel v0.08 已完整开源,采用 GPL v3 协议。项目地址:
👉 github.com/donoot/PortSentinel
如果你也想自己动手改一版,或者发现 bug 想提 issue,非常欢迎。
未来迭代方向
SYN 半开扫描:接入 WinPcap/Npcap,提升扫描速率
Banner 抓取 + TTL 探测:OS 指纹精度再升一档
AI 威胁分析:把端口 + CVE 摘要喂给大语言模型,输出自然语言加固方案
云端漏洞库同步:对接 NVD,每周自动拉取最新 CVE
实时连接推送:基于 ETW(Event Tracing for Windows)监控连接变化
写在最后
从那个发现满屏开放端口的早晨,到 PortSentinel 跑通全端口扫描,前后不过数日。
回顾这段经历,最大的感触是:Windows 底层 API 没那么可怕。 iphlpapi.dll 就在那里,ctypes 就是那座桥。两次调用、指针算术、字节序转换——搞懂了,你就拥有了和 netstat 一样的能力,还多出了强制关闭连接的超能力。
而 PyQt5 + QThread + 信号槽 的组合,是桌面 GUI 应用开发的黄金搭档。工作线程干重活,信号把结果安全送回主线程——UI 不卡,数据不错。
最重要的是,知识库让工具有了“灵魂”。107+ 端口的风险分级、20+ CVE 的漏洞匹配——这些让一个冷冰冰的扫描器变成了能给出安全建议的顾问。
门就在那里,推开它。
项目标签:#Python #PyQt5 #网络安全 #端口扫描 #Windows API #ctypes #开源
作者:donoot 日期:2026-08-04 版本:PortSentinel v0.08
网硕互联帮助中心




评论前必须登录!
注册