天天看點

這個情人節,工程師用阿裡雲來試着表達不一樣的愛意

    年輕的時候談的戀愛就像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/rfc792)中定義了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頭,四層傳輸頭)。以下是幾個比較重要和混淆的字段說明,結合示意圖說明:

指針head/end 從head指針到end指針區域指向的資料塊為真實存儲以太幀資料區域(包括了鍊路成之上的各層協定協定頭和資料封包,且一直不變)
指針data/tail data指針和tail指針表示目前正在處理的協定層的開始和結束為止(其随着處理協定的向高層/低層推進而變化)head<=data<=tail<=end
len data_len和len比較抽象。len表示skbuff中由head到tail指向的資料塊的大小+分片fragment(即skb_shared_info結構體中)非線性資料大小,其大小會随着在核心各層中移動而變化(去掉或者增加了各層協定頭)
data_len data_len僅為分片中非線性資料大小。

這個情人節,工程師用阿裡雲來試着表達不一樣的愛意

   上圖簡單說明了四個指針和指向區域之間的關系。另外對于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循環回複上述三句話。當然有創意的小夥伴可以增加更多表達難度。

四、建立一個阿裡雲ECS伺服器,十分鐘完成所有修改

    分析完了整個icmp處理流程,和修改方法,我們隻需要建立一個阿裡雲ECS,并簡單編譯修改後的核心即可。具體流程如下:

  1. 阿裡雲建立任意規格伺服器(大規格可以加快核心編譯速度,此處建立一個4vcpu伺服器),使用centos作為os
  2. 下載下傳linux核心代碼,并解壓放置到/usr/src/kernels目錄下,本文使用的是4.20.6核心版本。
  3. 編譯前基于原centos系統中/boot目錄下的config檔案,生成編譯配置項,根據此編譯項來定制核心。拷貝原配置檔案至核心檔案目錄 sudo cp /boot/config-3.10.0-693.el7.x86_64 ./.config;執行make oldconfig,生成新的.config檔案
  4. 編譯源碼:make -j 4 ,可能編譯過程中缺少某些庫,此時yum安裝缺少的庫,如openssl-devel, elfutils-libelf-devel
  5. 安裝核心子產品:make modules_install -j 4
  6. 拷貝核心和配置檔案至/boot目錄,并生成System.map檔案:make install -j 4
  7. 更新引導:grub2-mkconfig -o /boot/grub2/grub.cfg
  8. 修改預設預設啟動引導核心:修改/etc/default/grub檔案,将GRUB_DEFAULT設為0,0表示第一個啟動項,即為最新編譯的核心。
  9. 重新開機伺服器:reboot

    至此告訴你的女神/男神,你想說的話都在ping中。

部分參考文章:

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

繼續閱讀