自作OSに挑戦する日記 21日目
「30日でできる!OS自作入門」を読んで分かったことや、とりあえず書いておきたいことなどを書いていきます。
この本はChapterが1から30まであるので、各チャプター毎に1記事書いていきます。
Chapter 21 「OSを守ろう」
今回やった内容
- 文字列表示API完成
- Cでアプリケーションを書けるように
- 例外
環境
- メインPC
- MacBook Pro (13-inch, 2017, Four Thunderbolt 3 Ports)
- macOS Mojave 10.14
- 自作OSを動作させる環境
作業記録
文字列表示API完成
文字列表示APIを完成させます。原因はコードセグメントの設定をしていなかったことで正しいメモリ番地を指定できなかったことにあるので、OS側でちゃんと設定してあげます。
/* console.c */ // アプリケーション読み込み char *exec_addr = (char *) memman_alloc_4k(memman, file_info->size); read_fat(fat, (unsigned char *) (ADR_DISKIMG + 0x000200)); load_file(file_info->clustno, file_info->size, exec_addr, fat, (char *) (ADR_DISKIMG + 0x003e00)); // コードセグメントを保存しておく *((int *) 0xfe8) = (int) exec_addr;
実行部分からAPIへデータを直で渡す方法はないのでメモリ上に置いておきます。
/* api.c */ int *haribote_api(int edi, int esi, int edp, int esp, int ebx, int edx, int ecx, int eax){ // コードセグメント情報を取得する int cs_base = *((int *) 0xfe8); ~~~~~~~ }
これで文字列表示APIもちゃんと動作するようになりました〜。
Cでアプリケーションを書けるように
アセンブリも楽しいけどまだ慣れない…のでCでアプリケーションを書けるようにします。
コンソールへ何かを表示するためには INT 0x40
を実行する必要があるのですが、Cから実行することはできません。…なのでCとアセンブリの中間に立ってくれるような関数を作ります。
/* api_link_obj */ [BITS 32] GLOBAL api_putchar [SECTION .text] api_putchar: MOV EDX, 1 MOV AL, [ESP + 4] ; 引数1 INT 0x40 ; 1文字表示APIを呼ぶ RET
これで api_putchar
を経由してAPIが叩けるようになりました。Cからはこの関数を使うことでコンソールへの表示を行います。
早速Cでアプリケーションを書いてみます。
/* myapp.c */ void api_putchar(int c_code); void HariMain(void){ // Hello World! api_putchar('H'); api_putchar('e'); api_putchar('l'); api_putchar('l'); api_putchar('o'); api_putchar(' '); api_putchar('W'); api_putchar('o'); api_putchar('r'); api_putchar('l'); api_putchar('d'); api_putchar('!'); }
まだ1文字APIしか使えるようになっていないのでゴリ押しで出力させます。
このアプリケーションをコンパイルするには、api_link_obj.asm
と myapp.c
のオブジェクトファイルをそれぞれ作りリンクする…という2ステップを行う必要があります。
nasm -f bin -o api_link_obj.o api_link_obj.asm i386-elf-gcc -o myapp_c.o -c myapp.c i386-elf-gcc -m32 -O3 -march=i486 -nostdlib -fno-builtin -T har.ld -Wl,-Map=bootpack.map -o myapp.o api_link_obj.o myapp_c.o
myapp.c の実行ファイルである myapp.o
が完成しました!早速実行してみます。
ちゃんと動いてくれました〜!Cでアプリケーションが書けるようになったのはかなり嬉しいですね!(自作OS上でCで書かれた自作プログラムが動いている…)
例外
今のはりぼてOSで次のアプリケーションを実行すると、ディスク上のファイル情報が全く読めなくなってしまいます。(0x00102600はFAT情報が配置される先頭の番地)
void HariMain(void){ *((char *) 0x00102600) = 0; }
>>>crack<<< pic.twitter.com/QuHWIRepTe
— ゆん (@yn0014) 2019年4月25日
これはアプリケーションが本来アクセスすることができない場所へアクセスできてしまうことが原因です…。ということで悪意あるアプリケーションの実行からOSやディスクを守れるようにするために対策をしていきます。
まず、OSとアプリケーションでデータセグメントを分けるようにします。これでOS用のメモリ領域にアプリケーションはアクセスできなくなります。
// console.c char * exec_addr = (char *) memman_alloc_4k(memman, file_info->size); char *app_mem_addr = (char *) memman_alloc_4k(memman, 64 * 1024); ~~~~~ // アプリケーション用のセグメントを設定する set_segmdesc(gdt + 1003, file_info->size - 1, (int) exec_addr, AR_CODE32_ER); set_segmdesc(gdt + 1004, 64 * 1024 - 1, (int) app_mem_addr, AR_DATA32_RW); start_app(0, 1003 * 8, 64 * 1024, 1004 * 8); ~~~~~ // 後処理 memman_free_4k(memman, (int) exec_addr, file_info->size); memman_free_4k(memman, (int) app_mem_addr, 64 * 1024);
64KB分メモリを確保して、その領域をアプリケーション用のデータセグメントとして与えます。また、アプリケーションを実行するための関数を作り、その中で切り替え設定を行います。
start_app
内では各種レジスタの中身をアプリ用に切り替え、fat-Callを行なっています。また、APIや割り込みの処理を行う関数の処理も変え、OSとアプリでセグメントを切り替えて実行できるようにします。(かなり大きな変更だった…)
これで、アプリケーションは自分のデータセグメントのみしかアクセスできなくなるため、上の「crack」アプリケーションは動かなくなりました👍
ここまでの対処法だと下のアプリケーションのメモリ操作が阻止できません。現在のHariboteOSはDSなどの値をアプリケーション自体が操作出来てしまう状態なので、対策が意味の意味がなくなっています。
…どうやって対策をするかというと、DSをアプリケーションから弄れないように(=OS用のセグメントを弄れないように)します。弄られてしまうなら弄れないようにすれば良い!…ということです。
x86には、アプリケーションの実行中にOS用のセグメントを代入しようとした時に例外を投げてくれるような機能があるとのことなので、ありがたく使わさせてもらうことにします。
アクセス権の設定
0x60を足すとアプリケーション用のセグメントとして設定される。楽ちん。
/* console.c */ // セグメント設定->実行 // アクセス権に0x60を足すとアプリケーション用のセグメントになる set_segmdesc(gdt + 1003, file_info->size - 1, (int) exec_addr, AR_CODE32_ER + 0x60); set_segmdesc(gdt + 1004, 64 * 1024 - 1, (int) app_mem_addr, AR_DATA32_RW + 0x60); start_app(0, 1003 * 8, 64 * 1024, 1004 * 8, &(task->tss.esp0));
/* desctbl.c */ // 割り込みが発生したらasm_inthander_2*を呼び出すようにIDTを設定しているset_gatedesc(idt + 0x0d, (int) asm_inthandler0d, 2 << 3, AR_INTGATE32); set_gatedesc(idt + 0x20, (int) asm_inthandler20, 2 << 3, AR_INTGATE32); set_gatedesc(idt + 0x21, (int) asm_inthandler21, 2 << 3, AR_INTGATE32); set_gatedesc(idt + 0x2c, (int) asm_inthandler2C, 2 << 3, AR_INTGATE32); // APIをIDTに登録してどこからでも呼べるようにする set_gatedesc(idt + 0x40, (int) asm_haribote_api, 2 << 3, AR_INTGATE32 + 0x60);
APIにアプリ終了の機能をつける
アプリ実行時にESPの値を保存するようにしているので、その情報を元にRETするようにします。
end_app: MOV ESP, [EAX] ; apiからTSS.esp0の番地が返ってくる POPAD RET
/* api.c */ int *haribote_api(int edi, int esi, int edp, int esp, int ebx, int edx, int ecx, int eax){ int cs_base = *((int *) 0xfe8); struct CONSOLE *console = (struct CONSOLE *) *((int *) 0xfec); struct TASK *task = task_now(); if(edx == 1){ // 1文字表示 console_putchar(console, eax & 0xff, 1); }else if(edx == 2){ // 1行表示 console_putstr(console, (char *) ebx + cs_base); }else if(edx == 3){ console_putstr_with_size(console, (char *) ebx + cs_base, ecx); }else if(edx == 4){ // アプリ終了 return &(task->tss.esp0); } return 0; }
他のコードもかなり変わりましたが長すぎるので載せれません…。アプリとOSのセグメントを切り替えて実行するようにしたり、例外割り込みが飛んできたときの処理を書き換えたりしました。
アプリ終了をAPIから行うようにしたので、「myapp」や「gochiusa」のコードが少し変わります。
/* myapp.c */ void harimain(void){ api_putchar('h'); api_putchar('e'); api_putchar('l'); api_putchar('l'); api_putchar('o'); ~~~~~~~~~~ api_end(); // ここ }
/* gochiusa.asm */ [BITS 32] start: MOV EDX, 2 MOV EBX, gochiusa_url INT 0x40 fin: MOV EDX, 4 // ここ INT 0x40 // ここ gochiusa_url: DB "www.gochiusa.com", 0
これで不正なアプリケーションの実行からOSを守れるようになりました!21日目完成!
例外〜〜! pic.twitter.com/NvblWCYV6B
— ゆん (@yn0014) April 25, 2019
まとめ
21日目の内容はセキリュティ要素が濃くかなり面白い回でした!
TSSやセグメント周りの知識が少し甘いような気がしたので復習します。