Ein Überblick über MS-RPC und seine Sicherheitsmechanismen
Was ist RPC?
Ein Remote Procedure Call (RPCRemoteprozeduraufruf) ist eine Form der Kommunikation zwischen Prozessen (IPC). Er ermöglicht es einem Client, eine Prozedur aufzurufen, die von einem RPC-Server freigelegt wird. Der Client ruft die Funktion wie einen normalen Prozeduraufruf auf, (fast) ohne Details für die Remote-Interaktion codieren zu müssen. Der Server kann in einem anderen Prozess auf demselben Rechner oder auf einem Remote-Rechner gehostet werden.
In diesem Artikel befassen wir uns mit der Implementierung von Microsoft RPC (MS-RPC). MS-RPC wird von der Referenzimplementierung (V1.1) des RPC-Protokolls im Kern der Distributed Computing Environment abgeleitet.
RPC wird von Windows für viele verschiedene Services verwendet, wie z. B. Aufgabenplanung, Serviceerstellung, Drucker- und Freigabeeinstellungen und die Verwaltung verschlüsselter Daten, die remote gespeichert werden. RPC ist ein Remote-Vektor und erhält daher aus Sicht der Sicherheit viel Aufmerksamkeit. In diesem Blogbeitrag wird die grundlegende Funktionsweise von MS-RPC behandelt, wobei der Schwerpunkt auf den integrierten Sicherheitsmechanismen liegt.
Wie funktioniert MS-RPC?
Die Verfahren und ihre Parameter werden in einer beschreibenden Sprache definiert, der sogenannten Interface Definition Language (IDL). Jeder Schnittstelle wird eine einmalige ID zugewiesen, die UUID, die sowohl vom Server als auch vom Client verwendet wird, um die exakte Schnittstelle des Servers zu adressieren.
Die IDL-Datei wird dann mit dem Microsoft IDL-Compiler in Header- und Quellcodedateien, die Laufzeitfunktionen enthalten, kompiliert. Technisch gesehen handelt es sich dabei um Stubs, die sowohl vom Server als auch vom Client verwendet werden. Sie übergeben die Steuerung an die RPC-Laufzeit, die in rpcrt4.dll implementiert wird. Die RPC-Laufzeit auf der Clientseite führt ein Marshalling der Daten durch und übergibt sie an die RPC-Laufzeit auf der anderen Seite (Abbildung 1).
Abb. 1: RPC-Laufzeit
Sie fragen sich vielleicht, wie die Kommunikation zwischen Server und Client aus einer Netzwerk- (oder lokalen) Perspektive abläuft. Der Server registriert eine Kombination aus einer Protokollsequenz und einem Endpunkt und wartet so auf eingehende RPC-Verbindungen. Eine Protokollsequenz kann z. B. folgendermaßen aussehen: ncacn_ip_tcp (TCP), ncacn_np (benannte Pipe), oder ncalrpc (LPC). Der Endpunkt kann ein Port wie TCP 5555 oder \\pipe\\example sein, wenn eine benannte Pipe verwendet wird. Benannte Pipes werden über den SMB-Transport über den TCP-Port 445 übertragen, wobei die versteckte IPC$-Freigabe verwendet wird. Die vollständige Liste der Protokollsequenzen ist auf der Microsoft-Website verfügbar.
Der Server wartet also auf Verbindungen an einem Endpunkt. Woher weiß der Client, wo die Verbindung hergestellt werden muss? Die Antwort hängt von der Art des Endpunkts ab – dynamisch oder bekannt (statisch).
Ein dynamischer Endpunkt ist ein Endpunkt, der über den Endpunkt-Mapper auf der Serverseite registriert wird. Der Endpunkt-Mapper (auch epmapper) ist ein RPC-Service, der einen Service dem tatsächlichen Endpunkt zuordnet. Der epmapper verwendet für RPC über HTTP die TCP-Ports 135 und 593. Daher kann ein Client (mit festgelegten APIs) alle dynamisch registrierten RPC-Server auf einem Remote-Rechner aufzählen, die den epmapper verwenden.
Ein bekannter Endpunkt ist ein Endpunkt, der nicht über den epmapper registriert wurde. Der Client sollte den Endpunkt kennen, den der Server im Voraus registriert hat. Dazu muss der Endpunkt im Code von Client und Server hartcodiert sein oder der Endpunkt in der IDL-Datei stehen. Hier ist ein Beispiel aus dem IDL des Print Spooler-Services.
[
uuid(12345678-1234-ABCD-EF00-0123456789AB),
version(1.0),
ms_union,
endpoint("ncacn_np:[\\pipe\\spoolss]"),
pointer_default(unique)
]
RPC stellt eine logische Verbindung zwischen einem Client und einem Server über ein Bindungshandle dar. Sowohl Server als auch Client verwenden Funktionen, um die Bindungsdaten zu bearbeiten, z. B. um Authentifizierungsinformationen festzulegen. Wenn der Client die Authentifizierung für die Bindung festlegt, wird die Bindung als authentifiziert betrachtet. Wenn das Client-Programm die RPC-Funktion aufruft, erfolgt die Netzwerkbindung. Aus Sicht des Netzwerktraffics beginnt der RPC-Client die RPC-Interaktion, indem er eine Bindungsanfrage mit Authentifizierungsinformationen im Paket sendet. Der Server kann mit bind_ack (bestätigt) oder bind_nak (ein Fehler ist aufgetreten) antworten.
Abb. 2: Wireshark-Snippet mit dynamischer Endpunktauflösung
Im Wireshark-Snippet in Abbildung 2 sehen Sie die dynamische Endpunktauflösung für die Task Scheduler-Schnittstelle. Der Client erstellt eine neue Verbindung, sobald er Informationen über die Endpunkte für den Task Scheduler verfügt. Dann ist eine neue Verbindung mit einem anderen Bindungsprozess, einschließlich eines AUTH3-Pakets als Teil der Bindungsauthentifizierung, sichtbar.
Sowohl Endpunkte als auch Bindungen haben Typen: automatisch, implizit und explizit (Abbildung 3). Sie unterscheiden sich in dem Punkt, wie viel Kontrolle die Anwendung über den Bindungsprozess hat.
- Automatische Bindung (veraltet) – Die Client- und Serveranwendungen verarbeiten den Bindungsprozess nicht und überlassen stattdessen der RPC-Laufzeit die vollständige Kontrolle über diesen Prozess.
- Implizite Bindung – Der Client kann das Bindungshandle konfigurieren, bevor die Bindung stattfindet. Nachdem der Client eine Bindung eingerichtet hat, verarbeitet die RPC-Laufzeitbibliothek den Rest.
- Explizite Bindung – Der Client muss das Bindungshandle konfigurieren. Die RPC-Laufzeit übergibt sie dann einfach an den Server.
RPC-Sicherheit für Remote-Anfragen
Jetzt, da wir die grundlegende Funktionsweise von RPC kennen, müssen wir verstehen, welche Aktionen, Richtlinien und Mechanismen einen Client am Zugriff auf eine Funktion hindern können. Das ist sowohl aus offensiver als auch aus defensiver Perspektive interessant.
Transportauthentifizierung
Bei einigen Transporten, wie benannte Pipes oder HTTP, ist die Authentifizierung Teil des Protokolls. Beispielsweise werden benannte Pipes über SMB übertragen, das über eine Authentifizierung verfügt. Das bedeutet im Grunde, dass, wenn ein Server einen Endpunkt mit benannter Pipe registriert, nur Clients mit den Anmeldedaten eines gültigen Nutzers eine Verbindung zu diesem Endpunkt herstellen können. Ein Domain-Nutzer in derselben Domain-Umgebung ist ausreichend, um die Authentifizierungsprüfung zu bestehen. Wenn die Rechner nicht Teil einer Domain sind, muss der Client über die Anmeldedaten eines lokalen Nutzers auf dem Remote-Server verfügen (Abbildung 4). Die Ausnahmen werden später in diesem Beitrag besprochen.
Abb. 4: Beispiel für einen Systemfehler, der den Zugriff verweigert
Bindungsauthentifizierung
Der Server kann für die Authentifizierung eine Bindungsauthentifizierung verwenden. Dazu muss der Server Authentifizierungsinformationen registrieren, indem er RpcServerRegisterAuthInfo aufruft. Der Client muss den korrekten Dienstprinzipalnamen und die vom Server verwendete Security Support Provider-Methode verwenden, andernfalls erhält er vom Server die Meldung „RPC_S_UNKNOWN_AUTHN_SERVICE“.
Ein Client kann sich bei einem Server authentifizieren, indem er Authentifizierungs- und Autorisierungsdaten für die Bindung mithilfe von APIs wie RpcBindingSetAuthInfo und RpcBindingSetAuthInfoEx festlegt. Der Client bestimmt die Authentifizierungsmethode (z. B. NTLM, Kerberos, Negotiate, SCHANNEL etc.) und den Namen des Dienstprinzipals.
Zwei wichtige Dinge müssen Sie wissen:
Wenn ein Client eine Authentifizierung für die Bindung festlegt und im Server keine Authentifizierungsinformationen registriert sind, meldet der Server „RPC_S_UNKNOWN_AUTHN_SERVICE“ zurück.
Nur weil beim Server eine Authentifizierungsbindung registriert ist, muss der Client keine authentifizierte Bindung verwenden. Darüber hinaus sind gültige Anmeldedaten notwendig, damit die RPC-Laufzeit die Authentifizierungsbindung für den Client verteilt. Später in diesem Beitrag wird erklärt, wie der Server den Zugriff auf nicht authentifizierte Bindungen verhindern kann.
Es ist zu erwähnen, dass die RPC-Laufzeit die Authentifizierungsinformationen global speichert. Das bedeutet, wenn zwei RPC-Server sich denselben Prozess teilen und einer von ihnen Authentifizierungsinformationen registriert, verfügt der andere Server ebenfalls über Authentifizierungsinformationen. Ein Client kann nun die Bindung authentifizieren, wenn auf jeden der Server zugegriffen wird. Wir nennen dieses Verhalten SSPI-Multiplexingauszunutzen.
Endpoint-Sicherheit
Ein Server kann einen Sicherheitsdeskriptor für den Endpunkt festlegen. Der Sicherheitsdeskriptor ist ein allgemeiner Sicherheitsmechanismus für die Zugriffsprüfung in Windows. Mit ihm können „Regeln“ erstellt werden, mit denen festgelegt werden kann, wem der Zugriff auf ein Objekt erlaubt bzw. verweigert wird. Wenn versucht wird, auf ein Objekt zuzugreifen, vergleicht das Betriebssystem das Zugriffstoken des entsprechenden Nutzers mit dem Sicherheitsdeskriptor, um festzustellen, ob der Zugriff zulässig ist. In diesem Fall ist das Objekt der Endpunkt, und das Zugriffstoken ist das Zugriffstoken des Clients, das aus der Authentifizierung des Transportprotokolls abgeleitet wurde. Diese Prüfung gilt nur für authentifizierte Transporte wie benannte Pipes, ALPC und HTTP (wenn eine Authentifizierung verwendet wird). TCP ist ein nicht authentifiziertes Transportprotokoll und verfügt daher nicht über diese Zugriffsprüfung.
Ähnlich wie beim SSPI-Multiplexing werden auch Endpunkte gespiegelt – die Schnittstelle und der Endpunkt sind nicht gebunden. Ein Server registriert Schnittstellen und Endpunkte separat. Wenn ein Prozess mehrere registrierte Endpunkte hat, kann über jeden dieser Endpunkte auf jede in diesem Prozess registrierte Schnittstelle zugegriffen werden.
Angenommen, Sie registrieren Ihre Schnittstelle und erstellen einen Endpunkt, der auf \\pipe\mypipe hört. Ein anderer RPC-Server, der im selben Prozess gehostet wird, hat seinen eigenen Endpunkt bei TCP 7777 registriert. Auf diese Schnittstelle kann auch über TCP zugegriffen werden. Dadurch werden die Sicherheitseinschränkungen für den Endpunkt umgangen, wie z. B. beim Sicherheitsdeskriptor (Abbildung 5). Daher ist es empfehlenswert, sich nicht auf die Endpunkt-Sicherheit zu verlassen oder zu überprüfen, ob der Client das erwartete Transportprotokoll durchlaufen hat.
Abb. 5: Beispiel für die Umgehung des Deskriptors für Endpunktsicherheit aufgrund von Endpunkt-Multiplexing
Schnittstellensicherheit
Es gibt drei Möglichkeiten, eine Schnittstelle zu sichern: mit einem Sicherheitscallback, einem Sicherheitsdeskriptor für die Schnittstelle oder mit Schnittstellenflags.
Sicherheitscallback einrichten
Der erste Sicherheitsmechanismus der Schnittstelle ist ein Sicherheitscallback. Ein Sicherheitscallback ist ein nutzerdefinierter Callback, der vom Serverentwickler implementiert wird. Die Logik innerhalb des Sicherheitscallback hängt vom Entwickler ab und ermöglicht dem Server, den Zugriff auf die Schnittstelle einzuschränken. Wenn der Callback RPC_S_OK zurückgibt, dann ist der Client zulässig.
Wenn ein Sicherheitscallback registriert ist, werden nicht authentifizierte Clients automatisch von der RPC-Laufzeit abgelehnt, es sei denn, das Flag RPC_IF_ALLOW_CALLBACKS_WITH_NO_AUTH ist festgelegt.
Das Sicherheitscallback und das Flag RPC_IF_ALLOW_SECURE_ONLY einzurichten, bedeutet nicht, dass der Client hohe Rechte hat. Daher soll der Server mit dem Sicherheitscallback die Berechtigungen des Client abfragen. Das funktioniert über das Aufrufen von RpcBindingInqAuthClient. Eine weitere gängige Überprüfung ist die Transportmethode, die der Client verwendet, um die Verbindung über einen sicheren authentifizierten Transport durchzusetzen, beispielsweise benannte Pipes. Das funktioniert über das Aufrufen von RpcBindingServerFromClient → RpcBindingToStringBinding → RpcStringBindingParse und mit einem Vergleich der Protseq-Parameter (Protokollsequenz). Dadurch wird auch ein Missbrauch des Endpunkt-Multiplexing verhindert.
Sicherheitscallback-Caching
Wenn ein Sicherheitscallback registriert ist, wird er während der Sicherheitsprüfungen aufgerufen. Wenn der Sicherheitscallback erfolgreich verläuft (d. h. RPC_S_OK zurückgegeben wird) und Caching aktiviert ist, wird das Ergebnis zwischengespeichert. Wenn derselbe Client das nächste Mal eine Funktion auf der Schnittstelle aufruft, wird der Sicherheitscallback nicht erneut aufgerufen, sondern stattdessen der zwischengespeicherte Eintrag verwendet.
Caching zu implementieren, ist einfach, hängt jedoch von einigen Faktoren ab.
- Das Caching ist an den Sicherheitskontext des Clients gebunden, der aus der Bindung übernommen wird. Wenn also der Server (oder irgendein Server im Prozess) oder der Client keine Authentifizierungsbindung registriert bzw. festgelegt hat, wird das Caching für diesen Aufruf deaktiviert.
- Wenn der Server die Schnittstelle mit dem Flag RPC_IF_SEC_NO_CACHE registriert, erzwingt die RPC-Laufzeit den Aufruf des Sicherheitscallback für jeden Aufruf. Dadurch wird das Caching-Verfahren deaktiviert.
- Das nicht dokumentierte Schnittstellenflag RPC_IF_SEC_CACHE_PER_PROC wirkt sich ebenfalls auf das Caching-Verfahren aus. Wenn der Server dieses Flag spezifiziert hat, erfolgt das Caching bei jedem Aufruf und nicht bei jeder Schnittstelle . Das bedeutet, wenn der Cache einen erfolgreichen Wert für Funktion X auf Schnittstellen-TEST enthält, löst ein Aufruf von Funktion Y auf Schnittstellen-TEST erneut den Sicherheitscallback aus.
Der Caching-Mechanismus kann möglicherweise Logikschwachstellen bei jedem Sicherheitscallback verursachen, der von der Funktion, die der Client aufruft, abhängig ist. Als RPC-Entwickler oder Auditor sollten Sie sich mit dem Caching-Mechanismus auskennen. Wir haben gründliche Untersuchungen zur Caching-Implementierung veröffentlicht, einschließlich der Schwachstellen, die wir aufgrund dieses Mechanismus entdeckt haben.
Sicherheitsdeskriptor festlegen
Der zweite Mechanismus zum Sichern einer Schnittstelle besteht darin, einen Sicherheitsdeskriptor auf der Schnittstelle festzulegen. Nur mit der Funktion RpcServerRegisterIf3 kann ein Sicherheitsdeskriptor in der Schnittstelle registriert werden. Wenn kein Sicherheitsdeskriptor festgelegt ist, lautet der Standard-Sicherheitsdeskriptor folgendermaßen:
- NT AUTHORITY\ANONYMOUS LOGON
- Everyone
- NT AUTHORITY\RESTRICTED
- BUILTIN\Administrators
- SELF
Schnittstellenflags verwenden
Bei der dritten Möglichkeit zum Sichern einer Schnittstelle werden Schnittstellenflags verwendet, die beim Erstellen der Schnittstelle über RpcServerRegisterIf*-Funktionen festgelegt werden. Folgende Flags sind für die Sicherheit interessant:
RPC_IF_ALLOW_CALLBACKS_WITH_NO_AUTH – Der Sicherheitscallback wird für jeden Aufruf aufgerufen, unabhängig von der verwendeten Transportmethode oder der Authentifizierungsebene.
RPC_IF_ALLOW_LOCAL_ONLY – Nur lokale Anfragen sind zulässig.
RPC_IF_ALLOW_SECURE_ONLY – Verbindungen nur möglich für Clients mit einer höheren Authentifizierungsebene als RPC_C_AUTHN_LEVEL_NONE. Mit diesem Flag werden Clients abgelehnt, die die NULL-Session durchlaufen. Dieses Flag garantiert nicht die Berechtigungsebene des aufrufenden Nutzers, sondern nur, dass der Client gültige Anmeldedaten hat.
RPC_IF_SEC_NO_CACHE – Dieses Flag deaktiviert das Caching für den Sicherheitscallback der Schnittstelle vollständig.
RPC_IF_SEC_CACHE_PER_PROC – Dieses Flag ändert das Standardverhalten auf Aufrufbasis statt auf Schnittstellenbasis, anstatt den gesamten Caching-Mechanismus zu deaktivieren.
Systemweite Richtlinien
Es gibt je nach Rechnertyp mehrere systemweite Richtlinien: Client, Server oder Domain-Controller (DC).
Eine interessante Systemrichtlinie im Zusammenhang mit der Endpunktsicherheit ist die Richtlinie „Nicht authentifizierte RPC-Clients beschränken“. Es können drei Werte eingestellt werden.
- „Authentifiziert“ – die RPC-Laufzeit blockiert den Zugriff auf TCP-Clients, die nicht im Voraus authentifiziert wurden, wenn das Flag RPC_IF_ALLOW_CALLBACKS_WITH_NO_AUTH nicht auf der Schnittstelle festgelegt ist.
- „Authentifiziert ohne Ausnahmen“ – Alle nicht authentifizierten Verbindungen werden blockiert.
- "Keine" – Alle RPC-Clients dürfen eine Verbindung zu RPC-Servern herstellen, die auf dem Rechner ausgeführt werden.
Eine interessante Angriffsfläche für nicht authentifizierte Clients sind Schnittstellen, auf die über TCP zugegriffen werden kann und die das Flag RPC_IF_ALLOW_CALLBACKS_WITH_NO_AUTH registrieren. Der Client muss die Sicherheitscallback-Prüfung jedenfalls bestehen, um die Funktionen der Schnittstellen aufzurufen.
Wie bereits erwähnt, verfügen Verbindungen über benannte Pipes, die über SMB übertragen werden, über eine eigene Authentifizierung als Teil des SMB-Protokolls. Ein Client kann eine „anonyme Anmeldung“ (auch NULL-Session genannt) verwenden, wenn er eine Verbindung über SMB herstellt. Zwei damit zusammenhängende Systemrichtlinien lauten: „Beschränken des anonymen Zugriffs auf benannte Pipes und Freigaben“ und „Netzwerkzugriff: Benannte Pipes, auf die anonym zugegriffen werden kann“. Wenn die erste Richtlinie aktiviert ist, können nur benannte Pipes, die in der zweiten definiert sind, anonym verbunden werden. Für Workstations ist die zweite Richtlinie leer, was im Grunde bedeutet, dass Sie keine NULL-Session für Workstations in der Domain verwenden können. Bei DC-Maschinen umfasst die Liste der benannten Pipes in der Richtlinie „\pipe\netlogon“, „\pipe\samr“ und „\pipe\lsarpc“. Aus der Sicht eines Angreifers ist das interessant, da es sich hierbei um Endpunkte handelt, mit denen ein Rechner von außerhalb der Domain eine Verbindung herstellen kann.
Zu guter Letzt gibt es bezüglich der Prüfung des Deskriptors für die Endpunktsicherheit eine systemweite Richtlinie, die standardmäßig deaktiviert ist: „Netzwerkzugriff: Anonyme Nutzer erhalten „Jeder“-Berechtigungen.“ Wenn diese Option aktiviert ist, wird die Sicherheits-ID „Jeder“ dem Token hinzugefügt, das für anonyme Verbindungen erstellt wurde.
Sicherheitskontrollen der Prozedur
Der Serverprogrammierer kann Sicherheitsprüfungen als Teil der Funktionen implementieren, die auf der Schnittstelle angezeigt werden, und hat daher die Möglichkeit, die Prüflogik gemäß der aufgerufenen Funktion zu ändern oder anzupassen. Eine Standard-Sicherheitsprüfung ist eine Zugriffsprüfung, wie sie in Abbildung 6 dargestellt ist (aus srvsvc entnommen):
Abb. 6: Ein Beispiel für eine Standard-Sicherheitsprüfung, eine Zugriffsprüfung
In diesem Beispiel ruft LocalrSessionGetInfo SsAccessCheck auf. SsAccessCheck gibt sich im Grunde als Client aus, indem er RpcImpersonateClient und die Funktion NtAccessCheckAndAuditAlarm aufruft, wodurch die Zugriffsprüfung durchgeführt wird. Darauf folgt der Aufruf RpcRevertToSelf, um zum Token des Servers zurückzukehren. Es ist wichtig, den Rückgabewert von RpcImpersonateClient zu überprüfen, da der Server bei einem Ausfall weiterhin im Sicherheitstoken des Serverprozesses und nicht im Client-Prozess ausgeführt wird.
Zusammenfassung
In diesem Blogbeitrag wurden viele Informationen über MS-RPC und seine Sicherheitsmechanismen vorgestellt. Wir empfehlen anderen Forschern, sich verschiedene MS-RPC-Server anzusehen, da diese eine große Angriffsfläche mit möglichen neuen Schwachstellen darstellen. Wir hoffen, dass unser Beitrag andere Sicherheitsforscher dazu motiviert, MS-RPC weiter zu untersuchen.
Wir werden auf unserem GitHub-Repository weiterhin zahlreiche Ressourcen über RPC zusammenstellen. Vielen Dank an alle, die mit ihrem Wissen und ihrer Erfahrung zu diesem Thema einen Beitrag geleistet haben. Insbesondere James Forshaw und Carsten Sandker.