18.2 使用NPCAP庫抓取資料包

lyshark發表於2023-10-26

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 許可協議。轉載請註明出處!

相關文章