単一の引数を持つprintf(変換指定子なし)が廃止されたのはなぜですか?


102

私が読んでいる本でprintfは、単一の引数(変換指定子なし)は非推奨であると書かれています。代用をお勧めします

printf("Hello World!");

puts("Hello World!");

または

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

誰かがなぜprintf("Hello World!");間違っているのか教えてもらえますか?本には、脆弱性が含まれていると書かれています。これらの脆弱性は何ですか?


34
注:はと同じでprintf("Hello World!")はありませんputs("Hello World!")puts()を追加し'\n'ます。代わりprintf("abc")fputs("abc", stdout)
chux-復活モニカ2015年

5
その本は何ですか?printfたとえばgetsC99で廃止されたのと同じ方法で廃止されたとは思わないので、質問をより正確に編集することを検討してください。
el.pescado 2015

14
あなたが読んでいる本はあまり良くないようです-良い本はこのようなことを単に「非推奨」と言うべきではありません(著者が自分の意見を説明するために単語を使用しているのでなければ事実上誤りです) 「すべきでない」ことの例として安全/有効なコードを示すのではなく、実際には無効で危険です。
R .. GitHub STOP HELPING ICE

8
本を特定できますか?
キーストンプソン

7
本のタイトル、著者、ページ参照を指定してください。どうも。
Greenonline 2015

回答:


122

printf("Hello World!"); 私見は脆弱ではありませんが、これを考慮してください:

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

場合strを含む文字列へのポイントはどうなる%sの書式指定子、あなたのプログラムは一方で、未定義の動作(主にクラッシュ)を展示いたしますputs(str)ですよう単なる文字列が表示されます。

例:

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

21
プログラムをクラッシュさせるだけでなく、フォーマット文字列を使用する他の多くのエクスプロイトがあります。詳細については、こちらをご覧ください:en.wikipedia.org/wiki/Uncontrol_format_string
e.dan

9
別の理由は、putsおそらくそれがより速くなることです。
edmz

38
@black:puts「おそらく」高速であり、これはおそらく人々がそれを推奨するもう1つの理由ですが、実際には高速ではありません。"Hello, world!"両方の方法で100万回印刷したところです。printfそれは0.92秒かかりました。putsそれは0.93秒かかりました。効率に関しては気になる点がありますが、printfvs putsはその1つではありません。
スティーブサミット

10
@KonstantinWeitz:しかし、(a)私はgccを使用していませんでした。(b)「高速である」という主張が間違っている理由は問題ではありませんがputsまだ間違っています。
スティーブサミット

6
@KonstantinWeitz:私が証拠を提供した主張は、ユーザーの黒人が行った主張(の反対)でした。私は、プログラマーがputsこの理由で呼び出しを心配するべきではないことを明らかにしようとしています。(しかし、あなたがそれについて議論したい場合:どんな状況下putsよりもはるかに高速である現代のマシン用の現代のコンパイラを見つけることができれば私は驚きますprintf。)
Steve Summit

75

printf("Hello world");

問題なく、セキュリティ上の脆弱性はありません。

問題は次のとおりです。

printf(p);

ここpで、はユーザーが制御する入力へのポインターです。これは、フォーマット文字列攻撃の傾向があり ます。たとえば、%xメモリをダンプしたりメモリ%nを上書きしたりするなど、プログラムを制御するために変換仕様を挿入できます。

注意puts("Hello world")と行動で同等ではありませんprintf("Hello world")が、しますprintf("Hello world\n")。コンパイラーは通常、後者の呼び出しを最適化してに置き換えるのに十分なほどスマートputsです。


10
もちろん printf(p,x)、ユーザーがを制御できる場合も同様に問題になりますp。問題があるようでないの使用printfだけで一つの引数を持つのではなく、ユーザ制御フォーマット文字列を持ちます。
ハーゲンフォンアイツェン2015

2
@HagenvonEitzenこれは技術的には真実ですが、ユーザーが提供したフォーマット文字列を意図的に使用するものはほとんどありません。人々がを書くときprintf(p)、それはフォーマット文字列であることに気づいていないからです。彼らはリテラルを印刷していると思っているだけです。
Barmar 2015

33

他の答えに加えprintf("Hello world! I am 50% happy today")て、簡単なバグがあり、あらゆる種類の厄介なメモリ問題を引き起こす可能性があります(UBです!)。

プログラマーが逐語的文字列だけを必要とし、それ以外は何も必要としない場合は、プログラマーに絶対に明確することを要求する方が、単純で簡単で堅牢です

そして、それprintf("%s", "Hello world! I am 50% happy today")はあなたを得るものです。それは完全に簡単です。

(もちろん、スティーブprintf("He has %d cherries\n", ncherries)はまったく同じものではありません。この場合、プログラマーは「逐語的文字列」の考え方ではなく、「フォーマット文字列」の考え方です。)


2
これは議論の価値はありません。私はあなたが逐語的対フォーマット文字列の考え方についてあなたが言っていることを理解していますが、まあ、誰もがそのように考えているわけではありません。「で定数文字列を出力しprintfない」と言うのは、「常に書く」とほぼ同じif(NULL == p)です。これらのルールは、一部のプログラマーにとっては役立つかもしれませんが、すべてではありません。両方の場合(printf形式の不一致とYodaの条件文)、いずれにせよ、現代のコンパイラは間違いについて警告します。したがって、人為的なルールの重要性はさらに低くなります
スティーブサミット

1
@Steve何かを使用することの利点がまったくないが、かなりの欠点がある場合、はい、実際にそれを使用する理由はありません。一方、依田条件でください、彼らはコードが難しい読み取るために作ることの欠点を持っている(「ゼロがpであれば、」ない「pがゼロであれば、」あなたは直感的に言うだろう)。
Voo、2015

2
@Voo printf("%s", "hello")はより遅くなるprintf("hello")ため、マイナス面があります。IOはほとんどの場合、このような単純なフォーマットよりも速度が遅いため、小さな問題ですが、欠点があります。
Yakk-Adam Nevraumont 2015

1
@ヤク私はそれが遅くなることを疑う
MM

gcc -Wall -W -Werrorそのような間違いによる悪い結果を防ぎます。
chqrlie

17

ここで脆弱性の部分に関する情報少し追加します。

printf文字列形式の脆弱性のため、脆弱であると言われています。あなたの例では、文字列がハードコードされているので、それは無害です(このような文字列をハードコードすることが決して完全に推奨されていなくても)。ただし、パラメータのタイプを指定することは、習慣としてはよい習慣です。この例を見てみましょう:

誰かが通常の文字列の代わりにprintfにフォーマット文字列文字を入れた場合(たとえば、プログラムstdinを印刷したい場合)、printfはスタックにあるすべてのものを取得します。

たとえば、プログラムを悪用してスタックを探索し、非表示の情報にアクセスしたり、認証をバイパスしたりするために使用されました(現在も使用されています)。

例(C):

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

このプログラムの入力として置くと "%08x %08x %08x %08x %08x\n"

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

これは、printf関数にスタックから5つのパラメーターを取得し、それらを8桁の埋め込み16進数として表示するように指示します。したがって、可能な出力は次のようになります。

40012980 080628c4 bffff7a4 00000005 08059c04

詳細とその他の例については、こちらをご覧ください。


13

printfリテラル形式の文字列を使用した呼び出しは安全で効率的ですprintf。ユーザー提供の形式文字列を使用した呼び出しが安全でない場合に自動的に警告するツールが存在します。

に対する最も深刻な攻撃printfは、%nフォーマット指定子を利用します。他のすべてのフォーマット指定子とは対照的に、たとえば%d%n実際には、フォーマット引数の1つで指定されたメモリアドレスに値を書き込みます。これは、攻撃者がメモリを上書きし、プログラムを制御する可能性があることを意味します。ウィキペディア はより詳細を提供します。

printfリテラル形式の文字列で呼び出す場合、攻撃者は形式文字列に忍び込むことができないため、%n安全です。実際、gccはへの呼び出しをprintfへの呼び出しに変更するputsので、文字通り違いはありません(これを実行してテストしますgcc -O3 -S)。

printfユーザーが指定したフォーマット文字列を使用して呼び出すと、攻撃者がフォーマット文字列に潜入し、%nプログラムを制御する可能性があります。コンパイラは通常、安全でないことを警告します-Wformat-security。を参照してください 。またprintf、ユーザーが指定したフォーマット文字列を使用してもの呼び出しが安全であることを保証するより高度なツールがあり、適切な数とタイプの引数をに渡すことをチェックする場合もあります printf。たとえば、JavaにはGoogleのError ProneChecker Frameworkがあります。


12

これは見当違いのアドバイスです。はい、印刷するランタイム文字列がある場合、

printf(str);

非常に危険であり、常に使用する必要があります

printf("%s", str);

代わりに、一般にstr%標識が含まれているかどうかを知ることはできません。ただし、コンパイル時の定数文字列がある場合は、何も問題はありません。

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

(とりわけ、これは文字通りGenesisのCプログラミングブックからの、これまでで最も古典的なCプログラムです。そのため、その使用を廃止する人はどちらかと言えば異端的であり、私にとっては少し気分が悪くなります!)


because printf's first argument is always a constant stringどういう意味かよくわかりません。
セバスチャンマッハ

言ったように、"He has %d cherries\n"は定数文字列です。つまり、コンパイル時の定数です。しかし、公平を期すために、著者のアドバイスは「定数の文字列をprintf「最初の引数として渡さない」」ではなく%printf「「最初の引数として文字列を渡さない」」でした。
スティーブサミット

literally from the C programming book of Genesis. Anyone deprecating that usage is being quite offensively heretical-近年K&Rを実際に読んだことがない。廃止されたばかりでなく、最近の単純な悪い習慣だけでも、たくさんのアドバイスとコーディングスタイルがあります。
Voo、2015

@Voo:まあ、悪い習慣と考えられているすべてが実際に悪い習慣であるとは限らないとしましょう。(「平凡なものを使用しない」というアドバイスはint頭に浮かびます。)
スティーブサミット

1
@Steveどこで聞いたのかわかりませんが、それは確かに私たちが話しているような悪い(悪い?)プラクティスではありません。誤解しないでください。コードは完全に問題なかったのですが、最近では、k&rを歴史的なメモとして多く見たくありません。「それはk&rである」は、最近の良い品質の指標ではありません、それだけです
Voo

9

のかなり厄介な側面はprintf、浮遊メモリの読み取りが制限された(そして許容可能な)害しか引き起こさないプラットフォームでも、フォーマット文字の1つ%nが、次の引数を書き込み可能な整数へのポインターとして解釈させ、これまでに出力され、それによって識別される変数に格納される文字数。私はその機能を自分で使用したことがなく、実際に使用する機能のみを含むように記述した(そしてその機能は含まない)軽量のprintfスタイルのメソッドを使用することもありますが、受信した標準のprintf関数文字列をフィードします信頼できないソースからのものは、任意のストレージを読み取る能力を超えてセキュリティの脆弱性をさらす可能性があります。


8

誰も言及していないので、彼らのパフォーマンスに関するメモを追加します。

通常の状況では、コンパイラーの最適化が使用されていない(つまり、printf()実際にはが呼び出されprintf()、呼び出されていないfputs())と想定するprintf()と、特に長い文字列の場合、パフォーマンスが低下することが予想されます。これはprintf()、変換指定子があるかどうかを確認するために文字列を解析する必要があるためです。

これを確認するために、いくつかのテストを実行しました。テストはgcc 4.8.4を使用してUbuntu 14.04で実行されます。私のマシンはIntel i5 cpuを使用しています。テスト中のプログラムは次のとおりです。

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

どちらもでコンパイルされていgcc -Wall -O0ます。時間はを使用して測定されtime ./a.out > /dev/nullます。以下は典型的な実行の結果です(私は5回実行しましたが、すべての結果は0.002秒以内です)。

以下のためのprintf()変種:

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

以下のためのfputs()変種:

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

ストリングが非常に長い場合、この効果は増幅されます。

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

printf()バリアント(3回、本当のプラス/マイナス1.5秒を走りました):

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

fputs()バリアント(3回、本当のプラス/マイナス0.2秒を走りました):

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

注: gccによって生成されたアセンブリを調べたところ、gccはを使用してもfputs()fwrite()呼び出しの呼び出しを最適化していることに気付きました-O0。(printf()呼び出しは変更されません。)コンパイラーがfwrite()コンパイル時にの文字列長を計算するため、これがテストを無効にするかどうかはわかりません。


2
これは、前述したように、あなたのテストを無効にしませんfputs()、多くの場合、文字列定数とその最適化の機会を使用されてdynamicly生成された文字列とテストの実行を追加する、と述べたあなたがmake.Thisしたかったポイントの一部であり、fputs()そしてfprintf()素敵な補足データポイントになります。
PatrickSchlüter、2015

@PatrickSchlüter動的に生成された文字列を使用したテストは、この質問の目的に反するようです... OPは、出力される文字列リテラルのみに関心があるようです。
user12205

1
彼の例で文字列リテラルを使用しても、彼はそれを明示的に述べていません。実際、本のアドバイスに関する彼の混乱は、例で文字列リテラルを使用した結果だと思います。文字列リテラルでは、本のアドバイスはどうやら怪しいですが、動的文字列ではそれは良いアドバイスです。
PatrickSchlüter2015

1
/dev/nullある種、これをおもちゃにします。通常、フォーマットされた出力を生成するときの目標は、出力を破棄せずにどこかに移動することです。「実際にデータを破棄しない」時間を追加すると、それらはどのように比較されますか?
Yakk-Adam Nevraumont 2015

7
printf("Hello World\n")

自動的に同等のものにコンパイルします

puts("Hello World")

あなたはあなたの実行可能ファイルを分解することでそれをチェックすることができます:

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

を使用して

char *variable;
... 
printf(variable)

セキュリティの問題につながるので、printfをそのように使用しないください!

したがって、あなたの本は実際には正しいです。1つの変数でprintfを使用することは非推奨ですが、自動的にputsになるため、printf( "my string \ n")を使用できます。


12
この動作は、実際にはコンパイラに完全に依存しています。
Jabberwocky

6
これは誤解を招くものです。あなたが述べるA compiles to Bが、実際にはあなたが意味するA and B compile to C
セバスチャンマッハ

6

gccの場合、チェックprintf()およびに対して特定の警告を有効にすることができますscanf()

gccのドキュメントは次のように述べています:

-Wformatに含まれてい-Wallます。形式チェックのいくつかの側面をより細かく制御するために、オプション-Wformat-y2k-Wno-format-extra-args-Wno-format-zero-length-Wformat-nonliteral-Wformat-security、および-Wformat=2利用可能ですが、中に含まれていません-Wall

-Wformat内有効になっている-Wallオプションは、ヘルプは、これらのケースを見つけるには、いくつかの特別な警告を有効にしません。

  • -Wformat-nonliteral 文字列リテラルをフォーマット指定子として渡さないと警告が表示されます。
  • -Wformat-security危険な構造を含む可能性のある文字列を渡すと警告が表示されます。これはのサブセットです-Wformat-nonliteral

有効にする-Wformat-securityと、コードベースにあったいくつかのバグが明らかになったことを認めなければなりません(ロギングモジュール、エラー処理モジュール、xml出力モジュール、すべてパラメーターに%文字を使用して呼び出された場合に未定義の処理を実行できる関数がいくつかありました。詳細については、私たちのコードベースは現在約20年前のものであり、これらの種類の問題を認識していたとしても、これらの警告を有効にしたときに、これらのバグのいくつがまだコードベースに残っているかに驚いていました)。


1

その他の懸念事項がカバーされている他のよく説明された回答に加えて、提供された質問に対して正確で簡潔な回答を提供したいと思います。


なぜprintf(変換指定なし)単一の引数では廃止しましたか?

printf一般的には単一の引数を持つ関数の呼び出しがされていない非推奨とも何の脆弱性を持っていないときは、必ずコーディングしなければならないよう適切に使用します。

Cステータスの初心者からステータスの専門家まで、世界中のユーザーは、printfこの方法を使用して、コンソールへの出力として単純なテキストフレーズを提供します。

さらに、誰かがこの唯一の引数が文字列リテラルであるか、文字列へのポインタであるかを区別する必要があります。これは有効ですが、通常は使用されません。後者の場合、もちろん、ポインタが有効な文字列を指すように正しく設定されていない場合、不都合な出力やあらゆる種類の未定義の動作が発生する可能性がありますが、書式指定子が複数の引数。

もちろん、唯一の引数として提供される文字列にフォーマット指定または変換指定子があることも正しくありません。これは、変換が行われないためです。

そうは言っ"Hello World!"ても、質問で提供したように、その文字列内にフォーマット指定子なしで、引数としてのみのような単純な文字列リテラルを与える:

printf("Hello World!");

されていない非推奨または「悪い習慣」のすべてにも任意の脆弱性があります。

実際、多くのCプログラマーは、HelloWorldプログラムとこのprintfステートメントをその種の最初のものとして、C言語またはプログラミング言語さえも一般に学び、使用し始めました。

彼らが廃止された場合、彼らはそうではないでしょう。

私が読んでいる本でprintfは、単一の引数(変換指定子なし)は非推奨であると書かれています。

さて、私は本または著者自体に焦点を当てます。著者が本当にそうしている場合、私の意見では、誤った主張、さらにはなぜそうしているのかを明確に説明せずにその主張を教えている場合(それらの主張がその本で文字通り同等に提供されている場合)、私はそれを悪い本と見なします。良いこととは対照的に、本は、説明しなければならない理由は、プログラミング方法や機能の特定の種類を避けるために。

私が使用して、上記の言ったことによるprintfだけで一つの引数(文字列リテラル)とし、せずに任意のフォーマット指定は非推奨かと考えられ、いずれの場合ではありません「悪い習慣」

あなたは著者に、彼がそれで何を意味するのか、あるいはそれ以上に、次の版または刷り込み一般の関連セクションを明確にするか修正するように彼に気をつけるべきです。


あなたはそれprintf("Hello World!");がとにかく同等ではないことを追加するかもしれませputs("Hello World!");ん、それは推薦の作者について何かを伝えます。
chqrlie
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.