[Plug] Nkiller - a TCP exhaustion/stressing tool

ithilgore advent.cloud.strife at gmail.com
Wed Oct 22 14:40:10 EEST 2008


Giorgos Keramidas wrote:
> On Tue, 21 Oct 2008 23:29:04 +0300, ithilgore <advent.cloud.strife at gmail.com> wrote:
>   
>> http://sock-raw.homeunix.org/projects/nkiller/nkiller.c
>>     
>
> Ενδιαφέρον σαν concept.  Μερικά σχόλια, τώρα που δεν έχω πιεί καφέ ακόμα
> και μπορώ να γίνω ρόμπα δημοσίως χωρίς να φοβάμαι να χρησιμοποιήσω τη
> δικιολογία «ε ναι, αλλά νύσταζα λίγο ακόμα όταν το έγραψα!» είναι τα εξής...
>
> Με το παρακάτω Makefile θέλει NO_WERROR για να κάνει build, επειδή
> βγάζει κάποια warnings:
>
> : keramida at kobe:/var/tmp/nkiller$ cat -n Makefile
> :      1  PROG= nkiller
> :      2  NO_MAN?= No manpage available.
> :      3
> :      4  WARNS?= 6
> :      5  WFORMAT?= 2
> :      6
> :      7  DPADD+= ${LIBPCAP} ${LIBSSL} ${LIBCRYPTO}
> :      8  LDADD+= -lpcap -lssl -lcrypto
> :      9
> :     10  .include <bsd.prog.mk>
> : keramida at kobe:/var/tmp/nkiller$
>
> Τα warnings που είδα με FreeBSD 8.0-CURRENT είναι:
>
> : keramida at kobe:/var/tmp/nkiller$ make NO_WERROR=1
> : Warning: Object directory not changed from original /var/tmp/nkiller
> : cc -O2 -pipe -march=pentiumpro -g -fstack-protector -Wsystem-headers \
> :   -Wall -Wno-format-y2k -W -Wno-unused-parameter -Wstrict-prototypes \
> :   -Wmissing-prototypes -Wpointer-arith -Wreturn-type -Wcast-qual \
> :   -Wwrite-strings -Wswitch -Wshadow -Wcast-align -Wunused-parameter \
> :   -Wchar-subscripts -Winline -Wnested-externs -Wredundant-decls \
> :   -Wno-pointer-sign -Wformat=2 -Wno-format-extra-args -c nkiller.c
> : nkiller.c: In function 'check_replies':
> : nkiller.c:348: warning: cast discards qualifiers from pointer target type
> : nkiller.c: In function 'main':
> : nkiller.c:875: warning: comparison of unsigned expression < 0 is always false
> : nkiller.c:908: warning: comparison between signed and unsigned
> : nkiller.c:340: warning: 'datagram_len' may be used uninitialized in this function
> : nkiller.c:340: note: 'datagram_len' was declared here
> : cc -O2 -pipe -march=pentiumpro -g -fstack-protector -Wsystem-headers \
> :   -Wall -Wno-format-y2k -W -Wno-unused-parameter -Wstrict-prototypes \
> :   -Wmissing-prototypes -Wpointer-arith -Wreturn-type -Wcast-qual \
> :   -Wwrite-strings -Wswitch -Wshadow -Wcast-align -Wunused-parameter \
> :   -Wchar-subscripts -Winline -Wnested-externs -Wredundant-decls \
> :   -Wno-pointer-sign -Wformat=2 -Wno-format-extra-args \
> :   -o nkiller nkiller.o -lpcap -lssl -lcrypto
> : keramida at kobe:/var/tmp/nkiller$
>
> Ακόμα κι όταν κάνει compile όμως, πετάει core:
>
> : keramida at kobe:/var/tmp/nkiller$ sudo ./nkiller -t 127.0.0.1 -p 1024-65534
> : Segmentation fault: 11 (core dumped)
> : keramida at kobe:/var/tmp/nkiller$
>   


Μα δεν υποστηρίζει τέτοιου είδους port-parsing   (δηλαδή port-range από 
X μέχρι Υ) όπως άλλωστε αναφέρω και στο print_usage() του:

usage: ./nkiller -t <target> -p <port_list> [additional options]
Options:
 -t <target> : the target's IP address
 -p <port_list> : ports separated by commas e.g -p80,110 (specify only 
open ports or use -y)
...

Η αλήθεια είναι πως δεν έχει νόημα να υποστηρίζει port-range  καθώς 
*δεν* είναι port-scanner. Η χρήση του προϋποθέτει πώς  του δίνεις μόνο 
τα open ports ενός host (τα οποία open ports έχουν  βρεθεί μέσω ενός 
άλλου tool πχ Nmap) και αυτά τα open ports συνήθως δεν αναμένεται να 
είναι πολλά αλλά και ούτε διαδοχικά, πράγμα που κάνει τη μορφή  port1, 
port2, ..., portN κατάλληλη.



> Κάνοντας ένα debug build με:
>
> : keramida at kobe:/var/tmp/nkiller$ make clean
> : [...]
> : keramida at kobe:/var/tmp/nkiller$ env DEBUG_FLAGS='-ggdb' CFLAGS='' make NO_WERROR=1
> [...]
> : keramida at kobe:/var/tmp/nkiller$
>
> Είδα ότι το core dump φαίνεται να γίνεται όταν το πρόγραμμα προσπαθεί να
> προσπελάσει μνήμη που δεν έχει αρχικοποιηθεί:
>
> : keramida at kobe:/var/tmp/nkiller$ sudo gdb
> : [...]
> : (gdb) file nkiller
> : Reading symbols from nkiller...done.
> : (gdb) run -t 127.0.0.1 -p 1024-65534
> : Starting program: /var/tmp/nkiller/nkiller -t 127.0.0.1 -p 1024-65534
> :
> : Program received signal SIGSEGV, Segmentation fault.
> : 0x08049e7c in port_add (Target=0x8121020, port=1024) at nkiller.c:667
> : 667             current->next = newNode;
> : (gdb) bt
> : #0  0x08049e7c in port_add (Target=0x8121020, port=1024) at nkiller.c:667
> : #1  0x0804a4fe in main (argc=5, argv=0xbfbfece4) at nkiller.c:909
> : (gdb) p current
> : $1 = (port_elem *) 0xa5a5a5a5
> : (gdb)
>
> Αυτό γίνεται επειδή το -p option parsing απέτυχε, και στη main() ισχύει
> ότι portlen == 0.  Μόλις δεσμεύεται μνήμη για το `Target' object κάνεις:
>
>         Target = safe_malloc(sizeof(HostInfo));
>
> κι ύστερα:
>
>         Target->portlen = portlen;
>
> Αλλά το παρακάτω:
>
> 907             i = 0;
> 908             while (i < Target->portlen) {
> 909                     port_add(Target, o.portlist[i]);
>
> προσπαθεί (έμμεσα μέσα στην port_add() function) να προσπελάσει τα
> members του `Target' που δεν έχουν αρχικοποιηθεί ποτέ.  Μια λύση γι αυτό
> είναι να αντικαταστήσεις την:
>
>     /* malloc version for fewer checks in code */
>     static void *safe_malloc(size_t size)
>     {
>             void *mymem;
>             if ((int) size < 0)  /* Catch caller errors */
>                     fatal("Tried to malloc negative amount of memory!!!\n");
>             mymem = malloc(size);
>             if (mymem == NULL)
>                     fatal("Malloc Failed! Probably out of space.\n");
>             return mymem;
>     }
>
> με κάτι σαν αυτό το ζευγάρι από malloc/calloc wrappers:
>
>     /*
>      * malloc wrapper for fewer checks in code
>      */
>     static void *
>     xcalloc(size_t number, size_t objsize)
>     {
>             void *p;
>
>             /*
>              * Το casting σε `int' και το check για αρνητικές τιμές εδώ δε
>              * χρειάζεται.  Δεν υπάρχει κανένας λόγος γιατί ένα πρόγραμμα
>              * θα 'πρεπε να «σκάει» αν προσπαθήσει να δεσμεύσει πάνω από
>              * 2 GB μνήμης σε 32-bit μηχανήματα.
>              */
>
>             p = calloc(number, objsize);
>             if (p == NULL)
>                     fatal("xcalloc failed.  Running low on space?\n");
>             return p;
>     }
>
>     /*
>      * malloc wrapper for fewer checks in code
>      */
>     static void *
>     xmalloc(size_t nbytes)
>     {
>             void *p;
>
>             p = xcalloc(1, nbytes);
>             if (p == NULL)
>                     fatal("xmalloc failed.  Running low on space?\n");
>             return p;
>     }
>
> Η διαφορά είναι ότι πλέον κάθε malloc() return buffer θα αρχικοποιείται
> τουλάχιστον σε μηδενική τιμή, οπότε το assumption ότι οι μη
> αρχικοποιημένοι pointers είναι null pointers θα «ισχύει».
>
> ------------------------------------------------------------------------
>
> Μια πρόταση που έχω να κάνω είναι να μη χρησιμοποιείς linked list για τα
> port numbers, αλλά κάτι σαν array.  Έτσι κι αλλιώς, από ότι φαίνεται το
> post_list ενός HostInfo αρχικοποιείται μόνο μια φορά.
>   

Η αρχική ιδέα ήταν να γίνει array. Όμως προτιμήθηκε linked list για τον 
εξής λόγο.

Ήθελα να υπάρχει η δυνατότητα να μπορεί να γίνει remove δυναμικά ένα 
port από τη λίστα - κάτι που ενεργοποιείται με το option -y.  Αυτό 
ουσιαστικά που γίνεται είναι να κοιτάει το Νkiller αν πήρε ποτέ RST από 
κάποιο από τα ports που χτυπάει. Αυτό μπορεί να είναι αποτελέσμα 
διάφορων λόγων:

1) το service που άκουγε στο port έπεσε λόγω της επίθεσης, και αν 
ενδεχομένως δεν υπάρχει μηχανισμός αυτόματου resurrection, τότε το port 
θεωρείται πλέον closed

2) ο χρήστης δεν ήταν σίγουρος ότι το port που έδωσε είναι ανοιχτό αλλά 
έχει μια υποψία ότι μπορεί να είναι (άν παρ'ολα αυτά  έχει υποψίες ότι 
είναι πολλά port ανοιχτά, θα του έλεγα να χρησιμοποιήσει tool που κάνει 
αποκλειστικά αυτη τη  δουλειά (aka port-scanner -> Nmap) ; )

Έτσι, το data structure που αποθηκεύονται τα ports που χτυπάμε πρέπει να 
μπορεί να αλλάζει δυναμικά. Αυτό κάνει η function  port_remove(). 
Βέβαια, μια άλλη πιθανή λύση υπό εξέταση μπορεί να ήταν να χρησιμοποιήσω 
array και να βάζω μια special value ( πχ - 1 ) στις θέσει των ports που 
έκλεισαν δυναμικά λόγω RST.  Το πρόβλημα με αυτή τη μέθοδο είναι με το 
πως παίρνεις random value από αυτή τη δομή:

Αν πχ καλείς την rand() ώσπου να πάρεις ένα value που δεν είναι - 1,  
σκέψου τι θα γίνει όταν έχουν μείνει 2 ports και το ένα από αυτά είναι 
"κλεισμένο". Θα καλείς την rand() με πιθανότητα 1/2 να πετύχεις το -1, 
όπου θα πρέπει να την ξανακαλείς ξανά και ξανά μέχρι να πετύχεις το 
"καλό" port. Κάτι τέτοιο σπαταλάει χρόνο χωρίς λόγο.

Από την άλλη, αν κάθε φορά που άλλαζε το state ενός port λόγω RST, για 
να πάρεις random-port διάλεγες και έπαιρνες όλα τα [όχι -1], και τα 
μετέφερες σε ένα άλλο καθαρό array, τότε θα είχαμε περιττά memcpy, κάτι 
που πάλι θα σπαταλούσε χρόνο.

Τώρα όλα τα παραπάνω υποθέτουν ότι η μία λύση είναι θεωρητικά πιο 
σπάταλη από την άλλη, κάτι που στην πράξη χωρίς κατάλληλο profiling ή 
υπολογισμό πολυπλοκοτήτων για διάφορα cases, δεν μπορεί να αποδειχθεί. 
Γι'αυτό μιλάω με επιφύλαξη.


> Μια δεύτερη πρόταση είναι, αφού θέλεις να ξέρεις ποιές ports να κάνεις
> scan, να χρησιμοποιήσεις κάτι σαν `port bitmap' στην αρχικοποίηση, με
> ένα bit για κάθε port που σε ενδιαφέρει να κάνεις scan:
>
>     #define PORTMAP_SIZE        (IPPORT_MAX / CHAR_BIT)
>     uint8_t portmap[PORTMAP_SIZE];
>
> Στο πρώτο port range parsing, απλά θέτεις ένα bit στο portmap[] για τα
> ports που θα γίνουν scan.  Το «parsing» από port ranges όπως "1-100,201"
> ή "1-200,1024-4096" θα έχει _πλάκα_ αλλά είναι το πιο βαρετό κομμάτι.
>   
Indeed, απλά όπως είπα και παραπάνω δεν υπάρχει λόγος να γίνει τέτοιου 
είδους parsing (τέτοιου είδους parsing κάνει το Nmap με τις συναρτήσεις 
getpts_aux, getpts, getpts_simple στο nmap.cc) καθώς δεν είναι port-scanner.

> Το *ΠΡΑΓΜΑΤΙΚΑ* ενδιαφέρον αρχίζει ακριβώς μετά...
>
> Στο δεύτερο πέρασμα, διατρέχεις μία φορά το bitmap, και κρατάς σε ένα
> vector τα port numbers, π.χ.:
>
>     in_port_t *scanports;
>     in_port_t nscanports;
>     in_port_t pindex;
>
>     scanports = malloc(IPPORT_MAX * sizeof(*scanports));
>     if (scanports == NULL)
>         err(1, "malloc");
>     for (nscanports = 0, pindex = 0; pindex < IPPORT_MAX; pindex++) {
>             uint16_t bytepos = pindex / CHAR_BIT;
>             uint16_t bitpos = pindex % CHAR_BIT;
>
>             if ((portmap[bytepos] & (1 << bitpos)) != 0)
>                     scanports[nscanports++] = pindex;
>     }
>
> Μετά μπορείς να αντιγράψεις σε ένα δεύτερο vector το scanports[], να το
> «ανακατέψεις» με random τρόπο μία φορά στην αρχή (αντί να διαλέγεις ένα
> random port κάθε φορά που το χρειάζεσαι), και να κάνεις διάφορα άλλα
> κόλπα... όπως π.χ. να «μοιράσεις» το randomized port range σε κομμάτια
> και να ταΐσεις το κάθε sub-range σε ξεχωριστό scanner thread, κλπ.
>   

Τη λύση του αρχικού ανακατέματος δεν τη βρίσκω τόσο καλή για τον λόγο 
ότι απλά δεν είναι really random. (τουλάχιστον δεν είναι αρκετά random, 
αφού πραγματικά random σχεδον δεν υφίσταται) Tο "randomness" του 
συστήματος επαναλαμβάνεται αφού αν πχ έχουν ανακατευτεί τα ports ως εξής 
22,111,80,21 όταν τελειώσει ο ένας γύρος, ο επόμενος γύρος θα τα βρει 
στην ίδια σειρά (δηλαδή πάλι θα τα χτυπήσει με τη σειρά 22,111,80,21) - 
κάτι που είναι πιο "προδοτικό" στα IDSs ως signature για attack.
Άλλωστε, τέτοιου είδους αρχικό ανακάτεμα, θα μπορούσε να κάνει και ο 
χρήστης δίνοντας τα ports σε μη-άυξουσα και μη-φθίνουσα σειρά e.g 
-p111,80,3389,21

Όσο για τα scanner threads, ενώ είναι καλή ως ιδέα, στην προκειμένη 
περίπτωση, δεν υφίστανται καν threads και ούτε χρειάζεται να υπάρχουν.
Όλη η ταχύτητα του tool προέρχεται από το statelessness που του δίνουν 
τα reverse syn cookies. Αυτό που γίνεται είναι κάτι ανάλογο με τα syn 
cookies που όλοι ξέρουμε. Στην περίπτωση μας όμως, γίνεται από client 
πλευρά. Δηλαδή περνάνε τα { source ip, source port, destination ip, 
destination port } μέσα από μια hash function (πχ sha1) μαζί με ένα 
secret key και από το αποτέλεσμα παίρνουμε τα 32 πρώτα bit και τα 
τοποθετούμε στο TCP sequence field του TCP header του SYN packet που θα 
στείλουμε. Όταν λάβουμε ένα SYN|ACK packet, κάνουμε swap τα values { 
source ip, source port, destination ip, destination port } ώστε τα 
source να γίνουν destination και αντίστροφα και υπολογίζουμε εκ των 
προτέρων με το ίδιο secret key και την ίδια hash function, έναν μαγικό 
αριθμό. Αν αυτός ο μαγικός αριθμός (πάλι τα 32 πρώτα bit του) είναι ίσος 
με το acknowledgment number - 1 (το οποίο acknowledgement number στο 
3way handshake είναι ίσο με το (sequence + 1) του πακέτου στο οποίο 
απαντάμε) τότε σημαίνει πως πρόκειται για απάντηση σε ένα πακέτο που 
στείλαμε εμείς προηγουμένως.

Κατά τον παραπάνω τρόπο, μπορούμε απλά να στείλουμε ένα burst από 
packets, χωρίς να φροντίσουμε να κρατήσουμε καμία πληροφορία για αυτά. 
Αυτό είναι που κάνει και το tool τόσο ταχύ.

> HTH,
> Γιώργος
>
>   
Good to see real feedback!

-- ithilgore




More information about the Plug mailing list