Skip to content
← Todas las publicaciones
· Delta1 Labs Nebula.NETOfuscaciónNative AOT

Ofuscar aplicaciones .NET con Native AOT y de archivo único

Publicar como archivo único o con Native AOT es empaquetado y compilación, no protección. Esto es lo que cada modo hace realmente con tu código, por qué las aplicaciones de archivo único siguen distribuyendo IL gestionado totalmente recuperable, qué oculta y qué no oculta Native AOT, y por qué la ofuscación debe ejecutarse sobre el IL antes de que se construya el paquete o el compilador AOT lo elimine.

Circula una creencia reconfortante entre los equipos de .NET: que si publicas tu aplicación como archivo único, o la compilas con Native AOT, ya no necesitas ofuscarla. El razonamiento suena plausible —“ya no hay ninguna DLL que decompilar”—. También es falso, y en el caso del archivo único es peligrosamente falso. El formato de publicación no es una frontera de protección. El archivo único es empaquetado; Native AOT es compilación. Ninguno fue diseñado para frenar la ingeniería inversa, y tratarlos como si lo fueran deja justo el código que más querías ocultar a la vista de todos.

Este artículo trata de lo que cada modo de publicación hace realmente con tu código, qué protege y qué no, y del único hecho estructural que lo decide todo: la ofuscación tiene que ocurrir sobre el IL gestionado antes de que se construya el paquete o se ejecute el compilador AOT, porque después no queda nada que transformar.

El archivo único es un contenedor, no un candado

Cuando activas PublishSingleFile, el SDK no funde tu código en un bloque indivisible. Produce un ejecutable host nativo con los archivos de tu aplicación —ensamblados gestionados, configuración, dependencias nativas— incrustados dentro de él como recursos. Al arrancar, el host localiza esos ensamblados incrustados y los carga directamente desde el paquete. Y lo esencial: los ensamblados gestionados siguen siendo exactamente lo que emitió el compilador de C#, ensamblados .NET corrientes con IL corriente y metadatos completos.

Eso significa que el único .exe es un contenedor, y el contenedor tiene una estructura documentada. Los extractores de paquetes disponibles públicamente leen esa estructura y vuelcan cada ensamblado incrustado de nuevo a disco como una .dll normal. El viaje de ida y vuelta tarda segundos y no requiere ninguna habilidad:

# un atacante, ante tu aplicación de archivo único "protegida"
single-file-extractor MyApp.exe -o extracted/
# extracted/MyApp.dll, extracted/MyApp.Core.dll, ...  todo IL corriente

A partir de ahí es la misma historia que con cualquier ensamblado sin proteger: abre MyApp.dll en un decompilador y lee C# casi original. Tu comprobación de licencia, tus claves de API horneadas en literales de cadena, tu lógica de negocio: todo. El archivo único resolvió un problema de despliegue (un único archivo que copiar) y no hizo absolutamente nada por la confidencialidad. Si antes habrías ofuscado las DLL, sigues teniendo que ofuscarlas ahora; el paquete solo las esconde de un listado casual de archivos, no de quien escriba un comando.

Native AOT elimina el IL, pero eso no es una función de seguridad

Native AOT es algo genuinamente distinto. Con PublishAot, el compilador de IL (ILC) compila tu código gestionado a código máquina nativo de forma anticipada. El artefacto publicado no contiene JIT y, para el código totalmente compilado con AOT, no hay IL ni cuerpo de método gestionado que recuperar. Apunta un decompilador de .NET hacia él y no habrá nada que leer: solo entiende IL. La ingeniería inversa pasa a significar desensamblado nativo, que es un salto real de coste. Es una propiedad agradable.

También es un efecto secundario, no una garantía, y viene con límites importantes que los equipos pasan por alto de forma rutinaria:

  • AOT no está disponible para todas las cargas de trabajo. Prohíbe la generación de código en tiempo de ejecución, es estricto con la reflexión y la carga dinámica, y muchos frameworks, arquitecturas de plugins y aplicaciones con mucha reflexión sencillamente no pueden compilarse así. La gran mayoría de las aplicaciones .NET reales de hoy no son totalmente AOT, lo que significa que esta “protección” no existe para ellas.
  • Los despliegues mixtos y parciales siguen distribuyendo IL gestionado. Si alguna parte de tu producto es una biblioteca de clases consumida en otro lugar, un plugin o un componente fuera de la imagen AOT, esa parte vuelve a ser IL corriente y decompilable.
  • Los datos sobreviven a la compilación. Los literales de cadena, los recursos incrustados y la forma de tus constantes generalmente permanecen presentes en la imagen nativa. Un desensamblador más un volcado de cadenas sigue filtrando el prefijo "LIC-" y la URL del endpoint aunque la lógica circundante sea nativa.
  • No está documentado como protección. Confiar en AOT para ocultar tu algoritmo significa confiar en el comportamiento incidental de un compilador que Microsoft nunca prometió mantener. No es una postura de seguridad sobre la que quieras construir la aplicación de licencias.

Así que Native AOT eleva el listón donde aplica, y es una capa adicional bienvenida. No es un sustituto de proteger la capa gestionada, y menos aún para las muchas aplicaciones y componentes que de entrada no pueden compilarse con AOT.

El hecho estructural: ofusca el IL antes de que desaparezca

Aquí está el punto del que depende todo. La ofuscación —renombrado, cifrado de cadenas, aplanado del flujo de control, virtualización de métodos— es una transformación de IL a IL. Lee tus ensamblados compilados, reescribe el IL y vuelve a escribir ensamblados protegidos. Necesita IL tanto como entrada como salida.

Ahora mira dónde existe el IL en el proceso de publicación:

  • El compilador de C# convierte tu código fuente en ensamblados gestionados (IL). ← la ofuscación puede ejecutarse aquí
  • dotnet publish con archivo único incrusta esos ensamblados IL en el host. El IL sigue existiendo, pero ya está empaquetado, y si solo ofuscas después de esto, tendrías que desempaquetar, transformar y reempaquetar, lo que arruina la idea.
  • dotnet publish con Native AOT compila ese IL a código nativo y lo descarta. Después de este paso no hay IL en absoluto. No queda nada que ofuscar.

La conclusión es ineludible: la protección tiene que recaer sobre los ensamblados entre la compilación y la publicación. Ofusca primero el IL; luego deja que el empaquetador lo empaquete o que el compilador AOT lo consuma. Nebula.NET está hecho para encajar exactamente en esa ranura: opera sobre los ensamblados compilados, antes de la publicación.

Fuente .csIL gestionado(desde csc)Nebulaofusca el ILPaquete archivo únicoIL incrustado como recursosCompilación Native AOTIL compilado a nativoLa ofuscación se ejecuta sobre el IL, antes de que se construya el paquete o AOT lo elimine.

Integrarlo en un build de publicación

El .csproj de un proyecto de archivo único tiene este aspecto:

<PropertyGroup>
  <OutputType>Exe</OutputType>
  <TargetFramework>net8.0</TargetFramework>
  <PublishSingleFile>true</PublishSingleFile>
  <SelfContained>true</SelfContained>
  <!-- o, para Native AOT: -->
  <!-- <PublishAot>true</PublishAot> -->
</PropertyGroup>

El trabajo consiste en insertar un paso de ofuscación después de que se construyan tus ensamblados y antes de que publish los empaquete o compile con AOT. Tienes dos opciones limpias.

Opción A — proteger la salida del build en CI y luego publicar. Compila normalmente, ejecuta Nebula sobre los ensamblados de salida y apunta publish a las copias protegidas:

dotnet build -c Release
nebula --config nebula.config.json          # reescribe el IL in situ / en protected/
dotnet publish -c Release --no-build -o out  # empaqueta o compila con AOT el IL protegido

con un nebula.config.json que preserve todo lo que AOT y la reflexión necesitan:

{
  "schemaVersion": 1,
  "inputs": ["bin/Release/net8.0/MyApp.dll", "bin/Release/net8.0/MyApp.Core.dll"],
  "outputDirectory": "bin/Release/net8.0",
  "preservePublicApi": true,
  "encryptStrings": true,
  "controlFlowObfuscation": true,
  "autoProtectLicensing": true
}

Opción B — un hook de MSBuild que se ejecute entre Build y Publish, de modo que un simple dotnet publish ya quede protegido:

<Target Name="NebulaProtect" AfterTargets="Build" BeforeTargets="Publish">
  <Exec Command="nebula --config $(ProjectDir)nebula.config.json" />
</Target>

De cualquier forma, el compilador AOT o el empaquetador de archivo único consume ahora IL que ya está ofuscado. Para AOT en concreto, ten en cuenta la misma disciplina que usas para el recorte: el renombrado debe preservar tu superficie pública y cualquier miembro alcanzado por reflexión o serialización, porque la cadena de herramientas AOT es estricta con ambas. Consulta la configuración para las reglas de preservación, y el recorrido general en cómo ofuscar un ensamblado .NET.

Antes y después

La prueba siempre es la misma: mira el artefacto distribuido a través de las herramientas de un atacante.

Sin ofuscación, el caso del archivo único es el peor desenlace: una falsa sensación de seguridad sobre código trivialmente recuperable:

// tras ejecutar un extractor de paquetes sobre el .exe de archivo único y luego un decompilador:
public bool IsLicensed(string key)
{
    if (!key.StartsWith("LIC-")) return false;
    return ValidateSignature(key, "https://api.example.com/v1/activate");
}

Con la ofuscación aplicada primero al IL, el código extraído (o nativo) ya no cuenta su historia: el método es un despachador aplanado, el prefijo y la URL se descifran en tiempo de ejecución en lugar de imprimirse, y en Enterprise el método de aplicación de licencias está virtualizado a un bytecode sin decompilador:

public bool a(string A_0)
{
    int s = 0;
    while (true) { switch (s) { /* despachador sobre bloques reordenados */
        case 0: if (!A_0.StartsWith(Strings.Get(0x3f1))) { s = 9; break; } s = 4; break;
        case 9: return false;
        /* ... */ } }
}

El archivo único dejó la primera versión perfectamente legible. AOT habría hecho el caso nativo más difícil de decompilar, pero solo para el código totalmente compilado, solo donde AOT es siquiera una opción, y nunca para las cadenas. Ofuscar primero el IL es lo que realmente protege la lógica de aplicación de licencias en ambos mundos.

La conclusión

El archivo único y Native AOT son buenas decisiones de ingeniería para la distribución y el rendimiento de arranque. Ninguno es una estrategia de protección. Una aplicación de archivo único distribuye tu IL intacto dentro de un contenedor que cualquiera puede abrir; una aplicación Native AOT endurece la salida nativa como efecto secundario pero no puede usarse en todas partes y sigue filtrando tus datos. El único lugar fiable para proteger tu código es el IL gestionado, entre la compilación y la publicación: ofúscalo ahí y luego deja que el empaquetador o el compilador AOT hagan su trabajo. Si la protección se ejecuta después de aplicar el formato, no queda nada que proteger.

Prueba Nebula.NET

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