保存されたRBPが壊れると何が起きるか(episode-09)
― スタック崩壊の「本当の始まり」を理解する ―
「リターンアドレスの前に壊れるもの」
前回(episode-08)では、バッファオーバーフローが「メモリ境界をチェックしない設計」から生まれ、スタック上では下から順に壊れていく、という流れを説明しました。
ローカル変数
↓
保存されたRBP
↓
リターンアドレス
多くの解説では、「リターンアドレスが書き換わると制御が奪われる」という結論だけが強調されがちです。
しかし、その直前に必ず通過する重要な地点があります。
それが、保存されたRBP(Saved RBP)です。
ここでは、
・保存されたRBPとは何か
・それが壊れると何が起きるのか
・なぜ攻撃者にとって重要なのか
を、GDBで観察できる現象として整理していきます。
※イメージです。
保存されたRBPとは何か(再確認)
関数が呼び出されると、スタックフレームが作られます。
x86-64 の一般的な関数プロローグは次の形です。
(アセンブラ言語)
push rbp
mov rbp, rsp
sub rsp, 0x??
ここで行われていることは、
1.呼び出し元のRBPをスタックに保存。
2.RBPを「この関数の基準点」に設定。
3.ローカル変数用の領域を確保。
つまり、保存されたRBP = 呼び出し元のスタックフレームへ戻るための道標です。
[fuki-r]RBPって、ただのレジスタじゃないの?[/fuki-r][fuki-l]今どこにいるか”と“どこに戻るか”を同時に覚えている、と考えるといいわ[/fuki-l]
RBPが壊れると、何が起きるのか
関数の終了時、CPUは次の処理を行います。
(アセンブラ言語)
leave ; mov rsp, rbp → pop rbp
ret
この “leave" 命令は、
1.RSPをRBPの位置に戻す。
2.保存されていたRBPを取り出す。
という2段階の動きをします。
ここで重要なのは、保存されたRBPが壊れていると、RSPの復元先そのものが狂うという点です。
初心者がよく疑問に思うのが、
「RBPが壊れたら、すぐ落ちるのでは?」という点です。
しかし実際には、
・プログラムがしばらく動き続ける。
・その後、意味不明な場所でクラッシュする。
という挙動を示すことがよくあります。
理由は単純です。
・RBPは、次のスタックフレームの基準。
・すぐには使われない場合がある。
・しかし「次の関数」、「次の leave」で致命傷になる。
つまり、保存されたRBPの破壊は遅延して効いてくるタイプの破壊なのです。
[fuki-r]壊れたのに、すぐ倒れないのが逆に怖いね…[/fuki-r][fuki-l]ええ。“気づかない崩壊”こそが一番危険なの[/fuki-l]
スタックフレーム全体がずれる現象
保存されたRBPが壊れると、次に起きるのは、
・ローカル変数の参照が狂う。
・本来のスタック境界を無視してアクセスする。
・`leave` 後の `ret` が想定外の位置を読む。
結果として、
・Segmentation Fault。
・不正なアドレス参照。
・予測不能な挙動。
が発生します。
これは、スタック構造そのものが壊れた状態です。
攻撃者視点 なぜRBPは重要なのか
攻撃者は、いきなりリターンアドレスを書き換えません。
理由は、
・ASLR でアドレスが読めない。
・直接書くと即クラッシュする。
そこで注目されるのが保存されたRBPです。
・RBPを書き換える。
・次の “leave"で RSPを誘導。
・その先で、制御可能な位置にretを導く。
これが、Stack Pivot(スタックピボット)と呼ばれる技術の入口です。
[fuki-r]RBPって、“踏み台”にされるんだ…ん[/fuki-r][fuki-l]そう。直接飛ばず、まず足場を動かすの[/fuki-l]
GDBで見ると、すべては「納得できる」
GDBで観察すると、
・"info registers rbp rsp"
・"x/xx $rbp"
・"bt"
などを通して、RBPがどこを指しているか、どの時点で壊れたか、なぜ次の関数で落ちたかが、すべて論理的に説明できるようになります。
ここで重要なのは、バグは「魔法」ではなく構造が壊れた結果として起きているという事実です。
保存されたRBPが壊れると何が起きるかのまとめ
保存されたRBPは、単なる一時データではなく、スタックフレーム同士を正しくつなぐための重要な基準点です。
バッファオーバーフローによってこのRBPが破壊されると、関数終了時に行われるスタック復元処理そのものが狂い、RSPやリターンアドレスの読み取り位置がずれてしまいます。その結果、プログラムはすぐにクラッシュしない場合でも、後続の処理で予測不能な挙動や異常終了を引き起こします。
攻撃者はこの性質を利用し、保存されたRBPを書き換えることでスタックの基準を移動させ、制御フローを段階的に誘導します。
GDBで観察すれば、これらの現象はすべて論理的に理解でき、スタック破壊は再現可能な構造的問題であることが見えてきます。