unitario - metodos de distribucion de costos indirectos de fabricacion
¿Por qué está desaprobado printf con un solo argumento(sin especificadores de conversión)? (10)
Además de las otras respuestas,
printf("Hello world! I am 50% happy today")
es un error fácil de hacer, que puede causar todo tipo de problemas de memoria desagradables (¡es UB!).
Es más simple, más fácil y más robusto "exigir" a los programadores que sean absolutamente claros cuando quieren una cadena literal y nada más .
Y eso es lo que
printf("%s", "Hello world! I am 50% happy today")
te atrapa.
Es completamente infalible.
(Steve, por supuesto,
printf("He has %d cherries/n", ncherries)
es absolutamente lo mismo; en este caso, el programador no está en la mentalidad de "cadena literal"; ella está en la mentalidad de "cadena de formato". )
En un libro que estoy leyendo, está escrito que
printf
con un solo argumento (sin especificadores de conversión) está en desuso.
Recomienda sustituir
printf("Hello World!");
con
puts("Hello World!");
o
printf("%s", "Hello World!");
¿Alguien puede decirme por qué
printf("Hello World!");
¿Está Mal?
Está escrito en el libro que contiene vulnerabilidades.
¿Cuáles son estas vulnerabilidades?
Agregaré un poco de información sobre la parte de vulnerabilidad aquí.
Se dice que es vulnerable debido a la vulnerabilidad del formato de cadena printf. En su ejemplo, donde la cadena está codificada, es inofensiva (incluso si las cadenas de codificación como esta nunca se recomiendan por completo). Pero especificar los tipos de parámetros es un buen hábito. Toma este ejemplo:
Si alguien pone un carácter de cadena de formato en su printf en lugar de una cadena normal (por ejemplo, si desea imprimir el stdin del programa), printf tomará todo lo que pueda en la pila.
Fue (y sigue siendo) muy utilizado para explotar programas en pilas de exploración para acceder a información oculta o evitar la autenticación, por ejemplo.
Ejemplo (C):
int main(int argc, char *argv[])
{
printf(argv[argc - 1]); // takes the first argument if it exists
}
si pongo como entrada de este programa
"%08x %08x %08x %08x %08x/n"
printf ("%08x %08x %08x %08x %08x/n");
Esto le indica a la función printf que recupere cinco parámetros de la pila y los muestre como números hexadecimales de 8 dígitos. Entonces, un posible resultado puede verse así:
40012980 080628c4 bffff7a4 00000005 08059c04
Vea this para una explicación más completa y otros ejemplos.
Como nadie lo ha mencionado, agregaría una nota sobre su desempeño.
En circunstancias normales, suponiendo que no se usen optimizaciones del compilador (es decir,
printf()
realidad llama a
printf()
y no a
fputs()
), esperaría que
printf()
funcione de manera menos eficiente, especialmente para cadenas largas.
Esto se debe a que
printf()
tiene que analizar la cadena para verificar si hay algún especificador de conversión.
Para confirmar esto, he realizado algunas pruebas. La prueba se realiza en Ubuntu 14.04, con gcc 4.8.4. Mi máquina usa una CPU Intel i5. El programa que se prueba es el siguiente:
#include <stdio.h>
int main() {
int count = 10000000;
while(count--) {
// either
printf("qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM");
// or
fputs("qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM", stdout);
}
fflush(stdout);
return 0;
}
Ambos están compilados con
gcc -Wall -O0
.
El tiempo se mide usando
time ./a.out > /dev/null
.
El siguiente es el resultado de una ejecución típica (las he ejecutado cinco veces, todos los resultados están dentro de 0.002 segundos).
Para la variante
printf()
:
real 0m0.416s
user 0m0.384s
sys 0m0.033s
Para la variante
fputs()
:
real 0m0.297s
user 0m0.265s
sys 0m0.032s
Este efecto se amplifica si tiene una cadena muy larga.
#include <stdio.h>
#define STR "qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM"
#define STR2 STR STR
#define STR4 STR2 STR2
#define STR8 STR4 STR4
#define STR16 STR8 STR8
#define STR32 STR16 STR16
#define STR64 STR32 STR32
#define STR128 STR64 STR64
#define STR256 STR128 STR128
#define STR512 STR256 STR256
#define STR1024 STR512 STR512
int main() {
int count = 10000000;
while(count--) {
// either
printf(STR1024);
// or
fputs(STR1024, stdout);
}
fflush(stdout);
return 0;
}
Para la variante
printf()
(se ejecutó tres veces, real más / menos 1.5s):
real 0m39.259s
user 0m34.445s
sys 0m4.839s
Para la variante
fputs()
(se ejecutó tres veces, real más / menos 0.2s):
real 0m12.726s
user 0m8.152s
sys 0m4.581s
Nota:
Después de inspeccionar el ensamblaje generado por gcc, me di cuenta de que gcc optimiza la llamada
fputs()
a una llamada
fwrite()
, incluso con
-O0
.
(La llamada
printf()
permanece sin cambios.) No estoy seguro de si esto invalidará mi prueba, ya que el compilador calcula la longitud de la cadena para
fwrite()
en tiempo de compilación.
Este es un consejo equivocado. Sí, si tiene una cadena de tiempo de ejecución para imprimir,
printf(str);
es bastante peligroso, y siempre debes usar
printf("%s", str);
en cambio, porque en general nunca se puede saber si
str
podría contener un signo
%
.
Sin embargo, si tiene una cadena
constante en
tiempo de compilación, no hay nada de malo en
printf("Hello, world!/n");
(Entre otras cosas, ese es el programa C más clásico de todos los tiempos, literalmente del libro de programación C de Génesis. Por lo tanto, cualquiera que desaproveche ese uso es bastante herético, ¡y por mi parte me ofendería un poco!)
Llamar a
printf
con cadenas de formato literal es seguro y eficiente, y existen herramientas para advertirle automáticamente si su invocación de
printf
con cadenas de formato proporcionadas por el usuario no es segura.
Los ataques más severos en
printf
aprovechan el especificador de formato
%n
.
A diferencia de todos los demás especificadores de formato, por ejemplo,
%d
,
%n
realmente escribe un valor en una dirección de memoria proporcionada en uno de los argumentos de formato.
Esto significa que un atacante puede sobrescribir la memoria y, por lo tanto, potencialmente tomar el control de su programa.
en.wikipedia.org/wiki/Uncontrolled_format_string
proporciona más detalles.
Si llama a
printf
con una cadena de formato literal, un atacante no puede introducir un
%n
en su cadena de formato y, por lo tanto, está a salvo.
De hecho, gcc cambiará su llamada a
printf
en una llamada a
puts
, por lo que literalmente no hay ninguna diferencia (pruebe esto ejecutando
gcc -O3 -S
).
Si llama a
printf
con una cadena de formato proporcionada por el usuario, un atacante puede introducir un
%n
en su cadena de formato y tomar el control de su programa.
Su compilador generalmente le advertirá que el suyo no es seguro, vea
-Wformat-security
.
También hay herramientas más avanzadas que aseguran que una invocación de
printf
sea segura incluso con cadenas de formato proporcionadas por el usuario, e incluso pueden verificar que pase el número y tipo de argumentos correctos a
printf
.
Por ejemplo, para Java existe
el Propenso a errores de Google
y el
Checker Framework
.
Para gcc es posible habilitar advertencias específicas para verificar
printf()
y
scanf()
.
La documentación de gcc dice:
-Wformat
está incluido en-Wall
. Para un mayor control sobre algunos aspectos de la verificación de formato, las opciones-Wformat-y2k
,-Wno-format-extra-args
,-Wno-format-zero-length
,-Wformat-nonliteral
-Wformat-security
,-Wformat-security
y-Wformat=2
están disponibles, pero no están incluidos en-Wall
.
El
-Wformat
que está habilitado dentro de la opción
-Wall
no habilita varias advertencias especiales que ayudan a encontrar estos casos:
-
-Wformat-nonliteral
le avisará si no pasa una cadena literal como especificador de formato. -
-Wformat-security
le avisará si pasa una cadena que podría contener una construcción peligrosa. Es un subconjunto de-Wformat-nonliteral
.
Tengo que admitir que habilitar
-Wformat-security
reveló varios errores que teníamos en nuestra base de código (módulo de registro, módulo de manejo de errores, módulo de salida xml, todos tenían algunas funciones que podrían hacer cosas indefinidas si se hubieran llamado con% caracteres en su parámetro Para obtener información, nuestra base de código tiene ahora alrededor de 20 años e incluso si estuviéramos al tanto de este tipo de problemas, nos sorprendió mucho cuando habilitamos estas advertencias de cuántos de estos errores todavía estaban en la base de código).
Un aspecto bastante desagradable de
printf
es que incluso en plataformas donde las lecturas de memoria perdida solo pueden causar un daño limitado (y aceptable), uno de los caracteres de formato,
%n
, hace que el siguiente argumento se interprete como un puntero a un entero escribible, y hace que el número de caracteres de salida hasta ahora se almacene en la variable identificada de ese modo.
Nunca he usado esa característica yo mismo, y a veces uso métodos ligeros de estilo printf que he escrito para incluir solo las características que realmente uso (y no incluir esa o algo similar) pero alimentando cadenas de funciones printf estándar recibidas de fuentes no confiables puede exponer vulnerabilidades de seguridad más allá de la capacidad de leer almacenamiento arbitrario.
printf("Hello World!");
En mi humilde opinión no es vulnerable, pero considere esto:
const char *str;
...
printf(str);
Si
str
apunta a una cadena que contiene
%s
especificadores de formato
%s
, su programa exhibirá un comportamiento indefinido (principalmente un bloqueo), mientras que
puts(str)
solo mostrará la cadena tal como está.
Ejemplo:
printf("%s"); //undefined behaviour (mostly crash)
puts("%s"); // displays "%s"
printf("Hello world");
está bien y no tiene vulnerabilidad de seguridad.
El problema radica en:
printf(p);
donde
p
es un puntero a una entrada controlada por el usuario.
Es propenso a
formatear ataques de cadenas
: el usuario puede insertar especificaciones de conversión para tomar el control del programa, por ejemplo,
%x
para volcar la memoria o
%n
para sobrescribir la memoria.
Tenga en cuenta que
puts("Hello world")
no es equivalente en comportamiento a
printf("Hello world")
sino a
printf("Hello world/n")
.
Los compiladores generalmente son lo suficientemente inteligentes como para optimizar la última llamada para reemplazarla con
puts
.
printf("Hello World/n")
compila automáticamente al equivalente
puts("Hello World")
puedes verificarlo desarmando tu ejecutable:
push rbp
mov rbp,rsp
mov edi,str.Helloworld!
call dword imp.puts
mov eax,0x0
pop rbp
ret
utilizando
char *variable;
...
printf(variable)
conducirá a problemas de seguridad, ¡nunca use printf de esa manera!
por lo que su libro es correcto, el uso de printf con una variable está en desuso pero aún puede usar printf ("mi cadena / n") porque se convertirá automáticamente en put