18.2 使用NPCAP庫抓取封包

2023-10-26 09:00:43

NPCAP 庫是一種用於在Windows平臺上進行網路封包捕獲和分析的庫。它是WinPcap庫的一個分支,由Nmap開發團隊開發,並在Nmap軟體中使用。與WinPcap一樣,NPCAP庫提供了一些API,使開發人員可以輕鬆地在其應用程式中捕獲和處理網路封包。NPCAP庫可以通過WinPcap API進行程式設計,因此現有的WinPcap應用程式可以輕鬆地遷移到NPCAP庫上。

與WinPcap相比,NPCAP庫具有更好的效能和可靠性,支援最新的作業系統和硬體。它還提供了對802.11無線網路的本機支援,並可以通過Wireshark等網路分析工具進行使用。 NPCAP庫是在MIT許可證下發布的,因此可以在免費和商業軟體中使用。

該工具包分為兩部分組成驅動程式及SDK工具包,在使用本庫進行抓包時需要讀者自行安裝對應版本的驅動程式,此處讀者使用的版本是npcap-1.55.exe當下載後讀者可自行點選下一步即可,當安裝完成後即可看到如下圖所示的提示資訊;

當驅動程式安裝完成後,讀者就可以自行設定開發工具包到專案中,通常只需要將工具包內的includelib庫設定到專案中即可,如下圖所示設定後自行應用儲存即可。

接著我們來實現第一個功能,列舉當前主機中可以使用的網路卡資訊,該功能的實現主要依賴於pcap_findalldevs_ex()函數,該函數用於獲取當前系統中可用的所有網路介面卡的列表。

函數的原型宣告如下:

int pcap_findalldevs_ex(const char *source, struct pcap_rmtauth *auth,
                        pcap_if_t **alldevsp, char *errbuf);

其中,引數含義如下:

  • source:指定遠端介面的IP地址,或者為本地介面傳入NULL。
  • auth:一個指向pcap_rmtauth結構來指定遠端的IP和使用者名稱。
  • alldevsp:一個指向指標,返回主機上可用的裝置列表。
  • errbuf:一個用於儲存錯誤資訊的緩衝區。

該函數允許開發者通過一個結構來檢索所有網路介面卡的詳細資訊。它允許指定一個過濾器,以匹配使用者定義的網路介面卡和屬性。此外,pcap_findalldevs_ex()還提供用於儲存錯誤資訊的結構體,以便在函數呼叫失敗時提供錯誤資訊。

該函數返回值-1表示失敗;否則,返回值為0表示操作成功,並將返回所有可用的網路介面卡和它們的詳細資訊。這些詳細資訊包括介面卡的名稱、描述、MAC地址、IP地址和子網掩碼等,當讀者使用列舉函數結束後需要自行呼叫pcap_freealldevs函數釋放這個指標以避免記憶體漏失。

以下是pcap_freealldevs函數原型宣告:

void pcap_freealldevs(pcap_if_t *alldevs);

其中,alldevs引數是指向pcap_if_t型別結構體的指標,該型別結構體記錄了當前主機上所有可用的網路介面的詳細資訊。pcap_freealldevs() 會釋放傳入的pcap_if_t型連結串列,並將所有元素刪除。

呼叫pcap_freealldevs()函數時需要傳入之前通過pcap_findalldevs()pcap_findalldevs_ex()函數獲取到的的指向連結串列結構的指標作為引數。

當有了這兩個函數作為條件,那麼實現列舉網路卡則變得很簡單了,如下程式碼所示則是使用該工具包實現列舉的具體實現流程,讀者可自行編譯測試。

#include <iostream>
#include <winsock2.h>
#include <Windows.h>
#include <string>
#include <pcap.h>

#pragma comment(lib,"ws2_32.lib")
#pragma comment(lib, "packet.lib")
#pragma comment(lib, "wpcap.lib")

using namespace std;

// 輸出線條
void PrintLine(int x)
{
  for (size_t i = 0; i < x; i++)
  {
    printf("-");
  }
  printf("\n");
}

// 列舉當前網路卡
int enumAdapters()
{
  pcap_if_t *allAdapters;    // 所有網路卡裝置儲存
  pcap_if_t *ptr;            // 用於遍歷的指標
  int index = 0;
  char errbuf[PCAP_ERRBUF_SIZE];

  /* 獲取本地機器裝置列表 */
  if (pcap_findalldevs_ex(PCAP_SRC_IF_STRING, NULL, &allAdapters, errbuf) != -1)
  {
    PrintLine(100);
    printf("索引 \t 網路卡名 \n");
    PrintLine(100);

    /* 列印網路卡資訊列表 */
    for (ptr = allAdapters; ptr != NULL; ptr = ptr->next)
    {
      ++index;
      if (ptr->description)
      {
        printf("[ %d ] \t [ %s ] \n", index - 1, ptr->description);
      }
    }
  }

  /* 不再需要裝置列表了,釋放它 */
  pcap_freealldevs(allAdapters);
  return index;
}
int main(int argc, char* argv[])
{
  enumAdapters();
  system("pause");
  return 0;
}

編譯並以管理員身份執行程式,則讀者可看到如下圖所示輸出結果,其中第一列為網路卡索引編號,第二列為網路卡名稱;

當有了網路卡編號後則讀者就可以對特定編號進行抓包解析了,抓包功能的實現依賴於pcap_open()函數,該函數用於開啟一個指定網路介面卡並開始捕獲網路封包,函數的原型宣告如下所示:

pcap_t *pcap_open(const char *source, int snaplen, int flags, int read_timeout, 
     struct pcap_rmtauth *auth, char *errbuf);

其引數含義如下:

  • source:要開啟的網路介面的名稱或者是儲存在pcap_open_live()中獲取的名稱。
  • snaplen:設定捕獲封包的大小。
  • flags:設定捕獲封包的模式,在promiscuous控制器模式或非promiscuous模式下捕獲。
  • read_timeout:設定阻塞讀函數的超時時間以毫秒為單位。
  • auth:一個指向pcap_rmtauth結構,指定遠端的IP和使用者名稱。
  • errbuf:一個用於儲存錯誤資訊的緩衝區。

該函數返回一個指向pcap_t型別的指標,該型別結構提供了與網路介面卡通訊的介面,可以用於捕獲封包、關閉網路介面卡及其他操作,讀者在呼叫pcap_open()函數時,需要指定要開啟的網路介面卡的名稱source,如果需要設定為混雜模式的話,需要設定flags引數為PCAP_OPENFLAG_PROMISCUOUS,此外snaplen引數用於設定捕獲封包的大小,read_timeout引數用於設定阻塞讀函數的超時時間,auth引數則用於指定遠端的IP和使用者名稱,errbuf引數用於儲存錯誤資訊。如果該函數返回空,則表示未成功開啟指定的網路介面卡。

另一個需要注意的函數是pcap_next_ex()該函數用於從開啟的指定網路介面卡中讀取下一個網路封包,通常情況下此函數需要配合pcap_open()一起使用,其原型宣告:

int pcap_next_ex(pcap_t *p, struct pcap_pkthdr **pkt_header, const u_char **pkt_data);

引數含義如下:

  • p:指向pcap_t型別結構體的指標,代表開啟的網路介面卡。
  • pkt_header:一個指向指向pcap_pkthdr型別的指標,該型別結構體包含有關當前封包的後設資料,例如時間戳、封包長度、捕獲到封包的網路介面卡介面等。
  • pkt_data:一個指向被捕獲的封包的指標。

它返回以下三種返回值之一:

  • 1:成功捕獲一個封包,pkt_headerpkt_data則指向相關資訊;
  • 0:在指定的時間內未捕獲到任何封包;
  • -1:發生錯誤,導致無法從網路介面卡讀取封包。此時可以在errbuf引數中查詢錯誤資訊。

使用pcap_next_ex()函數時,需要提供一個指向pcap_t型別結構體的指標p用於確定要從哪個網路介面卡讀取封包。如果讀取封包時成功,則將包的後設資料儲存在傳遞的pcap_pkthdr指標中,將指向捕獲封包的指標儲存在pkt_data指標中。如果在指定的時間內未捕獲到任何封包,則函數返回0。如果在讀取封包時發生任何錯誤,則函數返回-1,並在errbuf引數中提供有關錯誤的詳細資訊。

當讀者理解了上述兩個關鍵函數的作用則就可以實現動態抓包功能,如下程式碼中的MonitorAdapter函數則是抓包的實現,該函數需要傳入兩個引數,引數1是需要抓包的網路卡序列號,此處我們就使用7號,第二個參數列示需要解碼的封包型別,此處我們可以傳入ether等用於解包,當然該函數還沒有實現封包的解析功能,這些功能的實現需要繼續完善。

#include <iostream>
#include <winsock2.h>
#include <Windows.h>
#include <string>
#include <pcap.h>

#pragma comment(lib,"ws2_32.lib")
#pragma comment(lib, "packet.lib")
#pragma comment(lib, "wpcap.lib")

using namespace std;

// 選擇網路卡並根據不同引數解析封包
void MonitorAdapter(int nChoose, char *Type)
{
  pcap_if_t *adapters;
  char errbuf[PCAP_ERRBUF_SIZE];

  if (pcap_findalldevs_ex(PCAP_SRC_IF_STRING, NULL, &adapters, errbuf) != -1)
  {
    // 找到指定的網路卡
    for (int x = 0; x < nChoose - 1; ++x)
      adapters = adapters->next;

    // PCAP_OPENFLAG_PROMISCUOUS = 網路卡設定為混雜模式
    // 1000 => 1000毫秒如果讀不到資料直接返回超時
    pcap_t * handle = pcap_open(adapters->name, 65534, 1, PCAP_OPENFLAG_PROMISCUOUS, 0, 0);

    if (adapters == NULL)
      return;

    // printf("開始偵聽: % \n", adapters->description);
    pcap_pkthdr *Packet_Header;    // 封包頭
    const u_char * Packet_Data;    // 資料本身
    int retValue;
    while ((retValue = pcap_next_ex(handle, &Packet_Header, &Packet_Data)) >= 0)
    {
      if (retValue == 0)
        continue;

      // printf("偵聽長度: %d \n", Packet_Header->len);
      if (strcmp(Type, "ether") == 0)
      {
        PrintEtherHeader(Packet_Data);
      }
      if (strcmp(Type, "ip") == 0)
      {
        PrintIPHeader(Packet_Data);
      }
      if (strcmp(Type, "tcp") == 0)
      {
        PrintTCPHeader(Packet_Data);
      }
      if (strcmp(Type, "udp") == 0)
      {
        PrintUDPHeader(Packet_Data);
      }
      if (strcmp(Type, "icmp") == 0)
      {
        PrintICMPHeader(Packet_Data);
      }
      if (strcmp(Type, "http") == 0)
      {
        PrintHttpHeader(Packet_Data);
      }
      if (strcmp(Type, "arp") == 0)
      {
        PrintArpHeader(Packet_Data);
      }
    }
  }
}

int main(int argc, char* argv[])
{
  MonitorAdapter(7,"ether");
  system("pause");
  return 0;
}

當讀者有了上述程式碼框架,則下一步就是依次實現PrintEtherHeader,PrintIPHeader,PrintTCPHeader,PrintUDPHeader,PrintICMPHeader,PrintHttpHeader,PrintArpHeader等函數,這些函數接收原始封包Packet_Data型別,並將其轉換為對應格式的封包輸出給使用者,接下來我們將依次實現這些功能。

解碼乙太網層封包

乙太網封包是一種在乙太網上傳送的封包格式。它通常包括乙太網頭部和乙太網資料部分。以下是它的各個部分的介紹:

  • 乙太網頭部:包括目標MAC地址、源MAC地址以及型別/長度欄位。目標MAC地址和源MAC地址是6個位元組的二進位制數,分別表示封包的目標和來源。型別/長度欄位用於表示資料部分的長度或指定所使用的網路層協定。如果型別/長度欄位小於等於1500,則指示資料部分的長度;否則,它表示使用的協定型別。

  • 乙太網資料部分:包括所有的上層網路協定檔頭和資料。乙太網資料部分的長度通常大於46個位元組,並且最大長度為1500個位元組。

乙太網封包通常用於在區域網上進行通訊。使用乙太網幀作為封包格式,將封包傳送到這個網路上的所有裝置。然後,目標裝置根據目標MAC地址,接收和處理這些幀,其它裝置會忽略這些幀。在乙太網封包中,目標MAC地址指的是封包要傳送到的目標裝置的唯一MAC地址,而源MAC地址則指的是傳送此訊息的裝置的MAC地址。

// 解碼資料鏈路封包 資料鏈路層為二層,解碼時只需要封裝一層ether乙太網封包頭即可.
#define hcons(A) (((WORD)(A)&0xFF00)>>8) | (((WORD)(A)&0x00FF)<<8)

void PrintEtherHeader(const u_char * packetData)
{
  typedef struct ether_header
  {
    u_char ether_dhost[6];    // 目標地址
    u_char ether_shost[6];    // 源地址
    u_short ether_type;       // 乙太網型別
  } ether_header;

  struct ether_header * eth_protocol;
  eth_protocol = (struct ether_header *)packetData;

  u_short ether_type = ntohs(eth_protocol->ether_type);  // 乙太網型別
  u_char *ether_src = eth_protocol->ether_shost;         // 乙太網原始MAC地址
  u_char *ether_dst = eth_protocol->ether_dhost;         // 乙太網目標MAC地址

  printf("型別: 0x%x \t", ether_type);
  printf("原MAC地址: %02X:%02X:%02X:%02X:%02X:%02X \t",
    ether_src[0], ether_src[1], ether_src[2], ether_src[3], ether_src[4], ether_src[5]);
  printf("目標MAC地址: %02X:%02X:%02X:%02X:%02X:%02X \n",
    ether_dst[0], ether_dst[1], ether_dst[2], ether_dst[3], ether_dst[4], ether_dst[5]);
}

由於乙太網太過於底層,所以解析乙太網我們只能得到一些基本的網路卡資訊,如下圖所示;

解碼IP層封包

IP(Internet Protocol)封包是在TCP/IP(傳輸控制協定/網際網路協定)協定棧中的第三層。它通常包括IP頭部和資料部分兩部分。

IP頭部通常包括以下內容:

  • 版本號:表示所使用的IP協定版本號。
  • 頭部長度:表示整個IP頭部的長度。TCP/IP協定中的長度都以位元組(byte)為單位計數。
  • 總長度:表示整個IP封包的長度,包括頭部和有效負載部分。
  • TTL:生存時間,用於限制路由器轉發該封包的次數。
  • 協定:表示上層使用的協定型別。
  • 源IP地址:傳送該封包的裝置的IP地址。
  • 目標IP地址:傳送該封包的目標裝置的IP地址。
  • 資料部分則是上層協定中傳輸的實際資料。

IP封包是在網路層傳輸的,它的主要功能是為網際網路中的各種應用程式之間提供包傳輸服務。它使用IP地址來確定封包從哪裡發出,以及封包應該被路由到達目標裝置。

在接收到IP封包時,網路裝置首先檢查封包頭的目標IP地址,然後使用路由表來找到傳輸該封包所需的下一個節點(下一跳),並將封包傳遞到該節點。如果某個路由器無法將封包傳遞到下一個節點,則該封包將被丟棄。每個節點都會檢查封包的TTL值,並將其減少1。如果TTL值變為0,則封包會被丟棄,以防止封包在網路中迴圈。

// 解碼IP封包,IP層在資料鏈路層的下面, 解碼時需要+14偏移值, 跳過資料鏈路層。
void PrintIPHeader(const u_char * packetData)
{
  typedef struct ip_header
  {
    char version : 4;
    char headerlength : 4;
    char cTOS;
    unsigned short totla_length;
    unsigned short identification;
    unsigned short flags_offset;
    char time_to_live;
    char Protocol;
    unsigned short check_sum;
    unsigned int SrcAddr;
    unsigned int DstAddr;
  }ip_header;

  struct ip_header *ip_protocol;

  // +14 跳過資料鏈路層
  ip_protocol = (struct ip_header *)(packetData + 14);
  SOCKADDR_IN Src_Addr, Dst_Addr = { 0 };

  u_short check_sum = ntohs(ip_protocol->check_sum);
  int ttl = ip_protocol->time_to_live;
  int proto = ip_protocol->Protocol;

  Src_Addr.sin_addr.s_addr = ip_protocol->SrcAddr;
  Dst_Addr.sin_addr.s_addr = ip_protocol->DstAddr;

  printf("源地址: %15s --> ", inet_ntoa(Src_Addr.sin_addr));
  printf("目標地址: %15s --> ", inet_ntoa(Dst_Addr.sin_addr));

  printf("校驗和: %5X --> TTL: %4d --> 協定型別: ", check_sum, ttl);
  switch (ip_protocol->Protocol)
  {
  case 1: printf("ICMP \n"); break;
  case 2: printf("IGMP \n"); break;
  case 6: printf("TCP \n");  break;
  case 17: printf("UDP \n"); break;
  case 89: printf("OSPF \n"); break;
  default: printf("None \n"); break;
  }
}

針對IP層封包的解析可能會較為複雜,因為IP協定上方可以包含ICMP,IGMP,TCP,UDP,OSPF等協定,在執行程式後讀者會看到如下圖所示的具體資訊;

解碼TCP層封包

TCP(Transmission Control Protocol)層封包是在TCP/IP(傳輸控制協定/網際網路協定)協定棧中的第四層。它包括TCP頭部和資料部分兩個部分。

TCP頭部通常包括以下內容:

  • 源埠號:表示傳送該封包的應用程式的埠號。
  • 目的埠號:表示接收該封包的應用程式的埠號。
  • 序列號:用於將多個封包排序,確保它們在正確的順序中到達接收方應用程式。
  • 確認號:用於確認接收方已經成功收到序列號或最後一個被成功接收的封包。
  • ACK和SYN標誌:這些是TCP頭部中的標誌位,用於控制TCP連線的建立和關閉。
  • 視窗大小:用於控制資料流傳送的速率,並確保不會傳送太多的封包,導致網路擁塞。
  • 校驗和:用於校驗TCP頭部和資料部分是否被損壞或篡改。
  • 資料部分則是上層應用程式傳遞到TCP層的應用資料。

TCP是一個面向連線的協定,因此在傳送資料之前,TCP會先在傳送方和接收方之間建立連線。該連線建立的過程包括三次握手(three-way handshake)過程,分別是使用者端發起連線請求、伺服器發回確認、使用者端再次傳送確認。完成連線後,TCP協定根據確認號和序列號來控制封包的傳輸次序和有效性(如ACK報文的確認和重傳訊息),以提供高效的資料傳輸服務。

當TCP封包到達目標裝置後,TCP層將在接收方重新組裝TCP資料,將TCP報文分割成應用層可用的更小的資料塊,並將其傳送到目標應用程式。如果傳送的TCP協定封包未被正確地接收,則TCP協定將重新嘗試傳送丟失的封包,以確保資料的完整性和正確性。

// 解碼TCP封包,需要先加14跳過資料鏈路層, 然後再加20跳過IP層。
void PrintTCPHeader(const unsigned char * packetData)
{
  typedef struct tcp_header
  {
    short SourPort;                 // 源埠號16bit
    short DestPort;                 // 目的埠號16bit
    unsigned int SequNum;           // 序列號32bit
    unsigned int AcknowledgeNum;    // 確認號32bit
    unsigned char reserved : 4, offset : 4; // 預留偏移

    unsigned char  flags;               // 標誌 

    short WindowSize;               // 視窗大小16bit
    short CheckSum;                 // 檢驗和16bit
    short surgentPointer;           // 緊急資料偏移量16bit
  }tcp_header;

  struct tcp_header *tcp_protocol;
  // +14 跳過資料鏈路層 +20 跳過IP層
  tcp_protocol = (struct tcp_header *)(packetData + 14 + 20);

  u_short sport = ntohs(tcp_protocol->SourPort);
  u_short dport = ntohs(tcp_protocol->DestPort);
  int window = tcp_protocol->WindowSize;
  int flags = tcp_protocol->flags;

  printf("源埠: %6d --> 目標埠: %6d --> 視窗大小: %7d --> 標誌: (%d)",
    sport, dport, window, flags);

  if (flags & 0x08) printf("PSH 資料傳輸\n");
  else if (flags & 0x10) printf("ACK 響應\n");
  else if (flags & 0x02) printf("SYN 建立連線\n");
  else if (flags & 0x20) printf("URG \n");
  else if (flags & 0x01) printf("FIN 關閉連線\n");
  else if (flags & 0x04) printf("RST 連線重置\n");
  else printf("None 未知\n");
}

針對TCP的解析也較為複雜,這是因為TCP協定存在多種狀態值,如PSH、ACK、SYN、URG、FINRST這些都是TCP報文段中用於標識不同資訊或狀態的標誌位。這些TCP標誌位的含義如下:

  • PSH(Push):該標誌位表示接收端應用程式應立即從接收快取中讀取資料。通常在傳送方需要儘快將所有資料傳送給接收方時使用。
  • ACK(Acknowledgment):該標誌位表示應答。用於確認已經成功接收到別的TCP包。在TCP連線建立完成後,所有TCP報文段都必須設定ACK標誌位。
  • SYN(Synchronous):該標誌位用於建立TCP連線。指示請求建立一個連線,同時序列號以亂數ISN開始。傳送SYN報文的一端會進入SYN_SENT狀態。
  • URG(Urgent):該標誌位表示緊急指標有效。它用於告知接收端在此報文段中存在緊急資料,緊急資料應該立即送達接收端的應用層。
  • FIN(Finish):此標誌用於終止TCP連線。FIN標誌位被置位的一端表明它已經傳送完所有資料並要求釋放連線。
  • RST(Reset):該標誌用於重置TCP連線。當TCP連線嘗試建立失敗,或一個已關閉的通訊端收到資料,都會傳送帶RST標誌的封包。

這些標誌位的設定和使用可以幫助TCP在應用層和網路層之間進行可靠的通訊,保證資料的傳輸和連線的建立以及關閉可以正確完成,我們工具同樣可以解析這些不同的標誌位情況,如下圖所示;

解碼UDP層封包

UDP(User Datagram Protocol)層封包是在TCP/IP(傳輸控制協定/網際網路協定)協定棧中的第四層。它比TCP更簡單,不保證封包的位置和有效性,也不進行連線的建立和維護。UDP封包僅包含UDP頭部和資料部分。

UDP頭部包括以下內容:

  • 源埠號:表示發起該封包的應用程式的埠號。
  • 目的埠號:表示接收該封包的應用程式的埠號。
  • 資料長度:表示封包中包含的資料長度。
  • 校驗和:用於校驗UDP頭部和資料部分是否被損壞或篡改。
  • 資料部分和TCP層封包類似,是上層應用程式傳遞到UDP層的應用資料。

UDP協定的優點是傳輸開銷小,速度快,延遲低,因為它不進行高負載的錯誤檢查,也不進行連線建立和維護。但這也意味著封包傳輸不可靠,不保證資料傳輸的完整性和正確性。如果未能正確地接收UDP封包,則不會嘗試重新傳送丟失的封包。UDP通常用於需要快速、簡單、低延遲的應用程式,例如線上遊戲、視訊和音訊串流媒體等。

// UDP層與TCP層如出一轍,僅僅只是在結構體的定義解包是有少許的不同而已.
void PrintUDPHeader(const unsigned char * packetData)
{
  typedef struct udp_header
  {
    uint32_t sport;   // 源埠
    uint32_t dport;   // 目標埠
    uint8_t zero;     // 保留位
    uint8_t proto;    // 協定標識
    uint16_t datalen; // UDP資料長度
  }udp_header;

  struct udp_header *udp_protocol;
  // +14 跳過資料鏈路層 +20 跳過IP層
  udp_protocol = (struct udp_header *)(packetData + 14 + 20);

  u_short sport = ntohs(udp_protocol->sport);
  u_short dport = ntohs(udp_protocol->dport);
  u_short datalen = ntohs(udp_protocol->datalen);

  printf("源埠: %5d --> 目標埠: %5d --> 大小: %5d \n", sport, dport, datalen);
}

針對UDP協定的解析就變得很簡單了,因為UDP是一種無狀態協定所以只能得到源埠與目標埠,解析效果如下圖所示;

解碼ICMP層封包

ICMP(Internet Control Message Protocol)層封包是在TCP/IP協定棧中的第三層。它是一種控制協定,用於網路通訊中的錯誤報告和網路狀態查詢。ICMP封包通常不攜帶應用資料或有效載荷。

ICMP封包通常包括以下型別的控制資訊:

  • Echo Request/Reply: 用於網路連通性測試,例如ping命令(12/0)
  • Destination unreachable: 該型別的ICMP封包用於向傳送者傳遞對目標無法到達的訊息(3/0、3/1、3/2、3/3、3/4、3/5、3/6、3/7、3/8、3/9、3/10)
  • Redirect: 用於告知傳送方使用新的路由器來傳送資料(5/0、5/1、5/2)
  • Time exceeded: 用於向傳送方報告基於TTL值無法到達目的地,表示躍點數超過了最大限制(11/0、11/1)
  • Parameter problem: 用於向傳送者報告轉發器無法處理IP封包中的某些欄位(12/0)

ICMP封包還用於其他用途,例如Multicast Listener Discovery(MLD)和Neighbor Discovery Protocol(NDP),用於組播和IPv6網路通訊中。

ICMP資料包通常由作業系統或網路裝置自動生成,並直接傳送給作業系統或網路裝置。然後,它們可以通過網路分析工具進行檢測和診斷,以確定網路中的錯誤或故障。

// 解碼ICMP封包,在解包是需要同樣需要跳過資料鏈路層和IP層, 然後再根據ICMP型別號解析, 常用的型別號為`type 8`它代表著傳送和接收封包的時間戳。
void PrintICMPHeader(const unsigned char * packetData)
{
  typedef struct icmp_header {
    uint8_t type;        // ICMP型別
    uint8_t code;        // 程式碼
    uint16_t checksum;   // 校驗和
    uint16_t identification; // 標識
    uint16_t sequence;       // 序列號
    uint32_t init_time;      // 發起時間戳
    uint16_t recv_time;      // 接受時間戳
    uint16_t send_time;      // 傳輸時間戳
  }icmp_header;

  struct icmp_header *icmp_protocol;

  // +14 跳過資料鏈路層 +20 跳過IP層
  icmp_protocol = (struct icmp_header *)(packetData + 14 + 20);

  int type = icmp_protocol->type;
  int init_time = icmp_protocol->init_time;
  int send_time = icmp_protocol->send_time;
  int recv_time = icmp_protocol->recv_time;
  if (type == 8)
  {
    printf("發起時間戳: %d --> 傳輸時間戳: %d --> 接收時間戳: %d 方向: ",
      init_time, send_time, recv_time);

    switch (type)
    {
    case 0: printf("回顯應答報文 \n"); break;
    case 8: printf("回顯請求報文 \n"); break;
    default:break;
    }
  }
}

針對ICMP協定的解析也很簡單在抓包時我們同樣只能得到一些基本的資訊,例如傳送時間戳,傳輸時間戳,接收時間戳,以及報文方向等,這裡的方向有兩種一種是0代表回顯應答,而8則代表回顯請求,具體輸出效果圖如下所示;

解碼HTTP層封包

HTTP(Hypertext Transfer Protocol)層封包是在TCP/IP協定棧中的第七層,它主要用於Web應用程式中的客戶機和伺服器之間的資料傳輸。HTTP封包通常包括HTTP頭部和資料部分兩個部分。

HTTP頭部通常包括以下內容:

  • 請求行:用於描述客戶機發起的請求。
  • 響應行:用於描述伺服器返回的響應。
  • 頭部欄位:用於向請求或響應新增額外的後設資料資訊,例如HTTP版本號、日期、內容型別等。
  • Cookie:用於在使用者端和伺服器之間來儲存狀態資訊。
  • Cache-Control:用於使用者端和伺服器之間控制快取的行為。
  • 資料部分是包含在HTTP請求或響應中的應用資料。

HTTP協定的工作方式是使用者端向伺服器傳送HTTP請求,伺服器通過HTTP響應返回請求結果。HTTP請求通常使用HTTP方法,如GET、POST、PUT、DELETE等,控制HTTP操作的型別和行為。HTTP響應通常包含HTTP狀態碼,如200、404、500等,以指示使用者端請求結果的狀態。

在實際的網路通訊中,HTTP層封包的格式和內容通常由應用程式或網路裝置生成和分析,例如Web瀏覽器和Web伺服器。

// 解碼HTTP封包,需要跳過資料鏈路層, IP層以及TCP層, 最後即可得到HTTP封包協定頭。
void PrintHttpHeader(const unsigned char * packetData)
{
  typedef struct tcp_port
  {
    unsigned short sport;
    unsigned short dport;
  }tcp_port;

  typedef struct http_header
  {
    char url[512];
  }http_header;

  struct tcp_port *tcp_protocol;
  struct http_header *http_protocol;

  tcp_protocol = (struct tcp_port *)(packetData + 14 + 20);
  int tcp_sport = ntohs(tcp_protocol->sport);
  int tcp_dport = ntohs(tcp_protocol->dport);

  if (tcp_sport == 80 || tcp_dport == 80)
  {
    // +14 跳過MAC層 +20 跳過IP層 +20 跳過TCP層
    http_protocol = (struct http_header *)(packetData + 14 + 20 + 20);
    printf("%s \n", http_protocol->url);
  }
}

針對HTTP協定的解析同樣可以,但由於HTTP協定已經用的很少了所以這段程式碼也只能演示,在實戰中一般會使用HTTPS,如下則是一個HTTP存取時捕獲的封包;

解碼ARP層封包

ARP(Address Resolution Protocol)層封包是在TCP/IP協定棧中的第二層。ARP協定主要用於將網路層地址(如IP地址)對映到資料鏈路層地址(如MAC地址)。

ARP封包通常包括以下內容:

  • ARP請求或響應:ARP請求用於獲取與IP地址關聯的MAC地址,而ARP響應用於提供目標MAC地址。
  • 傳送者的MAC地址:傳送ARP請求或響應的裝置的MAC地址。
  • 傳送者的IP地址:傳送ARP請求或響應的裝置的IP地址。
  • 目標的MAC地址:目標裝置的MAC地址。
  • 目標的IP地址:目標裝置的IP地址。

ARP協定工作的過程如下:

  • 傳送者主機傳送一個ARP請求,包含目標IP地址。
  • 網路中的所有裝置都收到該ARP請求。
  • 如果有裝置的IP地址與ARP請求中的目標IP地址匹配,該裝置會回覆ARP響應,包含自己的MAC地址。
  • 傳送者主機使用響應中的MAC地址來與該裝置通訊。

ARP協定的工作主要是在本地網路中實現地址對映,主要包括確定哪個裝置的MAC地址與特定的IP地址關聯,以及應答IP地址轉化成相應的MAC地址的對映請求。ARP通常用於乙太網和WiFi網路中,以實現區域網內的裝置通訊。

// 解碼ARP封包
void PrintArpHeader(const unsigned char * packetData)
{
  typedef struct arp_header
  {
    uint16_t arp_hardware_type;
    uint16_t arp_protocol_type;
    uint8_t arp_hardware_length;
    uint8_t arp_protocol_length;
    uint16_t arp_operation_code;
    uint8_t arp_source_ethernet_address[6];
    uint8_t arp_source_ip_address[4];
    uint8_t arp_destination_ethernet_address[6];
    uint8_t arp_destination_ip_address[4];
  }arp_header;

  struct arp_header *arp_protocol;

  arp_protocol = (struct arp_header *)(packetData + 14);

  u_short hardware_type = ntohs(arp_protocol->arp_hardware_type);
  u_short protocol_type = ntohs(arp_protocol->arp_protocol_type);
  int arp_hardware_length = arp_protocol->arp_hardware_length;
  int arp_protocol_length = arp_protocol->arp_protocol_length;
  u_short operation_code = ntohs(arp_protocol->arp_operation_code);

  // 判讀是否為ARP請求包
  if (arp_hardware_length == 6 && arp_protocol_length == 4)
  {
    printf("原MAC地址: ");
    for (int x = 0; x < 6; x++)
      printf("%x:", arp_protocol->arp_source_ethernet_address[x]);
    printf(" --> ");

    printf("目標MAC地址: ");
    for (int x = 0; x < 6; x++)
      printf("%x:", arp_protocol->arp_destination_ethernet_address[x]);
    printf(" --> ");

    switch (operation_code)
    {
    case 1: printf("ARP 請求 \n"); break;
    case 2: printf("ARP 應答 \n"); break;
    case 3: printf("RARP 請求 \n"); break;
    case 4: printf("RARP 應答 \n"); break;
    default: break;
    }
  }
}

解析ARP協定同樣可以實現,ARP協定同樣有多個狀態,一般1-2代表請求與應答,3-4代表RARP反向請求與應答,ARP協定由於觸發週期短所以讀者可能很少捕捉到這類資料,如下圖時讀者捕捉到的一條完整的ARP協定狀態;

本文作者: 王瑞
本文連結: https://www.lyshark.com/post/526b8a6.html
版權宣告: 本部落格所有文章除特別宣告外,均採用 BY-NC-SA 許可協定。轉載請註明出處!