網絡安全編程:原始套接字的使用
使用TCP或UDP時,需要在調用socket()函數時為它的第2個參數指定相應的類型,比如SOCK_STREAM是代表要使用TCP,而SOCK_DGRAM表示要使用UDP協(xié)議。除了可以指定這兩種類型以外,還可以指定為原始套接字類型,即SOCK_RAW。當socket()函數的第2個參數指定為SOCK_STREAM或SOCK_DGRAM時,第3個參數可以缺省。而當socket()函數的第2個參數指定為SOCK_RAW時,第3個參數就必須明確指定需要使用的協(xié)議。
當套接字類型指定為SOCK_RAW時,協(xié)議類型的常用取值有IPPROTO_IP、IPPROTO_ICMP、IPPROTO_TCP、IPPROTO_UDP和IPPROTO_RAW。使用前四種類型,當發(fā)送數據時,系統(tǒng)會自動為數據加上IP首部并設置IP首部中的上層協(xié)議字段(如果有IP_HDRINCL選項,則系統(tǒng)不會自動添加IP首部);當接收數據時,系統(tǒng)不會將IP首部移除,需要程序自行處理。如果使用IPPROTO_RAW,那么系統(tǒng)將數據包直接送到網絡層發(fā)送數據,并且需要程序自己構造IP首部中的字段。
本文通過介紹原始套接字實現(xiàn)經典的網絡命令,即Ping命令。通過完成一個Ping命令來初步了解和掌握原始套接字的使用。
1. Ping命令的使用
Ping命令的目的是為了測試另一臺主機是否可達,Ping命令發(fā)送一份ICMP回顯請求報文給主機,并等待返回ICMP回顯應答。一般來說,如果不能Ping到某臺主機,那么就不能與該主機進行通信(例外的情況是對方主機的防火墻將進入主機的回顯請求報文屏蔽掉了,這種情況雖然Ping不通,但是仍然可以正常進行通信)。
Ping命令有很多參數,打開命令行直接輸入Ping后按下回車鍵,這樣就可以看到Ping命令的參數列表,如圖1所示。
圖1 Ping命令的參數列表
通常情況下,用戶都只是簡單Ping一下某個主機的地址。Ping命令的參數可以是主機名稱、域名和IP地址,后兩者是較為常用的。下面簡單演示一個Ping的例子,具體如下:
- C:\>ping 8.8.4.4
- Pinging 8.8.4.4 with 32 bytes of data:
- Reply from 8.8.4.4: bytes=32 time=57ms TTL=47
- Reply from 8.8.4.4: bytes=32 time=54ms TTL=47
- Reply from 8.8.4.4: bytes=32 time=54ms TTL=47
- Reply from 8.8.4.4: bytes=32 time=51ms TTL=47
- Ping statistics for 8.8.4.4:
- Packets: Sent = 4, Received = 4, Lost = 0 (0% loss),
- Approximate round trip times in milli-seconds:
- Minimum = 51ms, Maximum = 57ms, Average = 54ms
上面就是使用Ping命令對8.8.4.4這個IP進行回顯請求后的輸出信息。這里來解釋一下請求后的回顯信息的含義。
- Pinging 8.8.4.4 with 32 bytes of data:
正在將32字節(jié)數據發(fā)送到遠程主機8.8.4.4,如果Ping的是一個域名或主機名的話,這里會將域名(主機名)轉換為IP地址顯示出來。
- Reply from 8.8.4.4: bytes=32 time=57ms TTL=47
本地主機已經收到回顯應答信息,bytes=32表示有32字節(jié),time=57ms表示公用了57毫秒,TTL表示的是生存時間值,該值可以進行設置,該值最大為255。每個處理數據包的路由器都需要把TTL的值減1或減去數據包在路由器中停留的秒數。由于大多數路由器轉發(fā)數據包的延時都小于1秒,因此TTL最終成為一個跳站的計數器,所經過的每個路由器都將其值減1,當該值被減到0值時,該包將被丟棄。
- Ping statistics for 8.8.4.4:
- Packets: Sent = 4, Received = 4, Lost = 0 (0% loss),
- Approximate round trip times in milli-seconds:
- Minimum = 51ms, Maximum = 57ms, Average = 54ms
Ping 8.8.4.4的統(tǒng)計信息為:Sent=4表示發(fā)送了4個數據包,Received=4表示接收了4個數據包,Lost=0(0% loss)表示丟失的數據包是0個,丟包率為0%。
發(fā)送時間的大概情況:Mininum=51ms,最快是51ms,Maximum=57ms,最慢是57ms,Average=54ms,平均為54ms。
2. Ping命令的構造
Ping命令依賴的不是TCP,也不是UDP,它依賴的是ICMP。ICMP是IP層的協(xié)議之一,它傳遞差錯報文以及其他需要注意的信息。ICMP報文通常被IP層或高層協(xié)議使用。ICMP封裝在IP數據報內部,如圖2所示。
圖2 ICMP封裝在IP數據報內部
ICMP報文的格式如圖3所示。
圖3 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 號,通常設置為進程 ID
- unsigned short icmp_sequence; // 序列號
- unsigned long icmp_timestamp; // 時間戳
- };
ICMP的數據結構在網絡開發(fā)中會經常用到,可以將其保存以備后用。
明白了ICMP協(xié)議的數據結構,現(xiàn)在用抓包工具(也可以稱為協(xié)議分析工具)Wireshark來分析一下ICMP結構真實的情況,如圖4所示。
圖4 ICMP數據結構分析
在圖4中,標識1的部分是對協(xié)議進行過濾設置的,在該部分輸入“ICMP”可以讓Wireshark只顯示ICMP的數據記錄。相應地,可以輸入“TCP”、“UDP”、“HTTP”等協(xié)議進行篩選過濾。標識2的部分用于顯示篩選后的ICMP記錄,從這里可以明顯看出源IP地址、目的IP地址和協(xié)議的類型。標識3的部分用于顯示ICMP數據結構的值和附加的數據內容。最下面的部分顯示了數據的原始的二進制數據,在熟練掌握協(xié)議后,查看原始的二進制數據也并不是不可能的。
3. Ping命令的實現(xiàn)
有了前面的基礎,就可以構造自己的ICMP數據報來構造自己的Ping命令了。首先,定義兩個常量,還有計算校驗和的函數,具體如下:
- struct icmp_header
- {
- unsigned char icmp_type; // 消息類型
- unsigned char icmp_code; // 代碼
- unsigned short icmp_checksum; // 校驗和
- unsigned short icmp_id; // 用來唯一標識此請求的 ID 號,通常設置為進程 ID
- unsigned short icmp_sequence; // 序列號
- unsigned long icmp_timestamp; // 時間戳
- };
- #define ICMP_HEADER_SIZE sizeof(icmp_header)
- #define ICMP_ECHO_REQUEST 0x08
- #define ICMP_ECHO_REPLY 0x00
- // 計算校驗和
- 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);
- sendto(s, szBuff, sizeof(szBuff), 0, (SOCKADDR *)&dest_addr, sizeof(SOCKADDR));
- recvfrom(s, szRecvBuff, MAXBYTE, 0, (SOCKADDR *)&from_addr, &nLen);
- // 判斷接收到的是否是自己請求的地址
- if ( lstrcmp(inet_ntoa(from_addr.sin_addr), szDestIp) )
- {
- bRet = FALSE;
- }
- else
- {
- struct icmp_header *pIcmp1 = (icmp_header *)(szRecvBuff + 20);
- printf("%s\r\n", inet_ntoa(from_addr.sin_addr));
- }
- return bRet;
- }
這就是Ping命令的全部代碼了。自己寫一個函數調用它進行測試。
在Windows XP以上的操作系統(tǒng)中運行時,比如Windows 8系統(tǒng),程序可能會無法正常的運行,這是因為操作系統(tǒng)權限所導致的。在被編譯好的程序上單擊右鍵,在彈出的菜單上選擇“以管理員身份運行”,這樣程序就可以正常的執(zhí)行了。