原始套接字(Raw Socket)繞過傳輸層協定處理,直接收發 IP 層封包,是實作嗅探器、防火牆、IDS 的核心技術。本文以 Windows 平台 SIO_RCVALL IOCTL 為例,完整實作一個可視化嗅探器,深入分析為何無法捕獲回傳封包(ACK/資料回應),並給出 WinPcap/Npcap 移植方案。
socket(AF_INET, SOCK_RAW, IPPROTO_IP) 建立原始套接字。gethostname + gethostbyname 取得本機 IP。bind 綁定本機介面。ioctlsocket(s, SIO_RCVALL, &on) 啟用混雜模式(接收所有經過網卡的封包)。recvfrom 讀取封包 → 解析 IP/TCP/UDP 標頭 → 顯示/儲存。
DWORD WINAPI CSniffer::SnifferThread(LPVOID lpData) {
CSniffer *pThis = (CSniffer*)lpData;
SOCKET hSniffer = socket(AF_INET, SOCK_RAW, IPPROTO_IP);
DWORD on = 1;
char hostname[32];
gethostname(hostname, 32);
hostent *he = gethostbyname(hostname);
SOCKADDR_IN local = {AF_INET, htons(0)};
memcpy(&local.sin_addr, he->h_addr_list[0], he->h_length);
bind(hSniffer, (sockaddr*)&local, sizeof(local));
ioctlsocket(hSniffer, SIO_RCVALL, &on); // 關鍵:開啟混雜模式
char buf[65536];
while (true) {
int len = recv(hSniffer, buf, sizeof(buf), 0);
if (len != SOCKET_ERROR)
pThis->IPHeadAnalyze(buf, len); // 解析封包
}
return 0;
}
#pragma pack(1)
struct ip_hdr {
unsigned char ver_ihl; // 版本(4) + 標頭長度(4)
unsigned char tos;
unsigned short total_len;
unsigned short id;
unsigned short frag_off;
unsigned char ttl;
unsigned char proto; // 6=TCP, 17=UDP, 1=ICMP
unsigned short checksum;
unsigned int src_ip;
unsigned int dst_ip;
};
#pragma pack()
int CSniffer::IPHeadAnalyze(const char *buf, int len) {
ip_hdr *iph = (ip_hdr*)buf;
int ihl = (iph->ver_ihl & 0x0F) * 4;
if (len < ihl) return -1;
// 檢查校驗和
if (checksum(iph, ihl) != 0) return -2;
// 解析 TCP/UDP 埠號
if (iph->proto == 6) { // TCP
tcp_hdr *tcph = (tcp_hdr*)(buf + ihl);
// 記錄 src_ip:src_port ↔ dst_ip:dst_port
}
// 存入虛擬清單控制項顯示
return 0;
}
| 現象 | 原因 | 驗證方式 |
|---|---|---|
| 僅見發出的 SYN/HTTP GET | NDIS 迷你埠驅動 僅在「發送路徑」複製封包給原始套接字,接收路徑未複製 | Wireshark 可見雙向 → 驅動層面無問題 |
| Chrome 下載幾百 KB/s 嗅探器無反應 | 大量資料封包走「接收路徑」,同上原因遺漏 | 對比 netsh trace 完整捕獲 |
| 原始碼註解「感覺沒真正進入混雜模式」 | SIO_RCVALL 在 Vista+ 僅對「本機發送/廣播/多播」有效,非真正混雜模式 | 需 WinPcap/Npcap 驅動層攔截 |
SIO_RCVALL 真正混雜模式(需管理員)。pcap_open_live(..., 1, ...) 第三參數 promisc=1 真正開啟混雜模式。libpcap)。
// 取代原始套接字建立
pcap_t *handle = pcap_open_live("\\Device\\NPF_{GUID}", 65536, 1, 1000, errbuf);
pcap_loop(handle, -1, packet_handler, (u_char*)pThis);
// 回調函式
void packet_handler(u_char *user, const struct pcap_pkthdr *h, const u_char *bytes) {
CSniffer *pThis = (CSniffer*)user;
pThis->IPHeadAnalyze((const char*)bytes, h->caplen);
}
大量封包時必須使用 LVS_OWNERDATA(虛擬模式):
std::vector,清單只負責繪製可見列。SetItemCountEx(count) 通知列數變更,OnGetDispInfo 按需提供文字。vec.push_back(...); SetItemCountEx(vec.size()); O(1) 擴展。SIO_RCVALL 適合本機流量審計(出站封包、本機服務存取),不適合一般網路嗅探。libnet, tins, pcapplusplus)避免手寫標頭結構體出錯。