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

賀小白同學發表於2019-02-15

    年輕的時候談的戀愛就像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的邏輯傳送以及處理應答的邏輯。

d5539fcdaacd6b67d62f24fec66dbac4.png    具體程式碼可以參考這個:https://github.com/xiaobaidemu/myping/blob/master/ping.c 整個流程非常簡單,需要說明的是,對於ping 127.0.0.1來說,程式極有可能先收到type為0的回顯請求報文,再收到type為8的回顯應答報文。這是因為icmp報文可以同時被核心接收處理,也會被原始套接字接收處理,如下為Understanding Linux Network Internals書中所述。

2bebcf5e8842c3df6a9f1ca34168228c.png

三、新增核心程式碼前,你只需要知道一個結構體和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僅為分片中非線性資料大小。

033ac771ec98b4a02b8ded38418841cc.png

   上圖簡單說明了四個指標和指向區域之間的關係。另外對於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中眾多函式中傳遞。

5ab37143fc3afc166e2e927d89a8dbbf.png

    首先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

 

 


相關文章