
Detecta cargas de memoria inventadas por el compilador que convierten C seguro en vulnerabilidades TOCTOU. Incluye auditorías automatizadas del código fuente, análisis binario basado en Unicorn y barridos de compilador/arquitectura/banderas en más de 100 proyectos.
"...la definición de 'compilador sensato' se vuelve cada vez más flexible."
El binario que ejecutas no es el programa que escribiste. El optimizador del compilador reescribe
tu código fuente de maneras que nunca ves — y algunos de esos cambios pueden
convertir silenciosa y legalmente código aparentemente seguro en
binarios vulnerables. La misma línea puede ser
segura con un compilador y explotable con otro,
sin nada en el código fuente que te indique cuál: una vulnerabilidad mantenida en
superposición, que solo se colapsa cuando compilas. Schrödinger's TOCTOU explora
cargas inventadas por el compilador y sus amplias
implicaciones para las vulnerabilidades de tiempo de verificación a tiempo de uso (TOCTOU) — encontradas
en
núcleos,
hipervisores,
enclaves,
firmware,
y bibliotecas de código abierto.
En todas partes, el código aparentemente seguro queda expuesto a los caprichos del
compilador. Pero esos son una muestra, no un límite; los
mismos errores están muy probablemente también en tu código.
Empieza con algo fácil.
¿Cuántas veces carga esta función *p?```c
unsigned int g(unsigned short *p)
{
short t = p; / copy *p into a local for safekeeping */
return (unsigned short)t - t;
}
Pista: la respuesta es 1 — el código fuente carga `*p` una sola vez en `t`.
Pégalo en [Compiler Explorer](https://godbolt.org/z/c5K9P4dPd) (`arm gcc 14.2.0`,
`-O2`) y cuenta las cargas desde `r0`, que contiene `p`:```asm
g:
ldrh r2, [r0] # load *p, once
ldrsh r0, [r0] # load *p, twice
subs r0, r2, r0
bx lr
Una carga en el código fuente, dos en el binario. La segunda es una carga inventada — una lectura que el compilador fabricó y que tú nunca escribiste. Es legal bajo la máquina abstracta de C, que asume que la memoria no puede cambiar entre dos lecturas. Pero cuando esa memoria puede ser escrita por un atacante, la suposición se convierte en una explotación: la carga inventada puede caer después de una comprobación de seguridad, reabriendo silenciosamente una ventana de tiempo de comprobación a tiempo de uso (TOCTOU) que el programador creía haber cerrado. El valor que validaste y el valor que usas ya no se garantiza que sean el mismo — aunque nunca escribiste código que lo volviera a leer.
El desafío demuestra que la carga inventada existe; veamos cómo eso se convierte en corrupción de memoria.
En una vulnerabilidad TOCTOU, un programa comprueba que un valor es seguro, y luego usa el valor. Sin embargo, existe una ventana de explotación si un atacante puede cambiar el valor en la fracción de tiempo entre esas dos lecturas – el valor inofensivo pasa la comprobación mientras que el peligroso es el que se usa:```c if (shared->len <= 20) // CHECK reads shared->len // ** attacker modifies shared->len ** memcpy(out, shared->data, shared->len); // USE reads it again: buffer overflow
La solución clásica es **crear una instantánea primero**: copiar cualquier dato que el atacante podría manipular a un lugar local al que el atacante no pueda acceder, y luego no confiar en nada más que en ese local. Una vez que `len` está en una variable local queda congelado — un atacante que compita por la memoria compartida ya no puede tocarlo — por lo que la comprobación y la copia están garantizadas de ver el mismo valor. Así es como el código en `receive` a continuación corrige el TOCTOU: crea una instantánea del mensaje, valida la instantánea y publica la copia validada en `slot` para que un consumidor la reenvíe:```c
#include <string.h>
struct message {
int len; /* payload length */
char data[20]; /* payload */
};
struct message slot; /* the most recently validated message */
char out[20]; /* fixed 20-byte destination */
void receive(struct message *shared) {
struct message local = *shared; /* 1. snapshot untrusted input */
if (local.len <= 20) /* 2. validate the snapshot */
slot = local; /* 3. publish the validated copy */
}
void forward(void) { /* the time of use, later */
memcpy(out, slot.data, slot.len); /* slot.len was checked <= 20 ... right? */
}
Según la fuente, esto es correcto. len se lee exactamente una vez — dentro de la instantánea —
por lo que el valor que supera la comprobación <= 20 es el valor publicado en slot.
La ventana TOCTOU queda cerrada y el código es seguro.
Excepto que no lo es. Bajo x86-64 gcc con -O2, receive lo lee de la
memoria compartida original dos veces: una
vez como escalar para la comprobación, y de nuevo como parte de la copia
masiva que se publica en slot:```nasm
receive:
cmp DWORD PTR [rdi], 20 ; READ #1: the CHECK reads shared->len directly
movdqu xmm0, XMMWORD PTR [rdi] ; READ #2: the bulk copy re-reads it (len is byte 0)
mov rax, QWORD PTR [rdi+16] ; (the bulk copy's tail: struct bytes 16-23)
jg .L1 ; len > 20? skip the publish
mov QWORD PTR slot[rip+16], rax ; (publish that tail)
movaps XMMWORD PTR slot[rip], xmm0 ; and publish the TOCTOU-vulnerable snapshot
.L1:
ret
forward:
movsx rdx, DWORD PTR slot[rip] ; copy size = slot.len, the unchecked READ #2 value
mov esi, OFFSET FLAT:slot+4 ; src = slot.data
mov edi, OFFSET FLAT:out ; dst = out[20]
jmp memcpy ; copies slot.len bytes into out[20]
El control se aplica a la LECTURA #1; el valor que llega a `slot.len` es la LECTURA #2. Un atacante que altera `len` entre ambas pasa un valor seguro al control `<= 20` mientras que uno sobredimensionado se publica en `slot` — y `forward` copia entonces esa cantidad de bytes en `out[20]`, el desbordamiento exacto que la instantánea pretendía evitar, reintroducido por el optimizador.