Skip to content
← Todas las publicaciones
· Delta1 Labs Nebula.NETOfuscación.NETAnálisis a fondo

Cuando nameof y [CallerMemberName] filtran tus miembros renombrados

Renombraste cada tipo y método, luego abriste el binario ofuscado y encontraste tus nombres originales en literales de cadena a la vista. El culpable no es un error del ofuscador: es el compilador. nameof(OrderService) incrusta el literal "OrderService" en el IL en tiempo de compilación, mucho antes de que se ejecute el renombrado, y [CallerMemberName] hace lo mismo con el nombre original del método como valor por defecto de un parámetro. Renombra el miembro y el literal sigue diciendo el nombre antiguo, así que un decompilador lee tu mapa gratis y cualquier reflexión que esperase el nombre en tiempo de ejecución se rompe en silencio. Aquí tienes exactamente de dónde salen estos literales en el IL, por qué el renombrado no puede tocarlos por sí solo, y cómo el cifrado de cadenas y el manejo consciente del renombrado de Nebula.NET cierran la fuga.

Activaste el renombrado, recompilaste y abriste el resultado en un decompilador para admirar el muro de a, b y c donde antes estaban tus tipos. Y entonces los encontraste de todos modos: OrderService, CustomerName, ValidateCoupon, sentados en literales de cadena a la vista, entregados gratis en el único sitio que el renombrado nunca tocó. Nada está roto. Esto es obra del compilador, no del ofuscador, y entender por qué es la diferencia entre una build protegida y una que publica silenciosamente su propio mapa de renombrado.

Dos características de C# ponen tus nombres originales en el ensamblado como datos antes de que el renombrado se ejecute siquiera: nameof, que incrusta un nombre en un literal en tiempo de compilación, y [CallerMemberName], que congela el nombre del miembro que llama en el valor por defecto de un parámetro en cada punto de llamada. El renombrado recorre referencias a tipos y miembros; una cadena suelta no es ninguna de las dos, así que el nombre antiguo se queda en el montón de cadenas. Este artículo explica exactamente de dónde salen esos literales en el IL, por qué el renombrado no puede tocarlos por sí solo, cuándo reescribirlos es correcto y cuándo rompería algo, y cómo Nebula.NET cierra la fuga con cifrado de cadenas y manejo consciente del renombrado.

El código fuente

Una clase que usa ambas características como lo hace el código real: categorías de registro, validación de argumentos e INotifyPropertyChanged:

public sealed class OrderService : INotifyPropertyChanged
{
    private string _customerName = "";

    public string CustomerName
    {
        get => _customerName;
        set { _customerName = value; OnPropertyChanged(); }   // [CallerMemberName] fills in "CustomerName"
    }

    public void ValidateCoupon(string code)
    {
        if (code is null)
            throw new ArgumentNullException(nameof(code));     // nameof -> literal "code"
        _logger.BeginScope(nameof(OrderService));              // nameof -> literal "OrderService"
    }

    public event PropertyChangedEventHandler? PropertyChanged;
    private void OnPropertyChanged([CallerMemberName] string? name = null) =>
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}

Dónde están realmente los nombres en el IL

nameof(OrderService) no hace referencia al tipo en el código emitido. El compilador lo sustituye, durante la compilación, por un simple literal de cadena: un único ldstr:

// ValidateCoupon body — the names are DATA, not references:
ldstr      "code"                      // nameof(code)        -> literal
newobj     instance void [System.Runtime]System.ArgumentNullException::.ctor(string)
...
ldstr      "OrderService"              // nameof(OrderService) -> literal
callvirt   instance class ... Microsoft.Extensions.Logging.ILogger::BeginScope<...>(...)

[CallerMemberName] es el mismo truco sobre un parámetro. OnPropertyChanged() se llama sin argumento en el código fuente, pero el compilador rellena el valor por defecto del parámetro opcional en el punto de llamada con el nombre del llamador como literal:

// CustomerName setter — the compiler injected the name at the CALL SITE:
ldarg.0
ldstr      "CustomerName"              // injected by [CallerMemberName] at compile time
call       instance void OrderService::OnPropertyChanged(string)

Ambos nombres son ahora operandos de ldstr que apuntan al montón #US (cadenas de usuario) del ensamblado. Son datos puros sin ningún vínculo de metadatos con el tipo o la propiedad de los que se derivaron.

Por qué el renombrado no puede tocarlos por sí solo

El renombrado reescribe referencias: un TypeRef a OrderService, un MethodDef para ValidateCoupon, cada call y ldfld que nombra un miembro. Deja deliberadamente intacto el contenido de las cadenas, porque una cadena no es una referencia y cambiar la semántica de las cadenas por defecto rompería cualquier programa que compare, serialice o muestre una. Así que cuando el renombrador convierte OrderService en b.c, el ldstr "OrderService" no se ve arrastrado: no hay nada que conecte el literal con el tipo. El resultado es un binario renombrado con un índice en texto plano de sus propios nombres originales.

compilar: nameof /CallerMemberNamemontón de cadenas #USldstr "OrderService"renombrador reescribe REFSOrderService → b.c...pero no el montónmontón SIN CAMBIOS"OrderService" ← fuganombre antiguo en claropaso de cifrado de cadenasvalor oculto en reposose descifra al usarse

Cifra el valor: la solución de confidencialidad

La fuga es ante todo una fuga de información: los nombres originales son legibles. El cifrado de cadenas cierra eso. Nebula sustituye cada ldstr por una llamada a un descriptor de acceso que descifra, de modo que el nombre en texto plano ya no está en el montón y un decompilador ve una llamada a una rutina ofuscada, no el nombre antiguo de tu tipo:

{
  "inputs": ["bin/Release/net8.0/OrderService.dll"],
  "outputDirectory": "bin/Release/net8.0/obf",
  "renameIdentifiers": true,
  "encryptStrings": true
}

Tras este paso el ldstr "OrderService" se convierte en algo como ldstr <cipher> / call string <decrypt>(...), y el montón de cadenas ya no anuncia OrderService, CustomerName ni ValidateCoupon. Para el caso común —nameof usado para una categoría de registro o el nombre de un argumento de excepción— esta es toda la solución: el valor sigue descifrándose a la cadena correcta en tiempo de ejecución, y nada dependía de que el nombre fuera legible.

Reescribe el valor: la solución de corrección, cuando aplica

El cifrado oculta el literal; no cambia a qué se descifra. Eso importa en dos direcciones, y tiran en sentidos opuestos:

  • El literal alimenta reflexión contra un miembro renombrado. typeof(T).GetProperty(nameof(SomeProp)) espera el nombre en tiempo de ejecución. Si SomeProp fue renombrado, el literal descifrado sigue diciendo SomeProp y la búsqueda falla. Aquí la solución correcta es hacer que el literal siga al renombrado, o excluir ese miembro del renombrado para que el nombre permanezca estable.
  • El literal es un contrato externo. Un nombre de propiedad serializado, un [CallerMemberName] que alimenta a INotifyPropertyChanged, una ruta de enlace de datos que el XAML también nombra: la cadena del otro lado no se está renombrando, así que reescribir tu literal rompería la coincidencia. Aquí no debes reescribir, y a menudo tampoco debes renombrar el miembro.

El valor por defecto seguro de Nebula es cifrar (ocultar) en lugar de reescribir (resignificar), porque el IL por sí solo no siempre puede distinguir un uso reflexivo de uno contractual. Tú marcas los casos reflexivos explícitamente: excluye los miembros cuyos nombres se buscan por cadena, para que el renombrado los deje estables y el literal de nameof siga coincidiendo:

{
  "inputs": ["bin/Release/net8.0/OrderService.dll"],
  "outputDirectory": "bin/Release/net8.0/obf",
  "renameIdentifiers": true,
  "encryptStrings": true,
  "excludeMembers": ["OrderService.CustomerName"]   // bound by name in XAML + INPC — keep it stable
}

El caso de INotifyPropertyChanged es la trampa de manual. OnPropertyChanged() pasa "CustomerName" mediante [CallerMemberName], y el enlace de WPF/XAML resuelve la propiedad por ese nombre en tiempo de ejecución. Cifra el literal y el enlace sigue funcionando (se descifra a "CustomerName"). Renombra la propiedad a b sin excluirla, y ahora el literal dice CustomerName mientras la propiedad es b: el enlace deja de actualizarse en silencio. La solución es excluir las propiedades enlazadas del renombrado, exactamente como en la configuración de arriba, para que el nombre que el enlace necesita siga siendo el nombre que tiene la propiedad.

Verifica que la fuga está cerrada

Tras proteger, confirma que los nombres han desaparecido del montón de cadenas: la comprobación directa de que nameof/[CallerMemberName] ya no los divulgan:

# Dump the user-string heap of the protected assembly; your type names must not appear.
ikdasm /metadata=US obf/OrderService.dll | grep -i "OrderService\|CustomerName\|ValidateCoupon" || echo "clean"

Un resultado clean significa que los literales se cifraron; una coincidencia significa que una cadena sobrevivió a un paso —normalmente una excluida del cifrado—, lo que merece una segunda mirada si además lleva un nombre que querías ocultar.

Qué llevarse

El renombrado protege referencias; no protege los nombres que el compilador ya convirtió en datos. nameof y [CallerMemberName] congelan tus nombres originales de tipos y miembros en literales de cadena en tiempo de compilación, así que un binario renombrado aún puede entregarle a un lector su propio mapa. Cierra la fuga de información con cifrado de cadenas para que los nombres en texto plano salgan del montón. Luego ocúpate de los dos casos que el cifrado no puede resolver: donde un literal alimenta reflexión contra un miembro renombrado, mantén estable el nombre de ese miembro excluyéndolo del renombrado; donde un literal es un contrato externo como un enlace de INotifyPropertyChanged, excluye el miembro para que el nombre que el otro lado espera no se mueva. Cifra para la confidencialidad, excluye para la corrección y verifica que el montón está limpio: y tu build renombrada dejará de publicar los nombres que renombraste.

Prueba Nebula.NET

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