La explicación que daba Pere Martra en su artículo Buffer Overflow para negados. I-Parte, es la más inteligible con las que me he topado. Naturalmente conocía desde hace mucho los problemas de desbordamiento de buffer (buffer overflow o buffer overrun). En los tiempos de DOS, cuando no había protección de memoria a nivel de sistema operativo, lo veíamos a menudo al programar en Turbo C.

Editabas tu programa, lo ejecutabas, aquella magnífica característica que ahora casi todos los entornos de desarrollo tienen, que automáticamente te hacía un make, y te lanzaba el ejecutable. Si había algún problema de overrun o underrun en tu programa, lo notabas porque el IDE acababa desestabilizándose, y tenías que acabar reiniciando MS-DOS (o DR-DOS o PC-DOS).

Como yo usaba conio.h y dirio.h, nunca llegué a plantearme lo débil que era en ese sentido gets(). Una función a la que se le pasa un buffer, y donde meterá todo lo que se haya leído por la entrada estándar (el teclado habitualmente). Es fácil pensar, que si declaro un buffer de 1024 bytes, la pila se corromperá si alguien introduce 1025 bytes por teclado. En realidad metiendo 1024 ya pasaría, pero ese es otro tema.

Es decir, cualquier programa que usase gets(), era susceptible a ese problema. Como yo usaba cgets(), que era más eficiente, y soportaba posicionamiento y colores, esto no ocurría. A cgets, se le pasaba como primer elemento del buffer, la longitud máxima a leer, y el devolvía como segundo elemento, la cantidad leída, y a partir de ahi los caracteres que se introdujeron. Si programabas bien, nunca ocurría un overflow de esta manera. Pero claro conio.h, no era una función estándar de C, sino una extensión, que si bien implementaban la mayoría de compiladores (Borland, Microsoft, Watcom, Symantec, …), no era tan portable.

Hemos tenido que llegar hasta C11 (ISO/IEC 9899:2011), el más reciente estándar de C, para que gets haya quedado obsoleta, reemplazada por su versión segura llamada gets_s(), y que Visual C++ soporta desde la versión 2005. En gets_s(), básicamente imitamos el funcionamiento de cgets, sólo que la longitud máxima a leer, se pasa como segundo argumento.

Como hay tanto código escrito que utiliza gets(), y que por tanto es vulnerable, muchos compiladores recientes, la marcan como obsoleta, y es el motivo por el que en Práctica fundamentos de la programación tuve que acudir a _CRT_SECURE_NO_DEPRECATE y a _CRT_SECURE_NO_WARNINGS. Si quieres asustarte un poco, mira esta búsqueda en searchcode.

Pero si tu compilador no soporta la nueva gets_s, el arreglo es muy sencillo, y se conoce desde hace años, porque fgets(), es también una función segura, al aceptar el máximo de caracteres que queremos leer. Pensarás que fgets es para ficheros (o flujos, o streams, como los llames), pero quizás recuerdes, que en C, el teclado (stdin), igual que la pantalla (stdout), son también un flujo, así que podemos hacer algo así:

fgets(acString, sizeof(acString), stdin);

Y si nos es más cómodo, aplicarlo mediante un macro de preprocesador:

#define gets_s(pacString, piSize) fgets((pacString), (piSize), stdin)

El mismo problema tienen otras funciones también de uso habitual tales como strcpy (strcpy_s), strncpy (strncpy_s), strcat (strcat_s), strncat (strncat_s), strlen (strlen_s), strtok (strtok_s), memcpy (memcpy_s), sprintf (sprintf_s), scanf (scanf_s), itoa (_itoa_s), itoa (_itoa_s) y otras más específicas como makepath (_makepath_s) y _splitpath (_splitpath_s).

Es curioso que aunque las funciones que ya tienen un límite de caracteres a procesar, como strncpy, o memcpy, tengan su “versión segura”. Por lo que dicen en Microsoft, es porque hay tanto código mal escrito usándolas, que es mejor migrarlo a las nuevas con _s.