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?
printf("Hello World!")
es lo mismo queputs("Hello World!")
.puts()
añade un'\n'
. En su lugar, compareprintf("abc")
confputs("abc", stdout)
printf
esté en desuso de la misma manera que, por ejemplo,gets
en C99, por lo que puede considerar editar su pregunta para ser más preciso.Respuestas:
printf("Hello World!");
En mi humilde opinión, no es vulnerable, pero considere esto:Si
str
pasa a apuntar a una cadena que contiene%s
especificadores de formato, su programa exhibirá un comportamiento indefinido (principalmente un bloqueo), mientrasputs(str)
que simplemente mostrará la cadena como está.Ejemplo:
fuente
puts
presumiblemente será más rápido.puts
es "presumiblemente" más rápido, y esta es probablemente otra razón por la que la gente lo recomienda, pero en realidad no es más rápido. Imprimí"Hello, world!"
1.000.000 de veces, en ambos sentidos. Conprintf
eso tomó 0.92 segundos. Conputs
eso tomó 0.93 segundos. Hay cosas de las que preocuparse cuando se trata de eficiencia, peroprintf
vs.puts
no es una de ellas.puts
es más rápido" es falsa, sigue siendo falsa.puts
por esta razón. (Pero si quisiera discutir al respecto: me sorprendería si pudiera encontrar un compilador moderno para cualquier máquina moderna dondeputs
sea significativamente más rápido queprintf
bajo cualquier circunstancia.)printf("Hello world");
está bien y no tiene ninguna vulnerabilidad de seguridad.
El problema radica en:
donde
p
es un puntero a una entrada que está controlada por el usuario. Es propenso a ataques de cadenas de formato : el usuario puede insertar especificaciones de conversión para tomar el control del programa, por ejemplo,%x
volcar memoria o%n
sobrescribir la memoria.Tenga en cuenta que
puts("Hello world")
no es equivalente en comportamiento aprintf("Hello world")
sino aprintf("Hello world\n")
. Los compiladores suelen ser lo suficientemente inteligentes como para optimizar la última llamada para reemplazarlaputs
.fuente
printf(p,x)
, sería igualmente problemático si el usuario tuviera el controlp
. Entonces, el problema no es el uso deprintf
con un solo argumento, sino con una cadena de formato controlada por el usuario.printf(p)
, es porque no se dan cuenta de que es una cadena de formato, simplemente piensan que están imprimiendo un literal.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 textual 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, no
printf("He has %d cherries\n", ncherries)
es en absoluto lo mismo; en este caso, el programador no está en la mentalidad de "cadena literal"; ella está en la mentalidad de "cadena de formato").fuente
printf
" es casi exactamente como decir "escribir siempreif(NULL == p)
. Estas reglas pueden ser útiles para algunos programadores, pero no para todos. Y en ambos casos (printf
formatos no coincidentes y condicionales de Yoda), los compiladores modernos advierten sobre errores de todos modos, así que las reglas artificiales son aún menos importantes.printf("%s", "hello")
va a ser más lento queprintf("hello")
, por lo que hay una desventaja. Uno pequeño, porque IO es casi siempre mucho más lento que un formato tan simple, pero una desventaja.gcc -Wall -W -Werror
evitará malas consecuencias de tales errores.Solo 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 nunca se recomienda por completo codificar cadenas como esta). 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 (digamos, si desea imprimir el programa stdin), printf tomará todo lo que pueda en la pila.
Fue (y sigue siendo) muy utilizado para explotar programas y explorar pilas para acceder a información oculta o eludir la autenticación, por ejemplo.
Ejemplo (C):
si pongo como entrada de este programa
"%08x %08x %08x %08x %08x\n"
Esto indica a la función printf que recupere cinco parámetros de la pila y los muestre como números hexadecimales rellenados de 8 dígitos. Entonces, una posible salida puede verse así:
Consulte esto para obtener una explicación más completa y otros ejemplos.
fuente
Llamar
printf
con cadenas de formato literal es seguro y eficiente, y existen herramientas para advertirle automáticamente si su invocación deprintf
con cadenas de formato proporcionadas por el usuario no es segura.Los ataques más graves
printf
se aprovechan del%n
especificador de formato. A diferencia de todos los demás especificadores de formato, por ejemplo%d
, en%n
realidad 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. Wikipedia proporciona más detalles.Si llama
printf
con una cadena de formato literal, un atacante no puede colarse%n
en su cadena de formato y, por lo tanto, está a salvo. De hecho, gcc cambiará su llamada aprintf
en una llamada aputs
, por lo que literalmente no hay ninguna diferencia (pruebe esto ejecutandogcc -O3 -S
).Si llama
printf
con una cadena de formato proporcionada por el usuario, un atacante puede potencialmente colarse%n
en su cadena de formato y tomar el control de su programa. Su compilador generalmente le advertirá que el suyo no es seguro-Wformat-security
. También hay herramientas más avanzadas que garantizan que una invocación deprintf
sea segura incluso con cadenas de formato proporcionadas por el usuario, e incluso pueden verificar que le pase el número y el tipo de argumentos correctosprintf
. Por ejemplo, para Java existe Error Propenso de Google y Checker Framework .fuente
Este es un consejo equivocado. Sí, si tiene una cadena de tiempo de ejecución para imprimir,
es bastante peligroso, y siempre debes usar
en cambio, porque en general nunca se puede saber si
str
puede contener un%
signo. Sin embargo, si tiene una cadena constante en tiempo de compilación , no hay nada de malo en(Entre otras cosas, ese es el programa en C más clásico de todos los tiempos, literalmente del libro de programación en C de Génesis. Por lo tanto, cualquiera que desapruebe ese uso está siendo bastante herético, ¡y yo estaría un poco ofendido!)
fuente
because printf's first argument is always a constant string
No estoy exactamente seguro de lo que quieres decir con eso."He has %d cherries\n"
es una cadena constante, lo que significa que es una constante en tiempo de compilación. Pero, para ser justos, el consejo del autor no era "no pase cadenas constantes comoprintf
'primer argumento s", que fue "no pases de cuerdas sin%
queprintf
' s primer argumento."literally from the C programming book of Genesis. Anyone deprecating that usage is being quite offensively heretical
- No has leído K&R en los últimos años. Hay un montón de consejos y estilos de codificación que no solo están desaprobados, sino que simplemente son una mala práctica en estos días.int
viene a la mente el consejo de "nunca usar lo normal ")Un aspecto bastante desagradable de
printf
es que incluso en plataformas donde las lecturas de memoria perdidas solo podrían 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 el momento para ser almacenados en la variable identificada de ese modo. Nunca he usado esa característica, y a veces uso métodos livianos de estilo printf que he escrito para incluir solo las características que realmente uso (y no incluyen esa ni nada similar), pero alimentando las cadenas de funciones estándar de printf recibidas de fuentes no confiables puede exponer vulnerabilidades de seguridad más allá de la capacidad de leer almacenamiento arbitrario.fuente
Como nadie lo ha mencionado, agregaría una nota sobre su desempeño.
En circunstancias normales, asumiendo que no se utilizan optimizaciones del compilador (es decir, en
printf()
realidad llamadasprintf()
y nofputs()
), esperaríaprintf()
tener un rendimiento menos eficiente, especialmente para cadenas largas. Esto se debeprintf()
a que debe analizar la cadena para verificar si hay especificadores 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 está probando es el siguiente:
Ambos se compilan con
gcc -Wall -O0
. El tiempo se mide usandotime ./a.out > /dev/null
. El siguiente es el resultado de una ejecución típica (los he ejecutado cinco veces, todos los resultados están dentro de 0.002 segundos).Para la
printf()
variante:Para la
fputs()
variante:Este efecto se amplifica si tiene una cuerda muy larga.
Para la
printf()
variante (se ejecutó tres veces, real más / menos 1,5 s):Para la
fputs()
variante (se ejecutó tres veces, real más / menos 0.2s):Nota: Después de inspeccionar el ensamblado generado por gcc, me di cuenta de que gcc optimiza la
fputs()
llamada a unafwrite()
llamada, incluso con-O0
. (Laprintf()
llamada permanece sin cambios). No estoy seguro de si esto invalidará mi prueba, ya que el compilador calcula la longitud de la cadena en tiempo defwrite()
compilación.fuente
fputs()
se usa a menudo con las constantes de cadena y esa oportunidad de optimización es parte del punto que quería hacer. Dicho esto, agregar una ejecución de prueba con una cadena generada dinámicamente confputs()
yfprintf()
sería un buen punto de datos complementario ./dev/null
hace que esto sea un juguete, ya que, por lo general, al generar una salida formateada, su objetivo es que la salida vaya a algún lugar, no se descarte. Una vez que agrega el tiempo de "en realidad no descartar los datos", ¿cómo se comparan?compila automáticamente al equivalente
puedes comprobarlo desmontando tu ejecutable:
utilizando
conducirá a problemas de seguridad, ¡nunca use printf de esa manera!
por lo que su libro es realmente correcto, usar printf con una variable está en desuso, pero aún puede usar printf ("mi cadena \ n") porque automáticamente se convertirá en put
fuente
A compiles to B
, pero en realidad te refieresA and B compile to C
.Para gcc es posible habilitar advertencias específicas para verificar
printf()
yscanf()
.La documentación de gcc dice:
El
-Wformat
que está habilitado dentro de la-Wall
opción no habilita varias advertencias especiales que ayudan a encontrar estos casos:-Wformat-nonliteral
advertirá si no pasa una cadena literal como especificador de formato.-Wformat-security
le advertirá si pasa una cadena que podría contener una construcción peligrosa. Es un subconjunto de-Wformat-nonliteral
.Debo admitir que la habilitación
-Wformat-security
reveló varios errores que teníamos en nuestro código base (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 les hubiera llamado con% caracteres en su parámetro. Para información, nuestra base de código tiene ahora alrededor de 20 años e incluso si estábamos 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).fuente
Además de las otras respuestas bien explicadas con cualquier preocupación secundaria cubierta, me gustaría dar una respuesta precisa y concisa a la pregunta proporcionada.
Una
printf
llamada a función con un solo argumento en general no está desaprobada y tampoco tiene vulnerabilidades cuando se usa correctamente, como siempre se codificará.C Los usuarios de todo el mundo, desde principiantes hasta expertos en estado, utilizan
printf
esa forma para dar una frase de texto simple como salida a la consola.Además, alguien tiene que distinguir si este único argumento es un literal de cadena o un puntero a una cadena, que es válido pero no se usa comúnmente. Para este último, por supuesto, pueden ocurrir salidas inconvenientes o cualquier tipo de comportamiento indefinido , cuando el puntero no está configurado correctamente para apuntar a una cadena válida, pero estas cosas también pueden ocurrir si los especificadores de formato no coinciden con los argumentos respectivos dando múltiples argumentos.
Por supuesto, tampoco es correcto ni apropiado que la cadena, proporcionada como un único argumento, tenga algún formato o especificadores de conversión, ya que no se producirá ninguna conversión.
Dicho esto, dar un literal de cadena simple
"Hello World!"
como único argumento sin ningún especificador de formato dentro de esa cadena como lo proporcionó en la pregunta:no está obsoleto o es una " mala práctica " en absoluto ni tiene vulnerabilidades.
De hecho, muchos programadores de C comienzan y comenzaron a aprender y usar C o incluso lenguajes de programación en general con ese programa HelloWorld y esta
printf
declaración como los primeros de su tipo.No lo serían si estuvieran en desuso.
Bueno, entonces me enfocaría en el libro o en el autor mismo. Si un autor realmente está haciendo, en mi opinión, afirmaciones incorrectas e incluso enseña eso sin explicar explícitamente por qué lo hace (si esas afirmaciones son realmente equivalentes literalmente en ese libro), lo consideraría un libro malo . Un buen libro, a diferencia de eso, explicará por qué evitar cierto tipo de métodos o funciones de programación.
De acuerdo con lo que dije anteriormente, el uso
printf
con un solo argumento (una cadena literal) y sin ningún especificador de formato no está en ningún caso desaprobado o considerado como una "mala práctica" .Debes preguntarle al autor, qué quiso decir con eso o mejor aún, tener en cuenta que aclare o corrija la sección relativa para la próxima edición o impresiones en general.
fuente
printf("Hello World!");
es equivalente a de todos modos, lo que dice algo sobre el autor de la recomendación.puts("Hello World!");