MENU

フォーマットストリング脆弱性とは?仕組み・攻撃手口・対策をわかりやすく解説

「printfにユーザー入力をそのまま渡しているだけなのに、サーバーが乗っ取られる」——そんな話を聞いて、にわかには信じられない方もいるのではないでしょうか。

フォーマットストリング脆弱性(Format String Vulnerability)は、C/C++のprintf系関数の書式文字列処理の欠陥を突く攻撃で、攻撃者がメモリ内の任意のデータを読み出したり、プロセスを強制終了させたりする可能性があります。1990年代末から知られる古い脆弱性ですが、組み込み機器や長年動き続けている業務システムのCコードでは今もゼロデイとして発見されることがある、侮れない脅威です。

この記事では、フォーマットストリング脆弱性について、仕組み・攻撃手口・防御手順を現場で使えるレベルで解説します。C/C++を使う開発者はもちろん、ライブラリのパッチ管理を担う情シス担当者にも役立てていただける内容です。

フォーマットストリング脆弱性とは?仕組み・攻撃手口・対策をわかりやすく解説 - 解説

目次

フォーマットストリング脆弱性とは?

フォーマットストリング脆弱性とは、printf・fprintf・sprintf・syslogなどの「書式文字列関数」の第1引数(フォーマット文字列)に、ユーザーが制御できる入力をそのまま渡してしまうコードの欠陥です。

書式文字列関数は、%d(整数)・%s(文字列)・%x(16進整数)・%n(書き込み済みバイト数を変数に格納)といった「変換指定子」を解釈します。本来は開発者が静的に定義した文字列を第1引数に渡すべきところを、ユーザー入力をそのまま渡してしまうと、攻撃者は変換指定子を含む文字列を送り込むことで意図しない動作を引き起こせます。

【脆弱なコードの例】

# 脆弱なコード(ユーザー入力をそのままフォーマット文字列として渡している) char user_input[256]; fgets(user_input, sizeof(user_input), stdin); printf(user_input); /* 危険: user_inputを第1引数に直接渡している */ # 安全なコード(固定のフォーマット文字列を使う) printf("%s", user_input); /* 安全: フォーマット文字列は定数 */

この一見小さな違いが、深刻なセキュリティ上の問題を引き起こします。

なぜ今も問題なのか

「C/C++を使うプロジェクトが減っているから関係ない」と思われるかもしれませんが、現実はそれほど単純ではありません。

・組み込み機器・産業制御システム: ルーター、PLC、ネットワーク機器のファームウェアは依然としてCで書かれているものが多く、フォーマットストリング脆弱性が発見されるケースがあります。
・レガシー業務システム: 10年以上動き続けているバッチ処理やサーバープログラムには、当時のコーディング慣行で書かれたコードが残っています。
・ライブラリ経由での影響: 直接C/C++を書かなくても、利用しているライブラリがフォーマットストリング脆弱性を持っている場合があります。

攻撃の仕組み(敵を知る)

フォーマットストリング攻撃がどのように機能するか、防御側の立場から理解しましょう。

1. スタック上の情報を読み出す(情報漏洩)

printf(user_input)のような脆弱なコードがある場合、攻撃者が%x %x %x %xのような文字列を入力すると、関数はスタック上にある値を次々と読み出して16進数で表示します。

スタック上にはメモリアドレス・ポインタ・ローカル変数などが保存されており、これらを読み出すことで:
・ASLRのランダム化されたアドレスを特定する(後続の攻撃に必要なアドレス情報の取得)
・プログラムの内部状態を把握する(パスワードハッシュ、セッショントークンなどがスタックに残っている場合)

といった情報漏洩が起こります。

2. メモリへの書き込み(%nの悪用)

最も危険なのが%n変換指定子の悪用です。%nは「ここまでに出力したバイト数を、対応するポインタ引数が指すアドレスに書き込む」という動作をします。

攻撃者が巧妙な書式文字列を組み立てると:

・特定のメモリアドレスに任意の値を書き込む
・関数の戻りアドレスを書き換える(シェルコードへのジャンプ)
・グローバル変数・関数ポインタを改ざんする

といった操作が可能になり、最悪の場合はリモートコード実行(RCE)につながります。リモートコード実行(RCE)の仕組みと防御策もあわせて参照してください。

3. サービス停止(クラッシュ)

%sを大量に送り込むと、有効なポインタでない値を文字列として読もうとしてセグメンテーション違反が発生し、プログラムがクラッシュします。サービス停止(DoS)を引き起こすことが可能です。

【攻撃の流れ(概念)】

# ステップ1: 情報収集(スタックの読み出し) # 攻撃者が送り込む文字列の例 %x.%x.%x.%x.%x.%x.%x.%x # → サーバーがスタック上の値を16進数で返す(アドレス情報が漏洩) # ステップ2: アドレス特定・ペイロード構築 # 目標となるアドレス(例: 戻りアドレスの保存場所)を特定し、 # %Nc%n を組み合わせて書き込む値を調整する # (実際のexploitコードはここでは記載しない) # ステップ3: 任意コード実行 # 関数ポインタや戻りアドレスを攻撃者のコードに書き換えた後、 # プログラムがそのアドレスに制御を移す

実際のエクスプロイトコードはここでは掲載しませんが、防御側としてはこのような手順で攻撃が成立することを理解しておくことが重要です。フォーマットストリング攻撃は権限昇格(Privilege Escalation)の足がかりになることも多く、攻撃チェーン全体を把握して対策を講じる必要があります。

具体的な防御手順

1. 根本的な対策: フォーマット文字列を固定する

最も効果的かつシンプルな対策は、書式文字列関数の第1引数に必ず定数の文字列を渡すことです。

/* NG: ユーザー入力をフォーマット文字列に直接渡す */ printf(user_input); fprintf(logfile, user_message); syslog(LOG_INFO, user_data); /* OK: フォーマット文字列は定数。ユーザー入力は引数として渡す */ printf("%s", user_input); fprintf(logfile, "%s", user_message); syslog(LOG_INFO, "%s", user_data);

これだけで、フォーマットストリング脆弱性の大部分は防げます。コードレビューの際にこのパターンを必ず確認するようにしてください。

2. コンパイラの警告を有効にして検出する

GCCやClangには、フォーマットストリングの誤用を検出するオプションがあります。

# GCC / Clangのコンパイルオプション # -Wformat: 書式文字列の基本的な問題を警告 # -Wformat-security: 書式文字列がリテラルでない場合に警告(最重要) # -Wformat=2: より厳格なフォーマットチェック(推奨) # -Werror: 警告をエラーとして扱い、ビルドを止める gcc -Wall -Wformat -Wformat-security -Wformat=2 -Werror -o myapp myapp.c # CMakeプロジェクトの場合 # CMakeLists.txt に追加: # target_compile_options(myapp PRIVATE -Wall -Wformat -Wformat-security)

-Wformat-securityを有効にすると、printf(variable)のような危険なパターンをコンパイル時に検出してくれます。CI/CDパイプラインに組み込んで、マージ前に自動チェックするのが理想です。

3. 静的解析ツールを導入する

コンパイラ警告だけでは検出しきれないケースもあります。静的解析ツールを補助的に使いましょう。

# Flawfinderのインストールと実行(フォーマットストリングを含む危険関数を検出) pip install flawfinder flawfinder --minlevel=2 ./src/ # Cppcheckの実行(C/C++の静的解析) apt install cppcheck # Ubuntu/Debian cppcheck --enable=all --inconclusive ./src/ 2>&1 | grep -i "format" # clang-tidyの実行(ClangベースのLinter) # cert-FIO47-C: フォーマットストリング引数の型チェック等 clang-tidy --checks=cert-* myapp.c -- -I./include

4. OSレベルの緩和策を確認する

根本対策の補完として、OSが提供する緩和機能が有効になっているか確認しましょう。

# ASLR(アドレス空間配置ランダム化)の確認・有効化 cat /proc/sys/kernel/randomize_va_space # 0: 無効 / 1: 部分的 / 2: 完全ランダム化(推奨) echo 2 > /proc/sys/kernel/randomize_va_space # sysctl.confへの永続化 echo "kernel.randomize_va_space = 2" >> /etc/sysctl.conf sysctl -p # PIE(位置独立実行)でコンパイルされているか確認 checksec --file=./myapp # PIE enabled が表示されれば攻撃が難しくなる # スタックのNX(実行不可)が有効か確認 checksec --file=./myapp # NX enabled が表示されることを確認

ASLRが有効でも、フォーマットストリング攻撃によってアドレスがリークされると無効化されます。根本対策(フォーマット文字列の固定)が最重要で、OSの緩和策は多層防御の一環として捉えましょう。

中小企業でも今日からできること

「C/C++は外注しているから関係ない」と思っている場合も、以下は確認しておく価値があります。

・使用しているOSS・ライブラリのパッチ確認: 業務システムが依存するC/C++製ライブラリ(OpenSSL・libcurl・zlib等)にフォーマットストリング脆弱性が発見されることがあります。JVN(脆弱性情報データベース)の配信を受け取り、定期的にアップデートを適用してください。
・組み込み機器・ルーターのファームウェア更新: ネットワーク機器のファームウェアは更新が後回しになりがちです。管理コンソールからファームウェアバージョンを確認し、ベンダーのセキュリティ情報をウォッチする習慣をつけましょう。
・開発委託先へのコーディング規約の要求: ソフトウェア開発を外注している場合、コンパイラ警告(-Wformat-security)の有効化と静的解析の実施をセキュリティ要件として明記した仕様書を作成してください。
・古いCコードの棚卸し: 「昔から動いているから」と放置されている自社開発ツールがあれば、コードをレビューしてフォーマット文字列関数の使い方を確認しましょう。パターンを探す場合はgrep -n "printf\|fprintf\|sprintf\|syslog" *.cで一覧を出して確認できます。

よくある誤解と注意点

【誤解1】「ログ出力だから攻撃されても影響は小さい」

フォーマットストリング脆弱性はログ出力関数(syslog・fprintfなど)でも成立します。「ログに記録するだけ」の処理でも、書き込まれたメモリアドレス情報が別の攻撃に使われたり、プロセスがクラッシュしたりするリスクがあります。

【誤解2】「%nはglibc側で無効にできる」

Linuxのglibcはprintf_set_format_arg_formatで%nを無効化できますが、これはアプリケーションコードで明示的に呼び出す必要があります。多くのシステムでは設定されておらず、根本対策の代替にはなりません。

【誤解3】「64ビット環境なら安全」

64ビット環境ではアドレス空間が大きくなるためexploitが難しくなりますが、不可能ではありません。情報漏洩(%xによるスタック読み出し)はビット数に関係なく成立します。根本対策は環境に関わらず必須です。

【注意】スプリントfの安全な使い方

sprintfはバッファオーバーフローとフォーマットストリングの両方のリスクを持ちます。可能であればsnprintfに置き換え、フォーマット文字列は定数を使いましょう。バッファオーバーフローの仕組みと対策についてもあわせてご確認ください。

/* NG: バッファオーバーフロー + フォーマットストリングの両方のリスク */ char buf[64]; sprintf(buf, user_input); /* OK: 長さ制限 + フォーマット文字列は定数 */ char buf[64]; snprintf(buf, sizeof(buf), "%s", user_input);

フォーマットストリング脆弱性とは?仕組み・攻撃手口・対策をわかりやすく解説 - まとめ

本記事のまとめ

項目 内容
脆弱性の原因 ユーザー入力をprintf系関数の第1引数(フォーマット文字列)に直接渡す
攻撃による影響 メモリ情報の漏洩・任意アドレスへの書き込み・リモートコード実行・サービス停止
根本的な対策 フォーマット文字列を定数にする(printf("%s", var))
検出方法 コンパイラ警告(-Wformat-security)・静的解析(Flawfinder・cppcheck・clang-tidy)
OSレベルの緩和 ASLR・PIE・NXビットを有効にする(補完的対策)
情シスがすべきこと ライブラリ・ファームウェアの定期パッチ適用、開発委託先へのコーディング規約の要求

フォーマットストリング脆弱性は「古い脆弱性だから現代には関係ない」と思われがちですが、組み込み機器・レガシーシステム・外部ライブラリを通じて今も現役の脅威です。根本対策はprintf("%s", var)という一行の修正であり、コンパイラ警告で自動検出できます。開発プロセスとパッチ管理の両面から対策を進めてください。

PR

サイバーセキュリティプログラミング 第2版(Justin Seitz・Tim Arnold/オライリー・ジャパン)

Pythonを使いながらネットワークパケット解析・脆弱性調査・エクスプロイト開発の仕組みを学べる実践書。フォーマットストリングを含む低レベルの脆弱性を「攻撃者の視点」で理解したいエンジニアに特に役立つ一冊です。

関連記事をもっと読む

同じテーマの記事をまとめています。あわせて読みたい記事はこちらからご覧いただけます。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

目次