Skip to content
← Tous les articles
· Delta1 Labs WCFNebula

Obfusquer un service WCF sans casser le contrat sur le fil

Nebula renomme et chiffre les composants internes de votre service WCF pendant qu'une liste de conservation épingle par leur nom chaque [ServiceContract], [DataContract] et [DataMember] — pour que les noms des éléments SOAP, le WSDL et les clients existants restent compatibles octet pour octet.

Un service WCF est l”une des choses les plus difficiles à obfusquer en toute sécurité, et la raison est subtile : l”obfuscateur et le sérialiseur se soucient tous deux des noms de vos membres, mais pour des raisons opposées. Nebula veut renommer CalculateDiscount en a pour qu”un décompilateur n”apprenne rien. Le DataContractSerializer veut lire ce même nom et l”estamper dans le XML SOAP que vos clients analysent. Renommez le mauvais membre et vous n”aurez pas cassé votre build : vous aurez cassé tous les clients en production, en silence, jusqu”à ce que le premier appel échoue à la désérialisation.

Cet article montre comment obfusquer de façon agressive les composants internes d”un service WCF — renommage, chiffrement des chaînes, flux de contrôle — tout en gardant la surface [ServiceContract] / [DataContract] compatible octet pour octet sur le fil avec les clients déjà déployés. L”outil est une liste de conservation dans nebula.config.json.

Où WCF transforme un nom CLR en nom sur le fil

Prenons un service de commandes ordinaire :

[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; }
}

Rien ici ne dit à quoi ressemblera le SOAP, et c”est précisément le piège. Sans surcharge Name=, WCF dérive chaque nom sur le fil de l”identifiant CLR. Le corps de la requête de PlaceOrder se sérialise ainsi :

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

Les noms d”élément PlaceOrder, CustomerId, UnitPrice, Quantity sont littéralement le nom de la méthode, le nom du paramètre et les noms des propriétés. Un client construit à partir de votre WSDL publié a ces chaînes codées en dur. Lancez maintenant un obfuscateur de renommage sans garde-fous et UnitPrice devient b :

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

Même logique, mêmes types, un fil totalement différent. Tous les clients existants lèvent une exception, car l”élément qu”ils recherchent n”existe plus dans le message.

Nebula renomme l''implémentation ; la liste de conservation épingle le contratimplémentation interneCalculateDiscount()PricingEnginerenommé → a(), bsurface du contrat[DataMember] UnitPrice[OperationContract] PlaceOrderconservé → nom préservéDataContractSerializerlit les nomsdu conservé seulSOAP sur le fil<PlaceOrder><UnitPrice>inchangéla porte : l''interne renommé n''atteint jamais le sérialiseur ; les noms conservés oui

La liste de conservation : épingler le contrat, obfusquer tout le reste

Le but n”est pas « n”obfusque pas cet assembly ». C”est « ne renomme pas la poignée de membres que le sérialiseur lit, et déchaîne-toi sur le reste ». Nebula l”exprime par une liste de conservation pilotée par les attributs WCF eux-mêmes, de sorte que vous déclarez l”intention une fois au lieu d”énumérer chaque membre à la main :

{
  "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 }
}

Deux choses font le travail. Les règles keep disent : tout type portant [ServiceContract], [DataContract] ou [MessageContract] conserve son nom, et tout membre portant [OperationContract], [DataMember], [EnumMember] ou [MessageBodyMember] conserve son nom. Tout le reste — votre PricingEngine, vos assistants privés, vos routines de validation — est renommé normalement. La règle strings.exclude garde les valeurs par défaut littérales et les chaînes d”énumération du fil hors de la passe de chiffrement des chaînes, car celles-ci aussi peuvent affleurer dans le contrat.

Ensuite vous protégez :

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

L”implémentation est maintenant renommée, ses chaînes chiffrées et son flux de contrôle aplati ; la surface du contrat est intacte. Comme les attributs [DataMember] restent sur exactement les mêmes noms CLR, le sérialiseur produit exactement le même XML.

Un Name= explicite rend le renommage agressif sûr

La liste de conservation ci-dessus protège les contrats implicites — le cas fragile où le nom CLR est le nom sur le fil. Vous pouvez faire mieux. Si vous découplez les deux avec des noms explicites, le sérialiseur cesse de se soucier de l”identifiant :

[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);
}

Maintenant le fil dit <order>, <customerId>, <unitPrice> et l”opération est PlaceOrder sur le fil, quel que soit le nom des membres CLR. Remarquez que Submit est le nom de la méthode mais PlaceOrder le nom de l”opération : Nebula peut renommer la méthode en a et l”action SOAP reste PlaceOrder, car elle est épinglée dans l”attribut, pas dérivée. Un Name= explicite transforme « vous devez conserver chaque identifiant » en « vous ne conservez que les coquilles des types », et même ces conservations deviennent affaire d”attributs, pas de noms.

C”est pourquoi les contrats WCF matures précisent Name= partout : c”est une bonne hygiène de versionnage et c”est ce qui permet à un outil de durcissement de renommer le maximum de code. Une liste de conservation est le filet de sécurité sous les contrats implicites ; les noms explicites réduisent ce que ce filet doit rattraper.

Vérifier : le WSDL et le SOAP doivent être identiques

Ne livrez jamais sur la foi. Le contrat est un artefact observable, alors observez-le des deux côtés de l”étape d”obfuscation.

  1. Comparez le WSDL. Exportez les métadonnées du service original (svcutil ou l”endpoint ?wsdl), exportez-les de nouveau depuis le build protégé, et comparez. Les entrées <xsd:element name="..."> et les noms wsdl:operation doivent correspondre exactement. Toute différence est un membre que vous avez renommé et que le sérialiseur lisait.
  2. Comparez une enveloppe SOAP réelle. Appelez le service original et le service protégé avec la même requête et comparez les corps de requête/réponse. Les noms d”élément, les espaces de noms et l”ordre doivent être identiques caractère par caractère.
  3. Lisez la carte de renommage. nebula protect écrit une carte des anciens vers les nouveaux noms. Confirmez que vos types [DataContract] et vos membres [DataMember] en sont absents (ils ont été conservés) et que vos types d”implémentation y figurent (ils ont été renommés).

Si les trois concordent, un client construit contre le WSDL publié — y compris des clients que vous ne contrôlez pas — appelle le service durci exactement comme avant, tandis qu”un décompilateur ouvrant la DLL trouve des types renommés, des chaînes chiffrées et un répartiteur aplati là où votre logique de tarification était auparavant lisible.

La règle à retenir

Le sérialiseur lit les noms ; l”obfuscateur réécrit les noms ; la liste de conservation est l”endroit où vous les distinguez. Épinglez la surface [ServiceContract], [DataContract], [DataMember] et [MessageContract] par attribut, préférez un Name= explicite pour que le fil n”ait jamais dépendu d”un identifiant, et vérifiez contre le WSDL. Faites cela et vous obtenez tout le poids du renommage, du chiffrement des chaînes et de l”obfuscation du flux de contrôle de Nebula sur votre logique propriétaire, sans aucun changement au contrat contre lequel vos clients s”intègrent.

Essayez Nebula.NET

Renforcez votre code .NET en quelques minutes — commencez avec l'édition gratuite.