Die Zwei-Schicht-Architektur
Gerade im Bereich der Rich-Clients-Prüfungen stoßen wir häufig auf folgende Konstellation: Eine Anwendung wurde vor vielen Jahren entwickelt. Dabei musste es damals schnell gehen, also entschied man sich dazu, die Architektur möglichst einfach zu halten und direkt auf die Backend-Systeme, wie beispielsweise Datenbanken, zuzugreifen – eine klassische Zwei-Schicht-Architektur also.
Die Anwendung wächst natürlich mit der Zeit und irgendwann kommen dann Anforderungen wie Berechtigungsmanagement oder Work-Flows hinzu. Diese werden dann aus Gründen der Einfachheit in die bestehende Software-Architektur integriert. Der Einsatz einer Middleware scheint zu der Zeit zu aufwändig.
Die Software wächst natürlich immer weiter und wird schließlich irgendwann einem Penetrationstest unterzogen. Der Pentester stellt dann fest: Eine der Grundvoraussetzungen für sichere Software wurde missachtet, schließlich wurden Sicherheitsfunktionen auf den Client ausgelagert und können somit vom Angreifer manipuliert werden.
Eine bittere Pille, denn klar ist auch: Die einzig nachhaltige Lösung ist der Umstieg auf eine dreischichtige Architektur, bei der alle sicherheitsrelevanten Funktionen auf die Middleware ausgelagert werden.
Bevor nun voreilige Schlüsse gezogen werden: Dieses Phänomen gibt es in allen von uns untersuchten Applikationsarten. Ist es im klassischen Web-Bereich im Laufe der Zeit etwas besser geworden, so tritt die Problematik bei mobilen Anwendungen oder bei neuartigen Webanwendungen, die hauptsächlich clientseitig laufen und nur noch Webservices ansprechen, wieder vermehrt auf.
Die Probleme der Zwei-Schicht-Architektur
Was sind denn nun aber die konkreten Probleme beim direkten Zugriff auf die Datenbank oder anderer Backend-Systeme von Client-Systemen?
In dem Artikel nächste Woche werden wir zeigen, wie leicht man Manipulationen am Programmcode realisieren kann. Hier demonstrieren wir den Einsatz des Tools dnSpy, welches C#-Programme direkt dekompilieren, ändern und neu kompilieren kann. Somit wird relativ schnell klar, dass die Absicherung der Anwendung auf dem Client nicht funktionieren kann.
Es gibt jedoch auch noch andere Implikationen. Wird direkt auf Datenbanken zugegriffen, müssen die Zugangsdaten zwangsweise auf dem Client hinterlegt werden. Sicherlich gibt es Konzepte, wie beispielsweise die integrierte Windows-Authentifizierung von MS-SQL-Datenbanken, mit der der Zugriff auf die Datenbank eingeschränkt werden kann. Eine Berechtigung auf Datensatzebene ist aber trotzdem nur durch zusätzlichen Aufwand umzusetzen [1].
Bei den hunderten Applikationen, die ich in den letzten Jahren auditiert habe, war nur bei einer der Fall, dass der Zugriff auf die Datenbank durch Stored Procedures eingeschränkt wurde. In diesem Fall kann man aber auch die Prozeduren als „Middleware“ sehen, da die Integrität von einem Angreifer nicht kompromittiert werden kann.
Auch andere Probleme können auftreten, wobei diese häufig aufgrund von unsicheren Designentscheidungen entstanden sind und nicht zwangsläufig ein typisches Problem der Zwei-Schicht-Architektur sind. Ein typischer Fall ist der Einsatz von hartkodierten Schlüsseln für die Verschlüsselung, das Ablegen eben dieser auf dem Client oder die Implementierung eigener „Verschlüsselungsalgorithmen“.
Was kann konkret getan werden?
Zuallererst möchte ich keine unnötigen Hoffnungen machen: Es gibt nur eine nachhaltige Lösung zur Absicherung einer Zwei-Schicht-Architektur und das ist der Umstieg auf eine Architektur mit drei Schichten, auf der alle sensiblen und vertraulichen Funktionen auf einer Middleware durchgeführt werden, auf die der normale Benutzer keinen Zugriff hat.
Es ist allerdings auch klar, dass ein Umstieg auf eine sichere Architektur nicht einfach umzusetzen ist, deswegen geplant werden will und seine Zeit dauert. Schnellschüsse sind leider nicht sinnvoll – in den meisten Fällen führen diese nur zu einem Mehraufwand, der jedoch sicherheitstechnisch keinen Nutzen bringt.
Aus diesem Grund möchte ich Ihnen ein paar Lösungsansätze vorstellen, die die Übergangszeit überbrücken und das Sicherheitsniveau erhöhen.
Folgende Maßnahmen sollen im Folgenden diskutiert werden:
- Code-Obfuscation
- Auslagerung der Datenbank-Zugriffe auf einen separaten Server
- Maßnahmen zum Speicherschutz
- Einsatz eines Terminal-Servers wie Citrix
Code-Obfuscation: Schutz von Geschäftslogik okay, für Sicherheit ungeeignet
Eine Möglichkeit ist es, den Code zu obfuskieren, also vor einer Analyse zu schützen. Vor allem bei Rich-Clients auf Byte-Code Basis wie Java oder .NET scheint dies eine gute Möglichkeit zu sein, da der unbehandelte Byte-Code sehr leicht wieder in seine ursprüngliche Form gebracht werden kann.
Allerdings muss ich Sie leider enttäuschen: Code-Obfuscation benutzen Entwickler von Malware bereits seit langer Zeit, sodass die Möglichkeiten, die Funktionsweise von Code wiederherzustellen, perfektioniert wurde.
Auch löst dies das grundlegende Problem nicht: Solange die Zugriffe auf den Applikationsserver bekannt sind (z.B. durch Reverse Engineering des Netzwerkverkehrs), kann ein eigener Client entwickelt werden. Auch hier wieder die Betonung: Nur ein Applikationsserver, der alle sicherheitsrelevanten Funktionen beherbergt, schafft wirklich Abhilfe.
Dennoch: Wenn Sie die Geschäftslogik Ihres Programms schützen wollen, ist eine Obfuskierung durchaus sinnvoll. Sie machen es einem Dieb damit zwar nicht unmöglich, aber Sie erschweren es ihm. Nur zum Schutz Ihrer Applikation ist es nicht ausreichend.
Auslagerung der Datenbank-Zugriffe: Leider keine Lösung
Eine Lösung, die ein Kunde vorgeschlagen hatte, war, alle Datenbankzugriffe auf einen Server auszulagern. Damit würde der Client keinen direkten Zugriff mehr auf die Datenbank haben und die Anwendung wäre geschützt.
Leider trifft dies nicht zu: Ein einfaches Auslagern des Datenbankzugriffs reicht nicht aus, da der Client sonst beliebige SQL-Abfragen an die Middleware übertragen kann und somit wieder kompletten Zugriff auf die Datenbank erhält.
Um das abzuwenden, müssten alle Datenbankabfragen schon auf dem Server gespeichert sein und nur die ID der Abfrage sowie die relevanten Parameter übergeben werden. Wenn man dann noch die Berechtigungsprüfung implementiert, hat man auch gleich einen aus Sicht der Sicherheit vernünftigen Architekturansatz gewählt. Der Client ist dann nämlich tatsächlich nur noch ein „dummes“ Anzeigeprogramm und die kritischen Funktionen sind auf dem Server abgebildet. Alle Zwischenlösungen funktionieren leider nicht.
Speicherschutz: Hat schon bei Spielen nicht funktioniert
Bei manchen Audits habe ich innerhalb eines Debuggers bestimmte Bereiche manipuliert, um beispielsweise das Berechtigungsmanagement zu umgehen. Daraufhin wurde der Vorschlag unterbreitet, dass es verschiedene Schutzmaßnahmen gibt, um zu verhindern, dass ein Programm im Debugger gestartet wird.
Bitte sparen Sie sich den Aufwand. Sie können ja mal die Hersteller von Spielen fragen, wie gut das bisher funktioniert hat. Um es klar zu sagen: Sie haben keine Möglichkeit, den lokalen Arbeitsspeicher Ihres Programms zu schützen. Hinzu kommt, dass es immer noch einen Netzwerkverkehr gibt, den ich als lokaler Angreifer nahezu immer manipulieren kann. Abgesehen davon würde es eh nur bei Rich-Clients funktionieren.
Terminal-Server: Verlagern des Problems
Die derzeit von uns präferierte Lösung ist der Einsatz eines Terminalservers, wie beispielsweise Citrix. Dabei wird die Applikation nicht direkt bereitgestellt, sondern über einen separaten Server. Im Prinzip wird nur die Bedienoberfläche an den Client übermittelt. Wichtig: auch dies ist nur eine Übergangslösung. Leider haben nämlich viele unserer Audits von Terminalservern gezeigt, dass es nahezu unmöglich ist, direkte Zugriffe auf die darüber bereitgestellten Programme zu verhindern.
Allerdings haben Sie zum einen eine weitere Authentifizierungsschicht und zum anderen sind Manipulationen im Arbeitsspeicher nicht mehr ganz so leicht umzusetzen (aber möglich!).
Damit der Terminalserver sein eingeschränktes Schutzniveau ausnutzen kann, müssen Sie außerdem sicherstellen, dass die Datenbank bzw. alle anderen Backend-Komponenten nur vom Terminal-Server aus angesprochen werden können.
Versuchen Sie zusätzlich, Ihren Terminal-Server so tiefgehend wie möglich zu härten. Im Endeffekt verlagern Sie nämlich die Schutzfunktion, die Ihre Anwendung bereitstellen muss, auf den Terminal-Server. Das kann natürlich nur in einer entsprechend restriktiven Umgebung funktionieren.
Diese Lösung lässt sich allerdings nur für Rich-Clients auf Java- bzw. .NET-Basis und native Windows-Anwendungen realisieren.
Zusammenfassung
Ich wünschte von ganzem Herzen, ich könnte Ihnen ein einfaches Patentrezept geben, um eine Zwei-Schicht-Architektur sicher zu machen. Leider gibt es diese nicht. Mein Anliegen war es, Ihnen in diesem Artikel zu erklären, warum an einer vernünftigen Software-Architektur kein Weg vorbeiführt.
Wenn Sie einen Rich-Client bereitstellen, können Sie für die Übergangszeit am besten einen Terminal-Server verwenden. Das verschafft Ihnen genug Zeit und Überwachungsmöglichkeiten, um Ihre Software auf eine sichere Architektur zu migrieren.
Wir beraten Sie auch gerne, wie eine sichere Software-Architektur aussehen muss. Fragen Sie uns einfach, wir freuen uns auf Ihre Anfrage!
Alternativ werden wir über die nächsten Wochen Artikel zur sicheren Gestaltung von Software-Architekturen veröffentlichen, die wir auch hier verlinken.
[1] https://docs.microsoft.com/en-us/dotnet/framework/data/adonet/sql/granting-row-level-permissions-in-sql-server