Instituto de Tecnología de Massachusetts. Conferencia Curso # 6.858. "Seguridad de los sistemas informáticos". Nikolai Zeldovich, James Mickens. Año 2014
Computer Systems Security es un curso sobre el desarrollo e implementación de sistemas informáticos seguros. Las conferencias cubren modelos de amenazas, ataques que comprometen la seguridad y técnicas de seguridad basadas en trabajos científicos recientes. Los temas incluyen seguridad del sistema operativo (SO), características, gestión del flujo de información, seguridad del idioma, protocolos de red, seguridad de hardware y seguridad de aplicaciones web.
Lección 1: "Introducción: modelos de amenaza"
Parte 1 /
Parte 2 /
Parte 3Lección 2: "Control de ataques de hackers"
Parte 1 /
Parte 2 /
Parte 3 Entonces, tenemos un buffer sobre el cual colocamos el "canario". Por encima de eso está el puntero del punto de interrupción
guardado, el valor
EBP guardado y la dirección de retorno se coloca encima de él. Si recuerdas, el desbordamiento va de abajo hacia arriba, así que antes de llegar a la dirección del remitente, primero destruirá el "canario".
Audiencia: ¿por qué afectará al "canario"?
Profesor: porque se supone que el atacante no sabe "saltar" arbitrariamente en la memoria. Los ataques tradicionales de desbordamiento de memoria comienzan con un pirata informático que examina el límite de tamaño del búfer, después de lo cual el desbordamiento comienza desde el fondo. Pero tiene razón: si un atacante puede entrar directamente en la barra de dirección de retorno, ningún "canario" nos ayudará. Sin embargo, con un ataque tradicional de desbordamiento del búfer, todo debería suceder exactamente de esa manera, de abajo hacia arriba.
Por lo tanto, la idea principal de usar un "canario" es que permitimos que un exploit malicioso desborde el búfer de memoria. Tenemos un código de tiempo de ejecución que, al regresar de una función, verifica el "canario" para asegurarse de que tenga el valor correcto.
Audiencia: ¿Puede un atacante reescribir la dirección del remitente y cambiar el "canario"? ¿Cómo puede verificar que ha sido modificado, pero que continúa cumpliendo su función?
Profesor: sí, tal vez. Por lo tanto, debe tener algún código que realmente verifique esto antes de que la función regrese. Es decir, en este caso, es necesario contar con el apoyo de un compilador, que en realidad ampliará la
convención de llamadas . De modo que parte de la secuencia de retorno ocurre antes de considerar la validez de este valor para asegurarnos de que el "canario" no haya sido destruido. Solo después de eso podemos pensar en otra cosa.
Público: ¿No puede un atacante saber o adivinar qué significa "canario"?
Profesor: ¡ esto es exactamente de lo que voy a hablar! ¿Cuál es el problema con este circuito? ¿Qué pasa si, por ejemplo, ponemos el valor A en cada programa? ¿O toda una rama de 4 valores de A? Por supuesto, cualquier hacker puede averiguar el tamaño del búfer, su capacidad y así determinar la posición del "canario" en cualquier sistema. Por lo tanto, podemos usar diferentes tipos de cantidades que ponemos en nuestro "canario" para evitar esto.
Hay una cosa que puedes hacer con nuestro "canario". Será un tipo muy divertido de "canario", que utiliza funciones de programa en C y procesa caracteres especiales, el llamado tipo determinista de "canario".

Imagine que utiliza el carácter 0 para el "canario". El valor binario de cero es el byte cero, el carácter cero en ASCII. Un valor de -1 significa un retorno a la posición anterior y así sucesivamente. Muchas funciones detienen o cambian la operación cuando encuentran caracteres o valores como 0, CR, LF, -1. Imagine que usted, como hacker, utiliza alguna función de gestión de cadenas para subir el búfer, encuentra el carácter 0 en el "canario" y el proceso se detiene. Si utiliza la función "retorno de carro" -1, que a menudo se utiliza como un terminador de línea, el proceso también se detendrá. Entonces -1 es otro signo mágico.
Hay una cosa más que se puede usar en el "canario": estos son valores aleatorios que son difíciles de adivinar para el atacante. El poder del valor aleatorio se basa en lo difícil que es para un atacante adivinarlo. Por ejemplo, si un atacante se da cuenta de que solo hay 3 bits de entropía en su sistema, podrá usar un ataque de fuerza bruta. Por lo tanto, las posibilidades de usar números aleatorios para protegerse contra los ataques son bastante limitadas.
Público: generalmente sucede que leo de otro búfer y escribo lo que leo en este búfer de esta pila. En esta situación, parece que el valor aleatorio del "canario" es inútil, porque leo los datos de otro búfer y sé dónde está el "canario". Tengo otro búfer que controlo y que nunca verifico. Y en este búfer puedo poner mucho de lo que quiero poner. No necesito un "canario" aleatorio, porque puedo reescribirlo de manera segura. Así que no veo cómo funciona realmente: en el escenario que propusiste, cuando la función se detiene al leer datos del búfer.
Profesor: Entiendo su pregunta, quiere decir que usamos un "canario" determinista, pero no utilizamos una de las funciones de la biblioteca estándar que puede ser "engañada" por nuestros caracteres 0, CR, LF, -1. Entonces sí, en la situación que describió, no se necesita un "canario".
La idea es que puede llenar este búfer con bytes desde cualquier lugar, pero cualquier cosa que le permita adivinar estos valores u obtenerlos aleatoriamente provocará un error.
Audiencia: ¿Es posible usar algo como el número de segundos o milisegundos como números aleatorios y usarlos en un "canario"?
Profesor: Las llamadas de datos no contienen tantos accidentes como cree. Porque el programa tiene registros o una función a la que puede llamar para averiguar cuándo se descargó el programa y otras cosas similares. Pero, en general, tiene razón: en la práctica, si puede usar un dispositivo de hardware, generalmente de bajo nivel, con mejores tiempos de sistema, este tipo de enfoque puede funcionar.
Público: incluso si logramos ver los registros sobre el comienzo del desbordamiento del búfer, aún es importante a qué hora rechazamos la solicitud. Y si no podemos obtener el control sobre el tiempo que demora la solicitud de una computadora al servidor, es dudoso que se pueda adivinar de manera determinista el tiempo exacto.
Profesor: muy cierto, ya dije que el mal yace en los detalles, este es solo un caso. En otras palabras, si tiene alguna manera de, por ejemplo, determinar el tipo de canal de tiempo, puede encontrar que la cantidad de entropía, o el número de aleatoriedad, no llena una marca de tiempo completa, sino mucho menos. Por lo tanto, un atacante puede determinar la hora y los minutos cuando hizo esto, pero no un segundo.
Público: para el registro, ¿tratar de reducir su propia aleatoriedad es una mala idea?
Profesor: absolutamente cierto!
Público: es decir, generalmente solo tenemos que usar todo lo que nuestros sistemas admiten, ¿verdad?
Profesor: sí, eso es verdad. Esto es como la invención de nuestro propio criptosistema, que es otra cosa popular que nuestros graduados a veces quieren hacer. Pero no somos la NSA, no somos matemáticos, por lo que esto generalmente falla. Entonces tienes toda la razón al respecto.
Pero incluso si usa la aleatoriedad del sistema, aún puede obtener menos bits de entropía de lo esperado. Permíteme darte un ejemplo de aleatorización de fase de direcciones. Es sobre este principio que el enfoque de
canarios de pila funciona . Dado que estamos comprometidos con la seguridad informática, probablemente se esté preguntando en qué casos los "canarios" no pueden hacer frente a su tarea y si hay formas de fallar al "canario".
Una de esas formas es un ataque reescribiendo punteros de función. Porque si se golpea el puntero de la función, el "canario" no puede hacer nada.
Supongamos que tiene un código de la forma
int * ptr ... .. , el puntero iniciador, no importa cómo, entonces tiene el buffer
char buf [128] , la función
gets (buf) , y en la parte inferior un puntero al que se le asigna algún valor :
* ptr = 5 .
Observo que no intentamos atacar la dirección de retorno de la función que contiene este código. Como puede ver, cuando el búfer se desborda, la dirección del puntero ubicada arriba se dañará. Si un atacante puede dañar este puntero, puede asignar 5 a una de las direcciones que controla. ¿Pueden ver todos que el "canario" no ayudará aquí? Porque no atacamos el camino por el que regresa la función.
Público: ¿ se puede ubicar el puntero debajo del búfer?
Profesor: puede, pero el orden de las variables específicas depende de muchas cosas diferentes, de la forma en que el compilador organiza el contenido, del tamaño de la columna de hardware, etc. Pero tiene razón, si el desbordamiento del búfer sube y el puntero se encuentra debajo del búfer, entonces el desbordamiento no puede dañarlo.
Público: ¿por qué no puede asociar el "canario" con la función "canario", como lo hizo con la dirección del remitente?
Profesor: este es un momento interesante! Puedes hacer tales cosas. De hecho, puede intentar imaginar un compilador que siempre que tenga un puntero, siempre intente agregar un complemento para algunas cosas. Sin embargo, verificar todas estas cosas será demasiado costoso. Debido a que cada vez que desee utilizar cualquier puntero o llamar a cualquier función, debe tener un código que verifique si este "canario" es correcto. Básicamente, podrías hacer algo similar, pero ¿tiene sentido? Vemos que los "canarios" no ayudan en esta situación.
Y una cosa más que discutimos anteriormente es que si el atacante puede adivinar la aleatoriedad, entonces, en principio, los "canarios" aleatorios no funcionarán. Crear recursos de seguridad basados en la aleatoriedad es un tema separado y muy complejo, por lo que no entraremos en ello.
Público: ¿Entonces el canario contiene menos bits que una dirección de retorno? Porque de lo contrario, no podría recordar esta dirección y verificar si ha cambiado.
Profesor: a ver. Está hablando de este esquema cuando el "canario" se encuentra sobre el búfer, y quiere decir que el sistema no puede ser seguro si es imposible mirar la dirección de retorno y verificar si se ha cambiado.

Si y no Tenga en cuenta que si se produce un ataque de desbordamiento del búfer, se sobrescribirá todo lo anterior, por lo que esto puede causar problemas. Pero básicamente, si estas cosas fueran inmutables de alguna manera, entonces podrías hacer algo como esto. Pero el problema es que, en muchos casos, manipular la dirección del remitente es algo bastante complicado. Porque puedes imaginar que se puede llamar a una función especial desde diferentes lugares, y así sucesivamente. En este caso, corremos un poco más adelante, y si hay tiempo al final de la conferencia, volveremos a esto.
Estas son situaciones en las que un "canario" puede fallar. Hay otros lugares donde es posible fallar, por ejemplo, al atacar las funciones
malloc y
free . La función malloc asigna un bloque de memoria de cierto tamaño en bytes y devuelve un puntero al comienzo del bloque. El contenido del bloque de memoria asignado no se inicializa, permanece con valores indefinidos. Y la función
libre libera memoria previamente asignada dinámicamente.
Este es un ataque único al estilo de C. Veamos qué sucede aquí. Imagine que tiene dos punteros aquí, p y q, para los cuales usamos
malloc para asignar 1.024 bytes de memoria a cada uno de estos punteros. Supongamos que hacemos la función
strcpy para p de algún tipo de error de búfer que es controlado por un atacante. Aquí es donde ocurre el desbordamiento. Y luego ejecutamos el comando
free q y
free p . Este es un código bastante simple, ¿verdad?

Tenemos 2 punteros para los cuales asignamos memoria, usamos uno de ellos para una determinada función, se produce un desbordamiento del búfer y liberamos la memoria de ambos punteros.
Suponga que las líneas de memoria de p y q están ubicadas una al lado de la otra en el espacio de memoria. En este caso, pueden pasar cosas malas, ¿verdad? Porque la función
strcpy se usa para copiar el contenido de
str2 a
str1 .
Str2 debe ser un puntero a una cadena que termina en cero, y
strcpy devuelve un puntero a
str1 . Si las líneas
str1 y
str2 se superponen, entonces el comportamiento de la función
strcpy es indefinido.
Por lo tanto, la función
strycpy que procesa la memoria
p puede afectar al mismo tiempo la memoria asignada para
q . Y eso puede causar problemas.
Es posible que haya hecho algo así en su propio código sin darse cuenta cuando usó algún tipo de punteros extraños. Y todo parece funcionar, pero cuando necesita llamar a la función
gratuita , se produce una molestia. Y un atacante puede aprovecharlo, explicaré por qué sucede esto.
Imagine que dentro de la implementación de las funciones
gratuitas y
malloc , el bloque resaltado se ve así.
Supongamos que en la parte superior del bloque hay datos visibles de la aplicación, y debajo tenemos el tamaño de la variable. Este tamaño no es lo que la aplicación ve directamente, sino una especie de "contabilidad" realizada por
free o
malloc , para que sepa el tamaño del búfer de memoria asignado. Un bloque libre se encuentra al lado del bloque resaltado. Suponga que un bloque libre tiene algunos metadatos que se ven así: tenemos el tamaño del bloque arriba, hay espacio libre debajo, el puntero posterior y el puntero delantero debajo. Y en la parte inferior del bloque, el tamaño se muestra nuevamente.

¿Por qué tenemos 2 punteros aquí? Debido a que el sistema de asignación de memoria en este caso usa una lista doblemente vinculada para rastrear cómo los bloques libres están relacionados entre sí. Por lo tanto, cuando selecciona un bloque libre, lo excluye de esta lista doblemente vinculada. Y luego, cuando lo liberes, harás algo de aritmética para el puntero y pondrás estas cosas en orden. Después de eso, lo agrega a esta lista vinculada, ¿verdad?
Cada vez que escuche sobre la aritmética de punteros, debe pensar que este es su "canario". Porque habrá muchos problemas. Permítame recordarle que tuvimos desbordamiento de búfer
p . Si suponemos que
pyq están uno al lado del otro, o muy cerca en el espacio de la memoria, puede ocurrir que este desbordamiento del búfer pueda sobrescribir algunos datos de tamaño para el puntero asignado
q : esta es la parte inferior de nuestro bloque asignado. Si continúas siguiendo mi pensamiento desde el principio, entonces tu imaginación te dirá dónde empieza a salir todo mal. De hecho, en esencia, lo que finalmente sucede con estas operaciones es
q libre y
p libre : observan estos metadatos en el bloque seleccionado para hacer todas las manipulaciones necesarias con el puntero.

Es decir, en algún momento de la ejecución, las funciones
libres obtendrán un cierto puntero en función del valor de tamaño:
p = get.free.block (tamaño) , y el tamaño es lo que controla el atacante, porque realizó un desbordamiento de búfer, correctamente ?
Hizo un montón de cálculos aritméticos, observó la función de
retroceso y los punteros de este bloque y ahora va a hacer algo como actualizar los punteros de "retroceso" y "avance": estas son las dos líneas inferiores.

Pero en realidad esto no debería molestarte. Este es solo un ejemplo del código que tiene lugar en este caso. Pero el hecho es que, debido al tamaño reescrito por el hacker, ahora controla este puntero, que pasa a través de la función
libre . Y debido a esto, los dos estados aquí en la línea inferior son en realidad actualizaciones de puntero. Y dado que el atacante pudo controlar este
p , en realidad controla estos dos punteros. Es en este lugar donde puede ocurrir un ataque.
Por lo tanto, cuando se ejecuta
libremente y trata de hacer algo como combinar estos dos bloques, tiene una lista doblemente vinculada. Porque si tiene dos bloques que chocan entre sí y ambos son libres, desea combinarlos en un bloque grande.
Pero si controlamos el tamaño, significa que controlamos todo el proceso desde las cuatro líneas anteriores. Esto significa que si entendemos cómo funciona el desbordamiento, entonces podemos escribir datos en la memoria de la manera que elijamos. Como dije, tales cosas a menudo suceden con su propio código, si no es inteligente con un puntero. Cuando cometes un error doble gratis como
free q y
free p o algo más, tu función se bloquea. Porque te equivocaste con los metadatos que viven en cada uno de estos bloques seleccionados, y en algún momento este cálculo indicará algún tipo de valor "basura", después de lo cual estarás "muerto". Pero si eres un atacante, puedes elegir este valor y usarlo para tu ventaja.
Pasemos a otro enfoque para evitar ataques de desbordamiento de búfer. Este enfoque es verificar los límites. El propósito de la comprobación de límites es asegurarse de que cuando use un puntero específico, solo se refiera a lo que es un objeto de memoria. Y este puntero está dentro de los límites permitidos de este objeto de memoria. Esta es la idea principal de la verificación. — . , C, . , : , , ?
, – . 1024 , :
char [1024] ,
char *y = & [108].
? ? Es dificil de decir. , , . , , - .
- , , , . . , , , . , , . , , .
, ,
struct union . , . :
integer ,
struct ,
int .
,
union , . ,
integer ,
struct , .
, , - :
int p: & (u,s,k) , : u, s, k.

, , , , . , ,
union integer ,
struct . , , , . .
p' ,
p ,
p' , .

, , . , ,
union . , - -
union , , , . , , X. , , , , . , , . .
, . ,
p p' , . .
? Electric fencing – . , , , , .

, - , . , , . , . , , , .
- C C++, , , . - , , - . , . , «» — , , , . , , .
, guard page – ! , .
59:00
:
MIT « ». 2: « », 2La versión completa del curso está disponible
aquí .
, . ? ? ,
30% entry-level , : VPS (KVM) E5-2650 v4 (6 Cores) 10GB DDR4 240GB SSD 1Gbps $20 ? ( RAID1 RAID10, 24 40GB DDR4).
Dell R730xd 2 ? 2 Intel Dodeca-Core Xeon E5-2650v4 128GB DDR4 6x480GB SSD 1Gbps 100 $249 ! . c Dell R730xd 5-2650 v4 9000 ?