
别再下那些来路不明的绿色版了我用 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………………………………135RPC——冲击波蠕虫的老巢445SMB——WannaCry 勒索病毒就是从这杀进来的3389RDP——BlueKeep 漏洞能让攻击者不认证就执行任意代码。新机器像没锁门的仓库门外就是整个互联网。怎么办用Nmap参数比我头发还多查手册翻到手抽筋。用TCPViewSysinternals 出品虽好但闭源、无风险评级、无漏洞库。用CurrPorts缺日志、缺知识库、缺本地化中文。去网上下个绿色版谁知道是哪年的版本有没有被植入后门一个念头冒了出来与其用别人的刀不如自己铸一把。不就是端口扫描加连接监控嘛——Windows 底层 API 摆在那里Python 的ctypes能调PyQt5能做界面我决定自己写一个。这个项目的名字就是PortSentinel目前v0.08版本已开源在 GitHubgithub.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 连接PortDatabase107 端口的 JSON 知识库四级风险分类OSFingerprint基于开放端口组合的加权评分 OS 推断VulnerabilityDatabase20 CVE 漏洞库永恒之蓝、BlueKeep、SMBGhost 等AppLogger结构化日志格式netinfo_YYYYMMDD_HHMMSS_主机名.logMainWindow双标签页 GUI扫描结果 活动连接19 项右键功能0x02 攻坚难点一用 ctypes 驯服 Windows IP Helper API为什么不用subprocess调netstat因为解析文本脆弱得跟玻璃一样——换个 Windows 版本输出格式一变程序就崩了。底层硬桥硬马才够稳健。IP Helper API 三件套Windows 的iphlpapi.dll提供了三个关键函数GetExtendedTcpTable获取 TCP 连接表含进程 PIDGetExtendedUdpTable获取 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_workersself.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: # 回调通知 UIPython 的 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≈ 364300109.83≈ 597500≈ 95≈ 6901000≈ 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正常#27ae6022/SSH, 443/HTTPSwarning警告#f39c1221/FTP, 25/SMTPdanger危险#e74c3c445/SMB, 3389/RDPunknown未知#95a5a649152 动态端口OS 指纹识别加权评分算法不同操作系统的典型端口组合不同Windows135, 139, 445, 3389Linux22, 80, 443macOS22, 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 19045CPUIntel i7-10700K8 核 16 线程内存31.72 GBPython3.11.3PyQt55.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可能为0x00000000dwRemotePort为 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实时连接推送基于 ETWEvent 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