Michael Howard und Keith Brown
In diesem Artikel wird davon ausgegangen, dass Sie mit C++, C# und SQL vertraut sind
Zusammenfassung: Wenn es um Sicherheitsprobleme geht, gibt es viele Situationen, die zu Problemen führen können. Sie vertrauen wahrscheinlich dem gesamten in Ihrem Netzwerk ausgeführten Code, gewähren allen Benutzern Zugriff auf wichtige Dateien und machen sich nie die Mühe, zu überprüfen, ob sich der Code auf Ihren Computern geändert hat. Es kann auch sein, dass Sie keine Antivirensoftware installiert haben, Ihren eigenen Code nicht sichern und zu vielen Konten zu viele Berechtigungen erteilen. Es kann sogar sein, dass Sie bei der Verwendung einer Reihe integrierter Funktionen, die böswillige Eingriffe ermöglichen, nachlässig sind und Server-Ports ohne Überwachung offen lassen. Natürlich können wir noch viele weitere Beispiele nennen. Was sind die wirklich wichtigen Probleme (d. h. die gefährlichsten Fehler, die sofort behoben werden sollten, um eine Gefährdung Ihrer Daten und Systeme zu vermeiden)? Die Sicherheitsexperten Michael Howard und Keith Brown geben zehn Tipps, die Ihnen weiterhelfen.
-------------------------------------------------- ----------------------------------
Sicherheitsprobleme umfassen viele Aspekte. Sicherheitsrisiken können von überall her kommen. Möglicherweise haben Sie ineffektiven Fehlerbehandlungscode geschrieben oder sind bei der Erteilung von Berechtigungen zu großzügig gewesen. Möglicherweise haben Sie vergessen, welche Dienste auf Ihrem Server ausgeführt werden. Sie können alle Benutzereingaben akzeptieren. Und so weiter. Um Ihnen beim Schutz Ihres Computers, Netzwerks und Codes einen Vorsprung zu verschaffen, finden Sie hier zehn Tipps, die Sie für eine sicherere Netzwerkstrategie befolgen können.
1. Wenn Sie Benutzereingaben vertrauen, sind Sie einem Risiko ausgesetzt.
Auch wenn Sie den Rest nicht lesen, denken Sie daran: „Vertrauen Sie Benutzereingaben nicht.“ Das Problem entsteht, wenn man immer davon ausgeht, dass die Daten gültig und nicht bösartig sind. Die meisten Sicherheitslücken bestehen darin, dass Angreifer in böswilliger Absicht geschriebene Daten an Server weiterleiten.
Das Vertrauen auf die Richtigkeit der Eingabe kann zu Pufferüberläufen, Cross-Site-Scripting-Angriffen, SQL-Einfügungscode-Angriffen und mehr führen.
Lassen Sie uns diese potenziellen Angriffsvektoren im Detail besprechen.
2. Verhindern Sie einen Pufferüberlauf.
Wenn ein Angreifer eine Datenlänge bereitstellt, die größer ist als von der Anwendung erwartet, kommt es zu einem Pufferüberlauf und die Daten laufen in den internen Speicherbereich über. Pufferüberlauf ist in erster Linie ein C/C++-Problem. Sie stellen eine Bedrohung dar, sind aber in der Regel leicht zu beheben. Wir haben nur zwei Pufferüberläufe gesehen, die nicht offensichtlich und schwer zu beheben waren. Der Entwickler hatte nicht damit gerechnet, dass die extern bereitgestellten Daten größer sein würden als der interne Puffer. Der Überlauf führt zur Beschädigung anderer Datenstrukturen im Speicher, was häufig von Angreifern ausgenutzt wird, um Schadcode auszuführen. Array-Indexfehler können auch Pufferunterläufe und -überläufe verursachen, dies kommt jedoch seltener vor.
Schauen Sie sich den folgenden C++-Codeausschnitt an:
void DoSomething(char *cBuffSrc, DWORD cbBuffSrc) {
char cBuffDest[32];
memcpy(cBuffDest,cBuffSrc,cbBuffSrc);
}
Was ist das Problem? Tatsächlich ist an diesem Code nichts auszusetzen, wenn cBuffSrc und cbBuffSrc aus einer vertrauenswürdigen Quelle stammen (z. B. Code, der den Daten nicht vertraut und daher deren Gültigkeit und Größe überprüft). Wenn die Daten jedoch aus einer nicht vertrauenswürdigen Quelle stammen und nicht verifiziert wurden, kann ein Angreifer (nicht vertrauenswürdige Quelle) cBuffSrc leicht größer als cBuffDest machen und auch cbBuffSrc größer als cBuffDest festlegen. Wenn memcpy die Daten in cBuffDest kopiert, wird die Rücksprungadresse von DoSomething geändert. Da cBuffDest im Stapelrahmen der Funktion neben der Rücksprungadresse liegt, kann der Angreifer über den Code einige böswillige Vorgänge ausführen.
Der Weg zum Ausgleich besteht darin, den Eingaben des Benutzers und den in cBuffSrc und cbBuffSrc enthaltenen Daten nicht zu vertrauen:
void DoSomething(char *cBuffSrc, DWORD cbBuffSrc) {
const DWORD cbBuffDest = 32;
char cBuffDest[cbBuffDest];
#ifdef _DEBUG
memset(cBuffDest, 0x33, cbBuffSrc);
#endif
memcpy(cBuffDest, cBuffSrc, min(cbBuffDest, cbBuffSrc));
}
Diese Funktion demonstriert drei Eigenschaften einer richtig geschriebenen Funktion, die Pufferüberläufe reduzieren kann. Zunächst muss der Aufrufer die Länge des Puffers angeben. Natürlich kann man diesem Wert nicht blind vertrauen! Als nächstes erkennt der Code in einem Debug-Build, ob der Puffer tatsächlich groß genug ist, um den Quellpuffer aufzunehmen. Andernfalls wird möglicherweise eine Zugriffsverletzung ausgelöst und der Code wird möglicherweise in den Debugger geladen. Beim Debuggen werden Sie überrascht sein, wie viele Fehler Sie finden. Schließlich und am wichtigsten ist, dass Aufrufe von memcpy insofern defensiv sind, als sie nicht mehr Daten kopieren, als der Zielpuffer aufnehmen kann.
Im Rahmen des Windows® Security Push bei Microsoft haben wir eine Liste sicherer String-Verarbeitungsfunktionen für C-Programmierer erstellt. Sie finden sie in Strsafe.h: Safer String Handling in C (Englisch).
3. Verhindern Sie Cross-Site-Scripting.
Cross-Site-Scripting-Angriffe sind ein einzigartiges Problem im Web. Sie können Clientdaten durch eine versteckte Schwachstelle in einer einzelnen Webseite schädigen. Stellen Sie sich die Konsequenzen des folgenden ASP.NET-Codeausschnitts vor:
<script language=c#>
Response.Write("Hallo," + Request.QueryString("name"));
</script>
Wie viele Leute haben ähnlichen Code gesehen? Aber überraschenderweise gibt es Probleme! Normalerweise greifen Benutzer über eine URL ähnlich der folgenden auf diesen Code zu:
http://explorationair.com/welcome.aspx?name=Michael
Der C#-Code geht davon aus, dass die Daten immer gültig sind und nur einen Namen enthalten. Allerdings könnte ein Angreifer diesen Code missbrauchen, indem er Skript- und HTML-Code als Namen angibt. Wenn Sie die folgende URL eingeben
http://northwindtraders.com/welcome.aspx?name=<script>alert(' Hallo!');
</script>
Sie erhalten eine Webseite mit einem Dialogfeld mit der Aufschrift „Hallo!“ Sie sagen vielleicht: „Na und?“ Stellen Sie sich vor, ein Angreifer könnte einen Benutzer dazu verleiten, auf einen Link wie diesen zu klicken, aber die Abfragezeichenfolge enthielt ein wirklich gefährliches Skript und HTML, wodurch er das Cookie des Benutzers erbeutete und es an eine Website sendete, deren Eigentümer er ist der Angreifer; jetzt hat der Angreifer Zugriff auf Ihre privaten Cookie-Informationen, oder noch schlimmer.
Um dies zu vermeiden, gibt es zwei Möglichkeiten. Die erste besteht darin, der Eingabe zu misstrauen und den Inhalt des Benutzernamens streng einzuschränken. Mithilfe regulärer Ausdrücke können Sie beispielsweise überprüfen, ob der Name nur eine gemeinsame Teilmenge von Zeichen enthält und nicht zu groß ist. Der folgende C#-Codeausschnitt zeigt, wie dieser Schritt ausgeführt wird:
Regex
r = new Regex(@"^[w]{1,40}$");
// Gut! Saite ist ok
} anders {
// nicht gut! Ungültige Zeichenfolge
}
Dieser Code verwendet reguläre Ausdrücke, um zu überprüfen, ob eine Zeichenfolge nur 1 bis 40 Buchstaben oder Zahlen enthält. Nur so lässt sich sicher feststellen, ob ein Wert korrekt ist.
Es gibt keine Möglichkeit, dass HTML oder Skript diesen regulären Ausdruck täuschen können! Verwenden Sie keine regulären Ausdrücke, um nach ungültigen Zeichen zu suchen und lehnen Sie Anfragen ab, wenn solche ungültigen Zeichen gefunden werden, da es leicht ist, etwas zu übersehen.
Die zweite Vorsichtsmaßnahme besteht darin, alle Eingaben als Ausgaben in HTML zu kodieren. Dadurch werden gefährliche HTML-Tags auf sicherere Escape-Zeichen reduziert. Sie können HttpServerUtility.HtmlEncode in ASP.NET oder Server.HTMLEncode in ASP verwenden, um potenziell problematische Zeichenfolgen zu maskieren.
4. Fordern Sie keine SA-Berechtigungen an.
Der letzte Input-Trust-Angriff, den wir besprechen werden, ist das Einfügen von SQL-Code. Viele Entwickler schreiben Code, der Eingaben entgegennimmt und diese zum Erstellen von SQL-Abfragen verwendet, die mit einem Backend-Datenspeicher wie Microsoft® SQL Server™ oder Oracle kommunizieren.
Sehen Sie sich den folgenden Codeausschnitt an:
void DoQuery(string Id) {
SqlConnection sql=new SqlConnection(@"data source=localhost;" +
"Benutzer-ID=sa;passwort=passwort;");
sql.Open();
sqlstring= „SELECT hasshipped“ +
" FROM shipping WHERE id='" + Id + "'";
SqlCommand cmd = new SqlCommand(sqlstring,sql);
•••
Dieser Code weist drei schwerwiegende Mängel auf. Zunächst wird eine Verbindung vom Webdienst zum SQL Server als Systemadministratorkonto sa hergestellt. Sie werden bald die Fallstricke erkennen. Zweitens: Achten Sie auf die kluge Praxis, „Passwort“ als Passwort für das SA-Konto zu verwenden!
Das eigentliche Problem ist jedoch die Zeichenfolgenverkettung, mit der SQL-Anweisungen erstellt werden. Wenn der Benutzer als ID 1001 eingibt, erhalten Sie die folgende SQL-Anweisung, die vollständig gültig ist.
SELECT hasshipped FROM shipping WHERE id = '1001'
Aber Angreifer sind viel kreativer. Sie würden für die ID eine „‘1001‘ DROP-Tabelle Versand –“ eingeben, die eine Abfrage wie diese ausführen würde:
SELECT hat versendet von
Versand WHERE id = '1001'
DROP-Tabellenversand -- ';
Es ändert die Art und Weise, wie die Abfrage funktioniert. Dieser Code versucht nicht nur festzustellen, ob etwas versendet wurde, sondern löscht auch die Versandtabelle! Operator – ist der Kommentaroperator in SQL, der es Angreifern erleichtert, eine Reihe gültiger, aber gefährlicher SQL-Anweisungen zu erstellen!
Zu diesem Zeitpunkt fragen Sie sich vielleicht, wie jeder Benutzer die Tabelle in der SQL Server-Datenbank löschen kann. Natürlich haben Sie Recht, nur Administratoren können eine solche Aufgabe übernehmen. Aber hier stellen Sie als sa eine Verbindung zur Datenbank her, und sa kann mit der SQL Server-Datenbank tun, was er will. Stellen Sie niemals von einer Anwendung aus eine Verbindung zu SQL Server her. Der richtige Ansatz besteht darin, gegebenenfalls die integrierte Windows-Authentifizierung zu verwenden oder eine Verbindung als vordefiniertes Konto mit den entsprechenden Berechtigungen herzustellen.
Das Beheben von SQL-Einfügungscodeproblemen ist einfach. Mithilfe gespeicherter SQL-Prozeduren und -Parameter zeigt der folgende Code, wie eine solche Abfrage erstellt wird – und wie reguläre Ausdrücke verwendet werden, um zu bestätigen, dass die Eingabe gültig ist, da unsere Transaktion angibt, dass die Sendungs-ID nur 4 bis 10 Ziffern lang sein darf:
Regex r = neuer Regex(@"^d{4,10}$");
if (!r.Match(Id).Success)
throw new Exception("Invalid ID")
;
string str="sp_HasShipped";
SqlCommand cmd = new SqlCommand(str,sqlConn);
cmd.CommandType = CommandType.StoredProcedure;
cmd.Parameters.Add("@ID",Id);
Pufferüberläufe, Cross-Site-Scripting und SQL-Einfügungscode-Angriffe sind Beispiele für vertrauenswürdige Eingabeprobleme. Alle diese Angriffe werden durch einen Mechanismus abgewehrt, der alle Eingaben als schädlich betrachtet, sofern nicht das Gegenteil bewiesen wird.
5. Achten Sie auf den Verschlüsselungscode!
Werfen wir einen Blick auf etwas, das uns überraschen könnte. Ich habe festgestellt, dass mehr als dreißig Prozent des von uns untersuchten Sicherheitscodes Sicherheitslücken aufwies. Die vielleicht häufigste Schwachstelle ist Ihr eigener Verschlüsselungscode, der wahrscheinlich angreifbar ist. Erstellen Sie niemals Ihren eigenen Verschlüsselungscode, das ist eine dumme Angelegenheit. Denken Sie nicht, dass andere ihn nicht knacken können, nur weil Sie über einen eigenen Verschlüsselungsalgorithmus verfügen. Angreifer haben Zugang zu Debuggern und verfügen außerdem über die Zeit und das Wissen, um zu bestimmen, wie Systeme funktionieren – und sie oft innerhalb weniger Stunden kaputt zu machen. Sie sollten die Win32® CryptoAPI verwenden. Der Namespace System.Security.Cryptography bietet eine Reihe hervorragender und getesteter Verschlüsselungsalgorithmen.
6. Reduzieren Sie die Möglichkeit eines Angriffs.
Wenn mehr als 90 % der Benutzer dies nicht anfordern, sollte eine Funktion nicht standardmäßig installiert werden. Internet Information Services (IIS) 6.0 folgt dieser Installationsempfehlung, über die Sie in Wayne Berrys Artikel „Innovationen in Internet Information Services Let You Tightly Guard Secure Data and Server Processes“ lesen können, der diesen Monat veröffentlicht wurde. Die Idee hinter dieser Installationsstrategie besteht darin, dass Sie nicht auf Dienste achten, die Sie nicht nutzen, und wenn diese Dienste ausgeführt werden, könnten sie von anderen ausgenutzt werden. Wenn eine Funktion standardmäßig installiert ist, sollte sie nach dem Prinzip der geringsten Autorisierung ausgeführt werden. Das bedeutet, dass Anwendungen nicht mit Administratorrechten ausgeführt werden dürfen, es sei denn, dies ist erforderlich. Befolgen Sie am besten diesen Rat.
7. Nutzen Sie das Prinzip der geringsten Autorisierung.
Betriebssysteme und Common Language Runtimes verfügen aus mehreren Gründen über eine Sicherheitsrichtlinie. Viele Leute gehen davon aus, dass der Hauptgrund für die Existenz dieser Sicherheitsrichtlinie darin besteht, Benutzer daran zu hindern, absichtlich Schaden anzurichten: Zugriff auf Dateien, zu deren Zugriff sie nicht berechtigt sind, Neukonfiguration des Netzwerks entsprechend ihren Bedürfnissen und anderes ungeheuerliches Verhalten. Ja, diese Art von Insider-Angriffen kommt häufig vor und muss geschützt werden, aber es gibt noch einen weiteren Grund, an dieser Sicherheitsstrategie festzuhalten. Das heißt, es werden Verteidigungsbarrieren um den Code herum aufgebaut, um zu verhindern, dass Benutzer durch absichtliche oder (wie es häufig vorkommt) unbeabsichtigte Aktionen Chaos im Netzwerk anrichten. Beispielsweise ist ein per E-Mail heruntergeladener Anhang bei der Ausführung auf Alices Computer auf Ressourcen beschränkt, auf die Alice zugreifen kann. Wenn ein Anhang ein Trojanisches Pferd enthält, besteht eine gute Sicherheitsstrategie darin, den möglichen Schaden zu begrenzen.
Wenn Sie Serveranwendungen entwerfen, erstellen und bereitstellen, können Sie nicht davon ausgehen, dass alle Anfragen von legitimen Benutzern stammen. Wenn Ihnen ein Bösewicht eine böswillige Anfrage sendet (hoffentlich nicht) und sich Ihr Code schlecht verhält, möchten Sie, dass Ihre Anwendung über alle möglichen Abwehrmaßnahmen verfügt, um den Schaden zu begrenzen. Daher glauben wir, dass Ihr Unternehmen Sicherheitsrichtlinien nicht nur implementiert, weil es Ihnen oder Ihrem Code nicht vertraut, sondern auch, um sich vor bösartigem Code von außen zu schützen.
Das Prinzip der geringsten Autorisierung besagt, dass die für den Code erforderlichen Mindestberechtigungen in kürzester Zeit erteilt werden sollten. Errichten Sie jedoch jederzeit so viele Schutzmauern wie möglich um Ihren Code. Wenn etwas Schlimmes passiert – wie Murphys Gesetz garantiert – werden Sie froh sein, dass diese Schutzmauern vorhanden sind. Daher finden Sie hier einige spezifische Methoden zum Ausführen von Code unter Verwendung des Prinzips der geringsten Autorisierung.
Wählen Sie eine sichere Umgebung für Ihren Servercode, die ihm nur den Zugriff auf die Ressourcen ermöglicht, die für die Ausführung seiner Aufgabe erforderlich sind. Wenn einige Teile Ihres Codes hohe Berechtigungen erfordern, sollten Sie erwägen, diesen Teil des Codes zu isolieren und ihn separat mit höheren Berechtigungen auszuführen. Um diesen Code, der mit unterschiedlichen Authentifizierungsinformationen des Betriebssystems ausgeführt wird, sicher zu trennen, ist es am besten, diesen Code in einem separaten Prozess auszuführen (der in einer sicheren Umgebung mit höheren Berechtigungen ausgeführt wird). Das bedeutet, dass Sie eine prozessübergreifende Kommunikation benötigen (z. B. COM oder Microsoft .NET Remoting) und die Schnittstelle zu diesem Code so gestalten müssen, dass Roundtrips minimiert werden.
Wenn Sie Code in einer .NET Framework-Umgebung in Assemblys aufteilen, berücksichtigen Sie die für jeden Codeabschnitt erforderlichen Berechtigungsstufen. Sie werden feststellen, dass es sich um einen einfachen Vorgang handelt: Trennen Sie den Code, der höhere Berechtigungen erfordert, in eine separate Assembly, die ihm mehr Berechtigungen gewährt, während die meisten verbleibenden Assemblys mit niedrigeren Berechtigungen ausgeführt werden, sodass Sie Ihrem Code weitere Schutzmaßnahmen hinzufügen können. Vergessen Sie dabei nicht, dass Sie aufgrund des Code Access Security (CAS)-Stacks nicht nur die Berechtigungen Ihrer eigenen Assembly, sondern auch aller von Ihnen aufgerufenen Assemblys einschränken.
Viele Menschen erstellen ihre eigenen Anwendungen, damit neue Komponenten in ihre Produkte integriert werden können, nachdem diese getestet und den Kunden zur Verfügung gestellt wurden. Die Sicherung dieser Art von Anwendung ist sehr schwierig, da Sie nicht jeden möglichen Codepfad testen können, um Fehler und Sicherheitslücken zu finden. Wenn Ihre Anwendung jedoch gehostet wird, bietet die CLR eine hervorragende Funktion, mit der diese Erweiterungspunkte geschlossen werden können. Indem Sie ein Berechtigungsobjekt oder einen Berechtigungssatz deklarieren und „PermitOnly“ oder „Deny“ aufrufen, fügen Sie Ihrem Stapel eine Markierung hinzu, die die Erteilung von Berechtigungen für jeden von Ihnen aufgerufenen Code blockiert. Indem Sie dies vor dem Aufrufen eines Plug-Ins tun, können Sie die Aufgaben einschränken, die das Plug-In ausführen kann. Beispielsweise benötigt ein Plugin zur Berechnung von Ratenzahlungen keinen Zugriff auf das Dateisystem. Dies ist nur ein weiteres Beispiel für das geringste Privileg, bei dem Sie sich im Voraus schützen. Beachten Sie unbedingt diese Einschränkungen und beachten Sie, dass Plug-Ins mit höheren Berechtigungen Assert-Anweisungen verwenden können, um diese Einschränkungen zu umgehen.
8. Machen Sie sich Fehlermuster bewusst
und akzeptieren Sie diese. Andere hassen es genauso sehr, Fehlerbehandlungscode zu schreiben wie Sie. Es gibt so viele Gründe, warum Code scheitern kann, und es kann frustrierend sein, nur daran zu denken. Die meisten Programmierer, uns eingeschlossen, konzentrieren sich lieber auf den normalen Ausführungspfad. Dort wird die Arbeit wirklich erledigt. Lassen Sie uns diese Fehlerbehandlung so schnell und schmerzlos wie möglich erledigen und dann mit der nächsten Zeile echten Codes fortfahren.
Leider ist dieses Gefühl nicht sicher. Stattdessen müssen wir den Fehlermustern in unserem Code mehr Aufmerksamkeit schenken. Dieser Code wird oft ohne große Aufmerksamkeit geschrieben und oft nicht vollständig getestet. Erinnern Sie sich an das letzte Mal, als Sie absolut sicher waren, jede Codezeile einer Funktion debuggt zu haben, einschließlich jedes kleinen Fehlerhandlers darin?
Nicht getesteter Code führt häufig zu Sicherheitslücken. Es gibt drei Dinge, die Ihnen helfen können, dieses Problem zu mildern. Schenken Sie diesen winzigen Fehlerbehandlern zunächst die gleiche Aufmerksamkeit wie Ihrem normalen Code. Berücksichtigen Sie den Zustand des Systems, wenn Ihr Fehlerbehandlungscode ausgeführt wird. Befindet sich das System in einem effizienten und sicheren Zustand? Zweitens: Sobald Sie eine Funktion geschrieben haben, gehen Sie sie ein paar Mal durch und debuggen Sie sie gründlich. Stellen Sie dabei sicher, dass Sie jeden Fehlerbehandler testen. Beachten Sie, dass selbst mit solchen Techniken sehr subtile Timing-Fehler möglicherweise nicht entdeckt werden. Möglicherweise müssen Sie einen Fehlerparameter an Ihre Funktion übergeben oder den Status des Systems auf irgendeine Weise anpassen, damit Ihr Fehlerhandler ausgeführt werden kann. Indem Sie sich die Zeit nehmen, Ihren Code schrittweise durchzugehen, können Sie langsamer werden und haben genug Zeit, Ihren Code und den Status Ihres Systems während der Ausführung zu sehen. Beim sorgfältigen Durchgehen des Codes im Debugger entdeckten wir viele Fehler in unserer Programmierlogik. Dies ist eine bewährte Technologie. Bitte nutzen Sie diese Technik. Stellen Sie abschließend sicher, dass Ihre Testsuite dazu führt, dass Ihre Funktion fehlschlägt. Versuchen Sie, eine Testsuite zu haben, die jede Codezeile in der Funktion untersucht. Dies kann Ihnen helfen, Muster zu erkennen, insbesondere wenn Sie Ihre Tests automatisieren und sie jedes Mal ausführen, wenn Sie Ihren Code erstellen.
Es gibt eine sehr wichtige Sache über Fehlermodi zu sagen. Stellen Sie sicher, dass sich Ihr System im sichersten Zustand befindet, wenn Ihr Code ausfällt. Einige der problematischen Codes sind unten aufgeführt:
bool accessGranted = true; // Zu optimistisch!
versuchen {
// Sehen Sie, ob wir auf c:test.txt zugreifen können
neuer FileStream(@"c:test.txt",
FileMode.Open,
FileAccess.Read).Close();
}
Catch (SecurityException x) {
// Zugriff verweigert
accessGranted = false;
}
fangen (...) {
// Etwas anderes ist passiert
}
Auch wenn wir die CLR verwenden, dürfen wir weiterhin auf die Datei zugreifen. In diesem Fall wird keine SecurityException ausgelöst. Was aber, wenn beispielsweise die Discretionary Access Control List (DACL) der Datei uns keinen Zugriff gewährt? Zu diesem Zeitpunkt wird eine andere Art von Ausnahme ausgelöst. Aufgrund der optimistischen Annahmen in der ersten Codezeile werden wir dies jedoch nie erfahren.
Eine bessere Möglichkeit, diesen Code zu schreiben, besteht darin, vorsichtig zu sein:
bool accessGranted = false; // Seien Sie vorsichtig!
versuchen {
// Sehen Sie, ob wir auf c:test.txt zugreifen können
neuer FileStream(@"c:test.txt",
FileMode.Open,
FileAccess.Read).Close();
// Wenn wir noch hier sind, großartig!
accessGranted = true;
}
Catch (...) {}
Dies wird stabiler sein, denn egal, wie wir scheitern, wir werden immer auf den sichersten Modus zurückgreifen.
9. Identitätswechsel ist äußerst anfällig
Beim Schreiben von Serveranwendungen werden Sie häufig direkt oder indirekt eine praktische Funktion von Windows nutzen, die als Identitätswechsel bezeichnet wird. Durch Identitätswechsel kann jeder Thread in einem Prozess in einer anderen Sicherheitsumgebung ausgeführt werden, typischerweise der des Clients. Wenn beispielsweise ein Dateisystem-Redirector eine Dateianforderung über das Netzwerk empfängt, authentifiziert er den Remote-Client, prüft, ob die Anforderung des Clients nicht gegen die DACL auf der Freigabe verstößt, und hängt dann das Flag des Clients an den Thread an, der die Anforderung verarbeitet . um den Client zu simulieren. Dieser Thread kann dann über die Sicherheitsumgebung des Clients auf das lokale Dateisystem auf dem Server zugreifen. Dies ist praktisch, da das lokale Dateisystem bereits sicher ist. Es führt eine Zugriffsprüfung durch, die den angeforderten Zugriffstyp, die DACL für die Datei und das Identitätswechsel-Flag für den Thread berücksichtigt. Wenn die Zugriffsprüfung fehlschlägt, meldet das lokale Dateisystem dies an den Dateisystem-Redirector, der dann einen Fehler an den Remote-Client sendet. Dies ist zweifellos praktisch für den Dateisystem-Redirector, da er die Anfrage einfach an das lokale Dateisystem weiterleitet und es seine eigenen Zugriffsprüfungen durchführen lässt, so als ob der Client lokal wäre.
Das ist alles schön und gut für ein einfaches Gateway wie einen Datei-Redirector. Aber auch in anderen, komplexeren Anwendungen kommen Simulationen häufig zum Einsatz. Nehmen Sie als Beispiel eine Webanwendung. Wenn Sie ein klassisches nicht verwaltetes ASP-Programm, eine ISAPI-Erweiterung oder eine ASP.NET-Anwendung schreiben und die folgende Spezifikation in der Web.config-Datei
<identity impersonate='true'>
haben, verfügt Ihre laufende Umgebung über zwei verschiedene Sicherheitsumgebungen: Sie haben eine Prozess-Tag und ein Thread-Tag Im Allgemeinen wird das Thread-Tag zur Zugriffsprüfung verwendet (siehe Abbildung 3). Angenommen, Sie schreiben eine ISAPI-Anwendung, die in einem Webserverprozess ausgeführt wird, und gehen davon aus, dass die meisten Anforderungen nicht authentifiziert sind, lautet Ihr Thread-Tag möglicherweise IUSR_MACHINE, Ihr Prozess-Tag jedoch SYSTEM! Angenommen, Ihr Code kann durch einen Pufferüberlauf von einem Angreifer ausgenutzt werden. Glauben Sie, dass es damit zufrieden sein wird, einfach als IUSR_MACHINE zu laufen? Natürlich nicht. Sein Angriffscode ruft höchstwahrscheinlich RevertToSelf dazu auf, das Identitätswechsel-Flag zu entfernen, in der Hoffnung, seine Berechtigungsstufe zu erhöhen. In diesem Fall wird ihm das leicht gelingen. Er kann auch CreateProcess aufrufen. Das Tag des neuen Prozesses wird nicht vom Impersonation-Tag, sondern vom Prozess-Tag kopiert, sodass der neue Prozess als SYSTEM ausgeführt werden kann.
Wie kann man dieses kleine Problem lösen? Denken Sie nicht nur daran, sicherzustellen, dass es überhaupt nicht zu Pufferüberläufen kommt, sondern auch an das Prinzip der geringsten Autorisierung. Wenn Ihr Code keine so hohen Berechtigungen wie SYSTEM erfordert, konfigurieren Sie Ihre Webanwendung nicht für die Ausführung in einem Webserverprozess. Wenn Sie Ihre Webanwendung einfach so konfigurieren, dass sie in einer Umgebung mit mittlerer oder hoher Isolation ausgeführt wird, lautet Ihr Prozess-Tag IWAM_MACHINE. Sie haben eigentlich keine Berechtigungen, daher hat dieser Angriff fast keine Auswirkungen. Beachten Sie, dass in IIS 6.0 (bald eine Komponente von Windows .NET Server) vom Benutzer geschriebener Code standardmäßig nicht als SYSTEM ausgeführt wird. Basierend auf der Erkenntnis, dass Entwickler Fehler machen, ist jede Hilfe, die der Webserver bei der Reduzierung der dem Code erteilten Berechtigungen leisten kann, hilfreich, falls ein Sicherheitsproblem im Code vorliegt.
Hier ist eine weitere Gefahr, auf die COM-Programmierer stoßen können. COM neigt stark dazu, Threads zu ignorieren. Wenn Sie einen In-Process-COM-Server aufrufen und sein Thread-Modell nicht mit dem Modell des aufrufenden Threads übereinstimmt, führt COM den Aufruf in einem anderen Thread aus. COM gibt das Identitätswechselflag nicht an den aufrufenden Thread weiter, sodass der Aufruf im Sicherheitskontext des Prozesses und nicht im Sicherheitskontext des aufrufenden Threads ausgeführt wird. Was für eine Überraschung!
Hier ist ein weiteres Beispiel für die Fallstricke der Simulation. Gehen Sie davon aus, dass Ihr Server Anfragen akzeptiert, die über Named Pipes, DCOM oder RPC gesendet werden. Sie authentifizieren Clients und geben sich als sie aus, indem Sie durch Identitätswechsel Kernelobjekte in ihrem Namen öffnen. Und Sie haben vergessen, eines der Objekte (z. B. eine Datei) zu schließen, als die Verbindung zum Client getrennt wurde. Wenn der nächste Kunde hereinkommt, authentifizieren Sie sich, geben sich als dieser aus und raten Sie mal, was passiert? Sie können weiterhin auf Dateien zugreifen, die vom vorherigen Client „übersehen“ wurden, auch wenn der neue Client keinen Zugriff auf die Datei erhalten hat. Aus Leistungsgründen führt der Kernel nur beim ersten Öffnen eines Objekts Zugriffsprüfungen durch. Sie können weiterhin auf diese Datei zugreifen, auch wenn Sie später Ihre Sicherheitsumgebung ändern, weil Sie sich als ein anderer Benutzer ausgeben.
Die oben genannten Situationen sollen Sie alle daran erinnern, dass die Simulation den Serverentwicklern Komfort bietet, dieser Komfort jedoch große versteckte Gefahren birgt. Wenn Sie ein Programm mit einem Mock-Flag ausführen, achten Sie sorgfältig auf Ihren Code.
10. Schreiben Sie Anwendungen, die Benutzer ohne Administratorrechte tatsächlich verwenden können.
Dies ist eigentlich eine Folge des Prinzips der geringsten Autorisierung. Wenn Programmierer weiterhin Code entwickeln, für dessen ordnungsgemäße Ausführung unter Windows ein Administrator erforderlich ist, können wir nicht mit einer Verbesserung der Systemsicherheit rechnen. Windows verfügt über sehr solide Sicherheitsfunktionen, die Benutzer jedoch nicht nutzen können, wenn sie für die Bedienung als Administrator erforderlich sind.
Wie können Sie sich verbessern? Probieren Sie es zunächst selbst aus, ohne es als Administrator auszuführen. Sie werden schnell merken, wie schmerzhaft es ist, ein Programm zu verwenden, das nicht auf Sicherheit ausgelegt ist. Eines Tages installierte ich (Keith) eine vom Hersteller meines Handheld-Geräts bereitgestellte Software, die Daten zwischen meinem Desktop-Computer und meinem Handheld-Gerät synchronisierte. Wie üblich habe ich mich von meinem normalen Benutzerkonto abgemeldet, mich erneut mit dem integrierten Administratorkonto angemeldet, die Software installiert, mich dann erneut bei meinem normalen Konto angemeldet und versucht, die Software auszuführen. Infolgedessen öffnet die Anwendung ein Dialogfeld mit der Meldung, dass auf eine erforderliche Datendatei nicht zugegriffen werden kann, und gibt dann eine Meldung über eine Zugriffsverletzung aus. Freunde, das ist das Softwareprodukt eines großen Herstellers von Handheld-Geräten. Gibt es eine Entschuldigung für diesen Fehler?
Nachdem ich FILEMON von http://sysinternals.com (auf Englisch) ausgeführt hatte, stellte ich schnell fest, dass die Anwendung versuchte, eine Datendatei für den Schreibzugriff zu öffnen, die im selben Verzeichnis wie die ausführbare Datei der Anwendung installiert war. Wenn Anwendungen wie erwartet im Verzeichnis „Programme“ installiert werden, dürfen sie nicht versuchen, Daten in dieses Verzeichnis zu schreiben. Program Files verfügt nicht ohne Grund über eine derart restriktive Zugriffskontrollrichtlinie. Wir möchten nicht, dass Benutzer in diese Verzeichnisse schreiben, da dies es einem Benutzer leicht machen würde, einen Trojaner zu hinterlassen, damit ein anderer Benutzer ihn ausführen kann. Tatsächlich ist diese Konvention eine der grundlegenden Signaturanforderungen von Windows XP (siehe http://www.microsoft.com/winlogo [Englisch]).
Wir hören zu viele Programmierer, die sich entschuldigen, warum sie bei der Codeentwicklung als Administrator arbeiten. Wenn wir dieses Problem weiterhin ignorieren, machen wir die Sache nur noch schlimmer. Freunde, Sie benötigen keine Administratorrechte, um eine Textdatei zu bearbeiten. Auch zum Bearbeiten oder Debuggen eines Programms sind keine Administratorrechte erforderlich. Wenn Sie Administratorrechte benötigen, verwenden Sie die RunAs-Funktion des Betriebssystems, um 玎com.asp?TARGET=/winlogo/">http://www.microsoft.com/winlogo [Englisch]) auszuführen.
Wir hören das zu oft. Programmierer geben Ausreden Warum sie sich für die Ausführung als Administrator entscheiden, wenn wir dieses Problem weiterhin ignorieren. Für die Bearbeitung einer Textdatei sind keine Administratorrechte erforderlich Verwenden Sie die RunAs-Funktion des Betriebssystems, um ein separates Programm mit erhöhten Rechten auszuführen (siehe Spalte „Sicherheitsbriefe“ vom November 2001). Wenn Sie Tools für Entwickler schreiben, müssen Sie eine zusätzliche Verantwortung für diese Gruppe übernehmen Stoppen Sie diesen Teufelskreis des Schreibens von Code, der nur als Administrator ausgeführt werden kann.
Weitere Informationen dazu, wie Entwickler problemlos als Nicht-Administrator ausgeführt werden können, finden Sie auf der Website von Keith unter http://www.develop. com/kbrown (auf Englisch). Schauen Sie sich Michaels Buch Writing Secure Code (Microsoft Press, 2001) an, das Tipps zum Schreiben von Anwendungen gibt, die in einer Nicht-Administrator-Umgebung gut laufen.