¿Por qué está obsoleto printf con un solo argumento (sin especificadores de conversión)?

102

En un libro que estoy leyendo, está escrito que printfcon 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?

StackUser
fuente
34
Nota: noprintf("Hello World!") es lo mismo que puts("Hello World!"). puts()añade un '\n'. En su lugar, compare printf("abc")confputs("abc", stdout)
chux - Reincorporar a Monica
5
¿Qué es ese libro? No creo que printfesté en desuso de la misma manera que, por ejemplo, getsen C99, por lo que puede considerar editar su pregunta para ser más preciso.
el.pescado
14
Parece que el libro que estás leyendo no es muy bueno; un buen libro no debe decir simplemente que algo como esto está "desaprobado" (eso es falso a menos que el autor esté usando la palabra para describir su propia opinión) y debe explicar el uso es realmente inválido y peligroso en lugar de mostrar un código válido / seguro como un ejemplo de algo que "no debería hacer".
R .. GitHub DEJA DE AYUDAR A ICE
8
¿Puedes identificar el libro?
Keith Thompson
7
Especifique el título del libro, el autor y la referencia de la página. Gracias.
Greenonline

Respuestas:

122

printf("Hello World!"); En mi humilde opinión, no es vulnerable, pero considere esto:

const char *str;
...
printf(str);

Si strpasa a apuntar a una cadena que contiene %sespecificadores de formato, su programa exhibirá un comportamiento indefinido (principalmente un bloqueo), mientras puts(str)que simplemente mostrará la cadena como está.

Ejemplo:

printf("%s");   //undefined behaviour (mostly crash)
puts("%s");     // displays "%s\n"
Jabberwocky
fuente
21
Además de hacer que el programa se bloquee, existen muchas otras vulnerabilidades posibles con cadenas de formato. Consulte aquí para obtener más información: en.wikipedia.org/wiki/Uncontrolled_format_string
e.dan
9
Otra razón es que putspresumiblemente será más rápido.
edmz
38
@black: putses "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. Con printfeso tomó 0.92 segundos. Con putseso tomó 0.93 segundos. Hay cosas de las que preocuparse cuando se trata de eficiencia, pero printfvs. putsno es una de ellas.
Steve Summit
10
@KonstantinWeitz: Pero (a) no estaba usando gcc, y (b) no importa por qué la afirmación " putses más rápido" es falsa, sigue siendo falsa.
Steve Summit
6
@KonstantinWeitz: La afirmación de la que proporcioné evidencia fue (lo opuesto) a la afirmación que hacía el usuario Black. Solo estoy tratando de aclarar que los programadores no deberían preocuparse por llamar putspor esta razón. (Pero si quisiera discutir al respecto: me sorprendería si pudiera encontrar un compilador moderno para cualquier máquina moderna donde putssea ​​significativamente más rápido que printfbajo cualquier circunstancia.)
Steve Summit
75

printf("Hello world");

está bien y no tiene ninguna vulnerabilidad de seguridad.

El problema radica en:

printf(p);

donde pes 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, %xvolcar memoria o %nsobrescribir 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 suelen ser lo suficientemente inteligentes como para optimizar la última llamada para reemplazarla puts.

ouah
fuente
10
Por supuesto printf(p,x), sería igualmente problemático si el usuario tuviera el control p. Entonces, el problema no es el uso de printfcon un solo argumento, sino con una cadena de formato controlada por el usuario.
Hagen von Eitzen
2
@HagenvonEitzen Eso es técnicamente cierto, pero pocos usarían deliberadamente una cadena de formato proporcionada por el usuario. Cuando la gente escribe printf(p), es porque no se dan cuenta de que es una cadena de formato, simplemente piensan que están imprimiendo un literal.
Barmar
33

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").

Carreras de ligereza en órbita
fuente
2
No vale la pena discutir esto, y entiendo lo que está diciendo acerca de la mentalidad de cadena textual versus formato, pero, bueno, no todos piensan de esa manera, que es una de las razones por las que las reglas de talla única pueden molestar. Decir "nunca imprimir cadenas constantes con printf" es casi exactamente como decir "escribir siempre if(NULL == p). Estas reglas pueden ser útiles para algunos programadores, pero no para todos. Y en ambos casos ( printfformatos 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.
Steve Summit
1
@Steve Si hay exactamente cero ventajas para usar algo, pero bastantes desventajas, entonces sí, realmente no hay razón para usarlo. Las condiciones de Yoda, por otro lado , tienen la desventaja de que hacen que el código sea más difícil de leer (intuitivamente diría "si p es cero" no "si cero es p").
Voo
2
@Voo printf("%s", "hello")va a ser más lento que printf("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.
Yakk - Adam Nevraumont
1
@Yakk Dudo que sea más lento
MM
gcc -Wall -W -Werrorevitará malas consecuencias de tales errores.
chqrlie
17

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):

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 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í:

40012980 080628c4 bffff7a4 00000005 08059c04

Consulte esto para obtener una explicación más completa y otros ejemplos.

P1kachu
fuente
13

Llamar printfcon cadenas de formato literal es seguro y eficiente, y existen herramientas para advertirle automáticamente si su invocación de printfcon cadenas de formato proporcionadas por el usuario no es segura.

Los ataques más graves printfse aprovechan del %nespecificador de formato. A diferencia de todos los demás especificadores de formato, por ejemplo %d, en %nrealidad 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 printfcon una cadena de formato literal, un atacante no puede colarse %nen su cadena de formato y, por lo tanto, está a salvo. De hecho, gcc cambiará su llamada a printfen una llamada a puts, por lo que literalmente no hay ninguna diferencia (pruebe esto ejecutando gcc -O3 -S).

Si llama printfcon una cadena de formato proporcionada por el usuario, un atacante puede potencialmente colarse %nen 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 de printfsea ​​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 correctos printf. Por ejemplo, para Java existe Error Propenso de Google y Checker Framework .

Konstantin Weitz
fuente
12

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 strpuede 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 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!)

Steve Summit
fuente
because printf's first argument is always a constant stringNo estoy exactamente seguro de lo que quieres decir con eso.
Sebastian Mach
Como dije, "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 como printf'primer argumento s", que fue "no pases de cuerdas sin %que printf' s primer argumento."
Steve Summit
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.
Voo
@Voo: Bueno, digamos que no todo lo que se considera una mala práctica es en realidad una mala práctica. (Me intviene a la mente el consejo de "nunca usar lo normal ")
Steve Summit
1
@Steve No tengo idea de dónde escuchaste eso, pero ciertamente no es el tipo de mala (¿mala?) Práctica de la que estamos hablando allí. No me malinterpretes, por el momento el código estaba perfectamente bien, pero realmente no quieres mirar a k & r por mucho más que como una nota histórica en estos días. "Está en k & r" simplemente no es un indicador de buena calidad en estos días, eso es todo
Voo
9

Un aspecto bastante desagradable de printfes 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.

Super gato
fuente
8

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 llamadas printf()y no fputs()), esperaría printf()tener un rendimiento menos eficiente, especialmente para cadenas largas. Esto se debe printf()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:

#include <stdio.h>
int main() {
    int count = 10000000;
    while(count--) {
        // either
        printf("qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM");
        // or
        fputs("qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM", stdout);
    }
    fflush(stdout);
    return 0;
}

Ambos se compilan 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 (los he ejecutado cinco veces, todos los resultados están dentro de 0.002 segundos).

Para la printf()variante:

real    0m0.416s
user    0m0.384s
sys     0m0.033s

Para la fputs()variante:

real    0m0.297s
user    0m0.265s
sys     0m0.032s

Este efecto se amplifica si tiene una cuerda 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 printf()variante (se ejecutó tres veces, real más / menos 1,5 s):

real    0m39.259s
user    0m34.445s
sys     0m4.839s

Para la fputs()variante (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 ensamblado generado por gcc, me di cuenta de que gcc optimiza la fputs()llamada a una fwrite()llamada, incluso con -O0. (La printf()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 de fwrite()compilación.

user12205
fuente
2
No invalidará su prueba, como 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 con fputs()y fprintf()sería un buen punto de datos complementario .
Patrick Schlüter
@ PatrickSchlüter Las pruebas con cadenas generadas dinámicamente parecen frustrar el propósito de esta pregunta, aunque ... OP parece estar interesado en que solo se impriman los literales de cadena.
user12205
1
No lo dice explícitamente incluso si su ejemplo usa cadenas literales. De hecho, creo que su confusión sobre el consejo del libro es el resultado del uso de cadenas literales en el ejemplo. Con cadenas literales, el consejo de los libros es de alguna manera dudoso, con cadenas dinámicas es un buen consejo.
Patrick Schlüter
1
/dev/nullhace 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?
Yakk - Adam Nevraumont
7
printf("Hello World\n")

compila automáticamente al equivalente

puts("Hello World")

puedes comprobarlo desmontando 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 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

Ábrahám Endre
fuente
12
En realidad, este comportamiento depende completamente del compilador.
Jabberwocky
6
Esto es engañoso. Dices A compiles to B, pero en realidad te refieres A and B compile to C.
Sebastian Mach
6

Para gcc es posible habilitar advertencias específicas para verificar printf()y scanf().

La documentación de gcc dice:

-Wformatestá incluido en -Wall. Para obtener más control sobre algunos aspectos del formato de cheques, las opciones -Wformat-y2k, -Wno-format-extra-args, -Wno-format-zero-length, -Wformat-nonliteral, -Wformat-security, y -Wformat=2están disponibles, pero no se incluyen en -Wall.

El -Wformatque está habilitado dentro de la -Wallopció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-securityle 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-securityreveló 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).

Patrick Schlüter
fuente
1

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.


¿Por qué está printfobsoleto con un solo argumento (sin especificadores de conversión)?

Una printfllamada 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 printfesa 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:

printf("Hello World!");

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 printfdeclaración como los primeros de su tipo.

No lo serían si estuvieran en desuso.

En un libro que estoy leyendo, está escrito que printfcon un solo argumento (sin especificadores de conversión) está 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 printfcon 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.

RobertS apoya a Monica Cellio
fuente
Puede agregar que noprintf("Hello World!"); es equivalente a de todos modos, lo que dice algo sobre el autor de la recomendación. puts("Hello World!");
chqrlie