淺談原始套接字 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
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)
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)
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() 函式:
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() 函式,兩個函式如下:
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,如下
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 原始碼:
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, } }; |
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
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); } |
上述程式只可以接收tcp 包,當然udp 和 icmp 可以這樣寫:
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包,如前所述,這是錯誤的。
上述程式只能監測到輸入的資料包,而且讀取的資料包中已經沒有了乙太網頭部。
只需要稍稍改進一下:
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.
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 頭部
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、計算校驗和的函式
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層的資訊,可以使用下面的結構體:
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 為總共的大小。
參考:
TCP Implementation in Linux: A Brief Tutorial.pdf
《UNP》
《TCP/IP 協議詳解 卷一》
相關文章
- 阻止SYN Flood攻擊
- SYN Flood攻擊的基本原理(轉)
- 教你如何在Linux中防止SYN Flood攻擊Linux
- LINUX下SYN FLOOD攻擊及LINUX下SYN攻防簡述Linux
- 3 分鐘視訊講解 SYN Flood 攻擊原理
- 和同事談談Flood Fill 演算法演算法
- 淺談webscoket原理及其應用Web
- LeetCode 733. Flood FillLeetCode
- 淺談 K-D Tree 及其進階應用
- 淺談Java String內幕(1)Java
- 學習--原始套接字(轉)
- 帶你學習Flood Fill演算法與最短路模型演算法模型
- Python 套接字內建方法Python
- 將普通的套接字網路應用修改為ssl網路應用
- 淺析RunLoop原理及其應用OOP
- 應用安全淺談
- 淺談session及其安全Session
- 淺談例外表的應用
- SO_REUSEADDR 套接字選項應用
- CDPR收購開發《洪潮之焰》、《空穴》的工作室The Molasses Flood
- Python原始套接字程式設計Python程式設計
- 淺談TCP、UDP、ICMP三種常見協議TCPUDP協議
- 淺談混合應用的演進
- 淺談 URI 及其轉義
- 淺談深度學習的技術原理及其在計算機視覺的應用深度學習計算機視覺
- 單頁應用SEO淺談
- 淺談桌面應用程式的開發
- [部落格搬家]淺談Vmware的應用
- [譯]淺析t-SNE原理及其應用
- “淺談” Flutter 應用落地心得Flutter
- 從原始碼角度談談AsyncTask的使用及其原理原始碼
- 淺談AB實驗及其在轉轉B2B報價系統中的應用
- IPC之套接字
- 網路套接字
- Linux :套接字Linux
- 零拷貝(Zero-copy) 淺析及其應用
- Linux網路程式設計--原始套接字(轉)Linux程式設計
- 傳輸層協議、應用層、socket套接字、半連結池協議