Wie man in einer Windows-App konkret nur die „Vorgänge, die Administratorrechte brauchen“ isoliert
· Go Komura · Windows-Entwicklung, Sicherheit, UAC, C# / .NET, Win32
Im früher erschienenen Artikel „Eine Mindest-Checkliste für die Sicherheit in der Windows-Anwendungsentwicklung“ haben wir die Linie gezogen: grundsätzlich asInvoker, und nur die Vorgänge isolieren, die Administratorrechte benötigen.
Diesmal gehen wir bis dahin, wie man das konkret schreibt.
In einer Windows-App lässt sich nicht bequem nur ein Teil desselben Prozesses „als Administrator“ ausführen. Elevation ist eine Frage der Prozessgrenze, daher braucht man ein Design, das genau diesen Vorgang in eine separate Ausführungseinheit auslagert.
Dieser Artikel geht in dieser Reihenfolge vor.
- Zunächst die Grundlagen
- Welches Trennungsmodell man wählt
- Die im Alltag am leichtesten einzusetzende Form:
asInvoker+ ein Administrator-Helper-EXE - Fallen, die man bei der Umsetzung nicht übersehen sollte
- Konkrete Codebeispiele
Die Codebeispiele setzen .NET 8 / eine Windows-Desktop-App voraus. Als UI-Framework eignet sich WPF, WinForms oder WinUI gleichermaßen; Unterschiede zeigen sich höchstens bei den Event-Handlern auf der UI-Seite.
Der in diesem Artikel gezeigte Code ist außerdem als eigenständiges, buildbares und lauffähiges Beispielpaket auf GitHub veröffentlicht (eine gemeinsame Vertragsbibliothek, Demos für UI und Administrator-Helper sowie Unit-Tests, die auch unter Linux laufen).
windows-admin-broker-deep-dive - komurasoft-blog-samples (GitHub)
Wie Sie diesen Artikel lesen
Der Artikel ist lang, deshalb hier vorab eine Orientierung.
| Was Sie wissen möchten | Wo Sie lesen sollten |
|---|---|
| Nur die Wahl des Trennungsmodells | Kapitel 1–4 (Fazit, Vergleich der vier Modelle, die empfohlene Form) |
| Die Design-Entscheidungen und ihre Begründung | Kapitel 5 (Allowlist, feste Pfade, runas, ACL der Pipe, PID-Verifizierung) |
| Der Implementierungscode | Kapitel 7–14 (Aufbau, Manifest, gemeinsamer Vertrag, UI-Seite, Helper-Seite) |
| Wie man prüft, ob die Trennung wirklich gelungen ist | 15.6 |
| Was man nicht tun sollte | Kapitel 16 |
Der vollständige Code steckt ebenso im GitHub-Beispiel. Wer nur den Design-Teil möchte, liest Kapitel 1–5 und 15–16; wer bis zur Implementierung will, liest durchgehend.
Was Sie vor dem Ausprobieren brauchen
Um dieses Beispiel tatsächlich auszuführen, benötigen Sie Folgendes.
- Einen Windows-Rechner (der UAC-Elevationsprompt, das explizite ACL über
PipeSecurity,GetNamedPipeClientProcessIdund das Schreiben in HKLM sind allesamt Windows-spezifisch) - .NET 8 SDK oder neuer
- Ein Konto, das eine Elevation genehmigen kann. Bei einem Administratorkonto erscheint eine Zustimmungsabfrage, bei einem Standardbenutzer eine Anmeldeinformationsabfrage. Wenn Sie beide Pfade testen möchten, halten Sie beide Kontoarten bereit
- Einen Rechner, auf dem HKLM verändert werden darf. Das Beispiel legt
HKLM\SOFTWARE\Classes\*\shell\MyApp.Openrechnerweit an. Testen Sie es besser auf einer Evaluierungs-VM als auf Ihrem gewöhnlichen Entwicklungsrechner
Die konkreten Build- und Ausführungsbefehle stehen in der README des Beispiels. Merken Sie sich vorab nur, dass Sie UI und Helper in denselben Ordner publishen müssen, bevor Sie sie ausführen (der Helper löst MyApp.exe fest im eigenen Ordner auf).
1. Fazit zuerst
Die praktischen Landepunkte gleich vorweg.
- Die gewöhnliche UI-App läuft weiterhin unter
asInvoker - Vorgänge, die Administratorrechte benötigen, werden in eine separate EXE ausgelagert
- Diese Helper-EXE erhält
requireAdministrator - Der Start erfolgt über
runas - Für die Kommunikation mit dem Helper wird IPC wie eine Named Pipe verwendet, nicht die mit
runasschlecht verträgliche Standard-Ein-/Ausgabe - An den Helper wird nicht die „rohe Kommandozeichenkette“, sondern nur eine typisierte Anfrage übergeben
- Auf der Helper-Seite wird der Inhalt der Anfrage erneut validiert
- Der Verbindungsursprung der IPC wird über die SID des aufrufenden Benutzers und die erwartete PID eingegrenzt
„Als Administrator laufen ist bequemer“ stimmt nur beim ersten Mal. Später gibt es dafür bei UAC, Drag & Drop, Log-Design, externen Eingaben, Support-Betrieb, DLL-Laden und dem Speicherort von Einstellungen ziemlich sicher böse Blicke.
2. Grundlagen: Man kann nicht nur einen Teil desselben Prozesses zum Administrator machen
Die UAC von Windows steuert nicht auf „Funktionsebene“, sondern danach, mit welchem Token / welcher Integritätsstufe ein Prozess läuft. Apps, die ein Administrator-Zugriffstoken benötigen, sind Ziel des Elevationspromptes, und Eltern- und Kindprozesse erben das Token auf derselben Integritätsstufe. Das heißt: Das Design, innerhalb eines nicht erhöhten UI-Prozesses plötzlich eine bestimmte Methode mit Administratorrechten auszuführen, ist nicht möglich. Wird das benötigt, verwendet man eine andere Ausführungseinheit — einen separaten Prozess, einen Dienst, eine Aufgabe, erhöhtes COM und so weiter.
Denkt man ohne diese Grundannahme weiter, landet man bei der etwas bedauernswerten Design-Anfrage: „Ich möchte, dass es genau in dem Moment, in dem dieser Button gedrückt wird, zum Administrator wird.“ Windows füllt diese Lücke nicht mit Magie.
2.1 Zuerst die Zuordnung der Integritätsstufe (integrity level) festlegen
In diesem Artikel tauchen im Folgenden die Begriffe medium integrity / high integrity auf. Legen wir die Zuordnung vorab fest.
| Integritätsstufe | Bedeutung in diesem Artikel | Beispiel |
|---|---|---|
| medium | Ein Prozess, der mit Standardbenutzerrechten läuft | Die UI-App unter asInvoker |
| high | Ein erhöhter (elevierter) Prozess | Die Helper-EXE unter requireAdministrator |
Windows’ Mandatory Integrity Control definiert vier Stufen — low / medium / high / system —, und Standardbenutzer erhalten medium, erhöhte Benutzer erhalten high. Das Design dieses Artikels bedeutet also: „den UI-Prozess auf medium“ und „den Helper-Prozess auf high“ mit einer klar gezogenen Linie miteinander sprechen zu lassen.
Wenn diese Zuordnung sitzt, lassen sich sowohl das CurrentUserOnly in 5.6 als auch 16.4 unmittelbar verstehen.
3. Welches Trennungsmodell man wählt
Microsoft Learn nennt für Apps, die Administratorrechte benötigen, im Wesentlichen die folgenden vier Trennungsmodelle.
| Modell | Grobe Form | Geeignet für |
|---|---|---|
| Administrator Broker Model | UI-App mit Standardrechten + Administrator-Helper-EXE | Administratoraktionen sind sporadisch, es reicht, UAC genau im nötigen Moment zu zeigen |
| Operating System Service Model | UI mit Standardrechten + residenter Dienst | Ständig laufende Verwaltungsfunktionen, Hintergrundüberwachung, unbeaufsichtigte Verarbeitung |
| Elevated Task Model | UI mit Standardrechten + geplante Aufgabe mit Administratorrechten | Kurze, einmalige Standardaufgaben |
| Administrator COM Object Model | UI mit Standardrechten + erhöhtes COM | Ein bestehendes COM-Design liegt vor, der Funktionsumfang ist recht eng begrenzt |
Als Anhaltspunkt für die Wahl gilt Folgendes.
3.1 Als Erstes bietet sich meist die Broker-EXE an
Eine Broker-EXE passt gut zu Vorgängen wie diesen:
- Registrieren / Deregistrieren von Explorer-Integration
- Rechnerweite Konfigurationsänderungen unter HKLM
- Registrieren / Deregistrieren des eigenen Dienstes der App
- Hinzufügen / Entfernen von Firewallregeln
- Administratoroperationen unter Program Files
Diese sind im Alltag meist nicht nötig und werden nur gebraucht, wenn ein bestimmter Button im Einstellungsdialog gedrückt wird. In diesem Fall ist es natürlicher, einmalig eine Administrator-Helper-EXE zu starten, die dann endet, als gleich einen residenten Dienst einzusetzen.
3.2 Einen Dienst wählt man bei „ständig“, „unbeaufsichtigt“, „häufig“
Ein Dienst ist das Modell, bei dem die App mit Standardrechten per RPC oder Ähnlichem kommuniziert. Der Vorteil ist, Verwaltungsvorgänge ohne Elevationsprompt entgegennehmen zu können — im Gegenzug steigt jedoch die Verantwortung, einen residenten Prozess zu betreiben.
Ein Dienst passt zu Einsatzzwecken wie diesen:
- Ständige Überwachung
- Log-Erfassung
- Hintergrund-Updates
- Ständige Anbindung an Geräte oder Daemons
- Verwaltungsfunktionen, die von mehreren UI-Sitzungen gemeinsam genutzt werden
3.3 Eine Task passt zu „kurzer Standardverarbeitung“
Das Elevated Task Model startet aus der App mit Standardrechten heraus eine geplante Aufgabe, die mit Administratorrechten läuft. Es ist leichter als ein Dienst und schließt sich nach Erledigung, weshalb es zu einmaligen Standardjobs passt.
3.4 Erhöhtes COM ist recht eng begrenzt
Der COM-Elevation-Moniker sieht praktisch aus, sein Einsatzbereich ist jedoch schmal. Auch Microsoft Learn hält fest, dass die UI, die erhöhtes COM steuert, von der COM-Seite selbst bereitgestellt werden muss — die Richtung „eine nicht erhöhte UI lässt erhöhtes COM nach Belieben schalten und walten“ passt hier nicht.
4. Die Empfehlung dieses Artikels: asInvoker-UI + requireAdministrator-Helper-EXE
Ab hier konkretisieren wir die im Alltag am leichtesten einzusetzende Form.
flowchart TB
subgraph MED["medium integrity ── bleibt bis zuletzt nicht erhöht"]
UI["MyApp.exe(asInvoker)<br/>nimmt nur Benutzeraktionen entgegen und baut Anfragen zusammen"]
end
subgraph HIGH["high integrity ── kurzlebiger erhöhter Prozess"]
BR["MyApp.AdminBroker.exe<br/>(requireAdministrator)"]
PIPE["Annahmestelle der Named Pipe<br/>Verbindung nur für die SID des UI-Benutzers erlaubt<br/>PID des Verbindungsursprungs wird ebenfalls geprüft"]
DISP["Verteilung über eine Allowlist von Operationen<br/>Argumente werden auf der Helper-Seite erneut validiert"]
end
TGT["Fester Zielbereich, der Administratorrechte braucht<br/>Schlüssel unter HKLM / Dienstregistrierung / Firewallregeln"]
UI -->|"Start mit absolutem Pfad + Verb=runas<br/>(hier erscheint der UAC-Prompt)"| BR
BR --> PIPE
UI -->|"Typisierte Anfrage<br/>(keine rohe Kommandozeichenkette)"| PIPE
PIPE --> DISP
DISP --> TGT
Abbildung 1: Die Elevationsgrenze deckt sich mit der Prozessgrenze. Die UI-Seite bleibt bei medium, nur der Helper läuft kurz bei high
Drei Punkte sind entscheidend.
- Der UI-Prozess bleibt bis zum Schluss nicht erhöht
- Der Administrator-Helper ist kurzlebig
- Der Helper akzeptiert nur eine feste Allowlist von Operationen
Schon das Einhalten dieser drei Punkte räumt das Design erheblich auf.
5. Regeln, die man bei der Implementierung nicht auslassen sollte
Das sollte man besser vor dem Schreiben des Codes festlegen.
5.1 Den Helper nicht zum „Alleskönner“ machen
Schlechte Beispiele sind diese:
- Die UI übergibt dem Helper ein komplettes
reg add ...als Zeichenkette - Die UI übergibt dem Helper ein komplettes
sc.exe ...als Zeichenkette - Die UI übergibt dem Helper beliebige Registrierungspfade oder beliebige EXE-Pfade
Tut man das, reißt eine kompromittierte UI den Helper mit sich. Der Administrator-Helper liegt innerhalb der Elevationsgrenze. Dort eine „Kann-alles-ausführen“-Öffnung zu schaffen, ist ziemlich riskant.
Die gute Form sieht so aus:
set-explorer-context-menuinstall-serviceadd-firewall-rule
Die Operationen selbst werden fest vorgegeben, und die benötigten Argumente werden auf bool / enum / Zahl / eingeschränkte Zeichenketten reduziert.
5.2 Der an den Helper übergebene Pfad ist absolut — und die UI sollte nicht zu viel selbst entscheiden
Die per runas gestartete Helper-EXE selbst wird mit absolutem Pfad angegeben.
PATH-Suche oder relative Pfade werden vermieden.
Darüber hinaus wird auch das, worauf der Helper einwirkt, möglichst auf der Helper-Seite selbst fest aufgelöst.
In diesem Beispiel wird die im Explorer-Kontextmenü registrierte Ziel-EXE fest auf die MyApp.exe im selben Ordner wie der Helper festgelegt.
5.3 Wer Verb="runas" verwendet, sollte UseShellExecute=true explizit setzen
In .NET ist ProcessStartInfo.Verb nur wirksam, wenn UseShellExecute=true gesetzt ist.
Außerdem unterscheidet sich der Standardwert von UseShellExecute zwischen .NET Framework und .NET Core / .NET.
Überlässt man das dem Standardwert, entsteht später der unangenehme Unfall, dass „es in manchen Umgebungen läuft und in anderen nicht“.
Deshalb setzt man das grundsätzlich explizit.
5.4 runas und die Umleitung von Standard-Ein-/Ausgabe vertragen sich schlecht
Mit UseShellExecute=true lässt sich Kommunikation, die auf der Umleitung von Standard-Ein-/Ausgabe beruht, kaum noch nutzen.
Für den Austausch mit dem Helper ist es daher natürlicher, ein anderes IPC-Mittel wie eine Named Pipe zu verwenden.
5.5 Eine Named Pipe sollte sich nicht auf das Standard-ACL verlassen
Bei einer Named Pipe gewährt der Standard-Sicherheitsdeskriptor standardmäßig Everyone und anonymen Verbindungen Lesezugriff. Das für die IPC eines Administrator-Helpers unverändert zu übernehmen, ist ziemlich nachlässig.
Man sollte unbedingt eine explizite PipeSecurity setzen.
5.6 PipeOptions.CurrentUserOnly wird für diesen Einsatzzweck nicht verwendet
Auf den ersten Blick wirkt das praktisch.
Unter Windows prüft CurrentUserOnly jedoch nicht nur das Benutzerkonto, sondern auch die Erhöhungsstufe.
Das heißt: Für die Kommunikation zwischen einer nicht erhöhten UI und einem erhöhten Helper ist es nicht geeignet.
Hinzu kommt, dass wer sich mit welchem Token mit der Pipe verbindet, von der Art des UAC-Prompts abhängt. Hält man das in einer Tabelle fest, lässt sich nachvollziehen, warum ein explizites ACL nötig ist.
| Ausführungskonto der UI | Erscheinender UAC-Prompt | Konto, unter dem der Helper läuft | WindowsIdentity.GetCurrent() des Pipe erzeugenden Helpers |
SID der sich verbindenden UI-Seite |
|---|---|---|---|---|
| Administratorkonto (nicht erhöht) | Zustimmungsabfrage (nur „Ja“ drücken) | Erhöhtes Token desselben Benutzers | Derselbe Benutzer wie die UI | UI-Benutzer |
| Standardbenutzer | Anmeldeinformationsabfrage (Zugangsdaten eines anderen Kontos eingeben) | Das eingegebene andere Administratorkonto | Ein anderer Benutzer als die UI | UI-Benutzer |
So liest man diese Tabelle:
- In der oberen Zeile gilt „aktueller Benutzer des Helpers = UI-Benutzer“, weshalb ein ACL, das auf der Helper-Seite nur die eigene SID betrachtet, zufällig trotzdem funktioniert
- In der unteren Zeile sind aktueller Benutzer des Helpers und UI-Benutzer verschiedene Personen. Baut man das ACL hier allein anhand von
WindowsIdentity.GetCurrent()auf, kann der ursprüngliche UI-Benutzer seine eigene Anfrage nicht mehr senden - In beiden Zeilen wird
CurrentUserOnlydurch den Unterschied der Erhöhungsstufe zwischen „UI auf medium“ und „Helper auf high“ blockiert
Die einzige Form, die in beiden Zeilen funktioniert, ist also: die UI-Seite übergibt ihre SID, und diese SID erhält die Verbindungsberechtigung.
Deshalb wird hier wie folgt vorgegangen:
- Die UI-Seite ermittelt ihre eigene SID und übergibt sie an den Helper
- Der Helper erteilt die Pipe-Verbindungsberechtigung nur der SID des UI-Benutzers
- Zusätzlich wird mit
GetNamedPipeClientProcessIdauch die PID des Verbindungsursprungs geprüft
5.7 Die PID-Verifizierung ist eine zusätzliche Verteidigung gegen „grobes Dazwischendrängen“
Ein zufälliger Pipe-Name allein hilft schon viel, aber die Möglichkeit, dass ein anderer, unter demselben Benutzer laufender Prozess sich zuerst verbindet, ist nicht null.
Deshalb prüft der Helper mit GetNamedPipeClientProcessId, ob sie mit der erwarteten PID des UI-Prozesses übereinstimmt.
Natürlich bedeutet eine passende PID nicht, dass alles vertrauenswürdig ist. Ist die UI kompromittiert, erreichen auch den Helper gefährliche Anfragen. Genau deshalb sind die Allowlist der Operationen und die Argumentvalidierung auf der Helper-Seite nötig.
Ordnet man diese Regeln in der Reihenfolge vom Start bis zum Ende an, ergibt sich Folgendes. Es genügt zu erkennen, dass jedem Schritt der UI-Seite eine entsprechende Prüfung auf der Helper-Seite gegenübersteht.
sequenceDiagram
participant UI as MyApp.exe(medium)
participant OS as Windows / UAC
participant BR as AdminBroker.exe(high)
participant TGT as Ziel mit Administratorrechten
UI->>UI: Legt den Pipe-Namen fest und bereitet eigene SID und PID vor
UI->>OS: Start mit absolutem Pfad + Verb=runas
OS->>BR: Nach Genehmigung Start mit erhöhtem Token
BR->>BR: Erzeugt die Pipe mit einem ACL, das nur diese SID erlaubt
UI->>BR: Verbindet sich mit der Pipe
BR->>BR: Gleicht die PID des Verbindungsursprungs ab
UI->>BR: Typisierte Anfrage (Operationsname und Argumente)
BR->>BR: Weist Operationen außerhalb der Allowlist und unerwartete Argumente zurück
BR->>TGT: Wirkt nur auf das festgelegte Ziel ein
BR-->>UI: Gibt das Ergebnis zurück
BR->>BR: Beendet sich, ohne einen erhöhten Zustand zu hinterlassen
Abbildung 2: Start, Verbindung und Anfrage haben jeweils eine entsprechende Prüfung auf der Helper-Seite. Lässt man auch nur eine davon aus, wird genau dieser Schritt durchgereicht
6. Das Beispielszenario
In diesem Artikel dient als Beispiel das rechnerweite Registrieren / Deregistrieren eines Explorer-Rechtsklickmenü-Eintrags.
Der Grund ist einfach:
- Es benötigt Administratorrechte
- Die Grenze des Vorgangs ist klar
- Dem Helper muss keine beliebige Kommandozeichenkette übergeben werden
- Es kommt in der Praxis ganz normal vor
Die Registrierungsziele sind feste Schlüssel wie diese:
HKLM\SOFTWARE\Classes\*\shell\MyApp.OpenHKLM\SOFTWARE\Classes\*\shell\MyApp.Open\command
Die UI besitzt nur die Checkbox „Im Explorer-Rechtsklickmenü registrieren“; die eigentliche Registry-Operation erfolgt auf der Helper-Seite.
7. Lösungsstruktur
MyApp/
MyApp/ UI-App (asInvoker)
app.manifest
ElevationBrokerClient.cs
SettingsPage.xaml.cs
MyApp.AdminBroker/ Administrator-Helper (requireAdministrator)
app.manifest
Program.cs
BrokerLaunchOptions.cs
ExplorerContextMenuRegistration.cs
MyApp.BrokerProtocol/ Gemeinsamer Vertrag
BrokerProtocol.cs
Hält man den gemeinsamen Vertrag in einem eigenen Projekt, lässt sich zwischen UI und Helper leichter angleichen:
- die Namen der Operationen
- die Request-/Response-Typen
- das Nachrichtenformat der Pipe
8. Manifeste
8.1 UI-Seite (MyApp/app.manifest)
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="1.0.0.0" name="MyApp.app" />
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security>
<requestedPrivileges>
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
</requestedPrivileges>
</security>
</trustInfo>
</assembly>
8.2 Helper-Seite (MyApp.AdminBroker/app.manifest)
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="1.0.0.0" name="MyApp.AdminBroker.app" />
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security>
<requestedPrivileges>
<requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
</requestedPrivileges>
</security>
</trustInfo>
</assembly>
Die UI bleibt durchgehend asInvoker.
Nur der Helper erhält requireAdministrator.
Vertauscht man das, verschwindet der Sinn der Trennung.
9. Gemeinsamer Vertrags-Code
9.1 MyApp.BrokerProtocol/BrokerProtocol.cs
using System.Buffers.Binary;
using System.Text.Json;
namespace MyApp.BrokerProtocol;
public static class BrokerJson
{
public static readonly JsonSerializerOptions Options = new(JsonSerializerDefaults.Web)
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
}
public static class BrokerOperations
{
public const string SetExplorerContextMenu = "set-explorer-context-menu";
}
public sealed record BrokerRequest(string Operation, JsonElement Payload);
public sealed record BrokerResponse(bool Success, string? ErrorCode, string? Message)
{
public static BrokerResponse Ok(string? message = null) => new(true, null, message);
public static BrokerResponse Fail(string errorCode, string message) =>
new(false, errorCode, message);
}
public sealed record SetExplorerContextMenuRequest(bool Enabled);
public static class PipeMessageSerializer
{
private const int MaxPayloadBytes = 256 * 1024;
public static async Task WriteAsync<T>(Stream stream, T value, CancellationToken cancellationToken)
{
byte[] payload = JsonSerializer.SerializeToUtf8Bytes(value, BrokerJson.Options);
if (payload.Length > MaxPayloadBytes)
{
throw new InvalidDataException($"Payload is too large: {payload.Length} bytes.");
}
byte[] header = new byte[sizeof(int)];
BinaryPrimitives.WriteInt32LittleEndian(header, payload.Length);
await stream.WriteAsync(header.AsMemory(0, header.Length), cancellationToken);
await stream.WriteAsync(payload.AsMemory(0, payload.Length), cancellationToken);
await stream.FlushAsync(cancellationToken);
}
public static async Task<T> ReadAsync<T>(Stream stream, CancellationToken cancellationToken)
{
byte[] header = await ReadExactAsync(stream, sizeof(int), cancellationToken);
int payloadLength = BinaryPrimitives.ReadInt32LittleEndian(header);
if (payloadLength <= 0 || payloadLength > MaxPayloadBytes)
{
throw new InvalidDataException($"Invalid payload length: {payloadLength}");
}
byte[] payload = await ReadExactAsync(stream, payloadLength, cancellationToken);
return JsonSerializer.Deserialize<T>(payload, BrokerJson.Options)
?? throw new InvalidDataException($"Failed to deserialize {typeof(T).FullName}.");
}
private static async Task<byte[]> ReadExactAsync(Stream stream, int length, CancellationToken cancellationToken)
{
byte[] buffer = new byte[length];
int offset = 0;
while (offset < length)
{
int read = await stream.ReadAsync(buffer.AsMemory(offset, length - offset), cancellationToken);
if (read == 0)
{
throw new EndOfStreamException("Pipe was closed before the expected number of bytes was read.");
}
offset += read;
}
return buffer;
}
}
Entscheidend ist, JSON nicht einfach unstrukturiert durch die Pipe fließen zu lassen, sondern es mit einer vorangestellten Länge zu senden. Ein einfaches Protokoll — eine Anfrage, eine Antwort — hält die Fehleranfälligkeit gering.
10. UI-Seite: Start und Kommunikation mit dem Helper
10.1 MyApp/ElevationBrokerClient.cs
using System.ComponentModel;
using System.Diagnostics;
using System.Globalization;
using System.IO.Pipes;
using System.Security.Principal;
using System.Text.Json;
using MyApp.BrokerProtocol;
namespace MyApp;
public sealed class ElevationBrokerClient
{
private readonly string _helperExePath;
public ElevationBrokerClient(string helperExePath)
{
_helperExePath = Path.GetFullPath(helperExePath);
if (!Path.IsPathRooted(_helperExePath))
{
throw new ArgumentException("Helper executable path must be absolute.", nameof(helperExePath));
}
if (!File.Exists(_helperExePath))
{
throw new FileNotFoundException("Helper executable was not found.", _helperExePath);
}
}
public async Task SetExplorerContextMenuEnabledAsync(bool enabled, CancellationToken cancellationToken = default)
{
string pipeName = $"myapp-broker-{Guid.NewGuid():N}";
int clientPid = Environment.ProcessId;
string clientSid = GetCurrentUserSid();
StartHelper(pipeName, clientPid, clientSid);
using var pipe = new NamedPipeClientStream(
serverName: ".",
pipeName: pipeName,
direction: PipeDirection.InOut,
options: PipeOptions.Asynchronous);
using var connectCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
connectCts.CancelAfter(TimeSpan.FromSeconds(30));
await pipe.ConnectAsync(connectCts.Token);
BrokerRequest request = new(
BrokerOperations.SetExplorerContextMenu,
JsonSerializer.SerializeToElement(
new SetExplorerContextMenuRequest(enabled),
BrokerJson.Options));
await PipeMessageSerializer.WriteAsync(pipe, request, cancellationToken);
BrokerResponse response = await PipeMessageSerializer.ReadAsync<BrokerResponse>(pipe, cancellationToken);
if (!response.Success)
{
throw new InvalidOperationException(
$"Admin broker returned an error. Code={response.ErrorCode}, Message={response.Message}");
}
}
private void StartHelper(string pipeName, int clientPid, string clientSid)
{
string workingDirectory = Path.GetDirectoryName(_helperExePath)
?? throw new InvalidOperationException("Helper executable directory could not be resolved.");
var startInfo = new ProcessStartInfo
{
FileName = _helperExePath,
Arguments = BuildArguments(pipeName, clientPid, clientSid),
WorkingDirectory = workingDirectory,
UseShellExecute = true,
Verb = "runas"
};
try
{
Process.Start(startInfo)
?? throw new InvalidOperationException("The helper process could not be started.");
}
catch (Win32Exception ex) when (ex.NativeErrorCode == 1223)
{
throw new OperationCanceledException("Die Zustimmung zur Administratorrechte-Erhöhung wurde abgebrochen.", ex);
}
}
private static string GetCurrentUserSid()
{
using WindowsIdentity identity = WindowsIdentity.GetCurrent();
return identity.User?.Value
?? throw new InvalidOperationException("Current user SID could not be resolved.");
}
private static string BuildArguments(string pipeName, int clientPid, string clientSid)
{
return string.Join(
" ",
"--pipe",
QuoteArgument(pipeName),
"--client-pid",
clientPid.ToString(CultureInfo.InvariantCulture),
"--client-sid",
QuoteArgument(clientSid));
}
private static string QuoteArgument(string value)
{
return "\"" + value.Replace("\\", "\\\\").Replace("\"", "\\\"") + "\"";
}
}
Was hier an den Helper übergeben wird, ist nur der Pipe-Name und das Minimum an Informationen, das für die Verifizierung des Verbindungsursprungs nötig ist.
Die eigentliche Administratoroperation ist in die typisierte Request eingeschlossen, die durch die Pipe gesendet wird.
Dieses QuoteArgument ist eine minimale Implementierung, die die in diesem Beispiel übergebenen einfachen Werte voraussetzt — Pipe-Name, PID, SID. Wenn Sie beliebige Windows-Pfade oder freie Zeichenketten als Kommandozeilenargumente übergeben, ersetzen Sie es durch ein dediziertes Escaping, das den Windows-Regeln für die argv-Analyse folgt.
11. Helper-Seite: Parsen der Startargumente
11.1 MyApp.AdminBroker/BrokerLaunchOptions.cs
namespace MyApp.AdminBroker;
internal sealed class BrokerLaunchOptions
{
public required string PipeName { get; init; }
public required int ExpectedClientProcessId { get; init; }
public required string ClientUserSid { get; init; }
public static BrokerLaunchOptions Parse(string[] args)
{
string? pipeName = null;
int? clientPid = null;
string? clientSid = null;
for (int i = 0; i < args.Length; i++)
{
switch (args[i])
{
case "--pipe":
pipeName = ReadNextValue(args, ref i, "--pipe");
break;
case "--client-pid":
string pidText = ReadNextValue(args, ref i, "--client-pid");
if (!int.TryParse(pidText, out int pid) || pid <= 0)
{
throw new ArgumentException($"Invalid client PID: {pidText}");
}
clientPid = pid;
break;
case "--client-sid":
clientSid = ReadNextValue(args, ref i, "--client-sid");
break;
default:
throw new ArgumentException($"Unknown argument: {args[i]}");
}
}
if (string.IsNullOrWhiteSpace(pipeName))
{
throw new ArgumentException("--pipe is required.");
}
if (clientPid is null)
{
throw new ArgumentException("--client-pid is required.");
}
if (string.IsNullOrWhiteSpace(clientSid))
{
throw new ArgumentException("--client-sid is required.");
}
return new BrokerLaunchOptions
{
PipeName = pipeName,
ExpectedClientProcessId = clientPid.Value,
ClientUserSid = clientSid
};
}
private static string ReadNextValue(string[] args, ref int index, string optionName)
{
if (index + 1 >= args.Length)
{
throw new ArgumentException($"A value is required after {optionName}.");
}
index++;
return args[index];
}
}
Die Helper-Seite meldet einen Fehler, sobald Argumente fehlen oder überzählige Argumente vorhanden sind. Innerhalb der Elevationsgrenze sollte man auf ein „interpretieren wir es eben so gut wie möglich“ verzichten.
12. Helper-Seite: Pipe-Erstellung, Verifizierung der Verbindungs-PID und Dispatch
12.1 MyApp.AdminBroker/Program.cs
using System.ComponentModel;
using System.IO.Pipes;
using System.Runtime.InteropServices;
using System.Security.AccessControl;
using System.Security.Principal;
using System.Text.Json;
using MyApp.BrokerProtocol;
namespace MyApp.AdminBroker;
internal static class Program
{
public static async Task<int> Main(string[] args)
{
BrokerLaunchOptions options = BrokerLaunchOptions.Parse(args);
using var brokerCts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
using NamedPipeServerStream pipe = CreatePipeServer(options);
await pipe.WaitForConnectionAsync(brokerCts.Token);
VerifyClientProcessId(pipe, options.ExpectedClientProcessId);
BrokerRequest request = await PipeMessageSerializer.ReadAsync<BrokerRequest>(pipe, brokerCts.Token);
BrokerResponse response = await DispatchAsync(request);
await PipeMessageSerializer.WriteAsync(pipe, response, brokerCts.Token);
return response.Success ? 0 : 2;
}
private static Task<BrokerResponse> DispatchAsync(BrokerRequest request)
{
try
{
return request.Operation switch
{
BrokerOperations.SetExplorerContextMenu => HandleSetExplorerContextMenuAsync(request.Payload),
_ => Task.FromResult(
BrokerResponse.Fail(
"unsupported_operation",
$"Unsupported operation: {request.Operation}"))
};
}
catch (JsonException ex)
{
return Task.FromResult(BrokerResponse.Fail("invalid_payload", ex.Message));
}
catch (Exception ex)
{
return Task.FromResult(BrokerResponse.Fail("broker_failure", ex.Message));
}
}
private static NamedPipeServerStream CreatePipeServer(BrokerLaunchOptions options)
{
var pipeSecurity = new PipeSecurity();
var clientSid = new SecurityIdentifier(options.ClientUserSid);
SecurityIdentifier helperSid = WindowsIdentity.GetCurrent().User
?? throw new InvalidOperationException("Helper user SID could not be resolved.");
pipeSecurity.AddAccessRule(new PipeAccessRule(
clientSid,
PipeAccessRights.ReadWrite,
AccessControlType.Allow));
pipeSecurity.AddAccessRule(new PipeAccessRule(
helperSid,
PipeAccessRights.FullControl,
AccessControlType.Allow));
pipeSecurity.AddAccessRule(new PipeAccessRule(
new SecurityIdentifier(WellKnownSidType.LocalSystemSid, null),
PipeAccessRights.FullControl,
AccessControlType.Allow));
return NamedPipeServerStreamAcl.Create(
options.PipeName,
PipeDirection.InOut,
maxNumberOfServerInstances: 1,
transmissionMode: PipeTransmissionMode.Byte,
options: PipeOptions.Asynchronous | PipeOptions.WriteThrough,
inBufferSize: 0,
outBufferSize: 0,
pipeSecurity: pipeSecurity);
}
private static void VerifyClientProcessId(NamedPipeServerStream pipe, int expectedClientProcessId)
{
if (!GetNamedPipeClientProcessId(
pipe.SafePipeHandle.DangerousGetHandle(),
out uint actualClientProcessId))
{
throw new Win32Exception(Marshal.GetLastWin32Error());
}
if (actualClientProcessId != (uint)expectedClientProcessId)
{
throw new InvalidOperationException(
$"Unexpected pipe client PID. Expected={expectedClientProcessId}, Actual={actualClientProcessId}");
}
}
private static Task<BrokerResponse> HandleSetExplorerContextMenuAsync(JsonElement payload)
{
SetExplorerContextMenuRequest request = payload.Deserialize<SetExplorerContextMenuRequest>(BrokerJson.Options)
?? throw new JsonException("Payload could not be parsed.");
ExplorerContextMenuRegistration.Apply(request.Enabled);
return Task.FromResult(BrokerResponse.Ok("Explorer context menu setting was updated."));
}
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool GetNamedPipeClientProcessId(
IntPtr pipe,
out uint clientProcessId);
}
Hier greifen folgende Punkte:
- Das ACL der Pipe wird explizit zusammengestellt
- Das ACL wird nicht nur der aktuellen Benutzer-SID des Helpers, sondern auch der SID des aufrufenden UI-Benutzers erteilt
- Nach der Verbindung wird die Client-PID verifiziert
- Auch nach Empfang der Request erfolgt der Dispatch nach Operationsname
Hält man die Form ein, bei der switch (request.Operation) nur feste Operationen durchlässt, wird der Helper seltener zu einer „erhöhten Alles-Box“.
13. Die eigentliche Administratoroperation: Registrierung des Explorer-Rechtsklickmenüs
13.1 MyApp.AdminBroker/ExplorerContextMenuRegistration.cs
using System;
using System.IO;
using Microsoft.Win32;
namespace MyApp.AdminBroker;
internal static class ExplorerContextMenuRegistration
{
private const string MenuKeyPath = @"SOFTWARE\Classes\*\shell\MyApp.Open";
private const string CommandKeyPath = @"SOFTWARE\Classes\*\shell\MyApp.Open\command";
private const string MenuText = "Open with MyApp";
private const string ClientExecutableName = "MyApp.exe";
public static void Apply(bool enabled)
{
string clientExePath = ResolveClientExecutablePath();
using RegistryKey hklm = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, GetRegistryView());
if (enabled)
{
using RegistryKey menuKey = hklm.CreateSubKey(MenuKeyPath)
?? throw new InvalidOperationException($"Failed to create registry key: {MenuKeyPath}");
menuKey.SetValue(null, MenuText, RegistryValueKind.String);
menuKey.SetValue("Icon", $"\"{clientExePath}\",0", RegistryValueKind.String);
using RegistryKey commandKey = hklm.CreateSubKey(CommandKeyPath)
?? throw new InvalidOperationException($"Failed to create registry key: {CommandKeyPath}");
commandKey.SetValue(null, $"\"{clientExePath}\" \"%1\"", RegistryValueKind.String);
}
else
{
hklm.DeleteSubKeyTree(@"SOFTWARE\Classes\*\shell\MyApp.Open", throwOnMissingSubKey: false);
}
}
private static string ResolveClientExecutablePath()
{
string clientExePath = Path.GetFullPath(
Path.Combine(AppContext.BaseDirectory, ClientExecutableName));
if (!File.Exists(clientExePath))
{
throw new FileNotFoundException("Client executable was not found.", clientExePath);
}
return clientExePath;
}
private static RegistryView GetRegistryView()
{
return Environment.Is64BitOperatingSystem
? RegistryView.Registry64
: RegistryView.Registry32;
}
}
Der Kern dieses Codes liegt darin, was er von der UI nicht entgegennimmt.
- Er nimmt von der UI keine beliebigen Registrierungspfade entgegen
- Er nimmt von der UI keine beliebigen Kommandozeichenketten entgegen
- Die zu registrierende EXE wird auf der Helper-Seite fest aufgelöst
- Der Inhalt der Request ist nur
Enabled
Der Helper ist also so eingerichtet, dass er genau eine Bedeutung hat: „den Registrierungsstatus des Explorer-Rechtsklickmenüs umschalten“.
14. Aufrufbeispiel aus der UI
14.1 MyApp/SettingsPage.xaml.cs
using System.Windows;
namespace MyApp;
public partial class SettingsPage
{
private readonly ElevationBrokerClient _broker = new(
Path.Combine(AppContext.BaseDirectory, "MyApp.AdminBroker.exe"));
private async void ExplorerMenuCheckBox_Click(object sender, RoutedEventArgs e)
{
bool enabled = ExplorerMenuCheckBox.IsChecked == true;
try
{
await _broker.SetExplorerContextMenuEnabledAsync(enabled);
MessageBox.Show("Die Einstellung wurde aktualisiert.", "MyApp");
}
catch (OperationCanceledException)
{
MessageBox.Show("Die Administratorgenehmigung wurde abgebrochen.", "MyApp");
ExplorerMenuCheckBox.IsChecked = !enabled;
}
catch (Exception ex)
{
MessageBox.Show(ex.Message, "Die Einstellung konnte nicht aktualisiert werden.");
ExplorerMenuCheckBox.IsChecked = !enabled;
}
}
}
Die UI-Seite ist gewöhnlich.
- Den Zustand der Checkbox lesen
- Den Broker-Client aufrufen
- Bei einem Fehler die UI zurücksetzen
Das ist alles. Sie greift nicht direkt auf die Registry zu. Genau das ist Trennung.
15. Was diese Implementierung einhält
Die Linien, die dieses Beispiel tatsächlich verteidigt, sehen so aus.
15.1 Trennung der Zuständigkeiten zwischen UI und Helper
- Die UI nimmt nur Benutzeraktionen entgegen
- Der Helper führt nur feste Administratoroperationen aus
15.2 Keine „Öffnung für beliebige Ausführung“ im Helper
- Er nimmt keine beliebigen Registrierungspfade entgegen
- Er nimmt keine beliebigen Kommandozeilen entgegen
- Er nimmt keine beliebigen EXE-Pfade entgegen
15.3 Der Startpfad ist fest
- Die Helper-EXE wird per absolutem Pfad angegeben
runaswird explizit angegebenUseShellExecute = truewird explizit angegeben
15.4 Der Verbindungsursprung der IPC ist eingegrenzt
- Das ACL der Pipe ist auf die SID des UI-Benutzers beschränkt
- Nach der Verbindung wird die Client-PID geprüft
15.5 Auch das Ziel der Administratoroperation ist fest
- Hive und Pfad der Registry sind fest
- Auch die zu registrierende EXE wird fest aufgelöst
Geht man so weit, entfernt man sich deutlich vom Zustand „ist die UI kompromittiert, lässt sich über den Helper alles anstellen“.
15.6 Prüfen, ob die Trennung tatsächlich gelungen ist
Bis hierhin ging es um das Design. Ob das Geschriebene wirklich getrennt ist, lässt sich nur durch tatsächliches Ausführen und Prüfen feststellen. „Die gesamte UI war unbemerkt erhöht“ ist ein Unfall, den man selbst beim Lesen des Codes leicht übersieht.
Die Prüfung erfolgt der Reihe nach anhand dieser vier Punkte.
1. Bleibt der UI-Prozess nicht erhöht?
Das ist der wichtigste Punkt. Starten Sie die UI und prüfen Sie, nachdem Sie einmal eine Administratoroperation ausgeführt haben.
- Task-Manager: Klicken Sie im Tab „Details“ mit der rechten Maustaste auf die Spaltenüberschriften und blenden Sie die Spalte „Erhöht“ ein. Zeigt
MyApp.exe„Nein“ und nurMyApp.AdminBroker.exe„Ja“, entspricht das den Erwartungen - Process Explorer: Blenden Sie die Spalte „Integrity“ ein. Steht die UI auf
Mediumund der Helper aufHigh, ist es korrekt (die Zuordnung der Integritätsstufe von Windows entspricht 2.1: Standardbenutzer = medium, erhöht = high)
Wer es aus dem Code sehen möchte: Eine einmalige Prüfung direkt nach dem Start der UI genügt.
using System.Security.Principal;
using WindowsIdentity identity = WindowsIdentity.GetCurrent();
var principal = new WindowsPrincipal(identity);
// Im UI-Prozess sollte dies false sein
bool isElevatedAdmin = principal.IsInRole(WindowsBuiltInRole.Administrator);
2. Erscheint der UAC-Prompt nur beim Start des Helpers?
- Erscheint der Prompt schon beim Start der UI -> das Manifest der UI-Seite ist nicht auf
asInvokergesetzt - Erscheint er in dem Moment, in dem die Einstellungs-Checkbox gedrückt wird -> das entspricht den Erwartungen
- Ändert sich die Einstellung, ohne dass jemals ein Prompt erscheint -> möglicherweise ist der Helper über einen anderen Weg dauerhaft erhöht
Bei einem Administratorkonto erscheint eine Zustimmungsabfrage, bei einem Standardbenutzer eine Anmeldeinformationsabfrage (Tabelle in 5.6). Probieren Sie beide aus, um auch die korrekte Übergabe der SID zu prüfen.
3. Hat die Administratoroperation tatsächlich gewirkt?
Bei der Menüregistrierung im Explorer ist es am schnellsten, direkt in die Registry zu schauen.
reg query "HKLM\SOFTWARE\Classes\*\shell\MyApp.Open" /s
Prüfen Sie die Deregistrierung ebenso. Testen Sie nicht nur die Registrierung, sondern auch die Deregistrierung — sonst bleibt ein Fehler auf der Seite von DeleteSubKeyTree unentdeckt.
4. Schlägt es „dann fehl, wenn es fehlschlagen sollte“?
Ohne diese Prüfung lässt sich nicht sagen, ob die Trennung gelungen ist.
- Den Elevationsprompt abbrechen -> die Einstellung darf sich nicht ändern, und auch die Checkbox der UI muss zurückgesetzt werden (die Behandlung von
ERROR_CANCELLED= 1223, Kapitel 10) - Den Helper direkt starten -> auch wenn man ihn von Hand aufruft, etwa
MyApp.AdminBroker.exe --pipe x --client-pid 1 --client-sid S-1-5-18, darf die Verarbeitung wegen der Verifizierung des Verbindungsursprungs und des Timeouts nicht fortschreiten - Eine Operation senden, die nicht in der Allowlist steht -> sie muss mit
unsupported_operationzurückgewiesen werden (DispatchAsyncaus Kapitel 12)
Die konkreten Befehle für diese Schritte stehen im selben Ablauf im Abschnitt „Prüfschritte unter Windows“ der README des Beispiels.
16. Häufige Fehler
16.1 Die gesamte UI auf requireAdministrator setzen
Obwohl nur ein einzelner Button im Einstellungsdialog Administratorrechte braucht, startet alles erhöht. Das walzt die Rechtegrenze nachlässig platt.
16.2 Dem Helper eine rohe Zeichenketten-Kommandozeile übergeben
Zum Beispiel ein Design wie dieses:
UI -> Helper erhält "reg add HKLM\\.... /v ... /d ..."
Damit wird der Helper zu einem Command-Executor. Besser lassen.
16.3 Das Standard-ACL der Named Pipe unverändert übernehmen
„Es ist ja lokale IPC, das wird schon gutgehen“ ist etwas gefährlich. Eine Pipe unterliegt der Windows-Sicherheit, daher sollte man wirklich ein eigenes ACL aufbauen.
16.4 Vorschnell zu CurrentUserOnly greifen
Das wirkt praktisch, passt aber nicht zur hier vorliegenden Kombination aus UI auf medium integrity und Helper auf high integrity. Hier ist ein explizites ACL die handhabbarere Wahl.
16.5 Der Helper nimmt beliebige Pfade entgegen und operiert darauf
Zum Beispiel so etwas:
- Eine beliebige Datei nach Program Files kopieren
- Einen beliebigen Schlüssel in HKLM schreiben
- Einen beliebigen Dienstnamen löschen
- Mit einem beliebigen Befehl eine Firewallregel hinzufügen
Nimmt der Helper das entgegen, wird der Helper selbst zu einer allgemeinen Ausführungsöffnung mit Administratorrechten. Operationen sollten unbedingt fest vorgegeben sein.
17. Zusammenfassung
Dass in einer Windows-App „nur ein Teil der Verarbeitung Administratorrechte braucht“, ist keine Seltenheit.
Die Lösung dafür ist jedoch nicht „alles auf requireAdministrator setzen“, sondern eine Ausführungsgrenze zu ziehen.
Die Form, die man am leichtesten zuerst aufgreifen kann, ist diese:
- Die UI läuft unter
asInvoker - Administratorverarbeitung wird in eine Helper-EXE ausgelagert
- Der Helper erhält
requireAdministrator - Der Start erfolgt über
runas - Die Kommunikation läuft über eine Named Pipe
- Der Helper akzeptiert nur feste Operationen
- Der ACL der Pipe und die Client-PID grenzen den Verbindungsursprung ein
- Der Helper validiert die Argumente auf seiner Seite erneut
Mit dieser Form fällt es später auch leichter, auf einen Dienst umzustellen. Trennt man den Operationsvertrag sauber, wird die Grenze zwischen UI und Administratorverarbeitung selbst zu einem Designvermögen.
Bei Sicherheit zählt weniger, auffällige Funktionen hinzuzufügen, als vielmehr, keine nachlässigen Grenzen zurückzulassen. Für Administratorrechte gilt dasselbe. Nicht alles gebündelt vergeben, sondern nur dort, wo es nötig ist, so eng wie möglich. Diese Art von unauffälliger Sorgfalt zahlt sich später aus.
18. Referenzen
Bei einigen der folgenden Links enthält die URL eine Versionsangabe wie view=net-10.0. Das gibt lediglich an, welche .NET-Version Microsoft Learn auf der Seite anzeigt, und bedeutet nicht, dass ein Widerspruch zur .NET-8-Voraussetzung dieses Artikels besteht. PipeOptions / NamedPipeServerStreamAcl / RegistryView, wie sie hier verwendet werden, stehen alle auch unter .NET 8 zur Verfügung. Wenn Sie die Anzeige auf .NET 8 umstellen möchten, nutzen Sie den Versionswähler oben auf der jeweiligen Seite.
- Das vollständige Beispielcode-Paket dieses Artikels (gemeinsame Vertragsbibliothek, Demo, Unit-Tests) https://github.com/gomurin0428/komurasoft-blog-samples/tree/main/windows-admin-broker-deep-dive
- Ursprünglicher Artikel: Eine Mindest-Checkliste für die Sicherheit in der Windows-Anwendungsentwicklung /de/blog/windows-app-security-minimum-checklist/
- Administrator Broker Model - Win32 apps https://learn.microsoft.com/de-de/windows/win32/secauthz/administrator-broker-model
- Developing Applications that Require Administrator Privilege https://learn.microsoft.com/en-us/windows/win32/secauthz/developing-applications-that-require-administrator-privilege
- Operating System Service Model - Win32 apps https://learn.microsoft.com/de-de/windows/win32/secauthz/operating-system-service-model
- Elevated Task Model - Win32 apps https://learn.microsoft.com/en-us/windows/win32/secauthz/elevated-task-model
- Administrator COM Object Model - Win32 apps https://learn.microsoft.com/de-de/windows/win32/secauthz/administrator-com-object-model
- The COM Elevation Moniker https://learn.microsoft.com/en-us/windows/win32/com/the-com-elevation-moniker
- How User Account Control works https://learn.microsoft.com/en-us/windows/security/application-security/application-control/user-account-control/how-it-works
- Mandatory Integrity Control - Win32 apps https://learn.microsoft.com/en-us/windows/win32/secauthz/mandatory-integrity-control
- Process Explorer - Sysinternals https://learn.microsoft.com/en-us/sysinternals/downloads/process-explorer
- WindowsPrincipal.IsInRole Method https://learn.microsoft.com/en-us/dotnet/api/system.security.principal.windowsprincipal.isinrole
- ProcessStartInfo.UseShellExecute https://learn.microsoft.com/de-de/dotnet/fundamentals/runtime-libraries/system-diagnostics-processstartinfo-useshellexecute
- Named Pipe Security and Access Rights https://learn.microsoft.com/de-de/windows/win32/ipc/named-pipe-security-and-access-rights
- PipeOptions Enum https://learn.microsoft.com/en-us/dotnet/api/system.io.pipes.pipeoptions?view=net-10.0
- NamedPipeServerStreamAcl.Create https://learn.microsoft.com/en-us/dotnet/api/system.io.pipes.namedpipeserverstreamacl.create?view=net-10.0
- GetNamedPipeClientProcessId https://learn.microsoft.com/de-de/windows/win32/api/winbase/nf-winbase-getnamedpipeclientprocessid
- RegistryView Enum https://learn.microsoft.com/de-de/dotnet/api/microsoft.win32.registryview?view=net-8.0
Verwandte Artikel
Aktuelle Artikel mit denselben Schlagwörtern führen zu verwandten Themen weiter.
Geheimnisse in Windows-Apps speichern – Klartextkonfiguration mit DPAPI vermeiden
Damit Windows-Apps Verbindungsdaten und API-Tokens nicht im Klartext in Konfigurationsdateien speichern, ordnet dieser Artikel das Konzep...
Eine Mindest-Sicherheitscheckliste für die Windows-Anwendungsentwicklung
Ordnet für WPF / WinForms / WinUI / C++ / C#-Businessanwendungen die Grundlagen zu Rechten, Signatur, Updates, Geheimnissen, HTTPS, Einga...
Wann sind unter Windows tatsächlich Administratorrechte nötig? - UAC, geschützte Bereiche und wie man es am Design erkennt
Wir ordnen praxisnah ein, wann unter Windows Administratorrechte erforderlich sind – aus der Perspektive von UAC, geschützten Bereichen, ...
Die Tiefen von Windows I/O (Teil 6, Finale) ── Filtertreiber und Minifilter: Warum Procmon und Virenscanner sich in die I/O einklinken können
Der Abschluss einer bebilderten Artikelserie über Windows-Filtertreiber und Minifilter. Er behandelt den Filter-Manager und Altitudes, Pr...
Wenn die eigene Windows-App als Virus gemeldet wird — Umgang mit Fehlalarmen von Microsoft Defender und den Auswirkungen auf die Leistung
Wir legen dar, wie richtig reagiert wird, wenn Microsoft Defender die eigene Windows-App als Malware meldet: wie moderne Antivirus-Erkenn...
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
Das Thema berührt das Rechte-Design einer ganzen Windows-App — UAC, Helper-EXEs, die Entscheidung zwischen Broker und Dienst und rechnerweite Konfigurationsänderungen —, weshalb es gut zur Windows-Anwendungsentwicklung passt.
Technische Beratung und Design-Review
Wer sich vom gewohnheitsmäßigen `requireAdministrator` einer bestehenden App lösen und Broker-Design sowie IPC-Grenzen neu ordnen möchte, kann dieses Thema gut als technische Beratung / Design-Review angehen.
Häufige Fragen
Fragen, die in Beratungen zu diesem Artikelthema häufig gestellt werden.
- Kann ich innerhalb desselben Prozesses nur einen Teil der Verarbeitung mit Administratorrechten ausführen?
- Nein. Die UAC von Windows steuert nicht auf Funktionsebene, sondern danach, mit welchem Token und welcher Integritätsstufe ein Prozess läuft. Eltern- und Kindprozesse erben das Token auf derselben Integritätsstufe, daher lässt sich innerhalb eines nicht erhöhten UI-Prozesses keine einzelne Methode plötzlich mit Administratorrechten ausführen. Die benötigte Verarbeitung wird stattdessen in eine andere Ausführungseinheit ausgelagert — einen separaten Prozess, einen Dienst, eine geplante Aufgabe oder erhöhtes COM.
- Welche Möglichkeiten gibt es, um Vorgänge zu isolieren, die Administratorrechte benötigen?
- Microsoft Learn nennt im Wesentlichen vier Modelle. Das Administrator Broker Model kombiniert eine UI mit Standardrechten mit einer Administrator-Helper-EXE, das Operating System Service Model nutzt einen residenten Dienst, das Elevated Task Model nutzt eine geplante Aufgabe mit Administratorrechten, und das Administrator COM Object Model nutzt erhöhtes COM. Sind Administratoraufgaben nur sporadisch nötig und reicht es, UAC genau im richtigen Moment anzuzeigen, passt eine Broker-EXE; ist die Aufgabe ständig, unbeaufsichtigt und häufig, passt ein Dienst; für kurze, einmalige Standardaufgaben passt eine Aufgabe (Task).
- Kann ich für die Kommunikation mit einer per runas gestarteten Helper-EXE Standard-Ein-/Ausgabe verwenden?
- Das lässt sich nur schwer nutzen und sollte vermieden werden. In .NET ist ProcessStartInfo.Verb nur wirksam, wenn UseShellExecute=true gesetzt ist, und mit UseShellExecute=true lässt sich keine Kommunikation mehr aufbauen, die auf der Umleitung von Standard-Ein-/Ausgabe beruht. Für den Austausch mit dem Helper bietet sich daher IPC über Named Pipes an. Verlassen Sie sich dabei nicht auf das Standard-ACL der Pipe, sondern setzen Sie explizit eine PipeSecurity, die die Verbindungsberechtigung auf die SID des aufrufenden Benutzers beschränkt, und prüfen Sie zusätzlich mit GetNamedPipeClientProcessId die PID des Verbindungsursprungs.
- Ist PipeOptions.CurrentUserOnly bei einer Named Pipe nicht sicher genug?
- Für die Kommunikation zwischen einer nicht erhöhten UI und einem erhöhten Helper ist das nicht geeignet. CurrentUserOnly prüft unter Windows nicht nur das Benutzerkonto, sondern auch die Erhöhungsstufe, sodass sich Prozesse mit unterschiedlicher Integritätsstufe darüber nicht verbinden können. Hinzu kommt, dass UAC in einer Umgebung mit Standardrechten zu einer Anmeldeinformationsabfrage wird und der Helper dann unter einem anderen Administratorkonto laufen kann. Praktikabler ist es, wenn die UI-Seite ihre eigene SID ermittelt und an den Helper übergibt und der Helper die Verbindungsberechtigung für die Pipe explizit nur dieser SID erteilt.
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.