Rheinwerk Computing < openbook > Rheinwerk Computing - Professionelle Bücher. Auch für Einsteiger.
Professionelle Bücher. Auch für Einsteiger.

Inhaltsverzeichnis
Vorwort
1 Neues in Java 7
2 Threads und nebenläufige Programmierung
3 Datenstrukturen und Algorithmen
4 Raum und Zeit
5 Dateien, Verzeichnisse und Dateizugriffe
6 Datenströme
7 Die eXtensible Markup Language (XML)
8 Dateiformate
9 Grafische Oberflächen mit Swing
10 Grafikprogrammierung
11 Netzwerkprogrammierung
12 Verteilte Programmierung mit RMI
13 RESTful und SOAP Web-Services
14 JavaServer Pages und Servlets
15 Applets
16 Datenbankmanagement mit JDBC
17 Technologien für die Infrastruktur
18 Reflection und Annotationen
19 Dynamische Übersetzung und Skriptsprachen
20 Logging und Monitoring
21 Java Native Interface (JNI)
22 Sicherheitskonzepte
23 Dienstprogramme für die Java-Umgebung
Stichwort

Buch bestellen
Ihre Meinung?

Spacer
Java 7 - Mehr als eine Insel von Christian Ullenboom
Das Handbuch zu den Java SE-Bibliotheken
Buch: Java 7 - Mehr als eine Insel

Java 7 - Mehr als eine Insel
Rheinwerk Computing
1433 S., 2012, geb.
49,90 Euro, ISBN 978-3-8362-1507-7
Pfeil 23 Dienstprogramme für die Java-Umgebung
Pfeil 23.1 Programme des JDK
Pfeil 23.2 Monitoringprogramme vom JDK
Pfeil 23.2.1 jps
Pfeil 23.2.2 jstat
Pfeil 23.2.3 jmap
Pfeil 23.2.4 jstack
Pfeil 23.2.5 VisualVM
Pfeil 23.3 Programmieren mit der Tools-API
Pfeil 23.3.1 Eigene Doclets
Pfeil 23.4 Ant
Pfeil 23.4.1 Bezug und Installation von Ant
Pfeil 23.4.2 Das Build-Skript build.xml
Pfeil 23.4.3 Build den Build
Pfeil 23.4.4 Properties
Pfeil 23.4.5 Externe und vordefinierte Properties
Pfeil 23.4.6 Weitere Ant-Tasks
Pfeil 23.5 Disassembler, Decompiler und Obfuscator
Pfeil 23.5.1 Der Diassembler javap
Pfeil 23.5.2 Decompiler
Pfeil 23.5.3 Obfuscatoren
Pfeil 23.6 Weitere Dienstprogramme
Pfeil 23.6.1 Sourcecode Beautifier
Pfeil 23.6.2 Java-Programme als Systemdienst ausführen
Pfeil 23.7 Zum Weiterlesen

Rheinwerk Computing - Zum Seitenanfang

23.5 Disassembler, Decompiler und ObfuscatorZur nächsten Überschrift

Ein Disassembler ist ein Werkzeug, das den Bytecode und die Struktur einer Java-Klassendatei anzeigt. Ein Decompiler geht einen Schritt weiter und versucht aus Klassendateien wieder Quellcodedateien zu gewinnen, die, wenn sie später compiliert werden, wieder den gleichen Bytecode ergeben würden. Genau diese »leichte« Lesbarkeit von Bytecode oder eine Decompliation möchte ein anderes Werkzeug, der Obfuscator, erschweren. Das Ziel von Obfuscation ist das möglichst effektive Verschleiern aller Bytecodeinformationen, sodass Menschen den Spaß am Bytecode verlieren und Decompiler bei ihrer Rücktransformation sogar aus dem Tritt kommen.

Das Eclipse-Plugin ByteCodeDeluxe unter http://www.idedeluxe.com/bytecode/ visualisiert die Struktur einer Bytecode-Datei.


Rheinwerk Computing - Zum Seitenanfang

23.5.1 Der Diassembler javapZur nächsten ÜberschriftZur vorigen Überschrift

Das JDK liefert im bin-Verzeichnis der Installation mit javap ein Werkzeug aus, das zwar nicht die Implementierung von Methoden hervorzaubert, aber immerhin die statische Struktur einer Klasse mit den Vererbungsbeziehungen, Variablen, Methoden, Parametern anzeigt.

In der einfachsten Variante wird javap mit dem Klassennamen aufgerufen. Wir nehmen im Folgenden an, dass javap im Suchpfad steht und wir uns auf der Kommandozeile direkt im bin-Verzeichnis vom JDK befinden. Wir setzen zuerst den Klassenpfad und geben anschließend als Kommandozeilenargument für javap die Klasse Quadrat aus dem ersten Kapitel an, die disassembliert werden soll.

$ javap -classpath Z:\Documents\Insel\programme\1_01_Intro Quadrat
Compiled from "Quadrat.java"
public class Quadrat extends java.lang.Object{
public Quadrat();
static int quadrat(int);
static void ausgabe(int);
public static void main(java.lang.String[]);
}

Abzulesen sind auch Dinge, die der Compiler automatisch generiert und die im Bytecode stehen, die wir aber im Allgemeinen nicht schreiben würden, etwa der voll qualifizierte Klassenname java.lang.Object oder java.lang.String, die Vererbungsbeziehung zu Object oder der automatisch angelegte Standardkonstruktor.

Das Tool javap erlaubt noch mehr Parameter, die die Option -help anzeigt.

$ javap -help
Usage: javap <options> <classes>...

where options include:
-c Disassemble the code
-classpath <pathlist> Specify where to find user class files
-extdirs <dirs> Override location of installed extensions
-help Print this usage message
-J<flag> Pass <flag> directly to the runtime system
-l Print line number and local variable tables
-public Show only public classes and members
-protected Show protected/public classes and members
-package Show package/protected/public classes
and members (default)
-private Show all classes and members
-s Print internal type signatures
-bootclasspath <pathlist> Override location of class files loaded
by the bootstrap class loader
-verbose Print stack size, number of locals and args for methods
If verifying, print reasons for failure
Hinweis

Auch der Java-Compiler kann seit Java 7 mit dem neuen Schalter –Xprint ähnliche Informationen ausgeben:

$ javac.exe -Xprint Quadrat.java
public class Quadrat {
public Quadrat();
static int quadrat(int n);
static void ausgabe(int n);
public static void main(java.lang.String[] args);
}

Java-Bytecode am Beispiel

Interessanter ist der Schalter -c, der den Bytecode der Methoden/Konstruktoren/Initialisierer anzeigt. Am Beispiel:

$ javap -c -classpath Z:\Documents\Insel\programme\1_01_Intro Quadrat
Compiled from "Quadrat.java"
public class Quadrat extends java.lang.Object{

Steigen wir nicht chronologisch ein, sondern bei der statischen Methode quadrat(). Sie bekommt als Argument ein int und liefert es multipliziert mit sich selbst zurück:

static int quadrat(int);
Code:
0: iload_0
1: iload_0
2: imul
3: ireturn

Die Ausgabe macht die Stack-Natur des JVM sichtbar. Der Übergabeparameter n von quadrat(int n) steht auf Position 0 im Stack. Um das Quadrat zu bilden, wird der aktuelle Parameter zweimal mit iload_0 auf den Stapel gelegt und dann mit imul multipliziert. imul löscht die beiden Werte vom Stapel und ersetzt sie durch das Ergebnis der Multiplikation. ireturn liefert den obersten Stackwert als int zurück. Das Präfix »i« bei imul und ireturn zeigt, dass die Operationen auf Ganzzahlen durchgeführt werden. Andere Präfixe sind »b« für byte, »c« für char, »d« für double oder »a« für Objektreferenzen.

Kommen wir zur statischen main-Methode:

public static void main(java.lang.String[]);
Code:
0: iconst_4
1: invokestatic #59; //Method ausgabe:(I)V
4: return
}

Im Rumpf der main()-Methode steht der Aufruf ausgabe(4), was im Bytecode dazu führt, dass mit iconst_4 der Wert 4 auf den Stack gelegt wird und dann invokestatic die Methode ausgabe() aufruft.

Der Standardkonstruktor von Quadrat ruft lediglich den Standardkonstruktor der Oberklasse Object auf.

public Quadrat();
Code:
0: aload_0
1: invokespecial #8; //Method java/lang/Object."<init>":()V
4: return

Bei Konstruktoren und Objektmethoden ist automatisch an Position 0 im Stack die this-Referenz gesetzt, da die Aufrufer automatisch an Position 0 diese Referenz übergeben. (Bei statischen Methoden ist das nicht so.) Bei unserem Konstruktor Quadrat() setzt also aload_0 diese this-Referenz auf den Stack, damit der Konstruktor der Oberklasse, also Object aufgerufen werden kann. Konstrukturaufrufe werden im Bytecode mit invokespecial durchgeführt. Im Kern ist ein Konstruktor eine Methode mit dem speziellen Namen <init> und der Rückgabe void. Die Angabe #8 ist ein Verweis auf eine interne Tabelle, doch der Java-Kommentar macht den Eintrag für uns lesbar.

Am komplexesten ist die statische Methode ausgabe(). Im Original sieht sie so aus:

String s;
int i;
for ( i = 1; i <= n; i = i + 1 )
{
s = "Quadrat(" + i + ") = " + quadrat(i);
System.out.println( s );
}

Parameter und lokale Variablen werden auf dem Stack gespeichert und haben intern nur Positionen.

Tabelle 23.4: Variablen im Beispiel und Stack-Positionen

Variable Positionen der Parameter/Variablen

n

0

s

1

i

2

Der erste Ausschnitt ohne Schleifenrumpf sieht so aus:

static void ausgabe(int);
Code:
0: iconst_1
1: istore_2
2: goto 44
5: ...
...
41: iinc 2, 1
44: iload_2
45: iload_0
46: if_icmple 5
49: return

Unsere for-Schleife beginnt mit 1, und so setzt iconst_1 den Wert 1 auf den Stack, und istore_2 speichert den Wert für die Variable i auf dem Stack. Da for-Schleifen kopfgesteuerte Schleifen sind, also erst testen, bevor sie den Rumpf durchlaufen, springt goto – im Bytecode gibt es diesen Sprungbefehl – über den Schleifenrumpf zum Test. iload_2 lädt die Variable i und vergleicht sie mit n, der Parametervariablen auf Position 0 auf dem Stack. Ist i < n, dann springt if_icmple (für engl. integer compare less equal) zur Position 5, was den Schleifenrumpf ausführt. In der Zeile 41 inkrementiert iinc 2, 1 die Variable i (2 steht hier für den Index der Variablen, denn die Variable i steht auf Platz 2 des Stacks) und 1 ist das Inkrement.

Der Rumpf der Schleife besteht aus zwei Teilen: einmal aus dem Zusammenhängen der Strings und einmal aus der Ausgabe auf dem Bildschirm. Zur Konkatenation müssen wir uns zurückerinnern, dass das + bei Strings zu einer Folge von append()-Aufrufen auf ein StringBuilder (vor Java 5 StringBuffer) wird.

 5:   new     #20; //class java/lang/StringBuilder
8: dup
9: ldc #22; //String Quadrat(
11: invokespecial #24; //Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
14: iload_2
15: invokevirtual #27; //Method java/lang/StringBuilder.append:(I)Ljava/lang/Zeilenumbruch
StringBuilder;
18: ldc #31; //String ) =
20: invokevirtual #33; //Method java/lang/StringBuilder.append:(Ljava/lang/Zeilenumbruch
String;)Ljava/lang/StringBuilder;
23: iload_2
24: invokestatic #36; //Method quadrat:(I)I
27: invokevirtual #27; //Method java/lang/StringBuilder.append:(I)Ljava/lang/Zeilenumbruch
StringBuilder;
30: invokevirtual #38; //Method java/lang/StringBuilder.toString:()Ljava/lang/String;
33: astore_1

Nach dem Ablauf steht die Referenz auf das String-Objekt auf dem Stack, und astore_1 überträgt sie auf den Stack an Position 1, der für die String-Variable s reserviert war.

Es folgt die Ausgabe auf dem Bildschirm:

34:  getstatic       #42; //Field java/lang/System.out:Ljava/io/PrintStream;
37: aload_1
38: invokevirtual #48; //Method java/io/PrintStream.println:(Ljava/lang/String;)V

Hier greift getstatic auf die statische Variable System.out zu und setzt die Referenz auf den aktuellen PrintStream auf den Stack. Dann setzt aload_1 die Referenz vom String s auf den Stack, und invokevirtual ruft println() aus.

Dieses Beispiel gibt einen kleinen Einblick in den Bytecode von Java. Weitere Informationen gibt »The Java Virtual Machine Specification« und insbesondere das Kapitel »Compiling for the Java Virtual Machine« unter http://java.sun.com/docs/books/jvms/second_edition/html/Compiling.doc.html. Normale Programmierer müssen Bytecode nicht lesen und schreiben können, aber das Wissen ist nützlich und auch nötig, wenn Bytecode selbst erstellt werden soll.

Interna

Wichtig zu wissen ist, dass das Programm im Bytecode keine strikte Ablaufanweisung für die JVM ist. Java-Bytecode ist nur das »Transportformat«, in dem Programmcode zur JVM gelangt. Die JVM wiederum liest den Bytecode ein und transformiert ihn in hochoptimierten Maschinencode. Eine Interpretation gibt es zwar noch am Anfang der Ausführung, aber nach ein paar Durchläufen beginnt HotSpot mit der Übersetzung in Maschinencode, und da fallen diverse Operationen aus dem Bytecode heraus.


Rheinwerk Computing - Zum Seitenanfang

23.5.2 DecompilerZur nächsten ÜberschriftZur vorigen Überschrift

Der Java-Compiler erzeugt aus der Quellcodedatei eine Klassendatei, und der Decompiler dreht die Arbeitsweise um. Decompiler sind Werkzeuge zum Reverse Engineering, bei dem es darum geht, aus einer fertigen Software, die nur etwa in Form von Jar-Dateien vorliegt, die Java-Quellen und Ressourcen zurückzugewinnen. Decompiler gibt es für die verschiedenen Programmiersprachen, und Java gehört zu den Sprachen, bei denen die Zurückübersetzung einfacher ist als bei optimierten Maschinenprogrammen, die zum Beispiel ein C++-Compiler erzeugt. Der Grund ist, dass im Bytecode viele wertvolle Informationen enthalten sind, die im herkömmlichen Maschinencode nicht auftauchen. Darunter sind etwa Typinformationen oder Hinweise, ob ein Methodenaufruf virtuell ist oder nicht. Sie sind für die Java-Laufzeitumgebung wichtig und eine große Hilfe, wenn es darum geht, mit einem Decompiler verlorenen Quellcode wiederzubeleben oder an fehlende Informationen aus Paketen von Fremdherstellern zu gelangen.

Ein Decompiler liest die Klassendatei als Bytefeld ein und beginnt mit der Analyse. Da der Bytecode gut dokumentiert ist, ist das Extrahieren von Variablen- oder Methodennamen einfach. Schwierig sind die Anweisungen. Aus dem Java-Bytecode für eine Methode baut ein Decompiler einen Kontrollfluss-Graphen auf und versucht, Anweisungen und Ausdrücke zu erkennen, die bei der Übersetzung bestimmter Sprachkonstrukte entstanden sein müssten. Das ist eine nicht-triviale Aufgabe und immer noch Gegenstand einiger Diplomarbeiten. Und da Variablennamen durch einen Obfuscator eventuell ungültig gemacht worden sind, muss ein guter Decompiler diese illegalen Bezeichnernamen korrigieren und weitere Tricksereien des Obfuscators rückgängig machen. Diese Umbenennung ändert den Algorithmus nicht, und ein Decompiler hat es bei dieser Art von Verschleierung einfach.

Ist das legal?

Wenn wir einen Decompiler auf fremden Programmcode loslassen, kann das ein rechtliches Problem darstellen. Das Reverse Engineering von vollständigen, unter Urheberschutz stehenden Anwendungen muss nicht unbedingt ein juristisches Nachspiel haben. Vielmehr beginnt die Straftat, wenn dieser zurückgewonnene Quelltext verändert und als Eigenleistung verkauft wird.

Da mittlerweile auch andere Compiler auf dem Markt sind, die Java-Bytecode erzeugen – etwa aus EIFFEL-Programmen oder aus diversen LISP-Dialekten –, ist über den Umweg Compiler/Klassendatei/Decompiler ein Crosscompiling denkbar. Hier sind jedoch einige Einschränkungen bezüglich der auf dem Markt befindlichen Decompiler erkennbar. Denn fremde Compiler, die Java-Bytecode erstellen, haben andere Techniken, die der Decompiler dann nicht immer passend übersetzen kann.

Java Decompiler Project (JD) und Alternativen

Das Angebot an leistungsstarken Decompilatoren ist sehr übersichtlich. Das beste Tool (aber auch nicht ganz fehlerfrei) ist zurzeit JD (http://java.decompiler.free.fr/). Das frei verfügbare – aber nicht quelloffene – Programm ist als Bibliothek JD-Core, als alleinstehende grafische Anwendung JD-GUI und Eclipse-Plugin[121](Für NetBeans und IntelliJ IDEA gibt es Initiativen von anderer Seite.) JD-Eclipse verfügbar. JD selbst ist in C++ geschrieben und benötigt daher keine JVM. JD verarbeitet den Bytecode verschiedener Compiler, wobei das JDK 1.1 bis (bisher) JDK 6 selbstverständlich mit in der Liste ist, genauso wie der Eclipse-Compiler. (Die Unterscheidung ist nicht ganz uninteressant, da die Compiler sich in machen Details in der Bytecode-Abbildung doch unterscheiden.) JD-GUI ist für die Plattformen Windows, Linux und Mac unter http://java.decompiler.free.fr/?q=jdgui#downloads verfügbar und bietet neben dem Decompilieren einzelner Java-Klassen und ganzer Java-Archive eine angenehme Quellcodedarstellung mit farblicher Unterlegung und Drag & Drop.

Sehr lange war der Decompiler Jad die Referenz. Doch nur von 1997 bis 2001 hat Pavel Kouznetsov das Kommandozeilenprogramm in C++ entwickelt, und dann hat er 2009 seine Webseite vom Netz genommen. Eine Privatperson hat jedoch die Webseite gespiegelt, und unter http://www.varaneckas.com/jad lebt das Projekt (auf unbestimmte Zeit) weiter. Wer Projekte bis Java 1.4 decompilieren möchte, ist mit dem Tool sehr gut bedient. Für Java 5-Projekte hilft JadRetro (http://jadretro.sourceforge.net/) noch ein wenig nach, indem es Java 5-Bytecode auf Java 1.4 anpasst und kleine Änderungen im Bytecode durchführt. FrontEnd Plus ist eine grafische Oberfläche für Jad, doch auch sie ist aus dem Internet verschwunden, seitdem es Jad nicht mehr offiziell gibt.

Abbildung

Abbildung 23.7: Decompilation der Quadrat-Klasse mit JD-GUI


Rheinwerk Computing - Zum Seitenanfang

23.5.3 ObfuscatorenZur nächsten ÜberschriftZur vorigen Überschrift

Die Existenz eines Disassemblers wie javap und eines Decompilers wie JD verunsichert Hersteller, da diese in der Regel nicht möchten, dass ihr Quellcode untersucht wird und vielleicht neu zusammengesetzt den Weg zurück in den Markt findet. Ein Obfuscator ist ein Werkzeug, das diverse Transformationen am Bytecode vornimmt, um eine einfache Zurückverwandlung zu verhindern. Es bieten sich eine Reihe von Versteck-Aktionen an:

  • das Löschen von Debug-Informationen
  • das Umbenennen aller Bezeichner für Pakete, Klassen, Methoden und Variablen. Klassen heißen dann zum Beispiel C1, C2, ... und Methoden m1, m2, ...
  • das Benennen von Bezeichnern mit Java-Schlüsselwörtern oder nicht in Java erlaubten Zeichenfolgen
  • das Verlegen von Anweisungen oder Anweisungsfolgen in Methoden (also die Umkehroperation zum Methoden-Inline)
  • das Berechnen von Konstanten, sodass etwa 1 in println(1); dynamisch durch int abcde = 2; println(abcde-1); berechnet wird
  • das Einführen leerer Anweisungen, die zur Laufzeit wegoptimiert werden, aber den Bytecode füllen, etwa if ( 1 != 1) { int a = 123334; }
  • das dynamische Entschlüsseln von Strings, sodass etwa aus println("google") ein println($("tokepa")) wird; die Methode $() entschlüsselt »topeka« zurück zu »google«.
  • das Umsortieren oder das Einfügen unsinniger Bytecode-Folgen, sodass sie nicht mehr dem Muster entsprechen, die ein Java-Compiler erzeugt und auch nicht so einfach vom Decompiler wiedererkannt werden.[122](Ideen beschreibt das Paper »Protection Methods of Java Bytecode« unter http://tutego.de/go/bytecodeprotection.)
Effektivität

Ein Obfuscator verhindert das einfache Zurückverwandeln in Java-Quellcode, kann aber niemals verhindern, dass javap den Bytecode offenbart. Ein Obfuscator kann somit nur Dinge möglichst unleserlich machen, aber wirklich geschützt ist das Programm nicht. Dennoch lohnt sich der Einsatz eines Obfuscators, denn er schreckt ab und verdirbt die Lust am Schnüffeln, denn Klassennamen von C1 bis C3484 mit Methoden von m1 bis m12743 verraten erst einmal überhaupt nichts. Noch besserer Schutz ist nur mit einem eigenen Klassenlader zu erreichen, der verschlüsselten Bytecode einliest. Hier muss dann die JVM gepatcht werden, um an diese Klassendateien zu kommen. Wirklich unmöglich ist das auch nicht.

Das Obfuscator-Programm ProGuard

ProGuard (http://proguard.sf.net/) ist ein Open-Source-Projekt unter der GPL-Lizenz,[123](Obwohl ProGuard selbst unter der GPL steht, darf die Software auch auf Software angewendet werden, die nicht unter der GPL steht.) das Java-Klassen und Jar-Archive verschleiert. Neben der Verschleierung hat es den Nebeneffekt, dass die Klassendateien kleiner werden, was insbesondere für Applets und Midlets (mobile Anwendungen) interessant ist. ProGuard 3 bietet Unterstützung für das Bytecode-Format von Java 5 und Java ME – ProGuard 4 bietet Obfuscation für Java 6. Die Software lässt sich über eine Swing-Oberfläche oder über die Kommandozeile bedienen oder aber auch über ein Ant-Skript steuern. Die Webseite nennt ein Beispiel, das alle Typen bis auf Applets eines Java-Archivs berücksichtigt. Auf der Kommandozeile ist Folgendes anzugeben:

$ java -jar proguard.jar –injars in.jar -outjars out.jar Zeilenumbruch
–libraryjars $JAVA_HOME/lib/rt.jarkeep public class * extends java.applet.Applet

Einige Dinge dürfen nicht umbenannt werden, sodass sie von ProGuard ausgenommen werden müssen. Das betrifft etwa bestimmte Klassen, die dynamisch geladen werden, wie Plugins, wo es auf den genauen Paket- und Klassennamen ankommt.

Unter http://www.certiv.net/projects/proguarddt.html gibt es ein Plugin, das die ProGuard-Konfigurationen in Eclipse editieren und ProGuard über ein Menü komfortabel aufrufen kann.



Ihr Kommentar

Wie hat Ihnen das <openbook> gefallen? Wir freuen uns immer über Ihre freundlichen und kritischen Rückmeldungen.

>> Zum Feedback-Formular
<< zurück
  Zum Katalog
Neuauflage: Java SE 8 Standard-Bibliothek
Neuauflage: Java SE 8 Standard-Bibliothek
Jetzt bestellen


 Ihre Meinung?
Wie hat Ihnen das <openbook> gefallen?
Ihre Meinung

 Buchempfehlungen
Zum Katalog: Professionell entwickeln mit Java EE 7






 Professionell
 entwickeln mit
 Java EE 7


Zum Katalog: Java ist auch eine Insel






 Java ist auch
 eine Insel


Zum Katalog: Einstieg in Eclipse






 Einstieg in Eclipse


Zum Katalog: Einstieg in Java






 Einstieg in Java


 Shopping
Versandkostenfrei bestellen in Deutschland und Österreich
InfoInfo




Copyright © Rheinwerk Verlag GmbH 2012
Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken. Ansonsten unterliegt das denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt. Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.


Nutzungsbestimmungen | Datenschutz | Impressum

Rheinwerk Verlag GmbH, Rheinwerkallee 4, 53227 Bonn, Tel.: 0228.42150.0, Fax 0228.42150.77, service@rheinwerk-verlag.de