年轻的时候谈的恋爱就像TCP连接,恋爱时三次握手便可,可分手时却分了四次。而经常久久的爱情,更像是icmp协议,不管对方身在何处,不管是不是可靠链接,不管你什么时候去ping她/他,她/他都默默地响应你。这篇文章就是说说,如何在内核中增长几行代码,让你的女神/男神当ping你(的服务器)的时候,来传达表达你的爱。效果以下(左边为ping的结果,须要破解ascii码转换为对应字符,右边为使用tcpdump抓包直接读取的信息):html
对于UNIX_LIKE系统来讲,若是ping的发送内容与接收内容不一样,会显示不一样的部分,那么就让你的女神或者男神,慢慢将ASCII码解析成你想告诉她/他的话吧。或者告诉她/他,使用tcpdump来直接抓包隐藏在ping中的悄悄话。(对于windows来讲本人没有充分测试,只是知道不会像unix_like系统同样直接显示出请求消息和回显消息的不一样,因此须要你们抓包认真提取信息)linux
学过计算机网络的必定知道,一个网络包的封装主要由多个属于不一样网络协议层的报文头和用户数据共同组成:链路层报文头+网络层IP报文头+传输层报文头+携带的内容+帧尾。而ICMP报文在整个以太帧位于以下位置: git
上图显示的是一个未分片ICMP报文或者是一个较长ICMP报文的第一个IP分片的报文(被分片的报文中不会带有ICMP报头)。RFC792(https://tools.ietf.org/html/r...)中定义了11种ICMP报文类型,经过ICMP报头8bit"类型"字段进行区分。而且每种"类型“会和其”代码"字段以及报文头的最后4字节,共同表达每种报文类型所表示的信息。这些ICMP报文类型被主要分为差错报文和查询报文:github
ping做为ICMP协议最为典型的运用,主要和回送请求,和回送应答这两个类型相关,这也是本文主要关心的两个类型。固然,当主机不可达或者网络路由不可达出现的时候,ping会收到路由器传来的TYPE为3的目标主机不可达的报文(咱们能够经过tcpdump抓包获取)。对于其余的类型,有兴趣的同窗能够自行学习,如icmp重定向攻击,洪水攻击都是利用了ICMP协议进行的网络攻击。编程
做为本文的主角之一ping,有必要动手写一个简单的ping,帮助咱们更好的理解整个请求应答的过程。我本人的测试机器centos 7中使用的是iputils这个工具进行ping操做,因此咱们能够从iputils源码入手学习如何写一个简单的ping。windows
学习过c网络编程的必定都了解socket套接字这个概念。对于ping来讲发送请求和接受应答也一样是经过套接字来完成。只不过,ICMP协议虽然在内核中和TCP、UDP类似属于L4层协议,可是本质是附属于IP协议的网络层协议,因此须要使用原始套接字(SOCK_RAW)构建套接字,而非TCP或UDP使用的流式套接字(SOCK_STREAM)和数据包式套接字(SOCK_DGRAM)。SOCK_RAW的用途在于用户能够自定义填充IP报文头,而且对于ICMP报文自定义填充ICMP报文头。下面一张图,展现了代码中整个ping的逻辑发送以及处理应答的逻辑。centos
具体代码能够参考这个:https://github.com/xiaobaidemu/myping/blob/master/ping.c 整个流程很是简单,须要说明的是,对于ping 127.0.0.1来讲,程序极有可能先收到type为0的回显请求报文,再收到type为8的回显应答报文。这是由于icmp报文能够同时被内核接收处理,也会被原始套接字接收处理,以下为Understanding Linux Network Internals书中所述。服务器
理解了ping的整个过程,接下来就是须要修改内核来传达你想说的话。可是最重要的是,须要分析出修改的位置,即回显应答可能发送的字节在内核代码中的位置。这里有一个很是重要的结构体——struct sk_buff,其定义位于<include/linux/skbuff.h>。网络
内核中sk_buff结构体作到了能够不使用拷贝或删除的方式,使得数据在各层协议之间传输——即移动指针头的方式,具体为在处理不一样的协议头时,表明协议头的指针,指向的是不一样数据区域(如从L2到L4层协议,分别指向二层mac头,三层IP头,四层传输头)。如下是几个比较重要和混淆的字段说明,结合示意图说明:并发
上图简单说明了四个指针和指向区域之间的关系。另外对于data_len和len的关系,若是假设icmp报文比较小,ip层不会对其分片,那么data_len即为0,而len即为当前协议头长度+数据报文长度。关于data_len和len之间的关系涉及到skb_shared_info这个结构体的相关内容,由于和文章中心关系不大,有兴趣的同窗能够自行查阅一下文章来学习
上述内容中data指针和表征协议层数据长度的len,和后文中修改的sk_buff指向的数据直接相关。另外sk_buff关联了众多其余结构体,这里只简要的讲解部分重要的字段含义,更为具体详细的说明能够参考Understanding Linux Network Internal第二章或者https://blog.csdn.net/YuZhiHui_No1/article/details/38666589系列文章进行更深刻学习。
了解了sk_buff结构体,以后须要定位处理icmp协议的文件在哪里。icmp.c位于内核目录中net/ipv4/icmp.c中,且ICMP协议一般是静态编译至内核中,而非经过模块配置的。这里我从Understanding Linux Network Internal这本书中抠出来一张Big Picture,来简要说明一下对于ping发出的回显请求,sk_buff结构体对象是如何在icmp中众多函数中传递。
首先ip_local_deliver_finish会传递ICMP消息到icmp_rcv, icmp_rcv会解析icmp报头中类型字段,对于属于查询报文的类型(如type8)会传递给icmp_reply, 而对于差错报文会传递给icmp_send处理,而且ICMP协议也会和其余诸如TCP/UDP协议进行交互传递信息。对于ping进程发出的请求,会先传递给icmp_echo函数进行处理。而icmp_echo正是处理ping请求很重要的一步,内核会把请求中附带的数据报文部分原封不动的拷贝并发送回源主机。所以咱们能够在icmp_echo函数中,添加进咱们"爱的语句"。
static bool icmp_echo(struct sk_buff *skb) { struct net *net; net = dev_net(skb_dst(skb)->dev); if (!net->ipv4.sysctl_icmp_echo_ignore_all) { struct icmp_bxm icmp_param; icmp_param.data.icmph = *icmp_hdr(skb); icmp_param.data.icmph.type = ICMP_ECHOREPLY; icmp_param.skb = skb; //-----------添加开始----------- char sentence1[] = "I LOVE U, xxxx."; char sentence2[] = "I MISS U, xxxx."; char sentence3[] = "Happy Valentine's Day!"; int sentence_len_list[] = {sizeof(sentence1), sizeof(sentence2), sizeof(sentence3)}; char* sentence_list[] = {sentence1, sentence2, sentence3}; int sentence_index = icmp_param.data.icmph.un.echo.sequence % 3; if(skb->len >= 16 + sentence_len_list[sentence_index]) { char* tmp = (char*)(skb->data+16); char* target_sentence = sentence_list[sentence_index]; int i=0; for(;i<sentence_len_list[sentence_index];++i) { tmp[i] = target_sentence[i]; } for(;i < skb->len-16;++i) { tmp[i] = 0; } } //-----------添加结束------------ icmp_param.offset = 0; icmp_param.data_len = skb->len; icmp_param.head_len = sizeof(struct icmphdr); icmp_reply(&icmp_param, skb); } /* should there be an ICMP stat for ignored echos? */ return true; }
上述代码中icmp_bxm结构体包含了在后续icmp消息传递过程当中的全部须要的信息,包括icmp报文头,sk_buff对象,icmp 报文payload大小等。须要注意的是,因为icmp_rcv已经解析过sk_buff中属于icmp协议的报文头部分,因此参数中skb->data指向的是icmp数据部分,即不包含报文头,而skb->len也只有icmp数据部分的长度。假设ping请求中所带的数据部分为56字节,则此时skb->len大小为56。因为ping数据部分的前16字节为携带的是发送是struct timeval对象——发送时的时间,因此在真实替换时,从data指向的数据部分的第16个字节开始,用memcpy复制到对应区域,或者如上例子傻傻的循环赋值便可。上面代码所表示的就是根据echo请求中seq_id循环回复上述三句话。固然有创意的小伙伴能够增长更多表达难度。
分析完了整个icmp处理流程,和修改方法,咱们只须要建立一个阿里云ECS,并简单编译修改后的内核便可。具体流程以下:
至此告诉你的女神/男神,你想说的话都在ping中。
部分参考文章:
本文为云栖社区原创内容,未经容许不得转载。