Skip to content
← Todas las publicaciones
· Delta1 Labs Ofuscación.NETGuía

Qué rompe la ofuscación: reflexión, serialización y qué excluir

La primera vez que ofuscas una app que funcionaba puede romperse de formas sorprendentes: el JSON sale mal, la reflexión devuelve null, el data binding deja de funcionar. La causa siempre es la misma: algo resolvió por su nombre a un miembro renombrado. Esto es exactamente qué se rompe, por qué, y cómo excluir lo correcto sin debilitar la protección.

Añades un ofuscador a una app .NET que funcionaba, compilas, ejecutas, y algo que ayer funcionaba está roto. El JSON que devuelve tu API tiene campos llamados a y b. Una llamada a Type.GetMethod("Process") devuelve null. Tu ventana de WPF no vincula con nada. La inyección de dependencias no encuentra un servicio. Nada de esto es un error del ofuscador; es el error de ofuscación más común, y siempre tiene la misma causa raíz: algo resolvió por su nombre a un miembro renombrado en tiempo de ejecución.

Por qué los nombres son la línea de fractura

El renombrado —el núcleo de la mayoría de la ofuscación— convierte CustomerName en a, CalculateTotal en b. El código compilado que llama a esos miembros se reescribe para coincidir, así que las llamadas directas siguen funcionando; el compilador ya los resolvió a tokens, no a nombres. El peligro es cualquier ruta de código que vaya a buscar un miembro por su nombre de cadena en tiempo de ejecución, porque esa búsqueda sigue pidiendo el nombre original, que ya no existe en el ensamblado.

Ese es todo el modo de fallo. Todo lo de abajo es una instancia concreta de él.

Las víctimas habituales

Serialización. Un serializador mapea entre nombres de miembros y un formato de cable. Renombra los miembros y la salida se renombra con ellos:

public class Invoice { public decimal Total { get; set; } public string Customer { get; set; } }

JsonSerializer.Serialize(new Invoice { Total = 42, Customer = "Acme" });
// Expected: {"Total":42,"Customer":"Acme"}
// After renaming: {"a":42,"b":"Acme"}   ← your API contract just changed

Peor aún, la deserialización de datos previamente almacenados ahora falla al vincular, porque el JSON dice Total y la propiedad se llama a. Cualquier tipo que cruce una frontera de serialización —DTOs de API, archivos de configuración, documentos guardados, cargas de mensajes— está en riesgo.

Reflexión. Cualquier búsqueda por nombre devuelve el miembro equivocado o nada:

var mi = typeof(Report).GetMethod("Generate"); // null — it's called 'a' now
typeof(Plugin).GetProperty("Name")?.GetValue(obj); // null
Activator.CreateInstance(Type.GetType("MyApp.Widgets.Chart")); // type name changed → null
Enum.Parse<Status>("Active"); // enum member renamed → throws

El data binding es reflexión disfrazada. {Binding CustomerName} de WPF/XAML, @bind de Blazor y las columnas automáticas de DataGrid resuelven nombres de propiedad como cadenas en tiempo de ejecución; renombra la propiedad y el binding muestra nada en silencio.

Frameworks basados en convención. La DI que mapea IFooService → FooService por nombre, EF Core mapeando una propiedad a una columna por nombre, AutoMapper emparejando miembros por nombre, el model binding de MVC: cualquier cosa cuya «magia» sea la coincidencia de nombres se rompe cuando los nombres cambian.

La regla: excluye el contrato, no la lógica

El arreglo no es ofuscar menos; es excluir del renombrado exactamente los nombres que se resuelven por nombre, y nada más. En la práctica ese es un conjunto pequeño e identificable:

  • Tipos que se serializan (DTOs, configuración, contratos de mensajes).
  • Miembros alcanzados por reflexión, incluido todo lo que un framework resuelve por nombre.
  • Tipos/miembros referenciados desde XAML u otro marcado.
  • Superficie de API pública de una biblioteca contra la que otros compilan.

Marcas estos para omitir el renombrado, con un atributo o una regla de configuración. Nebula.NET admite reglas de exclusión de renombrado por espacio de nombres, tipo o atributo, así que mantienes una frontera limpia: tu espacio de nombres Contracts conserva sus nombres, el resto del ensamblado se renombra libremente.

// Exclude a serialized contract from RENAMING, keep everything else renamed.
[Obfuscation(Feature = "renaming", Exclude = true, ApplyToMembers = true)]
public class Invoice
{
    public decimal Total { get; set; }
    public string Customer { get; set; }
}
Resuelto por nombre — CONSERVA nombresDTOs, objetivos de reflexión, XAML, API públicaLógica interna — RENOMBRA librementetodo lo llamado directamente en el códigoAmbos reciben igual:aplanamiento del flujo de controlcifrado de cadenasenmascaramiento de constantes — exclusión solo de nombres

El arreglo más robusto: desacopla el nombre del cable

Excluir tipos funciona, pero para la serialización hay un patrón mejor que elimina la fragilidad por completo: fija los nombres serializados de forma explícita para que nunca dependan del nombre del miembro.

public class Invoice
{
    [JsonPropertyName("total")]    public decimal Total { get; set; }
    [JsonPropertyName("customer")] public string Customer { get; set; }
}

Ahora el JSON dice total y customer sin importar cómo se llame la propiedad en IL; renombra Total a a y el formato de cable no cambia. Esto desacopla tu contrato de tu código, lo cual es buena ingeniería independientemente de la ofuscación (una simple refactorización de renombrado en C# tampoco alterará en silencio tu API). El mismo razonamiento aplica en otros sitios: referencia los valores de enum por el tipo de enum, no con Enum.Parse sobre una cadena; resuelve la DI con registro explícito, no por convención; prefiere los bindings compilados (x:Bind en UWP/WinUI) sobre las rutas de cadena donde la plataforma lo ofrezca.

La exclusión es a nivel de nombre, no de protección

El matiz más importante: excluir un tipo del renombrado no lo deja sin protección. El renombrado es una transformación entre varias. Un DTO cuyos nombres de propiedad conservas aún puede tener sus cuerpos de método aplanados en el flujo de control, sus literales de cadena cifrados y sus constantes enmascaradas. Le estás diciendo al ofuscador «no cambies estos nombres», no «no protejas este código». Así que el coste de hacer las exclusiones correctamente es esencialmente cero en términos de protección: mantienes cobertura completa sobre la lógica y renuncias al renombrado solo en el puñado de nombres que el runtime busca como cadenas.

Un flujo de trabajo fiable

Ofusca y luego ejecuta tu suite de pruebas real contra la build ofuscada, no solo contra la no ofuscada. Las roturas por resolución de nombres son invisibles en tiempo de compilación y obvias en el momento en que se ejecuta un ida y vuelta de serialización o una ruta de reflexión, así que una prueba de integración que serialice un DTO, llame a una API o ejercite un plugin las detectará de inmediato. Arregla cada una excluyendo ese contrato concreto (o fijando sus nombres de cable), nunca desactivando el renombrado por completo. Hazlo una vez y «la ofuscación rompió mi app» se convierte en un paso de configuración único en lugar de un misterio recurrente: tu lógica sigue siendo difícil de leer, y los nombres de los que depende tu programa se quedan exactamente donde los espera.

Prueba Nebula.NET

Endurece tu código .NET en minutos — empieza con la edición gratuita.