Npcap 是一個功能強大的開源網路抓包庫,它是 WinPcap 的一個分支,並提供了一些增強和改進。特別適用於在 Windows 環境下進行網路流量捕獲和分析。除了支援通常的網路抓包功能外,Npcap 還提供了對封包的拼合與構造,使其成為實現 UDP 封包發包的理想選擇。本章將通過Npcap庫構造一個UDP原始封包,並實現對特定主機的發包功能,通過本章的學習讀者可以掌握如何使用Npcap庫偽造特定的封包格式。
Npcap的主要特點和概述:
UDP 是一種無連線、輕量級的傳輸層協定,與 TCP 相比,它不提供可靠性、流控制和錯誤恢復機制,但卻更加簡單且具有較低的開銷。UDP 主要用於那些對傳輸速度要求較高、可以容忍少量丟失的應用場景。
UDP 封包結構: UDP 封包由報頭和資料兩部分組成。
UDP 的特點:
UDP 的應用場景:
使用 WinPcap(Windows Packet Capture)庫列舉系統上的網路介面以及它們的 IP 地址。WinPcap 是一個用於 Windows 作業系統的網路封包捕獲庫,可以用於網路封包的捕獲和分析。
程式碼主要做了以下幾個事情:
pcap_findalldevs_ex
函數查詢系統上的所有網路介面。pcap_findalldevs_ex
用於查詢系統上所有網路介面的函數。它的原型如下:
int pcap_findalldevs_ex(const char *source, struct pcap_rmtauth *auth, pcap_if_t **alldevs, char *errbuf);
函數引數說明:
source
:一個字串,用於指定網路介面的來源。可以為 NULL
,表示從系統獲取網路介面資訊。也可以指定為一個網路地址,用於遠端捕獲。auth
:一個 pcap_rmtauth
結構的指標,用於指定遠端捕獲的認證資訊。一般情況下可以為 NULL
。alldevs
:一個 pcap_if_t
型別的指標的地址,用於儲存查詢到的網路介面連結串列的頭指標。errbuf
:一個字元陣列,用於儲存錯誤資訊。函數返回值:
errbuf
中。函數功能:
pcap_findalldevs_ex
主要用於查詢系統上的網路介面資訊。當呼叫成功後,alldevs
將指向一個連結串列,連結串列中的每個節點都包含一個網路介面的資訊。這個連結串列的頭指標是 alldevs
。
pcap_freealldevs
用於釋放 pcap_findalldevs_ex
函數分配的資源的函數。其原型如下:
void pcap_freealldevs(pcap_if_t *alldevs);
函數引數說明:
alldevs
:由 pcap_findalldevs_ex
返回的連結串列的頭指標。函數功能:
pcap_freealldevs
主要用於釋放 pcap_findalldevs_ex
函數返回的連結串列中分配的資源,包括每個節點和節點中儲存的介面資訊。
輸出當前系統中活動網路卡資訊,可以這樣來寫,如下程式碼所示;
#include <WinSock2.h>
#include <Windows.h>
#include <iostream>
#include <pcap.h>
#pragma comment(lib,"ws2_32.lib")
#pragma comment(lib, "packet.lib")
#pragma comment(lib, "wpcap.lib")
// 開啟網路卡返回的指標
pcap_t* m_adhandle;
unsigned char* FinalPacket;
unsigned int UserDataLen;
int main(int argc, char *argv[])
{
// 開啟網路卡
pcap_if_t* alldevs = NULL, *d = NULL;
char szErr[MAX_PATH] = { 0 };
if (-1 == pcap_findalldevs_ex(PCAP_SRC_IF_STRING, NULL, &alldevs, szErr))
{
return 0;
}
// 遍歷網路卡
char* lpszIP = NULL;
d = alldevs;
while (NULL != d)
{
// 遍歷網路卡IP
char szAddress[1024] = { 0 };
pcap_addr_t* p = d->addresses;
while (p)
{
lpszIP = inet_ntoa(((sockaddr_in*)p->addr)->sin_addr);
strcpy(szAddress, lpszIP);
p = p->next;
}
std::cout << "地址列表: " << szAddress << std::endl;
d = d->next;
}
// 釋放資源
pcap_freealldevs(alldevs);
system("pause");
return 0;
}
輸出效果如下圖所示;
開啟網路介面卡的函數,通過傳入本機的IP地址,該函數會查詢與該IP地址匹配的網路介面卡並開啟。以下是對該函數的簡要分析:
查詢網路卡裝置指標:
if (-1 == pcap_findalldevs_ex(PCAP_SRC_IF_STRING, NULL, &alldevs, errbuf))
使用 pcap_findalldevs_ex
函數來獲取本機所有網路卡裝置的連結串列。如果返回值為 -1,說明發生了錯誤,這時函數會輸出錯誤資訊並直接返回。
選取適合網路卡:
for (d = alldevs; d; d = d->next)
通過遍歷網路卡裝置連結串列,查詢與傳入的本機IP地址匹配的網路卡。首先,通過檢查每個網路卡的地址列表,找到第一個匹配的網路卡。如果找到了,將 flag
標記設為1,然後跳出迴圈。如果未找到匹配的網路卡,輸出錯誤資訊並返回。
獲取子網掩碼:
netmask = ((sockaddr_in*)d->addresses->netmask)->sin_addr.S_un.S_addr;
獲取匹配網路卡的子網掩碼。
開啟網路卡:
m_adhandle = pcap_open(d->name, 65536, PCAP_OPENFLAG_PROMISCUOUS, 1000, NULL, errbuf);
使用 pcap_open
函數開啟選擇的網路卡,該函數的宣告如下:
pcap_t *pcap_open(const char *source, int snaplen, int flags, int read_timeout, struct pcap_rmtauth *auth, char *errbuf);
這裡是對引數的簡要解釋:
source
: 要開啟的網路介面卡的名稱,例如 "eth0"。
snaplen
: 指定捕獲封包時每個封包的最大長度。如果封包超過這個長度,它將被截斷。通常設定為封包的最大可能長度。
flags
: 控制捕獲的方式,可以使用位掩碼進行組合。常見的標誌包括:
PCAP_OPENFLAG_PROMISCUOUS
: 開啟混雜模式,允許捕獲所有經過網路卡的封包。PCAP_OPENFLAG_MAX_RESPONSIVENESS
: 最大響應性標誌,可能在某些平臺上影響效能。read_timeout
: 設定超時值,以毫秒為單位。如果設定為0,表示無限期等待封包。
auth
: 可以指定用於遠端捕獲的身份驗證資訊,通常為 NULL
。
errbuf
: 用於儲存錯誤資訊的緩衝區,如果函數執行失敗,會將錯誤資訊寫入這個緩衝區。
函數返回一個 pcap_t
型別的指標,它是一個表示開啟的網路介面卡的結構。如果開啟失敗,返回 NULL
。
檢查乙太網:
if (DLT_EN10MB != pcap_datalink(m_adhandle))
pcap_datalink
函數是 PCAP 庫中用於獲取網路介面卡資料鏈路型別(datalink type)的函數,確保是乙太網,如果不是乙太網,輸出錯誤資訊並返回。
該函數的宣告如下:
int pcap_datalink(pcap_t *p);
這裡是對引數的簡要解釋:
p
: 表示一個已經開啟的網路介面卡的 pcap_t
結構指標。函數返回一個整數,表示資料鏈路型別。這個值通常是預定義的常數之一,用於標識不同型別的網路資料鏈路。
常見的一些資料鏈路型別常數包括:
DLT_EN10MB
(Ethernet): 表示乙太網資料鏈路。DLT_IEEE802
(802.5 Token Ring): 表示 IEEE 802.5 Token Ring 資料鏈路。DLT_PPP
(Point-to-Point Protocol): 表示對等協定資料鏈路。DLT_ARCNET
(ARCNET): 表示 ARCNET 資料鏈路。釋放網路卡裝置列表:
pcap_freealldevs(alldevs);
最後,釋放 pcap_findalldevs_ex
函數返回的網路卡裝置列表,避免記憶體漏失。
該函數的其他全域性變數 m_adhandle
,FinalPacket
,UserDataLen
已經在文章開頭宣告和定義。
// 通過傳入本機IP地址開啟網路卡
void OpenAdapter(std::string local_address)
{
pcap_if_t* alldevs = NULL, * d = NULL;
char errbuf[256] = { 0 };
bpf_program fcode;
u_int netmask;
// 獲取網路卡裝置指標
if (-1 == pcap_findalldevs_ex(PCAP_SRC_IF_STRING, NULL, &alldevs, errbuf))
{
std::cout << "獲取網路卡裝置指標出錯" << std::endl;
return;
}
// 選取適合網路卡
int flag = 0;
for (d = alldevs; d; d = d->next)
{
pcap_addr_t* p = d->addresses;
while (p)
{
if (local_address == inet_ntoa(((sockaddr_in*)p->addr)->sin_addr))
{
flag = 1;
break;
}
p = p->next;
}
if (1 == flag)
break;
}
if (0 == flag)
{
std::cout << "請檢查本機IP地址是否正確" << std::endl;
std::cout << local_address.c_str() << std::endl;
return;
}
// 獲取子網掩碼
netmask = ((sockaddr_in*)d->addresses->netmask)->sin_addr.S_un.S_addr;
// 開啟網路卡
m_adhandle = pcap_open(d->name, 65536, PCAP_OPENFLAG_PROMISCUOUS, 1000, NULL, errbuf);
if (NULL == m_adhandle)
{
std::cout << "開啟網路卡出錯" << std::endl;
pcap_freealldevs(alldevs);
return;
}
//檢查乙太網
if (DLT_EN10MB != pcap_datalink(m_adhandle))
{
std::cout << "此程式僅在乙太網下工作" << std::endl;
pcap_freealldevs(alldevs);
return;
}
// 釋放網路卡裝置列表
pcap_freealldevs(alldevs);
}
MAC地址轉換為Bytes位元組
將MAC 地址的字串表示形式轉換為位元組陣列(unsigned char
陣列),函數首先建立了一個臨時緩衝區 Tmp
來儲存輸入字串的拷貝,然後使用 sscanf
函數將字串中的每兩個字元解析為一個十六進位制數,儲存到 Returned
陣列中。最後,通過調整指標的位置,跳過已經處理的字元,實現了對整個字串的解析。
下面是這段程式碼的解釋:
// MAC地址轉Bytes
unsigned char* MACStringToBytes(std::string String)
{
// 獲取輸入字串的長度
int iLen = strlen(String.c_str());
// 建立一個臨時緩衝區,用於儲存輸入字串的拷貝
char* Tmp = new char[(iLen + 1)];
// 將輸入字串拷貝到臨時緩衝區
strcpy(Tmp, String.c_str());
// 建立一個用於儲存結果的unsigned char陣列,陣列大小為6
unsigned char* Returned = new unsigned char[6];
// 迴圈處理每個位元組
for (int i = 0; i < 6; i++)
{
// 使用sscanf將字串中的兩個字元轉換為16進位制數,儲存到Returned陣列中
sscanf(Tmp, "%2X", &Returned[i]);
// 移動臨時緩衝區的指標,跳過已經處理過的字元
memmove((void*)(Tmp), (void*)(Tmp + 3), 19 - i * 3);
}
// 返回儲存結果的陣列
return Returned;
}
Bytes位元組轉換為16進位制
將兩個位元組(unsigned char
型別的 X
和 Y
)組成一個16位元的無符號整數。函數的目的是將兩個位元組的資料合併成一個16位元的整數。首先,將 X
左移8位元,然後與 Y
進行按位元或操作,得到一個包含兩個位元組資訊的16位元整數。最後,將這個16位元整數返回。這種操作通常在處理網路協定或二進位制資料時會經常遇到。
下面是這段程式碼的解釋:
// Bytes地址轉16進位制
unsigned short BytesTo16(unsigned char X, unsigned char Y)
{
// 將 X 左移8位元,然後與 Y 進行按位元或操作,得到一個16位元的無符號整數
unsigned short Tmp = X;
Tmp = Tmp << 8;
Tmp = Tmp | Y;
return Tmp;
}
計算 IP 資料包的校驗和
這個函數主要通過遍歷 IP 頭中的每兩個位元組,將它們合併為一個16位元整數,並逐步累加到校驗和中。在每次累加時,還需要檢查是否發生了溢位,如果溢位則需要額外加1。最後,對累加得到的校驗和進行取反操作,得到最終的 IP 校驗和,並將其返回。這種校驗和計算通常用於驗證 IP 資料包的完整性。
下面是這段程式碼的解釋:
// 計算IP校驗和
unsigned short CalculateIPChecksum(UINT TotalLen, UINT ID, UINT SourceIP, UINT DestIP)
{
// 初始化校驗和
unsigned short CheckSum = 0;
// 遍歷 IP 頭的每兩個位元組
for (int i = 14; i < 34; i += 2)
{
// 將每兩個位元組合併為一個16位元整數
unsigned short Tmp = BytesTo16(FinalPacket[i], FinalPacket[i + 1]);
// 計算校驗和
unsigned short Difference = 65535 - CheckSum;
CheckSum += Tmp;
// 處理溢位
if (Tmp > Difference) { CheckSum += 1; }
}
// 取反得到最終的校驗和
CheckSum = ~CheckSum;
return CheckSum;
}
計算 UDP 資料包的校驗和
這個函數主要通過構造 UDP 資料包的偽首部,包括源 IP、目標 IP、協定型別(UDP)、UDP 長度、源埠、目標埠以及 UDP 資料等欄位,並通過遍歷偽首部的每兩個位元組計算校驗和。最後取反得到最終的 UDP 校驗和,並將其返回。這種校驗和計算通常用於驗證 UDP 資料包的完整性。
下面是這段程式碼的解釋:
// 計算UDP校驗和
unsigned short CalculateUDPChecksum(unsigned char* UserData, int UserDataLen, UINT SourceIP, UINT DestIP, USHORT SourcePort, USHORT DestinationPort, UCHAR Protocol)
{
unsigned short CheckSum = 0;
// 計算 UDP 資料包的偽首部長度
unsigned short PseudoLength = UserDataLen + 8 + 9; // 長度包括 UDP 頭(8位元組)和偽首部(9位元組)
// 如果長度不是偶數,新增一個額外的位元組
PseudoLength += PseudoLength % 2;
// 建立 UDP 偽首部
unsigned char* PseudoHeader = new unsigned char[PseudoLength];
RtlZeroMemory(PseudoHeader, PseudoLength);
// 設定偽首部中的協定欄位為 UDP (0x11)
PseudoHeader[0] = 0x11;
// 複製源和目標 IP 地址到偽首部
memcpy((void*)(PseudoHeader + 1), (void*)(FinalPacket + 26), 8);
// 將 UDP 頭的長度欄位拷貝到偽首部
unsigned short Length = UserDataLen + 8;
Length = htons(Length);
memcpy((void*)(PseudoHeader + 9), (void*)&Length, 2);
memcpy((void*)(PseudoHeader + 11), (void*)&Length, 2);
// 將源埠、目標埠和 UDP 資料拷貝到偽首部
memcpy((void*)(PseudoHeader + 13), (void*)(FinalPacket + 34), 2);
memcpy((void*)(PseudoHeader + 15), (void*)(FinalPacket + 36), 2);
memcpy((void*)(PseudoHeader + 17), (void*)UserData, UserDataLen);
// 遍歷偽首部的每兩個位元組,計算校驗和
for (int i = 0; i < PseudoLength; i += 2)
{
unsigned short Tmp = BytesTo16(PseudoHeader[i], PseudoHeader[i + 1]);
unsigned short Difference = 65535 - CheckSum;
CheckSum += Tmp;
if (Tmp > Difference) { CheckSum += 1; }
}
// 取反得到最終的校驗和
CheckSum = ~CheckSum;
// 釋放偽首部的記憶體
delete[] PseudoHeader;
return CheckSum;
}
這段程式碼的分析:
PseudoHeader
陣列來構造偽首部。memcpy
等操作將源和目標IP地址、UDP頭的長度欄位以及UDP的源埠、目標埠、UDP資料等內容填充到偽首部中。Tmp
,然後進行累加。如果累加結果大於65535,則向結果中再加1。這是為了處理累加和溢位的情況。需要注意的是,UDP校驗和是一個16位元的值,用於驗證UDP資料包在傳輸過程中是否被修改。這段程式碼主要完成了構造UDP偽首部和計算校驗和的過程。在實際網路通訊中,校驗和的計算是為了保證資料的完整性,防止在傳輸過程中的錯誤。
建立UDP封包函數
建立一個UDP封包,該程式碼是一個簡單的網路程式設計範例,用於建立和傳送UDP封包。其中,UDP封包的內容和頭部資訊都可以根據實際需求進行客製化。
程式碼的概述:
pcap_findalldevs_ex
函數獲取本機的網路卡裝置列表,並在控制檯輸出每個網路卡的地址列表。pcap_open
函數開啟選擇的網路卡,獲取到網路卡的控制程式碼。CreatePacket
函數建立一個UDP封包。該函數包括以下步驟:
new
運運算元為FinalPacket
分配記憶體,記憶體大小為UserDataLength + 42
位元組。FinalPacket
的前12個位元組。CalculateIPChecksum
函數計算IP頭的校驗和。CalculateUDPChecksum
函數計算UDP頭的校驗和。FinalPacket
中。void CreatePacket(unsigned char* SourceMAC, unsigned char* DestinationMAC,unsigned int SourceIP, unsigned int DestIP,unsigned short SourcePort, unsigned short DestinationPort,unsigned char* UserData, unsigned int UserDataLength)
{
UserDataLen = UserDataLength;
FinalPacket = new unsigned char[UserDataLength + 42]; // 為資料長度加上42位元組的檔頭保留足夠的記憶體
USHORT TotalLen = UserDataLength + 20 + 8; // IP報頭使用資料長度加上IP報頭長度(通常為20位元組)加上udp報頭長度(通常為8位元組)
// 開始填充乙太網包頭
memcpy((void*)FinalPacket, (void*)DestinationMAC, 6);
memcpy((void*)(FinalPacket + 6), (void*)SourceMAC, 6);
USHORT TmpType = 8;
memcpy((void*)(FinalPacket + 12), (void*)&TmpType, 2); // 使用的協定型別(USHORT)型別0x08是UDP。可以為其他協定(例如TCP)更改此設定
// 開始填充IP頭封包
memcpy((void*)(FinalPacket + 14), (void*)"\x45", 1); // 前3位的版本(4)和最後5位的標題長度。
memcpy((void*)(FinalPacket + 15), (void*)"\x00", 1); // 通常為0
TmpType = htons(TotalLen);
memcpy((void*)(FinalPacket + 16), (void*)&TmpType, 2);
TmpType = htons(0x1337);
memcpy((void*)(FinalPacket + 18), (void*)&TmpType, 2); // Identification
memcpy((void*)(FinalPacket + 20), (void*)"\x00", 1); // Flags
memcpy((void*)(FinalPacket + 21), (void*)"\x00", 1); // Offset
memcpy((void*)(FinalPacket + 22), (void*)"\x80", 1); // Time to live.
memcpy((void*)(FinalPacket + 23), (void*)"\x11", 1); // 協定UDP為0x11(17)TCP為6 ICMP為1等
memcpy((void*)(FinalPacket + 24), (void*)"\x00\x00", 2); // 計算校驗和
memcpy((void*)(FinalPacket + 26), (void*)&SourceIP, 4); //inet_addr does htonl() for us
memcpy((void*)(FinalPacket + 30), (void*)&DestIP, 4);
// 開始填充UDP頭部封包
TmpType = htons(SourcePort);
memcpy((void*)(FinalPacket + 34), (void*)&TmpType, 2);
TmpType = htons(DestinationPort);
memcpy((void*)(FinalPacket + 36), (void*)&TmpType, 2);
USHORT UDPTotalLen = htons(UserDataLength + 8); // UDP Length does not include length of IP header
memcpy((void*)(FinalPacket + 38), (void*)&UDPTotalLen, 2);
//memcpy((void*)(FinalPacket+40),(void*)&TmpType,2); //checksum
memcpy((void*)(FinalPacket + 42), (void*)UserData, UserDataLength);
unsigned short UDPChecksum = CalculateUDPChecksum(UserData, UserDataLength, SourceIP, DestIP, htons(SourcePort), htons(DestinationPort), 0x11);
memcpy((void*)(FinalPacket + 40), (void*)&UDPChecksum, 2);
unsigned short IPChecksum = htons(CalculateIPChecksum(TotalLen, 0x1337, SourceIP, DestIP));
memcpy((void*)(FinalPacket + 24), (void*)&IPChecksum, 2);
return;
}
對該程式碼的分析:
new
運運算元為FinalPacket
分配記憶體,記憶體大小為UserDataLength + 42
位元組。這足夠容納UDP資料以及乙太網、IP和UDP頭的長度。memcpy
函數將目標MAC地址、源MAC地址和協定型別(這裡是IPv4)拷貝到FinalPacket
的前12個位元組。FinalPacket
的第14個位元組開始,填充IPv4頭部。這包括版本、標題長度、總長度、標識、標誌、偏移、生存時間、協定(UDP為0x11),校驗和、源IP和目標IP。FinalPacket
的第34個位元組開始,填充UDP頭。這包括源埠、目標埠、UDP長度(包括UDP頭和資料)和校驗和。其中,UDP校驗和的計算通過呼叫CalculateUDPChecksum
函數完成。CalculateIPChecksum
函數計算IP頭的校驗和。這個校驗和是IPv4頭的一個欄位。FinalPacket
中,可以將其用於傳送到網路。需要注意的是,這段程式碼中的寫死可能需要根據實際需求進行修改,例如協定型別、標識、生存時間等。此外,計算校驗和是網路協定中用於檢測資料完整性的一種機制。
傳送UDP封包
程式碼演示瞭如何開啟網路卡,生成UDP封包,並通過pcap_sendpacket
函數傳送封包到網路。需要注意的是,封包的內容和地址是寫死的,實際應用中可能需要根據需要進行更改。
int main(int argc, char* argv[])
{
// 開啟網路卡
OpenAdapter("10.0.66.24");
// 填充地址並生成封包包頭
char SourceMAC[MAX_PATH] = "8C-ff-ff-ff-ff-ff";
char SourceIP[MAX_PATH] = "192.168.93.11";
char SourcePort[MAX_PATH] = "80";
char DestinationMAC[MAX_PATH] = "8C-dd-dd-dd-dd-dd";
char DestinationIP[MAX_PATH] = "192.168.93.11";
char DestinationPort[MAX_PATH] = "8080";
char DataString[MAX_PATH] = "hello lyshark";
CreatePacket(MACStringToBytes(SourceMAC), MACStringToBytes(DestinationMAC), inet_addr(SourceIP), inet_addr(DestinationIP), atoi(SourcePort), atoi(DestinationPort), (UCHAR*)DataString, (strlen(DataString) + 1));
// 迴圈發包
for (int x = 0; x < 10; x++)
{
if (0 != pcap_sendpacket(m_adhandle, FinalPacket, (UserDataLen + 42)))
{
char* szErr = pcap_geterr(m_adhandle);
return 0;
}
}
system("pause");
return 0;
}
開啟wireshark抓包工具,過濾目標地址為ip.dst==192.168.93.11
然後抓包,執行編譯後的程式,則你會看到我們自己構建的封包被傳送了10次,如下圖所示;
隨便開啟一個封包看下結構,源地址目標地址均是偽造的地址,封包中的內容是hello lyshark
,如下圖所示;