Ofuscar código que genera código: Reflection.Emit, DynamicMethod y árboles de expresión
Cuando tu aplicación emite IL en tiempo de ejecución — DynamicMethod, TypeBuilder, Expression.Compile — el renombrado puede romperla de formas que el compilador nunca advierte. Esto es exactamente qué patrones de generación de código sobreviven a la ofuscación y cuáles necesitan reglas de conservación.
Los serializadores, los ORM, los contenedores de inyección de dependencias, los mapeadores y los motores de reglas hacen todos el mismo truco por dentro: escriben IL en tiempo de ejecución en lugar de llamar a tus métodos directamente, porque un método generado y especializado es mucho más rápido que la reflexión en cada llamada. .NET te da tres formas principales de hacerlo — DynamicMethod + ILGenerator, TypeBuilder vía Reflection.Emit y Expression.Compile sobre árboles de expresión LINQ. Las tres interactúan con la ofuscación de una manera que el compilador no puede advertirte, porque la rotura ocurre en tiempo de ejecución, en código que no existía en tiempo de compilación. Este es el modelo preciso de qué es seguro y qué no.
La distinción clave: el generador se publica, el código generado no
Hay dos piezas de código distintas en juego, y confundirlas es donde empieza la confusión.
El código generado — el cuerpo del DynamicMethod, el tipo de TypeBuilder, el delegado de la expresión compilada — se crea en memoria en tiempo de ejecución y nunca se escribe en tu ensamblado, así que el ofuscador no lo ve: ni se protege ni se daña. (Si protegerlo importara, de todos modos no podrías; es efímero.)
El generador — el método lleno de llamadas a il.Emit(...), o el código que construye la expresión — sí está en tu ensamblado. Es IL corriente y se renombra, se aplana el flujo de control y se cifran sus cadenas como todo lo demás. Eso está bien, con una salvedad: el generador normalmente referencia tus tipos y miembros, y cómo los referencia decide si la ofuscación lo rompe.
Dónde se rompe: la búsqueda de miembros por cadena
El generador que se rompe es el que resuelve por nombre (cadena) los miembros a los que emite llamadas. Considera un DynamicMethod que llama a un getter que encontró por nombre:
MethodInfo getter = typeof(Order).GetMethod("get_Total")!; // string lookup
var dm = new DynamicMethod("ReadTotal", typeof(decimal), new[] { typeof(Order) });
var il = dm.GetILGenerator();
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Callvirt, getter); // emits a call to whatever `getter` resolved to
il.Emit(OpCodes.Ret);
Después de que la ofuscación renombre Order.Total (y su descriptor de acceso get_Total), la cadena "get_Total" ya no coincide con nada, así que GetMethod devuelve null y la línea siguiente lanza NullReferenceException / ArgumentNullException, en tiempo de ejecución y sin ninguna pista en compilación. El mismo fallo ocurre con el código de TypeBuilder que define sobrescrituras por nombre, y con Expression.Property(p, "Total").
La solución es una de dos cosas. Conservar el nombre de los miembros concretos que el generador busca por cadena (una regla de conservación específica, no una exclusión general). O, mejor, dejar de usar cadenas: captura el miembro mediante algo que el ofuscador pueda reescribir.
Lo que sobrevive: referencias que el ofuscador puede ver
Cuando el generador captura un miembro como referencia de metadatos en lugar de como cadena, el ofuscador reescribe esa referencia como parte del renombrado, y todo sigue funcionando:
// Survives renaming — MethodInfo captured from a delegate, not a string.
MethodInfo getter = ((Func<Order, decimal>)(o => o.Total)).Method; // won't help: that's a lambda's Method
// Better: read the MemberInfo out of an expression tree
Expression<Func<Order, decimal>> e = o => o.Total;
var prop = (PropertyInfo)((MemberExpression)e.Body).Member; // real PropertyInfo, token-based
Los árboles de expresión compilados son el caso amable: o => o.Total lleva una referencia PropertyInfo que el ofuscador renombra junto con la propiedad, de modo que Expression.Compile() enlaza con el miembro correcto después. La regla práctica: los árboles de expresión y typeof(T).GetMethod(referencia-derivada-de-MethodInfo) son seguros ante la reescritura; cualquier nombre de miembro como "cadena" en un generador no lo es.
Una lista de comprobación práctica
- Audita tus puntos de generación de código en tiempo de ejecución en busca de literales de cadena que nombren tus propios miembros; esos son los puntos de rotura.
- Prefiere árboles de expresión o captura de delegado/
MethodInfofrente aGetMethod("Nombre")cuando puedas. - Para las búsquedas por cadena inevitables, añade reglas de conservación específicas para exactamente esos miembros, y recuerda que sus cuerpos se siguen ofuscando; solo sobrevive el nombre.
- Prueba explícitamente las rutas de generación de código de la compilación ofuscada, porque ni el compilador ni una prueba de humo que se salte la ruta generada detectarán una emisión rota.
Cómo hacerlo con Nebula
Nebula renombra, aplana y cifra las cadenas de tus generadores como de cualquier otro código, y su modelo de reglas de conservación es lo bastante preciso para preservar exactamente los miembros que tu código de Reflection.Emit o de construcción de expresiones resuelve por nombre, mientras sigue ofuscando sus cuerpos. Combínalo con la guía de reflexión y serialización y podrás publicar código ofuscado que genera código sin una sola sorpresa en tiempo de ejecución.
Empieza con la edición gratuita, o consulta los planes.
Prueba Nebula.NET
Endurece tu código .NET en minutos — empieza con la edición gratuita.