web-dev-qa-db-fra.com

Pourquoi printf avec un seul argument (sans spécificateurs de conversion) est-il déconseillé?

Dans un livre que je lis, il est écrit que printf avec un seul argument (sans spécificateurs de conversion) est déconseillé. 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 incorrect? Il est écrit dans le livre qu'il contient des vulnérabilités. Quelles sont ces vulnérabilités?

97
Iu Tub

printf("Hello World!"); est à mon humble avis pas vulnérable mais considérez ceci:

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

Si str pointe vers une chaîne contenant des spécificateurs de format %s, Votre programme affichera un comportement indéfini (principalement un plantage), tandis que puts(str) affichera simplement la chaîne telle quelle .

Exemple:

printf("%s");   //undefined behaviour (mostly crash)
puts("%s");     // displays "%s"
118
Jabberwocky

printf("Hello world");

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

Le problème réside dans:

printf(p);

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

Notez que puts("Hello world") n'est pas équivalent dans son comportement à 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.

73
ouah

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

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

Et c'est ce que printf("%s", "Hello world! I am 50% happy today") vous apporte. C'est entièrement infaillible.

(Steve, bien sûr 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 textuelle"; il est dans l'état d'esprit "format chaîne".)

32

Je vais juste ajouter un peu d'informations concernant la 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, lorsque la chaîne est codée en dur, elle est inoffensive (même si le codage en dur comme celui-ci n'est jamais entièrement recommandé). Mais spécifier les types de paramètres est une bonne habitude à prendre. Prenez 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 dans l'exploration de 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 demande à la fonction printf de récupérer cinq paramètres de la pile et de les afficher sous forme de nombres hexadécimaux à 8 chiffres. Ainsi, une sortie possible peut ressembler à:

40012980 080628c4 bffff7a4 00000005 08059c04

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

16
P1kachu

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

Les attaques les plus graves sur printf tirent parti de %n spécificateur de format. Contrairement à tous les autres spécificateurs de format, par ex. %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 printf avec une chaîne de format littérale, un attaquant ne peut pas furtivement un %n dans votre chaîne de formatage, et vous êtes donc en sécurité. En fait, gcc changera votre appel en printf en un appel en puts, donc il n'y a littéralement aucune différence (testez ceci en exécutant gcc -O3 -S).

Si vous appelez printf avec une chaîne de format fournie par l'utilisateur, un attaquant peut potentiellement dissimuler un %n dans votre chaîne de formatage et prenez le contrôle de votre programme. Votre compilateur vous avertit généralement que le sien est dangereux, voir -Wformat-security. Il existe également des outils plus avancés qui garantissent qu'une invocation de printf est sûre 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 y a Google's Error Prone et Checker Framework .

12
Konstantin Weitz

Ceci est un conseil erroné. Oui, si vous avez une chaîne d'exécution à imprimer,

printf(str);

est assez dangereux, et vous devez toujours utiliser

printf("%s", str);

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

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éprécie cet usage est plutôt hérétique, et moi, je serais quelque peu offensé!)

11
Steve Summit

Un aspect assez désagréable de printf est que même sur les plates-formes où la mémoire errante lit ne peut causer que des dommages limités (et acceptables), l'un des caractères de formatage, %n, fait en sorte que l'argument suivant soit interprété comme un pointeur sur un entier accessible en écriture et que le nombre de caractères émis jusqu'à présent soit stocké 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 inclure uniquement les fonctionnalités que j'utilise réellement (et ne pas inclure celle-là ou quelque chose de similaire) mais en alimentant les chaînes de fonctions d'impression standard reçues à partir de sources non fiables peuvent exposer des vulnérabilités de sécurité au-delà de la capacité de lire un stockage arbitraire.

9
supercat

Puisque personne n'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 que printf() appelle en fait printf() et non fputs()), je m'attendrais à ce que printf() effectuer moins efficacement, en particulier pour les longues cordes. Cela est dû au fait que printf() doit analyser la chaîne pour vérifier s'il existe des spécificateurs de conversion.

Pour confirmer cela, j'ai effectué quelques tests. Le test est effectué 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 un délai de 0,002 secondes).

Pour la variante printf():

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

Pour la variante fputs():

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

Cet effet est amplifié si vous avez une chaîne très longue .

#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 variante printf() (exécutée trois fois, réel plus/moins 1,5 s):

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

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

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

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

8
ace
printf("Hello World\n")

compile automatiquement à l'équivalent

puts("Hello World")

vous pouvez le vérifier en démontant 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!

donc votre livre est en fait correct, utiliser printf avec une variable est obsolète mais vous pouvez toujours utiliser printf ("ma chaîne\n") car il deviendra automatiquement des put

6
Ábrahám Endre

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

La documentation de gcc indique:

-Wformat Est inclus dans -Wall. Pour mieux contrôler certains aspects de la vérification du format, les options -Wformat-y2k, -Wno-format-extra-args, -Wno-format-zero-length, -Wformat-nonliteral, -Wformat-security Et -Wformat=2 Sont disponibles, mais ne sont pas inclus dans -Wall.

L'option -Wformat Qui est activée dans l'option -Wall N'active pas plusieurs avertissements spéciaux qui aident à trouver ces cas:

  • -Wformat-nonliteral Avertira si vous ne passez pas une chaîne littérale comme spécificateur de format.
  • -Wformat-security Avertira si vous passez une chaîne qui pourrait contenir une construction dangereuse. Il s'agit d'un sous-ensemble de -Wformat-nonliteral.

Je dois admettre que l'activation de -Wformat-security A ré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 pourraient faire des choses indéfinies si elles avaient été appelées avec% caractères dans 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).

4
Patrick Schlüter