套接字

来源:互联网 发布:淘宝原始头像 编辑:程序博客网 时间:2024/04/29 12:19
 
1.引言
大多数程序员所接触到的套接字(Socket)为两类:
(1)流式套接字(SOCK_STREAM):一种面向连接的Socket,针对于面向连接的TCP服务应用;
(2)数据报式套接字(SOCK_DGRAM):一种无连接的Socket,对应于无连接的UDP服务应用。
从用户的角度来看,SOCK_STREAM、SOCK_DGRAM这两类套接字似乎的确涵盖了TCP/IP应用的全部,因为基于TCP/IP的应用,从协议栈的层次上讲,在传输层的确只可能建立于TCP或UDP协议之上(图1),而SOCK_STREAM、SOCK_DGRAM又分别对应于TCP和UDP,所以几乎所有的应用都可以用这两类套接字实现。
 
图1 TCP/IP协议栈
但是,当我们面对如下问题时,SOCK_STREAM、SOCK_DGRAM将显得这样无助:
(1) 怎样发送一个自定义的IP包?
(2) 怎样发送一个ICMP协议包?
(3) 怎样使本机进入杂糅模式,从而能够进行网络sniffer?
(4) 怎样分析所有经过网络的包,而不管这样包是否是发给自己的?
(5) 怎样伪装本地的IP地址?
这使得我们必须面对另外一个深刻的主题――原始套接字(Raw Socket)。Raw Socket广泛应用于高级网络编程,也是一种广泛的黑客手段。著名的网络sniffer、拒绝服务攻击(DOS)、IP欺骗等都可以以Raw Socket实现。
Raw Socket与标准套接字(SOCK_STREAM、SOCK_DGRAM)的区别在于前者直接置“根”于操作系统网络核心(Network Core),而SOCK_STREAM、SOCK_DGRAM则“悬浮”于TCP和UDP协议的外围,如图2所示:
 
图2 Raw Socket与标准Socket
当我们使用Raw Socket的时候,可以完全自定义IP包,一切形式的包都可以“制造”出来。因此,本文事先必须对TCP/IP所涉及IP包结构进行必要的交待。
目前,IPv4的报头结构为:
版本号(4) 包头长(4) 服务类型(8) 数据包长度(16)
标识(16) 偏移量(16)
生存时间(8) 传输协议(8) 校验和(16)
源地址(32) 
目的地址(32) 
选项(8) ......... 填充
对其进行数据结构封装:
typedef struct _iphdr //定义IP报头
{
 unsigned char h_lenver; //4位首部长度+4位IP版本号
 unsigned char tos; //8位服务类型TOS
 unsigned short total_len; //16位总长度(字节)
 unsigned short ident; //16位标识
 unsigned short frag_and_flags; //3位标志位
 unsigned char ttl; //8位生存时间 TTL
 unsigned char proto; //8位协议 (TCP, UDP 或其他)
 unsigned short checksum; //16位IP首部校验和
 unsigned int sourceIP; //32位源IP地址
 unsigned int destIP; //32位目的IP地址
} IP_HEADER;
或者将上述定义中的第一字节按位拆分:
typedef struct _iphdr //定义IP报头
{
 unsigned char h_len : 4; //4位首部长度
 unsigned char ver : 4;   //4位IP版本号
 unsigned char tos;
 unsigned short total_len;
 unsigned short ident;
 unsigned short frag_and_flags;
 unsigned char ttl;
 unsigned char proto;
 unsigned short checksum;
 unsigned int sourceIP;
 unsigned int destIP;
} IP_HEADER;
更加严格地讲,上述定义中h_len、ver字段的内存存放顺序还与具体CPU的Endian有关,因此,更加严格的IP_HEADER可定义为:
typedef struct _iphdr //定义IP报头
{
#if defined(__LITTLE_ENDIAN_BITFIELD)
 unsigned char h_len : 4; //4位首部长度
 unsigned char ver : 4;   //4位IP版本号
#elif defined (__BIG_ENDIAN_BITFIELD)
 unsigned char ver : 4;   //4位IP版本号
 unsigned char h_len : 4; //4位首部长度
#endif
 unsigned char tos;
 unsigned short total_len;
 unsigned short ident;
 unsigned short frag_and_flags;
 unsigned char ttl;
 unsigned char proto;
 unsigned short checksum;
 unsigned int sourceIP;
 unsigned int destIP;
} IP_HEADER;
TCP报头结构为:
源端口(16) 目的端口(16)
序列号(32)
确认号(32)
TCP偏移量(4) 保留(6) 标志(6) 窗口(16)
校验和(16) 紧急(16)
选项(0或32)
数据(可变)
    对应数据结构:
typedef struct psd_hdr //定义TCP伪报头
{
 unsigned long saddr; //源地址
 unsigned long daddr; //目的地址
 char mbz;
 char ptcl; //协议类型
 unsigned short tcpl; //TCP长度
}PSD_HEADER;
typedef struct _tcphdr //定义TCP报头
{
 unsigned short th_sport; //16位源端口
 unsigned short th_dport; //16位目的端口
 unsigned int th_seq; //32位序列号
 unsigned int th_ack; //32位确认号
 unsigned char th_lenres;   //4位首部长度/4位保留字
 unsigned char th_flag; //6位标志位
 unsigned short th_win; //16位窗口大小
 unsigned short th_sum; //16位校验和
 unsigned short th_urp; //16位紧急数据偏移量
} TCP_HEADER;
同样地,TCP头的定义也可以将位域拆分:
typedef struct _tcphdr
{
 unsigned short th_sport;
 unsigned short th_dport;
 unsigned int th_seq;
 unsigned int th_ack;
 /*little-endian*/
 unsigned short tcp_res1: 4,  tcp_hlen: 4, tcp_fin: 1, tcp_syn: 1, tcp_rst: 1, tcp_psh: 1,     tcp_ack: 1, tcp_urg: 1, tcp_res2: 2;
 unsigned short th_win;
 unsigned short th_sum;
 unsigned short th_urp;
} TCP_HEADER;
UDP报头为:
源端口(16) 目的端口(16)
报文长(16) 校验和(16)
对应的数据结构为:
typedef struct _udphdr //定义UDP报头
{
 unsigned short uh_sport;//16位源端口
 unsigned short uh_dport;//16位目的端口
 unsigned short uh_len;//16位长度
 unsigned short uh_sum;//16位校验和
} UDP_HEADER;
ICMP协议是网络层中一个非常重要的协议,其全称为Internet Control Message Protocol(因特网控制报文协议),ICMP协议弥补了IP的缺限,它使用IP协议进行信息传递,向数据包中的源端节点提供发生在网络层的错误信息反馈。ICMP报头为:
类型(8) 代码(8) 校验和(16)
消息内容
常用的回送与或回送响应ICMP消息对应数据结构为:
typedef struct _icmphdr //定义ICMP报头(回送与或回送响应)
{
    unsigned char i_type;//8位类型
    unsigned char i_code; //8位代码
    unsigned short i_cksum; //16位校验和
    unsigned short i_id; //识别号(一般用进程号作为识别号)
    unsigned short i_seq; //报文序列号
    unsigned int timestamp;//时间戳
} ICMP_HEADER;
常用的ICMP报文包括ECHO-REQUEST(响应请求消息)、ECHO-REPLY(响应应答消息)、Destination Unreachable(目标不可到达消息)、Time Exceeded(超时消息)、Parameter Problems(参数错误消息)、Source Quenchs(源抑制消息)、Redirects(重定向消息)、Timestamps(时间戳消息)、Timestamp Replies(时间戳响应消息)、Address Masks(地址掩码请求消息)、Address Mask Replies(地址掩码响应消息)等,是Internet上十分重要的消息。后面章节中所涉及到的ping命令、ICMP拒绝服务攻击、路由欺骗都与ICMP协议息息相关。
 
2.Raw Socket基础
在进入Raw Socket多种强大的应用之前,我们先讲解怎样建立一个Raw Socket及怎样用建立的Raw Socket发送和接收IP包。
2.1建立Raw Socket
在Windows平台上,为了使用Raw Socket,需先初始化WINSOCK:
// 启动 Winsock
WSAData wsaData;
if (WSAStartup(MAKEWORD(2, 1), &wsaData) != 0)
{
  cerr << "Failed to find Winsock 2.1 or better." << endl;
  return 1;
}
MAKEWORD(2, 1)组成一个版本字段,2.1版,同样的,MAKEWORD(2, 2)意味着2.2版。MAKEWORD本身定义为:
inline word MakeWord(const byte wHigh, const byte wLow)
{
  return ((word)wHigh) << 8 | wLow;
}
因此MAKEWORD(2, 1)实际等同于0x0201。同样地,0x0101可等同于MAKEWORD(1, 1)。
与WSAStartup()的函数为WSACleanup(),在所有的socket都使用完后调用,如:
void sock_cleanup()
{
#ifdef WIN32
 sockcount--;
 if (sockcount == 0)
  WSACleanup();
#endif
}
接下来,定义一个Socket句柄:
SOCKET sd; // RAW Socket句柄
创建Socket并将句柄赋值给定义的sd,可以使用WSASocket()函数来完成,其原型为:
SOCKET WSASocket(int af, int type, int protocol, LPWSAPROTOCOL_INFO
  lpProtocolInfo, GROUP g, DWORD dwFlags);
其中的参数定义为:
af:地址家族,一般为AF_INET,指代IPv4(The Internet Protocol version 4)地址家族。
type:套接字类型,如果创建原始套接字,应该使用SOCK_RAW;
Protocol:协议类型,如IPPROTO_TCP、IPPROTO_UDP等;
lpProtocolInfo :WSAPROTOCOL_INFO结构体指针;
dwFlags:套接字属性标志。
例如,下面的代码定义ICMP协议类型的原始套接字:
sd = WSASocket(AF_INET, SOCK_RAW, IPPROTO_ICMP, 0, 0, 0);
创建Socket也可以使用socket()函数:
SOCKET WSAAPI socket( int af, int type, int protocol);
参数的定义与WSASocket()函数相同。
为了使用socket()函数创建的Socket,还需要将这个Socket与sockaddr绑定:
SOCKADDR_IN addr_in;
addr_in.sin_family = AF_INET;
addr_in.sin_port = INADDR_ANY;
addr_in.sin_addr.S_un.S_addr = GetLocalIP();
nRetCode = bind(sd, (struct sockaddr*) &addr_in, sizeof(addr_in));
if (SOCKET_ERROR == nRetCode)
{
  printf("BIND Error!%d/n", WSAGetLastError());
}
其中使用的struct sockaddr_in(即SOCKADDR_IN)为:
struct sockaddr_in
{
  unsigned short sin_family;
  unsigned short int sin_port;
  struct in_addr sin_addr;
  unsigned char sin_zero[8];
}
而bind()函数第二个参数的struct sockaddr类型定义为:
struct sockaddr
{
  unisgned short as_family;
  char sa_data[14];
};
实际上,bind()函数采用struct sockaddr是为了考虑兼容性,最终struct sockaddr和struct sockaddr_in的内存占用是等同的。struct sockaddr_in中的struct in_addr成员占用4个字节,为32位的IP地址,定义为:
typedef struct in_addr
{
  union
  {
    struct
    {
      u_char s_b1, s_b2, s_b3, s_b4;
    } S_un_b;
    struct
    {
      u_short s_w1, s_w2;
    } S_un_w;
    u_long S_addr;
  }
  S_un;
} IN_ADDR, *PIN_ADDR, FAR *LPIN_ADDR;
把32位的IP地址定义为上述联合体将使用户可以以字节、半字或字方式读写同一个IP地址。同志们,注意了,这个技巧在许多软件开发中定义数据结构时被广泛采用。
为了控制包的发送方式,我们可能会用到如下的这个十分重要的函数来设置套接字选项:
int setsockopt(
  SOCKET s,  //套接字句柄
  int level,   //选项level,如SOL_SOCKET
  int optname,   //选项名,如SO_BROADCAST
  const char* optval,  //选项值buffer指针
  int optlen   //选项buffer长度
);
例如,当level为SOL_SOCKET时,我们可以设置布尔型选项SO_BROADCAST从而控制套接字是否传送和接收广播消息。
下面的代码通过设置IPPROTO_IP level的IP_HDRINCL选项为TRUE从而使能程序员亲自处理IP包报头:
//设置 IP 头操作选项
BOOL flag = TRUE;
setsockopt(sd, IPPROTO_IP, IP_HDRINCL, (char*) &flag, sizeof(flag);
下面的函数用于控制套接字:
int ioctlsocket(
  SOCKET s,
  long cmd,  //命令
  u_long* argp  //命令参数指针
);
如下面的代码让socket接收所有报文(sniffer模式):
u_long iMode = 1;
ioctlsocket(sd, SIO_RCVALL, & iMode);       //让 sockRaw 接受所有的数据
2.2Raw Socket发送报文
发送报文的函数为:
 int sendto(
  SOCKET s,  //套接字句柄
  const char* buf,   //发送缓冲区
  int len,   //要发送的字节数
  int flags,  //方式标志
  const struct sockaddr* to, //目标地址
  int tolen  //目标地址长度
);

int send(
  SOCKET s,   //已经建立连接的套接字句柄
  const char* buf,
  int len,
  int flags
);
 send()函数的第1个参数只能是一个已经建立连接的套接字句柄,所以这个函数就不再需要目标地址参数输入。
 函数的返回值为实际发送的字节数,如果返回SOCKET_ERROR,可以通过WSAGetLastError()获得错误原因。请看下面的示例:
int bwrote = sendto(sd, (char*)send_buf, packet_size, 0, (sockaddr*) &dest,
  sizeof(dest));
if (bwrote == SOCKET_ERROR)
{
  //…发送失败
  if(WSAGetLastError()==…)
  {
  //…
  }
  return - 1;
}
else if (bwrote < packet_size)
{
  //…发送字节 < 欲发送字节
}
2.3Raw Socket接收报文
接收报文的函数为:
int recvfrom(
  SOCKET s,  //套接字句柄
  char* buf,  //接收缓冲区
  int len,    //缓冲区字节数
  int flags,   //方式标志
  struct sockaddr* from,  //源地址
  int* fromlen 
);

int recv(
  SOCKET s, //已经建立连接的套接字句柄
  char* buf,
  int len,
  int flags
);
recv()函数的第1个参数只能是一个已经建立连接的套接字句柄,所以这个函数就不再需要源地址参数输入。
函数的返回值为实际接收的字节数,如果返回SOCKET_ERROR,我们可以通过WSAGetLastError()函数获得错误原因。请看下面的示例:
int bread = recvfrom(sd, (char*)recv_buf, packet_size + sizeof(IPHeader), 0,
  (sockaddr*) &source, &fromlen);
if (bread == SOCKET_ERROR)
{
  //…读失败
  if(WSAGetLastError()==WSAEMSGSIZE)
  {
  //…接收buffer太小
  }
  return  - 1;
}
原始套接字按如下规则接收报文:若接收的报文中协议类型和定义的原始套接字匹配,那么,接收的所有数据拷贝入套接字中;如果套接字绑定了本地地址,那么只有接收数据IP头中对应的目的地址等于本地地址,接收到的数据才拷贝到套接字中;如果套接字定义了远端地址,那么,只有接收数据IP头中对应的源地址与远端地址匹配,接收的数据才拷贝到套接字中。
2.4建立报文
 在利用Raw Socket发送报文时,报文的IP头、TCP头、UDP头等需要程序员亲自赋值,从而达到极大的灵活性。下面的程序利用Raw Socket发送TCP报文,并完全手工建立报头:
 int sendTcp(unsigned short desPort, unsigned long desIP)
{
  WSADATA WSAData;
  SOCKET sock;
  SOCKADDR_IN addr_in;
  IPHEADER ipHeader;
  TCPHEADER tcpHeader;
  PSDHEADER psdHeader;
  char szSendBuf[MAX_LEN] =  { 0 };
  BOOL flag;
  int rect, nTimeOver;
  if (WSAStartup(MAKEWORD(2, 2), &WSAData) != 0)
  {
    printf("WSAStartup Error!/n");
    return false;
  }
  if ((sock = WSASocket(AF_INET, SOCK_RAW, IPPROTO_RAW, NULL, 0,
    WSA_FLAG_OVERLAPPED)) == INVALID_SOCKET)
  {
    printf("Socket Setup Error!/n");
    return false;
  }
  flag = true;
  if (setsockopt(sock, IPPROTO_IP, IP_HDRINCL, (char*) &flag, sizeof(flag)) ==
    SOCKET_ERROR)
  {
    printf("setsockopt IP_HDRINCL error!/n");
    return false;
  }
  nTimeOver = 1000;
  if (setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, (char*) &nTimeOver, sizeof
    (nTimeOver)) == SOCKET_ERROR)
  {
    printf("setsockopt SO_SNDTIMEO error!/n");
    return false;
  }
  addr_in.sin_family = AF_INET;
  addr_in.sin_port = htons(desPort);
  addr_in.sin_addr.S_un.S_addr = inet_addr(desIP);
  //填充IP报头
  ipHeader.h_verlen = (4 << 4 | sizeof(ipHeader) / sizeof(unsigned long));
  // ipHeader.tos=0;
  ipHeader.total_len = htons(sizeof(ipHeader) + sizeof(tcpHeader));
  ipHeader.ident = 1;
  ipHeader.frag_and_flags = 0;
  ipHeader.ttl = 128;
  ipHeader.proto = IPPROTO_TCP;
  ipHeader.checksum = 0;
  ipHeader.sourceIP = inet_addr("localhost");
  ipHeader.destIP = desIP;
  //填充TCP报头
  tcpHeader.th_dport = htons(desPort);
  tcpHeader.th_sport = htons(SOURCE_PORT); //源端口号
  tcpHeader.th_seq = htonl(0x12345678);
  tcpHeader.th_ack = 0;
  tcpHeader.th_lenres = (sizeof(tcpHeader) / 4 << 4 | 0);
  tcpHeader.th_flag = 2;  //标志位探测,2是SYN
  tcpHeader.th_win = htons(512);
  tcpHeader.th_urp = 0;
  tcpHeader.th_sum = 0;
  psdHeader.saddr = ipHeader.sourceIP;
  psdHeader.daddr = ipHeader.destIP;
  psdHeader.mbz = 0;
  psdHeader.ptcl = IPPROTO_TCP;
  psdHeader.tcpl = htons(sizeof(tcpHeader));
  //计算校验和
  memcpy(szSendBuf, &psdHeader, sizeof(psdHeader));
  memcpy(szSendBuf + sizeof(psdHeader), &tcpHeader, sizeof(tcpHeader));
  tcpHeader.th_sum = checksum((unsigned short*)szSendBuf, sizeof(psdHeader) + sizeof
    (tcpHeader));
  memcpy(szSendBuf, &ipHeader, sizeof(ipHeader));
  memcpy(szSendBuf + sizeof(ipHeader), &tcpHeader, sizeof(tcpHeader));
  memset(szSendBuf + sizeof(ipHeader) + sizeof(tcpHeader), 0, 4);
  ipHeader.checksum = checksum((unsigned short*)szSendBuf, sizeof(ipHeader) + sizeof
    (tcpHeader));
  memcpy(szSendBuf, &ipHeader, sizeof(ipHeader));
  rect = sendto(sock, szSendBuf, sizeof(ipHeader) + sizeof(tcpHeader), 0,
    (struct sockaddr*) &addr_in, sizeof(addr_in));
  if (rect == SOCKET_ERROR)
  {
    printf("send error!:%d/n", WSAGetLastError());
    return false;
  }
  else
    printf("send ok!/n");
  closesocket(sock);
  WSACleanup();
  return rect;
}
 
3.用Raw Socket实现Ping
极其常用的Ping命令通过向计算机发送ICMP Echo请求报文并且监听回应报文的返回,以校验与远程计算机或本地计算机的连接。
3.1 使用ICMP.DLL实现Ping
在Windows平台编程中实现Ping的一个最简单方法是调用ICMP.DLL这个动态链接库,引用ICMP.DLL中的三个函数即可:
HANDLE IcmpCreateFile(void);
这个函数打开个ICMP Echo请求能使用的句柄;
BOOL IcmpCloseHandle(HANDLE IcmpHandle);
这个函数关闭由IcmpCreateFile打开的句柄;
DWORD IcmpSendEcho(
  HANDLE IcmpHandle,  // IcmpCreateFile打开的句柄
  IPAddr DestinationAddress, //Echo请求的目的地址
  LPVOID RequestData,  //发送数据buffer
  WORD RequestSize,   //发送数据长度
  PIP_OPTION_INFORMATION RequestOptions,  // IP_OPTION_INFORMATION指针
  LPVOID ReplyBuffer, //接收回复buffer
  DWORD ReplySize, //接收回复buffer大小
  DWORD Timeout   //等待超时
);
这个函数发送Echo请求并等待回复或超时。
把这个函数和相关数据封装成一个类CPing,CPing类的头文件如下:
class CPing
{
public:
 CPing();
 ~CPing();
 BOOL Ping(char* strHost);
private:
 // ICMP.DLL 导出函数指针
 HANDLE (WINAPI *pIcmpCreateFile)(VOID);
 BOOL (WINAPI *pIcmpCloseHandle)(HANDLE);
 DWORD (WINAPI *pIcmpSendEcho)
  (HANDLE,DWORD,LPVOID,WORD,PIPINFO,LPVOID,DWORD,DWORD);
 HANDLE hndlIcmp; // 加载ICMP.DLL库句柄
 BOOL bValid; //是否构造(获得ICMP.DLL导出函数指针和初始化WinSock)成功
};
CPing类的构造函数获得ICMP.DLL中导出函数的指针并初始化WinSock:
CPing::CPing()
{
 bValid = FALSE;
 WSADATA wsaData;
 int nRet; 
 // 动态加载ICMP.DLL
 hndlIcmp = LoadLibrary("ICMP.DLL");
 if (hndlIcmp == NULL)
 {
  ::MessageBox(NULL, "Could not load ICMP.DLL", "Error:", MB_OK);
  return;
 }
 // 获得ICMP.DLL中导出函数指针
 pIcmpCreateFile  = (HANDLE (WINAPI *)(void))
  GetProcAddress((HMODULE)hndlIcmp,"IcmpCreateFile");
 pIcmpCloseHandle = (BOOL (WINAPI *)(HANDLE))
  GetProcAddress((HMODULE)hndlIcmp,"IcmpCloseHandle");
 pIcmpSendEcho = (DWORD (WINAPI *)
  (HANDLE,DWORD,LPVOID,WORD,PIPINFO,LPVOID,DWORD,DWORD))
  GetProcAddress((HMODULE)hndlIcmp,"IcmpSendEcho");
 // 检查所有的指针
 if (pIcmpCreateFile == NULL  ||
  pIcmpCloseHandle == NULL ||
  pIcmpSendEcho == NULL)
 {
  ::MessageBox(NULL, "Error loading ICMP.DLL", "Error:", MB_OK);
  FreeLibrary((HMODULE)hndlIcmp);
  return;
 }
 // 初始化WinSock
 nRet = WSAStartup(0x0101, &wsaData );
    if (nRet)
    {
  ::MessageBox(NULL, "WSAStartup() error:", "Error:", MB_OK);
        WSACleanup();
  FreeLibrary((HMODULE)hndlIcmp);
        return;
    }
    // 检查WinSock的版本
    if (0x0101 != wsaData.wVersion)
    {
  ::MessageBox(NULL, "No WinSock version 1.1 support found", "Error:", MB_OK);
        WSACleanup();
  FreeLibrary((HMODULE)hndlIcmp);
        return;
    }
 bValid = TRUE;
}
CPing类的析构函数完成相反的动作:
CPing::~CPing()
{
    WSACleanup();
 FreeLibrary((HMODULE)hndlIcmp);
}
CPing类的Ping函数是最核心的函数,实现真正的ping操作:
int CPing::Ping(char *strHost)
{
  struct in_addr iaDest; // Internet地址结构体
  LPHOSTENT pHost; // 主机入口结构体指针
  DWORD *dwAddress; // IP地址
  IPINFO ipInfo; // IP选项结构体
  ICMPECHO icmpEcho; // ICMP Echo回复buffer
  HANDLE hndlFile; // IcmpCreateFile函数打开的句柄
  if (!bValid)
  {
    return FALSE;
  }
  //使用inet_addr()以判定ping目标为地址还是名称
  iaDest.s_addr = inet_addr(strHost);
  if (iaDest.s_addr == INADDR_NONE)
    pHost = gethostbyname(strHost);
  else
    pHost = gethostbyaddr((const char*) &iaDest, sizeof(struct in_addr),
      AF_INET);
  if (pHost == NULL)
  {
    return FALSE;
  }
  // 拷贝IP地址
  dwAddress = (DWORD*)(*pHost->h_addr_list);
  // 获得ICMP Echo句柄
  hndlFile = pIcmpCreateFile();
  // 设置发送信息缺省值
  ipInfo.Ttl = 255;
  ipInfo.Tos = 0;
  ipInfo.IPFlags = 0;
  ipInfo.OptSize = 0;
  ipInfo.Options = NULL;
  icmpEcho.Status = 0;
  // 请求一个ICMP echo
  pIcmpSendEcho(hndlFile, *dwAddress, NULL, 0,  &ipInfo,  &icmpEcho, sizeof
    (struct tagICMPECHO), 1000);
  //设置结果
  iaDest.s_addr = icmpEcho.Source;
  if (icmpEcho.Status)
  {
    return FALSE;
  }
  // 关闭ICMP Echo句柄
  pIcmpCloseHandle(hndlFile);
  return TRUE;
}
其中所使用的相关结构体定义为:
typedef struct tagIPINFO
{
  u_char Ttl; // TTL
  u_char Tos; // 服务类型
  u_char IPFlags; // IP标志
  u_char OptSize; // 可选数据大小
  u_char *Options; // 可选数据buffer
} IPINFO,  *PIPINFO;
typedef struct tagICMPECHO
{
  u_long Source; // 源地址
  u_long Status; // IP状态
  u_long RTTime; // RTT
  u_short DataSize; // 回复数据大小
  u_short Reserved; // 保留
  void *pData; // 回复数据buffer
  IPINFO ipInfo; // 回复IP选项
} ICMPECHO, *PICMPECHO;
3.2 使用Raw Socket实现Ping
仅仅采用ICMP.DLL并不能完全实现ICMP灵活多变的各类报文,只有使用Raw Socket才是ICMP的终极解决之道。
使用Raw Socket发送ICMP报文前,我们要完全依靠自己的代码组装报文:
//功能:初始化ICMP的报头, 给data部分填充数据, 计算校验和
void init_ping_packet(ICMPHeader *icmp_hdr, int packet_size, int seq_no)
{
  //设置ICMP报头字段
  icmp_hdr->type = ICMP_ECHO_REQUEST;
  icmp_hdr->code = 0;
  icmp_hdr->checksum = 0;
  icmp_hdr->id = (unsigned short)GetCurrentProcessId();
  icmp_hdr->seq = seq_no;
  icmp_hdr->timestamp = GetTickCount();
  // 填充data域
  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);
  }
  // 计算校验和
  icmp_hdr->checksum = ip_checksum((unsigned short*)icmp_hdr, packet_size);
}
计算校验和(Checksum)的函数为:
//功能:计算ICMP包的校验和
unsigned short ip_checksum(unsigned short *buffer, int size)
{
  unsigned long cksum = 0;
  // 将所有的16数相加
  while (size > 1)
  {
    cksum +=  *buffer++;
    size -= sizeof(unsigned short);
  }
  if (size)   //加上最后一个BYTE
  {
    cksum += *(unsigned char*)buffer;
  }
  //和的前16位和后16位相加
  cksum = (cksum >> 16) + (cksum &0xffff);
  cksum += (cksum >> 16);
  return (unsigned short)(~cksum);
}
在真正发送Ping报文前,需要先初始化Raw Socket:
// 功能:初始化RAW Socket, 设置ttl, 初始化目标地址
// 返回值:<0 失败
int setup_for_ping(char *host, int ttl, SOCKET &sd, sockaddr_in &dest)
{
  // 创建原始套接字
  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;
  }
  // 初始化目标主机信息块
  memset(&dest, 0, sizeof(dest));
  // 将第1个参数转换为目标IP地址
  unsigned int addr = inet_addr(host);
  if (addr != INADDR_NONE)
  {
    // 为IP地址
    dest.sin_addr.s_addr = addr;
    dest.sin_family = AF_INET;
  }
  else
  {
    // 非IP地址,进行主机名和IP地址的转换
    hostent *hp = gethostbyname(host);
    if (hp != 0)
    {
      // 查找主机名对应的IP地址
      memcpy(&(dest.sin_addr), hp->h_addr, hp->h_length);
      dest.sin_family = hp->h_addrtype;
    }
    else
    {
      // 不能识别的主机名
      cerr << "Failed to resolve " << host << endl;
      return  - 1;
    }
  }
  return 0;
}
下面可以利用Raw Socket发送生成的ICMP报文:
//功能:发送生成的ICMP包
//返回值:<0 发送失败
int send_ping(SOCKET sd, const sockaddr_in &dest, ICMPHeader *send_buf, int
  packet_size)
{
  // 发送send_buf缓冲区中的报文
  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;
}
发送Ping报文后,我们需要接收Ping回复ICMP报文:
//功能:接收Ping回复
//返回值: <0 接收失败
int recv_ping(SOCKET sd, sockaddr_in &source, IPHeader *recv_buf, int
  packet_size)
{
  // 等待Ping回复
  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)
{
  // 偏移到ICMP报头
  unsigned short header_len = reply->h_len *4;
  ICMPHeader *icmphdr = (ICMPHeader*)((char*)reply + header_len);
  // 报文太短
  if (bytes < header_len + ICMP_MIN)
  {
    cerr << "too few bytes from " << inet_ntoa(from->sin_addr) << endl;
    return  - 1;
  }
  // 解析回复报文类型
  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 != (unsigned short)GetCurrentProcessId())
  {
    //不是本进程发的包, 可能是同机的其它ping进程发的
    return  - 2;
  }
  // 指出往返时间TTL
  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;
}
为了在Visual C++中更加方便地使用发送和接收ICMP报文,我们可以使用由Jay Wheeler编写的CIcmp(An ICMP Class For MFC)类,在著名的开发网站的如下地址可以下载:
http://www.codeguru.com/cpp/i-n/internet/network/article.php/c3395/
这个类的简要框架如下:
class CIcmp: public CSocket
{
    // Attributes
  public:
    BOOL OpenNewSocket(HWND hWnd, unsigned int NotificationMessage, long
      NotifyEvents);
    BOOL OpenNewSocket(HWND hWnd, unsigned int NotificationMessage, long
      NotifyEvents, int AFamily, int AType, int AProtocol);
    int CloseIcmpSocket(void);
    BOOL Connect(int ReceiveTimeout, int SendTimeout);
    BOOL Connect(LPINT ReceiveTimeout, LPINT SendTimeout, int AFamily, int
      AType, int AProtocol);
    int SetTTL(int TTL);
    int SetAsynchNotification(HWND hWnd, unsigned int Message, long Events);
    int Receive(LPSTR pIcmpBuffer, int IcmpBufferSize);
    unsigned long GetIPAddress(LPSTR iHostName);
    int Ping(LPSTR pIcmpBuffer, int IcmpBufferSize);
    unsigned short IcmpChecksum(unsigned short FAR *lpBuf, int Len);
    void DisplayError(CString ErrorType, CString FunctionName);
    // Operations
  public:
    CIcmp(void);
    CIcmp(CIcmp &copy);
    ~CIcmp(void);
  public:
    //  I/O Buffer Pointers
    LPIcmpHeader pIcmpHeader;
    LPIpHeader pIpHeader;
    SOCKET icmpSocket;
    SOCKADDR_IN icmpSockAddr;
    SOCKADDR_IN rcvSockAddr;
    DWORD icmpRoundTripTime;
    DWORD icmpPingSentAt;
    DWORD icmpPingReceivedAt;
    int icmpRcvLen;
    int icmpHops;
    int icmpMaxHops;
    int icmpCurSeq;
    int icmpCurId;
    int icmpPingTimer;
    int icmpSocketError;
    int icmpSocketErrorMod;
    unsigned long icmpHostAddress;
  protected:
};
初始化网络连接的函数:
BOOL CIcmp::Connect(LPINT ReceiveTimeout, LPINT SendTimeout, int AFamily, int
  AType, int AProtocol)
{
  int Result;
  icmpSocket = NULL;
  icmpSocket = socket(AFamily, AType, AProtocol);
  if (icmpSocket == INVALID_SOCKET)
  {
    icmpSocketError = WSAGetLastError();
    icmpSocketErrorMod = 1;
    return FALSE;
  }
  //
  //  Set receive timeout
  //
  Result = setsockopt(icmpSocket, SOL_SOCKET, SO_RCVTIMEO, (char*)
    ReceiveTimeout, sizeof(int));
  if (Result == SOCKET_ERROR)
  {
    icmpSocketError = WSAGetLastError();
    icmpSocketErrorMod = 2;
    closesocket(icmpSocket);
    icmpSocket = INVALID_SOCKET;
    return FALSE;
  }
  //
  //  Set send timeout
  //
  Result = setsockopt(icmpSocket, SOL_SOCKET, SO_SNDTIMEO, (char*)SendTimeout,
    sizeof(int));
  if (Result == SOCKET_ERROR)
  {
    icmpSocketError = WSAGetLastError();
    icmpSocketErrorMod = 3;
    closesocket(icmpSocket);
    icmpSocket = INVALID_SOCKET;
    return FALSE;
  }
  icmpCurSeq = 0;
  icmpCurId = (USHORT)GetCurrentProcessId();
  icmpHops = 0;
  return TRUE;
}
接收的函数:
int CIcmp::Receive(LPSTR pIcmpBuffer, int IcmpBufferSize)
{
 LPSOCKADDR  pRcvSockAddr = (LPSOCKADDR)&rcvSockAddr;
 int    Result;
 int    RcvIpHdrLen;
 icmpPingReceivedAt = GetTickCount();
 icmpCurId = 0;
 rcvSockAddr.sin_family = AF_INET;
 rcvSockAddr.sin_addr.s_addr = INADDR_ANY;
 rcvSockAddr.sin_port = 0;
 RcvIpHdrLen = sizeof rcvSockAddr;
 Result = recvfrom (icmpSocket,
        pIcmpBuffer,
        IcmpBufferSize,
        0,
        pRcvSockAddr,
        &RcvIpHdrLen);
 if (Result == SOCKET_ERROR)
 {
  icmpSocketError = WSAGetLastError();
  icmpSocketErrorMod = 1;
  DisplayError ("Receive","CIcmp::Receive");
  return Result;
 }
 icmpRcvLen = Result;
 pIpHeader = (LPIpHeader)pIcmpBuffer;
 RcvIpHdrLen = pIpHeader->HeaderLength * 4;
 if (Result < RcvIpHdrLen + ICMP_MIN)
 {
  //
  // Too few bytes received
  //
  MessageBox(NULL,
       "Short message!",
       "CIcmp::Receive",
       MB_OK|MB_SYSTEMMODAL);
  icmpSocketErrorMod = 2;
  return Result;
 }
 pIcmpHeader = (LPIcmpHeader)(pIcmpBuffer + RcvIpHdrLen);
 icmpCurId = pIcmpHeader->IcmpId;
 icmpRoundTripTime = icmpPingReceivedAt - pIcmpHeader->IcmpTimestamp;
 if (pIcmpHeader->IcmpType != ICMP_ECHOREPLY)
 {
  //
  // Not an echo response!
  //
  return Result;
 }
 icmpCurSeq = pIcmpHeader->IcmpSeq;
 return Result;
}
异步通知主窗口:
int CIcmp::SetAsynchNotification(HWND hWnd, unsigned int Message, long Events)
{
 int Result = WSAAsyncSelect (icmpSocket,
             hWnd,
         Message,
         Events);
 if (Result == SOCKET_ERROR)
 {
  icmpSocketError = WSAGetLastError();
  icmpSocketErrorMod = 1;
  icmpSocket = INVALID_SOCKET;
 }
 return Result;
}
设置TTL:
int CIcmp::SetTTL(int TTL)
{
 int Result;
 Result = setsockopt (icmpSocket, IPPROTO_IP, IP_TTL, (LPSTR)&TTL, sizeof(int));
 if (Result == SOCKET_ERROR)
 {
  icmpSocketErrorMod = 1;
  icmpSocketError = WSAGetLastError();
 }
 return Result;
}
Ping命令的函数:
int CIcmp::Ping (LPSTR pIcmpBuffer, int DataLen)
{
 int Result;
 int IcmpBufferSize = DataLen + IcmpHeaderLength;
 pIcmpHeader = (LPIcmpHeader)pIcmpBuffer;
 memset (pIcmpBuffer, 'E', IcmpBufferSize);
 memset (pIcmpHeader, 0, IcmpHeaderLength);
 pIcmpHeader->IcmpType = ICMP_ECHO;
 pIcmpHeader->IcmpCode = 0;
 pIcmpHeader->IcmpChecksum = 0;
 pIcmpHeader->IcmpId = icmpCurId;
 pIcmpHeader->IcmpSeq = icmpCurSeq;
 pIcmpHeader->IcmpTimestamp = GetCurrentTime();
 pIcmpHeader->IcmpChecksum = IcmpChecksum ((USHORT FAR *)pIcmpBuffer,
           IcmpBufferSize);
 icmpPingSentAt = GetCurrentTime();
 Result = sendto (icmpSocket,
      pIcmpBuffer,
      IcmpBufferSize,
      0,
      (LPSOCKADDR)&icmpSockAddr,
      sizeof icmpSockAddr);
 if (Result == SOCKET_ERROR)
 {
  icmpSocketError = WSAGetLastError();
  icmpSocketErrorMod = 1;
 }
 return Result;
}
原创粉丝点击