web-dev-qa-db-ja.com

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

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

_printf("Hello World!");
_

_puts("Hello World!");
_

または

_printf("%s", "Hello World!");
_

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

97
Iu Tub

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

_const char *str;
...
printf(str);
_

strが_%s_形式指定子を含む文字列を指す場合、プログラムは未定義の動作(ほとんどがクラッシュ)を示しますが、puts(str)は文字列をそのまま表示します。

例:

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

printf("Hello world");

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

問題は次のとおりです。

_printf(p);
_

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

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

73
ouah

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

プログラマーに絶対的に明確にすることを「要求」するのは、単純で、簡単で、堅牢です逐語的な文字列だけが必要な場合

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

(Steve、もちろんprintf("He has %d cherries\n", ncherries)はまったく同じものではありません。この場合、プログラマーは「逐語的ストリング」マインドセットではなく、「フォーマットストリング」マインドセットです。)

ここで少し情報を追加します脆弱性に関して一部。

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

より完全な説明と他の例については、 this を参照してください。

16
P1kachu

リテラル形式文字列でprintfを呼び出すことは安全で効率的です。また、ユーザーが指定した形式文字列でprintfを呼び出しても安全でない場合に自動的に警告するツールがあります。

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

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

ユーザーが指定した書式文字列でprintfを呼び出すと、攻撃者が%nを書式文字列に忍び込み、プログラムを制御する可能性があります。通常、コンパイラは安全でないことを警告します。-Wformat-securityを参照してください。 printfの呼び出しがユーザー指定のフォーマット文字列であっても安全であることを保証する、より高度なツールもあります。また、printfに正しい数とタイプの引数を渡すことを確認することもあります。たとえば、Javaには Googleのエラー傾向Checker Framework があります。

12

これは見当違いのアドバイスです。はい、印刷する実行時文字列がある場合、

printf(str);

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

printf("%s", str);

代わりに、一般にstr%サイン。ただし、コンパイル時constant文字列がある場合、何も問題はありません。

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

(とりわけ、これは史上最も古典的なCプログラムであり、文字通りGenesisのCプログラミング本からのものです。ですから、その使用を非難する人はどちらかといえば異端であり、私はやや気分を害するでしょう!)

11
Steve Summit

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

9
supercat

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

通常の状況では、コンパイラの最適化が使用されていないと仮定します(つまり、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
_

この効果は、very長い文字列がある場合に増幅されます。

_#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は_-O0_を使用しても、fputs()呼び出しに対するfwrite()呼び出しを最適化することに気付きました。 (printf()呼び出しは変更されません。)コンパイラがコンパイル時にfwrite()の文字列の長さを計算するため、これがテストを無効にするかどうかわかりません。

8
ace
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を使用することは推奨されませんが、自動的にputになるため、printf( "my string\n")を使用することができます

6
Ábrahám Endre

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年前であり、この種の問題に気付いていたとしても、これらの警告を有効にすると、これらのバグの数がまだコードベースに残っていることに非常に驚きました)。

4