Unlimited Plugins, WordPress themes, videos & courses! Unlimited asset downloads! From $16.50/m
Advertisement
  1. Code
  2. Security

Sichere Codierung mit Gleichzeitigkeit in Swift 4

by
Read Time:14 minsLanguages:

German (Deutsch) translation by Valentina (you can also view the original English article)

In meinem vorherigen Artikel über sichere Codierung in Swift habe ich grundlegende Sicherheitslücken in Swift wie Injektionsangriffe erörtert. Während Injektionsangriffe häufig sind, gibt es andere Möglichkeiten, wie Ihre App kompromittiert werden kann. Eine häufige, aber manchmal übersehene Art von Sicherheitslücke sind die Rennbedingungen.

Swift 4 führt den exklusiven Zugriff auf den Speicher ein, der aus einer Reihe von Regeln besteht, um zu verhindern, dass gleichzeitig auf denselben Speicherbereich zugegriffen wird. Das inout-Argument in Swift teilt einer Methode beispielsweise mit, dass der Wert des Parameters innerhalb der Methode geändert werden kann.

Aber was passiert, wenn wir dieselbe Variable übergeben, um sie gleichzeitig zu ändern?

Swift 4 hat Verbesserungen vorgenommen, die das Kompilieren verhindern. Obwohl Swift diese offensichtlichen Szenarien zur Kompilierungszeit finden kann, ist es insbesondere aus Leistungsgründen schwierig, Speicherzugriffsprobleme im gleichzeitigen Code zu finden, und die meisten Sicherheitslücken bestehen in Form von Rennbedingungen.

Rennbedingungen

Sobald Sie mehr als einen Thread haben, der gleichzeitig in dieselben Daten schreiben muss, kann eine Race-Bedingung auftreten. Rennbedingungen verursachen Datenbeschädigung. Bei diesen Arten von Angriffen sind die Sicherheitslücken normalerweise subtiler - und die Exploits kreativer. Beispielsweise besteht möglicherweise die Möglichkeit, eine gemeinsam genutzte Ressource zu ändern, um den Fluss des Sicherheitscodes in einem anderen Thread zu ändern, oder im Fall des Authentifizierungsstatus kann ein Angreifer möglicherweise eine Zeitlücke zwischen dem Zeitpunkt der Überprüfung ausnutzen und den Zeitpunkt der Verwendung einer Flagge.

Um Rennbedingungen zu vermeiden, müssen die Daten synchronisiert werden. Das Synchronisieren von Daten bedeutet normalerweise, sie zu "sperren", sodass jeweils nur ein Thread auf diesen Teil des Codes zugreifen kann (als Mutex bezeichnet - zum gegenseitigen Ausschluss). Während Sie dies explizit mit der NSLock-Klasse tun können, besteht die Möglichkeit, dass Stellen übersehen werden, an denen der Code hätte synchronisiert werden sollen. Es kann schwierig sein, die Schlösser im Auge zu behalten und festzustellen, ob sie bereits verriegelt sind oder nicht.

 Grand Central Dispatch 

Anstatt primitive Sperren zu verwenden, können Sie Grand Central Dispatch (GCD) verwenden - Apples moderne Gleichzeitigkeit-API, die auf Leistung und Sicherheit ausgelegt ist. Sie müssen nicht selbst an die Schlösser denken. es erledigt die Arbeit für Sie hinter den Kulissen.

Wie Sie sehen, handelt es sich um eine recht einfache API. Verwenden Sie daher GCD als erste Wahl, wenn Sie Ihre App für die Gleichzeitigkeit entwerfen.

Die Laufzeit-Sicherheitsüberprüfungen von Swift können nicht über GCD-Threads hinweg durchgeführt werden, da dies zu einem erheblichen Leistungseinbruch führt. Die Lösung besteht darin, das Thread Sanitizer-Werkzeug zu verwenden, wenn Sie mit mehreren Threads arbeiten. Das Thread Sanitizer-Werkzeug eignet sich hervorragend zum Auffinden von Problemen, die Sie möglicherweise nie finden, wenn Sie sich den Code selbst ansehen. Sie können es aktivieren, indem Sie zu Produkt > Schema > Schema bearbeiten > Diagnostik gehen und die Option Thread Sanitizer aktivieren.

Wenn Sie aufgrund des Designs Ihrer App mit mehreren Threads arbeiten, können Sie sich auch vor den Sicherheitsproblemen der Parallelität schützen, indem Sie versuchen, Ihre Klassen so zu gestalten, dass sie sperrenfrei sind, sodass zunächst kein Synchronisierungscode erforderlich ist. Dies erfordert einige echte Überlegungen zum Design Ihrer Benutzeroberfläche und kann sogar als eigenständige Kunst betrachtet werden!

Der Haupt-Thread-Checker

Es ist wichtig zu erwähnen, dass Datenbeschädigung auch auftreten kann, wenn Sie UI-Updates für einen anderen Thread als den Hauptthread durchführen (jeder andere Thread wird als Hintergrundthread bezeichnet).

Manchmal ist es nicht einmal offensichtlich, dass Sie sich in einem Hintergrund-Thread befinden. Wenn NSURLSession beispielsweise delegateQueue auf nil gesetzt ist, wird standardmäßig ein Hintergrundthread zurückgerufen. Wenn Sie UI-Updates durchführen oder in Ihre Daten in diesem Block schreiben, besteht eine gute Chance für die Rennbedingungen. (Beheben Sie dies, indem Sie die UI-Updates in DispatchQueue.main.async {} einschließen oder OperationQueue.main als Delegatenwarteschlange übergeben.)

Neu in Xcode 9 und standardmäßig aktiviert ist die Haupt-Thread-Prüfung (Produkt > Schema > Schema bearbeiten > Diagnostik > Laufzeit-API-Checking > Haupt-Thread-Checker). Wenn Ihr Code nicht synchronisiert ist, werden Probleme in den Laufzeitproblemen im linken Navigationsbereich von Xcode angezeigt. Achten Sie also beim Testen Ihrer App darauf.

Aus Sicherheitsgründen sollten alle von Ihnen geschriebenen Rückrufe oder Abschlusshandler dokumentiert werden, unabhängig davon, ob sie im Hauptthread zurückgegeben werden oder nicht. Befolgen Sie noch besser das neuere API-Design von Apple, mit dem Sie eine completionQueue in der Methode übergeben können, damit Sie klar entscheiden und sehen können, auf welchen Thread der Completion-Block zurückkehrt.

Ein reales Beispiel

Genug Gerede! Lassen Sie uns in ein Beispiel eintauchen.

Hier haben wir keine Synchronisation, aber mehr als ein Thread greift gleichzeitig auf die Daten zu. Das Gute an Thread Sanitizer ist, dass es einen solchen Fall erkennt. Die moderne GCD-Methode, um dies zu beheben, besteht darin, Ihre Daten einer seriellen Versandwarteschlange zuzuordnen.

Jetzt wird der Code mit dem .async-Block synchronisiert. Möglicherweise fragen Sie sich, wann Sie .async auswählen und wann Sie .sync verwenden sollen. Sie können .async verwenden, wenn Ihre App nicht warten muss, bis der Vorgang im Block abgeschlossen ist. Es könnte besser mit einem Beispiel erklärt werden.

In diesem Beispiel stellt der Thread, der das Transaktionsarray fragt, ob es eine bestimmte Transaktion enthält, eine Ausgabe bereit, sodass er warten muss. Der andere Thread führt nach dem Anhängen an das Transaktionsarray keine Aktion aus, sodass er nicht warten muss, bis der Block abgeschlossen ist.

Diese Synchronisierungs- und Asynchronisierungsblöcke können in Methoden eingeschlossen werden, die Ihre internen Daten zurückgeben, z. B. Getter-Methoden.

Das Streuen von GCD-Blöcken in allen Bereichen Ihres Codes, die auf gemeinsam genutzte Daten zugreifen, ist keine gute Vorgehensweise, da es schwieriger ist, alle Stellen zu verfolgen, die synchronisiert werden müssen. Es ist viel besser, all diese Funktionen an einem Ort zu speichern.

Gutes Design mit Accessor-Methoden ist eine Möglichkeit, dieses Problem zu lösen. Wenn Sie Getter- und Setter-Methoden verwenden und nur diese Methoden verwenden, um auf die Daten zuzugreifen, können Sie an einem Ort synchronisieren. Auf diese Weise müssen Sie nicht viele Teile Ihres Codes aktualisieren, wenn Sie den GCD-Bereich Ihres Codes ändern oder umgestalten.

Strukturen

Während einzelne gespeicherte Eigenschaften in einer Klasse synchronisiert werden können, wirkt sich das Ändern der Eigenschaften einer Struktur tatsächlich auf die gesamte Struktur aus. Swift 4 enthält jetzt Schutz für Methoden, die die Strukturen mutieren.

Schauen wir uns zunächst an, wie eine Strukturkorruption (als "Swift Access Race" bezeichnet) aussieht.

Die beiden Methoden im Beispiel ändern die gespeicherten Eigenschaften, sodass sie als mutating markiert sind. Nehmen wir an, Thread 1-Aufrufe begin() und Thread 2-Aufrufe finish(). Auch wenn begin() nur die id und finish() nur den timestamp ändert, handelt es sich immer noch um ein Zugriffsrennen. Während es normalerweise besser ist, in Accessor-Methoden zu sperren, gilt dies nicht für Strukturen, da die gesamte Struktur exklusiv sein muss.

Eine Lösung besteht darin, die Struktur bei der Implementierung Ihres gleichzeitigen Codes in eine Klasse zu ändern. Wenn Sie die Struktur aus irgendeinem Grund benötigen, können Sie in diesem Beispiel eine Bank-Klasse erstellen, in der Transaction-Strukturen gespeichert sind. Dann können die Aufrufer der Strukturen innerhalb der Klasse synchronisiert werden.

Hier ist ein Beispiel:

Zugangskontrolle

Es wäre sinnlos, all diesen Schutz zu haben, wenn Ihre Schnittstelle ein mutierendes Objekt oder UnsafeMutablePointer für die gemeinsam genutzten Daten verfügbar macht, da jetzt jeder Benutzer Ihrer Klasse ohne den Schutz von GCD mit den Daten tun kann, was er will. Geben Sie stattdessen Kopien zu den Daten im Getter zurück. Sorgfältiges Schnittstellendesign und Datenkapselung sind wichtig, insbesondere beim Entwerfen gleichzeitiger Programme, um sicherzustellen, dass die gemeinsam genutzten Daten wirklich geschützt sind.

Stellen Sie sicher, dass die synchronisierten Variablen als private und nicht als open oder public markiert sind, damit Mitglieder aus jeder Quelldatei darauf zugreifen können. Eine interessante Änderung in Swift 4 besteht darin, dass der Bereich der private-Zugriffsebene erweitert wird, um in Erweiterungen verfügbar zu sein. Bisher konnte es nur innerhalb der umschließenden Deklaration verwendet werden. In Swift 4 kann jedoch in einer Erweiterung auf eine private-Variable zugegriffen werden, solange sich die Erweiterung dieser Deklaration in derselben Quelldatei befindet.

Variablen sind nicht nur einem Risiko für Datenkorruption ausgesetzt, sondern auch Dateien. Verwenden Sie die thread-sichere FileManager Foundation-Klasse und überprüfen Sie die Ergebnisflags der Dateivorgänge, bevor Sie mit Ihrem Code fortfahren.

Schnittstelle zu Objective-C

Viele Objective-C-Objekte haben ein veränderliches Gegenstück, das durch ihren Titel dargestellt wird. Die veränderbare Version von NSString heißt NSMutableString, die von NSArray ist NSMutableArray und so weiter. Neben der Tatsache, dass diese Objekte außerhalb der Synchronisation mutiert werden können, untergraben Zeigertypen aus Objective-C auch Swift-Optionen. Es besteht eine gute Chance, dass Sie ein Objekt in Swift erwarten, aber von Objective-C wird es als Null zurückgegeben.

Wenn die App abstürzt, erhalten Sie wertvolle Einblicke in die interne Logik. In diesem Fall kann es sein, dass Benutzereingaben nicht ordnungsgemäß überprüft wurden und dass es sich lohnt, diesen Bereich des App-Flusses zu betrachten, um ihn auszunutzen.

Die Lösung besteht darin, Ihren Objective-C-Code so zu aktualisieren, dass er Anmerkungen zur Nullbarkeit enthält. Wir können hier eine leichte Ablenkung vornehmen, da dieser Rat für die sichere Interoperabilität im Allgemeinen gilt, sei es zwischen Swift und Objective-C oder zwischen zwei anderen Programmiersprachen.

Stellen Sie Ihren Objective-C-Variablen nullable voran, wenn nil zurückgegeben werden kann, und nonnull, wenn dies nicht der Fall sein sollte.

Sie können der Attributliste der Objective-C-Eigenschaften auch nullable und nonnull hinzufügen.

Das Static Analyzer-Werkzeug in Xcode war schon immer großartig, um Objective-C-Fehler zu finden. Mit Annotationen zur Nullbarkeit können Sie in Xcode 9 den Static Analyzer für Ihren Objective-C-Code verwenden. In Ihrer Datei werden Nullbarkeitsinkonsistenzen festgestellt. Navigieren Sie dazu zu Produkt > Aktion ausführen > Analysieren.

Während es standardmäßig aktiviert ist, können Sie die Nullfähigkeitsprüfungen in LLVM auch mit den Flags-Wnullability* steuern.

Nullabilitätsprüfungen sind gut, um Probleme beim Kompilieren zu finden, aber sie finden keine Laufzeitprobleme. Zum Beispiel nehmen wir manchmal in einem Teil unseres Codes an, dass immer ein optionaler Wert vorhanden ist, und verwenden das Force Unwrap ! darauf. Dies ist eine implizit ausgepackte Option, aber es gibt wirklich keine Garantie dafür, dass sie immer existiert. Wenn es als optional markiert wäre, wäre es wahrscheinlich irgendwann Null. Daher ist es eine gute Idee, das Auspacken mit Gewalt zu vermeiden !. Stattdessen besteht eine elegante Lösung darin, zur Laufzeit Folgendes zu überprüfen:

Um Ihnen weiter zu helfen, wurde in Xcode 9 eine neue Funktion hinzugefügt, mit der zur Laufzeit Nullfähigkeitsprüfungen durchgeführt werden können. Es ist Teil des Undefined Behaviour Sanitizer. Obwohl es nicht standardmäßig aktiviert ist, können Sie es aktivieren, indem Sie zu Build-Einstellungen > Undefiniertes Verhalten Sanitizer gehen und Aktivieren Sie die Prüfung der Nullbarkeit von Kommentaren die Option Ja festlegen.

Lesbarkeit

Es wird empfohlen, Ihre Methoden mit nur einem Ein- und einem Ausstiegspunkt zu schreiben. Dies ist nicht nur gut für die Lesbarkeit, sondern auch für die erweiterte Multithreading-Unterstützung.

Angenommen, eine Klasse wurde ohne Berücksichtigung der Parallelität entworfen. Später wurden die Anforderungen geändert, sodass jetzt die Methoden .lock() und .unlock() von NSLock unterstützt werden müssen. Wenn es an der Zeit ist, Teile Ihres Codes zu sperren, müssen Sie möglicherweise viele Ihrer Methoden neu schreiben, um Thread-sicher zu sein. Es ist leicht, eine return zu übersehen, die mitten in einer Methode versteckt ist, die später Ihre NSLock-Instanz sperren sollte, was dann zu einer Race-Bedingung führen kann. Auch Anweisungen wie return entsperren die Sperre nicht automatisch. Ein anderer Teil Ihres Codes, der davon ausgeht, dass die Sperre entsperrt ist und erneut versucht zu sperren, blockiert die App (die App friert ein und wird schließlich vom System beendet). Abstürze können auch Sicherheitslücken in Multithread-Code darstellen, wenn temporäre Arbeitsdateien vor dem Beenden des Threads nie bereinigt werden. Wenn Ihr Code diese Struktur hat:

Sie können stattdessen den Booleschen Wert speichern, ihn unterwegs aktualisieren und am Ende der Methode zurückgeben. Dann kann der Synchronisationscode ohne großen Aufwand einfach in die Methode eingeschlossen werden.

Die .unlock()-Methode muss von demselben Thread aufgerufen werden, der .lock() aufgerufen hat. Andernfalls führt dies zu einem undefinierten Verhalten.

Testen

Das Auffinden und Beheben von Schwachstellen im gleichzeitigen Code hängt häufig von der Fehlersuche ab. Wenn Sie einen Fehler finden, ist es, als würden Sie einen Spiegel vor sich halten - eine großartige Gelegenheit zum Lernen. Wenn Sie vergessen haben, an einer Stelle zu synchronisieren, ist es wahrscheinlich, dass der gleiche Fehler an anderer Stelle im Code liegt. Wenn Sie sich die Zeit nehmen, den Rest Ihres Codes auf denselben Fehler zu überprüfen, wenn Sie auf einen Fehler stoßen, können Sie auf sehr effiziente Weise Sicherheitslücken vermeiden, die in zukünftigen App-Versionen immer wieder auftreten.

Tatsächlich waren viele der jüngsten iOS-Jailbreaks auf wiederholte Codierungsfehler zurückzuführen, die in Apples IOKit gefunden wurden. Sobald Sie den Stil des Entwicklers kennen, können Sie andere Teile des Codes auf ähnliche Fehler überprüfen.

Das Auffinden von Fehlern ist eine gute Motivation für die Wiederverwendung von Code. Zu wissen, dass Sie ein Problem an einer Stelle behoben haben und nicht dieselben Vorkommen im Code zum Kopieren/Einfügen finden müssen, kann eine große Erleichterung sein.

Es kann schwierig sein, die Rennbedingungen während des Testens zu finden, da der Speicher möglicherweise auf die „richtige Weise“ beschädigt werden muss, um das Problem zu erkennen, und manchmal treten die Probleme lange später in der Ausführung der App auf.

Decken Sie beim Testen Ihren gesamten Code ab. Gehen Sie jeden Ablauf und Fall durch und testen Sie jede Codezeile mindestens einmal. Manchmal hilft es, zufällige Daten einzugeben (die Eingaben zu verwischen) oder Extremwerte zu wählen, in der Hoffnung, einen Randfall zu finden, der beim Betrachten des Codes oder bei der normalen Verwendung der App nicht offensichtlich wäre. Dies kann zusammen mit den neuen verfügbaren Xcode-Werkzeuge einen großen Beitrag zur Verhinderung von Sicherheitslücken leisten. Obwohl kein Code 100% sicher ist, zahlt sich das Befolgen einer Routine wie frühzeitiger Funktionstests, Komponententests, Systemtests, Stress- und Regressionstests wirklich aus.

Neben dem Debuggen Ihrer App unterscheidet sich bei der Release-Konfiguration (der Konfiguration für im Store veröffentlichte Apps) eine Code-Optimierung. Was der Compiler beispielsweise für eine nicht verwendete Operation hält, kann optimiert werden, oder eine Variable bleibt möglicherweise nicht länger als nötig in einem gleichzeitigen Block. Für Ihre veröffentlichte App wird Ihr Code tatsächlich geändert oder unterscheidet sich von dem, den Sie getestet haben. Dies bedeutet, dass Fehler eingeführt werden können, die erst auftreten, wenn Sie Ihre App freigeben.

Wenn Sie keine Testkonfiguration verwenden, stellen Sie sicher, dass Sie Ihre App im Release-Modus testen, indem Sie zu Produkt > Schema > Schema bearbeiten navigieren. Wählen Sie links in der Liste die Option Ausführen aus, und ändern Sie im Info-Bereich rechts die Option Build-Konfiguration in Freigeben. Während es gut ist, Ihre gesamte App in diesem Modus abzudecken, sollten Sie wissen, dass sich die Haltepunkte und der Debugger aufgrund von Optimierungen nicht wie erwartet verhalten. Beispielsweise sind Variablenbeschreibungen möglicherweise nicht verfügbar, obwohl der Code korrekt ausgeführt wird.

Abschluss

In diesem Beitrag haben wir uns mit den Rennbedingungen befasst und wie man sie vermeidet, indem man sicher codiert und Tools wie den Thread Sanitizer verwendet. Wir haben auch über den exklusiven Zugriff auf den Speicher gesprochen, der eine großartige Ergänzung zu Swift 4 darstellt. Stellen Sie sicher, dass in den Build-Einstellungen > Exklusiver Zugriff auf den Speicher die Option Vollständige Durchsetzung aktiviert ist!

Denken Sie daran, dass diese Durchsetzungsmaßnahmen nur für den Debug-Modus aktiviert sind. Wenn Sie weiterhin Swift 3.2 verwenden, werden viele der beschriebenen Durchsetzungsmaßnahmen nur in Form von Warnungen ausgeführt. Nehmen Sie die Warnungen ernst oder nutzen Sie noch besser alle neuen Funktionen, indem Sie Swift 4 noch heute einsetzen!

Und während Sie hier sind, lesen Sie einige meiner anderen Beiträge zur sicheren Codierung für iOS und Swift!

Advertisement
Did you find this post useful?
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.