Skip to content
← Alle Beiträge
· Delta1 Labs ObfuscationNebula

Eine SignalR-Anwendung obfuskieren, ohne die Hub-Methodennamen zu zerstören, die Clients per Zeichenkette aufrufen

SignalR verteilt jede Echtzeitnachricht, indem es eine Zeichenkette auf eine Methode abbildet. Ein Client ruft `connection.InvokeAsync("SendMessage", ...)` auf und registriert `hubConnection.On("ReceiveMessage", ...)`, und der Server löst diese Zeichenketten zur Laufzeit per Name auf Hub-Methoden auf. Benennt man eine öffentliche Hub-Methode oder ein Mitglied einer stark typisierten Client-Schnittstelle um, schlägt die Verteilung stillschweigend fehl: Die Verbindung bleibt bestehen, aber die Nachricht kommt nie an. Dieser Beitrag zeigt, wie der Dispatcher von SignalR die Zielzeichenkette der Invocation auf Ihre Hub-Methode abbildet, warum `[HubMethodName]` und `Hub<T>` ändern, welche Namen kritisch sind, wie man eine Keep-Liste in nebula.config.json aufbaut, die genau die auf der Leitung sichtbare Methodenoberfläche fixiert und dabei Hub-Rümpfe, interne Dienste und DTO-Interna umbenennt, und wie man in Glass.NET bestätigt, dass nur die vorgesehenen Namen überlebt haben.

Ein SignalR-Assembly sieht aus wie gewöhnliches C#, aber eine Handvoll seiner Namen sind in Wahrheit gar keine C#-Namen: Es sind Zeichenketten auf einer Leitung, und der Obfuskator muss sie als das Gegenteil von allem behandeln, was sie umgibt.

Der Echtzeitvertrag von SignalR ist vollständig aus Zeichenketten gebaut. Ein Client ruft eine Servermethode per Name auf – connection.InvokeAsync("SendMessage", roomId, text) – und registriert einen Handler für eine Server-zu-Client-Nachricht per Name – hubConnection.On("ReceiveMessage", ...). Auf dem Server stellt Ihr Hub eine öffentliche Methode SendMessage bereit und schiebt mit Clients.All.SendAsync("ReceiveMessage", ...) an die Clients. Nichts in dieser Schleife wird vom Compiler geprüft. Die Verbindung zwischen "SendMessage" der Zeichenkette und SendMessage der Methode entsteht zur Laufzeit, per Name, im Dispatcher von SignalR. Benennen Sie die Methode um, und die Zeichenkette löst sich nicht mehr auf – der Socket bleibt offen, der Handshake gelingt trotzdem, und die Nachricht kommt schlicht nie an.

Das ist das ganze Problem. Ein blinder Umbenennungsdurchlauf plättet SendMessage zu a, und Ihre Chat-App liefert keine Nachrichten mehr aus – ohne Fehler beim Build und ohne Absturz beim Start. Die Lösung besteht nicht darin, das Obfuskieren zu lassen; sie besteht darin, genau zu wissen, welche Namen auf der Leitung liegen, und präzise diese zu fixieren, während alles andere umbenannt wird.

CLIENT — ruft per ZeichenketteInvokeAsync("SendMessage", ...)On("ReceiveMessage", handler)die Zeichenkette ist der VertragSIGNALR-DISPATCHERreflektiert Hub beim Start,baut Name → Methode-Mapbildet ZielzeichenkettePER NAME auf Methode abkeine Compiler-PrüfungDIESE NAMEN BEHALTENHub-Methode: SendMessageClient-Iface: ReceiveMessageauf Leitung sichtbar → fixierenFREI UMBENENNBARHub-MethodenrümpfeIMessageStore, Diensteinterne DTO-Mitgliederprivate Helfer, DInie als Zeichenkette gelesen

Wie SignalR einen Methodennamen tatsächlich auflöst

Beim Start Ihrer App untersucht SignalR jeden Hub-Typ und baut ein Wörterbuch seiner aufrufbaren Methoden auf, indexiert nach Name. Konzeptionell tut der Standard-Dispatcher beim Start das Äquivalent von Folgendem:

// Vereinfacht aus SignalRs DefaultHubDispatcher<THub>.
foreach (var method in typeof(ChatHub).GetMethods(/* public instance */))
{
    // Der Wörterbuchschlüssel ist der NAME der Methode — eine Zeichenkette.
    var name = method.GetCustomAttribute<HubMethodNameAttribute>()?.Name
               ?? method.Name;
    _methods[name] = new HubMethodDescriptor(method, ...);
}

Jede eingehende Invocation trägt eine target-Zeichenkette. Die Verteilung ist eine Wörterbuchsuche auf dieser Zeichenkette:

if (!_methods.TryGetValue(invocation.Target, out var descriptor))
    throw new HubException($"Unknown hub method '{invocation.Target}'.");

Der Hub-Methodenname ist also der Schlüssel, und der Schlüssel ist eine Zeichenkette, die der Compiler nie validiert. Es ist dieselbe Kategorie kritischer Namen, die man überall dort findet, wo ein Framework per Reflexion eine Grenze überschreitet – und es bedeutet, dass das Umbenennen der Methode den Schlüssel ändert, während der Client weiterhin die alte Zeichenkette sendet.

Hier ist der Hub, aus dem die Suche aufgebaut wird:

public class ChatHub : Hub
{
    private readonly IMessageStore _store;          // intern — wird frei umbenannt
    public ChatHub(IMessageStore store) => _store = store;

    // ÖFFENTLICHE Hub-Methode: "SendMessage" ist der Leitungsname, den Clients aufrufen.
    public async Task SendMessage(string roomId, string text)
    {
        var saved = await _store.AppendAsync(roomId, text, Context.ConnectionId);
        // "ReceiveMessage" ist der Leitungsname, auf den Clients lauschen.
        await Clients.Group(roomId).SendAsync("ReceiveMessage", saved.Author, saved.Text);
    }
}

Der zugehörige JavaScript-Client referenziert nie einen C#-Bezeichner – nur die beiden Zeichenketten:

const connection = new signalR.HubConnectionBuilder().withUrl("/chat").build();
connection.on("ReceiveMessage", (author, text) => appendLine(author, text));
await connection.start();
await connection.invoke("SendMessage", roomId, text);

Benennen Sie das C#-SendMessage in a um, und connection.invoke("SendMessage", ...) zielt nun auf einen Schlüssel, der nicht mehr existiert; SignalR antwortet mit Unknown hub method 'SendMessage'. Benennen Sie nichts um, liefern aber den "ReceiveMessage"-Versand unter einem wegbenannten Namen aus, so feuert das .on("ReceiveMessage") des Clients nie. Beide Fehler treten erst zur Laufzeit auf, gegen eine lebendige Verbindung.

Den Leitungsnamen mit [HubMethodName] entkoppeln

Den C#-Bezeichner zu behalten ist eine Möglichkeit, den Namen zu schützen, aber es koppelt die Benennung Ihres Codes für immer an Ihren Leitungsvertrag. Die sauberere Lösung besteht darin, den Leitungsnamen ausdrücklich anzugeben, damit der Bezeichner wieder frei wird:

public class ChatHub : Hub
{
    [HubMethodName("SendMessage")]           // der Leitungsname wird hier fixiert…
    public async Task DispatchChatMessage(   // …also kann dieser Bezeichner in 'a' umbenannt werden
        string roomId, string text)
    {
        // ...
    }
}

Jetzt registriert der Dispatcher die Methode unter "SendMessage", egal wie die C#-Methode heißt, sodass der Bezeichner DispatchChatMessage vollständig aus dem Leitungsvertrag herausfällt und umbenannt werden kann. Das lohnt sich, selbst wenn Sie nie obfuskieren: Es verhindert, dass Ihre öffentliche Echtzeit-API stillschweigend von den Namen Ihrer internen Methoden abhängt.

Stark typisierte Hubs verlagern die Namen auf eine Schnittstelle

Mit Hub<T> sind die Server-zu-Client-Aufrufe keine losen Zeichenketten mehr in Ihrem Code, sondern Methodenaufrufe auf einer Schnittstelle – aber SignalR verwandelt diese Namen der Schnittstellenmethoden weiterhin in Leitungszeichenketten:

public interface IChatClient
{
    // Der NAME jedes Mitglieds wird zum Leitungsnamen, der an die Clients gesendet wird.
    Task ReceiveMessage(string author, string text);
    Task UserJoined(string connectionId);
}

public class ChatHub : Hub<IChatClient>
{
    public async Task SendMessage(string roomId, string text)
    {
        // Stark typisiert — aber dies sendet trotzdem den Leitungsnamen "ReceiveMessage".
        await Clients.Group(roomId).ReceiveMessage("system", text);
    }
}

SignalR baut einen Proxy über IChatClient auf und sendet, wenn Sie ReceiveMessage(...) aufrufen, eine Invocation, deren Zielzeichenkette "ReceiveMessage" ist – der Mitgliedsname. Der Client registriert weiterhin hubConnection.On("ReceiveMessage", ...). Die Mitgliedsnamen der Schnittstelle liegen also genau so auf der Leitung wie öffentliche Hub-Methoden, und das Umbenennen von IChatClient.ReceiveMessage in a lässt den Server "a" aussenden, während jeder Client auf "ReceiveMessage" lauscht. Die Implementierung des Hubs hingegen ist intern und benennt sich sauber um.

Die Keep-Liste in nebula.config.json

Nebula rät nicht, welche Zeichenketten auf der Leitung liegen. Sie deklarieren die auf der Leitung sichtbare Oberfläche – die öffentlichen Hub-Methoden und die stark typisierte Client-Schnittstelle – als keep-Regeln und lassen einen Platzhalter alles andere umbenennen:

{
  "schemaVersion": 1,
  "inputs": [ "bin/Release/net8.0/Chat.Server.dll" ],
  "outputDirectory": "protected",

  "encryptStrings": true,
  "controlFlowObfuscation": true,

  "renaming": {
    "rules": [
      { "pattern": "Chat.Server.Hubs.*",      "action": "keep",   "members": "public" },
      { "pattern": "Chat.Server.Contracts.I*Client", "action": "keep", "members": "public" },
      { "pattern": "Chat.Server.*",            "action": "rename" }
    ]
  }
}

Die erste Regel fixiert die Hub-Typen und die Namen ihrer öffentlichen Methoden – SendMessage überlebt –, während das private Feld _store, die Konstruktor-Verdrahtung und jeder private Helfer im Hub sich trotzdem umbenennen, weil members: "public" das Behalten auf die öffentliche Oberfläche beschränkt. Die zweite Regel fixiert die stark typisierten Client-Schnittstellen (hier über die Konvention I…Client erkannt) und ihre Mitgliedsnamen, sodass ReceiveMessage und UserJoined intakt bleiben. Der abschließende rename-Platzhalter fegt alles andere im Assembly – Ihren IMessageStore und seine Implementierung, DTO-Interna, Interceptoren, DI-Registrierungen – in die vollständige Umbenennung.

Wenn Sie [HubMethodName] auf jeder Hub-Methode und ausdrückliche Namen auf der Client-Schnittstelle übernommen haben, können Sie sogar die ersten beiden Regeln verschärfen, denn die Leitungsnamen leben dann gar nicht mehr in den Bezeichnern: Nur die Attributwerte zählen, und das sind Zeichenkettenliterale, die Nebula bewahrt. Für eine einzelne Hub-Methode, die die Ausnahme und nicht die Regel ist, ist ein Attribut auf genau dem Mitglied präziser als ein Konfigurationsmuster und lebt direkt neben dem Code:

[Obfuscation(Feature = "renaming", Exclude = true, ApplyToMembers = false)]
public async Task SendMessage(string roomId, string text) { /* ... */ }

Führen Sie den Schutz als Teil des Veröffentlichungs-Builds aus, nicht auf Entwicklerrechnern:

<Target Name="NebulaProtect" AfterTargets="Publish"
        Condition="'$(CI)' == 'true'">
  <Exec Command="nebula protect --config nebula.config.json" />
</Target>

Oder rufen Sie das CLI direkt gegen einen veröffentlichten Build auf:

nebula protect --config nebula.config.json

Die Oberfläche mit Glass.NET überprüfen

Eine Keep-Liste ist eine Behauptung darüber, welche Namen überlebt haben, und Sie sollten die Behauptung prüfen, statt ihr zu vertrauen. Öffnen Sie das geschützte Assembly in Glass.NET, dem kostenlosen .NET-Decompiler von Delta1 Labs, und schauen Sie sich zwei Dinge an.

Zunächst die behaltene Oberfläche. Ihr ChatHub sollte weiterhin öffentliche Methoden namens SendMessage zeigen (oder welche Leitungsnamen Sie auch fixiert haben), und die Schnittstelle IChatClient sollte weiterhin ReceiveMessage und UserJoined auflisten. Dies sind die Zeichenketten, die Ihre Clients senden und auf die sie lauschen; ist eine davon zu a umbenannt herausgekommen, hat Ihr Keep-Muster sie verfehlt, und die Echtzeitverteilung wird in der Produktion fehlschlagen.

Zweitens alles hinter der Oberfläche. Das private Feld _store des Hubs, der Rumpf von SendMessage, die Implementierung von IMessageStore und Ihre DTO-Interna sollten alle umbenannte Bezeichner zeigen und – mit aktiviertem encryptStrings – undurchsichtige Zeichenkettenkonstanten. Das ist der Schutz, dessentwegen Sie gekommen sind, intakt auf genau dem Code, der es wert ist, geschützt zu werden.

Die Regel, die all dem zugrunde liegt, ist dieselbe, die jedes reflexionsgesteuerte Framework befolgt: Ein Name ist kritisch genau dann, wenn etwas außerhalb der eigenen Token-Auflösung der CLR ihn als Zeichenkette liest. In SignalR ist diese Menge klein und erkennbar – die Namen der öffentlichen Hub-Methoden und die Mitglieder der stark typisierten Client-Schnittstelle – und sobald Sie genau diese fixiert haben, benennt Nebula die Hub-Rümpfe, die Dienste und die DTOs frei um und schützt Ihre eigene ausgelieferte Echtzeitanwendung, ohne je einen Namen zu bewegen, von dem ein Client abhängt.

Nebula.NET testen

Härten Sie Ihren .NET-Code in wenigen Minuten — starten Sie mit der kostenlosen Edition.