简介:这是一套面向网络安全初学者与课程设计者的C++网络扫描器完整实现方案,基于MFC框架开发,可在Windows XP及以上系统运行,帮助读者理解主机发现、端口探测与漏洞检测的底层原理。资源包共98个文件,约5.12MB,以cpp与h源码为核心,辅以png、jpg运行截图、html测试页面、dic字典文件及vcproj、sln等工程配置,覆盖主机扫描、端口扫描、NetBIOS、SNMP、弱密码、嗅探器、DOS攻击与注入检测八大功能模块,并附有paper.pdf设计文档与ReadMe说明。目前已有905人学习下载。读者可获取完整可编译的VC6工程、各模块测试用例与结果截图、注入检测Demo页面及字典文件,便于对照复现扫描流程、理解网络协议交互细节,并在此基础上进行二次开发或课程设计参考。
1. 从一份 VC6 老工程说起:网络扫描器到底能扫出什么
很多人第一次接触网络扫描,是从nmap一行命令开始的,但真要把「扫描」这件事拆开看,你会发现它背后是一整套套接字编程、协议解析和并发控制的组合拳。这份Network_Scanner-master就是一套把常见扫描能力全部塞进 MFC 界面的 C++ 工程,运行在 Windows XP 及以上,用 VC6.0 编译,源码里能看到HostScan.cpp、PortScan.cpp、NetBIOSScan.cpp、SNMPScan.cpp、WeakKeyScan.cpp、SnifferScan.cpp、DosScan.cpp、SQLScan.cpp这些模块文件,基本覆盖了主机存活探测、端口开放探测、NetBIOS 信息枚举、SNMP 信息读取、弱口令字典枚举、关键字嗅探、连接压力测试和注入点检测这几类典型场景。它适合两类人:一类是正在做课程设计或毕设、需要一份能跑起来、能截图、能讲清原理的 C++ 网络编程参考;另一类是想从「调库」回到「自己写 socket」的从业者,拿它当一份可读可改的协议实现样本。下面我按「先跑通、再拆模块、最后说坑」的顺序,把这份工程从头到尾过一遍。
2. 把工程跑起来:VC6 编译、依赖库与第一次主机扫描
2.1 环境准备与工程文件结构
这套代码的目标平台写得很明确:Windows XP 及以上,开发工具 Microsoft Visual C++ 6.0,语言 C/C++ 加 MFC。工程主文件是Scanner.dsw和Scanner.dsp,这是 VC6 时代的 workspace 和 project 文件,用 VS2008 之后的版本打开会提示升级,升级过程里 MFC 相关的资源脚本Scanner.rc、resource.h一般不会出问题,但IPHLPAPI.LIB这类静态库引用需要确认路径还在。目录里还有Scanner.sln和Scanner.vcproj,说明作者后来用更高版本 VS 也维护过一版,如果你本地是 VS2010 以上,优先走.sln这条线,能省掉不少字符集和库路径的麻烦。
工程里几个关键文件的分工是这样的:
| 文件 | 作用 |
|---|---|
ScannerDlg.cpp/ScannerDlg.h | 主对话框逻辑,各扫描模块的入口按钮都挂在这里 |
HostScan.cpp | 主机存活扫描,基于 ICMP 或 TCP 探测 |
PortScan.cpp | 端口扫描,支持指定 IP 段和端口范围 |
NetBIOSScan.cpp | NetBIOS 名称与共享信息枚举 |
SNMPScan.cpp | SNMP 团体字探测与信息读取 |
WeakKeyScan.cpp | 弱口令字典枚举 |
SnifferScan.cpp | 关键字嗅探,抓取明文提交内容 |
DosScan.cpp | 多线程连接压力测试 |
SQLScan.cpp | 注入点特征检测 |
IPHLPAPI.LIB/Iphlpapi.h | Windows IP 辅助 API,取本机网卡和路由信息 |
ReadMe.txt和README.md里通常写了编译顺序和依赖说明,动手前先读一遍,比直接点编译省时间。
2.2 编译依赖与常见报错处理
VC6 工程最容易翻车的地方不是代码本身,而是库和字符集。IPHLPAPI.LIB是 Windows 自带的,但 VC6 默认的库搜索路径不一定能找到,需要在Project Settings → Link → Object/library modules里显式加上IPHLPAPI.LIB,或者把Iphlpapi.h和IPEXPORT.H、IPTYPES.H这几个头文件的路径加到Additional include directories。如果编译时报cannot open file "IPHLPAPI.LIB",八成是 SDK 没装全,装一个 Platform SDK 或者把库文件从系统目录拷到工程目录都能解决。
另一个高频问题是 MFC 版本。VC6 用的是 MFC 4.2,如果你用 VS2019 打开.dsw,它会提示升级到 MFC 14,升级后CString到LPCTSTR的隐式转换、AfxMessageBox的参数类型都可能报错。常见做法是先把项目字符集从 Unicode 改成多字节,再逐个修const char*和CString混用的地方。这一步没有捷径,但报错信息基本都指向具体行号,改起来不玄学。
2.3 主机扫描模块:从 IP 段到存活列表
主机扫描的测试用例在摘要里写得很清楚:起始 IP192.168.0.100,结束 IP192.168.0.103,结果返回两台存活主机192.168.0.100和192.168.0.102。这个模块的核心逻辑是把 IP 段展开成单个地址,然后对每个地址发探测包。常见做法有两种:一种是发 ICMP Echo,也就是 ping;另一种是对常见端口发 TCP SYN 或 connect,看是否有响应。这份工程里HostScan.cpp走的是哪种,编译后看抓包最直接,但从代码结构看,它用了IPHLPAPI取本机信息,探测部分大概率是原始套接字加 ICMP。
下面这段是 IP 段展开的典型写法,你可以对照HostScan.cpp里的循环看:
// 将起始和结束 IP 展开为单个地址列表 DWORD start = ntohl(inet_addr("192.168.0.100")); DWORD end = ntohl(inet_addr("192.168.0.103")); for (DWORD ip = start; ip <= end; ip++) { struct in_addr addr; addr.s_addr = htonl(ip); CString strIP = inet_ntoa(addr); // 对 strIP 发起存活探测,结果写入结果列表 if (ProbeHost(strIP)) { m_ResultList.AddString(strIP + _T(" 存活")); } }inet_addr返回的是网络字节序,ntohl转成主机字节序后才能做数值比较,循环结束再htonl转回去。这个转换如果漏了,IP 段会乱序甚至死循环,是新手最容易踩的坑之一。ProbeHost的具体实现决定了扫描速度和准确率:ICMP 方式快但可能被防火墙拦,TCP 方式准但慢。工程里如果两种都支持,界面上一般会有选项切换。
提示:在 Windows 上发原始 ICMP 包需要管理员权限,普通用户运行会直接失败,编译后记得右键「以管理员身份运行」。
3. 端口、NetBIOS 与 SNMP:三个信息收集模块的拆解
3.1 端口扫描:1~1024 范围内的 connect 探测
端口扫描的测试用例是 IP 段192.168.0.100到192.168.0.102,端口范围1~1024,结果192.168.0.100开放 80,192.168.0.102开放 135、139、445。这三个端口很有代表性:135 是 RPC,139 是 NetBIOS 会话服务,445 是 SMB,一台 Windows 主机同时开这三个口,基本可以判断是没做端口过滤的默认配置。
PortScan.cpp的实现思路通常是connect扫描:对目标 IP 的每个端口调connect,返回成功就是开放,返回WSAECONNREFUSED就是关闭,超时就是被过滤。这种方式的优点是实现简单、不需要原始套接字权限,缺点是慢,因为每个端口都要等超时。1024 个端口串行扫,按每个超时 1 秒算,最坏情况要十几分钟。工程里如果做了多线程,速度会好很多,但线程数不是越多越好,后面避坑章节会细说。
// 单端口 connect 探测 SOCKET sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); struct sockaddr_in target; target.sin_family = AF_INET; target.sin_port = htons(port); target.sin_addr.s_addr = inet_addr(ip); // 设置非阻塞和超时,避免卡死 u_long mode = 1; ioctlsocket(sock, FIONBIO, &mode); int ret = connect(sock, (struct sockaddr*)&target, sizeof(target)); if (ret == 0) { // 立即成功,端口开放 } else if (WSAGetLastError() == WSAEWOULDBLOCK) { // 用 select 等待可写,再判断是否真正连上 } closesocket(sock);非阻塞connect返回WSAEWOULDBLOCK是正常现象,需要用select监听可写事件,再通过getsockopt(SO_ERROR)取真实结果。如果直接把WSAEWOULDBLOCK当成失败,会漏掉大量开放端口,这是 connect 扫描最经典的错误。
3.2 NetBIOS 扫描:137 端口与名称表解析
NetBIOS 扫描模块选192.168.0.102作为目标,结果与实际相符。NetBIOS 名称服务跑在 UDP 137 上,查询方式是发一个 NetBIOS 名称查询包,解析返回的名称表。名称表里能看到主机名、工作组、域控标识等信息,对内网资产梳理很有用。NetBIOSScan.cpp里一般会构造一个NBSTAT查询,然后按 RFC 1002 的格式解析响应。
名称表里每个条目 18 字节,前 15 字节是名称,第 16 字节是后缀,最后 2 字节是标志位。后缀含义需要对照表看,比如<00>是工作站服务,<20>是文件服务器服务,<1C>是域控。解析时如果按固定偏移读,遇到名称里有填充的0x20要跳过,否则会读出乱码。这个模块的价值在于:端口扫描只能告诉你 139 开着,NetBIOS 扫描能告诉你这台机器叫什么、属于哪个工作组,信息维度完全不同。
3.3 SNMP 扫描:团体字与 MIB 树遍历
SNMP 扫描同样以192.168.0.102为目标,结果与实际相符。SNMP 默认跑在 UDP 161,用团体字(community string)做认证,常见默认值是public和private。SNMPScan.cpp的核心是构造 SNMP GET 或 GETNEXT 请求,遍历 MIB 树拿系统信息,比如sysDescr、sysName、sysUpTime。
团体字探测的逻辑很简单:拿一个候选团体字发请求,有响应就说明团体字正确。工程里如果只试public,命中率取决于目标配置,很多设备改过默认值就扫不到。更实用的做法是内置一个小字典,public、private、cisco、admin都试一遍。SNMP 返回的数据是 BER 编码的,解析时需要按 TLV 格式逐层拆,这部分代码在SNMPScan.cpp里占的篇幅不小,读的时候重点看长度字段的处理,长度超过 127 字节时用的是多字节编码,漏了这一步解析就会错位。
注意:SNMP 和 NetBIOS 都是 UDP 协议,扫描时不要用 TCP 的超时逻辑去套,UDP 没有连接状态,只能靠
recvfrom的返回值和超时判断。
4. 弱口令、嗅探与注入检测:三个「结果导向」模块的实现边界
4.1 弱密码扫描:字典枚举与账号锁定风险
弱密码扫描模块的测试用例是目标192.168.0.102,用户名test,密码用字典文件枚举,结果与实际相符。WeakKeyScan.cpp的实现通常是针对特定服务做登录尝试,比如 SMB、FTP、Telnet 或者 Web 表单。字典文件的格式一般是一行一个密码,代码里逐行读取,拼成登录请求发出去,根据返回判断成功与否。
这里有一个必须说清的边界:字典枚举会触发账号锁定策略。Windows 默认域策略是多次失败后锁定账号,如果你拿这份工具去扫生产环境,很可能把正常账号锁掉,这是血泪经验。测试时要么在隔离环境,要么把尝试间隔拉长,要么先确认目标没有锁定策略。工程里如果没做失败计数和延迟,你自己加一个Sleep是最简单的缓解办法。
// 逐行读取字典并尝试登录 CStdioFile dictFile; if (dictFile.Open(_T("passwords.txt"), CFile::modeRead)) { CString line; while (dictFile.ReadString(line)) { line.Trim(); if (line.IsEmpty()) continue; if (TryLogin(target, user, line)) { // 命中弱口令,记录并退出 break; } Sleep(500); // 降低触发锁定的概率 } dictFile.Close(); }TryLogin的具体协议取决于目标服务,SMB 用NetUseAdd或者直接发 SMB 协商包,Web 表单就是构造 POST。Sleep(500)不是必须的,但在没有锁定策略确认的情况下,它是成本最低的后悔药。
4.2 嗅探器扫描:关键字匹配与明文协议
嗅探器模块的测试方式是添加Pass、Password、pwd三个关键字监听本机,然后通过一个登录表单提交用户名密码,成功拦截到数据包。SnifferScan.cpp用的是原始套接字SOCK_RAW加SIO_RCVALL,把网卡设成混杂模式,抓所有经过的包,然后在 payload 里做关键字匹配。
这个模块能工作的前提是目标协议是明文的。HTTP 表单提交、FTP 登录、Telnet 都是明文,关键字能直接命中;HTTPS 抓到的只是密文,匹配不到任何东西。工程里如果只做了字符串查找,没有解析 TCP 流重组,跨包的关键字会漏掉,比如Pass和word分在两个包里就匹配不上。要解决这个问题,需要按 TCP 序列号做流重组,代码量会大不少,这也是这类工具和成熟抓包工具之间的差距所在。
4.3 注入检测:welcome 特征与误报控制
注入检测模块的测试环境是作者自己搭的login.html加respone.php,登录成功返回welcome+用户名,失败返回The username or password is wrong!。检测逻辑是把welcome当作注入成功的标志,如果登录后页面出现welcome,就判断存在注入漏洞。SQLScan.cpp里一般会构造常见的注入 payload,比如' or '1'='1,然后看返回内容里有没有特征字符串。
这个方法的边界很明显:它依赖目标页面的特定返回内容,换一个页面特征词就失效了。更通用的做法是布尔盲注检测,比较真条件和假条件下页面响应的差异,或者时间盲注,看sleep是否生效。工程里用的是特征匹配,适合教学演示,实际用的时候要清楚它的适用范围。另外,注入检测会往目标发畸形请求,如果目标有 WAF,可能直接封 IP,测试前确认授权范围是底线。
5. 避坑与排查:多线程、权限和协议解析的五个真实翻车点
5.1 现象:DOS 攻击模块启动 2048 线程后本机卡死
原因:DosScan.cpp的测试用例是启动 2048 个线程对目标 80 端口发起连接,这个线程数在 Windows 上会迅速耗尽本机的端口资源和句柄。每个线程创建一个 socket,2048 个 socket 加上 TIME_WAIT 状态,本机网络栈直接过载,表现就是界面无响应、鼠标卡顿。
解决:把线程数降到 200 以内,或者改用 IOCP 做异步连接,不要一个线程一个 socket。测试压力功能时先在本机或隔离环境跑,确认资源占用可控再换目标。另外,TIME_WAIT堆积可以通过设置SO_REUSEADDR和调整注册表TcpTimedWaitDelay缓解,但根本办法还是控制并发量。
5.2 现象:主机扫描在部分机器上全部返回「不存活」
原因:ICMP 探测被目标防火墙拦截,或者本机没有管理员权限导致原始套接字创建失败。HostScan.cpp如果只走 ICMP 一条路,遇到禁 ping 的主机就会全部漏报。
解决:加 TCP 探测作为补充,对 80、135、445 这些常见端口做 connect,只要有一个通就判定存活。同时检查程序是否以管理员身份运行,WSAEACCES错误就是权限不足的信号。
5.3 现象:端口扫描结果里开放端口比实际少
原因:非阻塞connect的错误处理写错了,把WSAEWOULDBLOCK当成失败直接跳过,或者select的超时设得太短,慢速响应的端口还没连上就被判死。
解决:按 3.1 节的写法,用select等可写,再用getsockopt(SO_ERROR)取真实错误码,超时设 1 到 2 秒。如果目标跨网段,超时还要放宽。
5.4 现象:NetBIOS 扫描返回的名称是乱码
原因:名称表里的名称字段是固定 15 字节,不足部分用空格或0x00填充,解析时没做 trim,或者把后缀字节当成了名称的一部分。
解决:读名称时只取前 15 字节,遇到0x00或0x20截断,第 16 字节单独拿出来查后缀表。编码上 NetBIOS 名称是 OEM 字符集,不是 UTF-8,转码时用MultiByteToWideChar指定CP_OEMCP。
5.5 现象:嗅探模块抓不到任何包
原因:SIO_RCVALL需要绑定到具体网卡 IP,绑定INADDR_ANY在部分 Windows 版本上不生效;或者没有管理员权限,原始套接字创建失败。
解决:先用GetAdaptersInfo枚举本机网卡,让用户选一个具体 IP 再绑定。权限问题看WSAGetLastError返回的WSAEACCES,以管理员身份重跑即可。
6. 进阶用法:把扫描结果落成可复用的报告与验证习惯
工程里带了一个rebort.html,从命名看是扫描结果输出成 HTML 报告的模板。这个思路值得展开:扫描本身只是拿数据,数据不落成结构化格式,后面没法做对比和复盘。我一般会做两件事,一是把每次扫描的结果按「时间戳 + 目标段 + 模块名」存成 CSV,二是用rebort.html做展示层,把 CSV 渲染成表格。这样同一段 IP 隔一周再扫,直接 diff 两个 CSV 就能看出端口变化。
验证扫描结果准确性有一个简单办法:拿nmap做交叉验证。同一段 IP、同一端口范围,分别用这份工具和nmap -sT扫一遍,对比开放端口列表。如果差异超过一两个,说明超时或错误处理有问题,回去查 5.3 节。主机存活也可以用nmap -sn对照。交叉验证不是不信任自己的代码,而是扫描这件事本身受网络环境影响太大,单一工具的结论不够稳。
报告输出这部分,rebort.html如果只是静态模板,可以自己加一段 JavaScript 读取内嵌的 JSON 数据渲染表格,这样不用每次改 HTML。数据格式建议至少包含 IP、端口、协议、状态、扫描时间五个字段,少了任何一个,后面做趋势分析都要返工。
// 从内嵌 JSON 渲染扫描结果表格 const data = JSON.parse(document.getElementById('scan-data').textContent); const tbody = document.querySelector('#result-table tbody'); data.forEach(row => { const tr = document.createElement('tr'); tr.innerHTML = `<td>${row.ip}</td><td>${row.port}</td> <td>${row.protocol}</td><td>${row.status}</td> <td>${row.time}</td>`; tbody.appendChild(tr); });这段代码的关键点是数据源用script标签的textContent而不是fetch,因为本地打开 HTML 时fetch会被跨域策略拦掉,textContent没有这个问题。字段名和 CSV 表头保持一致,改起来只动一处。
从那以后我每次跑扫描,不管工具多顺手,都强制先用nmap对一小段做交叉验证,确认超时和权限没问题再放开全段。这个习惯帮我省掉过好几次「扫出来全是关闭」的尴尬。希望帮到你。
本文还有配套的精品资源,点击获取