Ofuscar una herramienta global de .NET sin romper la superficie de comandos
Una dotnet tool es un paquete NuGet contra el que nadie compila, así que puedes renombrar mucho más agresivamente que en una biblioteca. Aquí te mostramos cómo proteger la DLL dentro del paquete manteniendo intactos el punto de entrada, los nombres de los comandos y `dotnet tool install`, con Nebula.NET.
Una herramienta global de .NET — una aplicación de consola que empaquetas con dotnet pack y PackAsTool e instalas con dotnet tool install -g — es una de las cosas más fáciles de proteger, por una razón que vale la pena entender antes de tocar cualquier configuración. Es un paquete NuGet, como una biblioteca, pero a diferencia de una biblioteca nadie compila contra ella. No hay ningún contrato de API pública que preservar. Lo único que hay al otro lado del paquete es un shell que invoca un ejecutable. Ese solo hecho cambia con cuánta agresividad puedes renombrar, y este artículo recorre la protección de la herramienta de principio a fin: qué puedes renombrar (casi todo), el puñado de nombres que realmente tienen que sobrevivir, cómo la superficie de comandos permanece intacta aunque las clases que la respaldan desaparezcan, y cómo integrarlo en dotnet pack para que cada versión se publique protegida.
Por qué una herramienta no es una biblioteca
Cuando ofuscas una biblioteca que distribuyes, la regla rectora es mantener la API pública: los consumidores referencian tus tipos y miembros públicos por su nombre, así que renombrarlos rompe su compilación. Una herramienta global no tiene tales consumidores. Su paquete contiene un ensamblado y un manifiesto; un usuario teclea mytool build --release y el runtime lanza tu punto de entrada. Nada enlaza contra MyTool.RootCommandBuilder ni MyTool.Handlers.BuildHandler, así que no hay motivo para dejar esos nombres legibles.
Por eso el primer ajuste es una omisión deliberada:
{
"schemaVersion": 1,
"inputs": [ "bin/Release/MyTool.1.4.0.nupkg" ],
"outputDirectory": "protected",
"preservePublicApi": false,
"encryptStrings": true,
"controlFlowObfuscation": true,
"renaming": {
"rules": [
{ "pattern": "MyTool.*", "action": "rename" }
]
}
}
preservePublicApi: false es lo contrario del valor por defecto para una biblioteca, y aquí es lo correcto: deja que el renombrador reescriba las clases de comando public, los handlers y los builders de opción igual que reescribe los helpers internos. La única regla amplia rename barre todo el espacio de nombres MyTool.*. Hay un nombre que esto no debe aplastar — el punto de entrada — y eso se maneja más abajo, no con una regla de conservación.
El único nombre que tiene que sobrevivir: el punto de entrada
El paquete de una herramienta global lleva un manifiesto DotnetToolSettings.xml que indica al host de herramientas de .NET qué nombre de comando se asocia a qué ensamblado:
<DotNetCliTool Version="1">
<Commands>
<Command Name="mytool" EntryPoint="MyTool.dll" Runner="dotnet" />
</Commands>
</DotNetCliTool>
El punto de entrada se fija por nombre de ensamblado y de módulo (MyTool.dll) y el CLR encuentra Main a través del token de punto de entrada del ensamblado en la cabecera PE, no buscando un tipo llamado Program. Eso importa: Nebula preserva el nombre del ensamblado y reescribe el token de punto de entrada para que siga al método renombrado, de modo que tu Program.Main puede convertirse en a.b y la herramienta sigue arrancando. No necesitas una regla de conservación para Program. Lo que no debes hacer es renombrar el ensamblado ni descartar el manifiesto — y como le entregas a Nebula el .nupkg completo, él conserva el manifiesto y la identidad del ensamblado por ti.
La superficie de comandos está hecha de cadenas, no de nombres
El instinto que frena a la gente de ofuscar una herramienta con agresividad es el miedo a que renombrar las clases de comando cambie los verbos que los usuarios teclean. No lo hará, y la razón es estructural. En System.CommandLine — y en prácticamente todos los frameworks de CLI — los nombres de comando y de opción son literales de cadena que pasas a los constructores, no los nombres de los tipos que los construyen:
var build = new Command("build", "Compile the project");
var release = new Option<bool>("--release", "Build in Release configuration");
build.AddOption(release);
build.SetHandler(ctx => BuildHandler.Run(ctx));
Renombra la clase en la que vive esto de RootCommandBuilder a a, renombra BuildHandler a b, y los literales "build", "--release" y el texto de ayuda quedan intactos — son datos en el cuerpo del método, no identificadores de metadatos. El analizador sigue produciendo exactamente la misma superficie mytool build --release. Mejor aún, con encryptStrings activado, esos literales ya no están en el heap de cadenas para que alguien los busque con grep: se descifran bajo demanda cuando se construye el árbol de comandos. Así que el renombrado oculta cómo está cableada la herramienta, el cifrado de cadenas oculta el vocabulario, y la CLI que ve el usuario es idéntica byte por byte.
Así se ve un handler tras la protección, descompilado. Antes:
internal static class BuildHandler
{
public static int Run(InvocationContext ctx)
{
bool release = ctx.ParseResult.GetValueForOption(ReleaseOption);
return Compiler.Invoke(release ? "Release" : "Debug");
}
}
Tras el renombrado, el aplanamiento del flujo de control y el cifrado de cadenas, el mismo método se descompila a un bucle despachador sobre una llamada b.c(...) con los literales "Release" / "Debug" reemplazados por llamadas a la rutina de descifrado de cadenas — la lógica está intacta y se ejecuta de forma idéntica, pero el método se lee como ruido generado, sin nombres ni constantes reconocibles.
Vigila las mismas costuras de siempre
“Renombrar todo” tiene las mismas pocas excepciones que cualquier aplicación .NET — los lugares donde algo resuelve un nombre como cadena en tiempo de ejecución. En una herramienta suelen ser pocas, pero compruébalas:
- Enlace de configuración. Si tu herramienta lee ajustes mediante
IConfigurationyConfigure<TOptions>, los nombres de las propiedades enlazadas deben sobrevivir. Conserva los nombres de propiedad públicos de esas clases de opciones, o enlaza explícitamente. - Salida serializada. Si la herramienta emite JSON que los scripts analizan —
mytool list --json— esos nombres de propiedad del DTO son un contrato con los pipelines de tus usuarios, aunque ningún código compile contra ellos. Fíjalos con[JsonPropertyName("...")]para que los nombres en el cable dejen de depender de los nombres de C#, como en fijar un contrato JSON. - Reflexión y escaneo de DI. Una herramienta que descubre comandos escaneando una convención de nombres (todo tipo que termina en
Command) se rompe cuando esos nombres se renombran. Prefiere el registro explícito; donde no puedas, conserva lo que el escaneo coincide. El tratamiento completo está en ofuscar el escaneo de ensamblados y la DI.
La disciplina para todos estos casos es la misma disciplina de lista de conservación mínima que se cubre en construir una lista de conservación de Nebula: conserva solo el nombre hoja que algo lee como cadena, nunca el espacio de nombres que lo rodea.
Intégralo en dotnet pack
El lugar limpio para proteger una herramienta es justo después de empaquetarla, para que el artefacto que sale de tu build ya esté protegido y tu código fuente quede intacto. Marca el proyecto como herramienta como de costumbre:
<PropertyGroup>
<PackAsTool>true</PackAsTool>
<ToolCommandName>mytool</ToolCommandName>
<PackageId>MyTool</PackageId>
<Version>1.4.0</Version>
</PropertyGroup>
Luego ejecuta Nebula contra el paquete empaquetado en tu pipeline de publicación:
dotnet pack -c Release
nebula --config nebula.config.json
El MyTool.1.4.0.nupkg protegido aterriza en protected/, llevando su manifiesto y punto de entrada, listo para dotnet nuget push. Para una configuración totalmente desatendida puedes enganchar la ofuscación en MSBuild para que se ejecute automáticamente tras Pack — consulta MSBuild & CI — y limitarla a Release para que tu dotnet run del ciclo interno siga siendo rápido y depurable.
Compruébalo
Instala el paquete protegido en un entorno desechable y ejercítalo como lo haría un usuario: dotnet tool install -g --add-source ./protected MyTool, y luego ejecuta tus comandos reales — mytool build --release, mytool list --json — y confirma que los verbos se analizan, las opciones se enlazan y la salida no cambia. Después abre la DLL de dentro del paquete protegido en Glass.NET, el descompilador estático gratuito, y confirma la otra mitad: tus clases de comando y handlers se leen como a, b, c, el heap de cadenas ya no contiene tus verbos ni tu texto de ayuda, y el punto de entrada aún se resuelve. Renombrar de más se manifiesta como un comando roto en tiempo de ejecución; proteger de menos se manifiesta como lógica legible en el descompilador. Cuando ambas comprobaciones pasan, la herramienta que tus usuarios instalan hace exactamente lo que siempre hizo, y revela casi nada sobre cómo.
Descarga Nebula.NET gratis y protege tu próxima versión de dotnet tool.
Prueba Nebula.NET
Endurece tu código .NET en minutos — empieza con la edición gratuita.