Re: an detailed explaination why land attack works?

Bill Paul (wpaul@CTR.COLUMBIA.EDU)
Wed, 03 Dec 1997 13:20:23 -0500

Of all the gin joints in all the towns in all the world, Feiyi Wang had
to walk into mine and say:

> Hi, there
>
> Can anyone give a detailed explaination about why land attack works on
> some TCP/IP stack (say BSD-derived)? Which loop is trapped in by this
> "self-connect" request? What's the state transition internally? I can't
> figure it out.
>
> A related question is I can't use tcpdump get any output from the victim
> machine, once it is received the "self-connect" request, it freeze, not
> even a ACK packet. (I am trying it on FreeBSD 2.2.5)
>
> Any information is appreciated.
>
> /Feiyi

With BSD-derived TCP implementations, the ultimate effect of the 'loopback
SYN' attack is that the TCP layer in the kernel gets into an ACK war with
itself. That is, things ultimately reach the point where the kernel is
stuck transmitting ACK segments to itself over an over again.

The reason you don't see the packets on the network with tcpdump is that
they're actually being exchanged internally via the loopback interface.
Generally there's no point in a host putting traffic on the wire just to
talk to itself.

If you have a BSD-derived system and can enable TCP debugging (using
options TCPDEBUG in the kernel config file), then you can see the state
transitions. I did this with FreeBSD 2.2.5 and 2.1.0 (which is vulnerable
to this problem) using a sample program I wrote which creates a TCP socket
and listens for connection on port 8000. The progam also enables the
SO_DEBUG flag on the socket using setsockopt(2). Lastly, in order to
get the kernel to generate printf()s that show the state transitions,
you need to set tcpconsdebug to 1 (it defaults to 0). You can do this
either by editing /sys/netinet/tcp_debug.c and recompiling the kernel
or flipping it on in a running system with the debugger. On FreeBSD,
you can use gdb to do this:

# gdb -q --wcore -k /kernel /dev/mem
(no debugging symbols found)...
IdlePTD 23e000
current pcb at 37d2000
#0 0xf0118557 in mi_switch ()
(kgdb) set tcpconsdebug = 1
(kgdb) quit

Also, since the problem essentially is that the kernel gets stuck in an
infinite loop, you need to use the kernel debugger to set a breakpoint
at tcp_output() so you can see each step in the cycle without your screen
being flooded with printf()s. You need 'options DDB' in your kernel config
file for this, then you can use the console BREAK sequence to drop into
the debugger:

# Debugger("manual escape to debugger")
Stopped at _Debugger+0x35: movb $0,_in_Debugger.122
db> break tcp_output
db> continue

Here's a sample session showing what happens on a FreeBSD 2.2.5 machine
(please excuse the wrapped lines):

- First, I run a small program that creates an TCP socket, bind()s it
to port 8000, turns on the SO_DEBUG flag, and does a listen.

# ./a.out &
[1] 157
# 0xf06cda00 CLOSED:user BIND -> CLOSED
rcv_(nxt,wnd,up) (0,0,0) snd_(una,nxt,max) (0,0,0)
snd_(wl1,wl2,wnd) (0,0,0)
0xf06cda00 CLOSED:user LISTEN -> LISTEN
rcv_(nxt,wnd,up) (0,0,0) snd_(una,nxt,max) (0,0,0)
snd_(wl1,wl2,wnd) (0,0,0)

- Now I transmit a 'loopback SYN' to the listening program. Note that
my program transmits the SYN with a TCP sequence number of 0xdead.

# ./lose2 victimhost 8000
0xf0725900 CLOSED:user ATTACH -> CLOSED
rcv_(nxt,wnd,up) (0,0,0) snd_(una,nxt,max) (0,0,0)
snd_(wl1,wl2,wnd) (0,0,0)

- The first step is that the SYN is received and the connection goes into
the SYN_RECEIVED state. Since we start in the LISTEN state, tcp_input()
allocates a fresh initial sequence number for the receive side of the
connection.

0xf0725900 LISTEN:input dead@0, urp=0<SYN> -> SYN_RCVD
rcv_(nxt,wnd,up) (deae,4000,deae) snd_(una,nxt,max) (e6ab6b,e6ab6b,e6ab6b)
snd_(wl1,wl2,wnd) (dead,0,0)

Breakpoint at _tcp_output: pushl %ebp

- Next, tcp_input() initiates the second part of the 3-way handshake by
sending a SYN,ACK to what it thinks is a remote host.

db> continue
0xf0725900 SYN_RCVD:output [e6ab6b..e6ab6f)@deae, urp=0<SYN,ACK> -> SYN_RCVD
rcv_(nxt,wnd,up) (deae,4000,deae) snd_(una,nxt,max) (e6ab6b,e6ab6c,e6ab6c)
snd_(wl1,wl2,wnd) (dead,0,0)

- Because the source and destination addresses and ports in the forged
SYN packet were the same, the SYN,ACK is reflected back to tcp_input().
The TCP sequence numbers in the packet are the same as those that
were sent out before, which is where the problem starts: tcp_input()
expects that the sequence numbers will relate to the ACK segment that
it wants as part of the three way handshake. Instead, it gets a segment
with the same sequence numbers as before, which places the segment
outside the window. The fact that the ACK bit is set is probably what
keeps tcp_input() from throwing the segment away right at the start.
This is part of the bug: in the SYN_RECEIVED state, you're only supposed
to get an ACK, not a SYN,ACK.

Eventually we arrive at the code detailed on page 958 of _TCP/IP
Illustrated Vol. 2_. This is near line 1030 in our version of
tcp_input.c. According to the commentary on page 959, "The entire
segment lies outside the window and is not a window probe, so the
segment is discarded and an ACK is sent. This ACK will contain the
expected sequence number."

This code does a 'goto dropafterack' which causes an ACK to be sent.
In effect, tcp_input() is saying: "No, that's not the segment I want;
try again." The connection meanwhile stays in the SYN_RECEIVED state.

0xf0725900 SYN_RCVD:drop e6ab6b@deae, urp=0<SYN,ACK> -> SYN_RCVD
rcv_(nxt,wnd,up) (deae,4470,deae) snd_(una,nxt,max) (e6ab6b,e6ab6c,e6ab6c)
snd_(wl1,wl2,wnd) (dead,0,0)
Breakpoint at _tcp_output: pushl %ebp
db> continue
0xf0725900 SYN_RCVD:output e6ab6c@deae, urp=0<ACK> -> SYN_RCVD
rcv_(nxt,wnd,up) (deae,4470,deae) snd_(una,nxt,max) (e6ab6b,e6ab6c,e6ab6c)
snd_(wl1,wl2,wnd) (dead,0,0)

- Now the ACK segment is again reflected back to tcp_input(), again with
exactly the same sequence numbers, which are _not_ the sequence numbers
tcp_input wants. So again, tcp_input() acknowledges the segment but
says: "No stupid, that's still not the segment I wanted; try again"

0xf0725900 SYN_RCVD:drop e6ab6c@deae, urp=0<ACK> -> SYN_RCVD
rcv_(nxt,wnd,up) (deae,4470,deae) snd_(una,nxt,max) (e6ab6b,e6ab6c,e6ab6c)
snd_(wl1,wl2,wnd) (dead,0,0)
Breakpoint at _tcp_output: pushl %ebp
db> continue
0xf0725900 SYN_RCVD:output e6ab6c@deae, urp=0<ACK> -> SYN_RCVD
rcv_(nxt,wnd,up) (deae,4470,deae) snd_(una,nxt,max) (e6ab6b,e6ab6c,e6ab6c)
snd_(wl1,wl2,wnd) (dead,0,0)

The kernel is now trapped in a loop: tcp_input() will again send an ACK
with the same sequence numbers, which will be reflected right back to
tcp_input(), which will cause tcp_input() to send another ACK with the
same sequence numbers, which will be reflected right back to tcp_input(),
which will cause tcp_input() to send another ACK with the same sequence
numbers, which will be reflected right back to tcp_input(), which will
cause tcp_input() to send another ACK with the same sequence numbers,
which will be reflected right back to tcp_input(), which will cause
tcp_input() to send another ACK with the same sequence numbers, and so
on, and so on, and so on, etc...

All this takes place with some interrupts masked, which means the OS
is spinning out of control with no way to stop it short of a shutgun
blast to the head. With BSD you can still manually break into the kernel
debugger and regain partial control of the system and at the very least
force a panic rather than pushing the big red button.

A very quick (but not entirely correct) way to short-circuit the loop
is to test tp->t_state and tiflags before unconditionally branching to
dropafterack; if we are in the TCPS_SYN_RECEIVED and tiflags has something
other than the TH_ACK bit set, then we should not jump directly to
dropafterack. This traps the case where the SYN,ACK is received while
in the SYN_RECEIVED case (which is bogus) and stops the loop condition.

This is not the completely right thing to do though because there's
another case where it won't help. Say for a moment that an attacker has
a way to predict the initial sequence number chosen by the 'victim
machine' and uses that in his forged 'loopback SYN' segment. If this
happens, the connection will get all the way to the ESTABLISHED state,
and the process listen()ing on the socket will end up talking to itself.
I tested this on my home machine by hacking tcp_input() to choose a
predictable sequence number and them modifying the 'loopback SYN'
program to use the same ISS that the kernel would. This time, the
reflected SYN,ACK that comes back to tcp_input() appears to be inside
the expected window, so it avoids the branch to dropafterack. Instead,
the connection becomes ESTABLISHED and the server processes becomes
schitzophrenic and talks to itself.

Whether or not this problem affects a given system depends (obviously)
on the TCP input processing. Some systems that are BSD-derived had made
changes that affect SYN processing (like deflecting SYN floods for
instance) which also happen to provide protection from this attack.
Non-BSD systems like Solaris 2.x may perform more stringent checks,
or do the state processing and window checking in a different order
which allows them to spot the bogus segment before it does any damage.

-Bill

--
=============================================================================
-Bill Paul            (212) 854-6020 | System Manager, Master of Unix-Fu
Work:         wpaul@ctr.columbia.edu | Center for Telecommunications Research
Home:  wpaul@skynet.ctr.columbia.edu | Columbia University, New York City
=============================================================================
  "Now, that's "Open" as used in the sentence "Open your wallet", right?"
=============================================================================