In dieser Folge übernimmt Martin Förster, Entwicklungsleiter bei Rhebo, das Steuer. Er spricht mit seinen Entwicklerkollegen Ingmar Pörner und Rafael Peters darüber, wie die Programmiersprache Rust für robuste und sichere Software sorgt und wie ein hochwertiger Entwicklungsprozess in einem OT-Sicherheitsunternehmen aussieht.
Zu Gast in dieser Folge:
Martin Förster
Hallo, mein Name ist Martin Förster. Willkommen zu dieser neuen Folge des OT Security Made Simple Podcast. Ich leite die Entwicklung bei Rhebo und habe zwei Experten mitgebracht, die einen Einblick geben, wie wir die Herausforderungen der Sicherheit in unserer Produktentwicklung angehen, insbesondere wie wir die Programmiersprache Rust verwenden. Aber auch wie wir im Entwicklungsprozess arbeiten, um die Sprache zu nutzen, um zusätzliche Sicherheitsmaßnahmen hinzuzufügen und wie all dies zusammenkommt, um ein sicheres und robustes Softwareprodukt zu schaffen.
Raphael Peters
Hallo, ich bin Raphael Peters. Ich bin Backend-Entwickler bei Rhebo und arbeite an unserer Deep Packet Inspection, aber auch an anderen Komponenten in unserem Produkt.
Ingmar Pörner
Und mein Name ist Ingmar Pörner. Ich bin ein leitender Softwareentwickler bei Rhebo und arbeite hauptsächlich an dem [industriellen Netzwerk-Angriffserkennungssystem] Rhebo Industrial Protector. Neben den Aufgaben, die mir zugewiesen sind, darf ich auch viel Code in Rust schreiben. Ich freue mich auf das Gespräch.
Vielleicht beginnen wir mit einer kleinen Einschätzung, denn natürlich steht der Schutz der Versorgungsnetze unserer Kunden bei Rhebo im Mittelpunkt unseres Geschäfts. Insbesondere Rhebo Industrial Protector muss zu diesem Zweck den gesamten Datenverkehr aus dem OT-Netz eines Kunden aufnehmen, um die Datenverarbeitung und -analyse durchzuführen. Dies hat verschiedene Auswirkungen auf die Sicherheit und die Robustheit unserer Software. Die Software muss in der Lage sein, mit Situationen umzugehen, die darauf abzielen, einzelne [Netzwerksegmente] oder vielleicht das gesamte Netzwerk eines Kunden zum Absturz zu bringen, und sie muss die Dinge im Griff behalten. Das hat mit Robustheit zu tun. Der Datenstrom, den wir einspeisen, ist so heterogen, wie er nur sein kann. Wir sprechen immer noch von OT-Netzen, aber er enthält eine Menge unerwarteter Elemente. Wir haben diese Anforderungen. Darüber hinaus stellt unser Produkt auch ein sehr interessantes Ziel für mögliche Angreifende dar. Alle Informationen des Netzes laufen in unserer Anwendung zusammen, und sie nimmt eine fast allwissende Position im Netz ein. Aus diesem Grund muss die Software sicher und robust sein.
Martin
Ich verstehe. So wie du es beschreibst, befindet sich unser Produkt innerhalb des Kundennetzes, und wir müssen sicherstellen, dass das Produkt nicht für die üblichen bösartigen Angriffsvektoren anfällig ist, wie z. B. speicherbezogene Schwachstellen oder Denial-of-Service-Angriffe, die auf eine Überlastung der Software oder der Netzkomponenten abzielen. Es muss selbst robust sein, aber es muss auch robuster sein als alle anderen im Netz, denn wenn das Kundennetz angegriffen wird, z. B. durch hohe Überlastung oder durch einige Schwachstellen, muss es immer noch überleben, um dem Benutzer zu sagen: "Hey, dein Netz wird angegriffen".
Deshalb sind Sicherheit und Schutz für uns wichtig. Wir müssen nicht nur das Produkt selbst, sondern auch den gesamten Einsatz und die Umgebung, in der das Produkt arbeitet, berücksichtigen.
Ingmar
Ja.
Martin
Okay. Eine interessante Frage ist also, wie wir sicherstellen, dass das Produkt selbst robust und sicher ist, und wie es auf dem neuesten Stand ist, was zum Beispiel bekannte Schwachstellen angeht. Gibt es etwas im Produkt oder in der Art und Weise, wie wir das Produkt bereitstellen und handhaben, das wir berücksichtigen müssen? Oder wie funktioniert die Bereitstellung, und wie wird sichergestellt, dass das Produkt robust ist, wenn wir es in das Kundennetz einbringen?
Ingmar
Nun, das Bereitstellungsmanagement ist für uns eine Herausforderung, weil unsere Kundennetzwerke nicht unbedingt mit dem Internet verbunden sind, so dass man nicht die kontinuierlichen Updates nutzen kann, die man zum Beispiel von seinem Laptop gewohnt ist. Wir haben unsere Software auf einem weit verbreiteten, freien und quelloffenen Betriebssystem aufgebaut und dieses auf ein Minimum reduziert, um mögliche Angriffsvektoren zu begrenzen. Dadurch können wir die Software als Disk-Image ausliefern. Stell dir das als das gesamte Paket deines Betriebssystems vor, einschließlich der Bibliotheken, Tools, des Kernels und natürlich unserer Software. Auf diese Weise stellen wir die Software beim Kunden bereit. Wir sorgen dafür, dass der Kunde über alle Aspekte der Installation auf dem Laufenden bleibt.
Die Alternative wäre zum Beispiel, unsere Software einfach auf einen Windows-PC zu laden und nur die Software selbst zu aktualisieren. Aber das wird natürlich nicht akzeptiert, denn wir wissen, je länger eine Anwendung läuft, desto mehr Schwachstellen werden bekannt und desto mehr Dinge sollten aktualisiert werden. Das können wir nicht einfach so machen. Das ist die eine Sache.
Was wir außerdem tun, ist, dass wir jede Änderung an der Software intern begutachten, einschließlich Änderungen am grundlegenden OS [Betriebssystem]. Wir stellen sicher, dass alles, was wir hinzufügen, auch eine zweite Meinung erhält. Wir übernehmen nicht einfach blindlings die Aktualisierungen, die wir aus externen Abhängigkeiten usw. erhalten. Apropos externe Abhängigkeiten: Auch die Abhängigkeiten von Drittanbietern werden ständig auf bekannte Schwachstellen überprüft.
Eine letzte Sache, die ich vielleicht noch erwähnen möchte, ist, dass wir tatsächlich formalisierte Tests haben, gegen die wir unsere Software laufen lassen, und diese können für automatische Tests verwendet werden. Es handelt sich dabei um eine Spezifikation, die wir in all den Jahren der Entwicklung des Produkts erarbeitet haben. Bei jedem Build unserer Software können wir im Grunde sicherstellen, dass sie immer noch der Spezifikation entspricht. Die Spezifikation enthält natürlich verschiedene Aspekte der Software, aber vor allem auch Sicherheitsaspekte.
Martin
Das von uns erstellte Produkt oder Artefakt, das wir im Kundennetzwerk bereitstellen, wird automatisch daraufhin getestet, ob es tatsächlich den Anforderungen entspricht, d. h. ob [jede] Funktion funktioniert, z. B. ob diese Taste rot und diese blau ist. Außerdem werden alle Abhängigkeiten, die wir verwenden, wie das grundlegende Betriebssystem und die Bibliotheken, die wir ebenfalls nutzen, auf Schwachstellen geprüft. Wir wissen, dass sie sicher eingesetzt werden können. Aber was ist mit dem eigentlichen Code, den wir für dieses Produkt schreiben? Dieser ist in Rust geschrieben. Wie hilft uns Rust bei der Entwicklung eines robusteren, sicheren Produkts?
Raphael
Bei Rust geht es, wie bei jeder anderen Programmiersprache auch, nicht nur um die Programmiersprache, sondern um das gesamte Ökosystem, das man damit erhält. Wir verwenden viele Bibliotheken, die ebenfalls in Rust geschrieben sind, für bestimmte Teile unserer Programme, die wir aufgrund von Ressourcen nicht selbst schreiben oder pflegen. Diese Bibliotheken können natürlich auch Schwachstellen haben, aber da sie weit verbreitet sind, finden andere Leute sehr wahrscheinlich die Schwachstellen und melden sie auch.
Wie bereits erwähnt, führen wir auch selbst Schwachstellen-Scans durch, und wir verwenden dafür ein Tool namens Cargo Audit. Dieses läuft regelmäßig in unserer kontinuierlichen Integration, zum Beispiel jeden Tag, und untersucht den Abhängigkeitsgraphen. Es ist also ein Graph, weil jede Abhängigkeit auch Abhängigkeiten haben kann. Und dann prüfen wir anhand einer öffentlichen Datenbank, ob wir verwundbare Versionen dieser Bibliotheken haben, und dann werden wir gewarnt. Dann müssen wir uns das Problem ansehen und prüfen, ob wir überhaupt davon betroffen sind, denn manche Probleme [der Bibliotheken] können gar nicht bei uns auftreten und sind daher kein großes Problem. Andere müssen jedoch sehr schnell behoben werden, da sie sonst eine [Sicherheitslücke darstellen und] von anderen Personen ausgenutzt werden könnten. Wir haben also auch viele andere Werkzeuge.
Wir wollen auch ein schnelles Produkt haben. Wenn wir etwas an einem Produkt ändern, könnte es aber auch langsamer werden. Deshalb benutzen wir Benchmarking. Bei einem Benchmark lässt man sein Programm mit einem festen Datensatz laufen. Dann wird für jeden Teil des Programms gemessen, wie lange er dauert, und das wird in eine Datenbank geschrieben. Dann kann man sehen, ob das Programm nach einer Änderung langsamer oder, wenn man Glück hat, sogar schneller geworden ist. Dann können wir uns diese Daten und die Änderung ansehen und feststellen, ob wir einen Fehler gemacht haben und ob man ihn beheben kann.
Martin
Okay, verstanden. Wir haben Audits des Quellcodes selbst, wenn wir ihn schreiben. Wir haben die eigenständigen Tests des Quellcodes. Dann messen wir, ob der Quellcode oder der Code, den wir schreiben, schnell genug für unser Produkt ist, um die Anforderungen zu erfüllen. Wir wissen, dass es zum Beispiel ein Szenario mit hoher Last überstehen muss. Es ist auch in der Lage, den zu erwartenden Datenverkehr zu überwachen und so weiter, denn das ist natürlich sehr CPU- oder rechenintensiv. Ich schreibe diesen Code-Teil. Gibt es dabei auch Aspekte, die damit zusammenhängen, wie die Leute beim Schreiben des Codes zusammenarbeiten?
Ingmar
In der Tat, ja. Die erwähnten Code-Reviews zum Beispiel, die wir in unserem Unternehmen sogar zur Pflicht gemacht haben. Dadurch wird sichergestellt, dass mindestens vier Augen auf den Code schauen. Aber nicht nur das, wir fördern auch die Paarprogrammierung [Anm.: pair programming], d. h. zwei Entwickler:innen setzen sich zusammen und schreiben gleichzeitig an einem neuen Stück Code. Wir setzen dort das Vier-Augen-Prinzip ein.
Dann kommen noch zusätzliche Überprüfungen hinzu, sodass bis zu vier Personen den Code durchsehen. Dies ist nicht nur ein Mittel, um zu verhindern, dass Fehler in unsere Codebasis eingeschleust werden. Es hilft uns auch dabei, ein gemeinsames Verständnis der Codebasis zu fördern, denn wir wollen vermeiden, dass es in unserem Unternehmen Kompetenz- und Wissensinseln gibt, in denen einzelne Personen die Verantwortung dafür tragen, dass der Code sicher ist. Das ist die eine Sache.
Was ich hier noch hinzufügen möchte, ist, dass wir ein ISO27001-zertifiziertes Unternehmen sind, so dass wir unseren gesamten Code und unser gesamtes Produkt regelmäßig externen Sicherheits- und Penetrationstests unterziehen.
Martin
Wenn ich das richtig verstanden habe, schreiben und lesen mindestens vier Personen den Code, sie arbeiten also zusammen, um den Code zu erstellen. Dann wird der Code getestet, dann wird der Code geprüft, und wenn der Code schließlich in das Produkt einfließt, wird das Produkt selbst geprüft und getestet, bevor es freigegeben wird. Ich denke immer noch darüber nach, wie das geht... Das klingt nach einem sehr komplexen Prozess. Er braucht verhältnismäßig viele Personen. Verlässt sich der Prozess auf menschliche Erfahrung oder Fähigkeiten? Oder gibt es Maßnahmen, die sicherstellen, dass alle Schritte des Prozesses durchgeführt werden?
Ingmar
Nun, natürlich sorgt das von uns verwendete Build-System dafür, dass die meisten dieser Prüfungen durchgeführt werden, wenn du lokal auf deinem Laptop oder deiner Workstation entwickelst. Aber um den Wert hinter all dem zu verstehen, musst du auch bedenken, dass wir eine kontinuierliche Integration haben, die Raphael bereits erwähnt hat.
Das bedeutet, dass wir irgendwann eine gemeinsame Codebasis haben, zu der wir alle beitragen. Wenn einzelne Entwickler:innen zu irgendeinem Zeitpunkt eine neue Änderung an der Codebasis vornehmen, wird das gesamte Verfahren durchlaufen. Alle Sicherheitsprüfungen, alle Werkzeuge, alles. Alle Prozesse laufen in der kontinuierlichen Integration zusammen. Dies ist im Grunde eine Barriere, die nicht umgangen oder gemieden werden kann. Wir lassen nicht zu, dass ein Code in unsere Codebasis aufgenommen wird, der nicht die gesamte Pipeline durchlaufen hat.
Martin
Okay. Durch die Paarprogrammierung, die Überprüfungen und die durchgesetzten Richtlinien wird sichergestellt, dass ein gewisses Maß an Qualität vorhanden ist oder dass der Code bestimmte Qualitätsziele erreicht, die durchgesetzt und garantiert werden, bevor der Code tatsächlich in das Produkt einfließt.
Igmar
Das ist korrekt.
Martin
Das hat nichts mit der Sprache selbst zu tun, denn das ist unabhängig von der Sprache. Aber es geht heute auch darum, wie Rust uns hilft oder wie Rust robuster ist als andere Sprachen. Gibt es zum Beispiel Eigenschaften in Rust, die robuster sind oder sie sicherer machen als zum Beispiel C oder C++ oder andere Systemprogrammiersprachen?
Raphael Peters
Ja. Rust hat ein strengeres Speichermodell als C oder C++.
C prüft nicht, ob [das, was] du tun willst, [tatsächlich erlaubt oder sinnvoll ist]. Du kannst zum Beispiel eine Liste mit 10 Einträgen erstellen und dann einfach den C-Compiler bitten, dir den 11. oder 12. [Eintrag] zu geben. Auch Angreifende können das vielleicht versuchen. Und wenn du als Entwickler keinen manuellen Check für [das Verbot dieser Abfrage] hinzufügst, wird das [in C] nicht geprüft, und die Angreifenden bekommen dann den 11. oder 12. Eintrag. Da dieser aber nicht Teil der Liste ist, gibt der Computer einfach das aus, was er an dieser Stelle im Speicher findet. Er kann dabei sowohl zufällige Daten finden, als auch Zugangsdaten oder andere vertrauliche Daten. In manchen Fällen können Angreifende sogar Daten schreiben und z. B. Maschinencode hinzufügen, der etwas für sie tut.
Außerdem können Entwickler:innen natürlich Fehler machen. Und auch wenn es keine Angreifenden gibt, kann das Programm abstürzen, weil es diese sogenannten Bound Checks nicht gibt. Die meisten modernen Programmiersprachen, wie Java, Teile von C++, Python und natürlich auch Rust, benutzen Bound Checks, um zu verhindern, dass Angreifende auf diese Daten zugreifen.
Aber im Gegensatz zu [den anderen Sprachen] ist Rust wirklich sehr schnell und hat auch einige zusätzliche Funktionen wie das explizite Ownership von Speicher. Wenn du etwas in den Speicher legst, bekommst du in vielen Programmiersprachen eine Referenz. Und mit dieser Referenz kann man auf diese Daten zugreifen und sie lesen oder schreiben. Wenn du womöglich einen Teil des Programms hast, der diese Daten liest, ist das in Ordnung. Wenn du mehrere Teile des Programms hast, die diese Daten gleichzeitig lesen, kann das auch noch unproblematisch sein. Aber wenn du die Daten liest, sie gleichzeitig in einem anderen Teil des Programms veränderst, störst du den lesenden Teil des Programms. Dann gibt es plötzlich Teile der Daten, die nicht mehr zur selben Sache oder zum selben Zustand in der Zeit gehören. Und dein Programm könnte einige seltsame Dinge tun. Das wollen wir natürlich nicht. [Im Gegensatz zu C/C++] stellt Rust sicher, dass das nicht passiert. Und weil es diese explizite Eigentumsverwaltung gibt, ist es für Entwickler:innen auch einfacher, Dinge parallel laufen zu lassen, was uns erlaubt, die Hardware noch besser zu nutzen.
Ingmar
Ich möchte noch ein wenig mehr betonen, dass keine dieser Eigenschaften oder die Eigenschaften, für die Rust so bekannt ist, wirklich exklusiv für die Sprache Rust sind. Es gibt viele moderne Programmiersprachen mit Speicher-Sicherheit und so weiter.
Aber was man mit Rust bekommt, ist die Kombination von Leistung und Stabilität bzw. Robustheit. Zum Beispiel hat sie auch einen kleinen Speicherbedarf, wenn man ihn braucht. Traditionell war dies ein Kompromiss, bei dem man sagte: "Okay, entweder verwende ich eine sicherere Sprache [mit einem größeren Speicherbedarf] oder ich verwende diese klassischen und leistungsfähigeren Sprachen [mit kleinerem Speicherbedarf]". Bei Rust ist das nicht der Fall. Man bekommt im Grunde das volle Paket. Das macht es für uns so attraktiv, es zu verwenden. Es verhindert, dass wir in die üblichen Fallen tappen, die jeder Entwickler und jede Entwicklerin macht, weil wir auch nur Menschen sind, oder?
Martin
Du hast erwähnt, dass wir nur Menschen sind. Ich kann mir vorstellen, dass es in Rust schwieriger ist, bestimmte Arten von Schwachstellen oder Bugs zu schreiben, weil es schwieriger ist, Code zu schreiben, der Speicher liest oder in Speicher schreibt, wo er nicht hingehört. Dann ist es auch aufgrund des Eigentumsmodells schwieriger, dass mehrere Leser und Schreiber ihre Daten oder sich gegenseitig beschädigen.
Dennoch glaube ich, dass man komplexere Muster und Schwachstellen in den Code einbauen kann, die bei einer Überprüfung möglicherweise nicht erkannt werden. Zum Beispiel, wenn die vier Personen, die den Code schreiben oder lesen, weniger erfahren sind oder wenn wir diese mögliche Schwachstelle nicht kennen, aber der Code selbst korrekt ist. Und mit korrekt meine ich, dass er das tut, was er tun soll, aber er könnte immer noch Sicherheitslücken enthalten. Haben wir also etwas in unserer kontinuierlichen Integrationspipeline, das den Code prüft, das nach Mustern im Code sucht, um solche Schwachstellen zu entdecken, die ein Mensch nicht erkennt?
Raphael
Wenn man Bugs hat, gibt es in vielen Fällen irgendeinen Weg, diese zu erreichen. Wenn du als Angreifende etwas ausnutzen willst, musst du natürlich eine Eingabe für das Programm finden, damit dieser ungeprüfte Teil ausgeführt wird. Dazu würde man ein Werkzeug namens Fuzzer verwenden, das dies automatisch macht. Ein Fuzzer ist im Grunde ein Werkzeug, das in den letzten Jahren von vielen Sicherheitsforschern für große Projekte verwendet wurde, auch für den Linux-Kernel, zum Beispiel, um Bugs/Fehler in einigen Hardware-Treibern zu finden. Und sie haben tatsächlich etwas gefunden. Und wir nutzen das einfach für unser eigenes Produkt.
Der Fuzzer generiert im Grunde Daten, die für das Programm, das wir haben, eher zufällig sind. Und dann schaut der Fuzzer, welche Teile des Programms ausgeführt werden. Und wenn er eine Eingabe findet, bei der ein neuer Teil ausgeführt wird, den er vorher noch nicht gesehen hat, behält er diese spezifische Eingabe und verändert sie ein wenig, um neue Dinge von dort aus zu finden. Das garantiert nicht, dass alles gefunden wird, aber es kann viele Dinge finden, und es ist ein sehr gutes Werkzeug für uns, um Sicherheitsprobleme aufzuspüren.
Martin
Okay, super. Vielen Dank. Lasst uns vielleicht zusammenfassen, was wir heute erfahren haben. Ich habe verstanden, dass Sicherheit für uns sehr wichtig ist, und zwar nicht nur, weil wir ein Sicherheitsunternehmen sind, das seinen Kunden Sicherheitsdienste [und -produkte] anbietet, sondern weil unser Produkt robuster und leistungsfähiger sein muss als die Umgebung, in der es eingesetzt wird, um Benutzende oder Kunden tatsächlich darauf aufmerksam zu machen, dass etwas [in deren OT-Netzwerk] passiert. Es muss also den Angriff überleben, den das Netzwerk möglicherweise erleidet.
Zu diesem Zweck verwenden wir auf der untersten Ebene die Programmiersprache Rust, die über viele Sicherheitsmaßnahmen verfügt, die die Implementierung einiger oder vieler Klassen von Schwachstellen erschweren oder unmöglich machen, wie z. B. die Beschädigung des Speichers durch das Lesen oder Schreiben der falschen Speicherbereiche, oder durch die Verwechslung von Lese- und Schreibvorgängen im selben Speicher.
Dann gibt es noch die Codeanalyse, die zwar automatisiert ist, aber auch von Menschen durchgeführt wird.
Wir haben den Code, der aufgrund des Designs der Sprache robuster ist.
Wir haben vier Personen, die jede Codezeile lesen und schreiben.
Dann wird jede geänderte Codezeile dem kontinuierlichen Integrationsprozess unterzogen, zu dem auch der Fuzzing-Test gehört, der den gesamten Code mit zufälligen Eingaben bombardiert, um Schwachstellen zu finden. Das ist im Grunde genommen alles, was er tut.
Dann gibt es noch den Test, der sicherstellt, dass der Code tatsächlich das tut, was er tun muss.
Dann gibt es noch automatisierte Audits, die alle Codebasen und alle anderen Komponenten wie das Betriebssystem mit öffentlichen Schwachstellendatenbanken vergleichen und Schwachstellen aufspüren, bevor wir den Code einsetzen.
Dann haben wir auf einer größeren Ebene die Überprüfung und die anderen Richtlinien, die nicht codebasiert sind, sondern auf den gesamten Entwicklungsprozess ausgerichtet sind. Zum Beispiel die Best Practices, die wir anwenden. Dann gibt es die ISO-Zertifizierungsschritte, die wir bei jeder Codeänderung, bei jedem Arbeitsschritt anwenden.
Auf allerhöchster Ebene haben wir einen kontinuierlichen Verbesserungsprozess über den Entwicklungsprozess hinaus, der darauf abzielt, den Entwicklungsprozess selbst zu verbessern, wo wir rückwirkend "Lessons Learned" haben, wo wir diskutieren, was wir falsch gemacht haben, was wir verbessern müssen, oder was gut gelaufen ist und was wir in Zukunft fördern müssen. Dabei geht es auch darum, wie wir das Produkt entwickeln, wie die Produktentwicklung wiederum eine sichere und robustere Produktentwicklung in der Zukunft ermöglicht, und auch darum, wie wir das Team verbessern können, indem wir z. B. Qualifikationslücken durch Schulungen schließen. Das ist die höchste Ebene, wie wir das Know-how des Teams verbessern können und wie wir den Prozess mit der Zeit anwenden und verbessern können.
Vielen Dank für all diese Einblicke. Es war sehr interessant. Ich habe heute eine Menge gelernt. Vielen Dank für die Teilnahme an dieser Folge.
Ingmar & Raphael
Vielen Dank für die Einladung.
Martin
Bis zum nächsten Mal. Bleiben Sie dran. Auf Wiedersehen.