Vulnerabilidad TOCTOU — Time Of Check To Time Of Use


Bueno, arranquemos. Hoy vamos a ver una vulnerabilidad clásica en sistemas Unix/Linux que se llama TOCTOU, que viene de “Time Of Check To Time Of Use” — o sea, “Tiempo de Verificación vs Tiempo de Uso”. El nombre ya nos da una pista de qué se trata. Ver Diapositiva


¿Cuál es la idea central?

Imaginemos que un programa hace dos cosas en secuencia:

  1. Verifica si tenés permiso para hacer algo
  2. Hace esa cosa

El problema es que entre el paso 1 y el paso 2 pasa tiempo. Y en ese tiempo… un atacante puede cambiar el estado del sistema. El programa ya verificó, y ahora actúa sobre algo diferente a lo que verificó.

Es como si el portero de un boliche te mira el documento, te dice “sí, pasá”, y en el segundo que tardás en entrar… alguien cambia la puerta. Ya te dieron acceso, pero ahora entrás a otro lado.


Conceptos previos que necesitamos entender Permisos en Unix En Linux, cada archivo tiene permisos para tres actores: el dueño (user), el grupo, y otros. Cada uno puede tener permisos de lectura (r), escritura (w) y ejecución (x). Hay además permisos especiales, y el que nos importa hoy es el SUID (Set User ID). Cuando un ejecutable tiene el bit SUID activado y su dueño es root, cualquier usuario que lo ejecute lo corre con privilegios de root. Eso es muy poderoso… y muy peligroso si el programa tiene bugs. Links (enlaces) en Unix Hay dos tipos:

  • Hard link: otro nombre que apunta directamente al mismo inode (los datos reales del archivo)
  • Soft link / Symlink: un acceso directo que apunta a otro archivo por nombre

Los symlinks son los protagonistas del ataque de hoy.


El programa vulnerable

Miremos el código de la slide:

[ C ][Copiar]
int main(int argc, char *argv[]) {

    char *file = argv[1];      // recibe el nombre del archivo como argumento

    scanf("%50s", buffer);     // lee input del usuario

    // VERIFICACIÓN
    if (!access(file, W_OK)) {     // ← chequea si el usuario tiene permiso de escritura

        // DELAY (procesamiento)
        for(i = 0; i < 99999999; i++) { int a = i^2; }

        // USO
        fd = fopen(file, "a+");    // ← abre y escribe el archivo
        fwrite(buffer, ...);
        fclose(fd);

    } else {
        printf("Sin permisos\n");
    }

    return 0;
}

¿Ven el problema? Hay tres momentos separados:

  1. access() — verifica permisos
  2. El loop de delay — simula procesamiento (en la vida real sería trabajo real)
  3. fopen() — abre el archivo

La función access() chequea los permisos usando el UID real del usuario (o sea, el usuario común, no root). Pero fopen() usa el UID efectivo — que en un binario SUID es root. Ahí está la trampa.


El ataque paso a paso

El diagrama de la slide lo muestra muy bien. Hay dos “actores” corriendo en paralelo:


PROGRAMA (línea azul)          ATACANTE (línea roja)
─────────────────────────────────────────────────────▶ tiempo

[verifica access(f)]

        │  ← VENTANA DE ATAQUE →
        │                          [unlink(f)]        ← borra el archivo original
        │                          [symlink(f, /etc/passwd)]  ← crea symlink con el mismo nombre

[fopen(f)]   ← ahora "f" apunta a /etc/passwd !!
[fwrite()]   ← escribe en /etc/passwd como root

El ataque funciona así:

El atacante tiene un archivo miarchivo.txt que sí le pertenece → el access() lo aprueba ✅ Durante el delay, el atacante borra ese archivo y crea un symlink con el mismo nombre apuntando a un archivo protegido (como /etc/passwd, /etc/shadow, etc.) Cuando el programa ejecuta fopen(), sigue el symlink y abre el archivo protegido fwrite() escribe ahí con privilegios de root 💀


¿Por qué no siempre funciona a la primera?

La slide pregunta: ¿qué pasa si ejecutan el exploit múltiples veces seguidas? La respuesta es que el ataque es probabilístico — dependés del timing. Si el symlink no está listo justo en la ventana entre access() y fopen(), el ataque falla. Por eso el script de exploit se corre varias veces: para aumentar las probabilidades de ganar la “carrera” (de ahí que también se llame race condition o condición de carrera).


¿Cómo se mitiga?

Algunas formas de defenderse:

  • Usar open() con O_NOFOLLOW — no seguir symlinks al abrir archivos.
  • Usar descriptores de archivo en lugar de paths — una vez abierto el fd, trabajar solo con él openat() con flags seguros — operaciones atómicas sobre el filesystem.
  • Reducir el delay al mínimo indispensable.
  • No usar access() para tomar decisiones de seguridad — la manpage de Linux de hecho lo advierte explícitamente.
Ver documentación →

Resumen

 ────────────────────────────────────────────────────────────────────────────────────────
│  Concepto:        │                               Qué es?                              │
 ────────────────────────────────────────────────────────────────────────────────────────
│  TOCTOU:          │  Vulnerabilidad por ventana de tiempo entre verificar y actuar     │
 ────────────────────────────────────────────────────────────────────────────────────────
│  SUID:            │  Bit que hace que un ejecutable corra con los permisos de su dueño │
 ────────────────────────────────────────────────────────────────────────────────────────
│  Symlink:         │  Enlace simbólico que puede ser redirigido por el atacante         │
 ────────────────────────────────────────────────────────────────────────────────────────
│  Race condition:  │  Condición de carrera: el éxito depende del timing                 │
 ────────────────────────────────────────────────────────────────────────────────────────

En seguridad, una verificación y una acción nunca son atómicas si hay tiempo entre ellas. Un atacante paciente (o un script rápido) puede colarse en esa ventana.