---
marp: true
theme: default
paginate: true
backgroundColor: #fff
backgroundImage: url('https://marp.app/assets/hero-background.svg')
style: |
section {
font-size: 28px;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: flex-start;
padding-top: 50px;
}
img[alt~="align-right"] {
position: absolute;
margin-top:0px
}
img[alt~="align-center"] {
position: absolute;
left: 50%;
transform: translateX(-50%);
}
/* Right-align terminal commands */
.terminal-commands {
text-align: right;
margin-left: 400px;
}
/* Make sub-bullets lighter and smaller */
ul ul li, ol ol li {
color: #666666;
font-size: 0.9em;
}
/* Make nested sub-bullets even lighter, italic, and smaller */
ul ul ul li, ol ol ol li {
color: #666666;
font-style: italic;
font-size: 0.8em;
}
/* Alternative: Use opacity for a more subtle effect */
ul ul, ol ol {
opacity: 0.8;
}
---
# Algunos Recordatorios de C
* int a
* &a
* int *a = malloc(sizeof(int))
* *a
---
# Algunos Recordatorios de C (cont.)
```c
void modifyValue(int x) {
x = 100;
}
int main() {
int x = 50;
printf("Before modification: %d\n", x); // Output: 50
modifyValue(x);
printf("After modification: %d\n", x); // Output: ?
return 0;
}
```
---
# Algunos Recordatorios de C (cont.)
```c
void modifyValue(int *ptr_to_x) {
// Dereference the pointer to access
// and modify the original value
*ptr_to_x = 100;
}
int main() {
int x = 50;
printf("Before modification: %d\n", x); // Output: 50
modifyValue(&x); // Pass the address of x
printf("After modification: %d\n", x); // Output: 100
return 0;
}
```
---
# Manejo de Memoria
```c
void modifyValue(int *ptr_to_x)
{
// Dereference the pointer to access
// and modify the original value
*ptr_to_x = 100;
printValue(*ptr_to_x);
}
void printValue(int x)
{
int anotherNumber = 75;
printf("In Function: %d\n", x);
printf("In Function: %d\n", anotherNumber);
}
int main()
{
int *x = malloc(sizeof(*x));
*x = 50;
printf("Before modification: %d\n", *x); // Output: 50
modifyValue(x); // Pass the address of x
printf("After modification: %d\n", *x); // Output: 100
return 0;
}
```

---
# Defendiendo Buffer Overflows
* Problema básico: código C con errores que escribe más allá del final del búfer/array.
* Los buffer overflows son una ruta de ataque popular, vale la pena entenderlos. Incluso ahora.
* 5 buffer overflows de alto perfil en 2019 (ej., whatsapp):
* https://securityboulevard.com/2019/11/5-buffer-overflow-vulnerabilities-in-popular-apps/
* https://blog.zimperium.com/whatsapp-buffer-overflow-vulnerability-under-the-scope/
* Ejemplo de evolución de defensa/ataque a lo largo del tiempo.
* Ha sido muy valioso elevar la barra.
* Aunque las defensas aún no son perfectas.
---
# Ejemplo: buffer overflows
* Una clase importante de problemas de seguridad, para los cuales se conocen muchos ataques y defensas.
* Este es el tema del Laboratorio 1.
* Supongamos que nuestro servidor web tiene un error en el parsing de entrada HTTP.
* En ciertas entradas, se cae.
* ¿Deberíamos preocuparnos?
* Echemos un vistazo a un ejemplo simplificado.
---
# Ejemplo: buffer overflows
```c
#include
#include
char *
gets(char *buf) {
int c;
while((c = getchar()) != EOF && c != '\n')
*buf++ = c;
*buf = '\0';
return buf;
}
int
read_req(void) {
char buf[128];
int i;
gets(buf);
i = atoi(buf);
return i;
}
int
main() {
int x = read_req();
printf("x = %d\n", x);
}
```
% gcc -g -static -fno-stack-protector -fcf-protection=none readreq.c -o readreq
% ./readreq
1234
% ./readreq
AAAAAAAAAAAA....AAAA
---
# Ejemplo: buffer overflows (cont.)
* ¿Por qué se cayó?
* Deberíamos pensar "este es un error; ¿podría un atacante explotarlo?"
* Vamos a averiguar qué está pasando exactamente.
* Necesita conocer detalles de x86-64.
```
% gdb ./readreq
b read_req
r
disas $rip
info reg
```
---
# Ejemplo: buffer overflows (cont.)
* Dibujemos una imagen de lo que está en el stack.
```
+------------------+
| main()'s frame |
| |
| |
+------------------+
| return address |
+------------------+
%rbp ------> | saved %rbp |
+------------------+
| i |
+------------------+
| ... |
+------------------+
| buf[127] |
| ... |
%rsp ------> | buf[0] |
+------------------+
```
---
# Ejemplo: buffer overflows (cont.)
* ¿Dónde está buf[]?
```
print &buf[0]
print $rsp
print &i
```
* Ajá, buf[] está en el stack, seguido por i.
* El sub $0x90, %rsp asigna espacio para buf[] e i.
---
# Ejemplo: buffer overflows (cont.)
* Dibujemos una imagen de lo que está en el stack.
```
+------------------+
| main()'s frame |
| |
| |
+------------------+
| return address |
+------------------+
%rbp ------> | saved %rbp |
+------------------+
| i |
+------------------+
| ... |
+------------------+
| buf[127] |
| ... |
%rsp ------> | buf[0] |
+------------------+
```
---
# Ejemplo: buffer overflows (cont.)
* OK, de vuelta a read_req, justo antes de gets()
```
disas $rip
next
AAAAAAA...AAA (190 veces)
```
* ¿Qué hizo gets() al stack?
```
print &buf[0]
```
* Hmm, 190 es más que 128!
* ¿Cómo puede ser?
```
x/g $rbp
x/g $rbp+8
```
---
# Ejemplo: buffer overflows (cont.)
* ¡El frame pointer guardado y return eip son 0x41414141!
* ¿Qué es 0x41?
```
next
disas
stepi
stepi
disas
```
* Ahora a punto de ejecutar la instrucción de retorno de read_req().
```
x/g $rsp
note rip será 0x41414141
stepi -- crash, este es nuestro seg fault
```
---
# Ejemplo: buffer overflows (cont.)
* ¿Es este un problema serio?
* Es decir, si nuestro código del servidor web tuviera este error, ¿podría un atacante explotarlo para entrar en nuestra computadora?
* ¿Está el atacante limitado a saltar a algún lugar aleatorio?
* No: ataque de "inyección de código".
* ¿Cómo sabe el adversario la dirección del búfer?
* Simular ataque:
```
handle SIGSEGV nopass
set{long}$rsp=.
```
---
# Ejemplo: buffer overflows (cont.)
* ¿Qué puede hacer el adversario una vez que están ejecutando código inyectado?
* Si el proceso está ejecutándose como root o Administrator, puede hacer cualquier cosa.
* Incluso si no, aún puede enviar spam, leer archivos (servidor web, base de datos), ..
* Puede cargar un programa más grande desde algún lugar de la red.
* ¿Qué pasa si el stack crece hacia arriba, en lugar de hacia abajo?
* El frame del stack para read_req() tiene buf[] en la dirección más alta, así que no se desbordará sobre el return %rip de read_req().
* ¿Puede un atacante aún explotar este error?
---
# Resumen de la situación
* La solución ideal es usar un lenguaje que haga cumplir los límites, ej. Python o Java.
* Es un gran esfuerzo re-entrenar programadores y re-escribir software, pero no imposible (ej. Microsoft y C#).
* Pero C se usa para muchas aplicaciones y bibliotecas valiosas, así que no podemos abandonarlo, y a menudo no podemos evitar escribir nuevo código C.
* La definición de C hace difícil o imposible verificar límites automáticamente con precisión.
* El programador perfecto verificaría límites 100% del tiempo, pero resulta que ningún programador es perfecto.
* Así que necesitamos defensas que hagan los desbordamientos de búfer más difíciles de explotar, para programas C grandes y con errores que no entendemos.
---
# Empecemos con buffer overflows de stack "clásicos"
* Los ataques tienen dos partes:
1) escribir algunas instrucciones en el búfer del stack.
2) sobrescribir el PC de retorno para apuntar a las instrucciones del atacante.
* Las instrucciones del atacante pueden hacer cualquier cosa que la aplicación pueda hacer.
* Por lo tanto es un ataque poderoso.
---
# ¿Cómo defenderse contra buffer overflows?
* Usar un lenguaje que verifique automáticamente los límites de array.
* Para C:
* No llamar gets().
* Intel permite marcar el stack como no ejecutable.
* ¿Es esta una solución 100% para C?
* Aleatorizar layout, canaries, etc.
* Estructurar la aplicación para limitar el daño de errores (Laboratorio 2).
* Buenas noticias: buffer overflows simples como este ya no funcionan.
* Lecciones de buffer overflows:
* Los errores son un problema en todas las partes del código, no solo en el mecanismo de seguridad.
* La política puede ser irrelevante si la implementación tiene errores.
* Pero manténganse atentos; hay esperanza para la defensa.
---
# Idea de defensa: O/S le dice al hardware que no ejecute en el stack
* Ej. O/S establece el bit NX (No eXecute) de Intel en cada página del stack.
* Esto previene la ejecución de instrucciones inyectadas en el stack.
* P: ¿NX significa que no tenemos que preocuparnos por buffer overflows?
* No
* P: ¿NX es una pérdida de tiempo si no es perfecto?
* Tampoco
---
# Idea de defensa: stack canaries (ej., StackGuard, Stack Smashing Protector de gcc)
* Detecta modificación del PC de retorno en el stack *antes* de que sea usado por RET.
* El compilador genera código que empuja un valor "canario" en el stack al
entrar a la función, hace pop y verifica el valor antes del retorno.
* El canario se sienta entre variables y dirección de retorno, ej.:
```
| |
+------------------+
entry %esp ----> | return address | ^
+------------------+ |
new %ebp ------> | saved %rbp | |
+------------------+ |
| CANARY | | Overflow goes
+------------------+ | this way.
| buf[127] | |
| ... | |
| buf[0] | |
+------------------+
| |
```
* P: ¿qué valor deberíamos usar para el canario?
* R: tal vez un número aleatorio, elegido al inicio del programa, almacenado en algún lugar.
---
# Idea de defensa: aleatorización del layout del espacio de direcciones (ASLR)
* Colocar memoria del proceso en direcciones aleatorias.
* El adversario no conoce la dirección precisa del stack, código, heap de la aplicación, ...
* Requiere soporte del compilador para hacer todas las secciones reubicables.
---
# ¿Ya terminamos?
* ¿Qué tipos de ataques podrían funcionar a pesar de ASLR?
* ¿Qué tipos de ataques podrían funcionar a pesar de los stack canaries?
* Tal vez el atacante puede escribir o leer el valor secreto aleatorio de alguna manera.
* Sobrescribir puntero de función en el stack antes del canary.
* Sobrescribir alguna otra variable crucial en el stack, ej.
```
bool ok = ...;
char buf[128];
gets(buf);
if(ok){
...
}
```
* Desbordamiento de una variable global a la siguiente (muy parecido al stack).
* Desbordamiento de búfer asignado en heap.
---
# ¿Son explotables los buffer overflows asignados en heap?
* Importante porque el código moderno tiende a usar heap mucho.
```
foo(){
char *p = malloc(16);
gets(p);
}
```
* ¿Puede el atacante predecir qué está después de p en memoria?
[diagrama de bloques malloc libres/usados]
---
# Resulta que ¡hay algunos ataques de heap bastante poderosos!
* Aquí hay una versión simplificada de un ataque real.
* Algunos malloc()s organizan bloques de memoria libre/usada así en lista doblemente enlazada:
```
^
| data
|-- next
prev --|
|-> ---- |
| data |
|-- next |
prev --|-|
|-> ---- <-| |
| data |
|-- next |
prev <---
```
---
# Ataques de heap (cont.)
* Malloc mantiene bloques libres en una lista doblemente enlazada.
* En orden de dirección, para poder fusionar bloques pequeños adyacentes en uno grande.
* Cuando se asigna un bloque libre, aquí está parte de lo que hace malloc():
* b = elegir un bloque libre
* b->next->prev = b->prev;
* b->prev->next = b->next;
* Si el atacante desborda un bloque malloc()ed, el atacante puede
* modificar los punteros next y prev en el siguiente bloque.
---
# Ataques de heap (cont.)
* Así: supongamos que el atacante escribe x y al inicio del siguiente bloque.
* Llamar siguiente bloque b, entonces b->prev = x, b->next = y.
* Supongamos que b es libre y resulta ser elegido por el siguiente malloc():
* malloc() efectivamente ejecutará
* *y = x
* ¡Así escribiendo un valor elegido por el atacante a cualquier ubicación de memoria!
---
# Ataques de heap (cont.)
* Si el atacante puede adivinar la dirección del PC de retorno guardado,
* y puede adivinar la dirección del búfer siendo desbordado,
* puede cargar instrucciones en el búfer y causar
* que PC apunte a instrucciones inyectadas.
* Similarmente para *cualquier* puntero de función con dirección predecible.
* P: ¿cómo podría el atacante predecir tales direcciones?
* Los ataques reales tienen que ser más complejos; busque en la web
* Smashing the Heap for Fun and Profit, o Vudo Malloc Tricks.
---
# Las piezas que el atacante debe ensamblar
* Encontrar un error de buffer overflows en la aplicación o librería.
* Encontrar una forma de hacer que el programa ejecute el código con errores
* de una manera que cause que los bytes del atacante produzcan un buffer overflow.
* Entender la implementación de malloc().
* Encontrar un puntero de código y adivinar su dirección.
* Adivinar la dirección del búfer, es decir, las instrucciones inyectadas del atacante.
---
# Estos ataques requieren esfuerzo y habilidad del atacante
* El atacante debe entender casos extremos en la lógica de la aplicación, malloc(), salida del compilador.
* Malas noticias para el defensor: pocos programadores de aplicaciones piensan en casos extremos.
* Buenas noticias para el defensor: múltiples piezas frágiles en el rompecabezas del atacante.
* Un punto de alto nivel: si hay un error de buffer overflow, un atacante lo suficientemente inteligente probablemente puede explotarlo.
* Más generalmente, muchos errores que parecen inofensivos pueden ser convertidos en ventaja del atacante, tal vez en combinación con otras fallas.
---
# Afortunadamente, como muestra el paper, ¡uno puede adaptar verificación automática
* de límites de array a programas C existentes!
* Si estamos dispuestos a modificar el compilador, recompilar aplicaciones,
* y tal vez modificar las aplicaciones.
---
# Enfoque de verificación de límites #1: Fat pointers
* Directo aunque no muy práctico.
* Cada puntero contiene inicio y fin del objeto de memoria original,
* así como el valor actual del puntero:
```
+--------------+------------+-------------+
| 32-bit start | 32-bit end | 32-bit curr |
+--------------+------------+-------------+
```
* Modificar compilador para generar código que:
* Establece p.start y p.end en malloc() y p = &x.
* Durante la desreferencia, verifica start <= curr < end,
* panic si está fuera.
* Durante la asignación de puntero, copia todo el puntero gordo.
---
# Punteros gordos (cont.)
* Así:
```c
p = malloc(4); // malloc llena start y end.
*p = 1; // start <= curr < end, así que la verificación tiene éxito.
q = p; // copia puntero gordo a q.
q = q + 8; // suma 8 a q.curr.
*q = 1; // verificación falla, ya que curr >= end.
```
* Pero:
```c
* q = q - 6;
* *q = 1; // verificación tiene éxito.
```
* La disposición start/end recuerda cuál era el objeto original.
* Mucho código C requiere esto, ej. puntero a uno más allá del final.
---
# Punteros gordos (cont.)
* Problema #1: Las verificaciones pueden ser costosas.
* Problema #2: No muy compatible.
* No puedes pasar un puntero gordo a una biblioteca no modificada.
* Cambia el tamaño de las estructuras de datos.
* Las actualizaciones a punteros gordos no son atómicas, porque abarcan
* múltiples palabras. Las aplicaciones con hilos y interrupciones/señales
* asíncronas pueden fallar debido a estas actualizaciones no atómicas.
---
# Enfoque de verificación de límites #2: Mantener información de límites en una tabla separada
* Para que no tengamos que cambiar la representación del puntero.
* Baggy Bounds (y otros) hacen esto.
* Idea básica: Para cada objeto asignado, almacenar inicio y tamaño del objeto.
* malloc() o código generado por el compilador mantiene la tabla.
* ej. tabla[p] = ???
* Establecer un bit OOB (Out Of Bounds) en el puntero si sale de su objeto original.
---
# Enfoque de verificación de límites #2: Mantener información de límites en una tabla separada
* Para cada puntero, el compilador genera código para dos tipos de operaciones
* (El compilador "interpone" en ellas)
* Aritmética de punteros: char *q = p + 256;
* Debe detectar si el puntero ya no apunta al objeto original, y establecer el bit OOB.
* comparar q con tabla[p]
* Desreferencia de punteros: char ch = *q;
* Debe hacer pánico si el bit OOB de q está establecido.
---
# Desafíos de Baggy Bounds
* Desafío: necesitamos indexar tabla[] con tanto p como ej. p + 10,
* es decir, si p apunta al medio del objeto en lugar del inicio.
* Desafío: si la aritmética modifica p para apuntar a un objeto diferente,
* tenemos que saber NO mirar tabla[p] en usos futuros de p.
* podemos usar el bit OOB en el puntero para esto.
* Desafío: si la aritmética modifica p para volver a su objeto original,
* debemos detectar eso y limpiar OOB, ya que el puntero ahora es válido de nuevo.
* en particular, ¿cómo saber cuál era el objeto original?
* Desafío: prevenir que la tabla sea enorme, o costosa de acceder.
---
# ¿Por qué rastrear OOB? Porque puede pasar en programas C legales; por ejemplo:
```c
size_t array_size = ...;
int *arr = malloc(array_size * sizeof(*arr));
int *end = arr + array_size; // end es un puntero fuera de
// límites. si se desreferencia,
// baggy lanzará un error
for (int *i = end - 1; i >= arr; i--) {
// loop hacia atrás sobre el array
}
```
---
# Baggy Bounds usa el truco de potencia de dos para optimizar la tabla de límites
* La entrada de la tabla mantiene solo el tamaño, como log base 2, para caber en un byte: tabla más pequeña.
* 2^tabla[i] produce tamaño, así que ej. 20 significa 1 megabyte.
* Asignar solo tamaños de potencia de dos en límites de potencia de dos.
* El código compilado puede calcular el inicio de un objeto desde el puntero y la entrada tabla[].
* Solo limpiar los bits bajos log2(tamaño).
* Optimización: cada entrada de tabla cubre slot_size=16 bytes, así que la tabla no es enorme.
* Objeto de 64 bytes en dirección x usa 4 entradas de tabla empezando en tabla[x/16].
---
# Baggy Bounds usa truco de memoria virtual para manejar punteros fuera de límites
* Establecer el bit más significativo en un puntero OOB.
* Marcar páginas en la mitad superior del espacio de direcciones como inaccesibles.
* ¡El compilador no tiene que insertar instrucciones de verificación para desreferencias!
* Marca OOB significa "ptr está dentro de slot_size/2 del objeto original".
* El código de aritmética puede reconstruir cuál era el objeto original.
* Limpiar OOB si ptr se mueve de vuelta al objeto original.
* Pánico si la aritmética trata de mover OOB más de slot_size/2 lejos.
---
# Dado objeto p, aquí está cómo encontrar tamaño e inicio del objeto
```c
p apunta, para slot_size=16:
tamaño = 1 << tabla[p >> 4]; // 4 es el número de bits en 16.
inicio = p & ~(tamaño - 1); // limpia los bits bajos log2(tamaño).
```
---
# Ejemplo:
```c
p = malloc(25); // redondea hacia arriba a 32, establece dos entradas tabla[].
q = p + 10; // aritmética; lee tabla[], ve OK.
r = p + 35; // aritmética; no OK; establece OOB.
*r = 1; // sistema VM fuerza crash.
s = p + 35; // establece OOB.
s = s - 20; // aritmética limpia OOB.
*s = 1; // OK
t = p + 100; // aritmética fuerza crash: no dentro de slot_size/2.
```
---
# Así que:
* Baggy Bounds puede ser aplicado a programas C grandes y con errores existentes, y automáticamente detiene el programa si un atacante trata de explotar un buffer overflow. ¡Esto es una gran victoria!
* ¿Puede Baggy Bounds alguna vez hacer pánico cuando el código es realmente legal?
* Considerar código C que convierte punteros a enteros, y luego los compara.
* Puede romperse debido al bit OOB.
---
# ¿Qué errores de buffer overflows podría Baggy Bounds no detectar?
* Buffer overflow de array dentro de una struct malloc()'d más grande.
* Convertir puntero a int, modificar, convertir de vuelta a puntero.
* La aplicación podría implementar su propio asignador.
* Punteros colgantes a memoria re-asignada.
---
# Baggy Bounds, y buffer overflows en general, ilustran un patrón general:
* Los defensores habitualmente cometen un tipo específico de error.
* Los atacantes encuentran una forma de explotar ese error.
* Los defensores construyen una herramienta para detectar/arreglar el error.
---
# Referencias
* Atacar ciegamente un servidor de red
* https://css.csail.mit.edu/6.858/2014/readings/brop.pdf
* Vudo Malloc Tricks
* http://phrack.org/issues/57/8.html
* Smashing the heap for fun and profit
* http://doc.bughunter.net/buffer-overflow/heap-corruption.html
* Mucha investigación en esquemas de seguimiento: ej.,
* https://www.comp.nus.edu.sg/~gregory/papers/cc16lowfatptrs.pdf