Mantener estables los nombres ofuscados entre versiones con el mapa semilla de Nebula
Por defecto, cada ejecución de ofuscación renombra desde cero, así que un método que era "a" en 1.4.0 pasa a ser "q" en 1.4.1 y cada mapa de desofuscación archivado se pudre en el momento en que publicas. El seedMapFile de Nebula.NET fija la salida en su sitio: los miembros sin cambios conservan el nombre ofuscado que tenían en la versión anterior y solo el código nuevo recibe nombres nuevos. Así funciona el mapa de símbolos, por qué los nombres estables hacen tratables la simbolización de crashes y los diffs binarios, y cómo integrar la ofuscación incremental en CI.
Publica una compilación .NET ofuscada y obtienes una correspondencia: MyApp.Billing.Invoice.Recalculate pasó a ser n.a.b, y guardas el mapa para poder decodificar los informes de crash que irán llegando. Publica 1.4.1 una semana después con un arreglo de una línea, y ese mismo método ahora es q.c.f. Nada en él cambió, pero su nombre ofuscado sí, porque la ejecución de ofuscación por defecto renombra el ensamblado completo desde cero, y la numeración del renombrador se desplazó en el momento en que añadiste un campo tres clases más allá.
Ese vaivén es silenciosamente caro. Tu mapa de 1.4.0 no puede leer una traza de pila de 1.4.1. Un diff binario entre las dos versiones es un mar de renombrados con tu arreglo real enterrado en él. Y nunca puedes mirar una traza ofuscada en crudo y pensar «ah, es otra vez el bug de la factura», porque el nombre es distinto en cada compilación. El mapa semilla de Nebula.NET elimina el vaivén: devuélvele el mapa de la versión anterior y los miembros sin cambios conservan los nombres que ya tenían. Este artículo explica cómo funciona y por qué vale la pena configurarlo.
Lo que una ejecución ya emite
No tienes que activar nada para obtener la materia prima. Cada ejecución de Nebula escribe un mapa de símbolos junto al ensamblado ofuscado —MyApp.dll produce MyApp.symbols.json— registrando cada renombrado que realizó:
{
"tool": "Nebula.NET",
"toolVersion": "1.1.6",
"generatedUtc": "2026-10-10T09:00:00Z",
"assembly": "MyApp",
"entries": [
{ "kind": "Method", "original": "MyApp.Billing.Invoice.Recalculate", "obfuscated": "n.a.b", "token": "0x06000123" },
{ "kind": "Type", "original": "MyApp.Billing.Invoice", "obfuscated": "n.a", "token": "0x02000011" },
{ "kind": "Field", "original": "MyApp.Billing.Invoice._subtotal", "obfuscated": "n.a.d", "token": "0x04000045" }
]
}
Cada entrada lleva cuatro cosas: el kind del símbolo, el nombre original completamente cualificado, el nombre obfuscated que recibió y el token de metadatos: el RID de MethodDef/TypeDef/FieldDef en el módulo de salida. Ese token es lo que convierte el mapa en un índice bidireccional preciso en lugar de una lista difusa de nombres, y es lo que nebula deobfuscate --map MyApp.symbols.json lee para convertir una traza de pila ofuscada de vuelta a nombres reales. Hasta aquí, todo normal: archivas este archivo con cada versión y decodificas las trazas contra él.
El problema es que el mapa de la siguiente versión es un archivo distinto con valores obfuscated distintos para los mismos nombres original. El archivo crece un mapa incompatible por versión.
Semilla: llevar los nombres hacia adelante
El mapa semilla cierra el ciclo. Apunta seedMapFile al .symbols.json de la versión anterior y Nebula lo consulta antes de asignar nombres:
{
"preservePublicApi": true,
"controlFlowObfuscation": true,
"encryptStrings": true,
"obfuscateConstants": true,
"metadataHardening": true,
"seedMapFile": "maps/MyApp-1.4.0.symbols.json"
}
Ahora el renombrador se ejecuta en dos fases. Para cada miembro que sigue existiendo bajo el mismo nombre original, busca el nombre ofuscado que la semilla le asignó y lo reutiliza textualmente. Solo los miembros sin entrada en la semilla —código genuinamente nuevo, o código cuya identidad cambió— pasan a la generación de nombres nuevos, y esos nombres nuevos se eligen para evitar colisionar con cualquier nombre que la semilla ya haya repartido. El resultado es un ensamblado donde el renombrado es incremental: el diff contra la versión anterior es tu cambio real, no una renumeración global.
Por qué los nombres estables compensan
Simbolización de crashes entre versiones. La ganancia práctica es que un mapa archivado decodifica más de una compilación. Cuando Recalculate es n.a.b en 1.4.0, 1.4.1 y 1.4.2, una traza que cae en n.a.b se resuelve contra cualquiera de esos mapas, y aprendes a reconocer el nombre ofuscado a simple vista: n.a.b es «el recálculo de la factura», y punto. Un pipeline de informes de crash puede agrupar por frame ofuscado y mostrarte que el mismo método es responsable a lo largo de una serie de versiones antes de que nadie ejecute nebula deobfuscate, porque el símbolo es la identidad estable contra la que has estado recopilando todo el tiempo.
Diffs que muestran el cambio, no el vaivén. Los parcheadores delta (ClickOnce, Squirrel, tu propio actualizador de diff binario) producen actualizaciones diminutas solo cuando la mayoría de los bytes del ensamblado no han cambiado. Un renombrado desde cero mueve casi todos los nombres y, por tanto, casi todos los bytes, así que un arreglo de una línea se distribuye como una descarga casi completa. La ofuscación con semilla mantiene los miembros sin cambios comparables byte a byte, de modo que el parche es proporcional al cambio real. Lo mismo ocurre cuando una persona revisa dos compilaciones decompiladas lado a lado para confirmar que un hotfix hizo solo lo que decía: con nombres estables el diff es legible; sin ellos, es ruido.
Una correspondencia reconocible sobre la que puedes razonar. A lo largo de una serie de versiones, el mapa se convierte en un libro mayor casi solo de adiciones. Aparecen entradas nuevas a medida que añades código; las entradas existentes se quedan donde están. Puedes hacer un diff de dos mapas y leer, en términos sencillos, exactamente qué miembros son nuevos en esta versión: una señal sorprendentemente útil por sí misma.
Integrarlo en CI
La mecánica es: compilar, ofuscar con semilla a partir del mapa de la última versión y archivar el mapa de esta compilación para que la siguiente pueda usarlo como semilla. El mapa vive fuera del árbol de código fuente porque nombra tus símbolos; un almacén de artefactos o una rama protegida es su hogar habitual.
# Pseudocódigo de un paso de un pipeline de release.
steps:
- run: dotnet build -c Release
# Trae el mapa de la versión anterior; en la primerísima versión esto no hace nada
# y Nebula renombra desde cero (todavía no hay nada que usar como semilla).
- run: fetch-artifact MyApp-latest.symbols.json -> maps/MyApp-prev.symbols.json
- run: nebula protect --config nebula.json # la config fija seedMapFile: maps/MyApp-prev.symbols.json
# Archiva el mapa de ESTA compilación como el nuevo "latest" para que la siguiente versión lo use
# como semilla, y conserva una copia con la versión estampada para decodificar sus crashes.
- run: publish-artifact MyApp.symbols.json as MyApp-latest.symbols.json
- run: publish-artifact MyApp.symbols.json as MyApp-${VERSION}.symbols.json
Dos notas prácticas. Primera, la primerísima versión no tiene semilla; no pasa nada: Nebula renombra desde cero y emite el primer mapa, que se convierte en la semilla de la versión dos. Segunda, conserva una copia con la versión estampada de cada mapa (MyApp-1.4.1.symbols.json) además del latest rotativo, porque eso es lo que decodifica los informes de crash de esa compilación concreta. El latest rotativo sirve para sembrar la siguiente compilación; las copias estampadas sirven para simbolizar para siempre.
Lo que la semilla no hace
La semilla es una herramienta operativa, no un control de seguridad, y conviene ser preciso al respecto. Fija qué nombre ofuscado recibe un miembro; no debilita la ofuscación de ninguna compilación individual. Un atacante que tenga tu ensamblado 1.4.0 ya posee un binario completamente protegido: cadenas cifradas, flujo de control aplanado, metadatos endurecidos. Reutilizar los mismos nombres en 1.4.1 no le dice a ese atacante nada que el binario 1.4.0 no le hubiera dicho ya, porque los nombres son opacos en ambos. El renombrado sigue siendo determinista y libre de colisiones, cada una de las demás pasadas se ejecuta exactamente como se configuró y no hay fuga de nombres originales en la salida: el mapa que contiene los originales es tuyo, archivado de tu lado, y nunca se distribuye con el ensamblado. Lo que ganas está puramente de tu lado de la valla: mapas que siguen funcionando, diffs que se mantienen pequeños e informes de crash que puedes leer de un vistazo a lo largo de toda una serie de versiones.
Prueba Nebula.NET
Endurece tu código .NET en minutos — empieza con la edición gratuita.