自作OS「CHNOSProject」 https://osdn.jp/projects/chnosproject/ https://github.com/hikalium/chnos 「30日でできる!OS自作入門」のはりぼてOSを参考にしながら,ブートローダーを含むすべてのコードを書き直し,VESA対応,ページング対応,仮想86モードによる保護モードからのBIOS呼び出しなどを実装した.
OSECPUのJavaScript移植「WebCPU-VM」 http://osecpu.osask.jp/wiki/?hikarupsp_WebCPU-VM 非常に小さなバイナリコード体系が特徴のOSECPU-VMを,C言語からJavaScriptに移植したもの.Canvasで描画もできる.
OSECPUバイナリを出力するC言語風言語「ELCHNOS」 http://osecpu.osask.jp/wiki/?hikarupsp_ELCHNOS http://osecpu.osask.jp/wiki/?hikarupsp_ELCHNOS_IDE OSECPUバイナリを出力する独自言語のコンパイラと,上記のWebCPU-VMと統合した開発環境のようなもの.
グラフ構造表示HTML5アプリ「Mind Graph Canvas」 https://hikalium.com/projects/mgcanvas/index.html https://github.com/hikalium/mgcanvas グラフ構造を編集したりできる.マウスで操作可.iPadなどのタッチ操作にも対応.将来的にはデータベースにつながるようにする予定.
その他の成果物 https://hikalium.com/page/products
CHNOSProject 言語: アセンブリ言語・C言語 川合秀実氏著「30日でできる!OS自作入門」に付属のtolsetとよばれるツール群を利用して開発した. それ以外のライブラリ等は使用していない.printfも自分で実装した. https://github.com/hikalium/chnos/blob/master/tolset_chn_000/chnos_010/chnos/cfunc.c
WebCPU-VM 言語: JavaScript ライブラリ不使用.OSECPU-Wikiの情報のみを参考にして開発した.
ELCHNOS 言語: JavaScript ライブラリ不使用.OSECPU-Wikiの情報のみを参考にして開発した.仕様も文法解析も書籍を読まずに思いつきで実装した.
Mind Graph Canvas 言語: TypeScript ライブラリ不使用.むしろJavaScriptの自作関数群がこの頃にはできあがってきて,それをWebCPUやELCHNOSで流用している https://github.com/hikalium/mgcanvas/blob/master/src/core/ext.ts https://github.com/hikalium/mgcanvas/blob/master/src/core/vector2d.ts https://github.com/hikalium/mgcanvas/blob/master/src/core/uuid.ts
・(ブログではありませんが)ホームページ https://hikalium.com ・各種Wiki内のページ http://osecpu.osask.jp/wiki/?hikarupsp http://osask.net/w/520.html
過去の記録: http://osask.net/w/520.html#gce3e816 昔OSを自作していた頃に,メモリのある部分がいつの間にか破壊されていて,システムが正しく動作しなくなるという問題に遭遇したことがある. フルスクラッチで書いたOSだったので,QEMUのデバッグ機能を使ってメモリやレジスタを確認するか,シリアルコンソールにデバッグ情報を出力することしかできず,どうやってメモリ破壊の犯人を捜すか,という問題にぶつかった.
CPUのドキュメント(IA-32 インテル アーキテクチャソフトウェア・デベロッパーズ・マニュアル)を読んでいると,デバッグ例外という例外が存在することがわかった.このデバッグ例外は,特定のメモリアドレスに対するアクセスを試みた時に,発生してくれる例外である.そこで,デバッグ例外を捕捉する関数を実装し,そしてメモリ破壊が起こる物理アドレスをデバッグレジスタに設定した. しかし,デバッグ例外はQEMUとBochsでは発生せず,さらにMicrosoftVirtualPC2007では,発生はするものの,仕様と異なる動作をしていることが判明した.そこで,応急処置的にVirtualPCの仕様に合うようにプログラムを書き換え,再度実行した.すると,メモリ破壊を起こしている犯人を突き止めることができた. …それはメモリ管理システムでした.
・まずは自分の書いたコードをよく読んでミスがないかチェックする バグは意外なところに潜んでいます.複雑そうに見えるバグも,変数名を一つ間違えていただけだったりします.こういったバグを避けるためにも,変数名には紛らわしい文字を使わないようにするべきです.(iとj, oと0など)
・エミュレーターはあくまでもエミュレーター,実機を信じよう エミュレーターは仕様と異なる動作をすることが「よく」あります(稀ではありません).そして,エミュレーターと実機のどちらが正しいかと言われれば,それは常に実機です.なぜなら,ユーザーは実機を使うからです. 特に,ハードウエアに近いレイヤのプログラムを書いていると,仕様と異なる動作をすることがよくあります. なので,仕様やエミュレーターを信じ込むのではなく,実際にどう動作しているかを注意深く観察し,それに合うように実装しましょう.
・さまざまな環境で実行しよう 今回の場合はエミュレーターの問題でしたが,環境によって動作しない機能があるかもしれません.仕様に書いてあるからといってその例外が発生すると信じるのではなく,確実にその例外が発生するようなコードを書いてみて,想定通りの動きをするかどうかをチェックしたり,実機を含む様々な環境でテストをすることで,無駄に頭をひねる必要がなくなります.
ネットワーク上をパケット,ひいては電気信号が流れているという事実は忘れてしまいがちだが,実際に電気的な手法でパケットを生成する実習を通して,ネットワークの仕組みをより深め,さらに電気的に流れるパケットのイメージをつかめるようになりたいと考えているから..
ソフトウエアではなく,ハードウエア自体のリバースエンジニアリングは今までほとんどしたことがないので,物理的なシステムのことも考慮しながらのリバースエンジニアリングをしてみたいと感じたから.
OpenCVのように,機械学習を応用したソフトウエアを用いて,実際のハードウエアを動かしてみるのは,動作が完全に予想できないという点で難しそうだが,それをうまく制御してゆく手法を習得したいと考えている.
選択課題8もROPの一種だと思うが,それを解析していて面白いと感じ,もっとROPをじっくり考えてみたいと感じたため.
USBは現在では広く普及したため,逆に標的になりやすいインターフェイスである.そのようなUSBを介してキー入力を自動化するBadUSBを自作するというのは,ハードウエア的にもソフトウエア的にも面白そうな課題であると感じたため.
リアルタイムOSは一般的なOSとは少し違う仕組みを採用している部分があると聞いたことがあり,OSを自作したことのある私にとっても,また違った種類のOSを知るいい機会であると感じたから.
私はこれまで,OS開発のような低レイヤのプログラミングや,逆にWebサーバーやWebアプリケーションなどの高レイヤの開発を経験してきたが,セキュリティを考慮したコードや,逆にその弱点を突くようなコードを,セキュリティという面に特に集中して書いたことは今までなかった. そのため,後になってから「こんな脆弱性を作り込んでいたかもしれない」と気づいたり,「本当にこれで安全」かどうか,確証がもてないことが多々あった. このように曖昧な私のセキュリティに関する知識を,5日間という短くも密度の濃い時間を通して,より確実なものにしてゆきたいと考えている. また,キャンプの中で実施される講義では,一人では学習することが難しい,ハードウエアのセキュリティについての実習も多く設定されているため,これらの講義を通して,ソフトウエアだけでなく,ハードウエアの面におけるセキュリティについてもより理解を深めていきたい.
以下は,一般的なAT互換機についての話である.
電源ボタンが押されたとき,PC全体は初期状態にリセットされる. そして,CPUはリセットの結果,0xFFFFFFF0から実行を始める.通常この場所にはリセットベクタと呼ばれるジャンプ命令があり,BIOS ROM内の初期化コードへジャンプする. BIOS ROMはメインメモリにマップされており,電源を切っても情報が失われることはない.そのため,主記憶装置上のデータしか扱えないCPUが,初期化作業を実行することができる. BIOS初期化コードの中では,PCに接続されているデバイスの検出と初期化,そして起動可能なデバイスの検出・IPLのロードが行われ,初期化がすべて終了した後に,IPLのコードへJMPする. IPLもしくはMBRと呼ばれるプログラムは,512Byteの大きさをもち,0x7C00番地に配置され,実行される. この小さなコードによってOS本体を補助記憶装置から主記憶装置へ読み込んでゆく.(これは非UEFIブートの場合だが,UEFIブートの場合も,小さなプログラムによって,OS本体を補助記憶装置から主記憶装置へ読み込むという流れは変わらない). OS本体を主記憶装置上に読み込んだら,ブートローダーはOS本体のコードへ制御を移す. この後,OS本体のコードが周辺機器の設定などを行い,入力や出力の制御を得て,ユーザーとの対話環境を提供する.
これ以降は,ユーザーがプログラムの実行を指示すると,OSが補助記憶装置からプログラムのデータを主記憶装置に読み込み,そして主記憶装置上のコードを実行する.これによって,私たちは主記憶装置と補助記憶装置の差異をあまり気にせずに,プログラムを実行することができるのである. OSは,ユーザーの目に触れないところで,プログラムの実行を支援するために様々な仕事をしているのだ.
そもそもOSとは,「面倒なことを代わりにしてくれる縁の下の力持ち,というか,家そのもののような存在」であると私は考える. 「OSが無い」環境でプログラミングをするということは,大自然の中でサバイバル生活をするのと近い部分がある.どこに行っても何をしても自由だが(メモリは自由に使え保護機構は存在しないので自由に読み書きできるが),いつ何に襲われるか分からないし,突然地面に空いた穴に足を取られる可能性だってある(ほかのプログラムや自分自身によってプログラムが上書きされるかもしれないし,実メモリのつながっていない領域に読み書きしたせいで帰って来れなくなるかもしれない). OSが存在すれば,自由にできる範囲は限られるけれど,風雨はしのげるし,鍵をかければ泥棒が入ってくることもない(メモリはすべてつかえるというわけではないが,保護機能が提供される). 組み込みOSと汎用OSの違いは,住もうとしている星または島の大きさや,その島の資源の差異と対応させることができる(メモリの大きさや,CPUの性能,外部デバイスの種類など). 組み込みOSは資源の限られた小さな無人島に似ている.小さな島に大きな家を建てることはできない(メモリの限られた組み込み環境に巨大なOSを入れることはできない). 逆に,汎用OSは家だけでなく街や行政システムをも内包したものだと考えてもよい. 大きな土地(メモリ空間)を開発して,ひとつの街をつくりあげる(システムコールなどを整備する)ものが汎用OSである. 食べ物(入力)が欲しければ,都会(汎用OS)であれば,近くのコンビニ(システムコール)にでも行けばよい.一方無人島(ベアメタル環境)だった場合は,地面に種をまいて自分で育てる必要があるかもしれない(自分でメモリやIOポートを叩いて取得する必要がある). 私自身,OS自作をした経験があるが,OSの無い世界は生きていくのが難しい(デバッグとかが大変).それでも,制限が何もない世界でプログラムを書くという経験はとてもおもしろいものだった. しかし,制限がない,自由すぎる世界というものは逆に扱いにくい.そのようなベアメタル環境を整備し,プログラムやプログラマにとって居心地のよい環境を提供するものが,OSではないだろうか.
与えられたobjdumpと同等のアセンブリソースは以下のようになる.
.code64
.text
.global _start
_start:
pushq $0x400119
pushq $0x1
pushq $0x400106
pushq $0x400119
pushq $0x400129
pushq $0x3c# 60: exit?
pushq $0x400102
pushq $0x400110
movabs $0x63391a67251b1536,%rax
push %rax
pushq $0x400102
pushq $0x0
pushq $0x400106
pushq $0x400114
pushq $0x40010c
pushq $0x400102
pushq $0x400126
pushq $0x400114
pushq $0x7
pushq $0x40010a
pushq $0xffffffffffffffe0
pushq $0x400108
pushq $0x400119
pushq $0x8
pushq $0x400104
pushq $0x0
pushq $0x40011c
pushq $0x0
pushq $0x400106
pushq $0x0
pushq $0x400102
retq
pop %rax
retq
pop %rdx
retq
pop %rdi
retq
pop %rbp
retq
pop %rcx
retq
add %rbp,%rsp
retq
cmp %rax,(%rsi)
retq
xorb $0x55,(%rsi,%rcx,1)
retq
syscall
retq
mov %rsp,%rsi
pop %r10
retq
mov %rsi,%rcx
retq
dec %rcx
jne L40012c
retq
L40012c:
pop %r10
retq
これを,4-8.Sとして保存し, as 4-8.S ld -T 4-8.ld -o challenge00 a.out としてバイナリを生成した.ここで,リンカスクリプト4-8.ldは以下の通りの内容とした.
SECTIONS
{
.text 0x400080:
{
*(.text)
}
}
生成されたバイナリに対して, objdump -d challenge00 と実行し,与えられたダンプと同一の内容のダンプが得られることを確認した.
ここで,ソース全体を眺めてみて,以下の点に気づいた. ・cmp命令と,条件jmp命令jne, syscall命令, xor命令がそれぞれ1箇所ずつある.
また, ./challenge00 としてバイナリを実行してみると,一回入力待ちになった後,何らかの入力を行うと終了することがわかった.
そこでまず,syscall命令がどのようなパラメータで呼び出されているかを確認することにした.
as -g 4-8.S ld -T 4-8.ld -o challenge00 a.out として,デバッグ情報付きでバイナリを生成し, gdb challenge00 として,GDBでデバッグを開始した. まず,syscallの実行直前にブレークポイントを追加した. break *0x400119 そして,実行を開始し,syscall命令でブレークしたところで info register を実行し,レジスタの値を確認した. すると,rax==0であったから,readシステムコールが実行されていることがわかった. また,第一引数=0, 第二引数=rsiのアドレス, 第三引数=8より, read(stdin, rsi, 8); が実行されていることがわかった. ここで,syscallの機能番号と引数の渡し方は,以下のサイトを参考にした. http://www.mztn.org/lxasm64/x86_x64_table.html
よって,最大8文字を標準入力から読み込んでいることがわかる.
ここから, si として命令単位でステップ実行したところ,プログラムが終了するまでにxor命令が常に8回実行されることがわかった.したがって,このxor命令は文字列の各バイトに0x55をxorしているのではないかと予想をたてた.
そこで,xor命令にブレークポイント break *0x400114 を設定し,xor命令の実行前後で値がどのように変化するか確認した. すると,このxor命令は,入力された文字列の7文字目から0文字目まで順に0x55をxorしていることがわかった.
さらに,すべての文字にxorを実行した後,ステップ実行していくと,cmp命令に到達することがわかった. そこで,cmp命令の直前にブレークポイント break *0x400110 を追加して,どのようなデータと照合しているかを調べた. すると, rax==0x63391a67251b1536 と,rsi番地から8バイトのデータを照合していることがわかった.
また,ここのcmpがnot equalだった場合は, 再度syscall命令の番地に到達し,そのときの引数は rax=0x3c(exit), rdi=1 で異常終了していることがわかった.
そこで,cmp命令がequalとなるような入力を求めることにした. raxのデータをリトルエンディアンで並べると, 36 15 1b 25 67 1a 39 63 であった. 入力された文字列の各バイトに0x55をXORした結果がこのバイト列になれば,cmp結果がequalになるはずである. XOR演算は,同じデータを再度XORすると元にもどる性質があるから,上記の各バイトに0x55をXORすれば元の文字列が得られる.
そこで,MacOSXに付属の電卓(プログラマ表示)を用いて,XORした結果を求めた. すると,その結果は “C@Np2Ol6” になった.
プログラムを再起動し,上記文字列を入力したところ, cmp結果はequalになり,その後syscall命令を rax=0x3c(exit), rdi=0 で呼び出して正常終了することがわかった.
これを元にプログラムの動作をまとめると, 以下のような流れであると推測できる.
したがって,このプログラムは入力が特定の値”c@Np2Ol6”であれば正常終了し,そうでなければ異常終了する,チェックプログラムであると推測できる.
プログラムを実行した環境は以下の通りである.
各入力に対して,出力はそれぞれ以下のようになった.
./a.out 0
OK: 1-0, 1-1, 1-2
./a.out 1
OK: 1-0, 1-1
NG: 1-2
./a.out 2
OK: 1-0, 1-1, 1-2
また,シグナルハンドラをシグナルごとに別にして実行した結果,OKと表示された際には,以下の例外が発生していることがわかった.
./a.out 0->SIGSEGV
./a.out 1->SIGSEGV
./a.out 2->SIGILL
したがって,QEMUのみが入力1に対してNGを返した.
ここで,check00-check02をobjdump -dした結果を以下に示す.
0000000100000db0 <_check00>:
100000db0:b8 68 58 4d 56 mov $0x564d5868,%eax
100000db5:66 b9 0a 00 mov $0xa,%cx
100000db9:66 ba 58 56 mov $0x5658,%dx
100000dbd:ed in (%dx),%eax
100000dbe:c3 retq
0000000100000dbf <_check01>:
100000dbf:f3 f3 f3 f3 f3 f3 f3 repz repz repz repz repz repz repz repz repz repz repz repz repz repz
100000dc6:f3 f3 f3 f3 f3 f3 f3
100000dcd:f3 c3 repz retq
0000000100000dcf <_check02>:
100000dcf:0f 3f (bad)
100000dd1:07 (bad)
100000dd2:0b c3 or %ebx,%eax
100000dd4:66 66 66 2e 0f 1f 84 data16 data16 nopw %cs:0x0(%rax,%rax,1)
100000ddb:00 00 00 00 00
check00について,読み込んでいるIOポートについて検索すると以下のサイトを発見した. http://pythxsh.blogspot.jp/2011/07/vmware.html https://sites.google.com/site/chitchatvmback/backdoor#cmd0ah これによると,IOポート0x5658は,VMwareにおいてバックドアポートとして用意されており,eax=0x564d5868, cx=0x0aとしてIOポート0x5658をINすると,VMwareでは例外は発生せず,EAXにバージョン番号が代入されるという機能をもっている. したがって,VMwareで実行した際には,NGと表示されるはずである.
check01について,ダンプを確認すると,15個のrepzプリフィックスとretq命令から構成されていることがわかる.Intel 64 and IA-32 Architectures Software Developer’s Manual Volume 2 2.1.1 Instruction Prefixesによると,repzプレフィックスはストリング命令等の特定の命令以外に付加すると,予期しない結果を生むと書かれている. また, https://books.google.co.jp/books?id=VwyZnnOSv1UC&pg=PA205&lpg=PA205&dq=qemu+repz&source=bl&ots=pApEMfmxyw&sig=-qCGEYaxPDzn8lpmlHlkeM7RQOE&hl=ja&sa=X&ved=0ahUKEwi8yuunxYDNAhWGl5QKHcrnD9cQ6AEIJDAB#v=onepage&q=qemu%20repz&f=false によると,QEMUは実行を高速化するため,jmp命令に相当するような命令に出会うまで一気に翻訳すると書かれている.また,QEMUはCPUの動作をソフトウエアでエミュレーションするため,実機の動作を完全には反映していない場合がある.そのため,QEMUでcheck01を実行した際は,例外が発生せずそのまま実行されてしまった(未定義動作のため仕様上は例外を起こさないでもよい)と考えられる.
check03については,ダンプにも示されているとおり,最初の3バイトが正しい命令ではない.したがって,無効命令例外が発生するはずである.実際,今回試した範囲では,すべての環境でSIGILLが発生しており,無効命令が検出され,実行が中断されたことがわかる.
仮想マシンでも物理PCと同じ振る舞いを実現するためには,まず物理PCの動作を詳細に把握する必要がある.仕様上は定義されていない入力に対しても,物理PCは何らかの動作をし,そして何らかの出力をするはずであるから,仕様書を読むだけでなく,実際の動作に従ってエミュレーターを実装する必要がある.
また,各VMに共通なバックドアを設けないことも必要かもしれない.VMであることを検出できてしまうようなバックドアを用意してしまうと,VM環境下では活動停止するウイルスなどを作成できてしまうため,セキュリティ的にも問題がある.一方で,バックドアを設けた方が利便性が向上する場合もあるため,仮想マシンの設定で,バックドアの有効/無効を切り替えられるようにすればよいのではないだろうか.