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

Einen WCF-Dienst verschleiern, ohne den Vertrag auf der Leitung zu brechen

Nebula benennt die internen Bestandteile Ihres WCF-Dienstes um und verschlüsselt sie, während eine Keep-Liste jeden [ServiceContract], [DataContract] und [DataMember] namentlich festpinnt — so bleiben die SOAP-Elementnamen, das WSDL und bestehende Clients Byte für Byte kompatibel.

Ein WCF-Dienst gehört zu den Dingen, die sich am schwersten sicher verschleiern lassen, und der Grund ist subtil: Sowohl der Verschleierer als auch der Serialisierer kümmern sich um Ihre Member-Namen, aber aus gegensätzlichen Gründen. Nebula möchte CalculateDiscount in a umbenennen, damit ein Dekompilierer nichts erfährt. Der DataContractSerializer möchte denselben Namen lesen und ihn in das SOAP-XML prägen, das Ihre Clients parsen. Benennen Sie den falschen Member um, und Sie haben nicht Ihren Build zerstört — Sie haben jeden Client im Feld zerstört, lautlos, bis der erste Aufruf bei der Deserialisierung fehlschlägt.

Dieser Beitrag zeigt, wie Sie die internen Bestandteile eines WCF-Dienstes aggressiv verschleiern — Umbenennung, String-Verschlüsselung, Kontrollfluss — und dabei die [ServiceContract]- / [DataContract]-Oberfläche Byte für Byte leitungskompatibel mit bereits ausgelieferten Clients halten. Das Werkzeug ist eine Keep-Liste in nebula.config.json.

Wo WCF einen CLR-Namen in einen Leitungsnamen verwandelt

Betrachten Sie einen gewöhnlichen Bestelldienst:

[ServiceContract(Namespace = "http://contoso.com/orders")]
public interface IOrderService
{
    [OperationContract]
    OrderConfirmation PlaceOrder(OrderRequest request);
}

[DataContract(Namespace = "http://contoso.com/orders")]
public class OrderRequest
{
    [DataMember]
    public string CustomerId { get; set; }

    [DataMember]
    public decimal UnitPrice { get; set; }

    [DataMember]
    public int Quantity { get; set; }
}

Nichts hier sagt, wie das SOAP aussehen wird, und genau das ist die Falle. Ohne Name=-Überschreibung leitet WCF jeden Leitungsnamen aus dem CLR-Bezeichner ab. Der Anfragekörper von PlaceOrder wird so serialisiert:

<PlaceOrder xmlns="http://contoso.com/orders">
  <request>
    <CustomerId>C-4471</CustomerId>
    <UnitPrice>19.99</UnitPrice>
    <Quantity>3</Quantity>
  </request>
</PlaceOrder>

Die Elementnamen PlaceOrder, CustomerId, UnitPrice, Quantity sind buchstäblich der Methodenname, der Parametername und die Eigenschaftsnamen. Ein aus Ihrem veröffentlichten WSDL erzeugter Client hat diese Zeichenketten fest verdrahtet. Lassen Sie nun einen Umbenennungs-Verschleierer ohne Schutzvorkehrungen laufen, und UnitPrice wird zu b:

<a xmlns="http://contoso.com/orders">
  <request>
    <c>C-4471</c>
    <b>19.99</b>
    <d>3</d>
  </request>
</a>

Dieselbe Logik, dieselben Typen, eine völlig andere Leitung. Jeder bestehende Client wirft eine Ausnahme, weil das gesuchte Element in der Nachricht nicht mehr existiert.

Nebula benennt die Implementierung um; die Keep-Liste pinnt den Vertrag festinterne ImplementierungCalculateDiscount()PricingEngineumbenannt → a(), bVertragsoberfläche[DataMember] UnitPrice[OperationContract] PlaceOrdererhalten → Name bewahrtDataContractSerializerliest Namen nurauf ErhaltenemSOAP auf der Leitung<PlaceOrder><UnitPrice>unverändertdas Tor: umbenannte Interna erreichen den Serialisierer nie; erhaltene Namen schon

Die Keep-Liste: den Vertrag festpinnen, alles andere verschleiern

Das Ziel lautet nicht „verschleiere diese Assembly nicht“. Es lautet „benenne die Handvoll Member nicht um, die der Serialisierer liest, und toste dich am Rest aus“. Nebula drückt das mit einer Keep-Liste aus, die von den WCF-Attributen selbst gesteuert wird, sodass Sie die Absicht einmal deklarieren, statt jeden Member von Hand aufzuzählen:

{
  "input": "bin/Release/Contoso.Orders.dll",
  "rename": {
    "enabled": true,
    "keep": [
      { "hasAttribute": "System.ServiceModel.ServiceContractAttribute", "scope": "type" },
      { "hasAttribute": "System.ServiceModel.OperationContractAttribute", "scope": "member" },
      { "hasAttribute": "System.Runtime.Serialization.DataContractAttribute", "scope": "type" },
      { "hasAttribute": "System.Runtime.Serialization.DataMemberAttribute", "scope": "member" },
      { "hasAttribute": "System.Runtime.Serialization.EnumMemberAttribute", "scope": "member" },
      { "hasAttribute": "System.ServiceModel.MessageContractAttribute", "scope": "type" },
      { "hasAttribute": "System.ServiceModel.MessageBodyMemberAttribute", "scope": "member" }
    ]
  },
  "strings": { "encrypt": true, "exclude": ["hasAttribute:DataMemberAttribute"] },
  "controlFlow": { "enabled": true }
}

Zwei Dinge leisten die Arbeit. Die keep-Regeln besagen: Jeder Typ, der [ServiceContract], [DataContract] oder [MessageContract] trägt, behält seinen Namen, und jeder Member, der [OperationContract], [DataMember], [EnumMember] oder [MessageBodyMember] trägt, behält seinen Namen. Alles andere — Ihre PricingEngine, Ihre privaten Hilfsmethoden, Ihre Validierungsroutinen — wird normal umbenannt. Die Regel strings.exclude hält literale Standardwerte und Enum-Leitungszeichenketten aus dem String-Verschlüsselungsdurchlauf heraus, denn auch diese können im Vertrag zutage treten.

Dann schützen Sie:

nebula protect --config nebula.config.json --out bin/Protected

Die Implementierung ist nun umbenannt, ihre Zeichenketten verschlüsselt und ihr Kontrollfluss abgeflacht; die Vertragsoberfläche ist unberührt. Da die [DataMember]-Attribute auf genau denselben CLR-Namen bleiben, erzeugt der Serialisierer genau dasselbe XML.

Ein explizites Name= macht aggressives Umbenennen sicher

Die obige Keep-Liste schützt implizite Verträge — den fragilen Fall, in dem der CLR-Name der Leitungsname ist. Sie können es besser machen. Entkoppeln Sie beide mit expliziten Namen, kümmert sich der Serialisierer überhaupt nicht mehr um den Bezeichner:

[DataContract(Name = "order", Namespace = "http://contoso.com/orders")]
public class OrderRequest
{
    [DataMember(Name = "customerId", Order = 0)]
    public string CustomerId { get; set; }

    [DataMember(Name = "unitPrice", Order = 1)]
    public decimal UnitPrice { get; set; }
}

[ServiceContract(Namespace = "http://contoso.com/orders")]
public interface IOrderService
{
    [OperationContract(Name = "PlaceOrder", Action = "http://contoso.com/orders/place")]
    OrderConfirmation Submit(OrderRequest request);
}

Jetzt sagt die Leitung <order>, <customerId>, <unitPrice>, und die Operation ist auf der Leitung PlaceOrder — unabhängig davon, wie die CLR-Member heißen. Beachten Sie, dass Submit der Methodenname ist, PlaceOrder aber der Operationsname: Nebula kann die Methode in a umbenennen, und die SOAP-Aktion bleibt PlaceOrder, weil sie im Attribut festgepinnt und nicht abgeleitet ist. Ein explizites Name= verwandelt „Sie müssen jeden Bezeichner erhalten“ in „Sie erhalten nur die Typhüllen“, und selbst diese Erhaltungen werden eine Sache der Attribute, nicht der Namen.

Darum buchstabieren ausgereifte WCF-Verträge Name= überall aus: Es ist gute Versionierungshygiene und es ist das, was einem Härtungswerkzeug erlaubt, die maximale Menge an Code umzubenennen. Eine Keep-Liste ist das Sicherheitsnetz unter impliziten Verträgen; explizite Namen verkleinern, was das Netz auffangen muss.

Überprüfen: WSDL und SOAP müssen identisch sein

Liefern Sie nie auf gut Glück aus. Der Vertrag ist ein beobachtbares Artefakt, also beobachten Sie ihn auf beiden Seiten des Verschleierungsschritts.

  1. Vergleichen Sie das WSDL. Exportieren Sie die Metadaten des ursprünglichen Dienstes (svcutil oder der ?wsdl-Endpunkt), exportieren Sie sie erneut aus dem geschützten Build und vergleichen Sie. Die <xsd:element name="...">-Einträge und die wsdl:operation-Namen müssen exakt übereinstimmen. Jeder Unterschied ist ein Member, den Sie umbenannt haben und den der Serialisierer gelesen hat.
  2. Vergleichen Sie einen echten SOAP-Umschlag. Rufen Sie den ursprünglichen und den geschützten Dienst mit derselben Anfrage auf und vergleichen Sie die Anfrage-/Antwortkörper. Elementnamen, Namensräume und Reihenfolge sollten Zeichen für Zeichen gleich sein.
  3. Lesen Sie die Umbenennungskarte. nebula protect schreibt eine Karte von alten zu neuen Namen. Bestätigen Sie, dass Ihre [DataContract]-Typen und [DataMember]-Member darin fehlen (sie wurden erhalten) und Ihre Implementierungstypen vorhanden sind (sie wurden umbenannt).

Stimmen alle drei überein, ruft ein gegen das veröffentlichte WSDL erzeugter Client — einschließlich Clients, die Sie nicht kontrollieren — den gehärteten Dienst genau wie zuvor auf, während ein Dekompilierer, der die DLL öffnet, umbenannte Typen, verschlüsselte Zeichenketten und einen abgeflachten Dispatcher dort vorfindet, wo Ihre Preislogik früher lesbar war.

Die Regel zum Merken

Der Serialisierer liest Namen; der Verschleierer schreibt Namen um; die Keep-Liste ist der Ort, an dem Sie die beiden auseinanderhalten. Pinnen Sie die [ServiceContract]-, [DataContract]-, [DataMember]- und [MessageContract]-Oberfläche per Attribut fest, bevorzugen Sie ein explizites Name=, damit die Leitung von vornherein nie von einem Bezeichner abhing, und überprüfen Sie gegen das WSDL. Tun Sie das, und Sie erhalten das volle Gewicht von Nebulas Umbenennung, String-Verschlüsselung und Kontrollfluss-Verschleierung auf Ihrer proprietären Logik, ohne jede Änderung an dem Vertrag, gegen den Ihre Kunden integrieren.

Nebula.NET testen

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