Pourquoi printf avec un seul argument (sans spécificateurs de conversion) est-il obsolète?


102

Dans un livre que je lis, il est écrit printfqu'avec un seul argument (sans spécificateurs de conversion) est obsolète. Il recommande de remplacer

printf("Hello World!");

avec

puts("Hello World!");

ou

printf("%s", "Hello World!");

Quelqu'un peut-il me dire pourquoi printf("Hello World!");est faux? Il est écrit dans le livre qu'il contient des vulnérabilités. Quelles sont ces vulnérabilités?


34
Remarque: ce printf("Hello World!")n'est pas la même chose que puts("Hello World!"). puts()ajoute un '\n'. Comparez plutôt printf("abc")àfputs("abc", stdout)
chux - Réintégrer Monica

5
Quel est ce livre? Je ne pense pas que ce printfsoit obsolète de la même manière que, par exemple, getsdans C99, vous pouvez donc envisager de modifier votre question pour être plus précis.
el.pescado

14
Il semble que le livre que vous lisez n'est pas très bon - un bon livre ne devrait pas simplement dire quelque chose comme celui-ci est "obsolète" (c'est en fait faux à moins que l'auteur n'utilise le mot pour décrire sa propre opinion) et devrait expliquer quel usage est en fait invalide et dangereux plutôt que de montrer un code sûr / valide comme exemple de quelque chose que vous "ne devriez pas faire".
R .. GitHub STOP HELPING ICE

8
Pouvez-vous identifier le livre?
Keith Thompson du

7
Veuillez préciser le titre du livre, l'auteur et la référence de la page. THX.
Greenonline

Réponses:


122

printf("Hello World!"); IMHO n'est pas vulnérable mais considérez ceci:

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

S'il strarrive à pointer vers une chaîne contenant des %sspécificateurs de format, votre programme affichera un comportement indéfini (principalement un plantage), alors puts(str)qu'il affichera simplement la chaîne telle quelle.

Exemple:

printf("%s");   //undefined behaviour (mostly crash)
puts("%s");     // displays "%s\n"

21
En plus de faire planter le programme, il existe de nombreux autres exploits possibles avec les chaînes de format. Voir ici pour plus d'informations: en.wikipedia.org/wiki/Uncontrolled_format_string
e.dan

9
Une autre raison est que ce putssera probablement plus rapide.
edmz

38
@black: putsest "vraisemblablement" plus rapide, et c'est probablement une autre raison pour laquelle les gens le recommandent, mais ce n'est pas vraiment plus rapide. Je viens d'imprimer "Hello, world!"1 000 000 fois, dans les deux sens. Avec printfcela a pris 0,92 secondes. Avec putscela a pris 0,93 secondes. Il y a des choses à craindre en matière d'efficacité, mais printfvs. putsn'en fait pas partie.
Steve Summit

10
@KonstantinWeitz: Mais (a) je n'utilisais pas gcc, et (b) peu importe pourquoi l'affirmation " putsest plus rapide" est fausse, c'est toujours faux.
Steve Summit

6
@KonstantinWeitz: La réclamation pour laquelle j'ai fourni des preuves était (le contraire de) la réclamation que l'utilisateur Black faisait. J'essaie juste de clarifier que les programmeurs ne devraient pas s'inquiéter d'appeler putspour cette raison. (Mais si vous vouliez en discuter: je serais surpris si vous pouviez trouver un compilateur moderne pour toute machine moderne où il putsest nettement plus rapide que printfdans toutes les circonstances.)
Steve Summit

75

printf("Hello world");

est bien et n'a aucune vulnérabilité de sécurité.

Le problème réside dans:

printf(p);

pest un pointeur vers une entrée contrôlée par l'utilisateur. Il est sujet aux attaques de chaînes de formatage : l'utilisateur peut insérer des spécifications de conversion pour prendre le contrôle du programme, par exemple %xpour vider la mémoire ou %npour écraser la mémoire.

Notez que son puts("Hello world")comportement n'est pas équivalent à printf("Hello world")mais à printf("Hello world\n"). Les compilateurs sont généralement assez intelligents pour optimiser ce dernier appel pour le remplacer par puts.


10
Bien sûr, ce printf(p,x)serait tout aussi problématique si l'utilisateur avait le contrôle sur p. Le problème n'est donc pas l'utilisation de printfavec un seul argument, mais plutôt avec une chaîne de format contrôlée par l'utilisateur.
Hagen von Eitzen

2
@HagenvonEitzen C'est techniquement vrai, mais peu utiliseraient délibérément une chaîne de format fournie par l'utilisateur. Quand les gens écrivent printf(p), c'est parce qu'ils ne se rendent pas compte que c'est une chaîne de format, ils pensent juste qu'ils impriment un littéral.
Barmar le

33

En plus des autres réponses, il printf("Hello world! I am 50% happy today")y a un bug facile à faire, causant potentiellement toutes sortes de problèmes de mémoire désagréables (c'est UB!).

Il est simplement plus simple, plus facile et plus robuste de «demander» aux programmeurs d'être absolument clairs lorsqu'ils veulent une chaîne textuelle et rien d'autre .

Et c'est ce qui printf("%s", "Hello world! I am 50% happy today")vous attire. C'est totalement infaillible.

(Steve, bien sûr, ce printf("He has %d cherries\n", ncherries)n'est absolument pas la même chose; dans ce cas, le programmeur n'est pas dans l'état d'esprit "chaîne verbatim"; elle est dans l'état d'esprit "chaîne de formatage".)


2
Cela ne vaut pas la peine d'être discuté, et je comprends ce que vous dites à propos de l'état d'esprit verbatim vs format string, mais bon, tout le monde ne pense pas de cette façon, ce qui est l'une des raisons pour lesquelles les règles universelles peuvent être dérangeantes. Dire "ne jamais imprimer les chaînes constantes avec printf" revient exactement à dire "toujours écrire if(NULL == p). Ces règles peuvent être utiles pour certains programmeurs, mais pas pour tous. Et dans les deux cas ( printfformats incompatibles et conditions Yoda), les compilateurs modernes avertissent quand même des erreurs, donc les règles artificielles sont encore moins importantes.
Steve Summit

1
@Steve S'il n'y a exactement aucun avantage à utiliser quelque chose, mais pas mal d'inconvénients, alors oui, il n'y a vraiment aucune raison de l'utiliser. Les conditions Yoda, d'un autre côté , ont l'inconvénient de rendre le code plus difficile à lire (vous diriez intuitivement "si p est zéro" et non "si zéro est p").
Voo le

2
@Voo printf("%s", "hello")va être plus lent que printf("hello"), donc il y a un inconvénient. Un petit, car IO est presque toujours beaucoup plus lent qu'un formatage aussi simple, mais un inconvénient.
Yakk - Adam Nevraumont

1
@Yakk Je doute que ce soit plus lent
MM

gcc -Wall -W -Werrorévitera les mauvaises conséquences de telles erreurs.
chqrlie le

17

Je vais juste ajouter quelques informations concernant la partie vulnérabilité ici.

On dit qu'il est vulnérable en raison de la vulnérabilité du format de chaîne printf. Dans votre exemple, où la chaîne est codée en dur, elle est inoffensive (même si le codage en dur de telles chaînes n'est jamais entièrement recommandé). Mais spécifier les types de paramètres est une bonne habitude à prendre. Prenons cet exemple:

Si quelqu'un met un caractère de chaîne de format dans votre printf au lieu d'une chaîne régulière (par exemple, si vous voulez imprimer le programme stdin), printf prendra tout ce qu'il peut sur la pile.

Il était (et est toujours) très utilisé pour exploiter des programmes en explorant des piles pour accéder à des informations cachées ou contourner l'authentification par exemple.

Exemple (C):

int main(int argc, char *argv[])
{
    printf(argv[argc - 1]); // takes the first argument if it exists
}

si je mets en entrée de ce programme "%08x %08x %08x %08x %08x\n"

printf ("%08x %08x %08x %08x %08x\n"); 

Cela indique à la fonction printf de récupérer cinq paramètres de la pile et de les afficher sous forme de nombres hexadécimaux remplis à 8 chiffres. Ainsi, une sortie possible peut ressembler à:

40012980 080628c4 bffff7a4 00000005 08059c04

Voir ceci pour une explication plus complète et d'autres exemples.


13

L'appel printfavec des chaînes de format littérales est sûr et efficace, et il existe des outils pour vous avertir automatiquement si votre appel printfavec les chaînes de format fournies par l'utilisateur n'est pas sûr.

Les attaques les plus sévères printftirent parti du %nspécificateur de format. Contrairement à tous les autres spécificateurs de format, par exemple %d, %nécrit en fait une valeur dans une adresse mémoire fournie dans l'un des arguments de format. Cela signifie qu'un attaquant peut écraser la mémoire et ainsi potentiellement prendre le contrôle de votre programme. Wikipedia fournit plus de détails.

Si vous appelez printfavec une chaîne de format littérale, un attaquant ne peut pas se faufiler %ndans votre chaîne de format et vous êtes donc en sécurité. En fait, gcc transformera votre appel printfen un appel à puts, donc il n'y a littéralement aucune différence (testez ceci en exécutant gcc -O3 -S).

Si vous appelez printfavec une chaîne de format fournie par l'utilisateur, un attaquant peut potentiellement insérer un %ndans votre chaîne de format et prendre le contrôle de votre programme. Votre compilateur vous avertira généralement que le sien n'est pas sûr, voir -Wformat-security. Il existe également des outils plus avancés qui garantissent qu'un appel de printfest sûr, même avec des chaînes de format fournies par l'utilisateur, et ils peuvent même vérifier que vous passez le bon nombre et le bon type d'arguments à printf. Par exemple, pour Java, il existe Google's Error Prone et Checker Framework .


12

Ce sont des conseils mal avisés. Oui, si vous avez une chaîne d'exécution à imprimer,

printf(str);

est assez dangereux et vous devriez toujours utiliser

printf("%s", str);

au lieu de cela, car en général, vous ne pouvez jamais savoir si strpeut contenir un %signe. Cependant, si vous avez une chaîne constante au moment de la compilation , il n'y a rien de mal avec

printf("Hello, world!\n");

(Entre autres choses, c'est le programme C le plus classique de tous les temps, littéralement du livre de programmation C de Genesis. Donc, quiconque désapprouve cet usage est plutôt hérétique, et moi, je serais quelque peu offensé!)


because printf's first argument is always a constant stringJe ne sais pas exactement ce que vous entendez par là.
Sebastian Mach

Comme je l'ai dit, "He has %d cherries\n"est une chaîne constante, ce qui signifie qu'il s'agit d'une constante au moment de la compilation. Mais, pour être honnête, le conseil de l'auteur n'était pas "Ne passez pas les chaînes constantes comme printfpremier argument", mais "Ne passez pas les chaînes sans %comme printfpremier argument".
Steve Summit

literally from the C programming book of Genesis. Anyone deprecating that usage is being quite offensively heretical- vous n'avez pas lu K&R ces dernières années. Il y a une tonne de conseils et de styles de codage qui sont non seulement obsolètes, mais tout simplement une mauvaise pratique de nos jours.
Voo le

@Voo: Eh bien, disons simplement que tout ce qui est considéré comme une mauvaise pratique n'est pas en fait une mauvaise pratique. (Le conseil de "ne jamais utiliser de plaine int" me vient à l'esprit.)
Steve Summit

1
@Steve Je n'ai aucune idée d'où vous avez entendu celui-là, mais ce n'est certainement pas le genre de mauvaise (mauvaise?) Pratique dont nous parlons ici. Ne vous méprenez pas, pour l'époque, le code était parfaitement correct, mais vous ne voulez vraiment pas regarder k & r pour beaucoup, mais comme une note historique ces jours-ci. "It's in k & r" n'est tout simplement pas un indicateur de bonne qualité ces jours-ci, c'est tout
Voo

9

Un aspect plutôt désagréable de printfest que même sur les plates-formes où la lecture de la mémoire parasite ne peut causer que des dommages limités (et acceptables), l'un des caractères de formatage %n, fait que l'argument suivant est interprété comme un pointeur vers un entier inscriptible, et provoque le nombre de caractères sortis jusqu'à présent pour être stockés dans la variable ainsi identifiée. Je n'ai jamais utilisé cette fonctionnalité moi-même, et parfois j'utilise des méthodes légères de style printf que j'ai écrites pour n'inclure que les fonctionnalités que j'utilise réellement (et n'inclut pas celle-là ou quoi que ce soit de similaire) mais alimentant les chaînes de fonctions printf standard reçues provenant de sources non fiables peuvent exposer des failles de sécurité au-delà de la capacité de lire un stockage arbitraire.


8

Puisque personne ne l'a mentionné, j'ajouterais une note concernant leurs performances.

Dans des circonstances normales, en supposant qu'aucune optimisation du compilateur ne soit utilisée (c'est-à-dire en printf()fait des appels printf()et non fputs()), je m'attendrais printf()à être moins efficace, en particulier pour les longues chaînes. En effet, printf()il faut analyser la chaîne pour vérifier s'il existe des spécificateurs de conversion.

Pour confirmer cela, j'ai effectué quelques tests. Les tests sont effectués sur Ubuntu 14.04, avec gcc 4.8.4. Ma machine utilise un processeur Intel i5. Le programme testé est le suivant:

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

Les deux sont compilés avec gcc -Wall -O0. Le temps est mesuré à l'aide de time ./a.out > /dev/null. Ce qui suit est le résultat d'une exécution typique (je les ai exécutés cinq fois, tous les résultats sont dans les 0,002 secondes).

Pour la printf()variante:

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

Pour la fputs()variante:

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

Cet effet est amplifié si vous avez une très longue corde.

#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;
}

Pour la printf()variante (exécutée trois fois, réel plus / moins 1,5 s):

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

Pour la fputs()variante (exécutée trois fois, réel plus / moins 0,2 s):

real    0m12.726s
user    0m8.152s
sys     0m4.581s

Remarque: Après avoir inspecté l'assembly généré par gcc, j'ai réalisé que gcc optimise l' fputs()appel à un fwrite()appel, même avec -O0. (L' printf()appel reste inchangé.) Je ne suis pas sûr que cela invalidera mon test, car le compilateur calcule la longueur de la chaîne fwrite()au moment de la compilation.


2
Cela n'invalidera pas votre test, comme cela fputs()est souvent utilisé avec des constantes de chaîne et cette opportunité d'optimisation fait partie du point que vous vouliez faire.Cela dit, ajouter un test avec une chaîne générée dynamiquement avec fputs()et fprintf()serait un bon point de données supplémentaire .
Patrick Schlüter

@ PatrickSchlüter Tester avec des chaînes générées dynamiquement semble cependant aller à l'encontre du but de cette question ... OP semble s'intéresser uniquement aux chaînes littérales à imprimer.
user12205

1
Il ne le déclare pas explicitement même si son exemple utilise des chaînes littérales. En fait, je pense que sa confusion à propos des conseils du livre est le résultat de l'utilisation de chaînes littérales dans l'exemple. Avec les chaînes littérales, les conseils des livres sont en quelque sorte douteux, avec des chaînes dynamiques, c'est un bon conseil.
Patrick Schlüter

1
/dev/nullen fait un jouet, dans la mesure où généralement lors de la génération d'une sortie formatée, votre objectif est que la sortie aille quelque part et ne soit pas rejetée. Une fois que vous avez ajouté le temps "ne pas supprimer les données", comment se comparent-ils?
Yakk - Adam Nevraumont

7
printf("Hello World\n")

compile automatiquement vers l'équivalent

puts("Hello World")

vous pouvez le vérifier en désassemblant votre exécutable:

push rbp
mov rbp,rsp
mov edi,str.Helloworld!
call dword imp.puts
mov eax,0x0
pop rbp
ret

en utilisant

char *variable;
... 
printf(variable)

entraînera des problèmes de sécurité, n'utilisez jamais printf de cette façon!

votre livre est donc correct, l'utilisation de printf avec une variable est obsolète mais vous pouvez toujours utiliser printf ("ma chaîne \ n") car elle deviendra automatiquement put


12
Ce comportement dépend en fait entièrement du compilateur.
Jabberwocky

6
C'est trompeur. Vous A compiles to Bdites, mais en réalité vous voulez dire A and B compile to C.
Sebastian Mach

6

Pour gcc, il est possible d'activer des avertissements spécifiques pour vérifier printf()et scanf().

La documentation gcc indique:

-Wformatest inclus dans -Wall. Pour plus de contrôle sur certains aspects de la forme de contrôle, les options -Wformat-y2k, -Wno-format-extra-args, -Wno-format-zero-length, -Wformat-nonliteral, -Wformat-securityet -Wformat=2sont disponibles, mais ne sont pas inclus dans -Wall.

Le -Wformatqui est activé dans l' -Walloption n'active pas plusieurs avertissements spéciaux qui aident à trouver ces cas:

  • -Wformat-nonliteral vous avertira si vous ne passez pas une chaîne littérale comme spécificateur de format.
  • -Wformat-securityvous avertira si vous passez une chaîne qui pourrait contenir une construction dangereuse. C'est un sous-ensemble de -Wformat-nonliteral.

Je dois admettre que l'activation a -Wformat-securityrévélé plusieurs bogues que nous avions dans notre base de code (module de journalisation, module de gestion des erreurs, module de sortie xml, tous avaient des fonctions qui pouvaient faire des choses indéfinies si elles avaient été appelées avec% caractères dans leur paramètre. Pour info, notre base de code a maintenant environ 20 ans et même si nous étions conscients de ce genre de problèmes, nous avons été extrêmement surpris lorsque nous avons activé ces avertissements, combien de ces bogues étaient encore dans la base de code).


1

En plus des autres réponses bien expliquées avec toutes les préoccupations secondaires couvertes, je voudrais donner une réponse précise et concise à la question posée.


Pourquoi printfun argument unique (sans spécificateurs de conversion) est-il obsolète?

Un printfappel de fonction avec un seul argument en général n'est pas obsolète et n'a pas non plus de vulnérabilités lorsqu'il est utilisé correctement comme vous coderez toujours.

C Les utilisateurs du monde entier, du statut de débutant à celui d'expert en statut, l'utilisent printfpour donner une simple phrase de texte en sortie à la console.

De plus, quelqu'un doit distinguer si ce seul et unique argument est une chaîne littérale ou un pointeur vers une chaîne, ce qui est valide mais généralement pas utilisé. Pour ce dernier, bien sûr, il peut se produire des sorties gênantes ou tout type de comportement non défini, lorsque le pointeur n'est pas correctement défini pour pointer vers une chaîne valide, mais ces choses peuvent également se produire si les spécificateurs de format ne correspondent pas aux arguments respectifs en donnant arguments multiples.

Bien sûr, il n'est pas non plus juste et correct que la chaîne, fournie comme un seul et unique argument, ait des spécificateurs de format ou de conversion, car il n'y aura pas de conversion.

Cela dit, donner un littéral de chaîne simple comme "Hello World!"comme seul argument sans aucun spécificateur de format à l'intérieur de cette chaîne comme vous l'avez fourni dans la question:

printf("Hello World!");

n'est pas du tout obsolète ou « mauvaise pratique » et n'a aucune vulnérabilité.

En fait, de nombreux programmeurs C commencent et ont commencé à apprendre et à utiliser C ou même les langages de programmation en général avec ce programme HelloWorld et cette printfdéclaration comme les premiers du genre.

Ils ne le seraient pas s'ils étaient obsolètes.

Dans un livre que je lis, il est écrit printfqu'avec un seul argument (sans spécificateurs de conversion) est obsolète.

Eh bien, alors je me concentrerais sur le livre ou sur l'auteur lui-même. Si un auteur fait vraiment de telles affirmations , à mon avis, des affirmations incorrectes et même enseigne cela sans expliquer explicitement pourquoi il / elle le fait (si ces affirmations sont vraiment littéralement équivalentes fournies dans ce livre), je considérerais cela comme un mauvais livre. Un bon livre, par opposition à cela, expliquera pourquoi éviter certains types de méthodes ou de fonctions de programmation.

D'après ce que j'ai dit ci-dessus, l'utilisation printfavec un seul argument (une chaîne littérale) et sans aucun spécificateur de format n'est en aucun cas déconseillée ou considérée comme une "mauvaise pratique" .

Vous devriez demander à l'auteur ce qu'il voulait dire par là ou, mieux encore, lui demander de clarifier ou de corriger la section relative pour la prochaine édition ou les empreintes en général.


Vous pourriez ajouter que ce printf("Hello World!");n'est pas équivalent à de puts("Hello World!");toute façon, ce qui en dit long sur l'auteur de la recommandation.
chqrlie le
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.