Optimizaciones fatales

Los compiladores transforman el código que escribimos para hacerlo más eficiente, sin embargo, hay veces en las que estas optimizaciones hechas por el compilador pueden causar problemas de seguridad. Un claro ejemplo de esto es la eliminación de código muerto (dead code elimination), una optimización que tiene como objetivo eliminar trozos de código que no afectan a los resultados del programa, y por tanto son considerados irrelevantes. Sin embargo, estos trozos de código, que el compilador considera irrelevantes, pueden llegar a ocasionar grandes fallos de diseño en una aplicación si son eliminados. Concretamente, la eliminación de los almacenamientos muertos (dead stores) es un fallo de seguridad en muchas aplicaciones. Una sentencia se considera un dead store cuando en esa sentencia almacenamos un valor en que posteriormente no va a ser leído, un ejemplo trivial sería:

int a, b, c;
a = 10;
b = a + 1;
c = 0;
printf(“a + b = %d\n”, a + b);

En este trozo de código, claramente la sentencia c = 0 sobra, y la variable c en sí es irrelevante. Un buen compilador daría un warning de que c no se usa, e internamente no produciría el código máquina equivalente a c = 0. Por otro lado, el siguiente código tiene un dead store y sí es necesario, aunque el compilador piense igualmente que el dead store debería eliminarse:

char *password = malloc(tamaño_password);
// la aplicación lee la contraseña y hace alguna operación con ella...
memset(password, 0, tamaño_passsword);
free(password); 

En este segundo ejemplo, aunque el memset parece inútil, tiene unas consideraciones de seguridad importantes, ya que si no escribiéramos ceros en las direcciones de la heap en las que password está almacenada, en el caso de que se diera algún error de memoria explotable en la aplicación, se podría leakear la contraseña. Si el compilador no quitase el dead store y hubiese una vulnerabilidad, la contraseña estaría protegida.

Evitar que se eliminen los dead stores no es una tarea fácil, ya que las aplicaciones que se usan en producción se suelen compilar con las optimizaciones habilitadas, porque se necesita que el código generado sea el óptimo. Algunas optimizaciones pueden depender de que previamente se limpie el código muerto, o simplemente se quiere eliminar el código muerto siempre, salvo en contadas excepciones.

Este problema aún sigue estudiándose, y no existe una solución multiplataforma universal para evitar que los dead stores se eliminen en casos concretos. Esto ha llevado a que los desarrolladores usen funciones específicas de su sistema operativo diseñadas tieniendo en cuenta estos problemas u ofusquen las operaciones de dead store para que el compilador no las entienda y no pueda optimizarlas (eliminarlas).

Irene Díez
Acerca de
Investigadora de DT
Expertise: Operating systems, program analysis