6. 境界外書き込みは何を壊すのか

6.1 問題は一行にある

char name[16] = {0};
read(STDIN_FILENO, name, 128);

nameに書けるのは16バイトである。しかしreadへ渡す最大値は128バイトである。Cのポインタに配列長は付随しないため、readは第3引数を信用する。

つまり、本質は「文字列が長い」ではない。書き込み先の容量と、書き込み関数へ渡した長さの契約が食い違っている。

6.2 書き込みとretは別の時点

時点T1: readがバイトを連続して書く
          name[0..15]
          → 保存RBP
          → 保存された戻り先

時点T2: greetなど後続処理が動く

時点T3: read_nameの終了処理がスタック先頭を戻り先へ合わせる

時点T4: retがその8バイトをRIPへ入れる

書き込んだ瞬間に分岐が起きるのではない。その時点では、後で読まれる制御データが壊れただけである。関数が戻るとき、初めて壊れた値が命令位置に使われる。

6.3 正常系と異常系を同じ時点で比べる

正常系と異常系のread後およびret後の比較

図: 違いは「長い入力」だけではない。retが読む位置の8バイトが、正しい戻り先から別のアドレスへ変わっている。

時点 正常系 異常系
read前 nameは16バイト、戻り先はmainの続き 同じ
read後 5バイトだけ変化 name、保存RBP、戻り先まで変化
ret直前 スタック先頭は正しい戻り先 スタック先頭は書き換えられた値
ret後 RIP = mainの続き RIP = 書き換えられたアドレス
命令取得 Goodbyeへ進む 例ではwinから取得する