Documentos de Académico
Documentos de Profesional
Documentos de Cultura
Índice
Objetivos
Objetivos de la protección
Principios de protección
Anillos de protección
Dominio de protección
Estructura del dominio
UNIX
ID de aplicación de Android
Matriz de acceso
Objetivos
● Discutir los objetivos y principios de protección en un sistema informático moderno.
● Explicar cómo se utilizan los dominios de protección, combinados con una matriz de
acceso, para especificar los recursos a los que puede acceder un proceso.
● Examinar los sistemas de protección basados en la capacidad y el lenguaje.
● Describir cómo los mecanismos de protección pueden mitigar los ataques al sistema.
Objetivos de la protección
A medida que los sistemas informáticos se han vuelto más sofisticados y generalizados en
sus aplicaciones, también ha aumentado la necesidad de proteger su integridad. La
protección se concibió originalmente como un complemento de los sistemas operativos
multiprogramados, de modo que los usuarios no confiables pudieran compartir de manera
segura un espacio de nombre lógico común, como un directorio de archivos, o un espacio
de nombre físico común, como la memoria. Los conceptos de protección modernos han
evolucionado para aumentar la confiabilidad de cualquier sistema complejo que hace uso de
recursos compartidos y está conectado a plataformas de comunicaciones inseguras como
Internet.
Las políticas para el uso de recursos pueden variar según la aplicación y pueden cambiar
con el tiempo. Por estas razones, la protección ya no es asunto exclusivo del diseñador de
un sistema operativo. El programador de aplicaciones también necesita utilizar mecanismos
de protección para proteger los recursos creados y respaldados por un subsistema de
aplicaciones contra el mal uso. Describimos los mecanismos de protección que debería
proporcionar el sistema operativo, pero los diseñadores de aplicaciones también pueden
utilizarlos para diseñar su propio software de protección.
Tenga en cuenta que los mecanismos son distintos de las políticas. Los mecanismos
determinan cómo se hará algo; las políticas deciden lo que se hará. La separación de
política y mecanismo es importante para la flexibilidad. Es probable que las políticas
cambien de un lugar a otro o de vez en cuando. En el peor de los casos, todo cambio de
política requeriría un cambio en el mecanismo subyacente. El uso de mecanismos
generales nos permite evitar tal situación.
Principios de protección
Con frecuencia, se puede utilizar un principio rector a lo largo de un proyecto, como el
diseño de un sistema operativo. Seguir este principio simplifica las decisiones de diseño y
mantiene el sistema consistente y fácil de entender. Un principio rector clave y comprobado
para la protección es el principio de privilegio mínimo. Este principio dicta que a los
programas, usuarios e incluso a los sistemas se les otorguen los privilegios necesarios para
realizar sus tareas.
Considere uno de los principios de UNIX: que un usuario no debe ejecutarse como root. (En
UNIX, solo el usuario root puede ejecutar comandos con privilegios). La mayoría de los
usuarios respetan eso de forma innata, por temor a una operación de eliminación accidental
para la que no hay una recuperación correspondiente. Debido a que root es virtualmente
omnipotente, el potencial de error humano cuando un usuario actúa como root es grave y
sus consecuencias de gran alcance.
Ahora considere que, más que un error humano, el daño puede resultar de un ataque
malintencionado. Un virus lanzado por un clic accidental en un archivo adjunto es un
ejemplo. Otro es un desbordamiento de búfer u otro ataque de inyección de código que se
lleva a cabo con éxito contra un proceso con privilegios de root (o, en Windows, un proceso
con privilegios de administrador). Cualquiera de los dos casos podría resultar catastrófico
para el sistema.
El principio de privilegio mínimo adopta muchas formas, que examinaremos con más detalle
más adelante. Otro principio importante, a menudo visto como un derivado del principio de
privilegio mínimo, es la compartimentación. La compartimentación es el proceso de
proteger cada componente individual del sistema mediante el uso de permisos específicos y
restricciones de acceso. Entonces, si un componente es subvertido, otra línea de defensa
se activará y evitará que el atacante comprometa más el sistema. La compartimentación se
implementa de muchas formas, desde las zonas desmilitarizadas de la red (DMZ) hasta la
virtualización.
El uso cuidadoso de las restricciones de acceso puede ayudar a que un sistema sea más
seguro y también puede ser beneficioso para producir una pista de auditoría, que rastrea
las divergencias de los accesos permitidos. Una pista de auditoría es un registro duro en los
registros del sistema. Si se monitorea de cerca, puede revelar advertencias tempranas de
un ataque o (si su integridad se mantiene a pesar de un ataque) proporcionar pistas sobre
qué vectores de ataque se usaron, así como evaluar con precisión el daño causado.
Quizás lo más importante es que ningún principio es una panacea para las vulnerabilidades
de seguridad. Se debe usar una defensa en profundidad: se deben aplicar múltiples capas
de protección una encima de la otra (piense en un castillo con una guarnición, un muro y un
foso para protegerlo). Al mismo tiempo, por supuesto, los atacantes utilizan múltiples
medios para eludir la defensa en profundidad, lo que resulta en una carrera armamentista
cada vez mayor.
Anillos de protección
Como hemos visto, el componente principal de los sistemas operativos modernos es el
kernel, que administra el acceso a los recursos del sistema y al hardware. El kernel, por
definición, es un componente confiable y privilegiado y, por lo tanto, debe ejecutarse con un
nivel más alto de privilegios que los procesos de usuario.
Cuando el sistema arranca, arranca con el nivel de privilegio más alto. El código de ese
nivel realiza la inicialización necesaria antes de pasar a un nivel con menos privilegios. Para
volver a un nivel de privilegio superior, el código suele llamar a una instrucción especial, a
veces denominada puerta, que proporciona un portal entre anillos. La instrucción syscall
(en Intel) es un ejemplo. Llamar a esta instrucción cambia la ejecución del modo de usuario
al modo de kernel. Como hemos visto, la ejecución de una llamada al sistema siempre
Las arquitecturas Intel siguen este modelo, colocando el código de modo usuario en el anillo
3 y el código de modo kernel en el anillo 0. La distinción se hace mediante dos bits en el
registro especial EFLAGS. El acceso a este registro no está permitido en el anillo 3, lo que
evita que un proceso malicioso aumente los privilegios. Con el advenimiento de la
virtualización, Intel definió un anillo adicional (-1) para permitir hipervisores o
administradores de máquinas virtuales, que crean y ejecutan máquinas virtuales. Los
hipervisores tienen más capacidades que los núcleos de los sistemas operativos invitados.
La arquitectura del procesador ARM inicialmente permitía solo los modos USR y SVC, para
el modo de usuario y kernel (supervisor), respectivamente. En los procesadores ARMv7,
ARM introdujo TrustZone (TZ), que proporcionó un anillo adicional. Este entorno de
ejecución más privilegiado también tiene acceso exclusivo a funciones criptográficas
respaldadas por hardware, como NFC Secure Element y una clave criptográfica en chip,
que hacen que el manejo de contraseñas e información confidencial sea más seguro.
Incluso el kernel en sí no tiene acceso a la clave en el chip, y solo puede solicitar servicios
de cifrado y descifrado del entorno TrustZone (mediante una instrucción especializada,
Secure Monitor Call (SMC)), que solo se puede usar desde el modo kernel . Al igual que
con las llamadas al sistema, el kernel no tiene la capacidad de ejecutarse directamente en
direcciones específicas en TrustZone, solo para pasar argumentos a través de registros.
Android usa TrustZone ampliamente a partir de la versión 5, como se muestra en la figura 2.
En la arquitectura ARMv8 de 64 bits, ARM extendió su modelo para admitir cuatro niveles,
llamados "niveles de excepción", numerados de EL0 a EL3. El modo de usuario se ejecuta
en EL0 y el modo de kernel en EL1. EL2 está reservado para hipervisores y EL3 (el más
privilegiado) está reservado para el monitor seguro (la capa TrustZone). Cualquiera de los
niveles de excepción permite ejecutar sistemas operativos separados uno al lado del otro,
como se muestra en la figura 3.
Tenga en cuenta que el monitor seguro se ejecuta a un nivel de ejecución más alto que los
núcleos de uso general, lo que lo convierte en el lugar perfecto para implementar código
que verificará la integridad de los núcleos. Esta funcionalidad está incluida en Realtime
Kernel Protection (RKP) de Samsung para Android y WatchTower de Apple (también
conocida como KPP, para Kernel Patch Protection) para iOS.
Dominio de protección
Los anillos de protección separan las funciones en dominios y las ordenan jerárquicamente.
Una generalización de los anillos es el uso de dominios sin jerarquía. Un sistema
informático puede tratarse como una colección de procesos y objetos. Por objetos, nos
Las operaciones que son posibles dependen del objeto. Por ejemplo, en una CPU, solo
podemos ejecutar. Las palabras de la memoria se pueden leer y escribir, mientras que un
DVD-ROM solo se puede leer. Las unidades de cinta se pueden leer, escribir y rebobinar.
Los archivos de datos se pueden crear, abrir, leer, escribir, cerrar y eliminar; los archivos de
programa se pueden leer, escribir, ejecutar y eliminar.
Se debe permitir que un proceso acceda solo a aquellos objetos para los que tiene
autorización. Además, en cualquier momento, un proceso debería poder acceder solo a
aquellos objetos que actualmente requiere para completar su tarea. Este segundo requisito,
el principio de necesidad de saber, es útil para limitar la cantidad de daño que un proceso
defectuoso o un atacante pueden causar en el sistema. Por ejemplo, cuando el proceso p
invoca el procedimiento A(), al procedimiento se le debe permitir acceder solo a sus propias
variables y los parámetros formales que se le pasan; no debería poder acceder a todas las
variables del proceso p. De manera similar, considere el caso en el que el proceso p invoca
a un compilador para compilar un archivo en particular. El compilador no debería poder
acceder a los archivos de forma arbitraria, sino que debería tener acceso solo a un
subconjunto bien definido de archivos (como el archivo de origen, el archivo de objeto de
salida, etc.) relacionados con el archivo que se va a compilar. Por el contrario, el compilador
puede tener archivos privados utilizados con fines de contabilidad u optimización a los que
el proceso p no debería poder acceder.
Al comparar la necesidad de saber con el privilegio mínimo, puede ser más fácil pensar en
la necesidad de saber como la política y el privilegio mínimo como el mecanismo para lograr
esta política. Por ejemplo, en los permisos de archivos, la necesidad de conocer puede
exigir que un usuario tenga acceso de lectura pero no acceso de escritura o ejecución a un
archivo. El principio de privilegio mínimo requeriría que el sistema operativo proporcione un
mecanismo para permitir el acceso de lectura, pero no de escritura o ejecución.
Los dominios pueden compartir derechos de acceso. Por ejemplo, en la figura 4, tenemos
tres dominios: D1, D2 y D3. El derecho de acceso <O4, {print}> es compartido por D2 y
D3, lo que implica que un proceso que se ejecuta en cualquiera de estos dos dominios
puede imprimir el objeto O4. Tenga en cuenta que un proceso debe estar ejecutándose en el
dominio D1 para leer y escribir el objeto O1, mientras que solo los procesos en el dominio D3
pueden ejecutar el objeto O1.
● Cada usuario puede ser un dominio. En este caso, el conjunto de objetos a los que
se puede acceder depende de la identidad del usuario. El cambio de dominio ocurre
cuando se cambia de usuario, generalmente cuando un usuario cierra sesión y otro
usuario inicia sesión.
● Cada proceso puede ser un dominio. En este caso, el conjunto de objetos a los que
se puede acceder depende de la identidad del proceso. El cambio de dominio ocurre
cuando un proceso envía un mensaje a otro proceso y luego espera una respuesta.
● Cada procedimiento puede ser un dominio. En este caso, el conjunto de objetos a
los que se puede acceder corresponde a las variables locales definidas dentro del
procedimiento. La conmutación de dominio se produce cuando se realiza una
llamada a procedimiento.
Considere el modelo estándar de modo dual (modo kernel-usuario) de ejecución del sistema
operativo. Cuando un proceso está en modo kernel, puede ejecutar instrucciones
privilegiadas y así obtener el control completo del sistema informático. Por el contrario,
cuando un proceso se ejecuta en modo de usuario, solo puede invocar instrucciones sin
privilegios. En consecuencia, solo se puede ejecutar dentro de su espacio de memoria
predefinido. Estos dos modos protegen el sistema operativo (que se ejecuta en el dominio
del kernel) de los procesos del usuario (que se ejecuta en el dominio del usuario). En un
sistema operativo multiprogramado, dos dominios de protección son insuficientes, ya que
los usuarios también quieren protegerse entre sí. Por lo tanto, se necesita un esquema más
elaborado. Ilustramos un esquema de este tipo examinando dos sistemas operativos
influyentes, UNIX y Android, para ver cómo implementan estos conceptos.
UNIX
Como se señaló anteriormente, en UNIX, el usuario root puede ejecutar comandos con
privilegios, mientras que otros usuarios no pueden. Sin embargo, restringir ciertas
operaciones al usuario root puede afectar a otros usuarios en sus operaciones diarias.
Considere, por ejemplo, un usuario que quiere cambiar su contraseña. Inevitablemente, esto
requiere acceso a la base de datos de contraseñas (comúnmente, /etc/shadow), a la que
solo puede acceder el root. Se encuentra un desafío similar al configurar un trabajo
planificado (usando el comando at); hacerlo requiere acceso a directorios privilegiados que
están fuera del alcance de un usuario normal.
Si eso le parece alarmante, es por una buena razón. Debido a su poder potencial, se espera
que los binarios ejecutables de setuid sean estériles (afectando solo los archivos necesarios
bajo restricciones específicas) y herméticos (por ejemplo, a prueba de manipulaciones e
imposibles de subvertir). Los programas de Setuid deben escribirse con mucho cuidado
para garantizar estas capacidades. Volviendo al ejemplo de cambio de contraseñas, el
comando passwd es setuid-root y de hecho modificará la base de datos de contraseñas,
pero solo si primero se le presenta la contraseña válida del usuario, y luego se limitará a
editar la contraseña de ese usuario y solo ese usuario.
ID de aplicación de Android
En Android, se proporcionan ID de usuario distintos para cada aplicación. Cuando se instala
una aplicación, el demonio installd le asigna un ID de usuario (UID) y un ID de grupo
(GID) distintos, junto con un directorio de datos privados (/data/data/<appname>) cuya
propiedad se otorga solo a esta combinación de UID/GID. De esta manera, las aplicaciones
en el dispositivo disfrutan del mismo nivel de protección proporcionado por los sistemas
UNIX para usuarios separados. Esta es una forma rápida y sencilla de proporcionar
aislamiento, seguridad y privacidad. El mecanismo se amplía modificando el kernel para
permitir ciertas operaciones (como sockets de red) solo a miembros de un GID en particular
(por ejemplo, AID_INET, 3003). Una mejora adicional de Android es definir ciertos UID como
"aislados", lo que les impide iniciar solicitudes RPC a cualquier servicio que no sea un
mínimo.
Matriz de acceso
El modelo general de protección puede verse de forma abstracta como una matriz,
denominada matriz de acceso. Las filas de la matriz de acceso representan dominios y las
columnas representan objetos. Cada entrada de la matriz consta de un conjunto de
derechos de acceso. Debido a que la columna define objetos explícitamente, podemos
omitir el nombre del objeto del derecho de acceso. La entrada acceso (i, j) define el conjunto
de operaciones que un proceso que se ejecuta en el dominio Di puede invocar sobre el
objeto Oj.
Para ilustrar estos conceptos, consideramos la matriz de acceso que se muestra en la figura
5. Hay cuatro dominios y cuatro objetos: tres archivos (F1, F2, F3) y una impresora láser. Un
proceso que se ejecuta en el dominio D1 puede leer los archivos F1 y F3. Un proceso que
se ejecuta en el dominio D4 tiene los mismos privilegios que uno que se ejecuta en el
dominio D1; pero además, también puede escribir en archivos F1 y F3. Solo se puede
acceder a la impresora láser mediante un proceso que se ejecuta en el dominio D2.
(i, j). También debemos decidir el dominio en el que se ejecuta cada proceso. Esta última
política generalmente la decide el sistema operativo.
Los procesos deberían poder cambiar de un dominio a otro. El cambio del dominio Di al
dominio Dj está permitido si y solo si el derecho de acceso cambio ∈ access(i, j). Por
tanto, en la figura 6, un proceso que se ejecuta en el dominio D2 puede cambiar al dominio
D3 o al dominio D4. Un proceso en el dominio D4 puede cambiar a D1 y uno en el dominio D1
puede cambiar a D2.
Un sistema puede seleccionar solo uno de estos tres derechos de copia, o puede
proporcionar los tres identificándolos como derechos separados: copia, transferencia
y copia limitada.
Los derechos de copia y propietario permiten que un proceso cambie las entradas en
una columna. También se necesita un mecanismo para cambiar las entradas en una fila. El
derecho de control es aplicable solo a los objetos de dominio. Si el acceso(i,j)
incluye el derecho de control, entonces un proceso que se ejecuta en el dominio Di
puede eliminar cualquier derecho de acceso de la fila j. Por ejemplo, suponga que, en la
figura 6, incluimos el derecho de control en el acceso(D2,D4). Entonces, un proceso
que se ejecuta en el dominio D2 podría modificar el dominio D4, como se muestra en la
figura 9.
Los derechos de copia y de propietario nos proporcionan un mecanismo para limitar la
propagación de los derechos de acceso. Sin embargo, no nos brindan las herramientas
adecuadas para prevenir la propagación (o divulgación) de información. El problema de
garantizar que ninguna información contenida inicialmente en un objeto pueda migrar fuera
de su entorno de ejecución se denomina problema de confinamiento. Este problema es,
en general, irresoluble (véanse las notas bibliográficas al final del capítulo).
Tabla global
La implementación más simple de la matriz de acceso es una tabla global que consta de un
conjunto de triples ordenados <dominio, objeto, conjunto-de-derechos>.
Siempre que se ejecuta una operación M en un objeto Oj dentro del dominio Di, se busca en
la tabla global un triple <Di, Oj, Rk>, con M ∈ Rk. Si se encuentra este triple, se permite que
la operación continúe; de lo contrario, se genera una condición de excepción (o error).
Esta implementación adolece de varios inconvenientes. La tabla suele ser grande y, por lo
tanto, no se puede guardar en la memoria principal, por lo que se necesitan E/S adicionales.
Las técnicas de memoria virtual se utilizan a menudo para gestionar esta tabla. Además, es
difícil aprovechar agrupaciones especiales de objetos o dominios. Por ejemplo, si todos
pueden leer un objeto en particular, este objeto debe tener una entrada separada en cada
dominio.
Este enfoque se puede ampliar fácilmente para definir una lista más un conjunto
predeterminado de derechos de acceso. Cuando se intenta una operación M sobre un
objeto Oj en el dominio Di, buscamos en la lista de acceso el objeto Oj, buscando una
entrada <Di, Rk> con M ∈ Rk. Si se encuentra la entrada, permitimos la operación; si no es
así, verificamos el conjunto predeterminado. Si M está en el conjunto predeterminado,
permitimos el acceso. De lo contrario, se deniega el acceso y se produce una condición de
excepción. Para mayor eficiencia, podemos verificar primero el conjunto predeterminado y
luego buscar en la lista de acceso.
Las capacidades se propusieron originalmente como una especie de puntero seguro, para
satisfacer la necesidad de protección de recursos que se preveía a medida que los sistemas
informáticos multiprogramados alcanzaban la mayoría de edad. La idea de un puntero
inherentemente protegido proporciona una base para la protección que puede extenderse
hasta el nivel de la aplicación.
Para proporcionar protección inherente, debemos distinguir las capacidades de otros tipos
de objetos, y deben ser interpretados por una máquina abstracta en la que se ejecutan
programas de nivel superior. Las capacidades generalmente se distinguen de otros datos en
una de dos formas:
● Cada objeto tiene una etiqueta para indicar si es una capacidad o datos accesibles.
Las etiquetas en sí mismas no deben ser directamente accesibles por un programa
de aplicación. Se puede usar soporte de hardware o firmware para hacer cumplir
esta restricción. Aunque solo se necesita un bit para distinguir entre capacidades y
otros objetos, a menudo se utilizan más bits. Esta extensión permite que todos los
objetos sean etiquetados con sus tipos por el hardware. Por lo tanto, el hardware
puede distinguir enteros, números de punto flotante, punteros, booleanos,
caracteres, instrucciones, capacidades y valores no inicializados por sus etiquetas.
● Alternativamente, el espacio de direcciones asociado con un programa se puede
dividir en dos partes. Una parte es accesible para el programa y contiene los datos e
instrucciones normales del programa. La otra parte, que contiene la lista de
capacidades, es accesible solo por el sistema operativo. Un espacio de memoria
segmentado es útil para respaldar este enfoque.
Comparación
Como era de esperar, la elección de una técnica para implementar una matriz de acceso
implica varias compensaciones. Usar una tabla global es simple; sin embargo, la tabla
puede ser bastante grande y, a menudo, no puede aprovechar agrupaciones especiales de
objetos o dominios. Las listas de acceso corresponden directamente a las necesidades de
los usuarios. Cuando un usuario crea un objeto, puede especificar qué dominios pueden
acceder al objeto, así como qué operaciones están permitidas. Sin embargo, debido a que
la información de derechos de acceso para un dominio en particular no está localizada, es
difícil determinar el conjunto de derechos de acceso para cada dominio. Además, se debe
verificar cada acceso al objeto, lo que requiere una búsqueda en la lista de acceso. En un
sistema grande con listas de acceso largas, esta búsqueda puede llevar mucho tiempo.
Las capacidades, sin embargo, presentan un problema de revocación mucho más difícil,
como se mencionó anteriormente. Dado que las capacidades se distribuyen por todo el
sistema, debemos encontrarlas antes de poder revocarlas. Los esquemas que implementan
la revocación de las capacidades incluyen los siguientes:
Este esquema no permite la revocación selectiva, ya que solo se asocia una clave
maestra a cada objeto. Si asociamos una lista de claves con cada objeto, entonces
se puede implementar la revocación selectiva. Finalmente, podemos agrupar todas
las claves en una tabla global de claves. Una capacidad es válida solo si su clave
coincide con alguna clave en la tabla global. Implementamos la revocación
eliminando la clave correspondiente de la tabla. Con este esquema, una clave se
puede asociar con varios objetos y varias claves se pueden asociar con cada objeto,
proporcionando la máxima flexibilidad.
En los esquemas basados en claves, las operaciones de definir claves, insertarlas
en listas y eliminarlas de listas no deberían estar disponibles para todos los
usuarios. En particular, sería razonable permitir que solo el propietario de un objeto
establezca las claves para ese objeto. Esta elección, sin embargo, es una decisión
política que el sistema de protección puede implementar pero no debe definir.
Control de acceso basado en roles
Anteriormente, describimos cómo se pueden usar los controles de acceso en archivos
dentro de un sistema de archivos. A cada archivo y directorio se le asigna un propietario, un
grupo o posiblemente una lista de usuarios, y para cada una de esas entidades, se asigna
información de control de acceso. Se puede agregar una función similar a otros aspectos de
un sistema informático. Un buen ejemplo de esto se encuentra en Solaris 10 y versiones
posteriores.
manera, un usuario puede asumir un rol que habilita un privilegio, lo que le permite ejecutar
un programa para realizar una tarea específica, como se muestra en la figura 10. Esta
implementación de privilegios reduce el riesgo de seguridad asociado con los superusuarios
y los programas setuid.
Sin embargo, los DAC han resultado insuficientes a lo largo de los años. Una debilidad
clave radica en su naturaleza discrecional, que permite al propietario de un recurso
establecer o modificar sus permisos. Otra debilidad es el acceso ilimitado permitido para el
administrador o usuario root. Como hemos visto, este diseño puede dejar al sistema
vulnerable a ataques tanto accidentales como maliciosos y no ofrece ninguna defensa
cuando los piratas informáticos obtienen privilegios de root.
Por tanto, surgió la necesidad de una forma de protección más fuerte, que se introdujo en
forma de control de acceso obligatorio (MAC). MAC se aplica como una política del
sistema que incluso el usuario root no puede modificar (a menos que la política permita
explícitamente modificaciones o se reinicie el sistema, generalmente en una configuración
alternativa). Las restricciones impuestas por las reglas de la política de MAC son más
poderosas que las capacidades del usuario root y pueden usarse para hacer que los
recursos sean inaccesibles para cualquier persona que no sea su propietario.
Todos los sistemas operativos modernos proporcionan MAC junto con DAC, aunque las
implementaciones difieren. Solaris fue uno de los primeros en introducir MAC, que formaba
parte de Trusted Solaris (2.5). FreeBSD hizo que DAC fuera parte de su implementación de
TrustedBSD (FreeBSD 5.0). La implementación de FreeBSD fue adoptada por Apple en
macOS 10.5 y ha servido como el sustrato sobre el cual se implementan la mayoría de las
características de seguridad de MAC e iOS. La implementación de MAC de Linux es parte
del proyecto SELinux, que fue ideado por la NSA y se ha integrado en la mayoría de las
distribuciones. Microsoft Windows se unió a la tendencia con el control de integridad
obligatorio de Windows Vista.
Capacidades de Linux
Linux usa capacidades para abordar las limitaciones del modelo UNIX, que describimos
anteriormente. El grupo de estándares POSIX introdujo capacidades en POSIX 1003.1e.
Aunque POSIX.1e finalmente se retiró, Linux adoptó rápidamente capacidades en la Versión
2.2 y ha seguido agregando nuevos desarrollos.
En esencia, las capacidades de Linux "dividen" los poderes de root en áreas distintas, cada
una representada por un bit en una máscara de bits, como se muestra en la figura 11. Se
puede lograr un control detallado sobre las operaciones privilegiadas alternando bits en la
máscara de bits.
En la práctica, se utilizan tres máscaras de bits, que indican las capacidades permitidas,
efectivas y heredables. Las máscaras de bits se pueden aplicar por proceso o por
subproceso. Además, una vez revocadas, las capacidades no se pueden volver a adquirir.
La secuencia habitual de eventos es que un proceso o subproceso comienza con el
conjunto completo de capacidades permitidas y voluntariamente disminuye ese conjunto
durante la ejecución. Por ejemplo, después de abrir un puerto de red, un subproceso puede
eliminar esa capacidad para que no se puedan abrir más puertos.
Probablemente pueda ver que las capacidades son una implementación directa del principio
de privilegio mínimo. Como se explicó anteriormente, este principio de seguridad dicta que a
una aplicación o usuario solo se le deben otorgar los derechos necesarios para su
funcionamiento normal.
Android (que se basa en Linux) también utiliza capacidades, que habilitan los procesos del
sistema (en particular, el "servidor del sistema"), para evitar la propiedad de root, en lugar
de habilitar selectivamente solo aquellas operaciones requeridas.
El modelo de capacidades de Linux es una gran mejora con respecto al modelo tradicional
de UNIX, pero aún es inflexible. Por un lado, el uso de un mapa de bits con un bit que
representa cada capacidad hace que sea imposible agregar capacidades dinámicamente y
requiere recompilar el kernel para agregar más. Además, la función se aplica solo a las
capacidades aplicadas por el kernel.
Derechos de Darwin
La protección del sistema de Apple toma la forma de derechos. Los derechos son permisos
declarativos: lista de propiedades XML que indica qué permisos reclama el programa como
necesarios (consulte la figura 12). Cuando el proceso intenta una operación privilegiada (en
la figura, cargando una extensión del kernel), se verifican sus derechos y solo si los
Para evitar que los programas reclamen arbitrariamente un derecho, Apple incrusta los
derechos en la firma del código. Una vez cargado, un proceso no tiene forma de acceder a
su firma de código. Otros procesos (y el kernel) pueden consultar fácilmente la firma y, en
particular, los derechos. Por lo tanto, verificar un derecho es una simple operación de
coincidencia de cadenas. De esta manera, solo las aplicaciones autenticadas y verificables
pueden reclamar derechos. Todos los derechos del sistema (com.apple.*) están además
restringidos a los propios binarios de Apple.
Bajo SIP, aunque root sigue siendo el usuario más poderoso del sistema, puede hacer
mucho menos que antes. El usuario root aún puede administrar los archivos de otros
usuarios, así como instalar y eliminar programas, pero de ninguna manera que reemplace o
modifique los componentes del sistema operativo. SIP se implementa como una pantalla
global ineludible en todos los procesos, con las únicas excepciones permitidas para los
binarios del sistema (por ejemplo, fsck o kextload, como se muestra en la figura 12),
que están específicamente autorizados para operaciones para su propósito designado.
Una segunda forma de filtrado de llamadas al sistema es aún más profunda e inspecciona
los argumentos de cada llamada al sistema. Esta forma de protección se considera mucho
más sólida, ya que incluso las llamadas al sistema aparentemente benignas pueden
albergar vulnerabilidades graves. Este fue el caso de la llamada al sistema fast mutex
(futex) de Linux. La condición de Arace en su implementación condujo a una sobrescritura
de la memoria del kernel controlada por el atacante y al compromiso total del sistema. Los
mutex son un componente fundamental de la multitarea y, por lo tanto, la llamada del
sistema en sí no se puede filtrar por completo.
Un desafío que se encuentra con ambos enfoques es mantenerlos lo más flexibles posible
y, al mismo tiempo, evitar la necesidad de reconstruir el kernel cuando se requieren cambios
o nuevos filtros, una ocurrencia común debido a las diferentes necesidades de los diferentes
procesos. La flexibilidad es especialmente importante dada la naturaleza impredecible de
las vulnerabilidades. Todos los días se descubren nuevas vulnerabilidades y los atacantes
pueden explotarlas de inmediato.
Un enfoque para afrontar este desafío es desacoplar la implementación del filtro del propio
kernel. El kernel solo necesita contener un conjunto de llamadas, que luego se pueden
implementar en un controlador especializado (Windows), módulo del kernel (Linux) o
extensión (Darwin). Debido a que un componente modular externo proporciona la lógica de
filtrado, se puede actualizar independientemente del kernel. Este componente suele utilizar
un lenguaje de creación de perfiles especializado al incluir un intérprete o analizador
integrado. Por lo tanto, el perfil en sí puede desacoplarse del código, proporcionando un
perfil editable y legible por humanos y simplificando aún más las actualizaciones. También
es posible que el componente de filtrado llame a un proceso de demonio en modo de
usuario de confianza para ayudar con la lógica de validación.
Sandboxing
Sandboxing implica la ejecución de procesos en entornos que limitan lo que pueden hacer.
En un sistema básico, un proceso se ejecuta con las credenciales del usuario que lo inició y
tiene acceso a todas las cosas a las que el usuario puede acceder. Si se ejecuta con
privilegios del sistema, como root, el proceso literalmente puede hacer cualquier cosa en el
sistema. Casi siempre ocurre que un proceso no necesita privilegios completos de usuario o
del sistema. Por ejemplo, ¿un procesador de texto necesita aceptar conexiones de red?
¿Un servicio de red que proporciona la hora del día necesita acceder a archivos más allá de
un conjunto específico?
Existen numerosos enfoques para el sandboxing. Java y .net, por ejemplo, imponen
restricciones de espacio aislado en el nivel de la máquina virtual. Otros sistemas imponen el
sandboxing como parte de su política de control de acceso obligatorio (MAC). Un ejemplo
es Android, que se basa en una política de SELinux mejorada con etiquetas específicas
para las propiedades del sistema y los puntos finales del servicio.
Entre los principales proveedores, Apple fue el primero en implementar el sandboxing, que
apareció en macOS 10.5 ("Tiger") como "Seatbelt". El cinturón de seguridad era "opt-in" en
lugar de obligatorio, lo que permitía pero no requería que las aplicaciones lo usaran. El
sandbox de Apple se basaba en perfiles dinámicos escritos en el lenguaje Scheme, que
brindaba la capacidad de controlar no solo qué operaciones debían permitirse o bloquearse,
sino también sus argumentos. Esta capacidad permitió a Apple crear diferentes perfiles
personalizados para cada binario en el sistema, una práctica que continúa hasta el día de
hoy. La figura 13 muestra un ejemplo de perfil.
Las políticas para el uso de recursos también pueden variar, según la aplicación, y pueden
estar sujetas a cambios con el tiempo. Por estas razones, la protección ya no puede
considerarse un asunto de interés exclusivo para el diseñador de un sistema operativo.
También debe estar disponible como herramienta para que la utilice el diseñador de la
aplicación, de modo que los recursos de un subsistema de la aplicación puedan protegerse
contra la manipulación o la influencia de un error.
Una forma de hacer que la protección esté disponible para el programa de aplicación es
mediante el uso de una capacidad de software que podría usarse como un objeto de
cálculo. Inherente a este concepto es la idea de que ciertos componentes del programa
pueden tener el privilegio de crear o examinar estas capacidades de software. Un programa
de creación de capacidad sería capaz de ejecutar una operación primitiva que sellaría una
estructura de datos, haciendo que el contenido de este último fuera inaccesible para
cualquier componente del programa que no tuviera el privilegio de seal o unseal. Dichos
componentes pueden copiar la estructura de datos o pasar su dirección a otros
componentes del programa, pero no pueden acceder a su contenido. La razón para
introducir tales capacidades de software es incorporar un mecanismo de protección al
lenguaje de programación. El único problema con el concepto propuesto es que el uso del
sello y las operaciones de apertura del sello adopta un enfoque de procedimiento para
especificar la protección. Una notación declarativa o no procesal parece una forma
preferible de hacer que la protección esté disponible para el programador de aplicaciones.
1. Distribuir capacidades de manera segura y eficiente entre los procesos del cliente.
En particular, los mecanismos aseguran que un proceso de usuario utilizará el
recurso administrado solo si se le otorgó una capacidad para ese recurso.
2. Especificar el tipo de operaciones que un proceso en particular puede invocar en un
recurso asignado (por ejemplo, un lector de un archivo debe poder leer sólo el
archivo, mientras que un escritor debe poder leer y escribir). No debería ser
necesario otorgar el mismo conjunto de derechos a cada proceso de usuario, y
debería ser imposible que un proceso amplíe su conjunto de derechos de acceso,
excepto con la autorización del mecanismo de control de acceso.
3. Especificar el orden en el que un proceso en particular puede invocar las diversas
operaciones de un recurso (por ejemplo, un archivo debe abrirse antes de que
pueda leerse). Debería ser posible dar dos procesos diferentes restricciones sobre el
orden en el que pueden invocar las operaciones del recurso asignado.
La incorporación de conceptos de protección a los lenguajes de programación, como una
herramienta práctica para el diseño de sistemas, está en su infancia. La protección
probablemente se convertirá en un tema de mayor preocupación para los diseñadores de
nuevos sistemas con arquitecturas distribuidas y requisitos cada vez más estrictos sobre la
seguridad de los datos. Entonces se reconocerá más ampliamente la importancia de las
notaciones lingüísticas adecuadas para expresar los requisitos de protección.
Debido a estas capacidades, la protección es una preocupación primordial. Las clases que
se ejecutan en la misma JVM pueden provenir de diferentes fuentes y pueden no ser
igualmente confiables. Como resultado, hacer cumplir la protección en la granularidad del
proceso JVM es insuficiente. Intuitivamente, si se debe permitir una solicitud para abrir un
archivo, generalmente dependerá de qué clase haya solicitado la apertura. El sistema
operativo carece de este conocimiento.
Por lo tanto, dichas decisiones de protección se manejan dentro de la JVM. Cuando la JVM
carga una clase, la asigna a un dominio de protección que otorga los permisos de esa clase.
El dominio de protección al que se asigna la clase depende de la URL desde la que se
cargó la clase y de cualquier firma digital en el archivo de clase. Un archivo de política
configurable determina los permisos otorgados al dominio (y sus clases). Por ejemplo, las
clases cargadas desde un servidor confiable pueden ubicarse en un dominio de protección
que les permite acceder a archivos en el directorio de inicio del usuario, mientras que las
clases cargadas desde un servidor que no es de confianza pueden no tener ningún permiso
de acceso a archivos.
Puede resultar complicado para la JVM determinar qué clase es responsable de una
solicitud para acceder a un recurso protegido. Los accesos a menudo se realizan de forma
indirecta, a través de bibliotecas del sistema u otras clases. Por ejemplo, considere una
clase a la que no se le permite abrir conexiones de red. Podría llamar a una biblioteca del
sistema para solicitar la carga del contenido de una URL. La JVM debe decidir si abre o no
una conexión de red para esta solicitud. Pero, ¿qué clase debe usarse para determinar si se
debe permitir la conexión, la aplicación o la biblioteca del sistema?
acceso a un recurso protegido, ya sea por este método o por un método al que llama, se
usa una llamada a checkPermissions() para invocar la inspección de la pila para
determinar si la solicitud debe ser permitido. La inspección examina los marcos de la pila en
la pila del subproceso que realiza la llamada, comenzando desde el marco agregado más
recientemente y avanzando hacia el más antiguo. Si primero se encuentra un marco de pila
que tiene la anotación doPrivileged(), checkPermissions() regresa de forma
inmediata y silenciosa, permitiendo el acceso. Si primero se encuentra un marco de pila
para el que no se permite el acceso en función del dominio de protección de la clase del
método, check-permissions() arroja una AccessControlException. Si la
inspección de la pila agota la pila sin encontrar ningún tipo de marco, entonces si se permite
el acceso depende de la implementación (algunas implementaciones de la JVM pueden
permitir el acceso, mientras que otras implementaciones no).
La inspección de la pila se ilustra en la figura 14. Aquí, el método gui() de una clase en el
dominio de protección del subprograma que no es de confianza realiza dos operaciones,
primero un get() y luego un open(). El primero es una invocación del método get() de
una clase en el dominio de protección del cargador de URL, que puede abrir sesiones ()
a sitios en el dominio lucent.com, en particular un servidor proxy proxy.lucent.com
para recuperar URLs. Por esta razón, la invocación get() del subprograma que no es de
confianza tendrá éxito: la llamada checkPermissions() en la biblioteca de red en
contrarresta el marco de pila del método get(), que realizó su open() en un bloque
doPrivileged. Sin embargo, la invocación open() del subprograma que no es de
confianza dará como resultado una excepción, porque la llamada checkPermissions()
no encuentra ninguna anotación doPrivileged antes de encontrar el marco de pila del
método gui().
Por supuesto, para que funcione la inspección de la pila, un programa debe ser incapaz de
modificar las anotaciones en su propio marco de pila o manipular la inspección de la pila.
Esta es una de las diferencias más importantes entre Java y muchos otros lenguajes
(incluido C ++). Un programa Java no puede acceder directamente a la memoria; sólo
puede manipular un objeto para el que tiene una referencia. Las referencias no se pueden
falsificar y las manipulaciones se realizan solo a través de interfaces bien definidas. El
cumplimiento se aplica a través de una colección sofisticada de comprobaciones de tiempo
de carga y tiempo de ejecución. Como resultado, un objeto no puede manipular su pila en
tiempo de ejecución porque no puede obtener una referencia a la pila u otros componentes
del sistema de protección.