Linux網絡編程:原始套接字編程及實例分析
一、原始套接字能干什么?
通常情況下程序員接所接觸到的套接字(Socket)為兩類:
(1)流式套接字(SOCK_STREAM):一種面向連接的Socket,針對于面向連接的TCP 服務應用;
(2)數據報式套接字(SOCK_DGRAM):一種無連接的Socket,對應于無連接的UDP 服務應用。
從用戶的角度來看,SOCK_STREAM、SOCK_DGRAM 這兩類套接字似乎的確涵蓋了TCP/IP 應用的全部,因為基于TCP/IP 的應用,從協議棧的層次上講,在傳輸層的確只可能建立于TCP 或 UDP協議之上,而SOCK_STREAM、SOCK_DGRAM 又分別對應于TCP和UDP,所以幾乎所有的應用都可以用這兩類套接字實現。
但是,當我們面對如下問題時,SOCK_STREAM、SOCK_DGRAM 將顯得這樣無助:
(1)怎樣發送一個自定義的IP 包?
(2)怎樣發送一個ICMP 協議包?
(3)怎樣分析所有經過網絡的包,而不管這樣包是否是發給自己的?
(4)怎樣偽裝本地的IP 地址?
這使得我們必須面對另外一個深刻的主題——原始套接字(SOCK_RAW)。原始套接字廣泛應用于高級網絡編程,也是一種廣泛的黑客手段。著名的網絡sniffer(一種基于被動偵聽原理的網絡分析方式)、拒絕服務攻擊(DOS)、IP 欺騙等都可以通過原始套接字實現。
原始套接字(SOCK_RAW)可以用來自行組裝數據包,可以接收本機網卡上所有的數據幀(數據包),對于監聽網絡流量和分析網絡數據很有作用。
原始套接字是基于IP 數據包的編程(SOCK_PACKET 是基于數據鏈路層的編程)。另外,必須在管理員權限下才能使用原始套接字。
原始套接字(SOCK_RAW)與標準套接字(SOCK_STREAM、SOCK_DGRAM)的區別在于原始套接字直接置“根”于操作系統網絡核心(Network Core),而 SOCK_STREAM、SOCK_DGRAM 則“懸浮”于 TCP 和 UDP 協議的外圍。
流式套接字只能收發 TCP 協議的數據,數據報套接字只能收發 UDP 協議的數據,原始套接字可以收發內核沒有處理的數據包。
#p#
二、原始套接字編程
原始套接字編程和之前的UDP 編程差不多,無非就是創建一個套接字后,通過這個套接字接收數據或者發送數據。區別在于,原始套接字可以自行組裝數據包(偽裝本地 IP,本地 MAC),可以接收本機網卡上所有的數據幀(數據包)。另外,必須在管理員權限下才能使用原始套接字。
原始套接字的創建:
int socket ( int family, int type, int protocol );
參數:
family:協議族 這里寫 PF_PACKET
type: 套接字類,這里寫 SOCK_RAW
protocol:協議類別,指定可以接收或發送的數據包類型,不能寫 “0”,取值如下,注意,傳參時需要用 htons() 進行字節序轉換。
ETH_P_IP:IPV4數據包
ETH_P_ARP:ARP數據包
ETH_P_ALL:任何協議類型的數據包
返回值:
成功( >0 ):套接字,這里為鏈路層的套接字
失敗( <0 ):出錯
實例如下:
- // 所需頭文件
- #include <sys/socket.h>
- #include <netinet/ether.h>
- #include <stdio.h> // perror
- int main(int argc,charchar *argv[])
- {
- int sock_raw_fd = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL) );
- if(sock_raw_fd < 0){
- perror("socket");
- return -1;
- }
- return 0;
- }
獲取鏈路層的數據包:
ssize_t recvfrom( int sockfd,
void *buf,
size_t nbytes,
int flags,
struct sockaddr *from,
socklen_t *addrlen );
參數:
sockfd: 原始套接字
buf: 接收數據緩沖區
nbytes: 接收數據緩沖區的大小
flags: 套接字標志(常為0)
from: 這里沒有用,寫 NULL
addrlen:這里沒有用,寫 NULL
返回值:
成功:接收到的字符數
失?。?1
實例如下:
- #include <stdio.h>
- #include <netinet/in.h>
- #include <sys/socket.h>
- #include <netinet/ether.h>
- int main(int argc,charchar *argv[])
- {
- unsigned char buf[1024] = {0};
- int sock_raw_fd = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
- //獲取鏈路層的數據包
- int len = recvfrom(sock_raw_fd, buf, sizeof(buf), 0, NULL, NULL);
- printf("len = %d\n", len);
- return 0;
- }
混雜模式
默認的情況下,我們接收數據,目的地址是本地地址,才會接收。有時候我們想接收所有經過網卡的所有數據流,而不論其目的地址是否是它,這時候我們需要設置網卡為混雜模式。
網卡的混雜模式一般在網絡管理員分析網絡數據作為網絡故障診斷手段時用到,同時這個模式也被網絡黑客利用來作為網絡數據竊聽的入口。在 Linux 操作系統中設置網卡混雜模式時需要管理員權限。在 Windows 操作系統和 Linux 操作系統中都有使用混雜模式的抓包工具,比如著名的開源軟件 Wireshark。
通過命令給 Linux 網卡設置混雜模式(需要管理員權限)
設置混雜模式:ifconfig eth0 promisc
取消混雜模式:ifconfig eth0 -promisc
通過代碼給 Linux 網卡設置混雜模式
代碼如下:
- struct ifreq ethreq; //網絡接口地址
- strncpy(ethreq.ifr_name, "eth0", IFNAMSIZ); //指定網卡名稱
- if(-1 == ioctl(sock_raw_fd, SIOCGIFINDEX, ðreq)) //獲取網絡接口
- {
- perror("ioctl");
- close(sock_raw_fd);
- exit(-1);
- }
- ethreq.ifr_flags |= IFF_PROMISC;
- if(-1 == ioctl(sock_raw_fd, SIOCSIFINDEX, ðreq)) //網卡設置混雜模式
- {
- perror("ioctl");
- close(sock_raw_fd);
- exit(-1);
- }
發送自定義的數據包:
ssize_t sendto( int sockfd,
const void *buf,
size_t nbytes,int flags,
const struct sockaddr *to,
socklen_t addrlen );
參數:
sockfd: 原始套接字
buf: 發送數據緩沖區
nbytes: 發送數據緩沖區的大小
flags: 一般為 0
to: 本機網絡接口,指發送的數據應該從本機的哪個網卡出去,而不是以前的目的地址
addrlen:to 所指向內容的長度
返回值:
成功:發送數據的字符數
失?。?-1
本機網絡接口的定義
發送完整代碼如下:
- struct sockaddr_ll sll; //原始套接字地址結構
- struct ifreq ethreq; //網絡接口地址
- strncpy(ethreq.ifr_name, "eth0", IFNAMSIZ); //指定網卡名稱
- if(-1 == ioctl(sock_raw_fd, SIOCGIFINDEX, ðreq)) //獲取網絡接口
- {
- perror("ioctl");
- close(sock_raw_fd);
- exit(-1);
- }
- /*將網絡接口賦值給原始套接字地址結構*/
- bzero(&sll, sizeof(sll));
- sll.sll_ifindex = ethreq.ifr_ifindex;
- // 發送數據
- // send_msg, msg_len 這里還沒有定義,模擬一下
- int len = sendto(sock_raw_fd, send_msg, msg_len, 0 , (struct sockaddr *)&sll, sizeof(sll));
- if(len == -1)
- {
- perror("sendto");
- }
這里頭文件情況如下:
- #include <net/if.h>// struct ifreq
- #include <sys/ioctl.h> // ioctl、SIOCGIFADDR
- #include <sys/socket.h> // socket
- #include <netinet/ether.h> // ETH_P_ALL
- #include <netpacket/packet.h> // struct sockaddr_ll
#p#
三、原始套接字實例:MAC頭部報文分析
由上得知,我們可以通過原始套接字以及 recvfrom( ) 可以獲取鏈路層的數據包,那我們接收的鏈路層數據包到底長什么樣的呢?
鏈路層封包格式
MAC 頭部(有線局域網)
注意:CRC、PAD 在組包時可以忽略
鏈路層數據包的其中一種情況:
- unsigned char msg[1024] = {
- //--------------組MAC--------14------
- 0xb8, 0x88, 0xe3, 0xe1, 0x10, 0xe6, // dst_mac: b8:88:e3:e1:10:e6
- 0xc8, 0x9c, 0xdc, 0xb7, 0x0f, 0x19, // src_mac: c8:9c:dc:b7:0f:19
- 0x08, 0x00, // 類型:0x0800 IP協議
- // …… ……
- // …… ……
- };
接收的鏈路層數據包,并對其進行簡單分析:
- #include <stdio.h>
- #include <string.h>
- #include <stdlib.h>
- #include <sys/socket.h>
- #include <netinet/in.h>
- #include <arpa/inet.h>
- #include <netinet/ether.h>
- int main(int argc,charchar *argv[])
- {
- int i = 0;
- unsigned char buf[1024] = "";
- int sock_raw_fd = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
- while(1)
- {
- unsigned char src_mac[18] = "";
- unsigned char dst_mac[18] = "";
- //獲取鏈路層的數據幀
- recvfrom(sock_raw_fd, buf, sizeof(buf),0,NULL,NULL);
- //從buf里提取目的mac、源mac
- sprintf(dst_mac,"%02x:%02x:%02x:%02x:%02x:%02x", buf[0], buf[1], buf[2], buf[3], buf[4], buf[5]);
- sprintf(src_mac,"%02x:%02x:%02x:%02x:%02x:%02x", buf[6], buf[7], buf[8], buf[9], buf[10], buf[11]);
- //判斷是否為IP數據包
- if(buf[12]==0x08 && buf[13]==0x00)
- {
- printf("______________IP數據報_______________\n");
- printf("MAC:%s >> %s\n",src_mac,dst_mac);
- }//判斷是否為ARP數據包
- else if(buf[12]==0x08 && buf[13]==0x06)
- {
- printf("______________ARP數據報_______________\n");
- printf("MAC:%s >> %s\n",src_mac,dst_mac);
- }//判斷是否為RARP數據包
- else if(buf[12]==0x80 && buf[13]==0x35)
- {
- printf("______________RARP數據報_______________\n");
- printf("MAC:%s>>%s\n",src_mac,dst_mac);
- }
- }
- return 0;
- }
記得以管理者權限運行程序: