1. Was zuerst zu verstehen ist
Wenn man Windows-Anwendungen oder Windows-Dienste entwickelt, gibt es Situationen, in denen man „nur diesen einen Vorgang als anderer Benutzer ausführen“ möchte.
Zum Beispiel in folgenden Fällen:
- Ein Windows-Dienst soll mit den Rechten des jeweiligen Benutzers auf einen Dateiserver zugreifen
- In einer Verwaltungsanwendung soll geprüft werden, was ein bestimmter Benutzer sehen kann
- Bei Named Pipes, RPC, COM, IIS oder ASP.NET Core soll ein Teil der Verarbeitung mit den Rechten des aufrufenden Benutzers erfolgen
- Aus Gründen bestehender Altbestände soll das verwendete Windows-Konto pro Verarbeitungseinheit gewechselt werden
Hier kommen Identitätswechsel (Impersonation), Zugriffstoken und Identitätswechseltoken ins Spiel.
Zunächst aber eine Sache, die vorab betont werden soll.
Ein Identitätswechsel unter Windows ist keine „Verwandlung in einen Administrator“. Es ist ein Mechanismus, der den bei Zugriffsprüfungen verwendeten Sicherheitskontext hauptsächlich pro Thread umschaltet.
Wer diesen Unterschied bei der Implementierung nicht beachtet, stößt auf Probleme wie diese:
- Man glaubt, einen Identitätswechsel vollzogen zu haben, doch der Dateizugriff endet mit
Access denied - Lokale Dateien lassen sich lesen, aber nur der Zugriff auf die Netzwerkfreigabe schlägt fehl
- Mitten in
Task.Runoderasyncist man unbemerkt wieder der ursprüngliche Benutzer - Protokollausgaben und nachfolgende Verarbeitung laufen weiterhin unter dem Identitätswechsel, sodass die Rechtegrenze verschwimmt
- Primäres Token und Identitätswechseltoken werden verwechselt, wodurch der Prozessstart fehlschlägt
- Man hängt bei der Frage fest: „Der Benutzer ist doch in der Gruppe Administrators – warum kann er nicht schreiben?“
Dieser Artikel ordnet die Denkweise, mit der Windows-Identitätswechseltoken in der Praxis sicher gehandhabt werden.
Es geht nicht um Angriffstechniken oder Rechteausweitung. Es geht darum, in Windows-Anwendungen, Windows-Diensten und .NET-Anwendungen Rechtegrenzen korrekt zu behandeln.
Der in diesem Artikel gezeigte Code ist als vollständiges, buildbares Beispielpaket (Bibliothek, unter Windows ausführbare Demo, Unit-Tests für Argumentprüfung und Guard-Verhalten) auf GitHub veröffentlicht.
windows-impersonation-token - komurasoft-blog-samples (GitHub)
2. Was ist ein Zugriffstoken?
Unter Windows wird ein Zugriffstoken verwendet, um den Sicherheitskontext eines Benutzers oder Prozesses darzustellen.
Ein Zugriffstoken enthält grob die folgenden Informationen. Da diese Begriffe in den folgenden Kapiteln immer wieder auftauchen, ist hier jede Zeile mit ihrer Bedeutung versehen.
| Element | Bedeutung |
|---|---|
| SID des Benutzers | SID steht für Security Identifier und ist ein eindeutiger Bezeichner für Benutzer und Gruppen. Die Zugriffsprüfung erfolgt nicht anhand des Anzeigenamens, sondern anhand dieses Werts |
| Gruppenzugehörigkeit | Die Menge der SIDs der Gruppen, denen der Benutzer angehört. Auch ob er zu Administrators gehört, zeigt sich hier |
| Rechte (Privileges) | Sonderrechte wie „Sicherung durchführen“ oder „Herunterfahren“, die getrennt von der ACL einzelner Objekte vergeben werden. Sie besitzen einen Status aktiviert/deaktiviert |
| Standardbesitzer | Die SID, die Besitzer neu mit diesem Token erstellter Objekte wird |
| Standard-DACL | DACL steht für Discretionary Access Control List, eine Liste, die auflistet, wem was erlaubt wird. Sie wird neu erstellten Objekten standardmäßig zugewiesen |
| Eingeschränkte SIDs | Eine Liste von SIDs, die bei eingeschränkten Token verwendet wird. Auch gegen diese Liste wird eine zusätzliche Zugriffsprüfung durchgeführt, sodass ein Zugriff trotz Gruppenzugehörigkeit verweigert werden kann |
| Integritätsstufe | Eine Hierarchie mit Stufen wie Low, Medium und High. Von einer niedrigen Integritätsstufe aus kann nicht in Objekte einer höheren Stufe geschrieben werden. Wird in Kapitel 17 behandelt |
| Erhöhungsstatus | In einer Umgebung mit aktiviertem UAC, ob dieses Token bereits erhöht (elevated) ist. Wird in Kapitel 17 behandelt |
| Identitätswechselebene | Ein Wert, den nur Identitätswechseltoken besitzen. Er legt fest, ob nur eine Identifizierung möglich ist, ob tatsächlich zugegriffen werden kann oder ob an ein entferntes System delegiert werden kann. Wird in Kapitel 7 behandelt |
| Tokentyp | Die Unterscheidung zwischen primärem Token und Identitätswechseltoken. Wird in Kapitel 6 behandelt |
Wichtig ist hier, dass bei der Zugriffsprüfung nicht der Name zählt, sondern SID, Gruppenzugehörigkeit und Integritätsstufe – dieser Punkt macht es in späteren Kapiteln leichter zu verstehen, warum trotz korrektem Namen ein Access denied auftreten kann.
Dateien, die Registrierung, Dienste, Named Pipes, Prozesse, Threads, Ereignisse, Mutexe und viele weitere Windows-Objekte besitzen einen Sicherheitsdeskriptor.
Wenn ein Thread versucht, ein geschütztes Objekt zu öffnen, gleicht Windows die Informationen des Tokens mit der ACL des Zielobjekts ab.
Zugreifender Thread
↓
In welchem Sicherheitskontext wird zugegriffen?
↓
Benutzer, Gruppen und Rechte des Tokens ansehen
↓
Mit der ACL des Zielobjekts abgleichen
↓
Erlauben / Verweigern entscheiden
Zu verstehen, „in welchem Sicherheitskontext zugegriffen wird“, ist der Einstiegspunkt zum Verständnis von Identitätswechseltoken.
3. Ein Prozess besitzt ein primäres Token
Jeder Windows-Prozess besitzt normalerweise ein primäres Zugriffstoken.
Startet zum Beispiel ein Benutzer eine Anwendung vom Desktop aus, erhält dieser Prozess ein primäres Token, das den Sicherheitskontext dieses Benutzers repräsentiert.
Bei einem Windows-Dienst erhält der Prozess das primäre Token des Ausführungskontos des Dienstes.
Zum Beispiel so:
MyService.exe
Primary Token: DOMAIN\svc-app
Wenn ein Thread innerhalb dieses Dienstes keinen Identitätswechsel vollzieht, wird beim Zugriff auf Dateien oder die Registrierung das primäre Token des Prozesses verwendet.
Standardmäßig gilt also:
Thread A
Impersonation Token: keines
↓
Bei der Zugriffsprüfung wird das Primary Token des Process verwendet
Öffnet man in diesem Zustand C:\Data\foo.txt, wird geprüft, ob DOMAIN\svc-app über Zugriffsrechte verfügt.
4. Das Identitätswechseltoken hängt am Thread
Beginnt ein Identitätswechsel, wird dem Thread ein Identitätswechseltoken zugewiesen. Das ist der entscheidende Punkt: Ein Identitätswechsel bedeutet grundsätzlich nicht, dass „der gesamte Prozess zu einem anderen Benutzer wird“ – treffender ist die Vorstellung, dass dieser eine Thread Zugriffsprüfungen in einem anderen Sicherheitskontext durchläuft.
MyService.exe
Primary Token: DOMAIN\svc-app
Thread A
Impersonation Token: DOMAIN\alice
Thread B
Impersonation Token: keines
Öffnet Thread A in diesem Zustand eine Datei, wird die Zugriffsprüfung mit den Rechten von DOMAIN\alice durchgeführt.
Thread B hingegen wechselt die Identität nicht und wird daher mit den Rechten von DOMAIN\svc-app geprüft.
Wer diesen Unterschied nicht versteht, gerät in Verwirrung wie hier:
// Vermeintlicher Identitätswechsel auf Thread A
StartImpersonation(token);
// Die Verarbeitung wird jedoch an einen anderen Thread übergeben
Task.Run(() =>
{
File.ReadAllText(path);
});
// Und sofort wird zurückgesetzt
RevertToSelf();
In diesem Fall läuft der Thread, der die Datei tatsächlich liest, nicht notwendigerweise im erwarteten Identitätswechsel-Zustand.
Ein Identitätswechsel muss mit klarem Blick auf sein Verhältnis zu Scope, Thread und asynchroner Verarbeitung behandelt werden.
5. „Identitätswechsel“ ist keine Rechteausweitung
Das Wort „Identitätswechsel“ klingt zwar etwas stark, aber in der Praxis kommt es darauf an, es nicht mit „Rechteausweitung“ zu verwechseln.
Was mit einem Identitätswechsel grundsätzlich möglich ist, lässt sich so zusammenfassen:
Mit den Rechten des Serverprozesses verarbeiten
↓
Nur für einen Teil der Verarbeitung die Zugriffsprüfung mit den Rechten des Client-Benutzers durchführen
Möchte man zum Beispiel die ACL eines Dateiservers unmittelbar als Rechtekontrolle nutzen, aber die Serveranwendung liest Dateien stets über das Dienstkonto, lassen sich die benutzerspezifischen ACLs nicht abbilden.
Deshalb wechselt man nur für einen Teil der Anfragenverarbeitung die Identität zum aufrufenden Benutzer und führt darüber den Dateizugriff aus.
HTTP- / RPC- / Named-Pipe-Anfrage
Benutzer: DOMAIN\alice
↓
Serveranwendung
Prozess: DOMAIN\svc-app
↓
Nur für den Dateizugriffsteil Identitätswechsel zu DOMAIN\alice
↓
Erlauben / Verweigern durch die ACL des Dateiservers
Das ist hilfreich, wenn man nicht eine eigene Rechteprüfung der Anwendung, sondern die bestehenden ACLs von Windows nutzen möchte.
Allerdings ist ein Identitätswechsel zwar praktisch, macht aber bei fehlerhaftem Design die Rechtegrenzen unklar.
- Welche Verarbeitung läuft unter welcher Identität?
- Wo beginnt der Identitätswechsel?
- Wo wird zuverlässig zurückgesetzt?
- Mit welchen Benutzerrechten werden welche Protokollzeilen geschrieben?
- Wird auch bei einer Ausnahme zurückgesetzt?
- Bleibt der Identitätswechsel bis zum Ende der asynchronen Verarbeitung gültig?
All das im Code klar zu machen, ist entscheidend.
6. Primäres Token und Identitätswechseltoken getrennt betrachten
Bei Windows-Token wird besonders häufig das primäre Token mit dem Identitätswechseltoken verwechselt.
Grob betrachtet gilt:
| Token | Hauptzweck | Typische Beispiele |
|---|---|---|
| Primäres Token | Repräsentiert den Sicherheitskontext eines Prozesses | Prozessstart, CreateProcessAsUser |
| Identitätswechseltoken | Ermöglicht einem Thread, in einem anderen Sicherheitskontext zu laufen | ImpersonateLoggedOnUser, SetThreadToken, Identitätswechsel des Clients bei Named Pipes |
Besonders wichtig: Wer einen Prozess starten möchte, benötigt grundsätzlich ein primäres Token.
Der Besitz eines Identitätswechseltokens bedeutet nicht automatisch, dass es sich unmittelbar für den Start eines Prozesses als anderer Benutzer verwenden lässt.
Typischerweise sieht der Ablauf so aus:
Identitätswechsel zum Client
↓
Identitätswechseltoken mit OpenThreadToken abrufen
↓
Mit DuplicateTokenEx ein primäres Token erstellen
↓
An CreateProcessAsUser o. Ä. übergeben
Umgekehrt: Möchte man „auf diesem Thread nur den Dateizugriff als anderer Benutzer durchführen“, ist das eine Frage des Identitätswechseltokens, nicht des Prozessstarts.
Vermischt man beides, gerät man trotz scheinbar korrekter API-Argumente bei Fehlern wie Access denied oder The parameter is incorrect ins Grübeln.
7. Identitätswechselebenen verstehen
Identitätswechseltoken besitzen eine Identitätswechselebene.
Es gibt vier typische Stufen:
| Identitätswechselebene | Grobe Bedeutung |
|---|---|
| Anonymous | Der Server kann die Identifizierungsinformationen des Clients nicht erhalten |
| Identification | Der Server kann den Client identifizieren, aber diese Identität nicht nutzen, um mit deren Rechten auf Objekte zuzugreifen |
| Impersonation | Der Server kann auf dem lokalen System mit den Rechten des Clients agieren |
| Delegation | Der Server kann die Rechte des Clients auch an entfernte Systeme delegieren |
In der Praxis kommt es am häufigsten zu Verwirrung beim Unterschied zwischen Identification und Impersonation.
Identification ist, wie der Name sagt, eine Stufe, um zu erfahren, wer der Gegenüber ist.
Für einen Anwendungsfall wie das Öffnen einer Datei mit den Rechten dieses Benutzers reicht sie nicht aus.
Deshalb kommt es zu Folgendem:
WindowsIdentity.GetCurrent().Name sieht wie der erwartete Benutzername aus
↓
Doch der Dateizugriff endet mit Access denied
In diesem Fall muss nicht nur der Name, sondern auch die Identitätswechselebene geprüft werden.
In .NET liefert WindowsIdentity.ImpersonationLevel einen Anhaltspunkt.
using System.Security.Principal;
WindowsIdentity identity = WindowsIdentity.GetCurrent();
Console.WriteLine(identity.Name);
Console.WriteLine(identity.ImpersonationLevel);
Bei Zugriffen über das Netzwerk ist noch mehr Vorsicht geboten.
Bei Konstellationen wie „ein Webserver wechselt zur Identität eines Benutzers und greift als dieser Benutzer auf einen weiteren Dateiserver oder DB-Server zu“ kann das sogenannte Double-Hop-Problem auftreten.
In diesem Fall löst ein Identitätswechsel im Anwendungscode allein das Problem nicht zwangsläufig. Das Design muss Kerberos, SPN, Delegation, eingeschränkte Delegation, Dienstkonten und das Authentifizierungsverfahren der Gegenstelle einbeziehen.
8. Die Grundform des Identitätswechsels
Die konzeptionelle Form, wenn man den Identitätswechsel über die Win32-API handhabt, sieht so aus:
1. Das für den Identitätswechsel zu verwendende Token beschaffen
2. Den aktuellen Thread mit diesem Token in den Identitätswechsel versetzen
3. Nur die notwendige Verarbeitung ausführen
4. Zwingend in den ursprünglichen Sicherheitskontext zurückkehren
5. Das Token-Handle schließen
Im Code wird dafür immer try / finally verwendet.
if (!ImpersonateLoggedOnUser(tokenHandle))
{
throw new Win32Exception(Marshal.GetLastWin32Error());
}
try
{
// Nur dieser Teil läuft als der identitätsgewechselte Benutzer
DoWorkAsImpersonatedUser();
}
finally
{
if (!RevertToSelf())
{
// Ein Zustand, in dem nicht zurückgesetzt werden kann, ist gefährlich – die Verarbeitung darf mindestens nicht fortgesetzt werden
throw new Win32Exception(Marshal.GetLastWin32Error());
}
}
Zeitlich betrachtet sieht das so aus:
sequenceDiagram
participant App as Aufrufer
participant Th as Verarbeitungs-Thread
participant Win as Windows
participant FS as Datei
App->>Th: Verarbeitung beauftragen
Th->>Win: Mit ImpersonateLoggedOnUser das Token anhängen
Note over Th: Ab hier wird als Benutzer alice<br/>die Zugriffsprüfung durchgeführt
Th->>FS: Datei öffnen
FS-->>Th: Die ACL wird mit den Rechten von alice geprüft
Th->>Win: Mit RevertToSelf das Token entfernen
Note over Th: Ab hier wieder<br/>Dienstkonto svc-app
Th-->>App: Ergebnis zurückgeben
Der Identitätswechsel wirkt nur in dem Bereich zwischen ImpersonateLoggedOnUser und RevertToSelf. I/O, das außerhalb dieses Bereichs erfolgt, wird nicht mit dem identitätsgewechselten Benutzer, sondern mit dem Konto des Prozesses geprüft. Die in Kapitel 14 behandelte Falle bei asynchroner Verarbeitung lässt sich so leichter verstehen: Das eigentliche I/O tritt aus diesem Bereich heraus.
Wichtiger als der Beginn des Identitätswechsels ist das zuverlässige Zurücksetzen. Vergisst man das Zurücksetzen, läuft die nachfolgende Verarbeitung auf diesem Thread weiterhin als der identitätsgewechselte Benutzer.
Besonders in Anwendungen, die einen Thread-Pool verwenden, kann sich das, was als eine einzelne Verarbeitung gedacht war, auf eine andere Anfrage oder eine andere Verarbeitung auswirken.
Deshalb sollte man einen Identitätswechsel nicht als „beginnen, dann zurücksetzen“, sondern als in einen kleinen Scope eingeschlossen betrachten.
9. In .NET WindowsIdentity.RunImpersonated verwenden
In .NET lässt sich mit WindowsIdentity.RunImpersonated, wo möglich, der Identitätswechsel-Scope im Code leicht ausdrücken.
Hält man ein SafeAccessTokenHandle, kann man wie folgt schreiben:
using Microsoft.Win32.SafeHandles;
using System.Security.Principal;
static string ReadFileAsUser(SafeAccessTokenHandle token, string path)
{
return WindowsIdentity.RunImpersonated(token, () =>
{
return File.ReadAllText(path);
});
}
Der Vorteil dieser Form ist, dass der Bereich des Identitätswechsels im Lambda-Ausdruck eingeschlossen ist.
RunImpersonated(token, () =>
{
// Nur hier Identitätswechsel
});
// Ab hier wieder der ursprüngliche Kontext
Möchte man nur bestimmte Vorgänge wie Datei- oder Registrierungszugriff oder den Aufruf einer bestehenden Bibliothek mit Identitätswechsel ausführen, ist diese Form gut lesbar und sicher.
Bei asynchroner Verarbeitung verwendet man RunImpersonatedAsync.
using Microsoft.Win32.SafeHandles;
using System.Security.Principal;
static Task WriteFileAsUserAsync(
SafeAccessTokenHandle token,
string path,
string text,
CancellationToken cancellationToken)
{
return WindowsIdentity.RunImpersonatedAsync(token, async () =>
{
await File.WriteAllTextAsync(path, text, cancellationToken);
});
}
Zu vermeiden ist es, aus dem Identitätswechsel-Scope heraus einen Fire-and-forget-Task zu starten.
// Schlechtes Beispiel
WindowsIdentity.RunImpersonated(token, () =>
{
_ = Task.Run(() =>
{
File.WriteAllText(path, text);
});
});
Bei diesem Code wird unklar, wann und in welchem Ausführungskontext „die eigentlich die Datei schreibende Verarbeitung“ läuft.
Asynchrone Verarbeitung, die mit Identitätswechsel ausgeführt werden soll, sollte innerhalb von RunImpersonatedAsync awaited werden, sodass der Scope erst nach Abschluss der Verarbeitung verlassen wird.
10. Wenn man mit LogonUser ein Token erhält
Eine typische API, um aus den Anmeldeinformationen eines anderen Benutzers ein Token zu erhalten, ist LogonUser – doch diese API ist mit Vorsicht zu behandeln.
LogonUser erhält Benutzername, Domäne und Passwort als Parameter.
Das heißt, die Anwendung selbst handhabt Anmeldeinformationen.
In der Praxis ist hierbei Folgendes zu beachten:
- Das Passwort nicht im Klartext in Code oder Konfigurationsdateien ablegen
- Nach Möglichkeit die Windows-Authentifizierung, Dienstkonten, Delegation oder bestehende Windows-Authentifizierung nutzen
- Geheime Informationen in einem geeigneten Secret Store oder einer Betriebsplattform verwalten
- Token-Handles immer schließen
- In Protokollen außer dem Benutzernamen keine geheimen Informationen ausgeben
- Den Umfang des Identitätswechsels minimieren
Im Folgenden wird ein Minimalbeispiel gezeigt, aber kopieren Sie es bitte nicht einfach so und schreiben Sie das Passwort nicht als String-Literal in password hinein. Im Quellcode abgelegte Anmeldeinformationen bleiben in der Repository-Historie, in Build-Artefakten und im Ergebnis einer Dekompilierung erhalten. Dasselbe gilt für die Klartextablage in einer Konfigurationsdatei. Auf diesen Punkt kommen wir in 21.6 noch einmal zurück.
Wo also legt man das Passwort ab? Hier eine Übersicht der Optionen:
| Speicherort | Einsatzbereich | Zu beachten |
|---|---|---|
| Von vornherein kein Passwort besitzen | Erste Wahl. Mit Windows-Authentifizierung, Dienstkonten oder Delegation auskommen | Muss in der Entwurfsphase entschieden werden. Nachträglich zu entfernen ist aufwendig |
| Zur Laufzeit interaktiv eingeben lassen | Betriebswerkzeuge, die ein Administrator manuell ausführt | Für unbeaufsichtigte Ausführung oder Dienste nicht nutzbar. Die eingelesene Zeichenfolge sollte nach Gebrauch sofort freigegeben werden |
| Windows-Anmeldeinformationsverwaltung | Wiederholte Verwendung mit demselben Konto auf demselben PC | Ein pro Benutzer verwalteter Tresor. Bei Verwendung mit einem Dienstkonto muss die Registrierung mit diesem Konto erfolgen |
| DPAPI | Werte einer Konfigurationsdatei innerhalb eines einzelnen PCs schützen | Auf den Scope von ProtectedData achten. Bei CurrentUser kann nur der verschlüsselnde Benutzer entschlüsseln, bei LocalMachine auch andere Benutzer auf diesem PC |
| Secret-Management-Plattformen wie Key Vault | Bei mehreren Servern, CI oder Cloud-Anbindung | Erfordert eine Authentifizierung der Plattform selbst. Der Umgang mit dem abgerufenen Geheimnis im Arbeitsspeicher muss separat bedacht werden |
Die konkrete Schreibweise bei Verwendung von DPAPI ist in einem separaten Artikel zusammengefasst: Vertrauliche Informationen in Konfigurationsdateien sicher speichern.
Bei allen Methoden gilt gemeinsam: Bevor man eine Speichermethode wählt, sollte man prüfen, ob die Anwendung das Passwort überhaupt erhalten muss.
Hier ein Minimalbeispiel:
using Microsoft.Win32.SafeHandles;
using System.ComponentModel;
using System.Runtime.InteropServices;
using System.Security.Principal;
internal static class NativeMethods
{
private const int LOGON32_LOGON_INTERACTIVE = 2;
private const int LOGON32_PROVIDER_DEFAULT = 0;
[DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
internal static extern bool LogonUser(
string lpszUsername,
string? lpszDomain,
string lpszPassword,
int dwLogonType,
int dwLogonProvider,
out SafeAccessTokenHandle phToken);
public static SafeAccessTokenHandle Logon(
string userName,
string? domain,
string password)
{
bool ok = LogonUser(
userName,
domain,
password,
LOGON32_LOGON_INTERACTIVE,
LOGON32_PROVIDER_DEFAULT,
out SafeAccessTokenHandle token);
if (!ok)
{
throw new Win32Exception(Marshal.GetLastWin32Error());
}
return token;
}
}
public static string ReadFileWithExplicitCredential(
string userName,
string? domain,
string password,
string path)
{
using SafeAccessTokenHandle token = NativeMethods.Logon(userName, domain, password);
return WindowsIdentity.RunImpersonated(token, () =>
{
return File.ReadAllText(path);
});
}
Dieses Beispiel dient allein dazu, die Form der API zu zeigen.
In der Praxis sollte man zuerst prüfen, ob es überhaupt richtig ist, dass die Anwendung so gestaltet ist, dass sie ein Passwort entgegennimmt.
In vielen Fällen ist es sicherer, folgende Alternativen in Betracht zu ziehen:
| Ziel | Alternative |
|---|---|
| Der gesamte Dienst soll auf eine bestimmte Ressource zugreifen | Einem dedizierten Dienstkonto die minimal notwendige ACL zuweisen |
| Mit den Rechten des jeweiligen Benutzers auf eine Datei zugreifen | Windows-Authentifizierung und ein Delegationsdesign verwenden |
| Einen Vorgang durchführen, der Administratorrechte erfordert | Auf Dienstseite eine klare Verwaltungs-API einrichten und über die Autorisierung auf Anwendungsseite steuern |
| Nur einen Teil der Verarbeitung mit einem anderen Konto durchführen | Den Identitätswechsel-Bereich auf eine Methode begrenzen und ein Audit-Protokoll führen |
11. Auf den Anmeldetyp von LogonUser achten
Die Eigenschaften des mit LogonUser erhaltenen Tokens ändern sich je nach Anmeldetyp.
Kopiert man diesen Unterschied, ohne ihn zu verstehen, funktioniert der Code nicht wie erwartet.
| Anmeldetyp | Zu beachten |
|---|---|
| Interactive | Nahe an einer interaktiven Anmeldung. Gut für lokale Vorgänge geeignet, hängt aber von Ausführungsumgebung und Rechten ab |
| Network | Für Netzwerkanmeldungen gedacht. Das zurückgegebene Token lässt sich mitunter nicht direkt für den Prozessstart verwenden |
| NewCredentials | Lokal nahe an den aktuellen Anmeldeinformationen; wird teils verwendet, um bei Remoteverbindungen die angegebenen Anmeldeinformationen zu nutzen |
Hier geht es nicht darum, einen bestimmten Anmeldetyp auswendig zu lernen.
Wichtig sind zwei Punkte:
- Der Anmeldetyp beeinflusst das Verhalten bei lokalem Zugriff, Netzwerkzugriff und Prozessstart
- Es muss geprüft werden, ob das zurückgegebene Token ein primäres Token oder ein Identitätswechseltoken ist
Ein typisches Beispiel für Verwirrung: Ein mit LOGON32_LOGON_NETWORK erhaltenes Token wird unverändert an CreateProcessAsUser übergeben und schlägt fehl.
Ist das Ziel ein Prozessstart, wird ein primäres Token benötigt.
Bei Bedarf wird dieses mit DuplicateTokenEx erzeugt.
12. RevertToSelf nicht auf die leichte Schulter nehmen
Wird der Identitätswechsel über die Win32-API gestartet, beendet man ihn mit RevertToSelf. Dieses Zurücksetzen ist keine bloße Aufräumarbeit, sondern der entscheidende Vorgang, der die Sicherheitsgrenze wiederherstellt.
Ein schlechtes Beispiel:
ImpersonateLoggedOnUser(token);
DoWork();
RevertToSelf();
Auf den ersten Blick scheint nichts falsch zu sein, doch wenn DoWork() eine Ausnahme wirft, wird RevertToSelf() nicht aufgerufen.
Man verwendet zwingend finally.
if (!ImpersonateLoggedOnUser(token))
{
throw new Win32Exception(Marshal.GetLastWin32Error());
}
try
{
DoWork();
}
finally
{
if (!RevertToSelf())
{
throw new Win32Exception(Marshal.GetLastWin32Error());
}
}
Auch wenn man nach einem Fehlschlag von RevertToSelf einfach weitermacht, ist das gefährlich. Setzt man die Verarbeitung fort, obwohl möglicherweise nicht zu den ursprünglichen Rechten zurückgekehrt wurde, läuft die nachfolgende Verarbeitung mit den Rechten eines unbeabsichtigten Benutzers weiter. Mindestens sollte diese Verarbeitungseinheit als fehlgeschlagen behandelt und auf der sicheren Seite geblieben werden.
RunImpersonated / RunImpersonatedAsync in .NET sind gerade als Scope-Ausdruck nützlich, um dieses Vergessen des Zurücksetzens zu vermeiden.
13. Den Identitätswechsel-Scope so klein wie möglich halten
Der wichtigste Entwurfsgrundsatz beim Identitätswechsel ist, nur die notwendige Verarbeitung mit Identitätswechsel auszuführen.
Ein schlechtes Beispiel:
WindowsIdentity.RunImpersonated(token, () =>
{
ValidateRequest();
LoadConfiguration();
WriteDebugLog();
ReadUserFile();
UpdateDatabase();
SendNotification();
});
Wechselt man auf diese Weise für einen so breiten Bereich die Identität, wird unklar, welcher Vorgang mit welchen Rechten ausgeführt wird.
So könnte zum Beispiel der Zugriff auf das Protokollziel mit den Rechten des identitätsgewechselten Benutzers erfolgen und das Schreiben ins Protokoll fehlschlagen. Die DB-Verbindung könnte statt mit dem Dienstkonto mit den Anmeldeinformationen des identitätsgewechselten Benutzers versucht werden. Auch Benachrichtigungsverarbeitung und das Anlegen temporärer Dateien könnten von einem unnötigen Rechtekontext betroffen sein.
Ein gutes Beispiel schneidet nur die Vorgänge heraus, die den Identitätswechsel benötigen:
ValidateRequest();
LoadConfiguration();
string content = WindowsIdentity.RunImpersonated(token, () =>
{
return File.ReadAllText(userFilePath);
});
UpdateDatabase(content);
WriteAuditLog(userName, userFilePath, success: true);
In dieser Form wird klar, dass nur der Teil File.ReadAllText den Identitätswechsel benötigt.
Ein Identitätswechsel ist praktisch, aber je breiter er ausfällt, desto schwerer lesbar und desto anfälliger für Fehler wird der Code.
14. Bei asynchroner Verarbeitung prüfen: „Läuft der Identitätswechsel bis zum Ende?“
In modernen .NET-Anwendungen ist ein großer Teil der Verarbeitung asynchron: Dateien, HTTP, DB, Warteschlangen, Speicherdienste.
Deshalb erfordert die Kombination aus Identitätswechsel und async / await besondere Sorgfalt.
Der Grundsatz lautet nur so:
Asynchrone Verarbeitung, die einen Identitätswechsel benötigt, wird innerhalb von RunImpersonatedAsync awaited
Was passiert, wenn man das nicht beachtet, ist im selben Format wie das Diagramm aus Kapitel 8 gegenübergestellt. Vorher aber ein wichtiger Hinweis: „An einen anderen Thread übergeben“ bedeutet nicht zwangsläufig, dass der Identitätswechsel entfällt. Je nachdem, mit welchem Mechanismus der Identitätswechsel erfolgt ist, ist die Art des Fehlverhaltens genau entgegengesetzt.
| Art des Identitätswechsels | Was mit an Task.Run übergebener Verarbeitung geschieht |
Was daraufhin passiert |
|---|---|---|
.NET-RunImpersonated / RunImpersonatedAsync |
Läuft weiterhin mit Identitätswechsel | Der Identitätswechsel reicht länger als der Scope im Code. Die aufrufende Seite erhält weder den Abschluss noch eine Ausnahme mitgeteilt |
Direkter Aufruf von Win32-ImpersonateLoggedOnUser |
Läuft mit dem Konto des Prozesses | Das tatsächliche I/O verlässt den Identitätswechselbereich und endet mit Access denied |
Dass sich .NET so verhält, liegt daran, dass RunImpersonated das Identitätswechseltoken in einem AsyncLocal ablegt. Der Wert eines AsyncLocal fließt zusammen mit dem ExecutionContext, und Task.Run erfasst den zum Aufrufzeitpunkt gültigen ExecutionContext und stellt ihn auf einem Thread des Pools wieder her. Da bei jeder Wiederherstellung ein Änderungs-Handler ausgeführt wird, der auf diesem Thread erneut ImpersonateLoggedOnUser aufruft, erhält auch der Thread des Pools denselben Identitätswechsel. So verhält sich die Implementierung der Laufzeit (s_currentImpersonatedToken und CurrentImpersonatedTokenChanged von WindowsIdentity).
Ein Win32-Token hingegen ist nur an den Thread gebunden und wird nicht ohne Weiteres auf einen anderen Thread übertragen.
Verfolgt man den zeitlichen Ablauf auf der .NET-Seite, ergibt sich Folgendes:
sequenceDiagram
participant Th as Aufrufender Thread
participant Pool as Anderer Thread des Thread-Pools
participant FS as Datei
Th->>Th: Mit RunImpersonated den Identitätswechsel beginnen
Th->>Pool: Mit Task.Run den Schreibvorgang übergeben
Note over Pool: Zusammen mit dem ExecutionContext<br/>wird auch das Identitätswechseltoken mitgeführt
Th->>Th: Den Scope verlassen und den Identitätswechsel aufheben
Note over Th: Aufgehoben wird nur<br/>der Identitätswechsel dieses Threads
Pool->>FS: Der eigentliche Schreibvorgang läuft erst danach
Note over Pool: Weiterhin als identitätsgewechselter Benutzer.<br/>Die aufrufende Seite weiß nicht, wann dies endet
FS-->>Pool: Weder Ergebnis noch Ausnahme werden von jemandem entgegengenommen
Im Diagramm aus Kapitel 8 lag das I/O innerhalb des Identitätswechselbereichs. In diesem Diagramm wechselt die Verarbeitung mit der Übergabe an Task.Run auf einen anderen Thread, und der eigentliche Schreibvorgang verlässt den im Code sichtbaren Bereich. Nicht das Wegfallen des Identitätswechsels ist gefährlich, sondern dass er nicht wegfällt, sich aber nicht mehr nachverfolgen lässt – das ist das Problem dieser Form. Konkret geschehen drei Dinge gleichzeitig.
- Der Umfang des Identitätswechsels wird unlesbar. Der im Code als „von hier bis hier“ notierte Scope und der tatsächlich mit Identitätswechsel laufende Bereich stimmen nicht mehr überein
- Fehlschläge werden verschluckt. Da niemand die übergebene Task awaitet, tritt bei einem
Access deniednirgendwo eine Ausnahme zutage - Es ist unklar, wann das Token geschlossen werden darf. Schließt man
SafeAccessTokenHandlemitusing, geschieht das unter den Füßen einer noch laufenden Verarbeitung
Wechselt man dagegen direkt über Win32 die Identität, läuft die übergebene Verarbeitung umgekehrt mit dem Konto des Prozesses. In der Testumgebung besitzt oft auch das Dienstkonto die nötigen Rechte, sodass der Vorgang gelingt, und erst in der Produktion tritt zum ersten Mal Access denied auf – ein Muster, das sich so häufig zeigt.
In beiden Fällen tritt das Problem nicht auf, wenn man innerhalb von RunImpersonatedAsync bis zum Abschluss awaitet.
Ein gutes Beispiel:
await WindowsIdentity.RunImpersonatedAsync(token, async () =>
{
await using FileStream stream = File.OpenRead(path);
using var reader = new StreamReader(stream);
string text = await reader.ReadToEndAsync();
await ProcessTextAsync(text);
});
Doch auch bei dieser Form gibt es etwas zu bedenken.
Muss auch ProcessTextAsync mit dem identitätsgewechselten Benutzer laufen?
Reicht es, nur den Dateizugriff mit Identitätswechsel auszuführen, ist folgende Aufteilung sicherer:
string text = await WindowsIdentity.RunImpersonatedAsync(token, async () =>
{
return await File.ReadAllTextAsync(path);
});
await ProcessTextAsync(text);
Nur weil man innerhalb des Identitätswechsel-Scopes awaiten kann, heißt das nicht, dass dort alles hineingehört.
Auch bei asynchroner Verarbeitung sollte der Umfang des Identitätswechsels minimiert werden.
15. ASP.NET Core und Identitätswechsel
Auch bei Verwendung der Windows-Authentifizierung in ASP.NET Core ist beim Umgang mit dem Identitätswechsel Vorsicht geboten.
Es ist gefährlich anzunehmen: „Da die Anmeldung über die Windows-Authentifizierung erfolgt, läuft die gesamte Anfrageverarbeitung als dieser Benutzer.“
Im Allgemeinen läuft der Prozess der Anwendung selbst unter der Anwendungspool-ID oder dem Ausführungskonto des Dienstes. Die Windows-Identität des Benutzers steht als Authentifizierungsinformation zur Verfügung, doch das bedeutet nicht, dass automatisch die gesamte Verarbeitung mit den Rechten des Benutzers erfolgt.
Muss ein bestimmter Vorgang mit den Rechten des Benutzers durchgeführt werden, wird explizit ein Scope mit RunImpersonated / RunImpersonatedAsync erzeugt.
Der Code sieht dann etwa so aus:
app.MapGet("/download", async (HttpContext context) =>
{
if (context.User.Identity is not WindowsIdentity user)
{
return Results.Unauthorized();
}
string path = GetPathFromRequest(context);
byte[] bytes = await WindowsIdentity.RunImpersonatedAsync(
user.AccessToken,
async () => await File.ReadAllBytesAsync(path));
return Results.File(bytes, "application/octet-stream");
});
Auch in diesem Beispiel wird nur der Teil des Dateilesens mit Identitätswechsel ausgeführt.
Ob Antwortgenerierung, Protokollierung und die anwendungsseitige Autorisierungsprüfung vollständig in den Identitätswechsel-Scope gehören, sollte sorgfältig überlegt werden.
16. Wie man Access denied betrachtet
Bei Implementierungen mit Identitätswechsel ist Access denied der am häufigsten auftretende Fehler.
Tritt dieser Fehler auf, führt es zu Umwegen, wenn man nur denkt: „Der Identitätswechsel ist fehlgeschlagen.“
Die zu prüfenden Aspekte lassen sich unterteilen:
| Aspekt | Zu prüfender Inhalt |
|---|---|
| Findet der Identitätswechsel wirklich statt? | Innerhalb des Identitätswechsel-Scopes WindowsIdentity.GetCurrent().Name prüfen |
| Reicht die Identitätswechselebene aus? | Liegt nicht nur Identification, sondern die benötigte Ebene vor? |
| Ist die ACL der Zielressource korrekt? | Besitzt der identitätsgewechselte Benutzer Lese-/Schreibrechte? |
| Lokal oder entfernt? | Gelingt der Zugriff bei lokalen Dateien und scheitert nur bei UNC-Pfaden? |
| Handelt es sich um Double-Hop? | Wird versucht, vom Webserver mit den Rechten des Benutzers auf einen Dateiserver zuzugreifen? |
| Wurde der Identitätswechsel-Scope verlassen? | Läuft das eigentliche I/O außerhalb des Scopes oder in einer anderen Task? |
| Stimmt der Tokentyp? | Wird ein Identitätswechseltoken an einen Prozessstart übergeben? |
| Gibt es Einflüsse von UAC / Integritätsstufe? | Liegt trotz Zugehörigkeit zu Administrators ein nicht erhöhtes Token vor? |
Vor allem sollte man sich nicht allein deshalb sicher fühlen, weil der Name stimmt.
WindowsIdentity identity = WindowsIdentity.GetCurrent();
Console.WriteLine(identity.Name);
Diese Protokollzeile ist nützlich, aber nicht ausreichend.
Mindestens sollte man auch Folgendes betrachten:
Console.WriteLine(identity.ImpersonationLevel);
Console.WriteLine(identity.IsAuthenticated);
Handelt es sich beim Ziel um eine Netzwerkfreigabe, sollte man nicht nur den Anwendungscode prüfen, sondern auch Authentifizierungsverfahren, Delegationseinstellungen, SPNs, Dienstkonten und die ACL auf dem Dateiserver.
17. UAC und das Problem „Administrator, und trotzdem schlägt es fehl“
Unter Windows ist die Zugehörigkeit eines Benutzers zur Gruppe Administrators nicht dasselbe wie ein bereits erhöhtes (elevated) aktuelles Token.
In Umgebungen mit aktiviertem UAC laufen Prozesse selbst für Administratorbenutzer normalerweise mit einem eingeschränkten Token, und Vorgänge, die Administratorrechte erfordern, benötigen eine Erhöhung (Elevation).
Deshalb kommt es zu Folgendem:
Der Benutzer gehört zu Administrators
↓
Doch das aktuelle Token ist nicht erhöht
↓
Beim Schreiben nach Program Files oder HKLM: Access denied
Beim Identitätswechsel verhält es sich genauso.
Statt anzunehmen „der identitätsgewechselte Benutzer ist Administrator, also müsste das Schreiben klappen“, muss geprüft werden, in welchem Zustand sich das tatsächlich übergebene Token befindet.
Beim Debuggen betrachtet man unter anderem:
- Gruppenzugehörigkeit
- Aktivierung / Deaktivierung von Privileges
- Integritätsstufe
- Erhöhungsstatus
- Ob es sich um ein eingeschränktes Token handelt
- Ob ein verknüpftes erhöhtes Token existiert
Über die Win32-API lässt sich mit GetTokenInformation prüfen, welche Werte TokenType, TokenImpersonationLevel, TokenElevationType und TokenIntegrityLevel unter anderem annehmen.
Als praktische Entwurfsentscheidung ist es jedoch sicherer, notwendige minimale Vorgänge in einen dedizierten Dienst oder eine Verwaltungs-API einzuschließen, statt „einen Administratorbenutzer zu impersonieren und alles zu erlauben“.
18. Netzwerkfreigaben und Double-Hop
Eine sehr häufige Anfrage rund um den Identitätswechsel betrifft den Zugriff auf Netzwerkfreigaben.
Client-PC
↓ Windows-Authentifizierung
Web- / API-Server
↓ Möchte mit Identitätswechsel zugreifen
Dateiserver
In dieser Konstellation kann es passieren, dass „auf dem Webserver der Benutzername ermittelt werden kann, der Zugriff auf den Dateiserver jedoch fehlschlägt“.
Dies ist die Frage, ob die Anmeldeinformationen des Benutzers an einen weiteren Server weiterdelegiert werden können.
Der Identitätswechsel auf dem lokalen Server und die Delegation an einen anderen Server sind nicht dasselbe.
Eine Ebene Impersonation mag zwar für lokale Vorgänge ausreichen, für das Auftreten als Client gegenüber einem entfernten Server jedoch nicht genügen.
Möchte man über das Netzwerk hinweg die Rechte des jeweiligen Benutzers nutzen, ist ein Design erforderlich, das Kerberos-Delegation, eingeschränkte Delegation, SPN, Dienstkonten und das Authentifizierungsverfahren umfasst.
Da man an dieser Stelle leicht in eine Sackgasse gerät, werden hier die Handlungsoptionen für den nächsten Schritt aufgelistet.
| Richtung | Was zu tun ist | Geeignet für | Zu beachten |
|---|---|---|---|
| Kerberos-Constrained-Delegation | Dem Konto des vermittelnden Servers die Einstellung „Delegation nur an diesen Dienst erlaubt“ hinzufügen | Vermittelnder Server und Dateiserver befinden sich in derselben Domäne, und die Mitarbeit des Domänenadministrators ist verfügbar | Die Konfiguration erfolgt nicht auf Anwendungs-, sondern auf Domänenseite. Die SPNs der Delegationsziele werden explizit aufgelistet |
| Ressourcenbasierte Constrained-Delegation | Die Einstellung, die die Delegation erlaubt, dem Konto der delegierten Seite geben, also dem Konto des Dateiservers | Domänenübergreifende Fälle oder wenn der Administrator der Ressourcenseite die Führung übernehmen soll | Ein Mechanismus ab Windows Server 2012. Der Ort der Konfiguration ist gegenüber der klassischen Constrained-Delegation vertauscht |
| Anmeldeinformationen explizit übergeben | Statt des jeweiligen Benutzers mit den Anmeldeinformationen eines dedizierten, zweckgebundenen Kontos verbinden | Wenn Delegation nicht konfiguriert werden kann oder „es der jeweilige Benutzer sein muss“ keine geschäftliche Anforderung ist | Die Verwahrung der Anmeldeinformationen wird erforderlich. Siehe die Tabelle in Kapitel 10 |
| Design ohne Vermittlung | Den Zugriff auf den Dateiserver auf ein Dienstkonto bündeln und die Autorisierung anwendungsseitig durchführen | Wenn die Geschäftsregeln auf Anwendungsseite liegen | Die ACL ist dann nicht mehr die endgültige Entscheidung. Das Design des Audit-Protokolls wird wichtig |
| Direkt vom Client aus zugreifen lassen | Nicht über den Server, sondern direkt vom Client-PC aus auf den Dateiserver zugreifen | Wenn es genügt, die Freigabe direkt vom Bildschirm aus zu öffnen | Eine zentrale Steuerung und Protokollerfassung serverseitig entfällt |
Bei der Entscheidung geht es zunächst darum, festzulegen, ob wirklich mit den Windows-Rechten des jeweiligen Benutzers auf den Dateiserver zugegriffen werden muss. Ist das nötig, geht man in das Delegationsdesign; ist es nicht nötig, entscheidet man sich für ein Design ohne Vermittlung – so verzweigt sich die Entscheidung. Da die Konfiguration der Delegation nicht allein von den Anwendungsentwicklern abgeschlossen werden kann, ist es realistisch, sobald der Bedarf feststeht, frühzeitig den Domänenadministrator hinzuzuziehen.
Andererseits gibt es je nach Geschäftsanforderung auch Fälle, in denen kein Zugriff auf den Dateiserver mit den Windows-Rechten des jeweiligen Benutzers nötig ist.
In diesem Fall ist folgendes Design einfacher:
Authentifizierung und Autorisierung des Benutzers erfolgen in der Anwendung
↓
Der Zugriff auf den Dateiserver erfolgt mit einem dedizierten Dienstkonto
↓
Im Betriebsprotokoll werden Benutzer-ID und Zieldatei festgehalten
Das ist nicht das Design, bei dem „die ACL des Betriebssystems die endgültige Entscheidung trifft“, sondern eines, bei dem „die Autorisierung der Anwendung die endgültige Entscheidung trifft“.
Welches richtig ist, hängt von den Geschäftsanforderungen ab.
Macht man jedoch nicht deutlich, für welches Verfahren man sich entschieden hat, vermischen sich Identitätswechsel, Delegation, ACL und anwendungsseitige Autorisierung zu etwas Unübersichtlichem.
19. Die Lebensdauer von Token-Handles
Ein Token ist ein Handle auf ein Kernelobjekt und muss daher geschlossen werden, sobald es nicht mehr benötigt wird.
In .NET ist die Grundregel, SafeAccessTokenHandle zu verwenden und den Scope mit using zu verwalten.
using SafeAccessTokenHandle token = NativeMethods.Logon(userName, domain, password);
string result = WindowsIdentity.RunImpersonated(token, () =>
{
return File.ReadAllText(path);
});
Ein schlechtes Beispiel:
// Schlechtes Beispiel: Das Token wird dauerhaft global gehalten
private static SafeAccessTokenHandle? _cachedToken;
Ein Token über einen langen Zeitraum zu halten, führt zu Problemen wie diesen:
- Handle-Lecks
- Es wird unklar, welche Verarbeitung welches Token verwendet
- Die Konsistenz mit Kontodeaktivierung und Rechteänderungen wird unklar
- Es entsteht tendenziell ein Design, das Anmeldeinformationen lange hält
- Schwer im Rahmen eines Audits zu erklären
Der Grundsatz lautet:
Bei Bedarf beschaffen
↓
Im minimalen Umfang verwenden
↓
Immer schließen
Natürlich kann es je nach Authentifizierungskosten und betrieblichen Anforderungen sinnvoll sein, ein Caching in Betracht zu ziehen. Auch dann müssen jedoch Ablaufzeit, Verwerfung, Kontoänderungen, Audit-Protokollierung und der Umgang mit Rechteänderungen ins Design einbezogen werden.
20. Was im Audit-Protokoll festgehalten werden sollte
Bei Verarbeitungen mit Identitätswechsel ist auch das Protokolldesign wichtig.
Wenn man mindestens die Informationen aus dieser Tabelle festhalten kann, lässt sich später leichter nachforschen.
| Element | Beispiel |
|---|---|
| Anfragender Benutzer | DOMAIN\alice |
| Konto des ausführenden Prozesses | DOMAIN\svc-app |
| Identitätsgewechseltes Konto | DOMAIN\alice oder ein dediziertes Konto |
| Zielressource | Dateipfad, Freigabename, Registrierungsschlüssel usw. |
| Vorgang | Read, Write, Delete, CreateProcess usw. |
| Ergebnis | Success, AccessDenied, Timeout, UnexpectedError |
| Fehlercode | Win32-Fehlercode, HRESULT, Ausnahmetyp |
| Identitätswechsel-Scope | Welche Methode, welche Verarbeitungseinheit war betroffen |
Es gibt jedoch auch Dinge, die nicht ins Protokoll gehören.
- Passwörter
- Werte von Zugriffstoken
- Authentifizierungs-Header
- Kerberos-Tickets oder die Anmeldeinformationen selbst
- Dateiinhalte mit personenbezogenen Daten
Der Zweck des Protokolls ist es, nachträglich nachvollziehen zu können, „wer was auf wessen Anfrage hin, mit welchem Konto, versucht hat und wie es fehlgeschlagen oder gelungen ist“.
Die Anmeldeinformationen selbst müssen dabei nicht festgehalten werden.
21. Häufige Antipatterns
Hier eine Zusammenstellung häufig anzutreffender riskanter Implementierungen rund um den Identitätswechsel.
21.1 Die gesamte Anwendung mit Identitätswechsel ausführen
WindowsIdentity.RunImpersonated(token, () =>
{
RunEntireApplication();
});
Wechselt man für die gesamte Anwendung die Identität, wird unklar, welcher Vorgang mit welchen Rechten ausgeführt wird.
Der Identitätswechsel sollte auf das notwendige I/O und bestimmte API-Aufrufe begrenzt sein.
21.2 Zurücksetzen ohne finally
ImpersonateLoggedOnUser(token);
DoWork();
RevertToSelf();
Gefährlich, weil bei einer Ausnahme nicht zurückgesetzt wird.
Man verwendet zwingend try / finally oder RunImpersonated.
21.3 Fire-and-forget während des Identitätswechsels
WindowsIdentity.RunImpersonated(token, () =>
{
_ = Task.Run(DoWorkAsync);
});
Der Identitätswechsel-Scope wird vor Abschluss der Verarbeitung verlassen. Bei dieser Schreibweise läuft die übergebene Task (wie in Kapitel 14 beschrieben) weiterhin mit Identitätswechsel, sodass der im Code sichtbare Identitätswechselbereich und der tatsächliche Bereich auseinanderdriften. Da zudem niemand await aufruft, tritt bei einem Fehlschlag keine Ausnahme zutage. Bei einem direkten Identitätswechsel über Win32 läuft die Verarbeitung dagegen umgekehrt mit dem Konto des Prozesses.
Wird der Identitätswechsel benötigt, sollte innerhalb von RunImpersonatedAsync awaited werden.
21.4 Erfolg allein am Benutzernamen festmachen
Console.WriteLine(WindowsIdentity.GetCurrent().Name);
Auch wenn der Benutzername den Erwartungen entspricht, können Identitätswechselebene oder Rechte unzureichend sein.
Man sollte auch ImpersonationLevel, die Ziel-ACL, den Anmeldetyp und die Netzwerkdelegation betrachten.
21.5 Ein Administratorkonto als bequemes Konto impersonieren
Der Ansatz „dieser Vorgang soll auf keinen Fall fehlschlagen, also wechseln wir zur Identität eines Administratorkontos“ ist gefährlich.
Sicherer ist es, ein dediziertes Konto mit minimal notwendigen Rechten bereitzustellen und den Umfang der Vorgänge einzugrenzen.
21.6 Das Passwort in einer Konfigurationsdatei ablegen
{
"UserName": "DOMAIN\\admin",
"Password": "P@ssw0rd!"
}
Das sollte vermieden werden.
Müssen Anmeldeinformationen gehandhabt werden, sollte ein zur Umgebung passender Mechanismus verwendet werden: ein Secret Store, der Windows-Anmeldeinformations-Manager, DPAPI, ein Cloud-Key-Vault oder das Secret-Management der Betriebsplattform.
21.7 Prozessstart und Dateizugriff als dasselbe Thema behandeln
Für reinen Dateizugriff kann ein Identitätswechseltoken ausreichen.
Möchte man dagegen einen Prozess als anderer Benutzer starten, tauchen andere Aspekte auf: primäres Token, Profil, Desktop, Umgebungsvariablen, Sitzung, Rechte.
Bei Verwendung von CreateProcessAsUser oder CreateProcessWithTokenW sollte dies als eigenständiges Designthema, getrennt vom Identitätswechsel, behandelt werden.
22. Testperspektiven
Prüft man die Identitätswechsel-Verarbeitung nur in der eigenen Administratorumgebung, entstehen leicht blinde Flecken.
Mindestens diese Testfälle sollten vorbereitet werden:
| Fall | Was zu prüfen ist |
|---|---|
| Berechtigter Benutzer | Kann die Zieldatei lesen / schreiben |
| Unberechtigter Benutzer | Schlägt korrekt mit Access denied fehl |
| Nicht existierender Benutzer | Wird als Authentifizierungsfehler behandelt |
| Falsches Passwort | Schlägt fehl, ohne geheime Informationen im Protokoll preiszugeben |
| Netzwerkfreigabe | Verhaltensunterschied zwischen lokal und UNC prüfen |
| Asynchrone Verarbeitung | Läuft auch nach await im erwarteten Umfang |
| Ausnahme | Der Identitätswechsel wird zuverlässig aufgehoben |
| Parallele Anfragen | Identitätswechsel verschiedener Benutzer vermischen sich nicht |
| Ausführung als Dienst | Läuft mit dem tatsächlichen Dienstkonto, nicht mit der interaktiven Anmeldung des Entwicklers |
Zwei davon sind unverzichtbar:
Erfolg
Fehlschlag, wenn ein Fehlschlag erwartet wird
Bei Identitätswechsel-Verarbeitung testet man nicht nur die Erfolgsfälle, sondern auch, dass Benutzer ohne Berechtigung zuverlässig abgewiesen werden.
Gelingt ein Vorgang, der eigentlich abgewiesen werden sollte, ist das Design des Identitätswechsels oder der Autorisierung möglicherweise fehlerhaft.
23. Implementierungs-Checkliste
Vor und nach der Implementierung sollte diese Checkliste geprüft werden.
| Aspekt | Prüfinhalt |
|---|---|
| Zweck | Kann erklärt werden, warum der Identitätswechsel nötig ist? |
| Alternativen | Wurde geprüft, ob ein Dienstkonto oder anwendungsseitige Autorisierung nicht ausreichen? |
| Umfang | Ist der Identitätswechsel-Scope minimal? |
| Zurücksetzen | Wird auch bei einer Ausnahme zuverlässig zurückgesetzt? |
| Asynchron | Wird innerhalb von RunImpersonatedAsync vollständig bis zum Abschluss awaitet? |
| Token | Werden primäres Token und Identitätswechseltoken nicht verwechselt? |
| Identitätswechselebene | Wird zwischen Identification und Impersonation / Delegation unterschieden? |
| Netzwerk | Wurde geprüft, ob UNC, Double-Hop oder Kerberos-Delegation eine Rolle spielen? |
| UAC | Werden Zugehörigkeit zu Administrators und tatsächliche Erhöhung nicht verwechselt? |
| Geheime Informationen | Werden Passwörter irgendwo im Klartext gespeichert? |
| Handle | Wird SafeAccessTokenHandle mit using geschlossen? |
| Protokoll | Lassen sich Benutzer, identitätsgewechseltes Konto, Ziel und Ergebnis nachvollziehen? |
| Tests | Wurden Fälle mit / ohne Berechtigung, Ausnahmen und Parallelität geprüft? |
Fällt man bei vielen Punkten dieser Checkliste durch, ist es besser, das Design zu überarbeiten, bevor man Code schreibt.
24. Den Einsatzbereich richtig einschätzen
Identitätswechseltoken sind mächtig, aber nicht immer das zuerst zu wählende Mittel.
Als Entwurfsansatz hilft es, nach Einsatzzweck getrennt zu denken, um nicht die Orientierung zu verlieren.
24.1 Wenn die ACL des Betriebssystems direkt genutzt werden soll
Bilden die ACLs eines Dateiservers oder gemeinsamer Ordner den Kern der Geschäftsregeln und soll sich auch die Anwendung dieser Entscheidung unterordnen, ist ein Identitätswechsel als der jeweilige Benutzer sinnvoll.
Zugriff als der jeweilige Benutzer
↓
Die Windows-ACL trifft die endgültige Entscheidung
In diesem Fall werden Windows-Authentifizierung, Identitätswechselebene, Delegation und Netzwerktopologie gemeinsam entworfen.
24.2 Wenn die Anwendung selbst autorisieren soll
Liegen die Geschäftsregeln auf Anwendungsseite und stehen Dateiserver oder DB unter der Verwaltung der Anwendung, ist es oft klarer, mit einem Dienstkonto zuzugreifen und anwendungsseitig zu autorisieren.
Benutzer authentifizieren
↓
In der Anwendung autorisieren
↓
Mit dem Dienstkonto auf Ressourcen zugreifen
↓
Die Benutzer-ID im Audit-Protokoll festhalten
Bei diesem Ansatz tragen anstelle des Identitätswechsels die Autorisierungslogik der Anwendung und die Audit-Protokolle die Hauptlast.
24.3 Wenn Verwaltungsvorgänge durchgeführt werden sollen
Statt Verwaltungsvorgänge direkt mit dem Token des Benutzers auszuführen, ist es meist sicherer, einen dedizierten Verwaltungsdienst oder eine API bereitzustellen, die Autorisierung, Eingabevalidierung, Auditing und Rollback übernimmt.
Client
↓
Anfrage an die Verwaltungs-API
↓
Die Verwaltungs-API autorisiert
↓
Vorgang mit minimal notwendigen Rechten
↓
Audit-Protokoll
„Einfach einen Administrator impersonieren“ wirkt kurzfristig bequem.
Langfristig macht es jedoch Auditing, Störungsanalyse, Rechteänderungen und Sicherheitsüberprüfungen mühsam.
25. Zusammenfassung
Windows-Identitätswechseltoken sind ein wichtiger Mechanismus, um die Rechteverwaltung von Windows korrekt zu nutzen.
Zugleich ist dies ein Bereich, in dem Code, der ohne Verständnis verwendet wird, leicht funktionierend aussieht, obwohl er gefährlich ist.
Die wichtigsten Punkte zusammengefasst:
- Ein Zugriffstoken repräsentiert den Sicherheitskontext: Benutzer, Gruppen, Rechte und mehr
- Ein Prozess besitzt ein primäres Token
- Identitätswechseltoken hängen hauptsächlich an Threads und werden bei Zugriffsprüfungen verwendet
- Ein Identitätswechsel ist keine Rechteausweitung
- Primäres Token und Identitätswechseltoken dienen unterschiedlichen Zwecken
- Die Identitätswechselebene bestimmt, ob nur identifiziert, tatsächlich zugegriffen oder an ein entferntes System delegiert werden kann
- Wechselt man über die Win32-API die Identität, muss
RevertToSelfzwingend intry/finallyaufgerufen werden - In .NET drückt man kleine Scopes mit
WindowsIdentity.RunImpersonated/RunImpersonatedAsyncaus - Bei Verwendung von
LogonUsersind Verwaltung der Anmeldeinformationen und Anmeldetyp zu beachten - Bei Netzwerkfreigaben spielen neben dem Identitätswechsel auch Kerberos-Delegation und das Design der Dienstkonten eine Rolle
- Token-Handles werden mit
SafeAccessTokenHandleundusingverwaltet - Es werden nicht nur Erfolgsfälle getestet, sondern auch Fälle, die abgewiesen werden sollen
Bei der Implementierung des Identitätswechsels kommt es nicht auf die eine Tatsache an, dass „es als anderer Benutzer lief“, sondern darauf, Folgendes erklären zu können:
Welche Verarbeitung
auf wessen Anfrage
mit welchem Konto
in welchem Umfang ausgeführt wurde,
wo sie zurückgesetzt wurde
und wie Erfolg und Fehlschlag festgehalten werden.
Ist dies alles geklärt, ist der Identitätswechsel kein beängstigender Mechanismus mehr.
Er wird zu einem praktischen Werkzeug, um Windows-ACLs, Dienstkonten, bestehende Dateiserver und die eigenen Domänen-Altbestände optimal zu nutzen.
Referenzlinks
- Das vollständige Beispielcode-Paket zu diesem Artikel (Bibliothek, Demo, Unit-Tests)
https://github.com/gomurin0428/komurasoft-blog-samples/tree/main/windows-impersonation-token - Microsoft Learn: Access Tokens
https://learn.microsoft.com/en-us/windows/win32/secauthz/access-tokens - Microsoft Learn: Impersonation Tokens
https://learn.microsoft.com/en-us/windows/win32/secauthz/impersonation-tokens - Microsoft Learn: Impersonation Levels
https://learn.microsoft.com/en-us/windows/win32/secauthz/impersonation-levels - Microsoft Learn:
SECURITY_IMPERSONATION_LEVELenumeration
https://learn.microsoft.com/en-us/windows/win32/api/winnt/ne-winnt-security_impersonation_level - Microsoft Learn:
ImpersonateLoggedOnUserfunction
https://learn.microsoft.com/en-us/windows/win32/api/securitybaseapi/nf-securitybaseapi-impersonateloggedonuser - Microsoft Learn:
RevertToSelffunction
https://learn.microsoft.com/en-us/windows/win32/api/securitybaseapi/nf-securitybaseapi-reverttoself - Microsoft Learn:
LogonUserfunction
https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-logonusera - Microsoft Learn:
DuplicateTokenExfunction
https://learn.microsoft.com/en-us/windows/win32/api/securitybaseapi/nf-securitybaseapi-duplicatetokenex - Microsoft Learn:
TOKEN_INFORMATION_CLASSenumeration
https://learn.microsoft.com/en-us/windows/win32/api/winnt/ne-winnt-token_information_class - Microsoft Learn:
WindowsIdentity.RunImpersonated
https://learn.microsoft.com/en-us/dotnet/api/system.security.principal.windowsidentity.runimpersonated - Microsoft Learn:
WindowsIdentity.RunImpersonatedAsync
https://learn.microsoft.com/en-us/dotnet/api/system.security.principal.windowsidentity.runimpersonatedasync - .NET-Laufzeitimplementierung:
WindowsIdentity.cs(dasAsyncLocal, das das Identitätswechseltoken hält, sowieCurrentImpersonatedTokenChanged, das den Identitätswechsel bei einem Thread-Wechsel neu ansetzt)
https://github.com/dotnet/runtime/blob/main/src/libraries/System.Security.Principal.Windows/src/System/Security/Principal/WindowsIdentity.cs - Microsoft Learn: Configure Windows Authentication in ASP.NET Core
https://learn.microsoft.com/en-us/aspnet/core/security/authentication/windowsauth - Microsoft Learn: Kerberos Constrained Delegation Overview
https://learn.microsoft.com/en-us/windows-server/security/kerberos/kerberos-constrained-delegation-overview - Microsoft Learn:
ProtectedDataClass
https://learn.microsoft.com/en-us/dotnet/api/system.security.cryptography.protecteddata
Verwandte Artikel
Aktuelle Artikel mit denselben Schlagwörtern führen zu verwandten Themen weiter.
Die Tiefen von Windows-I/O (Teil 4) — Cache-Manager: Wann erreicht Ihr WriteFile tatsächlich die Festplatte?
Teil 4 einer bebilderten Reihe über den Windows-Cache-Manager. Behandelt den als Dateizuordnung implementierten Cache, Read-Ahead und ver...
Die Tiefen von Windows-I/O (Teil 3) — I/O-Completion-Ports (IOCP) und der .NET-Thread-Pool: Der Keller unter async/await
Teil 3 einer Reihe, die I/O-Completion-Ports (IOCP) anhand von Diagrammen erklärt. Behandelt das Design, das Completion-Queue und Thread-...
Die Tiefen von Windows I/O (Teil 2) — Synchrones und asynchrones I/O: Was OVERLAPPED wirklich bedeutet
Teil 2 einer Serie, die Windows' synchrones und asynchrones I/O (Overlapped I/O) anhand von Diagrammen erklärt. Wir stellen die Bedeutung...
Die Tiefen von Windows I/O (Teil 1) — Jedes Lesen und Schreiben wird zu einem IRP: Das Gesamtbild des I/O-Systems
Teil 1 einer Serie, die das Windows-I/O-System von Grund auf erklärt. Wir stellen den Namensraum des Object Managers, die drei Objektarte...
Was vor der Entsorgung eines Windows-PCs zu erledigen ist ── Praktische Checkliste für Datenlöschung, Kontotrennung und Backup
Was vor der Entsorgung, Übergabe, dem Verkauf oder der Rückgabe eines geleasten Windows-PCs zu erledigen ist – mit Blick auf Backup, Date...
Verwandte Themen
Diese Seiten ordnen den Artikel in einen größeren Leistungs- und Entscheidungskontext ein.
Technische Windows-Themen
Portal zu Windows-Entwicklung, Fehleranalyse und der Nutzung bestehender Assets.
Leistungen zu diesem Thema
Dieser Artikel ist direkt mit den folgenden Leistungen verbunden.
Windows-App-Entwicklung
Geschäftsanwendungen, Geräteintegration und Kommunikationstools von den Anforderungen bis zur Umsetzung.
Häufige Fragen
Fragen, die in Beratungen zu diesem Artikelthema häufig gestellt werden.
- Was ist ein Windows-Identitätswechseltoken?
- Ein Identitätswechseltoken (Impersonation Token) ist ein Token, das an einen Thread angehängt wird, damit dieser Thread Zugriffsprüfungen in einem anderen Sicherheitskontext durchläuft. Nicht der gesamte Prozess wird zu einem anderen Benutzer – nur der Thread, der den Identitätswechsel vollzieht, wird bei Zugriffen auf Dateien, die Registrierung und Ähnliches mit den Rechten dieses Benutzers geprüft. Es ist keine „Verwandlung in einen Administrator“, also keine Rechteausweitung, sondern ein Mechanismus, mit dem nur ein Teil der Verarbeitung eines Serverprozesses mit den Rechten des Client-Benutzers geprüft wird.
- Worin unterscheiden sich primäres Token und Identitätswechseltoken?
- Das primäre Token repräsentiert den Sicherheitskontext eines Prozesses und wird für das Starten von Prozessen verwendet, etwa über CreateProcessAsUser. Das Identitätswechseltoken dient dazu, dass ein Thread in einem anderen Sicherheitskontext läuft, und kommt bei ImpersonateLoggedOnUser oder beim Identitätswechsel des Clients bei Named Pipes zum Einsatz. Wer einen Prozess starten möchte, benötigt grundsätzlich ein primäres Token; liegt nur ein Identitätswechseltoken vor, muss mit DuplicateTokenEx daraus ein primäres Token erzeugt werden. Wer beides verwechselt, gerät bei Fehlern wie Access denied ins Grübeln.
- Warum kommt trotz Identitätswechsel ein Access denied?
- Dafür gibt es mehrere Prüfpunkte. Innerhalb des Identitätswechsel-Scopes sollte man nicht nur den Namen über WindowsIdentity.GetCurrent() prüfen, sondern auch die ImpersonationLevel – steht sie auf Impersonation oder höher und nicht nur auf Identification? Weitere Punkte sind die ACL der Zielressource, ob das tatsächliche I/O außerhalb des Identitätswechsel-Scopes oder in einer anderen Task läuft, ob es sich um das Double-Hop-Problem handelt, bei dem lokale Zugriffe gelingen und nur UNC-Zugriffe scheitern, sowie der Einfluss eines durch UAC nicht erhöhten Tokens. Auch wenn der Name stimmt, können die Rechte fehlen.
- Wie implementiert man einen Identitätswechsel in .NET sicher?
- Grundlage ist die Verwendung von WindowsIdentity.RunImpersonated / RunImpersonatedAsync, wobei der Identitätswechsel-Scope in einen Lambda-Ausdruck eingeschlossen wird. Asynchrone Verarbeitung, die den Identitätswechsel benötigt, sollte innerhalb von RunImpersonatedAsync awaited werden, und aus dem Identitätswechsel-Scope heraus sollte kein Fire-and-forget-Task gestartet werden. Bei einem Identitätswechsel über die Win32-API muss RevertToSelf zwingend in try/finally aufgerufen werden. Der Identitätswechsel-Scope sollte auf das nötige I/O minimiert werden, und Token-Handles sollten mit SafeAccessTokenHandle und using verwaltet werden.
Autorenprofil
Profilseite des Artikelautors.
Go Komura
Geschäftsführer von KomuraSoft LLC
Spezialisiert auf Windows-Softwareentwicklung, technische Beratung und Fehleranalyse, insbesondere bei bestehenden Systemen und schwer reproduzierbaren Störungen.