Sunteți pe pagina 1din 100

Tema 3

Codificación segura
Tema 3: Codificación segura

La posibilidad de desarrollar software no confiable es atribuible a todas las partes


involucradas en el ciclo de vida de su desarrollo y en concreto los desarrolladores, que
escriben el código, desempeñan un papel vital en el desarrollo de software seguro libre
de vulnerabilidades. En este tema se van a explicar los defectos más comunes que se
pueden cometer al codificar en lenguajes como C y Java. Si se entienden bien los defectos
que se pueden cometer se estará en disposición de poder analizar el código para
encontrarlos y posteriormente corregirlos. La formación en esta materia de los
programadores es muy importante, pues si se conoce qué tipo de defectos de seguridad
se pueden cometer en un lenguaje de programación, se pueden evitar muchos de ellos
desde el principio. Además se hacen una serie de recomendaciones a tener en cuenta a la
hora de la implementación, tanto de buenas como de malas prácticas.

Javier Bermejo Higuera

3.1. Introducción a la codificación segura

Las principales características que diferencian a un desarrollador que programa teniendo


en cuenta reglas de codificación segura de uno que no, son el conocimiento,
intención y precaución. Un profesional del software que se preocupa por la seguridad
y actúa en conciencia reconoce que las vulnerabilidades y debilidades del software se
pueden originar en cualquier punto del ciclo de desarrollo del software, como la
aplicación de requisitos inadecuados, malas decisiones de diseño e implementación y la
inclusión de involuntarios errores de codificación o de configuración.

El profesional con conocimiento de seguridad de software, es consciente de que una de


las maneras de evitar problemas de seguridad en las aplicaciones es, aparte de requisitos
adecuados y uso de principios de diseño seguro, el seguimiento por los
programadores de prácticas de codificación seguras, que eviten la codificación
errores y debilidades explotables que deriven en vulnerabilidades explotables por los
diferentes tipos de agentes maliciosos.

La responsabilidad de desarrollar software no confiable es atribuible a todas las


partes involucradas en el ciclo de vida de desarrollo y en concreto a los
desarrolladores, que escriben el código, desempeñan un papel vital en el desarrollo de
aplicaciones seguras libres de vulnerabilidades.

© Universidad Internacional de La Rioja (UNIR)


Tema 3: Codificación segura

En sitio Web CVE Detail1, se muestra un gráfico con los tipos de vulnerabilidades más
comunes registradas, desde el año 1999 hasta el momento en que se visualiza la web, y la
proporción que cada uno de ellos representa sobre el total de vulnerabilidades
registradas. La mayoría de ellas son debidas a errores de codificación por no seguir
prácticas adecuadas, ni tener los conocimientos pertinentes.

Figura 1. Vulnerabilidades más comunes por tipo desde el año 1999 hasta el 2017.

Cabe destacar que gran parte de las vulnerabilidades como, denegación de servicio,
inyección de código, cross-site scripting (explotados en aplicaciones Web), inyección de
código y SQL (explotados en todo tipo de aplicaciones), están presentes
mayoritariamente en el gráfico, aunque otro muy común como la gestión de la memoria
sigue presente desde hace muchos años. Otro tipo de vulnerabilidad que es cada vez más
popular, es la escalada de privilegios.

En este tema se van a explicar los defectos más comunes que se pueden cometer al
codificar en lenguajes como C y Java. Si se entienden bien los defectos que se pueden
cometer se estará en disposición de poder analizar el código para encontrarlos y
posteriormente corregirlos. La formación por tanto en esta materia de los
programadores es muy importante, porque si se conoce que tipo de defectos
de seguridad se pueden cometer en un lenguaje de programación se pueden
evitar muchos de ellos desde el principio. Además se hacen una serie de
recomendaciones a tener en cuenta a la hora de la implementación, tanto de buenas
como de malas prácticas.

1
http://www.cvedetails.com/vulnerabilities-by-types.php

© Universidad Internacional de La Rioja (UNIR)


Tema 3: Codificación segura

El encontrar bugs tempranamente conlleva una reducción del coste económico, en varios
órdenes, con respecto a su descubrimiento en fases posteriores. Este hecho tiene que
incitar a las organizaciones a poner especial interés en esta actividad y encuadrarla en
los procesos y procedimientos de desarrollo.

De todas formas inevitablemente siempre se dejarán defectos en el código una


vez terminada la fase de desarrollo, es por ello por lo que todavía se puede hacer más
por eliminar la mayor parte de defectos posible realizando un análisis de código
fuente durante la codificación y al final de la misma.

3.2. Características de una buena implementación, prácticas y


defectos a evitar

Para iniciarse en la implementación segura de código, es conveniente empezar con


una serie de recomendaciones generales, a tener en cuenta, como línea a seguir en la
profundización y conocimiento de esta disciplina. En el libro Secure Coding: Principles
& Practices [3] se presenta una serie de recomendaciones que caen en las siguientes
categorías:

Buenas prácticas:
o Formarse uno mismo.
o Formación continúa.
o Manejo de los datos con precaución:
– Comprobar los datos de entrada al sistema.
– Comprobar los límites de las estructuras de datos.
– Comprobar los ficheros de configuración.
– Comprobar los parámetros de los comandos codificados en programas.
– No confiar en URLs.
– Tener cuidado con el contenido WEB.
– Comprobar las variables de entorno.
– Comprobar cookies.
– Inicializar las variables correctamente.
– Identificar las referencias a ficheros correctamente y utilizarlas correctamente.
– Poner especial atención en el almacenamiento de información de carácter
confidencial.

© Universidad Internacional de La Rioja (UNIR)


Tema 3: Codificación segura

o Rehusar código que no ha sido testeado y probado.


o Insistir en el proceso de revisión de código:
– Efectuar revisiones por parejas de programadores.
– Usar herramientas de seguridad disponibles.
o Usar listas de comprobación.
o Revisar el proceso de mantenimiento:
– Usar estándares.
– Eliminar código obsoleto.
– Probar todos los cambios de código.

Malas prácticas:
o Usar en el código nombres relativos de ficheros.
o Referirse a un fichero con el mismo nombre dos veces en el mismo programa.
o Invocar programas en los que no se confía desde otros en los que se confía.
o Asumir que los usuarios no son maliciosos.
o No utilizar librerías seguras que generan números aleatorios como ramdom.
o Invocar Shell desde líneas de comandos.
o Utilizar direcciones IP, MAC o direcciones de correo para identificar usuarios.
o Utilizar zonas de memoria accesibles por todos los usuarios.
o Almacenar información sensible en una base de datos sin protección.
o Visualizar passwords en las pantallas de los usuarios.
o Codificar passwords en la aplicación.
o Almacenar passwords en disco sin cifrar.
o Transmitir passwords sin cifrar.
o Confiar solamente en los mecanismos de protección de ficheros del sistema
operativo, se debe implementar mecanismos adicionales en la aplicación.
o Tomar decisiones de acceso basadas en variables de entorno o parámetros de línea
de comando pasados en tiempo de ejecución.
o Almacenar la aplicación en un sistema de ficheros NFS.
o Confiar en software de terceros en operaciones críticas.

Estos pueden ser solo algunos de los aspectos más generales a tener en cuenta a la hora
de desarrollar y como se verá más adelante, sobre todo a la hora de hablar de defectos de
implementación de código, hay que tener en cuenta los defectos particulares que se
pueden cometer con el lenguaje de programación que se está utilizando, el compilador
que se utiliza, y otro software de terceros que seguramente no se tendrá posibilidad de

© Universidad Internacional de La Rioja (UNIR)


Tema 3: Codificación segura

verificar, pero que formará parte del sistema, y por tanto podría introducir errores en el
sistema que se está implementando.

Defectos de implementación. Los defectos de implementación que se pueden


cometer se pueden cometer caen dentro de las siguientes categorías:

Manejo de la entrada de datos

Desbordamiento de Buffer

Errores y excepciones

Privacidad y confidencialidad

Programas privilegiados

Errores aplicaciones web

Figura 2. Defectos de implementación.

Los errores en aplicaciones web serán desarrollados en la asignatura de «Seguridad de


Aplicaciones Online».

3.3. Manejo de la entrada de datos

Introducción

Una de las medidas de defensa más importantes que un programador puede llevar a
cabo, es validar todas las entradas que el sistema recibe. Son la fuente de
algunas de las peores vulnerabilidades de las aplicaciones, como son el
desbordamiento de buffer, inyección de SQL y otras.

Los atacantes pueden manipular los diferentes tipos de datos de entrada con el
fin de entregar cargas maliciosas a las aplicaciones. Los tipos de entrada siempre deben
ser probados por un sistema de validación de entrada, para evitar este tipo de
ataques.

© Universidad Internacional de La Rioja (UNIR)


Tema 3: Codificación segura

El programador es el individuo más cualificado para definir las clases de entrada que son
válidas en el contexto de su código. No puede ser responsable de saber si todas la
entradas que se reciben son correctas, sin embargo, si lo es de asegurar que la
entrada que se acepta no es obviamente incorrecta. No hay que esperar que la
entrada sea formateada correctamente, hay que tener sentido común, ser coherente,
seguir convenciones de codificación y estándares.

Validación de la entrada: conjunto de actividades que aceptan la entrada sin confiar


en ella y comprueban la validez de la misma, limitándola solo a los valores que se sabe
son aceptables.

Este apartado se presenta varias formas de validar la entrada de una aplicación y


los diferentes modos de verificar que su estrategia ha sido puesta en práctica
correctamente. A lo largo del apartado, se estudian una multitud de problemas de
seguridad que resultaron de la validación inadecuada de entradas y dos aspectos de la
validación de entrada: las clases de entrada que requieren validación y las clases de
operaciones que dependen de la entrada validada.

Qué validar
o Validar toda la entrada. Validar cualquier entrada que utilice el programa.
Hacer fácil verificar que toda la entrada sea validada antes de que sea usada.
o Validar la entrada de todas las fuentes. Incluyendo parámetros de línea de
comandos, archivos de configuración, consultas de base de datos, variables de
entorno, servicios de red, valores de registro, propiedades de sistema, archivos
temporales, y cualquier otra fuente externa de entrada.
o Establecer fronteras de confianza. Almacenar datos en los que se confía
separadamente de los que no se confía, para asegurar que la validación siempre se
realiza.

Cómo validar
o Usar una fuerte validación de entrada. Usar la forma más fuerte de
validación de entrada aplicable en un contexto dado.
o Evitar poner en la lista negra. No echar mano de la lista negra solamente
porque una validación más fuerte de la entrada es difícil de realizar.
o No confundir usabilidad y seguridad.
o No confundir la validación de funcionalidad con la validación de
entrada para la seguridad.

© Universidad Internacional de La Rioja (UNIR)


Tema 3: Codificación segura

o Rechazar datos maliciosos. Desechar datos que no pasan aquellas


comprobaciones de validación. No adecuarlos para su uso posterior.
o Hacer una buena validación de entrada por defecto. Usar una capa de
abstracción alrededor de operaciones importantes o peligrosas para asegurar que
las comprobaciones de seguridad siempre se realizan y qué condiciones peligrosas
no pueden ocurrir.
o Siempre comprobar la longitud de entrada. Validar la entrada contra un
mínimo y un máximo de la longitud que se espera.
o Limitar la entrada numérica. Comprobar la entrada numérica tanto contra un
valor máximo como contra un valor mínimo como parte de validación de entrada.
Tener cuidado con las operaciones que podrían ser capaces de llevar un número
más allá de su valor máximo o mínimo.

La mayoría de los programas aceptan entradas de múltiples fuentes y operan con los
datos que aceptan de muchas maneras. La validación de entrada juega un papel crítico
en la seguridad. Un programa primero lee datos de una fuente exterior y después los usa
en cualquier contexto relevante de seguridad.

Validación de toda la entrada

Estas tres palabras serán repetidas durante todo el apartado: validar toda la entrada.
Hay que definir la entrada detalladamente y pensar más allá de los datos que un usuario
puede enviar. Si una aplicación consiste en más de un proceso, validar la entrada de
cada uno, incluso si aquella entrada llega de otra parte de la aplicación.
Validar la entrada incluso si llega por una conexión segura, llega de una fuente confiable
o está protegida según permisos de archivo estrictos.

Validar la entrada incluso para los programas que son accedidos por usuarios validados
o en los que se confía. No hacer suposiciones optimistas sobre ninguna parte de la
entrada heredada del entorno, incluyendo valores de registro y nombres de ruta de
ficheros. Cada comprobación que se realiza niega a su adversario una oportunidad y
proporciona un grado añadido de aseguramiento.

Las rutinas de validación de entrada se pueden dividir en dos grupos principales:


Comprobaciones de sintaxis que prueban el formato de la entrada y a menudo
puede ser desacoplada de la lógica de aplicación y aplicada cerca del punto donde los
datos entran en el programa.

© Universidad Internacional de La Rioja (UNIR)


Tema 3: Codificación segura

Comprobaciones semánticas que determinan si la entrada es apropiada,


considerando la lógica de la aplicación y la función. Comúnmente tienen que aparecer
junto a la lógica de aplicación porque las dos están estrechamente relacionadas.

Validación de todas las fuentes de entrada

No hay que permitir que la seguridad de su software dependa del gran intelecto, la
perspicacia profunda, o la buena voluntad de las personas que lo configuran, desplieguan
y mantienen. Realizar la validación de entrada no solo sobre la entrada de
usuario, sino también sobre datos de cualquier fuente externa al código del
sistema que se está desarrollado. Esta lista debería incluir, pero sin limitarse a ella,
lo siguiente:
Parámetros de línea de comando.
Archivos de configuración.
Datos recuperados de una base de datos.
Subida de archivos.
Variables de entorno.
Importaciones de archivos planos.
Servicios de red.
Valores de registro.
Propiedades de sistema.
Archivos temporales.
Las variables de entorno.
Localizadores de recursos universales (direcciones URL) e identificadores (URI).
Referencias de otros nombre de archivo.
Encabezados Hyper Text Transfer Protocol (HTTP).
Parámetros HTTP GET.
Campos de formulario (especialmente los ocultos).
Las listas de selección, listas desplegables.
Cookies.
Comunicaciones Java applets.

Con frecuencia los desarrolladores realizan suposiciones del tipo: «No espero que nadie
me ataque en aquellos puntos. ¿Por qué debería gastar tiempo y esfuerzo para hacerlos
seguros?» Esta actitud conduce exactamente a los puntos ciegos que hacen que
el trabajo de un atacante sea fácil. Un programador miope podría pensar, «Si alguien
puede cambiar una propiedad del sistema, ellos ya habrán ganado». Entonces un

© Universidad Internacional de La Rioja (UNIR)


Tema 3: Codificación segura

atacante encontrará un modo de hacer una pequeña alteración a un archivo de sistema,


script, o podría encontrar un camino a un error de configuración.
En el uno o el otro caso, la carencia de validación de entrada significa una pasarela para
los atacantes en el camino a un compromiso del sistema. No todas las formas de entrada
son iguales. La entrada de un archivo de configuración casi seguramente recibirá
tratamiento diferente que la entrada de un usuario. Pero independientemente de la
fuente, toda la entrada debería ser sujeta a la validación de al menos la
consistencia y la sintaxis.

A continuación se va a ver un ejemplo de estos tipos de errores o ausencia de validación


en la entrada que conduce una vulnerabilidad conocida como inyección de SQL:
La Versión 2.1.9, Hiberne [1], un paquete popular open source, contiene un ejemplo
excelente de qué no hacer con la entrada de línea de comando. La versión Java
de la herramienta SchemaExport de Hibernate acepta un parámetro de línea de comando
llamado " -- delimiter ", que se utilizaba para separar los comandos SQL en
los scripts que generaba. El código se muestra en la figura 3 de una forma
simplificada:
String delimiter;
for (int i=0; i < args.length; i++) {
if ( args[i].startsWith("--delimiter=") ) {
delimiter = args[i].substring(12);
}
}
...
for (int i = 0; i < dropSQL.length; i++) {
try {
String formatted = dropSQL[i];
if (delimiter!=null) formatted += delimiter;
...
fileOutput.write( formatted + "\n" );
}
Figura 3. Ejemplo de no validación de la entrada que puede ocasionar una vulnerabilidad de
inyección de SQL. Extraída de [1].

La opción -- delimiter existe para que un usuario pueda especificar el separador que
debería aparecer entre sentencias SQL. Valores típicos podrían ser un punto y coma o un
retorno de carro.

© Universidad Internacional de La Rioja (UNIR)


Tema 3: Codificación segura

Pero el programa no coloca ninguna restricción contra el valor del argumento, por tanto
a través de un parámetro de línea de comando, se puede escribir cualquier cadena que se
quiera en la sentencia SQL generada, incluyendo órdenes de SQL adicionales.

Por ejemplo, si una simple SELECT fue generada con -- delimiter ';', esto generaría un
script para ejecutar la sentencia siguiente:

SELECT * FROM items WHERE owner = "admin"


Pero si la misma consulta fue emitida con la opción malévola -- delimiter '; DELETE
FROM items;', generaría una sentencia que limpia a fondo la tabla items con la sentencia
siguiente:
SELECT * FROM items WHERE owner = "admin"; DELETE FROM items

Establecer los límites de confianza

Un límite de confianza se puede pensar como una línea dibujada a través de


un programa, fuera de la cual, no se confía en los datos. Al otro lado de la línea,
los datos se asumen seguros para alguna operación particular. El objetivo de la lógica de
validación es el de permitir a los datos cruzar el límite de confianza, moverse de la zona
no segura a la zona en la que se confía. Los problemas de confianza de frontera ocurren
cuando un programa enturbia la línea entre lo que es confiable y lo que no es. El modo
más fácil de cometer este error es el de permitir datos confiables y no
confiables mezclados en la misma estructura de datos.

El ejemplo de la figura siguiente, Figura 4, demuestra un problema común de frontera


de confianza en lenguaje Java. Los datos no confiables llegan a una petición HTTP y el
programa almacena los datos en un objeto de sesión HTTP sin realizar primero una
validación. Como el usuario directamente no puede tener acceso al objeto de sesión, los
programadores típicamente creen que pueden confiar en la información almacenada en
el objeto de sesión. Combinando datos validados y no validados en la sesión, este código
viola un límite implícito de confianza.

status = request.getParameter("status");
if (status != null && status.length() > 0) {
session.setAttribute("USER_STATUS", status);
}
Figura 4. Ejemplo de violación de los límites de confianza. [1]

© Universidad Internacional de La Rioja (UNIR)


Tema 3: Codificación segura

La entrada no validada almacenada en la sesión HTTP del Ejemplo de la figura anterior,


figura 4, se imprime más adelante en la misma página JSP, figura 5. Aunque el valor
viene de un objeto de sesión HTTP, que es de confianza por lo general, el atributo
USER_STATUS nunca fue validado, haciendo de este código vulnerable a un ataque del
tipo crosssite scripting (XSS).

String user_state = "Unknown";


try {
HttpSession user_session = Init.sessions.get(tmpUser.getUser());
user_state = user_session == null ? "Unknown" :
(String)user_session.getAttribute("USER_STATUS");
user_state = user_state == null ? "Available" : user_state;
}
...
%>
<%=user_state %>
Figura 5. Uso de datos invalidados. Extraída de [1]

Sin unos límites de confianza bien establecidos los programadores inevitablemente


perderán la pista de los datos que han sido validados y los que no, llevando al hecho de
que se usarán datos en la aplicación sin haber sido validados.

Las fronteras de confianza a veces se estiran cuando la entrada se compone de


una serie de interacciones de usuario antes de ser procesada. No puede ser
posible realizar la validación completa de la entrada hasta que toda la
entrada esté disponible. Este problema típicamente se muestra en operaciones como
una nueva secuencia de registro de usuario nuevo o un proceso de comprobación de
tienda Web. En situaciones difíciles como estas, es aún más importante mantener un
límite de confianza. Los datos no validados deberían ser almacenados en una estructura
de datos no validados y luego movidos a una estructura validada.

Cuando se escribe el código, hay que hacer fácil indicar en qué lado del límite de
confianza de cualquier conjunto dado de datos se está. Cuando se realiza una revisión
de código, no esperar hasta encontrar una situación, en la cual, los datos que no han sido
validados puedan utilizarse. Si las fronteras de confianza no están claramente
delimitadas, los errores de validación son inevitables. En vez de gastar el tiempo

© Universidad Internacional de La Rioja (UNIR)


Tema 3: Codificación segura

buscando un argumento explotable, hay que concentrarse en la creación de fronteras de


confianza precisas.

Realizar fuerte validación de entrada

Una correcta aproximación a la validación de entrada es comprobar la entrada


frente a una lista de valores correctos conocidos. Una buena validación de
entrada no intenta comprobar valores específicos no válidos. Poniendo como símil una
fiesta, si se quiere asegurar que solo los invitados entran, se hace una lista de invitados y
se comprueban los asistentes contra ella en la puerta. No se trata de hacer una lista de
toda la gente a la que no se invita. Lo anterior quiere decir que hay que evitar hacer
comprobaciones frente a listas negras. Se considera mejor la comprobación contra:

Whitelisting: lista de valores conocidos válidos

Cuando el juego de valores de entrada posibles es pequeño, se puede usar la selección


indirecta para conseguir que el whitelist sea imposible de evitar, es la
primera opción cuando se realiza la validación de entrada.

Al final se pone:
Lista negra (Blacklinting): intento de enumerar todas las entradas posibles
inaceptables.

Se puede utilizar algún nivel de indirección en el programa haciendo que el usuario


especifique primero algún subconjunto de datos de entre todos los que el conjunto total
de datos que se pueda dividir, de esta forma si el usuario no sabe alguno de estos
subconjuntos válidos de datos se evita tratar directamente de validar entradas complejas
de datos y solo se valida en principio un índice de los subconjuntos de datos legítimos.

El ejemplo de la figura siguiente, es un programa que limita el acceso a una serie de


juegos de UNIX. El programa funciona leyendo el nombre del juego por la entrada
estándar. Notar que la selección indirecta no permite al usuario especificar un
camino al ejecutable, así cualquier futura tentativa de usar la entrada
directamente probablemente hará que el programa falle.
/* Description: the argument to execl() is selected based on user-provided
input, but only if it matches an item in a safe list. */
#include <stdio.h>

© Universidad Internacional de La Rioja (UNIR)


Tema 3: Codificación segura

#include <string.h>
#include <unistd.h>
char *validGames[] = { "moria", "fortune", "adventure", "zork",
"rogue", "worm", "trek", NULL };
#define MAXSIZE 40
void runGame(char *str) {
char buf[MAXSIZE];
int x;
for(x = 0; validGames[x] != NULL; x++) {
if (strcmp(str, validGames[x]) == 0) {
break;
}
}
if (validGames[x] == NULL) {
return;
}
snprintf(buf, sizeof buf, "/usr/games/%s", validGames[x]);
buf[MAXSIZE-1] = 0;
/* user input affects the exec command only indirectly */
execl(buf, validGames[x], 0);
}
int main(int argc, char **argv)
{
char *userstr;
if(argc > 1) {
userstr = argv[1];
runGame(userstr);
}
return 0;
}
Figura 6. Uso de indirección a los datos de entrada. Extraída de [1].

El programa podría haber aceptado la petición del usuario en forma de una ruta al
ejecutable, pero requeriría la comprobación de la entrada de usuario para asegurarse que
la ruta indicada es aceptable. Se tendría que comenzar a pensar en permisos de sistema
de ficheros, enlaces simbólicos y en la posibilidad de ataques de escalada de privilegios.
En muchas situaciones, la indirección no es factible porque el conjunto de
valores legítimos es demasiado grande o demasiado difícil de definir
explícitamente. Si se tiene que aceptar un número de teléfono como entrada, mantener

© Universidad Internacional de La Rioja (UNIR)


Tema 3: Codificación segura

una lista de todos los números de teléfono legítimos no es una opción realista. La mejor
solución en tales casos es de crear una whitelist de valores de entrada aceptables.

Las expresiones regulares son una potente herramienta para ayudar en la


validación de entrada, han sido incluidas por ejemplo en el paquete de java
java.util.regex en el jdk 1.5. Pero la experiencia observada dice que los programadores C
y C ++ tienden a huir de expresiones regulares, quizás porque no existe de hecho una
puesta en práctica estándar. Aunque POSIX realmente confiera por mandato un interfaz
estándar (por lo general encontrado en regex.h), el interfaz tiene reputación de
problemas de transportabilidad.

Una buena solución es el paquete de open source Perl Expresiones Compatibles


Regulares (PCRE)2. Como el nombre sugiere, soporta la sintaxis de expresiones regulares
Perl extensamente usada. Está distribuida bajo una licencia de BSD y viene con un
conjunto de clases de wrapper C ++ donadas por Google. Ejemplo de la figura siguiente:
Este programa en C usa la biblioteca de expresión regular PCRE, acepta números
de teléfono como argumentos y usa una expresión regular para comprobar su entrada y
asegurarse que contiene solo dígitos, espacios y períodos.

#include <stdio.h>
#include <string.h>
#include <pcre.h>
int main(int argc, char* argv[]) {
pcre* re;
const char* err;
int errOffset;
char* regex = "^[0-9\\-\\. ]+$";
int i;
re = pcre_compile(regex, 0, &err, &errOffset, NULL);
if (re == NULL) {
printf("PCRE compilation failed\n");
return 1;
}
for (i = 1; i < argc; i++) {
char* str = argv[i];

2 http://www.pcre.org/

© Universidad Internacional de La Rioja (UNIR)


Tema 3: Codificación segura

int len = strlen(str);


int rc = pcre_exec(re, NULL, str, len, 0, 0, NULL, 0);
if (rc >= 0) {
dialNumber(argv[i]);
} else {
printf("Not a valid phone number.\n");
}
}
free(re);
}
Figura 7. Uso de la librería de expresiones regulares PCRE de PERL. Extraída de [1]

No confundir usabilidad por seguridad

No confundir la validación que una aplicación realiza por objetivos de utilidad con la
validación de entrada por seguridad.

La validación de entrada por usabilidad pretende capturar errores comunes y


proporcionar realimentación fácil de comprender a los usuarios legítimos cuando los
cometen. La validación de entrada por objetivos de seguridad intenta impedir
entrada no válida y no amistosa. El ejemplo de la figura siguiente, lista un bloque de
código que ayuda a asegurarse de que un usuario ha introducido la nueva contraseña
deseada pidiendo al usuario introducirla dos veces.

void changePassword() {
char* pass1 = getPasswordFromUser("enter new password: ");
char* pass2 = getPasswordFromUser("re-enter new password: ");
if (strcmp(pass1, pass2)) {
printf("passwords do not match\n");
} else {
setPassword(pass1);
}
bzero(pass1, strlen(pass1)); /* don't leave in memory */
bzero(pass2, strlen(pass2)); /* don't leave in memory */
free(pass1);
free(pass2);

© Universidad Internacional de La Rioja (UNIR)


Tema 3: Codificación segura

}
char* getPasswordFromUser(char* prompt) {
char* tmp = getpass(prompt);
int len = strlen(tmp);
char* ret = (char*) malloc(len+1);
strncpy(ret, tmp, len+1);
bzero(tmp, len); /* don't leave passwd copy in memory */
return ret;
}
Figura 8. La comprobación de la contraseña 2 veces no impide a un atacante introducir datos
maliciosos. [1].

Rechazar directamente datos maliciosos

Se podrían parchear ciertas clases de fracaso de validación de entrada. Una omisión del
campo requerido podría ser restaurada poniéndolo a un valor por defecto, un campo de
contraseña que excede una longitud máxima podría ser truncado, o código de JavaScript
en un campo de entrada podría ser substituido por caracteres de escape.

¡Evitar esa tentación! No reparar datos que fallan en comprobaciones de


validación de entrada, rechazar la entrada directamente. La validación de la
entrada es compleja por sí misma, pero el código que combina la validación de entrada
con la recuperación de errores automatizada crea una explosión de complejidad, sobre
todo si los datos pueden ser codificados de más de un modo o si las codificaciones pueden
estar anidadas.

Un código de recuperación de error automatizado podría cambiar el


significado de la lógica de validación de la entrada.

No convertir una clase de entrada maliciosa en otra para el atacante. Desechar la entrada
que falla en la validación rotundamente.

Realizar una buena validación de entrada por defecto

© Universidad Internacional de La Rioja (UNIR)


Tema 3: Codificación segura

Las posibilidades son tan buenas que, independientemente del lenguaje de


programación que se usa, los métodos estándar para aceptar la entrada no proporcionan
una facilidad empotrada que realice la validación de entrada.

Esto quiere decir que, siempre, los programadores tienen que añadir código para
recuperar alguna nueva clase de entrada y afrontar la validación de la gama de trampas
de entrada. En vez de codificar una nueva solución al problema de validación de entrada
cada vez, diseñar el programa de modo que haya un lugar claro, constante, y
obvio para la validación de entrada. La aplicación debería hacer pasar toda la
entrada por esta lógica de validación y ésta debería rechazar cualquier entrada que no
pueda ser validada

Esto requiere la creación de una capa de abstracción sobre las bibliotecas del sistema en
la que el programa suele ser introducido. Hacer una buena validación de entrada por
defecto creando una capa de funciones o métodos, se denomina seguridad API. La figura
siguiente, muestra cómo se interpone el API de seguridad entre el programa y las
librerías de sistema.

Lógica de programa

API seguridad

Entrada Librerías de sistema

Figura 9. APIs de seguridad implican un amplio contexto para hacer la validación de entrada.
Extraída de [1]

Una mejora con API de seguridad aumenta la capacidad de hacer lo siguiente:


Aplicar una validación de entrada sensible a contexto coherentemente a
toda la entrada. La alternativa de tener para cada módulo implementando su propia
validación de entrada, es difícil de establecer una política de validación de entrada
uniforme.

Entender y mantener la lógica de validación de entrada. La validación de


entrada es difícil. Múltiples puestas en práctica de la misma, multiplican la cantidad

© Universidad Internacional de La Rioja (UNIR)


Tema 3: Codificación segura

de pruebas y verificacines que hay que realizar. Con esto conseguinos el realizar una
tarea difícil aún más difícil.

Actualizar y modificar el intento de introducir la validación


coherentemente. ¿Cuándo se encuentra un problema con la lógica de validación de
entrada, seremos capaces de solucionarlo? Si la lógica de validación no se centraliza,
la respuesta es casi siempre «no sé».

Ser constante. Si la validación de entrada no es la de por defecto, es fácil para un


desarrollador olvidarse de hacerlo. Realizando una validación de entrada mediante
un procedimiento estándar, se puede garantizar que toda la entrada que el programa
acepta (ahora y en el futuro) es validada.

Es crítico no sobre-generalizar, evitar la tentación de aceptar la validación de


entrada menos rigurosa a cambio de generalización una mayor. Reutilizar la lógica de
validación para hacer cumplir las restricciones necesarias de la entrada, de forma que
sea segura en el contexto en que se usa.

No se sabe de ninguna función estándar de C más necesitada de una API de seguridad


que readlink(). El objetivo de readlink() es determinar el nombre del archivo referido por
un enlace simbólico. La función puede causar problemas de seguridad.

A diferencia de la mayoría de las funciones del lenguaje C que manipulan cadenas,


readlink() no añade un terminador nulo al contenido del buffer que devuelve. Esto da la
oportunidad a los programadores de olvidar el terminador nulo totalmente y crear el
potencial riesgo de un desbordamiento de buffer posterior. Este es, entre otros, uno de
los potenciales riesgos que se pueden correr con esta función. El ejemplo de la figura
siguiente, muestra un wraper para readlink() que iterativamente llama readlink() hasta
que se haya asignado un buffer bastante grande para albergar la ruta completa que el
enlace simbólico indica. Se requieren más de 30 líneas de código para llamar
correctamente a readlink().
char* readlinkWrapper(char* path, int maxSize) {
int size = 32;
char* buf = NULL;
char* tmp = NULL;
int ret = 0;

© Universidad Internacional de La Rioja (UNIR)


Tema 3: Codificación segura

while (size <= maxSize) {


tmp = (char*) realloc(buf, size);
if (tmp == NULL) {
perror("error allocating memory for readlink");
goto errExit;
}
buf = tmp;
ret = readlink(path, buf, size);
if (ret < 0) {
perror("error reading link");
goto errExit;
}
if (ret < size) {
break;
}
size *= 2;
}
if (size <= maxSize) {
buf[ret] = '\0';
return buf;
}
errExit:
free(buf);
return NULL;
}
Figura 10. Ejemplo de wrapper de readlink(). Extraída de [1]

Las APIs para mejora de la seguridad también pueden hacer más fácil mantener las
fronteras de confianza haciendo obvia la validación cuando los datos cruzan el límite.
Considerar el límite de confianza hablado anteriormente, el contenido del objeto de
HttpSession por lo general es considerado confiable porque no puede ser cambiado nada
más que por la aplicación misma. Como HttpSession a menudo lleva la información que
entra en el programa vía una petición no validada en http, el método
HttpSession.setAttribute() (método que permite que los datos sean almacenados en un
HttpSession) forma un límite natural de confianza en una aplicación Web Java.

© Universidad Internacional de La Rioja (UNIR)


Tema 3: Codificación segura

Comprobar la longitud de la entrada

La lógica de validación front-end siempre debería comprobar la longitud de


la entrada contra un mínimo y máximo. Las comprobaciones de longitud son por
lo general fáciles de añadir, pues no se requiere mucho conocimiento sobre el significado
de la entrada. Tener cuidado cuando el programa transforma la entrada antes del
tratamiento, pues esta puede hacerse más larga durante el proceso. La validación
correcta de la entrada consiste en mucho más que la evaluación de su longitud, una
comprobación de longitud es una parte mínima de la validación.

Cuanto más contexto de programa pueda ser tenido en cuenta durante la validación de
la entrada, mejor. Si el programa tiene que validar un campo de entrada, cuanto más
sepa la lógica de validación de los valores legales para ese campo, más rigurosa
puede ser la misma.

Por ejemplo, si hay un campo de entrada para contener la parte de abreviatura estatal
de una dirección postal, la lógica de validación puede usar selección indirecta para
comprobar el valor de la misma contra una lista de abreviaturas postales válidas para
estados. Un esquema de validación de entrada más sofisticado podría la comprobar la
parte de prefijo local de un campo de número de teléfono contra la abreviatura estatal.

El dictado de buenas prácticas de diseño puntualiza que el código de validación frontal y


la lógica de negocio no deberían ser entremezclados. El código de validación raras veces
tiene el contexto ideal para hacer el mejor trabajo posible de validar la entrada. La línea
perfecta entre la validación frontal y demás comprobaciones de validación que son
entremezcladas con la lógica de aplicación depende del contexto del programa. Como
mínimo, sin embargo, siempre debería ser posible comprobar la longitud de
entrada como parte de la validación frontal. En la figura siguiente, se puede ver
cómo se realiza una simple comprobación de la longitud de una ruta a un fichero.

if (path != null &&


path.length() > 0 && path.length() <= MAXPATH) {
fileOperation(path);
}
Figura 11. Comprobación de la longitud de un campo. Extraída de [1].

© Universidad Internacional de La Rioja (UNIR)


Tema 3: Codificación segura

En este otro ejemplo de la figura siguiente, se muestra cómo se realiza la comprobación


de la entrada utilizando una expresión regular para comprobarla frente a una whitelist y
su longitud al mismo tiempo.

// limit character content,


// also limit length to between 1 and MAXPATH
final String PATH_REGEX = "[a-zA-Z0-9/]{1,"+MAXPATH+"}";
final Pattern PATH_PATTERN = Pattern.compile(PATH_REGEX);
...
if (path != null && PATH_PATTERN.matcher(path).matches()) {
fileOperation(path);
}
Figura 12. Comprobación de la longitud de un campo. Extraída de [1].

En C y C++, como ya se verá más adelante hay varias funciones que pueden hacer
fácilmente que ocurran desbordamientos de buffer. Java hace la comprobación de
límites y comprueba la seguridad de tipos, de esta forma el desbordamiento de
buffer no es problema en Java de la manera que lo es en C y C ++.

Comprobar el tamaño de los campos numéricos

Comprobar la entrada numérica tanto contra un valor máximo como contra un


valor mínimo como parte de validación de la entrada. Tener cuidado con las operaciones
que podrían ser capaces de llevar un número más allá de su valor máximo o mínimo.

Cuando un atacante aprovecha la capacidad limitada de una variable de tipo entero, el


problema es el desbordamiento de este tipo de datos. En C y C ++, el desbordamiento de
número entero típicamente supone parte de la preparación para un ataque de
desbordamiento de buffer. El desbordamiento de número entero en Java no conduce
vulnerabilidades de desbordamiento, pero puede causar un comportamiento indeseable.

El mejor modo de evitar problemas de desbordamiento de un número entero


es comprobar toda la entrada, de este tipo de datos, tanto contra una cota
superior como contra una cota inferior. Inmejorablemente, los límites deberían
ser escogidos de modo que cualquier cálculo subsecuente que se realiza no exceda la
capacidad de uso de la variable. Si tales límites fueran demasiado restrictivos, el

© Universidad Internacional de La Rioja (UNIR)


Tema 3: Codificación segura

programa debe incluir comprobaciones internas para asegurarse de que los valores no
causan un desbordamiento.

Java ofrece varios tamaños diferentes de valores enteros: char (8 bits), short (16 bits), int
(32 bits), y long (64 bits). Sin embargo, no ofrece tipos de entero sin signo, por lo que no
hay ningún modo de evitar comprobar la cota inferior como la superior. Si se quiere
manejar números no negativos en Java se debe comprobar que los valores que se reciben
son mayores o iguales que cero explícitamente. Por defecto el compilador de java
advertirá si un valor de un tipo más grande es asignado a una variable de un tipo más
pequeño.

En C y C++ leer campos de entrada usando enteros sin signo, obvia la necesidad de
comprobar el límite inferior, pero hay que tener cuidado porque hay que ser conscientes
de que las operaciones que mezclan enteros con y sin signo pueden acarrear resultados
negativos y convertir resultados con signo en otro sin signo podría producir un resultado
inesperado demasiado grande.
Ejemplo: malloc() acepta un entero y realiza una conversión implícita a entero si signo,
si doAlloc() recibe un número negativo como argumento puede resultar en un
intento de reservar una gran cantidad de memoria.

void* doAlloc(int sz) {


return malloc(sz); /* Aqui se realiza una conversion implícita: malloc()
acepta un argumento sin signo. */
}
Figura 13. Comprobación de tipos. Extraída de [1]

Prevenir vulnerabilidades de meta-caracteres

Si se permite a los atacantes controlar los comandos que se envían a la base


de datos, sistemas de ficheros, navegador u otros subsistemas, se podría para
cambiar la finalidad del mismo. Se pueden tener problemas serios como
consecuencia de ello. Los programadores no lo hacen intencionadamente pero debido a
las interfaces que diseñan, si no se realiza teniendo en cuenta una serie de conceptos
clave, indirectamente los problemas pueden aparecer en forma de ataques que
aprovechan la puerta que se les ha dejado abierta. Como ejemplos de ellos tenemos los
siguientes [23]:

© Universidad Internacional de La Rioja (UNIR)


Tema 3: Codificación segura

Ataque Implicaciones
SQL Injection Accede/modifica datos en la base de datos
XPath Injection Accede/modifica datos en formato XML
SSI Injection Ejecuta comandos sobre el servidor y accede a datos
sensibles
LDAP Injection Bypass de autenticación
MX Injection Usa el servidor de correos como una máquina de spam
HTTP Injection Modifica o intoxica el caché de las web
Tabla 1 Defectos de inyección.

Inyección de SQL
La realización de sentencias SQL donde se combinan palabras clave con datos tiene el
problema de que alguien malintencionadamente pueda alterar las estructuras de
control y por tanto el significado de la sentencia, cuando la intención era solamente
suministrar datos a la sentencia sin alterar las estructuras de control. Un atacante
explota este tipo de vulnerabilidades especificando meta-caracteres que tienen un
significado especial como:
o Simples comillas (‘).
o Dos puntos (..), peligroso en sistema de ficheros.
Para comandos de Shell:
o Punto y coma (;).
o (&&).
o Carácter de nueva línea (/n) para los ficheros de log.
En el ejemplo de la figura siguiente, se muestra este tipo de vulnerabilidad en una
sentencia SQL que se forma concatenando strings de tal forma que se deja abierta a
un ataque conocido como inyección de SQL:

String userName = ctx.getAuthenticatedUserName();


String itemName = request.getParameter("itemName");
String query = "SELECT * FROM items WHERE owner = '"
+ userName + "' AND itemname = '"
+ itemName + "'";
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(query);
Figura 14. Ejemplo de bug que deja abierto el código abierto a un ataque de inyección de SQL.
Extraída de [1]

© Universidad Internacional de La Rioja (UNIR)


Tema 3: Codificación segura

El programador que escribió el código pretendía formar consultas como:


SELECT * FROM itemsWHERE owner = <userName> AND itemname =
<itemName>;

Pero un atacante puede asignar al campo itemname la cadena "name' OR 'a'='a" con
lo que la sentencia se convierte en:
SELECT * FROM items WHERE owner = 'wiley' AND itemname = 'name'
OR 'a'='a';

La adición de OR 'a'='a' convierte la sentencia pretendida a esta: SELECT * FROM


ítems Esta sentencia extrae todas las filas de la tabla ítems, posibilitando que el
atacante las pueda visualizar. Este es un pequeño ejemplo de lo que un atacante puede
hacer. Muestra que se podrían también borrar todos los datos de la tabla y ejecutar
muchas más sentencias distintas de la pretendida que solo mostrar una fila al usuario.

La forma de ayudar a prevenir este tipo de ataques es utilizar consultas


parametrizadas que fuerzan a la distinción entre estructuras de control
como son las palabras clave de una sentencia SQL de lo que son
puramente datos de entrada por parte del usuario de la aplicación. Los
programadores pueden explícitamente especificar a la base de datos lo que debe ser
tratado como comando y lo que debe ser tratado como datos.

String userName = ctx.getAuthenticatedUserName();


String itemName = request.getParameter("itemName");
String query = "SELECT * FROM items WHERE owner = ?"
+ " AND itemname = ?";
PreparedStatement stmt = conn.prepareStatement(query);
stmt.setString(1, userName);
stmt.setString(2, itemName);
ResultSet rs = stmt.executeQuery();
Figura 15. Ejemplo de consulta SQL construida con el uso de sentencias parametrizadas. Extraída
de [1]

Debe evitarse formar consultas formadas con sentencias parametrizadas y cadenas


concatenadas:

String item = request.getParamater("item");

© Universidad Internacional de La Rioja (UNIR)


Tema 3: Codificación segura

String q="SELECT * FROM records WHERE item=" + item;


PreparedStatement stmt = conn.prepareStatement(q);
ResultSet results = stmt.executeQuery();
Figura 16. Ejemplo de consulta SQL construida con el uso de sentencias parametrizadas y
concatenadas. [1]

Otro ejemplo extraído de la referencia [23], es el siguiente:

Figura 17. Ejemplo de consulta SQL [23].

Manipulación de rutas
Este tipo de error tiene lugar cuando se permite en las entradas de un usuario incluir
meta-caracteres de sistemas de ficheros como:
o slash (/).
o backslash (\).
o punto (.).
donde se espera una ruta relativa un atacante puede convertirla en una ruta absoluta
o recorrer el sistema de ficheros a una posición no planeada subiendo en el árbol de
directorio. Se denomina a un acceso al sistema de ficheros no autorizado
de este tipo manipulación de ruta. El código del ejemplo de la figura siguiente,
muestra la entrada a la aplicación desde una petición HTTP para crear un nombre de
archivo. El programador no ha considerado la posibilidad de que un atacante podría
proporcionar un nombre del archivo como ../../tomcat/conf/server.xml, que hace que
la aplicación suprima uno de sus propios archivos de configuración.

© Universidad Internacional de La Rioja (UNIR)


Tema 3: Codificación segura

String rName = request.getParameter("reportName");


File rFile = new File("/usr/local/apfr/reports/" + rName);
rFile.delete();
Figura 18. Ejemplo de vulnerabilidad de manipulación de ruta. Extraída de [1].

Las vulnerabilidades de manipulación de ruta son relativamente fáciles de prevenir


con whitelists. En el ejemplo de la figura siguiente, se emplea una expresión
regular para asegurar que los nombres de archivo pueden ser sólo de hasta 50
caracteres alfanuméricos, seguidos de un punto opcional y con una extensión de 5
caracteres:

final static int MAXNAME = 50;


final static int MAXSUFFIX = 5;
final static String FILE_REGEX =
"[a-zA-Z0-9]{1,"+MAXNAME+"}" // vanilla chars in prefix
+ "\\.?" // optional dot
+ "[a-zA-Z0-9]{0,"+MAXSUFFIX+"}"; // optional extension
final static Pattern FILE_PATTERN = Pattern.compile(FILE_REGEX);
public void validateFilename(String filename) {
if (!FILE_PATTERN.matcher(filename).matches()) {
throw new ValidationException("illegal filename");
}
}
Figura 19. Uso de whitelist para prevenir manipulación de ruta. Extraída de [1].

Inyección de comandos
Si se permite al usuario, especificar comandos de sistema que su programa
ejecuta, los atacantes podrían ser capaces de hacer que el sistema ejecute
comandos maliciosos en su nombre. Si en la entrada se pueden incluir rutas del
sistema de ficheros o shell metacharacters, un atacante podría especificar una ruta
absoluta donde se espera una ruta relativa o añadir un segundo comando malévolo
después del comando que el programa tiene la intención de ejecutar. Se denomina
inyección de comandos a la ejecución de comandos no autorizada.

El código en el ejemplo de la figura siguiente, es para ejecutar un backup de una base


de datos ORACLE en Windows. Se acepta un parámetro que especifica un tipo de
backup con la utilidad rman. Se necesita un acceso privilegiado para poder acceder a

© Universidad Internacional de La Rioja (UNIR)


Tema 3: Codificación segura

la BD, por tanto la aplicación se ejecuta con un usuario privilegiado. Un atacante


podría pasar como parámetro "&& del c:\\dbms\\*.*“, cualquier comando que se
inyectara se ejecutaría con los privilegios que se tenían para trabajar con la BD.

String btype = request.getParameter("backuptype");


String cmd = new String("cmd.exe /K \"c:\\util\\rmanDB.bat "
+ btype + "&&c:\\utl\\cleanup.bat\"")
Runtime.getRuntime().exec(cmd);
Figura 20. Ejemplo de inyección de comandos. Extraída de [1]

El ejemplo de la figura siguiente, muestra el ejemplo anterior corregido utilizando una


whitelist para prevenir la posible inyección de comandos.

final static int MAXNAME = 50;


final static String FILE_REGEX =
"[a-zA-Z]{1,"+MAXNAME+"}"; // vanilla chars in prefix
final static Pattern BACKUP_PATTERN = Pattern.compile(FILE_REGEX);
public void validateBackupName(String backupname) {
if(backupname == null
|| !BACKUP_PATTERN.matcher(backupname).matches()) {
throw new ValidationException("illegal backupname");
}
}
...
String btype = validateBackupName(request.getParameter("backuptype"));
String cmd = new String("cmd.exe /K \"c:\\util\\rmanDB.bat "
+ btype + "&&c:\\utl\\cleanup.bat\"")
Runtime.getRuntime().exec(cmd);
Figura 21. Ejemplo de inyección de comandos solucionado con una whitelist. Extraída de [1]

Falsificación de logs
Los logs son un objetivo para atacantes pues son un recurso valioso para
administradores de sistema y desarrolladores. Si los atacantes pueden falsificar el
valor que es escrito en los log, podrían ser capaces de fabricar eventos en el sistema
mediante la inclusión de entradas corrompidas. Si se permite a un atacante inyectar
entradas en los logs debido a una carencia de validación, la interpretación de los
archivos de logs podría ser dificultosa o mal dirigida, disminuyendo su valor.

© Universidad Internacional de La Rioja (UNIR)


Tema 3: Codificación segura

En el caso más benigno un atacante podría insertar entradas falsas de log incluyendo
caracteres especiales. Si el archivo de log se procesa automáticamente, el atacante
puede dejar el archivo inutilizable corrompiendo su formato. Un ataque más sutil
podría implicar el sesgar la estadística del archivo de log.

Los archivos de log falsificados o de otro modo, archivos de log corrompidos,


pueden usarse para encubrir las pistas de un atacante o implicar a otra parte en la
comisión de un acto malicioso. Si su log contiene información relevante de seguridad,
los atacantes podrían introducir en el log una falsa alarma: si pueden llenar el log
de falsas alarmas que nadie presta atención, nadie prestará atención
cuando ocurra el ataque verdadero. El ejemplo de la figura siguiente, muestra
un trozo de código de una aplicación WEB que intenta leer un número entero desde
un objeto request. Si el valor al ser comprobado no es entero se escribe el evento de
log correspondiente indicando lo ocurrido.

String val = request.getParameter("val");


try {
int value = Integer.parseInt(val);
}
catch (NumberFormatException) {
log.info("Failed to parse val = " + val);
}
Figura 22. Ejemplo de falsificación de log. Extraída de [1]

Si un usuario envía a la aplicación «twenty-one» en el parámetro val se registra la


siguiente entrada:
INFO: Failed to parse val=twenty-one
Sin embargo si un atacante envía:
«twenty-one%0a%0aINFO:+User+logged+out%3dbadguy» se registra la siguiente
entrada:
INFO: Failed to parse val=twenty-one INFO: User logged out=badguy
Claramente, los atacantes pueden usar este mismo mecanismo para insertar entradas
de log arbitrarias. Si se planifica escribir un filtro propio de salida, el carácter más
crítico es típicamente el \n (nueva línea), pero el conjunto de caracteres
importantes depende completamente del formato del archivo de log y de las
herramientas que se usan para examinar el log.

© Universidad Internacional de La Rioja (UNIR)


Tema 3: Codificación segura

Es prudente codificar cualquier carácter que no sea un carácter de ASCII. Una forma
de prevenir falsificación de log es codificar los datos antes de registrarlos en el log.

El ejemplo de la figura siguiente, corrige el ejemplo anterior URL codificando los


datos del objeto request antes de proceder a su registro.

String val = request.getParameter("val");


try {
int value = Integer.parseInt(val);
}
catch (NumberFormatException) {
log.info("Failed to parse val = " +
URLEncoder.encode(val, "UTF8"));
}
Figura 23. Vulnerable log modificado URL-codificando los datos antes de registrarlos. Extraída de [1]

Resumen
Una validación correcta de la entrada requiere todo lo siguiente:

o Identificar las fuentes de entrada de todo el programa; es el interfaz principal de


usuario o la conexión de red. Asegurarse de que se consideran todas las formas en
que su programa interactúa con su entorno, incluyendo la línea de comandos,
variables de entorno, librerías dinámicas y el almacenamiento temporal.

o Usar una estrategia como la selección indirecta o whitelisting que se enfoca en la


identificación de la entrada que se sabe está bien. Evitar utilizar listas negras, en
las cuales el objetivo es casi imposible de identificar todas las cosas que
posiblemente podrían ser malas. Como mínimo, hay que estar seguro de que
siempre se comprueba la longitud de las entradas y, para las entradas numéricas,
valores máximos y mínimos. Desechar datos de las comprobaciones de validación
que fallan, no tratar de parchear esos datos.

o Seguir la pista de qué valores han sido validados y qué propiedades aquella
validación comprobó. Si el programa tiene fronteras de confianza bien
establecidas, esto es fácil.

© Universidad Internacional de La Rioja (UNIR)


Tema 3: Codificación segura

o Intentar no violar las fronteras de confianza construyendo la validación de entrada


en el código alrededor de donde el programa suele mover datos. Esta clase de
comprobación por lo general no estará por defecto implementada en las bibliotecas
de entrada-salida previamente empaquetadas, por tanto se tendrán que crear APIs
de seguridad para hacer la validación de entrada correcta por defecto.

o No dejar pasar por alto el modo en que los diferentes componentes interpretan los
datos que pasan por el programa. No permitir que los atacantes secuestren las
peticiones al sistema de ficheros, la base de datos, o el navegador de Web, entre
otros. Esto por lo general implica cuidadosamente pensar en codificaciones de
datos y en meta-caracteres.

3.4. Desbordamiento de buffer

Introducción

Un desbordamiento de buffer ocurre cuando un programa escribe datos fuera


de los límites de la memoria asignada.

El MITRE define este error como [14]:«Un programa intenta poner más datos en un
búfer de lo que realmente puede almacenar, o cuando un programa trata de poner los
datos en un área de memoria fuera de los límites de un búfer… la causa más común de
desbordamientos de búfer, es el típico caso en el que el programa copia el búfer sin
restringir el número de bytes a copiar.»

Las vulnerabilidades por desbordamiento de buffer por lo general son


explotadas con el objetivo de superponer valores en la memoria en provecho del atacante.
Los errores de desbordamiento buffer están muy extendidos y normalmente dan a un
atacante un control alto del código vulnerable. Se pueden provocar como consecuencia
del manejo incorrecto tanto de cadenas de caracteres como de la manipulación de
números enteros. Este tipo de vulnerabilidades se puede clasificar en los siguientes tipos:

© Universidad Internacional de La Rioja (UNIR)


Tema 3: Codificación segura

Figura 24. Tipos de vulnerabilidades de Desbordamiento de Buffer.

Desde que Morris Worm hizo público el primer uso de un ataque de desbordamiento
buffer contra el servicio fingerd que posibilitó su extensión a través de la Internet en
1988, el desbordamiento de buffer ha llegado a ser la vulnerabilidad de
seguridad de software más conocida.

Con casi 20 años de exposición prominente, se podría esperar que el desbordamiento de


buffer no planteara una amenaza significativa, se cometería una equivocación. En 2000,
David Wagner encontró que casi el 50 % de incidencias del CERT para aquel año fueron
causadas por vulnerabilidades de desbordamiento de buffer [13], en 2006 contribuyó a
14 de las 20 primeras vulnerabilidades [12] y según detalla el documento de referencia
[14] de INTECO-CERT, durante el primer y segundo semestre de 2011, de un total de
4160 vulnerabilidades, aquellas clasificadas como «Error de Buffer» y «XSS» son las más
abundantes [14]. En el siguiente gráfico extraído de dicho informe un 12 % y un 11 %
durante el primer y segundo semestre respectivamente se corresponden con
vulnerabilidades de tipo «desbordamiento de buffer». Este tipo de vulnerabilidad está
detrás de la mayoría de los gusanos y virus recientes, entre ellos Zotob, Sasser, Blaster,
Slammer, Nimda, y Code Red.

Figura 25. Vulnerabilidades 2011. Extraída de [14].

© Universidad Internacional de La Rioja (UNIR)


Tema 3: Codificación segura

La mejor forma de prevenir el desbordamiento de buffer es utilizar un lenguaje


de programación que fuerce la comprobación de tipos y de memoria de forma
que su gestión sea segura. C#, Java, Python, Ruby ó dialectos de C como CCured y
Cyclone son lenguajes de este tipo. Se usa el término «safe» para referirse a lenguajes
que automáticamente realizan comprobaciones en tiempo de ejecución para impedir a
los programas violar los límites de la memoria asignada.

Los lenguajes seguros deben proporcionar dos propiedades para asegurar que los
programas respetan límites de asignación:

Seguridad de memoria. Requiere que el programa no leerá o escribirá datos fuera


de los límites de la memoria asignada. Para alcanzar la seguridad de memoria, un
lenguaje también debe hacer cumplir la seguridad de tipo de modo que pueda seguir
la pista de los límites de asignación de memoria.

Seguridad de tipo. En los lenguajes que cumplen esa propiedad, se asigna un tipo
a los objetos, como por ejemplo un tipo int, puntero a int, puntero a función, etc. Esta
propiedad asegura que las operaciones en el objeto son siempre compatibles con su
tipo. Sin esta característica cualquier valor arbitrario podría usarse como una
referencia en la memoria. C y C++ son lenguajes inseguros y ampliamente utilizados
y por tanto el programador es el responsable de prevenir que las operaciones que
manipulan la memoria puedan resultar en desbordamientos de buffer.

Un ejemplo de este tipo de error que se pude comenter en este lenguaje, es cuando se
declara un puntero a un número entero (int) y luego se utiliza como un puntero a una
función, tal y como se muestra en eejemplo siguiente:

int (*cmp)(char*,char*);
int *p = (int*)malloc(sizeof(int));
*p = 1;
cmp = (int (*)(char*,char*))p;
cmp(“hola”,”adios”); // El programa realiza realiza un crash
Figura 26. Error de seguridad de tipos

Las operaciones que conducen a desbordamientos se reducen a un pequeño conjunto,


por ejemplo Java es un lenguaje esencialmente seguro:
No tiene explícita manipulación de punteros.

© Universidad Internacional de La Rioja (UNIR)


Tema 3: Codificación segura

Límites de arrays, strings son automáticamente comprobados.


Intentos de referencias a un objeto null son capturados.
Las operaciones aritméticas están bien definidas.
Control de acceso a ficheros, sockets mediante: JVM -> Security Manager

No obstante hay que tener en cuenta una serie de consideraciones sobre potenciales
vulnerabilidades de Java:
Seguridad de tipos. Los campos que son declarados privados o protegidos
o que tienen protección por defecto no deberían ser públicamente
accesibles. Sin embargo, hay un número de vulnerabilidades construidas para Java
que hacen que esta protección pueda ser violada. Esto no debería ser ninguna sorpresa
para expertos en Java, porque están bien documentados, pero pueden aprovecharse
del imprudente. Podría ser accesible el campo privado theCrownJewels como también
el público fredJunk. Más generalmente, «un ataque de confusión de tipo» podría
comprometer la seguridad de Java exponiendo los componentes del security
manager de la JVM.

public class TowerofLondon { public class GarageSale {


private Treasure public Treasure
theCrownJewels fredsJunk
... ....
} }
Figura 27. Seguridad de tipos en Java. Extraída de [13]

Campos públicos. Un campo que es declarado público directamente puede ser


accedido por cualquier parte de un programa Java y puede ser modificado por las
mismas (a no ser que el campo sea declarado como final). Claramente, la
información sensible no debe ser almacenada en un campo público,
porque podría ser comprometido por alguien que podría tener acceso a la JVM que
ejecuta el programa.

Serialización. Esta facilidad posibilita que el estado del programa sea capturado y
convertido en una byte stream que puede ser restaurado por la operación inversa que
es la deserialización. También permite que un objeto sea transmitido por la red. La
serialización captura todos los campos de una clase incluidos los privados y protegidos
que pueden ser expuestos si se transmiten por la red. En este caso es necesario el uso
de criptografía (SSL) para preservar la confidencialidad de los datos. El

© Universidad Internacional de La Rioja (UNIR)


Tema 3: Codificación segura

security manager no previene que un objeto con campos privados pueda ser
serializado.

JVM. La JVM en sí misma está escrita en C para una plataforma dada. Esto quiere
decir que la JVM en sí misma puede ser susceptible a problemas de
desbordamiento. La versión de la JVM de SUNS Microsystems está bastante bien
inspeccionada. Java Native Inteface (JNI), permite crear código nativo y crear,
acceder y modificar objetos de Java. Si el programa en Java usa JNI para llamar a
código escrito en un lenguaje no seguro como C, la garantía de seguridad de memoria
proporcionada por Java se pierde y vulnerabilidades de buffer overflow pueden llegar
a ser posibles. Se puede comprometer la integridad de la JVM porque el código nativo
se ejecuta en el mismo espacio de direcciones que la JVM.

class Echo {
public native void runEcho();
static {
System.loadLibrary("echo");
}
public static void main(String[] args) {
new Echo().runEcho();
}
}
Figura 28. Llamada a un método nativo JNI. Extraída [1]

El código de la figura siguiente, implementa el método native definido en la clase


Echo. El código es vulnerable a buffer overflow causada por una llamada ilimitada a
gets().

© Universidad Internacional de La Rioja (UNIR)


Tema 3: Codificación segura

#include <jni.h>
#include "Echo.h" // Echo class compiled with javah
#include <stdio.h>
JNIEXPORT void JNICALL
Java_Echo_runEcho(JNIEnv *env, jobject obj)
{
char buf[64];
gets(buf);
printf(buf);
}
Figura 29. Vulnerabilidad de buffer overflow en un método nativo. Extraída de [1]

Ataque de desbordamiento de buffer basado en el stack

En un ataque clásico por desbordamiento de buffer, el atacante envía los datos que
contienen un segmento de código malévolo a un programa que es vulnerable a este error,
basado en el stack. Además del código malévolo, el atacante incluye la dirección de
memoria del principio del código.

El código malévolo, suele ser un shellcode: conjunto de instrucciones de programación


generalmente en lenguaje ensamblador y trasladadas a opcodes que suelen ser
inyectadas en la pila (o stack) de ejecución de un programa para conseguir que la
máquina en la que reside se ejecute la operación de tipo malicioso. Su nombre
proviene de un exploit que se usaba para obtener una Shell. Deben ser de longitud corta
para poder ser inyectadas dentro de la pila en un espacio reducido y se utilizan para
ejecutar código malévolo aprovechando ciertas vulnerabilidades en el código, relativas a
llamadas con desbordamiento de buffer. Principalmente se programa para permitir
ejecutar un intérprete de comandos en el equipo afectado. En la figura se muestra un
ejemplo:
#include <unistd.h>
int main() {
char *scode[2];
scode[0] = "/bin/sh";
scode[1] = NULL;
execve (scode[0], scode, NULL);
}
Figura 30. Shellcode. Extraída de http://es.wikipedia.org/wiki/Shellcode

© Universidad Internacional de La Rioja (UNIR)


Tema 3: Codificación segura

Esta shellcode, que ejecuta la shell /bin/sh, realiza una llamada al sistema execve para
realizar la ejecución de la shell contenida dentro del array scode.

Cuando el desbordamiento de buffer ocurre, el programa escribe los datos del atacante
en el buffer y sigue más allá de los límites del buffer hasta que superponga la
dirección de vuelta de la función con la dirección del principio del código
malicioso. Cuando la función devuelve el control, salta al valor almacenado en su
dirección de retorno.

Normalmente, esto lo devolvería al contexto de la función llamada, pero debido a que la


dirección de retorno ha sido superpuesta, el control salta el buffer y comienza a ejecutar
el código malicioso del atacante. Para aumentar la probabilidad de adivinar la dirección
correcta del código malicioso, los atacantes típicamente rellenan el principio de su
entrada «con una serie de instrucciones NOP» (ninguna operación).

El código del ejemplo de la figura siguiente, define el problema de función trouble(), que
asigna un buffer char y un int en el stack (pila)3 y lee una línea de texto de stdin con gets()
en el buffer. Como gets() sigue leyendo la entrada hasta que se encuentra un
carácter de final-de-línea, un atacante puede desbordar el buffer line con
datos maliciosos. En el ejemplo, esta función declara dos variables locales y usa gets()
para leer una línea de texto en el buffer line del stack de 128 octetos.

void trouble() {
int a = 32; /*integer*/
char line[128]; /*character array*/
gets(line); /*read a line from stdin*/
}
Figura 31. Empleo incorrecto de gets() que puede incurrir en desbordamiento de buffer.
Extraída de [1]
Este ejemplo se representa de forma gráfica en la siguiente figura, muestra lo que ocurre
en un clásico ataque de buffer overflow.
El primer marco de stack representa el contenido de la memoria después de llamar a
trouble() pero antes de que sea ejecutada. La variable local line es asignada sobre el
stack que comienza en la dirección 0xNN. La variable local a está justo encima de line

3 La pila es un lugar en la memoria de un equipo en el que todas las variables que se declaran e
inicializan antes de pasarlas a tiempo de ejecución se almacenan.

© Universidad Internacional de La Rioja (UNIR)


Tema 3: Codificación segura

en la memoria; la dirección de vuelta (0x <retorno>) está encima de a. Asumir que 0x


<retorno> apunta a la función que llamó a trouble().

El segundo marco de stack ilustra un argumento en el cual el trouble() se comporta


normalmente. ¡Se lee la entrada ¡ EJEMPLO BUENO! y retorna. Se puede ver ahora
que line está parcialmente llena con un string de entrada y que otros valores
almacenados sobre el stack son inalterados.

El tercer marco de stack ilustra un escenario en el cual un atacante explota la


vulnerabilidad de desbordamiento de buffer en trouble() y hace que se ejecute el
código malévolo en vez de retornar normalmente. En este caso, line ha estado llena
de una serie de NOPs, el código del exploit, y la dirección del principio del buffer,
0xNN.
ANTES DE LA EJECUCION

Dirección de
Línea a Retorno

32 0X<RETORNO>

EJECUCION NORMAL

Dirección de
Línea a Retorno

¡HOLA MUNDO! 32 0X<RETORNO>

ATAQUE DESBORDAMIENTO DE BUFFER

Dirección de
Línea a Retorno

¡HOLA MUNDO! 32 0X<RETORNO> 0xNN

Figura 32. Ejemplo de ataque de buffer overflow. [1]

Ataques relacionados con la reserva dinámica de memoria (Heap overflow)

Uno de los errores de concepto que se tienen sobre las vulnerabilidades de buffer
overflow, es que son solo explotables cuando el buffer está en el stack. Los ataques
basados en el heap4 pueden sobrescribir datos importantes almacenados en el mismo y

4Heap, es la sección de la memoria de un equipo donde todas las variables creadas o inicializadas
en tiempo de ejecución se almacenan.

© Universidad Internacional de La Rioja (UNIR)


Tema 3: Codificación segura

cambiar el flujo del programa. Por ejemplo, se podría sobrescribir el valor de un


puntero a una función, de tal forma que cuando el programa invoque la
función referenciada por el puntero a la función se ejecutará el exploit.

Finalmente, aunque un atacante no pueda inyectar código malicioso en un sistema, una


técnica de exploit conocida como arc injection o return-inito-libc (debido a su
dependencia de librerías de funciones estándar) podría permitir un ataque de buffer
overflow el alterar el flujo de control del programa. Estos ataques utilizan ataques de
buffer overflow para cambiar una dirección de retorno o una referencia a una función
con la dirección de una función ya definida en el sistema, la cual podría permitir a un
atacante preparar una llamada a una librería de sistema como system(“/bin/sh”).

Figura 33. Heap overflow. Extraída de presentación de Microsoft Basics of Secure Design,
Development, and Test

Gran parte de las vulnerabilidades relacionadas con la reserva de memoria dinámica se


corresponden a fallos de programación que encajan en alguna de las siguientes
categorías:

Figura 34. Errores heap overflows

© Universidad Internacional de La Rioja (UNIR)


Tema 3: Codificación segura

Memoria no utilizada, sin desasignar (memory leaks). Favorece de forma potencial


ataques de denegación de servicio. Se produce cuando una aplicación adquiere la memoria
y falla al liberarla de nuevo al sistema operativo. La siguiente función en «C»
deliberadamente pierde memoria al no liberar el puntero a la memoria asignada, una vez el
puntero 'a' está fuera de ámbito, una vez ejecutada la function_which_allocates () finaliza
sin liberar 'a'.
#include <stdlib.h>
void function_which_allocates(void) {
/* reserve en memoria un array de 45 datos tipo floats */
float * a = malloc(sizeof(float) * 45);
/* la zona de memora direccionada ppor el punter a 'a' */
return
/* al volver a la función main se olvida de liberar la memoria reservada con la función
malloc*/
}
int main(void) {
function_which_allocates();
/* el puntero “a” ya no exixte por lo que no se puede liberar y por tanto la memoria
seguirá estando asignada*/
Figura 35. Ejemplo memory leaks. Extraída de http://en.wikipedia.org
Uso de memoria después de ser liberada (use after free). Ocurre cuando un
programa continúa usando un puntero que ha sido previamente liberado. Si esa
memoria se vuelve a reservar, un atacante puede lanzar un ataque de buffer overflow.
Catalogado con el CWE-416.

char* ptr = (char*)malloc (SIZE);


...
if (tryOperation() == OPERATION_FAILED) {
free(ptr);
errors++;
}
...
if (errors > 0) {
logError("operation abortada antes de commit", ptr);
}
Figura 36. Ejemplo use after free. Extraída de [1]

© Universidad Internacional de La Rioja (UNIR)


Tema 3: Codificación segura

Acceso a una variable después de liberarla (Dereference5 After Free). Caso


concreto de use-after-free, que ocurre cuando intentamos acceder a la memoria
dinámica previamente liberada como consecuencia de un free(). En el ejemplo de
abajo [14], el puntero a char buf2R1 es liberado, aunque más adelante se vuelve a
referenciar desde la función strncpy. Como consecuencia, es probable que se
corrompan datos asignados posteriormente en dicha dirección de memoria tras haber
liberado la misma, o que se produzca un crash de la aplicación al intentar
sobreescribir «memoria aleatoria» en la que el proceso no tiene permiso.

#include <stdio.h>
#include <unistd.h>
#define BUFSIZER1 512
#define BUFSIZER2 ((BUFSIZER1/2) - 8)
int main(int argc, char **argv) {
char *buf1R1;
char *buf2R1;
char *buf2R2;
char *buf3R2;
buf1R1 = (char *) malloc(BUFSIZER1);
buf2R1 = (char *) malloc(BUFSIZER1);
free(buf2R1); /* libero el puntero*/
buf2R2 = (char *) malloc(BUFSIZER2);
buf3R2 = (char *) malloc(BUFSIZER2);
strncpy(buf2R1, argv[1], BUFSIZER1-1); /* lo vuelvo a utilizar*/
free(buf1R1);
free(buf2R2);
free(buf3R2);
}
Figura 37. Ejemplo dereference free. Extraída de [14]

Liberar la memoria más de una vez (double free). Cuando un programa libera
un trozo de memoria más de una vez free(), las estructuras de datos para gestión de la
memoria pueden llegar a corromperse, lo que puede ocasionar que el programa falle
o causar que se llame dos veces a malloc(). Esto último puede dar el control a un

5Acción de acceder a la variable a la que apunta el puntero en cuestión. Ej: int * ptr= &b; *ptr=
15;

© Universidad Internacional de La Rioja (UNIR)


Tema 3: Codificación segura

atacante de los datos que se escriben en esa memoria doblemente reservada. Entonces
se tiene un programa potencialmente expuesto a ataque de buffer overflow.

int * ab = (int*)malloc (SIZE);


...
if (c == 'EOF') {
free(ab); }
...
free(ab);
Figura 38. Ejemplo double free. Extraída de [14]
Uso de un puntero con valor NULL (null dereference). El uso de un puntero
con valor null puede ocasionar fallos de segmentación en la gestión de la memoria.

#include <stdlib.h>
int main(int argc, char *argv[]){
int k=0;
int *p=(int *)NULL;
switch(k) {
case 0:
if (*p) k = *p;
default:
break;
}
return 0;
}
Figura 39. Ejemplo null dereference

Control del tamaño de buffer

Es esencial para evitar los ataques de buffer overflow controlar el tamaño de


los buffers que se utilizan en el programa y no sobrepasar nunca su tamaño en otras
partes del código. Para ello es conveniente almacenar el tamaño de los buffers en
estructuras compuestas de datos como structs o clases en C, C++. El ejemplo de la figura
siguiente, muestra cómo se puede implementar este concepto, se utiliza una estructura
llamada buffer para albergar tanto el propio buffer como su tamaño. Es necesario
mantener correctamente este tamaño ante cambios de la longitud del buffer.

© Universidad Internacional de La Rioja (UNIR)


Tema 3: Codificación segura

typedef struct{
char* ptr;
int bufsize;
} buffer;
int main(int argc, char **argv) {
buffer str;
int len;
if ((str.ptr = (char *)malloc(BUFSIZE)) == NULL) {
return FAILURE_MEMORY;
}
str.bufsize = BUFSIZE;
len = snprintf(str.ptr, str.bufsize, "%s(%d)", argv[0], argc);
if (len >= BUFSIZE) {
free(str.ptr);
if (len >= MAX_ALLOC) {
return FAILURE_TOOBIG;
}
if ((str.ptr = (char *)malloc(len + 1)) == NULL) {
return FAILURE_MEMORY;
}
str.bufsize = len + 1;
snprintf(str.ptr, str.bufsize, "%s(%d)", argv[0], argc);
}
printf("%s\n", str.ptr);
free(str.ptr);
str.ptr = NULL;
str.bufsize = 0;
return SUCCESS;
}
Figura 40. Ejemplo de código para controlar el tamaño de un buffer. Extraída de [1]

© Universidad Internacional de La Rioja (UNIR)


Tema 3: Codificación segura

Manipulación de Strings

Introducción

La estructura de datos de strings de C básica serie de caracteres terminada por el


carácter nulo, es propensa a error y además las funciones de biblioteca para
la manipulación de los mismos solo empeoran el panorama.

En el presente apartado se introduce al alumno en el estudio de los problemas de


seguridades relativas a las funciones de manipulación de strings y las de
segunda generación, más seguras pues limitan el número de datos copiados por
parámetro.

Funciones inherentemente peligrosas

Operaciones con strings de tamaño limitado

Fallos comunes en funciones limitadas

Errores de truncado

Mantenimiento del carácter nulo de terminación

Figura 41. Problemas con funciones que manipulan string.

Funciones inherentemente peligrosas


Específicamente hay que evitar el uso de funciones como gets(), scanf(), strcpy(), o
sprintf().
o gets(). Esta función lee de la entrada estándar una corriente de bytes y las alacena
el en array apuntado por s hasta que se encuentra el carácter de nueva línea o de
fin de fichero. Puede producir desbordamiento del buffer, que se le pasa como
argumento, si es más pequeño que la fuente de entrada que en esta caso es la
entrada estándar: el teclado. La función getws(), se comporta de la misma forma.

char gets(char *s)


char line[512];
gets(line);
Figura 42. Llamada insegura a gets () similar a la explotada por el gusano Morris.[1]

© Universidad Internacional de La Rioja (UNIR)


Tema 3: Codificación segura

o scanf(). Función que lee de la entrada estándar los caracteres que corresponden
al formato que se le especifica. Por ejemplo si se especifica %s, se leerán los
caracteres de la entrada en el buffer hasta que llegue un carácter no-ascii, pudiendo
ocasionar un desbordamiento del buffer. También se puede especificar % 255s
limitando a ese número de caracteres los que se pueden leer de la entrada y usarse
la función de un modo más seguro. Funciones de similar comportamiento son
fscanf(), wscanf().

scanf(const char *FORMAT [, ARG, ...])

char var[128], val[15 * 1024], ..., boundary[128], buffer[15 * 1024];


...
for(;;) {
...
// if the variable is followed by '; filename="name"' it is a file
inChar = getchar();
if (inChar == ';') {
...
// scan in the content type if present, but simply ignore it
scanf(" Content-Type: %s ", buffer);
Figura 43. Código de w3-msql 2.0.11 vulnerable a un desbordamiento de búfer remoto causada por
una llamada insegura a scanf (). Extraída de [1]

o strcpy(). A diferencia de las funciones anteriores opera en datos ya almacenados


en una variable de programa, lo cual hace que su manejo suponga obviamente
menos riesgo de seguridad. Esta función copia el contenido de un buffer en otro
hasta que se encuentra un carácter nulo en el buffer fuente, por tanto hay que tener
cuidado de que el buffer fuente sea más pequeño que el destino y que esté
terminado por el carácter nulo. Funciones con similar comportamiento son
wcscpy(), lstrcpy().

char strcpy(char *DST, const char *SRC)


char *FixFilename(char *filename, int cd, int *ret) {
...
char fn[128], user[128], *s;
...

© Universidad Internacional de La Rioja (UNIR)


Tema 3: Codificación segura

s = strrchr(filename,'/');
if(s) {
strcpy(fn,s+1);
Figura 44. Código del programa php.cgi en PHP / FI 2.0beta10 vulnerable a un desbordamiento de
búfer remoto causada por una llamada insegura para strcpy (). Extraída de [1]

o sprintf(). Para usarla de forma segura hay que comprobar que el buffer de destino
pueda acomodarse a la combinación de todos los argumentos de la fuente de
entrada, controlando los tamaños fuente-destino y las conversiones de
formato que se especifican. Si la longitud del argumento fuente es mayor que
la del destino se producirá un buffer overflow. Funciones de comportamiento
similar son fprintf(), printf(), swprintf().

int sprintf(char *STR, const char *FORMAT [, ARG, ...])

char speed[128];
...
sprintf(speed, "%s/%d", (cp = getenv("TERM")) ? cp : "",
(def_rspeed > 0) ? def_rspeed : 9600);
Figura 45. Código de la Versión 1.0 del daemon de Telnet Kerberos 5 que contiene un
desbordamiento de búfer debido a que la longitud de la variable de entorno TERM, que nunca se valida.
Extraída de [1]

Operaciones con strings de tamaño limitado


Muchas de las primeras vulnerabilidades descubiertas en C, C++ eran causa de
operaciones con strings. Cuando se revisó el estándar de C se introdujeron nuevas
funciones equivalentes, a las analizadas en el apartado anterior, con un parámetro que
limita el número de los datos a copiar en el destino. Conceptualmente el uso de estas
funciones con limitación de buffer destino se realiza reemplazando las antiguas
funciones con las nuevas:
strcpy(buf, src) → strncpy(buf, src, sizeof(buf))

La figura siguiente, muestra la equivalencia entre las funciones del primitivo estándar
de C y las nuevas funciones para controlar el tamaño del buffer para plataformas
Windows con Microsoft visual studio. La librería, Microsoft safe CRT library,
suministra funciones de manipulación de strings con límites con «_s» como sufijo.

© Universidad Internacional de La Rioja (UNIR)


Tema 3: Codificación segura

En algunos casos, la información podría estar dispersa a través del programa.


Considerar una llamada a strcpy():
strcpy(dest, src)

Para determinar la seguridad de la llamada, una herramienta debe buscar los caminos
en que el string almacenado en src podría ser más largo que la cantidad de espacio
asignado para dest. Esto quiere decir que hay que analizar donde dest es asignado y
de dónde viene el valor de src (así como la presencia o no de terminadores nulos). Lo
anterior podría requerir la exploración de muchos caminos dispares en el programa.
Ahora analizar strncpy():
strncpy (dest, src, dest_size);

Para asegurarse que ningún desbordamiento de buffer ocurre con esta función, una
herramienta tiene que asegurarse solo que dest_size no es más grande que la cantidad
de espacio asignado para dest. El tamaño y el contenido de src no importan. En la
mayoría de los casos, es mucho más fácil comprobar que dest y dest_size están de
acuerdo el uno con el otro.
Función no Función limitada equivalente Función limitada
limitada librería C librería C estándar equivalente Windows Safe
estándar CRT
char * gets(char *dst) char * fgets(char *dst, int bound, FILE char * gets_s(char *s, size_t
*FP) bound)
Int scanf(const char Ninguna errno_t scanf_s(const char *FMT
*FMT [, arg, ...]) [, ARG, size_t bound,...])
Int sprintf(char *str, Int snprintf(char *str, size_t bound, errno_t sprintf_s(char *dst,
const char *FMT [, arg, const char *FMT, [, arg, ...]) size_t bound, const char *FMT [,
...]) arg, ...]) w
char * strcat(char *str, char * strncat(char *dst, const char errno_t strcat_s(char *dst,size_t
const char *SRC) *SRC, size_t bound) bound, const char *SRC)
char * strcpy(char *dst, char * strncpy(char *dst, const char errno_t strcpy_s(char *dst, size_t
const char *SRC) *SRC, size_t bound) bound, const char *SRC)
Tabla 2 Funciones ilimitadas con sus correspondientes limitadas. [1].

El código de la figura siguiente, muestra una vulnerabilidad de desbordamiento de


buffer que es resultado del empleo incorrecto de funciones de manipulación de strings
ilimitadas strcat() y strcpy() en Kerberos 5 Versión 1.0.6, CERT CA-2000-06, [15].
Dependiendo de la longitud de cp y copy, con cualquiera de las llamadas final a
strcat() o a strcpy() podría desbordarse cmdbuf. La vulnerabilidad ocurre en
el código responsable de manipular un string de un comando, que inmediatamente
sugiere que pudiera ser explotable debido a su proximidad a la entrada de usuario. De
hecho, esta vulnerabilidad públicamente ha sido explotada para ejecutar órdenes no
autorizadas sobre sistemas comprometidos.

© Universidad Internacional de La Rioja (UNIR)


Tema 3: Codificación segura

if (auth_sys == KRB5_RECVAUTH_V4) {
strcat(cmdbuf, "/v4rcp");
} else {
strcat(cmdbuf, "/rcp");
}
if (stat((char *)cmdbuf + offst, &s) >= 0)
strcat(cmdbuf, cp);
else
strcpy(cmdbuf, copy);
Figura 46. Función ilimitada, vulnerabilidad en Kerberos 5. Extraída de [1]

La figura siguiente, muestra el mismo código de la Versión 1.0.7 posteriormente


liberado para Kerberos 5. En el código parcheado, las tres llamadas a strcat() y la
llamada a strcpy() han sido substituidas todas por equivalentes correctamente
limitadas para corregir la vulnerabilidad.

cmdbuf[sizeof(cmdbuf) - 1] = '\0';
if (auth_sys == KRB5_RECVAUTH_V4) {
strncat(cmdbuf, "/v4rcp", sizeof(cmdbuf) - 1 - strlen(cmdbuf));
} else {
strncat(cmdbuf, "/rcp", sizeof(cmdbuf) - 1 - strlen(cmdbuf));
}
if (stat((char *)cmdbuf + offst, &s) >= 0)
strncat(cmdbuf, cp, sizeof(cmdbuf) - 1 - strlen(cmdbuf));
else
strncpy(cmdbuf, copy, sizeof(cmdbuf) - 1 - strlen(cmdbuf));
Figura 47. Función limitada, vulnerabilidad corregida en Kerberos 5. Extraída de [1]

Fallos comunes en funciones limitadas

Las funciones de strings limitadas son más seguras que las funciones ilimitadas, pero
hay todavía mucho margen para el error. Los fallos de programación más comunes
que se pueden cometer con las funciones de string limitadas:
o El buffer de destino se desborda porque el límite depende del tamaño de los datos
de la fuente, más bien que del tamaño del buffer de destino.
o El buffer de destino se deja sin un terminador nulo.

© Universidad Internacional de La Rioja (UNIR)


Tema 3: Codificación segura

o El buffer de destino se desborda porque su límite se especifica como el tamaño total


del buffer más bien que como el espacio restante.
o El programa escribe a una posición arbitraria en la memoria porque el buffer de
destino no se termina con el carácter nulo y la función comienza a escribir en la
posición del primer carácter nulo en el buffer de destino.

Para evitar errores comunes con strncpy(), se pueden seguir dos directrices
simples:
o Usar límites seguros. Llamadas seguras a strncpy(), limitadas con un valor
obtenido del tamaño buffer de destino.
o Terminar con carácter nulo manualmente el buffer de destino
inmediatamente después de la llamada strncpy().

Para evitar casos de mal uso comunes y errores que usan strncat(), se pueden seguir
dos directrices:
o Usar límites seguros. Calcular el límite pasado a strncat() restando la longitud
actual del string de destino, reportado por strlen(), del tamaño total del buffer.
o Terminar manualmente con carácter nulo. Asegurarse que los buffers de la
fuente y el destino pasados a strncat(), contienen terminadores nulos.

Errores de truncado

Incluso cuando se usan correctamente, las funciones de strings limitadas pueden


introducir errores porque truncan los datos que exceden el límite especificado. Las
operaciones susceptibles a errores de truncamiento pueden o modificar los datos
originales o, más comúnmente, truncar los datos en el proceso de copiar los datos de
una posición a otra. Los efectos de errores de truncamiento son difíciles de predecir.

Los datos truncados podrían tener un significado inesperado o estar sintácticamente


o semánticamente mal formados de modo que operaciones subsecuentes sobre ellos
produzcan errores o un comportamiento incorrecto. Por ejemplo, si una
comprobación de control de acceso se realiza sobre un nombre de archivo y el
nombre del archivo posteriormente se trunca, el programa podría asumir que se
refiere a un recurso que el usuario tiene autorización para tener acceso. Un atacante
entonces puede usar esta situación para tener acceso a un recurso no disponible de
otra manera.

© Universidad Internacional de La Rioja (UNIR)


Tema 3: Codificación segura

El código de la figura siguiente, muestra un error de truncamiento de string que se


convierte en un problema de terminación. El error está relacionado con el empleo de
la función readlink() que no termina con carácter nulo su buffer destino y puede
devolver hasta el número de octetos especificados en su tercer argumento, el código
de la figura cae en la trampa demasiado común de terminar a mano con carácter nulo
la ruta expandida, buf, en este caso, un octeto más allá del final del buffer.

Este error por un byte denominado «off by one» podría ser inconsecuente,
dependiendo de lo que se almacena en la memoria justo más allá del buffer, pues
permanecerá con eficacia terminado por el carácter uno hasta que otra posición de
memoria sea superpuesta. Es decir, strlen(buf) devolverá solo el tamaño real del
buffer más uno, PATH_MAX + 1, en este caso.

Sin embargo, cuando buf posteriormente sea copiado en otro buffer con el valor de
vuelta de readlink() → len como el límite pasado a strncpy(), los datos en buf están
truncados y el buffer de destino se deja sin terminar con el carácter nulo. Este «error
de por uno» probablemente causará un serio desbordamiento de buffer.

char path[PATH_MAX];
char buf[PATH_MAX];
if(S_ISLNK(st.st_mode)) {
len = readlink(link, buf, sizeof(path));
buf[len] = '\0';
}
strncpy(path, buf, len);
Figura 48. Llamada a strncpy() que podría causar error de truncado. Extraída de [1]

Una de las decisiones más importantes para evitar errores de truncado, es


si el programa emplea asignación de memoria estática o dinámica. El
código que manipula strings, puede ser codificado para reasignar dinámicamente
buffers basados en el tamaño de los datos con los que operan y es buena
opción porque evita truncar datos en la mayoría de los casos. Dentro de los
límites de la memoria total del sistema, los programas que típicamente realizan la
asignación de memoria dinámica raras veces necesitan truncar datos. Los programas
que emplean la asignación de memoria estática deben escoger entre dos clases de
errores de truncamiento.

© Universidad Internacional de La Rioja (UNIR)


Tema 3: Codificación segura

Ninguna opción es tan deseable como la reasignación dinámica porque


cualquiera puede causar que el programa viole las expectativas del
usuario. Si los datos exceden la capacidad de un buffer existente, el programa debe
o truncar los datos para alinearlos con los recursos disponibles o rechazar realizar la
operación y exigir una entrada más pequeña.
La comparación entre el truncado y el fallo controlado es difícil. La más simple de las
dos opciones es rehusar realizar la operación solicitada, que probablemente no tendrá
ningún impacto inesperado sobre el resto del programa.

Sin embargo, esto puede causar una pobre utilidad si el sistema con frecuencia recibe
una entrada que no puede acomodar, o bien, si el programa trunca los datos y se sigue
ejecutando normalmente, pueden surgir una variedad de errores. Estos errores
típicamente se caen en dos campos: El string podría no tener el mismo significado
después de ser truncado o podría no estar terminado con carácter nulo. En la figura
siguiente, se muestra un resumen de diversas librerías seguras de manejo de cadenas.
Api Prohibidas Strsafe Safe C/C++
strcpy, wcscpy, _tcscpy, StringCchCopy, StringCbCopy, strcpy_s
_mbscpy, lstrcpy, lstrcpyA, StringCchCopyEx, StringCbCopyEx
lstrcpyW, strcpyA, strcpyW
StringCchCat, StringCbCat, strcat_s
strcat, wcscat
StringCchCatEx, StringCbCatEx
wnsprintf, wnsprintfA, StringCchPrintf, StringCbPrintf, sprintf_s
wnsprintfW StringCchPrintfEx, StirngCbPrintfEx
StringCchPrintf, StringCbPrintf, _snprintf_s
_snwprintf, _snprintf
StringCchPrintfEx, StirngCbPrintfEx _snwprintf_s
wvsprintf, wvsprintfA, StringCchVPrintf, StringCbVPrintf, _vstprintf_s
wvsprintfW, vsprintf StringCchVPrintfEx, StirngCbVPrintfEx
StringCchVPrintf, StringCbVPrintf, vsntprintf_s
_vsnprintf, _vsnwprintf
StringCchVPrintfEx, StirngCbVPrintfEx
StringCchCopyN, StringCbCopyN, strncpy_s
strncpy, wcsncpy
StringCchCopyNEx, StringCbCopyNEx
StringCchCatN, StringCbCatN, strncat_s
strncat, wcsncat
StringCchCatNEx, StringCbCatNEx
scanf, wcsanf Ninguna sscanf_s
strlen, wcslen, _mbslen, StringCchLength, StringCbLength strlen_s
_mbstrlen
Tabla 3 Librerías seguras equivalentes para manejo de strings. Extraídad de [1]

Conclusión:
Hay que evitar truncar los datos. Si la entrada proporcionada es demasiado grande
para una operación dada, hay que intentar manejar el problema redimensionando

© Universidad Internacional de La Rioja (UNIR)


Tema 3: Codificación segura

dinámicamente los buffers, o directamente rehusar realizar la operación e indicar al


usuario lo que tiene que ocurrir para que la operación tenga éxito.

Como una opción en el peor de los casos, truncar los datos e informar al
usuario que el truncado ha ocurrido. Las funciones de string de Microsoft
Strsafe y las librerías seguras CRT (figura 103) facilitan la identificación
y reporte de errores. Ambos conjuntos de funciones ponen en práctica las
comprobaciones en tiempo de ejecución que hacen que las funciones fallen y se
invoquen los correspondientes manejadores de error cuando ocurren errores de
truncado y otros.

Mantenimiento del carácter nulo de terminación

En C, los strings dependen de la terminación nula apropiada, sin ello, su tamaño no


puede ser determinado. Esta dependencia es frágil porque se confía en el contenido
del string para asegurar que las operaciones realizadas sobre él se comportan
correctamente.

Los errores de terminación de strings fácilmente pueden conducir a


desbordamientos y errores lógicos. Estos problemas normalmente se hacen más
insidiosos porque ocurren aparentemente de manera no determinista
dependiendo del estado de la memoria cuando el programa se ejecuta. Durante una
ejecución de un bloque de código, la memoria que sigue a una variable de string no
terminada, podría ser nula y enmascarar el error completamente, mientras que en una
ejecución subsecuente del mismo bloque de código, la memoria que sigue al string
podría ser no nula y causar operaciones sobre el string que se comportan
erróneamente. Considerar el código de la figura siguiente, Como readlink() no
termina con carácter nulo su buffer de salida, strlen() lee la memoria hasta que
encuentra un carácter nulo y por lo que a veces produce valores incorrectos de
longitud, dependiendo del contenido de memoria después de buf.
char buf[MAXPATH];
readlink(path, buf, MAXPATH);
int length = strlen(buf);
Figura 49. String no terminado en carácter nulo producido por readlink().
Extraída de [1]
Los strings incorrectamente terminados, normalmente se introducen en un programa de
varias formas:

© Universidad Internacional de La Rioja (UNIR)


Tema 3: Codificación segura

o Un pequeño conjunto de funciones, como RegQueryValueEx() y readlink(),


intencionadamente producen strings no terminados.
o Ciertas funciones que copian strings de un buffer a otro, como strncpy(), tienen
que terminar el buffer de destino con un carácter nulo.
o Funciones que leen octetos genéricos de fuera del programa, como fread() y
recvms(), pueden usarse para leer strings. Como estas funciones no distinguen
entre strings y otras estructuras de datos, no garantizan que los strings terminarán
con carácter nulo.

Nunca hay asumir que los datos del mundo exterior están correctamente terminados
con carácter nulo. Si el string no se termina correctamente, la adición de un octeto
nulo en el último octeto del buffer impedirá a otras operaciones como
calcular mal la longitud del string o desbordar el buffer.

L A C A D E N A M A S L A R \0 Terminación
manual

Una táctica común para prevenir errores de terminación de string debe


inicializar un buffer entero a cero y luego para limitar todas las
operaciones para conservar al menos el octeto final nulo. Esta táctica es un
sustituto pobre de la terminación explícita manual porque se confía en el
programador para especificar límites correctos. Tal estrategia es aventurada porque
cada operación sobre el string proporciona una oportunidad de introducir un bug.

Errores de formato de cadena (format strings)

Los errores de formato de string ocurren:

Cuando se permite al usuario influir o no definir el argumento del tipo de dato a


mostrar en la entrada estándar, durante la declaración de la función encargada de la
manipulación de strings.

En este apartado se repasan las vulnerabilidades de formato de strings y se perfilan


los modos en que ocurren. Al final se enseña cómo se puede explotar una
vulnerabilidad de formato de string típica para analizar cómo los atacantes
aprovechan estas vulnerabilidades. Para dar una visión previa, el código de la figura
siguiente, presenta un ejemplo que contiene una vulnerabilidad de formato de string
clásica.

© Universidad Internacional de La Rioja (UNIR)


Tema 3: Codificación segura

Una función de formato es un tipo especial de función ANSI C, que toma un número
variable de argumentos, de los cuales uno es la llamada cadena de formato. Los
Format String son simples cadenas, caracterizadas por el formato que se les aplica.
Los tipos de formato a aplicar son los siguientes:

Par. Salida Pasado como


%d. Formato de enteros. Valor
%f Formato de punto flotante. Valor
%s Formato cadena. Referencia
%x Formato hexadecimal. Valor
%p Muestra el correspondiente valor del puntero. Referencia
%n En el entero correspondiente a esta especificación de formato se Referencia
almacena el número de caracteres hasta ahora escritos en el búfer.
<n>$ Parámetro de acceso directo Valor
Tabla 4 Parámetros de formato utilizados en lenguaje C.

Vulnerable.c:
#include <stdio.h>
int main(void) {
char texto[30];
scanf(“%29s”, texto);
printf(texto);
return 0;
}
Figura 50. Ejemplo que contiene una vulnerabilidad de formato de string clásica. Extraída de [14].

Como se muestra en negrita el usuario no ha especificado que la variable «texto» sea


mostrada como de tipo cadena de caracteres (string), al no especificar el tipo de
dato mediante la sintaxis «%s». Este es el motivo por la que el código presentaría
una vulnerabilidad de tipo format string. Las especificaciones de formato más
utilizadas en este tipo de ataque son:
 %x y %s. Para acceder a los contenidos de las direcciones de memoria de la
pila.
 %n. Para escribir directamente en de las direcciones de memoria de la pila

Otro ejemplo real que contiene una vulnerabilidad de formato de string clásica del
demonio de FTP popular wuftpd (que ha sufrido de numerosas vulnerabilidades).

© Universidad Internacional de La Rioja (UNIR)


Tema 3: Codificación segura

while (fgets(buf, sizeof buf, f)) {


lreply (200, buf);
...
}
void lreply(int n, char *fmt, ...) {
char buf[BUFSIZ];
...
vsnprintf(buf, sizeof buf, fmt, ap);
...
}
Figura 51. Clásica vulnerabilidad de format String en wuftpd 2.6.0. [1]

Una de las vulnerabilidades de formato de string más conocidas fue encontrada en la


forma en que la función lreply() es invocada en la Versión 2.6.0, de wuftpd que, de
una forma simplificada, se parece al código de la figura anterior.

En el código vulnerable, un string es leído desde un socket de red y pasado


sin validación como argumento de formato string a la función vsnprintf().
Enviando un comando especial al servidor, un atacante puede remotamente explotar
esta vulnerabilidad para obtener privilegios de root de la máquina.

El problema es que el argumento de formato de string no es requerido si


no se quiere usar y de esta forma los programadores pueden utilizar por
ejemplo printf(str) en lugar de printf,(”%s”,str). Un atacante podría
incluir conversiones de tipo string mientras que el programador asumía
que no contendría nada. En el caso más benigno un ataque incluiría
especificaciones de conversión diseñadas a leer valores del stack y conseguir acceso a
información no autorizada. En casos más serios el atacante puede usar la
directiva %n para escribir varias posiciones de memoria lo que posibilita
todos los ataques más usuales de buffer overflow modificando la
dirección de retorno del stack, modificando el valor del puntero a una función,
etc.

Formas de prevención:
o Pasar siempre un argumento estático a cualquier función que acepte un argumento
de formato de string.

© Universidad Internacional de La Rioja (UNIR)


Tema 3: Codificación segura

o Si un simple formato de string es demasiado restrictivo, definir un conjunto de


formatos válidos y hacer selecciones de este conjunto seguro.
o Si una situación realmente exige que un formato de string incluya una entrada leída
desde fuera el programa, realizar un riguroso whitelist validando cualquier valor
leído desde fuera del programa que se incluye en el formato de string.

El código de la figura siguiente, muestra la corrección del código de wuftpd 2.6.0 con
vulnerabilidad de formato de string de la figura anterior.

while (fgets(buf, sizeof buf, f)) {


lreply(200, "%s", buf);
...
}
void lreply(int n, const char* fmt, ...) {
char buf[BUFSIZ];
...
vsnprintf(buf, sizeof buf, fmt, ap);
...
}
Figura 52. Vulnerabilidad de format String en wuftpd 2.6.0 corregida. [1]

En el corazón de muchos ataques de formato de string está la directiva %n, que


causa que el número de caracteres procesados, sean escritos en la
memoria. Otra característica importante de la directiva %n es que escribe el número
de octetos que deberían haber sido escritos en vez del número real que se especificó.
Esto es importante cuando la salida es escrita a un tamaño fijo de string y es truncado
debido a una carencia de espacio disponible. Incluso con capacidad de escribir valores
a la memoria, un atacante debe tener dos cosas para montar un ataque sumamente
eficaz:
o El control de la posición de la memoria a la cual la directiva %n escribe.
o El control del valor que la directiva %n escribe.

A causa del modo en que los frames del stack se construyen, el primer desafío
fácilmente puede conseguirse normalmente colocando la dirección objetivo para la
directiva % n en el formato de string que se procesa. Como los caracteres
probablemente serán copiados inalterados en el stack, hay que tener cuidado con los
datos específicos de conversión de números que pueden hacer que la función de

© Universidad Internacional de La Rioja (UNIR)


Tema 3: Codificación segura

formato de string interprete el valor suministrado por el atacante como una


dirección en la memoria y escribir el número procesado de octetos a la posición
especificada.

Un análisis del código binario de ejecución o mediante tentativas de prueba-


error, es necesario para determinar la posición de la dirección objetivo en la
memoria. Ahora que el atacante puede controlar la posición en la memoria a la cual la
directiva % n escribirá, todo lo que le queda es escribir valores interesantes en esa
posición. Este desafío es más fácil porque el atacante puede especificar una longitud
arbitraria de campo como parte de los datos.
/* Format string en un buffer*/
#include <stdio.h>
Int main(int argc, char **argv)
Ejecución Root%./format_string
{ “\x58\x74\x04\x08%d%n"
char buffer[100]; buffer (5): X1
x is 5/x05 (@ 0x8047458)
int x;
if(argc != 2) • Al principio snprintf copia los primeros cuatro bytes en buf.
• A continuación, escanea el formato "% d" e imprime el valor de x.
exit(1); • A continuación escanea "% n“ que extrae el valor de la pila "\ x58
\ x74 \ x04 \ x08", o interpretados como un entero, 0x08047458
x = 1; (dir var x)
• snprintf luego escribe la cantidad de bytes de salida hasta el
snprintf(buffer, sizeof buffer, argv[1]); momento, cinco.

buffer[sizeof buf - 1] = 0;
printf("buffer (%d): %s\n", strlen(buffer), buffer);
printf("x is %d/%#x (@ %p)\n", x, x, &x);
return 0
Figura 53. Error de format string
La combinación de anchura de campo y la característica de que la directiva %n
escribirá el número de los caracteres que habrían sido procesados,
independientemente de cualquier truncamiento que podría haber ocurrido, da al
atacante la capacidad de construir valores arbitrariamente grandes. Una versión aún
más sofisticada de este ataque usa cuatro %n directivas para controlar el valor de un
puntero.

Librerías alternativas de manejo de strings


Considerar usar una biblioteca de strings alternativa que puede ser una gran solución
para prevenir muchas vulnerabilidades de desbordamiento de buffer, porque extraen
las mejores prácticas para operaciones sobre strings seguras. En la figura siguiente,
se muestra una serie de librerías alternativas de manejo de strings más seguras.

© Universidad Internacional de La Rioja (UNIR)


Tema 3: Codificación segura

Linux Windows
Bstrlib: Vstr: http://www.and.org/vstr/
http://bstring.sourceforge.net/
FireString: http://firestuff.org/ Safe CRT:
http://msdn.microsoft.com/msdnmag/
issues/05/05/safecandc/default.aspx
GLib: http://developer.gnome.org StrSafe:
http://msdn2.microsoft.com/en-
us/library/ms995353.aspx
Libmib:
http://www.mibsoftware.com/libmib/
astring/
SafeStr: http://www.zork.org/safestr/
Tabla 5 Librerías alternativas de manejo de strings para C para Windows y Linux. [1]

Integers overflows, errores de truncado y problemas con conversiones de


tipo entre números enteros

Integer overflow
Todos los tipos de representación de números enteros tienen una limitada capacidad
debido a que representan con un limitado número de bits. Esto es un hecho que
normalmente se les olvida a los programadores y pueden ocasionar errores de tipo
integer overflow al intentar almacenar un valor demasiado grande en la
variable asociada que sobrepase esos límites inferior y superior o un
valor grande positivo se convierta en un valor grande negativo y viceversa,
generando un resultado inesperado (valores negativos, valores inferiores, etc.).

En el documento de INTECO «Software Exploitation» [14] se indica que:

Este tipo de error puede tener consecuencias graves cuando el valor que genera
el integer overflow es resultado de alguna entrada de usuario (es decir, que
puede ser controlado por el mismo) y cuando, de este valor, se toman decisiones
de seguridad, se toma como base para hacer asignaciones de memoria, índice
de un array, concatenar datos, hacer bucles. etc. Este ha sido el caso de
vulnerabilidades como CVE-2001-014459 en SSH1 y que permitiría a un
atacante ejecutar código arbitrario con los privilegios del servicio ssh; o algunos
más recientes como CVE-2011-2371 en Mozilla Firefox.

Un integer overflow puede causar varios tipos de problemas pero en C, C++ puede
conducir a un ataque de buffer overflow que podría ocurrir cuando una variable

© Universidad Internacional de La Rioja (UNIR)


Tema 3: Codificación segura

wrapped-around se usa para reservar memoria, se limita una operación sobre strings
o como índice de un buffer. Integers overflows también pueden ocurrir en Java, pero
debido a que Java fuerza la comprobación de seguridad de la memoria son más
difíciles de explotar.

El código de la figura siguiente, muestra un ejemplo de integer overflow que se da con


una función que reserva memoria para un string con un tamaño que se lee de un
fichero. Antes de reservar el buffer, varias líneas de código comprueban que el tamaño
no es mayor de 1024 y decrementa su valor en uno para no copiar el carácter de nueva
línea del fichero.

Sin embargo dependiendo del valor de readamt, decrementar su valor podría causar
resultados erróneos, si se declara sin signo y un atacante ocasiona que getstringsize()
devuelva «0», malloc() será invocada con 4294267295 que es el valor más grande en
una representación de 32-bit y la operación fallará por insuficiencia de memoria.

unsigned int readamt;


readamt = getstringsize();
if (readamt > 1024)
return -1;
readamt--; // don't allocate space for '\n'
buf = malloc(readamt);
Figura 54. Integer overflow. [1]

Otro error de tipo aritmético, es el siguiente:


int ConcatBuffers(char *buf1, char *buf2, size_t len1, size_t len2){
char buf[0xFF];
if((len1 + len2) > 0xFF) return -1; 0x103 len1
memcpy(buf, buf1, len1); + 0xFFFFFFFC len2
memcpy(buf + len1, buf2, len2); 0xFF

// do stuff with buf

return 0;
}
Figura 55. Error aritmético. Extraída presentación Microsoft «Basic of Secure Development Test»

© Universidad Internacional de La Rioja (UNIR)


Tema 3: Codificación segura

La dos funciones mencpy intentan realizar una copia > de 255 bytes.

Errores de truncado

Otro tipo de problemas que se pueden presentar en operaciones con números


que se pueden truncar cuando un tipo de dato entero con gran cantidad
de bits se convierte a otro con menor cantidad de bits, C por ejemplo desecha
los bits de mayor orden, si un entero con signo se convierte desde otro más pequeño
en número de bits, los bits extra se rellenan de tal forma que el número nuevo
conserve el mismo signo.

Suponiendo que se tienen cuatro bits para representar enteros con y sin signo se
tendría:

0 0 0 1 1 0 0 0 1 0 0 0

1 1 1 1 1 0 0 0
0 0 0 1 1 0 0 0
Extensión signo
Truncado

Figura 56. Error de truncado que ocurre cuando un entro de 8-bit es truncado a uno de 4 bits y una
de extensión de signo cuando un entero de 4 bit es truncado a un entero con signo de 8 bits. [1]

Esto significa que si un número negativo es convertido a un tipo de datos más grande,
su signo será el mismo, pero su valor aumentará considerablemente porque sus bits
más significativos se modificarán.

Si un programador no entiende cuándo y cómo las conversiones tienen lugar pueden


tener lugar vulnerabilidades. Conversiones entre enteros con signo y sin signo.

No pueden representar el mismo número de valores y solo algunos valores pueden ser
convertidos desde un dato de tipo sin signo a otro con signo y viceversa sin cambiar el
significado, mientras otros no. En el caso de valores positivos el problema es que el 50
% de valores sin signo requieren poner a uno el bit más significativo.

1 0 0 0 =-8 1 0 0 0 = 15
Con signo Sin signo
Figura 57. Error que ocurre cuando un entero de 4 bit es truncado de con signo a sin signo y
viceversa. [1]

© Universidad Internacional de La Rioja (UNIR)


Tema 3: Codificación segura

En el código de la figura siguiente, se procesa la entrada de usuario de una serie de


strings de longitud variable almacenadas en una posición simple char*. Los primeros
2 octetos de entrada dictan el tamaño de la estructura a ser procesada. El programador
ha puesto una cota superior en el tamaño del string: la entrada será procesada solo si
len es menor o igual a 512. El problema es que len está declarado como tipo short (con
signo) entonces la comprobación contra la longitud de string máxima es una
comparación con signo. Más tarde, len implícitamente se convierte a un número
entero sin signo en la llamada a memcpy(). Si el valor de len es negativo, aparecerá
que el string tiene un tamaño apropiado (se toma la alternativa if), pero la cantidad
de memoria copiada por memcpy() será muy grande (mucho más grande que 512) y
el atacante será capaz de desbordar buf con los datos en strm.

char* processNext(char* strm) {


char buf[512];
short len = *(short*) strm;
strm += sizeof(len);
if (len <= 512) {
memcpy(buf, strm, len);
process(buf);
return strm + len;
}
else {
return -1;
}
}
Figura 58. Integer overflow causado por conversión signo-sin signo. [1]

Métodos para detectar y prevenir intergers overflows:


o Usar tipos sin signo.
o Esperar malas suposiciones.
o Restringir la entrada numérica de usuario.
o Comprobar los valores usados para acceder y reservar memoria.
o Respetar los warnings del compilador.
o Entender las reglas de conversión entre enteros.
o En GCC6:

6
The GNU Compiler Collection incluye front ends para C, C++, Objective-C, Fortran, Java, Ada, and Go,
así como librerias para esos lenguajes (libstdc++, libgcj,...). http://gcc.gnu.org/

© Universidad Internacional de La Rioja (UNIR)


Tema 3: Codificación segura

– Habilitar opciones de warning de conversiones de tipo: -Wconversion,


-Wsign-compare.
– Investigar el uso de directivas como #pragma GCC diagnostic ignored
option, que deshabilitan warnings del compilador relativos a enteros.
– Habilitar las comprobaciones de tiempo de ejecución de enteros con:
-ftrapv.
o Verificar las precondiciones y postcondiciones con operadores que
puedan causar desbordes.

3.5. Errores y excepciones

Introducción

Los problemas de seguridad muchas veces comienzan cuando un atacante busca la forma
de violar las expectativas del programador, que muchas veces dan menos
importancia a las condiciones de error y manejo de excepciones. Por el contrario es uno
de los aspectos que tienen en cuenta los atacantes.

C usa códigos de error que generan las funciones.


C++ usa una combinación de códigos de error y excepciones sin comprobar.
Java usa excepciones comprobadas.

Manejar errores mediante códigos de retorno

El lenguaje C usa el valor de retorno de una función para comunicar el éxito o el fracaso.
Esta aproximación tiene varios efectos secundarios no deseables:
Hace fácil no hacer caso de errores simplemente no haciendo caso del valor de retorno
de una función.
Conectar la información de error con el código para manejar el error hace los
programas más difíciles de leer. La lógica de manejo del error se mezcla con la lógica
para manejar casos esperados, que aumentan la tentación de ignorar errores.
No hay ninguna convención universal para comunicar la información de error, por
tanto los programadores deben investigar el mecanismo que maneja para cada
función.

Vale la pena notar que estos son algunos motivos para que los diseñadores de C++ y Java
incluyeran excepciones como un elemento del lenguaje. El efecto que tiene ignorar un

© Universidad Internacional de La Rioja (UNIR)


Tema 3: Codificación segura

código de error como en el ejemplo de la figura siguiente, es que el programador espera


que buf terminará en nulo, pero si no es así debido, por ejemplo, a un I/O error, como
no se notifica el error la siguiente llamada a strcpy() puede resultar en buffer overflow
(buf no tiene terminador nulo).

char buf[10], cp_buf[10];


fgets(buf, 10, stdin);
strcpy(cp_buf, buf);
Figura 59. No se comprueba el código de retorno. [1]
El código de la figura siguiente, muestra el de la figura anterior corregido comprobando
el código de retorno.

char buf[10], cp_buf[10];


char* ret = fgets(buf, 10, stdin);
if (ret != buf) {
report_error(errno);
return;
}
strcpy(cp_buf, buf);
Figura 60. Se comprueba el código de retorno. [1]

En Java, los errores y los eventos inusuales generan una excepción en la


mayoría de los casos, sin embargo las clases stream y reader no consideran insólito o
excepcional leer menos datos que los que el programador solicitó. Estas clases
simplemente agregan cualquier dato disponible al buffer de retorno y pone en el valor de
retorno al número de octetos o caracteres leídos. No hay ninguna garantía de que
la cantidad de datos devueltos es igual a la suma de datos solicitados. Este
comportamiento hace necesario para programadores examinar el valor de retorno de
read() y otros métodos de I/O para asegurar que reciben la cantidad de datos que
esperan.

El código Java de la figura siguiente, recorre un conjunto de usuarios, leyendo un fichero


de datos privado de cada usuario. El programador asume que los archivos tienen siempre
exactamente 1 kb de tamaño y por tanto, no hace caso del valor de retorno de read(). Si
un atacante puede crear un archivo más pequeño, el programa reutilizará el resto de los
datos del archivo anterior, haciendo que el código trate los datos de otro usuario como si
los datos pertenecieran al atacante.

© Universidad Internacional de La Rioja (UNIR)


Tema 3: Codificación segura

FileInputStream fis;
byte[] byteArray = new byte[1024];
for (Iterator i=users.iterator(); i.hasNext();) {
String userName = (String) i.next();
String pFileName = PFILE_ROOT + "/" + userName;
FileInputStream fis = new FileInputStream(pFileName);
try {
fis.read(byteArray); // El fichero es siempre de 1k bytes
processPFile(userName, byteArray);
}
finally {
fis.close();
}

Figura 61. No se comprueba el código de retorno del método read().[1]

El código de la figura siguiente, muestra el de la anterior corregido. Comprueba el código


de retorno del método read().

FileInputStream fis;
byte[] byteArray = new byte[1024];
for (Iterator i=users.iterator(); i.hasNext();) {
String userName = (String) i.next();
String pFileName = PFILE_ROOT + "/" + userName;
fis = new FileInputStream(pFileName);
try {
int bRead = 0;
while (bRead < 1024) {
int rd = fis.read(byteArray, bRead, 1024 - bRead);
if (rd == -1) {
throw new IOException("fichero es inusualmete pequeñ0");
}
bRead += rd;
}
}
finally {

© Universidad Internacional de La Rioja (UNIR)


Tema 3: Codificación segura

fis.close();
}
// se podría añadir código para comprobar si el fichero es demasiado largo
processPFile(userName, byteArray) ;
}
Figura 62. Se comprueba el código de retorno del método read().[1]

Manejo de errores mediante excepciones

Las excepciones solucionan muchos problemas de manejo de errores. Aunque sea fácil
ignorar el valor de retorno de una función simplemente omitiendo la comprobación del
código, no hacer caso de una excepción comprobada requiere justo lo
contrario, un programador tiene que escribir el código expresamente para ignorarlo.
Las excepciones también permiten la separación entre el código que sigue
un camino esperado y el código que maneja circunstancias anormales.

Las excepciones tienen dos versiones, la diferencia es relativa a si el compilador usa


análisis estático para asegurar que la excepción es manejada:
Cheked. Si un método declara que lanza una excepción checked, todos los objetos
que lo utilizan deben o manejar la excepción o declarar que lo lanzan también. Esto
fuerza al programador a pensar en excepciones checked en cualquier parte donde
pudieran ocurrir. Los compiladores de java hacen cumplir las reglas en cuanto a
excepciones checked, y la biblioteca de clases de Java hace un empleo liberal de
excepciones checked. La clase Java java.lang.Exception es una excepción checked.

Unchecked. Las excepciones unchecked no tienen que ser declaradas o manejadas.


Todas las excepciones en C++ son unchecked, que quiere decir que un programador
podría completamente ignorar el hecho de que las excepciones son posibles y el
compilador no se quejaría. Java ofrece excepciones unchecked, también. El peligro
con excepciones unchecked consiste en que los programadores podrían ser
inconscientes de que una excepción puede ocurrir en un contexto dado y podría omitir
el manejo del error apropiado. Por ejemplo, la función alloca() de windows asigna
memoria en el stack. Si una petición de asignación es demasiado grande para el
espacio de stack disponible, alloca() lanza una excepción de desbordamiento de stack
unchecked. Si la excepción no es capturada, el programa fallará, potencialmente
posibilitando un ataque de denegación de servicio.

Siempre que sea posible:

© Universidad Internacional de La Rioja (UNIR)


Tema 3: Codificación segura

Capturar las excepciones en el nivel superior, las excepciones se pueden capturar o


volver a lanzar.
No incluir sentencias return en el bloque finally de manejo de la excepción, el
programa se comporta como si ninguna excepción hubiera ocurrido.
Capturar solamente lo que se esté preparado para manejar. La captura de todas las
excepciones en el nivel superior es una buena idea, pero la captura de las excepciones
dentro de un programa después de una gran cantidad de código puede causar
problemas.
No dejar excepciones checked con el bloque catch vacío. Si no se sabe qué hacer para
tratar una excepción capturada se puede lanzar un excepción que por lo menos
notifica que algo raro ha ocurrido:

catch (RareException e) {
throw RuntimeException("Esto nunca debe ocurrir ", e);
}
Figura 63. Excepción con el bloque catch relleno.

Desasignar los recursos que no se van a utilizar en adelante

No liberar recursos como memoria, ficheros, conexiones de red, etc. puede


causar problemas de rendimiento. Este concepto se conoce como «resource
leaks» que supone un riesgo de seguridad pues favorecen claramente ataques de
denegación de servicio. En cualquier lenguaje que se utilice se tiene que procurar
desasignar cualquier recurso que no se vaya a utilizar en adelante en el resto de la
aplicación. En el código de la figura siguiente, se muestra un ejemplo de memory leak y
su versión corregida que libera la memoria previamente reservada en la figura posterior.

char* getBlock(int fd) {


char* buf = (char*) malloc(BLOCK_SIZE);
if (!buf) {
return NULL;
}
if (read(fd, buf, BLOCK_SIZE) != BLOCK_SIZE) {
return NULL;
}
return buf;
}
Figura 64. No se libera la memoria de buf. [1]

© Universidad Internacional de La Rioja (UNIR)


Tema 3: Codificación segura

char* getBlock(int fd) {


char* buf = (char*) malloc(BLOCK_SIZE);
if (!buf) {
goto ERR;
}
if (read(fd, buf, BLOCK_SIZE) != BLOCK_SIZE) {
goto ERR;
}
return buf;
ERR:
if (buf) {
free(buf);
}
return NULL;
}
Figura 65. Se libera la memoria de buf al producirse el error. [1]

Registro y depuración

Ambas operaciones de registro y depuración implican llegar a comprender bien qué


ocurre durante la ejecución de programa, en particular cuando se experimentan
errores o condiciones inesperadas. En este apartado, se sugieren una serie de prácticas a
realizar, las ventajas de hacer log constante y de depurar fallos del código de producción.

Usar un framework de log centralizado como log4j o el paquete java.util.logging. Un


framework centralizado hace más fácil lo siguiente:
Proporcionan una vista constante y uniforme del sistema reflejado en los logs.
Facilita cambios, como el movimiento del log a otra máquina, conmutación de registro
a un archivo, del registro a una base de datos, o la puesta al día de medidas de
privacidad o validación de actualización.

Se recomienda:
Relacionar las entradas de log con marcas de tiempo (time-stamp).
Registrar cada acción importante.
Proteger los logs.
Diseñar ayudas de depuración de fallos y no tener puertas traseras de acceso en el
entorno de producción. Agregar con cuidado código de depuración de fallos dentro

© Universidad Internacional de La Rioja (UNIR)


Tema 3: Codificación segura

del sistema de modo que se pueda asegurar que nunca aparece en un despliegue de
producción. El problema es que si este comportamiento ampliado abandona el
ambiente de depuración de fallos y va a producción, puede tener un efecto
potencialmente degradante sobre la seguridad del sistema. La depuración de fallos del
código no recibe el mismo nivel de revisión y prueba, como el resto del programa y
raras veces se escribe con idea de estabilidad, o de seguridad en mente.
El acceso al código por puertas traseras es un caso especial de eliminar fallos del
código. El código de acceso back-door está diseñado para permitir a desarrolladores e
ingenieros de prueba tener acceso a una aplicación de un modo que no se permite para
el usuario final. Normalmente es necesario probar los componentes de una aplicación
aislada o antes de que el sistema sea desplegado en su ambiente de producción. Sin
embargo, si el código back-door es enviado a producción, puede dar a atacantes
nuevos caminos de ataque a la aplicación que no fueron considerados durante el
diseño o las pruebas.
Seguir una adecuada política de backup que permita proteger los ficheros de backup
de ser accedidos.
Easter eggs, son elementos de aplicaciones ocultos que se añaden para diversión de
los programadores. Un Easter egg elaborado podría revelar un videojuego oculto
embebido en la aplicación. ¿Cómo determinar si una vulnerabilidad en un Easter egg
es intencionada por parte del programador o no? Lo mejor es no tolerarlos.

3.6. Privacidad y confidencialidad

En este apartado se va a analizar cómo hay que manejar las contraseñas, tanto las
que utilizan los usuarios para autenticarse (intbound passwords) como las que utiliza un
programa que actúa como cliente para autenticarse frente a un servicio final (outbound
passwords), como una base de datos o un servidor LDAP. Intbound passwords pueden
ser hashed, sin embargo, outbound passwords deben ser accesibles al programa en texto
claro. Para profundizar sobre intbound passwords se recomienda la lectura de Security
Engineering [16].

Mantener las passwords fuera del código fuente. Cifrar las contraseñas con
algoritmo seguro y almacenarlas fuera del código. La figura siguiente, muestra un trozo
de código que muestra un mal uso de password codificada dentro de código Java.

public class DbUtil {

© Universidad Internacional de La Rioja (UNIR)


Tema 3: Codificación segura

public static Connection getConnection() throws SQLException {


return DriverManager.getConnection(
"jdbc:mysql://ixne.com/rxsql", "chan", "0uigoT0");
}
}
Figura 66. Password hardcoded. [1]

Una buena estrategia consiste en:

Cifrar las passwords con una pública implementación de algoritmos


criptográficos robustos y almacenarlas cifradas en un fichero de
configuración. Almacenar la clave necesaria para descifrar la password en un
fichero separado donde la aplicación pueda acceder pero no la mayoría de los
administradores de sistemas. A continuación se muestra una utilidad de java para
cifrar y descifrar passwords utilizando secret key.

import javax.crypto.*;
import java.security.*;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.IOException;
public class PasswordProtector {
final static String CIPHER_NAME = "AES";
final static Reader in = new BufferedReader(
new InputStreamReader(System.in));
/*
Esta utilidad tiene tres modos:
* 1) Generar una nueva clave:
* PasswordProtector nuevo
* Genera una nueva clave e imprime en stdout. Usted puede
* utilizar esta clave en operaciones posteriores.
* 2) Cifrar una contraseña:
* PasswordProtector cifrar <clave> <contraseña>
* Cifra la contraseña e imprime los resultados en stdout.
* 3) Descifrar una contraseña:
* PasswordProtector descifrar <clave> <contraseña cifrada>

© Universidad Internacional de La Rioja (UNIR)


Tema 3: Codificación segura

* Descifra una contraseña y la imprime en stdout.


*
* Este código hace uso de la fuente abierta Base64
* (http://iharder.sourceforge.net/base64/).
*/
public static void main(String[] args) throws Exception {
String cmd = args[0];
String out = "commands are 'new', 'encrypt' and 'decrypt'";
Key k;
if ("new".equals(cmd)) {
out = StringFromKey(makeKey());
}
else if ("encrypt".equals(cmd)) {
k = keyFromString(getString("Entra la clave"));
String pswd = getString("Entra la password");
out = encryptPassword(k, pswd);
}
else if ("decrypt".equals(cmd)) {
k = keyFromString(getString("Entra la clave"));
String enc = getString("Entra la password encryptada ");
out = decryptPassword(k, enc);
}
System.out.println(out) ;
}
private static String getString(String msg) throws IOException {
System.out.print(msg + ": ");
return new BufferedReader(in).readLine();
}
/* generar las nuevas claves */
private static Key makeKey() throws Exception {
Cipher c = Cipher.getInstance(CIPHER_NAME);
KeyGenerator keyGen = KeyGenerator.getInstance(CIPHER_NAME);
SecureRandom sr = new SecureRandom();
keyGen.init(sr); return keyGen.generateKey();
}
private static Key keyFromString(String ks) {
return (Key) Base64.decodeToObject(ks);

© Universidad Internacional de La Rioja (UNIR)


Tema 3: Codificación segura

}
private static String StringFromKey(Key k) {
return Base64.encodeObject(k, Base64.DONT_BREAK_LINES);
}
/* Cifrar la contraseña dada con la clave. Se codifica el texto cifrado en Base64 */
private static String encryptPassword(Key k, String passwd) throws Exception {
Cipher c = Cipher.getInstance(CIPHER_NAME);
c.init(Cipher.ENCRYPT_MODE, k);
byte[] bytes = c.doFinal(passwd.getBytes());
return Base64.encodeObject(bytes);
}
/*Descifrar una contraseña cifrada (asume que el texto cifrado es codificado en base64*/
private static String decryptPassword(Key k, String encrypted) throws Exception {
byte[] encryptedBytes;
encryptedBytes = (byte[]) Base64.decodeToObject(encrypted);
Cipher c = Cipher.getInstance(CIPHER_NAME);
c.init(Cipher.DECRYPT_MODE, k) ;
return new String(c.doFinal(encryptedBytes));
}
}
Figura 67. Utilidad de java para cifrar y descifrar passwords utilizando secret key

Usar passwords robustas. Usar generadores de números aleatorios seguros para las
passwords de conexiones outbounds. Hay un compromiso entre los conceptos de
robustez y de facilidad, a la hora de elegir un password. Las outbound passwords no
requieren ser recordadas deben ser largas y aleatoriamente generadas usando un método
seguro de generación de números aleatorios.

Números aleatorios. Si se consigue una buena fuente de generación de números


aleatorios se tiene una buena fuente de secreto. La generación de nuevos secretos es una
parte crítica en el establecimiento de muchas clases de relaciones de confianza. Los
números aleatorios son importantes en la criptografía.

Son también importantes para los identificadores de sesión, y cualquier situación en la


cual la seguridad depende de tener un valor que es difícil de adivinar. John Von
Neumann dijo, «Alguien que intenta producir números aleatorios usando puramente
cálculos aritméticos está en un estado de pecado».

© Universidad Internacional de La Rioja (UNIR)


Tema 3: Codificación segura

Los computadores son máquinas intrínsecamente deterministas y no son una fuente de


aleatoriedad. Si un programa requiere un volumen grande de valores realmente
arbitrarios, hay que pensar en comprar un generador de números aleatorios
hardware. Los generadores de pseudo-números aleatorios caen en dos categorías:

PRNGS estadísticos, que producen valores que son uniformemente distribuidos a


través de rango especificado, pero son calculados usando algoritmos que crean una
corriente de valores fácil de reproducir y triviales de predecir. Los PRNGS estadísticos
pueden ser convenientes solo para las aplicaciones en las cuales los valores no tienen
que ser difíciles de adivinar, como sistemas de simulación o identificadores internos
únicos.

PRNGS criptográficos, usan algoritmos criptográficos para ampliar la entropía de


una semilla de valor aleatorio en una corriente de valores imprevisibles que, con alta
probabilidad, no puede ser distinguido de un valor realmente arbitrario. Los PRNGS
criptográficos son convenientes cuando la imprevisibilidad es importante, incluyendo
los usos siguientes:
o Criptografía.
o Generación de contraseñas.
o Aleatoria de puertos para seguridad.
o Identificadores externos únicos (identificadores de sesión).
o Códigos de descuento.

Los errores más comunes y más obvios relacionados con números aleatorios ocurren
cuando se usa PRNG estadístico en una situación que exige que los valores sean
sumamente imprevisibles y por tanto producidos por PRNG criptográfico. Otro error
ocurre cuando un PRNG no se crea con entropía insuficiente, causando que la corriente
de números sea predecible. En ambos casos, el PRNG producirá una corriente fiable
de valores que un atacante puede adivinar y potencialmente comprometer la seguridad
del programa. El resto de la sección se dirige a cómo evitar estos errores en java y en C.

Números aleatorios en Java. Java tiene métodos de generación de números


aleatorios estadísticos y criptográficos:

Método estadístico: Random.nextInt().

© Universidad Internacional de La Rioja (UNIR)


Tema 3: Codificación segura

String generateCouponCode(String couponBase) {


Random ranGen = new Random();
ranGen.setSeed((new Date()).getTime());
return(couponBase + Gen.nextInt(400000000));
}
Figura 68. Función aleatoria no segura. [1]

Clase con métodos criptográficos: Java.security.SecureRandom. El algoritmo


por defecto y las fuentes de entropía usadas por SecureRandom son opciones buenas
para la mayor parte de aplicaciones de software y los usuarios raras veces deberían
tener que proporcionar su propia semilla. Como la entropía inicial usada para
alimentar el PRNG es integral a la aleatoriedad de todos los valores que genera, es
crítico que esta semilla inicial proporcione una cantidad suficiente de entropía.

String generateCouponCode(String couponBase) {


SecureRandom secRanGen = new SecureRandom();
return(couponBase + secRanGen.nextInt());
}
Figura 69. Función aleatoria segura. [1]

Números aleatorios en C y C++. C/C++ tienen métodos de generación de


números aleatorios estadísticos y aleatorios:

o Funciones estadísticas: evitar utilizar rand(), srand(), srand48(), drand48(),


lrand48(), random(), srandom().

o Funciones criptográficas: Sobre plataformas de Microsoft, rand_s() (del API


de seguridad de Microsoft CRT) y CryptGenRandom() (de Microsoft CryptoAPI)
proporcionan el acceso a valores criptográficamente fuertes arbitrarios producidos
por el subyacente RtlGenRandom() PRNG. Una precaución: a pesar de la carencia
de pruebas empíricas de lo contrario, la fuerza de los valores arbitrarios que
Microsoft CryptoAPI produce está basada solo en las propias reclamaciones a
Microsoft. La puesta en práctica no ha sido públicamente examinada.

3.7. Programas privilegiados

© Universidad Internacional de La Rioja (UNIR)


Tema 3: Codificación segura

Introducción

La mayor parte de los programas se ejecutan con un conjunto de privilegios


heredados del usuario con el que se ejecutan. Por ejemplo, un editor de textos
puede mostrar solo los archivos que su usuario tiene el permiso de lectura.
Los programas privilegiados tienen privilegios adicionales que les permiten
realizar operaciones que sus usuarios de otra forma no realizarían.
Cuando está escrito correctamente, un programa privilegiado concede a
usuarios regulares una cantidad limitada de accesos a algún recurso
compartido, como la memoria física, un dispositivo de hardware, o archivos
especiales como el archivo de contraseñas o la base de datos de correo.

Cuando está escrito incorrectamente, un programa vulnerable privilegiado deja a


los atacantes campo para actuar. En el peor caso, esto puede permitir un ataque de
escalada de privilegios vertical, en el cual el agente malicioso gana acceso incontrolado
a los privilegios elevados del programa.

Otro tipo común de ataque conocido como «escalada de privilegios horizontal»,


ocurre cuando un atacante engaña a los mecanismos de control de acceso de una
aplicación, para tener acceso a recursos que pertenecen a otro usuario. La figura
siguiente, ilustra la diferencia entre la escalada de privilegio vertical y horizontal.
Administrador

Escalada Privilegios
Alto Vertical
Privilegio

Bajo Escalada Privilegios


Privilegio Horizontal

Atacante Usuario

Figura 70. Ataques de elevación de privilegios vertical y horizontal. [1]

La mayoría de los programas privilegiados, son programas de sistemas


implementados en C que se ejecutan en Linux o Unix como root, lo cual requiere una
gran cantidad de esfuerzo defenderse de estos programas.

© Universidad Internacional de La Rioja (UNIR)


Tema 3: Codificación segura

Principio de mínimos privilegios

Evidentemente es uno de los principales principios de diseño, como ya se comentó en el


tema 1, que a la hora de implementar el código hay que ser lo más consecuente posible
con lo que se especificó durante la fase de análisis de requisitos. La motivación detrás de
este principio está clara: el privilegio es peligroso. Cuantos más privilegios tiene
un programa, mayor daño potencial puede causar.
Un conjunto reducido de acciones posibles disminuye el riesgo potencial de un
programa. La mejor gestión de privilegios, es ninguna gestión de privilegio. El modo más
fácil de prevenir vulnerabilidades relacionadas con el privilegio es diseñar los sistemas
que no requieren componentes privilegiados. Cuando se aplica al código, el principio
de mínimos privilegios implica dos cosas:
Los programas no deberían requerir que sus usuarios tengan privilegios
extraordinarios para realizar tareas ordinarias.

Los programas privilegiados deberían reducir al mínimo la cantidad de daño que


pueden causar cuando algo va mal.

La primera implicación ha sido un verdadero problema sobre las plataformas de


Microsoft Windows, donde, sin privilegios de administrador, más del 90 % del
software de Windows no se instalará y más del 70 % fallará en ejecutarse correctamente
[17]. El resultado es que la mayor parte de usuarios de Windows corren con privilegios
sumamente elevados o privilegios de administrador. Microsoft reconoce la cuestión y
comienza la transición hacia usuarios que no corren con privilegios de administrador en
Windows Vista y posteriores con un componente llamado Control de Cuenta de
Usuario (UAC).

Bajo UAC, los usuarios todavía tienen privilegios de administrador, pero los programas
se ejecutan con privilegios disminuidos por defecto. Los programas todavía pueden
ejecutarse con privilegios de administrador, pero el usuario explícitamente debe
permitirles hacerlo así. Esto fuerza a los desarrolladores de software a escribir
programas diseñados para correr con privilegios menos elevados, pero la transición
tardara muchos años.

Al contrario, en UNIX y sistemas Linux, la mayor parte de usuarios disponen de


privilegios restringidos la mayor parte del tiempo. Los privilegios que un programa tiene

© Universidad Internacional de La Rioja (UNIR)


Tema 3: Codificación segura

son controlados por el usuario y el grupo IDs que heredan cuando se ejecutan o aquellos
que se cambian posteriormente. Los programas deben manejar su IDs activos para
controlar los privilegios que ellos tienen en cualquier tiempo dado. El modo privilegiado
más común da un acceso root al programa (el control administrativo total). Se
denominan root setuid a tales programas. Las operaciones que un programa realiza
limitan la capacidad de reducir al mínimo los privilegios.

Dependiendo de la funcionalidad del programa y de sus necesidades de privilegio,


se pueden elevar y bajar sus privilegios en puntos diferentes de su ejecución. Los
programas requieren privilegios para una variedad de motivos:
Comunicarse directamente al hardware.
Modificar el comportamiento del OS.
Enviar a señales a ciertos procesos.
Trabajar con recursos compartidos.
o Apertura de puertos de red con baja numeración.
o Cambio de la configuración global (el registro y/o archivos).
Protecciones de sistema de archivos principales.
Instalación de nuevos archivos en directorios de sistema.
Puesta al día de archivos protegidos.
Tener acceso a archivos que pertenecen a otros usuarios.

Las transiciones entre estados privilegiados y no privilegiados definen el perfil de


privilegio de un programa. El perfil de privilegio de un programa típicamente puede
ser colocado en una de las cuatro clases siguientes:
Programas normales que corren con los mismos privilegios que sus usuarios.
Ejemplo: Emacs.
Programas de sistema que corren con privilegios de root para la duración de su
ejecución. Ejemplo: Init.
Programas que necesitan privilegios de root para usar un conjunto fijo de
recursos del sistema cuando se ejecutan al principio. Ejemplo: Apache httpd, que
necesita el acceso de root para usar puertos bajos numerados.
Programas que requieren privilegios de root intermitentemente a lo largo
de su ejecución. Ejemplo: Un demonio de FTP, que usa puertos de baja numeración
intermitentemente en todas partes de su ejecución. En la figura siguiente, se
representa un esquema de los ejemplos mencionados y cuándo necesitarían
privilegios de root.

Emac
s

© Universidad Internacional de La Rioja (UNIR)


Tema 3: Codificación segura

Root

Usuario

Init
Root

Usuario
Apache
Root

Usuario

Ftp
Root

Usuario

Figura 71. Ejemplo de utilización de privilegios de root en la ejecución de varios programas. [1]

El concepto de mínimos privilegios tiene implicaciones diferentes para cada uno de


estos tipos de programas. Los programas que corren con los mismos privilegios que sus
usuarios trivialmente se adhieren al principio mínimos privilegios sin discusión.

Como contraposición están programas que se ejecutan con privilegios elevados. Los
programas que no proporcionan la funcionalidad de sistema de bajo nivel raras veces
requieren el privilegio de root continuo, y un programa setuid que nunca reduce su
privilegio podría ser sintomático de un mal diseño. Si algunas operaciones de un
programa no requieren privilegios de root, la llave para la consecución de mínimos
privilegios correctamente, es reducir la cantidad de código que funciona con privilegios
de root al mínimo necesario. Incluso si un programa elimina sus privilegios de un modo
recuperable, un agente maliciosos podría encontrar un modo de recuperarlos como parte
de un ataque. Idealmente, los programas deberían ser diseñados de tal modo que
permanentemente puedan eliminar privilegios. Con un demonio de FTP por ejemplo,
esto no es siempre posible.

El concepto de seguridad que la mayoría de la gente debería repetir es «No confiar en


nada». Aunque esto sea un ideal que es imposible de realizar, no hay nada que debería
ser tan perseguido enérgicamente como un programa privilegiado.

Los atacantes usarán para montar ataques de escalada de privilegios:


Los argumentos al programa.

© Universidad Internacional de La Rioja (UNIR)


Tema 3: Codificación segura

El entorno de ejecución del programa.


Los recursos de que el programa depende (sistema de ficheros).

Un ataque de este tipo podría aprovechar solo un pequeño resquicio, en la defensa de un


programa privilegiado, para que un atacante pueda tomar el mando del sistema.

Manejo de los privilegios

Los errores en la gestión de privilegios son a menudo errores de omisión. En este


apartado, se analiza cómo los programas pueden cambiar sus privilegios y algunos
errores que ocurren en el transcurso de estas operaciones con privilegios. Los privilegios
sobre UNIX y sistemas Linux principalmente son controlados por la clase de ID de
usuario, que gobierna las decisiones de control de acceso.
Cada proceso UNIX tiene tres IDs de usuario:
o Real user ID correspondiente al usuario que creó el proceso.
o Efffective user ID usado para tomar decisiones de control de acceso.
o Saved user ID mantiene uid inactivo que es recuperable.

Cada proceso también tiene tres IDs de grupo:


o The real group ID (real gid, or rgid)
o The effective group ID (effective gid, or egid)
o The saved group ID (saved gid, or sgid)

Como los IDs de grupo tienen impacto en los privilegios y se manejan del mismo modo
que el IDs de usuario, se enfocan como los IDs de usuario y se excluyen los IDs de grupo
de los ejemplos de este apartado en favor de la simplicidad.

Los procesos LINUX tienen un fsuid y un fsgid, que se usan para decisiones de control
de acceso sobre recursos del sistema de archivos y otras operaciones privilegiadas.
Permiten distinguir entre los privilegios usados para acceder a los sistemas de ficheros y
otras operaciones privilegiadas. Estos valores típicamente permanecen sincronizados
con el euid y egid, y por lo general juegan un papel importante en la gestión de privilegios.

Los privilegios sobre sistemas de UNIX comienzan con el login Shell de usuario. El
programa login comienza a ejecutarse como root. Después de autenticar al usuario, su

© Universidad Internacional de La Rioja (UNIR)


Tema 3: Codificación segura

cometido es lanzar el login Shell del usuario con los IDs de usuario y de grupo
apropiados.

Como los procesos non-setuid heredan los IDs de usuario y de grupo del proceso que los
invoca, login debe cambiar sus propios IDs de usuario y de grupo antes de que este
invoque el login shell del mismo. Los sistemas UNIX proporcionan una familia de
funciones con sufijos uid y gid para cambiar el ID de usuario y de grupo que un programa
usa. El comportamiento complejo de estas funciones no está totalmente estandarizado a
través de todas las plataformas y ha conducido a una variedad de bugs relacionados
con la gestión de privilegios. El resto de este apartado trata del empleo correcto de
funciones de gestión de privilegios de sistemas UNIX.

La figura siguiente, lista cuatro funciones de gestión de privilegios comunes, con una
breve descripción de su semántica desde la correspondiente descripción de la ayuda man
de Linux. Aunque seteuid(), setuid(), y setreuid() están definidas en el estándar POSIX y
está disponible en todas las plataformas, la mayor parte de su funcionalidad se deja a
puestas en práctica individuales.
Función Prototipo Descripción
int setuid(uid_t uid) Establece el ID de usuario efectivo del proceso actual. Si el UID efectivo del
usuario que llama es root, también se establece el UID real y el ID de usuario
configurado.
int seteuid(uid_t euid) Establece el ID de usuario efectivo del proceso actual. Los procesos de usuario
no privilegiados sólo pueden establecer el ID de usuario efectivo para el ID del
usuario real, el ID de usuario efectivo o el ID de usuario de grupo.

int setreuid(uid_t Establece los ID de usuario reales y efectivos del proceso actual. Si se establece
ruid, uid_t euid) el ID de usuario real o el ID de usuario efectivo se establece en un valor que no
es igual al ID de usuario real anterior, el ID de usuario guardado se establecerá
en el nuevo ID de usuario efectivo. Proporciona un valor de -1 para el ID de
usuario real o efectivo y obliga al sistema a dejar ese ID sin cambios. Los
procesos no privilegiados sólo pueden establecer el ID de usuario efectivo para
el ID de usuario real, el ID de usuario efectivo o el ID de grupo de usuarios.
int setresuid(uid_t Establece el ID de usuario real, el ID de usuario efectivo y el ID de grupo del
ruid, uid_t euid, proceso actual. Proporcionar un valor de -1 para el ID de usuario real o efectivo
uid_t suid) y obliga al sistema a dejar ese ID sin cambios. Los procesos de usuario sin
privilegios pueden cambiar el UID real, el UID efectivo y el ID de grupo.
Tabla 6 Prototipos de funciones de gestión de privilegios en LINUX. [1]

Esto conduce a una variedad de errores comunes y malos usos de los cuales se habla
más adelante. La función de gestión de privilegios más interesante, setresuid(), ofrece
una semántica clara y está disponible sobre muchos Sistemas Operativos, como Linux
modernos y distribuciones de Unix, pero no sobre Solaris.

© Universidad Internacional de La Rioja (UNIR)


Tema 3: Codificación segura

A causa de restricciones de plataforma, setresuid() debe ser evitado en código que


tenga que ser sumamente portátil. Con todas estas opciones y sutilezas en el
comportamiento, no es sorprendente que los problemas relacionados con la gestión de
privilegios sean tan comunes.

En plataformas donde está definida, es preferible setresuid() para manejar privilegios


porque proporciona un comportamiento más simple y mejor definido. La función
requiere que el programador explícitamente declare cuál de los tres IDs de usuario
individual debe ser modificado y garantizar que la llamada tendrá un total o ningún
efecto: Si cualquiera de los IDs de usuario proporcionados se cambian, todos ellos son
cambiados. Sobre Solaris y otras plataformas donde setresuid() no está disponible, usar
seteuid() para hacer modificaciones que cambian solo el ID effective user y usar
setreuid() para las modificaciones que afectan a los tres IDs de usuario. La figura
siguiente, muestra como un programa simple privilegiado podría ser estructurado
temporalmente para eliminar privilegios, adquirirlos de nuevo para realizar una
operación privilegiada, y luego eliminarlos permanentemente cuando no se van a
necesitar más.

#include <string>
#include <iostream>
#include "task/tarea.h"

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


uid_t caller_uid = getuid();
uid_t owner_uid = geteuid();
/* Drop privileges right up front, but we'll need them back in a
little bit, so use effective id */
if (setresuid(-1, caller_uid, owner_uid) != 0) {
exit(-1);
}
/* Privileges not necessary or desirable at this point */
processCommandLine(argc, argv);
/* Regain privileges */
if (setresuid(-1, owner_uid, caller_uid) != 0) {
exit(-1);
}

© Universidad Internacional de La Rioja (UNIR)


Tema 3: Codificación segura

openSocket(88); /* requires root */


/* Drop privileges for good */
if (setresuid(caller_uid, caller_uid, caller_uid) != 0) {
exit(-1);
}
doWork();
}
Figura 72. Ejemplos de gestión de privilegios 1: setresuid(). [1]

Muchos errores en programas privilegiados se derivan del mal uso de las funciones de
gestión de privilegios. Aunque setresuid() se comporta consistentemente en las
plataformas donde está soportado, muchas otras funciones de gestión de privilegios no
lo hacen. A pesar del hecho de que se incluyen en el estándar POSIX, setuid() y setreuid()
tienen matices en su implementación dependiendo de la plataforma. Hay que tener
cuidado con las inconsistencias en cambios de plataforma, en particular con
programas que cambian entre dos usuarios con IDs no raíz. En los sistemas operativos
Linux y Solaris, si el ID effective user no es 0, el nuevo ID usuario pasado a setuid() debe
igualar el ID real user o el ID saved user. Esto quiere decir que la tentativa de poner a los
tres IDs de usuario, al actual ID, podría fallar en algunas circunstancias. Sobre FreeBSD,
esta operación siempre tiene éxito. Incluso sobre una sola plataforma, los efectos
implícitos que estos métodos pueden tener sobre los IDs real user y saved user hacen
su semántica confusa y propensos a errores.

Evitar setuid() totalmente y usar setreuid() solo cuando setresuid() esté


disponible y cambiar tanto los IDs tanto real como effective.

No importa la plataforma y funciones que se usan, siempre prestar atención al valor de


retorno de una llamada a una función de gestión de privilegios. Si los atacantes pueden
impedir que una transición de privilegios tenga lugar, podrían ser capaces de
aprovechar el hecho de que el programa se ejecuta bajo inesperadas condiciones. Algunas
funciones son más propensas al fallo que otras y los factores que podrían hacer que su
funcionamiento fuera correcto, varían de una plataforma a otra. No obviar una simple
comprobación del valor de retorno pensando que el comportamiento de una función no
puede sorprender.

Los atacantes podrían ser capaces de hacer que las funciones de gestión de privilegios
fallen agotando recursos o cambiando el modo en que un programa se ejecuta [18].

© Universidad Internacional de La Rioja (UNIR)


Tema 3: Codificación segura

Cuando una llamada a una función de gestión de privilegios falla de improviso, hay que
asegurarse de que el comportamiento del programa es seguro. Si la operación que falla
es un intento de eliminar privilegios, el programa debe parar su ejecución más bien que
seguir ejecutándose con mayores privilegios que los deseados.

Para combatir la difícil naturaleza de la gestión de privilegios, existe un conjunto de


funciones wrapper que proporcionan un interfaz consistente y fácilmente entendible
[19]. Los autores advierten que la mayoría de programas privilegiados realizan tres
operaciones claves sobre sus privilegios:
Eliminar privilegios con la intención de recuperarlos.
Eliminar privilegios permanentemente.
Recuperar privilegios previamente almacenados.

Con estas tres operaciones en mente, los autores diseñaron un API para ejecutar estas
operaciones y no otras. Se da un prototipo de función y una breve descripción para cada
una de las funciones en la Figura 66, el ejemplo de la figura 67, muestra como un
programa simple privilegiado se puede escribir de dos formas distintas: con
setresuid() y con este API [19]. Aunque estos ejemplos pongan en práctica una gestión
de privilegios correcta en general, su seguridad podría mejorarse deshabilitando señales
antes de la elevación de sus privilegios, la figura siguiente, muestra un ejemplo con la
versión corregida totalmente con deshabilitación de señales de este código.

Función Prototipo Descripción


Int drop_priv_temp(uid_t Drop privileges temporarily. Move the privileged user
new_uid) ID from the effective uid to the saved uid. Assign new
uid to the effective uid.
intdrop_priv_perm(uid_t Drop privileges permanently. Assign new uid to all the
new_uid) real uid, effective uid, and saved uid.
Int restore_priv() Copy the privileged user ID from the saved uid
to the effective uid.
Tabla 7 API de funciones para gestión de privilegios. [19]

int main(int argc, char** argv) {


/* Drop privileges right up front, but we'll need them back in a
little bit, so use effective id */
if (drop_priv_temp(getuid()) != 0) {
exit(-1);

© Universidad Internacional de La Rioja (UNIR)


Tema 3: Codificación segura

}
/* Privileges not necessary or desirable at this point */
processCommandLine(argc, argv);
/* Regain privileges */
if (restore_priv() != 0) {
exit(-1);
}
openSocket(88); /* requires root */
/* Drop privileges for good */
if (drop_priv_perm(getuid()) != 0) {
exit(-1);
}
doWork();
}
Figura 73. Ejemplos de gestión de privilegios 2: API. [1]

int main(int argc, char** argv) {


uid_t caller_uid = getuid();
uid_t owner_uid = geteuid();
int sigmask;
sigset_t maskall, saved;
sigfillset(&maskall);
/* Drop privileges right up front, but we'll need them back in a
little bit, so use effective id */
if (setresuid(-1, caller_uid, owner_uid) != 0){
exit(-1);
}
/* Privileges not necessary or desirable at this point */
processCommandLine(argc, argv);
/* disable signal handling */
if (sigprocmask(SIG_SETMASK, &maskall, &saved) != 0) {
exit(-1);
}
/* Regain privileges */
if (setresuid(-1, owner_uid, caller_uid) != 0) {
sigprocmask(sigmask);
exit(-1);

© Universidad Internacional de La Rioja (UNIR)


Tema 3: Codificación segura

}
openSocket(88); /* requires root */
/* Drop privileges for good */
if (setresuid(caller_uid, caller_uid, caller_uid) != 0) {
sigprocmask(sigmask);
exit(-1);
}
/* re-enable signals */
if (sigprocmask(SIG_SETMASK, &saved, NULL) != 0) {
exit(-1);
}
doWork();
}
Figura 74. Ejemplo de gestión de privilegios deshabilitando señales antes de elevar privilegios. [1]

Otra aproximación de gestión de privilegios de acuerdo con el principio mínimos


privilegios es dividir un programa en componentes privilegiados y no
privilegiados. Si las operaciones que requieren privilegios pueden ser categorizadas en
un proceso separado, esta solución puede reducir bastante el trabajo requerido para
asegurar que los privilegios se administran adecuadamente. Crear dos procesos: uno
para realizar la mayor parte del trabajo ejecutándose sin privilegios, y un segundo que se
ejecuta con privilegios y realiza un número limitado de operaciones. En muchas
situaciones tiene la ventaja añadida de que el número de las líneas de código que se
ejecuta con privilegios es mucho más pequeño.

Para un ejemplo de división de privilegios, referirse al trabajo de Provos, Friedl, y


Honeyman sobre el proyecto OpenSSH de Privilegios Separados en la Universidad de
Michigan [20]. El equipo dividió OpenSSH en procesos privilegiados y no privilegiados
y el proyecto oficialmente fue adoptado en la distribución OpenBSD.

Restringir privilegios sobre los sistemas de ficheros

Además de la gestión explícita de privilegios y el control de acceso basado en el ID de


Usuario, se puede restringir los privilegios de un programa limitando su visión del
sistema de archivos usando la función de sistema chroot(). Después una correcta
invocación a chroot(), un proceso no puede tener acceso a ningún archivo fuera del árbol
de directorio especificado para esa llamada. Se denomina jaula chroot a tal entorno y

© Universidad Internacional de La Rioja (UNIR)


Tema 3: Codificación segura

se usa comúnmente para prevenir la posibilidad de que se pueda corromper un proceso


y usarlo para tener acceso a archivos protegidos. Por ejemplo, muchos servidores de
FTP se ejecutan en jaulas chroot para prevenir a un atacante que descubre una nueva
vulnerabilidad en el servidor de ser capaz de descargar otros archivos sobre el sistema.

En sistemas FreeBSD, hay que pensar en usar la utilidad jaula, que pone en práctica
protecciones aún más fuertes que la jaula chroot [21].

El empleo impropio de chroot() puede permitir a atacantes escaparse de la jaula chroot.


Lamentablemente, hay al menos cuatro modos fáciles de causar problemas con chroot().
El código es responsable de leer un nombre de archivo de la red, abre el archivo
correspondiente sobre la máquina local y de enviar el contenido sobre la red, lo cual
podría usarse para implementar el comando FTP GET. Para limitar el daño que un bug
podría causar, el código intenta crear una jaula chroot antes de responder a alguna
solicitud. Esta tentativa de crear una jaula chroot tiene cuatro problemas:

Llamar a chroot() no afecta a ningún descriptor de archivo que está actualmente


abierto, entonces se puede apuntar a archivos fuera de la jaula chroot. Para ser seguro,
cerrar cada archivo abierto antes de la llamada chroot().

La llamada a la función chroot(), no cambia el directorio actual de trabajo del proceso,


entonces rutas relativas como ./../../../../etc/passwd todavía pueden referirse a
recursos del sistema de archivos fuera de la jaula chroot después de que se ha llamado
a chroot(). Siempre seguir una llamada a chroot() con la llamada chdir ("/"). Verificar
que cualquier error de manejo de código entre la llamada a chroot() y chdir() no abre
ningún archivo y no devuelve el control a ninguna otra parte del programa que no
sean rutinas de parada. Las funciones chroot() y chdir() deberían estar tan cerca como
sea posible para limitar la exposición a esta vulnerabilidad.

Es también posible y recomendable, crear una jaula apropiada chroot llamando a


chdir() primero. La llamada a chdir() entonces puede tomar un argumento constante
("/"), que es más fácil verificar durante una revisión de código.

Una llamada a chroot() puede fallar. Comprobar el valor de retorno de chroot() para
asegurarse del resultado de la llamada.

Para llamar chroot(), el programa debe ejecutarse con privilegios de root. En cuanto
la operación privilegiada se ha completado, el programa debería eliminar los

© Universidad Internacional de La Rioja (UNIR)


Tema 3: Codificación segura

privilegios de root y retornar a los privilegios del usuario de invocación. El código en


el ejemplo de la figura 69, sigue ejecutándose como root.

chroot("/var/ftproot");

if (fgets(filename, sizeof(filename), network) != NULL) {


if (filename[strlen(filename) - 1] == '\n') {
filename[strlen(filename) - 1] = '\0';
}
localfile = fopen(filename, "r");
while ((len = fread(buf, 1, sizeof(buf), localfile)) != EOF) {
(void)fwrite(buf, 1, len, network);
}
}
Figura 75. Código de un simple FTP server. [1]

// close all open file descriptors


for (int i = 0; i < sysconf(_SC_OPEN_MAX); i++) {
if (close(i) != 0) {
exit(-1);
}
}
// call chroot, check for errors,
// then immediately call chdir
if ((chroot("/var/ftproot") != 0) || (chdir("/") != 0)) {
exit(-1);
}
// drop privileges
if (drop_priv_temp(getuid()) != 0) {
exit(-1);
}
if (fgets(filename, sizeof(filename), network) != NULL) {
if (filename[strlen(filename) - 1] == '\n') {
filename[strlen(filename) - 1] = '\0';
}
localfile = fopen(filename, "r");
while ((len = fread(buf, 1, sizeof(buf), localfile)) != EOF) {
(void)fwrite(buf, 1, len, network) ;

© Universidad Internacional de La Rioja (UNIR)


Tema 3: Codificación segura

}
}
Figura 76. Código de un simple FTP server reescrito para crear una jaula chroot correctamente. [1]

Manejo de excepciones en programas privilegiados

Hay que tener en cuenta una serie de directrices generales para manejar eventos
inesperados y excepciones que toman una importancia aún mayor en programas
privilegiados, donde los ataques son más probables y el coste de un exploit consumado
es mayor.

Comprobar cada condición de error. Muchos bugs al principio misteriosos, son


consecuencia de una llamada de sistema fallida con un valor de retorno ignorado. Los
atacantes frecuentemente se aprovechan de condiciones de error inesperadas
para violar las suposiciones de los programadores, que pueden ser particularmente
peligrosas en código que se ejecuta con privilegios. El software eventualmente se
ejecuta en distintos sistemas operativos, con diferentes versiones de sistema
operativo, configuraciones de hardware, o entornos de ejecución, sufre con mayor
probabilidad de condiciones de error inesperadas. Si una función devuelve un código
de error o cualquier otra prueba de su éxito o fracaso, siempre hay que comprobar las
condiciones de error, incluso si no hay ningún indicio obvio de que el error pueda
ocurrir.

Seguridad sobre robustez: terminación ante errores. No intentar reponerse


de errores inesperados o mal entendidos. Cuando los errores ocurren realmente, la
seguridad prima sobre la robustez. Esto quiere decir que el programa debería o parar
la acción que actualmente realiza o detenerse completamente. Si hay pruebas
razonables de que un error ocurrió debido a alguna actividad malévola, no hay que
permitir que el supuesto atacante tenga la capacidad de interactuar con el programa.
Al mismo tiempo, siempre hay que procurar que los fallos se produzcan de forma
segura, un programa que termina en un estado que expone información sensible o
ayuda a atacantes de algún otro modo, es hacer el trabajo de los atacantes más fácil.

Inhabilitar señales antes de la elevación de privilegios. De esta forma se evita


tener señales que controlan código que se ejecuta con privilegios. Rehabilitar las
señales después volver a los privilegios estándar que tenía el usuario. Los manejadores
de Señales y los procesos hijos se ejecutan con los mismos privilegios del proceso

© Universidad Internacional de La Rioja (UNIR)


Tema 3: Codificación segura

padre, por tanto si un proceso se ejecuta con privilegios de root cuando una señal se
activa o un subproceso es ejecutado, el manejador de la señal o el subproceso se
ejecutará con privilegios de root. El código que maneja las señales nunca debería
requerir privilegios elevados.

Desde luego, un manejador de señales bien escrito debería ser bastante pequeño y
bastante simple, de forma que no pueda causar errores cuando se ejecuta con privilegios:
manejadores de señales que tienen una complejidad mínima son siempre un objetivo que
vale la pena.

Ataques de escalada de privilegios

A veces un ataque contra un programa privilegiado se realiza por un usuario legítimo


del sistema. Normalmente un atacante usa un proceso de tres pasos para obtener
privilegios sobre una máquina.
Intenta encontrar una debilidad en un servicio de red o en una cuenta de
usuario mal protegida y obtiene acceso a una cuenta con bajos privilegios sobre la
máquina.

Usa este acceso de bajos privilegios para explotar una vulnerabilidad en un


programa privilegiado y tomar el control de la máquina.
Los ataques de escalada de privilegios pueden tener como objetivo cualquier variedad de
vulnerabilidades de software, en este apartado, se tratan las clases de vulnerabilidades
que son principalmente un riesgo en programas privilegiados:
Condiciones de carrera de acceso a archivos.
Permisos de archivo débiles.
Archivos temporales inseguros.
Inyección de comandos.
Mal uso de descriptores de archivo estándar.

Los ataques que aprovechan algunas de estas vulnerabilidades requieren la capacidad de


ejecutar un programa privilegiado directamente y controlar el uid de los programas. Los
otros son igualmente peligrosos para programas que se ejecutan con el usuario root u
otro usuario privilegiado. Todas las vulnerabilidades comparten el hecho de que confían
en la capacidad de un atacante de actuar recíprocamente con un programa privilegiado
a través de su entorno local.

© Universidad Internacional de La Rioja (UNIR)


Tema 3: Codificación segura

Ataque de condiciones de carrera en el acceso a ficheros

Se encuentran entre un grupo de vulnerabilidades conocidas como time-of-check,


time-of-use (TOCTOU), MITRE clasifica este tipo de vulnerabilidades con el CWE
367. Los errores generados como consecuencia de una condición de carrera
se producen por el cambio que experimenta el estado de un recurso
(ficheros, memoria, registros, etc.) cuando un programa intenta verificar
una propiedad y más tarde toma una decisión suponiendo que la propiedad
no ha cambiado. Los ataques TOCTOU sobre vulnerabilidades del sistema de archivos
generalmente siguen esta secuencia:


Un programa comprueba alguna propiedad de un archivo,
refiriéndose al archivo por su nombre más bien que por el
objeto del sistema de fichas subyacente

Un atacante cambia el significado del nombre del archivo
que el programa comprobó de modo que refiera un objeto
del sistema de archivos diferente.

El programa más tarde realiza una operación sobre el
sistema de archivos que usa el mismo nombre del archivo y
asume que la propiedad previamente comprobada se
mantiene
Figura 77. Secuencia ataque TOCTOU
La figura siguiente, muestra el modo que las operaciones en lpr podrían intercalarse con
la ejecución del código de un agente malicioso de un ataque con éxito. Un atacante
invoca lpr con el argumento/tmp/attack y redirecciona el archivo para que apunte al
fichero de sistema/etc/shadow durante el tiempo que pasa entre que es comprobado y
utilizado.
Tiempo Lpr Atacante
Tiempo 1 access(“/tmp/attack”)
Tiempo 2 unlink(“/tmp/attack”)
Tiempo 3 symlink(“/etc/shadow”,“/tmp/attack”)
Tiempo 4 open(“/tmp/attack”)
Figura 78. Esquema de tiempos de una vulnerabilidad TOCTOU en el acceso a un fichero. [1]

Aunque dos operaciones pudieran parecer secuenciales en el código original de un


programa, cualquier número de instrucciones puede ser ejecutado entre ellas.

© Universidad Internacional de La Rioja (UNIR)


Tema 3: Codificación segura

for (int i=1; i < argc; i++) {


/* make sure that the user can read the file, then open it */
if (!access(argv[i], O_RDONLY)) {
fd = open(argv[i], O_RDONLY);
}
print(fd);
}
Figura 79. Ejemplo de código con una vulnerabilidad TOCTOU en el acceso a un fichero. [1]

La ventana de vulnerabilidad para tal ataque es el período de tiempo entre cuando una
propiedad es comprobada y cuando se usa el archivo. Los atacantes tienen una variedad
de técnicas para ampliar la longitud de esta ventana para hacer los exploits más fáciles,
como el enviar a una señal al proceso víctima que hace que ceda la CPU a otro proceso.
Incluso con una pequeña ventana, una tentativa de exploit puede repetirse hasta que se
consiga llevar a cabo.

En la figura siguiente, se muestra el ejemplo corregido.

for (int i=1; i < argc; i++) {


int caller_uid = getuid();
int owner_uid = geteuid();
/* set effective user id before opening the file */
if (setresuid(-1, caller_uid, owner_uid) != 0){
exit(-1);
}
if (fd = open(argv[i], O_RDONLY);
/* reset the effective user id to its original value */
if (setresuid(-1, owner_uid, caller_uid) != 0){
exit(-1);
}
if (fd != -1) {
print(fd);
}
}
Figura 80. Ejemplo de código con vulnerabilidad TOCTOU en el acceso a un fichero corregida. [1]

© Universidad Internacional de La Rioja (UNIR)


Tema 3: Codificación segura

Hay que tener en cuenta que un ataque de condición de carrera todavía puede darse
después de abrir el archivo, si alguna operación posterior depende de una propiedad
comprobada anterior a la apertura del archivo. Por ejemplo, si la estructura pasada a
stat() se obtiene antes de que un archivo sea abierto y una decisión posterior sobre si hay
que operar sobre el archivo está basada en un valor leído de la estructura de stat, entonces
una condición de carrera TOCTOU puede darse entre la llamada a stat() y la llamada
open().

Se pueden evitar muchas vulnerabilidades TOCTOU si las operaciones a realizar


sobre el sistema de ficheros son sobre descriptores de ficheros abiertos. En la tabla
siguiente, se enumeran funciones que utilizan paths como argumentos y descriptores de
fichero equivalentes. Esto es solo una solución parcial porque varias funciones de la
biblioteca de C estándar que interactúan con el sistema de archivos aceptan solo nombres
de ruta.

Por suerte, la mayor parte de estas funciones hacen cumplir permisos de acceso al
sistema de archivos cuando se ejecutan, como son link(), mkdir(), mknod(), rename(),
rmdir(), symlink (), unlink(), y utime(). Si se disminuyen los privilegios del usuario actual
antes de la realización de cualquiera de estas operaciones, se asegura la comprobación
de control de acceso al sistema de archivos estándar. Si estas operaciones se
realizan con privilegios debido a los recursos que utilizan, el único modo de conseguir
seguridad es usando permisos de sistema de archivos restrictivos para impedir a los
usuarios cambiar el significado del nombre del archivo simbólico usado.

Funciones que utilizan paths como Funciones que utilizan descriptores


argumentos de fichero equivalentes
Int chmod (const char *filename, mode_t Int fchmod (int filedes, int mode)
mode)
Int chown (const char *filename, uid_t Int fchown (int filedes, int owner, int group)
owner, gid_t group)
Int chdir (const char *filename) Int fchdir (int filedes)
Int stat (const char *filename, struct stat Int fstat (int filedes, struct stat *buf)
*buf)
Tabla 8 Funciones que utilizan paths como argumentos y descriptores de fichero equivalentes. [1]

© Universidad Internacional de La Rioja (UNIR)


Tema 3: Codificación segura

Ficheros temporales inseguros

La biblioteca del lenguaje C estándar contiene muchos mecanismos diferentes para


crear archivos temporales. Algunas funciones intentan crear un único nombre de
archivo simbólico que el programador puede usar para crear un archivo temporal. Otras
funciones van un poco más lejos e intentan abrir un descriptor de archivo, además.
Ambos tipos de funciones son susceptibles a una variedad de vulnerabilidades que
podrían permitir a un atacante inyectar contenido malévolo en un programa, hacer que
el programa realice operaciones malévolas en nombre del atacante, dar acceso al atacante
a información sensible que el programa almacena sobre el sistema de archivos, o permitir
un ataque de denegación de servicio contra el programa.

Las tablas 9 y 10, listan una serie de funciones de la biblioteca de C que intentan generar
un nombre de archivo único para un nuevo archivo temporal. Estas funciones sufren
inherentemente de vulnerabilidad de condición de carrera, TOCTOU, subyacente
en el nombre del archivo escogido. Aunque las funciones garanticen que el nombre del
archivo es único en el tiempo que es seleccionado, no hay ningún mecanismo que impida
a un atacante crear un archivo con el mismo nombre seleccionado antes de que la
aplicación intente abrir el archivo.

La probabilidad de ataques con éxito contra estas funciones aumenta por el hecho de que
usan fuentes muy pobres de aleatoriedad en los nombres que generan, lo cual hace
que un atacante pueda tener éxito. Si un atacante realmente logra crear el archivo
primero, dependiendo de cómo el archivo es abierto, el contenido o los permisos de
acceso del archivo podrían permanecer intactos. Si el contenido del archivo es malévolo
en naturaleza, un atacante podría inyectar datos peligrosos en la aplicación cuando lee
datos del archivo temporal. Si un atacante pre-crea el archivo con más permisos de
acceso de los necesarios, podría más tarde tener acceso, modificar, o corromper datos
que la aplicación almacena en el archivo temporal. Si el atacante pre-crea el archivo como
un enlace a otro archivo importante, la aplicación podría truncar o escribir datos al
archivo y sin ser consciente realizar operaciones perjudiciales.

© Universidad Internacional de La Rioja (UNIR)


Tema 3: Codificación segura

Finalmente, en el mejor caso, el archivo puede ser abierto con open() usando O_CREAT
y O_EXCL flags, que fallará si el archivo ya existe y por tanto, prevenir estos tipos de
ataques. Sin embargo, si un atacante con exactitud puede predecir una secuencia de
nombres del archivo temporales, podría ser capaz de impedir al programa abrir el
almacenamiento temporal necesario, causando con eficacia un ataque de denegación de
servicio. Este tipo de ataque es trivial de montar, dada la pequeña cantidad de
aleatoriedad usada en la selección de los nombres del archivo que estas funciones
generan. En Windows, la función GetTempFileName() sufre de las mismas
vulnerabilidades.

Función Descripción
char* mktemp (char El mktemp () genera un nombre de archivo único por plantilla
*template) modificanda [...]. Si tiene éxito, devuelve la plantilla
modificada. Si mktemp () no puede encontrar un nombre de
archivo único, crea una plantilla vacía y la devuelve.
char* tmpnam (char *result) Esta función crea y devuelve un nombre de archivo válido que
no hace referencia a ningún archivo existente. Si el argumento
resultado es un puntero nulo, el valor de retorno es un
puntero a una cadena interna estática, que puede ser
modificado por llamadas posteriores. Si no, el resultado debe
ser un puntero a una matriz de al menos L_tmpnam
caracteres, y el resultado se escribe en la misma.
char* tempnam (const char Esta función genera un nombre de archivo temporal único. Si
*dir, prefijo no es un puntero nulo, hasta cinco caracteres de esta
const char *prefix) cadena usa como prefijo para el nombre de archivo. El valor
de retorno es una nueva cadena generada con malloc (), por
lo que debe liberar la memoria que ocups cuando ya no se
necesita.
Tabla 9 Funciones que intentan generar un único fichero temporal. [1]

© Universidad Internacional de La Rioja (UNIR)


Tema 3: Codificación segura

Función Descripción
FILE* tmpfile (void) Esta función crea un archivo binario temporal para el modo
de actualización, como si al llamar fopen () con modo wb+. El
archivo se borra automáticamente cuando se cierra o cuando
el programa termina.
Int mkstemp (char *template) El mkstemp () genera un nombre de archivo único y abre el
archivo con open () [con el flag O_EXCL]. Si tiene éxito, se
modifica la plantilla y devuelve un descriptor para el archivo
abierto en modo lectura y escritura. Si mkstemp () no puede
crear un archivo con un nombre único, devuelve -1. El archivo
se abre en el modo 0600.
Tabla 10 Funciones que intentan generar un único fichero temporal y abrirlo además. [1]

Donde está disponible, mkstemp(), es la mejor opción para crear archivos temporales
entre las funciones ofrecidas por la biblioteca estándar. Pero debido al problema de
permisos de archivo con mkstemp() en viejos sistemas, se debería requerir que todos los
archivos recién creados sean accesibles solo al usuario actual llamando a umask(077)
antes de la creación de cualquier archivo temporal, esto fuerza a todos los archivos recién
creados sean accesibles solo por el usuario que los crea. Como los permisos que asigna
umask() se heredan de un proceso a otro, no confiar en la umask puesta por defecto por
la shell. Un atacante explícitamente puede invocar su programa con una umask
inadecuada y conseguir su objetivo.

Esta solución no aborda el riesgo de un ataque de denegación de servicio montado


por un atacante que puede predecir los valores de los nombres de los archivos que serán
generados. Se tienen dos opciones para crear archivos temporales de forma segura:
Almacenar los archivos temporales bajo un directorio que no es
públicamente accesible, eliminando así toda la discusión con respecto a ataques.
La figura siguiente, es un ejemplo de código para crear tal directorio.

Generar los nombres de archivo temporales que sean difíciles de adivinar


usando un generador de números criptográficamente seguros pseudo-
aleatorios (PRNG) para crear un elemento aleatorio en cada nombre del archivo
temporal.

© Universidad Internacional de La Rioja (UNIR)


Tema 3: Codificación segura

static char safe_dir(char *dir, uid_t owner_uid) {


char newdir[PATH_MAX+1];
int cur = open(".", O_RDONLY);
struct stat linf, sinf; int fd;

if(cur == -1) {
return –1;
}
if(lstat(dir, &linf) == –1) {
close(cur);
return –2;
}
do {
chdir(dir);
if((fd = open(".", O_RDONLY)) == –1) {
fchdir(cur); close(cur); return –3;
}
if(fstat(fd, &sinf) == –1) {
fchdir(cur); close(cur);
close(fd); return –4;
}
close(fd);
Figura 81. Código para creación de un directorio seguro. [1]

Inyección de comandos

Los datos introducidos por un usuario pueden alterar el propósito de un comando,


consulta, etc. a ejecutar en el entorno de la aplicación, base de datos, etc. La secuencia de
actuación de un atacante sería:
Un atacante modifica el entorno de un programa.
El programa ejecuta un comando que usa el entorno modificado sin especificar
una ruta absoluta.
Ejecutando el comando, el programa da privilegios a un atacante o la capacidad
que de otra manera no tendría.
Para entender mejor el impacto potencial del entorno sobre la ejecución de comandos
en un programa, considerar la vulnerabilidad en la utilidad ChangePassword basada en

© Universidad Internacional de La Rioja (UNIR)


Tema 3: Codificación segura

CGI-Web que permite a los usuarios cambiar sus contraseñas en el sistema [22]. El
proceso de actualización de contraseña incluye ejecutar make en el directorio /var/yp.
Notar que debido a que el programa pone al día registros de contraseña, debe ser
instalado con setuid root.

El programa invoca make como sigue:

system("cd /var/yp && make &> /dev/null")

Como el programa no especifica una ruta absoluta para sus variables de entorno antes de
la invocación del comando, un atacante puede aprovecharse del programa ejecutándolo
localmente modificando la variable de entorno $PATH para apuntar su código malicioso
llamado make y luego ejecutando un script CGI desde un intérprete de comandos shell,
el atacante puede ejecutar código arbitrario con privilegios de root. En programas
seguros privilegiados, $PATH debería contener únicamente directorios de root, ya que
el programa probablemente tendrá que ejecutar utilidades de sistema con acceso a
directorios controlados por el propietario del progarama. No incluir el directorio actual
(.) rutas relativas que un atacante podría ser capaz de manipular. En la mayor parte de
entornos, $IFS debería ser puesto a su valor por defecto, \t\n.

Descriptores de ficheros

Los descriptores de archivo estándar stdin (FD 0), stdout (FD 1), y stderr (FD 2) son
típicamente abiertos por el terminal y son usados tanto explícitamente como
implícitamente por funciones como printf(). Algunos programas remiten uno o varios de
estos descriptores a streams de datos diferentes, como un archivo de log, para reutilizar
su comportamiento implícito de forma que sea más adecuado al diseño del programa,
pero la mayor parte los mismos nunca cambia sus valores. Como con umask y con
variables de entorno, un proceso hijo hereda descriptores de archivo estándar de su
padre. Los atacantes pueden aprovechar este hecho para hacer que un
programa vulnerable lea o escriba en archivos del sistema cuando en
realidad se espera interactuar con el terminal.

© Universidad Internacional de La Rioja (UNIR)


Tema 3: Codificación segura

fd = open("/etc/passwd", O_RDWR);
...
if (!process_ok(argv[1])) {
perror(argv[1]);
}
Figura 82. Código vulnerable a ataques a su descriptor de ficheros estándar. [1]

El ejemplo de la figura anterior, muestra un programa que como parte de su actividad


normal, abre el archivo/etc/passwd en modo lectura-escritura y después
condicionalmente pasa su primer argumento a stderr() usado por perror(). Se podía creer
que estas dos operaciones no están relacionadas y no tienen ningún impacto la uno sobre
la otra, pero el código realmente contiene un bug.

Un ejemplo de un ataque sobre la vulnerabilidad del ejemplo anterior se puede


ver en la figura siguiente, el código del ejemplo primero cierra stderr (descriptor de
archivo 2) y luego ejecuta el programa vulnerable. Cuando el programa abre/etc/passwd,
open() devolverá el primer descriptor de archivo disponible, que, debido a que el
programa del atacante cerró stderr, será el descriptor de archivo 2.

Ahora, en vez de inofensivamente pasar su primer parámetro al terminal, el programa


escribe la información a /etc/passwd. Aunque esta vulnerabilidad fuera perjudicial
independientemente de la información escrita, este ejemplo es en particular fatal porque
el atacante puede escribir una entrada válida a /etc/passwd. En los sistemas que
almacenan entradas de contraseña en /etc/passwd, se daría acceso con privilegios de
root al atacante sobre el sistema.

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


(void)close(2);
execl("victim", "victim", "attacker:<pw>:0:1:Super-User-2:...", NULL);
return 1;
}
Figura 83. Ejemplo de ataque a vulnerabilidad del ejemplo anterior. [1]

La solución con vulnerabilidades de descriptor de archivo estándar es franca. Los


programas privilegiados deben asegurar que los tres primeros descriptores de
archivo se abren a archivos seguros conocidos antes del comienzo de su ejecución. Un
programa puede: abrir los tres primeros descriptores de archivo al terminal deseado,

© Universidad Internacional de La Rioja (UNIR)


Tema 3: Codificación segura

abrir archivos streams (si el programa tiene la intención de usar funciones que
implícitamente dependen de estos valores), o abrir /dev/null tres veces para asegurar
que los tres primeros descriptores de archivo se usan. El ejemplo de la figura 84, ilustra
la segunda opción.

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


if (open("/dev/null", O_WRONLY) < 0 ||
open("/dev/null", O_WRONLY) < 0 ||
open("/dev/null", O_WRONLY) < 0) {
exit(-1);
}
...
}
Figura 84. Código que soluciona vulnerabilidades de descriptor de ficheros asegurando el uso de los
tres primeros descriptores de ficheros. [1]

2.8. Referencias

[1]. Chess, B., and West, J. Secure Programming with Static Analysis. Addison-
Wesley Software Security Series.

[2]. INTECO. (2012). Informe de vulnerabilidades, 1er semestre 2012.

[3]. Graff, M. G., and van Wyk, K. R. (2003). Secure Coding: Principles & Practices.
O'Reilly.

[4]. Howard, M., and LeBlanc, D. (2003). Writing Secure Code. Microsoft Press.

[5]. Allen, J. H., Barnum, S., Ellison, R. J., McGraw, G., and Mead, N. R. (2008).
Software Security Engineering: A Guide for Project Managers. Addison Wesley
Professional.

[6]. Goertzel, K. M., and Winograd, T. (2008). Enhancing the Development Life Cycle
to Produce Secure Software, Version 2.0. United States Department of Defense
Data and Analysis Center for Software.

© Universidad Internacional de La Rioja (UNIR)


Tema 3: Codificación segura

[7]. Howard, M., and Lipner, S. (2006).The Security Development Lifecycle: SDL: A
Process for Developing Demonstrably Secure Software. Microsoft Press.

[8]. Goertzel, K. M., Winograd, T. (2008). Enhancing the Development Life Cycle to
Produce Secure Software, Version 2.0. United States Department of Defense Data
and Analysis Center for Software.

[9]. Komaroff, M. (ASD/NII), and Baldwin, K. (OSD/AT&L) (2005). DoD Software


Assurance Initiative. Recuperado el 19 de abril de 2013 en
https://acc.dau.mil/CommunityBrowser.aspx?id=25749

[10]. Hoglund, G., and McGraw, G. Exploiting Software: How to Break Code. Addison-
Wesley Software Security Series.

[11]. The SANS Technology Institute. (2006). The Twenty Most Critical Internet
Security Vulnerabilities. Recuperado el 19 de abril de 2013 en
http://www.sans.org/top20/

[12]. Wagner, D., Foster, J. S., Brewer, E. A., and Aiken, A. (2002). A First Step Towards
Automated Detection of Buffer Overrun Vulnerabilities. Proceedings of the 7th
Network and Distributed System Security Symposium. San Diego, CA.

[13]. Long, F. Software Vulnerabilities in Java. CERT Technical Note CMU/SEI-2005-


TN-044.

[14]. Merino, B. (2011). Software Exploitation. INTECO.

[15]. CERT (2000). Malicious HTML Tags Embedded in Client Web Requests.
Recuperado el 19 de abril de 2013 en http://www.cert.org/advisories/CA-2000-
02.html

[16]. Anderson (2001). Security Engineering.

[17]. Brown, K. (2004). Windows Vista Technical Articles. Security in Longhorn: Focus
on Least Privilege, DevelopMentor.

© Universidad Internacional de La Rioja (UNIR)


Tema 3: Codificación segura

[18]. Purczynski, W. (2000). Linux Capabilities Vulnerability. Recuperado el 19 de abril


de 2013 en http://www.securityfocus.com/ bid/1322

[19]. Chen, H., et al. Setuid Demystified. 11th USENIX Security Symposium (San
Francisco, CA, 2002), 171–190. Recuperado el 19 de abril de 2013 en
http://www.cs.berkeley.edu/~daw/papers/setuid-usenix02.pdf

[20]. Provos, N., Friedl, M., and Honeyman, P. (2003). Preventing Privilege Escalation.
http://niels.xtdnet.nl/papers/privsep.pdf

[21]. Sarmiento, E. (2001). Chapter 4: The Jail Subsystem. Recuperado el 19 de abril de


2013 en http://www.freebsd.org/ doc/en_US.ISO8859-1/books/arch-
handbook/jail.html

[22]. Berkman, A. (2004). ChangePassword 0.8 Runs setuid Shell. Recuperado el 19 de


abril de 2013 en
http://tigger.uic.edu/~jlongs2/ holes/changepassword.txt

[23]. López, S. (2012). Descubriendo el valor de la Seguridad en las Aplicaciones Web


con IBM AppScan. IBM Corporation.

© Universidad Internacional de La Rioja (UNIR)

S-ar putea să vă placă și