原始套接字打造ping命令

码农UP2U

共 4804字,需浏览 10分钟

 · 2021-06-06

早期文章:



Ping 命令的构造

    ping 命令依赖的不是TCP 协议,也不是UDP 协议,它依赖的是ICMP协议。ICMPIP层的协议之一,它传递差错报文以及其他需要注意的信息。ICMP报文通常被IP层或高层协议使用。ICMP封装在IP数据内部,如下图。

    ICMP报文的格式如下图所示。

    ICMP协议的类型码与代码根据不同的情况,各自取不同的值。Ping命令类型码用到了2个值,分别是08而代码的取值都是0当类型码取值为0时,代码的0值表示回显应答;当类型码取值为8时,代码的0值表示请求回显。Ping命令发送一个ICMP数据报时,类型码为8,代码为0,表示向对方主机进行请求回显;当收到对方的ICMP数据报时,类型码为0,代码为0,表示收到了对方主机的回显应答。简单来说,ping命令发出的数据中,类型是8,代码是0,如果对方有回应,那么对方回应的数据中,类型是0,代码是0

    在自己实现Ping命令时,就是去自己构造一个请求回显的ICMP数据报,然后进行发送。ICMP的数据结构定义如下:

// ICMP协议结构体定义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协议的数据结构,现在用抓包工具(也可以称为协议分析工具)Wireshark来分析一下ICMP结构真实的情况,如下图所示。


    在上图中,标识1的部分是对协议进行过滤设置的,在该部分输入“ICMP”可以让Wireshark只显示ICMP协议的数据记录。相应地,可以输入“TCP”、“UDP”、“HTTP”等协议进行筛选过滤。标识2的部分用于显示筛选后的ICMP记录,从这里可以明显看出源IP地址、目的IP地址和协议的类型。标识3的部分用于显示ICMP数据结构的值和附加的数据内容。最下面的部分显示了数据的原始的二进制数据,在熟练掌握协议后,查看原始的二进制数据也并不是不可能的。


Ping命令的实现

    有了前面的基础,就可以构造自己的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协议头不的数据进行累加,当累加有溢出的话,会将溢出的部分也进行累加。具体计算校验和的算法就不过多介绍了,如果对校验和计算的代码不了解,可以进行单步调试来进行分析。再来看一下对于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); // 创建原始套接字 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是为了和系统提供的看起来一样 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++黑客编程揭秘与防范》(第三版)一书。


更多文章:


浏览 29
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报