http://blog.csdn.net/wangxg_7520/article/details/2795229
看了太多的“自己動手”,這次咱也“自己動手”一下,寫個簡單的網絡抓包工具吧。要寫出像tcpdump和wireshark(ethereal)這樣的大牛程序來,咱也沒那能耐,呵呵。所以這個工具只能抓取本地IP數據報,同時它還使用了BPF,目的是了解如何進行簡單有效的網絡抓包。
當打開一個標准SOCKET套接口時,我們比較熟悉的協議往往是用AF_INET來建立基於TCP(SOCK_STREAM)或UDP(SOCK_DGRAM)的鏈接。但是這些只用於IP層以上,要想從更底層抓包,我們需要使用AF_PACKET來建立套接字,它支持SOCK_RAW和SOCK_DGRAM,它們都能從底層抓包,不同的是后者得到的數據不包括以太網幀頭(最開始的14個字節)。好了,現在我們就知道該怎樣建立SOCKET套接口了:
- sock = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_IP));
最后一個參數 ETH_P_IP 指出,我們只對IP包感興趣,而不是ARP,RARP等。之后就可以用recvfrom從套接口讀取數據了。
現在我們可以抓到發往本地的所有IP數據報了,那么有沒有辦法抓到那些“流經”本地的數據呢?呵呵,當然可以了,這種技術叫網絡嗅探(sniff),它很能威脅網絡安全,也非常有用,尤其是當你對網內其他用戶的隱私感興趣時:( 由於以太網數據包是對整個網段廣播的,所以網內所有用戶都能收到其他用戶發出的數據,只是默認的,網卡只接收目的地址是自己或廣播地址的數據,而把不是發往自己的數據包丟棄。但是多數網卡驅動會提供一種混雜模式(promiscous mode),工作在這種模式下的網卡會接收網絡內的所有數據,不管它是發給誰的。下面的方法可以把網卡設成混雜模式:
// set NIC to promiscous mode, so we can recieve all packets of the network strncpy(ethreq.ifr_name, "eth0", IFNAMSIZ); ioctl(sock, SIOCGIFFLAGS, ðreq); ethreq.ifr_flags |= IFF_PROMISC; ioctl(sock, SIOCSIFFLAGS, ðreq);
通過ifconfig可以很容易的查看當前網卡是否工作在混雜模式(PROMISC)。但是請注意,程序退出后,網卡的工作模式不會改變,所以別忘了關閉網卡的混雜模式:
// turn off promiscous mode ethreq.ifr_flags &= ~IFF_PROMISC; ioctl(sock, SIOCSIFFLAGS, ðreq);
現在我們可以抓到本網段的所有IP數據包了,但是問題也來了:那么多的數據,怎么處理?CPU可能會被嚴重占用,而且絕大多數的數據我們可能根本就不敢興趣!那怎么辦呢?用if語句?可能要n多個,而且絲毫不會降低內核的繁忙程度。最好的辦法就是告訴內核,把不感興趣的數據過濾掉,不要往應用層送。BPF就為此而生。
BPF(Berkeley Packet Filter)是一種類是匯編的偽代碼語言,它也有命令代碼和操作數。例如,如果我們只對用戶192.168.1.4的數據感興趣,可以用tcpdump的-d選項生成BPF代碼如下:
$tcpdump -d host 192.168.1.4 (000) ldh [12] (001) jeq #0x800 jt 2 jf 6 (002) ld [26] (003) jeq #0xc0a80104 jt 12 jf 4 (004) ld [30] (005) jeq #0xc0a80104 jt 12 jf 13 (006) jeq #0x806 jt 8 jf 7 (007) jeq #0x8035 jt 8 jf 13 (008) ld [28] (009) jeq #0xc0a80104 jt 12 jf 10 (010) ld [38] (011) jeq #0xc0a80104 jt 12 jf 13 (012) ret #96 (013) ret #0
其中第一列代表行號,第二列是命令代碼,后面是操作數。下面我們采用匯編注釋的方式簡單的解釋一下:
(000) ldh [12] ;load h?? (2 bytes) from ABS offset 12 (the TYPE of ethernet header)
(001) jeq #0x800 jt 2 jf 6 ;compare and jump, jump to line 2 if true; else jump to line 6
(002) ld [26] ;load word (4 bytes) from ABS offset 26 (src IP address of IP header)
(003) jeq #0xc0a80104 jt 12 jf 4 ;compare and jump, jump to line 12 if true, else jump to line 4
(004) ld [30] ; load word (4 bytes) from ABS offset 30 (dst IP address of IP header)
(005) jeq #0xc0a80104 jt 12 jf 13 ;see line 3
(006) jeq #0x806 jt 8 jf 7 ;compare with ARP, see line 1
(007) jeq #0x8035 jt 8 jf 13 ;compare with RARP, see line 1
(008) ld [28] ;src IP address for other protocols
(009) jeq #0xc0a80104 jt 12 jf 10
(010) ld [38] ;dst IP address for other protocols
(011) jeq #0xc0a80104 jt 12 jf 13
(012) ret #96 ;return 96 bytes to user application
(013) ret #0 ;drop the packet
但是這樣的偽代碼我們是無法在應用程序里使用的,所以tcpdum提供了一個-dd選項來輸出一段等效的C代碼:
- $tcpdump -dd host 192.168.1.4
- { 0x28, 0, 0, 0x0000000c },
- { 0x15, 0, 4, 0x00000800 },
- { 0x20, 0, 0, 0x0000001a },
- { 0x15, 8, 0, 0xc0a80104 },
- { 0x20, 0, 0, 0x0000001e },
- { 0x15, 6, 7, 0xc0a80104 },
- { 0x15, 1, 0, 0x00000806 },
- { 0x15, 0, 5, 0x00008035 },
- { 0x20, 0, 0, 0x0000001c },
- { 0x15, 2, 0, 0xc0a80104 },
- { 0x20, 0, 0, 0x00000026 },
- { 0x15, 0, 1, 0xc0a80104 },
- { 0x6, 0, 0, 0x00000060 },
- { 0x6, 0, 0, 0x00000000 },
該代碼對應的數據結構是struct sock_filter,該結構在linux/filter.h中定義如下:
struct sock_filter // Filter block { __u16 code; // Actual filter code __u8 jt; // Jump true __u8 jf; // Jump false __u32 k; // Generic multiuse field };
code對應命令代碼;jt是jump if true后面的操作數,注意這里用的是相對行偏移,如2就表示向前跳轉2行,而不像偽代碼中使用絕對行號;jf為jump if false后面的操作數;k對應偽代碼中第3列的操作數。
了解了BPF偽代碼和結構,我們就可以自己定制更加簡單有效的BPF filter了,如上例中的6-11行不是針對IP協議的,而我們的套接字已經指定只讀取IP數據了,所以就可以把他們刪除,不過要注意,行偏移也要做相應的修改。
另外,tcpdump默認只返回96字節的數據,但對大部分應用來說,96字節是遠遠不夠的,所以tcpdump提供了-s選項用於指定返回的數據長度。
OK,下面我們就來看看怎樣把過濾器安裝到套接口上吧:
$tcpdump ip -d -s 2048 host 192.168.1.2 (000) ldh [12] (001) jeq #0x800 jt 2 jf 7 (002) ld [26] (003) jeq #0xc0a80102 jt 6 jf 4 (004) ld [30] (005) jeq #0xc0a80102 jt 6 jf 7 (006) ret #2048 (007) ret #0 struct sock_filter bpf_code[] = { { 0x28, 0, 0, 0x0000000c }, { 0x15, 0, 5, 0x00000800 }, { 0x20, 0, 0, 0x0000001a }, { 0x15, 2, 0, 0xc0a80102 }, { 0x20, 0, 0, 0x0000001e }, { 0x15, 0, 1, 0xc0a80102 }, { 0x6, 0, 0, 0x00000800 }, { 0x6, 0, 0, 0x00000000 } }; struct sock_fprog filter; filter.len = sizeof(bpf_code)/sizeof(bpf_code[0]); filter.filter = bpf_code; setsockopt(sock, SOL_SOCKET, SO_ATTACH_FILTER, &filter, sizeof(filter));
最后加上信號處理器,以便能在程序退出前恢復網卡的工作模式。到現在我們已經可以看到一個小聚規模抓包小工具了,呵呵,麻雀雖小,但也五臟俱全啊!下面給出完整的代碼。
#include <sys/types.h> #include <sys/time.h> #include <sys/ioctl.h> #include <sys/socket.h> #include <linux/types.h> #include <netinet/in.h> #include <netinet/udp.h> #include <netinet/ip.h> #include <netpacket/packet.h> #include <net/ethernet.h> #include <arpa/inet.h> #include <string.h> #include <signal.h> #include <net/if.h> #include <stdio.h> #include <sys/uio.h> #include <fcntl.h> #include <unistd.h> #include <linux/filter.h> #include <stdlib.h> #define ETH_HDR_LEN 14 #define IP_HDR_LEN 20 #define UDP_HDR_LEN 8 #define TCP_HDR_LEN 20 static int sock; void sig_handler(int sig) { struct ifreq ethreq; if(sig == SIGTERM) printf("SIGTERM recieved, exiting.../n"); else if(sig == SIGINT) printf("SIGINT recieved, exiting.../n"); else if(sig == SIGQUIT) printf("SIGQUIT recieved, exiting.../n"); // turn off the PROMISCOUS mode strncpy(ethreq.ifr_name, "eth0", IFNAMSIZ); if(ioctl(sock, SIOCGIFFLAGS, ðreq) != -1) { ethreq.ifr_flags &= ~IFF_PROMISC; ioctl(sock, SIOCSIFFLAGS, ðreq); } close(sock); exit(0); } int main(int argc, char ** argv) { int n; char buf[2048]; unsigned char *ethhead; unsigned char *iphead; struct ifreq ethreq; struct sigaction sighandle; #if 0 $tcpdump ip -s 2048 -d host 192.168.1.2 (000) ldh [12] (001) jeq #0x800 jt 2 jf 7 (002) ld [26] (003) jeq #0xc0a80102 jt 6 jf 4 (004) ld [30] (005) jeq #0xc0a80102 jt 6 jf 7 (006) ret #2048 (007) ret #0 #endif struct sock_filter bpf_code[] = { { 0x28, 0, 0, 0x0000000c }, { 0x15, 0, 5, 0x00000800 }, { 0x20, 0, 0, 0x0000001a }, { 0x15, 2, 0, 0xc0a80102 }, { 0x20, 0, 0, 0x0000001e }, { 0x15, 0, 1, 0xc0a80102 }, { 0x6, 0, 0, 0x00000800 }, { 0x6, 0, 0, 0x00000000 } }; struct sock_fprog filter; filter.len = sizeof(bpf_code)/sizeof(bpf_code[0]); filter.filter = bpf_code; sighandle.sa_flags = 0; sighandle.sa_handler = sig_handler; sigemptyset(&sighandle.sa_mask); //sigaddset(&sighandle.sa_mask, SIGTERM); //sigaddset(&sighandle.sa_mask, SIGINT); //sigaddset(&sighandle.sa_mask, SIGQUIT); sigaction(SIGTERM, &sighandle, NULL); sigaction(SIGINT, &sighandle, NULL); sigaction(SIGQUIT, &sighandle, NULL); // AF_PACKET allows application to read pecket from and write packet to network device // SOCK_DGRAM the packet exclude ethernet header // SOCK_RAW raw data from the device including ethernet header // ETH_P_IP all IP packets if((sock = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_IP))) == -1) { perror("socket"); exit(1); } // set NIC to promiscous mode, so we can recieve all packets of the network strncpy(ethreq.ifr_name, "eth0", IFNAMSIZ); if(ioctl(sock, SIOCGIFFLAGS, ðreq) == -1) { perror("ioctl"); close(sock); exit(1); } ethreq.ifr_flags |= IFF_PROMISC; if(ioctl(sock, SIOCSIFFLAGS, ðreq) == -1) { perror("ioctl"); close(sock); exit(1); } // attach the bpf filter if(setsockopt(sock, SOL_SOCKET, SO_ATTACH_FILTER, &filter, sizeof(filter)) == -1) { perror("setsockopt"); close(sock); exit(1); } while(1) { n = recvfrom(sock, buf, sizeof(buf), 0, NULL, NULL); if(n < (ETH_HDR_LEN+IP_HDR_LEN+UDP_HDR_LEN)) { printf("invalid packet/n"); continue; } printf("%d bytes recieved/n", n); ethhead = buf; printf("Ethernet: MAC[%02X:%02X:%02X:%02X:%02X:%02X]", ethhead[0], ethhead[1], ethhead[2], ethhead[3], ethhead[4], ethhead[5]); printf("->[%02X:%02X:%02X:%02X:%02X:%02X]", ethhead[6], ethhead[7], ethhead[8], ethhead[9], ethhead[10], ethhead[11]); printf(" type[%04x]/n", (ntohs(ethhead[12]|ethhead[13]<<8))); iphead = ethhead + ETH_HDR_LEN; // header length as 32-bit printf("IP: Version: %d HeaderLen: %d[%d]", (*iphead>>4), (*iphead & 0x0f), (*iphead & 0x0f)*4); printf(" TotalLen %d", (iphead[2]<<8|iphead[3])); printf(" IP [%d.%d.%d.%d]", iphead[12], iphead[13], iphead[14], iphead[15]); printf("->[%d.%d.%d.%d]", iphead[16], iphead[17], iphead[18], iphead[19]); printf(" %d", iphead[9]); if(iphead[9] == IPPROTO_TCP) printf("[TCP]"); else if(iphead[9] == IPPROTO_UDP) printf("[UDP]"); else if(iphead[9] == IPPROTO_ICMP) printf("[ICMP]"); else if(iphead[9] == IPPROTO_IGMP) printf("[IGMP]"); else if(iphead[9] == IPPROTO_IGMP) printf("[IGMP]"); else printf("[OTHERS]"); printf(" PORT [%d]->[%d]/n", (iphead[20]<<8|iphead[21]), (iphead[22]<<8|iphead[23])); } close(sock); exit(0); }
參考資料:
[1] Linux下Sniffer程序的實現
[2] 使用socket BPF