Openwall GNU/*/Linux - a small security-enhanced Linux distro for servers
[<prev] [next>] [thread-next>] [day] [month] [year] [list]
Date: Mon, 6 Nov 2017 16:31:41 +0100
From: Jonas 'Sortie' Termansen <sortie@...si.org>
To: oss-security@...ts.openwall.com
Subject: Race condition between UDP bind(2) and connect(2) delivers wrong
 datagrams

Hi oss-security,

When you connect(2) a UDP socket to an address, any subsequent recv(2) must
only receieve datagrams from that address. However, if the UDP socket is
first given a local address with bind(2), there is a race condition before
the connect(2) where datagrams received from any address is added to the
socket's receieve queue. Unfortunately, all of Darwin, DragonFly, FreeBSD,
GNU/Hurd, Haiku, Linux, Minix, NetBSD, OpenBSD, and OpenIndiana don't purge
the receieve queue of datagrams with the wrong source on connect(2).
Instead, they deliver datagrams already in the recieve queue even if they
have the wrong source. I've failed to find any operating system that handles
this case correctly.

This race condition affects software with a UDP socket that bind(2) to a
particular address or port before using connect(2) to ensure datagrams are
only received from an approved source, and then not using recvfrom(2) nor
recvmsg(2) and checking the sender's address (instead relying on the kernel
having done this check).

I don't believe the impact to be high, but I wouldn't know. I think it's
rare that software bind(2) a UDP socket to a particular local port and then
connect(2) (since connecting would bind to an appropriate inferface and port
for you). DHCP is a case where the client will want to use port 68, but
there's no reason to connect(2) due to the broadcast nature of DHCP. Another
case is software that bind(2) to a local address to ensure the traffic is
routed over a particular network interface, and then connect(2) to a remote.
I don't know how common that case is.

Even though it can be difficult to exploit this bug, it is a validation bug
in the kernels. POSIX 2008 (2016 edition) says[1]:

    "For SOCK_DGRAM sockets, the peer address identifies where all datagrams
     are sent on subsequent send() functions, and limits the remote sender
     for subsequent recv() functions."

Similar statements can be found in the manual pages for Darwin, DragonFly,
FreeBSD, GNU/Hurd, Linux, NetBSD, and OpenBSD. Haiku, Minix, and OpenIndiana
do not document this behavior as far as I can tell.

Software can work around this bug by using recvfrom(2) or recvmsg(2) and
verifying the sender's address.

The nc(1) provided in the netbsd-openbsd package (netcat-1.10) on my local
Linux distro is vulnerable:

    strace nc -u -p 1234 192.0.2.0 1234
    ...
    bind(3, {sa_family=AF_INET, sin_port=htons(1234),
             sin_addr=inet_addr("0.0.0.0")}, 16) = 0
    ...
    connect(3, {sa_family=AF_INET, sin_port=htons(1234),
                sin_addr=inet_addr("192.0.2.0")}, 16) = 0
    ...
    read(3, "x\n", 2048)                    = 2

Here nc(1) fails to use recvfrom(2) and verify the sender and it will accept
any datagrams receieved during the race condition. It's unclear to me if
anyone uses nc(1) in this way.

I've not been able to think of / find any other software that bind(2) a UDP
socket to an address and then use connect(2) to fix a particular peer, but
I don't have time to do a thorough search. Please let me know if you can
think of any.

It would be prudent to modify each operating system kernel to warn if a UDP
socket is bound and then connected, and then use the systems normally, and
see if any system software is affected. I haven't had the time available to
do so.

Below you'll find a test program that checks for this bug. I discovered this
bug during the development of my UDP POSIX API testcase suite[2] and the
test program is a mix of the trio-send-wrong-y-connect set of testcases.

I have not reported this bug to any operating system vendors yet. I am
writing here to fully assess the security impact of this bug and come up
with a plan.

The solution is for connect(2) to drop any datagrams from the receive queue
that don't have the new source address. Note that a UDP socket can be
connected (and unconnected with AF_UNSPEC) multiple times. The source
information is available in the receieve queue since it might need to be
provided to a recvfrom(2)/recvmsg(2) call.

Note that it is insufficient to merely add another source check to
recvmsg(2), as the poll(2) and such will say there is a datagram in the
receive queue that mysteriously isn't there when recvmsg(2) is called. If
the problem is solved with a lazy check in recvmsg(2), one should also be
added to poll(2). However, such lazy checks can be observable if the socket
is reconnected multiple times. Note that is wrong for connect(2) to drop the
receive queue entirely as datagrams from the right source should remain in
the receive queue. Therefore, I believe the proper fix is for connect(2) to
iterate the receive queue and drop datagrams from the wrong source.

Jonas

[1] http://pubs.opengroup.org/onlinepubs/9699919799/functions/connect.html
[2] https://sortix.org/os-test/#udp

--

/*
 * Create three UDP sockets on the loopback address, connect the second socket
 * to the first socket, connect the third socket to the first socket, send 'y'
 * on the third socket to the first socket, send 'x' on the second socket to the
 * first socket, connect the first socket to the second socket, and then test
 * if 'x' or 'y' is received on the first socket.
 *
 * POSIX and the documentation of several operating systems says 'x' will be
 * received, however there is a race condition between bind and connect on the
 * first socket where the 'y' packet is received and not rejected, and connect
 * is supposed to remove the 'y' packet from the receive queue, or it would
 * wrongly deliver 'y' on the next recv call.
 *
 * This bug has been confirmed on:
 *
 *  - Darwin 17.0.0 x86_64
 *  - DragonFly 4.8-RELEASE x86_64
 *  - FreeBSD 11.1-RELEASE amd64
 *  - GNU 0.9 i686-AT386 (debian-hurd-2017-i386-CD-1.iso)
 *  - Haiku 1 x86_64 (haiku-nightly-hrev51498-x86_64-anyboot.zip)
 *  - Linux 4.4.0-98-generic x86_64
 *  - Minix 3.4.0 i386 (minix_R3.4.0rc6-d5e4fc0.iso)
 *  - NetBSD 7.1 amd64
 *  - OpenBSD 6.2 amd64
 *  - OpenIndiana (illumos-0c950529ae / SunOS 5.11 i86pc)
 */

#ifdef __HAIKU__
#define _BSD_SOURCE // and -lsocket -lbsd
#endif

#include <sys/socket.h>

#include <err.h>
#include <netinet/in.h>
#include <poll.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>

int main(void)
{
	int verdict = 0;
	struct sockaddr_in any;
	struct sockaddr_in local1, local2, local3;
	socklen_t locallen1 = sizeof(local1);
	socklen_t locallen2 = sizeof(local2);
	socklen_t locallen3 = sizeof(local3);
	int fd1, fd2, fd3;
	char x = 'x';
	char y = 'y';
	char z;
	struct pollfd pfd;
	int num_events;
	ssize_t amount;

	/* Create first UDP socket (server). */
	fd1 = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
	if ( fd1 < 0 )
		err(1, "Wrong: first socket");
	/* Bind first socket (socket) to the loopback interface. */
	memset(&any, 0, sizeof(any));
	any.sin_family = AF_INET;
	any.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
	any.sin_port = htons(0);
	if ( bind(fd1, (const struct sockaddr*) &any, sizeof(any)) < 0 )
		err(1, "Wrong: first bind");
	/* Find the address the first socket (server) was bound to. */
	if ( getsockname(fd1, (struct sockaddr*) &local1, &locallen1) < 0 )
		err(1, "Wrong: first getsockname");

	/* Create second socket (correct client). */
	fd2 = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
	if ( fd2 < 0 )
		err(1, "Wrong: second socket");
	/* Connect second socket (correct client) to the first socket
	   (server). */
	if ( connect(fd2, (const struct sockaddr*) &local1, locallen1) < 0 )
		err(1, "Wrong: second connect");
	/* Find the address the second UDP socket was bound to. */
	if ( getsockname(fd2, (struct sockaddr*) &local2, &locallen2) < 0 )
		err(1, "Wrong: second getsockname");

	/* Create third UDP socket (wrong client). */
	fd3 = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
	if ( fd3 < 0 )
		err(1, "Wrong: second socket");
	/* Connect third UDP socket to the first UDP socket. */
	if ( connect(fd3, (const struct sockaddr*) &local1, locallen1) < 0 )
		err(1, "Wrong: second connect");
	/* Find the address the third UDP socket was bound to. */
	if ( getsockname(fd3, (struct sockaddr*) &local3, &locallen3) < 0 )
		err(1, "Wrong: second getsockname");

	/* The first socket (server) is bound, but hasn't yet been connected to
	   the second socket (correct client). The third socket (wrong client
	   now sends a datagram to the first socket (server). */
	if ( send(fd3, &y, sizeof(y), 0) < 0 )
		err(1, "Wrong: send of y");
	/* Ensure the datagram has been delivered. */
	sleep(1);

	/* Connect the first socket (server) to the second socket (correct
	   client). This will according to POSIX "For SOCK_DGRAM sockets, the
	   peer address identifies where all datagrams are sent on subsequent
	   send() functions, and limits the remote sender for subsequent recv()
	   functions." */
	if ( connect(fd1, (const struct sockaddr*) &local2, locallen2) < 0 )
		err(1, "Wrong: first connect");

	/* Test if POLLIN is set suggesting the datagram from the third socket
	   (wrong client) is ready to be received, but a subsequent recv call
	   must not deliver that datagram. */
	memset(&pfd, 0, sizeof(pfd));
	pfd.fd = fd1;
	pfd.events = POLLIN;
	num_events = poll(&pfd, 1, 0);
	if ( num_events < 0 )
		err(1, "Wrong: poll");
	if ( num_events == 1 && pfd.revents & POLLIN )
		printf("Wrong: POLLIN says wrong datagram was received\n");
	else
	{
		printf("Correct: POLLIN unset after wrong datagram\n");
		verdict++;
	}

	/* Send a datagram from the second socket (correct client) to the first
	   socket (server). */
	if ( send(fd2, &x, sizeof(x), 0) < 0 )
		err(1, "Wrong: send of x");
	/* Ensure the datagram has been delivered. */
	sleep(1);

	/* Receive a datagram on the first socket (server). This must not
	   receive the datagram sent from the third socket (wrong client) as the
	   connect call must ensure this subsequent recv() only delivers
	   datagrams from the second socket (correct client). Rather the
	   datagram from the second socket (correct client) must be received. */
	amount = recv(fd1, &z, sizeof(z), MSG_DONTWAIT);
	if ( amount < 0 )
		err(1, "Wrong: recv");
	else if ( amount == 0 )
		puts("Wrong: Neither datagram was received");
	else if ( amount != 1 )
		printf("Wrong: %zi bytes were received instead of 1\n", amount);
	else if ( z == 'y' )
		puts("Wrong: Received datagram from wrong client");
	else if ( z == 'x' )
	{
		puts("Correct: Received datagram from correct client");
		verdict++;
	}
	else
		printf("Wrong: Received wrong byte");

	return verdict != 2 ? 1 : 0;
}

Powered by blists - more mailing lists

Your e-mail address:

Please check out the Open Source Software Security Wiki, which is counterpart to this mailing list.

Powered by Openwall GNU/*/Linux - Powered by OpenVZ