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

Construir una keep-list en Nebula: qué excluir del renombrado (y por qué)

Todo ofuscador te deja excluir nombres del renombrado, y el primer instinto cuando algo se rompe es excluir de más. Una keep-list es una frontera diseñada y mínima, no un cajón de sastre para todo lo que se rompió. Esta es la disciplina: qué debe quedarse de verdad sin renombrar (API pública, objetivos de reflexión, DTOs serializados, clases de options, escaneo de DI, XAML, puntos de entrada de un host), cómo expresarlo en nebula.config.json, cómo estrechar cada entrada hasta lo mínimo posible, y cómo demostrar el resultado en Glass.NET.

Todo ofuscador te da una forma de decir «no renombres esto». La tentación, la primera vez que algo se rompe, es tirar de ella con generosidad: excluye el tipo, excluye el espacio de nombres, excluye cualquier cosa que parezca frágil, y sigue adelante. Funciona, y desmantela tu protección en silencio. Una keep-list no es un cajón de sastre para todo lo que se rompió; es una frontera diseñada y mínima entre los nombres que tu programa resuelve como cadenas en tiempo de ejecución y todo lo demás. Esta entrada va sobre la disciplina de construir esa frontera y mantenerla pequeña: qué debe quedarse de verdad sin renombrar, cómo decirlo en la configuración, cómo estrechar cada entrada hasta lo mínimo posible, y cómo demostrar el resultado en un decompilador.

Si lo que quieres es el catálogo de qué se rompe y por qué, eso es ofuscación, reflexión y trampas de serialización. Esta es la pieza de proceso que la acompaña: la entrada de trampas nombra el síntoma; esta es cómo escribir y mantener el conjunto de reglas que lo previene, sin excluir de más y regalar la protección.

Por qué la keep-list es un coste, no una red de seguridad

Parte del modelo mental correcto: cada nombre de tu keep-list es un nombre que entregas a un atacante en texto claro. El renombrado es la protección más barata y amplia que tienes: convierte ValidateLicenseKey en a en todo el ensamblado. Cada entrada keep vuelve a abrir en ella un agujero legible. Mantén un espacio de nombres entero «por si acaso» y habrás dejado cada nombre de tipo, método y campo de ese espacio reposando en los metadatos para que cualquiera con un decompilador lo lea.

Así que la regla rectora es el minimalismo: mantén un nombre solo cuando algo lo resuelve por su cadena en tiempo de ejecución, y mantén solo ese nombre —no el tipo que lo rodea, ni el espacio de nombres que rodea al tipo—. Una keep-list construida con esta regla es corta, y lo corto es el objetivo.

Qué debe quedarse de verdad

A lo largo de apps .NET reales reaparecen las mismas categorías. Cada una es un sitio donde un nombre se lee como cadena en tiempo de ejecución —por tu propio código, un framework, un serializador o un host— de modo que renombrarlo cambia un contrato en lugar de simplemente ocultar un identificador.

La superficie de API pública de una biblioteca entregada. Si otros compilan contra tu ensamblado, cada nombre de tipo y miembro público es parte de tu contrato; renómbralo y su código deja de enlazar. Esta es la única categoría que casi nunca deberías escribir a mano: activa preservePublicApi y el ofuscador mantiene exactamente la superficie visible desde fuera mientras renombra todo lo interno. El caso de biblioteca completa está en proteger un SDK o una biblioteca .NET entregada.

Tipos y miembros alcanzados por reflexión. Cualquier cosa encontrada mediante Type.GetType("..."), GetMethod("..."), GetProperty("...") o Activator.CreateInstance se resuelve por una cadena que el renombrado no toca. Mantén esos miembros concretos, no, por reflejo, todo su tipo declarante.

Nombres de miembro de DTOs serializados. System.Text.Json, Newtonsoft.Json y los serializadores XML construyen su formato de cable a partir de los nombres de propiedad. Renombra Invoice.Total y el JSON pasa en silencio a {"a":…}, rompiendo a cada consumidor y cada documento almacenado. Mantenlos —o, mucho mejor, fíjalos con [JsonPropertyName("total")] para que el nombre de cable deje de depender del nombre en C#—, como en fijar un contrato JSON con JsonPropertyName.

Clases de options enlazadas desde la configuración. Configure<TOptions> hace coincidir las claves de configuración con los nombres de propiedad a través del enlazador. Son la categoría que más se olvida, porque las clases de options suelen ser POCOs internal que preservePublicApi no cubre, así que se renombran en silencio y caen a los valores por defecto en producción. Mantén sus nombres de propiedad enlazados.

Tipos escaneados por convención de DI. Si un contenedor registra servicios escaneando por un patrón de nombre —cada tipo cuyo nombre termina en Service, por ejemplo—, el renombrado borra la convención y no se registra nada. Prefiere arreglar el registro para que sea explícito o basado en marcadores; donde no puedas, mantén los nombres que el escaneo empareja. El tratamiento completo está en ofuscar el escaneo de ensamblados y la inyección de dependencias.

Tipos referenciados por nombre en XAML o marcado. Un {Binding CustomerName}, un x:Class, una referencia de tipo en un DataTemplate, un convertidor nombrado en el marcado: todos resuelven cadenas en tiempo de ejecución. Mantén los nombres de propiedad y tipo que el marcado nombra.

Puntos de entrada que un host carga por nombre. Un plugin que el host activa por nombre de tipo, una clase Startup que un host busca, un módulo que un framework carga desde una cadena de manifiesto: si algo fuera de tu ensamblado nombra el punto de entrada, mantenlo.

¿Algo resuelve este nombre como una cadena en tiempo de ejecución?SÍ — mantén el nombreAPI pública de una biblioteca entregadaobjetivos de reflexión (GetType / GetMethod)nombres de miembro de DTOs serializadospropiedades de options enlazadastipos escaneados por convención de DInombres referenciados en XAML / marcadopuntos de entrada cargados por un hostNO — renómbralotoda la lógica internamiembros privados, auxiliarestodo lo llamado en el códigoel cuerpo del método sigueprotegido en cualquier caso —keep es solo de nombre

Expresarla como reglas

Nebula.NET lee un nebula.config.json con un conjunto de reglas de renombrado evaluado de arriba abajo —la primera coincidencia de patrón gana—, así que pon tus reglas keep estrechas por encima del rename amplio que lo cubre todo. Aquí tienes una keep-list para una biblioteca entregada dividida en una superficie pública, un espacio de nombres de contratos serializados, un espacio de options y un motor interno:

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

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

  "renaming": {
    "rules": [
      { "pattern": "Acme.Sdk.Contracts.*", "action": "keep" },
      { "pattern": "Acme.Sdk.Options.*",   "action": "keep", "members": "public" },
      { "pattern": "Acme.Sdk.*",           "action": "rename" }
    ]
  }
}

preservePublicApi se encarga de la API visible desde fuera, así que no hay regla para ella. Las dos reglas keep fijan los contratos serializados y las options enlazadas; el calificador members: "public" estrecha el keep de options a los nombres de propiedad pública y deja que los campos privados se renombren. Todo lo demás del ensamblado cae en el rename que lo cubre todo.

Para un único objetivo de reflexión, un patrón de configuración es una herramienta tosca: un atributo sobre el miembro exacto es más preciso y vive junto al código que depende de él:

public sealed class ReportEngine
{
    [Obfuscation(Feature = "renaming", Exclude = true)]   // resuelto por nombre vía reflexión
    public ReportResult Run(string reportName) { /* ... */ }

    private decimal Accumulate(ReadOnlySpan<Line> lines) { /* ... */ }   // renombrado — nadie lo busca
}

El atributo mantiene el nombre de un método; Accumulate y el resto de la clase se renombran con normalidad. Echa mano de atributos para keeps individuales y quirúrgicos, y de patrones de configuración para espacios de nombres enteros que son un contrato por diseño.

Estrecha cada entrada

La diferencia entre una keep-list débil y una fuerte es casi toda cuestión de estrechamiento. Tres movimientos hacen casi todo el trabajo.

Mantén el nombre, protege igual el cuerpo. Excluir un miembro del renombrado no es excluirlo de la protección. Un DTO o una clase de options mantenidos aún reciben sus cuerpos de método aplanados en el flujo de control, sus cadenas cifradas y sus constantes enmascaradas: renunciaste a una transformación sobre un nombre, no al tratamiento completo sobre el código. Así que nunca dudes en mantener un nombre que debes mantener; el coste es solo la legibilidad de ese único nombre.

Mantén miembros, renombra los internos. Un tipo enlazado por serialización necesita que se mantengan sus nombres de propiedad pública, no sus campos de respaldo privados, sus constructores o sus métodos auxiliares. Acota el keep a los miembros que un serializador o un enlazador leen de verdad y deja que el resto se renombre —el calificador members: "public" de arriba, o un atributo solo sobre los miembros expuestos—.

Mantén la hoja, no la rama. La exclusión excesiva más común es mantener un espacio de nombres entero porque se hace reflexión sobre un tipo en él, o un tipo entero porque se hace sobre un método. Redúcelo hasta el único nombre que se resuelve como cadena. Un método mantenido en una clase de treinta miembros deja veintinueve renombrados; un espacio de nombres mantenido no deja ninguno.

Siempre que la plataforma lo permita, elimina la dependencia por nombre por completo en vez de mantener a su alrededor: [JsonPropertyName] en lugar de mantener nombres de DTO, registro explícito de DI en lugar de escaneo por convención, bindings compilados (x:Bind) en lugar de rutas de cadena. Cada dependencia que conviertes de un nombre a un tipo es una entrada keep que te permites borrar.

Demuestra el resultado

Una keep-list es correcta solo si dos cosas son ciertas a la vez, y no puedes saber cuál leyendo la configuración. Compila el ensamblado protegido y comprueba ambas en un decompilador. Glass.NET es el compañero gratuito y de solo análisis estático para esto: lee metadatos e IL y nunca ejecuta el binario.

Primero, confirma que no sobrevivió nada que debiera estar oculto: recorre los tipos internos, verifica que se leen como a, b, c, que los nombres de método de tu motor desaparecieron y que ninguna regla keep emparejó por accidente más de lo que pretendías. Segundo, confirma que no se renombró nada que deba sobrevivir: comprueba que tu superficie pública, tus propiedades de DTO mantenidas y tus nombres de options están intactos. Mantener de más se manifiesta como internos legibles; mantener de menos, como un contrato renombrado. El procedimiento completo está en ¿de verdad funcionó tu ofuscación?.

Luego ejecuta la prueba que la revisión de configuración no puede reemplazar: corre tu suite de integración contra la build protegida —serializa un DTO y haz un ida y vuelta, ejercita una ruta de reflexión, enlaza una sección de configuración—. Las roturas por resolución de nombres son invisibles en tiempo de compilación y obvias en el instante en que se ejecuta la ruta, así que una prueba que cruce una de esas costuras detecta de inmediato un keep demasiado estrecho.

Mantenla mínima con el tiempo

Una keep-list no es un archivo que se configura y se olvida. Cada nuevo DTO serializado, cada nuevo tipo sobre el que se hace reflexión, cada nueva clase de options es una entrada candidata —y cada refactorización que elimina una dependencia por nombre es una entrada que puedes borrar—. Revísala como código: cuando añadas un keep, escribe junto a él por qué —qué serializador, qué llamada de reflexión, qué binding— para que la siguiente persona pueda saber si sigue ganándose su lugar. Una keep-list cuya cada línea tiene un motivo es una que puedes mantener corta, y una keep-list corta es todo el juego: el conjunto más pequeño posible de nombres en texto claro, con Nebula.NET renombrando, cifrando y protegiendo todo lo demás.

Prueba Nebula.NET

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