運用Npcap庫實現SYN半開放掃描

lyshark發表於2024-08-10

Npcap 是一款高效能的網路捕獲和資料包分析庫,作為 Nmap 專案的一部分,Npcap 可用於捕獲、傳送和分析網路資料包。本章將介紹如何使用 Npcap 庫來實現半開放掃描功能。TCP SYN 半開放掃描是一種常見且廣泛使用的埠掃描技術,用於探測目標主機埠的開放狀態。由於這種方法並不完成完整的 TCP 三次握手過程,因此具有更高的隱蔽性和掃描效率。

筆者原本想為大家整理並分享如何使用Nmap工具進行埠掃描的,但覺得僅僅講解Nmap的命令使用方法並不能讓大家更好地理解其工作原理。實際上,Nmap 的底層使用的是Npcap庫,因此筆者決定演示如何使用Npcap庫開發一個簡單的掃描功能,從而幫助大家更好地理解Nmap的原理。

首先,若使用Nmap對目標主機進行SYN掃描,只需要執行nmap -sS 39.97.203.57命令即可,等待一段時間則可獲取到目標主機常規開放埠狀態,若要掃描特定埠開放狀態僅需要指定-p引數並攜帶掃描區間即可,如下命令所示;

┌──(lyshark㉿kali)-[~]
└─$ sudo nmap -sS 39.97.203.57
Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-08-08 15:28 CST
Nmap scan report for 39.97.203.57
Host is up (0.0038s latency).
Not shown: 997 filtered tcp ports (no-response)
PORT     STATE SERVICE
80/tcp   open  http
443/tcp  open  https
1935/tcp open  rtmp

┌──(lyshark㉿kali)-[~]
└─$ sudo nmap -sS -v 39.97.203.57 -p 1-2000
Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-08-08 15:32 CST
Scanning 39.97.203.57 [2000 ports]
Discovered open port 80/tcp on 39.97.203.57
Discovered open port 443/tcp on 39.97.203.57
Discovered open port 1935/tcp on 39.97.203.57
Completed SYN Stealth Scan at 15:32, 7.42s elapsed (2000 total ports)
Nmap scan report for 39.97.203.57
Host is up (0.0039s latency).
Not shown: 1997 filtered tcp ports (no-response)
PORT     STATE SERVICE
80/tcp   open  http
443/tcp  open  https
1935/tcp open  rtmp

Npcap庫的配置非常簡單,讀者僅需要去到官網下載,初次使用還需安裝Npcap 1.79 installer驅動程式,並下載Npcap SDK 1.13對應的開發工具包,如下圖所示;

接著,讀者需要自行解壓SDK開發工具包,並配置VC++目錄包含目錄與庫目錄,如下圖所示;

在進行開發之前,我們需要先定義三個結構體變數,首先定義eth_header資料包頭,乙太網包頭(Ethernet Frame Header)用於傳輸控制資訊和資料,它是資料鏈路層的一部分,負責在區域網中實現資料的可靠傳輸。

接著定義ip_header資料包頭,IP頭(IP Header)用於傳輸控制資訊和資料,IP頭是網路層的一部分,負責實現跨越不同網路的資料傳輸。

最後定義tcp_header資料包頭,TCP頭(TCP Header)用於傳輸控制資訊和資料,TCP頭是傳輸層的一部分,負責在主機之間提供可靠的、面向連線的通訊。

若要傳送TCP資料包,必須要構造一個完整的通訊協議頭,將乙太網資料包頭、IP資料包頭、TCP資料包頭封裝起來即可,其定義部分如下所示,其中每一個變數均對應於協議的每一個引數。

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

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

// 乙太網頭部結構體
struct eth_header
{
  uint8_t dest[6];   // 目的MAC地址 (6位元組)
  uint8_t src[6];    // 源MAC地址 (6位元組)
  uint16_t type;     // 乙太網型別欄位,表示上層協議 (2位元組)
};

// IPv4頭部結構體
struct ip_header
{
  uint8_t ihl : 4,     // 頭部長度 (4位),表示IP頭部的長度,以32位字為單位
      version : 4; // 版本 (4位),IPv4的版本號為4
  uint8_t tos;        // 服務型別 (1位元組)
  uint16_t tot_len;   // 總長度 (2位元組),表示整個IP資料包的長度,以位元組為單位
  uint16_t id;        // 標識 (2位元組),用於標識資料包片段
  uint16_t frag_off;  // 片段偏移 (2位元組),用於資料包片段
  uint8_t ttl;        // 生存時間 (1位元組),表示資料包在網路中的生存時間
  uint8_t protocol;   // 協議 (1位元組),表示上層協議 (例如,TCP為6,UDP為17)
  uint16_t check;     // 頭部校驗和 (2位元組),用於檢驗頭部的完整性
  uint32_t saddr;     // 源地址 (4位元組),表示傳送方的IPv4地址
  uint32_t daddr;     // 目的地址 (4位元組),表示接收方的IPv4地址
};

// TCP頭部結構體
struct tcp_header
{
  uint16_t source;    // 源埠號 (2位元組)
  uint16_t dest;      // 目的埠號 (2位元組)
  uint32_t seq;       // 序號 (4位元組),表示資料段的序列號
  uint32_t ack_seq;   // 確認號 (4位元組),表示期望接收的下一個序列號
  uint16_t res1 : 4,  // 保留位 (4位),通常設為0
  doff : 4,   // 資料偏移 (4位),表示TCP頭部的長度,以32位字為單位
  fin : 1,    // FIN標誌 (1位),表示傳送方沒有更多資料
  syn : 1,    // SYN標誌 (1位),表示同步序號,用於建立連線
  rst : 1,    // RST標誌 (1位),表示重置連線
  psh : 1,    // PSH標誌 (1位),表示推送資料
  ack : 1,    // ACK標誌 (1位),表示確認欄位有效
  urg : 1,    // URG標誌 (1位),表示緊急指標欄位有效
  res2 : 2;   // 保留位 (2位),通常設為0
  uint16_t window;    // 視窗大小 (2位元組),表示接收方的緩衝區大小
  uint16_t check;     // 校驗和 (2位元組),用於檢驗TCP頭部和資料的完整性
  uint16_t urg_ptr;   // 緊急指標 (2位元組),表示緊急資料的偏移量
};

unsigned short checksum(void *b, int len)
{
  unsigned short *buf = (unsigned short *)b;
  unsigned int sum = 0;
  unsigned short result;

  for (sum = 0; len > 1; len -= 2)
    sum += *buf++;
  if (len == 1)
    sum += *(unsigned char*)buf;
  sum = (sum >> 16) + (sum & 0xFFFF);
  sum += (sum >> 16);
  result = ~sum;
  return result;
}

接著需要實現兩個通用函式,其中EnumAdapters用於列舉當前系統中所有的網路卡資訊,並輸出其下標號與網路卡描述資訊,BindAdapters函式則用於根據使用者傳入的下標號對網路卡進行動態繫結,函式中透過迴圈的方式查詢網路卡下標若匹配則將下標所對應的控制代碼儲存到temp_adapter變數內,最後透過pcap_open_live實現對網路卡的開啟。

// 列舉當前網路卡
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)
  {
    // 列印網路卡資訊列表
    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;
}

// 根據編號繫結到對應網路卡
pcap_t* BindAdapters(int nChoose)
{
  pcap_if_t *adapters, *temp_adapter;
  char errbuf[PCAP_ERRBUF_SIZE];
  pcap_t *handle = NULL;

  if (pcap_findalldevs_ex(PCAP_SRC_IF_STRING, NULL, &adapters, errbuf) == -1)
  {
    return NULL;
  }

  // 遍歷找到指定的網路卡
  temp_adapter = adapters;
  for (int x = 0; x < nChoose - 1 && temp_adapter != NULL; ++x)
  {
    temp_adapter = temp_adapter->next;
  }

  // 若找不到繫結裝置則釋放控制代碼
  if (temp_adapter == NULL)
  {
    pcap_freealldevs(adapters);
    return NULL;
  }

  // 開啟指定的網路卡
  handle = pcap_open_live(temp_adapter->name, 65534, PCAP_OPENFLAG_PROMISCUOUS, 1000, errbuf);
  if (handle == NULL)
  {
    pcap_freealldevs(adapters);
    return NULL;
  }

  pcap_freealldevs(adapters);
  return handle;
}

抓包回撥函式packet_handlerpcap_loop呼叫,當啟用抓包後若控制代碼返回資料則會透過回撥函式通知使用者,使用者獲取到資料包header後,透過逐層解析即可得到所需要的欄位,若要實現SYN快速探測則需要判斷tcph標誌,若標誌被返回則可透過RST斷開會話,並以此節約掃描時間。

如下程式碼,定義了一個網路資料包回撥函式 packet_handler,用於處理透過 pcap 庫捕獲的網路資料包。函式首先列印資料包的長度,然後解析乙太網頭部以檢查其型別是否為 IP(0x0800)。如果是 IP 資料包,進一步解析 IP 頭部並列印相關資訊,包括 IP 版本、頭長度、源 IP 地址和目標 IP 地址。隨後檢查 IP 資料包的協議欄位是否為 TCP(6),若是,則解析 TCP 頭部並列印源埠、目標埠、序列號、確認號、頭部長度、標誌、視窗大小、校驗和及緊急指標等資訊。

// 網路資料包回撥函式
void packet_handler(u_char *param, const struct pcap_pkthdr *header, const u_char *pkt_data)
{
  // 列印資料包長度
  printf("資料包長度:%d\n", header->len);

  // 乙太網頭部
  struct eth_header *eth = (struct eth_header *)(pkt_data);

  // 檢查乙太網型別是否為 IP(0x0800)
  if (ntohs(eth->type) == 0x0800)
  {
    // IP 頭部
    struct ip_header *iph = (struct ip_header *)(pkt_data + sizeof(struct eth_header));

    // 列印 IP 頭部資訊
    printf("IP 版本: %d | ", iph->version);
    printf("IP 頭長度: %d | ", iph->ihl * 4);
    printf("源IP地址: %s | ", inet_ntoa(*(struct in_addr *)&iph->saddr));
    printf("目標IP地址: %s\n", inet_ntoa(*(struct in_addr *)&iph->daddr));

    // 檢查協議是否為 TCP(6)
    if (iph->protocol == 6)
    {
      // TCP 頭部
      struct tcp_header *tcph = (struct tcp_header *)(pkt_data + sizeof(struct eth_header) + iph->ihl * 4);

      // 列印 TCP 頭部資訊
      printf("源埠: %d | ", ntohs(tcph->source));
      printf("目標埠: %d | ", ntohs(tcph->dest));
      printf("序列號: %u | ", ntohl(tcph->seq));
      printf("確認號: %u | ", ntohl(tcph->ack_seq));
      printf("包頭長度: %d | ", tcph->doff * 4);
      printf("標誌: ");
      if (tcph->fin) printf("FIN ");
      if (tcph->syn) printf("SYN ");
      if (tcph->rst) printf("RST ");
      if (tcph->psh) printf("PSH ");
      if (tcph->ack) printf("ACK ");
      if (tcph->urg) printf("URG ");
      printf("\n");
      printf("窗體長度: %d | ", ntohs(tcph->window));
      printf("校驗和: 0x%04x | ", ntohs(tcph->check));
      printf("緊急資料指標: %d\n", ntohs(tcph->urg_ptr));
    }
  }
  printf("\n");
}

最後來看下主函式是如何實現的,首先透過呼叫EnumAdapters函式獲取到網路卡編號,並呼叫BindAdapters(4)函式繫結到指定的網路卡之上,套接字的建立依然採用原生API介面來實現,只不過在呼叫sendto傳送資料包時我們需要自行構建一個符合SYN掃描條件的資料包,在構建資料包時,乙太網資料包用於指定網路卡MAC地址等資訊,IP資料包頭則用於指定IP地址等資訊,TCP資料包頭則用於指定埠號資訊,並僅需將tcph->syn = 1;設定為1,透過checksum計算校驗和,並將校驗好的packet包透過sendto函式傳送到對端主機,如下所示;

int main(int argc, char* argv[])
{
  pcap_if_t *alldevs;
  pcap_t *adhandle;
  int i = 0;

  EnumAdapters();

  adhandle = BindAdapters(4);

  // 建立套接字
  SOCKET sock = socket(AF_INET, SOCK_RAW, IPPROTO_RAW);
  if (sock == INVALID_SOCKET)
  {
    return -1;
  }

  // 設定套接字屬性
  int one = 1;
  if (setsockopt(sock, IPPROTO_IP, IP_HDRINCL, (char *)&one, sizeof(one)) == SOCKET_ERROR)
  {
    return -1;
  }

  // ---------------------------------------------------------------
  // 構建網路資料包
  // ---------------------------------------------------------------

  char packet[4096];
  memset(packet, 0, 4096);

  struct eth_header *eth = (struct eth_header *)packet;
  struct ip_header *iph = (struct ip_header *)(packet + sizeof(struct eth_header));
  struct tcp_header *tcph = (struct tcp_header *)(packet + sizeof(struct eth_header) + sizeof(struct ip_header));

  // ---------------------------------------------------------------
  // 構建乙太網資料包頭
  // ---------------------------------------------------------------
  memset(eth->dest, 0xff, 6);  // 目標MAC地址
  memset(eth->src, 0x00, 6);   // 原MAC地址
  eth->type = htons(0x0800);   // IPv4

  // ---------------------------------------------------------------
  // 構建IP資料包頭
  // ---------------------------------------------------------------
  iph->ihl = 5;
  iph->version = 4;
  iph->tos = 0;
  iph->tot_len = sizeof(struct ip_header) + sizeof(struct tcp_header);
  iph->id = htons(54321);
  iph->frag_off = 0;
  iph->ttl = 255;
  iph->protocol = IPPROTO_TCP;
  iph->check = 0;
  iph->saddr = inet_addr("192.168.1.1");   // 原始IP地址
  iph->daddr = inet_addr("39.97.203.57");  // 目標IP地址

  // ---------------------------------------------------------------
  // 構建TCP資料包頭
  // ---------------------------------------------------------------
  tcph->source = htons(12345);           // 原始TCP埠
  tcph->dest = htons(80);                // 目標TCP埠
  tcph->seq = 0;
  tcph->ack_seq = 0;
  tcph->doff = 5; // TCP 頭部長度
  tcph->fin = 0;
  tcph->syn = 1;
  tcph->rst = 0;
  tcph->psh = 0;
  tcph->ack = 0;
  tcph->urg = 0;
  tcph->window = htons(5840);    // 分配Windows窗體數
  tcph->check = 0;               // 現在保留校驗和0,稍後用偽標頭填充
  tcph->urg_ptr = 0;

  // ---------------------------------------------------------------
  // 計算校驗和
  // ---------------------------------------------------------------

  // 計算IP校驗和
  iph->check = checksum((unsigned short *)packet, iph->tot_len);

  // TCP 校驗和
  struct
  {
    uint32_t src_addr;
    uint32_t dst_addr;
    uint8_t placeholder;
    uint8_t protocol;
    uint16_t tcp_length;
    struct tcp_header tcp;
  } pseudo_header;

  pseudo_header.src_addr = iph->saddr;
  pseudo_header.dst_addr = iph->daddr;
  pseudo_header.placeholder = 0;
  pseudo_header.protocol = IPPROTO_TCP;
  pseudo_header.tcp_length = htons(sizeof(struct tcp_header));
  memcpy(&pseudo_header.tcp, tcph, sizeof(struct tcp_header));

  tcph->check = checksum((unsigned short *)&pseudo_header, sizeof(pseudo_header));

  // ---------------------------------------------------------------
  // 傳送資料包
  // ---------------------------------------------------------------

  struct sockaddr_in dest;
  dest.sin_family = AF_INET;
  dest.sin_addr.s_addr = iph->daddr;

  if (sendto(sock, packet, iph->tot_len, 0, (struct sockaddr *)&dest, sizeof(dest)) == SOCKET_ERROR)
  {
    return -1;
  }

  // ---------------------------------------------------------------
  // 啟用抓包
  // ---------------------------------------------------------------

  pcap_loop(adhandle, 10, packet_handler, NULL);

  pcap_close(adhandle);
  closesocket(sock);
  pcap_freealldevs(alldevs);

  system("pause");
  return 0;
}

讀者可自行編譯並執行上述程式碼,當執行成功後則可看到資料包的方向及標誌型別,如下圖所示。

相關文章