在資料連結層,網路分為**交換式(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_TCPIPPROTO_UDPIPPROTO_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,但知道如何在多種架構上正確運作——tcpdumpdsniff 都用它,因此能在任何平台輕鬆編譯。

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_FINTH_SYNTH_RSTTH_PUSHTH_ACKTH_URG 等旗標位元。