歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
您现在的位置: Linux教程網 >> UnixLinux >  >> Linux編程 >> Linux編程

基於Linux C的socket抓包程序和Package分析

測試運行平台:CentOS 6.5發行版,內核版本3.11

1. Linux抓包源程序

在OSI七層模型中,網卡工作在物理層和數據鏈路層的MAC子層。

進行網絡通信時,源主機通過socket(或其它)應用程序產生IP報文,經過各個OSI層層封裝,數據包以Ethernet幀的形式進入物理層。Ethernet幀包含源主機地址、IP報文、目標地址(IP地址、端口號或映射的6字節MAC地址)和需要傳送到目標主機的其它信息。

目標的MAC地址是哪裡來的呢?這牽扯到一個ARP協議(介乎於網絡層和數據鏈路層的一個協議)。第一次傳送某個目的IP地址的數據的時候,先會發出一個ARP包,其MAC的目標地址是廣播地址,裡面說到:"誰是xxx.xxx.xxx.xxx這個IP地址的主人?"因為是廣播包,所有這個局域網的主機都收到了這個ARP請求。收到請求的主機將這個IP地址和自己的相比較,如果不相同就不予理會,如果相同就發出ARP響應包。這個IP地址的主機收到這個ARP請求包後回復的ARP響應裡說到:"我是這個IP地址的主人"。這個包裡面就包括了他的MAC地址。以後的給這個IP地址的幀的目標MAC地址就被確定了。

就這樣,以太網幀開始在數據鏈路層傳播。Ethernet幀在鏈路層基於廣播方式傳播,即網段內的所有網卡都能觀察該幀,但只有一個網卡通過對比6字節MAC地址發現與自己相符,然後它就接收該幀。而其它網卡則放棄該幀。(其它網卡也可以接受該幀,即實際的網絡Sniffer,可進行信息竊取等操作)

網卡得到Ethernet幀後,通過網絡驅動程序和上層協議對其進行還原操作,即層層剝離報文頭後將數據交由目標主機的socket(或其它)應用程序使用。

如果我們需要原始的以太網幀,以便觀察與目標主機進行通信的源主機信息,則可以通過建立基於PF_PACKET的socket應用程序實現。PF_PACKET協議簇允許應用程序直接獲得網絡驅動程序得到的數據幀信息。PF_PACKET支持SOCK_DGRAM和SOCK_RAW兩種socket類型,前者利用操作系統處理報文頭,而後者則將以太網幀直接交由應用程序處理。需要注意到是,只有root權限用戶才能使用PF_PACKET程序。

下面的代碼即可實現由應用程序直接獲得以太網幀的需求。

#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <linux/if_ether.h>
#include <linux/in.h>

#define BUFFER_MAX 2048
 
int main(int argc, char *argv[]){
    int  SOCKET_SRC;
    char buf[BUFFER_MAX];
    int n_rd;

    if( (SOCKET_SRC = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_IP))) < 0 ){
        fprintf(stderr, "create socket error.\n");
        exit(0);
    }
    while(1){
        n_rd = recvfrom(SOCKET_SRC, buf, BUFFER_MAX, 0, NULL, NULL);
      if (n_rd<46) {
          perror("recvfrom():");
           printf("Incomplete packet (errno is %d)\n",  errno);
           close(SOCKET_SRC);
           exit(0);
      }
      /* An Ethernet frame was written to buf, frame analysis can be processed here */
      /* Termination control */
    }
    close(SOCKET_SRC);
    return 0;
}

2. 數據包(以太網幀)分析

一個以太網幀(RFC894)的數據格式如下圖所示。

以太網幀(RFC894)格式

程序通過執行一次

n_rd = recvfrom(SOCKET_SRC, buf, BUFFER_MAX, 0, NULL, NULL);

就將上面一條以太網幀寫入buf中。

(1) 為了從buf中提取以太網報文頭,我們可以定義如下結構體。

 typedef struct mac_frm_hdr {
    char dest_addr[6]; //destination MAC address shall be defined first.
    char src_addr[6];
    short type;
 }__attribute__((packed)) MAC_FRM_HDR;

定義該結構體時,需要注意以下幾點。

a.各屬性需按幀格式的出現順序定義。

b.數據類型長度必須和幀中相應區域的長度相同。

c. 使用__attribute__((packed))取消編譯器自動優化對齊結構體,也是為了保證屬性長度和幀中相應區域的長度相同。

(2)為了提取IP報文頭,根據IP報文頭的格式,我們可定義如下結構體。

typedef struct ip_hdr{  //header of IPV4
    #ifdef __LITTLE_ENDIAN_BIFIELD
        u_char ip_len:4, ip_ver:4;
    #else
        u_char ip_ver:4, ip_len:4;
    #endif

    u_char  ip_tos;
    u_short ip_total_len;
    u_short ip_id;
    u_short ip_flags;
    u_char  ip_ttl;
    u_char  ip_protocol;
    u_short ip_chksum;
    u_int32 ip_src;
    u_int32 ip_dest;
}__attribute__((packed)) IP_HDR;

為保證各屬性長度與IP報文頭中一致,我們應該在定義該結構體前作如下聲明。

typedef int int32;
typedef unsigned int u_int32;
typedef unsigned char u_char;
typedef unsigned short u_short;

注意事項參考定義以太網幀結構體。

(3)為了提取UDP/TCP報文頭,可根據UDP/TCP報文頭格式,定義相應結構體,這裡不作贅述。

下面是對以太網幀進行解析的具體代碼。

MAC_FRM_HDR *mac_hdr; //define a Ethernet frame header
IP_HDR *ip_hdr;      //define a IP header
char *tmp1, *tmp2;
int AND_LOGIC = 0xFF;

mac_hdr = buf; //buf is what we got from the socket program
ip_hdr = buf + sizeof(MAC_FRM_HDR);
//udp_hdr = buf + sizeof(MAC_FRM_HDR) + sizeof(IP_HDR); //if we want to analyses the UDP/TCP

tmp1 = mac_hdr->src_addr;
tmp2 = mac_hdr->dest_addr;
/* print the MAC addresses of source and receiving host */
printf("MAC: %.2X:%.2X:%.2X:%.2X:%.2X:%.2X==>" "%.2X:%.2X:%.2X:%.2X:%.2X:%.2X",
            tmp1[0]&AND_LOGIC, tmp1[1]&AND_LOGIC, tmp1[2]&AND_LOGIC,tmp1[3]&AND_LOGIC,
            tmp1[4]&AND_LOGIC, tmp1[5]&AND_LOGIC,
            tmp2[0]&AND_LOGIC, tmp2[1]&AND_LOGIC, tmp2[2]&AND_LOGIC,tmp2[3]&AND_LOGIC,
            tmp2[4]&AND_LOGIC, tmp2[5]&AND_LOGIC);

tmp1 = (char*)&ip_hdr->ip_src;
tmp2 = (char*)&ip_hdr->ip_dest;
/* print the IP addresses of source and receiving host */
printf("IP: %d.%d.%d.%d => %d.%d.%d.%d",
            tmp1[0]&AND_LOGIC, tmp1[1]&AND_LOGIC, tmp1[2]&AND_LOGIC,tmp1[3]&AND_LOGIC,
            tmp2[0]&AND_LOGIC, tmp2[1]&AND_LOGIC, tmp2[2]&AND_LOGIC,tmp2[3]&AND_LOGIC);
/* print the IP protocol which was used by the socket communication */
switch(ip_hdr->ip_protocol) {
        case IPPROTO_ICMP: LOGI("ICMP"); break;
        case IPPROTO_IGMP: LOGI("IGMP"); break;
        case IPPROTO_IPIP: LOGI("IPIP"); break;
        case IPPROTO_TCP:
  case IPPROTO_UDP:
                            LOGI("Protocol: %s", ip_hdr->ip_protocol == IPPROTO_TCP ? "TCP" : "UDP");
                            LOGI("Source port: %u, destination port: %u", udp_hdr->s_port, udp_hdr->d_port);
                            break;
        case IPPROTO_RAW: LOGI("RAW"); break;
        default: printf("Unknown, please query in inclued/linux/in.h\n"); break;
}

更多詳情見請繼續閱讀下一頁的精彩內容: http://www.linuxidc.com/Linux/2014-11/109330p2.htm

Copyright © Linux教程網 All Rights Reserved