Dlaczego printf z jednym argumentem (bez specyfikatorów konwersji) jest przestarzały?

102

W książce, którą czytam, jest napisane, że printfpojedynczy argument (bez specyfikatorów konwersji) jest przestarzały. Zaleca się zastąpić

printf("Hello World!");

z

puts("Hello World!");

lub

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

Czy ktoś może mi powiedzieć, dlaczego printf("Hello World!");się myli? W książce jest napisane, że zawiera luki. Co to za luki?

StackUser
źródło
34
Uwaga: printf("Hello World!")to nie to samo co puts("Hello World!"). puts()dołącza plik '\n'. Zamiast tego porównaj printf("abc")zfputs("abc", stdout)
chux - Przywróć Monikę
5
Co to za książka? Myślę, że nie printfjest przestarzałe w taki sam sposób, jak na przykład getsw C99, więc możesz rozważyć edycję pytania, aby było bardziej precyzyjne.
el.pescado
14
Wygląda na to, że książka, którą czytasz, nie jest zbyt dobra - dobra książka nie powinna tak po prostu mówić, że coś takiego jest „przestarzałe” (jest to nieprawdziwe, chyba że autor używa tego słowa do opisania własnej opinii) i powinna wyjaśniać, jakiego użycia jest faktycznie nieprawidłowy i niebezpieczny, zamiast pokazywać bezpieczny / prawidłowy kod jako przykład czegoś, czego „nie powinieneś robić”.
R .. GitHub PRZESTAŃ POMÓC NA LODZIE,
8
Czy możesz zidentyfikować książkę?
Keith Thompson,
7
Podaj tytuł książki, autora i odsyłacz do strony. Dzięki.
Greenonline,

Odpowiedzi:

122

printf("Hello World!"); czy IMHO nie jest wrażliwe, ale rozważ to:

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

Jeśli strzdarzy się, że wskaże łańcuch zawierający %sspecyfikatory formatu, Twój program będzie wykazywał niezdefiniowane zachowanie (głównie awarię), podczas gdy puts(str)po prostu wyświetli ciąg taki, jaki jest.

Przykład:

printf("%s");   //undefined behaviour (mostly crash)
puts("%s");     // displays "%s\n"
Jabberwocky
źródło
21
Oprócz powodowania awarii programu, istnieje wiele innych exploitów możliwych do wykorzystania w łańcuchach formatujących. Zobacz tutaj, aby uzyskać więcej informacji: en.wikipedia.org/wiki/Uncontrolled_format_string
e.dan
9
Innym powodem jest to, że putsprawdopodobnie będzie to szybsze.
edmz,
38
@black: putsjest „prawdopodobnie” szybszy i prawdopodobnie jest to kolejny powód, dla którego ludzie go polecają, ale w rzeczywistości nie jest szybszy. Właśnie wydrukowałem "Hello, world!"milion razy w obie strony. Dzięki printftemu zajęło to 0,92 sekundy. Dzięki putstemu zajęło to 0,93 sekundy. Są rzeczy, o które należy się martwić, jeśli chodzi o wydajność, ale printfvs. putsnie jest jednym z nich.
Steve Summit
10
@KonstantinWeitz: Ale (a) nie używałem gcc i (b) nie ma znaczenia, dlaczego twierdzenie „ putsjest szybsze” jest fałszywe, nadal jest fałszywe.
Steve Summit
6
@KonstantinWeitz: Twierdzenie, dla którego przedstawiłem dowody, było (przeciwieństwem) roszczenia, które złożył użytkownik Black. Próbuję tylko wyjaśnić, że programiści nie powinni się martwić o dzwonienie putsz tego powodu. (Ale gdybyś chciał się o to spierać: byłbym zaskoczony, gdybyś znalazł jakikolwiek nowoczesny kompilator dla dowolnej nowoczesnej maszyny, w której putsjest znacznie szybszy niż printfw jakichkolwiek okolicznościach.)
Steve Summit
75

printf("Hello world");

jest w porządku i nie ma luki w zabezpieczeniach.

Problem polega na:

printf(p);

gdzie pjest wskaźnikiem do wejścia kontrolowanego przez użytkownika. Jest podatny na ataki typu string : użytkownik może wstawiać specyfikacje konwersji, aby przejąć kontrolę nad programem, np. W %xcelu zrzucenia pamięci lub%n nadpisania pamięci.

Zwróć na to uwagę puts("Hello world") w zachowaniu nie jest to równoważne z, printf("Hello world")ale z printf("Hello world\n"). Kompilatory są zwykle na tyle sprytne, aby zoptymalizować to drugie wywołanie i zastąpić je puts.

ouah
źródło
10
Oczywiście printf(p,x)byłoby równie problematyczne, gdyby użytkownik miał nad nimi kontrolę p. Tak więc problem nie polega na używaniu printftylko jednego argumentu, ale raczej w przypadku ciągu formatu kontrolowanego przez użytkownika.
Hagen von Eitzen
2
@HagenvonEitzen To technicznie prawda, ale niewielu celowo użyłoby ciągu formatu dostarczonego przez użytkownika. Kiedy ludzie piszą printf(p), dzieje się tak, ponieważ nie zdają sobie sprawy, że jest to ciąg formatu, po prostu myślą, że drukują literał.
Barmar
34

W nawiązaniu do innych odpowiedzi, printf("Hello world! I am 50% happy today") jest to łatwy błąd do zrobienia, potencjalnie powodujący wszelkiego rodzaju nieprzyjemne problemy z pamięcią (to UB!).

Po prostu prostsze, łatwiejsze i bardziej niezawodne jest „wymaganie” od programistów absolutnej jasności, gdy chcą otrzymać dosłowny ciąg znaków i nic więcej .

I to właśnie printf("%s", "Hello world! I am 50% happy today")cię dostaje. Jest całkowicie niezawodny.

(Steve, oczywiście, printf("He has %d cherries\n", ncherries)to absolutnie nie to samo; w tym przypadku programista nie jest nastawiony na „ciąg znaków”; jest w nastawieniu „ciąg formatu”).

Lekkość wyścigów na orbicie
źródło
2
Nie warto się o to kłócić i rozumiem, co mówisz o podejściu dosłownie do formatu ciągów, ale cóż, nie wszyscy myślą w ten sposób, co jest jednym z powodów, dla których uniwersalne zasady mogą powodować problemy. Powiedzenie „nigdy nie drukuj stałych ciągów za pomocą printf” jest prawie tak samo, jak powiedzenie „zawsze pisz if(NULL == p). Te reguły mogą być przydatne dla niektórych programistów, ale nie dla wszystkich. W obu przypadkach (niezgodne printfformaty i warunki Yoda), nowoczesne kompilatory i tak ostrzegają przed błędami, więc sztuczne zasady są jeszcze mniej ważne
Steve Summit
1
@Steve Jeśli używanie czegoś ma dokładnie zero plusów, ale ma kilka wad, to tak, naprawdę nie ma powodu, aby z tego korzystać. Warunki Yoda z drugiej strony zrób mają wadę, że robią kod trudniejsze do odczytania (którą intuicyjnie powiedzieć „jeśli p jest zerem” nie „jeśli zero p”).
Voo,
2
@Voo printf("%s", "hello")będzie wolniejsze niż printf("hello"), więc jest wada . Mały, ponieważ IO jest prawie zawsze wolniejszy niż takie proste formatowanie, ale ma wadę.
Yakk - Adam Nevraumont
1
@Yakk Wątpię, żeby było wolniej
MM
gcc -Wall -W -Werrorzapobiegnie złym konsekwencjom takich błędów.
chqrlie
17

Dodam tutaj tylko trochę informacji dotyczących części dotyczącej luki .

Mówi się, że jest podatny na atak z powodu luki w formacie ciągu printf. W twoim przykładzie, gdzie ciąg jest zakodowany na stałe, jest nieszkodliwy (nawet jeśli ciągi takie jak ten nie są w pełni zalecane). Ale określanie typów parametrów jest dobrym nawykiem. Weź ten przykład:

Jeśli ktoś umieści znak ciągu formatu w twoim printf zamiast zwykłego łańcucha (powiedzmy, jeśli chcesz wydrukować program stdin), printf weźmie wszystko, co tylko może na stosie.

Był (i nadal jest) bardzo używany do wykorzystywania programów do przeszukiwania stosów w celu uzyskania dostępu do ukrytych informacji lub na przykład ominięcia uwierzytelniania.

Przykład (C):

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

jeśli wstawię jako dane wejściowe tego programu "%08x %08x %08x %08x %08x\n"

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

To instruuje funkcję printf, aby pobrać pięć parametrów ze stosu i wyświetlić je jako 8-cyfrowe wypełnione liczbami szesnastkowymi. Możliwe wyjście może wyglądać następująco:

40012980 080628c4 bffff7a4 00000005 08059c04

Zobacz to, aby uzyskać pełniejsze wyjaśnienie i inne przykłady.

P1kachu
źródło
13

Wywołanie printfprzy użyciu ciągów formatu literału jest bezpieczne i wydajne, a istnieją narzędzia, które automatycznie ostrzegają użytkownika, jeśli wywołanie printfciągów formatu dostarczonych przez użytkownika jest niebezpieczne.

Najpoważniejsze ataki printfwykorzystują specyfikator %nformatu. W przeciwieństwie do wszystkich innych specyfikatorów formatu, np %d, %nfaktycznie zapisuje wartość do pamięci adres podany w jednym z argumentów wielkoformatowych. Oznacza to, że osoba atakująca może nadpisać pamięć, a tym samym potencjalnie przejąć kontrolę nad Twoim programem. Wikipedia zawiera więcej szczegółów.

Jeśli wywołujesz printfz literalnym ciągiem formatu, osoba atakująca nie może wkraść się %ndo twojego ciągu formatu, a zatem jesteś bezpieczny. W rzeczywistości gcc zmieni twoje wywołanie printfna wywołanie do puts, więc nie ma żadnej różnicy ( sprawdź to uruchamiającgcc -O3 -S ).

Jeśli wywołujesz printfciąg formatu dostarczony przez użytkownika, osoba atakująca może potencjalnie wkraść się %ndo ciągu formatu i przejąć kontrolę nad Twoim programem. Twój kompilator zwykle ostrzega cię, że jego jest niebezpieczny, widzisz -Wformat-security. Istnieją również bardziej zaawansowane narzędzia, które zapewniają, że wywołanie programu printfjest bezpieczne nawet w przypadku ciągów formatu dostarczonych przez użytkownika, a nawet mogą one sprawdzać, czy przekazujesz odpowiednią liczbę i typ argumentów printf. Na przykład w przypadku Javy jest Google Error Prone i Checker Framework .

Konstantin Weitz
źródło
12

To jest błędna rada. Tak, jeśli masz do wydrukowania ciąg czasu wykonywania,

printf(str);

jest dość niebezpieczny i zawsze należy go używać

printf("%s", str);

zamiast tego, ponieważ generalnie nigdy nie wiadomo, czy strmoże zawierać %znak. Jeśli jednak masz ciąg znaków stałych czasu kompilacji , nie ma w tym nic złego

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

(Między innymi jest to najbardziej klasyczny program w C, jaki kiedykolwiek powstał, dosłownie z książki Genesis o programowaniu w C. Więc każdy, kto potępia to użycie, jest raczej heretycki, a ja byłbym trochę obrażony!)

Steve Summit
źródło
because printf's first argument is always a constant stringNie jestem do końca pewien, co masz na myśli.
Sebastian Mach,
Jak powiedziałem, "He has %d cherries\n"jest ciągiem stałym, co oznacza, że ​​jest to stała czasu kompilacji. Ale, żeby być uczciwym, rada autora nie brzmiała „nie podawaj stałych ciągów jako printfpierwszego argumentu”, lecz „nie przekazuj ciągów bez %jako printfpierwszego argumentu”.
Steve Summit
literally from the C programming book of Genesis. Anyone deprecating that usage is being quite offensively heretical- tak naprawdę nie czytałeś K&R w ostatnich latach. Jest tam mnóstwo porad i stylów kodowania, które są obecnie nie tylko przestarzałe, ale po prostu po prostu złe praktyki.
Voo,
@Voo: Cóż, powiedzmy, że nie wszystko, co jest uważane za złą praktykę, jest w rzeczywistości złą praktyką. (Rada, aby „nigdy nie używać zwykłych int”, przychodzi na myśl).
Steve Summit
1
@Steve Nie mam pojęcia, gdzie to słyszałeś, ale z pewnością nie jest to rodzaj złej (złej?) Praktyki, o której mówimy. Nie zrozum mnie źle, na razie kod był całkowicie w porządku, ale tak naprawdę nie chcesz teraz zbytnio patrzeć na k & r, ale jako na notatkę historyczną. „Jest w k & r” po prostu nie jest obecnie wskaźnikiem dobrej jakości, to wszystko
Voo
9

Raczej nieprzyjemnym aspektem printfjest to, że nawet na platformach, na których zabłąkane odczyty pamięci mogą spowodować tylko ograniczoną (i akceptowalną) szkodę, jeden ze znaków formatujących %npowoduje, że następny argument jest interpretowany jako wskaźnik do zapisywalnej liczby całkowitej i powoduje liczba wypisywanych dotychczas znaków, które mają być zapisane w zidentyfikowanej w ten sposób zmiennej. Nigdy sam nie korzystałem z tej funkcji, a czasami używam lekkich metod w stylu printf, które napisałem, aby zawierały tylko funkcje, których faktycznie używam (i nie zawierają tego ani niczego podobnego), ale dostarczają otrzymane standardowe ciągi funkcji printf z niewiarygodnych źródeł może ujawnić luki w zabezpieczeniach wykraczające poza możliwość odczytu dowolnej pamięci.

supercat
źródło
8

Ponieważ nikt o tym nie wspomniał, dodałbym uwagę dotyczącą ich wydajności.

W normalnych okolicznościach, zakładając, że nie są używane żadne optymalizacje kompilatora (tj. printf()Faktycznie wywołuje, printf()a nie fputs()), spodziewałbym printf()się działać mniej wydajnie, szczególnie w przypadku długich ciągów. Dzieje się tak, ponieważ printf()musi przeanalizować ciąg, aby sprawdzić, czy istnieją jakiekolwiek specyfikatory konwersji.

Aby to potwierdzić, przeprowadziłem kilka testów. Testowanie odbywa się na Ubuntu 14.04, z gcc 4.8.4. Mój komputer korzysta z procesora Intel i5. Testowany program wygląda następująco:

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

Oba są kompilowane z gcc -Wall -O0. Czas mierzy się za pomocą time ./a.out > /dev/null. Poniżej przedstawiono wynik typowego przebiegu (uruchomiłem je pięć razy, wszystkie wyniki mieszczą się w ciągu 0,002 sekundy).

Dla printf()wariantu:

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

Dla fputs()wariantu:

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

Ten efekt jest wzmacniany, jeśli masz bardzo długą strunę.

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

Dla printf()wariantu (uruchomiony trzykrotnie, rzeczywisty plus / minus 1,5 s):

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

Dla fputs()wariantu (uruchomiony trzykrotnie, rzeczywisty plus / minus 0,2 s):

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

Uwaga: Po sprawdzeniu zestawu wygenerowanego przez gcc zdałem sobie sprawę, że gcc optymalizuje fputs()wywołanie fwrite()wywołania, nawet z -O0. ( printf()Wywołanie pozostaje niezmienione.) Nie jestem pewien, czy spowoduje to unieważnienie mojego testu, ponieważ kompilator oblicza długość ciągu dla fwrite()w czasie kompilacji.

user12205
źródło
2
To nie będzie unieważnić test, jak fputs()często stosować stałych łańcuchowych i ta możliwość optymalizacji jest częścią momencie chciał make.This powiedział, dodając próbny z dynamicznie generowanych ciągiem z fputs()i fprintf()byłoby miło miejski punkt danych .
Patrick Schlüter
@ PatrickSchlüter Testowanie za pomocą dynamicznie generowanych ciągów wydaje się być sprzeczne z celem tego pytania ... OP wydaje się być zainteresowany tylko drukowanymi literałami łańcuchowymi.
user12205
1
Nie podaje tego wprost, nawet jeśli jego przykład używa literałów tekstowych. W rzeczywistości myślę, że jego pomieszanie z poradami zawartymi w książce wynika z użycia w przykładzie literałów tekstowych. W przypadku literałów strunowych, porady w książkach są niejasne, w przypadku dynamicznych łańcuchów jest to dobra rada.
Patrick Schlüter
1
/dev/nullw pewnym sensie sprawia to, że jest to zabawka, ponieważ zwykle podczas generowania sformatowanych danych wyjściowych celem jest, aby dane wyjściowe gdzieś trafiły, a nie zostały odrzucone. Gdy dodasz czas „faktycznie nie odrzuca danych”, jak się one porównują?
Yakk - Adam Nevraumont
7
printf("Hello World\n")

automatycznie kompiluje się do odpowiednika

puts("Hello World")

możesz to sprawdzić, demontując swój plik wykonywalny:

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

za pomocą

char *variable;
... 
printf(variable)

doprowadzi do problemów z bezpieczeństwem, nigdy nie używaj printf w ten sposób!

więc twoja książka jest rzeczywiście poprawna, używanie printf z jedną zmienną jest przestarzałe, ale nadal możesz używać printf ("mój ciąg \ n"), ponieważ automatycznie stanie się to puts

Ábrahám Endre
źródło
12
To zachowanie w rzeczywistości zależy całkowicie od kompilatora.
Jabberwocky
6
To jest mylące. Oświadczasz A compiles to B, ale w rzeczywistości masz na myśli A and B compile to C.
Sebastian Mach,
6

W przypadku gcc możliwe jest włączenie określonych ostrzeżeń do sprawdzania printf()i scanf().

Dokumentacja gcc stwierdza:

-Wformatjest zawarte w -Wall. Aby uzyskać większą kontrolę nad niektórymi aspektami formacie sprawdzających, opcje -Wformat-y2k, -Wno-format-extra-args, -Wno-format-zero-length, -Wformat-nonliteral, -Wformat-security, i -Wformat=2są dostępne, ale nie są włączone -Wall.

To, -Wformatco jest włączone w tej -Wallopcji, nie włącza kilku specjalnych ostrzeżeń, które pomagają znaleźć te przypadki:

  • -Wformat-nonliteral ostrzeże, jeśli nie przekażesz łańcucha liter jako specyfikatora formatu.
  • -Wformat-securityostrzeże, jeśli przekażesz ciąg, który może zawierać niebezpieczną konstrukcję. To podzbiór -Wformat-nonliteral.

Muszę przyznać, że włączenie -Wformat-securityujawniło kilka błędów, które mieliśmy w naszej bazie kodu (moduł logowania, moduł obsługi błędów, moduł wyjściowy xml, wszystkie miały pewne funkcje, które mogłyby robić niezdefiniowane rzeczy, gdyby zostały wywołane z% znakami w swoim parametrze. nasza baza kodów ma teraz około 20 lat i nawet jeśli byliśmy świadomi tego rodzaju problemów, byliśmy bardzo zaskoczeni, gdy włączyliśmy te ostrzeżenia, ile z tych błędów nadal znajduje się w bazie kodu).

Patrick Schlüter
źródło
1

Oprócz innych dobrze wyjaśnionych odpowiedzi, z uwzględnieniem wszelkich wątpliwości pobocznych, chciałbym udzielić dokładnej i zwięzłej odpowiedzi na zadane pytanie.


Dlaczego printfpojedynczy argument (bez specyfikatorów konwersji) jest przestarzały?

printfWywołanie funkcji za pomocą jednego argumentu w ogóle jest nie przestarzałe i również nie ma słabych punktów , gdy stosowane właściwie jak zawsze będą kodować.

C Użytkownicy z całego świata, od początkujących do ekspertów zajmujących się statusem, używają printftego sposobu, aby przekazać do konsoli prostą frazę tekstową.

Co więcej, ktoś musi rozróżnić, czy ten jedyny argument jest literałem ciągu, czy wskaźnikiem do łańcucha, który jest prawidłowy, ale często nie jest używany. W tym ostatnim przypadku mogą oczywiście wystąpić niewygodne dane wyjściowe lub dowolny rodzaj niezdefiniowanego zachowania , gdy wskaźnik nie jest ustawiony prawidłowo, aby wskazywał na prawidłowy ciąg, ale te rzeczy mogą również wystąpić, jeśli specyfikatory formatu nie pasują do odpowiednich argumentów, dając wiele argumentów.

Oczywiście nie jest również właściwe i właściwe, aby łańcuch, podany jako jeden i jedyny argument, miał jakikolwiek specyfikator formatu lub konwersji, ponieważ konwersja nie będzie miała miejsca.

To powiedziawszy, podając prosty literał ciągu, taki jak "Hello World!"jako jedyny argument bez żadnych specyfikatorów formatu wewnątrz tego ciągu, tak jak podałeś go w pytaniu:

printf("Hello World!");

nie jest przestarzały ani nie jest „ złą praktyką” ”, ani nie ma żadnych luk w zabezpieczeniach.

W rzeczywistości wielu programistów C zaczyna i zaczyna uczyć się i używać języka C, a nawet ogólnie języków programowania, z tym programem HelloWorld i tym printfoświadczeniem jako pierwszymi w swoim rodzaju.

Nie byłyby takie, gdyby zostały wycofane.

W książce, którą czytam, jest napisane, że printfpojedynczy argument (bez specyfikatorów konwersji) jest przestarzały.

Cóż, wtedy skupiłbym się na książce lub samym autorze. Jeśli autor naprawdę robi takie, moim zdaniem, błędne twierdzenia, a nawet naucza, że ​​bez wyraźnego wyjaśnienia, dlaczego to robi (jeśli te twierdzenia są naprawdę dosłownie równoważne w tej książce), uznałbym ją za złą książkę. W przeciwieństwie do tego dobra książka powinna wyjaśniać, dlaczego należy unikać pewnego rodzaju metod lub funkcji programowania.

Zgodnie z tym, co powiedziałem powyżej, używanie printftylko jednego argumentu (literału ciągu znaków) i bez żadnych specyfikatorów formatu nie jest w żadnym wypadku uznawane za przestarzałe ani uważane za „złą praktykę” .

Powinieneś zapytać autora, co miał na myśli, a nawet lepiej, zwróć uwagę, aby wyjaśnił lub poprawił odpowiednią sekcję dla następnego wydania lub w ogóle.

RobertS wspiera Monikę Cellio
źródło
Możesz dodać, że nieprintf("Hello World!"); jest to równoznaczne z i puts("Hello World!");tak, co mówi coś o autorze rekomendacji.
chqrlie