乐趣区

这个情人节,工程师用阿里云来试着表达不一样的爱意

年轻的时候谈的恋爱就像 TCP 链接,恋爱时三次握手即可,可分手时却分了四次。而常常久久的爱情,更像是 icmp 协议,无论对方身在何处,无论是否是可靠连接,无论你何时去 ping 她 / 他,她 / 他都默默地响应你。这篇文章就是说说,如何在内核中增加几行代码,让你的女神 / 男神当 ping 你 (的服务器) 的时候,来传达表达你的爱。效果如下(左边为 ping 的结果,需要破解 ascii 码转换为对应字符,右边为使用 tcpdump 抓包直接读取的信息):

对于 UNIX_LIKE 系统来说,如果 ping 的发送内容与接收内容不同,会显示不同的部分,那么就让你的女神或者男神,慢慢将 ASCII 码解析成你想告诉她 / 他的话吧。或者告诉她 / 他,使用 tcpdump 来直接抓包隐藏在 ping 中的悄悄话。(对于 windows 来说本人没有充分测试,只是知道不会像 unix_like 系统一样直接显示出请求消息和回显消息的不同,所以需要大家抓包认真提取信息)
一、ICMP 协议这些你需要了解:
学过计算机网络的一定知道,一个网络包的封装主要由多个属于不同网络协议层的报文头和用户数据共同组成:链路层报文头 + 网络层 IP 报文头 + 传输层报文头 + 携带的内容 + 帧尾。而 ICMP 报文在整个以太帧位于如下位置:

上图显示的是一个未分片 ICMP 报文或者是一个较长 ICMP 报文的第一个 IP 分片的报文(被分片的报文中不会带有 ICMP 报头)。RFC792(https://tools.ietf.org/html/r…)中定义了 11 种 ICMP 报文类型,通过 ICMP 报头 8bit” 类型 ” 字段进行区分。并且每种 ” 类型“会和其”代码 ” 字段以及报文头的最后 4 字节,共同表达每种报文类型所表示的信息。这些 ICMP 报文类型被主要分为差错报文和查询报文:

查询报文主要包括:回送请求(TYPE8),回送应答(TYPE0),地址掩码或时间戳的请求 / 应答等
差错报文主要包括:目标主机不可达(TYPE3),超时,源抑制,路由重定向等

    ping 作为 ICMP 协议最为典型的运用,主要和回送请求,和回送应答这两个类型相关,这也是本文主要关心的两个类型。当然,当主机不可达或者网络路由不可达出现的时候,ping 会收到路由器传来的 TYPE 为 3 的目标主机不可达的报文(我们可以通过 tcpdump 抓包获取)。对于其他的类型,有兴趣的同学可以自行学习,如 icmp 重定向攻击,洪水攻击都是利用了 ICMP 协议进行的网络攻击。
二、动手写一个简单的 ping,了解 Linux ping
作为本文的主角之一 ping,有必要动手写一个简单的 ping,帮助我们更好的理解整个请求应答的过程。我本人的测试机器 centos 7 中使用的是 iputils 这个工具进行 ping 操作,所以我们可以从 iputils 源码入手学习如何写一个简单的 ping。
学习过 c 网络编程的一定都了解 socket 套接字这个概念。对于 ping 来说发送请求和接受应答也同样是通过套接字来完成。只不过,ICMP 协议虽然在内核中和 TCP、UDP 相似属于 L4 层协议,但是本质是附属于 IP 协议的网络层协议,所以需要使用原始套接字 (SOCK_RAW) 构建套接字,而非 TCP 或 UDP 使用的流式套接字 (SOCK_STREAM) 和数据包式套接字(SOCK_DGRAM)。SOCK_RAW 的用途在于用户可以自定义填充 IP 报文头,并且对于 ICMP 报文自定义填充 ICMP 报文头。下面一张图,展示了代码中整个 ping 的逻辑发送以及处理应答的逻辑。

具体代码可以参考这个:https://github.com/xiaobaidemu/myping/blob/master/ping.c 整个流程非常简单,需要说明的是,对于 ping 127.0.0.1 来说,程序极有可能先收到 type 为 0 的回显请求报文,再收到 type 为 8 的回显应答报文。这是因为 icmp 报文可以同时被内核接收处理,也会被原始套接字接收处理,如下为 Understanding Linux Network Internals 书中所述。

三、添加内核代码前,你只需要知道一个结构体和 icmp.c
理解了 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 这个结构体的相关内容,因为和文章中心关系不大,有兴趣的同学可以自行查阅一下文章来学习

http://blog.51cto.com/weiguozhihui/1586777
https://0x657573.wordpress.com/2010/11/22/the-relation-between-skb-len-and-skb-data_len-and-what-they-represent/
https://blog.csdn.net/farmwang/article/details/54233975

    上述内容中 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 循环回复上述三句话。当然有创意的小伙伴可以增加更多表达难度。
四、创建一个阿里云 ECS 服务器,十分钟完成所有修改
分析完了整个 icmp 处理流程,和修改方法,我们只需要创建一个阿里云 ECS,并简单编译修改后的内核即可。具体流程如下:

阿里云创建任意规格服务器(大规格可以加快内核编译速度,此处创建一个 4vcpu 服务器),使用 centos 作为 os
下载 linux 内核代码,并解压放置到 /usr/src/kernels 目录下,本文使用的是 4.20.6 内核版本。
编译前基于原 centos 系统中 /boot 目录下的 config 文件,生成编译配置项,根据此编译项来定制内核。拷贝原配置文件至内核文件目录 sudo cp /boot/config-3.10.0-693.el7.x86_64 ./.config;执行 make oldconfig,生成新的.config 文件
编译源码:make -j 4 , 可能编译过程中缺少某些库,此时 yum 安装缺少的库,如 openssl-devel, elfutils-libelf-devel
安装内核模块:make modules_install -j 4
拷贝内核和配置文件至 /boot 目录,并生成 System.map 文件:make install -j 4
更新引导:grub2-mkconfig -o /boot/grub2/grub.cfg
修改默认默认启动引导内核:修改 /etc/default/grub 文件,将 GRUB_DEFAULT 设为 0,0 表示第一个启动项,即为最新编译的内核。
重启服务器:reboot

    至此告诉你的女神 / 男神,你想说的话都在 ping 中。
部分参考文章:

Understanding Linux Network Internal 第 2 章 & 第 25 章
https://www.geeksforgeeks.org/ping-in-c/
https://medium.freecodecamp.org/building-and-installing-the-latest-linux-kernel-from-source-6d8df5345980
https://github.com/iputils/ip…

本文作者:贺小白同学阅读原文
本文为云栖社区原创内容,未经允许不得转载。

退出移动版