Perché printf con un singolo argomento (senza specificatori di conversione) è deprecato?


102

In un libro che sto leggendo, è scritto che printfcon un singolo argomento (senza specificatori di conversione) è deprecato. Si consiglia di sostituire

printf("Hello World!");

con

puts("Hello World!");

o

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

Qualcuno può dirmi perché printf("Hello World!");è sbagliato? È scritto nel libro che contiene vulnerabilità. Quali sono queste vulnerabilità?


34
Nota: nonprintf("Hello World!") è la stessa di . aggiunge un . Invece di confrontare conputs("Hello World!")puts()'\n'printf("abc")fputs("abc", stdout)
chux

5
Cos'è quel libro? Non penso che printfsia deprecato nello stesso modo in cui, ad esempio, getsè deprecato in C99, quindi potresti considerare di modificare la tua domanda per essere più preciso.
el.pescado

14
Sembra che il libro che stai leggendo non sia molto buono - un buon libro non dovrebbe semplicemente dire qualcosa del genere è "deprecato" (è effettivamente falso a meno che l'autore non usi la parola per descrivere la propria opinione) e dovrebbe spiegare quale uso è effettivamente non valido e pericoloso piuttosto che mostrare un codice sicuro / valido come esempio di qualcosa che "non dovresti fare".
R .. GitHub STOP AIUTO IL GHIACCIO

8
Sai identificare il libro?
Keith Thompson

7
Si prega di specificare il titolo del libro, l'autore e il riferimento della pagina. Grazie.
Greenonline

Risposte:


122

printf("Hello World!"); IMHO non è vulnerabile ma considera questo:

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

Se strcapita di puntare a una stringa contenente %sspecificatori di formato, il tuo programma mostrerà un comportamento indefinito (principalmente un arresto anomalo), mentre puts(str)mostrerà semplicemente la stringa così com'è.

Esempio:

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

21
Oltre a causare l'arresto anomalo del programma, sono possibili molti altri exploit con le stringhe di formato. Vedi qui per maggiori informazioni: en.wikipedia.org/wiki/Uncontrolled_format_string
e.dan

9
Un altro motivo è che putspresumibilmente sarà più veloce.
edmz

38
@black: putsè "presumibilmente" più veloce, e questo è probabilmente un altro motivo per cui la gente lo consiglia, ma in realtà non è più veloce. Ho appena stampato "Hello, world!"1.000.000 di volte, in entrambi i modi. Con printfci sono voluti 0,92 secondi. Con putsci sono voluti 0,93 secondi. Ci sono cose di cui preoccuparsi quando si tratta di efficienza, ma printfvs. putsnon è una di queste.
Steve Summit

10
@KonstantinWeitz: Ma (a) non stavo usando gcc e (b) non importa perché l'affermazione " putsè più veloce" è falsa, è ancora falsa.
Steve Summit

6
@KonstantinWeitz: L'affermazione per cui ho fornito le prove era (l'opposto) l'affermazione che l'utente nero stava facendo. Sto solo cercando di chiarire che i programmatori non dovrebbero preoccuparsi di chiamare putsper questo motivo. (Ma se volessi discuterne: sarei sorpreso se potessi trovare un compilatore moderno per qualsiasi macchina moderna dove putsè significativamente più veloce che printfin qualsiasi circostanza.)
Steve Summit

75

printf("Hello world");

va bene e non ha vulnerabilità di sicurezza.

Il problema sta con:

printf(p);

dove pè un puntatore a un input controllato dall'utente. È soggetto ad attacchi di stringhe di formattazione : l'utente può inserire specifiche di conversione per assumere il controllo del programma, ad esempio %xper eseguire il dump della memoria o %nper sovrascrivere la memoria.

Nota che puts("Hello world")non è equivalente nel comportamento a printf("Hello world")ma a printf("Hello world\n"). I compilatori di solito sono abbastanza intelligenti da ottimizzare l'ultima chiamata per sostituirla puts.


10
Naturalmente printf(p,x)sarebbe altrettanto problematico se l'utente avesse il controllo su p. Quindi il problema non è l'uso di printfcon un solo argomento, ma piuttosto con una stringa di formato controllata dall'utente.
Hagen von Eitzen

2
@HagenvonEitzen Tecnicamente è vero, ma pochi userebbero deliberatamente una stringa di formato fornita dall'utente. Quando le persone scrivono printf(p), è perché non si rendono conto che si tratta di una stringa di formato, pensano solo di stampare un letterale.
Barmar

33

Oltre alle altre risposte, printf("Hello world! I am 50% happy today")c'è un bug facile da creare, che potenzialmente causa ogni sorta di brutti problemi di memoria (è UB!).

È solo più semplice, più facile e più robusto "richiedere" ai programmatori di essere assolutamente chiari quando vogliono una stringa letterale e nient'altro .

Ed è questo che printf("%s", "Hello world! I am 50% happy today")ti prende. È del tutto infallibile.

(Steve, ovviamente non printf("He has %d cherries\n", ncherries)è assolutamente la stessa cosa; in questo caso, il programmatore non è nella mentalità "stringa verbatim"; lei è nella mentalità "stringa formato".)


2
Questo non vale la pena discutere, e capisco cosa stai dicendo sulla mentalità parola per parola e stringa di formato, ma, beh, non tutti la pensano in questo modo, che è una delle ragioni per cui le regole valide per tutti possono essere irritate. Dire "non stampare mai stringhe costanti con printf" è esattamente come dire "scrivi sempre if(NULL == p). Queste regole possono essere utili per alcuni programmatori, ma non tutti. E in entrambi i casi ( printfformati non corrispondenti e condizionali di Yoda), i compilatori moderni avvertono comunque degli errori, quindi le regole artificiali sono ancora meno importanti
Steve Summit

1
@Steve Se ci sono esattamente zero vantaggi nell'usare qualcosa, ma parecchi svantaggi, allora sì, non c'è davvero alcun motivo per usarlo. Condizioni Yoda invece do hanno il rovescio della medaglia che fanno il codice più difficile da leggere (si sarebbe intuitivamente dire "se p è zero", non "se zero è p").
Voo

2
@Voo printf("%s", "hello")sarà più lento di printf("hello"), quindi c'è uno svantaggio. Piccolo, perché l'IO è quasi sempre molto più lento di una formattazione così semplice, ma è un aspetto negativo.
Yakk - Adam Nevraumont

1
@Yakk dubito che sarebbe più lento
MM

gcc -Wall -W -Werroreviterà cattive conseguenze da tali errori.
chqrlie

17

Aggiungerò solo un po 'di informazioni sulla parte della vulnerabilità qui.

Si dice che sia vulnerabile a causa della vulnerabilità del formato stringa printf. Nel tuo esempio, dove la stringa è hardcoded, è innocua (anche se stringhe hardcoding come questa non sono mai completamente consigliate). Ma specificare i tipi del parametro è una buona abitudine da prendere. Prendi questo esempio:

Se qualcuno inserisce il carattere della stringa di formato nel tuo printf invece di una stringa normale (ad esempio, se vuoi stampare il programma stdin), printf prenderà tutto ciò che può nello stack.

Era (ed è ancora) molto utilizzato per sfruttare i programmi nell'esplorazione di stack per accedere a informazioni nascoste o bypassare l'autenticazione, ad esempio.

Esempio (C):

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

se metto come input di questo programma "%08x %08x %08x %08x %08x\n"

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

Questo indica alla funzione printf di recuperare cinque parametri dallo stack e di visualizzarli come numeri esadecimali riempiti di 8 cifre. Quindi un possibile output potrebbe essere simile a:

40012980 080628c4 bffff7a4 00000005 08059c04

Vedi questo per una spiegazione più completa e altri esempi.


13

La chiamata printfcon stringhe di formato letterali è sicura ed efficiente ed esistono strumenti per avvisarti automaticamente se la tua invocazione di printfstringhe di formato fornite dall'utente non è sicura.

Gli attacchi più gravi printfsfruttano l' %nidentificatore di formato. A differenza di tutti gli altri specificatori di formato, ad esempio %d, %nscrive effettivamente un valore in un indirizzo di memoria fornito in uno degli argomenti di formato. Ciò significa che un utente malintenzionato può sovrascrivere la memoria e quindi potenzialmente assumere il controllo del programma. Wikipedia fornisce maggiori dettagli.

Se chiami printfcon una stringa di formato letterale, un utente malintenzionato non può %nintrodurne una nella stringa di formato e sei così al sicuro. In effetti, gcc cambierà la tua chiamata printfin una chiamata a puts, quindi non c'è nessuna differenza (prova questo eseguendo gcc -O3 -S).

Se si chiama printfcon una stringa di formato fornita dall'utente, un utente malintenzionato può potenzialmente %nintrodurne una nella stringa di formato e assumere il controllo del programma. Il tuo compilatore di solito ti avvertirà che il suo non è sicuro, vedi -Wformat-security. Esistono anche strumenti più avanzati che assicurano che una chiamata di printfsia sicura anche con le stringhe di formato fornite dall'utente e potrebbero persino verificare che tu passi il numero e il tipo di argomenti corretti a printf. Ad esempio, per Java ci sono Google Error Prone e Checker Framework .


12

Questo è un consiglio fuorviante. Sì, se hai una stringa di runtime da stampare,

printf(str);

è abbastanza pericoloso e dovresti sempre usare

printf("%s", str);

invece, perché in generale non puoi mai sapere se strpotrebbe contenere un %segno. Tuttavia, se hai una stringa costante in fase di compilazione , non c'è niente di sbagliato in questo

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

(Tra le altre cose, questo è il programma in C più classico di sempre, letteralmente dal libro di programmazione in C della Genesi. Quindi chiunque deprecare quell'uso è piuttosto eretico, e io per primo sarei un po 'offeso!)


because printf's first argument is always a constant stringNon sono esattamente sicuro di cosa intendi con questo.
Sebastian Mach

Come ho detto, "He has %d cherries\n"è una stringa costante, il che significa che è una costante in fase di compilazione. Ma, per essere onesti, il consiglio dell'autore non è stato "non passare stringhe costanti come printfprimo argomento", è stato "non passare stringhe senza %come printfprimo argomento".
Steve Summit

literally from the C programming book of Genesis. Anyone deprecating that usage is being quite offensively heretical- Non hai effettivamente letto K&R negli ultimi anni. Ci sono un sacco di consigli e stili di codifica che non sono solo deprecati ma semplicemente pessimi pratiche in questi giorni.
Voo

@Voo: Beh, diciamo solo che non tutto ciò che è considerato una cattiva pratica è in realtà una cattiva pratica. ( intMi viene in mente il consiglio di "non usare mai la pianura ".)
Steve Summit

1
@Steve Non ho idea di dove l'hai sentito, ma non è certo il tipo di pratica cattiva (cattiva?) Di cui stiamo parlando lì. Non fraintendetemi, per l'epoca il codice andava benissimo, ma in questi giorni non volete guardare a k & r per molto, ma come una nota storica. "È in k & r" semplicemente non è un indicatore di buona qualità in questi giorni, tutto qui
Voo

9

Un aspetto piuttosto sgradevole printfè che anche su piattaforme in cui le letture vaganti della memoria potrebbero causare solo un danno limitato (e accettabile), uno dei caratteri di formattazione %n, fa sì che l'argomento successivo venga interpretato come un puntatore a un numero intero scrivibile e fa sì che il numero di caratteri emessi fino a quel momento da memorizzare nella variabile così identificata. Non ho mai usato quella funzionalità da solo, ea volte utilizzo metodi leggeri in stile printf che ho scritto per includere solo le funzionalità che uso effettivamente (e non includere quella o qualcosa di simile) ma alimentando le stringhe di funzioni printf standard ricevute da fonti inaffidabili possono esporre vulnerabilità di sicurezza oltre la capacità di leggere archivi arbitrari.


8

Dato che nessuno l'ha menzionato, aggiungerei una nota sulla loro performance.

In circostanze normali, supponendo che non vengano utilizzate ottimizzazioni del compilatore (cioè printf()chiamate effettivamente printf()e non fputs()), mi aspetto printf()di eseguire in modo meno efficiente, specialmente per stringhe lunghe. Questo perché printf()deve analizzare la stringa per verificare se ci sono specificatori di conversione.

A conferma di ciò, ho eseguito alcuni test. Il test viene eseguito su Ubuntu 14.04, con gcc 4.8.4. La mia macchina utilizza una CPU Intel i5. Il programma in prova è il seguente:

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

Entrambi sono compilati con gcc -Wall -O0. Il tempo viene misurato usando time ./a.out > /dev/null. Quello che segue è il risultato di una corsa tipica (li ho eseguiti cinque volte, tutti i risultati sono entro 0,002 secondi).

Per la printf()variante:

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

Per la fputs()variante:

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

Questo effetto è amplificato se hai una corda molto lunga.

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

Per la printf()variante (eseguita tre volte, più / meno 1,5 reali):

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

Per la fputs()variante (eseguita tre volte, più / meno 0,2 reali):

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

Nota: dopo aver ispezionato l'assembly generato da gcc, mi sono reso conto che gcc ottimizza la fputs()chiamata a una fwrite()chiamata, anche con -O0. (La printf()chiamata rimane invariata.) Non sono sicuro che ciò invaliderà il mio test, poiché il compilatore calcola la lunghezza della stringa fwrite()in fase di compilazione.


2
Non invaliderà il tuo test, poiché fputs()viene spesso utilizzato con le costanti di stringa e questa opportunità di ottimizzazione è parte del punto che volevi fare. Detto questo, aggiungere un test eseguito con una stringa generata dinamicamente con fputs()e fprintf()sarebbe un bel punto dati supplementare .
Patrick Schlüter

@ PatrickSchlüter Testare con stringhe generate dinamicamente sembra vanificare lo scopo di questa domanda, però ... OP sembra essere interessato solo alle stringhe letterali da stampare.
user12205

1
Non lo afferma esplicitamente anche se il suo esempio utilizza stringhe letterali. In effetti, penso che la sua confusione sui consigli del libro sia il risultato dell'uso di stringhe letterali nell'esempio. Con le stringhe letterali, il consiglio dei libri è in qualche modo dubbio, con le stringhe dinamiche è un buon consiglio.
Patrick Schlüter

1
/dev/nullin qualche modo lo rende un giocattolo, in quanto di solito quando si genera un output formattato, l'obiettivo è che l'output vada da qualche parte, non venga scartato. Una volta aggiunto il tempo "in realtà non scarto i dati", come si confrontano?
Yakk - Adam Nevraumont

7
printf("Hello World\n")

compila automaticamente l'equivalente

puts("Hello World")

puoi verificarlo smontando il tuo eseguibile:

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

utilizzando

char *variable;
... 
printf(variable)

porterà a problemi di sicurezza, non usare mai printf in questo modo!

quindi il tuo libro è effettivamente corretto, l'uso di printf con una variabile è deprecato ma puoi ancora usare printf ("la mia stringa \ n") perché diventerà automaticamente put


12
Questo comportamento in realtà dipende interamente dal compilatore.
Jabberwocky

6
Questo è fuorviante. Affermi A compiles to B, ma in realtà intendi A and B compile to C.
Sebastian Mach

6

Per gcc è possibile abilitare avvisi specifici per il controllo printf()e scanf().

La documentazione di gcc afferma:

-Wformatè incluso in -Wall. Per un maggiore controllo su alcuni aspetti del formato di controllo, le opzioni -Wformat-y2k, -Wno-format-extra-args, -Wno-format-zero-length, -Wformat-nonliteral, -Wformat-security, e -Wformat=2sono disponibili, ma non sono inclusi nel -Wall.

L' opzione -Wformatabilitata all'interno -Walldell'opzione non abilita diversi avvisi speciali che aiutano a trovare questi casi:

  • -Wformat-nonliteral avviserà se non si passa una stringa letterale come specificatore di formato.
  • -Wformat-securityavviserà se passi una stringa che potrebbe contenere un costrutto pericoloso. È un sottoinsieme di -Wformat-nonliteral.

Devo ammettere che l'abilitazione ha -Wformat-securityrivelato diversi bug che avevamo nella nostra base di codice (modulo di registrazione, modulo di gestione degli errori, modulo di output xml, tutti avevano alcune funzioni che potevano fare cose indefinite se fossero stati chiamati con caratteri% nel loro parametro. Per informazioni, la nostra base di codice ha ora circa 20 anni e anche se eravamo a conoscenza di questo tipo di problemi, siamo rimasti estremamente sorpresi quando abbiamo abilitato questi avvisi di quanti di questi bug erano ancora nella base di codice).


1

Oltre alle altre risposte ben spiegate con tutte le preoccupazioni secondarie trattate, vorrei dare una risposta precisa e concisa alla domanda fornita.


Perché printfcon un singolo argomento (senza specificatori di conversione) è deprecato?

Una printfchiamata di funzione con un singolo argomento in generale non è deprecata e non presenta vulnerabilità se usata correttamente, come sempre si codifica.

C Gli utenti di tutto il mondo, dallo status principiante all'esperto dello stato usano printfquesto modo per fornire una semplice frase di testo come output alla console.

Inoltre, Qualcuno deve distinguere se questo unico argomento è una stringa letterale o un puntatore a una stringa, che è valida ma comunemente non utilizzata. Per quest'ultimo, ovviamente, possono verificarsi output scomodi o qualsiasi tipo di comportamento indefinito , quando il puntatore non è impostato correttamente per puntare a una stringa valida ma queste cose possono verificarsi anche se gli specificatori di formato non corrispondono ai rispettivi argomenti dando più argomenti.

Naturalmente, non è nemmeno giusto e corretto che la stringa, fornita come unico argomento, abbia specificatori di formato o conversione, poiché non verrà eseguita alcuna conversione.

Detto questo, dare una semplice stringa letterale come "Hello World!"come unico argomento senza specificatori di formato all'interno di quella stringa come hai fornito nella domanda:

printf("Hello World!");

non è affatto deprecato o " cattiva pratica " né presenta vulnerabilità.

In effetti, molti programmatori C iniziano e hanno iniziato a imparare e utilizzare il C o anche i linguaggi di programmazione in generale con quel programma HelloWorld e questa printfdichiarazione come primi nel suo genere.

Non lo sarebbero se fossero deprecati.

In un libro che sto leggendo, è scritto che printfcon un singolo argomento (senza specificatori di conversione) è deprecato.

Bene, allora concentrerei l'attenzione sul libro o sull'autore stesso. Se un autore sta davvero facendo tali, a mio parere, affermazioni errate e persino insegnando che senza spiegare esplicitamente perché lo sta facendo (se quelle affermazioni sono davvero letteralmente equivalenti fornite in quel libro), lo considererei un brutto libro. Un buon libro, al contrario di quello, spiegherà perché evitare certi tipi di metodi o funzioni di programmazione.

Secondo quanto detto sopra, l'utilizzo printfcon un solo argomento (una stringa letterale) e senza specificatori di formato non è in ogni caso deprecato o considerato "cattiva pratica" .

Dovresti chiedere all'autore cosa intendeva con questo o, meglio ancora, badare a chiarire o correggere la sezione relativa per la prossima edizione o le impronte in generale.


Potresti aggiungere che nonprintf("Hello World!"); è comunque equivalente a puts("Hello World!");, il che dice qualcosa sull'autore della raccomandazione.
chqrlie
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.