原始套接字打造ping命令
早期文章:
Ping 命令的構造
ping 命令依賴的不是TCP 協(xié)議,也不是UDP 協(xié)議,它依賴的是ICMP協(xié)議。ICMP是IP層的協(xié)議之一,它傳遞差錯報文以及其他需要注意的信息。ICMP報文通常被IP層或高層協(xié)議使用。ICMP封裝在IP數據報內部,如下圖。

ICMP報文的格式如下圖所示。

ICMP協(xié)議的類型碼與代碼根據不同的情況,各自取不同的值。Ping命令類型碼用到了2個值,分別是0和8。而代碼的取值都是0。當類型碼取值為0時,代碼的0值表示回顯應答;當類型碼取值為8時,代碼的0值表示請求回顯。Ping命令發(fā)送一個ICMP數據報時,類型碼為8,代碼為0,表示向對方主機進行請求回顯;當收到對方的ICMP數據報時,類型碼為0,代碼為0,表示收到了對方主機的回顯應答。簡單來說,ping命令發(fā)出的數據中,類型是8,代碼是0,如果對方有回應,那么對方回應的數據中,類型是0,代碼是0。
在自己實現(xiàn)Ping命令時,就是去自己構造一個請求回顯的ICMP數據報,然后進行發(fā)送。ICMP的數據結構定義如下:
// ICMP協(xié)議結構體定義struct icmp_header{unsigned char icmp_type; // 消息類型unsigned char icmp_code; // 代碼unsigned short icmp_checksum; // 校驗和unsigned short icmp_id; // 用來唯一標識此請求的ID號,通常設置為進程IDunsigned short icmp_sequence; // 序列號unsigned long icmp_timestamp; // 時間戳};

在上圖中,標識1的部分是對協(xié)議進行過濾設置的,在該部分輸入“ICMP”可以讓Wireshark只顯示ICMP協(xié)議的數據記錄。相應地,可以輸入“TCP”、“UDP”、“HTTP”等協(xié)議進行篩選過濾。標識2的部分用于顯示篩選后的ICMP記錄,從這里可以明顯看出源IP地址、目的IP地址和協(xié)議的類型。標識3的部分用于顯示ICMP數據結構的值和附加的數據內容。最下面的部分顯示了數據的原始的二進制數據,在熟練掌握協(xié)議后,查看原始的二進制數據也并不是不可能的。
Ping命令的實現(xiàn)
struct icmp_header{unsigned char icmp_type; // 消息類型unsigned char icmp_code; // 代碼unsigned short icmp_checksum; // 校驗和unsigned short icmp_id; // 用來唯一標識此請求的ID號,通常設置為進程IDunsigned short icmp_sequence; // 序列號unsigned long icmp_timestamp; // 時間戳};// 計算校驗和unsigned short chsum(struct icmp_header *picmp, int len){long sum = 0;unsigned short *pusicmp = (unsigned short *)picmp;while ( len > 1 ){sum += *(pusicmp++);if ( sum & 0x80000000 ){sum = (sum & 0xffff) + (sum >> 16);}len -= 2;}if ( len ){sum += (unsigned short)*(unsigned char *)pusicmp;}while ( sum >> 16 ){sum = (sum & 0xffff) + (sum >> 16);}return (unsigned short)~sum;}
ICMP的校驗值是一個16位的無符號整型,它會將ICMP協(xié)議頭不的數據進行累加,當累加有溢出的話,會將溢出的部分也進行累加。具體計算校驗和的算法就不過多介紹了,如果對校驗和計算的代碼不了解,可以進行單步調試來進行分析。再來看一下對于ICMP結構體的填充,具體代碼如下:
BOOL MyPing(char *szDestIp){BOOL bRet = TRUE;WSADATA wsaData;int nTimeOut = 1000;char szBuff[ICMP_HEADER_SIZE + 32] = { 0 };icmp_header *pIcmp = (icmp_header *)szBuff;char icmp_data[32] = { 0 };WSAStartup(MAKEWORD(2, 2), &wsaData);// 創(chuàng)建原始套接字SOCKET s = socket(PF_INET, SOCK_RAW, IPPROTO_ICMP);// 設置接收超時setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, (char const*)&nTimeOut, sizeof(nTimeOut));// 設置目的地址sockaddr_in dest_addr;dest_addr.sin_family = AF_INET;dest_addr.sin_addr.S_un.S_addr = inet_addr(szDestIp);dest_addr.sin_port = htons(0);// 構造ICMP封包pIcmp->icmp_type = ICMP_ECHO_REQUEST;pIcmp->icmp_code = 0;pIcmp->icmp_id = (USHORT)::GetCurrentProcessId();pIcmp->icmp_sequence = 0;pIcmp->icmp_timestamp = 0;pIcmp->icmp_checksum = 0;// 拷貝數據// 這里的數據可以是任意的// 這里使用abc是為了和系統(tǒng)提供的看起來一樣memcpy((szBuff + ICMP_HEADER_SIZE), "abcdefghijklmnopqrstuvwabcdefghi", 32);// 計算校驗和pIcmp->icmp_checksum = chsum((struct icmp_header *)szBuff, sizeof(szBuff));sockaddr_in from_addr;char szRecvBuff[1024];int nLen = sizeof(from_addr);DWORD dwStart = GetTickCount();sendto(s, szBuff, sizeof(szBuff), 0, (SOCKADDR *)&dest_addr, sizeof(SOCKADDR));recvfrom(s, szRecvBuff, MAXBYTE, 0, (SOCKADDR *)&from_addr, &nLen);DWORD dwEnd = GetTickCount();// 判斷接收到的是否是自己請求的地址if ( lstrcmp(inet_ntoa(from_addr.sin_addr), szDestIp) ){bRet = FALSE;}else{struct icmp_header *pIcmp1 = (icmp_header *)(szRecvBuff + 20);printf("%s %d\r\n", inet_ntoa(from_addr.sin_addr), dwEnd - dwStart);}return bRet;}
調用運行輸出如下:

第一列是我們ping的IP地址,后面是數據包往返經過的毫秒數。
完整內容參考《C++黑客編程揭秘與防范》(第三版)一書。
更多文章:
