尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
ICMP校验和构造原理与伪首部计算详解
简介本资源是一份面向网络协议学习者与安全开发初学者的ICMP数据包构造实践代码包聚焦底层网络通信原理理解与手工构造能力训练。资源包含3个核心文件C实现的ICMP发送主程序ICMP.cpp、封装ICMP头部结构与校验逻辑的头文件ping.h以及说明构造要点与使用方法的文本指南Readme.txt整体压缩包仅5KB轻量易用。已有1611人学习下载适合计算机网络课程实验、CTF协议分析入门或Wireshark抓包验证教学场景。读者可直接编译运行代码生成自定义类型/代码字段的ICMP报文如回显请求/应答结合Wireshark实时捕获并比对字段值深入掌握IP封装流程、ICMP校验和计算、类型代码语义等关键细节为网络编程与协议逆向打下扎实基础。1. ICMP数据包构造为什么一个“ping不通”的错误可能根本不是网络断了而是你发出去的字节序列连校验和都没算对你执行ping 192.168.1.1成功但用自定义工具发一个结构完全一样的ICMP Echo Request却收不到响应——不是目标主机防火墙拦了也不是路由丢了而是你构造的那64字节里第20–21字节ICMP校验和字段填的是0x0000而内核协议栈在收到时直接把它当成了损坏包丢弃。这不是玄学是ICMP协议层最硬的铁律校验和不通过包在IP层之上就被静默丢弃连ICMP错误消息都不会触发。很多人以为“构造ICMP”就是memcpy几个字段结果卡在收不到reply上三天翻遍Wireshark抓包对比也看不出门道——因为Wireshark默认只显示“ICMP Echo (ping) request”它把底层字节校验失败这个关键判定过程藏成了灰色小字“Bad checksum”。这份《ICMP数据包构造》资源不是教你怎么调用ping命令而是带你从RFC 792原始定义出发手撕每一个字节的生成逻辑校验和怎么跨16位累加、为什么需要补零填充、ID与Sequence字段为何必须按主机字节序写入再按网络字节序传输、ICMP头部与载荷如何拼接才不越界。它解决的不是“能不能发”而是“发出去的每个字节是否被协议栈当作合法ICMP对待”。2. ICMP协议帧结构从RFC 792到内存布局为什么校验和计算必须覆盖整个ICMP报文含伪首部ICMP本身没有端口、不建连接、不重传但它对字节级精度的要求比TCP SYN包还苛刻。它的合法性判断发生在IP层交付给ICMP模块的瞬间而判断依据只有两个IP首部校验和由网卡或内核IP模块完成 ICMP校验和由ICMP模块独立计算。前者确保IP层没传错后者确保ICMP载荷本身无篡改。但关键在于ICMP校验和的计算范围并非仅限于ICMP头部数据而是必须包含一个“伪IP首部”——这是RFC 792明确定义、但绝大多数初学者直接忽略的致命细节。2.1 伪首部不是真实传输的字节却是校验和计算的强制输入RFC 792规定ICMP校验和计算前需临时拼接一个12字节的“伪首部”pseudo-header其结构如下字段长度字节含义示例值十六进制源IP地址4发送方IPv4地址C0 A8 01 01192.168.1.1目的IP地址4接收方IPv4地址C0 A8 01 02192.168.1.2零填充1固定为0x0000协议号1ICMP协议号101ICMP报文总长度2ICMP头部数据长度字节00 3856字节提示这个伪首部绝不会出现在实际网络帧中它只存在于校验和计算的内存上下文中。Wireshark抓包看到的ICMP帧开头就是真正的ICMP头部Type字段没有这12字节。但你的代码在调用checksum()函数时必须手动构造这个结构体并作为输入参数传入。2.2 校验和算法16位反码求和为什么必须“补零”且“高位在前”ICMP校验和采用经典的16位反码求和ones complement sum步骤严格如下将待校验数据伪首部 ICMP报文按每2字节为单位分组若总字节数为奇数则在末尾补一个字节0x00注意不是补0x0000是补单字节0将所有16位组相加若产生进位则将进位加到低16位即“回卷”对最终16位结果取反码bitwise NOT得到校验和。下面是一段可验证的C语言实现它严格遵循上述步骤// 计算校验和data指向待校验内存起始地址len为字节数 uint16_t icmp_checksum(const uint8_t *data, size_t len) { uint32_t sum 0; const uint16_t *ptr (const uint16_t *)data; // 按16位累加 while (len 1) { sum *ptr; len - 2; } // 处理剩余单字节奇数长度 if (len 1) { sum *(const uint8_t *)ptr; // 注意此处是uint8_t非uint16_t } // 回卷将32位sum的高16位加到低16位 while (sum 16) { sum (sum 0xFFFF) (sum 16); } // 取反码 return (uint16_t)(~sum); }这段代码的关键点在于ptr强制类型转换为uint16_t*意味着CPU会以网络字节序大端解释内存中的两个连续字节。如果你的机器是小端x86/x64*(uint16_t*)\x01\x02读出来是0x0201而非0x0102—— 这正是我们需要的校验和计算要求字节流按网络序解释所以直接按uint16_t*读取天然符合要求。补单字节0x00的逻辑在if (len 1)分支中且用uint8_t*读取避免越界或符号扩展。while (sum 16)循环处理多轮进位确保最终结果稳定在16位内。2.3 ICMP Echo Request完整帧布局从Type到Data每个字段的字节位置都决定生死一个标准的ICMP Echo RequestType8帧在内存中必须严格按以下顺序排列共56字节示例不含IP首部偏移字节字段长度值示例说明0Type108必须为8表示Echo Request1Code100必须为0ICMP Echo无子类型2–3Checksum2?? ??计算后填入初始置04–5Identifier212 34主机字节序写入但校验和计算时按网络序解释6–7Sequence Number200 01同Identifier用于匹配Request/Reply8–55Data48abcdefghijklmnopqrstuvw...可选载荷常为ASCII字符或递增字节注意Identifier和Sequence Number字段在内存中是以主机字节序写入的比如x86上htons(0x1234)写入为0x12 0x34但校验和计算时它们被当作uint16_t读取CPU自动按当前平台字节序解释。由于我们构造的是网络报文而校验和算法要求“按网络序解释字节”因此必须确保这两个字段在内存中的存储顺序与网络序一致——这就是为什么必须用htons()转换后再写入。如果直接写0x1234到内存而不转换在小端机上存为0x34 0x12校验和就会算错。3. 构造流程实战从零开始组装一个可被Linux内核接受的ICMP Echo Request构造一个能被目标主机正常响应的ICMP包不是简单地把字段塞进缓冲区而是一套有严格时序和依赖关系的操作链。核心矛盾在于校验和字段自身参与校验和计算因此必须先清零再计算最后填入。任何一步顺序错误都会导致整个包被丢弃。3.1 内存缓冲区分配与初始化为什么必须用memset清零整个缓冲区我们通常申请一块足够大的uint8_t buf[1024]作为原始报文缓冲区。关键第一步不是填Type而是memset(buf, 0, sizeof(buf)); // 必须全清零原因有三ICMP校验和算法要求未使用的字节如Data部分不足48字节时的尾部必须为0否则会影响累加结果Identifier和Sequence Number字段若未显式赋值残留垃圾值会导致校验和错误某些内核版本如较老的2.6.x在解析ICMP时会对Code字段做额外检查非0值可能被直接拒绝而memset确保Code0。3.2 伪首部构造如何安全地拼接IP地址与ICMP长度伪首部不占用真实网络带宽但它是校验和计算的强制输入。构造时需极度小心字节序struct pseudo_header { uint32_t src_ip; // 网络字节序 uint32_t dst_ip; // 网络字节序 uint8_t zeros; // 0x00 uint8_t protocol; // 1 (ICMP) uint16_t icmp_len; // ICMP报文总长网络字节序 }; struct pseudo_header ph; ph.src_ip htonl(INET_ADDR(192.168.1.1)); // 转为网络序 ph.dst_ip htonl(INET_ADDR(192.168.1.2)); ph.zeros 0; ph.protocol 1; ph.icmp_len htons(56); // ICMP总长56字节必须htons // 将伪首部拷贝到临时缓冲区用于校验和计算 uint8_t checksum_buf[1024]; memcpy(checksum_buf, ph, sizeof(ph)); memcpy(checksum_buf sizeof(ph), icmp_payload, icmp_payload_len);关键点ph.icmp_len必须用htons()转换因为伪首部中该字段定义为“网络字节序”而htons(56)在x86上生成0x00 0x38若直接赋值56则存为0x38 0x00小端校验和必然失败。3.3 校验和注入为什么必须“先置零再计算后填入”这是整个构造流程中最易出错的环节。正确顺序如下// 步骤1在ICMP payload缓冲区中将Checksum字段偏移2-3置零 uint8_t *icmp buf; // 指向ICMP头部起始 icmp[2] 0; icmp[3] 0; // 步骤2构造完整待校验数据伪首部 ICMP payload uint8_t *calc_data malloc(sizeof(ph) icmp_payload_len); memcpy(calc_data, ph, sizeof(ph)); memcpy(calc_data sizeof(ph), icmp, icmp_payload_len); // 步骤3计算校验和 uint16_t csum icmp_checksum(calc_data, sizeof(ph) icmp_payload_len); // 步骤4将结果以网络字节序填入ICMP头部 icmp[2] (csum 8) 0xFF; // 高字节 icmp[3] csum 0xFF; // 低字节 free(calc_data);如果跳过步骤1不置零那么icmp_checksum()会把原icmp[2-3]的随机值也纳入计算导致结果永远不匹配如果步骤4中直接写*(uint16_t*)icmp[2] csum在小端机上会把csum0x1234写成0x34 0x12而接收方按网络序解析期望0x12 0x34校验失败。4. 常见问题排查5个让开发者通宵调试的真实踩坑记录ICMP构造看似简单实则处处是坑。以下是某开发者在模拟项目X中反复验证的5个高频问题每个都附带Wireshark抓包特征、根本原因及修复方案。4.1 现象Wireshark显示“ICMP Echo (ping) request”但目标主机tcpdump无任何ICMP包捕获原因发送方使用了AF_INET套接字但未设置SOCK_RAW权限或未绑定到IPPROTO_ICMP协议号导致内核拒绝投递。更隐蔽的情况是程序以普通用户运行而Linux默认禁止非root用户创建RAW socketCAP_NET_RAWcapability缺失。解决检查socket创建int sock socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);若报错Operation not permitted用sudo setcap cap_net_rawep ./your_program授予权限或改用root运行在Wireshark中过滤ip.proto 1确认是否真发出而非仅显示“request”字样。4.2 现象Wireshark抓到请求包但显示“Bad checksum”且目标主机无响应原因校验和计算时未包含伪首部或伪首部中icmp_len未用htons()转换或Identifier/Sequence字段未用htons()写入导致字节序错乱。解决用Wireshark右键“Protocol Preferences → ICMP → Validate checksums”开启校验手动计算伪首部ICMP payload的校验和与Wireshark显示的“Calculated”值比对重点检查ph.icmp_len htons(56)和icmp[4-5] htons(id)两处。4.3 现象请求包能发出目标主机也返回Reply但Reply的Identifier或Sequence字段与Request不匹配原因Request中Identifier和Sequence字段在内存中以主机序写入但未用htons()转换导致小端机上存为低位在前而目标主机无论大小端均按网络序解析造成数值错乱。解决严格使用htons()*(uint16_t*)icmp[4] htons(my_id);验证在Request包中icmp[4-5]应为0x12 0x34对应id0x1234而非0x34 0x12。4.4 现象同一台机器上用ping命令能通但自定义程序发的包始终超时Wireshark显示请求包长度异常如60字节而非56字节原因ping命令默认发送56字节Data总ICMP长8(头)5664字节而你的程序可能误将icmp_len设为sizeof(ICMP_HEADER)data_len却忘了ICMP头部是8字节导致icmp_len64但实际payload只写了56字节末尾8字节为随机内存垃圾校验和必错。解决显式定义#define ICMP_HDR_LEN 8icmp_len ICMP_HDR_LEN data_len用hexdump -C打印你构造的buf前64字节确认第8–63字节是否为你预期的Data内容。4.5 现象程序在本机ping 127.0.0.1成功但发给局域网其他主机时Wireshark显示“Destination unreachable (Port unreachable)”原因目标主机防火墙如iptables默认丢弃非ping命令发起的ICMP Echo Request或系统禁用了net.ipv4.icmp_echo_ignore_all1。这不是你包的问题而是目标策略。解决在目标主机执行sysctl net.ipv4.icmp_echo_ignore_all若输出1则需sudo sysctl -w net.ipv4.icmp_echo_ignore_all0检查iptablessudo iptables -L INPUT | grep icmp若存在DROP规则临时清空sudo iptables -P INPUT ACCEPT。5. 设计取舍与验证方法为什么我坚持用sendto()而非send()以及如何用BPF过滤器做原子级验证构造ICMP包的终极目标不是“发出去”而是“被对方协议栈当作合法ICMP处理”。这就引出两个关键设计取舍传输接口的选择以及验证手段的可靠性。很多教程推荐用send()配合已连接的raw socket但我在模拟项目X中坚持用sendto()原因在于连接状态会隐式修改报文语义而ICMP本质是无连接的。5.1sendto()vssend()连接状态带来的字节序陷阱当你调用connect(sock, (struct sockaddr*)dest, sizeof(dest))后再用send(sock, buf, len, 0)发送内核会自动填充IP首部的源地址和目的地址。这看似省事但埋下隐患connect()传入的sockaddr_in.sin_addr是网络字节序但某些旧版glibc在内部处理时可能对sin_port字段做二次字节序转换更严重的是send()在填充IP首部时会将sockaddr_in.sin_addr直接复制为IP首部的daddr但此时若你的ICMP伪首部中dst_ip字段仍用主机序赋值如ph.dst_ip dest.sin_addr.s_addr就会出现“伪首部dst_ip”与“IP首部daddr”不一致——而校验和计算依赖前者IP层转发依赖后者两者错位必然失败。sendto()则完全规避此风险你显式提供sockaddr_in*内核仅从中提取目的IP用于IP首部填充而伪首部由你手动构造二者来源统一可控性极强。我的血泪经验是只要sendto()的第一个参数sock正确第二个参数buf内容正确第三个参数len准确第四个参数flags0第五个参数dest地址正确就一定能发出预期字节流。5.2 BPF过滤器在内核态做原子级验证绕过用户态抓包干扰Wireshark依赖libpcap而libpcap工作在AF_PACKET套接字上其抓包时机晚于内核协议栈处理。这意味着一个因校验和错误被内核静默丢弃的ICMP包Wireshark可能根本抓不到因为没进入AF_PACKET队列或者抓到但标记为“Malformed”。要真正确认“包是否到达ICMP模块”必须在更早的路径上拦截。Linux提供了SO_ATTACH_FILTER机制允许你在raw socket上挂载Berkeley Packet FilterBPF程序。下面是一个验证ICMP Echo Request是否被内核接收的最小BPFstruct sock_filter code[] { BPF_STMT(BPF_LD BPF_H BPF_ABS, 12), // 加载IP首部协议字段偏移12 BPF_JUMP(BPF_JMP BPF_JEQ BPF_K, 1, 0, 3), // 若!1ICMP跳过后续 BPF_STMT(BPF_LD BPF_B BPF_ABS, 20), // 加载IP首部IHL字段偏移20取低4位 BPF_STMT(BPF_ALU BPF_MUL BPF_K, 4), // IHL * 4 IP首部长度 BPF_STMT(BPF_LD BPF_B BPF_IND, 0), // 从IP首部起始偏移处读ICMP Type BPF_JUMP(BPF_JMP BPF_JEQ BPF_K, 8, 0, 1), // 若Type8accept否则reject BPF_STMT(BPF_RET BPF_K, 0), // 拒绝 BPF_STMT(BPF_RET BPF_K, 65535), // 接受全部64KB }; struct sock_fprog bpf { .len sizeof(code)/sizeof(code[0]), .filter code }; setsockopt(sock, SOL_SOCKET, SO_ATTACH_FILTER, bpf, sizeof(bpf));这段BPF在recvfrom()之前运行它直接从内核sk_buff中提取IP首部和ICMP Type仅当Type8时才将包交付给你的recvfrom()。如果recvfrom()始终阻塞说明包根本没通过校验和检查被ICMP模块丢弃如果能收到则证明你的构造已通过最严苛的内核检验。从那以后我每次调试ICMP构造都强制走一遍这个BPF验证流程先用sendto()发包再用带BPF的recvfrom()等响应同时开Wireshark抓包比对。三者结果一致才敢说“这个包是合法的”。希望帮到你。本文还有配套的精品资源点击获取
RELATED

相关推荐

千问Qwen大模型技术报告:预训练、对齐与开源选型解析

千问Qwen大模型技术报告:预训练、对齐与开源选型解析

简介:面向大语言模型研究者、算法工程师与高校师生的Qwen技术资料,聚焦通义千问开源模型的技术路线与实现细节,适合希望系统理解国产开源大模型训练与对齐方法的中高级读者。包内为1个PDF文件,压缩包约1.53MB,主要内容…

📅 2026/11/6 3:53:19
3d 打印缩略图处理操作手册

3d 打印缩略图处理操作手册

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📅 2026/11/6 3:48:19
第六篇:《Consumer 深度实战:位移提交、再平衡与消费组协调》

第六篇:《Consumer 深度实战:位移提交、再平衡与消费组协调》

在上一篇文章中,我们通过 acksall、幂等性生产者和事务消息,构建了数据写入端的可靠性。但写入可靠不等于消费可靠。Consumer 面临的问题完全不同:它需要跟踪“我消费到了哪里”,而这个跟踪机制(位移提交)的…

📅 2026/11/6 3:48:19
MORE NEWS

更多资讯

📰

Bittware VV4 移植 Corundum:开源 100G 网卡 FPGA 实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

RISC-V微内核Hypervisor Bao在BPI-SM10上的深度移植实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

ESP32-P4NRW32X工业芯片深度解析:RS485远距通信与双核异构架构

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

光伏逆变器参数设计:从组串配置到MPPT与LCL滤波的工程实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

STM32CubeMX从入门到精通:图形化配置与代码生成实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

AI编程代理工程化协作:从GitHub Trending看团队落地

1. 近几周的 Trending 榜单到底在释放什么信号说实话,我每周一早上刷 GitHub Trending 已经成了固定动作,比看行业资讯还要准时。最近几周的榜单肉眼可见地变了一个调性:AI 编程代理类项目依然霸榜,但热度明显从“炫技”转向了“怎…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬