Re: an detailed explaination why land attack works?

From: Bill Paul (wpaulat_private)
Date: Wed Dec 03 1997 - 10:20:23 PST

  • Next message: Phillip R. Jaenke: "Fw: Insufficient allocations in net/unix/garbage.c (fwd)"

    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:         wpaulat_private | Center for Telecommunications Research
    Home:  wpaulat_private | Columbia University, New York City
    =============================================================================
      "Now, that's "Open" as used in the sentence "Open your wallet", right?"
    =============================================================================
    



    This archive was generated by hypermail 2b30 : Fri Apr 13 2001 - 13:34:09 PDT