Seguridad Informática: Conceptos Clave
Seguridad Informática: Conceptos Clave
Seguridad Informática
Maximiliano Cristiá
Rosario – Argentina
1. Unidad I: Introducción
La definición clásica de seguridad informática es la siguiente.
Definición 1 (Seguridad Informática). La seguridad informática es la rama de la informática que
se encarga de estudiar cómo preservar la confidencialidad, integridad y disponibilidad de datos y
programas en sistemas de cómputo.
Como vemos, la seguridad informática se entiende como la combinación de tres atributos
de calidad: confidencialidad, integridad y disponibilidad. Estos atributos los definimos de la
siguiente forma.
Definición 2 (Confidencialidad, integridad y disponibilidad). Preservar la confidencialidad de los
datos significa que solo los usuarios autorizados puedan ver dichos datos. Preservar la integridad de
los datos y programas significa que solo los usuarios autorizados y solo por los mecanismos autorizados
puedan modificar dichos datos y programas. Preservar la disponibilidad de datos y programas significa
que los usuarios autorizados puedan usar los datos y programas cada vez que los necesiten.
Las definiciones anteriores hacen mención a los usuarios y a los datos y programas. Desde un
punto de vista más conceptual se utilizan los términos sujeto y objeto. Un sujeto es una entidad
activa capaz de acceder información o programas en un sistema de cómputo; un objeto es o bien
un sujeto o bien cualquier contenedor de información o código. Posibles sujetos son: personas,
procesos, computadoras; posibles objetos son archivos, directorios, sockets y cualquiera de los
sujetos. Algunos autores utilizan el término agente como sinónimo de sujeto.
Notar que el atributo de integridad menciona dos condiciones: que solo los sujetos autori-
zados modifiquen objetos y que lo hagan solo de las formas autorizadas. Observar que los otros
dos atributos solo exigen una condición cada uno. Veamos el siguiente ejemplo.
Ejemplo 1 (Integridad). Consideremos un sistema de sueldos. Solo el contador de la empresa está
autorizado a modificar el sueldo de un empleado. Sin embargo el contador no puede hacer la modificación
de cualquier forma: debe utilizar un ABM de sueldos. Por ejemplo, no puede acceder a la base de datos vía
SQL y hacer la modificación. Si el sistema le permite al contador modificar un sueldo vía SQL o le permite
a otros usuarios ejecutar el ABM de sueldos y modificar sueldos, el sistema no preserva la integridad de
los sueldos.
La idea de que los sueldos solo puedan modificarse por medio del ABM de sueldos se basa en que la
modificación de un sueldo puede requerir, precisamente, ciertos controles de integridad. Por ejemplo, no
debería ser posible reducir el sueldo de un empleado por debajo del sueldo mínimo fijado por el Estado.
Entonces dichos controles están implementados en el ABM de sueldos.
Por el contrario, la confidencialidad de los sueldos se viola cada vez que un usuario no autorizado
puede conocer el sueldo de un empleado (por medios informáticos). No importa qué mecanismos se utilicen
(por ejemplo, acceso vía SQL o vía el ABM de sueldos), si el sueldo es revelado la confidencialidad ya está
comprometida.
En este curso desarrollaremos con cierta profundidad lo concerniente al atributo de confi-
dencialidad y pondremos menos énfasis en lo relativo a integridad, mientras que el atributo de
disponibilidad no será abordado. En parte esta decisión se basa en que no podemos abarcar toda
la seguridad informática en un curso acotado pero también en una cuestión de fundamento. En
efecto, las técnicas para proteger un sistema de cómputo contra ataques a la confidencialidad
y la integridad son muy diferentes de aquellas que se utilizan para proteger la disponibilidad.
Para los dos primeros atributos es suficiente con restringir el uso del sistema, pero se requiere lo
4
Algo que se es: cualquier característica física del usuario. El ejemplo canónico son las huellas
dactilares. Otros ejemplos son la retina, ADN, voz, rostro, etc.
Obviamente cuántos más factores de autenticación se utilizan más seguro es el proceso pero
también más costoso (no solo computacionalmente sino también económicamente, por ejemplo
el costo de imprimir y distribuir tarjetas).
Una vez que la identidad ha sido corroborada se la utiliza en el proceso de autorización. Este
proceso está muy relacionado con lo que se denomina control de acceso. En este contexto, los
sujetos acceden objetos por medio de un conjunto de relaciones básicas que se pueden resumir
en leer (o read) y escribir (o write). A estas relaciones se las suele llamar permisos, modos o derechos
de acceso. En el proceso de autorización un sujeto solicita acceso a un objeto en uno de los dos
modos básicos, y el sistema se encarga de autorizar o negar dicha solicitud. En este sentido
se dice que el sistema controla el acceso de los sujetos a los objetos. Algunos autores ven a la
autorización como uno de los pasos del control de acceso, al cual le sigue, en caso afirmativo,
la entrega del objeto al sujeto en el modo solicitado.
Ejemplo 4 (Autorización en UNIX). En sistemas tipo UNIX los procesos corren con la identidad del
usuario que los lanza (el cual a su vez fue oportunamente autenticado). Por otro lado, cada archivo puede
ser leído o escrito por ciertos usuarios o grupos de usuarios. Cuando un proceso solicita acceso en un
cierto modo a un archivo, el sistema operativo controla que el usuario a nombre del cual está ejecutando
el proceso tenga el acceso solicitado. En caso afirmativo el proceso recibe un descriptor de archivo a través
del cual podrá leer o escribir (o ambas) en el archivo, según lo que haya solicitado.
En la práctica los modos de acceso básicos (lectura y escritura) se extienden a otros modos
tales como añadir4, crear, control, etc.
Ejemplo 5 (Dueños de archivos). El permiso de control permite modificar el modo de acceso en que
cada usuario puede acceder a un determinado objeto. En el sistema de archivos típico de UNIX esto está
dado por el propietario del archivo. Es decir cada archivo tiene un dueño el cual, implícitamente, tiene
el permiso de control sobre ese archivo. Es decir el dueño puede determinar qué otros usuarios o grupos
de usuarios pueden acceder al archivo y en qué modos.
El permiso de control es una forma del modo básico de escritura puesto que se lo interpreta como el
permiso de escritura sobre los meta-datos del archivo (el i-nodo en sistemas tipo UNIX).
Aunque la forma más común de control de acceso se basa en la identidad del sujeto existen
otras que se aplican en cierto contextos más específicos.
Ejemplo 6 (Control de acceso basado en roles). Una forma de control de acceso bastante difundida
entre los gestores de bases de datos relacionales se basa en el concepto de rol en lugar de la identidad del
sujeto. Por ejemplo, en un sistema de gestión comercial cada usuario puede o no tener el rol de gerente.
Entonces luego de que el usuario se autentica ante la base de datos, esta le asigna un rol con el cual podrá
acceder a ciertas tablas en ciertos modos de acceso. Eventualmente el usuario puede solicitarle al sistema
asumir otro rol en cuyo caso el sistema lo autorizará o no.
Finalmente, el proceso de auditoría refiere tanto al registro de los eventos del sistema con-
cernientes a la seguridad, como al análisis y gestión de dicha información. Algunos ejemplos
de eventos concernientes a la seguridad son: intento de login fallido, login exitoso, intento de
apertura de un archivo, creación de un archivo, establecer una conexión con otra computadora,
etc. Conceptualmente todos estos eventos se registran en un repositorio denominado bitácora
4‘append’, en inglés
7
de auditoría5. El contenido de la bitácora puede ser analizado para detectar ataques en progre-
so o para recolectar evidencia de un ataque anterior que permita, por ejemplo, determinar la
identidad del atacante.
Por cuestiones de tiempo, en este curso solo estudiaremos en profundidad el problema del
control de acceso o autorización (en la Unidad 2) y someramente el problema de la autenticación
(en la Unidad 4).
La seguridad de un sistema de cómputo también puede clasificarse en seguridad interna y
seguridad externa. La seguridad interna es la seguridad del software y de los mecanismos de
hardware que sirven a la protección de la información (por ejemplo la separación en anillos
de protección provista por los microprocesadores de la familia x86). En tanto que la seguri-
dad externa se ocupa de los controles y actividades de seguridad que el sistema mismo no
puede brindar. Entran dentro de la seguridad externa, por ejemplo, la seguridad física de las
instalaciones, la seguridad del personal y la seguridad de procedimientos. Si bien en este curso
estudiaremos exclusivamente la seguridad interna, la seguridad de un sistema de cómputo
empieza por la externa.
Ejemplo 7 (Seguridad interna vs. externa). Supongamos que instalamos en una PC el sistema
operativo más seguro que se conoce (seguridad interna) pero al mismo tiempo dejamos la computadora en
un pasillo público de la empresa (seguridad externa). En este caso el resultado neto será que la información
allí almacenada y procesada no estará segura puesto que queda expuesta a todo tipo de ataques tales como:
robo de la PC misma; robo de su disco rígido; arranque de la PC con un disco externo que permite acceder
a todo el contenido del disco interno; instalación por ese mismo medio de programas de ataque; etc.
El ejemplo anterior evidencia la importancia de la seguridad externa. Por consiguiente al
encarar la protección de un sistema de cómputo6 es conveniente comenzar por determinar la
frontera del sistema y el perímetro de seguridad, como se muestra en la Figura 1. Dentro de la frontera
del sistema está todo el hardware y software que debe ser protegido. Estos componentes deben
estar regidos por las reglas de la seguridad externa. Dentro del perímetro de seguridad está
todo el hardware y software que implementa la política de seguridad (interna). La política de
seguridad es, en términos de Ingeniería de Software, el requerimiento de seguridad (recordar
el gráfico con los cuatro elementos básicos R, S, D y P visto en clase de Ingeniería de Software I
y II).
Como puede apreciarse en la figura los usuarios están fuera de la frontera del sistema. Sin
embargo los usuarios poseen información valiosa y eventualmente tienen acceso a procesos
críticos de la organización. Como no hay forma de evitar que los usuarios traicionen a la
organización (por ejemplo pueden vender la información confidencial que han memorizado),
toda la teoría de la seguridad informática se basa sobre la hipótesis de que los usuarios son
confiables o de confianza. Es decir se asume que los usuarios autorizados no dañarán los activos
informáticos. Esto significa que un usuario que no está autorizado a acceder cierta información es
potencialmente un adversario, aunque pertenezca a la organización. O sea que los ingenieros de
seguridad informática deben actuar suponiendo que esta persona, a pesar de ser un miembro de
la organización, intentará acceder a esa información a la que no está autorizado y eventualmente
la divulgará, la corromperá o la tornará indisponible.
Hipótesis de seguridad La organización confía en que los usuarios no divulgarán, dañarán o harán
indisponible la información a la que están autorizados.
5‘audit log’ o ‘audit trail’, en inglés.
6Entendido como un conjunto de dispositivos de cómputo interconectados entre sí.
8
programa de usuario
programa de login
sistema operativo
archivo de usuario
Figura 1: Frontera del sistema y perímetro de seguridad. Se muestran algunos elementos dentro
y fuera de estos límites.
.. s1 .. s2 .. s3 .. s4 .. s5 .. s6 ..
. . . . . . .
... ...
o2 r, w, c r r, w, c r, w
... ...
o1 r, w, c r, c
... ...
.. .. .. .. .. .. ..
. . . . . . .
fijada discrecionalmente por los usuarios ordinarios. En sistemas DAC los usuarios y procesos
ordinarios pueden determinar y modificar los permisos de algunos objetos y pueden establecer
los permisos de algunos de los objetos que ellos crean (en general de los archivos, directorios y
otros objetos del sistema de archivos).
Conceptualmente, en DAC existe una matriz de control de acceso en cuyas filas están los objetos
del sistema y en cuyas columnas están los sujetos. En cada componente de la matriz se listan los
permisos que el sujeto tiene sobre el objeto (ver Figura 2). Los usuarios ordinarios con permiso
de control pueden cambiar los derechos de acceso sobre el objeto por medio de operaciones
provistas por el sistema operativo (en sistemas tipo UNIX son los comandos chmod, chown, etc.
que utilizan las llamadas al sistema correspondientes).
Sin embargo, en la práctica la mayoría de los sistemas operativos implementan alguna forma
de lista de control de acceso (ACL8). En este caso cada objeto tiene asociada una ACL donde se
listan los permisos de los sujetos que tienen alguno. Por ejemplo, respecto de la Figura 2, la lista
de control de acceso del objeto o1 sería [(s1 , {r, w, c}), (s3 , {r, c})]. Es decir una ACL es equivalente
a una fila de la matriz de acceso sin las celdas vacías.
En los párrafos anteriores hemos hecho referencia siempre a la seguridad de sistemas ope-
rativos. De alguna forma descartamos de plano la posibilidad de implementar la seguridad de
un sistema de cómputo en algún otro estrato. En particular las teorías de sistemas operativos
y seguridad informática no consideran apropiado ni seguro intentar implementar controles de
seguridad a nivel de aplicación. De todas formas se acepta como razonable que ciertas aplicacio-
nes, en general muy sofisticadas y que manejan sus propios datos e incluso sus propios sistemas
de archivos, tales como los gestores de bases de datos relacionales (DBMS), implementen sus
propios controles de seguridad. Por ejemplo, los DBMS suelen tener ACL que se pueden asociar
a bases de datos, tablas, filas, columnas e incluso celdas (en parte porque estos objetos no son
visibles a nivel del sistema operativo). En general, todo lo que mencionemos en esta unidad
asume que estamos hablando de sistemas operativos pero también se puede aplicar a DBMS.
control de acceso obligatorio (MAC10). Es decir, los usuarios ordinarios no pueden determinar ni
modificar la política de seguridad.
sistema operativo
Aun así, si el usuario no es precavido podría dar su contraseña a cualquier programa que se la
pida aunque no tenga la apariencia de un programa confiable. Incluso podría usar la misma contraseña
para acceder a servidores críticos y servidores no críticos. En definitiva esto nos lleva a una conclusión
importante: la seguridad es una cadena que se corta por el eslabón más débil. Y muchas veces ese eslabón
es el usuario. Ningún mecanismo de seguridad, por más sofisticado que sea, funcionará sin la colaboración
activa de sus usuarios.
En este caso particular los usuarios deberían estar capacitados de forma tal de no ingresar su contraseña
sin antes activar el camino confiable. Observar que sin el mecanismo de camino confiable los usuarios no
tienen una forma segura de ingresar su contraseña.
[CAT, OBJ]
SL == N
SUBJ : P OBJ
SUBJ ≠ ∅
Las clases de acceso son pares ordenados de niveles de seguridad y conjuntos de categorías.
SC == SL × P CAT
dominates : SC ↔ SC
∀a, b : SC • a dominates b ⇔ b.1 ≤ a.1 ∧ b.2 ⊆ a.2
Aunque BLP usa varios modos de acceso nosotros solo utilizaremos los dos modos de acceso
básicos.
AM ::= rm | wm
una función que asocia sujetos con los objetos que están accediendo y en qué modo, oobj
(por ‘Open OBJects’)
SecState
sc : OBJ →
↦ SC
oobj : SUBJ →
↦ (OBJ ↔ AM)
Si pensamos en un sistema operativo tipo UNIX, los sujetos serían procesos, los objetos archivos,
la variable sc los permisos guardados en los i-nodos, y la variable oobj los archivos que cada
proceso tiene abiertos. El estado inicial es trivial.
ISecState
SecState
sc = ∅
oobj = ∅
Antes de formalizar los invariantes de estado que dieron originalmente Bell y LaPadula
vamos a dar invariantes simples que complementan a los tipos que hemos definido.
InvType
SecState
dom oobj ⊆ SUBJ
dom oobj ⊆ dom sc
∀x : ran oobj • dom x ⊆ dom sc
Es decir, estos invariantes exigen que los sujetos que tienen abiertos objetos y esos mismos
objetos sean sujetos y objetos del sistema.
La primera propiedad enunciada por Bell y LaPadula (en forma de invariante de estados)
la llamaron condición de seguridad17. La condición de seguridad es la formalización directa de la
regla de acceso a la información estipulada en MLS: una persona puede leer un documento sí
y solo sí la clase de acceso de la persona domina a la clase de acceso del documento. Traducido
al estado del sistema que hemos definido significa que si un sujeto tiene acceso de lectura a un
objeto (i.e. (s, o, rm) ∈ oobj) es porque la clase de acceso de ese sujeto domina a la del objeto (i.e.
sc(s) dominates sc(o)).
InvSecCond
SecState
∀s : dom oobj; o : OBJ | (s, o, rm) ∈ oobj • sc(s) dominates sc(o)
La segunda propiedad exigida en BLP se llama propiedad estrella y se suele escribir propiedad-
*18. Esta propiedad no está enunciada en MLS y aparece en BLP pura y exclusivamente porque
17‘security condition’, en inglés.
18‘*-property’, en el original en inglés.
16
almacenamiento secundario
arreglo h
oh read s e c r e t o
ol write s e c r
arreglo l
se trata de un sistema de cómputo donde existe la posibilidad de que caballos de Troya escriban
datos. En efecto, si un sujeto tiene abiertos los objetos oh y ol , respectivamente, en modo de
lectura y escritura19, y son tales que sc(oh ) dominates sc(ol ), entonces le sería posible copiar los
datos de oh en ol produciendo la desclasificación20 no autorizada de la información contenida
en oh . Esto es posible porque un sistema operativo puede controlar a los procesos solo cuando
efectúan llamadas al sistema cosa que no es necesaria cuando copian información entre áreas
de memoria propias. Esta situación se representa gráficamente en la Figura 4. Allí se puede
observar que la lectura del archivo oh se efectúa por medio de la llamada al sistema read en
tanto que la escritura en el archivo ol se hace usando la llamada write; en ambos casos el sistema
operativo (o más precisamente la TCB) puede controlar que todo esté de acuerdo a la política
de seguridad. Sin embargo, la copia de los datos del arreglo h en el arreglo l (simbolizada por
la línea gruesa) escapa al control de la TBC. Por lo tanto esta no tiene forma de saber que el
contenido de l es de clase de acceso sc(oh ).
Entonces, la propiedad-* exige que, para cada sujeto, todos los objetos abiertos en modo de
lectura tengan una clase de acceso dominada por la de cualquiera de los objetos abiertos en
modo de escritura. En inglés esto se denomina no write-down; o sea que tenemos no read-up y no
write-down.
InvStarProp
SecState
∀s : dom oobj; o1 , o2 : OBJ |
{(s, o1 , wm), (s, o2 , rm)} ⊆ oobj • sc(o1 ) dominates sc(o2 )
A continuación especificaremos las dos operaciones claves del sistema: solicitar acceso a un
objeto en modo de lectura y en modo de escritura (es decir, las operaciones que representan la
llamada al sistema UNIX open). Comenzamos por la apertura en modo rm. Observar que en
19Usualmente la letra ‘h’ se utiliza para denotar objetos con clase de acceso alta (por high, en inglés), mientras que
‘l’ se usa para objetos de clase de acceso baja (por low, en inglés). Luego del trabajo de Bell y LaPadula la mayoría de
los investigadores del área comenzaron a proponer soluciones al problema de la confidencialidad donde las clases
de acceso se simplifican considerando solo el nivel de seguridad, la relación de dominación se vuelve un orden
total (<) y donde solo se consideran dos niveles, L < H, donde L es el nivel de la información pública y H es el de
la información secreta. En este caso se habla más de niveles de seguridad que de clases de acceso. La generalización
de estas soluciones a un retículo de clases de acceso en general es trivial. De aquí en más consideraremos esta
simplificación del problema cuando sea conveniente.
20‘downgrading’, en inglés.
17
OpenReadOk
ΔSecState
s?, o? : OBJ
s? ∈ SUBJ
{s?, o?} ⊆ dom sc
o? ∉ dom(oobj(s?) ▷ {rm})
sc(s?) dominates sc(o?)
∀o : dom(oobj(s?) ▷ {wm}) • sc(o) dominates sc(o?)
oobj′ = oobj ∪ {(s?, o?, rm)}
sc′ = sc
Es decir:
s? debe ser un sujeto
s? y o? deben ser objetos existentes en el sistema
s? no tiene abierto a o? en modo de lectura
La clase de acceso de s? domina a la de o?. Este predicado implementa la regla no read-up
de MLS.
La clase de acceso de o? está dominada por la clase de acceso de todos los objetos que
s? tiene abiertos en modo de escritura. Este predicado apunta a controlar posibles write-
downs.
En ese caso o? pasa a ser uno de los objetos que s? tiene abiertos en modo de lectura
No especificaremos los casos de error aunque Bell y LaPadula lo hacen en el trabajo ori-
ginal. Asumimos que la especificación final es el esquema OpenRead reúne la disyunción de
OpenReadOk y los esquemas de error.
La siguiente operación es solicitar acceso en modo de escritura a un objeto. Si bien desde el
punto de vista de MLS un sujeto podría escribir en objetos más altos, en BLP solo se permite la
escritura en objetos al mismo nivel que el sujeto.
OpenWriteOk
ΔSecState
s?, o? : OBJ
s? ∈ SUBJ
{s?, o?} ⊆ dom sc
o? ∉ dom(oobj(s?) ▷ {wm})
sc(s?) = sc(o?)
∀o : dom(oobj(s?) ▷ {rm}) • sc(o?) dominates sc(o)
oobj′ = oobj ∪ {(s?, o?, wm)}
sc′ = sc
18
Luego de especificar las demás operaciones del sistema, Bell y LaPadula demuestran que
todas ellas preservan la condición de seguridad y la propiedad-*. Básicamente escriben teoremas
como los que siguen:
theorem OpenReadSecCond
InvSecCond ∧ OpenRead ⇒ InvSecCond′
theorem OpenReadStarProp
InvStarProp ∧ OpenRead ⇒ InvStarProp′
theorem OpenWriteSecCond
InvSecCond ∧ OpenWrite ⇒ InvSecCond′
theorem OpenWriteStarProp
InvStarProp ∧ OpenWrite ⇒ InvStarProp′
theorem OpenReadInvType
InvType ∧ OpenRead ⇒ InvType′
theorem OpenWriteInvType
InvType ∧ OpenWrite ⇒ InvType′
todos los cuales pueden demostrarse formalmente con una herramienta como Z/EVES.
Dado que todas las operaciones preservan InvSecCond e InvStarProp, si el sistema parte de
un estado en donde esos invariantes se verifican entonces el sistema será siempre seguro.
Es posible verificar automáticamente que las operaciones de BLP verifican las dos propie-
dades claves del modelo usando {log} [9].
Otro problema con BLP, que se deriva del anterior, se denomina escalda de nivel21. Debido
a la propiedad-*, aun cuando un sujeto haya cerrado un objeto abierto en modo de lectura, no
podrá abrir objetos en modo de escritura cuya clase de acceso no domine a la del objeto que
acaba de ser cerrado. Esto implica que el sujeto (aunque en este caso sería más apropiado hablar
de proceso) queda clasificado con el supremo de las clases de acceso de todos los archivos que
haya abierto en modo de lectura durante toda su vida. Obviamente este supremo tiende a ser
cada vez mayor. Para frenar esta escalada de nivel habría que destruir al sujeto para iniciarlo
nuevamente (en cuyo caso comienza con el ínfimo del orden parcial). Nuevamente esto va en
detrimento de la usabilidad de los sistemas basados en BLP22.
Finalmente, otro de los problemas señalados por varios autores es que es muy difícil deter-
minar todos los objetos del sistema. Si un cierto contenedor de información no es clasificado
como objeto del sistema entonces no será considerado a la hora de controlar la validez de las
dos propiedades del modelo y en consecuencia un atacante podría aprovecharse de este tipo de
contenedores.
Ejemplo 11 (Meta-datos como objetos). Un caso típico, aunque bastante evidente como para que no
sea detectado por un ingeniero de seguridad, son los diversos meta-datos de los archivos (nombre, fechas
de acceso, etc.). Es decir, por ejemplo, si la la fecha de acceso no es considerada un objeto del sistema (y
por lo tanto no está clasificada con una clase de acceso) un atacante podría utilizarla para comunicar
información. Volveremos sobre esta idea en la siguiente sección cuando abordemos el concepto de canal
encubierto.
2.5. No inteferencia
El concepto de no iterferencia fue propuesto originalmente por Joseph Goguen y José Mese-
guer en 1982 [15]. Aun hoy es considerado ‘la’ definición del problema de la confidencialidad
en sistemas de cómputo.
Dados dos grupos de usuarios, Gh y Gl (recordar el significado de h y l), informalmente, se
dice que Gh no interfiere con Gl si las acciones de los usuarios de Gh no pueden ser percibidas
por los miembros del grupo Gl . En este contexto ‘percibir’ se entiende como la posibilidad de
ver una salida producida por el sistema. Entonces, el grupo Gh no interfiere con el grupo Gl si
la salida del sistema que pueden ver estos últimos no cambia con lo que hagan los usuarios de
Gh .
Goguen y Meseguer dan la definición de no interferencia a través de lo que ellos llaman
sistemas de capacidades. Un sistema de capacidades es un sistema donde lo que está permitido
hacer varía con el tiempo. Formalmente un sistema de capacidades M consta de los siguientes
elementos:
un conjunto de usuarios, U
un conjunto de estados, S
un conjunto de comandos o transiciones de estado, T
un conjunto de salidas del sistema, Out
Las salidas incluyen pantalla, impresora, disco, red, etc.; es decir todo aquello que el
usuario pueda sensar, percibir o ver del sistema.
21‘level creep’, en inglés.
22Si bien es siempre cierto que a mayores exigencias de seguridad menores niveles de usabilidad, en el caso de
BLP la reducción del nivel de seguridad es exagerada puesto que, como vimos, prohíbe comportamientos seguros.
20
ℰ :S × C × U × A → S × C
(
(𝒯 (s, c, u, a), c) si a ∈ T
(s, c, u, a) → ℰ(s, c, u, a) =
(s, 𝒫(c, u, a)) si a ∈ P
De esta forma un sistema de capacidades es una máquina de estados con S × C como espacio
de estados, U × A como espacio de entradas y espacio de salidas Out. Ahora extendemos ℰ a
secuencias de entradas siguiendo la forma habitual:
ℰ :S × C × (U × A)∗ → S × C
(
(s, c) si w = ⟨⟩
(s, c, w) → ℰ(s, c, w) =
ℰ(ℰ(s, c, v), u, a) si w = v ⌢ ⟨(u, a)⟩
donde ℰ se usa como nombre para ambas funciones dado que los tipos son distintos.
Antes de poder formalizar el concepto de no interferencia necesitamos un poco de notación.
Notamos con [[w]] a ℰ(s0 , c0 , w) para cualquier secuencia w ∈ (U × A)∗ ; y con [[w]]u a 𝒪([[w]], u)
para cualquier usuario u ∈ U. Es decir [[w]]u es todo lo que el usuario u puede ver como salida
del sistema luego de que este ejecute la secuencia de comandos w desde la configuración inicial.
Sea G un subconjunto de U, B un subconjunto de A y w ∈ (U × A)∗ . Entonces w̃G es la secuencia
que surge de eliminar de w todos los pares cuya primera componente es un elemento de G; w̃B
es la secuencia que surge de eliminar de w todos los pares cuya segunda componente es un
elemento de B; y w̃BG = (w̃G )B .
Definición 4 (No interferencia). Dado un sistema de capacidades M y grupos de usuarios Gh y Gl ,
decimos que Gh no interfiere con Gl , simbolizado Gh :| Gl , sí y solo sí:
O sea que la definición establece que lo que el usuario u puede ver como salida del sistema
M es lo mismo tanto si los usuarios de Gh ejecutan comandos en M como si no lo hacen. En otras
palabras, nada de lo que hacen con M los usuarios de Gh interfiere con lo que ven los usuarios de
Gl . Notar que la definición es una cuantificación universal sobre todas las posibles ejecuciones
de M.
De forma similar, dados B ⊆ A y G, G1 ⊆ U, definimos:
B :| G ⇔ ∀w ∈ (U × A)∗ , u ∈ G : [[w]]u = [[w̃B ]]u
B, G :| G1 ⇔ ∀w ∈ (U × A)∗ , u ∈ G1 : [[w]]u = [[w̃BG ]]u
Ejemplo 12 (Crear un archivo). Si B ⊆ A entonces B :| {u} significa que los comandos de B no tienen
efecto sobre lo que u puede ver como salida del sistema. Entonces si create (crear un archivo) pertenece a
B, la salida que ve u a través de ls (listar archivos) antes y después de ejecutar create debe ser la misma.
Por ejemplo, si en determinado momento u ejecuta ls y ve:
apunte.tex
apunte.pdf
pero luego alguien ejecuta create(examen.tex) y u vuelve a ejecutar ls debería volver a ver:
apunte.tex
apunte.pdf
Es decir que el sistema debería recordar el listado que le mostró a u la primera vez y debería mostrarle
siempre el mismo aunque cualquier usuario cree nuevos archivos. Si esto hay que hacerlo para todos los
directorios y usuarios, el sistema se puede volver muy ineficiente.
Ejemplo 13 (Seguridad multi-nivel). Un sistema de capacidades M es MLS sí y solo sí para toda clase
de acceso vale:
U+x :| U+x
Aun cuando estamos seguros de tener una buena definición de seguridad23, no es fácil saber
si un sistema la verifica o no. Esto está relacionado con el hecho de que safety y security son
dos clases de propiedades diferentes24. Como vimos en Ingeniería de Software I25, safety es una
de las clases de propiedades fundamentales de la teoría de Alpern-Schneider sobre sistemas
concurrentes; la otra propiedad es vitalidad. En cierto sentido las propiedades de safety son
más simples de implementar que las de vitalidad porque es suficiente con construir un sistema
con menos comportamientos que los indicados en la propiedad. En otras palabras, si S es
una propiedad de safety y P es una implementación tal que P ⇒ S, entonces P tiene menos
comportamientos que S. Y si P′ es una implementación más determinista que P (es decir con
aun menos comportamientos que P) entonces P′ también verificará S. En lo que concierne a las
propiedades de safety, aumentar el determinismo garantiza la corrección.
Sin embargo, en lo que concierne a las propiedades de vitalidad lo anterior no es cierto
en general. Aumentar el determinismo no es garantía de que la implementación verifique la
propiedad.
Se ha argumentado, e incluso a primera vista parece razonable, que las propiedades de
seguridad son propiedades de safety. El razonamiento es que si un sistema que no hace nada
es seguro, entonces la seguridad no requiere que algo ocurra. Si no tiene que ocurrir algo, en
consecuencia no son propiedades de vitalidad y entonces deberían ser de safety. Sin embargo,
las propiedades de security no son propiedades de safety, en particular porque en general no
vale la propiedad descripta más arriba. Es decir, una implementación más determinista que la
especificación de seguridad no necesariamente es segura.
Ejemplo 15 (Seguridad no es safety). Digamos que a nivel de especificación se establece que cuando
un proceso le solicita un área de memoria al sistema operativo, este entrega parte de la memoria que
tiene disponible pero no se especifica un valor particular para escribir en cada celda entregada. Es decir,
el sistema operativo puede entregar las celdas con cualquier valor posible. Desde el punto de vista de la
seguridad esta especificación es razonable pues el proceso no tiene forma de saber si los valores que recibe
en esas celdas contienen información importante o no.
Una implementación posible para esa especificación es que el sistema operativo sobrescriba cada
celda con cero antes de entregarla. Esta implementación es tan segura como la especificación y es más
determinista (pues hay menos comportamientos posibles ya que de todos los valores posibles siempre se
entrega el mismo).
Ahora pensemos en otra implementación más determinista: el sistema operativo entrega las celdas
con el último valor guardado. Es más determinista pues de todas las sobrescrituras posibles el sistema
operativo elige no sobrescribir nada. Sin embargo esta implementación es obviamente insegura pues un
caballo de Troya solo debería solicitar un área de memoria suficientemente grande, escribir en ella (con
repeticiones si fuese necesario) la información secreta y liberar la memoria. Por otro lado un programa
ejecutando con una clase de acceso baja solicita toda la memoria posible sabiendo que es muy probable que
contenga el secreto buscado.
Dos implementaciones más deterministas, una segura, la otra no. La seguridad no se preserva au-
mentando el determinismo. Las propiedades de seguridad no son propiedades de safety
Ejemplo 16 (Refinamiento no preserva no interferencia). Este ejemplo está tomado de [7]. Digamos
que un sistema produce de forma no determinista las salidas 0, 1 o un valor secreto h. Claramente este
23En esta sección ‘seguridad’ es sinónimo ‘confidencialidad’.
24Dado que ‘safety’ y ‘security’ se traducen como ‘seguridad’, en esta sección, cuando sea necesario, utilizaremos
las palabras en inglés para evitar confusiones.
25Sugerimos releer el apunte de clase “Seguridad, Vitalidad y Equidad”, de esa materia.
23
sistema verifica la propiedad de no interferencia: “las salidas posibles del sistema son independientes
de los valores secretos” ya que son independientes de la actividad de los usuarios H. Sin embargo, un
refinamiento posible del sistema es que solo emita la salida h en cuyo caso no cumpliría con la propiedad
de seguridad.
El hecho de que safety y security sean clases de propiedades diferentes, no significa que sean
disjuntas. Es decir, en particular, no significa que ninguna propiedad de security es de safety.
De hecho hay propiedades de security que son también propiedades de safety (por ejemplo, el
desbordamiento de arreglos, ver Sección 3.1).
No obstante, la no interferencia no es de safety. Peor aun, el mayor problema es que la no
interferencia no es una propiedad en el sentido de Alpern-Schneider. La validez de una pro-
piedad en el contexto de Alpern-Schneider depende de una ejecución26 mientras que la validez
de la no interferencia depende de un conjunto de ejecuciones (notar que la definición de no
interferencia requiere de al menos dos ejecuciones, una donde actúan los usuarios H y la otra
donde no lo hacen). De hecho Clarkson y Schneider [7] definen el concepto de hiperpropiedad,
como un predicado que depende de conjuntos de ejecuciones, dentro del cual pueden expresar
la no interferencia. Más aun, en ese mismo trabajo demuestran que, como ya hemos visto, el
refinamiento no puede invalidar las propiedades de safety pero sí puede invalidar una hiperpro-
piedad (o sea que puede invalidar la no interferencia). También definen las hiperpropiedades de
hypersafety y hyperliveness como generalizaciones naturales de las propiedades de safety y vitali-
dad. Clarkson y Schneider no pueden demostrar que la no interferencia es de hypersafety pero
solo por un tecnicismo; según ellos sería razonable que lo fuera. Finalmente, como demuestran
que el refinamiento preserva hypersafety, sigue siendo razonable que no necesariamente preserve
no interferencia (puesto que no es de hypersafety).
Esto tampoco significa que la no interferencia nunca sea de safety. Es decir, hay versiones de
la no interferencia (menos generales, obviamente) que se pueden expresar como propiedades
de safety. Estas versiones usualmente se expresan como sistemas de tipos y por lo tanto el
problema de verificar la no interferencia se convierte en un problema de verificación de tipos,
que es claramente de safety (ver por ejemplo el trabajo de Volpano y otros [32]). Uno de los
problemas con estas variantes de la no interferencia es que generan sistemas innecesariamente
restrictivos.
L para comunicar la información H. Este otro proceso suele llamarse proceso espía o simplemente
espía.
Ejemplo 17 (Canal encubierto). El Ejemplo 11 muestra un mecanismo que puede ser usado como
canal encubierto. Entonces tenemos un caballo de Troya que leerá información H y un proceso espía que
ejecutará siempre a nivel L. Ambos acuerdan en usar el archivo canal.txt como canal encubierto. Entonces
el caballo de Troya lee la información de nivel H, digamos la palabra ‘secreto’. En ese momento solo
podrá escrbir en archivos de ese nivel. Sin embargo usará los segundos de la fecha de acceso del archivo
canal.txt para comunicarle esa palabra al espía que estará consultando periódicamente tal fecha. Cuando
el caballo de Troya quiere comunicar la ‘s’ espera hasta el segundo 19 porque la ‘s’ es la décima novena
letra del alfabeto y escribe cualquier cosa en el archivo canal.txt; cuando quiere comunicar la ‘e’ espera
hasta el segundo 5 (del minuto siguiente) y vuelve a escribir cualquier cosa en el archivo canal.txt; y así
sucesivamente. Aunque el archivo canal.txt será de nivel H, como la fecha de acceso no fue clasificada
como objeto a proteger el espía al otro extremo del canal encubierto no tendrá problema en consultarla.
Como todo canal de comunicación los canales encubiertos tienen un cierto ancho de banda
que puede medirse en bits o bytes (o unidades superiores) por alguna unidad de tiempo. El
canal encubierto del ejemplo anterior tiene un ancho de banda de un caracter (byte) por minuto
pero esquemas más sofisticados podrían incrementarlo notablemente. Obviamente nadie espera
transmitir una base de datos de cientos de megabytes de a un caracter por minuto. De todas
formas no siempre es necesario transmitir la información en sí sino solo una clave de encriptación
de unos pocos bits.
Ejemplo 18 (Transmitir una clave por un canal encubierto). Supongamos que el objetivo de un
atacante es hacerse con los datos de una base de datos de cientos de megabytes. Esos datos son consultados
por clientes que están fuera de la red corporativa; o sea los datos viajan a través de canales públicos como
Internet. Para proteger los datos en tránsito la organización utiliza algún sistema criptográfico con claves
de 1024 bits (estos temas serán profundizados en la Unidad 4).
Como el atacante solo dispone del canal encubierto descripto en el Ejemplo 17, en lugar de transmitir
toda la base de datos por este canal solo trasmitirá los 1024 bits de la clave. De esta forma cuando
obtenga la clave ‘solo’ tendrá que intervenir28 el canal público, recuperar los datos que viajan encriptados
y desencriptarlos con la clave que obtuvo. Si bien estos pasos no son triviales el más complejo por lejos es
lograr sacar la clave fuera del servidor. Por ejemplo si la red pública está basada en tecnología ethernet no
es tan difícil intervenirla.
Observar que en este caso particular el canal encubierto podrá transmitir en cada comunicación una
secuencia de bits que represente un número no mayor a 59. Esto significa transmitir 6 bits por minuto o
sea que la clave podría transmitirse en 3 horas.
Si este ataque parece demasiado complicado, otro ataque posible usando el mismo canal encubierto
sería transmitir la contraseña de alguno de los administradores de la base de datos. Para ello el caballo de
Troya debería ser un keystroke logger. . . aunque habría que asegurarse que el sistema no cuenta con un
camino confiable para ingresar las contraseñas. Aun así el atacante podría instalar un keystroke logger
físico, es decir un dispositivo electrónico que se enchufa entre el teclado y la computadora. . . aunque si el
teclado es inalámbrico el ataque puede simplificarse notablemente. . . ¿tiene sentido un camino confiable
en este caso?
El ejemplo anterior muestra que en realidad se deben eliminar los canales encubiertos
que puedan transmitir al menos 1 bit cada alguna unidad de tiempo razonable (milisegundo,
28‘eavesdropping’, en inglés.
25
segundo, minuto, hora. . . aunque probablemente 1 bit/año sea un canal inútil). A este tipo de
canales encubiertos los llamaremos canales de un 1 bit de ancho de banda o simplemente de 1
bit.
De todas formas, como cualquier otro canal de comunicación, los canales encubiertos suelen
funcionar sobre medios o en entornos que pueden producir errores o ruido, lo que tiende a re-
ducir el ancho de banda efectivo del canal. Además se debe considerar el grado de coordinación
que se requiere entre el transmisor (caballo de Troya) y el receptor (el espía), el cual está muy
influenciado, entre otras cosas, por el algoritmo de scheduling y la cantidad de procesos activos
en el sistema operativo. Cuando el canal tiene mucho ruido los datos que recibe el espía no
son muy confiables. En este caso la confiabilidad requiere un tratamiento probabilístico y algún
protocolo de retransmisión o control de errores. Podemos simplificar la cuestión asumiendo
que existen canales encubiertos de 1 bit de ancho de banda pero donde hay un cierto nivel de
ruido que implica que el espía puede recibir tres respuestas: ‘sí’, ‘no’ o ‘tal vez’. El problema con
estos canales es que el caballo de Troya solo necesita repetir su mensaje lo suficiente hasta que
la respuesta no sea ‘tal vez’.
Notar que un canal como los anteriores sigue siendo útil para cierto tipo de información
muy crítica. Por ejemplo, la respuesta a la pregunta “¿Aceptará Amazon pagos con bitcoins?”
es binaria por lo que un canal de este tipo es más que suficiente. Conocer la respuesta a esa
pregunta con minutos de antelación a su anuncio público puede hacer al atacante muy rico.
Si los canales ruidosos de 1 bit son intolerables en ciertos sistemas, aun quedan los llamados
canales encubiertos de medio bit de ancho de banda. En este caso el caballo de Troya solo puede
transmitir una señal, no dos como en los canales de 1 bit.
Ejemplo 19 (Canal encubierto de medio bit). Las colas de impresión en general son estructuras de
datos finitas. Ahora imaginemos que el caballo de Troya puede imprimir documentos de una cola de
impresión que es usada por procesos que operan a nivel L. Entonces el espía completa la cola de impresión
y trata de enviar un trabajo más. Si el caballo de Troya quiere comunicar algo, imprime uno de los trabajos
lo que le permitirá al espía enviar el trabajo extra y de esta forma sabrá que el caballo de Troya se está
comunicando. Si el trabajo extra no es aceptado el espía no puede concluir nada pues puede ocurrir que
el caballo de Troya no ha encontrado la información que estaba buscando o que aun la está transmitiendo
(está intentando imprimir un documento).
Por ejemplo, con este canal el caballo de Troya solo podría comunicar un ‘sí’ o un ‘no’, pero no ambas,
como respuesta a la pregunta “¿Aceptará Amazon pagos con bitcoins?”. No imprimir un documento de
una cola de impresión completa no significa necesariamente que se está enviando la otra respuesta.
Sin embargo, el sistema del ejemplo no verifica no interferencia pues el proceso L recibe
salidas diferentes dependiendo de lo que hagan los procesos H. Por otro lado el problema
aparece porque la cola de impresión es finita porque de lo contrario el espía no podría completarla
y así recibir dos respuestas diferentes. Entonces, la solución pasaría por tener colas de impresión
infinitas o tener una cola de impresión para cada nivel que es usada solo por procesos de ese
nivel. La primera solución es, en general, irrealizable. La segunda restringe innecesariamente la
usabilidad del sistema pues los procesos de nivel L no pueden enviar datos a procesos de nivel
H cuando no hay nada en la política MLS ni en la no interferencia que lo impida. Por ejemplo,
un proceso L no podría enviar un mensaje de correo electrónico a un proceso H sin que esto
genere un canal encubierto.
Por otra parte, se podría argumentar que podemos convivir con canales de medio bit puesto
que transmiten muy poca información. El problema es que dos canales de medio bit se pueden
26
0|1
read
𝒮
H if 1 then call(𝒜 ) else call(ℬ )
𝒜 ℬ
caballo caballo
Troya Troya
buffer buffer
L espía espía
componer para generar un canal de 1 bit. Esta situación se grafica en la Figura 5. Digamos
que empaquetamos un caballo de Troya, su canal de medio bit y su espía en un sistema que
llamamos 𝒜; y hacemos lo propio con los elementos del otro canal de medio bit al que llamamos
ℬ. Asumimos que la respuesta positiva del sistema 𝒜 es ‘sí’ y la de ℬ es ‘no’. Ahora consideramos
un tercer caballo de Troya, que llamamos 𝒮, que leerá la información necesaria para determinar
la respuesta binaria que queremos descubrir. Obviamente 𝒮 leerá información H y operará a
nivel H. Si la respuesta binaria es ‘sí’ (por ejemplo Amazon aceptará pagos con bitcoins) 𝒮 le
enviará una señal al sistema 𝒜 el cual le permitirá a su espía ver el ‘sí’; si la respuesta binaria
es ‘no’ (Amazon no aceptará pagos con bitcoins) 𝒮 le enviará una señal al sistema ℬ el cual le
permitirá a su espía ver el ‘no’. Notar que 𝒮 se puede comunicar, a nivel H, con los caballos de
Troya de 𝒜 y ℬ pues ambos también operan a nivel H.
e → a1 → b1 → 𝑆𝑇𝑂𝑃 ∥ e → a2 → b2 → 𝑆𝑇𝑂𝑃
que representa a dos procesos que esperan una entrada en común (e) que luego es procesada por
cada uno de ellos de forma independiente (a1 y a2 ) lo que produce dos salidas diferentes (b1 y b2 ).
En este caso tenemos dos secuencias de salidas posibles, ⟨b1 , b2 ⟩ y ⟨b2 , b1 ⟩, dependiendo de cuál
de los dos procesos trabaja más rápido29. O sea que dos corridas diferentes de esta composición
concurrente pueden dar resultados distintos aunque se les haya provisto la misma entrada.
En otras palabras el sistema es no determinista. O sea que el modelo de Goguen-Meseguer no
aplicaría a sistemas operativos que al menos simulan la concurrencia.
McCullough [17, 18] extiende el concepto de no interferencia a sistemas no deterministas y
lo llama no interferencia generalizada30.
H H
L L
Cuando el proceso lee información H, el sistema operativo hace un fork creando dos
procesos, digamos PL y PH . PL es el mismo proceso que ejecutaba a nivel L y no se le
permite acceder a la información H; PH es un nuevo proceso clasificado con H al que se le
permite acceder a la información H.
Un proceso clasificado en L puede escribir información en objetos clasificados con L y H;
un proceso clasificado en H solo puede escribir información en objetos clasificados con H.
De esa forma, un sistema SME es seguro y a la vez tan usable como lo permite MLS.
Ejemplo 20 (SME es más usable que BLP). Si un usuario de un sistema BLP quiere editar un archivo
L y un archivo H, debe lanzar dos veces el editor de textos pasándole como parámetro cada uno de los
archivos, o sea:
$ gedit archivo L &
$ gedit archivo H &
En cambio en un sistema SME esto lo hace automáticamente el sistema operativo; no se requiere inter-
vención del usuario:
$ gedit archivo L archivo H &
crea dos procesos, uno que accede solo a archivo L y el otro que accede a ambos archivos pero que solo
puede escribir en archivo H, de manera tal que el usuario puede editar ambos.
Esta automatización es más evidente y útil en sistemas que funcionan con menos interacción con
el usuario, como por ejemplo un sistema de distribución de correo electrónico que debe operar a varios
niveles de seguridad diferentes.
𝒫 : M×E×P → M×E
𝒫(m, n) = n (𝒫9 )
𝒫(m, x) = m(x) (𝒫10 )
𝒫(m, ∗x) = m(−
x,−→
m) (𝒫11 )
𝒫(m, &x) = 𝒜 −1 (x) (𝒫12 )
𝒫(m, a1 ⊞ a2 ) = 𝒫(m, a1 ) ⊞ 𝒫(m, a2 ) (𝒫13 )
𝒮 : M×M×E×P → M×M×E
donde, informalmente, la primera memoria es la que usan los procesos que ejecutan a nivel L y la
segunda es la que usan aquellos que lo hacen a nivel H. Es decir que 𝒮(mL , mH , e, p) = (m′L , m′H , e′)
significa que el proceso p comienza a ejecutar con las memorias mL y mH y con el entorno e y
finaliza dejando las memorias y el entorno en el estado (m′L , m′H , e′). Notar que hay dos memorias
pero un único entorno; es decir ambas copias del proceso usan el mismo entorno.
La definición de 𝒮 la pueden ver en la Figura 10, donde las definiciones de m′L , m′H , etc.
se encuentran al final de la figura. En este caso 𝒮 funciona con cuatro dispositivos de E/S:
Db = {iL , iH .oL , oH } tales que sc(iL ) = sc(oL ) = L y sc(iH ) = sc(oH ) = H. Si bien se espera que cada
proceso use solo la memoria L hasta que sea necesario duplicarlo para así comenzar a usar la
H, la especificación de 𝒮 dada en la Figura 10 supone que el proceso ya fue duplicado. Esto se
puede apreciar, por ejemplo, en la regla (𝒮2 ) donde ante una asignación se modifican ambas
memorias.
Posiblemente los casos más interesantes sean los de read() y write() pues se puede ver cómo
32
P1 P2 P3
máquina de seguridad 𝒮
hardware
red
𝒮 altera la E/S normal de los procesos para imponer la no interferencia. Por ejemplo en (𝒮7 )
los dispositivos de salida de nivel L permanecen sin cambios; y en (𝒮4 ) se ve cómo la entrada
de nivel L la toman ambas copias del proceso. El caso (𝒮8 ) también es revelador pues se ve
que ambas copias del proceso ejecutan por separado pero 𝒮 junta las salidas de ambos (i.e.
e′ = e′L ∪ e′H ).
∀e1 , e2 ∈ E; m1 , m2 , m3 ∈ M; p ∈ P •
e1 (iL ) = e2 (iL )
∧ 𝒮(m1 , m2 , e1 , p) = (m′1 , m′2 , e′1 ) ∧ 𝒮(m1 , m3 , e2 , p) = (m′3 , m′4 , e′2 )
⇒ e′1 (oL ) = e′2 (oL )
Análisis de SME. SME no está exenta de problemas. En primer lugar es evidente que recarga
el sistema pues se necesita un proceso por cada objeto de clase de acceso diferente a las de los
que estén en uso. En el mismo sentido se requiere más memoria.
33
𝒮 : M×M×E×P → M×M×E
donde
m′L = 𝒫(mL , e, p̄).1, m′H = 𝒫(mH , e, p̄).1
e′L = 𝒫(mL , eL , p̄).2, e′H = 𝒫(mH , eH , p̄).2
e′ = e′L ∪ e′H
eL = {iL , oL } ◁ e, eH = {iH , oH } ◁ e
p̄ es el cuarto parámetro de la 𝒮 sobre la cual se aplica la regla correspondiente
También continúa habiendo problemas de usabilidad. Por ejemplo, supongamos que tene-
mos tres niveles de seguridad L < H < V. Un usuario lanza un editor de textos que inicia en
nivel L. Luego abre un archivo de nivel V por lo que el editor se duplica en dos, uno de nivel L y
otro de nivel V. ¿Para qué sirve el editor L si no tiene archivos abiertos? ¿Se lo puede cerrar? ¿Se
lo puede volver a abrir? ¿Cuál de los dos editores debería usar el usuario para abrir un archivo
de nivel H? ¿Puede hacerlo desde el editor de nivel V?
Una de las razones que llevaron a proponer SME fue la falta de practicidad de otras so-
luciones que proponen desarrollar todas las aplicaciones nuevamente de forma tal que esté
demostrado que son no interferentes. ¿De qué magnitud serían los cambios en las aplicaciones
existentes si tuvieran que ejecutar sobre un sistema SME? Por ejemplo, hay aplicaciones que de-
jan archivos temporales en un directorio definido por la configuración. ¿Podrían dejar archivos
temporales de diferentes niveles de seguridad en el mismo directorio?
Esta sección es opcional y solo la deben leer si no recuerdan o no saben cómo funciona el
mecanismo SUID en sistemas UNIX.
En UNIX un programa SUID es un programa que ejecuta un bloque de sentencias con la
identidad de un usuario distinto al que lo lanzó. Los programas SUID se pueden identificar con
el comando ls:
$ ls -l /usr/bin/passwd
da como resultado
-rwsr-xr-x 1 root root 54256 may 16 2017 passwd
donde la ‘s’ indica que el ejecutable passwd es un programa SUID.
¿Por qué el programa passwd (que sirve para que los usuarios cambien sus contraseñas) es
un programa SUID? Las contraseñas de los usuarios se guardan en el archivo /etc/shadow cuyos
permisos son:
ls -l /etc/shadow
-rw-r- - - - - 1 root shadow 1505 nov 22 21:42 shadow
donde se puede ver que solo el dueño del archivo (i.e. root) tiene permiso para escribirlo.
Entonces, ¿cómo hace un usuario ordinario para cambiar su contraseña siendo que solo root
puede modificar shadow? Precisamente, el usuario ejecuta el programa passwd el cual en algún
momento le solicita al sistema operativo cambiar su identidad a la de su dueño (es decir, root),
como el programa es SUID el sistema operativo acepta la petición y en consecuencia el programa
puede abrir shadow en modo de escritura. Cuando la nueva contraseña se graba en el archivo,
passwd lo cierra y le solicita al sistema operativo volver a su identidad original36.
La estructura básica del código de passwd sería la siguiente:
main(. . . ) {
......
. . . . . . // ejecuta a nombre del usuario ordinario
......
setuid(...) // comienza a ejecutar como root
. . . . . . grabar la nueva contraseña . . . . . .
setuid(...) // termina de ejecutar como root
......
36Las solicitudes de cambio de identidad se hacen por medio de la llamada al sistema setuid.
36
texto
datos
pila (stack)
el arreglo. Por ejemplo, podría ser una cantidad de filas o columnas inusualmente grande o el texto de
alguna celda contiene caracteres en una codificación no prevista, etc.
La Figura 11 muestra la estructura de la memoria de un proceso. Cada bloque se denomina
segmento de memoria. El segmento de texto tiene un tamaño fijo e incluye el código ejecutable. Este
segmento es generado por el compilador y el ensamblador y no se puede modificar. Puede ser
compartido por varios procesos que ejecutan el mismo programa. El segmento de datos contiene
el espacio para almacenar las variables globales y estáticas. El tamaño de este segmento puede
modificarse a medida que el proceso ejecuta.
El segmento que nos interesa es la pila. Aquí es donde se pueden producir los desbordamien-
tos de arreglos. Este segmento tiene un tamaño variable y su contenido puede ser modificado.
La pila se utiliza para administrar la ejecución de subrutinas junto a sus variables y datos.
La Figura 12 muestra cómo crece una pila a medida que se invocan subrutinas (es decir
estamos viendo más en detalle el segmento inferior de la Figura 11). A la izquierda tenemos
los marcos de pila correspondientes a cada subrutina de la derecha. Cada marco almacena los
parámetros y variables de la subrutina correspondiente. Observar que main() invoca a f() la cual
a su vez invoca a g(). Cuando el programa inicia el único marco en la pila es el correspondiente
a main(); cuando main() invoca a f() se apila su marco; y cuando esta última invoca a g() se apila
su marco. Notar que la pila crece hacia abajo. Simétricamente, cuando g() termina su marco se
quita de la pila; y así se continúa desapilando a medida que las subrutinas van terminando.
En la Figura 13 mostramos la estructura de cada marco de pila. Las variables locales a la
subrutina se almacenan en la parte inferior del marco, luego viene la dirección de retorno y
finalmente los parámetros pasados por valor. Si una variable local es un arreglo las componentes
con índice superior se ubican en las posiciones de memoria más altas. La dirección de retorno
corresponde a la ubicación de la instrucción siguiente a aquella donde se efectúo la invocación.
Las áreas en gris son las importantes puesto que una de ellas almacena un arreglo y la otra es
la dirección de retorno.
El primer paso en el ataque es sobrescribir la dirección de retorno. Para esto es necesario
que una subrutina refiera componentes de un arreglo declarado localmente que no existen. A
su vez esto se logra si tal subrutina no controla la longitud de algún arreglo (o lo hace de forma
errónea). La función strcpy() de la biblioteca estándar de C es famosa por no implementar ese
control. En la parte derecha de la Figura 14 podemos ver el código de strcpy() 37. Esta función
copia el contenido del arreglo t en el arreglo s 38. Notar que en el ciclo while no se tienen en
cuenta las longitudes de los arreglos. Por lo tanto si t es más largo que s habrá un desborde de
37En realidad en la figura el código está escrito de una forma que creemos es mucho más clara que la verdadera
implementación: while(*s++=*t++).
38Más que arreglos s y t son punteros a caracter.
38
g(. . . ) {
......
marco de main }
f(. . . ) {
marco de f ......
g(. . . );
......
marco de g }
parámetro
parámetro
dirección de retorno
a[1],a[2],. . . ,a[n]
variable local
variable local
......
b[6] int i = 0;
b[5] while(t[i] != ’\0’) {
ret b[4] s[i] = t[i];
a[3] b[3] i = i + 1;
marco de bof()
a[0] b[0] }
b[7] b[7]
b[6] b[6]
bof( ) {
b[5] b[5]
char a[4], b[8];
b[4] b[4]
for(i=0; i <= 7; i++) b[i] = ’A’;
b[3] b[3]
strcpy(a,b);
b[2] b[2]
}
b[1] b[1]
b[0] b[0]
Figura 14: A la izquierda: marco de bof() antes y después de invocar a strcpy(). A la derecha: código
de strcpy() y de bof() que al invocar a strcpy() produce un desbordamiento de arreglo.
arreglo. Es decir, dentro del ciclo, i tomará valores tales que s[i] no es un componente de s. Como
i crece, las direcciones de memoria s[i] son cada vez más grandes y en algún momento una de
ellas corresponderá a la celda donde está almacenada la dirección de retorno de la función.
En ese momento el valor de t[i] sobrescribirá la dirección de retorno. Esto se puede ver en la
parte izquierda de la figura donde se muestra el marco de la función bof() antes y después de
invocar a strcpy() (el código de bof() también se muestra en la figura). Esta función declara dos
arreglos y le pide a strcpy() que copie uno en el otro. El problema es que el arreglo de origen (b)
es más largo que el de destino (a) y por lo tanto la quinta componente de b se escribirá sobre
la dirección de retorno. Como b[4] guarda el código ASCII de la letra ‘A’, el sistema creerá que
debe retornar a la dirección 0x41 que es el hexadecimal de ‘A’39. Pero la dirección 0x41 es muy
baja y seguramente estará ocupada por el sistema operativo por lo que habrá un fallo grave y
el programa terminará abruptamente. Sin embargo, si el número con el que se sobrescribe la
dirección de retorno resulta ser una dirección dentro del espacio de memoria del proceso, no
habrá ningún error y el flujo de control continuará en ese punto.
El segundo paso del ataque es, entonces, sobrescribir la dirección de retorno con una di-
rección dentro del espacio de memoria del proceso. La técnica usada cuando este ataque fue
descubierto consiste en darle al programa una entrada parte de la cual es un pequeño progra-
ma assembler, llamado payload o shell code (recordar que el ataque comienza proveyendo una
entrada inesperada, ver Ejemplo 22). El nombre shell code proviene del hecho de que lo que casi
siempre intentan los atacantes es ejecutar un intérprete de comandos (shell) que les permita
ejecutar cualquier programa. La Figura 15 muestra esquemáticamente cómo sería una entrada
39En realidad la dirección de retorno ocupa 4 bytes por lo que se necesitarían 4 componentes del arreglo b para
sobrescribirla completamente. Hemos considerado solo un byte para simplicar la explicación.
40
direcciones 21 22 23 24 25 26 27
Figura 15: La dirección de retorno apunta al inicio del shell code (en gris)
que contiene un shell code y su ubicación en un marco de pila. Para simplificar la exposición
consideramos que ciertos elementos de la cadena de entrada ocupan un byte cuando en realidad
es más complejo y nos hemos tomado algunas licencias en cuanto a los tipos de datos usados.
La cadena de entrada contiene dos caracteres ‘A’ que se los usa para ubicar el shell code en el
lugar preciso. Luego viene el shell code el cual consta de la llamada al sistema exec a la cual
se le pasa como parámetro el programa /bin/bash. Luego sigue la instrucción exit para cerrar el
programa adecuadamente. El shell code se cierra con la dirección de memoria que corresponde
al inicio del shell code; este valor debe caer justo donde está almacenada la dirección de retorno.
En realidad programar y ubicar el shell code es algo más complicado. A continuación mostra-
mos el assembler (x86) correspondiente a un shell code que solo ejecuta el intérprete de comandos
bash; más abajo explicamos el programa.
1: jmp 0x2a
2: popl %esi
3: movl %esi, 0x8 ( %esi)
4: movb $0x0, 0x7 ( %esi)
5: movl $0x0, 0xc ( %esi)
6: movl $0xb, %eax
7: movl %esi, %ebx
8: leal 0x8 ( %esi), %ecx
9: leal 0xc ( %esi), %edx
10: int $0x80
11: movl $0x1, %eax
12: movl $0x0, %ebx
13: int $0x80
14: call -0x2b
15: .string "/bin/bash"
donde
1. Salta a la instrucción 14. Este salto se calcula luego de armar el shell code.
2. Recupera la dirección de la instrucción posterior al call y la almacena en el registro esi. Esto
es así pues en la instrucción 14 hay una instrucción call la cual introduce esa dirección en
la pila. O sea que el flujo de control de este shell code es: 1 → 14 → 2 → 3 → . . . . La dirección
que esta instrucción almacena en esi es importante porque es una dirección del área de
memoria asignada al proceso. De esta forma el shell code conoce al menos una dirección
de memoria del proceso lo que le permite, en las siguientes instrucciones, usarla como
41
pivote para operaciones con la memoria (es decir, esas operaciones toman direcciones de
memoria relativas a la almacenada en esi).
3. Copia la dirección de la cadena /bin/bash en algún lugar de la memoria.
4. Ídem anterior con el caracter 0x0.
5. Ídem anterior con el caracter 0x0.
6. Se prepara para ejecutar execve (11 o b es el índice en la tabla de llamadas al sistema).
7. Copia la dirección de la dirección de /bin/bash en el registro ebx pues así lo necesita execve.
8. Copia la dirección de la cadena /bin/bash en el registro ecx.
9. Copia la dirección de la cadena 0x0 en el registro edx
10. Ejecuta la interrupción x080 que se utiliza para llamadas al sistema. En este caso está
ejecutando execve con parámetro /bin/bash.
11. Las sentencias 11, 12 y 13 equivalen a ejecutar exit(0).
14. Se invoca una supuesta subrutina que comienza en 2.
El paso final es convertir este programa en una cadena de caracteres posiblemente agregán-
dole caracteres normales al inicio para que tenga la longitud justa. Normalmente varios de estos
pasos se pueden automatizar con herramientas de hacking modernas.
3.1.3. Contramedidas
No existe una bala de plata para evitar los ataques por desbordamiento de arreglo (como en
general no existen balas de plata para muchos otros problemas de la Ingeniería de Software).
Las defensas o contramedidas para hacer frente a este tipo de ataques requieren un diseño de
seguridad en capas. Es decir una serie de defensas en profundidad que abarquen medidas de
prevención, detección y respuesta. De esta forma un atacante deberá atravesar todas las barreras
para tener éxito en su ataque. En otras palabras se requiere un abordaje que abarque todo el
proceso de la ingeniería de seguridad informática.
Las medidas de prevención buscan evitar que existan desbordes de arreglos. Como la
posibilidad de desbordar un arreglo es básicamente un error de programación, un grupo
importante de medidas de prevención se focalizan en mejorar las prácticas de programación
para evitar este tipo de errores. Usualmente esto se llama programación segura y en general abarca
todas las vulnerabilidades que tienen origen en la programación. Algunas de las contramedidas
de esta clase son las siguientes:
Utilizar lenguajes de programación fuertemente tipados.
Definir una longitud máxima para todas las entradas del usuario y controlar este máximo
en todos los ciclos.
Controlar que los formatos de datos definidos por los desarrolladores (por ejemplo todas
las cadenas de caracteres terminan con ’\0’) se cumplen siempre.
Utilizar analizadores estáticos de código (dado que algunos de los desbordamientos de
arreglo se pueden detectar estáticamente).
42
Capacitar a los usuarios para que no abran contenido de origen sospechoso. Por ejemplo
si un usuario recibe un PDF de un remitente desconocido o de uno conocido pero que
no esperaba que se lo enviara, no debería abrirlo (o en todo caso debería seguir un
procedimiento seguro).
Una tercer área donde se pueden implementar defensas de prevención son las de tipo orga-
nizacional o de procesos. Estas defensas no son exclusivas de los ataques por desbordamiento de
arreglos sino que aplican a toda la seguridad informática. Algunas de estas medidas defensivas
son:
Contar con un área de seguridad informática separada del departamento de sistemas.
Contar con políticas, estándares y procedimientos para todas las tareas relacionadas con
el mantenimiento de la seguridad informática.
Contar con un plan de capacitación permanente en seguridad informática de todo el
personal que usa software y computadoras.
Contratar expertos externos que evalúen periódicamente el estado de la seguridad infor-
mática de la organización.
Las contramedidas para detección y respuesta, en general, no son exclusivas de los ataques
por desbordamiento de arreglo sino que aplican también a muchos otros ataques. Las defensas
de detección apuntan a detectar la presencia de un ataque o un intruso. Entre las de detección
podemos mencionar:
Introducción de canarios en la pila. Un canario es un valor en la pila que solo conoce el
binario y que cuando es sobrescrito por el desbordamiento es una indicación de ataque.
Cuando se utilizan técnicas de return-oriented programming se pueden implementar algo-
ritmos que detectan binarios que tienen ‘muchos’ returns.
Instalación de sistemas de detección de intrusos42 los cuales buscan patrones de ataque que
permiten levantar alarmas ante presuntos ataques.
Las defensas relativas a la respuesta en general pasan, primero, por mantener bitácoras de
la actividad del sistema relativa a la seguridad (cf. a auditoría). En efecto, si un ataque no se
pudo prevenir ni detectar, lo que queda es poder determinar cómo ocurrió, qué daños produjo,
de dónde provino, quién lo efectuó, etc. Para esto son muy valiosas las bitácoras de seguridad
pues pueden dar indicios de las IP desde donde se efectuaron conexiones, las computadoras
y cuentas de usuario que fueron comprometidas, etc. En segundo lugar, la respuesta incluye
desde acciones técnicas de reparación del daño (por ejemplo, restaurar respaldos o cortar una
conexión de red si el ataque aun no cesó) hasta acciones legales (por ejemplo, denunciar el ataque
a las autoridades), pasando por acciones de estudio que nos permitan mejorar la seguridad en
el futuro. Una buena ingeniería de seguridad informática debe contar con planes de respuesta
para cada tipo de ataque razonable, confeccionados con anticipación.
que sí necesita un ataque con caballo de Troya es que el usuario ejecute un programa, mientras
que con el desbordamiento de arreglos, en el peor de los casos, se necesita que el usuario abra
un archivo de datos (por ejemplo un PDF). Dada la facilidad con que muchos usuarios (en
particular usuarios avanzados, desarrolladores y administradores de sistemas) instalan y usan
software descargado de Internet, parecería que no es tan difícil lograr que un usuario ejecute un
programa más o menos desconocido. Por el contrario, si bien es cierto que en general el software
está repleto de errores, encontrar un desbordamiento de arreglos explotable no es cosa de todos
los días, y aun cuando lo encontramos requiere un trabajo nada trivial montar el ataque (más
aun si es preciso usar técnicas como ROP o ROP sin returns).
Aun así, los atacantes tipo hackers parecerían estar mucho más interesados en encontrar
desbordes de arreglos o ataques de una complejidad técnica igual o superior que en infiltrar
caballos de Troya en computadoras. Por ejemplo, ¿por qué la Agencia Nacional de Seguridad
(NSA) de los EE.UU. hace grandes esfuerzos por romper complejos sistemas criptográficos
cuando podría convertir cualquier versión de Linux y Windows (o sus programas más usados)
en caballos de Troya? ¿Serán ya caballos de Troya? El gobierno de los EE.UU., ¿no habrá
solicitado ya la colaboración de empresas esencialmente norteamericanas tales como Intel,
Microsoft, Oracle, IBM, Google, etc. para que sus productos se comporten como caballos de
Troya si el gobierno así lo necesita? ¿O tal vez habrá infiltrado esas empresas con programadores
que incluyeron unas pocas líneas de código entre los millones de sus principales productos de
tal manera que ahora son caballos de Troya al servicio del gobierno de ese país? ¿No le daría
eso acceso a virtualmente todas las computadoras del mundo? Probablemente hayan hecho esto
[12, 19, 31] y también intenten romper criptografía, encontrar desbordes de arreglo, etc.
Más aun, si el sistema operativo implementa alguna forma de no interferencia, un ataque
por medio de desbordamiento de arreglos sería totalmente inofensivo pues las protecciones
implementadas por el sistema asumen que todo el software fuera de la TCB puede ser desa-
rrollado por un enemigo letal. En este caso el único desborde de arreglo peligroso sería aquel
presente en uno de los componentes de la TCB pues destruiría las protecciones del sistema. Sin
embargo, el software de la TCB se espera haya sido sometido a verificación formal por lo cual
no debería existir la posibilidad de desbordar arreglos.
dado que el símbolo # es interpretado como el inicio de un comentario SQL. Entonces, esta sentencia
devolverá todos los valores del campo password ya que la condición firstname=’evil’ or 1=1 se satisface
trivialmente para todos los registros de la tabla.
De igual forma se pueden ejecutar otras sentencias SQL; por ejemplo, ingresando:
fn = evil’; delete from algunatabla #
ln = newman
el DBMS ejecuta:
select password from authors where firstname=’evil’; delete from algunatabla
3.2.1. Contramedidas
Muchas de las contramedidas de carácter general estudiadas para el ataque por desborda-
miento de arreglos, también aplican a ISQL. Por otro lado, hay al menos dos mecanismos de
defensa preventivos dentro del área de programación (que es la que nos ocupa en esta unidad)
que son exclusivos para ISQL.
La primera defensa que veremos se denomina sentencias preprogramadas45 o consultas para-
metrizadas46. Este estilo de programación SQL debería ser el primero que deberían aplicar los
desarrolladores de este tipo de aplicaciones. Las sentencias preprogramadas obligan a los pro-
gramadores a definir inicialmente todo el código SQL y luego pasarle los parámetros. Este estilo
de programación le permite al DBMS distinguir código de datos. Las sentencias preprograma-
das impiden que un atacante cambie el significado esperado de las sentencias SQL.
Cada lenguaje de programación define su propio entorno para preprogramar sentencias:
En Java se debe usar PreparedStatement()
En .NET se usan consultas parametrizadas como SqlCommand() o OleDbCommand()
En PHP se debe usar PHP Data Objects (PDO) con consultas parametrizadas fuertemente
tipadas (vía bindParam()).
En situaciones poco frecuentes las sentencias preprogramadas pueden afectar el desempeño
de la aplicación. Si esto ocurre las alternativas son:
Validar toda la entrada del usuario
Escapar toda la entrada del usuario utilizando una subrutina de escape de caracteres
provista por el fabricante del DBMS.
Usar procedimientos almacenados47 (ver más abajo)
Ejemplo 24 (Sentencias preprogramadas en Java). El código Java que sigue muestra el uso de las
sentencias preprogramadas.
String fn = request.getParameter(’firstname’);
String ln = request.getParameter(’lastname’);
String query = “select password from authors where firstname = ? and lastname = ?”;
PreparedStatement ps = connection.prepareStatement(query);
ps.setString(1,fn);
ps.setString(2,ln);
try {ResultSet res = ps.executeQuery();}
Los desarrolladores tienden a preferir las sentencias preprogramadas porque todo el código
SQL permanece en la aplicación y la mantiene relativamente independiente del DBMS. Sin
embargo, los procedimientos almacenados permiten guardar todo el código SQL en el mismo
DBMS lo que tiene ventajas desde la seguridad pero también desde otros aspectos (como el
desempeño).
Entonces el segundo mecanismo de defensa que veremos son los procedimientos almace-
nados. Estos tiene el mismo efecto que las sentencias preprogramadas si se los implementa de
manera segura (lo que significa no usar SQL dinámico). Como con las sentencias preprograma-
das, primero se deben definir las sentencias SQL y luego se les pasan los parámetros. Entonces,
dentro del DBMS se programa una subrutina (el procedimiento almacenado) que tiene un
identificador (nombre) y parámetros. Luego, desde la aplicación, se invoca esta subrutina pro-
veyéndole los parámetros. Obviamente esta solución es muy dependiente del DBMS concreto
pues cada uno define su propia interfaz y lenguaje para los procedimientos almacenados.
Ejemplo 25 (Procedimiento almacenado en Java). El código Java que sigue muestra el uso de los
procedimientos almacenados.
String fn = request.getParameter(’firstname’);
String ln = request.getParameter(’lastname’);
try {
CallableStatement cs = connection.prepareCall(“{call sp getPassword(?,?)}”);
cs.setString(1,fn);
cs.setString(2,ln);
ResultSet res = cs.executeQuery();
}
donde todo lo que está entre comillas en la primera sentencia del try es dependiente del DBMS (es decir,
por ejemplo, otro DBMS podría usar invoke en lugar de call); y sp getPassword() es el nombre del
procedimiento almacenado.
es muy difícil evitar estos ataques y cuando ocurren suelen tener consecuencias muy graves
para las organizaciones.
Supongamos entonces que tenemos un lenguaje de programación (de alto nivel), 𝒫, que
tiene enteros, pares ordenados, sumas disjuntas o uniones y listas como tipos de datos. La
gramática del sistema de tipos de 𝒫 es:
donde (·, ·) simboliza un par ordenado cuyas componentes pueden ser de distintos tipos; y (·; ·)
simboliza una suma disjunta o unión donde una expresión de ese tipo o bien tiene el tipo de la
izquierda o bien el de la derecha. Cuando se define un tipo unión se deben dar los constructores
correspondientes.
Pares ordenados. Cada par ordenado se representa como un puntero a dos palabras
contiguas.
Uniones. Cada valor de un tipo unión se representa como un puntero a dos palabras
contiguas donde la primera almacena el constructor usado (0 para el izquierdo y 1 para
el derecho) y la segunda almacena el valor del tipo correspondiente (el cual obviamente
depende del constructor utilizado).
Listas. La lista vacía se representa con 0. Una lista no vacía se representa con un puntero
a un par de palabras contiguas (i.e. cada nodo de la lista) donde la primera almacena el
primer valor de la lista y la segunda un puntero al siguiente nodo.
Ejemplo 27 (Representación binaria de los tipos de 𝒫). A continuación mostramos varios valores
de diferentes tipos de 𝒫 y su representación binaria en forma gráfica. Asumimos que contamos con la
declaración de la unión T del Ejemplo 26. Cada cuadrado representa una palabra; el cuadrado de la
columna de la izquierda representa la variable x correspondiente.
49Usamos el término ‘palabra’ (word) de memoria como una secuencia contigua de posiciones de memoria que
almacenan un valor indivisible. Por ejemplo, en una arquitectura dada, un entero se almacena en una palabra de
cuatro bytes. Suponemos que la primera dirección de memoria identifica la palabra. Entonces si e es una dirección
de memoria al inicio de una palabra, e + 4 es la dirección de memoria de la palabra siguiente si la longitud de palabra
es de 4 bytes.
50
Int x1 = 5 5
(Int,Int) x2 = (2,3) • 2 3
T x3 = par x2 • 1 •
T x4 = int 7 • 0 7
List(T) x5 = [x4,x3] • • • • 0
En la tercer línea, la primera de las dos palabras contiguas vale 1 porque x3 es un par ordenado de
tipo T, mientras que en la siguiente línea vale 0 porque x4 es un entero del mismo tipo.
El consumidor de código formaliza estas representaciones con proposiciones de tipos50 de la
forma:
m⊢e:𝜏
m ⊢ e : (𝜏1 , 𝜏2 )
(1)
m ⊢ e : Addr m ⊢ e + 4 : Addr m ⊢ m(e) : 𝜏1 m ⊢ m(e + 4) : 𝜏2
Algo similar ocurre con las uniones de tipo (𝜏1 ; 𝜏2 ) solo que en este caso la expresión o tiene un
tipo o tiene el otro, dependiendo del valor del constructor:
m ⊢ e : (𝜏1 ; 𝜏2 )
(2)
m ⊢ e : Addr m ⊢ e + 4 : Addr m(e) = 0 ⇒ m ⊢ m(e + 4) : 𝜏1 m(e) ≠ 0 ⇒ m ⊢ m(e + 4) : 𝜏2
m ⊢ e : List(𝜏) e≠0
(3)
m ⊢ e : Addr m ⊢ e + 4 : Addr m ⊢ m(e) : 𝜏 m ⊢ m(e + 4) : List(𝜏)
Por último, una regla que establece que la la suma de enteros tiene tipo entero:
m ⊢ e1 : Int m ⊢ e2 : Int
(4)
m ⊢ e1 + e2 : Int
50‘type judgement’, en inglés.
51
Estas reglas le permiten al productor de código anotar su código binario de forma tal que,
usando una adaptación de la lógica de Hoare, se puede verificar que si vale la pre-condición
(del programa) entonces vale la post-condición. Tanto la pre-condición como la post-condición
son, precisamente, proposiciones de tipos (m ⊢ e : 𝜏).
Entonces, veamos un ejemplo de cómo el productor de código debe anotar su código de
forma tal que luego pueda hacer la demostración de que el código verifica la política de tipos del
consumidor. Supongamos que el productor ha escrito una función, sum, que suma los elementos
de una lista de tipo List(T) donde T es el tipo definido en el Ejemplo 26. Por ejemplo sum([int 2,par
(3,-1),int 9,int -4]) debería sumar 2 + 3 - 1 + 9 - 4, o sea el resultado debería ser 9. El productor
compila el programa y produce el binario siguiente51.
1 pre m ⊢ r0 : List(T) ; r0 es la lista a sumar
2 mov r1 ,0 ; r1 es el acumulador, se inicializa a 0
3 : i n i inv m ⊢ r0 : List(T) ∧ m ⊢ r1 : Int ; invariante de ciclo
4 beq r0 , : f i n ; lista vacía? Si true a : fin
5 ld r2 , 0 ( r0 ) ; cargar primer elemento
6 ld r0 , 4 ( r0 ) ; cargar el resto
7 ld r3 , 0 ( r2 ) ; cargar constructor
8 ld r2 , 4 ( r2 ) ; cargar dato
9 beq r3 , : sum ; es un int? Si true a : sum
10 ld r3 , 0 ( r2 ) ; cargar x
11 ld r2 , 4 ( r2 ) ; cargar y
12 add r2 , r3 , r2 ;x+y
13 : sum add r1 , r2 , r1 ; sumar
14 br : ini ; reiniciar ciclo
15 : f i n mov r0 , r1 ; copiar resultado en r0
16 post m ⊢ r0 : Int ; post−condición
17 ret ; resultado en r0
Las instrucciones pre , inv y post son en realidad comentarios insertados al código binario
con el fin de proveer las proposiciones de tipos necesarias para realizar la verificación; es decir,
estas líneas no se ejecutan. Se asume que hay solo una instrucción pre al inicio del código; una
instrucción post antes de cada instrucción r e t ; y tantas instrucciones inv como sean necesarias
(ver más adelante). No es necesario que comprendan el código en detalle. Lo importante es que:
ri son registros del micro; en r0 se copia la lista a sumar; r1 es el acumulador para llevar la suma;
en la primera línea está la pre-condición52 de la función; de la línea 4 a la 14 se define un ciclo
(loop) cuyo invariante se da en la línea 3 y donde se itera sobre la lista para ir sumando sus
elementos; :ini, :sum y :fin son etiquetas para realizar saltos; en la línea 16 está la post-condición
de la función; y el resultado de la suma se devuelve en r0 .
Entonces, sum asume que al inicio en r0 hay algo de tipo List(T) y en ese caso garantiza
que al finalizar, en r0 habrá algo de tipo Int. El objetivo es que el consumidor de código pueda
comprobar que tal afirmación se verifica en el código que recibe.
Con este fin representamos el programa assembler con un vector Π tal que en casa posición
hay una instrucción del programa. O sea que al programa sum le corresponde un Π de 17
51Corresponde a arquitectura DEC Alpha. Ejemplo tomado de [22].
52Recordar que en este contexto pre-condición, invariante y post-condición refieren a proposiciones de tipos y no
a la especificación funcional de la función.
52
posiciones. O sea que Π incluye las pseudo-instrucciones pre , inv o post . Notamos con Πi
cada una de las posiciones de Π. Asociado a Π hay un vector de condiciones de verificación, VC.
Cada VCi contiene una proposición que se obtiene al aplicar una adaptación de la lógica de
Hoare al código binario. Concretamente, VC se calcula de la siguiente forma:
[n/rs ]VCi+1 si Πi = mov rs , n
si Πi = add rs , op, rd
[rs + op/rd ]VCi+1
si Πi = l d rd , n(rs )
m ⊢ rs + n : Addr ∧ [m(rs + n)/rd ]VCi+1
VCi = (rs = 0 ⇒ VCi+n+1 ) ∧ (rs ≠ 0 ⇒ VCi+1 ) si Πi = beq rs , n
VCi+n+1 si Πi = br n
𝒜 si Πi = post 𝒜 ∧ Πi+1 = r e t
𝒜
si Πi = inv 𝒜
donde [e/ri ]P indica que en P se sustituye ri por e.
Si miran con atención van a ver que la definición de VC está dada sustancialmente por
una formalización del assembler siguiendo la lógica de Hoare. Si bien esta definición de VC
alcanza para comprender el ejemplo que venimos desarrollando, en el caso real sería necesario
extenderla a más instrucciones assembler.
Ahora sea Inv el conjunto de números de línea donde hay una instrucción pre o inv , y sea
Invi el predicado de tipos correspondiente a la instrucción de la línea la i. Entonces, se define la
condición de verificación para todo el código de la siguiente forma:
Û
VC(Π, Inv) = ∀rj : Invi ⇒ VCi+1
i∈Inv
En el ejemplo de sum, tenemos Inv = {1, 3}. En consecuencia VC(sum, Inv) tiene dos conjun-
ciones:
puesto que Inv1 es m ⊢ r0 : List(T) y VC2 se calcula aplicando la regla para mov la cual indica
que se debe sustituir r1 por 0 en VC3 que es la proposición de la instrucción inv , o sea m ⊢ r0 :
List(T) ∧ m ⊢ r1 : Int.
El cálculo de Inv3 ⇒ VC4 es más complejo pues requiere derivar todas las condiciones hasta
el final de programa. Esencialmente esta condición establece que se preserva el invariante de
ciclo y que esta implica la post-condición cuando el ciclo termina. La dejamos como ejercicio.
Es importante observar que el productor de código debe proveer Π, el cual incluye la
especificación y los invariantes de ciclo, y una demostración de que vale VC(Π, Inv). La dificultad
en usar PCC yace tanto en proveer las especificaciones y los invariantes de ciclo, como en
proveer la demostración. Para muchas políticas de seguridad de tipos interesantes, es posible
demostrar VC(Π, Inv) automáticamente si se han dado las especificaciones e invariantes de ciclo.
En general, esto se consigue implementando compiladores de certificación53, es decir compiladores
53‘certifying compiler’, en inglés.
53
que generan el código objeto junto con una prueba de que este verifica ciertas propiedades.
Luego, el consumidor solo debe verificar la prueba respecto del código, lo que es siempre
automático y eficiente. Precisamente la ventaja de PCC, frente a otras soluciones, es que la
mayor parte del trabajo se hace off-line. Otra de las ventajas de PCC es que el consumidor o bien
detecta modificaciones en el código y en la prueba que tornan el código peligroso, o bien no las
detecta, aunque las haya habido, pero entonces el código no es peligroso.
3.4. Cuando security es safety: un sistema de tipos para flujo de información seguro
En esta sección vamos a ver un sistema de tipos definido para una cierta clase restringida
de programas que garantiza que cualquier programa correctamente tipado tiene un flujo de
información seguro (es decir, no hay filtración de información de nivel H hacia variables de nivel
L). Dicho de otro modo, vamos a resolver el problema de flujo de información planteado por
Denning [10] pero esta vez usando un sistema de tipos. Claramente, si podemos tratar el flujo
seguro de información como un problema de tipado estamos ante una situación donde security
es safety. En este caso la reducción de un problema de security a uno de safety es posible porque,
como dijimos más arriba, consideramos una clase restringida de programas. Concretamente, el
trabajo que vamos a estudiar es el de Volpano, Smith e Irvine [32]. Posteriormente a este artículo
se han publicado muchos otros siguiendo más o menos la misma línea de trabajo.
El lenguaje de programación sobre el cual se define el sistema de tipos consta de: asignación
(:=), sentencia condicional (if - then - else - fi), sentencia iterativa (while - do - done), concatenación
de sentencias ( ; ), expresiones aritméticas (+, −), expresiones booleanas (=, <) y variables y
constantes enteras54. El 0 se considera como el booleano false mientras que el 1 corresponde a
true.
Cada variable x de un programa tiene una clase de acceso que se denota con x̄55. La clase
de programas para la cual se define el sistema de tipos es aquella en la que la clase de acceso
de cualquier variable de un programa se puede determinar en tiempo de compilación (o sea es
estática) y en consecuencia no puede alterarse durante la ejecución del programa. Claramente
esto deja afuera una clase enorme de programas importantes. Por ejemplo, esta técnica no puede
usarse para un programa donde el usuario ingresa el nombre de un archivo a abrir puesto que
la clase de acceso de la variable donde se leerá el contenido del archivo se puede determinar
una vez leída la entrada del usuario (o sea en tiempo de ejecución). Esta restricción es clave para
poder convertir un problema de security en uno de safety.
Los tipos del sistema de tipos se estratifican en dos niveles. En un nivel están los tipos de
datos, denotados por 𝜏, los cuales corresponden a las clases de acceso. Es decir hay un tipo
de dato por cada clase de acceso. Por ejemplo, si una clase de acceso es (secret, {NATO, F35})
entonces habrá un tipo cuyo nombre es (secret, {NATO, F35}). En el otro nivel están los tipos de
las sentencias y expresiones del lenguaje, llamados tipos frase y denotados por 𝜌. Los tipos frase
incluyen a los tipos de datos que son los que se asignan a expresiones, los tipos de las variables
de la forma 𝜏 var, y los tipos de las sentencias que tienen la forma 𝜏 cmd, llamados tipos comando.
Es decir, los modificadores var y cmd se usan para saber si el tipo corresponde a una variable
54El lenguaje definido por Volpano et al. incluye algunas otras construcciones pero no es necesario tenerlas en
cuenta en este curso.
55En el artículo usan x.
54
𝛾⊢n:𝜏 (INT)
o a una sentencia (cuando se trata de una expresión el tipo se escribe sin modificador). Una
variable cuyo tipo es 𝜏 var guarda información cuya clase de acceso es 𝜏 o inferior. A su vez
una sentencia c tiene tipo 𝜏 cmd solo si está garantizado que cualquier asignación dentro de c se
hace a una variable cuya clase de acceso es 𝜏 o superior. Esta es una propiedad de confinamiento
necesaria para controlar los flujos de información implícitos. Finalmente, se extiende la relación
de dominación entre clases de acceso (⪯) a una relación de subtipado denotada con ⊆56.
Las reglas de tipado para las expresiones y sentencias del lenguaje se dan en la Figura 16.
Como puede apreciarse las reglas de tipado tienen la misma forma que las proposiciones de
tipos de PCC solo que varía su interpretación. En este caso 𝛾 ⊢ p : 𝜌 significa que la sentencia o
expresión p tiene tipo 𝜌 asumiendo que 𝛾 da los tipos para las variables de p. 𝛾 se denomina
contexto de tipado y es una función tal que si x es una variable 𝛾(x) = x̄ var, o sea 𝛾(x) devuelve
el tipo frase de x. Las reglas que definen la lógica de subtipado se dan en la Figura 17. La regla
(CMD− ) establece que la relación de subtipado es antimonotónica para los tipos comando—si
𝜏 ⊆ 𝜏′ entonces 𝜏′ cmd ⊆ 𝜏 cmd. Como es usual, existe una regla de coerción de tipos, (SUBTYPE),
la cual permite tipar una expresión o sentencia de tipo 𝜌 con un tipo 𝜌′ siempre y cuando se
verifique 𝜌 ⊆ 𝜌′.
Estas reglas garantizan la seguridad del flujo (explícito o implícito) de la información dentro
de un programa57. Consideremos por ejemplo la regla (ASSIGN). Esta regla dice que para que
el flujo explícito desde e′ a e sea seguro, e y e′ tienen que tener la misma clase de acceso lo que
se impone al tener 𝜏 en ambas hipótesis. De todas formas, un flujo hacia arriba desde e′ hacia e
es aun posible aplicando la regla (SUBTYPE). En efecto, si e : H var y e′ : L entonces el tipo de e′
puede ser forzado a H en cuyo caso la regla (ASSIGN) puede ser aplicada aun con 𝜏 = H.
56O sea que si 𝜏 y 𝜏′ son tipos de datos (es decir son clases de acceso) entonces 𝜏 ⪯ 𝜏′ ⇔ 𝜏 ⊆ 𝜏′ .
57En el artículo de Volpano et al. pueden encontrar la demostración de esta afirmación; no la veremos en clase.
55
𝜏 ⪯ 𝜏′
(BASE)
⊢ 𝜏 ⊆ 𝜏′
𝜌⊆𝜌 (REFLEX)
⊢ 𝜌 ⊆ 𝜌′ ⊢ 𝜌′ ⊆ 𝜌′′
(TRANS)
⊢ 𝜌 ⊆ 𝜌′′
𝜏 ⊆ 𝜏′
(CMD− )
𝜏′ cmd ⊆ 𝜏 cmd
𝛾⊢p:𝜌 ⊢ 𝜌 ⊆ 𝜌′
(SUBTYPE)
𝛾 ⊢ p : 𝜌′
De forma similar, la regla (IF) establece que el tipo del condicional es 𝜏 cmd con el fin
de controlar los flujos implícitos. Por ejemplo, supongamos que x es 0 o 1 y consideremos el
programa:
Aunque no hay un flujo explícito desde x a y, hay un flujo implícito porque el valor de x
se copia en y. Esto es, y := 1 e y := 0 se ejecutan en un contexto donde se conoce de manera
implícita información de nivel 𝛾(x). Por esta razón, las dos asignaciones solo pueden hacerse
hacia variables de nivel 𝛾(x) o superior. Aquí también se pueden usar las reglas de subtipado
para poder aplicar la regla. En este caso lo podemos hacer de dos formas: el tipo de x = 1
se puede forzar a un tipo mayor o el tipo de las asignaciones a uno menor usando la regla
(CMD− )—o sea la antimonoticidad de ⊆ sobre los tipos comando.
Por otra parte es sencillo comprobar que las coerciones de tipo no van a permitir que la
regla se aplique si hay un flujo descendente desde x = 1—o sea si 𝛾(x) ⪯̸ 𝛾(y). Por ejemplo,
supongamos que 𝛾(x) = H var y 𝛾(y) = L var. En ese caso tenemos: 𝛾 ⊢ x = 1 : H, 𝛾 ⊢ y := 1 :
L cmd y 𝛾 ⊢ y := 0 : L cmd. Entonces, si usamos (SUBTYPE) para x = 1 no sirve de nada porque
aumentaríamos aun más el tipo de la condición; y si aplicamos (CMD− ) a, por caso, y := 1 solo
reduciríamos aun más el tipo de la asignación. De esta forma podemos concluir que no es
posible tipar el programa (5) si el contexto de tipado es 𝛾(x) = H var y 𝛾(y) = L var.
Para concluir, el trabajo de Volpano et al. nos dice que dado el lenguaje de programación, el
sistema de tipos y las reglas de tipado que ellos proponen es posible construir un typechecker que
determina si un programa solo tiene flujos de información seguros. Dicho de otro modo, solo
programas seguros pasarán el control de tipos; solo programas seguros tiparán correctamente;
solo es posible tipar programas seguros. Por lo tanto vemos cómo una propiedad de safety (el
tipado) nos permite garantizar una propiedad de security (flujo de información seguro) pero
solo para una clase restringida de programas.
56
y por los usuarios. En general es aquí donde muchas de las hipótesis y principios sobre
los cuales se basa el sistema a nivel matemático-computacional dejan de ser válidas. Por
ejemplo, cualquier sistema criptográfico se basa en que las claves de encriptación perma-
necen en secreto pero en un sistema operativo tipo DAC un caballo de Troya invalidaría
esta condición. Es decir, no se ataca al sistema en sí sino el uso que se hace de él.
Este modelo de amenazas demuestra que la criptografía no es la bala de plata de la seguridad
informática como muchos creen. O sea, no basta con encriptar todo para mejorar la seguridad.
De hecho, la criptografía es, fundamentalmente (ver definición), una herramienta para proteger
comunicaciones (o sea información en tránsito) y no tanto para proteger información dentro de
una computadora.
Ejemplo 28 (La firma electrónica no es tan segura). Una de las aplicaciones rutilantes de la cripto-
grafía moderna es la denominada firma electrónica o digital. Es decir, de alguna forma (ver Sección 4.2),
se usa criptografía para firmar digitalmente un documento en el mismo sentido de la firma manuscrita.
Algunos sostienen que la firma digital es más segura que la firma manuscrita dada la fortaleza matemática
de los métodos utilizados.
Entonces una persona sentada frente a su computadora con sistema operativo Windows o Linux usa
una aplicación para firmar digitalmente un documento, por ejemplo un cheque. Supongamos que la apli-
cación de firma electrónica ha sido formalmente verificada contra la especificación matemática del sistema
criptográfico para firmar. Más aun supongamos que el método matemático no tiene vulnerabilidades. El
problema, sin embargo, no está ni en la aplicación ni en la matemática sino en el entorno computacional
donde es utilizada. Windows o Linux no pueden mantener el nivel de seguridad de la criptografía. En
particular, cualquier proceso podría invocar a la aplicación de firma electrónica para firmar cualquier
documento (por ejemplo otro cheque). Aun si el usuario debe ingresar una contraseña para validar la
firma, un caballo de Troya podría capturarla.
¿Qué mecanismos de seguridad de los que ya hemos visto sería necesario implementar para preservar
el nivel de seguridad de la firma electrónica?
De hecho la criptografía no impide, por ejemplo, que el archivo encriptado en tu compu-
tadora sea borrado por un caballo de Troya. Tampoco impide que el archivo sea leído antes de
ser encriptado y después de ser desencriptado (después de todo los humanos no podemos leer
texto encriptado y en general las computadoras tampoco). Peor aun, existen ataques que usan
criptografía.
Ejemplo 29 (Ransomware). El ransomware es un tipo de software hostil que, por ejemplo, puede
encriptar información almacenada en la computadora de una persona con una clave que solo conoce el
atacante. En general el ransomware puede usar las primitivas criptográficas instaladas en la computadora
de la víctima. En consecuencia la víctima no puede acceder a su información. Para desencriptar la
información el atacante solicita un rescate. De aquí a que este tipo de ataques se los llame secuestro de
información.
Notar que habiendo puesto en práctica procedimientos de seguridad razonables el ataque no es tan
dañino como parece. Con solo efectuar respaldos diarios de la información el daño que un ataque por
ransomware puede producir es limitado y la víctima no necesariamente debería pagar el rescate.
El ataque por ransomware, ¿es un ataque a la confidencialidad, la integridad o la disponibilidad?
La aplicación fundamental de la criptografía es asegurar la confidencialidad de los datos en
tránsito. Sin embargo, también se la usa para asegurar la integridad y autenticidad de datos, y el
no repudio de transacciones (ver Sección 4.2.3). Opuesto a estos requerimientos está, como casi
58
59‘cipher’, en inglés.
60‘plain text’, en inglés.
61Tener en cuenta que para un cifrador no hay diferencia entre texto legible y cifrado; toman y devuelven cadenas
de bytes. En otras palabras, ‘legible’ no significa que un humano lo pueda leer, es simplemente el nombre que le
damos a la entrada del algoritmo que encripta y a la salida del que desencripta (aunque es cierto que se justifica por
el uso más clásico de la criptografía).
59
lleguen a él. Si el enemigo descifra el mensaje a los cinco minutos ya será tarde pues el objetivo
ya habrá sido atacado (tres minutos antes). Es decir el tiempo de vida del mensaje (dos minutos)
es menor que el tiempo necesario para romper el sistema (cinco minutos). Aun así, ¿por qué
alguien usaría tal sistema criptográfico en esa situación pudiendo usar uno mucho más fuerte?
La razón suele ser la eficiencia. En general cuando un sistema criptográfico es más difícil
de romper que otro, también sus procesos de cifrado y descifrado consumen más recursos.
En algunos contextos estos recursos pueden ser escasos y por lo tanto se utilizan sistemas
criptográficos más débiles pero seguros en ese contexto de uso.
La criptografía suele dividirse en criptografía simétrica o de clave privada o de clave compartida
y criptografía asimétrica o de clave pública. En la primera la clave de cifrado y la de descifrado
son la misma; en la segunda, son diferentes. En la criptografía asimétrica, si k es una clave (de
cifrado o descifrado), la otra clave que reversa la operación no puede ser cualquiera sino que
debe mantener una cierta relación matemática con k. En este sentido se habla de par de claves
donde una de ellas se llama clave privada y la otra clave pública. Precisamente, la clave pública
puede ser divulgada sin que esto comprometa la seguridad del sistema.
Los algoritmos de cifrado y descifrado deben ser tales que su composición matemática sea
la identidad. Es decir, si ℰ es un algoritmo de cifrado y 𝒟 el de descifrado del mismo cifrador,
debe valer:
𝒟(ℰ(t, k1 ), k2 ) = t ∧ ℰ(𝒟(t, k1 ), k2 ) = t
para todo texto t y para todo par de claves (k1 , k2 ) (si se trata de un cifrador simétrico las claves
son iguales). O sea que en realidad son dos algoritmos que deben usarse de una cierta forma para
obtener el resultado esperado. No hay nada sustancial que diferencie el proceso de encriptar
del proceso de desencriptar. Cualquiera de los dos algoritmos de un cifrador se pueden usar
para cualquiera de las dos operaciones siempre y cuando se los use de manera consistente.
Cualquier sistema criptográfico se debe basar en el siguiente principio.
Principio de la criptografía La seguridad de un cifrador debe basarse en mantener la clave en secreto
y no en mantener en secreto los algoritmos.
Es decir que la descripción matemática del algoritmo (e incluso su implementación) pueden
ser públicos. Justamente, como dijimos más arriba, se espera que el algoritmo sea tal que
pequeñas diferencias en la clave produzcan grandes diferencias en el texto de salida. Por lo
tanto, aunque el atacante conozca los detalles del algoritmo, al probar con una clave que no es
la que corresponde obtendrá una salida muy diferente a la esperada. Si el espacio de claves es
suficientemente grande un ataque por fuerza bruta, en general, no será factible. El problema
con mantener en secreto el cifrador es que los atacantes disponen de una cantidad de tiempo
arbitraria para descubrirlo, o bien se lo debe cambiar periódicamente lo que implica tener que
proponer nuevos cifradores con la misma periodicidad (tarea que no es nada simple).
Este principio se considera tan importante que cuando el gobierno de EE.UU. debió reem-
plazar el algoritmo de encriptación usado para comunicaciones oficiales sensibles (i.e. DES),
organizó un concurso público internacional donde diversos grupos de criptógrafos propusieron
sus algoritmos. Muchos de estos algoritmos habían sido publicados en conferencias y revistas
científicas del área y por consiguiente eran ampliamente conocidos. El ganador del concurso
fue el algoritmo Rijndael diseñado por los criptógrafos belgas Vincent Rijmen y Joan Daemen62.
62Los otros finalistas del concurso fueron: RC6, MARS, Serpent y Twofish.
60
Rijndael es el primer cifrador público aprobado por la NSA para comunicaciones de nivel de
seguridad top secret (ver Unidad 2). Es decir, el cifrador usado en la actualidad por el gobierno
de EE.UU. para comunicaciones sensibles no fue diseñado por ciudadanos de ese país e incluso
los enemigos de EE.UU. conocen en detalle su diseño.
63Nótese la contemporaneidad del principio de la criptografía con el desarrollo de las primeras comunicaciones
a distancia. Por ejemplo, el telégrafo transcontinental de EE.UU., que conectaba ambas costas, comenzó a operar en
1861.
64‘stream cipher’ en inglés.
65‘iterated product cipher’, en inglés.
61
clave, llamada subclave. A cada iteración se la suele llamar ronda66 y a las transformaciones que se
efectúan en una ronda se las llama función de ronda. Una implementación muy utilizada de este
diseño se debe a Horst Feistel, un físico alemán devenido en criptógrafo norteamericano que
trabajó para IBM. La implementación de Feistel, llamada cifrador de Feistel, tiene la ventaja de que
los procedimientos de cifrado y descifrado son muy similares (idealmente idénticos) requiriendo
únicamente reversar la selección de la subclave67. Esta similitud entre los procedimientos implica
que la implementación requiere menos recursos (memoria o circuitos) lo que en los años setenta
del siglo XX era muy apreciado (y aun sigue siéndolo pero a otra escala). La selección de
la subclave es otro de los elementos del diseño propuesto por Shannon que consiste en un
algoritmo, llamado seleccionador de subclaves, que determina qué parte de la clave (i.e. qué
subclave) se utiliza en cada ronda. En este sentido, reversar la selección de la subclave significa
que la subclave utilizada en la primera ronda será utilizada en la última, la utilizada en la
segunda lo será en la penúltima, y así sucesivamente. Una forma trivial de incrementar la
seguridad de un cifrador producto iterativo es aumentar el número de rondas. Sin embargo,
normalmente esto va en detrimento de la eficiencia por lo cual la definición de una función de
ronda segura es crucial para lograr un cifrador seguro y eficiente.
Un cifrador de Feistel tiene una estructura matemática bien definida. Sea T el conjunto de
todos los textos posibles68 y sea K el espacio de claves. Sea ℱ : T × K → T la función de ronda
(en este caso también llamada función de Feistel), 𝒦 : [1, n] × K → K el seleccionador de subclaves
y n el número de rondas. Para encriptar, el texto legible se divide en dos partes iguales, (l1 , r1 ).
Para cada ronda i, se calcula:
li+1 = ri
ri+1 = li ⊕ ℱ (ri , 𝒦 (i, k))
donde ⊕ simboliza XOR y k es la clave. Esto implica que el texto cifrado es (rn+1 , ln+1 ). Entonces,
para desencriptar, el texto cifrado se divide en dos partes, (rn+1 , ln+1 ). Luego, para cada ronda i
(= n, n − 1, . . . , 1) se calcula:
donde el texto legible es (l1 , r1 ). Observar que la selección de claves se reversa durante el
descifrado pues la primera ronda usa kn , la segunda kn−1 y así sucesivamente.
4.1.1. DES
A continuación veremos con cierto detalle el que tal vez sea el cifrador simétrico de bloques
más conocido, estudiado, atacado, implementado y usado del mundo, y probablemente el
primero de la criptografía moderna. Se trata del Data Encryption Standard (DES) desarrollado
por un grupo de IBM, liderado por Feistel, a principios de los años setenta del siglo pasado. DES
fue propuesto por IBM, a pedido del National Institute of Standards and Technology (NIST) de
EE.UU., como cifrador para proteger información sensible del gobierno de ese país. Luego de
consultar con la NSA, el NIST tomó una versión ligeramente diferente que convirtió en estándar
66‘round’, en inglés.
67‘key schedule’, en inglés.
68Recordar que no hay una verdadera distinción entre textos legibles y cifrados.
62
Si bien 3DES se sigue usando hay otros cifradores que tienen al menos el mismo nivel de
seguridad pero que son más eficientes al no tener que realizar tres operaciones de cifrado.
DES es un cifrador de bloques de Feistel de 16 rondas que utiliza claves de 56 bits69 y
bloques de 64 bits. La longitud de la clave es el factor que hace que en la actualidad DES sea
inseguro (3DES es más o menos equivalente a DES con claves de 112 bits y por lo tanto sigue
siendo seguro). Antes de la primera ronda y luego de la última se efectúan dos permutaciones
(llamadas inicial y final) pero que no tienen significado criptográfico (se las incluyó por cuestiones
del hardware de la época).
Dado que DES es un cifrador de Feistel solo debemos dar la definición del seleccionador de
subclaves, 𝒦 , y de la función de ronda, ℱ . La estructura general del seleccionador de subclaves
se puede ver en la Figura 18. 𝒦 toma como entrada un número natural entre 1 y 16 y la clave de
64 bits, y devuelve un bloque de 48 bits (ki ). La selección permutada 1 está dada por una matriz
de 8 × 7 dividida en dos mitades horizontales de 4 × 7 tal que la mitad superior corresponde a
C0 y la inferior a D0 . Por ejemplo, la primera fila de esta matriz es:
57 49 41 33 25 17 9
selección permutada 1
C0 D0
C1 D1
selección
permutada 2 k1
Ci Di
selección
permutada 2 ki
C16 D16
selección
permutada 2 k16
La función ℱ recibe y devuelve bloques de 32 bits y está definida por la siguiente fórmula
(ver detalles en [21]):
donde
Cada Bi es un bloque de 6 bits tal que:
B1 . . . B8 = k̄ ⊕ E(r)
donde E es una función que recibe bloques de 32 bits y retorna bloques de 48 bits (notar
que r es la parte derecha del texto legible la cual tiene 32 bits y k̄ es una subclave que
tienen 48 bits). E se llama función de expansión.
Cada Si es una matriz de 4 × 16 tal que si B es un bloque de 6 bits, luego:
donde
• Si [m, j]2 es la representación binaria de la celda (m, j) de Si
• m es el número (de 0 a 3) que se obtiene al convertir a base 10 el número formado
por el primer y último bit de B
• j es el número (de 0 a 15) que se obtiene al convertir a base 10 el número formado
por los 4 bits centrales de B
P es una permutación que trabaja sobre bloques de 32 bits
Las operaciones representadas por las funciones P, Si y E se denominan caja-P71 (i.e. per-
mutación), caja-S (i.e. sustitución) y caja-E (i.e. expansión). Estas (junto a un par más) son
consideradas las operaciones básicas de cualquier cifrador de bloques, algunas de las cuales
fueron conceptualmente propuestas por Shannon. Cada una de estas funciones debe tener cier-
tas propiedades. Por ejemplo, una buena caja-S debe ser tal que: a) al cambiar un bit de la entrada
se modifiquen cerca de la mitad de los bits de la salida, y b) cada bit de la salida depende de
todos los bits de la entrada.
Precisamente, la controversia desatada a raíz del involucramiento de la NSA en la definición
final de DES se dio debido a que esta introdujo modificaciones en las cajas-S. Sin embargo, al
contrario de lo que se pensó durante años, la NSA fortaleció las cajas-S de forma tal de que
fuesen resistentes al criptoanálisis diferencial (técnica de ataque descubierta por aquellos años).
Importantes criptógrafos, como Adi Shamir (ver Sección 4.2), demostraron que aun pequeñas
modificaciones en las cajas-S pueden debilitar considerablemente a DES.
Electronic Code Book (ECB). El texto legible se divide en bloques de 64 bits y cada bloque
se (des)encripta con la misma clave. El resultado es la concatenación de los bloques de
texto cifrado. Por lo tanto, de bloques legibles iguales se obtienen bloques cifrados iguales.
Cipher Block Chaining (CBC). El funcionamiento de CBC se grafica en la Figura 19. El
texto legible se divide en bloques de 64 bits. El primer bloque se suma (XOR) con un
vector de inicialización y el resultado se encripta con la clave provista; el segundo bloque
se suma (XOR) con el texto cifrado del primero y el resultado se encripta con la clave
provista, y así sucesivamente. De esta forma porciones iguales de texto legible no resultan
en porciones iguales de texto cifrado. Este es el modo por defecto en la mayoría de las
implementaciones de software disponibles.
¿Es necesario comunicar el vector de inicialización para más tarde poder desencriptar?
¿Hay alguna forma en que no sea necesario hacerlo?
Sin embargo, la criptografía simétrica en general es más eficiente que la asimétrica por lo que
se busca combinarlas para obtener lo mejor de ambas (ver Sección 4.4.3).
La idea original de la criptografía asimétrica fue publicada por primera vez en 1976 por los
criptógrafos norteamericanos Whitfield Diffie y Martin Hellman. De todas formas, más tarde
se supo que tres criptógrafos del servicio secreto británico, James H. Ellis, Clifford Cocks y
Malcolm J. Williamson, habían obtenido resultados similares unos años antes pero que debie-
ron permanecer en secreto. Por otro lado, los primeros en proponer públicamente un sistema
asimétrico práctico fueron tres criptógrafos norteamericanos, Ronald Rivest, Adi Shamir y Len
Adleman, quienes en 1978 inventaron lo que hoy se conoce como el algotirmo RSA.
La idea básica de la criptografía asimétrica es la siguiente. Sean A y B dos sujetos que quieren
comunicarse a través de un canal público. Supongamos que A quiere mandarle el mensaje t de
forma confidencial a B. Sea (ℰ, 𝒟) un cifrador asimétrico y sea (sB , pB ) el par de claves de B
correspondiente a dicho cifrador (sB es la clave secreta o privada y pB la pública). Entonces A
debe enviar a B el resultado de ℰ(t, pB ) en tanto que B lo debe descifrar calculando 𝒟(ℰ(t, pB ), sB )
(notar que se usan claves diferentes para cifrar y descifrar).
La fortaleza del sistema yace en que se conjetura que es computacionalmente muy costoso
(NP) obtener sB a partir de pB y t a partir de ℰ(t, pB ) y pB . Como podemos ver, el sistema (casi
[ver Sección 4.2.4]) resuelve el problema de la distribución de claves dado que B puede usar
un canal inseguro para enviarle pB a A, puesto que la seguridad del sistema no depende de las
claves públicas.
La matemática que utiliza la criptografía asimétrica es bastante diferente de la usada por
la simétrica. Los sistemas asimétricos se basan en el álgebra modular, la teoría de números
y resultados de complejidad computacional. Por esta razón repasaremos brevemente algunos
resultados en estas áreas. De ahora en más todos los números que utilicemos son enteros, a
menos que se indique otra cosa. Si a y b son dos números, ab indica su producto.
Decimos que b ≠ 0 divide a a sí y solo sí a = mb para algún natural m; es decir a es múltiplo
de b. Sean a, b y n tres números con n > 0. Decimos que a es congruente con b módulo n,
simbolizado a ≡ b(mod n), sí y solo sí n divide a a − b; o sea existe un natural k tal que a − b = kn,
que es lo mismo que decir que a − b es múltiplo de n. Sean p y q dos números primos y n = pq,
entonces definimos 𝜑(n) = (p − 1)(q − 1). El número a es primo relativo a (o coprimo de) b si el
único divisor en común de ambos es 1. A continuación enunciamos el siguiente teorema.
Teorema (de Euler). Sean p y q dos números primos y n = pq. Sea a primo relativo a n. Entonces:
a𝜑(n) ≡ 1(mod n)
4.2.1. RSA
A continuación veremos con cierto detalle el algoritmo RSA, que es uno de los más utilizados
en la actualidad para todo tipo de comunicaciones cifradas. RSA opera con varias longitudes
de claves pero actualmente se considera que es seguro a partir de los 2048 bits. La presentación
responde al artículo de Rivest, Shamir y Adleman [26], aunque no tenemos en cuenta cuestiones
prácticas que hacen a la seguridad del algoritmo (por ejemplo la longitud y otras caracterís-
ticas de los números primos usados para inicializar el algoritmo). En general, muchas de las
consideraciones que se verán a continuación aplican a otros sistemas asimétricos.
La inicialización del algoritmo es de la siguiente forma:
Sean p y q dos números primos
67
Sea n = pq
Sean e y d tales que ed ≡ 1(mod 𝜑(n))
La clave pública es (n, e)
La clave privada es d (aunque p, q y 𝜑(n) también deben permanecer secretos, ver más
abajo)
Sea M la representación binaria del mensaje a encriptar
Sean mi con i ∈ [1, k] tales que:
• M = m1 m2 . . . mk
• mi < n
• mi es primo relativo a n
Entonces el mensaje M se encripta cifrando cada mi por separado calculado el número ci tal
que:
ci ≡ mei (mod n)
siendo el texto cifrado la concatenación de los ci . Notar que para encriptar se usan e y n que
constituyen la clave pública del destinatario.
A su vez, el destinatario, cuya clave privada es d, desencripta cada ci calculando:
Veamos ahora cómo comprobar que el cálculo anterior efectivamente devuelve el mensaje
original.
Veamos el primer paso que falta justificar: ed = k𝜑(n) + 1. En efecto, recordar que por cons-
trucción ed ≡ 1(mod 𝜑(n)). Entonces existe k > 0 tal que ed − 1 = k𝜑(n), lo que implica el paso.
El paso donde se aplica el Teorema de Euler se justifica también por la siguiente propiedad
del álgebra modular:
Como dijimos más arriba la criptografía asimétrica casi resuelve el problema de la distri-
bución de claves. Claramente, A y B pueden enviarse mutuamente sus claves públicas a través
de canales inseguros. No obstante, ¿qué seguridad tiene A de que la clave que recibe es la de
B (y viceversa)? Si el canal es inseguro, podría haber un atacante, C, que captura la clave de B
y le envía la suya a A diciéndole que es la de B. Por lo tanto, cuando A encripta un mensaje
secreto para B lo hace, en realidad, con la clave pública de C lo que le permite a este conocer el
contenido del mensaje (y además reenviar otro a B). A este tipo de ataque se lo denomina ataque
de intermediario76.
Ejemplo 30 (Servidores de claves). En la práctica las claves públicas se pueden subir a sitios web (por
ejemplo http://pgp.mit.edu/) que no controlan la identidad de los usuarios.
Por otro lado este problema es menos severo que el de la distribución de claves en el caso
simétrico. Por ejemplo, dos personas que se conocen podrían intercambiar sus claves públicas
telefónicamente (aunque un atacante esté escuchando la conversación). En el caso simétrico el
atacante solo debe intervenir el canal; en el caso asimétrico debe además ser capaz de eliminar,
agregar y rerutear mensajes. En el caso asimétrico al atacante no le basta con comprometer
la integridad de la clave pública sino que debe cambiarla por una bajo su control, sin que
los usuarios legítimos lo noten. En un sentido podemos decir que se cambia un problema
de confidencialidad por otro de autenticidad. En otro sentido, se cambia el problema de la
distribución de claves por el problema de la autenticidad de claves.
iniciativas no comerciales como Let’s Encrypt. En estos casos los CD raíz se distribuyen con
los sistemas operativos de uso masivo o los navegadores más populares. Además los gobiernos
suelen tener sus propias ACs para CDs que se usan en actividades oficiales o legales. La forma
de distribución depende de cada caso. Finalmente, cualquier usuario u organización puede
utilizar software PKI (abiertos y gratuitos como OpenSSL, EJBCA, OpenCA, etc.; o comerciales)
para gestionar sus propios CDs. En este caso la distribución de los CD raíz debe hacerse de
alguna forma definida por el usuario.
La última primitiva criptográfica que veremos son las denominadas funciones hash criptográ-
ficas. La entrada de una función hash criptográfica se llama mensaje y la salida resumen de mensaje
o solo resumen78. Las funciones de este tipo se utilizan para verificar la integridad y autenticidad
de mensajes (ver Secciones 4.4.1 y 4.4.2). Una función hash criptográfica es una función hash que
además, idealmente, debe tener las siguientes propiedades:
Dados dos mensajes muy similares, sus resúmenes deben ser muy diferentes.
Obviamente la idea detrás de las últimas tres propiedades es que un atacante sea incapaz
de reemplazar un mensaje por otro con el mismo resumen.
Una forma de encontrar funciones hash criptográficas es buscando funciones de compresión
unidireccionales libres de colisiones79 (llamado diseño de Merkle-Damgård80). Las funciones de
compresión unidireccionales libres de colisiones son básicamente algoritmos de cifrado simé-
trico de bloque (por ejemplo DES en modo CBC). Esto es útil en contextos donde los recursos son
muy escasos y se debe proveer tanto un cifrador de bloques y una función hash criptográfica. Dos
de las funciones hash criptográficas más usadas y basadas en el diseño de Merkle-Damgård son
SHA-1 (diseñada por la NSA) y MD5 (diseñada por Rivest; aunque actualmente se la considera
insegura, sigue siendo popular).
4.4. Aplicaciones
En esta sección veremos algunas aplicaciones típicas de las primitivas criptográficas que
hemos estudiado más arriba.
78En este contexto un mensaje debe verse como cualquier texto que incluso no necesariamente es transmitido
entre dos sujetos.
79Wikipedia.org: One-way compression function
80Wikipedia.org: Merkle-Damgård construction
72
SHA(salt ⌢ password)
Mientras que en el caso de DES, Robert Morris y Ken Thompson en 1979 modificaron la función
E (ver Sección 4.1.1) de manera tal que recibe como parámetro el salt. Entonces simbólicamente
se calcula lo siguiente:
8
z }| {
DESe (salt, 00000000, password)
No guardar claves. Un corolario del principio de la criptografía es que la clave nunca se trans-
mite junto al texto cifrado, aunque se la ofusque de diversas formas. Esta característica sería
parte del sistema criptográfico y entonces se debe asumir que eventualmente será descubierta
haciendo que el sistema se vuelva inseguro. De todas formas, las implementaciones de herra-
mientas criptográficas destinadas a ser usadas por personas (por ejemplo GNU Privacy Guard)
usualmente guardan junto al texto cifrado la ‘contraseña encriptada’, más o menos como hace
UNIX.
Ejemplo 31 (GPG). GNU Privacy Guard (GPG) es un software libre para que los usuarios puedan
realizar diversas operaciones criptográficas83. Para encriptar con GPG el archivo apunte.tex con el cifrador
simétrico por defecto (AES-128, i.e. Rijndael con claves de 128 bits), hacemos:
$ gpg --symmetric apunte.tex
Introduzca contraseña: hola
Repita contraseña: hola
lo que genera el archivo apunte.tex.gpg.
Ahora intentemos desencriptarlo pero con una contraseña errónea:
$ gpg -d -o apunte.tex apunte.tex.gpg
gpg: datos cifrados AES
Introduzca contraseña: chau
gpg: cifrado con 1 contraseña
gpg: descifrado fallido: clave incorrecta
o sea que GPG de alguna forma sabe cuál fue la contraseña que se usó al momento de encriptar el archivo.
Como era de esperarse, GPG no guarda la contraseña sino que calcula un hash criptográfico de ella
y guarda este hash junto al texto cifrado (similar a lo que hace UNIX con las contraseñas). Cuando el
usuario quiere desencriptar el archivo, GPG pide la contraseña, vuelve a calcular el hash y lo compara
con el que está almacenado. Si son iguales desencripta el archivo; caso contrario muestra el error de más
arriba. ¿Por qué GPG hace esto?
Para proveer integridad de datos se puede utilizar criptografía simétrica o asimétrica. Cuan-
do se usa criptografía simétrica se calcula un código de autenticación del mensaje (MAC85). Un
MAC es similar a un resumen de mensaje solo que se usa un cifrador de bloques (en general en
modo CBC) y una clave de encriptación para generarlo. El modo CBC de DES con un vector de
inicialización solo de ceros es una forma de calcular MACs. En este caso el MAC de los datos es
el último bloque encriptado, el cual depende de todos los bloques anteriores. En consecuencia
un cambio en cualquier bit del texto legible modificará sensiblemente el MAC. Entonces el
remitente envía el mensaje formado por el texto legible y el MAC correspondiente. Cuando el
destinatario lo recibe, recalcula el MAC (sobre el texto legible que recibió) y lo compara con
el MAC recibido. Si coinciden, el texto no fue modificado en tránsito; caso contrario el texto
recibido no corresponde al enviado. Observar que el destinatario necesita la clave que fue usa-
da para calcular el MAC (se supone que es una clave previamente acordada, como siempre en
criptografía simétrica). Simbólicamente, si t son los datos a enviar y k es la clave de encriptación
para calcular el MAC, el mensaje que el remitente debe enviar es el siguiente86:
(t, MACDES (t, k))
donde MACDES (t, k) se calcula de la siguiente forma. Sean t1 , . . . , tn bloques de 64 bits87 tales que
t = t1 ⌢ · · · ⌢ tn y sean c1 , . . . , cn tales que:
c1 = DESCBC
e (t1 , k) [se usa 0 como vector de inicialización]
ci = DESCBC
e (ci−1 ⊕ ti , k), para 1 < i ≤ n
entonces
MACDES (t, k) = cn
Cuando se usa criptografía asimétrica se utilizan funciones hash criptográficas para calcular
el resumen de los datos a enviar. En este caso, el remitente calcula el hash de los datos, lo
encripta con la clave pública del destinatario y lo envía junto a los datos. El destinatario recalcula
el hash (sobre el texto legible que recibió), desencripta (con su clave privada) el resumen de
mensaje recibido y los compara. Simbólicamente, si t son los datos a enviar, (ℰ, 𝒟) es el cifrador
asimétrico, ℋ es la función hash criptográfica y (sB , pB ) es el par de claves del destinatario, el
mensaje que el remitente debe enviar es el siguiente:
(t, ℰ(ℋ (t), pB ))
en tanto, asumiendo que B recibe (t′ , h), la comprobación que debe hacer es la siguiente:
𝒟(h, sB ) = ℋ (t′)
¿Qué ventajas tiene un método sobre el otro? ¿Por qué el vector de inicialización del método
simétrico es 0? ¿Cómo se podría proveer autenticidad en lugar de integridad usando métodos
similares? ¿Cómo se podrían proveer ambas?
Otra posibilidad para proveer integridad de datos es usar la firma digital. ¿Cómo se sería
el proceso en este caso? ¿Cuáles serían los ataques posibles (y por qué en la práctica no son
efectivos)? ¿Se podría proveer integridad y autenticidad al mismo tiempo?
85‘message authentication code’, en inglés.
86Observar que los datos se envían legibles.
87Usamos bloques de 64 bits porque usamos DES para calcular el MAC. Para otros cifradores de bloque se deben
usar las longitudes de bloque correspondientes.
75
M2. B → A: pB
M3. A → B: ℰa (k, pB ) (B desencripta con 𝒟a ( , sB ))
M4. B → A: ok
M5. A → B: ℰs (secreto, k) (B desencripta con 𝒟s ( , k))
M6. . . . → . . . : . . .
M7. B → A: ℰs (otro secreto, k) (A desencripta con 𝒟s ( , k))
M8. . . . → . . . : . . .
donde cada línea describe un mensaje entre los dos sujetos mencionados. Este intercambio de
mensajes no pretende ser un protocolo criptográfico sino solamente mostrar la combinación
de criptografía asimétrica y simétrica para establecer y usar una clave simétrica de sesión. Un
protocolo criptográfico real debe contener más información para evitar diversos ataques (ver
Secciones 4.4.4 y 4.4.5 y 5).
Hechas estas aclaraciones y salvedades, explicamos el intercambio de mensajes. En los
dos primeros mensajes A y B intercambian sus claves públicas en texto legible puesto que es
información que no compromete la seguridad de la sesión. Luego, A genera una clave simétrica
aleatoria y se la envía a B encriptándola con su clave pública. De esta forma B es el único que
la puede desencriptar. A partir del quinto mensaje comienza la etapa semántica de la sesión
donde A y B intercambian sus mensajes secretos (des)encriptándolos con la clave k. Como k solo
76
la conocen ellos, los mensajes están seguros. De esta forma la criptografía asimétrica se usa solo
para negociar la clave (pocos y pequeños mensajes) y la simétrica se usa en la etapa semántica
(muchos y grandes mensajes).
usando fuerza bruta. Ahora supongamos que una implementación de NS de aquella época
usaba DES como cifrador. Entonces, C podía escuchar una ejecución del protocolo, capturar
algunos mensajes, romper la clave por fuerza bruta en dos semanas, y volverla a usar en una
nueva sesión del protocolo. NS no debería permitir usar claves viejas porque todas las claves
tienen un tiempo máximo de vida.
′
Entonces, el ataque es el siguiente. Supongamos que C tiene la clave Kab usada en una corrida
vieja de NS entre A y B. Entonces inicia la siguiente secuencia de mensajes:
M1. C → S: A, B, Na (C se hace pasar por A)
El resultado es que B cree estar hablando con A cuando en realidad lo hace con C. El problema
de fondo es que B no tiene forma de comprobar que la clave es vieja. Una de las propiedades de las
claves negociadas en protocolos criptográficos es que todos los participantes puedan determinar
que son frescas (es decir que tengan las propiedades de los frescos). Específicamente, a C no le
interesa M2 porque viene encriptado con la clave entre A y S pero él sabe que para seguir NS
debe tomar el certificado y reenviarlo a B. Justamente C tiene el certificado de la corrida vieja
cuya clave conoce, y entonces le reenvía este certificado a B. Como dijimos, el problema es que
B no puede comprobar que el certificado es viejo y cae en la trampa. En otras palabras, B debe
′
asumir o confiar que Kab es fresca, no lo puede comprobar. Esta conjetura no fue tenida en cuenta
por Needham y Schroeder cuando diseñaron el protocolo.
Soluciones. Existen al menos dos soluciones a este ataque. Kerberos incluye time-stamps en
algunos mensajes pero esto tiene la desventaja de requerir una forma global y precisa de medir
el tiempo en la red. En 1987 Needham y Schroeder [24] propusieron una modificación del
protocolo donde B puede corroborar que la clave generada por S es fresca:
M1. A → B: A
M2. B → A: {A, Nb }Kbs
M3. A → S: A, B, Na , {A, Nb }Kbs
M4. S → A: {Na , B, Kab , {Kab , A, Nb }Kbs }Kas
M5. A → B: {Kab , A, Nb }Kbs
M6. B → A: {Nb′ }Kab
M7. A → B: {Nb′ − 1}Kab
con la desventaja de requerir más mensajes, más datos (Nb y Nb′ ) y que B mantenga más estado.
Ahora en M5 B puede corroborar que Kab es fresca porque el certificado incluye el fresco que él
mismo generó al inicio de la corrida. Recordar lo que decíamos al inicio de la sección sobre las
dificultades que entraña requerir mínimos en el diseño de protocolos.
79
A partir del secreto maestro, C y S generan dos claves, Kcw y Ksw , que serán las que cliente
y servidor, respectivamente, usen para encriptar los datos salientes95 a partir del mensaje M6.
Estas claves se calculan de la siguiente forma:
1. Se genera el material de claves96 con una fórmula similar a la usada para Kms :
donde FHC puede ser MD5 o SHA. Notar que se usa toda la información contenida en los dos
primeros mensajes.
El contenido de los mensajes M4 y M5 se envía cada vez que se negocia un nuevo paquete
de cifradores y claves98. CF y SF se calculan con:
donde sender es una constante diferente para CF y SF. Notar que estos son los primeros mensajes
que se protegen con todos los secretos y algoritmos negociados hasta el momento. La protección
se genera al enviarse un hash criptográfico de la concatenación de todo lo que se ha generado
hasta el momento.
A partir del mensaje M6, cliente y servidor intercambian mensajes a nivel de aplicación.
Suponiendo que se usa un cifrador de bloques, cada mensaje debe contener un control de
integridad99 y debe ser encriptado como se indica a continuación. El control de integridad se
incluye para evitar ataques por repetición.
Los mensajes enviados por C incluyen un control de integridad, Fc , que se calcula con la
siguiente fórmula:
Luego, C encripta con Kcw y desencripta con Ksw (también llamada Kcr )100.
El mensaje M7 muestra un ejemplo de esta situación.
Los mensajes enviados por S incluyen un control de integridad, Fs , que se calcula con la
siguiente fórmula:
BAN se basa en dos nociones centrales: creencias y mensajes vistos. La noción de creencia se
relaciona con que un sujeto cree que cierto dato es verdadero. En un sentido los mensajes vistos
representan lo que el sujeto conoce. Los sujetos van desarrollado o aumentando sus creencias y
conocimientos a medida que se ejecuta el protocolo102. Ambos se modelan como proposiciones
de la lógica.
El lenguaje. El lenguaje distingue tres sorts: sujetos, claves y sentencias. Los mensajes de un
protocolo criptográfico se asocian con las sentencias. En BAN el único conector proposicional
es la conjunción que se simboliza con la coma. En lo que sigue, los símbolos P, Q y R denotan
sujetos; X e Y denotan sentencias; y K denota claves. El lenguaje de BAN consta de las siguientes
construcciones103:
cree
P =⇒ X: el sujeto P puede actuar como si X fuera verdadera.
ve
P =⇒ X: alguien le ha mandado a P un mensaje conteniendo X, quien puede leerlo
(posiblemente luego de desencriptarlo) y reenviarlo.
dijo
P =⇒ X: el sujeto P alguna vez mandó un mensaje conteniendo la sentencia X. El mensaje
puede haberlo enviado mucho antes o durante la corrida actual del protocolo.
tjs
P =⇒ X: el sujeto P tiene jurisdicción sobre X. P tiene autoridad sobre X y los otros sujetos
deben tenerle confianza sobre X. Por ejemplo se usa para establecer propiedades de los
servidores de claves sobre las claves que ellos generan.
#(X): la sentencia X es fresca104; es decir, nunca fue incluida en un mensaje antes de la
ejecución actual del protocolo. Usualmente esto es así para los frescos.
K
P ↔ Q: K es una clave compartida entre P y Q. Esta clave nunca será conocida por otros
sujetos salvo aquel que la haya generado.
K
↦ P: K es la clave pública de P; su clave privada es K−1 . La clave privada jamás será
→
conocida por otro sujeto distindo de P excepto aquel que la haya generado.
{X}K : tiene casi el mismo significado que le dimos en secciones anteriores. La única
diferencia es que esta construcción es una abreviación de {X}K from P con el fin de poder
saber quién originó el mensaje. De todas formas, cuando el contexto lo permite la parte
from se omite.
La semántica formal de estas construcciones pueden verla en el trabajo original de Burrows,
Abadi y Needham [4] pero no es obligatorio para este curso.
Reglas de inferencia. En BAN se distingue entre pasado, todo aquello que ocurrió antes de
la corrida actual del protocolo; y presente, que es lo que ocurre durante la corrida actual del
protocolo. Todos los mensajes enviados antes de la corrida actual están en el pasado y el diseño
del protocolo debe ser tal que estos nunca sean considerados como pertenecientes al presente.
102Estas nociones estarían lejanamente relacionadas con las lógicas de conocimiento y creencias que se utilizan en
varias áreas de las ciencias de la computación (ver por ejemplo [30]).
103En el original se usan otros símbolos y se incluyen dos construcciones que nosotros decidimos no mostrar.
104‘fresh’, en inglés.
83
Todas las creencias del presente son estables durante la corrida del protocolo; se asume que si
P dice X entonces también cree en X. Sin embargo, las creencias del pasado no necesariamente
lo son del presente. Se asume que los sujetos pueden detectar e ignorar los mensajes que ellos
mismos envían. Dicho esto es posible presentar las reglas de inferencia de BAN105.
Las primeras reglas de inferencia tratan sobre el significado de los mensajes. Dos de ellas
dan significado a los mensajes encriptados (la tercera no la veremos). Todas explican cómo
derivar creencias partiendo de mensajes. La primera de las dos reglas que veremos se encarga
de mensajes encriptados con claves compartidas:
cree K ve
P =⇒ Q ↔ P, P =⇒ {X}K
(R1)
cree dijo
P =⇒ Q =⇒ X
Es decir que si P cree que comparte la clave K con Q y ve el mensaje X encriptado con esa clave,
entonces cree que Q alguna vez dijo X. Para que la regla sea consistente es necesario que P y
Q no sean el mismo sujeto (es decir, que P se haya enviado el mensaje a sí mismo). Esto queda
implícito pues {X}K es en realidad {X}K from Q y entonces basta con pedir Q ≠ P.
La segunda regla sobre el significado de los mensajes es similar a la anterior solo que trata
sobre claves públicas:
cree K ve
P =⇒ ↦→ Q, P =⇒ {X}K−1
(R2)
cree dijo
P =⇒ Q =⇒ X
donde por simplicidad se asume que X es texto legible, es decir no contiene ninguna sentencia
de la forma {Y}K .
cree dijo
La regla anterior es la única que deduce =⇒ partiendo de =⇒. Esta regla formaliza una
práctica común en el diseño de protocolos criptográficos que consiste en tomar como válidos
los mensajes que contienen un fresco o algo que se deriva de él. Notar que los frescos, cuando
son emitidos como desafíos, no necesitan estar encriptados, aunque sí cuando son parte de la
respuesta.
tjs
Ahora veremos el significado del conector =⇒:
cree dijo
P =⇒ Q =⇒ (X, Y)
(R6)
cree dijo
P =⇒ Q =⇒ X
cree dijo cree dijo cree dijo
aunque no es cierto que de P =⇒ Q =⇒ X y P =⇒ Q =⇒ Y se deduzca que P =⇒ Q =⇒ (X, Y)
porque ambas premisas podrían no haberse dicho al mismo momento.
Algo similar vale para los frescos:
cree
P =⇒ #(X)
cree
(R7)
P =⇒ #(X, Y)
También se podrían agregar reglas que establecieran que si X es fresco entonces también lo es
{X}K .
Además tenemos reglas que determinan que si un sujeto ve una sentencia entonces puede
ver sus componentes siempre que tenga las claves apropiadas.
ve cree K ve
P =⇒ (X, Y) P =⇒ Q ↔ P, P =⇒ {X}K
ve ve
(R8)
P =⇒ X P =⇒ X
cree K ve cree K ve
P =⇒ ↦→ P, P =⇒ {X}K P =⇒ ↦→ Q, P =⇒ {X}K−1
ve ve
(R9)
P =⇒ X P =⇒ X
La primera regla de la segunda línea se basa en el hecho de que si P cree que su clave pública
es K entonces también cree que su clave privada es K−1 y por lo tanto puede desencriptar {X}K .
Recordar que las sentencias de la forma {X}K son en realidad {X}K from R y siempre se pide
P ≠ R.
Finalmente damos reglas que indican que las claves simétricas se pueden usar en ambas
‘direcciones’.
cree K cree cree K
P =⇒ R ↔ R′ P =⇒ Q =⇒ R ↔ R′
(R10)
cree K cree cree K
P =⇒ R′ ↔R P =⇒ Q =⇒ R′ ↔R
Metodología para analizar protocolos. Para analizar un protocolo criptográfico usando BAN
se siguen los siguientes pasos (todos los predicados que se mencionan son predicados BAN):
85
En general las hipótesis establecen cuáles claves compartidas existen, cuáles sujetos han
generado frescos, y cuáles sujetos tienen autoridad o jurisdicción sobre ciertas cuestiones.
En la mayoría de los casos estas hipótesis quedan más o menos determinadas por el
tipo de protocolo (de clave pública o privada, para negociar una clave, para autenticar a
algún sujeto, etc.). Sin embargo, en ciertos casos los diseñadores originales, posiblemente
sin darse cuenta, han efectuado otras suposiciones. En ocasiones estas suposiciones más
ocultas dan lugar a vulnerabilidades.
3. Se asocian predicados que valen antes y después de cada mensaje de la especificación del
protocolo. Aquí se aplica la idea clásica de la lógica de Hoare. En otras palabras se anota
el protocolo.
Más precisamente, un protocolo es una secuencia de sentencias send de la forma P → Q : X
con P ≠ Q. Entonces, anotar el protocolo consiste en asociar una secuencia de predicados
antes y después de cada sentencia del protocolo; los predicados antes de la primera
sentencia son los supuestos (paso 2) y los predicados después de la última sentencia
son las conclusiones (paso 4). Los predicados BAN son conjunciones de fórmulas de la
cree ve
forma P =⇒ X y P =⇒ X. En particular si el predicado Y vale antes de la sentencia (ya
ve
formalizada) P → Q : X, entonces la anotación {Y}P → Q : X{Y, Q =⇒ X} es correcta.
4. Se aplican las reglas de inferencia para determinar las creencias de los participantes al
finalizar la ejecución del protocolo.
el paso 4 que nos permitan tener confianza en que el protocolo es seguro? En otras palabras,
¿cómo sabemos que un protocolo criptográfico es seguro? ¿Qué propiedades deben verificar
estos protocolos?
Aunque estas propiedades son materia de debate existen algunas casi ineludibles u obvias.
En general el objetivo más buscado es compartir una clave simétrica; o sea, la conclusión del
paso 4 debería ser:
cree K cree K
A =⇒ A ↔ B, B =⇒ A ↔ B
para algún X, lo que dice que A cree que B existe en el presente (seguramente porque ha enviado
mensajes recientes).
En el caso de protocolos que usan criptografía asimétrica, el objetivo podría ser que A tenga
certeza de que K es la clave pública de B:
cree K
A =⇒ ↦→ B
Aplicación de BAN al protocolo NS Con todos estos elementos podemos aplicar BAN al
análisis del protocolo NS (ver Sección 4.4.4). Entonces, siguiendo la metodología de cuatro
pasos comentada más arriba, comenzamos por formalizar el protocolo en BAN:
Kab Kab Kab
M2. S → A: {Na , A ↔ B, #(A ↔ B), {A ↔ B}Kbs }Kas
Kab
M3. A → B: {A ↔ B}Kbs
Kab
M4. B → A: {Nb , A ↔ B}Kab from B
Kab
M5. A → B: {Nb , A ↔ B}Kab from A
El primer mensaje del protocolo no se formaliza porque, como dijimos más arriba, los
mensajes de texto legible no aportan a la seguridad. Luego, por ejemplo, vemos que se formaliza
el hecho de que Kab es una clave compartida entre A y B. Ciertamente decir que Kab es una clave
compartida entre A y B solo por el hecho de que su subíndice es ab no constituye una sentencia
Kab
formal. Por el contrario, la construcción A ↔ B es una sentencia con una semántica formal
Kab
(ver [4, sección 13]). Además se incluye el hecho de que Kab es una clave nueva (#(A ↔ B)).
Finalmente, la respuesta de A al desafío planteado por B (i.e. en M5) se simplifica a Nb puesto
que retornar Nb − 1 es solo a los efectos de evitar la repetición (ciega) del desafío. Esto se logra
Kab
agregando el modificador from que permite distinguir ambos mensajes. La inclusión de A ↔ B
en los dos últimos mensajes captura el hecho (implícito en la descripción informal) de que A y
B creen que la clave es solo para ellos.
87
cree Kas cree Kbs cree Kas cree Kbs cree Kab
A =⇒ A ↔ S, B =⇒ B ↔ S, S =⇒ A ↔ S, S =⇒ B ↔ S, S =⇒ A ↔ B,
(H1)
La mayoría de estas hipótesis son obvias dados el contexto y la definición del protocolo.
Notar que A cree que S tiene jurisdicción sobre la frescura de Kab . B no puede asumir lo mismo
porque no interactúa con S. Resaltamos la última hipótesis, (H34), porque sin ella no se puede
cree Kab
demostrar una de las propiedades básicas del protocolo (i.e. B =⇒ A ↔ B) lo que en definitiva
permite el ataque visto en la Sección 4.4.4. Es decir, los autores del protocolo no se dieron cuenta
que esta hipótesis era necesaria para garantizar la seguridad del protocolo y por lo tanto su
diseño no lo refleja.
Según los pasos 3 y 4 de la metodología debemos anotar cada mensaje de la formalización
del protocolo y aplicar reglas de inferencia para deducir propiedades. De esta forma vamos a
cree Kab
deducir dos propiedades: (a) A =⇒ #(A ↔ B); y (b) para demostrar que sin la hipótesis (H34)
cree Kab
no es posible deducir B =⇒ A ↔ B. Notar que, como vimos en el apartado anterior, (b) es una
de las propiedades típicas de los protocolos criptográficos.
Empezamos con (a). La formalización comienza con el mensaje M2 que pueden ver más
arriba. Antes del mensaje M2 valen todas las hipótesis sobre el protocolo que acabamos de
mencionar. Luego de M2 A ve el contenido del mensaje que le envía S y por lo tanto podemos
agregar el predicado:
del cual, considerando la primera hipótesis de (H1) y la regla (R1), podemos deducir:
del cual, considerando la primera hipótesis de (H3) y la regla (R7), podemos deducir:
cree Kab
A =⇒ #(Na , #(A ↔ B)) (8)
del cual, considerando la tercera hipótesis de (H2) y aplicando la regla (R4), podemos deducir
(a).
Ahora vamos a demostrar (b). Iniciamos como cuando demostramos (a), es decir anotamos
M3 con:
ve Kab
B =⇒ {A ↔ B}Kbs (12)
de cual, considerando la segunda hipótesis de (H1) y aplicando la regla (R1), podemos deducir:
del cual, considerando la segunda hipótesis de (H2) y aplicando la regla (R4), podemos deducir
(b).
cree Kab
Dejamos como ejercicio demostrar que A =⇒ A ↔ B, lo que en este punto debería resultarles
muy simple.
Claramente, aunque aquí hayamos procedido a mano con las demostraciones, BAN puede
ser codificada como una teoría en asistentes de prueba tales como Coq e Isabelle/HOL, lo que
permite automatizar grandes porciones de la mayoría de las pruebas (e incluso, en algunos
casos, automatizarlas completamente).
Referencias
[1] Aleph One. Smashing the stack for fun and profit. Phrack, 7(49), November 1996. http://www.
phrack.com/issues.html?issue=49&id=14.
89
[2] D. Elliot Bell and Leonard LaPadula. Secure computer systems: Mathematical foundations. MTR
2547, The MITRE Corporation, May 1973.
[3] D. Elliot Bell and Leonard LaPadula. Secure computer systems: Mathematical model. ESD-TR
73-278, The MITRE Corporation, November 1973.
[4] Michael Burrows, Martín Abadi, and Reger Needham. A logic of authentication. Technical Report
no. 39, Digital Systems Research Center, Digital Equipment Corporation, 1989. http://www.hpl.
hp.com/techreports/Compaq-DEC/SRC-RR-39.pdf.
[5] Michael Burrows, Martín Abadi, and Roger M. Needham. A logic of authentication. ACM Trans.
Comput. Syst., 8(1):18–36, 1990.
[6] Shawn A. Butler. Security attribute evaluation method: a cost-benefit approach. In Will Tracz,
Michal Young, and Jeff Magee, editors, Proceedings of the 24th International Conference on Software
Engineering, ICSE 2002, 19-25 May 2002, Orlando, Florida, USA, pages 232–240. ACM, 2002.
[7] Michael R. Clarkson and Fred B. Schneider. Hyperproperties. Journal of Computer Security,
18(6):1157–1210, 2010.
[8] Maximiliano Cristiá and Pablo Mata. Runtime enforcement of noninterference by duplicating
processes and their memories. In Workshop de Seguridad Informática WSEGI 2009, Mar del Plata,
Argentina, 2009. SADIO.
[9] Maximiliano Cristiá and Gianfranco Rossi. Automated proof of Bell-LaPadula security properties.
J. Autom. Reason., 65(4):463–478, 2021.
[10] Dorothy E. Denning. A lattice model of secure information flow. Commun. ACM, 19(5):236–243,
1976.
[11] Dominique Devriese and Frank Piessens. Noninterference through secure multi-execution. In 31st
IEEE Symposium on Security and Privacy, S&P 2010, 16-19 May 2010, Berleley/Oakland, California, USA,
pages 109–124. IEEE Computer Society, 2010.
[12] Sergio Ferrari. La Crypyo CIA desata un terremoto. El Cohete a la Luna, 2020. https://www.
elcohetealaluna.com/la-crypto-cia-desata-un-terremoto/.
[13] A. Freier, P. Karlton, and P. Kocher. The secure sockets layer (ssl) protocol version 3.0. RFC 6101,
RFC Editor, August 2011.
[14] Morrie Gasser. Building a Secure Computer System. Van Nostrand Reinhold Co., New York, NY, USA,
1988.
[15] Joseph A. Goguen and José Meseguer. Security policies and security models. In 1982 IEEE Symposium
on Security and Privacy, Oakland, CA, USA, April 26-28, 1982, pages 11–20. IEEE Computer Society,
1982.
[16] Lawrence A. Gordon and Martin P. Loeb. The economics of information security investment. ACM
Transactions on Information and System Security, 5(4):438–457, November 2002.
[17] Daryl McCullough. Specifications for multi-level security and a hook-up property. In Proceedings of
the 1987 IEEE Symposium on Security and Privacy, Oakland, California, USA, April 27-29, 1987, pages
161–166. IEEE Computer Society, 1987.
[18] Daryl McCullough. Noninterference and the composability of security properties. In IEEE Sympo-
sium on Security and Privacy, pages 177–186, Oakland, CA, April 1988.
[19] Greg Miller. The intelligence coup of the century. The Whashington Post,
2020. https://www.washingtonpost.com/graphics/2020/world/national-security/
cia-crypto-encryption-machines-espionage/.
[20] National Institute of Standards and Technology. FIPS PUB 81: DES modes of operation. Decem-
ber 1980. https://csrc.nist.gov/csrc/media/publications/fips/81/archive/1980-12-02/
documents/fips81.pdf.
90
[21] National Institute of Standards and Technology. FIPS PUB 46-3: Data Encryption Standard (DES).
October 1999. Supersedes FIPS 46-2, https://csrc.nist.gov/csrc/media/publications/fips/
46/3/archive/1999-10-25/documents/fips46-3.pdf.
[22] George C. Necula. Proof-carrying code. In Peter Lee, Fritz Henglein, and Neil D. Jones, editors,
Conference Record of POPL’97: The 24th ACM SIGPLAN-SIGACT Symposium on Principles of Program-
ming Languages, Papers Presented at the Symposium, Paris, France, 15-17 January 1997, pages 106–119.
ACM Press, 1997.
[23] George C. Necula and Peter Lee. Research on proof-carrying code for untrusted-code security.
In 1997 IEEE Symposium on Security and Privacy, May 4-7, 1997, Oakland, CA, USA, page 204. IEEE
Computer Society, 1997.
[24] Roger M. Needham and Michael D. Schroeder. Authentication revisited. Operating Systems Review,
21(1):7, 1987.
[25] OWASP. Sql injection. https://www.owasp.org/index.php/SQL_Injection.
[26] Ronald L. Rivest, Adi Shamir, and Leonard M. Adleman. A method for obtaining digital signatures
and public-key cryptosystems. Commun. ACM, 21(2):120–126, 1978.
[27] Chris Salter, O. Sami Saydjari, Bruce Schneier, and Jim Wallner. Toward a secure system engineering
methodology. In Proceedings of the New Security Paradigms Workshop, pages 2–10, Charlottsville, VA,
September 1998.
[28] Bruce Schneier. Secrets and Lies: digital security in a networked world. Wiley Computer Publishing,
2000.
[29] Ian Sutherland and et. al. Romulus: A computer security properties modeling enviroment. The
theory of security. Technical Report RL-TR-91-36 Vol IIa, Rome Laboratory, April 1991.
[30] Hans van Ditmarsch, Joseph Y. Halpern, Wiebe van der Hoek, and Barteld P. Kooi. An introduction
to logics of knowledge and belief. CoRR, abs/1503.00806, 2015.
[31] Guido Vassallo. Las Malvinas y el Plan Cóndor, en el centro de un escándalo mundial de espionaje, 2020.
[32] Dennis M. Volpano, Cynthia E. Irvine, and Geoffrey Smith. A sound type system for secure flow
analysis. Journal of Computer Security, 4(2/3):167–188, 1996.