邪恶八进制信息安全团队技术讨论组's Archiver

金州 2006-1-17 03:45

[转载]透析ICMP协议

文章作者:bugfree
信息来源:net130

透析ICMP协议(一): 协议原理

对于熟悉网络的人来说, ICMP是再熟悉不过了. 它同IP协议一样工作在ISO模型的网络层, 它的全称是: Internet Control Message Protocal.  其在网络中的主要作用是:
- 主机探测
- 路由维护
- 路由选择
- 流量控制

对于主机探测来说有很多方法,主机某些服务的BANNER,一些使用的应用程序,或者使用工具来检测主机,如NMAP,在WEB上有[url]www.netcraft.com[/url]来简单的估测主机。下面所讲的是使用ICMP协议来探测主机,主要也是可以了解ICMP这个协议,这里最主要的也是将这个ICMP协议,

首先我来讲一下主机探测用到的ICMP报文我没有一一讲全部报文,详细请参见RFC792协议)

1. 回送或回送响应

  我们使用一个ICMPECHO数据包来探测主机地址是否存活(当然在主机没有被配置为过滤ICMP形式),通过简单的发送一个ICMPECHO(Type 8)数据包到目标主机,如果ICMPECHOReply(ICMPtype0)数据包接受到,说明主机是存活状态。  如果没有就可以初步判断主机没有在线或者使用了某些过滤设备过滤了ICMP的REPLY。这种机制就是我们通常所用的ping命令来检测目标主机是否可以ping到.

回送消息的源地址是回送响应消息的目的地址。若要形成一个回送响应消息,应该将源和目的地址交换,将类型代码更改为0,重新计算机校验码。

下面是这个报文的格式:

   0             1             2             3
   0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  |    Type      |    Code    |       Checksum         |
  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  |        Identifier        |      Sequence Number      |
  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  |    Data ...
  +-+-+-+-+-

类型:
8代表回送消息;
0代表回送响应消息。
代码:0
校验码:
16位数据(从ICMP类型开始)的反码和再取反而得。为计算校验码,校验码域应该为零。这些零在 以后会被校验码取代。
标识符:如果代码=0,帮助匹配回送和回送响应的代码可以为0。
序列码:如果代码=0,帮助匹配回送和回送响应的序列码可以为0。
说明:
回送消息中接收到的消息应该在回送响应消息中返回。标识符和序列码由回送发送者使用帮助匹配
回送请求的响应。代码: 从主机或网关接收0


2. 超时报文

   0             1             2             3
   0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  |    Type    |    Code    |       Checksum         |
  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  |                    unused                   |
  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  |    Internet Header + 64 bits of Original Data Datagram    |
  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  
类型:11
代码:
0 = 传送超时;
1 = 分段级装超时。
校验码:
16位数据(从ICMP类型开始)的反码和再取反而得。为计算校验码,校验码域应该为零。
这些零在以后会被校验码取代。
Internet包头+64位源数据报数据:
Internet包头加上源数据的头64位而得。此数据用于主机匹配信息到相应的进程。
如果高层协议使用端口号,应该假设其在源数据的头64个字节之中。
说明:
如果网关在处理数据报时发现生存周期域为零,此数据报必须抛弃。网关同时必须通过超
时信息通知源主机。如果主机在组装分段的数据报时因为丢失段未能在规定时间内组装数据,
此数据报必须抛弃。网关发送超时信息。
如果段零不可用则不用发送超时信息。
代码0由网关发送,代码1由主机发送。



3. 目标主机不可达报文

   0             1             2             3
   0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  |    Type    |    Code    |       Checksum         |
  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  |                    unused                   |
  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  |    Internet Header + 64 bits of Original Data Datagram    |
  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  
类型:3
代码:
0 = 网络不可达;
1 = 主机不可达;
2 = 协议不可用;
3 = 端口不可达;
4 = 需要段和DF设置;
5 = 源路由失败;

校验码:
16位数据(从ICMP类型开始)的反码和再取反而得。为计算校验码,校验码域应该为零。
这些零在以后会被校验码取代。
Internet包头+源数据报:
Internet包头加上源数据的头64位而得。此数据用于主机匹配信息到相应的进程。
如果高层协议使用端口号,应该假设其在源数据的头64个字节之中。
说明:
相应于网关的路由表,如果在目的域中指定的网络不可达,如网络距离为无限远,网关会向发送
源数据的主机发送目的不可达消息。而且,在一些网络中,网关有能力决定目的主机是否可达。
如果目的地不可达,它将向发送源数据的主机发送不可达信息。
在目的主机,如果IP模块因为指定的协议模块和进程端口不可用而不能提交数据报,目的主机将
向发送源数据的主机发送不可达信息。

另外一种情况是当数据报必须被分段传送,而“不可分段”位打开,在这种情况下,网关必须抛弃
此数据报,并向向发送源数据的主机发送不可达信息。

代码0,1,4和5由网关发送,而代码2和3由主机发送。

透析ICMP协议(二): Windows Socket简介

Windows 的Socket函数有许多, 我没有做详细介绍, 这里的函数都是简要说明其用途, 详细用法请参考MSDN.
这里的主要目的是为了后面的三个应用服务.

函数说明:
---------

WSAStartup函数
初始化Winsock
[声明]
int WSAStarup(WORD wVersionRequested,LPWSADATA lpWSAData);
[参数]
wVersionRequested - 要求使用Winsock的最低版本号
lpWSAData - Winsock的详细资料
[返回值]
当函数成功调用时返回0
失败时返回非0的值
---

socket函数
用于生成socket(soket Descriptor)
[声明]
SOCKET socket(int af,int type,int protocol);
[参数]
af - 地址家族(通常使用:AF_INET)
type - socket的种类
SOCK_STREAM : 用于TCP协议
SOCK_DGRAM : 用于UDP协议
protocol - 所使用的协议
[返回值]
当函数成功调用时返回一个新的SOCKET(Socket Descriptor)
失败时返回INVALID_SOCKET.
---

inet_addr函数
地址转换, 把"A.B.C.D"的IP地址转换为32位长整数
[声明]
unsigned long inet_addr ( const char FAR *cp );
[参数]
cp - 指向IP地址字符串的指针
[返回值]
当函数成功调用时返回用32位整数表示的IP地址
失败时返回INADDR_NONE.
---

gethostbyname函数
从主机名获取主机信息.
[声明]
struct hostent FAR * gethostbyname ( const char FAR *name );
[参数]
name - 指向主机名字符串的指针
[返回值]
当函数成功调用时返回主机信息
失败时返回NULL(空值)

---

recv函数
利用Socket进行接受数据.
[声明]
int recv ( SOCKET s , char FAR *buf , int len , int flags );
[参数]
s - 指向用Socket函数生成的Socket Descriptor
buf - 接受数据的缓冲区(数组)的指针
len - 缓冲区的大小
flag - 调用方式(MSG_PEEK 或 MSG_OOB)
[返回值]
成功时返回收到的字节数.
如果连接被中断则返回0
失败时返回 SOCKET_ERROR

---

sendto函数
发送数据.
[声明]
int sendto ( SOCKET s , const char FAR *buf , int len , int flags , const struct sockaddr FAR *to , int token );
[参数]
s - 指向用Socket函数生成的Socket Descriptor
buf - 接受数据的缓冲区(数组)的指针
len - 缓冲区的大小
flag - 调用方式(MSG_DONTROUTE , MSG_OOB)
to - 指向发送方SOCKET地址的指针
token - 发送方SOCKET地址的大小
[返回值]
成功时返回已经发送的字节数.
失败时返回SOCKET_ERROR

透析ICMP协议(三): 应用篇ping(ICMP.dll)
原理简介:
--------
这个例子演示了应用微软的ICMP.DLL怎样"ping"另一台机器. 这个DLL是没有文档话的发送ICMP回送包API接口, 也称为"pings," 就像潜水员对声纳信号的术语一样. 这段代码出自一个被一个名叫MarkG的家伙的GUI程序, 他的网页已经消失了.

ICMP.DLL API 现在在Windows平台上与微软的Winsocks工作的很好, 但是微软说更好的产品一出来他们将替换它. 微软说这个自从Windows 95时代就在用, 这些功能在在Windows 2000上仍然存在.

For more information on the ICMP.DLL API, check out sockets.com's ICMP API page.
更详细的ICMP.DLL API的信息到sockets.com的ICMP API网页获取.


具体实现:
--------
// Borland C++ 5.0: bcc32.cpp ping.cpp
// Visual C++ 5.0:  cl ping.cpp wsock32.lib
//
// This sample program is hereby placed in the public domain.

#include <iostream.h>
#include <winsock.h>
#include <windowsx.h>
#include "icmpdefs.h"

==================ping的实现部分==================
int doit(int argc, char* argv[])
{//[bugfree] 建议将这个argc和argv的处理拿到main函数中
   // 检查命令行参数
   if (argc < 2) {
      cerr << "usage: ping <host>" << endl;
      return 1;
   }
   
   // 装载ICMP.DLL连接库
   HINSTANCE hIcmp = LoadLibrary("ICMP.DLL");
   if (hIcmp == 0) {
      cerr << "Unable to locate ICMP.DLL!" << endl;
      return 2;
   }

   // 查找给定机器的IP地址信息
   struct hostent* phe;
   if ((phe = gethostbyname(argv[1])) == 0) {
      cerr << "Could not find IP address for " << argv[1] << endl;
      return 3;
   }

   // 定义函数三个指针类型
   typedef HANDLE (WINAPI* pfnHV)(VOID);
   typedef BOOL (WINAPI* pfnBH)(HANDLE);
   typedef DWORD (WINAPI* pfnDHDPWPipPDD)(HANDLE, DWORD, LPVOID, WORD,
        PIP_OPTION_INFORMATION, LPVOID, DWORD, DWORD); // evil, no?
   //定义三个指针函数
   pfnHV pIcmpCreateFile;
   pfnBH pIcmpCloseHandle;
   pfnDHDPWPipPDD pIcmpSendEcho;
   
   //从ICMP.DLL中得到函数入口地址
   pIcmpCreateFile = (pfnHV)GetProcAddress(hIcmp,  "IcmpCreateFile");
   pIcmpCloseHandle = (pfnBH)GetProcAddress(hIcmp, "IcmpCloseHandle");
   pIcmpSendEcho = (pfnDHDPWPipPDD)GetProcAddress(hIcmp, "IcmpSendEcho");
   if ((pIcmpCreateFile == 0) || (pIcmpCloseHandle == 0) ||
        (pIcmpSendEcho == 0)) {
      cerr << "Failed to get proc addr for function." << endl;
      return 4;
   }

   // 打开ping服务
   HANDLE hIP = pIcmpCreateFile();
   if (hIP == INVALID_HANDLE_VALUE) {
      cerr << "Unable to open ping service." << endl;
      return 5;
   }
  
   // 构造ping数据包
   char acPingBuffer[64];
   memset(acPingBuffer, &#39;\xAA&#39;, sizeof(acPingBuffer));
   PIP_ECHO_REPLY pIpe = (PIP_ECHO_REPLY)GlobalAlloc( GMEM_FIXED | GMEM_ZEROINIT,
        sizeof(IP_ECHO_REPLY) + sizeof(acPingBuffer));
   if (pIpe == 0) {
      cerr << "Failed to allocate global ping packet buffer." << endl;
      return 6;
   }
   pIpe->Data = acPingBuffer;
   pIpe->DataSize = sizeof(acPingBuffer);   

   // 发送ping数据包
   DWORD dwStatus = pIcmpSendEcho(hIP, *((DWORD*)phe->h_addr_list[0]),
        acPingBuffer, sizeof(acPingBuffer), NULL, pIpe,
        sizeof(IP_ECHO_REPLY) + sizeof(acPingBuffer), 5000);
   if (dwStatus != 0) {
      cout << "Addr: " <<
           int(LOBYTE(LOWORD(pIpe->Address))) << "." <<
           int(HIBYTE(LOWORD(pIpe->Address))) << "." <<
           int(LOBYTE(HIWORD(pIpe->Address))) << "." <<
           int(HIBYTE(HIWORD(pIpe->Address))) << ", " <<
           "RTT: " << int(pIpe->RoundTripTime) << "ms, " <<
           "TTL: " << int(pIpe->Options.Ttl) << endl;
   }
   else {
      cerr << "Error obtaining info from ping packet." << endl;
   }

   // 关闭,回收资源
   GlobalFree(pIpe);
   FreeLibrary(hIcmp);
   return 0;
}
==================主函数==================
int main(int argc, char* argv[])
{
   WSAData wsaData;
   if (WSAStartup(MAKEWORD(1, 1), &wsaData) != 0) {
      return 255;
   }

   int retval = doit(argc, argv);

   WSACleanup();
   return retval;
}

==================头文件==================
icmpdefs.h
//ICMP.DLL 函数中需要的结构


typedef struct {
   unsigned char Ttl;                 // Time To Live
   unsigned char Tos;                 // Type Of Service
   unsigned char Flags;                // IP header flags
   unsigned char OptionsSize;            // Size in bytes of options data
   unsigned char *OptionsData;           // Pointer to options data
} IP_OPTION_INFORMATION, * PIP_OPTION_INFORMATION;

typedef struct {
   DWORD Address;                    // Replying address
   unsigned long  Status;              // Reply status
   unsigned long  RoundTripTime;          // RTT in milliseconds
   unsigned short DataSize;             // Echo data size
   unsigned short Reserved;             // Reserved for system use
   void *Data;                      // Pointer to the echo data
   IP_OPTION_INFORMATION Options;         // Reply options
} IP_ECHO_REPLY, * PIP_ECHO_REPLY;


透析ICMP协议(四): 应用篇ping(RAW Socket)

原理简介:
--------
用RAW Socket实现的ping可能比上一节的应用ICMP.DLL的程序庞大些, 但是这才是我们需要关注的东西, 我的观点真正想做网络开发的程序员应该静下心来读读这篇文章, 相信你会从中获益颇多. 中间我也会讲解一些东西为后一章的路由追踪做一些铺垫.
另一个重要的要讲的东西, 微软宣布随时不支持上节讲的ping用到的开发接口, 但是本节的讲的是更一般的东西. 所以它不会过时, 甚至做很小的改动就可以移植到别的系统上去. 系统移植不是我们的讲的重点. 但是微软的长期支持足以引起我们充分的重视.
如何少作变动来使的这个程序实现追踪路由的功能, 这里只是抛砖引玉. 将ICMP包中IP包的包头该为特定的值就能得到那个路由器的IP(要求到达目的地的跳数大于你设的特定值).
这个程序需要windows2k/WindowsXP/WindowsNT平台和系统管理员的权限.

具体实现:
--------
这段源代码大部分来自:
  [url]http://tangentsoft.net/wskfaq/examples/rawping.html[/url]
[bugfree]只做了少量修改,给出了大量的注释, 最后结合经验给出了自己的建议.

----------

/*
* 程序名: rawping_driver.cpp
* 说明:   
*     驱动程序,也是主函数
*/
#include <winsock2.h>  
#include <iostream.h>

#include "rawping.h"

#define DEFAULT_PACKET_SIZE 32  // 默认ICMP包字节数
#define DEFAULT_TTL 30        // 默认TTL值
#define MAX_PING_DATA_SIZE 1024  // 最大数据块
#define MAX_PING_PACKET_SIZE (MAX_PING_DATA_SIZE + sizeof(IPHeader)) //最大ICMP包长度

/* 为 send_buf 和 recv_buf 分配内存
* send_buf大小为 packet_size
* recv_buf大小为 MAX_PING_PACKET_SIZE, 保证大于send_buf
*/
int allocate_buffers(ICMPHeader*& send_buf, IPHeader*& recv_buf,
      int packet_size);


///////////////////////////////////////////////////////////////////////
// Program entry point

int main(int argc, char* argv[])
{
      
   int seq_no = 0;  //用在发送和接受的ICMP包头中
   ICMPHeader* send_buf = 0;
   IPHeader* recv_buf = 0;

   // 判断命令行是否合法
   if (argc < 2) {
      cerr << "usage: " << argv[0] << " <host> [data_size] [ttl]" <<
           endl;
      cerr << "\tdata_size can be up to " << MAX_PING_DATA_SIZE <<
           " bytes.  Default is " << DEFAULT_PACKET_SIZE << "." <<
           endl;
      cerr << "\tttl should be 255 or lower.  Default is " <<
           DEFAULT_TTL << "." << endl;
      return 1;
   }

   // 处理命令行参数
   int packet_size = DEFAULT_PACKET_SIZE;
   int ttl = DEFAULT_TTL;
   if (argc > 2) {
      int temp = atoi(argv[2]);
      if (temp != 0) {
        packet_size = temp;
      }
      if (argc > 3) {
        temp = atoi(argv[3]);
        if ((temp >= 0) && (temp <= 255)) {
           ttl = temp;
        }
      }
   }
   packet_size = max(sizeof(ICMPHeader),
        min(MAX_PING_DATA_SIZE, (unsigned int)packet_size));

   // 启动 Winsock
   WSAData wsaData;
   if (WSAStartup(MAKEWORD(2, 1), &wsaData) != 0) {
      cerr << "Failed to find Winsock 2.1 or better." << endl;
      return 1;
   }

   SOCKET sd; // RAW Socket句柄
   sockaddr_in dest, source;
   
   // 三个任务(创建sd, 设置ttl, 初试dest的值)
   if (setup_for_ping(argv[1], ttl, sd, dest) < 0) {
      goto cleanup; //释放资源并退出
   }
   // 为send_buf和recv_buf分配内存
   if (allocate_buffers(send_buf, recv_buf, packet_size) < 0) {
      goto cleanup;
   }
   // 初试化IMCP数据包(type=8,code=0)
   init_ping_packet(send_buf, packet_size, seq_no);

   // 发送ICMP数据包
   if (send_ping(sd, dest, send_buf, packet_size) >= 0) {
      while (1) {
        // 接受回应包
        if (recv_ping(sd, source, recv_buf, MAX_PING_PACKET_SIZE) <
              0) {
           // Pull the sequence number out of the ICMP header.  If
           // it&#39;s bad, we just complain, but otherwise we take
           // off, because the read failed for some reason.
           unsigned short header_len = recv_buf->h_len * 4;
           ICMPHeader* icmphdr = (ICMPHeader*)
                ((char*)recv_buf + header_len);
           if (icmphdr->seq != seq_no) {
              cerr << "bad sequence number!" << endl;
              continue;
           }
           else {
              break;
           }
        }
        if (decode_reply(recv_buf, packet_size, &source) != -2) {
           // Success or fatal error (as opposed to a minor error)
           // so take off.
           break;
        }
      }
   }

cleanup:
   delete[]send_buf;  //释放分配的内存
   delete[]recv_buf;
   WSACleanup(); // 清理winsock
   return 0;
}

// 为send_buf 和 recv_buf的内存分配. 太简单, 我略过
int allocate_buffers(ICMPHeader*& send_buf, IPHeader*& recv_buf,
      int packet_size)
{
   // First the send buffer
   send_buf = (ICMPHeader*)new char[packet_size];  
   if (send_buf == 0) {
      cerr << "Failed to allocate output buffer." << endl;
      return -1;
   }

   // And then the receive buffer
   recv_buf = (IPHeader*)new char[MAX_PING_PACKET_SIZE];
   if (recv_buf == 0) {
      cerr << "Failed to allocate output buffer." << endl;
      return -1;
   }
   
   return 0;
}

/*
* 程序名: rawping.h
* 说明:   
*     主要函数库头文件
*/

#define WIN32_LEAN_AND_MEAN
#include <winsock2.h>

// ICMP 包类型, 具体参见本文的第一节
#define ICMP_ECHO_REPLY 0  
#define ICMP_DEST_UNREACH 3
#define ICMP_TTL_EXPIRE 11
#define ICMP_ECHO_REQUEST 8

// 最小的ICMP包大小
#define ICMP_MIN 8


// IP 包头
struct IPHeader {
   BYTE h_len:4;        // Length of the header in dwords
   BYTE version:4;      // Version of IP
   BYTE tos;          // Type of service
   USHORT total_len;     // Length of the packet in dwords
   USHORT ident;        // unique identifier
   USHORT flags;        // Flags
   BYTE ttl;          // Time to live, 这个字段我在下一节中用来实现Tracert功能
   BYTE proto;         // Protocol number (TCP, UDP etc)
   USHORT checksum;      // IP checksum
   ULONG source_ip;
   ULONG dest_ip;
};

// ICMP 包头(实际的包不包括timestamp字段,
// 作者用来计算包的回应时间,其实完全没有必要这样做)
struct ICMPHeader {
   BYTE type;       // ICMP packet type
   BYTE code;       // Type sub code
   USHORT checksum;
   USHORT id;
   USHORT seq;
   ULONG timestamp;   // not part of ICMP, but we need it
};


extern USHORT ip_checksum(USHORT* buffer, int size);
extern int setup_for_ping(char* host, int ttl, SOCKET& sd,  sockaddr_in& dest);
extern int send_ping(SOCKET sd, const sockaddr_in& dest, ICMPHeader* send_buf, int packet_size);
extern int recv_ping(SOCKET sd, sockaddr_in& source, IPHeader* recv_buf,
      int packet_size);
extern int decode_reply(IPHeader* reply, int bytes, sockaddr_in* from);
extern void init_ping_packet(ICMPHeader* icmp_hdr, int packet_size, int seq_no);

/*
* 程序名: rawping.cpp
* 说明:   
*     主要函数库实现部分
*/

include <winsock2.h>
#include <ws2tcpip.h>
#include <iostream.h>
#include "rawping.h"

// 计算ICMP包的校验和的简单算法, 很多地方都有说明, 这里没有必要详细将
// 只是一点要提, 做校验之前, 务必将ICMP包头的checksum字段置为0
USHORT ip_checksum(USHORT* buffer, int size)
{
   unsigned long cksum = 0;
   
   // Sum all the words together, adding the final byte if size is odd
   while (size > 1) {
      cksum += *buffer++;
      size -= sizeof(USHORT);
   }
   if (size) {
      cksum += *(UCHAR*)buffer;
   }

   // Do a little shuffling
   cksum = (cksum >> 16) + (cksum & 0xffff);
   cksum += (cksum >> 16);
   
   // Return the bitwise complement of the resulting mishmash
   return (USHORT)(~cksum);
}

//初试化RAW Socket, 设置ttl, 初试化dest
// 返回值 <0 表失败

int setup_for_ping(char* host, int ttl, SOCKET& sd, sockaddr_in& dest)
{
   // Create the socket
   sd = WSASocket(AF_INET, SOCK_RAW, IPPROTO_ICMP, 0, 0, 0);
   if (sd == INVALID_SOCKET) {
      cerr << "Failed to create raw socket: " << WSAGetLastError() <<
           endl;
      return -1;
   }

   if (setsockopt(sd, IPPROTO_IP, IP_TTL, (const char*)&ttl,
        sizeof(ttl)) == SOCKET_ERROR) {
      cerr << "TTL setsockopt failed: " << WSAGetLastError() << endl;
      return -1;
   }

   // Initialize the destination host info block
   memset(&dest, 0, sizeof(dest));

   // Turn first passed parameter into an IP address to ping
   unsigned int addr = inet_addr(host);
   if (addr != INADDR_NONE) {
      // It was a dotted quad number, so save result
      dest.sin_addr.s_addr = addr;
      dest.sin_family = AF_INET;
   }
   else {
      // Not in dotted quad form, so try and look it up
      hostent* hp = gethostbyname(host);
      if (hp != 0) {
        // Found an address for that host, so save it
        memcpy(&(dest.sin_addr), hp->h_addr, hp->h_length);
        dest.sin_family = hp->h_addrtype;
      }
      else {
        // Not a recognized hostname either!
        cerr << "Failed to resolve " << host << endl;
        return -1;
      }
   }

   return 0;
}



//初试化ICMP的包头, 给data部分填充数据, 最后计算整个包的校验和

void init_ping_packet(ICMPHeader* icmp_hdr, int packet_size, int seq_no)
{
   // Set up the packet&#39;s fields
   icmp_hdr->type = ICMP_ECHO_REQUEST;
   icmp_hdr->code = 0;
   icmp_hdr->checksum = 0;
   icmp_hdr->id = (USHORT)GetCurrentProcessId();
   icmp_hdr->seq = seq_no;
   icmp_hdr->timestamp = GetTickCount();

   // "You&#39;re dead meat now, packet!"
   const unsigned long int deadmeat = 0xDEADBEEF;
   char* datapart = (char*)icmp_hdr + sizeof(ICMPHeader);
   int bytes_left = packet_size - sizeof(ICMPHeader);
   while (bytes_left > 0) {
      memcpy(datapart, &deadmeat, min(int(sizeof(deadmeat)),
           bytes_left));
      bytes_left -= sizeof(deadmeat);
      datapart += sizeof(deadmeat);
   }

   // Calculate a checksum on the result
   icmp_hdr->checksum = ip_checksum((USHORT*)icmp_hdr, packet_size);
}

// 发送生成的ICMP包
// 返回值 <0 表失败

int send_ping(SOCKET sd, const sockaddr_in& dest, ICMPHeader* send_buf,
      int packet_size)
{
   // Send the ping packet in send_buf as-is
   cout << "Sending " << packet_size << " bytes to " <<
        inet_ntoa(dest.sin_addr) << "..." << flush;
   int bwrote = sendto(sd, (char*)send_buf, packet_size, 0,
        (sockaddr*)&dest, sizeof(dest));
   if (bwrote == SOCKET_ERROR) {
      cerr << "send failed: " << WSAGetLastError() << endl;
      return -1;
   }
   else if (bwrote < packet_size) {
      cout << "sent " << bwrote << " bytes..." << flush;
   }

   return 0;
}


// 接受ICMP包
// 返回值 <0 表失败
int recv_ping(SOCKET sd, sockaddr_in& source, IPHeader* recv_buf,
      int packet_size)
{
   // Wait for the ping reply
   int fromlen = sizeof(source);
   int bread = recvfrom(sd, (char*)recv_buf,
        packet_size + sizeof(IPHeader), 0,
        (sockaddr*)&source, &fromlen);
   if (bread == SOCKET_ERROR) {
      cerr << "read failed: ";
      if (WSAGetLastError() == WSAEMSGSIZE) {
        cerr << "buffer too small" << endl;
      }
      else {
        cerr << "error #" << WSAGetLastError() << endl;
      }
      return -1;
   }

   return 0;
}


// 对收到的ICMP解码
// 返回值 -2表忽略, -1 表失败, 0 成功
int decode_reply(IPHeader* reply, int bytes, sockaddr_in* from)
{
   // 跳过IP包头, 找到ICMP的包头
   unsigned short header_len = reply->h_len * 4;
   ICMPHeader* icmphdr = (ICMPHeader*)((char*)reply + header_len);

   // 包的长度合法, header_len + ICMP_MIN为最小ICMP包的长度
   if (bytes < header_len + ICMP_MIN) {
      cerr << "too few bytes from " << inet_ntoa(from->sin_addr) <<
           endl;
      return -1;
   }
   // 下面的包类型详细参见我的第一部分 "透析ICMP协议(一): 协议原理"
   else if (icmphdr->type != ICMP_ECHO_REPLY) {  //非正常回复
      if (icmphdr->type != ICMP_TTL_EXPIRE) {  //ttl减为零
        if (icmphdr->type == ICMP_DEST_UNREACH) { //主机不可达
           cerr << "Destination unreachable" << endl;
        }
        else {  //非法的ICMP包类型
           cerr << "Unknown ICMP packet type " << int(icmphdr->type) <<
                " received" << endl;
        }
        return -1;
      }
   }
   else if (icmphdr->id != (USHORT)GetCurrentProcessId()) {
//不是本进程发的包, 可能是同机的其它ping进程发的
      return -2;
   }

   // 指出包传递了多远
   // [bugfree]我认为作者这里有问题, 因为有些系统的ttl初值为128如winXP,
   // 有些为256如我的DNS服务器211.97.168.129, 作者假设为256有点武断,
   // 可以一起探讨这个问题, 回email:zhangliangsd@hotmail.com
   int nHops = int(256 - reply->ttl);
   if (nHops == 192) {
      // TTL came back 64, so ping was probably to a host on the
      // LAN -- call it a single hop.
      nHops = 1;
   }
   else if (nHops == 128) {
      // Probably localhost
      nHops = 0;
   }

   // 所有工作结束,打印信息
   cout << endl << bytes << " bytes from " <<
        inet_ntoa(from->sin_addr) << ", icmp_seq " <<
        icmphdr->seq << ", ";
   if (icmphdr->type == ICMP_TTL_EXPIRE) {
      cout << "TTL expired." << endl;
   }
   else {
      cout << nHops << " hop" << (nHops == 1 ? "" : "s");
      cout << ", time: " << (GetTickCount() - icmphdr->timestamp) <<
           " ms." << endl;
   }

   return 0;
}


总结和建议:
-----------
  bugfree建议其中的这些方面需要改进:
   1. 头文件iostream.h 改为 iostream, 后者是标准C++的头文件
     同时添加对std::cout 和 std::endl;的引用
     对于cerr 建议都改为std::cout(因为后者头文件不支持)
   2. 程序的发送和接受采用了同步的方式, 这使得如果出现网络问题recv_ping将陷入持续等待.
     这是我们不想看到的.
     这三种技术可以达到目的:
      - 使用多线程, 将ping封装进线程, 在主程序中对它的超时进行处理
      - 使用select()函数来实现
      - 使用windows的 WSAAsyncSelect()
     这里对这些方法不作具体讨论, 留给读者自已完成.

透析ICMP协议(五): 应用篇路由追踪
原理简介:
--------
  通过前四节的介绍, 可能大家对ICMP的应用有了初步的了解. 不过开始本节之前我对ICMP协议再从宏观上做些介绍. 大家都知道ICMP是为于ISO的第三层---网络层。 既是它同IP协议为于同一层, 然而大家可能也只到,ICMP协议要用到IP协议, 所以有一些书上说ICMP位ISO的第四层, 那是错误的。 同样这样那些书上这样画的的例子也是错误的, 我就发现某外资通讯公司的资料上有这样两种错误的画法
  --------------------------      
  | ICMP | TCP(SCTP)  |
  --------------------------
  |      IP             |
  --------------------------
  
  ---------------------------
  |...      | TCP(SCTP)  |
  ---------------------------
  | ICMP |  IP           |
  ----------------------------

其实如上的画法是错误的, 正确地画法应为:
  ---------------------      
  |...  | TCP(SCTP)  |
  ---------------------
  | ICMP |            |
  ----------           |
  |     IP            |
  ---------------------

接下来,让我们来说明怎样实现追踪路由的功能, 大家通过我的第一节的阅读可能已经了解了超时报文的具体内容(参见透析ICMP协议(一):  协议原理), 它在如果网关在处理数据报时发现生存周期域(ttl)为零,此数据报必须抛弃。网关同时必须通过超时信息通知源主机。这是它的报文的具体结构:
   0             1             2             3
   0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  |    Type(11)  |    Code(0/1) |       Checksum         |
  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  |                    unused                   |
  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  |    Internet Header + 64 bits of Original Data Datagram    |
  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

通过利用setsockopt()函数设置ICMP包的IP包头中的ttl字段便可以达到这种效果。 具体过程如下, 假设你的IP到达目标地址需要过n个路由器(n>1)。 则
1. 初始化第一个ICMP包,并设置IP包头中的TTL为1, 则得到第一个数据路由器发回的超时报文
2. 一般情况下:初始化第i(i<n)个ICMP包,并设置IP包头中的TTL为i, 则得到第一个数据路由器发回的超时报文

剩下的问题为如何确定超时ICMP报文的路由器IP地址得到它的机器名的信息。 这个问题可能很多读者都会求, 用gethostbyaddr()可以得到答案。

经过理论的论证后, 让我们看看如何实现。
  

具体实现:(具体如何初试化ICMP的数据包上节已有详细的介绍,这里只是补充路由追踪的代码)
--------
主要代码如下:

unsigned long ipback = 0; //超时报文的IP的初试值
unsigned long ms = 0; //超时值
struct hostent *hHost;
char m_address[256];

//直到找到目标主机, 或达到最大跳数(HOPS)
while (ipback != ipfinal){  
hHost = 0;


//对到目标主机中间的某个路由器发放ping的报文(ttl为1~N-1之间)
if (Ping(m_address,ttl,ipback,ms))
{
  sin.sin_family = AF_INET;
  sin.sin_addr.S_un.S_addr = ipback; // 由函数返回的IP地址
   // 查找主机名
  hHost = gethostbyaddr((char*)&sin.sin_addr, 4, PF_INET);
  //这里可以输出hHost的内容
}
ttl++;
if (ttl > MAX_HOPS)  //达到最大跳数
{
  break;
}
}

==================
ping函数的代码
==================
int Ping(const char * host, int ttl, unsigned long& ipback, unsigned long& ms)
{
SOCKET sockRaw;
struct sockaddr_in dest,from;
struct hostent * hp;
int bread,datasize;
int fromlen = sizeof(from);
int timeout = 100;
char *dest_ip;
char *icmp_data;
char *recvbuf;
unsigned int addr=0;
const int MAX_PACKET = 1024;

//初始化Socket
sockRaw = WSASocket (AF_INET,
    SOCK_RAW,
    IPPROTO_ICMP,
    NULL, 0, WSA_FLAG_OVERLAPPED);

if (sockRaw == INVALID_SOCKET)
{
  // 错误
}

  // 设置IP包头的ttl字段
bread = setsockopt(sockRaw, IPPROTO_IP, IP_TTL, (char *)&ttl, sizeof(int));
if(bread == SOCKET_ERROR)
{
  // 错误
}

// 设置接受超时为100ms
bread = setsockopt(sockRaw,SOL_SOCKET,SO_RCVTIMEO,(char*)&timeout,sizeof(timeout));
if(bread == SOCKET_ERROR)
{
  // 错误
}

//禁止用Nagle算法缓存数据
bread = setsockopt(sockRaw, SOL_SOCKET, TCP_NODELAY, (const char*)&killnagle, sizeof(int));
if (bread == SOCKET_ERROR)
{
  // 错误
}

timeout = 1000;
// 设置发送超时为100ms
bread = setsockopt(sockRaw,SOL_SOCKET,SO_SNDTIMEO,(char*)&timeout,
     sizeof(timeout));
if(bread == SOCKET_ERROR)
{
  // 错误
}

//下面的代码生成ICMP包
memset(&dest,0,sizeof(dest));
hp = gethostbyname(host);
if (!hp)
{
  addr = inet_addr(host);
}
if ((!hp)  && (addr == INADDR_NONE) )
{
  // 错误
}
if (hp != NULL)
  memcpy(&(dest.sin_addr),hp->h_addr,hp->h_length);
else
  dest.sin_addr.s_addr = addr;

//初始化dest
if (hp)
  dest.sin_family = hp->h_addrtype;
else
  dest.sin_family = AF_INET;

dest_ip = inet_ntoa(dest.sin_addr);

// 设置包长度
datasize = DEF_PACKET_SIZE;

// 计算包大小
datasize += sizeof(IcmpHeader);  

icmp_data = (char *)new[MAX_PACKET]; //分配内存,可以用new 和 delete
recvbuf = (char *)new[MAX_PACKET];

if (!icmp_data)
{
  // 释放内存,退出
}

if (!recvbuf)
{
  // 释放内存,退出 }
}

memset(icmp_data,0,MAX_PACKET);
fill_icmp_data(icmp_data,datasize); // 这个函数用来填充ICMP的数据包

int bwrote;
((IcmpHeader*)icmp_data)->i_cksum = 0;
((IcmpHeader*)icmp_data)->timestamp = GetTickCount(); // 存入当前时间值
((IcmpHeader*)icmp_data)->i_seq = seq_no++;

// 计算校验和
((IcmpHeader*)icmp_data)->i_cksum = checksum((USHORT*)icmp_data, datasize);

  // 为了最后计算ICMP包回来的总时间
unsigned long tc = GetTickCount();
//发送数据包
bwrote = sendto(sockRaw,icmp_data,datasize,0,(struct sockaddr*)&dest, sizeof(dest));

if (bwrote == SOCKET_ERROR)
{
  // 错误
}
if (bwrote < datasize ) //发送字节数对否
{
}

// 接受数据包
bread = recvfrom(sockRaw,recvbuf,MAX_PACKET,0,(struct sockaddr*)&from,
    &fromlen);
//计算总时间
ms = GetTickCount() - tc;

if (bread == SOCKET_ERROR)
{
  // 错误
}

// 得到返回的路由器
ipback = from.sin_addr.s_addr;

return 1;
}

===============================
函数fill_icmp_data()的源代码
===============================
//这个结构下面将用到
typedef struct _ihdr {
  BYTE i_type;
  BYTE i_code;
  USHORT i_cksum;
  USHORT i_id;
  USHORT i_seq;
  ULONG timestamp; /* 这不是ICMP包的一部分, 只是为了计算时间 */
}IcmpHeader;

void fill_icmp_data(char * icmp_data, int datasize){

  IcmpHeader *icmp_hdr;
  char *datapart;

  icmp_hdr = (IcmpHeader*)icmp_data;  

  icmp_hdr->i_type = ICMP_ECHO;
  icmp_hdr->i_code = 0;
  icmp_hdr->i_id = (USHORT)GetCurrentProcessId();
  icmp_hdr->i_cksum = 0;
  icmp_hdr->i_seq = 0;
  
  datapart = icmp_data + sizeof(IcmpHeader);  //计算数据域的开始地址
  // 初试化数据域
  memset(datapart,&#39;E&#39;, datasize - sizeof(IcmpHeader));

}

页: [1]
© 1999-2008 EvilOctal Security Team