Ofuscar los ensamblados de un juego Unity: Mono, IL2CPP y las exigencias de keep-rules de la serialización
Cómo ofuscar el C# de un juego Unity sin romper escenas, prefabs ni el inspector: qué protege cada backend, Mono e IL2CPP, y las keep-rules exactas que exigen la serialización y la reflexión de Unity.
Distribuye un juego Unity construido sobre el backend de scripting Mono y, en la práctica, habrás publicado tu código fuente. El Assembly-CSharp.dll que descansa en la carpeta Managed de tu player es IL de .NET corriente, y un decompilador lo convierte de vuelta en C# legible en segundos: tus fórmulas de combate, tu anti-cheat, tu validación de compras dentro de la app, tus URLs secretas, todo ahí mismo. La ofuscación es la respuesta habitual, pero Unity es el único entorno .NET donde renombrarlo todo de forma ingenua sí romperá tu juego. Unity se apoya en la serialización y la reflexión basadas en nombres más que casi cualquier otro framework, y hay que decirle al ofuscador qué no debe tocar.
Este artículo cubre qué expone realmente cada backend de Unity, las cosas concretas que Unity resuelve por nombre y las keep-rules que te permiten ofuscar de forma agresiva sin un solo prefab roto.
Dos backends, dos modelos de amenaza
Unity tiene dos backends de scripting, y cambian lo que la ofuscación significa siquiera.
Mono compila tus scripts a ensamblados gestionados que se distribuyen como archivos .dll reales (Assembly-CSharp.dll, Assembly-CSharp-firstpass.dll y cualquier ensamblado precompilado/asmdef). Son imágenes CLI estándar: decompilables y, por tanto, totalmente ofuscables. El renombrado, el aplanamiento del flujo de control y el cifrado de cadenas se aplican al binario distribuido exactamente como lo harían con cualquier aplicación .NET.
IL2CPP toma ese mismo IL gestionado y lo transpila a C++, que luego se compila a una biblioteca nativa para la plataforma de destino. No hay ningún ensamblado gestionado que renombrar en tiempo de ejecución, pero IL2CPP emite un archivo global-metadata.dat junto al binario que preserva en texto claro los nombres de tipos, los nombres de métodos, los nombres de campos y (lo más crítico) cada literal de cadena. Herramientas como Il2CppDumper leen ese archivo para reconstruir una imagen casi completa de tu código. Así que IL2CPP no es ofuscación; es transpilación con un sidecar de metadatos. Proteger ahí significa transformar los ensamblados gestionados antes de que se ejecute IL2CPP, de modo que los nombres y las cadenas que acaban integrados en global-metadata.dat ya estén cifrados o renombrados.
Lo que Unity resuelve por nombre, y se romperá si lo renombras
Tanto el editor como el runtime hacen una cantidad sorprendente de vinculación basada en cadenas y nombres. Cada uno de estos es un punto donde el renombrado a ciegas corrompe el juego.
- Campos serializados. Cada campo
publicy cada campo[SerializeField] privatede unMonoBehaviouroScriptableObjectse guarda en escenas, prefabs y archivos de assets indexado por su nombre exacto. Renombra el campo y el valor guardado dejará de cargarse. - Referencias a tipos de script. Una escena guarda “este GameObject tiene un componente de tipo
PlayerController” por la identidad del tipo (y del ensamblado). Renombrar la clase desvincula cada instancia. - UnityEvents. Los listeners de
UnityEventcableados en el inspector (tuOnClickdel botón, etc.) persisten el nombre del método objetivo como una cadena. Renombra el método y el botón no hace nada. - Llamadas despachadas por cadena.
SendMessage,BroadcastMessage,Invoke,InvokeRepeatingyAnimationEventllaman a los métodos por su nombre en cadena. - Hooks de ciclo de vida y de atributos. Unity encuentra
Awake,Start,Update,OnEnable, etc. por nombre mediante reflexión; también rastrea[RuntimeInitializeOnLoadMethod],[MenuItem]y similares. Los métodos mágicos suelen estar en una keep-list integrada, pero la reflexión personalizada no. - Addressables / asset bundles. Cualquier cosa que cargues por nombre de tipo o por dirección en cadena en tiempo de ejecución.
Un ejemplo concreto
He aquí un componente perfectamente normal que el renombrado arruinaría de tres maneras distintas:
using UnityEngine;
using UnityEngine.Events;
public class TurretController : MonoBehaviour // (1) type name is in every scene/prefab using it
{
[SerializeField] private float fireRate = 0.5f; // (2) field name is saved in the asset
[SerializeField] private UnityEvent onFire; // (3) persistent listeners store a method name
// Called by name from an AnimationEvent on the fire clip:
public void SpawnProjectile() { /* ... */ } // (4) resolved by string, not a direct call
}
Si Nebula renombra TurretController → a, fireRate → b y SpawnProjectile → c, entonces cada prefab de torreta pierde su valor de cadencia de fuego, cada escena pierde sus componentes de torreta y la animación de disparo deja de generar proyectiles, sin ningún error de compilación, porque ninguna de estas vinculaciones se comprueba en tiempo de compilación.
Las keep-rules que lo arreglan
El objetivo no es “excluir los MonoBehaviours de la ofuscación”. Eso dejaría la mayor parte de tu código al descubierto. El objetivo es conservar solo los nombres a los que Unity se vincula, y ofuscar todo lo que hay detrás de ellos: cuerpos de métodos, helpers privados, algoritmos y literales de cadena.
Nebula expresa esto a través de sus reglas de keep-list / exclusión de renombrado (el mismo mecanismo que se cubre en keep-lists y exclusiones de renombrado). Un conjunto de reglas práctico para Unity conserva:
- los nombres de todos los tipos que derivan de
UnityEngine.MonoBehaviouryUnityEngine.ScriptableObject; - todos los miembros serializados de esos tipos (campos públicos y campos
[SerializeField], y auto-propiedades[field: SerializeField]); - cualquier método al que apunte un UnityEvent o que se llame vía
SendMessage/Invoke/AnimationEvent.
Expresado contra los miembros, eso es “conserva el nombre de cualquier campo marcado [SerializeField] o público en una subclase de UnityEngine.Object, y conserva el nombre de tipo de cualquier subclase de MonoBehaviour/ScriptableObject”. Los cuerpos de los métodos nunca se ven afectados por las reglas de keep de nombres, así que el aplanamiento del flujo de control y el cifrado de cadenas siguen aplicándose a SpawnProjectile aunque su nombre se preserve: la lógica está protegida, solo sobrevive el identificador.
Todo lo demás —tu lógica de dominio en C# puro, el networking, el formato de guardado, el anti-cheat y todos los literales de cadena que contienen— recibe el tratamiento completo.
Encajarla en la build
Mantén la ofuscación fuera del editor y de las máquinas de desarrollo; ejecútala sobre el artefacto. Para el backend Mono, añade un paso posterior a la build (un post-proceso de BuildPipeline o una etapa de CI) que ofusque Assembly-CSharp.dll y tus demás ensamblados del juego en el directorio Managed del player, y luego reempaquete. Para IL2CPP, ofusca los ensamblados gestionados antes de la conversión de IL2CPP para que el C++ generado y global-metadata.dat hereden los símbolos renombrados y las cadenas cifradas. Ejecutar la protección en CI (consulta ofuscar solo en el servidor de build) significa que tu equipo sigue depurando código limpio y legible y solo las builds distribuidas quedan endurecidas.
Hacer esto con Nebula
Nebula ofusca los ensamblados gestionados a partir de los que se construye un juego Unity —renombrado, aplanamiento del flujo de control y cifrado de cadenas— con un modelo de keep-list lo bastante preciso para preservar exactamente los campos serializados, los nombres de tipo de los componentes y los métodos objetivo de eventos que Unity vincula por nombre, mientras sigue endureciendo cada cuerpo de método detrás de ellos. Se ejecuta como un paso posterior a la build o de CI, así que tu editor se mantiene rápido y solo el player distribuido queda protegido. Nebula Pro añade virtualización de código para el puñado de rutinas que más importan: comprobaciones de licencia, anti-cheat, validación de compras.
Empieza con la edición gratuita para endurecer tu primera build, o consulta los planes cuando estés listo para la virtualización.
Prueba Nebula.NET
Endurece tu código .NET en minutos — empieza con la edición gratuita.