Está en la página 1de 35

Protección

Í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

Implementación de la matriz de acceso


Tabla global
Listas de acceso para objetos
Listas de capacidad para dominios
Un mecanismo de bloqueo con llave
Comparación

Revocación de los derechos de acceso

Control de acceso basado en roles

Control de acceso obligatorio (MAC)

Sistemas basados ​en la capacidad


Capacidades de Linux
Derechos de Darwin

Otros métodos de mejora de la protección


Protección de la integridad del sistema
Filtrado de llamadas al sistema
Sandboxing
Firma de código

Protección basada en el lenguaje


Aplicación basada en compilador
Aplicación basada en el runtime: protección en Java
En el tema anterior, abordamos la seguridad, que implica proteger los recursos de la
computadora contra el acceso no autorizado, la destrucción o alteración maliciosa y la
introducción accidental de inconsistencias. En este capítulo pasamos a la protección, que
implica controlar el acceso de procesos y usuarios a los recursos definidos por un sistema
informático. Los procesos de un sistema operativo deben protegerse de las actividades de
los demás. Para brindar esta protección, podemos usar varios mecanismos para asegurar
que solo los procesos que hayan obtenido la autorización adecuada del sistema operativo
puedan operar en los archivos, segmentos de memoria, CPU, redes y otros recursos de un
sistema. Estos mecanismos deben proporcionar un medio para especificar los controles que
se impondrán, junto con un medio de ejecución.

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.

Necesitamos brindar protección por varias razones. La más obvia es la necesidad de


prevenir la violación intencional y maliciosa de una restricción de acceso por parte de un
usuario. Sin embargo, es de importancia más general la necesidad de garantizar que cada
proceso de un sistema utilice los recursos del sistema solo de manera coherente con las
políticas establecidas. Este requisito es absoluto para un sistema confiable.

La protección puede mejorar la confiabilidad al detectar errores latentes en las interfaces


entre los subsistemas de componentes. La detección temprana de errores de interfaz a
menudo puede prevenir la contaminación de un subsistema en buen estado por un
subsistema que funciona mal. Además, un recurso desprotegido no puede defenderse del
uso (o mal uso) por parte de un usuario no autorizado o incompetente. Un sistema orientado
a la protección proporciona medios para distinguir entre uso autorizado y no autorizado.

La función de la protección en un sistema informático es proporcionar un mecanismo para la


aplicación de las políticas que rigen el uso de recursos. Estas políticas se pueden
establecer de diversas formas. Algunas se fijan en el diseño del sistema, mientras que otras
se formulan mediante la gestión de un sistema. Otros son definidos por usuarios
individuales para proteger los recursos que "poseen". Un sistema de protección, entonces,
debe tener la flexibilidad para hacer cumplir una variedad de políticas.

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.

Observar el principio de privilegios mínimos le daría al sistema la oportunidad de mitigar el


ataque; si el código malicioso no puede obtener privilegios de root, existe la posibilidad de
que los permisos adecuadamente definidos bloqueen todas, o al menos algunas, las
operaciones dañinas. En este sentido, los permisos pueden actuar como un sistema
inmunológico a nivel del sistema operativo.

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.

Para llevar a cabo esta separación de privilegios, se requiere soporte de hardware. De


hecho, todo el hardware moderno admite la noción de niveles de ejecución separados,
aunque las implementaciones varían un poco. Un modelo popular de separación de
privilegios es el de los anillos de protección. En este modelo, la ejecución se define como un
conjunto de anillos concéntricos, con el anillo i proporcionando un subconjunto de la
funcionalidad del anillo j para cualquier j < i. El anillo más interno, el anillo 0, proporciona así
el conjunto completo de privilegios. Este patrón se muestra en la figura 1.

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

transferirá la ejecución a una dirección predefinida, lo que permitirá al llamador especificar


solo argumentos (incluido el número de llamada al sistema) y no direcciones arbitrarias del
kernel. De esta manera, generalmente se puede asegurar la integridad del anillo más
privilegiado.
Otra forma de terminar en un anillo con más privilegios es cuando se produce una
interrupción o una trampa del procesador. Cuando ocurre cualquiera de las dos, la ejecución
se transfiere inmediatamente al anillo de mayor privilegio. Una vez más, sin embargo, la
ejecución en el anillo de mayor privilegio está predefinida y restringida a una ruta de código
bien protegida.

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.

Emplear correctamente un entorno de ejecución confiable significa que, si el kernel está


comprometido, un atacante no puede simplemente recuperar la clave de la memoria del
kernel. Trasladar los servicios criptográficos a un entorno de confianza independiente
también hace que los ataques de fuerza bruta tengan menos probabilidades de éxito. (Estos
ataques implican probar todas las combinaciones posibles de caracteres de contraseña
válidos hasta que se encuentra la contraseña). Las diversas claves utilizadas por el sistema,
desde la contraseña del usuario hasta la del sistema, se almacenan en la clave del chip , al
que solo se puede acceder en un contexto de confianza. Cuando se ingresa una clave, por
ejemplo, una contraseña, se verifica mediante una solicitud al entorno TrustZone. Si no se
conoce una clave y debe adivinarse, el verificador TrustZone puede imponer limitaciones,
por ejemplo, limitando el número de intentos de verificación.

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

referimos tanto a objetos de hardware (como la CPU, segmentos de memoria, impresoras,


discos y unidades de cinta) como a objetos de software (como archivos, programas y
semáforos). Cada objeto tiene un nombre único que lo diferencia de todos los demás
objetos del sistema, y ​solo se puede acceder a cada uno mediante operaciones bien
definidas y significativas. Los objetos son esencialmente tipos de datos abstractos.

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.

Estructura del dominio


Para facilitar el tipo de esquema que se acaba de describir, un proceso puede operar dentro
de un dominio de protección, que especifica los recursos a los que puede acceder el
proceso. Cada dominio define un conjunto de objetos y los tipos de operaciones que pueden
invocarse en cada objeto. La capacidad de ejecutar una operación en un objeto es un
derecho de acceso. Un dominio es una colección de derechos de acceso, cada uno de los
cuales es un par ordenado <nombre de objeto, conjunto de derechos>. Por
ejemplo, si el dominio D tiene el derecho de acceso <archivo F, {leer,
escribir}>, entonces un proceso que se ejecuta en el dominio D puede leer y escribir el
archivo F. Sin embargo, no puede realizar ninguna otra operación en ese objeto.

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.

La asociación entre un proceso y un dominio puede ser estática, si el conjunto de recursos


disponibles para el proceso es fijo a lo largo de la vida del proceso, o dinámica. Como era
de esperar, establecer dominios de protección dinámica es más complicado que establecer
dominios de protección estática.

Si la asociación entre procesos y dominios es fija, y queremos adherirnos al principio de


necesidad de saber, entonces debe haber un mecanismo disponible para cambiar el
contenido de un dominio. La razón se debe al hecho de que un proceso puede ejecutarse
en dos fases diferentes y puede, por ejemplo, necesitar acceso de lectura en una fase y
acceso de escritura en otra. Si un dominio es estático, debemos definir el dominio para
incluir tanto el acceso de lectura como el de escritura. Sin embargo, esta disposición
proporciona más derechos de los necesarios en cada una de las dos fases, ya que tenemos
acceso de lectura en la fase en la que solo necesitamos acceso de escritura, y viceversa.
De esta forma, se viola el principio de necesidad de saber, debemos permitir que se
modifiquen los contenidos de un dominio para que el dominio refleje siempre los derechos
de acceso mínimos necesarios.

Si la asociación es dinámica, hay un mecanismo disponible para permitir el cambio de


dominio, permitiendo que el proceso cambie de un dominio a otro. También es posible que
queramos permitir que se cambie el contenido de un dominio. Si no podemos cambiar el
contenido de un dominio, podemos proporcionar el mismo efecto creando un nuevo dominio
con el contenido cambiado y cambiando a ese nuevo dominio cuando queramos cambiar el
contenido del dominio.

Un dominio se puede realizar de varias formas:

● 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.

La solución a este problema es el bit setuid. En UNIX, una identificación de propietario y un


bit de dominio, conocido como bit setuid, están asociados con cada archivo. El bit setuid
puede estar habilitado o no. Cuando el bit está habilitado en un archivo ejecutable (a través
de chmod +s), quien ejecuta el archivo asume temporalmente la identidad del propietario
del archivo. Eso significa que si un usuario logra crear un archivo con el ID de usuario "root"
y el bit setuid habilitado, cualquiera que obtenga acceso para ejecutar el archivo se
convierte en usuario "root" durante la duración del proceso.

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.

Desafortunadamente, la experiencia ha demostrado repetidamente que pocos binarios


setuid, si es que hay alguno, cumplen ambos criterios con éxito. Una y otra vez, los binarios
de setuid se han subvertido, algunos a través de condiciones de carrera y otros a través de
la inyección de código, lo que proporciona acceso de root instantáneo a los atacantes. Los
atacantes suelen tener éxito en lograr la escalada de privilegios de esta manera.

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.

El esquema de matriz de acceso nos proporciona el mecanismo para especificar una


variedad de políticas. El mecanismo consiste en implementar la matriz de acceso y asegurar
que las propiedades semánticas que hemos delineado se mantengan Más específicamente,
debemos asegurar que un proceso que se ejecuta en el dominio Di pueda acceder solo a
aquellos objetos especificados en la fila i, y luego solo lo permitido por el acceso -Entradas
de matriz.

La matriz de acceso puede implementar decisiones de política relacionadas con la


protección. Las decisiones de política involucran qué derechos deben incluirse en la entrada

(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 usuarios normalmente deciden el contenido de las entradas de la matriz de acceso.


Cuando un usuario crea un nuevo objeto Oj, la columna Oj se agrega a la matriz de acceso
con las entradas de inicialización apropiadas, según lo indique el creador. El usuario puede
decidir ingresar algunos derechos en algunas entradas en la columna j y otros derechos en
otras entradas, según sea necesario.

La matriz de acceso proporciona un mecanismo apropiado para definir e implementar un


control estricto para la asociación tanto estática como dinámica entre procesos y dominios.
Cuando cambiamos un proceso de un dominio a otro, estamos ejecutando una operación
(cambio) en un objeto (el dominio). Podemos controlar el cambio de dominio incluyendo
dominios entre los objetos de la matriz de acceso. De manera similar, cuando cambiamos el
contenido de la matriz de acceso, estamos realizando una operación en un objeto: la matriz
de acceso. Nuevamente, podemos controlar estos cambios incluyendo la propia matriz de
acceso como un objeto. En realidad, dado que cada entrada en la matriz de acceso se
puede modificar individualmente, debemos considerar cada entrada en la matriz de acceso
como un objeto a proteger. Ahora, debemos considerar solo las operaciones posibles en
estos nuevos objetos (dominios y la matriz de acceso) y decidir cómo queremos que los
procesos puedan ejecutar estas operaciones.

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.

Permitir un cambio controlado en el contenido de las entradas de la matriz de acceso


requiere tres operaciones adicionales: copy, owner y control. Examinamos estas
operaciones a continuación.

La capacidad de copiar un derecho de acceso de un dominio (o fila) de la matriz de acceso


a otro se indica con un asterisco (*) adjunto al derecho de acceso. El derecho de copia
permite copiar el derecho de acceso solo dentro de la columna (es decir, para el objeto)
para la que se define el derecho. Por ejemplo, en la figura 7 (a), un proceso que se ejecuta
en el dominio D2 puede copiar la operación de lectura en cualquier entrada asociada con el
archivo F2. Por lo tanto, la matriz de acceso de la figura 7 (a) se puede modificar a la matriz
de acceso que se muestra en la figura 7 (b).

Este esquema tiene dos variantes adicionales:

1. El derecho se copia del acceso(i,j) al acceso(k,j); luego se elimina del


acceso(i, j). Esta acción es una transferencia de un derecho, más que una
copia.
2. La propagación del derecho de copia puede estar limitada. Es decir, cuando se
copia el derecho R∗ del acceso(i,j) al acceso(k,j), solo se crea el derecho R
(no R∗). Un proceso que se ejecuta en el dominio Dk no puede seguir copiando el
derecho R.

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.

También necesitamos un mecanismo que permita la adición de nuevos derechos y la


eliminación de algunos derechos. El derecho de propiedad controla estas operaciones. Si
el acceso(i,j) incluye el derecho de propietario, entonces un proceso que se
ejecuta en el dominio Di puede agregar y eliminar cualquier derecho en cualquier entrada en
la columna j. Por ejemplo, en la figura 8 (a), el dominio D1 es el propietario de F1 y, por lo
tanto, puede agregar y eliminar cualquier derecho válido en la columna F1. De manera
similar, el dominio D2 es el propietario de F2 y F3 y, por lo tanto, puede agregar y eliminar
cualquier derecho válido dentro de estas dos columnas. Por tanto, la matriz de acceso de la
figura 8 (a) se puede modificar a la matriz de acceso que se muestra en la figura 8 (b).

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).

Estas operaciones en los dominios y la matriz de acceso no son importantes en sí mismas,


pero ilustran la capacidad del modelo de matriz de acceso para permitirnos implementar y
controlar los requisitos de protección dinámica. Se pueden crear nuevos objetos y nuevos
dominios de forma dinámica e incluirlos en el modelo de matriz de acceso. Sin embargo,
solo hemos demostrado que existe el mecanismo básico. Los diseñadores de sistemas y los
usuarios deben tomar las decisiones de política sobre qué dominios deben tener acceso a
qué objetos y de qué manera.

Implementación de la matriz de acceso


¿Cómo se puede implementar la matriz de acceso de manera efectiva? En general, la
matriz será escasa; es decir, la mayoría de las entradas estarán vacías. Aunque se dispone
de técnicas de estructura de datos para representar matrices dispersas, no son
particularmente útiles para esta aplicación, debido a la forma en que se utiliza la facilidad de
protección. Aquí, primero describimos varios métodos para implementar la matriz de acceso
y luego comparamos los métodos.

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.

Listas de acceso para objetos


Cada columna de la matriz de acceso se puede implementar como una lista de acceso para
un objeto. Obviamente, las entradas vacías se pueden descartar. La lista resultante para
cada objeto consta de pares ordenados <dominio, conjunto-de-derechos>, que
definen todos los dominios con un conjunto no vacío de derechos de acceso para ese
objeto.

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.

Listas de capacidad para dominios


En lugar de asociar las columnas de la matriz de acceso con los objetos como listas de
acceso, podemos asociar cada fila con su dominio. Una lista de capacidades para un
dominio es una lista de objetos junto con las operaciones permitidas en esos objetos. Un
objeto a menudo se representa por su nombre físico o dirección, llamado capacidad. Para
ejecutar la operación M en el objeto Oj, el proceso ejecuta la operación M, especificando la
capacidad (o puntero) para el objeto Oj como parámetro. La simple posesión de la
capacidad significa que se permite el acceso.

La lista de capacidades está asociada con un dominio, pero nunca es directamente


accesible para un proceso que se ejecuta en ese dominio. Más bien, la lista de capacidades
es en sí misma un objeto protegido, mantenido por el sistema operativo y al que el usuario
accede solo indirectamente. La protección basada en capacidades se basa en el hecho de
que las capacidades nunca pueden migrar a ningún espacio de direcciones directamente
accesible por un proceso de usuario (donde podrían modificarse). Si todas las capacidades
son seguras, el objeto que protegen también está seguro contra el acceso no autorizado.

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.

Un mecanismo de bloqueo con llave


El esquema de candado con llave es un compromiso entre las listas de acceso y las listas
de capacidades. Cada objeto tiene una lista de patrones de bits únicos llamados candados.
De manera similar, cada dominio tiene una lista de patrones de bits únicos llamados claves.
Un proceso que se ejecuta en un dominio puede acceder a un objeto solo si ese dominio
tiene una clave que coincide con uno de los candados del objeto. Al igual que con las listas
de capacidades, la lista de claves para un dominio debe ser administrada por el sistema
operativo en nombre del dominio. Los usuarios no pueden examinar o modificar la lista de
llaves (o candados) directamente.

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 listas de capacidades no corresponden directamente a las necesidades de los usuarios,


pero son útiles para localizar información para un proceso dado. El proceso que intenta
acceder debe presentar una capacidad para ese acceso. Entonces, el sistema de protección
solo necesita verificar que la capacidad sea válida. Sin embargo, la revocación de
capacidades puede ser ineficaz.

El mecanismo de llave de bloqueo, como se mencionó, es un compromiso entre las listas de


acceso y las listas de capacidades. El mecanismo puede ser eficaz y flexible, dependiendo
de la longitud de las teclas. Las claves se pueden pasar libremente de un dominio a otro.
Además, los privilegios de acceso se pueden revocar eficazmente mediante la sencilla
técnica de cambiar algunos de los bloqueos asociados con el objeto.

La mayoría de los sistemas utilizan una combinación de listas de acceso y capacidades.


Cuando un proceso intenta acceder a un objeto por primera vez, se busca en la lista de
acceso. Si se deniega el acceso, se produce una condición de excepción. De lo contrario,
se crea una capacidad y se adjunta al proceso. Las referencias adicionales utilizan la
capacidad para demostrar rápidamente que se permite el acceso. Después del último
acceso, la capacidad se destruye. Esta estrategia se utilizó en el sistema MULTICS y en el
sistema CAL.

Como ejemplo de cómo funciona dicha estrategia, considere un sistema de archivos en el


que cada archivo tiene una lista de acceso asociada. Cuando un proceso abre un archivo,
se busca en la estructura del directorio para encontrar el archivo, se verifica el permiso de
acceso y se asignan búferes. Toda esta información se registra en una nueva entrada en
una tabla de archivos asociada al proceso. La operación devuelve un índice en esta tabla
para el archivo recién abierto. Todas las operaciones en el archivo se realizan mediante la
especificación del índice en la tabla de archivos. La entrada en la tabla de archivos luego
apunta al archivo y sus búferes. Cuando se cierra el archivo, se elimina la entrada de la
tabla de archivos. Dado que el sistema operativo mantiene la tabla de archivos, el usuario
no puede dañarla accidentalmente. De esta forma, el usuario puede acceder únicamente a
aquellos archivos que se han abierto. Dado que el acceso se comprueba cuando se abre el
archivo, la protección está garantizada. Esta estrategia se utiliza en el sistema UNIX.

El derecho de acceso aún debe verificarse en cada acceso, y la entrada de la tabla de


archivos tiene una capacidad solo para las operaciones permitidas. Si se abre un archivo
para lectura, se coloca una capacidad de acceso de lectura en la entrada de la tabla de
archivos. Si se intenta escribir en el archivo, el sistema identifica esta violación de
protección comparando la operación solicitada con la capacidad en la entrada de la tabla de
archivos.

Revocación de los derechos de acceso


En un sistema de protección dinámico, es posible que a veces necesitemos revocar los
derechos de acceso a los objetos compartidos por diferentes usuarios. Pueden surgir varias
preguntas sobre la revocación:

● Inmediata versus diferida. ¿La revocación se produce de inmediato o se retrasa?


Si la revocación se retrasa, ¿podemos saber cuándo tendrá lugar?
● Selectivo versus general. Cuando se revoca un derecho de acceso a un objeto,
¿afecta a todos los usuarios que tienen derecho de acceso a ese objeto, o podemos
especificar un grupo selecto de usuarios cuyos derechos de acceso deberían ser
revocados?
● Parcial versus total. ¿Se puede revocar un subconjunto de los derechos asociados
con un objeto, o debemos revocar todos los derechos de acceso para este objeto?
● Temporal versus permanente. ¿Se puede revocar el acceso de forma permanente
(es decir, el derecho de acceso revocado nunca volverá a estar disponible), o se
puede revocar el acceso y volver a obtenerlo más tarde?

Con un esquema de lista de acceso, la revocación es fácil. Se busca en la lista de acceso


los derechos de acceso que se van a revocar y se eliminan de la lista. La revocación es
inmediata y puede ser general o selectiva, total o parcial, permanente o temporal.

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:

● Readquisición. Periódicamente, las capacidades se eliminan de cada dominio. Si un


proceso desea utilizar una capacidad, puede encontrar que esa capacidad ha sido
eliminada. El proceso puede intentar volver a adquirir la capacidad. Si se ha
revocado el acceso, el proceso no podrá volver a adquirir la capacidad.
● Back-pointers. Se mantiene una lista de punteros con cada objeto, apuntando a
todas las capacidades asociadas con ese objeto. Cuando se requiere la revocación,
podemos seguir estos punteros, cambiando las capacidades según sea necesario.
Este esquema fue adoptado en el sistema MULTICS. Es bastante general, pero su
implementación es costosa.
● Indirección. Las capacidades apuntan indirectamente, no directamente, a los
objetos. Cada capacidad apunta a una entrada única en una tabla global, que a su
vez apunta al objeto. Implementamos la revocación buscando en la tabla global la
entrada deseada y eliminándola. Luego, cuando se intenta un acceso, se encuentra
que la capacidad apunta a una entrada de tabla ilegal. Las entradas de la tabla se
pueden reutilizar para otras capacidades sin dificultad, ya que tanto la capacidad
como la entrada de la tabla contienen el nombre exclusivo del objeto. El objeto de
una capacidad y su entrada de tabla deben coincidir. Este esquema fue adoptado en
el sistema CAL. No permite la revocación selectiva.
● Llaves. Una clave es un patrón de bits único que se puede asociar con una
capacidad. Esta clave se define cuando se crea la capacidad y no puede ser
modificada ni inspeccionada por el proceso que posee la capacidad. Una clave
maestra está asociada a cada objeto; se puede definir o reemplazar con la operación
de set-key. Cuando se crea una capacidad, el valor actual de la clave maestra se
asocia con la capacidad. Cuando se ejerce la capacidad, su clave se compara con la
clave maestra. Si las claves coinciden, se permite que la operación continúe; de lo
contrario, se genera una condición de excepción. La revocación reemplaza la clave
maestra con un nuevo valor a través de la operación de establecer clave,
invalidando todas las capacidades anteriores para este objeto.

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.

La idea es avanzar en la protección disponible en el sistema operativo agregando


explícitamente el principio de privilegio mínimo a través del control de acceso basado en
roles (RBAC). Esta facilidad gira en torno a los privilegios. Un privilegio es el derecho a
ejecutar una llamada al sistema o utilizar una opción dentro de esa llamada al sistema
(como abrir un archivo con acceso de escritura). Se pueden asignar privilegios a los
procesos, limitándolos exactamente al acceso que necesitan para realizar su trabajo. Los
privilegios y programas también se pueden asignar a los roles. A los usuarios se les
asignan roles o pueden tomar roles según las contraseñas asignadas a los roles. De esta

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.

Control de acceso obligatorio (MAC)


Los sistemas operativos han utilizado tradicionalmente el control de acceso discrecional
(DAC) como un medio para restringir el acceso a archivos y otros objetos del sistema. Con
DAC, el acceso se controla en función de las identidades de usuarios individuales o grupos.
En el sistema basado en UNIX, DAC toma la forma de permisos de archivo (configurables
por chmod, chown y chgrp), mientras que Windows (y algunas variantes de UNIX) permiten
una granularidad más fina por medio de listas de control de acceso (ACL).

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.

En el corazón de MAC está el concepto de etiquetas. Una etiqueta es un identificador


(generalmente una cadena) asignado a un objeto (archivos, dispositivos y similares). Las
etiquetas también se pueden aplicar a sujetos (actores, como procesos). Cuando un sujeto
solicita realizar operaciones en los objetos. Cuando dichas solicitudes deben ser atendidas
por el sistema operativo, primero realiza comprobaciones definidas en una política, que
dicta si se permite o no a un sujeto que contiene la etiqueta realizar la operación en el
objeto etiquetado.

Como un breve ejemplo, considere un conjunto simple de etiquetas, ordenadas según el


nivel de privilegio: "sin clasificar", "secreto" y "alto secreto". Un usuario con autorización
"secreta" podrá crear procesos etiquetados de manera similar, que luego tendrán acceso a
archivos "no clasificados" y "secretos", pero no a archivos "ultrasecretos". Ni el usuario ni
sus procesos estarían al tanto de la existencia de archivos “ultrasecretos”, ya que el sistema
operativo los filtraría de todas las operaciones de archivos (por ejemplo, no se mostrarían al
listar el contenido del directorio). De manera similar, los procesos de usuario se protegerían
a sí mismos de esta manera, de modo que un proceso "no clasificado" no podría ver o
realizar solicitudes de IPC a un proceso "secreto" (o "ultrasecreto"). De esta manera, las
etiquetas MAC son una implementación de la matriz de acceso descrita anteriormente.
Sistemas basados ​en la capacidad
El concepto de protección basada en la capacidad se introdujo a principios de la década
de 1970. Los dos primeros sistemas de investigación fueron Hydra y CAP. Ninguno de los
sistemas fue ampliamente utilizado, pero ambos proporcionaron interesantes bases de
prueba para las teorías de protección. Aquí, consideramos dos enfoques más
contemporáneos de las capacidades.

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

derechos necesarios están presentes, la operación se permite.

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.

Otros métodos de mejora de la protección


A medida que aumenta la batalla para proteger los sistemas de daños accidentales y
maliciosos, los diseñadores de sistemas operativos están implementando más tipos de
mecanismos de protección en más niveles. Esta sección analiza algunas mejoras
importantes de protección en el mundo real.

Protección de la integridad del sistema


Apple introdujo en macOS 10.11 un nuevo mecanismo de protección llamado Protección
de la integridad del sistema (SIP). Los sistemas operativos basados ​en Darwin usan SIP
para restringir el acceso a los archivos y recursos del sistema de tal manera que incluso el
usuario root no pueda manipularlos. SIP usa atributos extendidos en los archivos para
marcarlos como restringidos y protege aún más los binarios del sistema para que no puedan
ser depurados o examinados, y mucho menos manipulados. Lo más importante es que solo
se permiten las extensiones del kernel con código firmado y SIP se puede configurar para
permitir solo binarios con código firmado.

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.

Filtrado de llamadas al sistema


Recordemos que los sistemas monolíticos colocan toda la funcionalidad del kernel en un
solo archivo que se ejecuta en un solo espacio de direcciones. Por lo general, los núcleos
del sistema operativo de propósito general son monolíticos y, por lo tanto, se confía
implícitamente en ellos como seguros. El límite de confianza, por lo tanto, se encuentra
entre el modo de kernel y el modo de usuario, en la capa del sistema. Podemos suponer
razonablemente que cualquier intento de comprometer la integridad del sistema se realizará
desde el modo de usuario mediante una llamada al sistema. Por ejemplo, un atacante
puede intentar obtener acceso explotando una llamada al sistema no protegida.

Por lo tanto, es imperativo implementar alguna forma de filtrado de llamadas al sistema.


Para lograr esto, podemos agregar código al kernel para realizar una inspección en la
puerta de llamada al sistema, restringiendo a una persona que llama a un subconjunto de
llamadas al sistema consideradas seguras o requeridas para la función de esa persona que
llama. Se pueden construir perfiles de llamadas al sistema específicos para procesos
individuales. El mecanismo de Linux SECCOMP-BPF hace precisamente eso,
aprovechando el lenguaje de filtro de paquetes de Berkeley para cargar un perfil
personalizado a través de la llamada al sistema prctl patentado de Linux. Este filtrado es
voluntario, pero puede aplicarse eficazmente si se llama desde una biblioteca en tiempo de
ejecución cuando se inicializa o desde el propio cargador antes de que transfiera el control
al punto de entrada del programa.

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?

El término sandboxing se refiere a la práctica de imponer limitaciones estrictas a un


proceso. En lugar de darle a ese proceso el conjunto completo de llamadas al sistema que
sus privilegios permitirían, imponemos un conjunto inamovible de restricciones en el
proceso en las primeras etapas de su inicio, mucho antes de la ejecución de su función
main() y, a menudo, tan pronto como su creación con la llamada al sistema fork. Luego, el
proceso se vuelve incapaz de realizar operaciones fuera de su conjunto permitido. De esta
manera, es posible evitar que el proceso se comunique con cualquier otro componente del
sistema, lo que resulta en una compartimentación estrecha que mitiga cualquier daño al
sistema incluso si el proceso está comprometido.

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.

El sandboxing también se puede implementar como una combinación de múltiples


mecanismos. Android ha encontrado que SELinux es útil pero deficiente, porque no puede
restringir de manera efectiva las llamadas al sistema individuales. Las últimas versiones de
Android ("Nougat" y "O") utilizan un mecanismo de Linux subyacente llamado
SECCOMP-BPF, mencionado anteriormente, para aplicar restricciones de llamadas al
sistema mediante el uso de una llamada al sistema especializada. La biblioteca de tiempo
de ejecución de C en Android ("Bionic") llama a esta llamada del sistema para imponer
restricciones en todos los procesos de Android y aplicaciones de terceros.

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.

El sandboxing de Apple ha evolucionado considerablemente desde sus inicios. Ahora se


usa en las variantes de iOS, donde sirve (junto con la firma de código) como la principal
protección contra código de terceros que no es de confianza. En iOS, y a partir de macOS
10.8, la zona de pruebas de macOS es obligatoria y se aplica automáticamente a todas las
aplicaciones descargadas de la tienda Mac. Más recientemente, como se mencionó
anteriormente, Apple adoptó la protección de integridad del sistema (SIP), utilizada en
macOS 10.11 y versiones posteriores. SIP es, en efecto, un "perfil de plataforma" de todo el
sistema. Apple lo aplica desde el inicio del sistema en todos los procesos del sistema. Solo
los procesos autorizados pueden realizar operaciones con privilegios, y Apple los firma con
código y, por lo tanto, son de confianza.
Firma de código
En un nivel fundamental, ¿cómo puede un sistema "confiar" en un programa o script?
Generalmente, si el artículo viene como parte del sistema operativo, se debe confiar en él.
Pero, ¿y si se cambia el artículo? Si se cambia mediante una actualización del sistema,
nuevamente es confiable, pero de lo contrario no debería ser ejecutable o debería requerir
un permiso especial (del usuario o administrador) antes de ejecutarse. Las herramientas de
terceros, comerciales o de otro tipo, son más difíciles de juzgar. ¿Cómo podemos estar
seguros de que la herramienta no se modificó en su camino desde donde se creó a
nuestros sistemas?

Actualmente, la firma de código es la mejor herramienta en el arsenal de protección para


resolver estos problemas. La firma de código es la firma digital de programas y ejecutables
para confirmar que no se han modificado desde que el autor los creó. Utiliza un hash
criptográfico para probar la integridad y autenticidad. La firma de código se utiliza para
distribuciones de sistemas operativos, parches y herramientas de terceros por igual.
Algunos sistemas operativos, incluidos iOS, Windows y macOS, se niegan a ejecutar
programas que no superan la verificación de firma de código. También puede mejorar la
funcionalidad del sistema de otras formas. Por ejemplo, Apple puede deshabilitar todos los
programas escritos para una versión ahora obsoleta de iOS deteniendo la firma de esos
programas cuando se descargan de la App Store.

Protección basada en el lenguaje


En la medida en que se brinde protección en los sistemas informáticos, generalmente se
logra a través de un kernel del sistema operativo, que actúa como un agente de seguridad
para inspeccionar y validar cada intento de acceder a un recurso protegido. Dado que la
validación de acceso integral puede ser una fuente de sobrecarga considerable, debemos
brindarle soporte de hardware para reducir el costo de cada validación, o debemos permitir
que el diseñador del sistema comprometa los objetivos de protección. Satisfacer todos estos
objetivos es difícil si la flexibilidad para implementar políticas de protección está restringida
por los mecanismos de apoyo provistos o si los entornos de protección se hacen más
grandes de lo necesario para asegurar una mayor eficiencia operativa.
A medida que los sistemas operativos se han vuelto más complejos y, en particular, han
intentado proporcionar interfaces de usuario de nivel superior, los objetivos de protección se
han vuelto mucho más refinados. Los diseñadores de sistemas de protección se han
basado en gran medida en ideas que se originaron en lenguajes de programación y
especialmente en los conceptos de objetos y tipos de datos abstractos. Los sistemas de
protección ahora se preocupan no solo por la identidad de un recurso al que se intenta
acceder, sino también por la naturaleza funcional de ese acceso. En los sistemas de
protección más nuevos, la preocupación por la función que se va a invocar se extiende más
allá de un conjunto de funciones definidas por el sistema, como los métodos estándar de
acceso a archivos, para incluir funciones que también pueden ser definidas por el usuario.

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.

Aplicación basada en compilador


En este punto, los lenguajes de programación entran en escena. Especificar el control
deseado de acceso a un recurso compartido en un sistema es hacer una declaración
declarativa sobre el recurso. Este tipo de declaración se puede integrar en un idioma
mediante una extensión de su capacidad de mecanografía. Cuando se declara la protección
junto con la tipificación de datos, el diseñador de cada subsistema puede especificar sus
requisitos de protección, así como su necesidad de utilizar otros recursos en un sistema.
Dicha especificación debe darse directamente a medida que se redacta un programa y en el
idioma en el que se expresa el programa.Este enfoque tiene varias ventajas importantes:

1. Las necesidades de protección se declaran simplemente, en lugar de programarse


como una secuencia de llamadas a los procedimientos de un sistema operativo.
2. Los requisitos de protección se pueden establecer independientemente de las
facilidades proporcionadas por un sistema operativo en particular.
3. No es necesario que el diseñador de un subsistema proporcione los medios para la
ejecución.
4. Una notación declarativa es natural porque los privilegios de acceso están
estrechamente relacionados con el concepto lingüístico de tipo de datos.

Una implementación de lenguaje de programación puede proporcionar una variedad de


técnicas para hacer cumplir la protección, pero cualquiera de ellas debe depender de algún
grado de soporte de una máquina subyacente y su sistema operativo. Por ejemplo, suponga
que se utiliza un idioma para generar código que se ejecutará en el sistema Cambridge
CAP. En este sistema, cada referencia de almacenamiento hecha en el hardware
subyacente ocurre indirectamente a través de una capacidad. Esta restricción evita que
cualquier proceso acceda a un recurso fuera de su entorno de protección en cualquier
momento. Sin embargo, un programa puede imponer restricciones arbitrarias sobre cómo se
puede utilizar un recurso durante la ejecución de un segmento de código en particular.
Podemos implementar tales restricciones más fácilmente utilizando las capacidades de
software proporcionadas por CAP. La implementación de un lenguaje podría proporcionar
procedimientos protegidos estándar para interpretar las capacidades del software que
realizarían las políticas de protección que podrían especificarse en el idioma. Este esquema
pone la especificación de políticas a disposición de los programadores, mientras que los
libera de implementar su aplicación.

Incluso si un sistema no proporciona un núcleo de protección tan poderoso como los de


Hydra o CAP, todavía hay mecanismos disponibles para implementar especificaciones de
protección dadas en un lenguaje de programación. La principal distinción es que la
seguridad de esta protección no será tan grande como la soportada por un núcleo de
protección, porque el mecanismo debe basarse en más supuestos sobre el estado operativo
del sistema. Un compilador puede separar las referencias para las que puede certificar que
no podría producirse una infracción de protección de aquellas para las que podría ser
posible una infracción, y puede tratarlas de forma diferente. La seguridad proporcionada por
esta forma de protección se basa en el supuesto de que el código generado por el
compilador no se modificará antes o durante su ejecución.

¿Cuáles son, entonces, los méritos relativos de la aplicación basada únicamente en un


kernel, en contraposición a en forzamiento proporcionado en gran parte por un compilador?

● Seguridad. La aplicación por parte de un kernel proporciona un mayor grado de


seguridad del sistema de protección en sí mismo que la generación de código de
verificación de protección por parte de un compilador. En un esquema soportado por
el compilador, la seguridad se basa en la corrección del traductor, en algún
mecanismo subyacente de administración de almacenamiento que protege los
segmentos desde los cuales se ejecuta el código compilado y, en última instancia,
en la seguridad de los archivos desde los que se carga un programa. Algunas de
estas consideraciones también se aplican a un kernel de protección compatible con
software, pero en menor grado, ya que el kernel puede residir en segmentos de
almacenamiento físico fijos y puede cargarse solo desde un archivo designado. Con
un sistema de capacidad de etiquetado, en el que todo el cálculo de direcciones se
realiza mediante hardware o mediante un microprograma fijo, es posible una
seguridad aún mayor. La protección con soporte de hardware también es
relativamente inmune a las violaciones de la protección que pueden ocurrir como
resultado de un mal funcionamiento del hardware o del software del sistema.
● Flexibilidad. Existen límites a la flexibilidad de un núcleo de protección en la
implementación de una política definida por el usuario, aunque puede proporcionar
las instalaciones adecuadas para que el sistema proporcione la aplicación de sus
propias políticas. Con un lenguaje de programación, la política de protección se
puede declarar y la aplicación puede proporcionarse según sea necesario mediante
una implementación. Si un lenguaje no proporciona suficiente flexibilidad, se puede
ampliar o reemplazar con menos perturbaciones de las que causaría la modificación
de un kernel del sistema operativo.
● Eficiencia. La mayor eficiencia se obtiene cuando la aplicación de la protección está
respaldada directamente por hardware (o microcódigo). En la medida en que se
requiera soporte de software, la aplicación basada en el lenguaje tiene la ventaja de
que la aplicación del acceso estático se puede verificar fuera de línea en el momento
de la compilación. Además, dado que un compilador inteligente puede adaptar el
mecanismo de ejecución para satisfacer la necesidad especificada, a menudo se
puede evitar la sobrecarga fija de las llamadas al kernel.

En resumen, la especificación de protección en un lenguaje de programación permite la


descripción de alto nivel de políticas para la asignación y uso de recursos. Una
implementación de lenguaje puede proporcionar software para la aplicación de la protección
cuando la verificación automática por hardware no está disponible. Además, puede
interpretar las especificaciones de protección para generar llamadas en cualquier sistema
de protección proporcionado por el hardware y el sistema operativo.

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.

Lo que se necesita es un mecanismo de control de acceso dinámico y seguro para distribuir


capacidades a los recursos del sistema entre los procesos de los usuarios. Para contribuir a
la confiabilidad general de un sistema, el mecanismo de control de acceso debe ser seguro
de usar. Para ser útil en la práctica, también debería ser razonablemente eficiente. Este
requisito ha llevado al desarrollo de una serie de construcciones de lenguaje que permiten
al programador declarar varias restricciones sobre el uso de un recurso administrado
específico. (Consulte las notas bibliográficas para obtener las referencias apropiadas.)
Estos constructos proporcionan mecanismos para tres funciones:

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.

Aplicación basada en el runtime: protección en Java


Dado que Java se diseñó para ejecutarse en un entorno distribuido, la máquina virtual Java,
o JVM, tiene muchos mecanismos de protección incorporados. Los programas Java se
componen de clases, cada una de las cuales es una colección de campos de datos y
funciones (llamados métodos) que operan en esos campos. La JVM carga una clase en
respuesta a una solicitud para crear instancias (u objetos) de esa clase. Una de las
características más novedosas y útiles de Java es su soporte para cargar dinámicamente
clases que no son de confianza a través de una red y para ejecutar clases que desconfían
mutuamente dentro de la misma JVM.

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?

La filosofía adoptada en Java es requerir que la clase de biblioteca permita explícitamente


una conexión de red. De manera más general, para acceder a un recurso protegido, algún
método en la secuencia de llamada que dio como resultado la solicitud debe afirmar
explícitamente el privilegio para acceder al recurso. Al hacerlo, este método asume la
responsabilidad de la solicitud. Es de suponer que también realizará las comprobaciones
necesarias para garantizar la seguridad de la solicitud. Por supuesto, no todos los métodos
pueden hacer valer un privilegio; un método puede hacer valer un privilegio sólo si su clase
está en un dominio de protección al que se le permite ejercer el privilegio.

Este enfoque de implementación se denomina inspección de pila. Cada subproceso de la


JVM tiene una pila asociada de sus invocaciones de métodos en curso. Cuando no se
puede confiar en una persona que llama, un método ejecuta una solicitud de acceso dentro
de un bloque doPrivileged para realizar el acceso a un recurso protegido directa o
indirectamente. doPrivileged() es un método estático en la clase AccessController
al que se le pasa una clase con un método run() para invocar. Cuando se ingresa el
bloque doPrivileged, el marco de pila para este método se anota para indicar este
hecho. Luego, se ejecuta el contenido del bloque. Cuando se solicita posteriormente un

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.

De manera más general, las comprobaciones de tiempo de carga y de ejecución de Java


refuerzan la seguridad de tipos de las clases de Java. La seguridad de tipos garantiza que
las clases no puedan tratar los números enteros como punteros, escribir más allá del final
de una matriz o acceder a la memoria de alguna otra manera de forma arbitraria. Más bien,
un programa puede acceder a un objeto solo a través de los métodos definidos en ese
objeto por su clase. Esta es la base de la protección de Java, ya que permite que una clase
encapsule y proteja eficazmente sus datos y métodos de otras clases cargadas en la misma
JVM. Por ejemplo, una variable se puede definir como privada para que solo la clase que la
contiene pueda acceder a ella o protegerse para que solo pueda acceder la clase que la
contiene, subclases de esa clase o clases en el mismo paquete. La seguridad de tipos
garantiza que se puedan hacer cumplir estas restricciones.

También podría gustarte