c++ printf stack-overflow language-lawyer buffer-overflow

c++ - ¿Debe la vulnerabilidad de memoria de la línea de código "printf("% s ", argv[1]);" ser descrita como un desbordamiento de pila?



stack-overflow language-lawyer (3)

Hoy tomé una breve "prueba de habilidades de C ++" de Elance.com. Una pregunta fue la siguiente:

¿Cuál es la vulnerabilidad de seguridad de la siguiente línea de código:

printf("%s", argv[1]);

Opción 1: Formato de cadena

Opción 2: Desbordamiento de pila <- Esto fue marcado por Elance como la respuesta correcta

Al usuario se le proporcionaron 10 segundos para responder esta pregunta después de unos pocos segundos iniciales de ver la pregunta (o fallar automáticamente la pregunta). (También hubo otras dos respuestas claramente irrelevantes que no fueron marcadas como la respuesta correcta por Elance).

Estaba buscando un desbordamiento de búfer o un desbordamiento de búfer como una opción.

Instintivamente no me gustó el desbordamiento de la pila de respuestas, porque en mis 10 segundos usé mentalmente lo que creo que es la definición estándar de "Desbordamiento de la pila" :

En el software, se produce un desbordamiento de pila cuando el puntero de pila supera el límite de pila. La pila de llamadas puede consistir en una cantidad limitada de espacio de direcciones, a menudo determinada al inicio del programa ...

Según esta definición de "Desbordamiento de pila", un desbordamiento de búfer es totalmente posible sin un desbordamiento de pila ; es un desbordamiento de pila solo si el programa intenta escribir fuera de la asignación total de pila del programa que llama (ya sea debido a una saturación del búfer, o si de lo contrario sería una escritura legítima, como asignar memoria a las variables basadas en pila un número excesivo de veces).

Mi instinto de 10 segundos me dijo que "saturación de búfer" es una descripción más precisa de la línea de código problemática, arriba, porque a menudo (en mi experiencia) hay suficientes caracteres nulos ( ''/0'' ) salpicados a través de datos basura en la RAM a menudo para evitar un desbordamiento de pila real en casos como este, pero un exceso de búfer en la implementación parece razonablemente posible o incluso probable. (Pero la posibilidad de que printf lea basura aquí podría suponer que argc == 1 , de modo que no hubo un argv[1] proporcionado por el usuario argv[1] ; si argv[1] está presente, quizás se pueda suponer que es probable que la función de llamada no esté presente. NULL insertado. No se indicó en el problema si argv[1] estaba presente.)

Debido a que imaginé que podría haber un problema de saturación de búfer aquí, incluso sin un desbordamiento de pila, respondí a Format String , porque simplemente al pasar una cadena de formato diferente como "%.8s" , el problema se puede evitar principalmente, por lo que parecía Como una respuesta en general más genérica, y por lo tanto mejor.

Mi respuesta fue marcada como incorrecta. La respuesta correcta se marcó como "Desbordamiento de pila".

Ahora se me ocurre que tal vez si uno asume que argv[1] está presente, que la única saturación de búfer posible es un desbordamiento de pila, en cuyo caso, el desbordamiento de pila podría ser la respuesta correcta. Sin embargo, incluso en este caso, ¿no se consideraría extraño llamar a esto un desbordamiento de pila ? ¿No es el desbordamiento del búfer una mejor manera de describir este problema, incluso suponiendo que argv[1] está presente? Y, si argv[1] no está presente, ¿no es bastante incorrecto afirmar que el problema es el desbordamiento de pila , en lugar de la saturación de búfer más precisa?

Me gustaría la opinión de los profesionales en este sitio: ¿Es el "desbordamiento de pila" la forma correcta de definir el problema de seguridad de la memoria con la línea de código anterior? O, más bien, ¿es "desbordamiento de búfer" o "desbordamiento de búfer" una forma mejor de describir el problema? Finalmente, dadas las dos opciones proporcionadas para la respuesta de la pregunta (arriba), ¿es la respuesta ambigua o "desbordamiento de pila" (o "cadena de formato") claramente la mejor respuesta?

Comentarios tangenciales sobre la prueba de Elance (no relacionados con la pregunta en esta publicación)

Ninguna de las preguntas de la "Prueba de habilidades de C ++ " de Elance se refería a ninguna característica específica de C ++, como clases, plantillas, cualquier cosa en el STL o cualquier aspecto del polimorfismo. Cada pregunta era una pregunta directa y directa de C.

Debido a que hubo muchas (al menos 3) otras preguntas en la llamada "prueba de habilidades de C ++" de Elance que fueron indiscutiblemente incorrectas (como esta pregunta: dado sizeof(int) == sizeof(int*) y sizeof(int) == 4 , luego en el código int *a, *b; a=b; b++; ba; ¿cuál es ba , con la respuesta correcta como 4 , en lugar de la respuesta correcta real de 1 ), y dado el hecho de que hay Si no hubiera preguntas específicas de C ++ en la prueba, me he contactado con Elance y planeo seguir seriamente su prueba problemática con la organización. Sin embargo, para la pregunta discutida en esta publicación, no estoy seguro si la pregunta / las respuestas son problemáticas.


Respuesta estándar de C ++

En lo que se refiere al idioma, pueden darse los siguientes casos:

  1. argc < 2
  2. argc >= 2

En el primer caso, printf("%s", argv[1]) es simplemente un comportamiento indefinido .

En el segundo caso, el programa está bien formado (desde argv[0] a argv[argc-1] se garantiza que son cadenas terminadas en nulo válidas:

§3.6.1 / 2 [basic.start.main]

En la última forma, para propósitos de exposición, el primer parámetro de función se llama argc y el segundo parámetro de función se llama argv, donde argc será el número de argumentos pasados ​​al programa desde el entorno en el que se ejecuta el programa. Si argc es distinto de cero, estos argumentos se proporcionarán en argv [0] a través de argv [argc-1] como punteros a los caracteres iniciales de cadenas multibyte terminadas en nulo (ntmbs s) (17.5.2.1.4.2) y argv [0] sea ​​el puntero al carácter inicial de un ntmbs que representa el nombre utilizado para invocar el programa o "". El valor de argc será no negativo. El valor de argv [argc] será 0. [Nota: se recomienda que se agregue cualquier parámetro adicional (opcional) después de cada carga. —Enuncia]

(énfasis mío).

¿Por qué el desbordamiento de pila es terriblemente impreciso?

Dado que no se proporcionó ninguna otra información (como compilador o arquitectura), la respuesta "Desbordamiento de pila" es simplemente imprecisa. El estándar de C ++ no intenta definir qué es una "pila" y, por lo tanto, "desbordamiento de pila" no significa casi nada para el estándar de C ++.

Las razones estándar en términos de una máquina abstracta con un modelo de memoria garantizada.

Lo que realmente sucede

En el caso en que argc < 2 , nadie sabe qué pasa. La norma no garantiza ni especifica nada. En el caso en que argc >= 2 el programa está bien definido.


En un sistema Unix, argv[1] puede ser un acceso de memoria no válido en sí mismo (caso argc==0 ), un puntero a una cadena bien formada ( argc >= 2 ) o NULL ( argc == 1 ).

El problema con printf("%s", argv[1]); está utilizando un puntero ( argv[1] ) sin haber comprobado que era válido. Cualquier cosa que suceda más tarde es sólo una consecuencia secundaria. El problema es la falta de validación de que argv[1] es lo que se pretende antes de usarlo. Podría caer bajo el muy general CWE20: Validación incorrecta de entrada . Es engañoso llamarlo desbordamiento de búfer o desbordamiento de pila.


No hay desbordamiento de pila potencial aquí.

El estándar garantiza que argc no es negativo, lo que significa que puede ser 0 . Si argc es positivo, argv[0] través de argv[argc-1] son punteros a cadenas.

Si argc == 0 , entonces argv[1] no es simplemente un puntero nulo, no existe en absoluto. En ese caso, argv[1] intenta acceder a un elemento de matriz inexistente. ( argv[1] es equivalente a *(argv+1) ; la adición del puntero está permitida, pero la diferencia no tiene un comportamiento definido). Tenga en cuenta que en este caso el nombre del programa, que de otro modo sería accesible a través de argv[0] no está disponible .

Si argc==1 , entonces argv[1] == NULL . Evaluar argv[1] es perfectamente válido, pero produce un puntero nulo. Pasar un puntero nulo a printf con una opción "%s" tiene un comportamiento indefinido. Supongo que podría llamar a esto un problema de cadena de formato, pero el problema real es usar un puntero nulo cuando se requiere un puntero no nulo a una cadena.

Si argc >= 2 , entonces se garantiza que argv[1] apunta a una cadena, printf("%s", argv[1]) simplemente imprimirá los caracteres de esa cadena, hasta pero sin incluir la terminación ''/0'' (que se garantiza que existe).

Todavía hay una vulnerabilidad potencial en ese caso. Citando N1570 7.21.6.1 párrafo 15:

El número de caracteres que puede producir cualquier conversión individual será de al menos 4095.

(N1570 es un borrador del estándar C; C ++ se refiere al estándar C para partes de su biblioteca estándar).

Lo que significa que una implementación puede limitar el número de caracteres producidos por la llamada a printf . En la práctica, probablemente no haya razón para imponer un límite fijo; printf puede simplemente imprimir caracteres, uno a la vez, hasta que llegue al final de la cadena. Pero en principio, si strlen(argv[1]) > 4095 , y si la implementación actual impone dicho límite, el comportamiento podría ser indefinido.

Aún así, esto no es lo que yo llamaría un "desbordamiento de pila", especialmente porque el estándar de C ++ no usa la palabra "pila" (a excepción de un par de referencias breves para "desenrollar la pila").

La mayoría de estos problemas se pueden evitar marcando primero:

if (argc >= 2) { printf("%s", argv[1]); }

o, si te sientes paranoico:

if (argc >= 2 && argv[1] != NULL) { printf("%s", argv[1]); }