重力に縋るな

千種夜羽です

セキュリティ・キャンプのススメ的なsomething(応募課題晒し)

超久しぶりの投稿です。

ところで、

今年のセキュリティ・キャンプの募集が開始されましたー!!!

www.ipa.go.jp

いやー、めでたい(?)っすね〜

で、なんかセキュキャン2016卒業生のプロ各位が応募用紙晒したり、それをまとめたスプレッドシート(黒歴史収集・・・?)

が爆誕したりしているので、僕もなんか書いてみようかなー、と思いました。

このスプレッドシートに載っているようなプロ各位の記事を読むと、

セキュキャンの卒業生=任意のプロって感じがしますが、

僕は全然プロではないので、全然進捗無いので、プロではないので(大事なことなので2回言いました)、あんまり参考にはならないと思いますが、ノリで書きます。あまり参考にはなりませんが、まあそんなかんじでお願いします。

まずは、去年の僕が応募用紙に何書いたかを晒していきます(ウワアアアア)。

去年の問題は、今は https://www.ipa.go.jp/files/000053055.pdf にあるようです。では、初めの方から見ていってみましょう。

とりあえず目次です

■はじめにお読みください

※このページは、60分程度でセッションが切断され、入力内容が無効となります。そのため、回答内容は応募者ご自身が適宜保存しながら回答するよう、 くれぐれもご注意ください。 ※一時保存機能による回答内容の保存は48時間有効です。一時保存URLは、①有効期限を過ぎた場合、②再度一時保存をした場合(新しく別の一時保 存URLが発行され、古いURLは無効となります)、③回答内容を送信した場合に、無効となりますので、ご注意ください。

はい。これ超重要です。もしかしたら僕がこの記事で書いた一番重要な情報かもしれませんよ! セキュキャンの応募課題はクソ時間かかります。いや、プロだと一瞬で出来るのかもしれませんが、僕は無理です。というわけで、ちゃんと保存しましょう。僕は当時安定的に使えるPCが無かったのと、どこでも編集できるようにクラウドサービス上で作業しました。僕はセキュキャン後にgitを知ったのでアレですが、git管理とかしてもいいかもしれません。応募課題をgit管理、かっけえ。

あと、消えてしまうととてつもなく悲しくなるので、バックアップを定期的に取ってもいいかもしれません(?)。今年は一時保存機能も無いようですし。

※回答の締め切りは、【2016年5月30日 17:00】です。再提出を含め、締め切りまでに回答を送信してください。

あー、これも重要ですよ。〆切。どんなに時間をかけてでも1限死んででも応募課題には時間をかけるべき(かもしれない)、と言いたいところですが、そんなに頑張っても出せなかったらなんの意味もありません。全部水の泡です。超もったいないです。まあ僕が学校の課題でもやるべきことなんですが。

はい、これで僕が言えるアドバイスとかもう9割9分9厘終わりですね。あとの記事は残りカスみたいなもんです。黒歴史ですし。

というわけで後はダラダラと去年の僕が書いた応募用紙を晒していきますが、まあまさかこんなところを読む人なんていないだろうということで、黒歴史ぶちまけちゃいましょうか。

共通問題

では共通問題から。

共通問題.1

共通問題.1 (1)

あなたが今まで作ってきたものにはどのようなものがありますか? いくつでもいいので、ありったけ自慢してください。

来ましたよ、この自慢コーナー。イキリオタク案件です。僕はどうイキリオタクしたんだっけか・・・

・ロケット
 小学生の頃からペットボトルロケットを作っていて、地学部に入ってからは先輩とペットボトルロケットを設計、製作、シミュレーション、実験を行っていた。先輩が卒業し、僕が部長兼宇宙科学班班長になってからは、「モデルロケット」という、火薬を使用したロケットの設計、製作、シミュレーション、実験を行っている。
・ロケットシミュレーション
 上述のロケットの設計及び、実験結果のフィードバックを論理的かつ効率的に行うために、物理シミュレーションを行っている。ロケットの軌道は空気抵抗にかなり左右されるため、空気抵抗の計算がかなり重要となる。現在は近似式で計算を行っているが、PC教室のPCを全てLANで接続してスーパーコンピューターのようにして有限要素法で正確な軌道計算を行う試みを考えもした。
・流体シミュレーション
 「粒子法入門」を読んで、SPH法を用いてナビエ‐ストークス方程式を離散化して解いて、流体シミュレーションを行っている。また、シミュレーションだけやっていてもつまらないので、ParaviewやPov-Rayによる可視化も行っている。
・天体シミュレーション
 流体シミュレーションで学んだ粒子法シミュレーションの手法を用いた上で、多体重力シミュレーションをどのようにやればよいか考えてプログラムを作っている。この成果は今年の部誌、ホルストに掲載する予定。
・HELIOS
 「OS自作入門」を読んで自分で作り始めたOS。この開発コードネーム(ヘリウムの語源で太陽の意)を思いついたときは、かなり中二病に近かった。そして実際に中2だった。2回ほど「はりぼてOS」を作り、少しづつ自分で考えてコーディングをした。現在は勉強とか部活とか忙しくて中断している。ちなみにAPMシャットダウン成功一歩手前まで行った(シャットダウンだけする16bitOSを作った)。アルゴリズムまで思いついたもののコーディングせずにほったらかしているアイデアがたくさんある。

アアアアアなんじゃこりゃあああああああ(吐血)

とりあえず次に進みます。。。

共通問題.1 (2)

それをどのように作りましたか?ソフトウェアの場合にはどんな言語で作ったのか、どんなライブラリを使ったのかなども教えてください。

・ロケット
 ペットボトルロケットは、ペットボトル・ビニールテープ・ホチキスで製作。小型カメラを搭載して動画撮影を行ったり、パラボリックフライトの真似事をして疑似無重力実験を行ったりした。
 モデルロケットは、残念ながらまだモデルロケットライセンス4級を取得していないためA型エンジンしか使用することができないが、近いうちに取得する。今年の文化祭までにArduinoを用いた無線打ち上げ制御を行いたいと考えている。
・ロケットシミュレーション
 日本モデルロケット協会の本を参考に、最初はExcelでシミュレーションを行っていたが、計算が遅い・開くたびに計算する・あくまで表計算ソフトなので精度が期待できない、などの理由で、C/C++(MinGW)言語によるシミュレーションプログラム開発に移行した。後述する流体シミュレーションから多大な影響を受け、計算精度などの問題をまじめに考え始めた。現在は、モデルロケットのシミュレーションに対応するため、エンジンデータなどを調べている。
・流体シミュレーション
 丸善で見た「粒子法入門」に感銘を受たので、学校の図書館に購入させ(理工書は高い)、何度も読み返して(僕以外借りる人がいなかった)、実際にコンパイル・実行してみて、「大学院の物理って面白そう」と感じた。あまり僕が手を加えられた部分は少ないので、これは「自分が作ったもの」とはいいがたいが、これを参考に後述の天体シミュレーションを行っているのでお許しいただきたい。
・天体シミュレーション
 前述の粒子法シミュレーションを参考に、「あれ、国立天文台の4D2Uってスパコン要るだけでやってることはそんなに難しくないんじゃないか?」と自惚れて作り始めたもの。数値積分の面倒くささを再認識した。言語はC/C++(MinGW)。衝突判定が足りないので、天体同士が接近すると計算不安定が大変なことになる。
・HELIOS
 「OS自作入門」を読んで作り始めた名前中二病OS。今は開発を中断している。言語はC/C++(GO)。「WinAPIが面倒なら自分でOSを作ればいいのに」という僕の迷言を生み出した。動的リンクライブラリとか実装したい。APMシャットダウンがうまくいかないまま中断している。

あうあう・・・

まだ自覚している節があるのでマシといえばマシですが、なんだこの厨2はってかんじですね。嘘にならない範囲でギリギリのことを書いているというか。これだけ見ると強そう。まだ終わってないことも多いし。

でも、この問題は、応募してくる人の、「どのくらいイキリオタクしてくるか」、ようするにやる気を見ていると個人的に思っている(ホントか?)ので、応募する人はバンバンイキリオタクするといいと思います(さあ黒歴史を作るんだ)

「俺に自慢できるものなんてねえよ」なんて人もいると思います。僕もそうでした。でもきっと大丈夫です。セキュリティ・キャンプは必ずしもプロを求めているわけではありません。もしそうなら僕は確実に落ちています。CTFの大会に出たり、CVEを持ってたり、言語作ったりしていたわけではありません。

こんな僕でもキャンプに行けた理由は、多分、セキュリティ・キャンプという事業が、「プロをさらにプロにする」というイベントではなく、人材発掘、ということに重点を置いているからだと思います。

「キャンプで成長するというよりも、あくまでキャンプはきっかけにしてほしい。」

こんなことをキャンプの偉い人が言ってました。これは本当にその通りだと思います。いやもちろん、キャンプに来た時点でプロでプロプロしてるプロもいたのですが、僕はキャンプで無双することなんて全然無理でした。今でこそ受けた講義の内容はなんとなく分かっている(はず)ですが、キャンプ中には全然イミワカランなんだこれ、みたいな講義もありました。

しかし、僕はセキュリティ・キャンプに行かなければ、「OS自作入門」の著者のKさんに会うことも無かったでしょうし、参加者のすごい人たちに会うことも無かったでしょうし、OSCにも行くことも無かったでしょうし、サイボウズ・ラボユースに応募してみようなんて微塵も思わなかったでしょう。最悪、プログラミングをやめていたかもしれません。

なにより、「こんなすごいことをしてる人たちがいるのか」ということを知ることができたのが、一番大きいと思っています。井の中の蛙が大海を知った訳です。海じゃなくて湖、ひょっとしたら池くらいかもしれませんが。

・・・あれ?まだ1つしか問題終わってないのに、4.7k文字・・・? なんかいいかんじのこと言ってますし、もう晒すの終わりで良いのでわ? え、ダメですか。仕方ないですね・・・

共通問題.2

共通問題.2 (1)

あなたが経験した中で印象に残っている技術的な壁はなんでしょうか?(例えば、C言語プログラムを複数ファイルに分割する方法)

・コンパイラがうまく動かなかった
 部のPCで、学習用C言語開発環境(ようするにtcc)がうまく動かなかった。Hello, Worldぐらいは動くのだが、少し行数が増えただけで全く違うところでコンパイルエラーが発生し、コンパイルできてもおかしな挙動をした(コードがおかしいわけではなかった)。
 仕方ないので、Visual Studio 2008をインストールしたが、何もコンパイルできなかった。
・クラスの概念が理解できなかった
 軽くCをやったあと、C++をやろうとしたが、オブジェクト指向が理解できなかった

今考えるともう少し原因究明しろよとか、何がどう理解出来なかったのか書こうよってかんじですが、当時は本当にこれに困ってたんですよね・・・

共通問題.2 (2)

また、その壁を乗り越えるために取った解決法を具体的に教えてください。(例えば、知人に勧められた「○○」という書籍を読んだ)

・コンパイラがうまく動かなかった
 なにを試してもうまくいかなかったが、MinGWとGOだけまともに動いたので、以後gccを多用し、IDEという文明の利器から離れていくことになる(eclipseは展開不良のため使えなかった)。そもそもPCに問題がある可能性がかなり大きい。
・クラスの概念が理解できなかった
 アセンブラからやり直し、構造化プログラミングの意義を理解し、その延長線上としてのオブジェクト指向を理解した

_人人人人人人人人人人人人人人人_ > オブジェクト指向を理解した <  ̄YYYYYYYYYYYYYY

ギャアアアアアアなんだこいつはアアアアアアア(黒歴史(吐血))

オブジェクト指向を理解とか、出来てないです。それこそJavaとかほとんどやったことないですし。

共通問題.2 (3)

その壁を今経験しているであろう初心者にアドバイスをするとしたら、あなたはどんなアドバイスをしますか?

・コンパイラがうまく動かなかった
 IDEなんて甘えです。古いPCだとうまく動かないし、遅いし、はたまたインストールできないことだってあります。別に、IDEを使うことは悪いとは思いません。生産性も上がるでしょう。なんだかんだで僕も今はたまにNetBeansとか使います。でも、IDEの恩恵を普通だと思ってしまったり、IDE特有の癖(特にVC++)とかが分からないと、IDEが使えないときや、何らかの事情で(例えば、OS作るとか)IDEを使えないときに、絶望しか味わえません。というわけで、コマンドラインコンパイルとかやるようにしましょう。
・クラスの概念が理解できなかった
 アセンブラからやり直しましょう。オブジェクト指向は楽ですが、それしかやらないとなぜそれが要求されたのかも、実際にはどのように動いているのかも理解できません。というわけで、低層からやりましょう。

いや、あのですね、こんなこと書いちゃいましたが、IDEがダメとか甘えとかは別に思ってません(言い訳)。この前Visual Studio 2017を使ってみたんですが、ヤバかったです。あれ、コード書かないで色々出来てやべえ。

ただですね、ちょーっと言わせてもらうと、僕はINF回Visual StudioやらEclipseやらの有名なIDEのインストールにコケていた(大体スペック不足)ので、IDEには未だにトラウマが・・・

当時のプログラミング環境は、gcc(MinGW or GO) + TeraPad でしたね。今はgcc(build-essential) + vimですね。vimはいいぞ。

えーっと、次はクラス云々の話ですか。これもちょっと黒歴史ですね。

まあでも、アセンブラやるのは悪いことではないと思います。そこらへんで売ってるC言語の入門書とか読んでも、僕は無限にポインタのことが分かりませんでしたし。

というわけで「OS自作入門」はいいぞ(N回目)。 https://www.amazon.co.jp/30%E6%97%A5%E3%81%A7%E3%81%A7%E3%81%8D%E3%82%8B-OS%E8%87%AA%E4%BD%9C%E5%85%A5%E9%96%80-%E5%B7%9D%E5%90%88-%E7%A7%80%E5%AE%9F/dp/4839919844www.amazon.co.jp

みなさんもOS自作してアセンブラC言語で遊びましょう!(低レイヤー沼)

次は講義の希望です。

共通問題.3

共通問題.3 (1)

あなたが今年のセキュリティ・キャンプで受講したいと思っている講義は何ですか?(複数可) そこで、どのようなことを学びたいですか?なぜそれを学びたいのですか?

■1-B BareMetalで遊ぶ Raspberry Pi 入門編
ラズパイを使ってみたかったので
■3-B フィジカル・リバースエンジニアリング入門
2歳ぐらいから分解大好きなので
■5-B USBメモリからブートしてみよう
HELIOSのUSBブートで失敗したので
■2-C 人工知能とセキュリティ
ディープラーニングの実装に失敗したので
■3・4・5E システムに新機能を追加したときのセキュリティを考えよう
川合さんの話はとても好きなので。また、あえて今「C言語に機能を追加する」というところに惹かれたから。

これ、「なんでセキュリティ・キャンプに行きたいのか」ってのに直結する質問なので、もう少し色々書くべきでしたね・・・

僕の場合、合格通知の後の講義の選択希望で、割と希望を変えましたし。

それこそ今年度の場合は、希望する講義で応募課題が変わってくるくらいですし、どの講義を希望するかはちゃんと考えて応募したほうがいいと思います。

共通問題.3 (2)

あなたがセキュリティ・キャンプでやりたいことは何ですか?身につけたいものは何ですか?(複数可) 自由に答えてください。

・自分と同じようなことに興味を持つ人と出会いたい(基本ぼっち)
・今自分が作っているものの参考にしたい
・アイデアの発想の仕方を学びたい
・セキュリティについて詳しく学びたい

あー・・・これも、もう少し色々書いておくべきでした。 なにを言いたいのかはなんとなくは分かるのですが、情報がとても少ないですね・・・

ただ、応募した時は、正直自分が何をやりたいのか、キャンプで何を学べるのかとかがイマイチ分かっていなかったので、ある意味しょうがなかったかもしれません。

まあ自分が何をやりたいのか、なんてことは今でもあまり分かってないのですが、キャンプに行ったことで割と考えさせられたところはありますね。

選択問題

以降は選択問題です。

どの問題を選択するかはかなり迷ったのですが、知識が圧倒的に足りなかったので、どうにかして答えられそうなものを選びました。

ただ、もう少しよく調べたり、考えたりして書けばよかったかなあと思っています。

選択問題.3

RAMは主記憶装置、HDDやSSDなどは補助記憶装置と呼ばれます。一般にCPUは主記憶装置上のプログラムしか実行できません。ではなぜ、私たちは普段から補助記憶装 置に書き込んだプログラムを実行できているのでしょうか?パソコンの電源を入れてからのストーリーを考えてみてください。

PCの電源を入れると、ROMなどに書き込まれたBIOSが自動的に起動し、BIOSがOSを呼び出すための初期プログラムローダ(IPL)が書かれたブートセクタ(MBR)をHDD、SSD、CDなどの補助記憶装置から事前に登録された順番で探していく。そして、IPLを発見すると、IPLを実行する。IPLは1セクタ分、つまり512バイト分しかないので、OSを起動するためのプログラムを主記憶装置に転送するプログラムが書かれている(はりぼてOSではサイズが小さく、HDD読み込みができないのでOSとそのほかのファイルを全て転送する)。こうして主記憶に転送されたプログラムが、さらに必要なデータを補助記憶から転送するなどしてOSが起動する準備を行う。こうして、ようやくOSが起動する準備が完了し、プログラムがOS本体を実行する(正確にはジャンプする)。
OSが起動した後は、OSがおそらくout命令で補助記憶装置の制御チップに色々と命令を送って補助記憶から主記憶にデータを転送させている。

はい。ブートプロセスを考えろってことですね。僕がプログラミングを始めたきっかけが「OS自作入門」なので、この問題(と次の問題)を見つけたときは、「アーッこれあの本で見た!」って思いましたね。というか、これらの問題があったから応募してみた、という節もあるかもしれません。

でも、これも今見ると理解がイマイチでしたね・・・

書いた時ははりぼてOSのことしか知らなかったのでこうなったのですが、Linuxなどのブートプロセスも調べたほうが良かったと思います(GRUBとか)。

選択問題.5

PCなどに搭載されているOSは「汎用OS」と呼ばれますが、それに対して、家電やAV機器などの「組込みシステム」に搭載されているOSは「組込みOS」と呼ばれます。 組込みOSと汎用OSの違い、「OSが無い」や「ベアメタル」という環境、そもそもOSとは何なのか?など、あなた自身はどう考えているのかを、 あなた自身の言葉で自由に説明してください。(「正しい答え」を聞いているわけではありません。あなた自身の考えを教えてください)

正直、「OSはこうあるべきだ」というような明確な定義は無意味だし、必要ないと思う。「カーネルがどうこう」なんて話はこの際無視する。僕が考えるOSとは、本当の意味での「オペレーティングシステム」、つまり、なんらかの電子機器を直接制御するシステムのことだ。それは、家電製品を制御しているかもしれないし、人工衛星を制御しているかもしれないし、はたまたPCを制御して、その上でアプリケーションを実行しているかもしれない。タブレットやPCを制御し、そのうえで様々なアプリケーションを実行して多くのエンドユーザーがいるのであれば、それは「汎用」OSと呼ばれるのだろうし、人工衛星や、ロケットや、家電製品など、特定作業に特化した電子機器を制御するため、必要な機能のみを必要なICなどに「組み込まれた」OSは、「組み込みOS」と呼ばれるのだと思う。

この考え方は、割と今も変わってないですね。でも、自作OSもくもく会とかに行くと、人によってOSの定義、とでもいうものが色々あるので、「OSってなんだろう」みたいな問いは、これからも考えていきたいですね。

選択問題.6

IDとパスワードを入力してユーザの認証を行うWebアプリがあります。あなたがこのアプリに対してセキュリティテストを行う場合、まず、どのようなテストをします か? なぜそのテストを選択したのか、その背景や技術的根拠と共に記載してください。アプリの内部で使われている技術やシステム構成に、前提を置いても構いません。

認証される組み合わせがくるまで延々とIDとパスワードを生成し、入力してくる辞書攻撃に対応できるか判断するため、IDとパスワードを延々と生成し入力するプログラムを作り、このアプリに対して攻撃を行う。そして、これにクリアした後は、「同じIPアドレスから大量に認証申請が来ている」ということだけをもとにして前回の攻撃をクリアしたのではないか、という観点から、常時IPを変えつつ前回と同じ攻撃を行う。

う、う〜ん、これだけかよってかんじですね・・・(小並感)

Webアプリとかなにも分かんなかったんです!許してくださいなんでもしますから(なんでもするとは言っていない)

選択問題.9

マイナンバーカードの配布システムを構築・運用することになりました。あなたなら何に気をつけてどんなサービスを構築しますか? マイナンバーカードの仕様は現実通りのICカードとします。 ※ 注意: マイナンバーでは無くマイナンバーカードです。

・ICカードは少し離れていても情報を読み取ることが可能なので、それを防ぐために、磁気などを遮断する厳重な金庫に入れて輸送する
・配布要員が不正を行わないようにするため、作業員は全員ICタグなどで出入を管理し、配達用の車両の現在位置を追尾する
・苦情に迅速に対応するため事前に苦情対策について行政と協議する
・配達員にタブレットを配布し、配達時に本人確認を行い、タブレットで配達完了を管理センターに知らせるようにする

マイナ◯バーカードwww

なんか、誤配達とかあって大変だったらしいですね。結構当時としてはタイムリーな話題でしたね。 一応そこも踏まえて書いたような気がしていたのですが、これだと全然伝わらないですね。しかも箇条書き・・・

終わりに

はい。これで僕の黒歴史晒しは終わりです。書いててちょっとつらかった・・・

他の過去の参加者のみなさんの応募用紙と比べると、あまりにもクソということが分かっていただけたと思います。

もし、「キャンプ勢とか強そう・・・自分には無理だ・・・」と思って応募を諦めようとしている人が「こんなのでも受かるのか!」と思っていただけたなら幸いです。

何回でも言いますが、セキュリティ・キャンプは応募に際して技術力を要求していません。それはこの記事を見れば明白です。

しかし、だからといって応募課題をテキトーにやっていいわけでは決してありません。こんな応募課題でも当時の僕はかなり時間をかけて書きましたし(時間かけてこれかよとか言わないであげて・・・死んじゃう・・・)。

特に、今年の応募課題は去年のものと比較しても時間をかけて試行錯誤することを要求している(気がする)ので、今年応募する人は頑張ってほしいですね。

今年のセキュリティ・キャンプはとてもパワーアップしていてすごく面白そうなので、今年応募する人、マジで応援してます!!!

僕もチューターやりたいので頑張ります!(あまりにも面白そうなので行きたくなった)

CTF for ビギナーズ に当選した

@hiwwさんのお誘いでTeam Harekazeの一員になったものの、stringsぐらいしか出来ない情弱なので申し訳ない気持ちになっていた。ハリネズミ本も終わってないし。

マトモにやれたのはsometa1ぐらいじゃなかろうか(といっても解けなかったのだけれど)。

そんなこんなでCTF力のNASAを感じていたので、ctf4bにダメ元で申し込んでみた。抽選になるほどの人気イベントなので(そもそも、抽選で参加出来なかった人がたくさんいたから今回追加でやることになったらしい)、まあどうせ受からんだろと高を括っていたら、
f:id:sksat:20170121232935j:image

当選してしまった(びっくり)。

ということで、CTFも頑張りたいと思います。

OpenGLでウィンドウのクローズにちょっとつまづいた話

自作エミュレータで自作OSを動かしてみようとした話 - sksat’s diary で書いたように、OpenGLを使ってQEMUBochsのようなx86エミュレータっぽいものを作っているのですが、ウィンドウのクローズに関してちょっとつまづいたのでメモしておきます。

何が起こったのか

今僕が作っているエミュレータでは、画面表示のエミュレーションにOpenGL(freeglut)を使っているのですが、機械語の実行が遅れると困るので、OpenGLの初期設定やメッセージループは別のスレッドで実行しています。ちょっとGUIスレッドの部分のソースコードを一部抜粋すると、

void GUI::ThreadProc(){
    int argc = 1;
    char *argv = new char[1];
    glutInit(&argc, &argv);   // fake command-line args
    glutInitDisplayMode(GLUT_RGBA);
    glutInitWindowSize(scrnx, scrny);

    hMainWin = glutCreateWindow("display");

    while(msgflg){  //message loop
        glutMainLoopEvent();
        this->display();
    }

    glutDestroyWindow(hMainWin);

}

GUI::~GUI(){
    msgflg = false;
    hThread->detach();
    delete hThread;
}

こんなかんじになります。GUIというクラスにGUIの制御を任せていて、GUIクラスのデストラクタが呼ばれるとフラグがfalseになってメッセージループが停止し、glutDestroyWindow()が呼ばれてウィンドウが閉じるようになっています。

そして、このGUIクラスを制御しているmain.cppは、

Emulator  *emu;
GUI       *gui;
Display   *disp;

int main(int argc, char **argv){
    emu = new Emulator();
    disp = new Display(emu->memory + VRAM_ADDR);
    gui = new GUI(disp);

    /* 中略 */

    gui->OpenWindow();    //これがスレッドを起動させる

    while(true){
        /* 中略(機械語の実行) */
    }

    delete emu;
    delete gui;
    delete disp;
}

というようになっています。つまり、エミュレータが途中で停止すれば自動的にウィンドウが閉じられるというわけです。 では、EAXに0xffを代入するだけの簡単なプログラム(MOV EAX,0xff)をこのエミュレータで実行してみましょう。

f:id:sksat:20161222212959g:plain

一瞬で終了しました。それもそのはず、メインスレッドで、MOV EAX,0xffを実行したすぐあと、EIPの指すメモリに実装されていない命令がある(正確には、なにも命令を配置していないので0x00があり、また0x00の機械語を実装していない)ので、エラーを出してメインスレッドの機械語実行ループが終了、delete gui;によりGUIクラスのデストラクタ内で、glutDestroyWindow()が呼ばれ、ウィンドウが目に見える間もなく閉じられてしまっているからです。

これだとせっかく実装したウィンドウが見えないので、ウィンドウが見えるように、実行させる機械語JMP 0x7c00を追加してループさせてみましょう(一応x86エミュレータなので、プログラムは0x7c00番地にロードする仕様になっている)。

f:id:sksat:20161223220005g:plain

うん、ちゃんと画面描画できてますね〜、って、ん??? あれ、これ、おかしくないですか?ウィンドウ閉じたら機械語の実行も止まるなんてコード、僕書いた覚えありませんよ?
まあ、実際のところ☓ボタンを押したらエミュレータには終了してもらいたいですが、それでも、意図しない動作というのは嫌なものです。
というか、最後にレジスタの値を表示するのができていないので、これはかなり重大なバグと言っていいでしょう。メモリもちゃんと解放できてるのか心配です。

うーん、何がダメなんでしょうか。 ウィンドウの細かいことは全部OpenGlに丸投げしてしまっていますが、☓ボタンが押された時にどのような処理がされているのかちょっと追ってみましょう。

    while(msgflg){  //message loop
        glutMainLoopEvent();
        this->display();
    }

    glutDestroyWindow(hMainWin);

メッセージループに入っている間はウィンドウの処理は全部glutMainLoopEvent()でした... つまり、glutMainLoopEvent()かその中で呼ばれている関数が☓ボタンの処理を行っているはずです。 しかし、考えてみると、☓ボタンが押されたらメッセージループは終了するべきですが、glutMainLoopEvent()``内からメッセージループを終了させることができないようになっています。 もしかしたら、glutMainLoop()ではglutMainLoopEvent()の返り値を見てメッセージループを終了させているのかもしれません。 ということで、glutMainLoopEvent()```の返り値を見てみます。

f:id:sksat:20161225123329p:plain

とりあえずintでいいかなと思ったんですが、voidだったんですね...コンパイラに怒られてしまいました。 glutMainLoopEvent()の返り値が無いとすると、次に考えられるのは、☓ボタンが押されるというイベントを処理するコールバック関数を登録する関数があるのではないか、ということです。

ではどんな関数があるのか、たまたますぐ近くにlibfreeglut.aが転がっていたので(Windows用な気もするけれど)、objdumpで調べてみます。

objdump -D libfreeglut.a

「お〜、こんな関数あったんだ〜」と思いながらスクロールしていくと、おや? f:id:sksat:20161225124417p:plain なんかそれっぽい関数名が出てきました。コールバック関数は「glut***Func」という名前なので、これは怪しいです。

ということで、さっそく関数を登録してみましょう。

void close(){
    cout<<"close"<<endl;
}

void GUI::ThreadProc(){
    /* 中略 */
    glutCloseFunc(close);
    while(msgflg){
        glutMainLoopEvent();
    }
    glutDestroyWindow(hMainWin);
}

f:id:sksat:20161225145024p:plain

...どこにいるんだよclose...
どこかにいるかもしれませんので見てみます。./vm test02.bin | grep close

f:id:sksat:20161225145325p:plain

うん、ちゃんといました。ウィンドウが閉じるときにちゃんと呼ばれているようです。しかし、結局はプロセスが終了しています。closeイベントが発生してglutCloseFuncで登録した関数が呼ばれた後にexit()かなにかを呼んでいるのでしょうか。

exit()が呼ばれているかどうかが気になったので、ちょっと`ltrace ./vm test02.binとかやってみました。 f:id:sksat:20161225150032p:plain わからん...ostreamとか書いてあるのは標準出力関係か...マルチスレッドだしな...(しかしシングルスレッドで試してみてもよく分からなかった..力不足...)   

よく分からなかったのですが、せっかくウィンドウのクローズ時にcloseが呼ばれるようになったので、close内でglutDestroyWindow()してみます。

int g_msgflg, g_hMainWin;
void close(){
    cout<<"close"<<endl;
    glutDestroyWindow(g_hMainWin);
    g_msgflg = false;
}

//ThreadProcの一部
void GUI::ThreadProc(){
    g_msgflg = true;
    while(g_msgflg){
        glutMainLoopEvent();
        this->display();
    }
    //glutDestroyWindw(hMainWin);
}

ハードコーディングは気にしないでください...
では、実行してみます。

f:id:sksat:20161225152644p:plain

正常終了どころかコアダンプ...むむう...

後日談、というか、今回のオチ

まだ調べ途中で結局できていないのですが、ここまで読んでくれた人ならお分かりでしょう。
「公式ドキュメント見ろよ!!! というか、関数名調べるだけならヘッダ見ればいいでしょ!!!」
はい、全くもってその通りです。CTFの本読んだからって調子に乗りました。

自作エミュレータで自作OSを動かしてみようとした話

これは自作OS Advent Calendar 2016の22日目の記事です。

『30日でできる! OS自作入門』という(このアドベントカレンダー5,6,9,15日目の記事のKさん著)本があります。 この本はhikaliumさんも書かれていましたが、まさに「OS自作における聖書」といえるような本で、例によって今回やったこともこの本が発端となっています。

実際何をやったのかというと、タイトルの通り「自作OSを自作エミュレータで動かしてみたい」と思ったので色々やってみた、ということです。

エミュレータと自作OS

突然ですが、「ソフトウェアを開発する」ということに着目した時、一般的なアプリケーションの開発とOSの開発の一番の違いはなんでしょうか? 僕は、「デバッグに仮想環境を使うか使わないか」ではないかと思います。一般的なアプリケーション開発ではデバッグしようと思ったらとりあえず実行してみればいいですが、OSだとそうもいきません。なぜなら、OSを作って動作確認をするとき、いちいちCDやDVDに焼いたり、ハードディスクを犠牲にするのは面倒だからです(また、ファイルシステムを作っている時などに安易に実機で実行してしまうと重要なファイルを壊してしまうかもしれません)。そのため、OSを開発するときにはよくエミュレータ(仮想環境)を使います。QEMUBochsなどですね。

しかし、ここで一つ疑問が生じます。一体全体この「エミュレータ」というプログラムは、「どうやってコンピュータをエミュレーションしているのか?」ということです。OSというとかなり低レイヤーな分野ですが、エミュレータを作ろうと思ったらそれより下層のハードウェアなどの動作を再現しなければなりません。ということで、エミュレータがどのような仕組みで動いているのかが気になりました。幸いにしてQEMUオープンソースですので、思い立ったが吉日、QEMUのソースを見てみましょう。

まずはGitHubからソースコードを取ってきます。とりあえずどんなファイルがあるのか見てみましょう。

$ git clone https://github.com/qemu/qemu.git
$ cd qemu
$ ls

f:id:sksat:20161220221252p:plain

ごめんなさい、ファイルがたくさんありすぎて何を見ればいいのかわかりましぇん。。。 ということで僕のエミュレータへの思考はかなり断絶していました(QEMU開発者の方、すみません・・・)。

しかし、今年の夏にあったセキュリティキャンプ全国大会参加前に、色々な講義の事前課題を眺めていたら、「USBメモリからブートしてみよう」というなかなかそそられる講義を受ける人には「自作エミュレータで学ぶ x86アーキテクチャ」(このアドベントカレンダー1日目,8日目,18日目のuchanさん著)なる本が教材として与えられるとのこと。 なんだそのとても面白そうな本は・・・ 残念ながら、僕はその講義の時間帯は別の講義を取っていたのでこの本を貰うことはありませんでしたが、とても気になっていたので、学校の文化祭も終わって落ち着いた10月に買いました。

「自作エミュレータで学ぶ x86アーキテクチャ」のエミュレータ

僕なんかが書くより本を読んでサポートページからtolset_p86をダウンロードしてソースコードを見たほうが速いとは思いますが、本のエミュレータがどのような仕組みなのか簡単に説明すると、

  • レジスタに対応した変数を用意する(構造体にしてまとめてある)
  • 仮想マシン用のメモリを用意する(とりあえず1MB)
  • 外部から実行するバイナリファイルを読み込んで、用意したメモリの0x7c00番地にコピーする(512バイト)
  • エミュレータのEIPに0x7c00を代入
  • EIPがメモリをあふれない範囲でループして、EIPの番地の機械語を逐次実行していく
typedef struct {
    uint32_t registers[REGISTERS_COUNT];    // 汎用レジスタ
    uint32_t eflags;                        // EFLAGSレジスタ(キャリーフラグなど)
    uint8_t* memory;                        // メモリのアドレス
    uint32_t eip;                           // EIPレジスタ
} Emulator;

int main(int argc, char **argv){
    /* 中略(初期化処理)*/
    while(emu->eip < MEMORY_SIZE){
        uint8_t code = get_code8(emu, 0);
        /* 中略(ログ出力したり、実装してない命令がきたらbreakしたり)*/
        instructions[code](emu);    // 命令の実行
        if(emu->eip == 0){
               break;
        }
    }
    /* 中略(終了処理)*/
    return 0;
}

instructionsというのは関数ポインタの配列で、なるほど頭いいなあと思ったのですが、ここに各機械語に対応した関数のアドレスを入れています。

で、結局なにをやったのか

はい。実はここからが本題です。 先ほど紹介したエミュレータプログラムを見て、僕は「おお!これにどんどん機能追加していけばQEMUBochsみたいなことができるのでは?」と思いました。 では、その目標のために僕がどのようなことをしたのかを書いていきたいと思います。

まずは、すでにあるプログラムをC++で書き直しました。なんでそんなことをしたのかというと、一から書き直すことでちゃんとプログラムを理解したかったのと、あとはクラスにまとめてみたかったからです(ここは趣味ですね...)。

次に、OSを起動するためにはどのような機能をエミュレーションする必要があるかを考えました。とは言っても、僕がある程度中身を知っているOSははりぼてOSぐらいしかないので、はりぼてOSのエミュレーションに必要な機能を考えてみました。

  • 起動時は16bitモードで、あとから32bitモードに遷移できる
  • リアルモードとプロテクトモード
  • セグメンテーション
  • 割り込み処理
  • 画面表示
  • バイスが使える

他にもあるかもしれませんが、簡単にまとめるとこんなかんじでしょうか。 この中で、デバイスについては本でIN,OUTが実装されているので、各ポートに対応した外部装置を実装していけばいいですかね。 リアルモード・プロテクトモードはどう違うのかイマイチよく分かっていないので今はパスです。 セグメンテーションや割り込みも難しそうなのでとりあえず後回しですね。

ということで、色々良くわからないところは飛ばして(いやIntelの資料とか見ろよというかんじですが)、画面表示をやってみたいと思いました。

ウィンドウを出す

画面表示というと、まずはウィンドウが無いとどうしようもありません。ウィンドウを作りましょう。
ウィンドウを作っていきたいのですが、去年ぐらいの僕がこんなことを考えると、Win32 APIを叩き初めてしまいます。まあそれでもできなくはないですが、最近Windowsマシンを学校の情報の授業以外で起動すらしていないので、ちょっとダメです。ということで、描画ライブラリを使いましょう。
さて、描画ライブラリを選ぶことになったわけですが、残念ながら僕はよく使われてる描画ライブラリがあんまり好きじゃなくてWin32 APIに走ったという経緯があるので、よく使われる描画ライブラリをまともに触ったことがありません。
じゃあどうするんだというと、流石に汎用性の高い描画ライブラリを作っていたら日が何回も暮れてしまうので、OpenGL、正確にはfreeglutを使うことにしました。
なんでOpenGLなのかというと、ほんのちょっとだけ使ったことがあるのと、設計がかなり汎用性を重視してるように思えたからです。
あとは、開発は基本的にUbuntuですが、Windowsでも使えたほうがいいだろうということでマルチプラットフォーム対応なものとして選びました。 なんでglutじゃなくてfreeglutなのかというのは、実はやってる途中に色々あって変えたので今回は割愛します。

また、ウィンドウ、というかGUIを作るためにはメッセージループを行わなければいけません。しかし、エミュレーションをしながらメッセージを処理するのはソースコードもぐちゃぐちゃになりますし僕もよくわからなくなります。そこで、新しくGUIのメッセージループ用にスレッドを作ることにしました。そして、スレッドやウィンドウの処理をGUIというクラスにまとめました。

class GUI {
private:
    std::thread *hThread;
    bool msgflg;            // メッセージループの制御フラグ
    int scrnx, scrny;       // 描画するX,Y方向サイズ
    void ThreadProc();      // メッセージループスレッド
    void display();         // 描画処理
public:
    GUI();
    ~GUI();
    void OpenWindow();
}

しかし、ここで問題が発生しました。OpenGLを使ったプログラムというと、

void display(void);

int main(int argc, char **argv){
    glutInit(&argc, argv);
    glutCreateWindow("title");
    glutDisplayFunc(display);
    glutMainLoop();               // 帰ってこない
}

とかやるのが普通なんです。でもこれは困ります。glutMainLoop()は決して帰ってこないので、メインスレッドの方で、「エミュレーションが終わったからウィンドウも閉じたい!」と思っても、何かのフラグを変えてglutMainLoop()から脱出する、というようなことはできません(後から考えてみればできなくはなかったけれどいずれにしろきれいに書けない)。じゃあどうするんだ、ということでglutMainLoopについて調べていたらちょうどいい関数を見つけました。glutMainLoopEvent()という関数です。これを

while(true){
    glutMainLoopEvent();
}

というようにループに入れてやればglutMainLoop()の代わりになります。恐らく、Win32 APIで言うところのGetMessage()とTlanslateMessage()を合わせたようなものでしょう。

画面表示の仕組み

ようやく画面表示を実装していく準備が整いました。それでは、画面表示がどのように行われているかを『30日でできる! OS自作入門』で復習してみました。

[VRAM]に0xa0000を入れているのですが、PCの世界でVRAMというのはビデオラムのことで「video RAM」と書き、画面用のメモリのことです。このメモリは、もちろんデータを記憶することがいつも通りできます。
しかしVRAMは普通のメモリ以上の存在で、それぞれの番地が画面上の画素に対応していて、これを利用することで画面に絵を出すことができるのです。

つまり、VRAMにあるデータを見てその通りにウィンドウに描画していけばどうにかなりそうですね。

ではまず、描画関数であるGUI::displayを整備します。

void GUI::display(){
    glClearColor(0.0, 0.0, 0.0, 0.0);    // 背景色を黒に設定
    glClear(GL_COLOR_BUFFER_BIT);
    glRasterPos2f(-1, 1);                // ラスター座標変更
    glDrawPixels(scrnx, scrny, GL_RGB, GL_UNSIGNED_BYTE, img);    // unsigned char配列の中身を描画
    glFlush();    // 描画を反映させる
}

表示するRGBデータを保存するunsigned char配列であるimgをGUIクラスに加えて、glDrawPixels()という関数で描画するようにしました。 これでimgにいいかんじにRGBデータを書き込んでやれば、このようにちゃんと表示されます。

f:id:sksat:20161221145414p:plain

これでもう画面表示のエミュレーションができたような気持ちになってしまいますが、実はそうではありません。
glDrawPixels()で指定しているアドレスはimgであって(Emulator.memory + VRAM_ADDR)ではないのです。

何故VRAMを直接glDrawPixels()に渡してはいけないのかというと、実はVRAMに保存してあるのはRGBデータではなく、0x00から0xffまでの色番号だからです。
これではRGBで色を指定できないように思えますが、実はRGBデータはパレットという外部装置に、「どの色番号をどのRGBと対応させるか」ということをあらかじめ設定しておくという仕様になっているので、VRAMには色番号を書くだけで画面描画ができるようになっているのです。おそらく、画面描画を高速に行えるように、このように設計されているのでしょう。

ということで、このエミュレータでもパレットにRGBを設定して、VRAMを読んでその色番号に登録されているRGBで描画するようにしたいと思います。
パレットの作り方ですが、ようは0xff個のRGBデータがあればいいので、unsigned char palette[0xff * 3];でいいでしょう。 そして、これは外部装置なのでin/outでpaletteのポート(0x03c7, 0x03c8, 0x0xc9)が来たときに設定とか読み出しができればいいわけです。
今回はin/outについていじる気はないのでやりませんが、外部装置はDeviceクラスを継承して作るようにしたので、Displayクラスを作ってその中にpaletteを置いています。

class Display : public Device {
private:
    unsigned char palett[oxff * 3];
    uint8_t *vram;
    unsigned char *img;
    int scrnx, scrny;

    void init_palette();
public:
    Display(uint8_t *vram);
    ~Display();
    unsigned char* Draw();
};

今回はin/outについていじる気は無いので、init_palette()でパレットの初期設定をしてしまいます。

void Display::init_palette(){
    static unsigned char table_rgb[16 * 3] = {
        0x00, 0x00, 0x00,
        0xff, 0x00, 0x00,
        0x00, 0xff, 0x00,
        0xff, 0xff, 0x00,
        0x00, 0x00, 0xff,
        0xff, 0x00, 0xff,
        0x00, 0xff, 0xff,
        0xff, 0xff, 0xff,
        0xc6, 0xc6, 0xc6,
        0x84, 0x00, 0x00,
        0x00, 0x84, 0x00,
        0x84, 0x84, 0x00,
        0x00, 0x00, 0x84,
        0x84, 0x00, 0x84,
        0x00, 0x84, 0x84,
        0x84, 0x84, 0x84
    };
    
    for(int i=0; i<(16*3); i++){
        palette[i]  = table_rgb[i];
    }
}

やっていることは「はりぼてOS」の画面表示初期化関数であるinit_palette()とほとんど同じです。 設定するRGBのデータをそのままpaletteにコピーしています。

パレットができたので、次は画面描画をパレットに対応させます。ここまでできているので、あとはそんなに難しくありません。

unsigned char* Display::Draw(){  
    for(int x=0;x<scrnx;x++){
        for(int y=0;y<scrny;y++){
            int i=(y*scrnx + x)*3;
            char n = vram[y*scrnx + x]; //当該座標の色番号
            img[i]  = palette[n*3]; //色番号に対応したRGB
            img[i+1]= palette[n*3+1];   //green
            img[i+2]= palette[n*3+2];   //blue
        }
    }
    return img;
}

Displayクラスの中にDraw()という関数を作りました。この関数はVRAMから色番号を読んで、その色番号のパレットのRGBを参照してRGBの画面データを作るものです。あとはこの関数で作った画面データをglDrawPixels()で指定してやれば、VRAMの内容を反映して画面描画できるようになるはずです。

void GUI::display(){
    glClearColor(0.0, 0.0, 0.0, 0.0);
    glClear(GL_COLOR_BUFFER_BIT);
    glRasterPos2f(-1,1);
    glDrawPixels(scrnx, scrny, GL_RGB, GL_UNSIGNED_BYTE, disp->Draw());
    glFlush();
}

glDrawPixels()で指定している画面データがimgからDisplay.Draw()になっただけですね。

ではmake runして動作確認してみましょう。

f:id:sksat:20161221182751p:plain

何も表示されません...
まあそれはそうですね。いまのところVRAMに何も書き込んでいないわけですから(正確には、色番号0が黒に設定されているから)。 というわけで、VRAMになにか書き込んでみましょう。
では、『OS自作入門』の4日目の「画面表示の練習」と同程度のことをやってみます。 ここで、VRAMにデータを書き込むバイナリを読み込んでできれば文句はないのですが、僕が機械語の実装をサボっていた(本にすでにあるものぐらいやっとけよ、というかんじですね・・・)ので、バイナリのほうからメモリに値を書き込むことができません。そこで、main.cpp内でVRAMに書き込んでしまいます。

int i;
char *p;
for(i = 0xa0000; i< = 0xaffff; i++){
    p = (char*)(emu->memory + i);
    *p = i & 0x0f;
}

これでVRAMに書き込めたはずなので、make runしてみます。

f:id:sksat:20161221184720p:plain

おお!ちゃんとしましま模様が表示されました!成功です!

終わりに

(内部からVRAMに書き込むというズルはしていますが)画面表示のエミュレーションが一通りできたので、この記事はここまでです。ここまで見てくださり、ありがとうございました!

とりあえず、しばらくはバイナリのほうからVRAMに書き込めるようにすることを目標にして頑張ってみます。進捗が上がったらまたなにか書くかもしれません。

この記事で作っているエミュレータは、以下のリポジトリで公開しています。良かったら見てみてください。

github.com

「はりぼてOS」の背景を表示してみた(おまけ)

f:id:sksat:20161221185532p:plain 内部でboxfill()を作って、はりぼてOSと同じ値でboxfill()しています。