平成30年度 秋期 情報処理安全確保支援士試験 午後Ⅰ 問1|たった一行のコピーが乗っ取りを招く—情報処理安全確保支援士で学ぶバッファオーバフローの実例
この記事で扱うセキュリティ事例
試験区分: 情報処理安全確保支援士
出典: 平成30年度 秋期 情報処理安全確保支援士試験 午後Ⅰ 問1
主なテーマ: バッファオーバフロー、メモリ破壊攻撃、DEP、Return-to-libc攻撃、SSP(canary)、ASLR、PIE、セキュアコーディング
この記事は「情報処理安全確保支援士 午後問題 全70問」シリーズの1本です。→ 全70問の一覧・テーマ別索引はこちら
導入: なぜこの事例が重要なのか
「文字列をコピーする」——プログラミングのなかでも、もっとも基本的な操作です。ところが、そのありふれた一行が、機器を乗っ取られる入口になることがあります。情報処理安全確保支援士の午後Ⅰで出題されたこの事例は、IoT機器を開発する企業を舞台に、メモリ破壊攻撃(プログラムが使うメモリを不正に書き換えて、機器の制御を奪う攻撃)の仕組みと、それを防ぐ技術を正面から扱っています。
メモリ破壊脆弱性、なかでもバッファオーバフローは、C/C言語で書かれたソフトウェアにつきまとう古典的な弱点です。古典的だからといって、過去の話ではありません。IoT機器やネットワーク機器の多くはいまもC/Cで書かれており、この弱点を突いた攻撃は現役で発生し続けています。
この記事を読むと、攻撃者がどうやってメモリを書き換えてプログラムの流れを乗っ取るのか、DEPやASLR、SSPといった防御技術がそれぞれ何を防いでいるのか、そしてなぜ「対策を入れたのに防げないケース」が残るのかが見えてきます。受験者には頻出のメモリ破壊・セキュアコーディングの論点が詰まっており、ソフトウェアを開発・発注する立場の管理者には「セキュリティは設計と開発環境の選定から始まる」ことを理解する教材になります。

インシデントの概要
舞台は、IoT機器の開発を行う従業員数400名の企業、U社です。U社では、IoT機器のソフトウェアをC/C++言語で開発していました。機器に載せるOS(基本ソフト)にはこれまでLinuxを使ってきましたが、今後はLinux以外も使う予定がありました。
これまで、製品に大きなトラブルはありませんでした。しかし、安心はできません。2020年の東京オリンピック・パラリンピックに向けて、IoT機器に関するセキュリティリスクが高まると経営層が判断し、開発におけるセキュリティ対策を強化することになったのです。実際に事故が起きてから慌てるのではなく、起きる前に守りを固める——この事例は、その「予防」の物語です。
対応にあたったのは、開発部のL部長とX主任。二人は、メモリ破壊攻撃に対する代表的な対策技術を調査し、自社の開発にどう取り入れるかを検討しました。題材として使われたのは、スタックバッファオーバフロー脆弱性の学習用に作られた、32ビット版Linuxで動く「Vuln」というプログラムです。このVulnを通して、攻撃の原理と防御の効きどころが、一つひとつ解き明かされていきます。
図1のプログラム Vuln を読み解く
試験問題の中心にあるのが、図1に示された脆弱なC言語プログラム「Vuln」です。まずコード全体の流れを押さえてから、攻撃の話に入ると理解しやすくなります。
プログラムの全体像(3つの関数)
Vulnは、次の3つの関数だけで構成されています。
main … コマンドライン引数(argv)からデータを受け取り、fooに渡す
foo … 受け取った文字列を100バイトの配列dにコピー。条件によってerr_outを呼ぶ
err_out … エラーメッセージを別の100バイト配列s1にコピーし、標準エラー出力に表示
データの流れは次のとおりです。
攻撃者が渡す長い文字列(argv)
↓
main(省略部分でメモリにコピー)
↓
foo(strcpy で d[100] へコピー) ← 脆弱性①
↓(d[0]が0なら)
err_out(while で s1[100] へコピー) ← 脆弱性②mainの内部は問題文で「省略」されていますが、要点は「外部から渡された文字列を、そのままfooに渡している」ことです。長さチェックがない前提で、攻撃者はここに100バイトを超えるデータを流し込めます。
図1のソースコード(試験掲載部分)
問題冊子の図1を、見やすい形で整理します(省略箇所はコメントで示します)。
int main(int argc, char *argv[]) {
char *a, *x;
/* 省略: argv の長さに応じてメモリを確保し、
a と x に argv の内容をコピーする */
foo(a, x);
/* 省略: その他の処理 */
}
int foo(char *b, char *c) {
char d[100]; /* スタック上の100バイト配列 */
/* 省略 */
strcpy(d, b); /* ← 7行目: 脆弱性の本丸 */
if (d[0] == 0) {
err_out(c);
/* 省略 */
}
/* 省略 */
return 0;
}
int err_out(char *errmsg) {
char s1[100]; /* スタック上の別の100バイト配列 */
int i = 0;
/* 省略 */
while ((s1[i++] = *errmsg++) != '\0'); /* ← 16行目: もう一つの穴 */
fprintf(stderr, "Error : %s \n", s1);
/* 省略 */
return 0;
}脆弱性①:strcpy(d, b)(7行目)
strcpyは、コピー元の文字列を、終端のヌル文字('\0')まで長さを調べずにコピーする標準ライブラリ関数です。
dは「100バイトしかない入れ物」なのに、bの中身が101バイト以上あると、次のようなことが起きます。
d[0]〜d[99]まで配列の範囲内に書き込まれる
100バイト目以降も書き込みが続き、配列dの外側のスタック領域にデータがはみ出す(バッファオーバフロー)
はみ出した先に、関数の戻り先であるリターンアドレスなどが並んでいれば、それらが攻撃者の用意した値で上書きされる
試験では、この「リターンアドレスを書き換えて、処理の流れを乗っ取る」流れが、図2のメモリマップとセットで問われます。fooが終わったあと、プログラムは本来mainに戻るはずですが、リターンアドレスが書き換えられていれば、攻撃者の指定したアドレス(shellcodeやライブラリ関数)へ飛んでしまいます。
脆弱性②:whileによる手動コピー(16行目)
err_outでは、ライブラリのstrcpyは使わず、次の1行で文字列をコピーしています。
while ((s1[i++] = *errmsg++) != '\0');意味は「errmsgの1文字ずつをs1に入れ、ヌル文字が来るまで繰り返す」です。見た目は丁寧に見えますが、iが99を超えても止まる処理がありません。errmsgが100バイトより長ければ、s1の外へはみ出します。
「止まる処理がない」とは、コードのどこで分かるのか
ポイントは、whileの条件に書いてあることと書いてないことを分けて読むことです。
この1行のwhileが見ているのは、次の1つだけです。
while ((s1[i++] = *errmsg++) != '\0');
// ↑ コピーした1文字がヌル文字か? → ヌルなら終了、そうでなければ続行ここには iの値を見る条件が一切ありません。たとえば次のような書き方はどこにも出てきません。
while (i < 99 && (s1[i++] = *errmsg++) != '\0'); /* こう書いていれば「99超えたら止める」 */s1は14行目で char s1[100]; と宣言されているので、使ってよい添字は 0〜99の100個 です(C言語では配列の要素数が100なら、最後の有効な要素はs1[99])。にもかかわらず、16行目のループは「errmsgにヌル文字が出るまで」しか止まらない。errmsgが長ければ、iは100、101、102…と増え続け、s1[100]以降(配列の外)に書き込みが続きます。
流れを追うと次のようになります(errmsgが十分長いと仮定)。
1回目 … s1[0]に1文字コピー、iは1になる
2回目 … s1[1]にコピー、iは2になる
…
100回目 … s1[99]にコピー、iは100になる(ここまでが配列の範囲内)
101回目 … s1[100]にコピー — もう配列の外。ここからバッファオーバフロー
ヌル文字が来るまで 5 を繰り返す
「丁寧に見える」のは、自前で1文字ずつコピーしているからです。しかし安全かどうかは「何でループを止めるか」で決まるので、ヌル文字だけを条件にしている限り、配列の大きさ(100バイト)は守られません。strcpyと同じく「終端のヌルまでコピーし続ける」構造で、境界チェックが欠けている点では同じ穴です。
安全に書くなら、ループの条件に配列の上限を足します。
while (i < 99 && (s1[i++] = *errmsg++) != '\0');
s1[99] = '\0'; /* 切り詰めたとき用の終端 */こうすれば100文字目以降はコピーされません。試験の図1にはこの上限チェックがないため、「iが99を超えても止まる処理がない」と読み取る、というのが解答の根拠です。
これが設問3(1)で問われるポイントです。Automatic Fortification(strcpyを安全な関数に自動置換する仕組み)は、ライブラリ関数の呼び出しが対象です。プログラマがポインタとwhileで自前実装したコピーは置換されないため、あふれの原因を排除できない——正解は「16行目」「ポインタを使って直接メモリ操作しているから」です。
スタック上で何が隣り合っているか(イメージ図)
fooが呼ばれたとき、スタック(関数の作業机)にはおおよそ次のような並びがあります(概念図。実際のレイアウトはコンパイラやオプションで変わります)。
高いアドレス
┌─────────────────┐
│ リターンアドレス │ ← foo終了後に戻る場所(攻撃の標的)
├─────────────────┤
│ (canary ※SSP時)│ ← 対策技術で後から挿入される見張り
├─────────────────┤
│ 配列 d[100] │ ← strcpy の書き込み先
├─────────────────┤
│ その他のデータ │
└─────────────────┘
低いアドレスstrcpyがdから右(または上)方向に書き続けると、リターンアドレスまで到達し得ます。だから「たった一行のコピー」が致命的なのです。
2つの脆弱性の比較(試験で押さえる対比)
7行目の strcpy(foo内)
原因 … 長さを確認しない標準ライブラリ関数の利用
試験での位置づけ … リターンアドレス書き換えによる乗っ取りの本丸
対策の例 … strncpyの利用、Automatic Fortification
16行目の while コピー(err_out内)
原因 … 境界チェックのない手書きループ(ポインタで直接メモリ操作)
試験での位置づけ … Fortificationが効かない「あふれの原因」の例
対策の例 … ループ内で i < 99 を確認する、安全な関数に置き換える
同じ「100バイトの配列」でも、ライブラリ経由か手書きかで、自動対策の効き方が変わります。U社が調べる防御技術も、この違いを前提に読む必要があります。
何が起きたのか
前節で見たとおり、攻撃の入口はfoo内のstrcpy一行です。ここからは、そのあふれがどう「乗っ取り」につながり、DEP・SSP・ASLRなどの防御とどうぶつかるかを物語として追います。
問題は、あふれた先の「隣」に何があるかです。プログラムが関数を呼び出すとき、メモリのスタック領域(関数呼び出しの一時的な情報を積み上げる領域)には、その関数が終わったあとに戻るべき場所——「リターンアドレス」が記録されています。配列dからあふれたデータは、このリターンアドレスにまで届き、それを上書きしてしまうことができます。
攻撃者の狙いは、まさにここです。リターンアドレスを、自分が用意した不正なコード——「shellコード」(攻撃者が機器を操るための小さなプログラム)の在り処に書き換える。すると、関数fooが終わった瞬間、プログラムは本来戻るべき場所ではなく、攻撃者の仕込んだshellコードへと処理を移してしまいます。問題のメモリマップ(図2)では、shellコードがアドレスbffff29d付近のスタック領域に置かれており、リターンアドレスXをこの値に書き換えれば、fooの終了時にshellコードへ処理が飛ぶ、という構図でした。たった一行の無防備なコピーが、機器の制御を攻撃者に明け渡す入口になるのです。
ところが、ここで一つ目の防御が立ちはだかります。DEP(Data Execution Prevention。データ実行防止機能)です。DEPは、データを置くための領域(スタックやヒープ)に書き込まれた内容を「コード」として実行することを禁じます。shellコードは、攻撃者がデータとしてスタックに流し込んだものです。DEPが有効なら、リターンアドレスをshellコードに向けても、そこは「実行禁止」のスタック領域なので、いざ実行しようとした瞬間にブロックされる。攻撃は失敗します。これが、設問で問われた「①このような遷移があっても攻撃が成功しない理由」——shellコードが、DEPで実行禁止にされているスタック領域にあるから、という答えの意味です。
しかし、攻撃者もここで諦めません。DEPがスタック上のコード実行を禁じるなら、「もともと実行が許されている場所にある、既存のコード」を悪用すればいい、と考えます。プログラムには、ライブラリ(よく使う機能をまとめた部品集)が読み込まれており、その中の関数は当然「実行可能」です。攻撃者は、リターンアドレスを、自分の仕込んだコードではなく、このライブラリ関数の先頭アドレスに書き換える。すると、攻撃者が意図したライブラリ関数を、好きな引数で呼び出させることができる。これがReturn-to-libc攻撃(libc=標準Cライブラリに戻る攻撃)です。データを実行するのではなく、すでにある正規のコードを「部品」として悪用するため、DEPでは防げません。
なぜ「ライブラリ関数を呼べる」だけで致命的なのか——ピンとこない方は、Linuxの標準ライブラリにsystem()という関数があることを思い出してください。system("/bin/sh")と呼べば、OSのシェル(コマンド入力画面)が開きます。つまり攻撃者は、リターンアドレスをsystem()のアドレスに書き換え、引数として"/bin/sh"を指すアドレスをスタック上に配置するだけで、機器上でシェルを起動し、好きなコマンドを実行できるようになります。ファイルの読み書き、別のマルウェアのダウンロード、他の端末への侵入——シェルが取れれば事実上なんでもできます。
「自分のコード」を一切持ち込まなくても、OSにもともと備わっている正規の機能だけで、攻撃者は機器を完全に支配できる。これがReturn-to-libc攻撃の本質的な恐ろしさです。DEPは「持ち込んだコードの実行」を禁じますが、「もとからあるコードの悪用」は止められません。鍵のかかったドアを破るのではなく、家の中にある鍵を使って内側から開けるようなものです。
攻撃と防御は、いたちごっこを続けます。次の防御がSSP(Stack Smashing Protection)です。SSPは、関数を呼び出す際に、リターンアドレスなどの大切な情報の手前(ベースポインタレジスタ保存値より下位)に「canary」(カナリア)と呼ばれる見張り役の値をこっそり挿入します。バッファオーバフローでリターンアドレスを書き換えようとすれば、その手前にあるcanaryも必ず上書きされる。関数の終了時にcanaryの値が変わっていれば「攻撃された」と判断し、プログラムを止めるのです。炭鉱のカナリアのように、異常をいち早く知らせる仕組みです。

それでもなお、攻撃者がcanaryの値を正確に推測して、同じ値で上書きできてしまえば、SSPの見張りをすり抜けてリターンアドレスを書き換え、Return-to-libc攻撃を成立させ得ます。そこで登場するのがASLR(Address Space Layout Randomization)。プログラムの実行ごとに、データ・ヒープ・スタック・ライブラリが置かれる位置をランダムに変える技術です。
ここで少し立ち止まって、「データ・ヒープ・スタック・ライブラリ」が何を指すのか整理しておきます。プログラムが起動すると、OSはメモリ上にいくつかの区画を用意します。マンションのフロアに例えると、用途ごとに部屋が分かれているイメージです。
テキスト領域 … プログラム本体の「命令」が並んでいる場所。料理でいえばレシピが書かれた紙。通常は読み取り専用で書き換えできない。
データ領域 … プログラムの起動時から最後まで消えない変数(グローバル変数など)を置く場所。キッチンに常備してある調味料のように、いつでも使える固定の置き場。
ヒープ … プログラムの実行中に「今だけ必要」な大きさのメモリを、プログラマが自分で借りたり返したりする場所。必要なときにレンタル倉庫を借り、不要になったら返す動きに似ている。malloc()などの関数で確保する。
スタック … 関数を呼び出すたびに自動で積み上がり、関数が終わると自動で片付けられる一時的な作業机。ローカル変数(関数内だけで使う変数)やリターンアドレスはここに置かれる。Return-to-libc攻撃で書き換えられるのもここ。
ライブラリ … OSや言語が用意した共有部品集(system()やstrcpy()などの関数群)。プログラム起動時にメモリ上に読み込まれ、複数のプログラムで使い回せる。Return-to-libc攻撃で「飛び先」にされるのがこの領域の関数。
ASLRは、これらの区画がメモリ上のどの番地に配置されるかを、実行するたびにランダムに変えます。ライブラリ関数のアドレスが毎回変われば、攻撃者は「どこに飛ばせばいいか」を当てられなくなり、Return-to-libc攻撃は困難になります。
ただし、ASLRにも穴があります。ASLRはライブラリなどの位置はランダム化しますが、テキスト領域(プログラム本体のコードが置かれる領域)はランダム化の対象外でした。攻撃者がテキスト領域にある実行可能なコードの断片を悪用すれば、ASLRは効きません。この穴をふさぐのがPIE(Position Independent Executable)で、テキスト領域までランダムに配置します。そして最後の一手がAutomatic Fortification——バッファオーバフローの原因になりやすい危険なライブラリ関数(strcpyなど)を、コンパイル時に「境界チェック(書き込み先のサイズを確認する処理)を行う安全な関数」に自動で置き換え、あふれそのものを起こさせない技術です。
こうして、DEP・SSP・ASLR・PIE・Automatic Fortificationという防御が、それぞれ異なる角度から攻撃を封じていく構図が見えてきます。しかしU社のL部長とX主任は、これらを並べただけでは終わりませんでした。表の備考欄に書かれた注意点以外にも、見落としがちな「防げないケース」が残っていることに気づくのです。
ここから先は

情報処理安全確保支援士(登録セキスペ)の午後問題、「読んでも頭に入らない」と感じていませんか?本シリーズでは、2017年〜2025年の過去…
この記事が気に入ったらチップで応援してみませんか?
