2012年8月23日 星期四

利用pcap編寫自己的sniffer程式


本文讀者對象:需要基本的C語言基礎知識,否則除非你只是想瞭解pcap程式設計的基本理論知識也可以閱讀此文。當然你也不一定必須是網路程式設計的高手,因為本文所涉及的領域僅需要為有豐富網路程式設計經驗的人所理解(言下之意是如果你對這方面不感興趣或無意于向這方面發展就無所謂了)。本文中的所有代碼示例均在缺省內核版本BSD4.3下經過測試(我在RedHat 6.2 with kernel-2.2.14-5下亦測試通過)。

Get Started: The format of a pcap application
首先讓我們瞭解一個pcap應用程式的常用設計。代碼的流程如下所示:
1  首先決定將要用來sniff的網路介面。在Linux下可能是eth0BSD下可能是xl1等。我們要麼在一個字串(char *)中定義這個設備,或用使用者在命令列直接指定用來sniff的設備介面名。
2  初始化pcap。這是明確指定用來sniff的的網路介面的地方。當然我們可以在多個周邊設備上sniff。通過控制碼(handle)我們可以區分這些不同的sniff設備介面。就像我們打開一個用來讀或寫的檔一樣,我們必須命名我們的sniffer session 以便於區別這些不同的session
3  通常情況下我們只希望sniff特定的網路通信(比如:tcp資料包,所有發往23埠的tcp資料包)。通常我們制定這樣一個定義特定網路通信的規則集,將其編譯以後載入(apply topcap引擎上。這是編寫pcap應用程式最主要的步驟,而且必須緊密關聯。規則被保存在一個字串中,通過編譯被轉換成pcap引擎能夠識別的格式。事實上所謂的編譯不過是在我們自己的程式中調用特定的函數就可以完成,並不涉及到任何外部的應用程式。然後我們可以告訴pcap引擎應用編譯的規則作為我們sniff的規則(filter)。
4  最後我們通知pcap引擎進入主要的處理流程:pcap接受並處理匹配指定規則的制定數目的資料包。每當捕獲一個新的資料包,pcap調用自訂的回呼函數進行相應的處理。在回呼函數中可以做任何我們想做的事情:解剖捕獲的資料包並列印到使用者控制台,或者保存到檔中,當然也可以什麼也不做(如果什麼也不做,我們為什麼要寫這些代碼呢?如果。。。那麼。。。呢,呵呵已經很多人開始嘔吐並暈倒了)
5  結束sniff並關閉pcap會話控制碼。

事實上,是用pcap程式設計是一個非常簡單的過程,一共5個步驟,而且令你備感困惑的第3步還是可選的。詳細實現如下。

Setting the Device
這是一個極其簡單的操作(原文:This is terribly simple)。有兩種方法可以設置用來sniff的網路介面。
1 用戶在命令列指定定監聽的網路介面:
#include <stdio.h>
#include <pcap.h>
int main(int argc, char *argv[])
{
    char *dev = argv[1];
    printf("Device: %s/n", dev);
    return(0);
}
使用者通過命令列參數傳入監聽介面。
譯注:在實際的專案開發中務必對命令列參數進行判斷:
      if (argc < 2) {
            printf(“Usage: %s <option>/n”, argv[0]);
            exit(1);
      }

2  通過pcap引擎設定監聽的網路介面:
#include <stdio.h>
#include <pcap.h>
int main()
{
    char *dev, errbuf[PCAP_ERRBUF_SIZE];
    dev = pcap_lookupdev(errbuf);
    printf("Device: %s/n", dev);
    return(0);
}
在這種情況下,pcap引擎自己設置用來監聽的介面。但是errbuf字串用來做什麼呢?大多數的pcap函數允許我們傳遞這樣一個字串作為其參數。這個字串參數用來在pcap函式呼叫失敗以後用來設置出錯資訊。在上面的例子中,如果pcap_lookup函式呼叫失敗,出錯資訊將被保存在errbuf中。
譯注:增加的錯誤檢查的代碼如下:
       if (NULL == (dev = pcap_lookupdev(errbuf))) {
              fprintf(stderr, “pcap_lookupdev() error: %s/n”, errbuf);
              exit(-1);
       }
       printf(“Device: %s/n”, dev);

Opening the device for sniffing
創建sniff會話的任務非常簡單。我們使用pcap_open_live()創建sniff會話。函數原型:
pcap_t *pcap_open_live(char *device, int snaplen, int promisc, int to_ms, char *ebuf)
       device:上節中我們制定的監聽設備介面;
       snaplen:制定pcap捕獲的最大數目的網路資料包;
       promisc>0指定device介面工作在混雜模式(promiscous Mode);
       to_ms:制定經過特定時間(ms)後讀超時;0表示遇到錯誤退出,-1指定永不超時;
       ebuf:制定用來存儲出錯資訊的字串
       pcap_t:返回值為用於監聽的pcap會話。
示例代碼:
#include <pcap.h>
    ...
    pcap_t *handle;
    handle = pcap_open_live(somedev, BUFSIZ, 1, 0, errbuf);
上面的代碼打開somedev指定的設備並讀取(捕獲)BUFSIZ位元組,同時我們設置介面工作在混雜模式,一直監聽到有任何錯誤發生則退出,並將出錯資訊保存在errbuf指定的字串中。

關於混雜模式vs.非混雜模式:通常情況在非混雜模式下僅監聽直接發往主機的資料包:發往、源自或通過主機路由的資料包都將被pcap捕獲;混雜模式下,所有發送到物理鏈路上的資料包都將被捕獲。在一個共用式的網路環境中,這將導致整個網路的資料流程被監聽。混合監聽模式是可以被檢測的:可以通過測試強可靠性來發現網路中是否有主機正在以混合模式監聽,另外混雜工作模式僅僅在非交換式的網路中有效,而且在一個高負載的網路環境中,混雜模式將消耗大量的系統資源。

Filter traffic
通常我們只對特定網路通信感興趣。比如我們只打算監聽Telnet服務(port 23)以捕獲用戶名和口令資訊。獲知對FTPport 21)或DNSUDP port 53)資料流程感興趣。可以通過pcap_compile()pcap_setfilter來設置資料流程過濾規則(filter
函數原型:
       int pcap_compile(pcap_t *p, struct bpf_program *fp, char *str, int optimize, bpf_u_int32 netmask)
p:表示pcap會話控制碼;
fp:存放編譯以後的規則;
str:規則運算式格式的過濾規則(filter),同tcpdump中的filter
optimize:制定優化選項:0 false, 1 true
netmask:監聽介面的網路遮罩;
返回值:-1表示操作失敗,其他值表成功。
int pcap_setfilter(pcap_t *p, struct bpf_program *fp)
              p:表示pcap的會話控制碼;
              fp:表示經過編譯後的過濾規則;
              返回值:-1表示操作失敗,其他值表成功。

示例代碼:
#include <pcap.h>
    ...
    pcap_t *handle;                           /* Session handle */
    char dev[] = "rl0";                        /* Device to sniff on */
    char errbuf[PCAP_ERRBUF_SIZE];    /* Error string */
    struct bpf_program filter;               /* The compiled filter expression */
    char filter_app[] = "port 23";          /* The filter expression */
    bpf_u_int32 mask;                        /* The netmask of our sniffing device */
    bpf_u_int32 net;                           /* The IP of our sniffing device */
    pcap_lookupnet(dev, &net, &mask, errbuf);
    handle = pcap_open_live(dev, BUFSIZ, 1, 0, errbuf);
    pcap_compile(handle, &filter, filter_app, 0, net);
    pcap_setfilter(handle, &filter);
     
上面的代碼設備rl0上以混雜模式監聽所有發往或源自埠23的資料包。Pcap_lookupnet()函數返回給定介面的IP位址和子網路遮罩。

The actual sniffing
現在我們開始準備捕獲資料包:有兩種方法可以用來捕獲資料包。要麼一次捕獲一個滿足條件的資料包,要麼進入一個迴圈過程捕獲指定數量資料包然後退出。首先來瞭解使用pcap_next()一次捕獲單一資料包。
函數原型:
u_char *pcap_next(pcap_t *p, struct pcap_pkthdr *h)
       ppcap會話控制碼;
       h:指向pcap_pkthdr介面的指標,在此結構中保存了所捕獲的資料包的通用資訊。包括:時間資訊、資料包的長度和包頭部分的長度(結構定義在後面定義)。
       返回值:返回指向實際捕獲的資料包的u_char *型指標。
代碼示例:
#include <pcap.h>
    #include <stdio.h>
    int main()
    {
        pcap_t *handle;                        /* Session handle */
        char *dev;                                /* The device to sniff on */
        char errbuf[PCAP_ERRBUF_SIZE]; /* Error string */
        struct bpf_program filter;            /* The compiled filter */
        char filter_app[] = "port 23";       /* The filter expression */
        bpf_u_int32 mask;                     /* Our netmask */
        bpf_u_int32 net;                        /* Our IP */
        struct pcap_pkthdr header;          /* The header that pcap gives us */
        const u_char *packet;                 /* The actual packet */
        /* Define the device */
        dev = pcap_lookupdev(errbuf);
        /* Find the properties for the device */
        pcap_lookupnet(dev, &net, &mask, errbuf);
        /* Open the session in promiscuous mode */
        handle = pcap_open_live(dev, BUFSIZ, 1, 0, errbuf);
        /* Compile and apply the filter */
        pcap_compile(handle, &filter, filter_app, 0, net);
        pcap_setfilter(handle, &filter);
        /* Grab a packet */
        packet = pcap_next(handle, &header);
        /* Print its length */
        printf("Jacked a packet with length of [%d]/n", header.len);
        /* And close the session */
        pcap_close(handle);
        return(0);
    }
上面的代碼將所有從pcap_lookupdev()返回的介面置於混雜模式監聽狀態。Pcap捕獲埠23的一個資料包並列印該包的長度。然後調用pcap_close()關閉pcap會話。

當然我們可以使用更複雜和更強大的功能pcap_looppcap_dispatch。通常很少有sniffer使用pcap_next,他們更通常的使用pcap_looppcap_dispatch。為便於理解這兩個函數,需要現瞭解回呼函數的概念。

回呼函數並不是一個新概念,在很多的API中都使用了回呼函數的概念。可以通過pcap_looppcap_dispatch定義用戶自己的回呼函數。事實上pcap_looppcap_dispatch的功能非常相似,當pcap捕獲的滿足規則的資料包時,著兩個函數將調用我們自己定義的回呼函數執行我們自己的處理。

函數原型:
int pcap_loop(pcap_t *p, int cnt, pcap_handler callback, u_char *user)
ppcap會話控制碼;
cnt:定義sniff捕獲的資料包的數目;
callback:自訂的回呼函數控制碼;
user:傳遞給回呼函數的參數,如沒有參數可以設為NULL
函數pcap_dispatchpcap_loop的用法幾乎相同,兩者之間的唯一的差別是處理超時的方式不同(在pcap_open_live()中設置的超時參數將在這裡起作用:pcap_loop將忽略超時參數而pcap_dispatch在制定時間到時將產生讀超時的錯誤)。查閱pcap的?明獲得更多資訊。

回呼函數的原型:
void got_packet(u_char *args, const struct pcap_pkthdr *header, const u_char *packet);
       args:對應於pcap_loop中的最後一個參數;
       header:指向pcap數據包包頭的指標;
       packet:指向pcap捕獲到的資料包的指標,packet指標指向的字串包含了整個資料包;
       返回值:回呼函數不能返回任何值。
定義回呼函數時,需要嚴格遵守原型定義,否則pcap_loop將不能正確調用回呼函數。
       pcap_pkthdr結構的定義如下:
       struct pcap_pkthdr {
  
           struct timeval ts; /* time stamp */
  
           bpf_u_int32 caplen; /* length of portion present */
  
           bpf_u_int32 len; /* length this packet (off wire) */
};

       怎樣使用(處理)packet指標變數呢?一個packet指標所指的結構包含了很多屬性,它並不是一個真正的字串,而是多個結構組成的集合(比如:一個TCP/IP資料包包括乙太網頭、IP包頭、TCP頭和資料包中有效的資料負載)。首先需要定義這些結構:
/* Ethernet header */
怎樣使用(處理)packet指標變數呢?一個packet指標所指的結構包含了很多屬性,它並不是一個真正的字串,而是多個結構組成的集合(比如:一個TCP/IP資料包包括乙太網頭、IP包頭、TCP頭和資料包中有效的資料負載)。首先需要定義這些結構:

/* Ethernet header */
struct sniff_ethernet {
  u_char ether_dhost[ETHER_ADDR_LEN]; /* Destination host address */
  u_char ether_shost[ETHER_ADDR_LEN]; /* Source host address */
  u_short ether_type; /* IP? ARP? RARP? etc */
};
/* IP header */
struct sniff_ip {
  #if BYTE_ORDER == LITTLE_ENDIAN
  u_int ip_hl:4, /* header length */
  ip_v:4; /* version */
  #if BYTE_ORDER == BIG_ENDIAN
  u_int ip_v:4, /* version */
  ip_hl:4; /* header length */
  #endif
  #endif /* not _IP_VHL */
  u_char ip_tos; /* type of service */
  u_short ip_len; /* total length */
  u_short ip_id; /* identification */
  u_short ip_off; /* fragment offset field */
  #define IP_RF 0x8000 /* reserved fragment flag */
  #define IP_DF 0x4000 /* dont fragment flag */
  #define IP_MF 0x2000 /* more fragments flag */
  #define IP_OFFMASK 0x1fff /* mask for fragmenting bits */
  u_char ip_ttl; /* time to live */
  u_char ip_p; /* protocol */
  u_short ip_sum; /* checksum */
  struct in_addr ip_src,ip_dst; /* source and dest address */
};
/* TCP header */
struct sniff_tcp {
  u_short th_sport; /* source port */
  u_short th_dport; /* destination port */
  tcp_seq th_seq; /* sequence number */
  tcp_seq th_ack; /* acknowledgement number */
  #if BYTE_ORDER == LITTLE_ENDIAN
  u_int th_x2:4, /* (unused) */
  th_off:4; /* data offset */
  #endif
  #if BYTE_ORDER == BIG_ENDIAN
  u_int th_off:4, /* data offset */
  th_x2:4; /* (unused) */
  #endif
  u_char th_flags;
  #define TH_FIN 0x01
  #define TH_SYN 0x02
  #define TH_RST 0x04
  #define TH_PUSH 0x08
  #define TH_ACK 0x10
  #define TH_URG 0x20
  #define TH_ECE 0x40
  #define TH_CWR 0x80
  #define TH_FLAGS (TH_FIN|TH_SYN|TH_RST|TH_ACK|TH_URG|TH_ECE|TH_CWR)
  u_short th_win; /* window */
  u_short th_sum; /* checksum */
  u_short th_urp; /* urgent pointer */
};

注:這些結構定義在不同的系統實現中可能存在差異,請查閱相關文檔。

另:搞不懂作者什麼意思,為什麼非要自己定義這些結構,幹嘛不用系統自己定義的實現呢?



省略了原作者關於定義這些結構的描述



假設我們通過乙太網處理TCP/IP資料包(其他的物理網路類似),如下代碼將packet指標所指的結構分解為不同的結構體:

const struct sniff_ethernet *ethernet; /* The ethernet header */
const struct sniff_ip *ip; /* The IP header */
const struct sniff_tcp *tcp; /* The TCP header */
const char *payload; /* Packet payload */
/* For readability, we'll make variables for the sizes of each of the structures */
int size_ethernet = sizeof(struct sniff_ethernet);
int size_ip = sizeof(struct sniff_ip);
int size_tcp = sizeof(struct sniff_tcp);

And now we do our magical typecasting:
ethernet = (struct sniff_ethernet*)(packet);
ip = (struct sniff_ip*)(packet + size_ethernet);
tcp = (struct sniff_tcp*)(packet + size_ethernet + size_ip);
payload = (u_char *)(packet + size_ethernet + size_ip + size_tcp);

如果packet值(該指標變數所指的位址)為X,則上面所述的結構在記憶體中的佈局如下所示:

Variable
Location (in bytes)

sniff_ethernet
X

sniff_ip
X + 14

sniff_tcp
X + 14 + 20

payload
X + 14 + 20 + 20




Wrapping up

到此為止,我們已經可以用pcap編寫一個sniffer應用程式了。我們已經瞭解了pcap程式設計的基礎知識,包括打開一個pcap會話控制碼,處理pcap會話控制碼的屬性,監聽資料包,應用過濾規則,並使用回檔函式定義我們自己的處理過程。隨原文提供的示例程式:sniffer.c



This document is Copyright 2002 Tim Carstens. All rights reserved. Redistribution and use, with or without modification, are permitted provided that the following conditions are met: 1. Redistribution must retain the above copyright notice and this list of conditions. 2. The name of Tim Carstens may not be used to endorse or promote products derived from this document without specific prior written permission.
/* Insert 'wh00t' for the BSD license here */



附錄:例用pcap編寫的示例程式

/*

    編譯:gcc –Wall –o testpcap testpcap.c -lpcap

*/

#include <stdio.h>

#include <stdlib.h>

#include <unistd.h>

#include <pcap.h>

#include <sys/types.h>

#include <netinet/ip.h>

#include <netinet/ether.h>

#include <net/ethernet.h>

#include <netinet/udp.h>

#include <netinet/tcp.h>

#include <netinet/in.h>

#include <arpa/inet.h>



/* MACRO to print debug info */

//#define DEBUG 1

#ifdef DEBUG  

#define debug(stderr, msg) fprintf(stderr, msg)

#define _ ,

#else /* if no define DEBUG */

#define debug(stderr, msg)

#endif /* end of BEBUG */





#define LOOKUPDEV_ERR -1

#define OPEN_LIVE_ERR -2

#define COMPILE_ERR  -3



/* protocol ID's */

#define IPPRO 8 /* IP protocol */



/* call back function invoke by pcap_loop, major process for ourselves */

void

got_packet(u_char *args, const struct pcap_pkthdr *header,

     const u_char *packet);

/* handle ethernet header */

u_int16_t

handle_ethernet(u_char *args,const struct pcap_pkthdr* pkthdr,

     const u_char* packet);

/* handle IP header */

void

handle_IP(u_char *args,const struct pcap_pkthdr* pkthdr,const u_char* packet);

     

    



int

main(int argc, char *argv[])

{

   char *dev = NULL; /* device to sniff on */

   char errbuf[PCAP_ERRBUF_SIZE]; /* buffer to store error msg */

   pcap_t *handle = NULL; /* pcap session handle */



   struct bpf_program filter; //compiled filter expression

   char filter_app[] = "port 23"; /* filter ruler for sniffing */

   bpf_u_int32 mask; //netmask of our sniffing device

   bpf_u_int32 net; //the ip of our sniffing device

   

   int num = 0; /* number of packets captured */



   /* variables for getopt */

   long total = -1; /* total packets to sniff */

   char *flter = filter_app; /* filter ruler for sniffing */

   int c; /* temprory char variable */



   while ((c = getopt(argc, argv, "n:f:")) != -1) {

     switch(c) {

       case 'n':

          total = atoi(optarg);

          break;



       case 'f':

          flter = optarg;

          break;

     

       case '?':

          fprintf(stderr, "Usage: %s -n <num> -f <filter string>/n", argv[0]);

          exit(1);

       default:

          fprintf(stdout, "Using fitler: port 23 and sniffing util interrupt by console!/n");

     }

   }



   if (NULL == (dev = pcap_lookupdev(errbuf))) {

     fprintf(stderr, "pcap_lookupdev() error: %s/n", errbuf);

     exit(LOOKUPDEV_ERR);

   }

   fprintf(stdout, "Sniffing on device: %s/n/n", dev);



   pcap_lookupnet(dev, &net, &mask, errbuf);



   /* open a new pcap session */

   if (NULL == (handle = pcap_open_live(dev, BUFSIZ, 1, 0, errbuf))) {

     fprintf(stderr, "pcap_open_live() error: %s/n", errbuf);

     exit(OPEN_LIVE_ERR);

   }

   /* compile capture rule */

   if (-1 == pcap_compile(handle, &filter, flter, 1, net)) {

     fprintf(stderr, "pcap_compile() error!/n");

     exit(COMPILE_ERR);

   }

   pcap_setfilter(handle, &filter);



/* using while + pcap_next instead of pcap_loop or pcap_dispatch */

/*  while (1) {

     debug(stderr, "in pcap_next while/n");

     packet = pcap_next(handle, &header);

     printf("Captured a packet with lengthen of [%d]/n", header.len);

     debug(stderr, "The packet captured: %s/n" _ packet + header.caplen);

   }

*/



   num = pcap_loop(handle, total, got_packet, NULL);

   if (-1 == num) {

     pcap_perror(handle, "pcap_loop error: ");

   }   

   if (-2 == num) {

     pcap_perror(handle, "pcap_loop break by pcap_breakloop: ");

   }



   pcap_close(handle);



   return 0;

}



void

got_packet(u_char *args, const struct pcap_pkthdr *header, const u_char *packet)

{

  u_int16_t type;



   type = handle_ethernet(args, header, packet);



   debug(stderr, "protocol type: %i/n" _ type);



  switch(type) {

     case IPPRO:

       debug(stderr, "protocol type: IP/n");

       handle_IP(args, header, packet);

       break;



     case ETHERTYPE_ARP:

       /* handle arp protocol */  

       break;

     case ETHERTYPE_REVARP:

       /* handle rarp protocol */  

       break;

     default:

       fprintf(stdout, "Protocol is ignored/n");

   }



     fprintf(stdout,"/n");

   return;

} //end of got_packet



u_int16_t

handle_ethernet(u_char *args,const struct pcap_pkthdr* pkthdr,

     const u_char* packet)

{

   struct ether_header *eptr; /* net/ethernet.h */



  /* lets start with the ether header... */

  eptr = (struct ether_header *) packet;



  fprintf(stdout,"ETH: %s --> "

      ,ether_ntoa((struct ether_addr *)(eptr->ether_shost)));

  fprintf(stdout,"%s "

      ,ether_ntoa((struct ether_addr *)(eptr->ether_dhost)));



  /* check to see if we have an ip packet */

  if (ntohs (eptr->ether_type) == ETHERTYPE_IP)

  {

    fprintf(stdout,"(IP)");

  }else if (ntohs (eptr->ether_type) == ETHERTYPE_ARP)

  {

    fprintf(stdout,"(ARP)");

  }else if (ntohs (eptr->ether_type) == ETHERTYPE_REVARP)

  {

    fprintf(stdout,"(RARP)");

  }else {

    fprintf(stdout,"(?)");

    exit(1);

  }



  return eptr->ether_type;

} //end of handle_ethernet



void

handle_IP(u_char *args,const struct pcap_pkthdr* pkthdr,const u_char* packet)

{

   const struct iphdr *ip = (const struct iphdr *)(packet + sizeof(struct ether_header));



   u_int length = pkthdr->len;

   u_int hlen,off,version;

   int len;

   struct in_addr in;



   debug(stderr, "total pcap pkt length: %i/n" _ length);

   debug(stderr, "total pcap pkt header length: %i/n" _ pkthdr->caplen);



  /* jump pass the ethernet header */

  length =- sizeof(struct ether_header);



  /* check to see we have a packet of valid length */

  if (length < sizeof(struct iphdr))

  {

    printf("truncated ip %d",length);

    return;

  }



  len   = ntohs(ip->tot_len);

   debug(stderr, "total ip pkt length: %i/n" _ len);

  hlen  = ip->ihl; /* header length */

   debug(stderr, "ip header length: %i/n" _ hlen);

  version = ip->version;/* ip version */

   debug(stderr, "ip version: %i/n" _ version);



  /* check version */

  if(version != 4)

  {

   fprintf(stdout,"Unknown version %d/n",version);

   return ;

  }



  /* check header length */

  if(hlen < 5 )

  {

    fprintf(stdout,"bad-hlen %d /n",hlen);

  }



  /* see if we have as much packet as we should */

  if(length < len)

    printf("/ntruncated IP - %d bytes missing/n",len - length);



  /* Check to see if we have the first fragment */

  off = ntohs(ip->frag_off);

  if((off & 0x1fff) == 0 )/* aka no 1's in first 13 bits */

  {/* print SOURCE DESTINATION hlen version len offset */

    fprintf(stdout,"/nIP: ");

   in.s_addr = ip->saddr;

    fprintf(stdout,"%s ",inet_ntoa(in));

   in.s_addr = ip->daddr;

    fprintf(stdout,"%s %d %d %d %d/n",

        inet_ntoa(in),

        hlen,version,len,off);

  }
  return;
}

沒有留言: