Die Bugs hinter den Schwachstellen Teil 3
Dies ist der dritte Teil unserer fünfteiligen Blogserie, in der wir uns mit den Code-Fehlern beschäftigen, die zu den täglich auftauchenden Sicherheitslücken führen. In diesem Teil behandeln wir die Bugs #15 bis #11 der Mitre CWE Top 25 Liste für das Jahr 2022, einschließlich des Lieblingsfehlers "NULL pointer dereference" auf Platz 11.
Sie finden Teil 1 hier und Teil 2 hier.
15 - Verwendung von fest kodierten Berechtigungsnachweisen
Probleme, die unter dieser Schwachstellenklasse klassifiziert sind, entstehen durch die Aufnahme von Zugangsdaten in ein kompiliertes Paket oder durch die Aufnahme in das Verteilungsmedium oder das Speichergerät, das den Endbenutzer erreicht. Das bedeutet, dass diese Anmeldeinformationen, wenn sie entdeckt werden, auf allen Geräten funktionieren würden. Dabei handelt es sich nicht um einen eigentlichen Programmierfehler, sondern eher um ein Problem bei der Paketierung und Freigabe.
Es gibt viele Möglichkeiten, wie dies geschehen kann - wie die Verwendung von fest kodierten Testanmeldeinformationen während CI/CD die am Ende des Prozesses in das Ergebnis einfließen (im Gegensatz zur Verwendung dedizierter Authentifizierungs-Tokens), oder die Einbeziehung einer Form von Backdoor für ein Produktionsgerät das es auf den Markt schafft.
Einmal gefunden, kann diese Art von Problem sehr schwer zu lösen sein. Wenn es in einem Hardware-Gerät auftritt, besteht die einzige Möglichkeit, eine fest codierte Zugangsberechtigung zu entfernen, darin, die Firmware zu aktualisieren. Wie schwierig dieser Vorgang ist, hängt von dem jeweiligen Gerät ab.
Dies kann jedoch auch bei Softwarepaketen - nicht nur bei Hardware - vorkommen, und das passiert auch. Da in diesem Fall der Berechtigungsnachweis in das eigentliche Softwarepaket integriert ist, ist in der Regel eine aktualisierte Version erforderlich, um das Problem zu lösen.
Auch beim Reverse Engineering von Software oder Hardware sind solche Zugangsdaten relativ leicht zu erkennen, so dass böswillige Akteure bei ihren Operationen gerne auf sie stoßen.
Das eigentliche Problem mit fest kodierten Zugangsdaten ist, dass sie alle Zugangsbeschränkungen, die Sie in einem bestimmten System einrichten können, außer Kraft setzen. Wenn ein fest codierter Administratorzugang existiert, ist es egal, ob Sie keine anderen administrativen Konten erstellt haben - der fest codierte Zugang ist immer noch möglich.
Auf architektonischer Ebene treten fest kodierte Anmeldeinformationen auch bei der Konfiguration von Verbindungen zwischen Systemen auf, wo eine Verbindungszeichenfolge oder ein Kennwort in einer Konfigurationsdatei gespeichert werden, die zum Aufbau der Verbindung gelesen wird. Ein Angreifer, der diese Datei findet, könnte die Verbindung imitieren.
14 - Unsachgemäße Authentifizierung
Unzulässige Authentifizierung beschreibt eine Situation, in der eine Anwendung einem Client vertraut, der behauptet, eine bestimmte Identität zu besitzen, anstatt eine Reihe von Anmeldeinformationen, Token oder andere Authentifizierungsmechanismen zu validieren.
Dies ist sehr häufig bei Website-Verbindungen der Fall, die das Vorhandensein eines Cookies validieren, anstatt die Authentifizierung gegenüber der Website zu überprüfen. Ein Cookie kann so gefälscht werden, dass es behauptet, bereits authentifiziert worden zu sein, und die Website wird ihm vertrauen.
Ein weiteres häufiges Szenario ist, dass die Authentifizierung nur clientseitig validiert wird - dies ermöglicht es einem Angreifer, die Client-Software zu verändern oder zu stören und Zugang zu serverseitigen Ressourcen zu erhalten, indem er vorgibt, bereits authentifiziert zu sein.
13 - Integer-Überlauf oder -Wraparound
Dies ist eine der frühesten Formen von Softwarefehlern. Bei einer bestimmten Computerarchitektur (32/64 Bit, ARM, Mips usw.) belegt jede Variable eine bestimmte Menge an Speicher. Je größer der Speicherplatz, desto größer der Wert, der in dieser Variablen gespeichert werden kann. Bei arithmetischen Operationen kann es jedoch leicht vorkommen, dass nicht überprüft wird, ob das Ergebnis einer solchen Operation noch in die Speichergröße der Variablen passt - wenn man beispielsweise zwei ganze Zahlen miteinander multipliziert und das Ergebnis speichert, kann der Wert so groß werden, dass er nicht mehr in die maximale Variablengröße passt. In diesem Fall wird die Ganzzahl "überlaufen".
Verschiedene Computerarchitekturen gehen mit diesem Ereignis unterschiedlich um: Einige verwerfen den zusätzlichen Wert, was dazu führt, dass der falsche Wert gespeichert wird; andere schreiben den Wert in den nächsten verfügbaren Speicherplatz und überschreiben damit möglicherweise etwas anderes, das dort bereits gespeichert ist; und wieder andere umkreisen den Wert - wie der Kilometerzähler in einem Auto, der auf Null zurückgeht, wenn er den Maximalwert erreicht hat, und Sie weiterfahren.
Stellt man sich vor, dass die Variable die Anzahl der Iterationen in einer Schleife steuert, kann dies zu einer Endlosschleife führen. In anderen Szenarien kann es zu einem Absturz, einer nicht reagierenden Anwendung oder einem nicht reagierenden Dienst führen, und es könnte auch den verfügbaren Speicher erschöpfen oder das System insgesamt instabil machen. Es kann auch direkt zu Pufferüberlaufszenarien führen (die in einem anderen Fehler in dieser Liste behandelt werden).
Da viele Computersprachen standardmäßig keine Bound Checks enthalten - und diese entweder durch Compiler-Flags oder durch die Verwendung spezieller Bibliotheken explizit aktiviert werden müssen - ist es wichtig, eine fundierte Entscheidung über die zu verwendende Sprache für eine geplante Anwendung zu treffen.
Wenn dies bei einer bereits entwickelten Anwendung festgestellt wird, müssen geeignete Grenzprüfungen hinzugefügt werden, um sicherzustellen, dass die Berechnungen niemals außerhalb des verfügbaren variablen Speichers liegen, wobei stets zu bedenken ist, dass bei einer plattformübergreifenden Anwendung die verschiedenen Plattformen/Architekturen unterschiedliche zulässige Größen haben werden.
Werkzeuge wie Fuzzers lösen sie in der Regel aus und können daher für Entwickler von großem Wert sein.
Die Suche nach Situationen im Ablauf einer Anwendung, die für diese Art von Fehlern anfällig sind, ist eine der grundlegendsten Aufgaben, die ein Angreifer zu erledigen hat.
12 - Deserialisierung von nicht vertrauenswürdigen Daten
Serialisierung (oder Marshaling) ist der Prozess der Konvertierung von im Speicher abgelegten Daten in ein Format, das gespeichert oder auf andere Weise (über eine Netzverbindung) an ein anderes System/eine andere Anwendung/Komponente gesendet werden kann. Deserialisierung (oder Unmarshalling) ist der umgekehrte Prozess, bei dem ein Datenstrom in eine Speicherdarstellung eines internen Objekts einer Anwendung umgewandelt wird ("Objekt" bedeutet eine Variable oder eine Gruppierung von Variablen).
Wenn eine Anwendung blind darauf vertraut, dass die Daten, die sie empfängt - sei es aus einer Datei gelesen, aus einem Formular auf einer Website akzeptiert oder über eine Netzwerkverbindung -, von Natur aus gültig sind, und dann versucht, sie an einem bestimmten Speicherort zu deserialisieren, setzt sich die Anwendung einem Risiko aus. Speziell gestaltete Datenströme können eine Anwendung dazu verleiten, vorhandene Speicherplätze außerhalb des vorgesehenen Speicherplatzes zu überschreiben oder nicht genügend Daten bereitzustellen, um alle Variablen zu füllen, was zu nicht initialisierten Variablen oder sogar zur Bereitstellung von Daten außerhalb der erwarteten Grenzen führen kann - was alles zu unvorhergesehenen Ereignissen führen kann.
Als Faustregel für die Entwicklung gilt, dass keine Eingabe jemals vertrauenswürdig sein sollte, ohne Ausnahmen. Selbst wenn der Entwickler den Client und die Serveranwendung selbst schreibt, ist es immer möglich, eine Verbindung zu unterwandern oder zu versuchen, die Kommunikation zwischen ihnen zu manipulieren oder sich als eine Seite der Konversation auszugeben, was zu einem Szenario führt, in dem die angenommenen Daten nicht mehr vertrauenswürdig sind und daher nicht deserialisiert werden sollten.
Der folgende Java-Code sieht harmlos genug aus:
try { File file = new File("object.obj"); ObjectInputStream in = new ObjectInputStream(new FileInputStream(file)); javax.swing.JButton button = (javax.swing.JButton) in.readObject(); in.close(); } [source: https://cwe.mitre.org/data/definitions/502.html] |
Bei näherer Betrachtung ist die Datei "object.obj" jedoch überhaupt nicht validiert und könnte etwas anderes als eine Schaltflächendefinition enthalten, wie es die Anwendung erwartet.
11 - NULL-Zeiger-Dereferenz
Dies ist eine der häufigsten Quellen für Sicherheitslücken in Anwendungen, die in einer zeigerfreundlichen Sprache wie C geschrieben wurden. Es wird versucht, den Wert zu erhalten, auf den ein Zeiger verweist, der zuvor für ungültig erklärt oder noch nicht initialisiert wurde.
Abgesehen von architektonischen Unterschieden ist das übliche Ergebnis, dass die Anwendung abstürzt, da das Betriebssystem den Versuch erkennt, einen Speicherplatz zu lesen, der außerhalb des für diese Anwendung zulässigen Speicherbereichs liegt.
Das folgende C#-Beispiel veranschaulicht das Problem:
string name; int k=0; if(k==1) { name=Console.ReadLine(); } Console.Writeline(name.Length); |
Wenn die Ausführung den letzten Aufruf erreicht, wird die "Length"-Methode bei einer Null-Zeichenkette aufgerufen und die Anwendung stürzt ab.
Interessanterweise haben viele Sprachen Änderungen eingeführt (nullbare Typen, Compiler-Flags, neue Warnungstypen), die speziell darauf abzielen, diese Art von Fehler frühzeitig im Entwicklungsprozess zu erkennen. Andere verzichten ganz auf die explizite Verwendung von Zeigern, um sich dagegen zu schützen, aber abgesehen von vorsichtigen Programmierpraktiken gibt es kein Patentrezept, das zu 100 % wirksam ist, um NULL-Zeiger-Dereferenzen zu finden und zu verhindern.
Situationen wie die in Fehler #12 beschriebene können dazu führen, dass unerwartete Werte in den Fluss der Anwendung eingeführt werden, was zu NULL-Zeiger-Dereferenzen führt.
Eine relativ einfache Lösung für diesen Fehler besteht darin, jeder geeigneten Variablenverwendung eine Prüfung voranzustellen, um sicherzustellen, dass sie nicht NULL ist, aber nebenläufige/parallele Programmierung kann dies zu einer weniger optimalen Lösung machen.