
简介这是一份面向高校计算机、网络安全相关专业学生的课程设计与期末大作业参考项目主题为基于PCAP的网络入侵检测系统采用C语言实现。项目已通过导师指导并获得97分高分评价下载后无需修改即可直接运行适合作为课程设计、期末大作业或网络编程与安全方向的实践素材。压缩包共17个文件约891KB包含6个C源文件、4个头文件、2个Makefile、1个Shell脚本、1份PDF报告、1个Markdown说明及Python辅助脚本等覆盖抓包嗅探、线程池调度、流量分析与告警等核心模块目录结构清晰便于按模块阅读与二次开发。目前已有142人学习下载。读者可获得完整可运行的源码工程、配套使用说明与课程报告PDF以及Makefile与测试脚本帮助快速理解PCAP抓包、入侵检测流程与多线程分析设计并对照报告梳理实现思路与排错方法。1. 从一份 PCAP 到告警这套 C 语言 NIDS 到底在做什么手里攥着一份几百 MB 的 PCAPWireshark 里翻得眼花却还是说不清刚才那波流量里到底有没有人扫端口、有没有人往内网打 webshell——这大概是很多做安全运维或课程设计的人共同的痛点。基于 PCAP 的网络入侵检测系统本质就是把这件「人肉翻包」的活交给程序读离线抓包文件或监听网卡按规则匹配流量特征命中就落一条告警。用 C 语言实现是因为它贴着 libpcap 这层抓包库最近零依赖、跑得快、内存自己管特别适合嵌入式网关、教学演示和需要极致性能的检测节点。这套「源码使用说明报告」的组合面向的是想真正搞懂 NIDS 内部机理的人不是调个 Suricata 规则就完事而是自己写解析器、自己定规则、自己算告警。读完你能拿到一条从 PCAP 解析到规则匹配再到告警输出的完整可复现路径也能看清哪些地方最容易翻车。2. 拆开一个包libpcap 抓包与协议解析的落地骨架2.1 为什么选 libpcap 而不是自己写 raw socket很多人第一反应是用AF_PACKET或SOCK_RAW直接抓觉得这样「更底层更可控」。但真写起来你会发现链路层类型判断、混杂模式设置、抓包过滤表达式编译、跨 Linux/BSD/macOS 的兼容全是体力活。libpcap 把这些都封装好了pcap_open_offline读文件、pcap_open_live抓网卡、pcap_compilepcap_setfilter下 BPF 过滤一套 API 通吃。对 NIDS 来说BPF 过滤尤其关键——你可以在内核层就把非目标流量丢掉只把 TCP/UDP 送进用户态解析性能差距是数量级的。常见做法是离线分析用pcap_open_offline实时检测用pcap_open_live配pcap_setnonblock避免阻塞主循环。2.2 从以太网帧到 TCP 载荷的解析链路一个包进来解析顺序是固定的以太网头14 字节注意可能有 VLAN 标签要偏移 4 字节→ 判断 EtherType 是不是 0x0800IPv4→ IP 头看 IHL 字段算实际长度别写死 20→ 判断协议号是不是 6TCP或 17UDP→ TCP 头看 data offset 算载荷偏移。每一步都要做长度校验否则畸形包直接让你段错误。下面是最小可跑的解析骨架#include pcap.h #include netinet/ip.h #include netinet/tcp.h #include netinet/ether.h void packet_handler(u_char *args, const struct pcap_pkthdr *hdr, const u_char *pkt) { // 以太网头固定 14 字节 if (hdr-caplen 14) return; const struct ether_header *eth (struct ether_header *)pkt; uint16_t eth_type ntohs(eth-ether_type); uint32_t offset 14; // 处理 VLAN 标签802.1Q偏移再加 4 if (eth_type 0x8100) { if (hdr-caplen 18) return; eth_type ntohs(*(uint16_t *)(pkt 16)); offset 4; } if (eth_type ! 0x0800) return; // 只处理 IPv4 if (hdr-caplen offset 20) return; const struct ip *iph (const struct ip *)(pkt offset); uint32_t ip_hlen iph-ip_hl * 4; // 头长度按 4 字节为单位 if (ip_hlen 20) return; // 非法头长丢弃 if (iph-ip_p ! IPPROTO_TCP) return; if (hdr-caplen offset ip_hlen 20) return; const struct tcphdr *tcph (const struct tcphdr *)(pkt offset ip_hlen); uint32_t tcp_hlen tcph-doff * 4; const u_char *payload pkt offset ip_hlen tcp_hlen; uint32_t payload_len ntohs(iph-ip_len) - ip_hlen - tcp_hlen; // 到这里 payload/payload_len 就是 TCP 载荷交给规则引擎 // 注意 payload_len 要用 caplen 再兜一次底防止越界 }逻辑说明ip_hl和doff都是「以 4 字节为单位」的字段忘了乘 4 是最经典的翻车点解析出来的载荷指针会偏。参数说明hdr-caplen是实际抓到的长度hdr-len是原始包长做载荷计算时两者都要考虑抓包时 snaplen 设太小会截断载荷导致漏检。我一般把 snaplen 设成 65535除非你明确只关心头部。2.3 主循环与离线/在线两种模式的切换主循环用pcap_loop或pcap_dispatch前者一直抓到 EOF 或出错后者抓一批就返回适合需要定期做超时检测的场景。离线模式传文件句柄在线模式传网卡句柄回调函数完全复用。下面这段把两种模式统一起来int main(int argc, char *argv[]) { char errbuf[PCAP_ERRBUF_SIZE]; pcap_t *handle; // 参数是文件就走离线是网卡名就走在线 if (argc 1 access(argv[1], F_OK) 0) { handle pcap_open_offline(argv[1], errbuf); } else { handle pcap_open_live(argv[1], 65535, 1, 1000, errbuf); } if (!handle) { fprintf(stderr, open failed: %s\n, errbuf); return 1; } // 只抓 TCP减少用户态负担 struct bpf_program fp; if (pcap_compile(handle, fp, tcp, 0, PCAP_NETMASK_UNKNOWN) 0) { pcap_setfilter(handle, fp); } pcap_loop(handle, 0, packet_handler, NULL); pcap_close(handle); return 0; }逻辑说明pcap_open_live的第三个参数 1 表示混杂模式第四个 1000 是超时毫秒。参数说明BPF 表达式tcp可以换成tcp port 80 or tcp port 443之类越精确内核丢得越多、用户态越轻松。注意pcap_compile的优化参数设 1 会生成更高效的过滤码但调试规则时设 0 更容易看懂。3. 规则引擎把「什么算入侵」翻译成 C 代码能跑的逻辑3.1 规则的数据结构设计从字符串到匹配树NIDS 的核心是规则。最简单的做法是每条规则一个结构体字段包括协议、源/目的 IP、端口、载荷关键字、动作。但规则一多逐条遍历就慢了。常见做法是先用协议和端口做一级索引把规则分桶再在桶内做字符串匹配。下面是一个够用的规则结构#define MAX_PAYLOAD_PATTERN 256 typedef struct { int proto; // IPPROTO_TCP / IPPROTO_UDP uint16_t dport; // 目的端口0 表示任意 char pattern[MAX_PAYLOAD_PATTERN]; // 载荷关键字 int pattern_len; char msg[128]; // 告警描述 int severity; // 1-3 } rule_t; rule_t rules[] { {IPPROTO_TCP, 80, /etc/passwd, 11, 疑似路径穿越, 3}, {IPPROTO_TCP, 80, union select, 12, 疑似 SQL 注入, 3}, {IPPROTO_TCP, 22, SSH-, 4, SSH 连接, 1}, }; int rule_count sizeof(rules) / sizeof(rules[0]);逻辑说明pattern用定长数组是为了避免动态内存嵌入式场景更稳。参数说明dport设 0 表示不限制端口适合做全局特征匹配severity用来分级报告里可以按级别统计。真实项目里规则通常从配置文件读但教学和快速验证阶段硬编码数组最省事。3.2 载荷匹配memmem 与大小写不敏感的取舍匹配载荷最直接的是memmem它在二进制数据里找子串比strstr安全不依赖\0结尾。但 HTTP 攻击特征经常大小写混写UNION SELECT和union select都得命中。这时候要么统一转小写再匹配要么用大小写不敏感的匹配函数。转小写会改动原始载荷如果后面还要做别的检测就得先拷贝。我一般对载荷做一份小写副本专门用于匹配// 在 packet_handler 里拿到 payload 之后 if (payload_len 0 payload_len 8192) { char lower[8192]; for (uint32_t i 0; i payload_len; i) lower[i] tolower(payload[i]); for (int i 0; i rule_count; i) { if (rules[i].proto ! IPPROTO_TCP) continue; if (rules[i].dport ! 0 rules[i].dport ! ntohs(tcph-th_dport)) continue; if (memmem(lower, payload_len, rules[i].pattern, rules[i].pattern_len)) { printf([ALERT] %s | %s:%u - %s:%u\n, rules[i].msg, inet_ntoa(iph-ip_src), ntohs(tcph-th_sport), inet_ntoa(iph-ip_dst), ntohs(tcph-th_dport)); } } }逻辑说明memmem是 GNU 扩展Linux 下直接用其他平台要自己实现一个。参数说明8192这个上限是经验值超过这个长度的载荷做全量小写转换性价比低可以只转前 N 字节或直接跳过。注意inet_ntoa不是线程安全的多线程场景要用inet_ntop。3.3 端口扫描与 SYN Flood 的统计型检测载荷匹配只能抓「内容里有特征」的攻击端口扫描和 SYN Flood 这类没有明显载荷的得靠统计。思路是维护一张源 IP 的计数表单位时间内目的端口数超过阈值就判扫描SYN 包数超过阈值就判 Flood。下面是一个极简的滑动窗口计数#define TABLE_SIZE 1024 typedef struct { uint32_t src_ip; uint16_t ports[64]; // 记录最近访问的端口 int port_cnt; int syn_cnt; time_t window_start; } stat_entry_t; stat_entry_t table[TABLE_SIZE]; void check_scan(uint32_t src_ip, uint16_t dport, int is_syn) { uint32_t idx src_ip % TABLE_SIZE; stat_entry_t *e table[idx]; time_t now time(NULL); if (now - e-window_start 10) { // 10 秒一个窗口 e-src_ip src_ip; e-port_cnt 0; e-syn_cnt 0; e-window_start now; } if (e-src_ip ! src_ip) return; // 哈希冲突简单丢弃 // 端口去重后计数 int found 0; for (int i 0; i e-port_cnt; i) if (e-ports[i] dport) { found 1; break; } if (!found e-port_cnt 64) e-ports[e-port_cnt] dport; if (is_syn) e-syn_cnt; if (e-port_cnt 20) printf([ALERT] 端口扫描 | 源 %u 在 10s 内访问 %d 个端口\n, src_ip, e-port_cnt); if (e-syn_cnt 100) printf([ALERT] SYN Flood | 源 %u 在 10s 内发送 %d 个 SYN\n, src_ip, e-syn_cnt); }逻辑说明哈希表用src_ip % TABLE_SIZE做索引冲突直接丢弃是简化处理真实场景要用链表或开放寻址。参数说明窗口 10 秒、端口阈值 20、SYN 阈值 100 都是可调参数报告里应该说明这些值的选取依据。注意time(NULL)精度是秒高流量场景要用gettimeofday做毫秒级窗口。4. 避坑与排查那些让 NIDS 漏报误报的细节4.1 现象明明有攻击流量程序一条告警都不出原因通常是 BPF 过滤器把流量提前丢了。比如你写了tcp port 80但攻击走的是 8080自然抓不到。另一个常见原因是 snaplen 设太小载荷被截断memmem匹配不到完整特征。解决先用tcpdump -r xxx.pcap -nn确认目标流量确实在文件里再把 BPF 表达式放宽到tcp甚至空逐步收窄定位。4.2 现象程序跑几分钟就段错误九成是解析时没做长度校验。畸形包、截断包、IP 头长度字段被恶意设成非法值都会让指针飞到非法内存。解决每一层解析前都检查caplen是否够ip_hl和doff是否在合法范围IP 头 20-60 字节TCP 头 20-60 字节载荷长度用caplen和ip_len取小值。血泪经验加断言不如加if return线上环境断言会直接崩。4.3 现象告警里 IP 和端口全是乱的多半是字节序问题。网络字节序是大端printf出来之前必须ntohs/ntohl。iph-ip_src是struct in_addr直接当整数打印会得到反的地址。解决IP 用inet_ntoa或inet_ntop端口用ntohs长度字段用ntohs。养成习惯凡是协议头里的多字节字段用之前先转。4.4 现象同一份 PCAP 跑两次告警数量不一样如果用了统计型检测时间窗口依赖time(NULL)两次运行的起始时间不同窗口边界就不同计数自然有差异。解决离线分析时不要用真实时间改成用包的时间戳hdr-ts做窗口基准这样同一份文件每次跑结果一致报告才可复现。4.5 现象规则一多处理速度断崖式下降逐条遍历规则是 O(规则数) 的复杂度规则上千条时每条流量都要跑上千次memmem。解决先按端口分桶只匹配该端口对应的规则再按协议过滤如果还慢上 Aho-Corasick 多模式匹配一次扫描命中所有关键字。教学项目里分桶通常就够了AC 自动机是进阶优化。5. 让检测结果可复现离线回放、告警归并与报告生成离线回放是验证 NIDS 最靠谱的手段。同一份 PCAP固定规则、固定窗口参数跑出来的告警应该完全一致。我一般会写一个回放脚本把告警输出重定向到文件再用sort | uniq -c做归并统计看看哪些规则命中最多、哪些源 IP 最活跃。下面这段把告警按「源 IP 规则」聚合# 假设程序输出格式为[ALERT] msg | src:sport - dst:dport ./nids test.pcap alerts.log # 提取源 IP 和告警类型做聚合 awk -F[|:] /ALERT/ {gsub(/ /,,$2); print $2, $1} alerts.log \ | sort | uniq -c | sort -rn | head -20逻辑说明awk按分隔符切出源 IP 和告警描述uniq -c计数sort -rn按次数倒序。参数说明分隔符要根据你实际的输出格式调整别照抄。这个统计结果直接可以放进报告说明「本次检测共命中 N 类告警其中端口扫描占比最高」。报告里还应该有一张参数表把关键阈值和选取理由写清楚评审或答辩时这是加分项参数取值选取理由snaplen65535避免载荷截断导致漏检BPF 过滤tcp只处理 TCP减少用户态负担扫描窗口10 秒兼顾检测灵敏度和误报率端口阈值20 个正常用户 10 秒内很少访问超 20 个端口SYN 阈值100 个正常建连远低于此值最后说个我自己的习惯每次改完规则或阈值一定拿同一份 PCAP 重跑一遍对比告警差异。没有基线对比的调参就是玄学今天调完觉得对了明天换个流量又翻车。把回放和归并脚本固化成流程比记住任何单个参数都管用。希望帮到你。本文还有配套的精品资源点击获取