Reverse Engineering 101

Dozent:

Prof. Dr. Michael Eichberg

Kontakt:

michael.eichberg@dhbw-mannheim.de

Version:
2024-05-09
Folien:

https://delors.github.io/sec-reversing101/folien.rst.html

https://delors.github.io/sec-reversing101/folien.rst.html.pdf

Fehler auf Folien melden:

https://github.com/Delors/delors.github.io/issues

Vorerfahrungen?

Reverse Engineering

Reverse Engineering ist die Analyse von Systemen mit dem Ziel, ihren Aufbau und ihre Funktionsweise zu verstehen.

Typische Anwendungsfälle:

Vom Reverse Engineering ist das Reengineering zu unterscheiden. Im Fall von letzteren geht es nur darum die Funktionalität eines bestehenden Systems mit neuen Techniken wiederherzustellen.

Zweck von Reverse Engineering

Reverse Engineering - grundlegende Schritte

  1. Informationsgewinnung zur Gewinnung aller relevanten Informationen über das Produkt

  2. Modellierung mit dem Ziel der (Wieder-)Gewinnung eines (abstrakten) Modells der relevanten Funktionalität.

  3. Überprüfung (review) des Modells auf seine Richtigkeit und Vollständigkeit.

Informationsgewinnung - Beispiel

Gegeben sei eine App zum Ver- und Entschlüsseln von Dateien sowie ein paar verschlüsselte Dateien. Mögliche erste Schritte vor der Analyse von Binärcode:

  • Die ausführbare Datei ggf. mit file überprüfen (z. B. wie kompiliert und für welches Betriebssystem und Architektur)

Beispiel:

$ file /usr/bin/openssl
/usr/bin/openssl: Mach-O universal binary with 2 archi...
/usr/bin/openssl (for architecture x86_64): Mach-O 64-bit
/usr/bin/openssl (for architecture arm64e): Mach-O 64-bit
  • Die Dateien mit einem (guten) Hexeditor auf Auffälligkeiten untersuchen.

    Hexeditor mit Dateninterpretation

Die Datei auf bekannte Viren und Malware überprüfen.

  • Eine Datei mit einem bekannten Inhalt verschlüsseln und danach vergleichen.

    Ist die Datei gleich groß?

    Falls ja, dann werden keine Metainformationen gespeichert und das Passwort kann (ggf.) nicht (leicht) verifiziert werden.

  • Eine Datei mit verschiedenen Passworten verschlüsseln.

    Sind die Dateien gleich?

    Falls ja, dann wäre die Verschlüsselung komplett nutzlos und es gilt nur noch den konstanten Schlüssel zu finden.

    Gibt es Gemeinsamkeiten?

    Falls ja, dann wäre es möglich, dass das Passwort (gehasht) in der Datei gespeichert wird.

  • Eine Datei mit einem wohldefinierten Muster verschlüsseln, um ggf. den "Mode of Operation" (insbesondere ECB) zu identifizieren.

  • Mehrere verschiedene Dateien mit dem gleichen Passwort verschlüsseln

    Gibt es Gemeinsamkeiten?

    Falls ja, dann wäre es möglich, dass die entsprechenden Teile direkt vom Passwort abgeleitet werden/damit verschlüsselt werden.

  • ...

Rechtliche Aspekte des Reverse Engineering

Software Reverse Engineering

Ansätze

statische Analyse:

Studieren des Programms ohne es auszuführen; typischerweise mittels eines Disassemblers oder eines Decompilers.

dynamische Analyse:

Ausführen des Programms; typischerweise unter Verwendung eines Debuggers oder eines instrumentations Frameworks (z. B. Frida).

hybride Analyse:

Kombination aus statischer und dynamischer Analyse.

Ansätze wie Unicorn, welches auf QEmu aufbaut, erlaubt zum Beispiel die Ausführung von (Teilen von) Binärcode auf einer anderen Architektur als der des Hosts.

Ein Beispiel wäre die Ausführung einer Methode, die im Code verschlüsselte hinterlegte Strings entschlüsselt (deobfuscation), um die Analyse zu vereinfachen.

Ggf. müssen für Teile des Codes, die die Hostfunktionalität nutzen, Stubs/Mocks bereitgestellt werden.

Disassembler

Überführt (maschinenlesbaren) Binärcode in Assemblercode

Beispiel:

Decompiler

Überführt (maschinenlesbarem) Binärcode bestmöglich in Hochsprache (meist C oder Java). Eine kleine Auswahl von verfügbaren Werkzeugen:

Mittels Decompiler ist es ggf. möglich Code, der zum Beispiel ursprünglich in Kotlin oder Scala geschrieben und für die JVM kompiliert wurde, als Java Code zurückzubekommen.

Die Ergebnisse sind für Analysezwecke zwar häufig ausreichend gut - von funktionierendem Code jedoch ggf. ((sehr) weit) entfernt.

cfr Decompiler

The CFR Decompiler (Java)

JD Decompiler

The JD Decompiler (Java)
pictures/jd-excerpt.png

Beispiel fehlgeschlagener Dekompilierung

JDec Decompiler

The JDec Decompiler (Java)

Debugger

Dient der schrittweisen Ausführung des zu analysierenden Codes oder Hardware; ermöglichen zum Beispiel Speicherinspektion und Manipulation.

Auch für das Debuggen von Hardware gibt es entsprechende Werkzeuge, z. B. Lauterbach Hardware Debugger Mittels solcher Werkzeuge ist es möglich die Ausführung von Hardware Schritt für Schritt (single step mode`) zu verfolgen und den Zustand der Hardware (Speicher und Register) zu inspizieren. Dies erfordert (z.Bsp.) eine JTAG Schnittstelle.

Erschwerung des Reverse Engineering

Obfuscation (Verschleierung)

Gerade im Umfeld von klassischen Binaries für Windows, Mac und Linux erhöhen Compiler Optimierungen, z. B. von C/C++ und Rust Compilern (-O2 / -O3), bereits den Aufwand, der notwendig ist den Code zu verstehen, erheblich.

Obfuscation - Techniken (Auszug)

Obfuscation auf Source Code Ebene: International Obfuscated C Code Contest

Obfuscation - Techniken (Auszug)

Umstellen von Instruktionen

Das Umstellen von Instruktionen erschwert die Analyse, da viele Werkzeuge zum Dekompilieren auf die Erkennung von bestimmten Mustern im Code angewiesen sind und ansonsten nur sehr generischen (Spagetti Code) oder gar unsinnigen Code zurückgeben.

Verschleierung von Strings

Das Verschleiern von Strings kann insbesondere das Reversen von Binärcode erschweren, da ein Angreifer häufig nur an einer ganz bestimmten Funktionalität interessiert ist und dann Strings ggf. einen sehr guten Einstiegspunkt für die weitergehende Analyse bieten.

Stellen Sie sich eine komplexe Java Anwendung vor, in der alle Namen von Klassen, Methoden und Attributen durch einzelne oder kurze Sequenzen von Buchstaben ersetzt wurden und sie suchen danach wie von der Anwendung Passworte verarbeitet werden. Handelt es sich um eine GUI Anwendung, dann wäre zum Beispiel die Suche nach Text, der in den Dialogen vorkommt (z. B. "Password") z. B. ein sehr guter Einstiegspunkt.

Eine sehr kurz Einführung in Java Bytecode

Die Java Virtual Machine

Java Bytecode - stackbasierte virtuelle Maschine

Die JVM ist eine stackbasierte virtuelle Maschine; die getypten Operanden eines Befehls werden auf einem Stack abgelegt und die Operationen arbeiten auf den obersten Elementen des Stacks. Jeder Thread hat seinen eigenen Stack.

Instruktion

nop
bipush 100                int

bipush  50                int


iadd         2  int     int

Stack

└─────┘
 100 
└─────┘
  50 
 100 
└─────┘
 150 
└─────┘

Java Bytecode - Methodenaufrufe und lokale Variablen

Beispiel: Default Constructor In Java Bytecode

Ein Constructor welcher keine expliziten Parameter hat und nur den super Konstruktor aufruft.

// Method descriptor #8 ()V
// Stack: 1, Locals: 1
public Main();
    0  aload_0 [this]
    1  invokespecial java.lang.Object() [31]
    4  return

Die Zeilennummern und die Informationen über die lokalen Variablen ist optional und wird nur für Debugging Zwecke benötigt.

Line numbers:         [pc: 0, line: 9]
Local variable table: [pc: 0, pc: 5]  local: this
                                      index: 0
                                      type:  de.dhbw.simplesecurepp.Main

Es gibt weitere Metainformationen, die nur für Debugging-Zwecke benötigt werden, z. B. Informationen über die ursprünglich Quelle des Codes oder die sogenannte "Local Variable Type Table" in Hinblick auf generische Typinformationen. Solche Informationen werden häufig vor Auslieferung entfernt bzw. nicht hineinkompiliert.

Beispiel: Aufruf einer komplexeren Methode

// Method descriptor #36 ([Ljava/lang/String;)V
// Stack: 5, Locals: 8
public static void main(java.lang.String[] args) throws ...;
    0  aload_0 [args]
    1  arraylength
    2  iconst_2
    3  if_icmpeq 74                // integer comparison for equality
    6  getstatic java.lang.System.err : java.io.PrintStream
    9  ldc <String "SimpleSecure++">
    11  invokevirtual java.io.PrintStream.println(java.lang.String) : void
    ...

Verschlüsselung von Daten

Alternativen zur Speicherung von Passwörtern

In einigen Anwendungsgebieten ist es möglich auf das explizite Speichern von Passwörtern ganz zu verzichten [*].

Stattdessen wird z. B. einfach versucht das Ziel zu entschlüsseln und danach evaluiert ob das Passwort (vermutlich) das Richtige war.

Kann darauf verzichtet werden zu überprüfen ob das Passwort korrekt war, dann sind keine Metainformationen notwendig und die verschlüsselte Datei kann genau so groß sein wie die unverschlüsselte Datei.

schematische Darstellung der Verschlüsselung von Containern (z. B., Veracrypt)

Schematische Darstellung von Containern.

Generische Dateiverschlüsselung ohne explizite Speicherung des Passworts

Beispiehafte Verschlüsselung von Containern.