Y tú, ¿dominas tus aplicaciones?
sábado 28 de octubre de 2006 | categorías: .net, los mejorcitos, seguridad | 2 comentarios -- da clic aquí para dejar el tuyoHace unos días, Eber Irigoyen escribió un post con un ejemplo muy padre de cómo invocar un método privado de una clase desde otra y otro de cómo instanciar una clase que tenga un constructor privado utilizando reflection (reflexión, pues).
En realidad es muy sencillo hacerlo, así que si eres tan neurótico como yo, lo primero que se te viene a la mente es "¡Ah jijos! Pero debe haber alguna manera de evitar eso, ¿no? Digo, ¿apoco cualquiera puede agarrar mi código y 'hackearlo' así tan pelada?" La respuesta es "sí hay manera de evitarlo, pero no es tan sencillo", debes tener una buena idea de cómo el CLR carga y ejecuta Assemblies, cómo funcionan los mecanismos de Code Access Security (CAS) que trae el .NET Framework, cómo se relacionan con los Application Domains, Reflection, etcétera.
Sería bastante material para un humilde post, así que para que no te me aburras voy a seguir la filosofía de "¿Cómo te comes un elefante?" y espero poder escribir varios artículos al respecto en los próximos días (bueno, semanas)--sí ya sé que todavía tengo pendientes terminar los de ADO.NET, no creas que se me ha olvidado.
Parte de este artículo está adaptado y (MUY) sintetizado del capítulo 8 del libro para la certificación MCTS, de Tony Nurthup [et.al.], que fue de donde yo lo aprendí. No me estoy pirateando su texto, pero me gustó mucho la manera en que explicó la "teoría", así que me basé en el, lo resumí y le agregué de lo mío. Sin embargo, lo menciono porque quizá te interese echarle un ojo a ese libro.
Assemblies y Application DomainsCuando compilas código de C# o VB.NET--o cualquier otro lenguaje que cumpla con el CLS--el resultado es un Assembly (¿ensamblaje?), o como decimos los mortales: un .EXE o .DLL. Sin embargo, ese ejecutable o DLL no está en código máquina, sino en código intermedio o MSIL (se pronuncia "misil").
Cuando quieres ejecutar o invocar el código en ese assembly, el Common Language Runtime (CLR) hace varias cosas, entre ellas:
- Carga el assembly y, de ser necesario, crea un Application Domain (dominio de aplicación) para ejecutarlo.
- Intenta "adivinar" los permisos con los que debe correr ese assembly (el nivel de seguridad) mediante evidencia y de acuerdo a la póliza de seguridad de la máquina.
- Hace una compilación Justo A Tiempo (JIT) del assembly a código nativo de acuerdo a la plataforma donde esté corriendo (x86, x64, ARM, etc.)
- Invoca el Entry Point (punto de entrada) del assembly para iniciar la ejecución.
Bueno, ¿pero qué carajos es un Application Domain? Es el análogo a un proceso del sistema operativo, pero para el .NET Runtime. Es decir, es un contenedor lógico que te permite correr varios assemblies bajo un mismo proceso (de S.O.), pero conservando muchos de los beneficios (p.ej., separación de memoria y de acceso a recursos).
Normalmente, el .NET Framework se encarga de crear el AppDomain default para tu assembly usando uno de los anfitriones que trae de cajón: ASP.NET Worker Process, Internet Explorer o Windows. Sin embargo, lo que muchos no saben es que puedes crear AppDomains a partir de tus propios assemblies que correrán contenidos dentro del AppDomain default.
Un buen ejemplo de esto son las aplicaciones web. Si tienes una aplicación web de ASP.NET y es accedida por N personas, el aspnet_wp.exe creará N instancias del assembly de tu aplicación, cada una dentro de un AppDomain separado.
Tú puedes hacer lo mismo en tus aplicaciones de una manera bastante sencilla. ¿Para qué? Se me ocurren 3 razones muy buenas:
- Seguridad: Puedes especificar evidencia (que determina el nivel de seguridad) al crear un AppDomain o al cargar un assembly en el AppDomain. Si estás usando, por ejemplo, un componente de terceros, es buena práctica no darle permisos FullTrust nomás porque sí, y limitarlo ya sea a nivel AppDomain o a nivel Assembly.
- Eficiencia: Si el assembly fue cargado en el AppDomain default, no podrás liberar su memoria hasta que termine el proceso anfitrión. Pero si lo cargas en un AppDomain separado, sí podrás hacerlo. Esto se puede ofrecer, por ejemplo, si tienes un servicio de Windows que hace uso de assemblies externos pero por un periodo corto de tiempo; te puede convenir liberar esa memoria cuando el servicio no esté chambeando.
- Confiabilidad: Puedes aislar código inestable que pudiera tronar tu aplicación (Crystal Reports anyone?).
Como podrás haber adivinado, dado que los AppDomains son importantes, el .NET Framework trae una clase especial para manejarlos: System.AppDomain
Estas son algunas de sus propiedades más interesantes (en mi humilde opinión):
- Evidence: Expone la evidencia asociada al AppDomain y que será verificada contra la póliza de seguridad de la máquina. Más sobre esto en el siguiente post.
- SetupInformation: Expone la configuración que se utilizó al crear el AppDomain.
- CurrentDomain: Devuelve una referencia al AppDomain del hilo actual de ejecución (current Thread).
- FriendlyName: Como su nombre lo dice, expone un nombre amigable que le puede asignar al crear el AppDomain. Si no lo especifican utiliza uno por omisión que quizá hayan visto en los mensajes del debugger(miassembly.vshost.exe)
Y de sus métodos, estos son los que me parecen más relevantes:
- CreateDomain: Sirve para, duh, crear un AppDomain. AppDomain usa el patrón Factory, así que en lugar de utilizar su constructor, utilizas este método.
- ExecuteAssembly y ExecuteAssemblyByName: Ejecutan un assembly dentro del AppDomain (otro duh). La única diferencia entre estos dos es que uno acepta la ruta al archivo del assembly y el otro solo el nombre (lo cual requiere que tengas una referencia a él en tu proyecto).
- Load: Carga un assembly dentro del AppDomain.
- Unload: Descarga el AppDomain completito. OJO: No puedes descargar assemblies o tipos individualmente.
- CreateInstance: Crea una instancia de algún tipo (clase) definida en un assembly.
- GetAssemblies: Expone los assemblies que han sido cargados en el contexto de ejecución actual.
Ahora sí, después del rollote, vamos aponerlo en práctica.
Hice una solución en en Visual Studio con dos proyectos de tipo Windows Console Application, uno llamado Anfitrion y otro llamado OtraAplicacion. Anfitrion tiene una referencia a OtraAplicacion, lo cual nos permite ejecutarlo por nombre. OtraAplicacion solo escribe un mensaje a la consola de "Hola, soy la Otra Aplicación".
// Crear el nuevo dominio. AppDomain usa el patron factory, así que se necesita // utilizar CreateDomain en lugar de su constructor. AppDomain d = AppDomain.CreateDomain("OtroDominio");
// Mostremos el nombre del dominio anfitrión y el del dominio que fue creado Console.WriteLine("Dominio anfitrion: {0}", AppDomain.CurrentDomain.FriendlyName);
Console.WriteLine("Dominio creado: {0}", d.FriendlyName);
// Cargar y ejecutar un assembly, sin especificar evidencia, en el nuevo dominio. d.ExecuteAssemblyByName("OtraAplicacion"); // Descargar el dominio AppDomain.Unload(d); Ahora sí, veamos qué tiene que ver todo esto con seguridad de código. Para correr un dominio o un assembly con privilegios limitados, necesitamos especificar evidencia en el momento de crear el AppDomain o de invocar los métodos Load, ExecuteAssembly o ExecuteAssemblyByName.
Para no desviar mucho del tema, solo te diré por ahora que evidencia se refiere a "pistas" que le puedes dar al .NET Framework acerca de la identidad del assembly y su procedencia. Basado en estas pistas, el runtime determinará a qué grupo de código pertenece y por lo tanto con qué permisos correrá. Si este párrafo solo te hizo pelotas, no te preocupes, intentaré explicarlo más a detalle en otro post.Ahora, cambiemos OtraAplicacion para que haga algo más travieso que simplemente escribir un mensaje:
using System; using System.IO; namespace OtraAplicacion { class Program
{ static void Main(string[] args)
{ //Console.WriteLine("Hola, soy la OtraAplicacion"); File.Delete(@"C:\boot.ini");
}
}
}
Podemos protegernos de él si cambiamos el código que lo invoca de la siguiente manera:
// asumiendo que se tienes // using System.Security.Policy // crear un objeto de evidencia para limitar permisos. object[] pistas = { new Zone(System.Security.SecurityZone.Internet) };
Evidence evidencia = new Evidence(pistas, null);
// crear dominio con permisos restringidos AppDomain d = AppDomain.CreateDomain("OtroDominio", evidencia);
try { // también se puede especificar la evidencia al // cargar o ejectuar el assembly. Por ejemplo // d.ExecuteAssemblyByName("OtraAplicacion", evidencia, null); d.ExecuteAssemblyByName("OtraAplicacion"); }
catch (System.Security.SecurityException ex)
{ Console.WriteLine(ex.Message); }
finally { AppDomain.Unload(d); }
Aunque este error no es el que esperaba (UIPermission se debe a que una aplicación corrida desde la zona de Internet no puede ejecutar aplicaciones con interfaz, ni siquiera de consola), sirve para demostrar que nuestro assembly está corriendo ahora en un AppDomain restringido.Habrás notado que para crear la evidencia tuve que utilizar un arreglo de object. Esto es porque puedes especificar varios tipos de objetos como "pistas" y estas pistas pueden ser cualquier cosa, strings, int's o tipos. Los más sencillos de usar son los tipos que vienen en el espacio de nombres System.Security.Policy, como Zone, que fué el que utilicé en el ejemplo.
Otra cosa importante que quizá notaste es que esta misma técnica podría utilizarse para correr una aplicación con permisos MENOS restrictivos ya que en realidad estamos proveyendo evidencia falsa sobre nuestro assembly. Después veremos cómo impedir esto.
Por ahora, espero haberte dado al menos una idea de cómo cargar y administrar tu código utilizando dominios de aplicación y su relación con la seguridad de tu código. En la próxima nos clavamos ya un poco más con lo que son los permisos, permission sets, códigos de grupo, etcétera.
Enjoy.





Por RSS o Atom

