Linux網絡編程:原始套接字的魔力【下】

可以接收鏈路層MAC幀的原始套接字
       前面我們介紹過了通過原始套接字socket(AF_INET, SOCK_RAW, protocol)我們可以直接實現自行構造整個IP報文,然後對其收發。提醒一點,在用這種方式構造原始IP報文時,第三個參數protocol不能用IPPROTO_IP,這樣會讓系統疑惑,不知道該用什麼協議來伺候你了。
       今天我們介紹原始套接字的另一種用法:直接從鏈路層收發數據幀,聽起來好像很神奇的樣子。在Linux系統中要從鏈路層(MAC)直接收發數幀,比較普遍的做法就是用libpcaplibnet兩個動態庫來實現。但今天我們就要用原始套接字來實現這個功能。
       這裏的2字節幀類型用來指示該數據幀所承載的上層協議是IPARP或其他。
       爲了實現直接從鏈路層收發數據幀,我們要用到原始套接字的如下形式:
 socket(PF_PACKET, type, protocol)
1、其中type字段可取SOCK_RAWSOCK_DGRAM。它們兩個都使用一種與設備無關的標準物理層地址結構struct sockaddr_ll{},但具體操作的報文格式不同:
SOCK_RAW:直接向網絡硬件驅動程序發送(或從網絡硬件驅動程序接收)沒有任何處理的完整數據報文(包括物理幀的幀頭),這就要求我們必須瞭解對應設備的物理幀幀頭結構,才能正確地裝載和分析報文。也就是說我們用這種套接字從網卡驅動上收上來的報文包含了MAC頭部,如果我們要用這種形式的套接字直接向網卡發送數據幀,那麼我們必須自己組裝我們MAC頭部。這正符合我們的需求。
SOCK_DGRAM:這種類型的套接字對於收到的數據報文的物理幀幀頭會被系統自動去掉,然後再將其往協議棧上層傳遞;同樣地,在發送時數據時,系統將會根據sockaddr_ll結構中的目的地址信息爲數據報文添加一個合適的MAC幀頭。
2protocol字段,常見的,一般情況下該字段取ETH_P_IPETH_P_ARPETH_P_RARPETH_P_ALL, 當然鏈路層協議很多,肯定不止我們說的這幾個,但我們一般只關心這幾個就夠我們用了。這裏簡單提一下網絡數據收發的一點基礎。協議棧在組織數據收發流程時 需要處理好兩個方面的問題:“從上倒下”,即數據發送的任務;“從下到上”,即數據接收的任務。數據發送相對接收來說要容易些,因爲對於數據接收而言,網 卡驅動還要明確什麼樣的數據該接收、什麼樣的不該接收等問題。protocol字段可選的四個值及其意義如下:
protocol
作用
ETH_P_IP
0X0800
只接收發往目的MAC本機IP類型的數據幀
ETH_P_ARP
0X0806
只接收發往目的MAC本機ARP類型的數據幀
ETH_P_RARP
0X8035
只接受發往目的MAC本機RARP類型的數據幀
ETH_P_ALL
0X0003
接收發往目的MAC是本機的所有類型(ip,arp,rarp)的數據幀,同時還可以接收從本機發出去的所有數據幀。在混雜模式打開的情況下,還會接收到發往目的MAC爲非本地硬件地址的數據幀。
      protocol字段可取的所有協議參見/usr/include/linux/if_ether.h頭文件裏的定義。
      最後,格外需要留心一點的就是,發送數據的時候需要自己組織整個以太網數據幀。和地址相關的結構體就不能再用前面的struct sockaddr_in{}了,而是struct sockaddr_ll{},如下:
點擊(此處)摺疊或打開
  1. struct sockaddr_ll{
  2.     unsigned short sll_family; /* 總是 AF_PACKET */
  3.     unsigned short sll_protocol; /* 物理層的協議 */
  4.     int sll_ifindex; /* 接口號 */
  5.     unsigned short sll_hatype; /* 報頭類型 */
  6.     unsigned char sll_pkttype; /* 分組類型 */
  7.     unsigned char sll_halen; /* 地址長度 */
  8.     unsigned char sll_addr[8]; /* 物理層地址 */
  9. };
 sll_protocoll:取值在linux/if_ether.h中,可以指定我們所感興趣的二層協議;
       sll_ifindex:置爲0表示處理所有接口,對於單網卡的機器就不存在“所有”的概念了。如果你有多網卡,該字段的值一般通過ioctl來搞定,模板代碼如下,如果我們要獲取eth0接口的序號,可以使用如下代碼來獲取:
點擊(此處)摺疊或打開
  1. struct  sockaddr_ll  sll;
  2. struct ifreq ifr;

  3. strcpy(ifr.ifr_name, "eth0");
  4. ioctl(sockfd, SIOCGIFINDEX, &ifr);
  5. sll.sll_ifindex = ifr.ifr_ifindex;
  sll_hatypeARP硬件地址類型,定義在 linux/if_arp.h 中。 ARPHRD_ETHER時表示爲以太網。
  sll_pkttype:包含分組類型。目前,有效的分組類型有:目標地址是本地主機的分組用的 PACKET_HOST,物理層廣播分組用的 PACKET_BROADCAST ,發送到一個物理層多路廣播地址的分組用的 PACKET_MULTICAST,在混雜(promiscuous)模式下的設備驅動器發向其他主機的分組用的 PACKET_OTHERHOST,源於本地主機的分組被環回到分組套接口用的 PACKET_OUTGOING這些類型只對接收到的分組有意義
       sll_addrsll_halen指示物理層(如以太網,802.3,802.4或802.5)地址及其長度,嚴格依賴於具體的硬件設備。類似於獲取接口索引sll_ifindex,要獲取接口的物理地址,可以採用如下代碼:
點擊(此處)摺疊或打開
  1. struct ifreq ifr;

  2. strcpy(ifr.ifr_name, "eth0");
  3. ioctl(sockfd, SIOCGIFHWADDR, &ifr);
 缺省情況下,從任何接口收到的符合指定協議的所有數據報文都會被傳送到原始PACKET套接字口,而使用bind系統調用並以一個sochddr_ll結構體對象將PACKET套接字與某個網絡接口相綁定,就可使我們的PACKET原始套接字只接收指定接口的數據報文。 
 接下來我們簡單介紹一下網卡是怎麼收報的,如果你對這部分已經很瞭解可以跳過這部分內容。網卡從線路上收到信號流,網卡的驅動程序會去檢查數據幀開始的前6個字節,即目的主機的MAC地址,如果和自己的網卡地址一致它纔會接收這個幀,不符合的一般都是直接無視。然後該數據幀會被網絡驅動程序分解,IP報文將通過網絡協議棧,最後傳送到應用程序那裏。往上層傳遞的過程就是一個校驗和“剝頭”的過程,由協議棧各層去實現。

       接下來我們來寫個簡單的抓包程序,將那些發給本機的IPv4報文全打印出來:
點擊(此處)摺疊或打開
  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <errno.h>
  4. #include <unistd.h>
  5. #include <sys/socket.h>
  6. #include <sys/types.h>
  7. #include <netinet/in.h>
  8. #include <netinet/ip.h>
  9. #include <netinet/if_ether.h>

  10. int main(int argc, char **argv) {
  11.    int sock, n;
  12.    char buffer[2048];
  13.    struct ethhdr *eth;
  14.    struct iphdr *iph;

  15.    if (0>(sock=socket(PF_PACKET, SOCK_RAW, htons(ETH_P_IP)))) {
  16.      perror("socket");
  17.      exit(1);
  18.    }

  19.    while (1) {
  20.      printf("=====================================\n");
  21.      //注意:在這之前我沒有調用bind函數,原因是什麼呢?
  22.      n = recvfrom(sock,buffer,2048,0,NULL,NULL);
  23.      printf("%d bytes read\n",n);

  24.      //接收到的數據幀頭6字節是目的MAC地址,緊接着6字節是源MAC地址。
  25.      eth=(struct ethhdr*)buffer;
  26.      printf("Dest MAC addr:%02x:%02x:%02x:%02x:%02x:%02x\n",eth->h_dest[0],eth->h_dest[1],eth->h_dest[2],eth->h_dest[3],eth->h_dest[4],eth->h_dest[5]);
  27.      printf("Source MAC addr:%02x:%02x:%02x:%02x:%02x:%02x\n",eth->h_source[0],eth->h_source[1],eth->h_source[2],eth->h_source[3],eth->h_source[4],eth->h_source[5]);

  28.      iph=(struct iphdr*)(buffer+sizeof(struct ethhdr));
  29.      //我們只對IPV4且沒有選項字段的IPv4報文感興趣
  30.      if(iph->version ==4 && iph->ihl == 5){
  31.              printf("Source host:%s\n",inet_ntoa(iph->saddr));
  32.              printf("Dest host:%s\n",inet_ntoa(iph->daddr));
  33.      }
  34.    }
  35. }
      編譯,然後運行,要以root身份纔可以運行該程序:
正如我們前面看到的,網卡丟棄所有不含有主機MAC地址00:0C:29:BA:CB:61的數據包,這是因爲網卡處於非混雜模式,即每個網卡只處理源地址是它自己的幀!
這裏有三個例外的情況:
1、如果一個幀的目的MAC地址是一個受限的廣播地址(255.255.255.255)那麼它將被所有的網卡接收。
2、如果一個幀的目的地址是組播地址,那麼它將被那些打開組播接收功能的網卡所接收。
3、網卡如被設置成混雜模式,那麼它將接收所有流經它的數據包。
       前面我們剛好提到過網卡的混雜模式,現在我們就來迫不及待的實踐一哈看看混雜模式是否可以讓我們抓到所有數據包,只要在while循環前加上如下代碼就OK了:

點擊(此處)摺疊或打開
  1. struct ifreq ethreq;
  2. … …
  3. strncpy(ethreq.ifr_name,"eth0",IFNAMSIZ);
  4. if(-1 == ioctl(sock,SIOCGIFFLAGS,&ethreq)){
  5.      perror("ioctl");
  6.      close(sock);
  7.      exit(1);
  8. }
  9. ethreq.ifr_flags |=IFF_PROMISC;
  10. if(-1 == ioctl(sock,SIOCGIFFLAGS,&ethreq)){
  11.      perror("ioctl");
  12.      close(sock);
  13.      exit(1);
  14. }
  15. while(1){
  16.    … …
  17. }
       至此,我們一個網絡抓包工具的雛形就出現了。大家可以基於此做更多的練習,加上多線程機制,對收到的不同類型的數據包做不同處理等等,反正由你發揮的空間是相當滴大,“狐狸未成精,只因太年輕”。把這塊吃透了,後面理解協議棧就會相當輕鬆。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章