diff mbox series

[v3,6/6] Add test for CVE 2020-25705

Message ID 20210505081845.7024-6-mdoucha@suse.cz
State Changes Requested
Headers show
Series [v3,1/6] Add SAFE_REALLOC() helper function to LTP library | expand

Commit Message

Martin Doucha May 5, 2021, 8:18 a.m. UTC
Fixes #742

Signed-off-by: Martin Doucha <mdoucha@suse.cz>
---

Changes since v1: New patch

Changes since v2: Added missing gitignore and runfile entry for the new test

 runtest/cve                    |   1 +
 testcases/cve/.gitignore       |   1 +
 testcases/cve/cve-2020-25705.c | 262 +++++++++++++++++++++++++++++++++
 3 files changed, 264 insertions(+)
 create mode 100644 testcases/cve/cve-2020-25705.c

Comments

Petr Vorel May 5, 2021, 10:04 a.m. UTC | #1
> Fixes #742

LGTM. Few unimportant comments below.

Reviewed-by: Petr Vorel <pvorel@suse.cz>

...
> diff --git a/testcases/cve/cve-2020-25705.c b/testcases/cve/cve-2020-25705.c
> new file mode 100644
> index 000000000..7d6bbafa8
> --- /dev/null
> +++ b/testcases/cve/cve-2020-25705.c
> @@ -0,0 +1,262 @@
> +// SPDX-License-Identifier: GPL-2.0-or-later
> +/*
> + * Copyright (C) 2020 SUSE LLC
> + * Author: Nicolai Stange <nstange@suse.de>
> + * LTP port: Martin Doucha <mdoucha@suse.cz>
> + *
> + * CVE-2020-25705
> + *
> + * Test of ICMP rate limiting behavior that may be abused for DNS cache
> + * poisoning attack. Send a few batches of 100 packets to a closed UDP port
> + * and count the ICMP errors. If the number of errors is always the same
> + * for each batch (not randomized), the system is vulnerable. Send packets
> + * from multiple IP addresses to bypass per-address ICMP throttling.
We probably turn this into docparse during merge.

> + *
> + * Fixed in:
> + *
> + *  commit b38e7819cae946e2edf869e604af1e65a5d241c5
> + *  Author: Eric Dumazet <edumazet@google.com>
> + *  Date:   Thu Oct 15 11:42:00 2020 -0700
> + *
> + *  icmp: randomize the global rate limiter
> + */
> +
> +#include <sys/socket.h>
> +#include <netinet/in.h>
> +#include <arpa/inet.h>
> +#include <linux/if_addr.h>
> +#include <linux/errqueue.h>

#include <time.h>

to fix build failure on MUSL:

cve-2020-25705.c:262:1: warning: missing initializer for field 'needs_cmds' of 'struct tst_test' [-Wmissing-field-initializers]
https://travis-ci.org/github/pevik/ltp/jobs/769551152

> +#include <sched.h>
> +#include <limits.h>
> +#include "tst_test.h"
> +#include "tst_netdevice.h"
> +
> +#define DSTADDR 0xfa444e02 /* 250.68.78.2 */
> +#define SRCADDR_BASE 0xfa444e41 /* 250.68.78.65 */
> +#define SRCADDR_COUNT 50
> +#define BATCH_COUNT 8
> +#define BUFSIZE 1024
> +
> +static int parentns = -1, childns = -1;
> +static int fds[SRCADDR_COUNT];
> +
> +static void setup(void)
> +{
...
> +	/*
> +	 * Create network namespace to hide the destination interface from
> +	 * the test process.
> +	 */
> +	parentns = SAFE_OPEN("/proc/self/ns/net", O_RDONLY);
> +	SAFE_UNSHARE(CLONE_NEWNET);
> +
> +	/* Do NOT close this FD, or both interfaces will be destroyed */
> +	childns = SAFE_OPEN("/proc/self/ns/net", O_RDONLY);
> +
> +	/* Configure child namespace */
> +	CREATE_VETH_PAIR("ltp_veth1", "ltp_veth2");
> +	addr = DSTADDR;
> +	NETDEV_ADD_ADDRESS_INET("ltp_veth2", htonl(addr), 26,
> +		IFA_F_NOPREFIXROUTE);

I wonder if it'd be useful *later* (not bothering with it now) to allow tests
just declare .needs_netdevice = 1 and have generic network setup done (similarly
it's done in tst_net.sh). Or just define addresses a prefixes and do library to
do the setup.

> +	NETDEV_SET_STATE("ltp_veth2", 1);
> +	NETDEV_ADD_ROUTE_INET("ltp_veth2", 0, 0, htonl(0xfa444e40), 26,
nit: maybe define 0xfa444e40 (and 0xfa444e00) and 26 as constants?
> +		0);
> +
> +	/* Configure parent namespace */
> +	NETDEV_CHANGE_NS_FD("ltp_veth1", parentns);
> +	SAFE_SETNS(parentns, CLONE_NEWNET);
> +	addr = SRCADDR_BASE; /* 250.68.78.65 */
nit: maybe repeating the address in the comment is not needed.
> +
> +	for (i = 0; i < SRCADDR_COUNT; i++, addr++) {
> +		NETDEV_ADD_ADDRESS_INET("ltp_veth1", htonl(addr), 26,
> +			IFA_F_NOPREFIXROUTE);
> +	}
> +
> +	NETDEV_SET_STATE("ltp_veth1", 1);
> +	NETDEV_ADD_ROUTE_INET("ltp_veth1", 0, 0, htonl(0xfa444e00), 26, 0);

> +	SAFE_FILE_PRINTF("/proc/sys/net/ipv4/conf/ltp_veth1/forwarding", "1");
> +
> +	/* Open test sockets */
> +	for (i = 0; i < SRCADDR_COUNT; i++) {
> +		ipaddr.sin_addr.s_addr = htonl(SRCADDR_BASE + i);
> +		fds[i] = SAFE_SOCKET(AF_INET, SOCK_DGRAM, 0);
> +		SAFE_SETSOCKOPT_INT(fds[i], IPPROTO_IP, IP_RECVERR, 1);
> +		SAFE_BIND(fds[i], (struct sockaddr *)&ipaddr, sizeof(ipaddr));
> +	}
> +}
> +
> +static int count_icmp_errors(int fd)
> +{
> +	int error_count = 0;
> +	ssize_t len;
> +	char msgbuf[BUFSIZE], errbuf[BUFSIZE];
> +	struct sockaddr_in addr;
> +	struct cmsghdr *cmsg;
> +	struct sock_extended_err exterr;
> +	struct iovec iov = {
> +		.iov_base = msgbuf,
> +		.iov_len = BUFSIZE
> +	};
> +
> +	while (1) {
> +		struct msghdr msg = {
> +			.msg_name = (struct sockaddr *)&addr,
> +			.msg_namelen = sizeof(addr),
> +			.msg_iov = &iov,
> +			.msg_iovlen = 1,
> +			.msg_flags = 0,
> +			.msg_control = errbuf,
> +			.msg_controllen = BUFSIZE
> +		};
> +
> +		memset(errbuf, 0, BUFSIZE);
> +		errno = 0;
> +		len = recvmsg(fd, &msg, MSG_ERRQUEUE);
> +
> +		if (len == -1) {
> +			if (errno == EWOULDBLOCK || errno == EAGAIN)
> +				break;
> +
> +			tst_brk(TBROK | TERRNO, "recvmsg() failed");
> +		}
> +
> +		if (len < 0) {
> +			tst_brk(TBROK | TERRNO,
> +				"Invalid recvmsg() return value %zd", len);
> +		}
> +
> +		for (cmsg = CMSG_FIRSTHDR(&msg); cmsg;
> +			cmsg = CMSG_NXTHDR(&msg, cmsg)) {
> +			if (cmsg->cmsg_level != SOL_IP)
> +				continue;
> +
> +			if (cmsg->cmsg_type != IP_RECVERR)
> +				continue;
> +
> +			memcpy(&exterr, CMSG_DATA(cmsg), sizeof(exterr));
> +
> +			if (exterr.ee_origin != SO_EE_ORIGIN_ICMP)
> +				tst_brk(TBROK, "Unexpected non-ICMP error");
> +
> +			if (exterr.ee_errno != ECONNREFUSED) {
> +				TST_ERR = exterr.ee_errno;
> +				tst_brk(TBROK | TTERRNO,
> +					"Unexpected ICMP error");
> +			}
> +
> +			error_count++;
> +		}
> +	}
> +
> +	return error_count;
> +}

FYI I tested the test on several VM. Very old kernel detects problem only on
more runs. But given it's 3.16 (and b38e7819cae9 is a fix for 4cdf507d5452 from
v3.18-rc1 we can ignore this).

Kind regards,
Petr

# ./cve-2020-25705
tst_kconfig.c:64: TINFO: Parsing kernel config '/boot/config-3.16.0-11-amd64'
tst_test.c:1313: TINFO: Timeout per run is 0h 05m 00s
cve-2020-25705.c:217: TINFO: Batch 0: Got 85 ICMP errors
cve-2020-25705.c:221: TINFO: Batch 1: Got 100 ICMP errors
cve-2020-25705.c:230: TPASS: ICMP rate limit is randomized

-i2
# ./cve-2020-25705 -i2
tst_kconfig.c:64: TINFO: Parsing kernel config '/boot/config-3.16.0-11-amd64'
tst_test.c:1313: TINFO: Timeout per run is 0h 05m 00s
cve-2020-25705.c:217: TINFO: Batch 0: Got 85 ICMP errors
cve-2020-25705.c:221: TINFO: Batch 1: Got 100 ICMP errors
cve-2020-25705.c:230: TPASS: ICMP rate limit is randomized
cve-2020-25705.c:217: TINFO: Batch 0: Got 100 ICMP errors
cve-2020-25705.c:221: TINFO: Batch 1: Got 100 ICMP errors
cve-2020-25705.c:221: TINFO: Batch 2: Got 100 ICMP errors
cve-2020-25705.c:221: TINFO: Batch 3: Got 100 ICMP errors
cve-2020-25705.c:221: TINFO: Batch 4: Got 100 ICMP errors
cve-2020-25705.c:221: TINFO: Batch 5: Got 100 ICMP errors
cve-2020-25705.c:221: TINFO: Batch 6: Got 100 ICMP errors
cve-2020-25705.c:221: TINFO: Batch 7: Got 100 ICMP errors
cve-2020-25705.c:226: TFAIL: ICMP rate limit not randomized, system is vulnerable

HINT: You _MAY_ be missing kernel fixes, see:

https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=b38e7819cae9

HINT: You _MAY_ be vulnerable to CVE(s), see:

https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-25705

Summary:
passed   1
failed   1
broken   0
skipped  0
warnings 0
Martin Doucha May 5, 2021, 10:33 a.m. UTC | #2
On 05. 05. 21 12:04, Petr Vorel wrote:
> I wonder if it'd be useful *later* (not bothering with it now) to allow tests
> just declare .needs_netdevice = 1 and have generic network setup done (similarly
> it's done in tst_net.sh). Or just define addresses a prefixes and do library to
> do the setup.

Sounds like a good idea.

> 
>> +	NETDEV_SET_STATE("ltp_veth2", 1);
>> +	NETDEV_ADD_ROUTE_INET("ltp_veth2", 0, 0, htonl(0xfa444e40), 26,
> nit: maybe define 0xfa444e40 (and 0xfa444e00) and 26 as constants?
>> +		0);
>> +
>> +	/* Configure parent namespace */
>> +	NETDEV_CHANGE_NS_FD("ltp_veth1", parentns);
>> +	SAFE_SETNS(parentns, CLONE_NEWNET);
>> +	addr = SRCADDR_BASE; /* 250.68.78.65 */
> nit: maybe repeating the address in the comment is not needed.

Yes, I should fix this and the <time.h> issue. Please push everything up
to patch 5 and then I'll resubmit v4 just for this patch.

> FYI I tested the test on several VM. Very old kernel detects problem only on
> more runs. But given it's 3.16 (and b38e7819cae9 is a fix for 4cdf507d5452 from
> v3.18-rc1 we can ignore this).

Pass is expected here. Vanilla v3.16 AFAIK does not have the global ICMP
rate limiter which introduced the real vulnerability in the first place.
Petr Vorel May 5, 2021, 11:13 a.m. UTC | #3
Hi Martin,

> Yes, I should fix this and the <time.h> issue. Please push everything up
> to patch 5 and then I'll resubmit v4 just for this patch.
NOTE: <time.h> must be included *before* <linux/errqueue.h>
(maybe bug in musl headers).
I suppose Cyril will merge these 4 patches soon.

> > FYI I tested the test on several VM. Very old kernel detects problem only on
> > more runs. But given it's 3.16 (and b38e7819cae9 is a fix for 4cdf507d5452 from
> > v3.18-rc1 we can ignore this).

> Pass is expected here. Vanilla v3.16 AFAIK does not have the global ICMP
> rate limiter which introduced the real vulnerability in the first place.
Sure. I was just surprised to get different result on -i1 and on -i >= 2.

Kind regards,
Petr
Cyril Hrubis May 5, 2021, 1:06 p.m. UTC | #4
Hi!
> diff --git a/testcases/cve/cve-2020-25705.c b/testcases/cve/cve-2020-25705.c

Can we please name the file icmp_rate_limit01.c or something more human
readable than the cve-2020-25705?

> new file mode 100644
> index 000000000..7d6bbafa8
> --- /dev/null
> +++ b/testcases/cve/cve-2020-25705.c
> @@ -0,0 +1,262 @@
> +// SPDX-License-Identifier: GPL-2.0-or-later
> +/*
> + * Copyright (C) 2020 SUSE LLC
> + * Author: Nicolai Stange <nstange@suse.de>
> + * LTP port: Martin Doucha <mdoucha@suse.cz>
> + *
> + * CVE-2020-25705
> + *
> + * Test of ICMP rate limiting behavior that may be abused for DNS cache
> + * poisoning attack. Send a few batches of 100 packets to a closed UDP port
> + * and count the ICMP errors. If the number of errors is always the same
> + * for each batch (not randomized), the system is vulnerable. Send packets
> + * from multiple IP addresses to bypass per-address ICMP throttling.
> + *
> + * Fixed in:
> + *
> + *  commit b38e7819cae946e2edf869e604af1e65a5d241c5
> + *  Author: Eric Dumazet <edumazet@google.com>
> + *  Date:   Thu Oct 15 11:42:00 2020 -0700
> + *
> + *  icmp: randomize the global rate limiter
> + */
> +
> +#include <sys/socket.h>
> +#include <netinet/in.h>
> +#include <arpa/inet.h>
> +#include <linux/if_addr.h>
> +#include <linux/errqueue.h>
> +
> +#include <sched.h>
> +#include <limits.h>
> +#include "tst_test.h"
> +#include "tst_netdevice.h"
> +
> +#define DSTADDR 0xfa444e02 /* 250.68.78.2 */
> +#define SRCADDR_BASE 0xfa444e41 /* 250.68.78.65 */
> +#define SRCADDR_COUNT 50
> +#define BATCH_COUNT 8
> +#define BUFSIZE 1024
> +
> +static int parentns = -1, childns = -1;
> +static int fds[SRCADDR_COUNT];
> +
> +static void setup(void)
> +{
> +	struct sockaddr_in ipaddr = { .sin_family = AF_INET };
> +	uint32_t addr;
> +	int i;
> +	int real_uid = getuid();
> +	int real_gid = getgid();
> +
> +	for (i = 0; i < SRCADDR_COUNT; i++)
> +		fds[i] = -1;
> +
> +	SAFE_UNSHARE(CLONE_NEWUSER);
> +	SAFE_UNSHARE(CLONE_NEWNET);
> +	SAFE_FILE_PRINTF("/proc/self/setgroups", "deny");
> +	SAFE_FILE_PRINTF("/proc/self/uid_map", "0 %d 1\n", real_uid);
> +	SAFE_FILE_PRINTF("/proc/self/gid_map", "0 %d 1\n", real_gid);
> +
> +	/*
> +	 * Create network namespace to hide the destination interface from
> +	 * the test process.
> +	 */
> +	parentns = SAFE_OPEN("/proc/self/ns/net", O_RDONLY);
> +	SAFE_UNSHARE(CLONE_NEWNET);
> +
> +	/* Do NOT close this FD, or both interfaces will be destroyed */
> +	childns = SAFE_OPEN("/proc/self/ns/net", O_RDONLY);
> +
> +	/* Configure child namespace */
> +	CREATE_VETH_PAIR("ltp_veth1", "ltp_veth2");
> +	addr = DSTADDR;
> +	NETDEV_ADD_ADDRESS_INET("ltp_veth2", htonl(addr), 26,
> +		IFA_F_NOPREFIXROUTE);
> +	NETDEV_SET_STATE("ltp_veth2", 1);
> +	NETDEV_ADD_ROUTE_INET("ltp_veth2", 0, 0, htonl(0xfa444e40), 26,
> +		0);
> +
> +	/* Configure parent namespace */
> +	NETDEV_CHANGE_NS_FD("ltp_veth1", parentns);
> +	SAFE_SETNS(parentns, CLONE_NEWNET);
> +	addr = SRCADDR_BASE; /* 250.68.78.65 */
> +
> +	for (i = 0; i < SRCADDR_COUNT; i++, addr++) {
> +		NETDEV_ADD_ADDRESS_INET("ltp_veth1", htonl(addr), 26,
> +			IFA_F_NOPREFIXROUTE);
> +	}
> +
> +	NETDEV_SET_STATE("ltp_veth1", 1);
> +	NETDEV_ADD_ROUTE_INET("ltp_veth1", 0, 0, htonl(0xfa444e00), 26, 0);
> +	SAFE_FILE_PRINTF("/proc/sys/net/ipv4/conf/ltp_veth1/forwarding", "1");
> +
> +	/* Open test sockets */
> +	for (i = 0; i < SRCADDR_COUNT; i++) {
> +		ipaddr.sin_addr.s_addr = htonl(SRCADDR_BASE + i);
> +		fds[i] = SAFE_SOCKET(AF_INET, SOCK_DGRAM, 0);
> +		SAFE_SETSOCKOPT_INT(fds[i], IPPROTO_IP, IP_RECVERR, 1);
> +		SAFE_BIND(fds[i], (struct sockaddr *)&ipaddr, sizeof(ipaddr));
> +	}
> +}
> +
> +static int count_icmp_errors(int fd)
> +{
> +	int error_count = 0;
> +	ssize_t len;
> +	char msgbuf[BUFSIZE], errbuf[BUFSIZE];
> +	struct sockaddr_in addr;
> +	struct cmsghdr *cmsg;
> +	struct sock_extended_err exterr;
> +	struct iovec iov = {
> +		.iov_base = msgbuf,
> +		.iov_len = BUFSIZE
> +	};
> +
> +	while (1) {
> +		struct msghdr msg = {
> +			.msg_name = (struct sockaddr *)&addr,
> +			.msg_namelen = sizeof(addr),
> +			.msg_iov = &iov,
> +			.msg_iovlen = 1,
> +			.msg_flags = 0,
> +			.msg_control = errbuf,
> +			.msg_controllen = BUFSIZE
> +		};
> +
> +		memset(errbuf, 0, BUFSIZE);
> +		errno = 0;
> +		len = recvmsg(fd, &msg, MSG_ERRQUEUE);
> +
> +		if (len == -1) {
> +			if (errno == EWOULDBLOCK || errno == EAGAIN)
> +				break;
> +
> +			tst_brk(TBROK | TERRNO, "recvmsg() failed");
> +		}
> +
> +		if (len < 0) {
> +			tst_brk(TBROK | TERRNO,
> +				"Invalid recvmsg() return value %zd", len);
> +		}
> +
> +		for (cmsg = CMSG_FIRSTHDR(&msg); cmsg;
> +			cmsg = CMSG_NXTHDR(&msg, cmsg)) {
> +			if (cmsg->cmsg_level != SOL_IP)
> +				continue;
> +
> +			if (cmsg->cmsg_type != IP_RECVERR)
> +				continue;
> +
> +			memcpy(&exterr, CMSG_DATA(cmsg), sizeof(exterr));
> +
> +			if (exterr.ee_origin != SO_EE_ORIGIN_ICMP)
> +				tst_brk(TBROK, "Unexpected non-ICMP error");
> +
> +			if (exterr.ee_errno != ECONNREFUSED) {
> +				TST_ERR = exterr.ee_errno;
> +				tst_brk(TBROK | TTERRNO,
> +					"Unexpected ICMP error");
> +			}
> +
> +			error_count++;
> +		}
> +	}
> +
> +	return error_count;
> +}
> +
> +static int packet_batch(const struct sockaddr *addr, socklen_t addrsize)
> +{
> +	int i, j, error_count = 0;
> +	char data = 0;
> +
> +	for (i = 0; i < SRCADDR_COUNT; i++) {
> +		for (j = 0; j < 2; j++) {
> +			error_count += count_icmp_errors(fds[i]);
> +			TEST(sendto(fds[i], &data, sizeof(data), 0, addr,
> +				addrsize));
> +
> +			if (TST_RET == -1) {
> +				if (TST_ERR == ECONNREFUSED) {
> +					j--; /* flush ICMP errors and retry */
> +					continue;
> +				}
> +
> +				tst_brk(TBROK | TTERRNO, "sento() failed");
> +			}
> +
> +			if (TST_RET < 0) {
> +				tst_brk(TBROK | TTERRNO,
> +					"Invalid sento() return value %ld",
> +					TST_RET);
> +			}
> +		}
> +	}
> +
> +	/* Wait and collect pending ICMP errors */
> +	sleep(2);

Can we do something better than sleep() here?

Maybe just loop over the loop that reads errors with some short sleep
and exit if we haven't got any new errors for a while?

Something as:

	unsigned int timeout_ms = 2000;
	unsigned int no_changes_ms = 0;

	while (timeout_ms) {
		int last_err_cnt = error_count;

		for (i = 0; i < SRCADDR_COUNT; i++)
			error_count += count_icmp_errors(fds[i]);

		if (last_err_cnt == error_count)
			no_changes_ms++;
		else
			no_changes_ms = 0;


		if (no_changes_ms > 500)
			break;

		usleep(1000);
		timeout_ms--;
	}


> +	for (i = 0; i < SRCADDR_COUNT; i++)
> +		error_count += count_icmp_errors(fds[i]);
> +
> +	return error_count;
> +}
> +
> +static void run(void)
> +{
> +	int i, errors_baseline, errors;
> +	struct sockaddr_in addr = {
> +		.sin_family = AF_INET,
> +		.sin_port = TST_GET_UNUSED_PORT(AF_INET, SOCK_DGRAM),
> +		.sin_addr = { htonl(DSTADDR) }
> +	};
> +
> +	errors_baseline = packet_batch((struct sockaddr *)&addr, sizeof(addr));
> +	errors = errors_baseline;
> +	tst_res(TINFO, "Batch 0: Got %d ICMP errors", errors);
> +
> +	for (i = 1; i < BATCH_COUNT && errors == errors_baseline; i++) {
> +		errors = packet_batch((struct sockaddr *)&addr, sizeof(addr));
> +		tst_res(TINFO, "Batch %d: Got %d ICMP errors", i, errors);
> +	}
> +
> +	if (errors == errors_baseline) {
> +		tst_res(TFAIL,
> +			"ICMP rate limit not randomized, system is vulnerable");
> +		return;
> +	}
> +
> +	tst_res(TPASS, "ICMP rate limit is randomized");
> +}
> +
> +static void cleanup(void)
> +{
> +	int i;
> +
> +	for (i = 0; i < SRCADDR_COUNT; i++)
> +		if (fds[i] >= 0)
> +			SAFE_CLOSE(fds[i]);
> +
> +	if (childns >= 0)
> +		SAFE_CLOSE(childns);
> +
> +	if (parentns >= 0)
> +		SAFE_CLOSE(parentns);
> +}
> +
> +static struct tst_test test = {
> +	.test_all = run,
> +	.setup = setup,
> +	.cleanup = cleanup,
> +	.needs_kconfigs = (const char *[]) {
> +		"CONFIG_USER_NS=y",
> +		"CONFIG_NET_NS=y",
> +		NULL
> +	},
> +	.tags = (const struct tst_tag[]) {
> +		{"linux-git", "b38e7819cae9"},
> +		{"CVE", "2020-25705"},
> +		{}
> +	}
> +};
> -- 
> 2.31.1
> 
> 
> -- 
> Mailing list info: https://lists.linux.it/listinfo/ltp
Martin Doucha May 5, 2021, 3:54 p.m. UTC | #5
Hello, Michal and Nicolai,
please see the question about sleep() near the end of this e-mail. This
is a new LTP test for the global ICMP rate limiter exploit that can
enable the SADDNS attack.

On 05. 05. 21 15:06, Cyril Hrubis wrote:
> Hi!
>> diff --git a/testcases/cve/cve-2020-25705.c b/testcases/cve/cve-2020-25705.c
> 
> Can we please name the file icmp_rate_limit01.c or something more human
> readable than the cve-2020-25705?

I'll rename it in v4.

>> new file mode 100644
>> index 000000000..7d6bbafa8
>> --- /dev/null
>> +++ b/testcases/cve/cve-2020-25705.c
>> @@ -0,0 +1,262 @@
>> +// SPDX-License-Identifier: GPL-2.0-or-later
>> +/*
>> + * Copyright (C) 2020 SUSE LLC
>> + * Author: Nicolai Stange <nstange@suse.de>
>> + * LTP port: Martin Doucha <mdoucha@suse.cz>
>> + *
>> + * CVE-2020-25705
>> + *
>> + * Test of ICMP rate limiting behavior that may be abused for DNS cache
>> + * poisoning attack. Send a few batches of 100 packets to a closed UDP port
>> + * and count the ICMP errors. If the number of errors is always the same
>> + * for each batch (not randomized), the system is vulnerable. Send packets
>> + * from multiple IP addresses to bypass per-address ICMP throttling.
>> + *
>> + * Fixed in:
>> + *
>> + *  commit b38e7819cae946e2edf869e604af1e65a5d241c5
>> + *  Author: Eric Dumazet <edumazet@google.com>
>> + *  Date:   Thu Oct 15 11:42:00 2020 -0700
>> + *
>> + *  icmp: randomize the global rate limiter
>> + */
>> +
>> +#include <sys/socket.h>
>> +#include <netinet/in.h>
>> +#include <arpa/inet.h>
>> +#include <linux/if_addr.h>
>> +#include <linux/errqueue.h>
>> +
>> +#include <sched.h>
>> +#include <limits.h>
>> +#include "tst_test.h"
>> +#include "tst_netdevice.h"
>> +
>> +#define DSTADDR 0xfa444e02 /* 250.68.78.2 */
>> +#define SRCADDR_BASE 0xfa444e41 /* 250.68.78.65 */
>> +#define SRCADDR_COUNT 50
>> +#define BATCH_COUNT 8
>> +#define BUFSIZE 1024
>> +
>> +static int parentns = -1, childns = -1;
>> +static int fds[SRCADDR_COUNT];
>> +
>> +static void setup(void)
>> +{
>> +	struct sockaddr_in ipaddr = { .sin_family = AF_INET };
>> +	uint32_t addr;
>> +	int i;
>> +	int real_uid = getuid();
>> +	int real_gid = getgid();
>> +
>> +	for (i = 0; i < SRCADDR_COUNT; i++)
>> +		fds[i] = -1;
>> +
>> +	SAFE_UNSHARE(CLONE_NEWUSER);
>> +	SAFE_UNSHARE(CLONE_NEWNET);
>> +	SAFE_FILE_PRINTF("/proc/self/setgroups", "deny");
>> +	SAFE_FILE_PRINTF("/proc/self/uid_map", "0 %d 1\n", real_uid);
>> +	SAFE_FILE_PRINTF("/proc/self/gid_map", "0 %d 1\n", real_gid);
>> +
>> +	/*
>> +	 * Create network namespace to hide the destination interface from
>> +	 * the test process.
>> +	 */
>> +	parentns = SAFE_OPEN("/proc/self/ns/net", O_RDONLY);
>> +	SAFE_UNSHARE(CLONE_NEWNET);
>> +
>> +	/* Do NOT close this FD, or both interfaces will be destroyed */
>> +	childns = SAFE_OPEN("/proc/self/ns/net", O_RDONLY);
>> +
>> +	/* Configure child namespace */
>> +	CREATE_VETH_PAIR("ltp_veth1", "ltp_veth2");
>> +	addr = DSTADDR;
>> +	NETDEV_ADD_ADDRESS_INET("ltp_veth2", htonl(addr), 26,
>> +		IFA_F_NOPREFIXROUTE);
>> +	NETDEV_SET_STATE("ltp_veth2", 1);
>> +	NETDEV_ADD_ROUTE_INET("ltp_veth2", 0, 0, htonl(0xfa444e40), 26,
>> +		0);
>> +
>> +	/* Configure parent namespace */
>> +	NETDEV_CHANGE_NS_FD("ltp_veth1", parentns);
>> +	SAFE_SETNS(parentns, CLONE_NEWNET);
>> +	addr = SRCADDR_BASE; /* 250.68.78.65 */
>> +
>> +	for (i = 0; i < SRCADDR_COUNT; i++, addr++) {
>> +		NETDEV_ADD_ADDRESS_INET("ltp_veth1", htonl(addr), 26,
>> +			IFA_F_NOPREFIXROUTE);
>> +	}
>> +
>> +	NETDEV_SET_STATE("ltp_veth1", 1);
>> +	NETDEV_ADD_ROUTE_INET("ltp_veth1", 0, 0, htonl(0xfa444e00), 26, 0);
>> +	SAFE_FILE_PRINTF("/proc/sys/net/ipv4/conf/ltp_veth1/forwarding", "1");
>> +
>> +	/* Open test sockets */
>> +	for (i = 0; i < SRCADDR_COUNT; i++) {
>> +		ipaddr.sin_addr.s_addr = htonl(SRCADDR_BASE + i);
>> +		fds[i] = SAFE_SOCKET(AF_INET, SOCK_DGRAM, 0);
>> +		SAFE_SETSOCKOPT_INT(fds[i], IPPROTO_IP, IP_RECVERR, 1);
>> +		SAFE_BIND(fds[i], (struct sockaddr *)&ipaddr, sizeof(ipaddr));
>> +	}
>> +}
>> +
>> +static int count_icmp_errors(int fd)
>> +{
>> +	int error_count = 0;
>> +	ssize_t len;
>> +	char msgbuf[BUFSIZE], errbuf[BUFSIZE];
>> +	struct sockaddr_in addr;
>> +	struct cmsghdr *cmsg;
>> +	struct sock_extended_err exterr;
>> +	struct iovec iov = {
>> +		.iov_base = msgbuf,
>> +		.iov_len = BUFSIZE
>> +	};
>> +
>> +	while (1) {
>> +		struct msghdr msg = {
>> +			.msg_name = (struct sockaddr *)&addr,
>> +			.msg_namelen = sizeof(addr),
>> +			.msg_iov = &iov,
>> +			.msg_iovlen = 1,
>> +			.msg_flags = 0,
>> +			.msg_control = errbuf,
>> +			.msg_controllen = BUFSIZE
>> +		};
>> +
>> +		memset(errbuf, 0, BUFSIZE);
>> +		errno = 0;
>> +		len = recvmsg(fd, &msg, MSG_ERRQUEUE);
>> +
>> +		if (len == -1) {
>> +			if (errno == EWOULDBLOCK || errno == EAGAIN)
>> +				break;
>> +
>> +			tst_brk(TBROK | TERRNO, "recvmsg() failed");
>> +		}
>> +
>> +		if (len < 0) {
>> +			tst_brk(TBROK | TERRNO,
>> +				"Invalid recvmsg() return value %zd", len);
>> +		}
>> +
>> +		for (cmsg = CMSG_FIRSTHDR(&msg); cmsg;
>> +			cmsg = CMSG_NXTHDR(&msg, cmsg)) {
>> +			if (cmsg->cmsg_level != SOL_IP)
>> +				continue;
>> +
>> +			if (cmsg->cmsg_type != IP_RECVERR)
>> +				continue;
>> +
>> +			memcpy(&exterr, CMSG_DATA(cmsg), sizeof(exterr));
>> +
>> +			if (exterr.ee_origin != SO_EE_ORIGIN_ICMP)
>> +				tst_brk(TBROK, "Unexpected non-ICMP error");
>> +
>> +			if (exterr.ee_errno != ECONNREFUSED) {
>> +				TST_ERR = exterr.ee_errno;
>> +				tst_brk(TBROK | TTERRNO,
>> +					"Unexpected ICMP error");
>> +			}
>> +
>> +			error_count++;
>> +		}
>> +	}
>> +
>> +	return error_count;
>> +}
>> +
>> +static int packet_batch(const struct sockaddr *addr, socklen_t addrsize)
>> +{
>> +	int i, j, error_count = 0;
>> +	char data = 0;
>> +
>> +	for (i = 0; i < SRCADDR_COUNT; i++) {
>> +		for (j = 0; j < 2; j++) {
>> +			error_count += count_icmp_errors(fds[i]);
>> +			TEST(sendto(fds[i], &data, sizeof(data), 0, addr,
>> +				addrsize));
>> +
>> +			if (TST_RET == -1) {
>> +				if (TST_ERR == ECONNREFUSED) {
>> +					j--; /* flush ICMP errors and retry */
>> +					continue;
>> +				}
>> +
>> +				tst_brk(TBROK | TTERRNO, "sento() failed");
>> +			}
>> +
>> +			if (TST_RET < 0) {
>> +				tst_brk(TBROK | TTERRNO,
>> +					"Invalid sento() return value %ld",
>> +					TST_RET);
>> +			}
>> +		}
>> +	}
>> +
>> +	/* Wait and collect pending ICMP errors */
>> +	sleep(2);
> 
> Can we do something better than sleep() here?
> 
> Maybe just loop over the loop that reads errors with some short sleep
> and exit if we haven't got any new errors for a while?
> 
> Something as:
> 
> 	unsigned int timeout_ms = 2000;
> 	unsigned int no_changes_ms = 0;
> 
> 	while (timeout_ms) {
> 		int last_err_cnt = error_count;
> 
> 		for (i = 0; i < SRCADDR_COUNT; i++)
> 			error_count += count_icmp_errors(fds[i]);
> 
> 		if (last_err_cnt == error_count)
> 			no_changes_ms++;
> 		else
> 			no_changes_ms = 0;
> 
> 
> 		if (no_changes_ms > 500)
> 			break;
> 
> 		usleep(1000);
> 		timeout_ms--;
> 	}

Well, that's a really tough question since we have no other way of
synchronizing here and we don't even know how many errors we're supposed
to get... Let's ask Michal and Nicolai for advice.

>> +	for (i = 0; i < SRCADDR_COUNT; i++)
>> +		error_count += count_icmp_errors(fds[i]);
>> +
>> +	return error_count;
>> +}
>> +
>> +static void run(void)
>> +{
>> +	int i, errors_baseline, errors;
>> +	struct sockaddr_in addr = {
>> +		.sin_family = AF_INET,
>> +		.sin_port = TST_GET_UNUSED_PORT(AF_INET, SOCK_DGRAM),
>> +		.sin_addr = { htonl(DSTADDR) }
>> +	};
>> +
>> +	errors_baseline = packet_batch((struct sockaddr *)&addr, sizeof(addr));
>> +	errors = errors_baseline;
>> +	tst_res(TINFO, "Batch 0: Got %d ICMP errors", errors);
>> +
>> +	for (i = 1; i < BATCH_COUNT && errors == errors_baseline; i++) {
>> +		errors = packet_batch((struct sockaddr *)&addr, sizeof(addr));
>> +		tst_res(TINFO, "Batch %d: Got %d ICMP errors", i, errors);
>> +	}
>> +
>> +	if (errors == errors_baseline) {
>> +		tst_res(TFAIL,
>> +			"ICMP rate limit not randomized, system is vulnerable");
>> +		return;
>> +	}
>> +
>> +	tst_res(TPASS, "ICMP rate limit is randomized");
>> +}
>> +
>> +static void cleanup(void)
>> +{
>> +	int i;
>> +
>> +	for (i = 0; i < SRCADDR_COUNT; i++)
>> +		if (fds[i] >= 0)
>> +			SAFE_CLOSE(fds[i]);
>> +
>> +	if (childns >= 0)
>> +		SAFE_CLOSE(childns);
>> +
>> +	if (parentns >= 0)
>> +		SAFE_CLOSE(parentns);
>> +}
>> +
>> +static struct tst_test test = {
>> +	.test_all = run,
>> +	.setup = setup,
>> +	.cleanup = cleanup,
>> +	.needs_kconfigs = (const char *[]) {
>> +		"CONFIG_USER_NS=y",
>> +		"CONFIG_NET_NS=y",
>> +		NULL
>> +	},
>> +	.tags = (const struct tst_tag[]) {
>> +		{"linux-git", "b38e7819cae9"},
>> +		{"CVE", "2020-25705"},
>> +		{}
>> +	}
>> +};
>> -- 
>> 2.31.1
>>
>>
>> -- 
>> Mailing list info: https://lists.linux.it/listinfo/ltp
>
diff mbox series

Patch

diff --git a/runtest/cve b/runtest/cve
index f650854f9..eea951576 100644
--- a/runtest/cve
+++ b/runtest/cve
@@ -60,4 +60,5 @@  cve-2019-8912 af_alg07
 cve-2020-11494 pty04
 cve-2020-14386 sendto03
 cve-2020-14416 pty03
+cve-2020-25705 cve-2020-25705
 cve-2020-29373 io_uring02
diff --git a/testcases/cve/.gitignore b/testcases/cve/.gitignore
index 01a3e4c8f..7078d1ac3 100644
--- a/testcases/cve/.gitignore
+++ b/testcases/cve/.gitignore
@@ -10,3 +10,4 @@  stack_clash
 cve-2017-17052
 cve-2017-16939
 cve-2017-17053
+cve-2020-25705
diff --git a/testcases/cve/cve-2020-25705.c b/testcases/cve/cve-2020-25705.c
new file mode 100644
index 000000000..7d6bbafa8
--- /dev/null
+++ b/testcases/cve/cve-2020-25705.c
@@ -0,0 +1,262 @@ 
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Copyright (C) 2020 SUSE LLC
+ * Author: Nicolai Stange <nstange@suse.de>
+ * LTP port: Martin Doucha <mdoucha@suse.cz>
+ *
+ * CVE-2020-25705
+ *
+ * Test of ICMP rate limiting behavior that may be abused for DNS cache
+ * poisoning attack. Send a few batches of 100 packets to a closed UDP port
+ * and count the ICMP errors. If the number of errors is always the same
+ * for each batch (not randomized), the system is vulnerable. Send packets
+ * from multiple IP addresses to bypass per-address ICMP throttling.
+ *
+ * Fixed in:
+ *
+ *  commit b38e7819cae946e2edf869e604af1e65a5d241c5
+ *  Author: Eric Dumazet <edumazet@google.com>
+ *  Date:   Thu Oct 15 11:42:00 2020 -0700
+ *
+ *  icmp: randomize the global rate limiter
+ */
+
+#include <sys/socket.h>
+#include <netinet/in.h>
+#include <arpa/inet.h>
+#include <linux/if_addr.h>
+#include <linux/errqueue.h>
+
+#include <sched.h>
+#include <limits.h>
+#include "tst_test.h"
+#include "tst_netdevice.h"
+
+#define DSTADDR 0xfa444e02 /* 250.68.78.2 */
+#define SRCADDR_BASE 0xfa444e41 /* 250.68.78.65 */
+#define SRCADDR_COUNT 50
+#define BATCH_COUNT 8
+#define BUFSIZE 1024
+
+static int parentns = -1, childns = -1;
+static int fds[SRCADDR_COUNT];
+
+static void setup(void)
+{
+	struct sockaddr_in ipaddr = { .sin_family = AF_INET };
+	uint32_t addr;
+	int i;
+	int real_uid = getuid();
+	int real_gid = getgid();
+
+	for (i = 0; i < SRCADDR_COUNT; i++)
+		fds[i] = -1;
+
+	SAFE_UNSHARE(CLONE_NEWUSER);
+	SAFE_UNSHARE(CLONE_NEWNET);
+	SAFE_FILE_PRINTF("/proc/self/setgroups", "deny");
+	SAFE_FILE_PRINTF("/proc/self/uid_map", "0 %d 1\n", real_uid);
+	SAFE_FILE_PRINTF("/proc/self/gid_map", "0 %d 1\n", real_gid);
+
+	/*
+	 * Create network namespace to hide the destination interface from
+	 * the test process.
+	 */
+	parentns = SAFE_OPEN("/proc/self/ns/net", O_RDONLY);
+	SAFE_UNSHARE(CLONE_NEWNET);
+
+	/* Do NOT close this FD, or both interfaces will be destroyed */
+	childns = SAFE_OPEN("/proc/self/ns/net", O_RDONLY);
+
+	/* Configure child namespace */
+	CREATE_VETH_PAIR("ltp_veth1", "ltp_veth2");
+	addr = DSTADDR;
+	NETDEV_ADD_ADDRESS_INET("ltp_veth2", htonl(addr), 26,
+		IFA_F_NOPREFIXROUTE);
+	NETDEV_SET_STATE("ltp_veth2", 1);
+	NETDEV_ADD_ROUTE_INET("ltp_veth2", 0, 0, htonl(0xfa444e40), 26,
+		0);
+
+	/* Configure parent namespace */
+	NETDEV_CHANGE_NS_FD("ltp_veth1", parentns);
+	SAFE_SETNS(parentns, CLONE_NEWNET);
+	addr = SRCADDR_BASE; /* 250.68.78.65 */
+
+	for (i = 0; i < SRCADDR_COUNT; i++, addr++) {
+		NETDEV_ADD_ADDRESS_INET("ltp_veth1", htonl(addr), 26,
+			IFA_F_NOPREFIXROUTE);
+	}
+
+	NETDEV_SET_STATE("ltp_veth1", 1);
+	NETDEV_ADD_ROUTE_INET("ltp_veth1", 0, 0, htonl(0xfa444e00), 26, 0);
+	SAFE_FILE_PRINTF("/proc/sys/net/ipv4/conf/ltp_veth1/forwarding", "1");
+
+	/* Open test sockets */
+	for (i = 0; i < SRCADDR_COUNT; i++) {
+		ipaddr.sin_addr.s_addr = htonl(SRCADDR_BASE + i);
+		fds[i] = SAFE_SOCKET(AF_INET, SOCK_DGRAM, 0);
+		SAFE_SETSOCKOPT_INT(fds[i], IPPROTO_IP, IP_RECVERR, 1);
+		SAFE_BIND(fds[i], (struct sockaddr *)&ipaddr, sizeof(ipaddr));
+	}
+}
+
+static int count_icmp_errors(int fd)
+{
+	int error_count = 0;
+	ssize_t len;
+	char msgbuf[BUFSIZE], errbuf[BUFSIZE];
+	struct sockaddr_in addr;
+	struct cmsghdr *cmsg;
+	struct sock_extended_err exterr;
+	struct iovec iov = {
+		.iov_base = msgbuf,
+		.iov_len = BUFSIZE
+	};
+
+	while (1) {
+		struct msghdr msg = {
+			.msg_name = (struct sockaddr *)&addr,
+			.msg_namelen = sizeof(addr),
+			.msg_iov = &iov,
+			.msg_iovlen = 1,
+			.msg_flags = 0,
+			.msg_control = errbuf,
+			.msg_controllen = BUFSIZE
+		};
+
+		memset(errbuf, 0, BUFSIZE);
+		errno = 0;
+		len = recvmsg(fd, &msg, MSG_ERRQUEUE);
+
+		if (len == -1) {
+			if (errno == EWOULDBLOCK || errno == EAGAIN)
+				break;
+
+			tst_brk(TBROK | TERRNO, "recvmsg() failed");
+		}
+
+		if (len < 0) {
+			tst_brk(TBROK | TERRNO,
+				"Invalid recvmsg() return value %zd", len);
+		}
+
+		for (cmsg = CMSG_FIRSTHDR(&msg); cmsg;
+			cmsg = CMSG_NXTHDR(&msg, cmsg)) {
+			if (cmsg->cmsg_level != SOL_IP)
+				continue;
+
+			if (cmsg->cmsg_type != IP_RECVERR)
+				continue;
+
+			memcpy(&exterr, CMSG_DATA(cmsg), sizeof(exterr));
+
+			if (exterr.ee_origin != SO_EE_ORIGIN_ICMP)
+				tst_brk(TBROK, "Unexpected non-ICMP error");
+
+			if (exterr.ee_errno != ECONNREFUSED) {
+				TST_ERR = exterr.ee_errno;
+				tst_brk(TBROK | TTERRNO,
+					"Unexpected ICMP error");
+			}
+
+			error_count++;
+		}
+	}
+
+	return error_count;
+}
+
+static int packet_batch(const struct sockaddr *addr, socklen_t addrsize)
+{
+	int i, j, error_count = 0;
+	char data = 0;
+
+	for (i = 0; i < SRCADDR_COUNT; i++) {
+		for (j = 0; j < 2; j++) {
+			error_count += count_icmp_errors(fds[i]);
+			TEST(sendto(fds[i], &data, sizeof(data), 0, addr,
+				addrsize));
+
+			if (TST_RET == -1) {
+				if (TST_ERR == ECONNREFUSED) {
+					j--; /* flush ICMP errors and retry */
+					continue;
+				}
+
+				tst_brk(TBROK | TTERRNO, "sento() failed");
+			}
+
+			if (TST_RET < 0) {
+				tst_brk(TBROK | TTERRNO,
+					"Invalid sento() return value %ld",
+					TST_RET);
+			}
+		}
+	}
+
+	/* Wait and collect pending ICMP errors */
+	sleep(2);
+
+	for (i = 0; i < SRCADDR_COUNT; i++)
+		error_count += count_icmp_errors(fds[i]);
+
+	return error_count;
+}
+
+static void run(void)
+{
+	int i, errors_baseline, errors;
+	struct sockaddr_in addr = {
+		.sin_family = AF_INET,
+		.sin_port = TST_GET_UNUSED_PORT(AF_INET, SOCK_DGRAM),
+		.sin_addr = { htonl(DSTADDR) }
+	};
+
+	errors_baseline = packet_batch((struct sockaddr *)&addr, sizeof(addr));
+	errors = errors_baseline;
+	tst_res(TINFO, "Batch 0: Got %d ICMP errors", errors);
+
+	for (i = 1; i < BATCH_COUNT && errors == errors_baseline; i++) {
+		errors = packet_batch((struct sockaddr *)&addr, sizeof(addr));
+		tst_res(TINFO, "Batch %d: Got %d ICMP errors", i, errors);
+	}
+
+	if (errors == errors_baseline) {
+		tst_res(TFAIL,
+			"ICMP rate limit not randomized, system is vulnerable");
+		return;
+	}
+
+	tst_res(TPASS, "ICMP rate limit is randomized");
+}
+
+static void cleanup(void)
+{
+	int i;
+
+	for (i = 0; i < SRCADDR_COUNT; i++)
+		if (fds[i] >= 0)
+			SAFE_CLOSE(fds[i]);
+
+	if (childns >= 0)
+		SAFE_CLOSE(childns);
+
+	if (parentns >= 0)
+		SAFE_CLOSE(parentns);
+}
+
+static struct tst_test test = {
+	.test_all = run,
+	.setup = setup,
+	.cleanup = cleanup,
+	.needs_kconfigs = (const char *[]) {
+		"CONFIG_USER_NS=y",
+		"CONFIG_NET_NS=y",
+		NULL
+	},
+	.tags = (const struct tst_tag[]) {
+		{"linux-git", "b38e7819cae9"},
+		{"CVE", "2020-25705"},
+		{}
+	}
+};