SYN Flood Basics
Over a year ago I talked about the basics of DNS amplification. Another common concept to the DDoS landscape is SYN floods. SYN floods are a core DDoS attack vector leveraging SYN packets and the connection initiation mechanism of the TCP protocol. Several layers of mitigations are commonly implemented out of the box on modern operating systems and large SYN floods nowadays exceed several million packets per second and are no longer necessarily targeting the specific TCP handshake vulnerabilities that made them lethal back when they were initially conceived. However, due to the fact that generating SYN floods is very easy and can still cause a lot of unwanted side effects, they are still relatively commonplace and more often than not they are now generated to attempt to overload the underlying processing capabilities of the network stack.Today I wanted to briefly share one of the initial attack vectors SYN floods historically tried to leverage.
The SYN Queue
TCP connections are established with a three way handshake: SYN, SYN-ACK, ACK. Servers receiving incoming TCP connections would normally keep the state of current connections in two queues which we shall call the SYN queue, and the accept queue. The SYN queue is intended to hold the state of connections currently being established, or better, everytime the underlying network stack of the operating system receives a SYN packet, it will save an entry in the SYN queue along with details relevant to the connection that the client is trying to establish (e.g. port, source IP etc.). It will then send back a SYN-ACK packet to the client, and only once it receives a response (the ACK packet) it will remove the entry from the SYN queue and insert it in the accept queue where it is ready for the relevant application to take over the connection.
The SYN queue has a specific size. On most Linux based operating systems you can check the maximum size of the SYN queue like so:
$ sysctl net.core.somaxconn
On my servers this value is set to 128. When SYN floods initially started appearing, they tried to fill up the SYN queue not making it possible for new clients to establish TCP connections with the server therefore succeeding in a denial of service (DoS) attack. And filling up the queue was relatively straightforward: just send a lot of SYN packets and never respond to the SYN-ACK the server sends back. Operating systems of course have a method of purging broken half established connections from the SYN queue, but not until after they have tried several times to contact the client again (by re sending the SYN-ACK packet) and several timeouts have expired. So as long as we are sending SYN packets faster than the operating system is purging entries from the SYN queue, the attack will eventually succeed. You can watch how many entries are in the SYN queue at a given time on most Linux based operating systems with something like:
$ watch -n 0.2 "ss -n state syn-recv sport = :80 | wc -l"
Generating SYN Packets
Just like in our DNS amplification basics post, we shall leverage Scapy to generate arbitrary SYN packets so we can observe the SYN queue filling up. A simple script to achieve this would look like so:
from scapy.all import * ip = IP(dst = sys.argv) tcp = TCP( sport = RandShort(), dport = 80, flags = "S" ) request = ip/tcp/"SYN Flood Test" srloop(request, inter = 0.1, retry = 2, timeout = 4)
The script is simple to follow:
- Create IP packet;
- Create TCP packet;
- Set SYN flag;
- Set random src port;
- Set destination port 80 (in this example I was targeting a web server);
- Send packets!
On the machine where we are running the python script from, we will also need to stop the operating system sending out RST packets in response to SYN-ACKs it does not recognise. If we don't do this the target machine will purge entries from the SYN queue immediately once it receives RST packets. On my system I achieved this like so:
$ firewall-cmd --permament --direct --add-rule ipv4 filter OUTPUT 0 -p tcp -m tcp -s X.X.X.X --tcp-flags RST RST -j DROP
With X.X.X.X being replaced by the IP address of the machine.
If we have two instances, one running a web server, and another running our python script, we would be able to observe the SYN queue on the target machine filling up, all the way to 128 in my case. But once it reaches 128, things keep working and you can still access the web server with a browser without issues!
The attack is not successful as the operating system starts using a common SYN flood mitigation mechanisms called SYN cookies as soon as the SYN queue is full.
TCP packets have a 32 bit sequence number. When SYN cookies are used, the machine receiving incoming connections, will craft a specific sequence number that will encode the minimum set of required information to establish a TCP connection along with a cryptographic hash. It will then use that sequence number in the SYN-ACK response. The sequence number itself is the cookie here which holds all the information required removing the need of the SYN queue altogether!
When the server receives an ACK packet, it will be able to retrieve the original sequence number (by subtracting 1), verify the hash, and build a complete connection by decoding the data present in the SYN cookie itself. SYN cookies are therefore completely offloading the requirement of storing data server side to the connection itself.
Of course this method does have disadvantages and side effects which is the reason it is not employed by default. 32 bits does not allow for much data to be stored therefore a number of variables will be dropped, which do affect specific use cases and often reduce the quality of the connection, at least initially. It also increases CPU load to establish TCP connections.
SYN cookies are a very old concept initially implemented by Daniel J. Bernstein and Eric Schenk in September 1996 and had a noticeable performance impact, at least within the Linux kernel, until 2016. SYN cookies nowadays are enabled by default and you can check with:
$ sysctl net.ipv4.tcp_syncookies
You can also disable them by setting:
net.ipv4.tcp_syncookies = 0 in
/etc/sysctl.conf and running:
$ sysctl -p
If you were to do so on your target machine you will observe that once the SYN queue is full, the server won't be able to accept any new incoming connections, and trying to connect to the machine with e.g. a web browser would fail, making the DoS attack a success!
Both the content in this post and the DNS amplification basics post were inspired by the SYN packet handling in the wild post by Marek Majkowski and was prepared by myself as part of a presentation for the London Web Performance group titled Large Scale DDoS Mitigation.