9

I’m trying to build a three-way handshake in Scapy. Using the following code,

#!/usr/local/bin/python

from scapy.all import *

sport = random.randint(1024, 65535)

# SYN
ip = IP(src='172.16.120.5', dst='172.16.100.101')
SYN = TCP(sport=sport, dport=443, flags='S', seq=1000)
SYNACK = sr1(ip/SYN)

# ACK
my_ack = SYNACK.seq + 1
ACK = TCP(sport=sport, dport=443, flags='A', seq=1001, ack=my_ack)
send(ip/ACK)

However, on the server I see only SYN_RECV, even though the return SYN-ACK is sent and the ACK is sent in return. Here is a capture from the server (172.16.100.101),

08:10:19.455038 IP 172.16.120.5.58972 > 172.16.100.101.https: S 1000:1000(0) win 8192
08:10:19.455343 IP 172.16.100.101.https > 172.16.120.5.58972: S 2541678705:2541678705(0) ack 1001 win 18484 <mss 1460>
08:10:19.545808 IP 172.16.120.5.58972 > 172.16.100.101.https: . ack 1 win 8192
08:10:24.015204 IP 172.16.100.101.https > 172.16.120.5.58972: S 2541678705:2541678705(0) ack 1001 win 18484 <mss 1460>

As you can see, the SYN-ACK is being sent twice, so it looks like the server doesn’t like the final ACK. Any ideas?

I’ve also printed the output of each of the packets directly from Python. Note this was for a different connection.

>>> SYN
<TCP  sport=26193 dport=https seq=1000 flags=S |>
>>>
>>> SYNACK
<IP  version=4L ihl=5L tos=0x0 len=44 id=0 flags=DF frag=0L ttl=63 proto=tcp chksum=0x741 src=172.16.100.101 dst=172.16.120.5 options=[] |<TCP  sport=https dport=26193 seq=1023579974 ack=1001 dataofs=6L reserved=0L flags=SA window=18484 chksum=0xdb18 urgptr=0 options=[('MSS', 1460)] |<Padding  load='\x00\x00' |>>>
>>>
>>> ACK
<TCP  sport=26193 dport=https seq=1001 ack=1023579975 flags=A |>

Below shows a successful and unsuccessful connection.

Scapy

20:58:37.357056 IP 172.16.120.5.35957 > 172.16.100.101.https: S 10:10(0) win 8192
20:58:37.357369 IP 172.16.100.101.https > 172.16.120.5.35957: S 900629853:900629853(0) ack 11 win 18484 <mss 1460>
20:58:37.445888 IP 172.16.120.5.35957 > 172.16.100.101.https: . ack 900629854 win 8192

cURL

20:58:46.165413 IP 172.16.120.5.33241 > 172.16.100.101.https: S 2266708589:2266708589(0) win 5840 <mss 1460,sackOK,timestamp 17370497 0,nop,wscale 6>
20:58:46.166296 IP 172.16.100.101.https > 172.16.120.5.33241: S 2138155488:2138155488(0) ack 2266708590 win 18460 <mss 1460,sackOK,timestamp 107550664 17370497,nop,wscale 7>
20:58:46.169026 IP 172.16.120.5.33241 > 172.16.100.101.https: . ack 2138155489 win 92 <nop,nop,timestamp 17370497 107550664>
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
felix001
  • 15,341
  • 32
  • 94
  • 121
  • The `SYN-ACK` packet is logged as `ack 1 win 8192`, which seems to indicate an `ack` with an expected sequence number of 1 instead of 2541678706. Can you print `SYNACK` and `my_ack` (though they seem to be set correctly)? – Yoel Oct 21 '14 at 19:12
  • Ive updated the question. Think you are right the finaly ACK has the wrong ack number. However if I take a working connection ie perform a curl to port 443 which is established successfully this also shows the final ACK with a 1 – felix001 Oct 21 '14 at 19:43
  • Can you also post an equivalent log of a successful `curl` connection? How is the server implemented? Can you replace it with a `scapy` script that prints the received packets? – Yoel Oct 21 '14 at 19:56
  • The final ACK isnt the problem as if I perform a tcpdump with the -S option i.e showing the relative seq/ack numbers it is as expected. – felix001 Oct 21 '14 at 19:57
  • I think the root cause of this might be [`SYN cookie`](http://en.wikipedia.org/wiki/SYN_cookies) mismatch, though I'm unsure of this and definitely don't understand why it would happen. If you're up for investigating this further, I would fully imitate the `curl` connection establishment via `scapy`, sending the exact same packets and expecting similar results. Then, I would incrementally modify more and more fields until the connection is no longer established, hoping to isolate the problematic field/s. – Yoel Oct 21 '14 at 23:30

2 Answers2

10

I managed to fix this in the end by incrementing the final SEQ number of the ACK.

from scapy.all import *

sport = random.randint(1024, 65535)

# SYN
ip = IP(src='172.16.120.5', dst='172.16.100.101')
SYN = TCP(sport=sport, dport=443, flags='S', seq=1000)
SYNACK = sr1(ip/SYN)

# SYN-ACK
ACK = TCP(sport=sport, dport=443, flags='A', seq=SYNACK.ack + 1, ack=SYNACK.seq + 1)
send(ip/ACK)

Here’s a tcpdump showing the behaviour...

20:47:54.226591 IP 172.16.120.5.55348 > 172.16.100.101.443: S 1000:1000(0) win 8192
20:47:54.227220 IP 172.16.100.101.443 > 172.16.120.5.55348: S 4265040634:4265040634(0) ack 1001 win 18484 <mss 1460>
20:47:54.317452 IP 172.16.120.5.55348 > 172.16.100.101.443: . ack 4265040635 win 8192
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
felix001
  • 15,341
  • 32
  • 94
  • 121
  • i did but your seq number was wrong , needs to be 1002. – felix001 Oct 23 '14 at 14:42
  • This is odd... why does this work? Can you post a trace log of the packets following this update? If you increment the sequence by more, does it work as well? – Yoel Oct 24 '14 at 19:31
  • Ive added a tcpdump. Also if I try anything higher it works. but I have to agree this is odd. Ive also tried raising the seq number above the 1002 and it works. Ive also tried this on another device (i.e QNAP NAS). – felix001 Oct 24 '14 at 20:14
5

This gist implements a simple Scapy three-way handshake class based on the example in scapy.layers.inet. For reference, this is the code:

#!/usr/bin/env python
# -*- coding: UTF-8 -*-
# Author: tintinweb@oststrom.com <github.com/tintinweb>
'''
A simple TCP three-way handshake example

#> python scapy_tcp_handshake.py
DEBUG:__main__:init: ('oststrom.com', 80)
DEBUG:__main__:start
DEBUG:__main__:SND: SYN
DEBUG:__main__:RCV: SYN+ACK
DEBUG:__main__:SND: SYN+ACK -> ACK
DEBUG:__main__:RCV: None
DEBUG:__main__:RCV: None
None
DEBUG:__main__:SND: FIN
DEBUG:__main__:RCV: None

Note: Linux might send an RST for forged SYN packets. Disable it by executing:
#> iptables -A OUTPUT -p tcp --tcp-flags RST RST -s <src_ip> -j DROP
'''
from scapy.all import *
import logging
logger = logging.getLogger(__name__)

class TcpHandshake(object):

    def __init__(self, target):
        self.seq = 0
        self.seq_next = 0
        self.target = target
        self.dst = iter(Net(target[0])).next()
        self.dport = target[1]
        self.sport = random.randrange(0, 2**16)
        self.l4 = IP(dst=target[0])/TCP(sport=self.sport, dport=self.dport, flags=0,
                                        seq=random.randrange(0, 2**32))
        self.src = self.l4.src
        self.swin = self.l4[TCP].window
        self.dwin = 1
        logger.debug("init: %s"%repr(target))

    def start(self):
        logger.debug("start")
        return self.send_syn()

    def match_packet(self, pkt):
        if pkt.haslayer(IP) and pkt[IP].dst == self.l4[IP].src \
           and pkt.haslayer(TCP) and pkt[TCP].dport == self.sport \
           and pkt[TCP].ack == self.seq_next:
           return True
        return False

    def _sr1(self, pkt):
        send(pkt)
        ans = sniff(filter="tcp port %s"%self.target[1], lfilter=self.match_packet, count=1, timeout=1)
        return ans[0] if ans else None

    def handle_recv(self, pkt):
        if pkt and pkt.haslayer(IP) and pkt.haslayer(TCP):
            if pkt[TCP].flags & 0x3f == 0x12:   # SYN+ACK
                logger.debug("RCV: SYN+ACK")
                return self.send_synack_ack(pkt)
            elif  pkt[TCP].flags & 4 != 0:      # RST
                logger.debug("RCV: RST")
                raise Exception("RST")
            elif pkt[TCP].flags & 0x1 == 1:     # FIN
                logger.debug("RCV: FIN")
                return self.send_finack(pkt)
            elif pkt[TCP].flags & 0x3f == 0x10: # FIN+ACK
                logger.debug("RCV: FIN+ACK")
                return self.send_ack(pkt)

        logger.debug("RCV: %s"%repr(pkt))
        return None

    def send_syn(self):
        logger.debug("SND: SYN")
        self.l4[TCP].flags = "S"
        self.seq_next = self.l4[TCP].seq + 1
        response = self._sr1(self.l4)
        self.l4[TCP].seq += 1
        return self.handle_recv(response)

    def send_synack_ack(self, pkt):
        logger.debug("SND: SYN+ACK -> ACK")
        self.l4[TCP].ack = pkt[TCP].seq + 1
        self.l4[TCP].flags = "A"
        self.seq_next = self.l4[TCP].seq
        response = self._sr1(self.l4)
        return self.handle_recv(response)

    def send_data(self, d):
        self.l4[TCP].flags = "PA"
        response = self._sr1(self.l4/d)
        self.seq_next = self.l4[TCP].seq + len(d)
        self.l4[TCP].seq += len(d)
        return self.handle_recv(response)

    def send_fin(self):
        logger.debug("SND: FIN")
        self.l4[TCP].flags = "F"
        self.seq_next = self.l4[TCP].seq + 1
        response = self._sr1(self.l4)
        self.l4[TCP].seq += 1
        return self.handle_recv(response)

    def send_finack(self, pkt):
        logger.debug("SND: FIN+ACK")
        self.l4[TCP].flags = "FA"
        self.l4[TCP].ack = pkt[TCP].seq + 1
        self.seq_next = self.l4[TCP].seq + 1
        response = send(self.l4)
        self.l4[TCP].seq += 1
        raise Exception("FIN+ACK")

    def send_ack(self, pkt):
        logger.debug("SND: ACK")
        self.l4[TCP].flags = "A"
        self.l4[TCP].ack = pkt[TCP].seq + 1
        self.seq_next = self.l4[TCP].seq + 1
        response = self._sr1(self.l4)
        self.l4[TCP].seq += 1

if __name__=='__main__':
    logging.basicConfig(level=logging.DEBUG)
    logger.setLevel(logging.DEBUG)
    conf.verb = 0
    tcp_hs = TcpHandshake(("oststrom.com", 80))
    tcp_hs.start()
    print repr(tcp_hs.send_data("INTENTIONAL BAD REQUEST\r\n\r\n\r\n"))
    tcp_hs.send_fin()
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
tintin
  • 3,176
  • 31
  • 34