ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

VC6网络扫描器源码解析:从Socket编程到多模块实战

VC6网络扫描器源码解析:从Socket编程到多模块实战 简介这是一套面向网络安全初学者与课程设计者的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.cppNetBIOS 名称与共享信息枚举SNMPScan.cppSNMP 团体字探测与信息读取WeakKeyScan.cpp弱口令字典枚举SnifferScan.cpp关键字嗅探抓取明文提交内容DosScan.cpp多线程连接压力测试SQLScan.cpp注入点特征检测IPHLPAPI.LIB/Iphlpapi.hWindows 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开放 80192.168.0.102开放 135、139、445。这三个端口很有代表性135 是 RPC139 是 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 11然后看返回内容里有没有特征字符串。这个方法的边界很明显它依赖目标页面的特定返回内容换一个页面特征词就失效了。更通用的做法是布尔盲注检测比较真条件和假条件下页面响应的差异或者时间盲注看sleep是否生效。工程里用的是特征匹配适合教学演示实际用的时候要清楚它的适用范围。另外注入检测会往目标发畸形请求如果目标有 WAF可能直接封 IP测试前确认授权范围是底线。5. 避坑与排查多线程、权限和协议解析的五个真实翻车点5.1 现象DOS 攻击模块启动 2048 线程后本机卡死原因DosScan.cpp的测试用例是启动 2048 个线程对目标 80 端口发起连接这个线程数在 Windows 上会迅速耗尽本机的端口资源和句柄。每个线程创建一个 socket2048 个 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}/tdtd${row.port}/td td${row.protocol}/tdtd${row.status}/td td${row.time}/td; tbody.appendChild(tr); });这段代码的关键点是数据源用script标签的textContent而不是fetch因为本地打开 HTML 时fetch会被跨域策略拦掉textContent没有这个问题。字段名和 CSV 表头保持一致改起来只动一处。从那以后我每次跑扫描不管工具多顺手都强制先用nmap对一小段做交叉验证确认超时和权限没问题再放开全段。这个习惯帮我省掉过好几次「扫出来全是关闭」的尴尬。希望帮到你。本文还有配套的精品资源点击获取
返回列表