IPv6 Router Advertisement packets cause a denial of service by CPU consumption. This is a known vulnerability in Windows systems, and it works against FreeBSD too. Here is a screen captures of the attack in action, with a slow attack of 100 packets per second: http://samsclass.info/ipv6/proj/FreeBSD-100RAps.png Here is a detailed vulnerability report I wrote about the Windows version: http://samsclass.info/ipv6/proj/flood-router6a.htm Thanks to ty Justin Hohner for telling me about this. Fix: Mac OS X and Ubuntu Linux are not vulnerable, because they ignore all RAs after the first ten or so. How-To-Repeat: To reproduce it, use Linux and the thc-ipv6 tools from http://www.thc.org/thc-ipv6/ If you run ./flood_router6 eth0 on the attacker, a FreeBSD network on the same LAN will freeze.
Responsible Changed From-To: freebsd-i386->freebsd-net reclassify.
OpenBSD is not vulnerable, so it could probably be fixed by porting code from there.
---------- Forwarded message ---------- From: Loganaden Velvindron <logan@elandsys.com> Date: Mon, Jul 1, 2013 at 4:30 PM Subject: Re: kern/157410: [ip6] IPv6 Router Advertisements Cause Excessive CPU Use To: freebsd-net@freebsd.org Cc: bz@freebsd.org On Mon, Jul 01, 2013 at 12:58:23PM -0700, Loganaden Velvindron wrote: > Hi I came across this old PR. It appears that it's not fixed in -current. > > I attempted to port the diff to our FreeBSD 9.1 release machines which > have IPv6 connectivity and are affected by RA flooding. > > I can report that it mitigates RA_flooding. > > Feedback welcomed. I'd be happy to polish it so that it can > make it to 9.2 and 10.0 :-) > > I broke down the diffs into separate ones. > > The last diff (nd6_rtr.diff) was garbled and another diff was missing. I'm resending the whole patchset. --- in6.c.orig 2013-06-30 23:07:46.000000000 +0400 +++ in6.c 2013-07-01 19:20:15.000000000 +0400 @@ -2694,6 +2694,8 @@ in6_domifattach(struct ifnet *ifp) ext->nd_ifinfo = nd6_ifattach(ifp); ext->scope6_id = scope6_ifattach(ifp); ext->lltable = lltable_init(ifp, AF_INET6); + ext->nprefixes = 0; + ext->ndefrouters = 0; if (ext->lltable != NULL) { ext->lltable->llt_free = in6_lltable_free; ext->lltable->llt_prefix_free = in6_lltable_prefix_free; --- in6_proto.c.orig 2013-06-30 23:07:58.000000000 +0400 +++ in6_proto.c 2013-07-01 21:05:08.000000000 +0400 @@ -413,7 +413,8 @@ VNET_DEFINE(int, ip6_rr_prune) = 5; /* r * walk list every 5 sec. */ VNET_DEFINE(int, ip6_mcast_pmtu) = 0; /* enable pMTU discovery for multicast? */ VNET_DEFINE(int, ip6_v6only) = 1; - +VNET_DEFINE(int, ip6_maxifprefixes) = 16; +VNET_DEFINE(int, ip6_maxifdefrouters) = 16; VNET_DEFINE(int, ip6_keepfaith) = 0; VNET_DEFINE(time_t, ip6_log_time) = (time_t)0L; #ifdef IPSTEALTH @@ -524,6 +525,10 @@ SYSCTL_VNET_STRUCT(_net_inet6_ip6, IPV6C &VNET_NAME(ip6stat), ip6stat, ""); SYSCTL_VNET_INT(_net_inet6_ip6, IPV6CTL_MAXFRAGPACKETS, maxfragpackets, CTLFLAG_RW, &VNET_NAME(ip6_maxfragpackets), 0, ""); +SYSCTL_VNET_INT(_net_inet6_ip6, IPV6CTL_MAXIFPREFIXES, maxifprefixes, + CTLFLAG_RW, &VNET_NAME(ip6_maxifprefixes), 0, ""); +SYSCTL_VNET_INT(_net_inet6_ip6, IPV6CTL_MAXIFDEFROUTERS, maxifdefrouters, + CTLFLAG_RW, &VNET_NAME(ip6_maxifdefrouters), 0, ""); SYSCTL_VNET_INT(_net_inet6_ip6, IPV6CTL_ACCEPT_RTADV, accept_rtadv, CTLFLAG_RW, &VNET_NAME(ip6_accept_rtadv), 0, "Default value of per-interface flag for accepting ICMPv6 Router" --- in6_var.h.orig 2013-06-30 23:08:28.000000000 +0400 +++ in6_var.h 2013-07-01 22:38:03.000000000 +0400 @@ -104,6 +104,8 @@ struct in6_ifextra { struct scope6_id *scope6_id; struct lltable *lltable; struct mld_ifinfo *mld_ifinfo; + int nprefixes; + int ndefrouters; }; #define LLTABLE6(ifp) (((struct in6_ifextra *)(ifp)->if_afdata[AF_INET6])->lltable) --- ip6_var.h.orig 2013-06-30 23:09:22.000000000 +0400 +++ ip6_var.h 2013-07-01 20:28:30.000000000 +0400 @@ -315,6 +315,8 @@ VNET_DECLARE(int, ip6_maxfragpackets); / * queue */ VNET_DECLARE(int, ip6_maxfrags); /* Maximum fragments in reassembly * queue */ +VNET_DECLARE(int, ip6_maxifprefixes); +VNET_DECLARE(int, ip6_maxifdefrouters); VNET_DECLARE(int, ip6_accept_rtadv); /* Acts as a host not a router */ VNET_DECLARE(int, ip6_no_radr); /* No defroute from RA */ VNET_DECLARE(int, ip6_norbit_raif); /* Disable R-bit in NA on RA --- nd6.h.orig 2013-06-30 23:09:42.000000000 +0400 +++ nd6.h 2013-07-01 22:16:09.000000000 +0400 @@ -277,6 +277,7 @@ struct nd_prefix { u_char ndpr_plen; int ndpr_refcnt; /* reference couter from addresses */ }; +#define ndpr_next ndpr_entry.le_next #define ndpr_raf ndpr_flags #define ndpr_raf_onlink ndpr_flags.onlink --- in6.h.orig 2013-07-02 00:24:36.000000000 +0400 +++ in6.h 2013-07-01 21:58:04.000000000 +0400 @@ -607,7 +607,6 @@ struct ip6_mtuinfo { #define IPV6CTL_ISATAPRTR 43 /* isatap router */ #endif #define IPV6CTL_MCAST_PMTU 44 /* enable pMTU discovery for multicast? */ - /* New entries should be added here from current IPV6CTL_MAXID value. */ /* to define items, should talk with KAME guys first, for *BSD compatibility */ #define IPV6CTL_STEALTH 45 @@ -619,6 +618,9 @@ struct ip6_mtuinfo { #define IPV6CTL_RFC6204W3 50 /* Accept defroute even when forwarding enabled */ #define IPV6CTL_MAXID 51 +#define IPV6CTL_MAXIFPREFIXES 52 +#define IPV6CTL_MAXIFDEFROUTERS 53 + #endif /* __BSD_VISIBLE */ /* --- nd6_rtr.c.orig 2013-06-30 23:10:24.000000000 +0400 +++ nd6_rtr.c 2013-07-01 22:40:29.000000000 +0400 @@ -83,6 +83,7 @@ static void nd6_rtmsg(int, struct rtentr static int in6_init_prefix_ltimes(struct nd_prefix *); static void in6_init_address_ltimes __P((struct nd_prefix *, struct in6_addrlifetime *)); +static void purge_detached(struct ifnet *); static int nd6_prefix_onlink(struct nd_prefix *); static int nd6_prefix_offlink(struct nd_prefix *); @@ -565,6 +566,7 @@ defrtrlist_del(struct nd_defrouter *dr) { struct nd_defrouter *deldr = NULL; struct nd_prefix *pr; + struct in6_ifextra *ext = dr->ifp->if_afdata[AF_INET6]; /* * Flush all the routing table entries that use the router @@ -597,6 +599,12 @@ defrtrlist_del(struct nd_defrouter *dr) if (deldr) defrouter_select(); + ext->ndefrouters--; + if (ext->ndefrouters < 0) { + log(LOG_WARNING, "defrtrlist_del: negative count on %s\n", + dr->ifp->if_xname); + } + free(dr, M_IP6NDP); } @@ -734,6 +742,7 @@ static struct nd_defrouter * defrtrlist_update(struct nd_defrouter *new) { struct nd_defrouter *dr, *n; + struct in6_ifextra *ext = new->ifp->if_afdata[AF_INET6]; int s = splnet(); if ((dr = defrouter_lookup(&new->rtaddr, new->ifp)) != NULL) { @@ -775,6 +784,12 @@ defrtrlist_update(struct nd_defrouter *n splx(s); return (dr); } + /*struct in6_ifextra *ext = new->ifp->if_afdata[AF_INET6];*/ + if (ip6_maxifdefrouters >= 0 && + ext->ndefrouters >= ip6_maxifdefrouters) { + splx(s); + return (NULL); + } /* entry does not exist */ if (new->rtlifetime == 0) { @@ -810,6 +825,8 @@ insert: defrouter_select(); + ext->ndefrouters++; + splx(s); return (n); @@ -868,6 +885,44 @@ nd6_prefix_lookup(struct nd_prefixctl *k return (search); } +static void +purge_detached(struct ifnet *ifp) +{ + struct nd_prefix *pr, *pr_next; + struct in6_ifaddr *ia; + struct ifaddr *ifa, *ifa_next; + + for (pr = nd_prefix.lh_first; pr; pr = pr_next) { + pr_next = pr->ndpr_next; + + /* + * This function is called when we need to make more room for + * new prefixes rather than keeping old, possibly stale ones. + * Detached prefixes would be a good candidate; if all routers + * that advertised the prefix expired, the prefix is also + * probably stale. + */ + if (pr->ndpr_ifp != ifp || + IN6_IS_ADDR_LINKLOCAL(&pr->ndpr_prefix.sin6_addr) || + ((pr->ndpr_stateflags & NDPRF_DETACHED) == 0 && + !LIST_EMPTY(&pr->ndpr_advrtrs))) + continue; + + for (ifa = ifp->if_addrlist.tqh_first; ifa; ifa = ifa_next) { + ifa_next = ifa->ifa_list.tqe_next; + if (ifa->ifa_addr->sa_family != AF_INET6) + continue; + ia = (struct in6_ifaddr *)ifa; + if ((ia->ia6_flags & IN6_IFF_AUTOCONF) == + IN6_IFF_AUTOCONF && ia->ia6_ndpr == pr) { + in6_purgeaddr(ifa); + } + } + if (pr->ndpr_refcnt == 0) + prelist_remove(pr); + } +} + int nd6_prelist_add(struct nd_prefixctl *pr, struct nd_defrouter *dr, struct nd_prefix **newp) @@ -876,6 +931,14 @@ nd6_prelist_add(struct nd_prefixctl *pr, int error = 0; int i, s; char ip6buf[INET6_ADDRSTRLEN]; + struct in6_ifextra *ext = pr->ndpr_ifp->if_afdata[AF_INET6]; + + if (ip6_maxifprefixes >= 0) { + if (ext->nprefixes >= ip6_maxifprefixes / 2) + purge_detached(pr->ndpr_ifp); + if (ext->nprefixes >= ip6_maxifprefixes) + return ENOMEM; + } new = (struct nd_prefix *)malloc(sizeof(*new), M_IP6NDP, M_NOWAIT); if (new == NULL) @@ -923,7 +986,7 @@ nd6_prelist_add(struct nd_prefixctl *pr, if (dr) pfxrtr_add(new, dr); - + ext->nprefixes++; return 0; } @@ -933,6 +996,7 @@ prelist_remove(struct nd_prefix *pr) struct nd_pfxrouter *pfr, *next; int e, s; char ip6buf[INET6_ADDRSTRLEN]; + struct in6_ifextra *ext = pr->ndpr_ifp->if_afdata[AF_INET6]; /* make sure to invalidate the prefix until it is really freed. */ pr->ndpr_vltime = 0; @@ -965,6 +1029,12 @@ prelist_remove(struct nd_prefix *pr) LIST_FOREACH_SAFE(pfr, &pr->ndpr_advrtrs, pfr_entry, next) { free(pfr, M_IP6NDP); } + + ext->nprefixes--; + if (ext->nprefixes < 0) { + log(LOG_WARNING, "prelist_remove: negative count on %s\n", + pr->ndpr_ifp->if_xname); + } splx(s); free(pr, M_IP6NDP); _______________________________________________ freebsd-net@freebsd.org mailing list http://lists.freebsd.org/mailman/listinfo/freebsd-net To unsubscribe, send any mail to "freebsd-net-unsubscribe@freebsd.org" -- Eitan Adler
Hi guys, I'd be happy to follow-up on my original diff if developers are willing to give me their feedback :-)
Created attachment 155329 [details] updated patch I have forward-ported this patch to head Read carefully: it compiles, but I have not tested this in any way. I also do not know what I'm touching. If someone could chime in on whether or not this is worth pursuing it would be greatly appreciated. I've known about this PR for a few years and the recent IPv6 SA prompted me to revisit this.
This can be tested by installing the nmap port and running: nmap -6 --script ipv6-ra-flood.nse --script-args 'interface=lagg0' (Pick your interface on the attacking machine. I used lagg0.)
(In reply to Mark Felder from comment #5) I don't see how it can compile. The function purge_detached is incomplete.
Created attachment 189428 [details] ported to 12-CURRENT r327568M Tested. It mitigates the bug. A couple of notes: 1. purge_detached() was incomplete in the original patch. I'm not sure this is the right thing to do anyway. It's been ifdef'd out. 2. The default limit should be higher. NetBSD uses 100. 3. Otherwise it needs a little polishing before commit. Does anyone want me to take ownership of this PR (not that I'm looking for more work) or can we look at otherwise moving this forward?
(In reply to Cy Schubert from comment #8) > Does anyone want me to take ownership of this PR If you're willing I think that would be great!
This area is on my TODO for the next months anyway. I'll try to get this sorted before September.
A bit later .. and years too late. I've put a first cut of changes into review: https://reviews.freebsd.org/D22447 People will notice that the diff differs. The default router and prefix lists are global structures and limits are counted globally (per-VNET). At the moment the goal is not to panic. While per-Interface counters can help mitigate an attack better (only 1 interface blocked, might still get legit information via another) we still have to deal with other bogus entires in the tables and the system wont work too nicely in most cases. In order to prevent some (a lot) more of that the code needs to be proactively a lot more clever and complicated and I haven't convinced myself yet that is worth it. On another note, the code also has limits on the neighbour table entries, which I didn't see in the patches here. Redirects are a lot harder to deal with in FreeBSD currently. While I prepared the stats the best way for the moment would be to simply disable them in case it still is a problem despite other limits. There is no expiry of bad entries currently. Once can however, given the system does not panic, flush tables manually ( which, if the attack continues will also not guarantee a working system afterwards ). Any feedback on this will be welcome.
(In reply to Bjoern A. Zeeb from comment #11) I probably should have added that we already have a way to throw away RAs on a per-interface base in ifconfig. So if we would have multi-interfaces machines under attack by only one interface we could always only just -no_radr or -accept_rtadv savign a lot more of cpu and memory (at the expense that the "good" one will not be received either) but frankly might often be better than all the other garbage in case of DoS.
A commit references this bug: Author: bz Date: Wed Mar 4 16:21:00 UTC 2020 New revision: 358620 URL: https://svnweb.freebsd.org/changeset/base/358620 Log: Add new ICMPv6 counters for Anti-DoS limits. Add four new counters for ND6 related Anti-DoS measures. We split these out into a separate upfront commit so that we only change the struct size one time. Implementations using them will follow. PR: 157410 Reviewed by: melifaro MFC after: 2 weeks X-MFC: cannot really MFC this without breaking netstat Sponsored by: Netflix (initially) Differential Revision: https://reviews.freebsd.org/D22711 Changes: head/sys/netinet/icmp6.h head/usr.bin/netstat/inet6.c
^Triage: committed back in 2020.
(In reply to Mark Linimon from comment #14) The commit message added statistics counters and notes "Implementations using them will follow." It isn't clear to me whether or not the underlying issue was resolved.