Relearning the PING Command

HungWei Chiu
8 min readSep 20, 2023

Preface

One evening recently, I was researching ICMP packets using the ping command. Suddenly, I had an unexpected inspiration and decided to remove the capabilities from the ping command. Surprisingly, the ping command continued to function perfectly, which was a result I had not anticipated.

After all, ICMP packets were not typically something users could create on their own, so the ping command, from its early days of setuid to the later introduction of Linux Capabilities for more precise permission management, was designed to allow the ping command to smoothly open a RAW socket to send ICMP packets.

At some point, it became apparent that the ping command no longer required the mechanisms mentioned above. Any user could easily send ICMP packets using the PING command. This article is a record of observations regarding this phenomenon.

Observations of the Ping Command

In this context, three different environments were used to examine the various configurations of the ping command.

Ubuntu 14.04

The first experimental environment is as follows:

  • Ubuntu 14.04.5
  • LTS Linux network-lab 4.4.0–31-generic #50~14.04.1-Ubuntu SMP Wed Jul 13 01:07:32 UTC 2016 x86_64 x86_64 x86_64 GNU/Linux
  • ping utility, iputils-s20121221

In this Ubuntu environment, the ping command comes from the iputils package with a version date of 2012/12/21.

By using the “ls -l” command to observe, you can notice that the permissions of the ping command are somewhat special, represented as “rwsr-xr-x.” The ‘s’ part stands for setuid, which, when set, temporarily elevates the user executing the program to the owner of that program. This means that any user running the ping application will temporarily gain root privileges.

vagrant@network-lab:~$ ls -l $(which ping)
-rwsr-xr-x 1 root root 44168 May 7 2014 /bin/ping*
vagrant@network-lab:~$ ping 8.8.8.8 -c 1
PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.
64 bytes from 8.8.8.8: icmp_seq=1 ttl=63 time=20.7 ms

--- 8.8.8.8 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 20.740/20.740/20.740/0.000 ms

As a result, using the ping command works perfectly fine, without any specific issues.

At this point, attempting to remove the setuid permission using “chmod u-s” will revert the ping command’s permissions to “rwxr-xr-x.” If you try to execute the ping command directly after removing setuid, you will encounter an error message: “icmp open socket: Operation not permitted.”

However, if you use the “sudo” command to elevate yourself to root, the ping command will work smoothly.

vagrant@network-lab:~$ sudo chmod u-s $(which ping)
vagrant@network-lab:~$ ls -l $(which ping)
-rwxr-xr-x 1 root root 44168 May 7 2014 /bin/ping
vagrant@network-lab:~$ ping 8.8.8.8 -c 1
ping: icmp open socket: Operation not permitted
vagrant@network-lab:~$
vagrant@network-lab:~$ sudo ping 8.8.8.8 -c1
PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.
64 bytes from 8.8.8.8: icmp_seq=1 ttl=63 time=14.0 ms

--- 8.8.8.8 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 14.020/14.020/14.020/0.000 ms
vagrant@network-lab:~$

When trying to observe the relationship between the “icmp open socket” error and the actions described above using “strace,” you can see that it is caused by the syscall “socket(PF_INET, SOCK_RAW, IPPROTO_ICMP) = -1 EPERM (Operation not permitted).” It appears that regular users are unable to create RAW sockets based on the ICMP protocol, which is why setuid is used for privilege escalation.

vagrant@network-lab:~$ strace ping 8.8.8.8 -c1
...
capget({_LINUX_CAPABILITY_VERSION_3, 0}, NULL) = 0
capget({_LINUX_CAPABILITY_VERSION_3, 0}, {0, 0, 0}) = 0
socket(PF_INET, SOCK_RAW, IPPROTO_ICMP) = -1 EPERM (Operation not permitted)
capget({_LINUX_CAPABILITY_VERSION_3, 0}, NULL) = 0
capget({_LINUX_CAPABILITY_VERSION_3, 0}, {0, 0, 0}) = 0
socket(PF_INET, SOCK_DGRAM, IPPROTO_IP) = 3
connect(3, {sa_family=AF_INET, sin_port=htons(1025), sin_addr=inet_addr("8.8.8.8")}, 16) = 0
getsockname(3, {sa_family=AF_INET, sin_port=htons(44904), sin_addr=inet_addr("10.0.2.15")}, [16]) = 0
close(3) = 0
dup(2) = 3
fcntl(3, F_GETFL) = 0x8002 (flags O_RDWR|O_LARGEFILE)
fstat(3, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 0), ...}) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f845fe4a000
lseek(3, 0, SEEK_CUR) = -1 ESPIPE (Illegal seek)
write(3, "ping: icmp open socket: Operatio"..., 48ping: icmp open socket: Operation not permitted
) = 48
close(3) = 0
munmap(0x7f845fe4a000, 4096) = 0
exit_group(2) = ?

CentOS 7

  • CentOS Linux release 7.9.2009 (Core)
  • 3.10.0–1062.18.1.el7.x86_64
  • ping utility, iputils-s20160308

In this environment, when initially observing the permissions of the ping command using the “ls” command, it appears as simple “rwx-r-xr-x.” However, the ping command functions correctly.

vagrant@network-lab:~$ ls -l $(which ping)
-rwxr-xr-x. 1 root root 66176 Aug 4 2017 /usr/bin/ping
vagrant@network-lab:~$ ping 8.8.8.8 -c1
PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.
64 bytes from 8.8.8.8: icmp_seq=1 ttl=93 time=7.85 ms

--- 8.8.8.8 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 7.851/7.851/7.851/0.000 ms

The reason why ping can operate under these circumstances is due to the Linux Capabilities framework granting additional privileges to this application. You can observe this using the “getcap” command.

vagrant@network-lab:~$ getcap $(which ping)
/usr/bin/ping = cap_net_admin,cap_net_raw+p

From the above command, you can see that the ping application has been granted two main capabilities: “net_admin” and “net_raw+p (permitted).” When you use “strace” to observe the execution of the ping command (note that by default, strace ignores setuid/capabilities, and you need to use the “-u” option to handle them), you can observe that the entire process smoothly opens a socket based on SOCK_RAW for the ICMP protocol. The socket file descriptor is 3, and subsequently, ICMP packet reading and writing operations are performed on this file descriptor.

vagrant@network-lab:~$ sudo strace -u vagrant ping 8.8.8.8 -c1
....
socket(AF_INET, SOCK_RAW, IPPROTO_ICMP) = 3
....
setsockopt(3, SOL_SOCKET, SO_TIMESTAMP, [1], 4) = 0
setsockopt(3, SOL_SOCKET, SO_SNDTIMEO, "\1\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0", 16) = 0
setsockopt(3, SOL_SOCKET, SO_RCVTIMEO, "\1\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0", 16) = 0
getpid() = 32035
...
sendto(3, "\10\0\230\34}#\0\1\271\0217a\0\0\0\0(y\v\0\0\0\0\0\20\21\22\23\24\25\26\27"..., 64, 0, {sa_family=AF_INET, sin_port=htons(0), sin_addr=inet_addr("8.8.8.8")}, 16) = 64
recvmsg(3, {msg_name={sa_family=AF_INET, sin_port=htons(0), sin_addr=inet_addr("8.8.8.8")}, msg_namelen=128->16, msg_iov=[{iov_base="E`\0T\0\0\0\0]\1\247q\10\10\10\10\n\36\233\252\0\0\240\34}#\0\1\271\0217a"..., iov_len=192}], msg_iovlen=1, msg_control=[{cmsg_len=32, cmsg_level=SOL_SOCKET, cmsg_type=SCM_TIMESTAMP, cmsg_data={tv_sec=1630998969, tv_usec=759874}}], msg_controllen=32, msg_flags=0}, 0) = 84
write(1, "64 bytes from 8.8.8.8: icmp_seq="..., 5464 bytes from 8.8.8.8: icmp_seq=1 ttl=93 time=7.96 ms
) = 54
write(1, "\n", 1
) = 1
write(1, "--- 8.8.8.8 ping statistics ---\n", 32--- 8.8.8.8 ping statistics ---
) = 32
write(1, "1 packets transmitted, 1 receive"..., 601 packets transmitted, 1 received, 0% packet loss, time 0ms

At this point, if you use “setcap” to remove the capability, the ping command will once again fail to operate correctly.

vagrant@network-lab:~$ sudo setcap -r $(which ping)
vagrant@network-lab:~$ ./ping 8.8.8.8 -c1
ping: socket: Operation not permitted
vagrant@network-lab:~$ sudo strace -u vagrant ping 8.8.8.8 -c1
...
socket(AF_INET, SOCK_DGRAM, IPPROTO_ICMP) = -1 EACCES (Permission denied)
socket(AF_INET, SOCK_RAW, IPPROTO_ICMP) = -1 EPERM (Operation not permitted)
open("/usr/share/locale/locale.alias", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=2502, ...}) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f088dd51000
read(3, "# Locale name alias data base.\n#"..., 4096) = 2502
read(3, "", 4096) = 0
close(3) = 0
...
write(2, "ping: socket: Operation not perm"..., 38ping: socket: Operation not permitted
...

Ubuntu 20.04

  • Ubuntu 20.04.1 LTS
  • Linux network-lab 5.4.0–58-generic #64-Ubuntu SMP Wed Dec 9 08:16:25 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux
  • ping: ping from iputils s20190709

The ping command on this system does not have SetUID permissions, but it does have the net_raw capability. Let’s try removing this capability and see how ping behaves.

vagrant@network-lab:~$ ls -l $(which ping)
-rwxr-xr-x 1 root root 72776 Jan 30 2020 /usr/bin/ping
vagrant@network-lab:~$ getcap $(which ping)
/usr/bin/ping = cap_net_raw+ep

vagrant@network-lab:~$ sudo setcap -r $(which ping)
vagrant@network-lab:~$ getcap $(which ping)
vagrant@network-lab:~$ ping 8.8.8.8 -c1
PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.
64 bytes from 8.8.8.8: icmp_seq=1 ttl=63 time=22.2 ms

--- 8.8.8.8 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 22.150/22.150/22.150/0.000 ms

Unlike the previous scenario, even without SetUID and capabilities, the ping command can still operate normally. Even when observing the ping command using a basic “strace,” it functions smoothly. It appears that there are additional mechanisms at play beyond SetUID and capabilities.

vagrant@network-lab:~$ strace ping 8.8.8.8 -c1
...
socket(AF_INET, SOCK_DGRAM, IPPROTO_ICMP) = 3
socket(AF_INET6, SOCK_DGRAM, IPPROTO_ICMPV6) = 4
...
sendto(3, "\10\0\v\372\0\0\0\1`\0247a\0\0\0\0\211\274\f\0\0\0\0\0\20\21\22\23\24\25\26\27"..., 64, 0, {sa_family=AF_INET, sin_port=htons(0), sin_addr=inet_addr("8.8.8.8")}, 16) = 64
setitimer(ITIMER_REAL, {it_interval={tv_sec=0, tv_usec=0}, it_value={tv_sec=10, tv_usec=0}}, NULL) = 0
recvmsg(3, {msg_name={sa_family=AF_INET, sin_port=htons(0), sin_addr=inet_addr("8.8.8.8")}, msg_namelen=128->16, msg_iov=[{iov_base="\0\0\23\367\0\3\0\1`\0247a\0\0\0\0\211\274\f\0\0\0\0\0\20\21\22\23\24\25\26\27"..., iov_len=192}], msg_iovlen=1, msg_control=[{cmsg_len=32, cmsg_level=SOL_SOCKET, cmsg_type=SO_TIMESTAMP_OLD, cmsg_data={tv_sec=1630999648, tv_usec=855986}}, {cmsg_len=20, cmsg_level=SOL_IP, cmsg_type=IP_TTL, cmsg_data=[63]}], msg_controllen=56, msg_flags=0}, 0) = 64
write(1, "64 bytes from 8.8.8.8: icmp_seq="..., 5464 bytes from 8.8.8.8: icmp_seq=1 ttl=63 time=21.3 ms
) = 54
write(1, "\n", 1
) = 1
write(1, "--- 8.8.8.8 ping statistics ---\n", 32--- 8.8.8.8 ping statistics ---
) = 32
write(1, "1 packets transmitted, 1 receive"..., 601 packets transmitted, 1 received, 0% packet loss, time 0ms
) = 60
write(1, "rtt min/avg/max/mdev = 21.289/21"..., 53rtt min/avg/max/mdev = 21.289/21.289/21.289/0.000 ms
) = 53
close(1) = 0
close(2) = 0
exit_group(0) = ?
+++ exited with 0 +++

Upon closer examination through strace, I found a slight difference in the system calls being made. In the previous system, it obtained an ICMP socket based on a RAW Socket using the syscall “socket(AF_INET, SOCK_RAW, IPPROTO_ICMP) = 3,” while in the new version, it used “socket(AF_INET, SOCK_DGRAM, IPPROTO_ICMP) = 3” to create an ICMP socket based on the DGRAM type.

Searching for this keyword led me to the initial discussion of the Linux Kernel Patch titled “ipv4: add ICMP socket kind.” This patch aims to add a new IPPROTO_ICMP packet type based on SOCK_DGRAM, allowing ICMP packets to be sent and received without the need for RAW Sockets. Instead, the Kernel takes care of ICMP packet handling, enabling the creation of a ping command that doesn’t require special privileges.

This is why the ping command on the third system doesn’t need SetUID or capabilities to send and receive ICMP packets. The underlying Kernel mechanism is different, shifting from the previous use of RAW Sockets to a dedicated socket for ICMP services.

However, the above-mentioned patch does come with some restrictions. Not all users can directly call “socket(AF_INET, SOCK_DGRAM, IPPROTO_ICMP)” to send and receive ICMP packets. Only users whose GID matches the “net.ipv4.ping_group_range” parameter can do so.

vagrant@network-lab:~$ sysctl net.ipv4.ping_group_range
net.ipv4.ping_group_range = 0 2147483647
vagrant@network-lab:~$ id
uid=1000(vagrant) gid=1000(vagrant) groups=1000(vagrant),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),111(lxd),118(lpadmin),119(sambashare),997(docker)

In the case of the third system, you can observe that any user with a GID between 0 and 2147483647 can call this special socket. To experiment further, you can modify the “ping_group_range” using the “sysctl” command to limit it to 2147483647, excluding the GID of the current vagrant user. When you attempt to run the ping command in this scenario, it will not start successfully.

vagrant@network-lab:~$ sudo sysctl -w net.ipv4.ping_group_range="2147483647 2147483647"
net.ipv4.ping_group_range = 2147483647 2147483647
vagrant@network-lab:~$ sysctl net.ipv4.ping_group_range
net.ipv4.ping_group_range = 2147483647 2147483647
vagrant@network-lab:~$ ping 8.8.8.8 -c1
ping: socket: Operation not permitted

As a final experiment, manually create a new group with a GID of 2147483647, name it “ping_test,” and add the vagrant user to the ping_test group. Then, try running the ping command again. As expected, ping will start successfully.

vagrant@network-lab:~$ sudo groupadd ping_test -g 2147483647
vagrant@network-lab:~$ sudo usermod -a -G ping_test vagrant
vagrant@network-lab:~$ id
uid=1000(vagrant) gid=1000(vagrant) groups=1000(vagrant),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),111(lxd),118(lpadmin),119(sambashare),997(docker),2147483647(ping_test)
vagrant@network-lab:~$ ping 8.8.8.8 -c1
PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.
64 bytes from 8.8.8.8: icmp_seq=1 ttl=63 time=15.0 ms

--- 8.8.8.8 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 15.027/15.027/15.027/0.000 ms

Through a series of simple experiments, we have observed three different implementations of the ping command on different systems. Interested readers can also investigate the implementation method used on their own systems.

Summary

  1. The implementation of the PING command itself is not as straightforward and simple as one might imagine.
  2. Linux employs various mechanisms to address permission issues, ranging from SetUID, SetCap, to later incorporating low-level implementations directly into the Kernel, along with the use of GID to restrict user access.

--

--