黒ネコと学ぶ、保護機構をやさしく理解する
ここまでの保護機構シリーズでは、
・NXによって、データを置く場所でそのまま命令を実行しにくくすること。
・Stack Canaryによって、return addressの手前に見張り役を置くこと。
・ASLRによって、メモリの場所を毎回変えて予測を難しくすること。
・PIEによって、プログラム本体の位置まで固定で考えにくくすること。
を学んできました。
すると次に、こんな疑問が出てきます。
・プログラムはライブラリの関数をどうやって呼んでいるの?
・その呼び出し先の情報は、どこかに表のようにまとまっているの?
・もしその表が書き換えられたら、どうなるの?
・それを守る仕組みはあるの?
この疑問に関係するのが、今回の「RELRO」です。
白猫先生
RELROをとてもやさしく言うと、「書き換えられたくない大事な表を、守りやすくする仕組み」です。
これまでの話では、スタックやアドレス、実行の可否といった守りを見てきました。
しかし、今回は少し視点を変えて、「プログラムが外の関数を呼ぶために使う表」に注目します。
今回は、
・RELROとは何か。
・何を守るのか。
・なぜ必要なのか。
・Partial RELROと、Full RELROはどう違うのか
を、やさしく順番に整理していきます。
※イメージです。
RELROとは何か
RELRO とは、簡単に言えば「プログラムの中にある大事な表を、書き換えにくくする仕組み」です。
RELRO はよく、「RELocation Read-Only」の略として説明されます。
英語だけ見ると少し難しいですが、意味をやさしく言い換えると、「必要な準備が終わったら、その表を読み取り中心にして守る」というイメージです。
ここでいう「表」とは、プログラムが外部の関数、たとえば "printf" や "puts" などを使うときに参照する情報の集まりです。
プログラム本体の中だけで全部が完結するわけではなく、実際には「ライブラリの中にある関数」を呼ぶことがよくあります。
そのとき、「どの関数をどこへ飛んで呼ぶか」という情報を持つ表が関係してきます。
その表がもし自由に書き換えられる状態だと、呼び出し先の情報まで変えられてしまう可能性があります。
そこで、RELROは、そうした大切な表を守りやすくするのです。
白猫先生
簡単にたとえるなら、RELROは「先生が使う連絡先一覧表を、勝手に書き換えられないようにするカバー」のようなものです。
どんな「表」を守っているのか
ここで、「どんな表なのか」をやさしく整理しましょう。
プログラムは、たとえば"printf()"のような関数を呼ぶとき、毎回ゼロから探しているわけではありません。
呼び出し先に関する情報を、ある程度まとまった形で使っています。
初心者向けには、次のようにイメージするとわかりやすいです。
・プログラム本体がある。
・外にあるライブラリ関数を使いたい。
・そのとき「この関数はここへ行けば呼べる」
という対応表がある。
この対応表の代表として、学習では「GOT」や「PLT」という言葉がよく出てきます。
ここでは、まだ細かい内部構造を全部覚えなくて大丈夫です。
今の段階では、
・PLTは「呼び出しの入口のようなもの」。
・GOTは「実際の行き先の情報を持つ表のようなもの」
と、ざっくり考えれば十分です。
そして RELRO が特に関係するのは、こうした「行き先の情報を持つ大切な表」です。
もしこの表が書き換えられると、本来"printf()"を呼ぶはずだったのに、別の場所へ進んでしまうかもしれません。
つまり、ただの一覧表ではなく、「プログラムの動きそのものに関わる表」なのです。
なぜRELROが必要なのか
では、なぜこの表を守る必要があるのでしょうか。
理由はとてもシンプルで、「関数の呼び出し先の情報が書き換えられると、プログラムの動きが変わってしまうから」です。
たとえば学校で、先生が使う連絡先一覧があるとします。
そこには、
・職員室
・保健室
・事務室
などの内線番号が正しく書かれています。
もし誰かがその表を書き換えてしまったら、先生が「保健室に電話したつもり」で、別の場所につながってしまうかもしれません。
大変ですよね。
プログラムの中でも似たことが起こります。
本来なら "puts()" に行くはずが、その表が書き換わっていたら、別の場所へ進む可能性があります。
だからこそ、「呼び出し先の表は自由に書き換えられてはいけない」のです。
RELRO は、この表を守ることで、「行き先一覧を勝手に変えられる」ことを難しくします。
ここで大切なのは、RELROは「プログラム全体を何でも守る万能バリアではないということです。
RELRO は特に、動的リンクまわりの大事な表を守ることに強い守りとして理解するとよいです。
サンプルコードでやさしく考える
ここで、学習用のシンプルなコードを見てみます。
-----
#include <stdio.h>
int main(void) {
puts("Hello");
return 0;
}
-----
このコードはとても短いですが、今回のテーマを考えるには十分です。
ここでは "puts("Hello");" を呼んでいます。
このときプログラムは、「puts はどこにあるのか」という情報を使って、実際の関数へ進みます。
初心者向けには、ここで「puts の行き先をまとめた表を参照している」と考えるとわかりやすいです。
もしその表が安全に守られていなければ、本来の"puts"ではない別の場所が行き先として入ってしまうかもしれません。
ここで、RELROがあると、その表は準備後に守られやすくなります。
すると、行き先情報を勝手に変えるのが難しくなります。
このコードから学ぶべきことは、ただ"puts"を呼んでいるだけに見えても、その裏では「行き先の表」が重要な役割を持っているという点です。
そしてRELROは、その裏で支えている大切な表を守るための仕組みなのです。
Partial RELRO と Full RELRO の違い
ここで、RELROには
・Partial RELRO
・Full RELRO
の2種類があることを整理しておきましょう。
初心者のうちは、この違いを「守りの強さに段階がある」と考えるとわかりやすいです。
1.Partial RELRO
Partial RELROは、ある程度の守りを入れた状態です。
何も守りがないよりはよいのですが、まだ完全ではありません。
イメージとしては、大事な表に部分的なカバーをかけた状態です。
2.Full RELRO
Full RELROは、より強い守りです。
必要な準備を早めに済ませたうえで、その表をしっかり守る方向になります。
イメージとしては、大事な表をラミネートして鍵付きのケースに入れるような感じです。
白猫先生
簡単にたとえるなら、Partial RELROは「ふたをした状態」、Full RELROは「ふたをして鍵もかけた状態」と考えるとわかりやすいです。
大切なのは、RELROはあるかないかだけでなく、どこまで強く守っているかにも差があるということです。
この感覚を持っておくと、あとで"checksec"などを見たときにも理解しやすくなります。
保護機構をやさしく理解するシリーズ。RELROとは何か? 書き換えられたくない表を守る仕組みのまとめ
今回学んだ大事なことは、RELROがプログラムの中にある大切な行き先の表を守り、関数の呼び出し先情報が勝手に書き換えられにくくする保護機構だという点です。
プログラムは、"puts"や"printf"などのライブラリ関数を使うとき、その行き先に関する情報を表のような形で参照しています。もしその表が自由に書き換えられると、本来とは違う場所へ進む可能性があるため、とても危険です。RELROは、その表を読み取り中心にして守りやすくすることで、こうした問題を起こしにくくします。
また、RELROには、Partial RELROとFull RELROがあり、後者のほうがより強く守る仕組みです。大切なのは、RELROを「書き換えられたくない大事な表を守る仕組み」としてイメージできるようになることです。