Ofuscar un servicio WCF sin romper el contrato del cable
Nebula renombra y cifra las partes internas de tu servicio WCF mientras una lista de conservación fija cada [ServiceContract], [DataContract] y [DataMember] por nombre, de modo que los nombres de los elementos SOAP, el WSDL y los clientes existentes siguen siendo compatibles byte a byte.
Un servicio WCF es una de las cosas más difíciles de ofuscar con seguridad, y la razón es sutil: tanto el ofuscador como el serializador se preocupan por los nombres de tus miembros, pero por motivos opuestos. Nebula quiere renombrar CalculateDiscount a a para que un descompilador no aprenda nada. El DataContractSerializer quiere leer ese mismo nombre y estamparlo en el XML SOAP que tus clientes analizan. Renombra el miembro equivocado y no habrás roto la compilación: habrás roto a todos los clientes que están en producción, en silencio, hasta que la primera llamada falle al deserializar.
Este artículo muestra cómo ofuscar de forma agresiva las partes internas de un servicio WCF —renombrado, cifrado de cadenas, control de flujo— manteniendo la superficie [ServiceContract] / [DataContract] compatible byte a byte en el cable con los clientes que ya están desplegados. La herramienta es una lista de conservación en nebula.config.json.
Dónde convierte WCF un nombre CLR en un nombre del cable
Considera un servicio de pedidos corriente:
[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; }
}
Nada aquí indica qué aspecto tendrá el SOAP, y esa es justamente la trampa. Sin sobrescritura Name=, WCF deriva cada nombre del cable a partir del identificador CLR. El cuerpo de la petición de PlaceOrder se serializa así:
<PlaceOrder xmlns="http://contoso.com/orders">
<request>
<CustomerId>C-4471</CustomerId>
<UnitPrice>19.99</UnitPrice>
<Quantity>3</Quantity>
</request>
</PlaceOrder>
Los nombres de elemento PlaceOrder, CustomerId, UnitPrice, Quantity son literalmente el nombre del método, el nombre del parámetro y los nombres de las propiedades. Un cliente construido a partir de tu WSDL publicado tiene esas cadenas codificadas en firme. Ahora ejecuta un ofuscador de renombrado sin salvaguardas y UnitPrice se convierte en b:
<a xmlns="http://contoso.com/orders">
<request>
<c>C-4471</c>
<b>19.99</b>
<d>3</d>
</request>
</a>
La misma lógica, los mismos tipos, un cable completamente distinto. Todos los clientes existentes lanzan una excepción, porque el elemento que buscan ya no existe en el mensaje.
La lista de conservación: fija el contrato, ofusca todo lo demás
El objetivo no es «no ofusques este ensamblado». Es «no renombres el puñado de miembros que lee el serializador, y arremete contra el resto». Nebula lo expresa con una lista de conservación guiada por los propios atributos de WCF, de modo que declaras la intención una vez en lugar de enumerar cada miembro a mano:
{
"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 }
}
Hay dos cosas haciendo el trabajo. Las reglas keep dicen: cualquier tipo que lleve [ServiceContract], [DataContract] o [MessageContract] conserva su nombre, y cualquier miembro que lleve [OperationContract], [DataMember], [EnumMember] o [MessageBodyMember] conserva su nombre. Todo lo demás —tu PricingEngine, tus ayudantes privados, tus rutinas de validación— se renombra con normalidad. La regla strings.exclude mantiene los valores por defecto literales y las cadenas de enumeración del cable fuera del paso de cifrado de cadenas, porque esas también pueden aflorar en el contrato.
Luego proteges:
nebula protect --config nebula.config.json --out bin/Protected
La implementación ahora está renombrada, sus cadenas cifradas y su flujo de control aplanado; la superficie del contrato queda intacta. Como los atributos [DataMember] permanecen sobre exactamente los mismos nombres CLR, el serializador produce exactamente el mismo XML.
Un Name= explícito hace seguro el renombrado agresivo
La lista de conservación anterior protege los contratos implícitos, el caso frágil en el que el nombre CLR es el nombre del cable. Puedes hacerlo mejor. Si desacoplas ambos con nombres explícitos, el serializador deja de preocuparse por el identificador:
[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);
}
Ahora el cable dice <order>, <customerId>, <unitPrice> y la operación es PlaceOrder en el cable, sin importar cómo se llamen los miembros CLR. Fíjate en que Submit es el nombre del método pero PlaceOrder es el nombre de la operación: Nebula puede renombrar el método a a y la acción SOAP sigue siendo PlaceOrder, porque está fijada en el atributo, no derivada. Un Name= explícito convierte «debes conservar cada identificador» en «solo conservas las carcasas de los tipos», e incluso esas conservaciones pasan a ser cuestión de los atributos, no de los nombres.
Por eso los contratos WCF maduros detallan Name= por todas partes: es buena higiene de versionado y es lo que permite a una herramienta de endurecimiento renombrar la máxima cantidad de código. Una lista de conservación es la red de seguridad bajo los contratos implícitos; los nombres explícitos reducen lo que esa red tiene que atrapar.
Verifica: el WSDL y el SOAP deben ser idénticos
Nunca lo entregues confiando a ciegas. El contrato es un artefacto observable, así que obsérvalo a ambos lados del paso de ofuscación.
- Compara el WSDL. Exporta los metadatos del servicio original (
svcutilo el endpoint?wsdl), expórtalos de nuevo desde la compilación protegida y compáralos. Las entradas<xsd:element name="...">y los nombreswsdl:operationdeben coincidir exactamente. Cualquier diferencia es un miembro que renombraste y que el serializador estaba leyendo. - Compara un sobre SOAP real. Llama al servicio original y al protegido con la misma petición y compara los cuerpos de petición/respuesta. Los nombres de elemento, los espacios de nombres y el orden deben ser iguales carácter por carácter.
- Lee el mapa de renombrado.
nebula protectescribe un mapa de nombres antiguos a nuevos. Confirma que tus tipos[DataContract]y tus miembros[DataMember]no aparecen en él (se conservaron) y que tus tipos de implementación sí aparecen (se renombraron).
Si los tres coinciden, un cliente construido contra el WSDL publicado —incluidos clientes que no controlas— llama al servicio endurecido exactamente como antes, mientras que un descompilador que abra la DLL encuentra tipos renombrados, cadenas cifradas y un despachador aplanado donde antes tu lógica de precios era legible.
La regla que hay que recordar
El serializador lee nombres; el ofuscador reescribe nombres; la lista de conservación es donde le dices al uno del otro. Fija la superficie [ServiceContract], [DataContract], [DataMember] y [MessageContract] por atributo, prefiere un Name= explícito para que el cable nunca haya dependido de un identificador, y verifica contra el WSDL. Haz eso y obtienes todo el peso del renombrado, el cifrado de cadenas y la ofuscación de flujo de control de Nebula sobre tu lógica propietaria sin ningún cambio en el contrato contra el que integran tus clientes.
Prueba Nebula.NET
Endurece tu código .NET en minutos — empieza con la edición gratuita.