Blog

Binary Patching von Java für Rich-Client Penetrationstests

Einleitung

Wie schon in einem vorangegangenen Blog-Artikel erwähnt, stellen wir bei der Durchführung von Rich-Client Penetrationstests immer wieder fest, dass sicherheitsrelevante Funktionen im Client verankert sind. Ein sehr häufiges Beispiel ist, dass die Berechtigungsprüfung dahingehend implementiert wird, dass gewisse Menüeinträge bzw. Buttons nur unter bestimmten Voraussetzungen angezeigt werden. Was aus Sicht der Benutzerfreundlichkeit durchaus Sinn macht, ist in Bezug auf die Sicherheit leider nicht empfehlenswert.

Da wir neben .NET-Applikationen auch häufiger mit Java-Applikationen zu tun haben, möchte ich im heutigen Artikel verdeutlichen, dass es auch in dieser Sprache äußerst einfach ist, Schutzmaßnahmen zu umgehen.

Vieles aus dem .NET-Artikel lässt sich auch auf dieses Thema übertragen, weshalb in dem heutigen Artikel der Fokus nicht auf der Frage liegt, warum Byte-Code-Analysen wichtig sind. Hierzu gibt Ihnen der Artikel zur .NET-Applikation nähere Informationen.

Zugriff auf den Byte-Code

Wie .NET auch bietet Java einige Möglichkeiten, den Source Code zu dekompilieren. Schauen wir uns das Ganze an einem Beispiel an. Vergleicht man den originalen Code mit der dekompilierten Version, ist deutlich zu erkennen, dass diese nahezu identisch sind:

 public class SuperSafe {
   public static boolean checkUsername() {
     String currentDomain = System.getProperty("user.name");
  
     if(currentDomain.toLowerCase() == "securai") {
       return true;
     } else {
       return false;
     }
   }
   public static void main(String[] args) {
     if (SuperSafe.checkUsername()) {
       System.out.println("You are trustworthy! Here is our secret: ...");
     } else {
       System.out.println("You shall not pass!");
     }
   } 
 }

Wird dieser Code kompiliert und mit “Luyten” angezeigt, erhält man folgendes Ergebnis:

Luyten Anzeige von SuperSafe.class

Es gibt Fälle, bei denen der Source nicht korrekt wiedergegeben wird, da er zu komplex ist. Doch auch hier bekommt man dadurch bereits einen ersten Überblick, was man mit der Applikation erreichen kann.

Analyse des Programms

Um die “Vertrauenswürdigkeit” eines Benutzers zu gewährleisten, greift der Code auf den Benutzernamen des Anwenders zurück und vergleicht diesen mit einem festgelegten Wert “securai”.

Nun gibt es mehrere Wege zu unserem Ziel, dem Geheimnis.

Am einfachsten wäre wohl, das Ganze auf einem Computer mit einem Benutzer mit dem Namen “securai” auszuführen. Dies funktioniert und sollte der Vollständigkeit halber erwähnt werden, da man sich nie auf Umgebungsvarianten verlassen sollte. Dennoch wollen wir hier mehr die technischen Varianten vorstellen.

Die ganze Ausführung hängt an der Überprüfung der checkUserName-Methode in der Main-Methode. Hier ist es am einfachsten, das Konstrukt zu negieren, sodass man immer vertrauenswürdig ist, wenn man nicht den Benutzernamen “securai” hat.

Disassemblierung

Um die eigentlichen Schritte eines Tools zu verstehen, sollte man erst einmal alle Schritte gesehen und von Hand vollzogen haben. Dies ist zwar ein umständlicher Weg, erleichtert aber später einiges.

Um ein näheres Verständnis von den Methoden zu erhalten, kann man sich mithilfe von “javap” den Byte-Code anzeigen lassen:

 public class SuperSafe {
   public SuperSafe();
     Code:
        0: aload_0
        1: invokespecial #10 // Method java/lang/Object."<init>":()V
        4: return
 
   public static boolean checkUsername();
     Code:
        0: ldc #1 // String user.name
        2: invokestatic #14 // Method java/lang/System.getProperty:(Ljava/lang/S
        5: astore_0
        6: aload_0
        7: invokevirtual #15 // Method java/lang/String.toLowerCase:()Ljava/lang/
       10: ldc #2 // String securai
       12: if_acmpne 17
       15: iconst_1
       16: ireturn
       17: iconst_0
       18: ireturn
 
   public static void main(java.lang.String[]);
     Code:
        0: invokestatic #20 // Method checkUsername:()Z
        3: ifeq 9
        6: getstatic #21 // Field java/lang/System.out:Ljava/io/PrintStream;
        9: ldc #3 // String You are trustworthy! Here is our secret: .
       11: invokevirtual #22 // Method java/io/PrintStream.println:(Ljava/lang/St
       14: goto 25
       17: getstatic #21 // Field java/lang/System.out:Ljava/io/PrintStream;
       20: ldc #4 // String You shall not pass!
       22: invokevirtual #22 // Method java/io/PrintStream.println:(Ljava/lang/St
       25: return
   }

In der Ansicht kann man mehrere Dinge erkennen; neben der initialen Klasse auch die Public-Methoden und die Funktionsblöcke. In den Funktionsblöcken ist der Funktionsablauf definiert, welcher in einzelnen Spalten Offset, Mnemonic, Typ und ASCII-Repräsentation darstellt.

Die Masochisten unter uns können sich jetzt die korrekten Offsets berechnen, die richtigen Hex-Werte heraussuchen und mithilfe eines Hex-Editors “iconst_1” (0x04), so wie “iconst_0” (0x03) in der Methode checkUserName austauschen, um die Überprüfung zu negieren.

Disassemblierung eines Programms

Wir können aber auch einfach einen Schritt weiter gehen und ein Programm namens “Krakatau” nutzen. Dieses erlaubt uns eine Disassemblierung in eine Zwischensprache und eine spätere Assemblierung, um die Veränderungen in der CLASS-Datei wirksam zu machen.

Krakatau liefert uns für die Methode checkUserName folgenden Code:

.method public static checkUsername : ()Z 
  .code stack 2 locals 1 
    L0: ldc 'user.name' 
    L2: invokestatic Method java/lang/System getProperty (Ljava/lang/String;)Ljava/lang/String; 
    L5: astore_0 
    L6: aload_0 
    L7: invokevirtual Method java/lang/String toLowerCase ()Ljava/lang/String; 
    L10: ldc 'securai' 
    L12: if_acmpne L17 
    L15: iconst_1
    L16: ireturn 
 
    .stack append Object java/lang/String 
      L17: iconst_0 
      L18: ireturn 
      L19: 
    .linenumbertable 
      L0 3 
      L6 5 
      L15 6 
      L17 8 
    .end linenumbertable 
   .end code 
 .end method 

Auf den ersten Blick unterscheiden sich javap und Krakatau Source nicht sehr. Man sollte auch hier seine Mnemonics kennen, um keinen Fehler zu machen. Doch kann mithilfe von Krakatau der Code wieder in funktionierenden Java Byte-Code übersetzt werden. Dies erlaubt einfache und schnelle Veränderungen am Byte-Code während eines Rich-Client-Penetrationtests ohne gründliches Wissen über Offsets.

Nach einem Austauschen von L15 und L17 kann die CLASS-Datei wieder assembliert und ausgeführt werden und man bekommt das Geheimnis ausgegeben, da die Überprüfung auf Benutzernamen geht, die nicht “securai” sind.

Natürlich können neben einfachen Negierungen von solchen Konstrukten auch weitaus komplexere Änderungen  vorgenommen werden. Doch ist dies meist nicht einmal nötig, um eine Anwendung anzugreifen.

Bei einem von mir durchgeführten Audit kam es tatsächlich vor, dass durch eine einfache Negierung eines IF-Konstrukts einem unprivilegierten Benutzer ein geheimes Menü mit Zusatzfunktionen freigeschalten werden konnte. Hierüber hatte der Benutzer sogar Shell-Zugriff zum Server, trotz einer Drei-Schicht-Architektur.

Und die Moral von der Geschicht’?

Ganz klar: Vertrau dem Client nicht!

Hier kann man nicht oft genug erwähnen, dass alle Daten, die vom Client kommen, manipuliert sein können. Deshalb ist der einzige Ort, an dem sicherheitsrelevante Prüfungen durchgeführt werden dürfen, der Applikationsserver.

Dies kann in nahezu allen Fällen nur durch eine Drei-Schicht-Architektur Ihrer Software umgesetzt werden, also der Schicht eines Clients, den Einsatz eines Applikationsservers und dahinterliegend der Datenbank. Wichtig ist noch zu erwähnen, dass der Applikationsserver allein auf die Datenbank zugreift. Greift der Client direkt auf die Datenbank zu, ist es fast unmöglich, ein vernünftiges Berechtigungskonzept zu implementieren.

Vorheriger Beitrag
Agile Penetrationstests – was ist das denn?
Nächster Beitrag
XXE: Angriff über ein Serialisierungsformat

Ähnliche Beiträge

Es wurden keine Ergebnisse gefunden, die deinen Suchkriterien entsprechen.