Linux下Sniffer程式的實現(PF_PACKET,SOCK_RAW,recvfrom,htons(ETH_P_IP),setsockopt(filter))

來源:互聯網
上載者:User
轉自:http://www.cnblogs.com/jinrize/archive/2009/11/24/1609902.html 重點:ioctl設定網卡混雜模式,setsockopt設定recvfrom過濾剩下的IP報文的進一步具體過濾(BPF格式)

作者:Gianluca Insolvibile
整理:Seal(永遠的FLASH)
日期:2004-11-05

嗅探——Sniffer技術是網路安全領域裡一項非常重要的技術!對於“Hacker”來說,他們可以以非常隱形方式得到網路中傳輸的大量的敏感資訊,如Telnet,ftp帳號和密碼等等明文傳送的資訊!與主動掃描相比,嗅探的行為更加難以被察覺,操作起來也不是很複雜!對於網路管理員來說,可以利用嗅探技術對網路活動進行監控,並及時發現各種攻擊行為!
在這篇文章裡,我們主要探討在Linux下如何利用C語言來實現一個Sniffer!我們將假設所有的主機在一個區域網路內。
    
首先,我們將簡短的回顧一下一個普通的乙太網路卡是怎麼工作的!(如果你對這方面的知識早已熟悉,那麼你可以直接跳到下一段)來源於應用程式的IP報文被封裝成乙太網路幀(這
是在乙太網路上傳播的資料報文的名稱),它是底層鏈路層報文上面的一層報文,包含有源地址
報文和一些需要用來傳送至目標主機的資訊。通常情況下,目的IP地址對應著一個6位元組的目的乙太網路址(經常叫做MAC地址),它們之間通過ARP協議進行映射!就這樣,包含著乙太網路幀的報文從源主機傳輸到目的主機,中間經過一些網路裝置,如交換器,路由器等等,當然,因為我們的前提是主機在同一網內,所以我們的討論不涉及以上這些網路裝置!

    在鏈路層中並不存在路線的概念,換句話說,源主機發出的幀不會直接指向目的主機,
而是基於廣播方式傳播,網路中的所有網卡都能看到它的傳輸。每個網卡會檢查幀開始的6個位元組(目的主機的MAC地址),但是只有一個網卡會發現自己的地址和其相符合,然後它接收這個幀,這個幀會被網路驅動程式分解,原來的IP報文將通過網路通訊協定棧傳送至接收的應用程式!

更準確的說,網路驅動程式會檢查幀中報文頭部的協議標識,以確定接收資料的上層協
議!大多數情況下,上層是IP協議,所以接收機制將去掉IP報文頭部,然後把剩下的傳送
至UDP或者TCP接收機制!這些協議,將把報文送到socket-handling機制,它將最後把報
文資料變成應用程式可接收的方式發送出去。在這個過程中,報文將失去所有的和其有關的
網路資訊,比如源地址(IP和MAC),連接埠號碼,IP選擇,TCP參數等等!所以如果目的主機沒
有一個包含正確參數的開啟連接埠,那麼這個報文將被丟棄而且永遠不會被送到應用程式層去!

因此我們在進行網路嗅探的時候有兩個不同的問題:一個和乙太網路址有關,我們不能抓
到不是發給自己主機的包,另一個和協議棧的運行過程有關,我們需要一個SOCKET去監聽每
個連接埠,得到那些沒有被丟棄的報文!

第一個問題不是最根本的,因為我們可能不會對發往其他主機的報文有興趣而只想嗅探
所有發往自己主機的報文。第二個問題是必須要解決的,下面我們將看到這個問題是怎麼樣
一步一步解決的!

當你開啟一個標準的SOCKET通訊端時,你需要指明你將使用哪個協議簇,大多數情況下
我們一般用PF_UNIX在本地機器間進行通訊,PF_INET在基於IPv4協議簇基礎之上進行通訊,
你還需要指明所用的協議類型及與協議簇相關的確切數值,,在PF_INET協議簇中,常用的有
SOCK_STREAM(與TCP相關),SOCK_DGRAM(與UDP相關)。在把報文發送到應用程式前核心對
其的處理與SOCKET類型有關,你指定的協議將處理報文在SOCKET的傳輸!(具體細節問題你
可以man socket(3))

在LINUX核心版本中(2.0 releases),一個名為PF_PACKET的協議簇被加了進來!這個簇允許應用程式直接利用網路驅動程式發送和接收報文,避免了原來的協議棧處理過程,在這種情況下,所有SOCKET發出的報文直接送到乙太網路卡介面,而介面收到的任何報文將直接送到應用程式

The PF_PACKET協議簇支援兩個稍微有點不同的SOCKET類型,SOCK_DGRAM和SOCK_RAW。
前者讓核心處理添加或者去除乙太網路報文頭部工作,而後者則讓應用程式對乙太網路報文頭部
有完全的控制!在SOCKET調用中的協議類型必須符合/usr/include/linux/if_ether.h
中定義的乙太網路IDs中的一個,除非遇到特別聲明的協議,一般你可以用ETH_P_IP來處理
IP的一組協議(TCP,UDP,ICMP,raw IP等等)因為它們容易受到一些很嚴重的安全問題的牽
連(比如你可以偽造一個MAC地址),所以只有具有root許可權才可以使用PF_PACKET-family
socket.這也就是為什麼只有具有root許可權後才能運行嗅探器的原因!

PF_PACKET-family 協議簇可以很容易解決協議棧處理嗅探來的資料報文時候遇到的問
題!我們一起來看看程式1,我們開啟一個屬於PF_PACKET-family 協議簇的SOCKET,指定
一個SOCK_RAW socket類型和IP相關協議類型。這時我們開始從SOCKET抓包,在一些相關
檢查後.我們開始得到從鏈路層和IP層抓來的頭部資訊,。通過閱讀程式一,你將會發現讓應
用程式從網路層抓包其實並不難!

Example 1.
#include <stdio.h>
#include <errno.h>  
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>  
#include <linux/in.h>
#include <linux/if_ether.h>

int main(int argc, char **argv) {
  int sock, n;
  char buffer[2048];
  unsigned char *iphead, *ethhead;
  
  if ( (sock=socket(PF_PACKET, SOCK_RAW,
                    htons(ETH_P_IP)))<0) {
    perror("socket");
    exit(1);
  }

  while (1) {
    printf("----------\n");
    n = recvfrom(sock,buffer,2048,0,NULL,NULL);
    printf("%d bytes read\n",n);

    /* Check to see if the packet contains at least
     * complete Ethernet (14), IP (20) and TCP/UDP
     * (8) headers.
     */
    if (n<42) {
      perror("recvfrom():");
      printf("Incomplete packet (errno is %d)\n",
             errno);
      close(sock);
      exit(0);
    }

    ethhead = buffer;
    printf("Source MAC address: "
           "%02x:%02x:%02x:%02x:%02x:%02x\n",
           ethhead[0],ethhead[1],ethhead[2],
           ethhead[3],ethhead[4],ethhead[5]);
    printf("Destination MAC address: "
           "%02x:%02x:%02x:%02x:%02x:%02x\n",
           ethhead[6],ethhead[7],ethhead[8],
           ethhead[9],ethhead[10],ethhead[11]);

    iphead = buffer+14; /* Skip Ethernet header */
    if (*iphead==0x45) { /* Double check for IPv4
                          * and no options present */
      printf("Source host %d.%d.%d.%d\n",
             iphead[12],iphead[13],
             iphead[14],iphead[15]);
      printf("Dest host %d.%d.%d.%d\n",
             iphead[16],iphead[17],
             iphead[18],iphead[19]);
      printf("Source,Dest ports %d,%d\n",
             (iphead[20]<<8)+iphead[21],
             (iphead[22]<<8)+iphead[23]);
      printf("Layer-4 protocol %d\n",iphead[9]);
    }
  }
  
}

PF_PACKET協議簇可以讓一個應用程式把資料包變成似乎從網路層接收的樣子,但是
沒有辦法抓到那些不是發向自己主機的包。正如我們前面看到的,網卡丟棄所有不含有主機
MAC地址的資料包,這是因為網卡處於非混雜模式,即每個網卡只處理源地址是它自己的幀!
只有三個例外:如果一個幀的目的MAC地址是一個受限的廣播位址(255.255.255.255)那麼
它將被所有的網卡接收:如果一個幀的目的地址是組播地址,那麼它將被那些開啟組播接收
功能的網卡所接收;網卡如被設定成混雜模式,那麼它將接收所有流經它的資料包

最後一種情況當然是我們最感興趣的了,把網卡設定成混雜模式,我們只需要發出一個
特殊的ioctl()調用在那個網卡上開啟一個socket,因為這是一個具有危險性的操作,所以這
個調用只有具有root許可權的使用者才可完成,假設那個“sock”包含一個已經開啟的socket,

下面的代碼將完成這個操作:

strncpy(ethreq.ifr_name,"eth0",IFNAMSIZ);
ioctl(sock, SIOCGIFFLAGS, &ethreq);
ethreq.ifr_flags |= IFF_PROMISC;
ioctl(sock, SIOCSIFFLAGS, &ethreq);

下面我們來看一個完整的例子:

Example 2.

#include <stdio.h>
#include <string.h>
#include <errno.h>  
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>  
#include <linux/in.h>
#include <linux/if_ether.h>
#include <net/if.h>
#include <sys/ioctl.h>

int main(int argc, char **argv) {
  int sock, n;
  char buffer[2048];
  unsigned char *iphead, *ethhead;
  struct ifreq ethreq;
  
  if ( (sock=socket(PF_PACKET, SOCK_RAW,
                    htons(ETH_P_IP)))<0) {
    perror("socket");
    exit(1);
  }

  /* Set the network card in promiscuos mode */
  strncpy(ethreq.ifr_name,"eth0",IFNAMSIZ);
  if (ioctl(sock,SIOCGIFFLAGS,?req)==-1) {
    perror("ioctl");
    close(sock);
    exit(1);
  }
  ethreq.ifr_flags|=IFF_PROMISC;
  if (ioctl(sock,SIOCSIFFLAGS,?req)==-1) {
    perror("ioctl");
    close(sock);
    exit(1);
  }
  
  while (1) {
    printf("----------\n");
    n = recvfrom(sock,buffer,2048,0,NULL,NULL);
    printf("%d bytes read\n",n);

    /* Check to see if the packet contains at least
     * complete Ethernet (14), IP (20) and TCP/UDP
     * (8) headers.
     */
    if (n<42) {
      perror("recvfrom():");
      printf("Incomplete packet (errno is %d)\n",
             errno);
      close(sock);
      exit(0);
    }

    ethhead = buffer;
    printf("Source MAC address: "
           "%02x:%02x:%02x:%02x:%02x:%02x\n",
           ethhead[0],ethhead[1],ethhead[2],
           ethhead[3],ethhead[4],ethhead[5]);
    printf("Destination MAC address: "
           "%02x:%02x:%02x:%02x:%02x:%02x\n",
           ethhead[6],ethhead[7],ethhead[8],
           ethhead[9],ethhead[10],ethhead[11]);

    iphead = buffer+14; /* Skip Ethernet header */
    if (*iphead==0x45) { /* Double check for IPv4
                          * and no options present */
      printf("Source host %d.%d.%d.%d\n",
             iphead[12],iphead[13],
             iphead[14],iphead[15]);
      printf("Dest host %d.%d.%d.%d\n",
             iphead[16],iphead[17],
             iphead[18],iphead[19]);
      printf("Source,Dest ports %d,%d\n",
             (iphead[20]<<8)+iphead[21],
             (iphead[22]<<8)+iphead[23]);
      printf("Layer-4 protocol %d\n",iphead[9]);
    }
  }
  
}
如果我們在一個與區域網路相連的主機上以root許可權編譯和運行這個程式,你將會看到流
經的所有資料包,即使它不是發向自己主機的,這是因為你的網卡現在處於混雜模式,你可
以很容易的用ifconfig這個命令看到!

注意,如果你的區域網路用的是交換器而不是集線器,那麼你只能抓到在你主機所在的交
換分支中的資料包,這是由於交換的工作原理,在這種情況下,你沒有什麼其他可做的了(除非你利用MAC地址欺騙來瞞過交換器,這個內容超出本文討論的範圍了),如果你想知道這方面更多的資訊,你可以去www.google.com,那裡你將會搜尋到很多你需要的東西!

我們關於嗅探方面的問題似乎都解決了,但是還有一個問題值得我們去思考!如果我們
實驗Example2的時候正好遇到了網路擁塞,我們將會看到我們的嗅探器將打出非常多的資料,隨著網路擁塞的加劇,主機將不能順暢的執行這個程式,嗅探器這時會開始丟包!

解決這個問題的方法就是對你抓到的包進行過濾,使它只顯示那些你感興趣的那些部分!
在原始碼中多使用一些if語句,這將有益於去除那些不需要的資訊,當然這樣也許效率不是
很高!核心仍然將處理所有的報文,這將浪費進程時間。

最好的解決辦法就是把過濾過程儘可能早的放報文進程鏈(packet-processing chain)
中(它開始於鏈路層,終止於應用程式層)LINUX的核心允許我們把一個名為LPF的過濾器直接放到PF_PACKET protocol-processing routines(在網卡接收中斷執行後立即執行)中,過濾器將決定哪些包被送到應用程式,哪些包被丟棄!

為了做到儘可能的靈活,這個過濾程式可以根據使用者的定義來運行!這個程式是由一種名為BPF的偽機器碼寫成的。BPF看上去很象帶著一對寄存器和儲存值的組合語言,完成數學運算和條件分支程式!過濾程式檢查每個包,BP進程操作的記憶體空間包含著報文資料!過濾的結果是一個整數,指出有多少位元組的報文需要送到應用程式層。這是一個更進一步的優點,因為我們一般只對報文的前面幾個位元組感興趣,這樣可以避免複製過量的資料以節省進程時間。

雖然BPF語言非常簡單易學,但是大多數人還是習慣於人類可以閱讀的表達方式!所以
為了不用BPF語言去描述那些細節和代碼,我們將下面開始討論如何得到一個過濾器的代碼!

首先,你需要安裝tcpdump,可以從LBL得到!但是,如果你正在讀本文,那似乎說明
你已經會使用tcpdump了!第一個版本的作者也是那些寫BPF的人,事實上,tcpdump要用
到BPF,它要用到一個叫libpcap的類庫,以此去抓包和過濾。這個類庫是一個與作業系統
無關的獨立的包,為BPF的實現提供支援,當在裝有LINUX的機器上使用時,BPF的功能就
由LINUX包過濾器實現了!

libpcap提供的一個最有用的函數是pcap_compile(), 它可以把一個輸入輸出的邏輯表
達式變為BPF代碼!tcpdump利用這個函數完成在使用者輸入的命令列和BPF代碼之間的轉換!
tcpdump有個我們高度興趣但是很少使用的參數 ,-d,可以輸出BPF代碼!

舉個例子,如果tcpdump host 192.168.9.10那麼將啟動嗅探器並只抓到源地址或者目
的地址是192.168.9.10的報文!如果tcpdump -d host 192.168.9.10那麼將顯示出BPF代
碼,見下面

Example 3.
Seal:~# tcpdump -d host 192.168.9.10
(000) ldh      [12]
(001) jeq      #0x800           jt 2    jf 6
(002) ld       [26]
(003) jeq      #0xc0a8090a      jt 12   jf 4
(004) ld       [30]
(005) jeq      #0xc0a8090a      jt 12   jf 13
(006) jeq      #0x806           jt 8    jf 7
(007) jeq      #0x8035          jt 8    jf 13
(008) ld       [28]
(009) jeq      #0xc0a8090a      jt 12   jf 10
(010) ld       [38]
(011) jeq      #0xc0a8090a      jt 12   jf 13
(012) ret      #68
(013) ret      #0

我們簡單的分析一下這個代碼,0-1,6-7行在確定被抓到的幀是否在傳輸著IP,ARP或
者RARP協議,通過比較幀的第12格的值與協議的特徵值(見/usr/include/linux/if_ether.h)!如果比較失敗,那麼丟棄這個包!

2-5,8-11行是比較幀的源地址和目的地址是否與192.168.9.10相同!注意,不同的
協議,地址值在幀中的位移位不同,如果是IP協議,它在26-30,如果是其他協議,它在
28-38如果有一個地址符合,那麼幀的前68位元組將被送到應用程式去(第12行)

這個過濾器也不是總是有效,因為它產生於一般的使用BPF的機器,沒考慮到一些特
殊結構的機器!在一些特殊情況下,過濾器由PF_PACKET進程運行,也許已經檢查過以太協
議了!這個根據你在socket()調用初使化的時候指定的那些協議!如果不是ETH_P_ALL(抓
所有的報文),那麼只有那些符合指定的協議類型的報文會流過過濾器!舉個例子,如果是
ETH_P_IP socket,我們可以重寫一個更快且代碼更緊湊的過濾器,代碼如下

(000) ld       [26]
(001) jeq      #0xc0a8090a      jt 4    jf 2
(002) ld       [30]
(003) jeq      #0xc0a8090a      jt 4    jf 5
(004) ret      #68(005) ret      #0

安裝LPF是一個一往直前的操作,所有你需要做的就是建立一個sock_filter並為
它綁定一個開啟的連接埠!一個過濾器很容易得到,只要把tcpdump -d中的-d換成-dd就可以了!過濾器將顯示出一段C代碼,你可以把它複製到自己的程式中,見Example 4,這樣你就可以通過調用setsockopt()來過濾連接埠!

Example 4.
Seal:~# tcpdump -dd host 192.168.9.01
{ 0x28, 0, 0, 0x0000000c },
{ 0x15, 0, 4, 0x00000800 },
{ 0x20, 0, 0, 0x0000001a },
{ 0x15, 8, 0, 0xc0a80901 },
{ 0x20, 0, 0, 0x0000001e },
{ 0x15, 6, 7, 0xc0a80901 },
{ 0x15, 1, 0, 0x00000806 },
{ 0x15, 0, 5, 0x00008035 },
{ 0x20, 0, 0, 0x0000001c },
{ 0x15, 2, 0, 0xc0a80901 },
{ 0x20, 0, 0, 0x00000026 },
{ 0x15, 0, 1, 0xc0a80901 },
{ 0x6, 0, 0, 0x00000044 },
{ 0x6, 0, 0, 0x00000000 },

A Complete Example

我們將用一個完整的程式Example 5來結束這篇文章

Example 5
#include <stdio.h>
#include <string.h>
#include <errno.h>  
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>  
#include <linux/in.h>
#include <linux/if_ether.h>
#include <net/if.h>
#include <linux/filter.h>
#include <sys/ioctl.h>

int main(int argc, char **argv) {
  int sock, n;
  char buffer[2048];
  unsigned char *iphead, *ethhead;
  struct ifreq ethreq;

  /*
    udp and host 192.168.9.10 and src port 5000
    (000) ldh      [12]
    (001) jeq      #0x800           jt 2        jf 14
    (002) ldb      [23]
    (003) jeq      #0x11            jt 4        jf 14
    (004) ld       [26]
    (005) jeq      #0xc0a8090a      jt 8        jf 6
    (006) ld       [30]
    (007) jeq      #0xc0a8090a      jt 8        jf 14
    (008) ldh      [20]
    (009) jset     #0x1fff          jt 14       jf 10
    (010) ldxb     4*([14]&0xf)
    (011) ldh      [x + 14]
    (012) jeq      #0x1388          jt 13       jf 14
    (013) ret      #68
    (014) ret      #0
  */
  struct sock_filter BPF_code[]= {
    { 0x28, 0, 0, 0x0000000c },
    { 0x15, 0, 12, 0x00000800 },
    { 0x30, 0, 0, 0x00000017 },
    { 0x15, 0, 10, 0x00000011 },
    { 0x20, 0, 0, 0x0000001a },
    { 0x15, 2, 0, 0xc0a8090a },
    { 0x20, 0, 0, 0x0000001e },
    { 0x15, 0, 6, 0xc0a8090a },
    { 0x28, 0, 0, 0x00000014 },
    { 0x45, 4, 0, 0x00001fff },
    { 0xb1, 0, 0, 0x0000000e },
    { 0x48, 0, 0, 0x0000000e },
    { 0x15, 0, 1, 0x00001388 },
    { 0x6, 0, 0, 0x00000044 },
    { 0x6, 0, 0, 0x00000000 }
  };                            
  struct sock_fprog Filter;
    
  Filter.len = 15;
  Filter.filter = BPF_code;
  
  if ( (sock=socket(PF_PACKET, SOCK_RAW,
                    htons(ETH_P_IP)))<0) {
    perror("socket");
    exit(1);
  }

  /* Set the network card in promiscuos mode */
  strncpy(ethreq.ifr_name,"eth0",IFNAMSIZ);
  if (ioctl(sock,SIOCGIFFLAGS,?req)==-1) {
    perror("ioctl");
    close(sock);
    exit(1);
  }
  ethreq.ifr_flags|=IFF_PROMISC;
  if (ioctl(sock,SIOCSIFFLAGS,?req)==-1) {
    perror("ioctl");
    close(sock);
    exit(1);
  }

  /* Attach the filter to the socket */
  if(setsockopt(sock, SOL_SOCKET, SO_ATTACH_FILTER,
                &Filter, sizeof(Filter))<0){
    perror("setsockopt");
    close(sock);
    exit(1);
  }

  while (1) {
    printf("----------\n");
    n = recvfrom(sock,buffer,2048,0,NULL,NULL);
    printf("%d bytes read\n",n);

    /* Check to see if the packet contains at least
     * complete Ethernet (14), IP (20) and TCP/UDP
     * (8) headers.
     */
    if (n<42) {
      perror("recvfrom():");
      printf("Incomplete packet (errno is %d)\n",
             errno);
      close(sock);
      exit(0);
    }

    ethhead = buffer;
    printf("Source MAC address: "
           "%02x:%02x:%02x:%02x:%02x:%02x\n",
           ethhead[0],ethhead[1],ethhead[2],
           ethhead[3],ethhead[4],ethhead[5]);
    printf("Destination MAC address: "
           "%02x:%02x:%02x:%02x:%02x:%02x\n",
           ethhead[6],ethhead[7],ethhead[8],
           ethhead[9],ethhead[10],ethhead[11]);

    iphead = buffer+14; /* Skip Ethernet  header */
    if (*iphead==0x45) { /* Double check for IPv4
                          * and no options present */
      printf("Source host %d.%d.%d.%d\n",
             iphead[12],iphead[13],
             iphead[14],iphead[15]);
      printf("Dest host %d.%d.%d.%d\n",
             iphead[16],iphead[17],
             iphead[18],iphead[19]);
      printf("Source,Dest ports %d,%d\n",
             (iphead[20]<<8)+iphead[21],
             (iphead[22]<<8)+iphead[23]);
      printf("Layer-4 protocol %d\n",iphead[9]);
    }
  }
  
}

這個程式和開始的前兩個程式很類似,只是增加了LSP代碼和setsockopt()調用,這個
過濾器被用來嗅探UDP報文(源地址或者目的地址為192.168.9.10且源地址的連接埠為5000)
為了測試這個程式,你需要一個簡單的方法去產生不斷的UDP報文(如發送或者接受IP),而且你可以把程式修改為你指定的機器的IP地址,你只需要把代碼中的0xc0a8090a替換成你要的IP地址的16進位形式!

最後想說的就是關閉程式後如果我們沒有重新設定網卡,那麼它還是處於混雜模式!你
可以加一個Control-C訊號控制代碼來使網卡在程式關閉前恢複以前的預設配置。

進行網路嗅探對於診斷網路運行錯誤或者進行流量分析都是非常重要的!有些時候常用
的工具如tcpdump或者Ethereal不能非常符合我們的需要的時候,我們可以自己寫一個嗅探
器!為此我們應該感謝LPF,它使我們做到這點變的很容易!

 
相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.