18.1 Socket 原生通訊端抓包

2023-10-25 12:01:40

原生通訊端抓包的實現原理依賴於Windows系統中提供的ioctlsocket函數,該函數可將指定的網路卡設定為混雜模式,網路卡混雜模式(Promiscuous Mode)是常用於計算機網路抓包的一種模式,也稱為監聽模式。在混雜模式下,網路卡可以收到經過主機的所有封包,而非只接收它所對應的MAC地址的封包。

一般情況下,網路卡會根據MAC地址過濾封包,只有MAC地址與網路卡所對應的裝置的通訊封包才會被接收和處理,其他封包則會被忽略。但在混雜模式下,網路卡會接收經過它所連線的網路中所有的封包,這些封包可以是面向其他裝置的通訊封包、廣播封包或多播封包等。

混雜模式可以通過軟體驅動程式或網路卡硬體實現。啟用混雜模式的主要用途之一是網路抓包分析,使用混雜模式可以捕獲網路中所有的封包,且不僅僅是它所連線的裝置的通訊封包。因此,可以完整獲取網路中的通訊內容,便於進行網路監控、安全風險感知、漏洞檢測等操作。

Windows系統下,開啟混雜模式可以使用ioctlsocket()函數,該函數原型定義如下:

int ioctlsocket (
   SOCKET s,        //要操作的通訊端
   long cmd,        //操作程式碼
   u_long *argp     //指向操作引數的指標
);

其中,引數說明如下:

  • s: 要執行I/O控制操作的通訊端。
  • cmd: 操作程式碼,用於控制對通訊端的特定操作。
  • argp: 與特定請求程式碼相關聯的引數指標。此引數的具體含義取決於請求程式碼。

在該函數中,引數cmd指定了I/O控制操作程式碼,是一個整數值,用於控制對通訊端的特定操作。argp是一個指向特定請求程式碼相關聯的引數的指標,它的具體含義將取決於請求程式碼。函數返回值為int型別,表示函數執行結果的狀態碼,若函數執行成功,則其返回值為0,否則返回一個錯誤程式碼,並將錯誤原因存入errno變數中。

要實現抓包前提是需要先選中繫結到那個網路卡,如下InitAndSelectNetworkRawSocket函數則是實現繫結通訊端到特定網路卡的實現流程,在程式碼中首先初始化並使用gethostname函數獲取到當前主機的主機名,主機IP地址等基本資訊,接著通過迴圈的方式將自身網路卡資訊追加到g_HostIp全域性結構體內進行儲存,通過使用一個互動式選擇選單讓使用者可以選中需要繫結的網路卡名稱,當用戶選中後則下一步是繫結通訊端,並通過呼叫ioctlsocket函數將網路卡設定為混雜模式,至此網路卡的繫結工作就算結束了,當讀者需要操作時只需要對全域性變數進行操作即可,而選擇函數僅僅只是獲取到網路卡資訊而已並沒有實際的作用。

#include <iostream>
#include <WinSock2.h>
#include <ws2tcpip.h>
#include <mstcpip.h>

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

// 全域性結構
typedef struct
{
  int iLen;
  char szIPArray[10][50];
}HOSTIP;

// 全域性變數
SOCKET g_RawSocket = 0;
HOSTIP g_HostIp;

// -------------------------------------------------------
// 初始化與選擇通訊端
// -------------------------------------------------------
BOOL InitAndSelectNetworkRawSocket()
{
  // 設定通訊端版本
  WSADATA wsaData = { 0 };
  if (0 != WSAStartup(MAKEWORD(2, 2), &wsaData))
  {
    return FALSE;
  }
  // 建立原始通訊端
  // Windows無法抓取RawSocket MAC層的封包,只能抓到IP層及以上的封包
  g_RawSocket = socket(AF_INET, SOCK_RAW, IPPROTO_IP);
  // g_RawSocket = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
  if (INVALID_SOCKET == g_RawSocket)
  {
    WSACleanup();
    return FALSE;
  }

  // 繫結到介面 獲取本機名
  char szHostName[MAX_PATH] = { 0 };
  if (SOCKET_ERROR == ::gethostname(szHostName, MAX_PATH))
  {
    closesocket(g_RawSocket);
    WSACleanup();
    return FALSE;
  }

  // 根據本機名獲取本機IP地址
  hostent* lpHostent = ::gethostbyname(szHostName);
  if (NULL == lpHostent)
  {
    closesocket(g_RawSocket);
    WSACleanup();
    return FALSE;
  }

  // IP地址轉換並儲存IP地址
  g_HostIp.iLen = 0;
  strcpy(g_HostIp.szIPArray[g_HostIp.iLen], "127.0.0.1");
  g_HostIp.iLen++;
  char* lpszHostIP = NULL;

  while (NULL != (lpHostent->h_addr_list[(g_HostIp.iLen - 1)]))
  {
    lpszHostIP = inet_ntoa(*(in_addr*)lpHostent->h_addr_list[(g_HostIp.iLen - 1)]);
    strcpy(g_HostIp.szIPArray[g_HostIp.iLen], lpszHostIP);
    g_HostIp.iLen++;
  }

  // 選擇IP地址對應的網路卡來嗅探
  printf("選擇偵聽網路卡 \n\n");
  for (int i = 0; i < g_HostIp.iLen; i++)
  {
    printf("\t [*] 序號: %d \t IP地址: %s \n", i, g_HostIp.szIPArray[i]);
  }

  printf("\n 選擇網路卡序號: ");
  int iChoose = 0;
  scanf("%d", &iChoose);

  // 如果選擇超出範圍則直接終止
  if ((0 > iChoose) || (iChoose >= g_HostIp.iLen))
  {
    exit(0);
  }
  if ((0 <= iChoose) && (iChoose < g_HostIp.iLen))
  {
    lpszHostIP = g_HostIp.szIPArray[iChoose];
  }

  // 構造地址結構
  sockaddr_in SockAddr = { 0 };
  RtlZeroMemory(&SockAddr, sizeof(sockaddr_in));
  SockAddr.sin_addr.S_un.S_addr = inet_addr(lpszHostIP);
  SockAddr.sin_family = AF_INET;
  SockAddr.sin_port = htons(0);

  // 繫結通訊端
  if (SOCKET_ERROR == bind(g_RawSocket, (sockaddr*)(&SockAddr), sizeof(sockaddr_in)))
  {
    closesocket(g_RawSocket);
    WSACleanup();
    return FALSE;
  }

  // 設定混雜模式 抓取所有經過網路卡的封包
  DWORD dwSetVal = 1;
  if (SOCKET_ERROR == ioctlsocket(g_RawSocket, SIO_RCVALL, &dwSetVal))
  {
    closesocket(g_RawSocket);
    WSACleanup();
    return FALSE;
  }
  return TRUE;
}

int main(int argc, char *argv[])
{
  // 選擇網路卡並設定網路為非阻塞模式
  BOOL SelectFlag = InitAndSelectNetworkRawSocket();
  if (SelectFlag == TRUE)
  {
    printf("[*] 網路卡已被選中 通訊端ID = %d | 通訊端IP = %s \n", g_RawSocket,g_HostIp.szIPArray);
  }

  system("pause");
  return 0;
}

讀者可自行編譯並以管理員身份執行上述程式碼片段,當讀者執行後會看到如下圖所示的程式碼片段,此處筆者就選擇三號網路卡進行繫結操作,當繫結後此時通訊端ID對應的則是特定的網路卡,後續的操作均可針對此通訊端ID進行,如下圖所示;

當讀者有了設定混雜模式的功能則下一步就是抓包了,抓包的實現很簡單,只需要在開啟了非阻塞混雜模式的網路卡上使用recvfrom函數迴圈進行監聽即可,當有封包產生時則直接輸出iRecvBytes中所儲存的資料即可,這段程式碼的實現如下所示;

int main(int argc, char *argv[])
{
  // 選擇網路卡並設定網路為非阻塞模式
  BOOL init_flag = InitAndSelectNetworkRawSocket();
  if (init_flag == FALSE)
  {
    return 0;
  }

  sockaddr_in RecvAddr = { 0 };
  int iRecvBytes = 0;
  int iRecvAddrLen = sizeof(sockaddr_in);

  // 定義緩衝區長度
  DWORD dwBufSize = 12000;
  BYTE* lpRecvBuf = new BYTE[dwBufSize];

  // 迴圈接收接收
  while (1)
  {
    RtlZeroMemory(&RecvAddr, iRecvAddrLen);
    iRecvBytes = recvfrom(g_RawSocket, (char*)lpRecvBuf, dwBufSize, 0, (sockaddr*)(&RecvAddr), &iRecvAddrLen);
    if (0 < iRecvBytes)
    {
      // 接收封包並輸出
      printf("[接收封包] %s \n", lpRecvBuf);
    }
  }

  // 釋放記憶體
  delete[]lpRecvBuf;
  lpRecvBuf = NULL;

  // 關閉通訊端
  Sleep(500);
  closesocket(g_RawSocket);
  WSACleanup();
  return 0;
}

當讀者選擇網路卡後即可看到如下所示的輸出結果,這些資料則是經過網路卡192.168.9.125的所有資料,由於此處沒有解碼和區分封包型別所以顯示出的字串並沒有任何意義,如下圖所示;

接下來我們就需要根據不同的封包型別對這些資料進行解包操作,在解包之前我們需要先來定義幾個關鍵的封包結構體,如下程式碼中ether_header代表的是乙太網包頭結構,該結構佔用14個位元組的儲存空間,arp_header則是ARP結構體,該結構體佔用28個位元組,ARK結構中還存在一個ARK報文結構,該結構佔用42位元組的記憶體長度,接著分別頂一個ipv4_headeripv6_headertcp_headerudp_header等結構體,這些結構體的完整定義如下所示;

#pragma pack(1)

/*乙太網幀頭格式結構體 14個位元組*/
typedef struct ether_header
{
    unsigned char ether_dhost[6];  // 目的MAC地址
    unsigned char ether_shost[6];  // 源MAC地址
    unsigned short ether_type;     // eh_type 的值需要考察上一層的協定,如果為ip則為0x0800
}ETHERHEADER, * PETHERHEADER;

/*以ARP欄位結構體 28個位元組*/
typedef struct arp_header
{
    unsigned short arp_hrd;
    unsigned short arp_pro;
    unsigned char arp_hln;
    unsigned char arp_pln;
    unsigned short arp_op;
    unsigned char arp_sourha[6];
    unsigned long arp_sourpa;
    unsigned char arp_destha[6];
    unsigned long arp_destpa;
}ARPHEADER, * PARPHEADER;

/*ARP報文結構體 42個位元組*/
typedef struct arp_packet
{
    ETHERHEADER etherHeader;
    ARPHEADER   arpHeader;
}ARPPACKET, * PARPPACKET;

/*IPv4報頭結構體 20個位元組*/
typedef struct ipv4_header
{
    unsigned char ipv4_ver_hl;        // Version(4 bits) + Internet Header Length(4 bits)長度按4位元組對齊
    unsigned char ipv4_stype;         // 服務型別
    unsigned short ipv4_plen;         // 總長度(包含IP資料頭,TCP資料頭以及資料)
    unsigned short ipv4_pidentify;    // ID定義單獨IP
    unsigned short ipv4_flag_offset;  // 標誌位偏移量
    unsigned char ipv4_ttl;           // 生存時間
    unsigned char ipv4_pro;           // 協定型別
    unsigned short ipv4_crc;          // 校驗和
    unsigned long  ipv4_sourpa;       // 源IP地址
    unsigned long  ipv4_destpa;       // 目的IP地址
}IPV4HEADER, * PIPV4HEADER;

/*IPv6報頭結構體 40個位元組*/
typedef struct ipv6_header
{
    unsigned char ipv6_ver_hl;
    unsigned char ipv6_priority;
    unsigned short ipv6_lable;
    unsigned short ipv6_plen;
    unsigned char  ipv6_nextheader;
    unsigned char  ipv6_limits;
    unsigned char ipv6_sourpa[16];
    unsigned char ipv6_destpa[16];
}IPV6HEADER, * PIPV6HEADER;

/*TCP報頭結構體 20個位元組*/
typedef struct tcp_header
{
    unsigned short tcp_sourport;     // 源埠
    unsigned short tcp_destport;     // 目的埠
    unsigned long  tcp_seqnu;        // 序列號
    unsigned long  tcp_acknu;        // 確認號
    unsigned char  tcp_hlen;         // 4位元首部長度
    unsigned char  tcp_reserved;     // 標誌位
    unsigned short tcp_window;       // 視窗大小
    unsigned short tcp_chksum;       // 檢驗和
    unsigned short tcp_urgpoint;     // 緊急指標
}TCPHEADER, * PTCPHEADER;

/*UDP報頭結構體 8個位元組*/
typedef struct udp_header
{
    unsigned short udp_sourport;   // 源埠 
    unsigned short udp_destport;   // 目的埠
    unsigned short udp_hlen;       // 長度
    unsigned short udp_crc;        // 校驗和
}UDPHEADER, * PUDPHEADER;
#pragma pack()

當有了結構體的定義部分,則實現對封包的解析只需要判斷封包的型別並使用不同的結構體對封包進行解包列印即可,如下是實現封包解析的完整程式碼,在程式碼中分別實現了幾個核心函數,其中printData函數可以實現對特定記憶體資料的十六進位制格式輸出方便檢查輸出效果,函數AnalyseRecvPacket_All用於解析除去TCP/UDP格式的其他封包,AnalyseRecvPacket_TCP用於解析TCP資料,AnalyseRecvPacket_UDP用於解析UDP資料,在主函數中通過使用ip->ipv4_pro判斷封包的具體型別,並根據型別的不同依次呼叫不同的函數實現封包解析。

// 輸出封包
void PrintData(BYTE* lpBuf, int iLen, int iPrintType)
{
  // 16進位制
  if (0 == iPrintType)
  {
    for (int i = 0; i < iLen; i++)
    {
      if ((0 == (i % 8)) && (0 != i))
      {
        printf("  ");
      }
      if ((0 == (i % 16)) && (0 != i))
      {
        printf("\n");
      }
      printf("%02x ", lpBuf[i]);

    }
    printf("\n");
  }
  // ASCII編碼
  else if (1 == iPrintType)
  {
    for (int i = 0; i < iLen; i++)
    {
      printf("%c", lpBuf[i]);
    }
    printf("\n");
  }
}

// 解析所有其他封包
void AnalyseRecvPacket_All(BYTE* lpBuf)
{
  struct sockaddr_in saddr, daddr;
  PIPV4HEADER ip = (PIPV4HEADER)lpBuf;
  saddr.sin_addr.s_addr = ip->ipv4_sourpa;
  daddr.sin_addr.s_addr = ip->ipv4_destpa;

  printf("From:%s --> ", inet_ntoa(saddr.sin_addr));
  printf("To:%s\n", inet_ntoa(daddr.sin_addr));
}

// 解析TCP封包
void AnalyseRecvPacket_TCP(BYTE* lpBuf)
{
  struct sockaddr_in saddr, daddr;
  PIPV4HEADER ip = (PIPV4HEADER)lpBuf;
  PTCPHEADER tcp = (PTCPHEADER)(lpBuf + (ip->ipv4_ver_hl & 0x0F) * 4);
  int hlen = (ip->ipv4_ver_hl & 0x0F) * 4 + tcp->tcp_hlen * 4;

  // 這裡要將網路位元組序轉換為本地位元組序
  int dlen = ntohs(ip->ipv4_plen) - hlen;
  saddr.sin_addr.s_addr = ip->ipv4_sourpa;
  daddr.sin_addr.s_addr = ip->ipv4_destpa;

  printf("From:%s:%d --> ", inet_ntoa(saddr.sin_addr), ntohs(tcp->tcp_sourport));
  printf("To:%s:%d  ", inet_ntoa(daddr.sin_addr), ntohs(tcp->tcp_destport));
  printf("ack:%u  syn:%u length=%d\n", tcp->tcp_acknu, tcp->tcp_seqnu, dlen);

  PrintData((lpBuf + hlen), dlen, 0);
}

// 解析UDP封包
void AnalyseRecvPacket_UDP(BYTE* lpBuf)
{
  struct sockaddr_in saddr, daddr;
  PIPV4HEADER ip = (PIPV4HEADER)lpBuf;
  PUDPHEADER udp = (PUDPHEADER)(lpBuf + (ip->ipv4_ver_hl & 0x0F) * 4);
  int hlen = (int)((ip->ipv4_ver_hl & 0x0F) * 4 + sizeof(UDPHEADER));
  int dlen = (int)(ntohs(udp->udp_hlen) - 8);

  //  int dlen = (int)(udp->udp_hlen - 8);
  saddr.sin_addr.s_addr = ip->ipv4_sourpa;
  daddr.sin_addr.s_addr = ip->ipv4_destpa;
  printf("Protocol:UDP  ");
  printf("From:%s:%d -->", inet_ntoa(saddr.sin_addr), ntohs(udp->udp_sourport));
  printf("To:%s:%d\n", inet_ntoa(daddr.sin_addr), ntohs(udp->udp_destport));

  PrintData((lpBuf + hlen), dlen, 0);
}

int main(int argc, char* argv[])
{
  // 選擇網路卡,並設定網路為非阻塞模式
  InitAndSelectNetworkRawSocket();

  sockaddr_in RecvAddr = { 0 };
  int iRecvBytes = 0;
  int iRecvAddrLen = sizeof(sockaddr_in);
  DWORD dwBufSize = 12000;
  BYTE* lpRecvBuf = new BYTE[dwBufSize];

  // 迴圈接收接收
  while (1)
  {
    RtlZeroMemory(&RecvAddr, iRecvAddrLen);
    iRecvBytes = recvfrom(g_RawSocket, (char*)lpRecvBuf, dwBufSize, 0, (sockaddr*)(&RecvAddr), &iRecvAddrLen);
    if (0 < iRecvBytes)
    {
      // 接收封包解碼輸出
      // 分析IP包的協定型別
      PIPV4HEADER ip = (PIPV4HEADER)lpRecvBuf;
      switch (ip->ipv4_pro)
      {
      case IPPROTO_ICMP:
      {
                // 分析ICMP
        printf("[ICMP]\n");
        AnalyseRecvPacket_All(lpRecvBuf);
        break;
      }
      case IPPROTO_IGMP:
      {
                // 分析IGMP
        printf("[IGMP]\n");
        AnalyseRecvPacket_All(lpRecvBuf);
        break;
      }
      case IPPROTO_TCP:
      {
        // 分析tcp協定
        printf("[TCP]\n");
        AnalyseRecvPacket_TCP(lpRecvBuf);
        break;
      }
      case IPPROTO_UDP:
      {
        // 分析udp協定
        printf("[UDP]\n");
        AnalyseRecvPacket_UDP(lpRecvBuf);
        break;
      }
      default:
      {
                // 其他封包
        printf("[OTHER IP]\n");
        AnalyseRecvPacket_All(lpRecvBuf);
        break;
      }
      }
    }
  }

  // 釋放記憶體
  delete[]lpRecvBuf;
  lpRecvBuf = NULL;

  // 關閉通訊端
  Sleep(500);
  closesocket(g_RawSocket);
  WSACleanup();
  return 0;
}

讀者可自行編譯並執行上述程式碼片段,當程式執行後可自行選擇希望監控的網路卡,當程式中檢測到TCP封包後會輸出如下圖所示的提示資訊,在圖中我們可以清晰的看出封包的流向資訊,以及封包長度封包內的資料等;

當讀者通過使用Ping命令探測目標主機時,此時同樣可以抓取到ICMP相關的資料流,只是在資料解析時並沒有太規範導致只能看到簡單的流向,當然讀者也可以自行完善這段程式碼,讓其能夠解析更多引數。

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