Letzte Änderung: 26.01.98

ASM86FAQ - Security



(Übersicht)

Anti-Debugger-Tips

Stefan Meisel 2:2480/403.5:

Vorab muß zu diesem Thema grundsätzlich gesagt werden, daß es keinen absolut sicheren Softwareschutz gibt. Mit viel Geduld und Erfahrung, aber spätestens mit Einsatz von Hardware-Debuggern, Code- und Prozessor-Emulatoren läßt sich jedes Stück Software entschlüsseln. Das Ziel solcher Verfahren kann also immer nur sein, das 'Reverse Engineering' so schwer und langwierig wie möglich zu machen. Oft führen solche Anstrengungen zu Instabilitäten des fertigen Programms, immer jedoch zu erschwerter Wartbarkeit des Code.

Der folgende Text ist eine freie Übersetzung einer englischen Originalvorlage von Inbar Raz, Eden Shochat, Yossi Gottlieb und anderen Autoren mit dem Titel 'Anti Debugging Tricks, Version 5', die im Fido frei verfügbar ist. Bei der Übersetzung wurden von mir an einigen Stellen Korrekturen und Vereinfachungen vorgenommen. Die meisten hier beschriebenen Maßnahmen zielen auf die typischen Debugger- Interrupts Int 1 (Trace) und Int 3 (Breakpoint). Ein moderner Debugger läßt sich mit diesen Methoden nicht mehr so leicht beeindrucken, aber ich bin der Meinung, daß diese Verfahren zunächst grundsätzlich bekannt sein sollten. Schon in der Originalvorlage wurde in den Beispielen zur Verbesserung der Lesbarkeit das Instruktionenpaar CLI/STI um jede Interrupt-Manipulation weggelassen. In der Praxis ist dies jedoch keinesfalls empfehlenswert, da ein Hardware-Interrupt in der Mitte einer solchen Aktion den Rechner zum Absturz bringen kann. --------------------- Anti Debugging Tricks --------------------- Anti-Debugging Tricks lassen sich in zwei Hauptklassen unterscheiden: 1. Preventive Maßnahmen 2. Selbstmodifizierender Code Die meisten Anti-Debugging Tricks werden heutzutage in Viren eingesetzt, um die Disassemblierung des Virus zu vermeiden. Beispiele hierfür finden sich weiter unten im Text. Außerdem enthalten Software-Schutzprogramme, die als Kopier- oder Hackschutz Anwendung finden, eine große Zahl von Finten, um das Knacken der Schutzmechanismen zu erschweren. 1. Preventive Maßnahmen ------------------------

Grundsätzlich versteht man unter preventiven bzw. vorbeugenden Maßnahmen eine Programmgestaltung, die es für den Anwender unmöglich macht, den Code zu disassemblieren oder zu tracen.

1.1 Abschalten von Interrupts

Das Abschalten von Interrupts ist wohl die gebräuchlichste Form von Anti- Debugging Tricks. Hier gibt es verschiedene Möglichkeiten: 1.1.1 Maskieren der Hardware Interrupts Übliches Vorgehen ist, einen Interrupt über den 8259 Programmable Interrupt Controller (PIC) auszuschalten, um ein Tracen des Codes zu verhindern. Der PIC, der die IRQ-Leitungen kontrolliert, kann über Portbefehle an Port 21h gesteuert werden. Das bedeutet, daß jeder IRQ zwischen 0 und 7 einzeln abgeschaltet werden kann. Bit 0 entspricht IRQ0, Bit1 entspricht IRQ1 usw. Nachdem IRQ1 der Tastatur-Interrupt ist, kann das Keyboard abgeschaltet und damit der Debugger abgehängt werden. Beispiel: CS:0100 E421 IN AL,21 CS:0102 0C02 OR AL,02 CS:0104 E621 OUT 21,AL Nebenbei bemerkt, kann die Tastatur auch über den Keyboard Controller 8042 (Ports 60h und 64h) abgeschaltet werden. Der Port 61h am Programmable Peripheral Interface (PPI) bietet ev. eine weitere Möglichkeit, funktioniert aber nicht immer.

Beispiel:

CS:0100 E461 IN AL,61 CS:0102 0C80 OR AL,80 CS:0104 E661 OUT 61,AL

1.1.2 Maskieren der Software Interrupts

Dies ist eine ziemlich einfache Maßnahme, die darin besteht, Interrupt- Adressen zu überschreiben, die von Debuggern üblicherweise benutzt werden. Es können auch andere Interrupts verändert werden, die das Programm nicht braucht oder deren Auftreten nicht zu erwarten ist. Dabei sollte niemals vergessen werden, die ursprünglichen Interrupt-Adressen vor Programmende zu restaurieren. Um einen Vektor zu verändern, empfiehlt sich die manuelle Methode, wie unten gezeigt, anstatt Interrupt 21h, Service 25h zu benutzen. Jeder Debugger, der die Kontrolle über Int 21h hat, könnte sonst den geänderten Interrupt auf sich selbst zeigen lassen. Das Beispiel zeigt ein Abfangen des Breakpoint Interrupt 3.

Beispiel:

CS:0100 EB04 JMP 0106 CS:0102 0000 ADD [BX+SI],AL CS:0104 0000 ADD [BX+SI],AL CS:0106 31C0 XOR AX,AX CS:0108 8EC0 MOV ES,AX CS:010A 268B1E0C00 MOV BX,ES:[000C] CS:010F 891E0201 MOV [0102],BX CS:0113 268B1E0E00 MOV BX,ES:[000E] CS:0118 891E0401 MOV [0104],BX CS:011C 26C7064C000000 MOV Word Ptr ES:[000C],0000 CS:0123 26C7064E000000 MOV Word Ptr ES:[000E],0000

1.1.3 Überschreiben von Vektoren

Diese Methode bezieht die Benutzung einer Interrupt-Adresse in die richtige Aktivierung einer Codesequenz mit ein. Eine solche Maßnahme wie im folgenden Beispiel könnte dafür eingesetzt werden, ein Stück Code zu entschlüsseln (siehe auch 2.1). Über den Stackpointer werden hier Daten auf der Interrupt- Adresse abgelegt. Nachdem die Interrupts 1 und 3 im normalen Programmablauf nicht benutzt oder verändert werden, funktioniert das Programm, solange man nicht versucht, es zu debuggen. Beispiel: CS:0100 31C0 XOR AX,AX CS:0102 8ED0 MOV SS,AX CS:0104 BC0E00 MOV SP,000E CS:0107 2E8B0E3412 MOV CX,CS:[1234] CS:010C 50 PUSH AX CS:010D 31C8 XOR AX,CX CS:010F 21C5 AND BP,AX CS:0111 58 POP AX CS:0112 E2F8 LOOP 010C 1.1.4 Austausch von Interrupts Dies ist ein ziemlich schmutziger Trick, der nur eingesetzt werden sollte, wenn man sich völlig sicher ist, daß man sein Programm nicht mehr debuggen muß. Hierbei werden die Adressen von häufig benutzten Interrupts, z.B. Int 16h und Int 21h, auf die Adressen von Int 1 und Int 3, die im Normalfall nicht gebraucht werden, kopiert. Der Anwender, der dieses Programm tracen will, muß jede Stelle, an der ein Int 1 auftritt, gegen den entsprechenden Interrupt austauschen. Erschwerend ist der Umstand, daß Int 3 durch einen 1- Byte Opcode (CCh) dargestellt werden kann. Dies macht den direkten Austausch gegen jeden anderen Interrupt unmöglich. Beispiel: CS:0100 FA CLI CS:0101 31C0 XOR AX,AX CS:0103 8EC0 MOV ES,AX CS:0105 26A18400 MOV AX,ES:[0084] CS:0109 26A30400 MOV ES:[0004],AX CS:010D 26A18600 MOV AX,ES:[0086] CS:0111 26A30600 MOV ES:[0006],AX CS:0115 B44C MOV AH,4C CS:0117 CD01 INT 01 1.2 Zeitkontrolle Diese Methode ist weniger gebräuchlich, aber nützlich gegen den Einsatz von Debuggern, die alle Interrupts außerhalb der Laufzeit des Programms abschalten, wie z.B. der Turbo Debugger von Borland. Hierbei wird einfach der Wert eines Zeitzählers, der sich mit Int 8 verändert, ausgelesen. In einer Endlosschleife wird dann solange gewartet, bis sich der Wert ändert. Ein anderes Beispiel besteht darin, den Timer Interrupt IRQ 0 zu maskieren, indem der Wert aus Port 21h gelesen, mit 1 Oder-verknüpft und zurück- geschrieben wird. (Einige Takte später muß man dies natürlich rückgängig machen.) CS:0100 2BC0 SUB AX,AX CS:0102 FB STI CS:0103 8ED8 MOV DS,AX CS:0105 8A266C04 MOV AH,[046C] CS:0109 A06C04 MOV AL,[046C] CS:010C 3AC4 CMP AL,AH CS:010E 74F9 JZ 0109 1.3 Debugger verwirren Diese nette Technik wirkt speziell und nur gegen Turbo Debugger sowie ähnliche Produkte. Hier wird ein Sprung in die Mitte einer Instruktion gelegt, wobei die direkt nachfolgende Adresse einen anderen Opcode enthält. Debug oder SymDeb lassen sich hiermit nicht täuschen, weil sie zum exakten Sprungziel verzweigen, während der Turbo Debugger im Einzelschrittmodus an den Anfang der nächstliegenden Adresse 0106 springt. Beispiel: CS:0100 E421 IN AL,21 CS:0102 B0FF MOV AL,FF CS:0104 EB02 JMP 0108 CS:0106 C606E62100 MOV Byte Ptr [21E6],00 CS:010B CD20 INT 20 Wobei das Sprungziel ist: CS:0108 E621 OUT 21,AL Hinweis: Dieser Trick beeinflußt den Ablauf des Programms in keinem Debugger. Er dient nur dem Zweck, den Anwendur über den wahren Opcode hinwegzutäuschen. 1.4 Überprüfen der CPU Flags Dieser Trick kann gegen fast jeden Real Mode Debugger eingesetzt werden. Man muß nur das Trace Flag irgendwo im Programm löschen und es später überprüfen. Wenn es eingeschaltet ist, läuft ein Debugger im Hintergrund. Beispiel: CS:0100 9C PUSHF CS:0101 58 POP AX CS:0102 25FFFE AND AX,FEFF CS:0105 50 PUSH AX CS:0106 9D POPF Und später im Programm: CS:1523 9C PUSHF CS:1524 58 POP AX CS:1525 250001 AND AX,0100 CS:1528 7402 JZ 152C CS:152A CD20 INT 20 1.5 Debugger anhalten Diese Technik läßt Debugger bei der Ausführung bestimmter Programme anhalten. Erreicht wird dies, indem man Int 3 Befehle zufällig über den Code verstreut, an denen der Debugger stoppen muß. Die beste Wirkung wird in Schleifen erzielt. Beispiel: CS:0100 B96402 MOV CX,0264 CS:0103 BE1001 MOV SI,0110 CS:0106 AC LODSB CS:0107 CC INT 3 CS:0108 98 CBW CS:0109 01C3 ADD BX,AX CS:010B E2F9 LOOP 0106 1.6 Stack Manipulationen Diese Methode geht davon aus, daß viele Debugger den Stack ihres Zielprogramms mitbenutzen. Im folgenden Beispiel wird der Stack in die Mitte eines Programmcodes gelegt, der selbst keinen Stack benötigt. Als Folge wird der Debugger einen Teil des Programmcodes durch seinen eigenen Stackverbrauch überschreiben (hauptsächlich durch Interrupt Return- Adressen). Mit weniger drastischen Auswirkungen als im Code läßt sich der Stackpointer auch in die eigenen Daten legen. Die CLI und STI Anweisungen im Beispiel dürfen nicht weggelassen werden, sonst hängt sich das Programm auch ohne Debugger auf. Beispiel: CS:0100 8CD0 MOV AX,SS CS:0102 89E3 MOV BX,SP CS:0104 0E PUSH CS CS:0105 17 POP SS CS:0106 BC0B01 MOV SP,010B CS:0109 90 NOP CS:010A 90 NOP CS:010B EB02 JMP 010F CS:010D 90 NOP CS:010E 90 NOP CS:010F 89DC MOV SP,BX CS:0111 8ED0 MOV SS,AX 1.7 TD386 im V86 Mode anhalten Dies ist eine Methode, das V86 Modul (TD386) des Turbo Debuggers zu stoppen. Es beruht auf dem Umstand, daß der TD386 nicht den Int 0 benutzt, um einen Division by Zero-Error (oder Registerüberlauf nach Division, der von der CPU genauso behandelt wird wie eine Division durch Null) zu melden. Wenn der TD386 einen Divisionsfehler entdeckt, endet er unter Angabe der fehlerhaften DIV Instruktion. Im Real Mode (und auch unter den konventionellen Debuggern) wird eine fehlerhafte Division den Interrupt 0 auslösen. Folglich, falls der Int 0 auf die nachfolgende Instruktion zeigt, läuft das Programm weiter.

Es ist natürlich notwendig, Int 0 sofort zu restaurieren, sonst wird der nächste Aufruf von Int 0 zum Absturz führen. Beispiel: CS:0100 31C0 XOR AX,AX CS:0102 8ED8 MOV DS,AX CS:0104 C70600001201 MOV WORD PTR [0000],0112 CS:010A 8C0E0200 MOV [0002],CS CS:010E B400 MOV AH,00 CS:0110 F6F4 DIV AH CS:0112 B8004C MOV AX,4C00 CS:0115 CD21 INT 21 1.8 Jeden V86 Prozess anhalten Eine andere Möglichkeit, den TD386 auszuschalten, ist, ihn in eine Exception zu treiben. Allerdings wird diese Exception auch unter jedem anderen Programm, das im V86-Mode läuft, ausgelöst. Die Exception ist #13 und wird durch Interrupt 0Dh ausgelöst. Die Idee ähnelt dem Trick mit der Division durch Null: Man löst eine Exception aus, während der Exception Interrupt irgendwo in den Programmcode zeigt. Das funktioniert nur im Real-Mode, nie aber im V86-Mode. Auch hier gilt wieder: Die originale Interrupt Adresse muß sofort restauriert werden, sonst hängt die nächste Exception die Maschine auf. Beispiel: CS:0100 31C0 XOR AX,AX CS:0102 8ED8 MOV DS,AX CS:0104 C70634001301 MOV WORD PTR [0034],0113 CS:010A 8C0E3600 MOV [0036],CS CS:010E 833EFFFF00 CMP WORD PTR [FFFF],+00 CS:0113 B8004C MOV AX,4C00 CS:0116 CD21 INT 21 2. Selbstmodifizierender Code ----------------------------- 2.1 Verschlüsselung/Entschlüsselung Die erste Kategorie ist einfach verschlüsselter Code, dem eine Entschlüsse- lungsroutine vorgeschaltet wurde. Die Methode erschwert das Setzen von Breakpoints. Wenn durch einen Debugger ein Breakpoint gesetzt wird, schreibt er üblicherweise an die gewünschte Adresse den Opcode CCh (Int 3). Sobald der Int 3 ausgeführt wird, erhält der Debugger die Kontrolle über das System. Nachdem die Entschlüsselungsroutine aus zahlreichen Schleifen- befehlen besteht, ist die Versuchung groß, den Breakpoint hinter die Entschlüsselungsroutine zu setzen. Dort aber wird der Code modifiziert und der Breakpoint zu einer undefinierten Instruktion verdreht. Das folgende Beispiel stammt aus dem Haifa Virus. Ein Breakpoint an der Stelle CS:110 wird niemals erreicht, weil das Resultat der Entschlüsselung nicht vorhersagbar ist. Um das Tracen des Codes zu erschweren, wird die Entschlüsselung am Ende des Programmcode begonnen, so daß alle Schleifen- operationen durchgemacht werden müssen, bis der Opcode am Ende der Ent- schlüsselungsroutine sichtbar wird. Beispiel: CS:0100 BB7109 MOV BX,0971 CS:0103 BE1001 MOV DI,0110 CS:0106 91 XCHG AX,CX CS:0107 91 XCHG AX,CX CS:0108 2E803597 XOR Byte Ptr CS:[DI],97 CS:010C 47 INC DI CS:010D 4B DEC BX CS:010E 75F6 JNZ 0106 CS:0110 07 POP ES CS:0111 07 POP ES 2.2 Selbstmodifizierender Code 2.2.1 Einfache Modifizierung In dieser Methode wird das selbe Prizip wie bei der Verschlüsselung ange- wandt: Der Opcode wird überschrieben, bevor er benutzt wird. Im folgenden Beispiel verändern wir den Code, der hinter dem Call liegt. Deshalb, falls der ganze Call übersprungen wird ('p' bei debug oder 'F8' beim Turbo Debugger) wird man hier die Kontrolle verlieren, denn der Debugger setzt sein CCh an Adresse 103h und die Subroutine überschreibt diesen Opcopde. Beispiel: CS:0100 E80400 CALL 0107 CS:0103 CD20 INT 20 CS:0105 CD21 INT 21 CS:0107 C7060301B44C MOV Word Ptr [0103],4CB4 CS:010D C3 RET und das ist der Inhalt der Subroutine: CS:0103 B44C MOV AH,4C 2.2.2 Die 'Running Line' (Selbstentschlüsselung) Hier kommt noch ein recht ausgefallenes Beispiel für einen selbstmodifi- zierenden Code, der sich selbst traced. Er wurde von Serge Pachkovsky vorgestellt und ist unter dem Namen 'The Running Line' bekannt. Er ist nicht einfach zu implementieren, aber ziemlich widerstandsfähig gegen Versuche, die Interrupttabelle zu schützen. Die Instruktionen werden immer nur einzeln entschlüsselt, dadurch wird nie eine längere Codesequenz der Analyse preis- gegeben. Das folgende Codefragment stellt das Verfahren vereinfacht dar. Beispiel: XOR AX, AX MOV ES, AX MOV WORD PTR ES:[4*1+0],OFFSET TRACER MOV WORD PTR ES:[4*1+2],CS MOV BP, SP PUSHF XOR BYTE PTR [BP-1], 1 POPF MOV AX, 4C00H ;Wird nicht getraced! DB 3 DUP ( 98H ) DB C5H, 21H TRACER: PUSH BP MOV BP, SP MOV BP, WORD PTR [BP+2] XOR BYTE PTR CS:[BP-1], 8 XOR BYTE PTR CS:[BP+0], 8 POP BP IRET ---------------------------- Soweit also der Text von Inbar Raz. Anmerkungen sind ausdrücklich erwünscht. Weitere Artikel zu diesem Thema sollen folgen, sobald sie verfügbar sind.


(Übersicht)

Verschlüsseln von Daten

Bastiaan Zapf 2:241/1149.77:

Das Ziel einer Verschlüsselung ist es, Daten so zu manipulieren, dass sie nur noch für Personen, die über einen bestimmten Wissensstand (nämlich den den Schlüssel und das Verschlüsselungsverfahren) verfügen, erkennbar sind.

Die Datenverschlüsselung ist besonders für den Shareware-Programmierer (ver- stecken des Registrierungshinweises o.ä.) und den Spiele-Programmierer (ver- schlüsselung der Spielstände gegen Patching) wichtig.

Generell gibt es mehrere Methoden, Daten zu verschluesseln:

- Vertauschungsalgorithmen - Ein-Schlüssel-Verschlüsselung - Zwei-Schlüssel-Verschlüsselung - (Einwegverschlüsselung)

Vertauschungsalgorithmen:

Vertauschungsalgorithmen sind zwar effektiv, da sie in den seltensten Fäl- len erwartet werden, sind jedoch zugleich relativ leicht zu knacken, wenn man den Algorithmus kennt.

Das Grundprinzip ist die Vertauschung einzelner Datenelemente in einer be- stimmten Reihenfolge. Die Entschlüsselung wird dann durch die Umkehrung dieser Vertauschung erreicht.

Ich führe hier kein Code-Beispiel an, da effiziente Algorithmen schnell übermässige Dimensionen erreichen. Übrigens: Schon die Griechen verwendet- en solche Algorithmen, wobei sie z.B. ein Lederband auf einen Stab wickel- ten und dieses parallel zum Stab beschrifteten. Wenn das Band abgewickelt und als fortlaufender Text betrachtet wird, haben alle Buchstaben ihre Plätze geändert.

Ein-Schlüssel-Verschlüsselung:

Ein-Schlüssel-Verschlüsselung ist die gebräuchlichste und zugleich, bei vertretbarem Aufwand, sicherste Methode.

Das Grundprinzip ist, daß jedes Datenelement mittels eines Schlüssels zu einem Datenelement umgewandelt wird, welches man mit demselben Schlüssel wieder zurückverwandeln kann. Ein einfaches Beispiel ist es, zu jedem Byte ein bestimmtes Byte hinzuzuaddieren. Die Entschlüsselung erfordert dann eine Subtraktion. Beispiel:

-+-+-+-+-+-+-+] Bitte hier knabbern [+-+-+-+-+-+-+- ; ds:si sei auf die zu verschlüsselnden Daten gerichtet ; es:di sei auf freien Speicher in gleicher Größe gerichtet ; cx Anzahl der zu verschlüsselnden Bytes ; dl Schlüssel

ver_schl: lodsb add al,dl stosb loop ver_schl

; Der Entschlüsselungsalgorithmus läuft genauso, bloß wird statt 'add' ; 'sub' verwendet -+-+-+-+-+-+-+] Bitte hier knabbern [+-+-+-+-+-+-+-

So einen Algorithmus verwendete auch Julius Caesar, der anstatt jedes Buchstaben dessen Nachfolger im Alphabet benutzte. Wenn man jetzt den Code etwas ändert, kann man Schlüssel von Word-Länge oder sogar DWord-Länge benutzen. Wenn man ganz raffiniert ist, ist es sogar möglich, Schluessel beliebiger Länge einzusetzen.

Dieses Verschlüsselungssystem hat jedoch einen gravierenden Nachteil: Wenn man lange Ketten gleicher Zeichen hat, hat man im verschlüsselten Text auch lange Ketten gleicher Zeichen.

Wenn man bespielsweise mit der Julius-Caesar-Methode den Text "AAAAA" ver- schlüsselt, hat man als Produkt "BBBBB". Das mag zwar bei kurzen, normalen Daten nicht weiter auffallen, aber wenn man z.B. Texte verschlüsselt (in denen ja z.B. das 'E' überdurchschnittlich häufig vorkommt), lässt sich durch statistische Untersuchungen der Schlüssel ermitteln.

Die einzige Möglichkeit, diesen Weg zu erschweren, ist der Einsatz eines längeren Schlüssels, oder, was am Ende auf dasselbe hinauskommt, die mehr- fache Anwendung des Algorithmus' mit verschiedenen Schlüsseln; am besten mit Schlüsseln, deren Längen nicht durcheinander teilbar sind.

Zwei-Schlüssel-Verschlüsselung:

Zwei-Schlüssel-Verschlüsselungen sind nur mit sehr komplizierten Algorith- men durchzuführen, die jedoch alle auf der 'Einseitigkeit' mancher mathe- mathischer Operationen beruhen.

Beispielsweise ist es relativ einfach 26^7 zu rechnen, es ist jedoch ex- trem schwer, die einzigen beiden ganzen Zahlen, die miteinander potenziert 8031010176 ergeben, zu finden.

Bei diesen Verschlüsselungsalgorithmen werden aus einem Satz pseudozufäl- liger Daten zwei Schlüssel ermittelt. Einer davon kann frei verteilt wer- den, und obwohl alle diesen Schlüssel haben können, ist es unmöglich, mit ihm verschlüsselte Daten zu entschlüsseln. Nur mit dem zweiten Schlüssel ist das möglich.

Einwegverschlüsselung:

Einwegverschlüsselungen sind keine 'echten' Verschlüsselungen, sie sind im Prinzip nur zur Sicherung der Integrität von Daten geeignet. Dabei werden einzelne Teile eines Datenelements miteinander oder mit einem Schlüssel kombiniert, wobei das Ergebnis ein sehr kleines Datenelement ist. Dieses Datenelement wird gespeichert.

Falls die Datenintegrität in Frage gestellt wird, wird der Algorithmus er- neut durchlaufen und das Ergebnis mit dem gespeicherten Wert verglichen. Wenn die Daten in der Zwischenzeit verfälscht wurden, sollten sich die beiden Werte voneinander unterscheiden.

Ein gutes Beispiel für solch ein Verfahren ist das Paritätsverfahren, das als Ergebnis pro Byte ein Bit liefert, das so gesetzt ist, daß die Anzahl der gesetzten Bits im Byte plus das Paritätsbit - je nachdem, ob man gera- de oder ungerade Parität verwendet - gerade oder ungerade ist.

Ein anderes häufig eingesetztes Verfahren ist das CRC-Verfahren, auf das hier jedoch nicht eingegangen wird, da die Umsetzung ziemlich umfangreich ausfällt.


(Übersicht)

Key Generation

Topic noch zu vergeben.








converted with af2html V 0.01d Win95/NT
a Green meadoW production MXMVIII
copyright © by Guido Wischrop