简介:一套基于Visual C++ 6.0开发的可直接编译运行的网络入侵检测系统源码,专注局域网环境下的实时流量捕获与深度协议解析。支持以太网帧、IP、ICMP、TCP、UDP、ARP等主流协议的逐层解码,通过内置规则引擎匹配异常通信行为。提供图形化操作界面,涵盖网络设备选择、BPF过滤参数设置、自定义入侵规则配置等功能模块。核心逻辑分离清晰:sniffer.cpp负责原始数据包捕获,Protocolanalysis.cpp统筹协议识别流程,各协议解析文件(如 tcpprotocol.cpp、ipprotocol.cpp、arpprotocol.cpp 等)独立实现对应协议字段提取与校验。配套完整VC6工程文件(.dsw/.dsp)、资源定义(resource.h)、多组对话框类(DeviceDialog、FilterDlg、IntrusionRuleDialog等),适用于网络协议分析教学、IDS底层机制理解或小型实验环境中的基础攻击特征识别。
1. 项目概述:为什么在2024年还要看VC6.0写的IDS?
你点开这个标题,第一反应可能是:“VC6.0?那不是Windows 98时代的老古董吗?”——没错,它确实是1998年发布的IDE,连.NET Framework都还没影子。但恰恰是这套“过时”的代码,藏着今天很多现代IDS教学里被层层封装、刻意隐藏的最原始、最赤裸的协议处理逻辑。它不依赖WinPcap/Npcap的高级封装接口,不调用libpcap的跨平台抽象层,而是直接和Windows原始套接字(SOCK_RAW)、NDIS驱动底层打交道;它不用JSON/YAML写规则,而是在内存里用结构体数组硬编码匹配条件;它的图形界面没有Qt或WPF的自动布局,每个按钮、下拉框、编辑框的位置都是用像素坐标手敲出来的。
我带过三届网络工程专业的毕业设计,发现一个普遍现象:学生能熟练配置Snort规则、会用Wireshark过滤HTTP请求,但一旦让你手写一段代码,从网卡收到的原始字节流中准确提取出TCP窗口大小字段、判断SYN-FIN同时置位是否异常、或者根据IP头校验和反推原始数据包是否被篡改——90%的人卡在第一步:不知道那个字节到底该从第几个偏移量开始读。而这套VC6.0源码,就是一本“可执行的《TCP/IP详解》卷一”,它把每一层协议头的字段位置、长度、字节序、校验算法,全部摊开在.cpp文件里,用最朴素的memcpy、ntohs、ntohl和指针偏移来实现。比如ipprotocol.cpp里这行:
iphdr->ihl = (ip_header[0] & 0x0F); // IP首部长度,单位是4字节,取低4位
它没用任何宏定义或类封装,就这一行,告诉你IP头长度藏在第一个字节的低4位里——这就是协议解析的起点,也是所有IDS规则匹配的根基。
这套系统适合谁?不是想部署生产环境的企业安全工程师(它没有日志归档、没有分布式协同、没有机器学习模型),而是三类人:
- 刚学完计算机网络课程的大三学生:你想知道“三次握手”在内存里到底长什么样?打开tcpprotocol.cpp,看它怎么从tcp_header[12]开始解析标志位,怎么用(tcp_header[12] & 0x02)判断SYN位是否置1;
- 准备讲授网络安全实验课的高校教师:你需要一套不依赖外部库、编译即跑、每行代码都可控的教学案例,让学生亲手修改规则、注入伪造包、观察检测结果;
- 嵌入式/工控领域开发者:你的设备资源极有限(64MB内存、无GUI),需要理解轻量级协议解析的核心骨架——这套VC6.0工程去掉MFC界面后,核心嗅探+解析模块不足50KB,正是这种极致精简的思路值得借鉴。
它解决的不是“如何建一个企业级SOC平台”,而是“当一个数据包以光速撞上网卡,你的程序第一毫秒该做什么”。关键词里的“协议解析”“网络嗅探”“TCP/IP分析”,在这里不是术语,而是sniffer.cpp里WSAIoctl(sock, SIO_RCVALL, ...)调用后的32768字节缓冲区,“入侵检测”不是AI模型输出的“可疑概率0.92”,而是Protocolanalysis.cpp里一个for循环遍历规则数组,对每个包逐字段比对的硬逻辑。接下来,我们就一层层拆解这个“老古董”里藏着的硬核功夫。
2. 整体架构与设计思路:为什么用VC6.0?为什么不用现成库?
2.1 选择VC6.0的深层考量:教学透明性压倒一切
很多人看到VC6.0第一反应是“兼容性差”“不支持STL”“调试器简陋”,但恰恰是这些“缺陷”,成就了它的教学价值。我们来对比三种常见开发路径:
| 开发方式 | 优点 | 教学盲区 | 本项目选择理由 |
|---|---|---|---|
| 现代C++ + libpcap + Qt | 跨平台、功能全、界面美观 | pcap_next_ex()返回的u_char*缓冲区如何映射到IP头?struct iphdr定义在哪?Qt信号槽如何触发规则匹配?学生只看到API调用,看不到内存布局 | 放弃易用性,换取零抽象层:所有协议解析直接操作原始字节,所有网络调用直面Windows Sockets API |
| Python + Scapy | 快速原型、语法简洁、社区丰富 | pkt[IP].src背后是Scapy自动生成的getfield()方法,学生无法理解IP地址如何从4字节整数转为点分十进制字符串 | 要求学生亲手写inet_ntoa()等效逻辑,强化字节序、网络字节序转换意识 |
| VC6.0 + 原始套接字 | 编译产物小(<1MB)、无运行时依赖、内存布局完全可控 | 需手动处理MFC消息循环、资源ID管理繁琐 | 教学确定性最高:同一份代码,在Windows 2000/XP/7上行为一致,不会因系统更新导致WSAStartup()失败 |
VC6.0的“落后”反而成了优势:它强制你面对每一个底层细节。比如sniffer.cpp中创建原始套接字的关键代码:
sock = socket(AF_INET, SOCK_RAW, IPPROTO_IP);
// 必须设置SOCK_RAW才能捕获所有IP包
if (sock == INVALID_SOCKET) { /* 错误处理 */ }
// 关键一步:启用混杂模式,让网卡接收所有经过的帧
DWORD dwValue = 1;
WSAIoctl(sock, SIO_RCVALL, &dwValue, sizeof(dwValue), NULL, 0, &dwBytes, NULL, NULL);
这段代码在现代VS中可能被封装进PacketCapture::StartPromiscuousMode()方法里,学生只管调用。但在VC6.0里,你必须亲手填SIO_RCVALL这个常量,查MSDN确认dwValue=1代表开启,否则网卡只收发给自己IP的包——这就是真实世界里“混杂模式”的代价:不是勾个选项框,而是向操作系统内核发送一个特定控制码。
再比如资源管理。现代IDE自动生成resource.h并关联对话框类,而这里你需要手动维护:
// resource.h 中定义
#define IDD_DEVICE_DIALOG 101
#define IDC_COMBO_DEVICE 1001
#define IDC_BUTTON_START 1002
// ...
然后在DeviceDialog.cpp里,DoDataExchange()函数必须严格按此ID绑定控件:
void CDeviceDialog::DoDataExchange(CDataExchange* pDX)
{
CDialog::DoDataExchange(pDX);
DDX_Control(pDX, IDC_COMBO_DEVICE, m_comboDevice); // ID必须和resource.h一致
DDX_Control(pDX, IDC_BUTTON_START, m_btnStart);
}
这种“笨办法”看似低效,却让学生深刻理解:GUI界面的本质是一组内存中的控件句柄和消息映射表,而不是魔法生成的XML。当你在IntrusionRuleDialog里双击添加一条规则,背后是OnAddRule()函数向m_rules数组插入一个RULE_ITEM结构体,而不是调用某个ORM框架的save()方法。
2.2 模块化设计:解耦嗅探、解析、检测三层逻辑
整个系统严格遵循“关注点分离”原则,将网络入侵检测拆解为三个正交模块,每个模块职责单一、接口清晰:
-
数据捕获层(Sniffer):由
sniffer.cpp/h实现,唯一职责是“把网卡收到的原始字节流喂给上层”。它不关心协议类型,不解析任何字段,只做两件事:① 初始化原始套接字并设为混杂模式;② 在while(1)循环中调用recvfrom()持续读取缓冲区。其输出是一个PACKET_INFO结构体:
cpp struct PACKET_INFO { BYTE* pData; // 指向原始字节流的指针 DWORD dwSize; // 数据包总长度(含以太网帧头) DWORD dwTimestamp; // 接收时间戳(毫秒级) char szDeviceName[64]; // 捕获设备名(如"\\Device\\NPF_{...}") };
这个结构体就是上下层之间的契约——解析层只认这个格式,不管数据从哪来。 -
协议解析层(Protocol Analysis):由
Protocolanalysis.cpp/h统筹,ethernetprotocol.cpp、ipprotocol.cpp等具体实现。它接收PACKET_INFO,按OSI模型从底向上逐层解包:
1. 先读以太网帧头(14字节),判断ether_type是0x0800(IPv4)还是0x0806(ARP);
2. 若是IPv4,则跳过14字节,读IP头(iphdr->ihl * 4字节),校验IP校验和;
3. 根据iphdr->protocol字段(如6=TCP,17=UDP),决定调用tcpprotocol.cpp还是udpprotocol.cpp;
4. TCP解析器再读tcp_header[12]获取标志位,读tcp_header[14]获取数据偏移量,最终定位应用层载荷起始位置。
这种“链式解析”设计,让新增协议(如ICMPv6)只需编写icmpv6protocol.cpp并注册到解析调度表,无需改动主逻辑。
- 入侵检测层(Rule Engine):由
intrusiondetect.cpp和IntrusionRuleDialog.cpp驱动。它不直接接触原始字节,而是接收解析层输出的PROTOCOL_CONTEXT结构体:
cpp struct PROTOCOL_CONTEXT { int nLayer; // 当前协议层(ETH=1, IP=2, TCP=3...) union { ETHERNET_HEADER eth; IP_HEADER ip; TCP_HEADER tcp; UDP_HEADER udp; ICMP_HEADER icmp; }; BYTE* pPayload; // 应用层载荷指针(如HTTP请求体) DWORD dwPayloadLen; // 载荷长度 };
规则匹配引擎(CheckRules()函数)遍历用户配置的规则数组,对每个PROTOCOL_CONTEXT字段进行硬比对。例如一条“阻断SYN Flood”的规则:
cpp RULE_ITEM rule = { .nProtocol = IPPROTO_TCP, .nFlags = TCP_FLAG_SYN, // 只匹配SYN位为1 .nSrcPort = 0, // 源端口不限 .nDstPort = 0, // 目的端口不限 .nThreshold = 100, // 1秒内超过100个SYN包即告警 .szAction = "ALERT" };
这种三层解耦,使得你可以单独测试任一模块:用sniffer.cpp生成测试包存为二进制文件,用Protocolanalysis.cpp离线解析验证字段提取正确性,最后用intrusiondetect.cpp加载规则验证匹配逻辑——这是现代微服务架构的思想,早在20年前就被VC6.0程序员用结构体和函数指针实现了。
2.3 图形界面设计:MFC对话框驱动的配置闭环
图形界面不是装饰品,而是整个检测流程的控制中枢。它通过四个核心对话框构成完整配置闭环:
-
设备选择对话框(
DeviceDialog):调用GetAdaptersInfo()枚举本机所有网络适配器,显示名称(如“本地连接”)、描述(如“Realtek PCIe GbE Family Controller”)和GUID。用户选择后,对话框将适配器GUID传给sniffer.cpp用于bind()绑定。这里有个关键细节:VC6.0不支持GetAdaptersAddresses()(Vista后引入),所以必须用GetAdaptersInfo()配合IP_ADAPTER_INFO结构体,手动解析AdapterName字段——这正是Windows网络编程的“历史包袱”,学生必须直面。 -
过滤参数对话框(
FilterDlg):实现BPF(Berkeley Packet Filter)语法的简化版。用户输入类似ip and tcp and port 80的字符串,对话框将其编译为BPF字节码(bpf_program结构体),传递给setsockopt(sock, SOL_SOCKET, SO_ATTACH_FILTER, ...)。源码中filterdlg.cpp包含一个微型BPF编译器,将文本规则转为指令数组,例如port 80会被编译为:
ldh [12] // 加载以太网类型(偏移12) jeq #0x0800, continue, drop // 如果是IPv4则继续 ldh [20] // 加载IP协议字段(偏移20) jeq #6, continue, drop // 如果是TCP则继续 ldh [36] // 加载TCP目的端口(IP头长20+TCP头长16=36) jeq #80, pass, drop // 如果是80端口则放行 -
入侵规则配置对话框(
IntrusionRuleDialog):提供表格化界面添加规则。每行对应一个RULE_ITEM,字段包括协议类型(下拉框)、源/目的IP(支持CIDR如192.168.1.0/24)、端口范围(1-1024)、TCP标志位(复选框组)、动作(告警/阻断)。点击“添加”时,对话框调用AddRule()将结构体插入全局g_rules数组,并序列化到rules.dat文件——这就是最原始的规则持久化。 -
主监控窗口(
CMainFrame):集成实时流量统计(每秒包数、协议分布饼图)、告警日志列表(双击可查看原始包十六进制视图)、以及启动/停止按钮。其核心是OnTimer()消息响应函数,每500ms刷新一次界面,从sniffer.cpp的共享缓冲区读取新包,触发解析和检测流程。
这套MFC界面的价值在于:它把抽象的安全概念(“选择网卡”“设置过滤器”“定义规则”)转化为学生可触摸、可调试的具体操作。当你在DeviceDialog里看到m_comboDevice.AddString(pAdapter->Description)这行代码,你就明白了“网络适配器描述”在Windows里就是一个字符串;当你在FilterDlg里调试CompileBPF()函数,你就知道了Wireshark的显示过滤器背后是怎样的字节码引擎。
3. 核心协议解析实现:从以太网帧到TCP标志位的逐字节解剖
3.1 以太网帧解析:MAC地址、类型字段与帧校验的硬核处理
以太网帧是所有网络通信的物理载体,它的解析是整个协议栈的基石。ethernetprotocol.cpp的实现极其朴素,却直击本质。我们来看它如何从PACKET_INFO.pData指向的原始字节流中,精准定位并提取关键字段。
首先明确以太网II帧格式(本项目仅支持此格式,不处理802.3/LLC):
| 目的MAC(6) | 源MAC(6) | 类型(2) | 数据(46-1500) | FCS(4) |
注意:FCS(帧校验序列)由网卡硬件计算并校验,通常不交付给上层软件,因此recvfrom()返回的数据不包含最后4字节FCS。这意味着PACKET_INFO.dwSize给出的长度,就是从目的MAC到数据结束的总字节数。
ParseEthernetHeader()函数的实现如下(已简化注释):
BOOL ParseEthernetHeader(BYTE* pBuf, DWORD dwSize, ETHERNET_HEADER* pEthHdr)
{
// 1. 长度检查:以太网帧最小长度64字节(含FCS),不含FCS则最小60字节
if (dwSize < 60) return FALSE;
// 2. 提取目的MAC地址(前6字节)
memcpy(pEthHdr->dst_mac, pBuf, 6);
// 3. 提取源MAC地址(第6-11字节)
memcpy(pEthHdr->src_mac, pBuf + 6, 6);
// 4. 提取类型字段(第12-13字节),注意网络字节序(大端)
// pBuf[12]是高位,pBuf[13]是低位,需组合为16位整数
pEthHdr->type = (pBuf[12] << 8) | pBuf[13];
// 5. 计算有效载荷起始位置:以太网头固定14字节
pEthHdr->payload_offset = 14;
pEthHdr->payload_len = dwSize - 14;
return TRUE;
}
这里有几个极易被忽略但至关重要的细节:
-
MAC地址的存储顺序:
memcpy(pEthHdr->dst_mac, pBuf, 6)直接复制,因为MAC地址本身是字节流,没有字节序概念。但后续在规则匹配中,若要比较MAC地址,必须用memcmp()而非数值比较——这是初学者常犯的错误,以为00-11-22-33-44-55可以转成整数比较。 -
类型字段的字节序处理:以太网类型字段(如
0x0800表示IPv4)在网络上传输时是大端序(高位在前),而x86 CPU是小端序。pBuf[12] << 8 | pBuf[13]这行代码,正是手动完成“网络字节序→主机字节序”的转换。如果错误地写成pBuf[13] << 8 | pBuf[12],所有协议识别都会失败。这个细节在Wireshark源码里也存在,只是被ntohs()宏封装了。 -
最小帧长的现实意义:
dwSize < 60的检查,不仅是协议合规性要求,更是防御“碎片攻击”的第一道防线。攻击者可能发送超短帧(如只有10字节)试图绕过检测,此检查直接丢弃。
实操心得:我在调试时曾遇到一个诡异问题——某些ARP包被解析为“未知类型”。追踪发现,部分网卡驱动在混杂模式下会返回带VLAN标签的帧(802.1Q),此时以太网头变为18字节(增加4字节标签),type字段移到了pBuf[16]。解决方案是在ParseEthernetHeader()开头增加VLAN检测:
// 检测VLAN标签:类型字段为0x8100表示有VLAN
if (pEthHdr->type == 0x8100) {
// 跳过4字节VLAN标签,重新读取真正的类型字段
pEthHdr->type = (pBuf[16] << 8) | pBuf[17];
pEthHdr->payload_offset = 18; // VLAN帧头长18字节
}
这个补丁虽小,却体现了真实网络环境的复杂性——教科书上的标准帧,在现实中总会有例外。
3.2 IP协议解析:首部长度、校验和与分片重组的实战逻辑
IP协议是网络层的核心,ipprotocol.cpp的解析逻辑堪称教科书级示范。我们以IPv4为例,解剖其关键字段的提取过程。
IPv4头部格式(无选项时20字节):
| 版本+首部长度(1) | 服务类型(1) | 总长度(2) | 标识(2) | 标志+片偏移(2) | 生存时间(1) | 协议(1) | 首部校验和(2) | 源IP(4) | 目的IP(4) |
ParseIPHeader()函数的核心步骤:
BOOL ParseIPHeader(BYTE* pBuf, DWORD dwSize, IP_HEADER* pIPHdr)
{
// 1. 检查缓冲区长度:IP头至少20字节
if (dwSize < 20) return FALSE;
// 2. 提取版本和首部长度(第一个字节)
BYTE ver_ihl = pBuf[0];
pIPHdr->version = (ver_ihl >> 4) & 0x0F; // 高4位是版本(IPv4=4)
pIPHdr->ihl = ver_ihl & 0x0F; // 低4位是首部长度(单位:4字节)
// 3. 验证版本:只处理IPv4
if (pIPHdr->version != 4) return FALSE;
// 4. 计算IP头实际长度(考虑选项字段)
DWORD dwIPHdrLen = pIPHdr->ihl * 4;
if (dwIPHdrLen < 20 || dwIPHdrLen > dwSize) return FALSE;
// 5. 提取总长度(第3-4字节),注意网络字节序
pIPHdr->tot_len = (pBuf[2] << 8) | pBuf[3];
// 6. 提取生存时间(TTL,第9字节)
pIPHdr->ttl = pBuf[8];
// 7. 提取协议字段(第10字节)
pIPHdr->protocol = pBuf[9];
// 8. 提取源和目的IP(第13-16字节,第17-20字节),转为主机字节序
pIPHdr->saddr = ntohl(*(DWORD*)(pBuf + 12)); // ntohl()处理字节序
pIPHdr->daddr = ntohl(*(DWORD*)(pBuf + 16));
// 9. 计算并验证IP首部校验和(关键!)
if (!VerifyIPChecksum(pBuf, dwIPHdrLen)) return FALSE;
// 10. 设置载荷偏移
pIPHdr->payload_offset = dwIPHdrLen;
pIPHdr->payload_len = pIPHdr->tot_len - dwIPHdrLen;
return TRUE;
}
其中,IP首部校验和的验证是最体现功底的部分。校验和计算规则是:将IP头按16位分组求和,溢出进位加到低16位,最后取反。VerifyIPChecksum()的实现如下:
BOOL VerifyIPChecksum(BYTE* pBuf, DWORD dwLen)
{
DWORD sum = 0;
WORD* pWord = (WORD*)pBuf;
// 将IP头按16位累加(注意:校验和字段本身置0参与计算)
for (DWORD i = 0; i < dwLen; i += 2) {
if (i == 10) continue; // 跳过校验和字段(偏移10-11字节)
sum += ntohs(pWord[i/2]);
if (sum & 0xFFFF0000) {
sum = (sum & 0xFFFF) + (sum >> 16); // 处理进位
}
}
// 最终结果应为0xFFFF
return (sum == 0xFFFF);
}
这个函数揭示了一个重要事实:IP校验和只校验IP头,不校验数据部分。这也是为什么UDP校验和可选而TCP必须校验——IP层的可靠性是有限的。
关于IP分片,本项目采用简化策略:只处理非分片包(flags & 0x01 == 0且frag_off == 0),对分片包直接丢弃。这是因为重组分片需要维护状态表(记录每个ID的分片集合),在轻量级IDS中开销过大。ParseIPHeader()中对此有明确检查:
pIPHdr->flags = (pBuf[6] & 0xE0) >> 5; // 取高3位:保留位、DF、MF
pIPHdr->frag_off = ((pBuf[6] & 0x1F) << 8) | pBuf[7]; // 片偏移(13位)
if (pIPHdr->flags & 0x01 || pIPHdr->frag_off != 0) {
// 是分片包,返回FALSE,由上层决定是否丢弃
return FALSE;
}
提示:在真实攻防演练中,分片攻击(如Teardrop)仍是绕过IDS的有效手段。本项目的“丢弃分片”策略虽简单,却是权衡性能与安全的务实选择。若需支持分片,可在
sniffer.cpp中维护一个FRAGMENT_TABLE哈希表,按ip_id索引,超时未收齐则清理。
3.3 TCP协议解析:标志位、窗口大小与序列号的精确提取
TCP是传输层最复杂的协议,tcpprotocol.cpp的解析逻辑充分展现了“逐位操作”的硬核风格。我们聚焦三个最易出错的字段:标志位、窗口大小、序列号。
TCP头部格式(无选项时20字节):
| 源端口(2) | 目的端口(2) | 序列号(4) | 确认号(4) | 数据偏移+标志(2) | 窗口大小(2) | 校验和(2) | 紧急指针(2) |
ParseTCPHeader()的关键步骤:
BOOL ParseTCPHeader(BYTE* pBuf, DWORD dwSize, TCP_HEADER* pTCPhdr)
{
// 1. 检查长度:TCP头最小20字节
if (dwSize < 20) return FALSE;
// 2. 提取源/目的端口(网络字节序→主机字节序)
pTCPhdr->source = ntohs(*(WORD*)pBuf);
pTCPhdr->dest = ntohs(*(WORD*)(pBuf + 2));
// 3. 提取序列号和确认号(32位,需ntohl)
pTCPhdr->seq = ntohl(*(DWORD*)(pBuf + 4));
pTCPhdr->ack_seq = ntohl(*(DWORD*)(pBuf + 8));
// 4. 提取数据偏移和标志位(第12字节)
BYTE data_off_flags = pBuf[12];
pTCPhdr->doff = (data_off_flags & 0xF0) >> 4; // 高4位是数据偏移(单位:4字节)
pTCPhdr->flags = data_off_flags & 0x3F; // 低6位是标志位(URG, ACK, PSH, RST, SYN, FIN)
// 5. 提取窗口大小(第14-15字节)
pTCPhdr->window = ntohs(*(WORD*)(pBuf + 14));
// 6. 计算TCP头实际长度(考虑选项)
DWORD dwTCPHdrLen = pTCPhdr->doff * 4;
if (dwTCPHdrLen < 20 || dwTCPHdrLen > dwSize) return FALSE;
// 7. 设置载荷偏移和长度
pTCPhdr->payload_offset = dwTCPHdrLen;
pTCPhdr->payload_len = dwSize - dwTCPHdrLen;
return TRUE;
}
这里最精妙的是标志位的位运算提取。pBuf[12] & 0x3F得到一个6位值,每一位对应一个TCP标志:
- Bit 0 (0x01):FIN
- Bit 1 (0x02):SYN
- Bit 2 (0x04):RST
- Bit 3 (0x08):PSH
- Bit 4 (0x10):ACK
- Bit 5 (0x20):URG
规则匹配引擎正是基于此进行检测。例如,检测“SYN Flood”攻击的逻辑:
if (pTCPhdr->flags & TCP_FLAG_SYN && !(pTCPhdr->flags & TCP_FLAG_ACK)) {
// 是SYN包(非SYN-ACK),计入计数器
g_syn_counter++;
}
注意:
!(pTCPhdr->flags & TCP_FLAG_ACK)这行至关重要。三次握手的第一步是纯SYN,第二步是SYN-ACK(SYN和ACK同时置位)。若不加此判断,SYN-ACK包也会被计入,导致误报。
另一个易错点是窗口大小的含义。pTCPhdr->window表示接收方通告的“接收窗口”,即还能接收多少字节数据。在“Smurf攻击”检测中,若发现大量窗口大小为0的TCP包(即pTCPhdr->window == 0),可能表示目标主机已崩溃或拒绝服务——这是非常隐蔽的异常信号,现代IDS常忽略。
实操心得:我在测试时发现,某些防火墙会篡改TCP窗口大小(如统一设为65535),导致基于窗口的规则失效。解决方案是在Protocolanalysis.cpp中增加启发式判断:若连续多个包窗口大小相同且为2的幂(如65535=0xFFFF),则标记为“可能被中间设备修改”,降低该特征的权重。这种“对抗性思维”,正是安全工程师的核心能力。
3.4 ARP协议解析:局域网地址解析的底层真相
ARP(地址解析协议)工作在数据链路层,是局域网通信的基石。arpprotocol.cpp的解析虽然代码量少,却揭示了网络最底层的运作机制。
ARP包格式(以太网帧内载荷):
| 硬件类型(2) | 协议类型(2) | 硬件地址长度(1) | 协议地址长度(1) | 操作码(2) | 发送方MAC(6) | 发送方IP(4) | 目标MAC(6) | 目标IP(4) |
ParseARPHeader()的实现:
BOOL ParseARPHeader(BYTE* pBuf, DWORD dwSize, ARP_HEADER* pARPhdr)
{
// ARP包最小长度28字节(无填充)
if (dwSize < 28) return FALSE;
// 提取硬件类型(以太网=1)
pARPhdr->htype = ntohs(*(WORD*)pBuf);
if (pARPhdr->htype != 1) return FALSE; // 只处理以太网ARP
// 提取协议类型(IPv4=0x0800)
pARPhdr->ptype = ntohs(*(WORD*)(pBuf + 2));
if (pARPhdr->ptype != 0x0800) return FALSE;
// 提取操作码(1=ARP请求,2=ARP响应)
pARPhdr->opcode = ntohs(*(WORD*)(pBuf + 6));
// 提取发送方MAC(偏移8-13)
memcpy(pARPhdr->sender_hwaddr, pBuf + 8, 6);
// 提取发送方IP(偏移14-17)
pARPhdr->sender_protoaddr = ntohl(*(DWORD*)(pBuf + 14));
// 提取目标MAC(偏移18-23)
memcpy(pARPhdr->target_hwaddr, pBuf + 18, 6);
// 提取目标IP(偏移24-27)
pARPhdr->target_protoaddr = ntohl(*(DWORD*)(pBuf + 24));
return TRUE;
}
ARP解析的价值在于:它是唯一能直接关联MAC地址与IP地址的协议。这为两类关键检测提供了基础:
- ARP欺骗检测(MitM):监控同一IP地址是否在短时间内关联了多个不同的MAC地址。intrusiondetect.cpp中维护一个ARP_CACHE哈希表,键为IP,值为MAC。当新ARP响应到达时,若ARP_CACHE[ip] != new_mac,则触发“ARP冲突”告警。
- 扫描行为识别:ARP请求(opcode==1)通常是主机在探测局域网内IP是否存活。若发现某IP在1秒内发出超过50个ARP请求(目标IP递增),即可判定为“ARP扫描”。
注意:ARP包不经过IP层,因此
sniffer.cpp捕获时,ethernetprotocol.cpp解析出type==0x0806后,直接交给arpprotocol.cpp,跳过IP解析层。这种“协议旁路”设计,体现了对OSI模型的深刻理解——不是所有流量都走完整栈。
4. 入侵检测规则引擎:从硬编码数组到可配置规则的演进
4.1 规则数据结构设计:平衡灵活性与性能的取舍
规则引擎是IDS的大脑,其设计直接决定了检测能力的上限。本项目采用“结构体数组+线性遍历”的极简方案,RULE_ITEM定义如下:
#define MAX_RULE_STR 128
typedef struct _RULE_ITEM {
int nProtocol; // 协议类型:IPPROTO_TCP, IPPROTO_UDP等
DWORD dwSrcIP; // 源IP(主机字节序),0表示任意
DWORD dwDstIP; // 目的IP(主机字节序),0表示任意
DWORD dwSrcIPMask; // 源IP掩码(如0xFFFFFF00表示/24)
DWORD dwDstIPMask; // 目的IP掩码
WORD wSrcPort; // 源端口,0表示任意
WORD wDstPort; // 目的端口,0表示任意
WORD wSrcPortHigh; // 源端口范围上限(wSrcPort <= port <= wSrcPortHigh)
WORD wDstPortHigh; // 目的端口范围上限
BYTE byFlags; // TCP标志位掩码(如0x02表示只匹配SYN)
BYTE byFlagsMask; // TCP标志位掩码(如0x02表示只看SYN位)
char szContent[MAX_RULE_STR]; // 载荷内容匹配(如"GET /admin")
char szAction[MAX_RULE_STR]; // 动作:"ALERT", "DROP", "LOG"
int nThreshold; // 阈值(用于速率限制)
DWORD dwLastTrigger; // 上次触发时间(毫秒)
} RULE_ITEM;
这个结构体的设计充满权衡智慧:
-
IP地址使用DWORD而非字符串:
dwSrcIP存储的是ntohl(inet_addr("192.168.1.100"))的结果,即0xC0A80164。这样在匹配时,只需一次==比较,而非strcmp()字符串比较,性能提升百倍。CIDR掩码(dwSrcIPMask)同样用DWORD存储(如/24对应0xFFFFFF00),匹配逻辑为:
cpp if ((context.ip.saddr & rule.dwSrcIPMask) == (rule.dwSrcIP & rule.dwSrcIPMask)) -
端口范围支持:
wSrcPortHigh允许定义端口区间(如1024-65535),满足“非特权端口扫描”检测需求。但未实现端口列表(如80,443,8080),因列表长度不定,会破坏结构体固定大小,增加内存管理复杂度。 -
TCP标志位的掩码机制:
byFlagsMask定义哪些位参与匹配,byFlags定义期望值。例如,检测“SYN-FIN同时置位”(非法组合):
cpp rule.byFlagsMask = 0x03; // 同时检查SYN(0x02)和FIN(0x01) rule.byFlags = 0x03; // 要求两者都为1
这比简单的if (flags == 0x03)更灵活,可扩展为“SYN或FIN置位”。 -
载荷内容匹配的妥协:
szContent字段支持字符串匹配,但采用朴素的strstr()算法,未实现Boyer-Moore等高效算法。这是为保持代码简洁性而做的性能牺牲——对于教学场景,1000条规则内的线性搜索足够快。
提示:在
intrusiondetect.cpp中,规则匹配函数CheckRules()被设计为可中断的。当检测到高危规则(如szAction=="DROP")时,立即返回,不继续遍历剩余规则。这避免了“检测到木马下载后,还继续检查是否是SQL注入”的冗余计算。
4.2 核心检测逻辑:速率限制、状态跟踪与载荷匹配的组合拳
规则匹配不是简单的“字段相等”,而是多维度的组合判断。CheckRules()函数的主干逻辑如下:
void CheckRules(PROTOCOL_CONTEXT* pContext)
{
DWORD dwNow = GetTickCount(); // 获取当前时间(毫秒)
for (int i = 0; i < g_nRules; i++) {
RULE_ITEM* pRule = &g_rules[i];
// 1. 协议类型匹配
if (pRule->nProtocol != IPPROTO_IP &&
pRule->nProtocol != pContext->nProtocol) {
continue;
}
// 2. IP地址匹配(支持CIDR)
if (pRule->dwSrcIP != 0) {
DWORD src_match = (pContext->ip.saddr & pRule->dwSrcIPMask);
DWORD rule_src = (pRule->dwSrcIP & pRule->dwSrcIPMask);
if (src_match != rule_src) continue;
}
if (pRule->dwDstIP != 0) {
DWORD dst_match = (pContext->ip.daddr & pRule->dwDstIPMask);
DWORD rule_dst = (pRule->dwDstIP & pRule->dwDstIPMask);
if (dst_match != rule_dst) continue;
}
// 3. 端口匹配(TCP/UDP)
if (pContext->nProtocol == IPPROTO_TCP ||
pContext->nProtocol == IPPROTO_UDP) {
if (pRule->wSrcPort != 0) {
if (pContext->tcp.source < pRule->wSrcPort ||
pContext->tcp.source > pRule->wSrcPortHigh) {
continue;
}
}
if (pRule->wDstPort != 0) {
if (pContext->tcp.dest < pRule->wDstPort ||
pContext->tcp.dest > pRule->wDstPortHigh) {
continue;
}
}
}
// 4. TCP标志位匹配
if (pContext->nProtocol == IPPROTO_TCP && pRule->byFlagsMask != 0) {
BYTE flags = pContext->tcp.flags;
if ((flags & pRule->byFlagsMask) != pRule->byFlags) {
continue;
}
}
// 5. 载荷内容匹配
if (pRule->szContent[0] != '\0' && pContext->pPayload != NULL) {
if (strstr((char*)pContext->pPayload, pRule->szContent) == NULL) {
continue;
}
}
// 6. 速率限制检查(阈值规则)
if (pRule->nThreshold > 0) {
if (dwNow - pRule->dwLastTrigger < 1000) { // 1秒内
g_rate_counter[i]++;
if (g_rate_counter[i] >= pRule->nThreshold) {
TriggerAlert(pRule, pContext);
pRule->dwLastTrigger = dwNow;
g_rate_counter[i] = 0;
}
continue;
} else {
g_rate_counter[i] = 0;
pRule->dwLastTrigger = dwNow;
}
}
// 7. 匹配成功,执行动作
TriggerAction(pRule, pContext);
}
}
这段代码体现了IDS检测的典型模式:
- 分层过滤:先做快速筛选(协议、IP),再做慢速计算(载荷匹配、速率统计),符合“漏斗式优化”原则。
- 状态跟踪:
g_rate_counter[]数组为每条规则维护独立计数器,dwLastTrigger记录上次触发时间,实现精确的“X次/秒”限速。 - 动作解耦:
TriggerAction()函数根据szAction字段分发,ALERT写日志,DROP调用sendto()发送RST包(TCP)或ICMP不可达(UDP),LOG仅记录。
实操心得:我在部署时遇到一个经典问题——“HTTP Slowloris攻击”检测失效。Slowloris通过发送不完整的HTTP头(如GET / HTTP/1.1\r\nHost: example.com\r\n后不发送空行),使服务器保持连接。原规则用strstr(payload, "GET ")匹配,但Slowloris的载荷可能被截断在GET之后。解决方案是增加“载荷长度检查”:
if (pRule->szContent[0] != '\0' && pContext->pPayload != NULL) {
if (pContext->dwPayloadLen < strlen(pRule->szContent)) continue;
if (strstr((char*)pContext->pPayload, pRule->szContent) == NULL) continue;
}
这个补丁虽小,却体现了真实攻防中“边界条件”的重要性。
4.3 图形化规则配置:从对话框控件到二进制序列化的完整链路
IntrusionRuleDialog是规则引擎的用户界面,它将抽象的安全策略转化为可视化的操作。我们追踪一条规则从创建到生效的完整链路:
-
用户操作:在对话框中填写“协议=TCP”,“源IP=192.168.1.0/24”,“目的端口=21”,“动作=ALERT”,点击“添加”。
-
控件数据提取:
OnAddRule()函数从各控件读取值:
```cpp
CString strSrcIP, strDstIP;
m_editSrcIP.GetWindowText(strSrcIP);
m_editDstIP.GetWindowText(strDstIP);
// 解析CIDR:192.168.1.0/24 -> dwIP=0xC0A80100, dwMask=0xFFFFFF00
ParseCIDR(strSrcIP, &rule.dwSrcIP, &rule.dwSrcIPMask);
rule.nProtocol = m_comboProtocol.GetCurSel() + 1; // TCP=6
rule.wDstPort = (WORD)_ttoi(m_editDstPort.GetBuffer());
_tcscpy(rule.szAction, _T(“ALERT”));
```
-
规则持久化:点击“保存”时,调用
SaveRulesToFile(),将g_rules数组序列化为二进制文件rules.dat:
cpp FILE* fp = _tfopen(_T("rules.dat"), _T("wb")); if (fp) { fwrite(&g_nRules, sizeof(int), 1, fp); // 先写规则总数 fwrite(g_rules, sizeof(RULE_ITEM), g_nRules, fp); // 再写所有规则 fclose(fp); } -
程序启动加载:
intrusiondetect.cpp的InitRules()函数在程序启动时读取rules.dat:
cpp FILE* fp = _tfopen(_T("rules.dat"), _T("rb")); if (fp) { fread(&g_nRules, sizeof(int), 1, fp); fread(g_rules, sizeof(RULE_ITEM), g_nRules, fp); fclose(fp); }
这个链路展示了MFC应用的经典数据流:UI控件 → 内存结构体 → 文件存储 → 内存加载 → 规则匹配。没有数据库、没有XML解析器,一切都在二进制层面完成,最大限度减少了外部依赖。
注意:
rules.dat是明文二进制,可用十六进制编辑器直接修改。这既是安全隐患(规则可被篡改),也是教学优势(学生可手动编辑规则,观察效果)。若需加固,可在序列化前用简单异或加密(如byte ^ 0xAA),成本几乎为零。
5. 实操部署与常见问题排查:从编译失败到误报率优化的全程指南
5.1 VC6.0编译环境搭建:绕过20年技术债的实用技巧
在Windows 10/11上编译VC6.0项目是首要挑战。以下是经过实测的可行方案:
步骤1:安装VC6.0及必要补丁
- 下载官方VC6.0安装包(注意:必须是完整版,精简版缺失ATL/MFC库)
- 安装后打上Processor Pack补丁(微软2003年发布),解决long long类型支持等问题
- 安装Visual Studio 6.0 Service Pack 6(SP6),修复大量安全漏洞和兼容性问题
步骤2:解决Windows SDK缺失问题
VC6.0默认不包含现代Windows头文件。需手动配置:
- 下载Windows Server 2003 R2 Platform SDK(微软已归档,可从archive.org获取)
- 安装后,在VC6.0中:Tools → Options → Directories,将SDK的Include和Lib路径添加到列表顶部
步骤3:处理MFC库链接错误
常见错误:LINK : fatal error LNK1104: cannot open file "mfc42.lib"
- 原因:VC6.0默认链接静态MFC库,但现代系统无此文件
- 解决:Project → Settings → General,将Use of MFC改为Use MFC in a Shared DLL
- 若仍报错,从Windows XP SP3的i386目录提取mfc42.dll和mfc42.lib(需合法授权)
步骤4:禁用DEP(数据执行保护)
Windows 7+默认启用DEP,而VC6.0生成的代码可能触发:
- 编译时:Project → Settings → Link,在Project Options中添加/NXCOMPAT:NO
- 运行时:右键程序→属性→兼容性→以兼容模式运行→Windows XP (Service Pack 3)
实操心得:我曾耗时两天解决
sniffer.cpp中WSAIoctl()调用失败的问题。最终发现是Windows 10的“核心隔离”功能(Core Isolation)阻止了原始套接字操作。解决方案:Windows安全中心→设备安全性→核心隔离→关闭内存完整性验证。这个教训提醒我们:老代码的调试,一半是技术,一半是和操作系统斗智斗勇。
5.2 运行时典型问题与根因分析
问题1:启动后无数据包捕获,设备列表为空
现象:DeviceDialog打开,下拉框无任何网卡名称
根因分析:
- GetAdaptersInfo()调用失败,通常因权限不足(需管理员运行)
- 网卡驱动不支持NDIS 5.0+(老旧网卡如RTL8139)
- Windows防火墙阻止了原始套接字
排查步骤:
1. 右键程序→以管理员身份运行
2. 在DeviceDialog.cpp的OnInitDialog()中添加调试输出:
cpp DWORD dwRet = GetAdaptersInfo(NULL, &dwSize); // 先获取所需缓冲区大小 TRACE(_T("GetAdaptersInfo size: %d, ret=%d\n"), dwSize, dwRet);
3. 若dwRet == ERROR_BUFFER_OVERFLOW,说明API可调用;若为ERROR_ACCESS_DENIED,则是权限问题
解决方案:
- 确保程序以管理员身份运行
- 在sniffer.cpp中,socket()创建后立即调用setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, ...)避免端口占用
问题2:捕获到大量“无效IP校验和”包,导致解析失败
现象:日志中频繁出现IP checksum error,大量包被丢弃
根因分析:
- 现代网卡支持“校验和卸载(Checksum Offload)”,由硬件计算IP/TCP校验和,但recvfrom()返回的是未计算校验和的原始数据
- VC6.0代码假设校验和已由网卡计算好,故校验失败
解决方案:
在sniffer.cpp的InitSocket()中,禁用校验和卸载:
// 禁用IPv4校验和卸载
DWORD dwOffload = 0;
WSAIoctl(sock, SIO_DISABLE_CIRCULAR_QUEUEING, &dwOffload, sizeof(dwOffload), NULL, 0, &dwBytes, NULL, NULL);
或更彻底地,在Windows设备管理器中,找到网卡→属性→高级→校验和卸载→禁用。
问题3:TCP规则匹配率低,特别是HTTP载荷检测失效
现象:配置szContent="POST"规则,但实际POST请求未触发告警
根因分析:
- TCP载荷可能被分段(segmentation),一个HTTP请求被拆成多个TCP包,pContext->pPayload只包含当前包的载荷片段
- strstr()在片段中找不到完整字符串
解决方案:
在Protocolanalysis.cpp中增加TCP流重组(Stream Reassembly)模块:
- 维护一个TCP_STREAM_TABLE,键为(src_ip, src_port, dst_ip, dst_port)
- 对每个TCP包,根据序列号追加到对应流的缓冲区
- 当检测到FIN或RST标志,或缓冲区超时(如30秒),才对完整流执行strstr()匹配
此功能虽增加复杂度,但对Web攻击检测至关重要。本项目源码中已预留ReassembleTCPStream()函数框架,只需填充逻辑。
5.3 误报率(False Positive)优化实战技巧
IDS最大的敌人不是漏报,而是误报。以下是我从数百次实验中总结的优化技巧:
技巧1:白名单优先于黑名单
不要写“阻断所有来自192.168.1.100的流量”,而应写“仅允许192.168.1.0/24网段访问内部DNS(53端口)”。白名单规则天然鲁棒,因正常业务流量模式稳定。
技巧2:时间窗口精细化
将“1秒内100个SYN包”改为“100毫秒内10个SYN包”。攻击者可轻易将速率控制在阈值以下,但正常业务极少在100ms内发起10个连接。
技巧3:多条件组合降噪
单一条件易误报,组合条件更可靠。例如检测“暴力破解SSH”:
- 条件1:TCP目的端口=22
- 条件2:载荷包含"ssh"或"SSH"(协议标识)
- 条件3:载荷长度<100字节(SSH握手包较小)
- 条件4:1分钟内同一源IP触发>5次
技巧4:利用协议语义
TCP标志位组合蕴含语义。例如:
- SYN包:应无载荷(payload_len==0),若有则可疑
- ACK包:序列号应等于之前SYN包的seq+1,否则可能是伪造
- RST包:通常不应有载荷,若有则可能是攻击载荷
这些技巧无需修改核心代码,只需在IntrusionRuleDialog中配置复合规则即可实现。
最后分享一个小技巧:在
intrusiondetect.cpp中,为每条规则添加nHitCount计数器,并在主界面显示“规则命中TOP10”。运行一周后,你会发现80%的告警来自3条规则,其余97条基本闲置。果断删除它们,系统性能提升,运维压力骤减——好的IDS不是规则越多越好,而是每条规则都精准命中要害。
6. 学习延伸与工程化演进:从教学原型到生产可用的升级路径
这套VC6.0源码的价值,不仅在于它能运行,更在于它是一张清晰的“技术路线图”。当你吃透每一个.cpp文件,就能自然推演出下一步该做什么。以下是三条切实可行的升级路径:
6.1 协议栈扩展:从IPv4到IPv6/HTTP2的渐进式增强
当前系统仅支持IPv4和基础协议。扩展IPv6的关键在于:
- 以太网类型字段:IPv6的ether_type为0x86DD,需在ethernetprotocol.cpp中增加分支
- IPv6头部解析:ipprotocol.cpp需新增ParseIPv6Header(),处理128位地址、流标签、跳数限制等新字段
- TCP/UDP不变:IPv6上层协议不变,现有tcpprotocol.cpp可复用,只需修改IP地址提取逻辑
HTTP/2的挑战在于二进制帧格式。与其重写解析器,不如采用“协议隧道”思想:
- 在Protocolanalysis.cpp中,当检测到TLS握手(pBuf[0]==0x16)且SNI扩展包含http/2时,标记为HTTP/2流
- 将后续TLS载荷交给开源库(如nghttp2)解析,本系统只做流量分类和速率控制
这种“核心不动,外围插件”的策略,是大型系统演进的黄金法则。
6.2 性能优化:从单线程到多核并行的架构重构
当前sniffer.cpp是单线程阻塞式捕获,成为性能瓶颈。升级为多线程需:
- 生产者-消费者模型:主线程负责recvfrom()捕获,写入无锁环形缓冲区(boost::lockfree::spsc_queue)
- 多解析线程:N个线程从缓冲区读取PACKET_INFO,并行执行协议解析
- 规则匹配分流:按协议类型(TCP/UDP/ICMP)将包分发到不同匹配线程,避免锁竞争
关键点:所有线程共享g_rules数组,但只读不写,故无需同步。这印证了“读多写少”场景下,无锁设计的威力。
6.3 规则引擎现代化:从硬编码到YAML+Lua脚本的范式转移
RULE_ITEM结构体的局限性日益明显。现代化方案是:
- 规则描述层:用YAML定义规则(人类可读)
yaml - name: "SSH Brute Force" protocol: tcp dst_port: 22 payload: "SSH-" threshold: 5/60s action: alert
- 执行引擎层:用Lua脚本解释YAML规则,调用C++导出的API(如get_src_ip(), get_payload())
- 热加载:修改YAML后,引擎自动重新加载,无需重启
这正是Suricata等现代IDS的架构。而本项目的intrusiondetect.cpp中,CheckRules()函数的接口设计(接收PROTOCOL_CONTEXT*)已为此预留了扩展空间——它不关心规则从哪来,只负责执行。
我个人在实际操作中的体会是:这套VC6.0代码就像一把生锈但刃口锋利的匕首。它不华丽,不智能,但每一次挥动,都让你真切感受到网络协议的肌肉与骨骼。当你在
tcpprotocol.cpp里亲手计算出TCP校验和,当你在sniffer.cpp中看到第一个原始字节流从网卡涌入内存,那种“掌控感”是任何高级框架都无法替代的。它不教你如何构建云原生安全平台,但它教会你——所有伟大的安全系统,都始于对一个字节的敬畏。
简介:一套基于Visual C++ 6.0开发的可直接编译运行的网络入侵检测系统源码,专注局域网环境下的实时流量捕获与深度协议解析。支持以太网帧、IP、ICMP、TCP、UDP、ARP等主流协议的逐层解码,通过内置规则引擎匹配异常通信行为。提供图形化操作界面,涵盖网络设备选择、BPF过滤参数设置、自定义入侵规则配置等功能模块。核心逻辑分离清晰:sniffer.cpp负责原始数据包捕获,Protocolanalysis.cpp统筹协议识别流程,各协议解析文件(如 tcpprotocol.cpp、ipprotocol.cpp、arpprotocol.cpp 等)独立实现对应协议字段提取与校验。配套完整VC6工程文件(.dsw/.dsp)、资源定义(resource.h)、多组对话框类(DeviceDialog、FilterDlg、IntrusionRuleDialog等),适用于网络协议分析教学、IDS底层机制理解或小型实验环境中的基础攻击特征识别。
&spm=1001.2101.3001.5002&articleId=161763734&d=1&t=3&u=f6e0a527da664c4e81ba72b67fbe8157)

被折叠的 条评论
为什么被折叠?



