简介:这是一套面向高校计算机与网络安全相关专业学生的课程设计/期末大作业完整项目,主题为基于PCAP的网络入侵检测系统,采用C语言实现。项目已通过导师指导并获得97分高分评价,下载后无需修改即可直接运行,适合作为课程设计、期末大作业或网络安全入门实践参考。压缩包共17个文件,约891KB,包含6个C源文件、4个头文件、2个Makefile、1个Shell脚本、1份PDF报告、1个Markdown说明文档及Python辅助脚本等,覆盖抓包嗅探、线程池调度、流量分析等核心模块,目录结构清晰,便于按模块阅读与二次开发。资源附有使用说明与课程报告PDF,可帮助读者快速理解系统架构、编译运行流程与设计思路,同时提供ARP欺骗测试脚本用于验证检测效果。目前已有142人学习下载,适合需要完整可运行项目、报告素材与排错参考的读者。
1. 从一份 PCAP 到告警:这套 C 语言 NIDS 到底在解决什么问题
手里有一份几百 MB 的 PCAP,Wireshark 打开卡到怀疑人生,想找“哪台机器在扫端口、哪个 IP 在传可疑载荷”,靠肉眼翻包基本等于大海捞针。基于 PCAP 的网络入侵检测系统,本质就是把这份离线流量喂给一个自己写的解析引擎,让它按规则吐出告警。用 C 语言实现这件事,不是为了炫技,而是因为抓包解析是典型的字节级操作,指针、结构体、内存布局这些底层能力直接决定你能不能把 TCP 重组、载荷匹配做对。这套源码加使用说明加报告 PDF 的组合,适合两类人:一是课程设计或毕设需要一份能跑通、能讲清原理的完整项目;二是想真正搞懂 IDS 内部怎么从原始字节走到一条告警的安全方向初学者。它不依赖重型框架,编译出来就是一个可执行文件,输入 PCAP,输出告警列表,链路短、可控性强,这正是它值得动手复现的理由。
2. 拆开一份 PCAP:链路层到应用层的解析链路怎么搭
2.1 为什么解析要从文件头而不是第一个包开始
PCAP 文件不是裸包堆叠,它有一个 24 字节的全局头,紧接着每个包前面还有 16 字节的包记录头。很多人第一次写解析器,直接 fread 一个缓冲区就当以太网帧处理,结果偏移全错,解析出来的 MAC 地址是乱的。全局头里最关键的是 magic number(4 字节),它决定后续字段是大端还是小端;还有 linktype 字段,常见值 1 表示以太网。包记录头里则是时间戳秒、微秒、抓到的长度、原始长度四个字段。理解这层结构,后面的偏移计算才有依据。
下面是最小可用的 PCAP 全局头读取代码,用结构体对齐的方式要小心,编译器可能插入填充字节,所以我一般用逐字段读取而不是直接 fread 整个结构体。
#include <stdio.h> #include <stdint.h> typedef struct { uint32_t magic; /* 字节序标识,0xa1b2c3d4 或 0xd4c3b2a1 */ uint16_t version_major; uint16_t version_minor; int32_t thiszone; uint32_t sigfigs; uint32_t snaplen; /* 抓包时截断长度 */ uint32_t network; /* 链路层类型,1 = Ethernet */ } pcap_global_header; int read_global_header(FILE *fp, pcap_global_header *gh) { /* 逐字段读取,避免结构体填充导致的偏移错位 */ if (fread(&gh->magic, 4, 1, fp) != 1) return -1; if (fread(&gh->version_major, 2, 1, fp) != 1) return -1; if (fread(&gh->version_minor, 2, 1, fp) != 1) return -1; if (fread(&gh->thiszone, 4, 1, fp) != 1) return -1; if (fread(&gh->sigfigs, 4, 1, fp) != 1) return -1; if (fread(&gh->snaplen, 4, 1, fp) != 1) return -1; if (fread(&gh->network, 4, 1, fp) != 1) return -1; return 0; }逻辑说明:这里没有用fread(&gh, sizeof(...), 1, fp)一次性读,是因为结构体在 64 位机器上可能因为对齐规则在version_minor后面插入填充,导致读到的thiszone偏移错误。逐字段读虽然啰嗦,但跨平台稳定。参数上,magic读进来后要判断是0xa1b2c3d4(大端存储)还是0xd4c3b2a1(小端存储),后续所有多字节字段都要按这个字节序做转换,否则时间戳和长度全是错的。snaplen决定了每个包最多存多少字节,解析时不能假设包长等于原始长度。
2.2 以太网帧到 IP 再到 TCP 的偏移计算
读完全局头,循环读每个包的记录头,拿到incl_len(实际存储长度)后分配缓冲区读入原始帧。以太网帧头 14 字节:目的 MAC 6 字节、源 MAC 6 字节、类型 2 字节。类型字段 0x0800 表示 IPv4,0x86DD 表示 IPv6,0x0806 是 ARP。如果是 IPv4,跳过 14 字节进入 IP 头。IP 头第一个字节低 4 位是首部长度(单位 4 字节),所以ip_header_len = (buf[14] & 0x0f) * 4。协议字段偏移 9 字节,值 6 表示 TCP,17 表示 UDP。TCP 头的数据偏移在偏移 12 字节的高 4 位,同样乘 4 得到 TCP 头长度。应用层载荷起始位置就是14 + ip_header_len + tcp_header_len。
/* 假设 buf 已读入一个完整包,incl_len 为其长度 */ uint16_t eth_type = (buf[12] << 8) | buf[13]; if (eth_type != 0x0800) return; /* 只处理 IPv4 */ uint8_t ip_hl = (buf[14] & 0x0f) * 4; uint8_t proto = buf[14 + 9]; if (proto != 6) return; /* 只处理 TCP */ uint16_t ip_total_len = (buf[14 + 2] << 8) | buf[14 + 3]; uint8_t tcp_hl = ((buf[14 + ip_hl + 12] >> 4) & 0x0f) * 4; uint8_t *payload = buf + 14 + ip_hl + tcp_hl; int payload_len = ip_total_len - ip_hl - tcp_hl; if (payload_len <= 0) return;逻辑说明:ip_hl和tcp_hl都是“首部长度”字段乘以 4,因为这两个字段的单位是 32 位字。ip_total_len来自 IP 头,表示整个 IP 数据报长度,用它减去 IP 头和 TCP 头才是真实载荷长度,不能直接用incl_len减,因为incl_len可能包含以太网填充或截断。参数上,payload指针指向应用层数据起始,payload_len是后续做特征匹配的输入长度。这里有个常见坑:如果抓包时 snaplen 设小了,incl_len小于ip_total_len,payload_len会算出负数或偏大,必须先做边界检查。
2.3 用规则匹配把载荷变成告警
解析出载荷后,检测逻辑就是拿载荷去匹配预定义的特征串。最简单的做法是维护一个规则数组,每条规则包含一个模式串和告警描述,用memmem或自己写的子串查找在载荷里搜索。比如匹配 HTTP 请求里的../路径穿越特征,或者匹配某个已知恶意工具的默认 User-Agent。规则可以放在一个文本文件里,启动时加载进内存,这样不用重新编译就能加规则。
typedef struct { char *pattern; /* 特征串 */ char *msg; /* 告警描述 */ } rule_t; rule_t rules[] = { {"../", "Possible path traversal"}, {"cmd.exe", "Windows command shell reference"}, {"SELECT *", "Possible SQL injection"}, }; void detect(uint8_t *payload, int len, const char *src_ip) { for (int i = 0; i < sizeof(rules)/sizeof(rules[0]); i++) { if (memmem(payload, len, rules[i].pattern, strlen(rules[i].pattern))) { printf("[ALERT] %s from %s\n", rules[i].msg, src_ip); } } }逻辑说明:memmem是 GNU 扩展,在二进制数据里查找子串,比strstr安全,因为载荷里可能有\0。参数上,payload和len来自上一步的解析结果,src_ip需要从 IP 头偏移 12 字节处取 4 字节并格式化成点分十进制。规则匹配是 O(n*m) 的朴素算法,对几百 MB 的 PCAP 可能偏慢,实际项目里可以换成 Aho-Corasick 多模式匹配,但作为课程设计,朴素匹配足够讲清原理。注意规则串不要设得太短,比如单个"a"会导致海量误报,一般至少 4 个字节以上才有区分度。
3. 把解析器跑起来:编译、输入输出与参数调优
3.1 编译命令与依赖处理
这套代码只依赖标准 C 库和 POSIX 的memmem,在 Linux 下直接 gcc 编译即可。如果是在 Windows 上用 MinGW,memmem可能不存在,需要自己实现一个或者用strstr加长度保护。编译时建议开-Wall -Wextra把警告都打开,指针偏移计算这类代码最容易出隐式类型转换问题。
gcc -O2 -Wall -Wextra -o nids main.c parser.c detect.c ./nids capture.pcap逻辑说明:-O2开启优化,解析循环是热点,优化后处理大文件快很多。-Wall -Wextra会提示比如buf[14] & 0x0f这种char提升为int的符号问题,如果buf是char *且某字节大于 127,符号扩展会导致ip_hl算错,所以缓冲区应该声明为uint8_t *或unsigned char *。参数上,输入文件路径作为argv[1]传入,输出默认打到 stdout,可以重定向到文件。如果 PCAP 很大,建议加一个-r参数支持只读模式并打印进度,避免以为程序卡死。
3.2 关键参数:snaplen、超时与规则阈值
虽然这是离线分析,但 PCAP 本身是抓包时生成的,抓包参数直接影响解析结果。snaplen如果设成 96 字节(很多老工具默认值),那只能看到以太网头加 IP 头加 TCP 头,应用层载荷基本被截断,检测规则全部失效。所以拿到一份 PCAP 先确认它的 snaplen,用capinfos或自己读全局头打印snaplen字段。规则阈值方面,同一条规则在同一个流里可能命中几十次,如果每次都打印告警,输出会爆炸。常见做法是按五元组加规则 ID 做去重,同一个流同一条规则只报一次。
| 参数 | 典型值 | 影响 |
|---|---|---|
| snaplen | 65535 | 小于 1500 会截断载荷,检测失效 |
| 规则最小长度 | 4 字节 | 太短误报率高,太长漏报 |
| 去重窗口 | 按流 | 避免同一流重复告警刷屏 |
| 缓冲区大小 | 65536 | 要大于最大包长,否则读不全 |
3.3 输出格式与报告 PDF 的对应关系
程序输出的告警列表通常包含时间戳、源 IP、目的 IP、规则描述。这份输出可以直接整理进报告 PDF 的“实验结果”章节。报告里一般要写清楚测试用的 PCAP 来源(比如公开的恶意流量样本集)、规则集内容、命中数量、误报分析。如果报告要求画图,可以把告警按协议或按源 IP 做统计,用 Python 或 Excel 生成柱状图。注意报告里不要只贴截图,要把关键代码片段和解析偏移的计算过程写进去,这才是高分项目区别于“只会跑脚本”的地方。
4. 避坑与排查:解析器翻车的五个真实场景
4.1 告警一条都不出,但 PCAP 明明有流量
现象:程序正常退出,输出为空。原因:最常见的是字节序没处理,magic判断反了,导致incl_len读成一个巨大值,后续fread直接失败但没检查返回值。解决:在read_global_header后打印magic和snaplen,确认字节序;每个fread都检查返回值,失败就break并打印已处理包数。
4.2 解析到一半程序崩溃
现象:处理某个包时 segfault。原因:ip_hl或tcp_hl算出来是 0 或超过缓冲区长度,指针越界。比如 IP 头首部长度字段被篡改或包被截断。解决:在计算payload之前加边界检查,if (14 + ip_hl + tcp_hl > incl_len) continue;,并且ip_hl至少为 20,tcp_hl至少为 20。
4.3 中文或二进制载荷匹配不到
现象:规则里写了中文关键词,但 PCAP 里明明有却匹配不到。原因:strlen对中文按字节算没问题,但如果规则文件用 UTF-8 保存而代码里用char比较,理论上可以匹配;真正的问题往往是载荷被 snaplen 截断,或者规则串里混入了不可见字符。解决:用十六进制编辑器确认规则串字节,打印payload_len确认载荷完整。
4.4 大文件处理内存暴涨
现象:处理 1GB PCAP 时内存占用持续上升。原因:每个包都malloc缓冲区但忘记free,或者把告警信息全部存进链表不释放。解决:包缓冲区在循环外分配一次,复用;告警如果要去重,用固定大小的哈希表而不是无界链表。
4.5 同一攻击被报了几百次
现象:一个扫描包触发几百条相同告警。原因:没有按流去重,每个包都独立匹配。解决:维护一个以五元组为键的表,记录已告警的规则 ID,命中过就跳过。简单实现可以用一个固定数组加线性查找,流数量不大时够用。
5. 从能跑到好用:规则热加载与检测效果验证
把解析器跑通只是第一步,真正让这套 NIDS 有实用价值的是规则可维护和效果可验证。我一般会做两件事:规则文件热加载和误报率统计。规则文件用简单的文本格式,每行“模式串|描述”,启动时读入数组,运行中如果收到 SIGHUP 信号就重新读一遍,这样加规则不用重启进程。验证方面,找一份带标注的 PCAP(比如公开的 CTF 流量或恶意样本集),跑完后统计命中规则数和实际恶意流数量的比值,算一个粗略的召回率。如果召回率低,先检查 snaplen 和规则长度;如果误报高,把规则串加长或加协议限定条件。
/* 规则热加载:SIGHUP 触发重读 */ volatile sig_atomic_t reload_flag = 0; void on_sighup(int sig) { reload_flag = 1; } /* 主循环里 */ if (reload_flag) { load_rules("rules.txt"); reload_flag = 0; }逻辑说明:信号处理函数里只设标志位,实际加载放在主循环,避免在信号上下文里做malloc等非异步安全操作。参数上,rules.txt路径可以做成命令行参数,默认当前目录。加载时先清空旧规则数组再重新读,注意释放旧字符串内存。
验证检测效果时,我会把告警按源 IP 聚合,看是不是集中在少数几个 IP 上。如果某个 IP 触发了几百条不同规则,大概率是扫描行为,可以单独加一条“端口扫描”规则来合并告警。反过来,如果告警分散在很多 IP 但每条只命中一次,可能是规则太宽泛,需要收紧。这套流程走下来,一个课程设计级别的 NIDS 就能讲出从字节解析到规则匹配再到效果评估的完整故事,报告里也有实打实的数据支撑。希望帮到你。
本文还有配套的精品资源,点击获取