前几天在做udp嗅探的时候,发现一个问题,苹果系统的ping(ping6)是不用提权的,没有s位,引起了我的好奇,因为在我使用过的ubuntu系统或者是centos系统也好,这个ping都是带s位的。在解决主要问题的时候也顺便把这个问题给解决了。
mac的ping:
centos的ping:
会创建s权限,是为了让一般用户在执行某些程序的时候,能够暂时具有该程序拥有者的权限,ubuntu系统或者centos系统下ping程序为了接收ICMP报文,套接字使用的是SOCK_RAW,而要创建这种套接字,需要有root权限,所以会带s位。这里找到一篇关于自己实现ping的文章,里面就是用到了SOCK_RAW: https://www.cnblogs.com/skyfsm/p/6348040.html
顺着这个原因去翻了下苹果系统的源码:
https://opensource.apple.com/source/network_cmds/network_cmds-328/ping.tproj/ping.c
发现它可以使用socket(AF_INET,SOCK_DGRAM, IPPROTP_ICMP)这种套接字,这种套接字怎么来理解呢?
socket函数的原型是socket(int family, int type, int protocol)。其中type参数指定一个套接口的类型,套接口可能的类型有:SOCK_STREAM、SOCK_DGRAM、SOCK_SEQPACKET、SOCK_RAW等等,它们分别表明字节流、数据报、有序分组、原始套接口,protocol指定相应的传输协议,也就是诸如TCP或UDP协议等等,一般默认是0,也就是IPPROTO_IP协议。我们平常使用最多的是TCP和UDP的协议,即socket(AF_INET,SOCK_STREAM, 0)和socket(AF_INET,SOCK_DGRAM, 0)这两种套接字。
当我们使用socket(AF_INET,SOCK_DGRAM, IPPROTP_ICMP)这种套接字的时候其实就是创建了一个ICMP协议的数据报 socket,我们都知道,数据报是网络传输的最小单元,而且ICMP不需要依附传输层的协议:
所以这种创建这种套接字是合法的,但并非所有的平台都能创建,这还是要取决于内核/proc/sys/net/ipv4/ping_group_range 这个属性值,是一对整数,指定了允许使用 ICMP 套接字的组 ID的范围(可修改,需要权限)。在Linux一些版本比如Ubuntu,centos,这个默认值是0 1,意味着没人能够使用这个特性,在Android上这个范围是0 2147483647,意味着进程都可以创建这种套接字。Mac也是可以的,所以也说明了为什么ubuntu下的ping是带s位的,而Mac和Android设备上的ping是不用带的,因为使用这种socket已经可以达到ping的功能。
这种套接字是有一定的局限性,不能跟SOCK_RAW相比,但也比它方便,内核会帮我们做一些处理,详情可看 https://lwn.net/Articles/420800/ ,而且这种socket是有漏洞的,可以通过这个漏洞来提权,主要是在Android设备上:http://www.codexiu.cn/android/blog/5827/
使用socket(AF_INET,SOCK_DGRAM, IPPROTP_ICMP)这种套接字来实现ping的功能,根据 https://lwn.net/Articles/420800/ 这篇文章的描述,类型(ECHO,只能为0)、code(只能为0)、校验和(不需要管)、id(不需要管)、序列号(无所谓)。
void icmp_test(const char* ip, int port) {
struct sockaddr_in addr;
struct icmp icmp_hdr;
int sock = socket(AF_INET, SOCK_DGRAM, 0);
if(sock < 0)
{
printf("socket() errno: %i\n", errno);
return;
}
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr(ip);
addr.sin_port = htons(port);
memset(&icmp_hdr, 0, sizeof(icmp_hdr));
// 只要设置这一个就好了
icmp_hdr.icmp_type = ICMP_ECHO;
// Initialize the packet data (header and payload)
struct timeval timeout = {1 , 0};
setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, (const char*)&timeout, sizeof(timeout));
if( sendto(sock, packetdata, sizeof(packetdata), 0, (struct sockaddr*) &addr, sizeof(addr)) >= 0)
{
unsigned char buf[1024];
memset(buf, 0, sizeof(buf));
socklen_t socklen;
i = recvfrom(sock, buf, sizeof(buf), 0, (struct sockaddr*)&addr, &socklen);
if (i < 0) {
printf("receive null!, errno : %d", errno);
}
}
close(sock);
}
如果主机不可达的话recvfrom是收不到任何信息的。至于具体的错误信息比如端口或主机不可到达错误,这个我在实验过程中倒是没有收到这类信息,有可能是我用法有错误。
如果要收到ICMP的差错报文,并非一定要使用socket(AF_INET,SOCK_DGRAM, IPPROTP_ICMP)这种套接字,其实使用基本的套接字比如socket(AF_INET,SOCK_DGRAM, 0)就可以了,然后给套接字设置一些额外的选项。详情可看: http://zkheartboy.blogspot.com/2007/10/blog-post.html ,我的主要问题即udp嗅探端口就是借鉴了这篇博客的代码,根据错误信息来判断是主机还是端口不可达,同时这种套接字也没有漏洞和平台的问题。不过在试验这段代码的时候一直收不到错误信息,后来我想了下可能因为是异步错误,信息不及时,只调用一次recvmsg可能来不及收到,就把它改成while循环去获取,尝试5次,间隔1秒,就好了。
while ( (bread = recvmsg( fd, &msg, MSG_ERRQUEUE ) ) == -1)