10. セキュリティ
10.1 オペレーティングシステムのセキュリティ
- アプリケーションソフトウェアを実行中のデータセグメントやスタックセグメントは、必ずしも適切に設定されているとは限らない
- アクセスした際にフォールトが発生する可能性があり危険
- 486は特権レベルの移行と連動して、必ずスタックポインタお切り替える仕組みになっている
- 特権レベルの移行(3→0)を伴うコールゲートへのCALL命令の例
- SS、ESPレジスタの内容を内部レジスタに保存
- 特権レベル0のスタック領域のアドレスを現在実行中のタスクのTSSから取り出す
- SS、ESPレジスタにロードする
- 内部レジスタに保存した旧SS、ESPレジスタの内容を切り替えたスタックにPUSH
- CS、EIPレジスタの内容をスタックにPUSHして呼び出し先のアドレスへジャンプ
- 通常のセグメント間RET命令では単にCS、EIPレジスタをスタックからPOPするだけだが、486では、POPしたCSレジスタの指すセグメントの特権レベル(DPL)が動作レベル(CPL)よりも低いことを検出すると、続けてSS、ESPをPOPしてスタックの切り替えを行う(元の特権レベルのスタックに戻る)
- 特権レベルの移行(3→0)を伴うコールゲートへのCALL命令の例
- オペレーティングシステムは、タスク起動時にタスク用のメモリを確保するとともに、自分自身のためのスタック領域を確保し、スタック領域の末尾アドレスをTSSに設定する
- TSSはタスク切り替え時にCPUの状態を保存するためだけでなく、スタックポインタの切り替えにも利用されるので、タスク機能を使用しないオペレーティングシステムであっても、特権レベルの移行を行う場合には、必ず1つはTSSを用意しなければならない
- 特権レベルの高いセグメント上の関数を呼び出した場合、その引数は特権レベルの低いスタックセグメントにPUSHされるため、呼び出した後に切り替わったスタックセグメントでは引数を参照できなくなる
- 参照するためには、元のセグメントにアクセスする設定を行い、引数を読み出す処理が必要だが、これではセキュリティのために効率が犠牲になってしまう
- そこで486では、スタックの切り替えに連動して切り替え前のスタックに積まれた引数を切り替え後のスタックにコピーする機能が備わっている
- コールゲートにはコピーカウントを設定する領域があり、これに応じたバイト数のメモリの内容を、切り替え前のスタックから切り替え後のスタックにコピーする
- コピーするバイト数は16ビットコールゲートでコピーカウンタを2倍した値、386や486のコールゲートではコピーカウンタを4倍した値となる
- RPL(要求者特権レベル)
- アプリケーションソフトウェアを実行しているときの動作レベル(CPL)は3で、特権レベル(DPL)0のセグメントの内容を読み出すことはできないが、コールゲートによってオペレーティングシステムを呼び出すと動作レベルは0になるため、特権レベル0のセグメントにアクセスできてしまう
- RPL(Requester’s Privilege Level)は、セグメントアクセスにおいて、本来そのセグメントをアクセスしようとしたプログラムの特権レベルのこと
- 486は、セグメントアクセスのたびに動作レベル(CPL)とセグメントの特権レベル(DPL)を比べ、さらに要求者特権レベル(RPL)とセグメントの特権レベルを比べる
- この機能により先ほどのコールゲートを使ったセキュリティ破りを防ぐことができる
- 要求者特権レベルのチェックは486が行うが、設定はオペレーティングシステムが行う
- オペレーティングシステムでは、受け取ったセレクタ値の下位2ビットに呼び出したプログラムの特権レベルをセットする
- 486では、DSレジスタにロードしようとするセレクタ値の下位2ビットを要求者特権レベルとして取り出し、セレクタ値の指すセグメントの特権レベルと比べる(低ければ一般保護例外を発生させる)
- コンフォーミングセグメント
- 特権レベルを移行する場合、スタック間の引数コピーの発生、セグメントレジスタの再設定や要求者特権レベルの設定が生じ、オペレーティングシステムの資源をアクセスせず、アプリケーションソフトウェア自身の資源のみを使う場合は非効率
- コンフォーミングセグメントは、特権レベルを移行することなく、アプリケーションからオペレーティングシステムに属する特権レベル0のセグメント内にあるプログラムを呼び出すための仕組み
- コンフォーミングセグメント上のプログラムを実行しているときに限り、セグメントの特権レベルとCPUの動作レベルが異なる
10.2 タスクのセキュリティ
- ローカルディスクリプタテーブル
- 486は、アプリケーションソフトウェアのセキュリティを保つために、ローカルディスクリプタテーブルによってタスクと連動した保護機能を実現している
- セレクタ値が8の倍数ならGDT、4から始まる8つおきの値ならLDTを指す
- GDTは全てのタスクに共通な一方、LDTはタスクごとにそれぞれ独立しており、タスクの数だけ存在する
- 異なるタスクでは、同じセレクタ値であっても対応するセグメントは全く別なものになる点に注意
- この仕組みによりプログラムミスから他のアプリケーションソフトウェアのセグメントのセレクタ値を使ってしまうようなことを防げる
- LDTR
- LDTをタスクごとに切り替えるには、GDT内にLDTを指すディスクリプタを作成し、そのセレクタ値をLDTRに設定する
- GDT内にある0008Hをアクセスする場合
- GDTRの指すGDTを調べる
- GDTから0008Hに対応する位置にあるセグメントディスクリプタを取り出す
- ディスクリプタの指すセグメントをアクセスする
- LDT内にある0004Hをアクセスする場合
- GDTRの指すGDTを調べる
- LDTRのセレクタ値に対応するディスクリプタを取り出す
- ディスクリプタの指すLDTからセグメントのセレクタ値(0004H)に対応するディスクリプタを取り出す
- ディスクリプタの指すセグメントをアクセスする
- GDT内にある0008Hをアクセスする場合
- タスク機能によるタスク切り替えでは、このLDTRの切り替えによるLDTの切り替えが自動的に行われる
- この仕組みは、タスク間の独立性を高め、メモリ管理を非常に信頼性の高いものにする
- 誤ったセレクタ値をセグメントレジスタにセットしても、他のタスクに割り当てられたセグメントをアクセスすることはできない
- LDTをタスクごとに切り替えるには、GDT内にLDTを指すディスクリプタを作成し、そのセレクタ値をLDTRに設定する
- I/O許可マップ
- 486では動作レベル(CPL)とフラグレジスタ中のIOPLビットの組み合わせによってI/Oポートのアクセスを制限するが、このままでは不十分
- 全I/Oポートのアクセスを許可するか禁止するかしか出来ない
- このため486はタスクごとにアクセスできるI/Oポートを限定する機能を持っている(I/O許可マップ)
- I/O許可マップはメモリの各ビットを1つのポート番号に対応させたものになっている
- 1なら禁止、0は許可
- I/Oマップを有効にするにはTSS構造体のなかのiobaseにTSS先頭からのバイト数を設定する
- iobaseアドレスが0であれば、I/O許可マップは存在しないものとして扱われ、全てのI/Oアクセスは禁止される
- 486では動作レベル(CPL)とフラグレジスタ中のIOPLビットの組み合わせによってI/Oポートのアクセスを制限するが、このままでは不十分
- アプリケーションソフトウェアのI/Oアクセスをオペレーティングシステムの管理下に置き、排他制御やエミュレーションを行うことをI/Oの仮想化という
- リアルモード用のアプリケーションソフトウェアには、I/Oポートを直接アクセスして周辺機器を操作するものがある
- こうしたソフトウェアをマルチタスク環境で使用すると、周辺機器への指示が食い違って誤作動を起こす可能性があるため、あるソフトウェアが周辺機器を操作している間は、他のソフトウェアが同じ周辺機器をアクセスするのを防止する必要がある
- オペレーティングシステムが状態を管理している周辺機器の場合は、排他制御だけでなくI/Oの仮想化を行う必要もある
- リアルモードのアプリケーションソフトウェアがI/Oをアクセスする命令を実行しようとした際、制御をオペレーティングシステムに移し、オペレーティングシステム内部の周辺機器操作プログラムを呼び出す
- アプリケーションソフトウェア側から見るとあたかも直接操作したかのように周辺機器が動作するが、実際にはオペレーティングシステムがI/Oアクセスで実行される処理をエミュレートしたために上手く動作したように見える
- 実際に処理したのはオペレーティングシステムなので、状態管理情報は正しく保たれる
- I/Oの実現方法は、TSSのI/O許可マップの設定でI/Oポートへのアクセスを禁止しておく
- アプリケーションソフトウェアがI/Oポートをアクセスすると、486がフォールを発生させ、オペレーティングシステムが排他制御やエミュレートを行う