ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

基于PCAP的C语言网络入侵检测系统:协议解析与规则匹配实战

基于PCAP的C语言网络入侵检测系统:协议解析与规则匹配实战 简介这是一套面向计算机、网络安全相关专业学生的网络入侵检测系统课程设计资料基于libpcap库在指定网卡接口嗅探并分析数据包重点实现TCP SYN泛洪攻击的检测帮助理解TCP/IP协议与多线程编程适合作为期末大作业或课设参考。资源包共17个文件约891KB以6个C源文件和4个头文件为核心涵盖抓包、分发、分析与线程池等模块另附Makefile与shell脚本便于编译运行并配有项目说明文档和课程报告PDF方便对照梳理设计思路与实验结论。内容预览中可见嗅探、分发、分析等源码结构及测试脚本读者可据此掌握从数据包捕获到恶意流量判定的完整流程并借鉴报告中的排错与验证方法。目前已有217人学习下载适合需要完整课设方案与实现细节的读者参考。1. 从一份 PCAP 到告警网络入侵检测系统到底在做什么手头攒了一堆抓包文件Wireshark 里翻来翻去只能靠肉眼找异常这是很多做网络安全课设的同学都会卡住的地方。基于 PCAP 的网络入侵检测系统本质上就是让程序替你干这件事离线读取 pcap 文件逐包解析协议头按规则判断流量里有没有扫描、爆破、异常载荷这类行为命中就输出告警。它不依赖内核模块也不碰网卡混杂模式纯用户态跑用 C 语言写出来体积小、依赖少答辩时能讲清楚每一层解析逻辑。适合谁做课设需要一份能跑通、能讲原理、能扩展规则的实现也适合刚接触网络安全、想从包结构入手理解 IDS 的开发者。下面按「先立住原理、再动手复现、最后避坑」的顺序拆开讲。2. 协议解析与规则匹配IDS 的两条主干2.1 为什么选 PCAP 离线分析而不是实时抓包实时抓包要处理网卡混杂模式、权限、丢包统计课设环境里往往没有 root虚拟机网卡也不一定支持。离线 PCAP 把「采集」和「分析」解耦你只需要一个文件句柄和循环读取逻辑干净复现成本低。常见做法是用 libpcap 的pcap_open_offline打开文件pcap_next_ex逐包取拿到的是链路层帧的起始指针和长度。这里有个容易忽略的点链路层类型不一定是 Ethernet可能是 Linux cooked captureSLL或 raw IP解析前必须用pcap_datalink判断否则偏移量全错。我一般会先写一个link_offset函数根据 DLT_EN10MB、DLT_LINUX_SLL、DLT_RAW 返回网络层起始偏移后面所有解析都基于这个偏移走。2.2 以太网/IP/TCP 头部解析的偏移与边界检查解析的核心是「按固定偏移取字段但每次取之前确认剩余长度够」。以太网头 14 字节含 2 字节类型IPv4 头最小 20 字节IHL 字段决定实际长度TCP 头最小 20 字节data offset 决定实际长度。下面是一段可抄的最小解析骨架#include pcap.h #include stdint.h #include arpa/inet.h #define ETH_HLEN 14 /* 返回网络层起始偏移失败返回 -1 */ int link_offset(int dlt) { switch (dlt) { case DLT_EN10MB: return ETH_HLEN; case DLT_LINUX_SLL: return 16; /* SLL 头固定 16 字节 */ case DLT_RAW: return 0; default: return -1; } } /* 解析单个包caplen 是实际捕获长度 */ void parse_packet(const u_char *pkt, uint32_t caplen, int dlt) { int off link_offset(dlt); if (off 0 || caplen (uint32_t)off 20) return; /* 不够 IPv4 最小头 */ const u_char *ip pkt off; uint8_t ver_ihl ip[0]; if ((ver_ihl 4) ! 4) return; /* 只处理 IPv4 */ uint8_t ihl (ver_ihl 0x0f) * 4; if (ihl 20 || caplen (uint32_t)off ihl) return; uint8_t proto ip[9]; if (proto ! 6) return; /* 只处理 TCPUDP 类似 */ const u_char *tcp ip ihl; if (caplen (uint32_t)off ihl 20) return; uint16_t sport ntohs(*(uint16_t *)(tcp)); uint16_t dport ntohs(*(uint16_t *)(tcp 2)); uint8_t flags tcp[13]; /* 后续把 sport/dport/flags 交给规则引擎 */ }逻辑说明link_offset把链路层差异吃掉parse_packet每取一层都先做长度校验避免越界读。参数上caplen是抓包时实际保存的字节数可能小于原始帧长所以不能用len代替。ihl和 TCP data offset 都要乘 4单位是 4 字节。失败时直接 return不打印因为高速解析时打印会拖慢整体吞吐。常见误用是直接pkt 14硬编码遇到 SLL 抓包就全乱。2.3 规则表设计用结构体数组而不是 if-else 堆砌规则匹配不要写成一长串 if-else扩展和维护都痛苦。我一般定义一个规则结构体字段包括协议、源/目的端口、TCP 标志位掩码、载荷关键字、告警级别然后用数组存规则循环匹配。下面是一个精简版typedef struct { int proto; /* 6TCP, 17UDP, 0任意 */ uint16_t dport; /* 0 表示不限制 */ uint8_t flag_mask; /* 需要匹配的标志位 */ uint8_t flag_val; /* 期望值 */ const char *keyword; /* 载荷关键字NULL 表示不检查 */ int severity; /* 1-3 */ const char *msg; } rule_t; static rule_t rules[] { {6, 22, 0x02, 0x02, NULL, 2, SYN to SSH port}, {6, 445, 0x02, 0x02, NULL, 3, SYN to SMB port}, {6, 0, 0x3f, 0x02, NULL, 1, SYN scan pattern}, {6, 80, 0, 0, select , 3, SQL injection keyword}, }; int match_rules(uint8_t proto, uint16_t dport, uint8_t flags, const u_char *payload, int plen) { int hits 0; for (size_t i 0; i sizeof(rules)/sizeof(rules[0]); i) { rule_t *r rules[i]; if (r-proto r-proto ! proto) continue; if (r-dport r-dport ! dport) continue; if (r-flag_mask (flags r-flag_mask) ! r-flag_val) continue; if (r-keyword payload) { if (plen 0) continue; /* 简化匹配生产环境应做大小写无关和多次出现统计 */ if (!memmem(payload, plen, r-keyword, strlen(r-keyword))) continue; } printf([ALERT][sev%d] %s\n, r-severity, r-msg); hits; } return hits; }逻辑说明flag_mask和flag_val配合可以表达「只看 SYN 位」或「SYNACK 都要」。dport为 0 表示通配。memmem是 GNU 扩展跨平台时可以用自己写的find_bytes替代。参数上severity用来分级答辩时能讲「误报分级处置」。注意载荷匹配要限制plen不能假设 payload 以\0结尾PCAP 里的载荷是二进制直接当字符串处理会读越界。2.4 从 pcap_next_ex 到告警输出的完整主循环主循环负责打开文件、循环取包、调用解析和匹配、最后统计。下面这段可以直接编译运行int main(int argc, char **argv) { if (argc 2) { fprintf(stderr, usage: %s file.pcap\n, argv[0]); return 1; } char errbuf[PCAP_ERRBUF_SIZE]; pcap_t *p pcap_open_offline(argv[1], errbuf); if (!p) { fprintf(stderr, open fail: %s\n, errbuf); return 1; } int dlt pcap_datalink(p); struct pcap_pkthdr *hdr; const u_char *pkt; int rc; uint64_t total 0, alerts 0; while ((rc pcap_next_ex(p, hdr, pkt)) 0) { if (rc 0) continue; /* 离线文件一般不会返回 0 */ total; /* 这里调用 parse_packet内部再调 match_rules */ alerts parse_and_match(pkt, hdr-caplen, dlt); } printf(total%llu alerts%llu\n, (unsigned long long)total, (unsigned long long)alerts); pcap_close(p); return 0; }逻辑说明pcap_next_ex返回 1 表示读到包0 表示超时离线文件基本不出现-1 出错-2 到文件尾。hdr-caplen是实际捕获长度hdr-len是原始长度解析用caplen。参数上errbuf必须传否则出错时没有可读信息。常见坑是忘记判断pcap_datalink以及把pcap_next_ex的返回值当布尔用-1 和 -2 都被当成真。3. 编译、运行与规则扩展把课设跑出可演示效果3.1 编译命令与依赖处理libpcap 在 Ubuntu/Debian 上装libpcap-devCentOS 上装libpcap-devel。编译命令gcc -O2 -Wall -Wextra -o ids ids.c -lpcap-O2开优化解析循环对性能敏感-Wall -Wextra把警告当信号指针偏移写错时能提前发现。如果链接报undefined reference to pcap_open_offline检查-lpcap是否放在源文件后面GCC 链接顺序是从左到右解析符号。Windows 上用 Npcap SDK需要把wpcap.lib和Packet.lib加进链接并且#define WPCAP和#define HAVE_REMOTE放在#include pcap.h之前。3.2 用公开 PCAP 样本验证规则命中验证规则是否生效不能只看程序不崩要看告警数量和预期一致。常见做法是拿公开的恶意流量样本比如包含端口扫描的 pcap先跑一遍统计。下面是一个快速验证脚本# 统计不同目的端口的 SYN 包数量辅助判断扫描行为 tcpdump -r sample.pcap -nn tcp[tcpflags] tcp-syn ! 0 2/dev/null \ | awk {print $5} | awk -F. {print $NF} | sort | uniq -c | sort -rn | head逻辑说明tcpdump -r读离线文件-nn不做名称解析过滤表达式只留 SYN 包。awk取第五列目的地址:端口再按端口统计。如果某个端口出现大量不同源 IP 的 SYN基本就是扫描。参数上head限制输出行数避免刷屏。这个脚本用来交叉验证你的 IDS 是否漏报如果 tcpdump 看到 200 个 SYN 到 22 端口你的程序只报 3 个说明规则或解析有问题。3.3 规则扩展从端口匹配到载荷特征课设要拿高分规则不能只有端口。可以加三类一是阈值类比如「同一源 IP 在 10 秒内对超过 20 个不同端口发 SYN」判为扫描二是载荷类比如 HTTP 请求里出现../判为路径穿越三是协议异常类比如 TCP 标志位同时置 SYN 和 FIN。阈值类需要维护一个哈希表或简单数组做计数下面是一个极简的源 IP 计数结构#define MAX_ENTRIES 4096 typedef struct { uint32_t ip; uint16_t ports[64]; int port_cnt; time_t first_seen; } scan_entry_t; static scan_entry_t table[MAX_ENTRIES]; static int table_cnt 0; /* 返回 1 表示触发扫描告警 */ int check_scan(uint32_t src_ip, uint16_t dport, time_t now) { for (int i 0; i table_cnt; i) { if (table[i].ip ! src_ip) continue; if (now - table[i].first_seen 10) { /* 超时重置 */ table[i].port_cnt 0; table[i].first_seen now; } for (int j 0; j table[i].port_cnt; j) if (table[i].ports[j] dport) return 0; /* 已记录 */ if (table[i].port_cnt 64) table[i].ports[table[i].port_cnt] dport; return table[i].port_cnt 20; } if (table_cnt MAX_ENTRIES) { table[table_cnt].ip src_ip; table[table_cnt].ports[0] dport; table[table_cnt].port_cnt 1; table[table_cnt].first_seen now; table_cnt; } return 0; }逻辑说明线性扫描在 4096 条以内够用课设流量规模不大。first_seen做时间窗口超过 10 秒重置计数。参数上ports数组上限 64超过就不再记录避免单个 IP 撑爆内存。生产环境应该用哈希表加 LRU但课设讲清楚「这里用线性结构简化复杂度 O(n)」反而显得有取舍。4. 避坑与排查PCAP 解析里最容易翻车的五件事4.1 现象程序不报错但一个告警都没有原因通常是链路层类型没判断硬编码pkt 14而样本是 SLL 或 raw IP 抓的偏移全错解析直接 return。解决在main里打印pcap_datalink的返回值对照 DLT 常量确认然后用link_offset统一处理。另一个可能是规则里dport写成了主机字节序而解析出来的是网络字节序转过的检查ntohs是否漏调。4.2 现象解析到载荷时程序崩溃或读到乱码原因把 payload 当 C 字符串处理用strstr或printf(%s)但 PCAP 载荷是二进制没有结尾\0。解决所有载荷操作都用「指针 长度」形式匹配用memmem打印用%.*s并限制长度。参数上payload 长度等于caplen - off - ihl - tcp_hlen算之前确认不为负。4.3 现象大文件跑到一半内存暴涨原因每个包都malloc存一份或者规则匹配时不断往链表里加节点没有释放。解决解析过程零拷贝只在栈上取字段计数表用固定数组超限就丢弃或覆盖最旧。参数上pcap_next_ex返回的pkt指针在下次调用后可能失效不要保存指针要保存就拷贝。4.4 现象同一份 pcap 在 Wireshark 里能看到异常程序却漏报原因Wireshark 做了重组和协议解码你的程序只看单包。比如 SQL 注入关键字跨了两个 TCP 段单包匹配不到。解决课设阶段可以明确「只做单包检测」在报告里写清边界要提升就加简单的流重组按四元组缓存最近几个包。注意流重组会引入内存和超时管理工作量翻倍量力而行。4.5 现象编译通过但运行时报pcap_open_offline返回 NULL原因文件路径不对、文件不是 pcap 格式、或者权限不足。解决先ls -l确认文件存在且可读再用file命令看是不是tcpdump capture file。如果文件是 pcapng 格式老版本 libpcap 可能不支持用editcap -F pcap转成经典 pcap。参数上errbuf里的信息一定要打印出来它比返回值有用得多。5. 把检测结果做成可答辩的证据链课设答辩最怕被问「你怎么证明检测有效」。我的习惯是准备三样东西一份带标注的 pcap 样本、一份程序输出的告警日志、一份和 tcpdump 统计的对照表。告警日志格式建议固定成时间戳 源IP:端口 - 目的IP:端口 规则名 级别方便 grep 和统计。下面是一个把告警重定向并统计的用法./ids sample.pcap alerts.log 21 grep \[ALERT\] alerts.log | awk {print $3} | sort | uniq -c | sort -rn逻辑说明第一行把标准输出和标准错误都写进日志第二行按规则名统计命中次数。参数上awk {print $3}假设告警格式里第三个字段是规则名如果你的格式不同调整列号。这个统计表可以直接放进报告说明每条规则各触发多少次比只写「实现了 IDS」有说服力。再进一步可以加一个--verbose开关把命中包的序号、时间戳、五元组打出来方便和 Wireshark 对照。实现上就是在match_rules命中时多打印几个字段用全局变量控制开关。注意 verbose 模式下不要在大文件上跑输出量会拖慢速度我一般先用head -c截取前几 MB 做样本。最后说一个我踩过的坑早期版本我把规则表写成全局数组匹配时用sizeof(rules)/sizeof(rules[0])算长度后来把规则拆到多个文件数组退化成指针长度算出来是 1所有规则只剩第一条生效。排查了半天才发现是数组退化。现在我会显式传rule_count或者用ARRAY_SIZE宏在定义处算好。这个教训让我养成了「凡是跨函数传数组必带长度」的习惯。希望帮到你。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进