天天看點

原始套接字-SOCK_RAW

原始套接字

簡介

套接口最常用的兩種類型:SOCK_STREAM和SOCK_DGRAM。

  • SOCK_STREAM: 流式套接口,傳輸的是位元組流,每次傳輸的資料沒有邊界,它是面向連接配接的,底層使用TCP協定。
  • SOCK_DGRAM: 資料報套接口,無連接配接,使用UDP協定
傳送的資料格式是預先定義好的

通過原始套接字,可以了解底層協定的實作細節,自己構造協定首部和資料,發送并接受

WinSock提供了另一種類型的套接口SOCK_RAW,也被稱為“原始套接口”。

當用選項IP_HDRINCL調用setsockopt時,使用者可以自己構造IPv4首部。

建立套接口時設定一個系統沒有處理的協定,可以在應用層實作自己的傳輸協定

通常網絡系統隻處理ICMP(1)、IGMP(2)、TCP(6)和UDP(17),使用原始套接口可以發送和接收ICMP和IGMP分組,系統在處理完後,會把資料報複制給原始套接口一份。

API

建立套接字

原始套接口也用socket函數來建立,第二個參數為SOCK_RAW,第三個參數protocol由使用者設定,可以使用WinSock2.h中定義的字首為IPPROTO_XXX的常值。另外,使用者也可以選擇一個頭檔案中沒有定義的數值,直接在socket函數中傳入數值即可。

設定選項

大部分選項的設定與TCP和UDP套接口是一樣的

有一個選項IP_HDRINCL,隻适用于原始套接口,用于控制是否由使用者自己構造IP首部。如果設定為非0值,使用者自己構造IP首部及後面的資料;設定為0值時,這是預設情況,使用者隻需要構造IP首部之後的資料部分,IP首部由系統填寫。

綁定套接口

在原始套接口上調用bind函數,隻是設定本地位址,系統不關心端口值,因為原始套接口沒有端口的概念。

通常原始套接口不調用bind,而是在發送資料時由系統自己選擇外出接口。

當bind中的位址不屬于本機任何網絡接口時,函數會失敗。

連接配接套接口

函數connect設定原始套接口的目的位址,也不關心端口值,并把套接口辨別為已連接配接,這裡的已連接配接隻表示設定了目的位址。

本地位址不受connect的影響,沒有調用bind時,仍然是未設定的。

發送資料

發送資料通常都用sendto,在參數to中指定要發送的目的位址,如果已經調用了connect,也可以用send發送資料。

接收資料

接收資料通常用recvfrom,如果不關心對方的位址,也可以用recv。

調用recvfrom之前必須調用過bind、connect或者sendto中的一個函數,将本地或目的位址資訊告訴系統,如果直接調用recvfrom,WinSock傳回失敗。

關閉

關閉原始套接口與關閉其他類型的套接口是一樣的,調用closesocket函數即可。

輸出處理
  1. 原始套接口發送資料通常用sendto,在第五個參數to中指定要發送的目的位址。
  2. 如果調用過connect,目的位址已經設定,發送資料時,可以直接調用send,當然也可以用sendto,并且第五個參數to設定為NULL。

    當to不為NULL時,系統會把資料發送到to所指定的目的位址,但調用connect時儲存在套接口中的目的位址不會改變。

這樣産生的問題是如果to中與connect的位址不同,由于這個套接口的目的位址是由connect指定的,故從to中位址輸入的資料報就不會在這個套接口上接收到。
  1. 發送的目的位址可以是任何有效的IP位址,包括廣播或多點傳播位址。為了向廣播位址發送資料,程式必須用選項SO_BROADCAST調用setsockopt設定套接口才能夠廣播,否則send或sendto将失敗,錯誤碼為WSAEACCES。

    使用原始套接口的應用程式不需要加入一個多點傳播組,就可以向該多點傳播位址發送資料。

  2. 原始套接口發送的資料長度,包括IP首部在内不能大于IP協定允許的最大值65535。
  3. 如果設定了IP_HDRINCL選項,使用者需要自己構造IP首部及其後面的資料,提供給系統的資料長度也包括IP首部在内。

    如果IP首部後面的資料需要校驗和,則必須自己計算。

    對IP首部,校驗和由系統計算,辨別符(Identification)可以設定為0,系統會設定辨別符字段。

    Windows中填寫IP首部時,各個字段的值都要使用網絡位元組序。

  4. 未設定IP_HDRINCL選項時,預設值,發送資料時傳給系統的緩沖區是IP首部之後的資料,系統會在使用者資料前增加一個IP首部,并填寫IP首部中的各個字段。

    其中協定字段設定為調用socket函數時的第三個參數,源位址是bind的本地位址,沒有調用bind時,系統根據外出接口自動設定。

    目的位址是connect或sendto中指定的位址。

  5. 輸出資料的長度超過外出網絡接口的最大傳輸單元MTU時,IP協定對資料分片。
原始套接字-SOCK_RAW
輸入處理
  1. 原始套接口接收資料通常用recvfrom,第五個參數from可以傳回對方的位址,如果應用程式不關心對方的位址,也可以用recv接收資料。
  2. 對于IPv4,應用程式接收到的是整個IP資料報,包含IP首部,即總是指向IP首部的第一個位元組,不管是否設定IP_HDRINCL選項,IP首部中的所有字段都是網絡位元組序。
  3. 如果資料報是分片的,IP協定在收到所有分片後進行重組,并把組裝完整的資料報交給原始套接口。

    在TCP/IP協定棧接收一個完整的資料報後,它檢查所有的套接口,找到與資料報中資訊比對的套接口,并把該資料報複制一份,複制到比對的套接口中。

  4. ·調用socket建立套接口時,當第三個參數protocol不為0時,則接收資料報IP首部中的協定字段必須與該值相等,不相等時,該資料報不會傳送給這個套接口。如果參數protocol為0,表示套接口不關心協定字段是否比對,隻要其他條件滿足,就接收該資料報。
  5. 當應用程式調用bind函數綁定了一個本地位址時,輸入資料報的目的IP位址必須與綁定的位址相等,否則資料報不會交給這個套接口。如果沒有指定本地位址,将不檢查資料的目的位址。
  6. 如果調用connect函數規定了對方IP位址,收到資料報的源IP位址要與該位址相等,不相等時,資料報不會交給這個套接口;如果沒有指定對方IP位址,協定棧不檢查接收資料報的源位址。

因為滿足上面條件的原始套接口會收到一份複制的資料報,是以使用原始套接口的應用程式可能會收到很多無關的資料報。

例: Ping程式會建立一個協定類型為ICMP的原始套接口,到達本機的其他ICMP分組,如目的不可達、重定向、時間戳也會交給應用程式,應用程式必須自己提供機制來識别它要處理的資料報,抛棄與它無關的資料報。Ping程式可以通過檢查ICMP首部中的辨別符來識别它要處理的資料報。

原始套接字-SOCK_RAW
原始套接口的限制

在Windows上使用原始套接口時要求具有管理者權限,如果使用者不屬于管理者組成員,運作原始套接口程式時,函數調用會失敗,錯誤碼為WSAEACCES。

在Windows7、Windows Vista、Windows XP帶有Service Pack 2或3上,使用原始套接口時有下面兩個限制:

  1. 不能發送TCP資料,建立協定類型為IPPROTO_TCP的原始套接口,調用bind或sendto函數時會失敗,錯誤碼為WSAEINVAL;
  2. 協定類型為IPPROTO_UDP的原始套接口,輸出UDP資料報的源IP位址必須是本機網絡接口的位址,如果源IP位址不是本機的,調用sendto時會失敗。
在Windows Server 2008 R2、Windows Server 2008、Windows Server 2003或Windows XP(SP2)的早期版本沒有上面的限制。
  1. 接收到TCP分組不會交給任何原始套接口,TCP分組由系統的協定棧處理。程序想要接收包含TCP首部的IP資料報,必須在資料鍊路層上讀取。
  2. 對于收到的UDP分組, 如果有在資料報的目的端口偵聽的UDP套接口,該分組交給UDP套接口處理,不會再交給原始套接口;如果沒有在資料報的目的端口偵聽的UDP套接口,系統再查找是否有協定類型為UDP的原始套接口,查到則把該分組放到原始套接口的接收緩沖區中。
  3. 接收到ICMP分組的IP資料報時,Windows在協定棧中幫助處理Echo請求(8)、時間戳請求(13)、位址掩碼請求(17),不會把這三種類型的ICMP資料報交給原始套接口,其他的ICMP分組都将交給對應的原始套接口處理。
  4. 協定棧對無法識别協定字段的IP資料報,都傳遞給對應的原始套接口。協定棧會對IP資料報做些基本的檢查,包括IP版本、長度、校驗和、選項及目的位址。
  5. 當協定棧處理完IGMP後,把所有IGMP分組交給原始套接口。如果原始套接口要接收IGMP分組的話,需要建立協定類型為IPPROTO_IGMP類型的原始套接口,調用bind綁定本地位址,然後用選項IP_ADD_MEMBERSHIP把本機加入一個多點傳播組中,才能接收到IGMP分組。發送時不用bind和加入多點傳播組,直接構造IGMP分組發送即可。
  6. windows有很多安全限制,使用原始套接口比較麻煩,預設配置,程式接收不到ICMP分組,調用recvfrom函數總是傳回WSAETIMEDOUT,需要做如下的配置才能夠接收到ICMP分組:
  • 管理者身份, 否則使用原始套接口會失敗。
  • 關閉UAC, User Account Control(UAC)使用者賬戶控制, 預設是打開的,隻允許寫到系統資料庫中經過認證的應用程式。

    控制台→ 使用者賬戶 → 更改使用者賬戶控制設定

  • 更改/關閉防火牆, 預設不允許輸入的ICMPv4分組
Demo--Ping程式

ping用來檢查網絡的連通性、另一台主機是否可達、測量兩台主機的延遲等。

原理:

  • 向目标主機發送一個ICMP類型的IP資料報,IP負載是ICMP的Echo請求,目标主機收到分組後,把ICMP的類型修改為Echo應答,并把同樣的資料報傳回給發送主機
  • Ping過程中,記錄了包丢失的個數,往返延遲,總結了發送和接收分組的個數、丢失情況、最小和最大及平均往返延遲。

Ping向目标主機發送一個ICMP的Echo請求,Echo請求中的資料必須在應答中傳回。

Echo中的辨別符和序列号可以幫助發送者識别相比對的應答消息,如:可以把辨別符設定為發送程序的ID,每次發送Echo請求都把序列号加1。另外,為了計算往返時間,通常在可選資料中儲存發送Echo請求的時間戳。

ICMP規定:接收者在應答中把辨別符、序列号及可選資料傳回給發送者。

  • 暴露目标主機資訊,可以确定目标主機的存在,解析IP首部字段,初步判斷目标主機使用的作業系統。
  • Ping死亡攻擊(Ping of Death),預設情況,Ping發送的資料大小是32位元組,包含IP和ICMP首部時是60位元組。

    當Ping資料報大于IPv4最大允許長度65535時,許多系統都不能處理。Windows為了解決這一漏洞,對Ping資料報的大小做了限制,最多允許發送65500位元組的資料,超過這個限制時會失敗。

  • 拒絕服務攻擊。持續地向同一台主機發送大量的Ping資料報,制造ICMP風暴,搶占了大量網絡帶寬
#include <stdio.h>
#include <winsock2.h>
#pragma comment(lib, "ws2_32.lib")   /* WinSock使用的庫函數 */
/* ICMP類型 */
#define
#define
#define ICMP_MIN_LEN                  8      /* ICMP最小長度, 隻有首部 */
#define ICMP_DEF_COUNT       4            /* 預設資料次數 */
#define ICMP_DEF_SIZE        32           /* 預設資料長度 */
#define ICMP_DEF_TIMEOUT     1000         /* 預設逾時時間, 毫秒 */
#define ICMP_MAX_SIZE        65500        /* 最大資料長度 */
/* IP首部 -- RFC 791 */
struct ip_hdr
    unsigned char vers_len;                /* 版本和首部長度 */
    unsigned char tos;                     /* 服務類型 */
    unsigned short total_len;              /* 資料報的總長度 */
    unsigned short id;                     /* 辨別符 */
    unsigned short frag;                   /* 标志和片偏移 */
    unsigned char ttl;                     /* 生存時間 */
    unsigned char proto;                   /* 協定 */
    unsigned short checksum;               /* 校驗和 */
    unsigned int sour;                     /* 源IP位址 */
    unsigned int dest;                     /* 目的IP位址 */
};
/* ICMP首部 -- RFC 792 */
struct icmp_hdr
    unsigned char type;                    /* 類型 */
    unsigned char code;                    /* 代碼 */
    unsigned short checksum;               /* 校驗和 */
    unsigned short id;                     /* 辨別符 */
    unsigned short seq;                    /* 序列号 */
    /* 這之後的不是标準ICMP首部, 用于記錄時間 */
    unsigned long timestamp;
};

struct icmp_user_opt
    unsigned int  persist;                 /* 一直Ping            */
    unsigned int  count;                   /* 發送Echo請求的數量 */
    unsigned int  size;                    /* 發送資料的大小       */
    unsigned int  timeout;                 /* 等待答複的逾時時間   */
    char          *host;                   /* 主機位址     */
    unsigned int  send;                    /* 發送數量     */
    unsigned int  recv;                    /* 接收數量     */
    unsigned int  min_t;                   /* 最短時間     */
    unsigned int  max_t;                   /* 最長時間     */
    unsigned int  total_t;                 /* 總的累計時間 */
};
/* 随機資料 */
const char icmp_rand_data[] = "abcdefghigklmnopqrstuvwxyz0123456789"
                                   "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
struct icmp_user_opt user_opt_g {
    0, ICMP_DEF_COUNT, ICMP_DEF_SIZE, ICMP_DEF_TIMEOUT, NULL,
    0, 0, 0xFFFF,0, 0
};

/*
* 計算校驗和
 http://t.zoukankan.com/Evil-Rebe-p-5043765.html
*/
unsigned short ip_checksum(unsigned short *buf, int{
    unsigned long cksum = 0;
    while(buf_len>1)
    {
        cksum += *buf++;
        buf_len -= sizeof(unsigned short);
    }
    if(buf_len)
    {
        cksum += *(unsigned char*)buf;
    }
    cksum = (cksum>>16) + (cksum&0xffff);
    cksum += (cksum>>16);
    return (unsigned short)(~cksum);
}

/**
* 構造ICMP資料
* @param icmp_data  資料緩沖區
* @param data_size  緩沖區長度
* @param sequence   ICMP序列号
*/
void icmp_make_data(char *icmp_data, int data_size, int
{
    struct icmp_hdr *icmp_hdr;
    char *data_buf;
    int data_len;
    int fill_count = sizeof(icmp_rand_data) / sizeof(icmp_rand_data[0]);
    /* 填寫ICMP資料 */
    data_buf = icmp_data + sizeof(struct icmp_hdr);
    data_len = data_size - sizeof(struct icmp_hdr);
    while (data_len > fill_count)
    {
        memcpy(data_buf, icmp_rand_data, fill_count);
        data_len -= fill_count;
    }
    if (data_len > 0)
        memcpy(data_buf, icmp_rand_data, data_len);
    /* 填寫ICMP首部 */
    icmp_hdr = (struct icmp_hdr *)icmp_data;
    icmp_hdr->type = ICMP_TYPE_ECHO;
    icmp_hdr->code = 0;
    icmp_hdr->id = (unsigned short)GetCurrentProcessId();
    icmp_hdr->checksum = 0;
    icmp_hdr->seq = sequence;
    icmp_hdr->timestamp = GetTickCount();
    icmp_hdr->checksum = ip_checksum((unsigned short*)icmp_data, data_size);
}

/**
* 解析接收到的ICMP響應
* @param buf 接收到的緩沖區
* @param buf_len  資料的長度
* @param from 對方的ip
*/
int icmp_parse_reply(char *buf, int buf_len,struct
{
    /*
    hdr_len : ip首部長度
    icmp_len: icmp頭部+ 資料的長度
    buf_len: icmp_資料的長度 ( 最後會賦為這個值 )
    */
    struct ip_hdr *ip_hdr;
    struct icmp_hdr *icmp_hdr;
    unsigned short hdr_len;
    int icmp_len;
    unsigned long trip_t;
    ip_hdr = (struct ip_hdr *)buf;
    hdr_len = (ip_hdr->vers_len & 0xf) << 2 ; /* IP首部長度 */
    if (buf_len < hdr_len + ICMP_MIN_LEN)
    {
        printf("[Ping] Too few bytes from %s\n", inet_ntoa(from->sin_addr));
        return -1;
    }
    icmp_hdr = (struct icmp_hdr *)(buf + hdr_len);
    icmp_len = ntohs(ip_hdr->total_len) - hdr_len;
    /* 檢查校驗和 */
    if (ip_checksum((unsigned short *)icmp_hdr, icmp_len))
    {
        printf("[Ping] icmp checksum error!\n");
        return -1;
    }
    /* 檢查ICMP類型 */
    if (icmp_hdr->type != ICMP_TYPE_ECHO_REPLY)
    {
        printf("[Ping] not echo reply : %d\n", icmp_hdr->type);
        return -1;
    }
    /* 檢查ICMP的ID */
    if (icmp_hdr->id != (unsigned short)GetCurrentProcessId())
    {
        printf("[Ping] someone else's message!\n");
        return -1;
    }
    /* 輸出響應資訊 */
    trip_t = GetTickCount() - icmp_hdr->timestamp;
    buf_len = ntohs(ip_hdr->total_len) - hdr_len - ICMP_MIN_LEN;
    printf("%d bytes from %s:", buf_len, inet_ntoa(from->sin_addr));
    printf(" icmp_seq = %d  time: %d ms\n",icmp_hdr->seq, trip_t);
    user_opt_g.recv++;
    user_opt_g.total_t += trip_t;
    /* 記錄傳回時間 */
    if (user_opt_g.min_t > trip_t)
         user_opt_g.min_t = trip_t;
    if (user_opt_g.max_t < trip_t)
        user_opt_g.max_t = trip_t;
    return 0;
}

/**
* 接收資料并處理ICMP響應
* @param icmp_soc 原始套接字描述符
*/
int icmp_process_reply(SOCKET icmp_soc)
{
    /*
    從原始套接字中接收到的資料包含了IP首部
    */
    struct sockaddr_in from_addr;
    int result, data_size = user_opt_g.size;
    int from_len = sizeof(from_addr);
    char *recv_buf;
    data_size += sizeof(struct ip_hdr) + sizeof(struct icmp_hdr);
    recv_buf = (char *)malloc(data_size);
    /* 接收資料 */
    result = recvfrom(icmp_soc, recv_buf, data_size, 0,
            (struct sockaddr*)&from_addr, &from_len);
    if (result == SOCKET_ERROR)
    {
        if (WSAGetLastError() == WSAETIMEDOUT)
            printf("timed out\n");
        else
            printf("[PING] recvfrom_ failed: %d\n", WSAGetLastError());
        return -1;
    }
    result = icmp_parse_reply(recv_buf, result, &from_addr);
    free(recv_buf);
    return result;
}

/**
* 列印幫助資訊
* @param prog_name 檔案名(xxx.c 或者 xxx\\yyy.c)
*/
void icmp_help(char
{
    char *file_name;
    file_name = strrchr(prog_name, '\\');
    if (file_name != NULL)
        file_name++;
    else
        file_name = prog_name;
    /* 顯示幫助資訊 */
    printf(" usage:     %s host_address [-t] [-n count] [-l size] "
        "[-w timeout]\n", file_name);
    printf(" -t         Ping the host until stopped.\n");
    printf(" -n count   the count to send ECHO\n");
    printf(" -l size    the size to send data\n");
    printf(" -w timeout timeout to wait the reply\n");
    exit(1); // 直接退出 
}

/**
* 解析使用者的輸入指令行參數
* t           一直ping
* n  count    ping的次數
* l  size    發送資料的大小
* w  timeout 等待響應的時間
*/
void icmp_parse_param(int argc, char
{
    int i;
    for(i = 1; i < argc; i++)
    {
        if ((argv[i][0] != '-') && (argv[i][0] != '/'))
        {
            /* 處理主機名 */
            if (user_opt_g.host)
                icmp_help(argv[0]);
            else
            {
                user_opt_g.host = argv[i];
                continue;
            }
        }
        switch (tolower(argv[i][1]))
        {
            case 't':   /* 持續Ping */
                user_opt_g.persist = 1;
                break;
            case 'n':   /* 發送請求的數量 */
                i++;
                user_opt_g.count = atoi(argv[i]);
                break;
            case 'l':   /* 發送資料的大小 */
                i++;
                user_opt_g.size = atoi(argv[i]);
                if (user_opt_g.size > ICMP_MAX_SIZE)
                    user_opt_g.size = ICMP_MAX_SIZE;
                break;
            case 'w':   /* 等待接收的逾時時間 */
                i++;
                user_opt_g.timeout = atoi(argv[i]);
                break;
           default:
                icmp_help(argv[0]);
                break;
        }
    }
}

int main(int argc, char
{
    WSADATA wsaData;
    SOCKET icmp_soc;
    struct sockaddr_in dest_addr;
    struct hostent *host_ent NULL;
    int result, data_size, send_len;
    unsigned int i, timeout, lost;
    char *icmp_data;
    unsigned int ip_addr = 0;
    unsigned short seq_no = 0;
    if (argc < 2)
        icmp_help(argv[0]);
    icmp_parse_param(argc, argv);
    WSAStartup(MAKEWORD(2,0),&wsaData);
    /* 解析主機位址 */
    ip_addr = inet_addr(user_opt_g.host);
    if (ip_addr == INADDR_NONE)
    {
        host_ent = gethostbyname(user_opt_g.host);
        if (!host_ent)
        {
            printf("[PING] Fail to resolve %s\n", user_opt_g.host);
            return -1;
        }
           memcpy(&ip_addr, host_ent->h_addr_list[0], host_ent->h_length);
    }
    icmp_soc = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
    if (icmp_soc == INVALID_SOCKET)
    {
        printf("[PING] socket() failed: %d\n", WSAGetLastError());
        return -1;
    }
    /* 設定選項, 接收和發送的逾時時間  */
    timeout = user_opt_g.timeout;
    result = setsockopt(icmp_soc, SOL_SOCKET, SO_RCVTIMEO,
                        (char*)&timeout, sizeof(timeout));
    timeout = 1000;
    result = setsockopt(icmp_soc, SOL_SOCKET, SO_SNDTIMEO,
                        (char*)&timeout, sizeof(timeout));
    memset(&dest_addr,0,sizeof(dest_addr));
    dest_addr.sin_family = AF_INET;
    dest_addr.sin_addr.s_addr = ip_addr;
    data_size = user_opt_g.size + sizeof(struct icmp_hdr) - sizeof(long);
    icmp_data = (char *)malloc(data_size);
    if (host_ent)
        printf("Ping %s [%s] with %d bytes data\n", user_opt_g.host,
            inet_ntoa(dest_addr.sin_addr), user_opt_g.size);
    else
        printf("Ping [%s] with %d bytes data\n", inet_ntoa(dest_addr.sin_addr),
            user_opt_g.size);
    /* 發送請求并接收響應 */
    for (i = 0; i < user_opt_g.count; i++)
    {
        icmp_make_data(icmp_data, data_size, seq_no++);
        send_len = sendto(icmp_soc, icmp_data, data_size, 0,
                            (struct sockaddr*)&dest_addr, sizeof(dest_addr));
        if (send_len == SOCKET_ERROR)
        {
            if (WSAGetLastError() == WSAETIMEDOUT)
            {
                printf("[PING] sendto is timeout\n");
                continue;
            }
           printf("[PING] sendto failed: %d\n", WSAGetLastError());
           break;
        }
        user_opt_g.send++;
        result = icmp_process_reply(icmp_soc);
        user_opt_g.persist ? i-- : i; /* 持續Ping */
        Sleep(1000); /* 延遲 1 秒 */
    }
    lost = user_opt_g.send - user_opt_g.recv;
    /* 列印統計資料 */
    printf("\nStatistic :\n");
    printf("    Packet : sent = %d, recv = %d, lost = %d (%3.f%% lost)\n",
    user_opt_g.send, user_opt_g.recv, lost, (float)lost*100/user_opt_g.send);
    if (user_opt_g.recv > 0)
    {
        printf("Roundtrip time (ms)\n");
        printf("    min = %d ms, max = %d ms, avg = %d ms\n", user_opt_g.min_t,
            user_opt_g.max_t, user_opt_g.total_t / user_opt_g.recv);
    }
    free(icmp_data);
    closesocket(icmp_soc);
    WSACleanup();
    return 0;
}      

繼續閱讀