在資料連結層,網路分為**交換式(switched)與非交換式(unswitched)**兩種。
混雜模式:看見不該看的封包#
在非交換式網路上,乙太封包會經過網路上每個裝置,理論上每個裝置只該看送給自己目的位址的封包。但要把裝置設成混雜模式(promiscuous mode)、讓它查看所有封包(不論目的位址)相當容易。
大多數封包擷取程式(如 tcpdump)預設就把裝置切到混雜模式。也可用 ifconfig 手動設定:
reader@hacking:~/booksrc $ sudo ifconfig eth0 promisc
擷取不一定給你看的封包,這個動作稱為嗅探(sniffing)。在非交換式網路以混雜模式嗅探,能翻出各種有用資訊。

圖 0x400-7:交換式網路依 MAC 位址把封包只送往對應的連接埠

圖 0x400-8:在交換式網路上,可透過 ARP 快取毒化把流量重導給攻擊者以進行嗅探
telnet、FTP、POP3 等服務傳輸的資料未加密。在
tcpdump輸出中,可以看到使用者leech用密碼l8@nite登入 FTP 伺服器——由於登入的認證過程也未加密,帳號密碼就直接躺在傳輸封包的資料部分。
tcpdump 是絕佳的通用嗅探器,但也有專門搜尋帳密的工具,例如 Dug Song 的 dsniff,聰明到能解析出看似重要的資料:
12/10/02 21:47:49 tcp 192.168.0.193.32785 -> 192.168.0.120.23 (telnet)
USER root
PASS 5eCr3t
原始 socket 嗅探器#
先前的例子都用串流 socket,其收送資料被整齊地包在 TCP/IP 連線中——作業系統打理了傳輸、更正與路由的所有低階細節。用**原始 socket(raw socket)**則能存取更低層,此時所有細節都暴露出來、必須由程式設計師明確處理。
原始 socket 以 SOCK_RAW 為型別指定,此時 protocol 就重要了(IPPROTO_TCP、IPPROTO_UDP 或 IPPROTO_ICMP):
if ((sockfd = socket(PF_INET, SOCK_RAW, IPPROTO_TCP)) == -1)
fatal("in socket");
for(i=0; i < 3; i++) {
recv_length = recv(sockfd, buffer, 8000, 0);
printf("Got a %d byte packet\n", recv_length);
dump(buffer, recv_length);
}使用原始 socket 需要 root 存取。而且這個程式並不可靠、流量大時會漏封包,還只擷取 TCP(要抓 UDP 或 ICMP 得各開額外的原始 socket)。原始 socket 最大的問題是各系統間出了名地不一致——Linux 的原始 socket 程式碼多半無法在 BSD 或 Solaris 上運作,使跨平台程式設計幾乎不可能。
libpcap 嗅探器#
標準化的程式庫 libpcap 能撫平原始 socket 的不一致。它底層仍用原始 socket,但知道如何在多種架構上正確運作——tcpdump 與 dsniff 都用它,因此能在任何平台輕鬆編譯。
pcap_sniff.c 用 libpcap 重寫嗅探器,函式相當直觀:
device = pcap_lookupdev(errbuf); // 找一個可嗅探的裝置
pcap_handle = pcap_open_live(device, 4096, 1, 0, errbuf); // 第 3 個引數 1=混雜模式
for(i=0; i < 3; i++) {
packet = pcap_next(pcap_handle, &header); // 抓下一個封包
printf("Got a %d byte packet\n", header.len);
dump(packet, header.len);
}
pcap_close(pcap_handle);編譯時需以
-l pcap連結 pcap 程式庫。pcap_handle的作用類似檔案描述元,但引用的是封包擷取物件;header是含擷取額外資訊(時間、長度)的pcap_pkthdr結構。
解碼各層#
原始封包擷取中,樣本文字前有許多相似的位元組——這些是乙太、IP、TCP 各層的標頭資訊。要解碼就需要對應的標頭結構。
Linux 的標頭結構定義於
/usr/include/linux/if_ethernet.h、/usr/include/netinet/ip.h、/usr/include/netinet/tcp.h。書中依這些結構與 RFC 圖,在hacking-network.h自訂了更好命名的版本——親手寫結構能獲得更好的理解。
自訂的乙太標頭:
#define ETHER_ADDR_LEN 6
#define ETHER_HDR_LEN 14
struct ether_hdr {
unsigned char ether_dest_addr[ETHER_ADDR_LEN]; // 目的 MAC
unsigned char ether_src_addr[ETHER_ADDR_LEN]; // 來源 MAC
unsigned short ether_type; // 乙太封包型別
};乙太標頭是 14 位元組(兩個 6 位元組 MAC + 2 位元組型別)。但許多編譯器會沿 4 位元組邊界填補結構以對齊,使
sizeof(struct ethhdr)回傳錯誤大小。因此標頭長度應用ETHER_HDR_LEN或固定值 14,而非sizeof。
IP 標頭永遠 20 位元組:
struct ip_hdr {
unsigned char ip_version_and_header_length; // 版本與標頭長度
unsigned char ip_tos; // 服務型別
unsigned short ip_len; // 總長度
unsigned short ip_id; // 識別號
unsigned short ip_frag_offset; // 分片偏移與旗標
unsigned char ip_ttl; // 存活時間
unsigned char ip_type; // 協定型別
unsigned short ip_checksum; // 檢查碼
unsigned int ip_src_addr; // 來源 IP
unsigned int ip_dest_addr; // 目的 IP
};RFC 中 Version 與 IHL 各只有 4 位元,C 沒有 4 位元變數型別,所以 Linux 標頭定義依主機位元組順序拆分該位元組。但我們用不到這兩個欄位,甚至不必拆分。TCP 標頭則參考
netinet/tcp.h與 RFC 793,其中定義了TH_FIN、TH_SYN、TH_RST、TH_PUSH、TH_ACK、TH_URG等旗標位元。