\appendix \section{付録} \subsection{Code Hardening - バッファーオーバーフロー対策 \texttt{scanf}編} {\Large C言語は比較的自由な言語だ。しかし自由には責任が伴う。} \vspace{0.25cm} 初学者からベテランCプログラマーが誰でも1度はやらかしてしまう間違いとして文字列の読み込みによるバッファーオーバーフローがある。 バッファーオーバーフローは静的・動的に割り当てられたメモリー領域外に書き込むことで発生する。 今回の演習課題は整数値を読み込んだが、バッファーオーバフローは起こらないが、整数値オーバーフロー・アンダーフローという別の問題が起こる。 しかし、このバグによる影響は比較的小さい。 なぜなら、これらの整数は関数のスタック上の変数として決まったサイズでメモリーに割り当てられており、領域外への値の書き込みがないからである。 しかし、ユーザーが入力する文字列となると話が違ってくる。 バッファーに過剰な量のデータが流入することバッファーオーバーフローが起こり、最悪の場合、バッファー外のデータを侵食・書き換えてしまい、プログラムが誤動作する。\cite{lowlevel_str} 静的に割り当てられた変数のバッファーオーバーフローは対処が容易であるが、実行するまでサイズが不明な場合が多い動的に割り当てられた変数の対策はその限りではない。 文字列・配列への範囲外読み書きはユーザーやネットワーク要求からの入力を扱う際にはより注意する必要がある。 開発者がすべてのユーザーが指示に従うと思い込むのは、はっきり言って\textbf{愚か}である。 プログラムはすべてメモリ安全性を第一に考えて書かれるべきである。 実際、このような脆弱性がシステムの全権を取得できてしまう程の問題を簡単なエラーによって引き起こされる場合がある。 例えば、「CVE-2021-3156」ではLinux等での非管理者ユーザーが管理者としてファイル編集できるようにする「sudoedit」コマンドでは、 配列のサイズが1つずれただけでバッファーオーバーフローを発生させ、認証なしで管理者権限が付与されてしまうバグが存在した。\cite{cve_sudo} PythonやJavaなどの多くの高級言語は配列のサイズを超える場所への値の代入はエラーとなりコンパイル時やプログラム実行中に停止するように設計されている。 しかしC言語はそのような設計はプログラム実行中には施されていない。\texttt{-Wall}フラグを使用しても警告を出すだけで止めることはない。 \footnote{gccなど最近のコンパイラでコンパイルされたプログラムはスタックカナリアと呼ばれる一昔の炭鉱夫が一酸化炭素検出のために使われた鳥のカナリアの様にバッファーオーバーフローなどによって関数スタックが破壊された際にプログラムを強制終了させるコードを追加する機能が常に有効化されている物が存在する。なおこの機能はヒープ(動的確保されたメモリ領域)には適応されない。\cite{lowlevel_canary}} 高級言語の文字列は本体の文字列と共にその長さが記録されているが、C言語はその長さは記録されず、代わりに終端文字が文字列の終わりを示めす。 \texttt{scanf}や\texttt{gets}は終端文字を検出するまで読み込みを続ける。 裏を返せば、終端文字を見つける前にバッファーが溢れているときでも、範囲外への書き込みを続けてしまうということにも繋がる。 次の例を考える:企業のシステムにて、ユーザーがパスワード(文字列)を入力し、正しければ社員データベースを操作するサブルーチーンに変移する。 パスワードの入力を司る処理とパスワードを照合する処理は個別にサブルーチーンがあるものとする。 ユーザーが入力したパスワードは動的に確保した64バイトのバッファー領域に書き込まれる。 \newpage \defaultlistingstyle \begin{lstlisting}[language=C,title={対策されていないシステムの例}] #include #include #define PASSLEN 65 void databaseManagement(); int checkPassword(char* passwd) { char passwd[PASSLEN]; scanf("%s", passwd); return isCorrectPassword(passwd); } int main(void) { printf("Database Login\nPassword: "); if (checkPassword()) databaseManagement(); return 0; } \end{lstlisting} この時、システムを悪用したいと企てるハッカーが65バイト以上の文字列を渡し、うまくバッファーオーバーフローを引き起こさせると、パスワードの入力を司るサブルーチーンの戻りアドレスが書き換えられ、照合サブルーチーンを介さずに直接データベース管理サブルーチーンを実行させる。 \texttt{scanf}関数での対策はいたってシンプルである:書式を\texttt{"\%<バッファーサイズ - 1>s"}に変更するだけである。 \footnote{この\texttt{-1}で終端文字が入るスペースを残す。} これによって、読み出す文字数を制限し、バッファーオーバーフローを防ぐことができる。 もう一つの方法はC11規格から追加された\texttt{scanf\_s}関数を代わりに使用することである。 \texttt{scanf\_s}関数はオリジナルの\texttt{scanf}関数に新たにバッファーのサイズを受け付る引数を最後尾に追加し、エラーチェックをより厳密にしたもので、 これにより前述の方法と併用しつつ、明示的にバッファーサイズを指定することができる。\cite{cppref_scanf} \defaultlistingstyle \begin{lstlisting}[language=C,title={\texttt{scanf}関数の対策例}] // scanf("%s", passwd); scanf("64%s", passwd); passwd[PASSLEN-1] = '\0'; // 終端文字の存在を保証する // C11規格以降のみ scanf_s("64%s", passwd, PASSLEN); \end{lstlisting}