Yabu.log

ITなどの雑記

30日OS自作本22日目1セキュリティ編

22日目の内容になります。

長すぎるので3つに分けます。 3日目以降でやや難易度が高くなっている章だと思われます。

今回はセキュリティ編です。

バグ発見のための拡張

  • スタック例外の実装
    • スタック例外(配列に対して大きすぎるインデックス等)は0x0cでハンドルされる。ここに割り込みハンドラを登録すればOK
    • QEMUではうまく動かないようだ
  • 強制終了の実現方法
    • bashのctrl + c 的なやつ
    • 強制終了(asm_end_app)の命令を作成し、この命令をtask.eipに書き込む
    • 本書ではctrl + F1を押したタイミングで発動
_asm_end_app,
;   EAXはtss.esp0の番地
        MOV     ESP        ,[EAX]
        MOV     DWORD [EAX+4]        ,0
        POPAD
        RET                 ; cmd_appへ帰る
  • 元のESPをEAXに代入した状態で呼びます。
  • このサブルーチン内でESPや各レジスタの値を退避してあるスタックからPOPADで戻してreturn(呼び出し元cmd_appへ戻る)します。

個人的にはバグ発見のツールとしては以下のものが欲しい

  • ホスト側のOSでメモリやレジスタをリアルタイムでダンプする
  • ホスト側のOSからゲストOSの機械語を1命令ずつステップ実行する
    • かつ、任意のタイミングでレジスタ、メモリが参照できる
  • ホスト側のOSにログが残せる
    • log.println("message")みたいにかけばホスト側のtxtファイルにログが残せる
  • デバッグ時の詳細出力

なお最後のものは自力で作れる範囲で作ってみた。 一般保護例外捕捉時にスタックに自動的に積まれる値を表示しているだけ。

一応この部分のソースを貼っておく

int *inthandler0d(int *esp)
{
    struct CONSOLE *cons = (struct CONSOLE *) *((int *) 0x0fec);
    struct TASK *task = task_now();
    char s[30];
    cons_putstr0(cons, "¥n0D General Protected Exception¥n");
    sprintf(s, "CS:EIP     = %04X:%08X¥n", esp[12],esp[11]);
    cons_putstr0(cons, s);
    sprintf(s, "SS:ESP     = %04X:%08X¥n",esp[15],esp[14]);
    cons_putstr0(cons, s);
    sprintf(s, "EFLAGS     = %08x¥n",esp[13]);
    cons_putstr0(cons, s);
    sprintf(s, "ERROR CODE = %08x¥n",esp[10]);
    cons_putstr0(cons, s);

    return &(task->tss.esp0);  /* 異常終了させる */
}

様々な攻撃を防ぐ

crack3,アプリケーションからタイマーを不正に弄る

GUI(マウス等)の描画崩壊を狙ったタイマーに不正な値を書き込むプログラムです。 アプリケーション領域からのIOポートの利用を制限されているのでこのプログラムはフォールトの例外が発生します。

EFLAGSレジスタのIOPLフィールドが動作レベル(CPL)より高い特権レベルを持っているのでIOポートの利用が制限されているということになります。

アプリケーション用のセグメントの特権レベルは21日目に3(一番弱い)で設定してあります。

EFLAGSレジスタはPOPFD命令で設定できるようです。 都合の良いことにnaskfunc.nasにPOPFD専用のサブルーチンがあったので利用してみます。

_io_store_eflags,    ; void io_store_eflags(int eflags);
        MOV     EAX        ,[ESP+4]
        PUSH    EAX
        POPFD       ; POP EFLAGS という意味
        RET

権限の高いセグメントで動いているPGでELGASを書き換えてcrack3を実行してみました。

//ファイル先頭らへんに定義
#define EFLAGS_IOPL_3 0x00003000

(中略)
//これをhariMainで実行
int eflg;
eflg = io_load_eflags();
eflg |= EFLAGS_IOPL_3; /* IOPL=3 */
io_store_eflags(eflg);

OSの方で画面出力してみましたが、

  • 改修前,0246
  • 改修後,3246

でうまく変更できているように思えます。

hex binary
eflags 00!!000000000000
0246 0000001001000110
3246 0011001001000110

eflagsの!のところがIOPL。上から3,4bit目

4バイト目の下位2ビットが1になっていることを確認できたのですが、

EFLAGSレジスタの値は正しく更新できているのですが、 特に変化はなしです。なんか俺が試したこと全部失敗してるな~

と思いましたが、例外補足時のログで実行時にIOPLが変化してしまっていることが発覚。 おそらくhariMain内で書いた後にどこかで変わっていたのだと思います。探しても見つかりませんでした。

IOPLが変化した理由

並行して読んでいるはじめて読む486がちょうどマルチタスクの章に入ったところで気づきました。

  • EFLAGSレジスタはOS全体で共有しているものではなくて、タスクごとに用意されている

そうです。hariMainでEFLAGSを変えても、別タスクであるconsoleのEFLAGSには変化はないのです。 TSSにEFLAGSが含まれているのをみて気付きましたが、これに気付いた時かなりスッキリしました。

...ということでなんとかしてconsoleのタスクからeflagsを変更する必要があります。

IOPLを変更できるコマンドを作成

とりあえずIOPLを変更できるコマンドを作ってみました。

IOPL SET <Privilege level>

最強の0から最弱の3まで全て設定可能です。これで実行時にアプリケーションがIOポートにアクセスすることができます。

www.youtube.com

cracks4,CLI + HLT命令で割り込みを無効化する

[INSTRSET "i486p"]
[BITS 32]
        CLI
fin,
        HLT
        JMP     fin

こちらも正しく例外でストップします。アプリケーションの権限で動いているときにHLTとCLI命令を実行すると例外扱いになります。 HLT命令をアプリケーションが直接実行するのはまずいので、タスクをスリープさせるAPIを呼ぶべきです。

crack5,OSの領域にあるCLI命令を含むサブルーチンを呼ぶ

[INSTRSET "i486p"]
[BITS 32]
        CALL    2*8,0xac1
        MOV     EDX        ,4
        INT     0x40

アプリのセグメントからOSのセグメントのPGを呼び出すfarCALLは失敗します。 コールゲート(ゲートディスクリプタ)、という仕組みを利用してCLIを含むサブルーチンがロードされているセグメントをアプリケーションから呼べるように設定すればうまくいく気がしますが、気が向いたら試してみます。

(おまけ),crack6

osのルートディレクトリを破壊するようなAPIを呼ぶcrack6です。 そんなAPI定義するなよ!!!  という感じですが、プロテクトモードのセグメントの設定をきちんとすれば、このくらいしかこのOSを攻撃する方法はないということです。 これは結構すごいことじゃないでしょうか。と思いましたが、多分まだまだ僕の知らない攻撃方法はあるのでしょう。

各攻撃バイナリのエラーを表示してみる

悪意のあるアプリ 攻撃内容 備考
crack1(p428) C言語でルートディレクトリにヌル文字(0x00)を書き込む QEMUで例外捕捉せず
crack2 アプリからOS用のセグメントにアクセスし、1と同じくルートディレクトリを壊す QEMUで例外捕捉
crack3 アプリからIOを不正利用 QEMUで例外捕捉
crack4 アプリからCLI HLT命令を実行 QEMUで例外補足
crack5 アプリからOSの命令をfar Call QEMUで例外捕捉

ログ内容

バイナリ CS,EIP SS,ESP FLAGS ERROR CODE
crack2 1F5B,00000005 1F63,00010000 00000206 00000008
crack3 1F5B,00000002 1F63,00010000 00000206 00000000
crack4 1F5B,00000000 1F63,00010000 00000206 00000000
crack5 1F5B,00000000 1F63,00010000 00000206 00000010

せっかく作ったログを使ってデータを参照したいのですが、どうやら.mapファイルがないと難しいようです。nasmでmapファイルを作る方法などをググって試してみましたが、うまくいきませんでした。(アセンブル時にエラー)

次回は.hrbファイル構造の解説とそれを利用したOSの改善について書きます。