淺談原始套接字 SOCK_RAW 的內幕及其應用(port scan, packet sniffer, syn flood, icmp flood)

一、SOCK_RAW 內幕

首先在講SOCK_RAW 之前,先來看創建socket 的函數:

int socket(int domain, int type, int protocol);

domain :指定通信協議族(protocol family/address)

/usr/include/i386-Linux-gnu/bits/socket.h

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
 
/* Supported address families. */
#define AF_UNSPEC   0
#define AF_UNIX     1   /* Unix domain sockets      */
#define AF_LOCAL    1   /* POSIX name for AF_UNIX   */
#define AF_INET     2   /* Internet IP Protocol     */
#define  PF_PACKET 17  /* Packet family.  */
/* ... */

/* Protocol families, same as address families. */
#define PF_UNSPEC   AF_UNSPEC
#define PF_UNIX     AF_UNIX
#define PF_LOCAL    AF_LOCAL
#define PF_INET     AF_INET
#define AF_PACKET   PF_PACKET
/* ... */

type:指定socket類型(type)

 C++ Code 
1
2
3
4
5
6
7
8
9
10
 
enum sock_type
{
    SOCK_STREAM = 1,
    SOCK_DGRAM  = 2,
    SOCK_RAW    = 3,
    SOCK_RDM    = 4,
    SOCK_SEQPACKET  = 5,
    SOCK_DCCP   = 6,
    SOCK_PACKET = 10,
};

protocol :協議類型(protocol)

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
 
/* Standard well-defined IP protocols.  */
enum
{
    IPPROTO_IP = 0,         /* Dummy protocol for TCP       */
    IPPROTO_ICMP = 1,       /* Internet Control Message Protocol    */
    IPPROTO_IGMP = 2,       /* Internet Group Management Protocol   */
    IPPROTO_IPIP = 4,       /* IPIP tunnels (older KA9Q tunnels use 94) */
    IPPROTO_TCP = 6,        /* Transmission Control Protocol    */
    IPPROTO_EGP = 8,        /* Exterior Gateway Protocol        */
    IPPROTO_PUP = 12,       /* PUP protocol             */
    IPPROTO_UDP = 17,       /* User Datagram Protocol       */
    IPPROTO_IDP = 22,       /* XNS IDP protocol         */
    IPPROTO_DCCP = 33,      /* Datagram Congestion Control Protocol */
    IPPROTO_RSVP = 46,      /* RSVP protocol            */
    IPPROTO_GRE = 47,       /* Cisco GRE tunnels (rfc 1701,1702)    */
    IPPROTO_IPV6 = 41,      /* IPv6-in-IPv4 tunnelling      */
    IPPROTO_ESP = 50,           /* Encapsulation Security Payload protocol */
    IPPROTO_AH = 51,                /* Authentication Header protocol       */
    IPPROTO_BEETPH = 94,            /* IP option pseudo header for BEET */
    IPPROTO_PIM    = 103,       /* Protocol Independent Multicast   */
    IPPROTO_COMP   = 108,           /* Compression Header protocol */
    IPPROTO_SCTP   = 132,       /* Stream Control Transport Protocol    */
    IPPROTO_UDPLITE = 136,      /* UDP-Lite (RFC 3828)          */
    IPPROTO_RAW  = 255,     /* Raw IP packets           */
    IPPROTO_MAX
};

你是否曾經有過這樣的疑惑,當我們在linux下這樣調用 socket(AF_INET, SOCK_STREAM, 0); 時,第三個參數爲0,內核是如何找到合適的協議如IPPROTO_TCP 的?實際上是調用 pffindtype 函數實現的。下面來看看FreeBSD的源碼,linux 的實現差不多,有個小區別等會指出。

在freeBSD 上創建一個socket 會調用socreate() 函數:

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
 
/*
 * socreate returns a socket with a ref count of 1.  The socket should be
 * closed with soclose().
 */

int
socreate(int dom, struct socket **aso, int type, int proto,
         struct ucred *cred, struct thread *td)
{
    struct protosw *prp;
    struct socket *so;
    int error;

    if (proto)
        prp = pffindproto(dom, proto, type);
    else
        prp = pffindtype(dom, type);

    /* .... */
}

從函數可以看出當proto 爲0 則調用pffindtype() 函數,否則調用pffindproto() 函數,兩個函數如下:

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
 
struct protosw *
pffindtype(int family, int type)
{
    struct domain *dp;
    struct protosw *pr;

    for (dp = domains; dp; dp = dp->dom_next)
        if (dp->dom_family == family)
            goto found;
    return (0);
found:
    for (pr = dp->dom_protosw; pr < dp->dom_protoswNPROTOSW; pr++)
        if (pr->pr_type && pr->pr_type == type)
            return (pr);
    return (0);
}

struct protosw *
pffindproto(int family, int protocol, int type)
{
    struct domain *dp;
    struct protosw *pr;
    struct protosw *maybe = 0;

    if (family == 0)
        return (0);
    for (dp = domains; dp; dp = dp->dom_next)
        if (dp->dom_family == family)
            goto found;
    return (0);
found:
    for (pr = dp->dom_protosw; pr < dp->dom_protoswNPROTOSW; pr++)
    {
        if ((pr->pr_protocol == protocol) && (pr->pr_type == type))
            return (pr);

        if (type == SOCK_RAW && pr->pr_type == SOCK_RAW &&
                pr->pr_protocol == 0 && maybe == (struct protosw *)0)
            maybe = pr;
    }
    return (maybe);
}

不要被它嚇到了,其實不難理解,但理解之前需要知道的是struct protosw 是個結構體,裏面有.pr_type(SOCK_XXX)  和.pr_protocol( IPPROTO_XXX )等成員,所有的struct protosw 結構體存儲於一個 inetsw[] 數組中,此外有一個全局的domain 鏈表,其中一個節點inetdomain 的成員指針指向了inetsw[] 數組,大致圖形如下(不是很準確):


注意最後一個wildcare entry,它的.pr_protocol 沒有賦值故爲0,如下

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
 
/* raw wildcard */
{
    .pr_type =      SOCK_RAW,
    .pr_domain =        &inetdomain,
    .pr_flags =     PR_ATOMIC | PR_ADDR,
    .pr_input =     rip_input,
    .pr_ctloutput =     rip_ctloutput,
    .pr_init =      rip_init,
    .pr_usrreqs =       &rip_usrreqs
},
};  /* end of inetsw[] */

回過頭來看pffindtype 和 pffindproto:

pffindtype:  1. 通過"family" 參數找到對應的domain 節點
    2. 返回inetsw [] 數組中匹配“type" 參數的第一個struct protosw 結構體指針

pffindproto: 1. 通過"family" 參數找到對應的domain 節點
    2. 返回inetsw [] 數組中匹配“type" --”protocol“ 參數對的第一個struct protosw 結構體指針
    3. 如果參數對不匹配而且”type" 爲 SOCK_RAW,則返回wildcard entry 指針

假設現在這樣調用 socket(AF_INET, SOCK_RAW, 30);  則使用pffindproto() 函數查找,但因爲協議值30未在內核中定義,故返回wildcard_RAW entry。同理,你可能看見過別人這樣寫:socket(AF_INET, SOCK_RAW, IPPROTO_TCP); 實際上在FreeBSD 下 用pffindproto 找,SOCK_RAW 與 IPPROTO_TCP 也是不匹配的,返回wildcard_RAW entry 。

再者,在FreeBSD 上這樣調用 socket(AF_INET, SOCK_RAW, 0/* IPPRORO_IP*/);  是可以的,使用pffindtype() 函數查找,返回的第一個是default entry;但在linux 上這樣調用會出錯,errno = EPROTONOSUPPORT,這就是前面提到的兩個系統中不同點。爲什麼會出錯,看linux 源碼:

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
 
/* Upon startup we insert all the elements in inetsw_array[] into
 * the linked list inetsw.
 */

static struct inet_protosw inetsw_array[] =
{
    {
        .type =       SOCK_STREAM,
        .protocol =   IPPROTO_TCP,
        .prot =       &tcp_prot,
        .ops =        &inet_stream_ops,
        .capability = -1,
        .no_check =   0,
        .flags =      INET_PROTOSW_PERMANENT |
        INET_PROTOSW_ICSK,
    },

    {
        .type =       SOCK_DGRAM,
        .protocol =   IPPROTO_UDP,
        .prot =       &udp_prot,
        .ops =        &inet_dgram_ops,
        .capability = -1,
        .no_check =   UDP_CSUM_DEFAULT,
        .flags =      INET_PROTOSW_PERMANENT,
    },


    {
        .type =       SOCK_RAW,
        .protocol =   IPPROTO_IP,   /* wild card */
        .prot =       &raw_prot,
        .ops =        &inet_sockraw_ops,
        .capability = CAP_NET_RAW,
        .no_check =   UDP_CSUM_DEFAULT,
        .flags =      INET_PROTOSW_REUSE,
    }
};

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
 
static int inet_create(struct net *net, struct socket *sock, int protocol)
{

    /* ... */

    /* Look for the requested type/protocol pair. */
    answer = NULL;
lookup_protocol:
    err = -ESOCKTNOSUPPORT;
    rcu_read_lock();
    list_for_each_rcu(p, &inetsw[sock->type])
    {
        answer = list_entry(p, struct inet_protosw, list);

        /* Check the non-wild match. */
        if (protocol == answer->protocol)
        {
            if (protocol != IPPROTO_IP)
                break;
        }
        else
        {
            /* Check for the two wild cases. */
            if (IPPROTO_IP == protocol)
            {
                protocol = answer->protocol;
                break;
            }
            if (IPPROTO_IP == answer->protocol)
                break;
        }
        err = -EPROTONOSUPPORT;
        answer = NULL;
    }

    /* ... */

}

在這裏提醒一下IPPROTO_IP = 0, 在inet_create()函數中,我們根據type的值,在全局數組struct inet_protosw inetsw[]裏找到我們對應的協議轉換開關。下面通過來分析幾個調用來走一下上面的inet_create  函數(linux 下):

1) socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
protocol = 6
*answer = inetsw_array[0]
protocol == answer->protocol && protocol != IPPROTO_IP :  TRUE
OK

2) socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
protocol = 17 
*answer = inetsw_array[1]
protocol == answer->protocol && protocol != IPPROTO_IP :  TRUE
OK

3) socket(AF_INET, SOCK_STREAM, 0);
protocol = 0
*answer = inetsw_array[0]
if (protocol == answer->protocol) : FALSE
check else : 
/* Check for the two wild cases. */
if (IPPROTO_IP == protocol) {
protocol = answer->protocol;
break;
}
: TRUE
note that protocol value 0 is substituted with the real value of IPPROTO_TCP in line:
protocol = answer->protocol;
OK

/* 上面例子(3)解釋了文章最開始提出的疑問,現在protocol 已經被替換成了6 */

4) socket(AF_INET, SOCK_DGRAM, 0);
protocol = 0
*answer = inetsw_array[1]
if (protocol == answer->protocol) : FALSE
check else : 
/* Check for the two wild cases. */
if (IPPROTO_IP == protocol) {
protocol = answer->protocol;
break;
}
: TRUE
note that protocol value 0 is substituted with the real value of IPPROTO_UDP in line:
protocol = answer->protocol;
OK

5) socket(AF_INET, SOCK_RAW, 0);
protocol = 0
*answer = inetsw_array[2]
protocol == answer->protocol && protocol == IPPROTO_IP so : if (protocol != IPPROTO_IP)  is FALSE
not OK -> EPROTONOSUPPORT

6) socket(AF_INET, SOCK_STREAM, 9); (where 9 can be any protocol except IPPROTO_TCP)
    protocol = 9
*answer = inetsw_array[0]
if (protocol == answer->protocol) : FALSE
check else : 
/* Check for the two wild cases. */
if (IPPROTO_IP == protocol) {
protocol = answer->protocol;
break;
}
if (IPPROTO_IP == answer->protocol)
break;


both are a FALSE
not OK -> EPROTONOSUPPORT


7) socket(AF_INET, SOCK_DGRAM, 9); (where 9 can be any protocol except  IPPROTO_UDP)
same as above
not OK -> EPROTONOSUPPORT


8) socket(AF_INET, SOCK_RAW, 9); (where 9 can be *any* protocol except 0)
protocol = 9
*answer = inetsw_array[2]
if (protocol == answer->protocol) : FALSE
check else : 
/* Check for the two wild cases. */
if (IPPROTO_IP == protocol) {
protocol = answer->protocol;
break;

: FALSE
if (IPPROTO_IP == answer->protocol)
break;
: TRUE
OK

那raw socket 接收緩衝區的數據是什麼呢?看下面這個圖:



真正從網卡進來的數據是完整的以太網幀,底層用sk_buff 數據結構描述,最終進入接收緩衝區recv buffer,而我們應用層調用read / recv /recvfrom 從接收緩衝區拷貝數據到應用層提供的buffer,對一般的套接字,如SOCK_STREAM, SOCK_DGRAM 來說,此時緩衝區只有user data,其他各層的頭部已經被去除,而對於SOCK_RAW 來說是IP head + IP payload,當然也可以是arp/rarp 包,甚至是完整的幀(加上MAC頭)。

假設現在我們要通過SOCK_RAW 發送數據,則需要調用setsockopt 設置IP_HDRINCL 選項(如果protocol 設爲IPPROTO_RAW 則默認設置了IP_HDRINCL),即告訴內核我們自己來封裝IP頭部,其實頭部中某些元素是可以偷懶讓內核填充的:


需要注意的是,如果我們自己來封裝IP頭部,那麼數據包傳遞出去的時候IP 層就不會參與運作,即如果數據包大於接口的MTU,那麼不會進行分片而直接丟棄。


二、SOCK_RAW 應用

1、packet sniffer

 C++ Code 
1
2
3
4
5
6
7
 
sock_raw = socket(AF_INET , SOCK_RAW , IPPROTO_TCP);
while(1)
{
    data_size = recvfrom(sock_raw , buffer , 65535 , 0 , &saddr , &saddr_size);
    //Now process the packet
    ProcessPacket(buffer , data_size);
}
即創建原始套接字,調用recvfrom 接收數據,再調用processpacket 處理IP包,可以讀出ip head 和 tcp head 各字段。

上述程序只可以接收tcp 包,當然udp 和 icmp 可以這樣寫:

 C++ Code 
1
2
 
sock_raw = socket(AF_INET , SOCK_RAW , IPPROTO_UDP);
sock_raw = socket(AF_INET , SOCK_RAW , IPPROTO_ICMP);

但是不能以爲 sock_raw = socket(AF_INET , SOCK_RAW , IPPROTO_IP); 就能接收所有種類的IP包,如前所述,這是錯誤的。

上述程序只能監測到輸入的數據包,而且讀取的數據包中已經沒有了以太網頭部。

只需要稍稍改進一下:

 C++ Code 
1
 
sock_raw = socket( AF_PACKET , SOCK_RAW , htons(ETH_P_ALL)) ;


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爲非本地硬件地址的數據幀。


注意family 是AF_PACKET,這樣就能監測所有輸入和輸出的數據包,而且不僅限於IP包(tcp/udp/icmp),如arp/rarp 包也可以監測,並且數據包還包含以太網頭部。最後提一點,packet sniffer 也可以使用libpcap 庫實現,著名的tcpdump 就使用了此庫。


2、Tcp syn port scan

TCP 三次握手就不說了,端口掃描過程如下:

1. Send a Syn packet to a port A
2. Wait for a reply of Syn+Ack till timeout.
3. Syn+Ack reply means the port is open , Rst packet means port is closed , and otherwise it might be inaccessible or in a filtered state.

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 
//Create a raw socket
int s = socket (AF_INET, SOCK_RAW , IPPROTO_TCP);
if (setsockopt (s, IPPROTO_IP, IP_HDRINCL, val, sizeof (one)) < 0)
{
    printf ("Error setting IP_HDRINCL. Error number : %d . Error message : %s \n" , errno , strerror(errno));
    exit(0);
}
for(port = 1 ; port < 100 ; port++)
{

    //Send the packet
    if ( sendto (s, datagram , sizeof(struct iphdr) + sizeof(struct tcphdr) , 0 , (struct sockaddr *) &dest, sizeof (dest)) < 0)
    {
        printf ("Error sending syn packet. Error number : %d . Error message : %s \n" , errno , strerror(errno));
        exit(0);
    }
}

創建一個原始套接字s,開啓IP_HDRINCL 選項(這兩步可以直接用 int s = socket (AF_INET, SOCK_RAW, IPPROTO_RAW); ),自己封裝IP 頭部和tcp 頭部,主要是標誌位syn 置爲1,然後循環端口進行發送數據包。另開一個線程創建另一個原始套接字,仿照packet sniffer 進行數據包的接收,分解tcp 頭部看是否syn == 1 && ack == 1 && dest_addr == src_addr,如果是則表明端口是打開的。如果不追求效率,很簡單的做法是直接用普通的套接字,循環端口去connect,成功就表明端口是打開的,只是三次握手完整了一回。


3、SYN Flood DOS Attack

仿照上面端口掃描程序,自己封裝頭部,主要是syn 置爲1,然後在一個死循環中死命地對某個地址發送數據包。不過現在的網站一般有防火牆,我們這種小兒科程序對他們來說,跟玩一樣。


4、ICMP ping flood

實際上跟SYN flood 類似的道理,不過發送的是icmp 包,即自己封裝icmp 頭部

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
 
//Raw socket - if you use IPPROTO_ICMP, then kernel will fill in the correct ICMP header checksum, if IPPROTO_RAW, then it won't
int sockfd = socket (AF_INET, SOCK_RAW, IPPROTO_RAW);

if (sockfd < 0)
{
    perror("could not create socket");
    return (0);
}

int on = 1;

// We shall provide IP headers
if (setsockopt (sockfd, IPPROTO_IP, IP_HDRINCL, (const char *)&on, sizeof (on)) == -1)
{
    perror("setsockopt");
    return (0);
}

//allow socket to send datagrams to broadcast addresses
if (setsockopt (sockfd, SOL_SOCKET, SO_BROADCAST, (const char *)&on, sizeof (on)) == -1)
{
    perror("setsockopt");
    return (0);
}
while (1)
{

    if ( (sent_size = sendto(sockfd, packet, packet_size, 0, (struct sockaddr *) &servaddr, sizeof (servaddr))) < 1)
    {
        perror("send failed\n");
        break;
    }


    usleep(10000);  //microseconds
}


附錄:

1、相關頭文件

#include<netinet/ip_icmp.h>   //Provides declarations for icmp header
#include<netinet/udp.h>   //Provides declarations for udp header
#include<netinet/tcp.h>   //Provides declarations for tcp header
#include<netinet/ip.h>    //Provides declarations for ip header
#include<netinet/if_ether.h>  //For ETH_P_ALL
#include<net/ethernet.h>  //For ether_header

2、計算校驗和的函數

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
 
/*
    Function calculate checksum
*/

unsigned short in_cksum(unsigned short *ptr, int nbytes)
{
    register long sum;
    u_short oddbyte;
    register u_short answer;

    sum = 0;
    while (nbytes > 1)
    {
        sum += *ptr++;
        nbytes -= 2;
    }

    if (nbytes == 1)
    {
        oddbyte = 0;
        *((u_char *) & oddbyte) = *(u_char *) ptr;
        sum += oddbyte;
    }

    sum = (sum >> 16) + (sum & 0xffff);
    sum += (sum >> 16);
    answer = ~sum;

    return (answer);
}

注意,IP頭部中的校驗和只校驗ip頭部的大小,而tcp 頭部的校驗和需要校驗tcp頭部和數據,按照封包原則,封裝到TCP層的時候,ip信息還沒有封裝上去,但是校驗值卻需要馬上進行計算,所以必須手工構造一個僞頭部來表示ip層的信息,可以使用下面的結構體:

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
 
struct pseudo_header    //needed for checksum calculation
{
    unsigned int source_address;
    unsigned int dest_address;
    unsigned char placeholder; // 0
    unsigned char protocol;
    unsigned short tcp_length;

    struct tcphdr tcp; //tcp head
};

將pseduo_header 和 use_data 都拷貝到同個緩衝區,傳遞給in_cksum 的ptr 爲緩衝區起始地址,bytes 爲總共的大小。


參考:

http://www.binarytides.com/

http://sock-raw.org/

TCP Implementation in Linux: A Brief Tutorial.pdf

《UNP》

《TCP/IP 協議詳解 卷一》


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