セキュリティキャンプ2017の応募用紙

Category: diary

セキュリティ・キャンプ2017の応募用紙を提出したので、どうせなので公開します。 私はこの記事がアップロードされたことを確認したら寝ます。 いい加減期限が迫ってから焦って取り組もうとするのはやめたほうが良いと思います。

追記: 受かりました。

推敲まで行っている余裕がとてもなかったため、悪文・乱文が多い可能性があります。 審査員の方々には申し訳ない限りです。

その上分量が多く、また最低限の整形しか行っていないため見づらいことがあるかと思いますがご容赦ください。 フォームに直接書いた生の文字列と、Markdownに則った形式とでは見え方が違うためです。 いずれ直そうと思いますが、私ははやく寝たいのです。

(追記:全体的に整形を加えました。 すべての行に改行を入れたり見出しやリストによる整形も行おうかと考えましたが、前者は画面右端の改行と被ってしまうため、後者はもともとの回答と見え方がずれてしまうためどちらも行っていません。)

また、応募用紙そのものに対する感想、注釈、検閲等については/* */で囲った部分、または//に続く箇所に書いているものとします。


共通問題

共-1(1).

あなたが今まで作ってきたものにはどのようなものがありますか? いくつでもいいので、ありったけ自慢してください。 プログラミングを始めた頃に作ったものから順に紹介していきます。

1.まさおコンストラクションと呼ばれる、ユーザーが自由にアクションゲームのステージを作成してWebページ上で公開することができるJavaアプレットゲーム上で作ったたくさんのステージ。また、それを公開するために作成したWebサイト。

これは基本的にはHTMLのparamタグにマップデータを配置したり、専用のマップエディタを用いてステージを作成できる機能が提供されているJavaアプレットですが、それに加えてJavaScriptを用いてJavaアプレット内のゲームの状態を取得したり、画像描画の命令を呼び出すことのできるAPIが提供されていました。 私は単に与えられた機能の範囲内で遊んでいて楽しいゲームを作るだけでなく、このJavaScript拡張の機能によって新しい仕掛けや敵キャラクター、強化された演出などを追加したゲームを作成し、自分のWebサイトで公開しました。

2.マルチプラットフォームで動作する、ソケット通信を使った小型のGUIチャットソフト。

一人がホストモードでソフトを起動し、他の人がホストに対してソケット接続を行うと、ホストが接続されているすべてのクライアントに通信を転送し、全員がリアルタイムでチャットを行うことができます。 Pythonから起動するほか、Windows用にexe形式の実行ファイルも用意されています。

3.Windows上で動作する、いわゆる「テトリス」のソフト。

GUIアプリケーションとして作ったものであり、ろくに活動実績のなかった廃部寸前の高校コンピュータ部において文化祭出展用の目玉プログラムの座を3年間維持し続けたものです。 コンパクトなUIでありながらゲームとして完結しており、落ちてくる四角い物のバリエーションの偏りの排除、次に落ちてくる物の予告、必要な時に一つだけ保持しておいたものと入れ替えるホールド、一度にたくさん消滅させたときのスコアの上昇とその連鎖、スコアとゲーム時間に応じた難易度の上昇、およびハイスコア登録が可能と、何度でも繰り返し遊べるような機能を備えています。

4.ブラウザ上で動作し、同時にそのページにアクセスしているプレイヤーとリアルタイムでの対戦ができる2Dアクションゲーム。

ブラウザ上でプレイすることができるジャンプアクションゲームを作成し、これをネットワークを介した人間のプレイヤー同士での対戦ができるように、リアルタイム通信機能を付け加えたものです。 一秒間におよそ14フレーム、1フレームあたり70msごとに画面が更新されてプレイヤーキャラクターを移動させることができる純粋なアクションゲームとしての性格を備えながら、ネットワーク越しの相手も同様にリアルタイムで移動し、お互いに攻撃しあうことで対戦が可能な通信ゲームとなっています。 アクションゲームとしては14fpsと、一般的な30fpsや60fpsよりも画面の更新周期がやや遅い設定であるものの、それでも完全なリアルタイム通信を行いたい場合は70msの遅延しか許されません。 プレイヤーの入力データが相手に届くまでにこれ以上の時間がかかった場合は、たとえば本当は自分と相手は同時に移動をしたはずなのに、相手からのデータが届いていないために自分だけが動いているように見えるというような事態が起こります。 これでは相手のクライアントが見ているゲーム画面と自分が見ているゲーム画面の間に差異が生じてしまううえ、このような誤差が蓄積されていくと同じキャラクターでもクライアントによって位置がどんどんずれていってしまう恐れがあります。 このゲームでは、行動を入力してから1~3フレーム程度の間だけ待機時間を持たせ、その間に入力データを先に送ってしまうことでロスを防ぐしくみを組み込んでいます。 そしてそれだけではなく、実際に通信が遅れて届いた場合でも、過去数フレーム分のプレイヤーキャラクターの位置や状態を逆算可能にしておくことで、本来正しく通信が受信されていたはずの時点まで巻き戻して再計算を行い、結果として相手に見えているはずの、正しいキャラクターの位置や状態に修正することができるようになっています。 とはいえこの場合でも、やはり一瞬の時間だけプレイヤー間で見えているものが違うという状態が起こり得ます。 たとえば敵のキャラクターがこちらのキャラクターに向かって攻撃を行い、明らかに敵の攻撃に当たってしまったとき、やられた!と思った瞬間に、実は敵のキャラクターの移動の入力データが届いておらず、本当は相手は全く違う方向に攻撃をしていたのだ、ということが判明したなら、これはゲームとしては楽しくないといえます。 そのため、キャラクターの移動は入力(と通信)が行われてから1フレームだけ後に行動に移され、ジャンプは2フレーム、攻撃動作は3フレームというように、そのデータが届かなかったときの影響の大きさによって入力から実際の行動までの間隔を調整しています。 これは単に通信が届かない確率を減らす効果が得られるだけではなく、それに加えて、3フレームの間隔が開くような動作に対しては、次のフレームでは2フレーム以下の間隔を持つような入力は無視され、その次のフレームでは1フレーム以下の間隔を持つような入力は無視される、という仕組みを取り入れることで、より同期ずれによる不都合が軽減できるようになっています。 たとえば、キャラクターが右を向いている状態で攻撃の入力を行い、即座に左に振り向こうとしたとします。 攻撃の入力が行われたのが0フレーム目だとすると、0,1,2フレームの間は入力から行動までの間隔が1である移動入力は無視され、結果として左方向への移動の入力は3フレーム目に有効になります。 この3フレーム目で実際の攻撃動作が行われ、3フレーム目に入力された移動動作は4フレーム目に行われることから、「攻撃の入力から実際に攻撃の動作が行われるまでの間、キャラクターが急に方向転換することはない」といえるようになります。 結果として、方向転換の通信が正しく届かなかったとしても、右を向いて攻撃をしようとしているキャラクターが攻撃の瞬間には左を向いている、という可能性を除外することができます。 ゲームをプレイしているプレイヤーは、キャラクターが動き始めるのが1フレーム遅れた、ということはあまり気になりませんが、右方向に発射された攻撃が一瞬後には左方向の攻撃に変わっていた、という事態には大きく戸惑うことが予想されます。 このように、プレイヤーが気にならないような誤差はあえて許容しつつ、深刻なずれが生じる可能性を極力減らすような設計とすることで、見かけ上はリアルタイムに同期がとれているかのようなゲームプレイを実現しています。

5.Freecivのユニット間の戦闘の勝率を計算するソフト。

私が約5年の間、週末に3時間から10時間程度は必ずプレイしているFreecivというゲームがあります。 「シヴィライゼーション」シリーズをもとにして20年前につくられたこのGPLソフトウェアでは、プレイヤーの帝国の手足となる軍事ユニットが絶えず他国のユニットと戦闘を行います。 この戦闘は、互いのユニットの戦闘力に加え、ユニットの熟練度の差、様々な地形の補正効果、要塞や城壁による防御効果、ユニット間の相性による能力値の変化、港湾都市に停泊中の艦船ユニットが敵からの攻撃に対して脆弱になる特殊補正など、様々な要素によって勝敗が決まります。 都市の中に籠っている複数体の相手ユニットに対し、こちらが何ユニットの攻城兵器を用意すれば撃破が可能なのか?という問いに答えることは、敵都市の防御ユニットをすべて倒すことができればその都市を占領して大きな優位性が得られ、一方で1ユニットでも相手が生き残ってしまえばただ自国軍が全滅しただけの大損害になってしまうというハイリスク・ハイリターンさから、攻撃の計画を立てる上で非常に重要となってきます。 そのため、戦闘の発生する状況を入力すれば、その戦闘での勝率、与えられるダメージの期待値などをweb上で算出できるアプリケーションを作成しました。 予めFreecivのルールセットに対応したユニット能力値や補正効果などのデータセットを作成しておくことで、複雑になりがちな補正効果の計算をすべて自動的に行うことができます。 またFreecivは自由度の高いゲームでもあり、ユーザーが自由にルールセットを改造して独自のユニットや効果を持った新しいルールセットを作ることができるようになっていますが、この計算ソフトではユーザーが計算に用いるためのデータセットを改変して新しいルールセットに対応したものとできるという柔軟性を持っています。 このソフトによって、経験によって「勘」で攻撃に必要なユニット数を把握できるような熟練プレイヤーのみでなく、このゲームの初心者であっても実際の数値をもとに戦略を立てることができるようになるため、プレイヤー全体のスキル向上に寄与できると考えています。

6.HTML5 Canvas上で動くアクションゲームと、そのフレームワーク。

先述した4.のブラウザ上で動くゲームはC#+Silverlightで開発していたのですが、気付けばSilverlightがほとんどのブラウザで動かなくなってきていたため、TypeScript+Canvasでの書き直しと、その洗練を試みたものです。ただし、通信に関する機能は含んでいません。 (なおSilverlightが動かなくなったためにJavaScript関連の技術に移植したのは、先述した5.の計算ソフトも同じ流れをたどっています)

7.いくつかのWebサイト。

スマートフォンでアクセスしたり、ウィンドウサイズが小さくなったりすると自動的にメニューが画面端に格納され、隅のボタンをクリックすることでメニューがアニメーションしながら開閉する、いわゆるハンバーガーメニューをJavaScriptを用いずにCSSのみで実装したWebサイトを作成しました。 また、ブログを作ってGitHubにMarkdownで書いた記事をpushすればビルドしてアップロードしてくれるようにもしました。 他に、wikiの設置・管理も行っています。

8.LALR(1)パーサジェネレータ。

漠然と「構文解析がしたい」と思ったので作りました。 構文ファイルを与えることでそれを構文木に変換できるようなパーサを生成し、解析を行うことができます。 また、パーサ生成時に構築される構文解析表と、それを読み取って解析を行う部分のみを取り出してソースコードとして出力することで、パーサの生成処理だけを予め行っておくこともできます。 このパーサジェネレータでは、構文規則を記述するための構文ファイル自体の規則もこのパーサジェネレータによって生成されています。 構文ファイルに使うための構文規則が自分自身の構文を定義しているため、構文を表現する方法を拡張したり、別の表現を用いることができるようにすることが容易です。 また、作っているうちにこれはYaccやBisonとよばれるパーサジェネレータの二番煎じにしかならないことに早々に気付いたため、単にパーサジェネレータの開発を進めるだけでなく、並行して開発の流れや理論的概略を紹介する記事をブログに書くことにしました。

共-1(2).

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

1.
当時は個人サイトが活発な時代だったため、同じようなステージを作って公開している人と掲示板などで交流をしながらステージ作りやWebサイト作りをしました。 このゲームのJavaScript拡張がプログラミングに触れたはじめてのきっかけでした。 初めはJavaScript拡張機能のサンプルを見て、そこに登場する敵キャラクターの動きの新鮮さに驚き(用意されている敵キャラクターはたいてい同じ動きや攻撃を繰り返すだけのものだったが、そのキャラクターは残りのHPに応じて動きを変えるボスキャラクターだった)、どのようにして攻撃を切り替えているのか、プログラムのどの部分が攻撃を出す部分なのか、などが気になり、ソースコードを書き替えては実行してを繰り返しました。 何度もエラーを出したりしながら攻撃を出す部分(関数呼び出し)を見つけ、その引数の数値を変えると攻撃の種類が変わることに気付くと、いろいろな攻撃を試したくなって朝方近くまでずっとそのサンプルコードで遊んでいました。 以前のそのゲームでは考えられなかったような目新しい動きをする強力なボスキャラクターに惹かれ、見よう見まねで自分なりのボスを作って公開したり、新しい仕掛けが作れないかどうか考えを巡らせたりしていました。 いくつかそういった機能を追加していくうちに、配列を使うことを覚えたり、ボスをオブジェクトにして、しかもそれを配列に入れてしまえば、わざわざ変数を分けて書かなくてもたくさんのボスキャラクターを出現させられることに気付いたりして、次第にプログラミングというものに興味が沸いていきました。

近年ではJavaアプレットはほぼ駆逐されつつあるため、このゲームもそのままでは遊ぶことができなくなってきています。 一方、このゲームをJavaからJavaScript+Canvasコードに変換した移植プロジェクトが存在しており、高い互換性から既存のステージのCanvas版への移行が進んでいます。 しかしもともとJavaScriptによる拡張を施していたステージについてはそのままでは移行できないこともあり、私が作ったそのようなステージは現在遊ぶことができない状態になっています。 そのため、いずれこのようなステージを遊ぶことができるように移行作業を行いたいと考えています。

2.
JavaScript以外のプログラミング言語にも触れてみたいと思っていた時にハッカーになろう(http://cruel.org/freeware/hacker.html )を読む機会があり、まんまと影響されたためにPythonを使って何か作ろうと思い、作りました。 Pythonと、Pythonで使用できるGUIツールであるTkinterを用いて作成しました。 exe形式の実行ファイルへの変換は、py2exeを使用しています。

3.
高校1年の文化祭の展示物として、GUIアプリケーションを作成できるQtを用いて、C++で作成しました。 元ネタが非常に洗練されたゲームであるため、ゲームとして必要な機能はおおよそまとまっているように思えるので、何か機能を追加するとしたら(ソースコードを紛失していることも忘れるとしたら)、演出を強化してより見栄えの良いものにしたいと思います。

4.
1.でとりあげたJavaアプレットゲームでは、私はあくまでJavaScriptを使って外側からゲームに手を加えている立場でしたが、自分でもアクションゲーム自体を作ってみたいと思うようになり、またどうせならJavaアプレットよりも新しい技術を使い、新しい機能を付け加えてみたい、と考えました。 そこで、当時新しく登場していたHTML5、Socket.io、Silverlightといった技術に目を付け、複数人で通信してゲームができればきっと楽しいだろうと思い、ブラウザ上でオンラインプレイ可能なアクションゲームを作ろうと思って作り始めました。 この当時はまだCanvasの仕様や実装が固まっていなかったため、C#+Silverlightを使って開発することに決めました。 またこの頃socket.ioのリアルタイム通信が騒がれていたため、これを使ってリアルタイムで通信をさせようと思いましたが、結局のところ生のWebSocketのほうがsocket.ioを使うよりも高速に通信できることから、サーバー側もC#で書き、WebSocketでの通信を行うことにしました。 (しかしながら、ただでさえ主要ブラウザの一部に実装されたばかりのような状態であるWebSocketが至る所で繋がらなかったり、それに加えてSilverlightのセキュリティポリシーのためclientaccesspolicy.xmlを別ポートで配信しておかないとクロスドメイン通信はできず、かつこのSilverlight上でWebSocket通信を確立しようと試みたため、実際のゲーム面のみならずインターネットを介して通信を確立させるというただ一点についてもかなり苦労することとなりました)

現在はSilverlightを動かすことさえ困難になりつつある一方で、Canvasを使えないブラウザはほとんどなくなり、またUDP上で構築されているためにWebSocketよりも速度が期待できるWebRTCが広まってきたことなどから、今後改善するとすればまずはHTML5への移植であると考えられます。

また、ブラウザ上での(見かけ上)同期のとれたリアルタイム通信は、いずれ再挑戦したい点だと考えています。 (1)で述べたようにこのプログラムには通信遅延による影響を抑えるために様々な工夫が凝らしてあるのですが、それ以前の問題があります。 たとえば互いに0.2秒ずれていても通信が成り立つとしても、そのずれが小さくなっていく方向に修正していく機能がなければ、恒常的に0.2秒ずれた状態で通信がつながっていて、たまたまそのずれが0.21秒に広がったときに破綻してラグが生じる、ということが起こりますから、こういった点は改善の余地があると思います。

5.
このソフトは現在Web上で公開していますが、バージョン番号に2がついています。 バージョン1はC#+Silverlightで、バージョン2はTypeScript+jQuery(+jQueryUI)となっています。 またバージョン2では、タスクランナーであるgulp.jsを用いることで、ソースコードの更新を検知して即座にコンパイルしてブラウザ上に反映しながら開発しました。 ユニットの戦闘は、戦闘力比をもとにした確率でどちらかがダメージを受け、それを片方が倒れるまで繰り返すという処理によって行われるため、二項係数を用いて計算することができます。 とはいえ数式上では120C60などの値が登場し、これを実際に計算しようと思えばlong型の最大値も越えてしまいます。 バージョン1では(ごく限られた機能の)多倍長整数演算ができるようなクラスを自作して頑張って計算していましたが、バージョン2では競技プログラミングに少し触れていたこともあって、二項係数はメモ化しておけば高速化できる上に、複数回計算を行った場合に前の計算の結果を使えばすぐ結果が返せる、また結果の勝率はせいぜい小数点3位程度までが正確であればよいので、double型の精度で生じるような誤差は無視して構わないと考えたことなどから、バージョン1よりも簡潔かつ高速に処理ができるようにしました。

バージョン2の開発時、ユニットをリストから選ぶことでも、名前を直接入力することでも、また名前の一部を入力することで表示されるサジェストを選択することでもユニットが選択できるようなコンボボックスを利用したかったため、この要件を満たせるようなUIが提供されるJavaScriptライブラリを探していたところjQueryUIが見つかりました。 そのためにバージョン2にはjQueryを使っていますが、この前同じようなコンボボックスがReact系のライブラリでも実現できることを知ったため、今後はこれをReactで書き直してみたいと思っています。

また、データセットにはJSONを読み込んでおり、その一部の文字列を正規表現で読み取って独自記法のようにしている部分があります。 今の私には8.で作った自作のパーサジェネレータがあるため、それを使って生成したパーサを用いてより簡潔かつ拡張性に富んだ形でデータセットが記述できるようにしたいと考えています。 UI部分のReactでの書き直しと合わせて、早いうちにバージョン3の開発に取り掛かりたいと思います。

6.
アクションゲームを作成するのと並行して、そのゲームを作成できるフレームワークを作り、アクションゲームだけでなくほかの種類のゲームの開発も容易に行えるようにしたい、と考えたため、単にゲームを実装しているのではなく、フレームワーク部分とゲーム部分の2つにプロジェクトを分け、フレームワーク部分の持つべき役割を考えながら作成していました。 外部のデータを読み込むローダーをそれ単体で利用しやすい形に設計したり、あるクラスを多様な用途に使えるようにするために、どこまでの役割をそのクラスに持たせて、残りを別のクラスの役割とするべきなのかを考えながら作っていましたが、これぞといった設計を捻りだすのは難しく、ウンウン唸ってばかりでなかなか開発が進まなかったりしていました。

7.
Freecivプレイヤーの知人が新しく常設ゲームサーバーを作ろうとしていたので、その公式サイトの作成を担当しました。 HTML5やCSS3の新機能はある程度追いかけていたのですが、実際にWebページを作った経験はHTML4.1の時代で止まっていたので、HTML5の仕様に準拠したモダンな作りのWebサイトにできるよう心がけました。 またデザインも自分が考えることになったので、派手にならず、かつ味気ないものにならないような見た目のページにするにはどのようにすればいいかを考えながら作りました。

ブログは、Twitter等ではあまり長い文章を書くことができないため、何らかの形で長めの文章や技術的な話題について発進/* 誤字 -> 発信 */できるような場所が欲しいと思ったことから作りました。 このブログには静的サイトジェネレータであるHugoを使用しています。 コメント機能やトラックバック機能などは必要ないと考えたため、サーバーサイドでブログ用のプログラムは走らせず、手元で記事を書くたびにサイトをビルドしてそれをアップロードするようにしました。 Hugoのテンプレートエンジン機能を利用して、さまざまなブログ向けのテンプレートが公開されていたのですが、あまりしっくりくるものがなかったため、テンプレートを自分で記述してブログのデザインを行いました。 また、ビルドからアップロードまでの手順を自動化しようと思い、CIサービスであるWerckerを利用してそちらでビルドが行えるようにしました。 手元で記事を書き、Markdownファイルを追加したコミットを作成してGitHubにpushすることで、Wercker側でビルドが行われ、用意しておいた別のGitHubリポジトリにビルド結果のコミットが行われて、その内容がサーバーにアップロードされることで記事が反映されます。 ブログのデザインや、カテゴリ・タグ表示の機能にはもっと見やすくするための改善の余地があると思っています。

8.
どうすれば構文解析ができるのかよくわからなくて調べたところ、Yaccなどのツールを使えばいいということがわかりましたが、どうもプログラミングっぽさに欠けるなあと思い、もう少し調べると、そのようなツールが用いているLR法などの手法を用いることで構文解析が実現できるとわかりました。 構文解析に関する大学の講義を受けていた上級生の知人がいたため、その方に理論を教えてもらったり、web上に上がっていた言語解析に関する大学の講義スライドを読んだりしてLR法について勉強し、これをコードに落とし込みました。 実装はTypeScriptによるフルスクラッチです。 LR法による構文解析は行う手順が多く、それらの手順が明確に分離されているため、「とりあえずこの機能さえ動けば次に進める」ような箇所が多くありました。 そのため、とにかく動作するように開発を進めていくことでテンポよく開発を進めていきましたが、その分だけ複雑な処理をしている部分のコードが非効率的なままになってしまう傾向がありました。 それでもまずは完成させることを優先して開発を進め、構文解析の成功に至った段階ですべてのソースコードの見直しを行ってリファクタリングと最適化を進めました。 結果として、自分自身の構文ファイルを解析してパーサーを生成する処理に2秒程度かかっていたのが、最終的に100msから50msにまで処理時間を減らすことができました。

追加していきたい機能は複数あります。 まず、このパーサジェネレータではLA(1)文法/* 誤字 -> LR(1)文法 */に厳密に従ったあいまいさのない構文しか処理することができません。 具体的には、 S -> S+S | x というような構文があったとき、x+x+xという入力の(x+x)+xとx+(x+x)の2通りを区別できないためにこの構文はコンフリクトを起こし、パーサの生成に失敗します。 演算子に右結合・左結合の設定を行ったり、演算子の優先順位の設定を行える機能を搭載し、例のような構文も処理できるようにしたほうが実用的になると考えています。

また、実際にパーサに入力を与えたときに構文解析に失敗した場合、失敗したという事実は通知されますが、どこでどのような失敗が起こったのかというエラーメッセージを表示する機能がないので、パーサがただ構文解析表を読み取っていくだけでなくどのような失敗が発生したのかも検知できるようにしたいと思っています。

そして、ただ構文木を作るだけでなく、規則ごとに割り当てられたコードをその場で実行できるように、構文ファイルに処理を書き込めるような機能も追加したいです。 現状ではコールバックを渡すことで同様の処理が可能ですが、渡した関数の中で今どの規則の解析を行ったのかを判断しないといけないため、煩雑になりがちです。 そのため、規則とコードを直接結びつけて構文ファイルに記述できるようにするのがよいと考えています。

共-1(3).

開発記のブログ、スライドなどの資料があれば、それも教えてください。コンテストなどに出品したことがあれば、それも教えてください。

4.は、立命館大学主催のソフトウェアコンテストであるICT Challenge+R2013高校版(http://www.2013.ict-challenger.jp/ )に出品してファイナリストに選ばれ、2つの企業賞を受賞しています。 またこれに参加した際のスライド発表の様子は、youtube上に公開されています(/*検閲により削除。探せば見れます。*/)。

7.のブログの構築については、ブログ上の記事でその流れを公開しています(http://tatamo.81.la/blog/tags/blog/ )。

また、8.のパーサジェネレータについても、構文解析入門、およびパーサジェネレータの自作方法の解説という体裁で紹介記事を同じブログで公開しています(http://tatamo.81.la/blog/tags/parser-generator/ )。

共-1(4).

Twitterアカウント、Github、ブログをお持ちでしたら、アカウント名、URL等を記載してください。

Twitter: @__tatamo__ https://twitter.com/__tatamo__
GitHub: https://github.com/tatamo
ブログ: http://tatamo.81.la/blog/

共-2(1).

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

GoFの23のデザインパターンについてインターネットで調べ、どのような設計のしかたが一般に行われているのかを勉強しました。 また、ゲームのプログラムをどのように構成すればよいのかを知るために、「ゲームプログラマになる前に覚えておきたい技術」「ゲームエンジン・アーキテクチャ」を読みました。 そして、実際にいろいろな言語で提供されているゲーム制作用のライブラリのリファレンスやソースコードを読んで、どのような機能が提供されていて、他の機能とどこで分けられているのか、それがどのような使い方をされるように作られているのかを調べました。 そして実際に、ゲームを作るというよりゲームのフレームワークを作るようにすれば、それを使って簡潔にゲームが作れるようになるのではないかと思い、自分でプログラムを作っていきました。

共-2(2).

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

GoFの23のデザインパターンについて調べ、そしてその前提として必要ならば、オブジェクト指向についての理解を深めるようにアドバイスをします。 そしてその上で、この壁についてあまり深く考えることはやめたほうがいい、と言いたいと思います。

ゲームのフレームワークを作らなくてもゲームを作ることはできますし、一般に個人的にゲームプログラムを作るぐらいなら、コード全体に影響を及ぼすような破壊的な変更を含む大幅な仕様変更はそう何度も行われるものではない(または、行われるべきではない)と思います。 そのような場合は、「そこそこ」の設計であってもゲームは正しく動作すると思われるので、設計に頭を悩ませるよりも、その時間でゲーム自体にどのような機能を追加できるか、それによってゲームを面白いものにできるかを考えるべきだと思います。 つまり、ゲームを作りたいのか、それともゲームのフレームワークを作りたいのか、自分がそのどちらを望んでいるのかを見失うべきではないです。

また、ただ考え続けるよりは実際に動くプログラムを書いたほうが、設計があいまいであっても全体がどのように動くのかを把握できるため、次に似たものを作るときによりよい設計が浮かびやすくなると思います。 プログラミングが好きだからゲームを作りたい、と思っていると、「既存のライブラリやフレームワークを使ってゲームを作るよりも、自分のプログラムでゲームを動かしたい」という気持ちになりがちだと思います。 しかしながら、既存の良く考えて設計されたフレームワークを使えば、その設計に触れ親しむことにもなり、また肝心のゲームを面白くすることにより注意を払うことができるようになります。 そのため、まずはあまり深く考えるのをやめて、とにかく動くゲームを作ってみたり、既存のライブラリやフレームワークを使ってみたりするほうがいい、とアドバイスしたいです。

/*
お察しの通り、入力フォームに1箇所ずれて回答を貼り付けてしまっており、共-2 (1)の回答が消滅しています。
ここに書いたところで何の意味もありませんが、以下に本来の2(1)の回答を掲載しておきます。

作りたいプログラムを書くときに、機能をどのように分け、クラスをどのように設計すればよいか。
特に、ゲームを作る際のキャラクターやエンティティを表すクラスについて、それぞれがゲーム全体の情報にどこまでアクセスできるようにするべきか、
またほかのゲーム内エンティティを表しているオブジェクトに対してどのようにメッセージのやりとりを行うべきか、
そして、どのようにゲームプログラム内のクラスの継承関係や依存関係を構築していけば、
全体の見通しを良くすることができたり、可能な限りコードクローンの量を小さくでき、
可能な限り多くの処理コードをスーパークラスや他のクラスから引き継げるのか。
*/

共-2(3).

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

GoFの23のデザインパターンについて調べ、そしてその前提として必要ならば、オブジェクト指向についての理解を深めるようにアドバイスをします。 そしてその上で、この壁についてあまり深く考えることはやめたほうがいい、と言いたいと思います。

ゲームのフレームワークを作らなくてもゲームを作ることはできますし、一般に個人的にゲームプログラムを作るぐらいなら、コード全体に影響を及ぼすような破壊的な変更を含む大幅な仕様変更はそう何度も行われるものではない(または、行われるべきではない)と思います。 そのような場合は、「そこそこ」の設計であってもゲームは正しく動作すると思われるので、設計に頭を悩ませるよりも、その時間でゲーム自体にどのような機能を追加できるか、それによってゲームを面白いものにできるかを考えるべきだと思います。 つまり、ゲームを作りたいのか、それともゲームのフレームワークを作りたいのか、自分がそのどちらを望んでいるのかを見失うべきではないです。

また、ただ考え続けるよりは実際に動くプログラムを書いたほうが、設計があいまいであっても全体がどのように動くのかを把握できるため、次に似たものを作るときによりよい設計が浮かびやすくなると思います。 プログラミングが好きだからゲームを作りたい、と思っていると、「既存のライブラリやフレームワークを使ってゲームを作るよりも、自分のプログラムでゲームを動かしたい」という気持ちになりがちだと思います。 しかしながら、既存の良く考えて設計されたフレームワークを使えば、その設計に触れ親しむことにもなり、また肝心のゲームを面白くすることにより注意を払うことができるようになります。 そのため、まずはあまり深く考えるのをやめて、とにかく動くゲームを作ってみたり、既存のライブラリやフレームワークを使ってみたりするほうがいい、とアドバイスしたいです。

共-3(1).

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

■C1 ブラウザの脆弱性とそのインパクト
私はJavaScript(やSilverlight)によってWeb上で動作するアプリケーションを開発しているので、実際に自分が作ったものが動いている基盤であるブラウザに関連した攻撃は他人事とは思えません。 また、ブラウザはサーバーサイドとクライアントサイドの間に立っているもので、かつ十分すぎるほどの複雑さを持ったものです。 サーバー側のセキュリティ、それに接続するクライアント側のセキュリティについて考えることは当然ですが、そこに介在しているブラウザの、その脆弱性が悪用されることについて見落とすべきではないといえるはずです。 しかしブラウザはいつも使っているものでありながら、その内側については何も知らないというような気がします。 そのため、ブラウザの脆弱性について具体的に考えられるようになるための知識、そして考え方を学びたいと思っています。

■D2~3 カーネルエクスプロイトによるシステム権限奪取
問題の出題順とは前後しますが、私はこの回答を選択問題A-5に取り組んでから書いています。 そして率直に言って、あの選択問題はさっぱりわかりませんでした。 カーネル部分のコードの脆弱性の発見、それを利用したコード実行、さらに実際に権限昇格を行うためのコードの作成、そしてそれを防ぐためのセキュリティの回避、これらについて私は全くといっていいほど知見を持っていません。 カーネルのソースコードを読むだけでなく、カーネルモードやユーザ―モードといったCPU・OSの動作の仕組み、仮想メモリの展開や領域確保のされかたなど、膨大かつ細部に渡るような知識を持っていなければ、このような脆弱性を見つけ出し、そして悪用することはとてもできないのではないかと感じました。 しかしながら、実際にセキュリティに携わる人々、そして攻撃を行う人々は大勢います。 つまり、すごい人たちがたくさんいるんだなあと実感しました。そのような世界についてとても学びたいです。

■E4 サイバー犯罪捜査の現場
サイバー攻撃は犯罪ですから、当然攻撃した人は逮捕されないといけません。 とはいえセキュリティの世界は、(何も知らない私から見た考えですが、)たとえばセキュリティソフトを開発している民間の企業や、脆弱性に対してパッチをあてる開発者、攻撃を受けたことを調べるネットワーク管理者など、警察とは関係のない人々が大半のように思えます。 このようなセキュリティの世界において犯罪捜査をどのように行っていくのか、ということは興味深く、そしてこれを学べる機会は貴重なものだと思います。 攻撃と防御だけでなく、捜査という観点からセキュリティについての理解を深めたいと思っています。

■A6 ハードウェアセキュリティ最前線
「暗号回路で実行される処理中に発生する副次的(サイドチャネル)な情報を取得し、それらの信号にどのような情報が埋め込まれていたかをデジタルオシロスコープを用いて観測し、実際に暗号解読を試みる」。 ワクワクする響きだと思いました。暗号をオシロスコープを使って解読する。 処理中に発生する情報、そしてそれを取得し利用するという発想は、ソフトウェアをプログラミングする観点からではそう出てくるものではないと思います。 結果として得られる暗号がソフトウェア的に利用されるものだとしても、それを生成するハードウェアから情報を盗み出せるという観点に興味を抱いたため、ぜひ受講したいと思っています。

■D7 ゲームセキュリティ入門
ブラウザの脆弱性同様、自分がこれまで触れてきたことに関係する分野であり、それでいてセキュリティと結びつけて考えたことはありませんでした。 ゲームとセキュリティをどのように結び付けて考えればいいのか?どのように解析されうるのか?といったことを実際に行って学び取ることができればと思います。

共-3(2).

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

私がセキュリティキャンプでやりたいことは、わかりません。 身に着けたいことも、よくわかりません。 このような積極性に欠ける姿勢はよくないだろうと思われるかもしれませんが、実際に私はセキュリティキャンプに対して受け身な姿勢で臨むだろうと思います。 そもそも私はこれまでセキュリティというものにほとんど触れたことがありません。 リバースエンジニアリングを行って実行ファイルを解析したこともありませんし、バイナリハックなども(フリーゲームのセーブデータをバイナリエディタで弄ってチートをしようと思ったことはありますが)全然やったことがありませんし、暗号技術がどのように運用されているかも知りません。 Linuxカーネルのソースコードを読んだこともなければ、流れているパケットをキャプチャしたこともありません。

私はこの回答を、選択問題をほとんど回答した後に記述しています。 適切な表現かはわかりませんが/* 何を今更 */、選択問題について、これはよくできた問題だなあ、ということを幾度となく感じました。 OSとCPUがどのように協調して動作しているのか、仮想メモリはどのように展開されているのか、TCP/IPはどのように構築されているのかということから始めて、それに関連してどのような脆弱性が存在するのか、それをどのように悪用して攻撃につなげるのか、など一つの問題の中で段階を踏んで何度もわからない箇所が現れ続けるので、壁の高さを感じました。 一方で、それを少しずつ紐解こうとしているうちに、基礎から実践、応用と幅広い範囲についてわずかではあるものの見通せるようになったような気がしました。

一般になにかの技術について勉強しようと思ったら、本を読んだり、実際に試してみたりして理解を深めていくものだと思います(勉強会に行ったりする人もいると思いますが、私は行ったことがありません)。 しかしながら、たとえば私がカーネルエクスプロイトなどについて勉強しようと思ったとして、まず何から手を付けていいかわからないでしょうし、知見が足りないためにこなすべき問題の設定も適切にできるとはいえないでしょう。 また、その分野について詳しい人が身の回りにいるとも限りません。 セキュリティキャンプの応募用紙の問題に取り組んでいて、この問題設定はよくできている、と思った点はそこにあります。 全くの知見がない私であっても、適切な問題が与えられることで基礎から応用まで俯瞰しながら取り組むことができたからです。

私はこのような受け身の姿勢でもってセキュリティキャンプに対して期待を持っています。 つまり、私の知らないことに対して、驚くほど適切に学ぶべき道を示してくれるだろうという期待です。

限りなく抽象的に述べるならば、私がセキュリティキャンプでやりたいこととは、何を学べばいいのかを見つけることであり、身に着けたいものはそのようにして与えられたものです。 当然、それらの与えられたものについては全力で学び取っていこうと思います。

もちろん、同じように学んでいける人々とつながりを持つということも求めていきたいです。 一緒に学んでいける人がいるということには、数えきれないほどのメリットがあるからです。 そしてそのような人々が詳しいこと、私が詳しいことは人によって皆違うはずなので、詳しい人からたくさん学ぶことができるようになり、セキュリティキャンプが終わった後も良い影響を及ぼしあうようになることができればと考えています。

選択問題

選-A-1.

添付したファイルに記録された通信を検知しました。この通信が意図するものは何か、攻撃であると判断する場合は何の脆弱性を狙っているか。また、通信フローに欠けている箇所があるがどのような内容が想定されるか、考えられるだけ全て回答してください。なお、通信内容を検証した結果があれば評価に加えます。

.pcapについて調べると、パケットキャプチャを記録しておくためのファイルだということが分かったため、Wiresharkを使用して開きました。 192.168.74.1および192.168.74.130の二者の間での14件のパケットが記録されており、そのうちの13件がTCPプロトコル、1件がHTTPプロトコルによるものだということがまず分かりました。 また、Wireshark上で警告表示のあるパケット4件も含まれていました。 理由は後述しますが、私は結論としてこの通信が攻撃であると判断しているため、簡単のために192.168.74.1を攻撃側、192.168.74.130をサーバー側と呼称することにします。 これらのパケットは、攻撃側55522番ポートとサーバー側8080番ポートの間の9件の通信、および攻撃側53653番ポートとサーバー側22番ポートの間の5件の通信に分けられます。 8080番ポートは80番ポートの代替としてHTTP用に用いられており、実際にこのポートに対してHTTPリクエストが行われていることからHTTPサーバーが起動しているものと考えられます。 また22番ポートはSSHに割り当てられたwell-knownポートであり、サーバー側に対して攻撃側がSSHに関連した接続を行っているものと思われます(ただしキャプチャされたパケットにはssh通信は記録されていません)。 192.168.74.130がこれらの既知のポートで通信を行っていることから、こちらが攻撃を受ける側であると考えられます。

まずは、8080番ポートに関連した通信に絞って調べました。 最初の3つのパケットは交互にSYN,SYN・ACK,ACKの通信が行われており、これはTCPの3ウェイハンドシェイクによる接続の確立だと判断できます。

次に、/struts2-rest-showcase/orders.xhtml に対してGETメソッドによるHTTPリクエストが行われています。 このリクエストは、HTTPヘッダのContent-Typeの項目に明らかにMIMEタイプやcharsetの定義ではない、何らかのコードであると思われる文字列を含んでいます。 リクエスト先にstruts2という文字列が含まれている点、およびContent-Type部分にコードが書き込まれていることから、これはStrtus2の脆弱性であるCVE-2017-5638(S2-045/S2-046)を狙ったものであると考えられます。 この脆弱性によってContent-Typeに含まれるOGNL式が評価されるため、OGNLによって表現された任意のコードが実行可能となります。 今回検知された通信に含まれているコードは、

cat /etc/passwd

というコマンドの実行を行うものです。 これにより、脆弱性に対する対策が取られていなかった場合、サーバー側は攻撃側に対して/etc/passwdの中身を送信します。 このGETリクエストに対するACKのパケットの次に検知されたパケットは、攻撃側からサーバー側に対してのACKであり、Wiresharkはこのパケットに対して警告を出しています。 このパケットでは攻撃側からサーバー側に対して、何らかの通信へのACKが行われ、Acknowledgment numberは最前の攻撃側からのパケットでの数値が1だったにもかかわらず1464となっています。 またこれより後に続くサーバー側の通信(11番目のパケットである、FIN・ACKの応答)に目を遣ると、Sequence numberが1464となっていることもわかります。 よって、このパケットの直前に攻撃側はサーバー側から長さ1463のデータを受け取っており、そのパケットは記録から欠けていると判断できます。 この失われたパケットは、攻撃側からのHTTPリクエストに対するサーバーからの返答であると考えられます。 ただし、TCP接続確立に用いられた最初の2つのパケットを確認すると、双方ともに最大セグメントサイズを1460バイトと規定していることがわかります。 このため、長さ1463のデータで通信する場合、パケットは2つに分割されていたのではないかと考えられます。 よって、ここで欠けているパケットは1つとは限らず、2分割された2つのパケット、またはそれらに加えて分割されたうち1つ目のパケットに対するACK応答などが含まれていた可能性があります。

このACKのパケットに続いて、FIN・ACKフラグがセットされた通信が攻撃側から行われています。 このパケットのAcknowledgment numberも1464であるため、HTTPレスポンスの通信を検知していないWiresharkはTCP ACKed unseen segmentの警告を出しています。 TCPヘッダにFINフラグがセットされているため、これは通信終了を示しています。 その次の通信はサーバー側からの通信であり、FIN・ACKフラグがセットされています。 このパケットに対しWiresharkはTCP Previous segment not captured警告を発しており、またTCPの通信終了の際はサーバー側からのFIN・ACKフラグのセットされた通信の前に、攻撃側からのFINに対するACKのパケットが送られるべきであることから、この直前の通信が欠けており、その内容は長さ0のACKであると判断できます。 その後攻撃側からACK応答が届き、このポートでの通信は終了しています。

次に、22番ポートにおける通信に着目します。 このポートでの通信は5回検知されていますが、そのすべてが攻撃側からサーバー側への、長さ0のACK応答となっています。 最初に検知されたパケットのTCPヘッダを確認すると、Sequence numberおよびAcknowledgment numberがともに1となっています。 ハンドシェイクによる接続確立のパケットは確認できないため、既に接続を確立した後のものであると考えられますが、何らかのデータの受信に対してACKの応答を行った場合、Acknowledgment numberは1より大きくなっているのではないかと推測されます。 よって、このACK応答は3ウェイハンドシェイクのうちの最後の1つであり、この前に攻撃側からのSYN→サーバー側からのSYN・ACKの通信が行われており、記録ではその2つのパケットが欠けていると考えることができます。 その後の4回の通信は、900から2500バイトのデータをサーバー側から連続して受け取ったことに対するACK応答であるように見えます。 しかしサーバー側から攻撃側に送信されたデータは記録されておらず、確認できません。 また、この間常に攻撃側のパケットのSequence numberは1のため、攻撃側が能動的にリクエストを発した形跡はないと考えられます。 また、実際にdockerでSSHを待ち受け、その接続の様子をキャプチャしてみましたが、SSHの接続処理によって攻撃側とサーバー側双方のSequence numberおよびAcknowledgment numberは1より大きくなったため、この攻撃者はSSHでの接続は行っていないと思いました。 一方でtelnetでSSHに接続を行ったところ、サーバー側からのSSHプロトコル通信によってやはりSequence numberおよびAcknowledgment numberの両方が1にはなりませんでした。 そのため、この一連の接続において攻撃側が何を行ったのかはわかりませんでした。

以上の通信記録より、これはCVE-2017-5638を悪用した攻撃であると判断します。 攻撃者がcat /etc/passwdというコマンドの実行を試みたこと、SSHに使用されるポートに対する接続を行っていることから、この通信の意図は3つ考えられます。

まず1つ目に、攻撃者が/etc/passwdに格納されている情報の利用を試みた可能性についてです。 この脆弱性が存在する最も古いバージョンであるStruts2.3.5のリリースは2012年であることから、パスワードハッシュが/etc/shadowではなく/etc/passwdに格納されているような大昔のUNIXシステム上で脆弱性のあるStruts2が動作していると考えるのは困難です。 またシステム管理者がシャドウパスワードを無効化するような設定を行っていたり、パスワードなしでログイン可能なユーザーが存在すれば攻撃対象とすることができますが、やはり非現実的であるように考えられます。

もう1つは、この通信は攻撃対象のシステムがCVE-2017-5638脆弱性を含んでいるかどうか調査するための通信であるという可能性です。 サーバー側からのレスポンスによって任意コード実行が成功したかどうかを判別できるので、この通信によってCVE-2017-5638を利用した攻撃が可能かどうかを知ることができます。 しかしながら、今回の通信時に攻撃用のコードを直接実行しなかったこと、SSHに使用されるポートに対しても接続を行っていることについては疑問が残るように思われます。 前者については、このシステムだけでなく複数のシステムに対しての調査を行い、攻撃可能な対象の情報が十分に集まってから攻撃に移ることを意図しているのではないか、後者については、別の脆弱性を利用して特権昇格を行うことで、rootの乗っ取りを図ろうとしているのではないか、またはそもそもSSHポートが開いているのかどうかを調査しようと考えたのではないかと考えることができます。

そして最後に、これらの一連の通信はサーバー管理者によるペネトレーションテストなのではないかという可能性です。 実行されたコードが攻撃性の少ないものであることに加え、通信している両者は同じローカルネットワーク上に存在しており、Ethernetヘッダに含まれているMACアドレスはどちらもVmware上の仮想システムを示しています。 そのため、この通信は管理者が本番環境に使われているのと同じ構築の仮想環境を用意し、自らが管理しているシステムに脆弱性が含まれているかどうかを実際に攻撃を行うことで調べようとして行ったものである可能性が考えられます(もっとも、その場合はこの通信が「攻撃」なのかどうか、という判断が揺らぎかねないような気もしますが)。

http://io.cyberdefense.jp/entry/2016/06/22/Docker%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%A6%E3%80%81Apache_Struts2%E3%81%AE%E8%84%86%E5%BC%B1%E6%80%A7S2-037%E3%81%AE%E3%82%84%E3%82%89%E3%82%8C%E7%92%B0%E5%A2%83%E3%82%92%E6%89%8B%E8%BB%BD%E3%81%AB%E4%BD%9C に記載されている方法によって、Docker上で脆弱性を含んだバージョンであるStruts2.5.10のサンプルアプリケーション(struts-2.5.10-apps.zip)を構築し、インターネットに接続していないコンピュータ上でこれを実行しました。 その上でtelnetを用い、localhostに向けて記録されたHTTPリクエストと同じ内容のリクエストを送信したところ、/etc/passwdの内容が返っており、この通信をキャプチャすると記録されたログとほぼ同じものであることが確認できました。 また、脆弱性が修正されたバージョンで同様にリクエストを送信したところ、このコードは無視され、本来返されるべきHTMLが正常に返されていることが確認できました。 また、リクエストが/struts2-rest-showcase/orders.xhtml であることから、記録された通信においても私が検証に用いたのと全く同じサンプルアプリケーションが使われていたのではないかと推測できます。 仮にそうであるならば、このアクセスに対する正常なレスポンスではデータ長が1000バイト前後に収まるはずであるため、Acknowledgment numberが1463も増えている今回の通信においては、このサーバー側システムには脆弱性が存在し、攻撃者は任意コード実行に成功したものと考えられます。

選-A-3.

自分がソフトウェア・ハードウェアを実装した部分について、自分とは意見が異なる実装を提案してきた人が現れた場合、あなたはどうしますか。
- 自分の実装のほうが優れていると思った場合どうしますか?
- 自分の実装のほうが優れていないと思った場合どうしますか?
- 相手の母国語が自分と違うために正確に議論が進まない場合はどうしますか?
- 相手がものすごく強硬で石頭でこちらのいうことを何も聞かず実装を勝手に修正してしまった場合どうしますか?

意見が異なる実装が提案された場合、まず互いに議論を進めることが最優先だと考えます。 自分の実装と相手の実装それぞれについて、それを採用する利点は何か、そして欠点は何かをお互いに理解し、その上でどちらを採用するかを決めるのが理想的です。 異なる実装を提案してくるということは、既存の(自分の)実装に対して差別化できる何らかの理由があるものと考えられます。 そのうえで自分の実装の方が優れていると思った場合、以下の状況が想定できます。

自分の実装には、自分が見逃している欠点がある。
2. 自分の実装に存在する欠点を、自分は理解していない。
3. 自分の実装について、自分が思っている利点が相手にとっては小さく、欠点が相手にとっては大きく感じられている(または、人によってどちらが良いか意見が割れるような問題である)。
またこれは相手にとっても同様のことがいえます。

ここで議論を交わすことによってそれぞれの実装の利点と欠点を互いに理解することができれば、1,2のパターンだった場合は誤った認識を正すことで合意に至れるのではないかと思います。または、3のようなパターンに移行する可能性もあります。 議論を進めた結果として3のようなことが争点になった場合、そのソフトウェア・ハードウェアの利用方法について自分と相手の想定が食い違っているのではないか、という可能性についてまず考慮したいと思います。 たとえば自分が想定している利用方法では特定の関数を数回しか呼び出さないものであり、一方で相手にとってはその関数が数万回呼び出されるべきものであった場合、自分にとっては少しのオーバーヘッドなら無視できるが、相手にとってはそれが大きな問題となるため、少しでも高速な実装を採用するべきだということになります。 このような利用方法の想定の違いについての齟齬を正すことは、お互いの認識をすりあわせるという点ももちろんですが、実装をしているときに考慮していなかったことについて気付かせてくれる機会でもあるため、相手がどのような利用を考えているのかは知るようにするべきだと思います。

また、その分野について自分や相手よりも詳しいと思われる第三者に意見を求めるのも重要だと考えます。 自分と相手の二者では、いくら意見を交換してもその問題についての知識が足りないために適切な決定に至れないおそれがあり、また自分と相手の両方が認識していなかった別の要素が存在している可能性もあるからです。

そして、自分と相手の実装がどのぐらい異なっているのかによっても、対処の仕方が変わってくると思います。 小規模な修正で、変更する範囲が小さいとか、パフォーマンスが大きくは変わらない、全体にとっては些細な修正であるというような場合には、実際には「どちらでもいい」可能性もあります。 また小規模な修正であっても、先述した利用シーンの想定での例のように、状況や使う人にとっては明確な違いが出る場合もあります。 相手が異なる実装の提案をしているという時点で、「どちらでもいい」の基準の閾値を越えている可能性の方が高いだろうと想定できるのですが(両者の誤解や見逃しが解消されたうえでのことで、ですが)、そうであるならば、両方の案を採用して設定で切り替えをできるようにするとか、ケースによって使い分けるようにするのが最も適切である、という可能性があることについては忘れないようにしたいです(もっとも、ハードウェアではこれは難しいことも多いかと思います)。 また大規模な修正であるならば、相手が単なる提案だけをしているのか、それとも実際に代替となる実装を用意しているのかも重要になってきます。 まだその代案での実装が存在しないのであれば、その実装にかかるコストや、その実装を行った場合に他の箇所に予期しない影響が出ないかどうか、既に自分が作った実装を使っている人やものに影響が出ないかどうかについても考える必要があります(これは小規模な修正であっても起こりうることですが、大規模な修正であればそのリスクは増えていくと思います)。 変更の範囲が大規模であればあるほど、変更を加えることによって全体の設計思想の一貫性が崩れてしまう可能性を含んでいることについても考えるべきで、もしかしたら、その部分の変更だけではなく全体の見直しも含めた修正が必要になることがあるかもしれないと思います。 このような場合、全体の設計思想と照らし合わせて本当にメリットになるのかどうか、ということについても相手の考えを問うべきだと思います。 逆に、自分が、またはチーム全体が、どのような設計思想のもとでそれを作ろうとしているのか、それをしっかりと意識したうえでの実装であったのかを自分に問うことも同様に必要です。

相手の母国語が自分と違っていて議論が正確に進まないというのは、かなり難しい状況だと思います。 (もっとも技術の世界では、とにかく英語が使えるなら相手の言っていることが全くわからない、ということにはなりづらいのではないかと思います。 実際にそのような状況が起こった場合には仕方のないことではあるのですが、まずこのような状況が発生しにくくなるように、個々人が、つまり私が、より高い英語のスキルを身に着けられるように努力することを忘れないようにしたいです。) やはり基本的には、これまで述べてきたような観点から相手の/* 誤字 -> 相手と */議論が進められるように根気よく議論していくのが一番であると思っています。 その上で、言語の違いが原因でなかなか議論が進まないのであれば、議論を行っている方法についても見直しをします。 たとえば口頭で議論をしている場合、発言の内容はログとして残ることがなく、聞き間違え、単語の意味がわからない、相手の言っている意味を理解しようとするのに必死になってその中身を吟味する余裕がない、などの副次的な問題が発生します。 また、メールや掲示板の形式で議論をしている場合、技術的な問題だけでなく言語間の差異による認識の違いを正すための発言がどうしても多くなってしまうため、やりとりの件数が増えてしまって本筋の議論が見失われがちになり、また一つのやりとりで多くの内容のメッセージを送るためやりとりの間隔が長くなるので、時間も多く必要になります。 ただし、メールおよびツリー型のBBSならば、言語の差のために正しく伝わらなかった情報を修正するために議論が脱線したとしても、もともとの発言に戻って本筋の議論を続けることができるという利点があります。 IRCのようなテキストチャットは、発言のログが残り、かつリアルタイムにやりとりをすることができるので、このような場合には比較的適していると思います。 しかしながら、チャットのログが増えるとやはり問題の本筋に関する発言が(文字通り)見失われてしまう可能性があります。 以上のメリットとデメリットから、このような場合の議論はできるだけテキストベースで行うようにして、またリアルタイム性が確保できるようにチャットの形をとりながら、要所ごとにそれまでの議論をまとめた文章をどこか別の場所に置くようにすれば、言語の違いを克服して議論を進めやすくなるのではないかと思います。

もしも、相手がものすごく強硬で石頭でこちらのいうことを何も聞かず実装を勝手に修正してしまった場合、これは難しいというより厄介な状況だと思います。 まず、相手が別の箇所の実装やプロジェクトでも同じようなふるまいをしているのかどうかを調べます。 そして、やはり別の箇所でも強硬で石頭なふるまいをしているようであれば、その相手自体が厄介な相手なのではないかという懸念が生まれるので、できるだけ自分と相手だけではなく第三者にも議論に参加してもらえるように助けをもとめたほうがよいと思います。 そのような様子があまり見当たらないのであれば、その相手にとってその実装はかなりの重要性を持っているのではないかと推測できます。 そのため、なぜ相手がその実装に拘ろうとしているのかについて聞くことで理由を知ることができるかもしれません。 とはいえ、相手が勝手に実装を修正してしまったのなら、ゆっくり話し合いをしている場合ではないことも多いのではないかと思います。 それをもともと実装した人である自分の同意を得ないうちに修正をしてしまうという状況は、チームや開発コミュニティ内でのルールや作法に違反している可能性が高いので、そのような場合は自分と相手だけで片付けようとせずに、ほかのメンバーにも相談して適切な対処方法を決めるようにします(チームの外にいる人間が既存の実装への修正の権限を持ちながら突然やってきて変更を加えていく、というのはむしろその状況自体が理解に苦しむので考えないことにします)。

全体を通して、できるだけお互いに間違った認識がないように注意を払いながら議論を続けていくことで合意を目指す、というのが基本的な方針です。 そして、状況が難しくなりそうならできるだけ早く第三者にも意見を求めていくべきだと思っています。

選-A-5.

以下のプログラムはLinuxカーネル3.8〜4.4に存在する脆弱性を悪用しています。このプログラムの実行により発生する不具合を説明してください。また、この脆弱性をさらに悪用することでroot権限昇格を行うエクスプロイトを記述し、自分が試した動作環境や工夫点等を説明してください。加えて、このような攻撃を緩和する対策手法をなるべく多く挙げ、それらを説明してください。 完全には分からなくても構いませんので、理解できたところまでの情報や試行の過程、感じた事等について自分の言葉で記述してください。また参考にしたサイトや文献があれば、それらの情報源を明記してください。

#include <stddef.h>
#include <stdio.h>
#include <sys/types.h>
#include <keyutils.h>
 
int main(int argc, const char *argv[])
{
    int i = 0;
    key_serial_t serial;
 
    serial = keyctl(KEYCTL_JOIN_SESSION_KEYRING, "leaked-keyring");
    if (serial < 0) {
        perror("keyctl");
        return -1;
    }
 
    if (keyctl(KEYCTL_SETPERM, serial, KEY_POS_ALL | KEY_USR_ALL) < 0) {
        perror("keyctl");
        return -1;
    }
 
    for (i = 0; i < 100; i++) {
        serial = keyctl(KEYCTL_JOIN_SESSION_KEYRING, "leaked-keyring");
        if (serial < 0) {
            perror("keyctl");
            return -1;
        }
    }
 
    return 0;
}

このプログラムは、CVE-2016-0728を悪用しています。 プロセスが使用しているセッションキーリングを、システムコール呼び出しによって別のキーリング(leaked-keyring)に切り替えたのち、それと同じ名前のキーリングへの切り替えを複数回試みています。 結果として、同じ名前のキーリングへの切り替えを行おうとした際に、keyctlシステムコール内に存在する脆弱性のために、内部的に生成された新しいキーリング(つまりleaked-keyring)の解放が行われず、このキーリングに対する参照が残り続けてしまいます。 keyctlシステムコールは鍵保存サービスの一部分であり、鍵保存サービスはそれぞれの鍵が使用されている数を表す参照カウントを持っていますが、上記の解放漏れのために、同じkeyring名でのkeyctl(KEYCTL_JOIN_SESSION_KEYRING,name)呼び出しが行われると、そのたびに参照カウントがインクリメントされ、この参照はプログラムが終了しても残り続けます。 よってこのキーリングは鍵保存サービスに残り続けてしまいます。

そこでまず、キーリングの参照リークが発生することを確かめようと、実際にDebian8.6のインストールされたマシン上でこのプログラムの実行を試みました。

$ uname -srvmo
Linux 3.16.0-4-amd64 #1 SMP Debian 3.16.36-1+deb8u2 (2016-10-19) x86_64 GNU/Linux

Linuxカーネルのバージョンが3.16であることから、脆弱性が存在しているバージョンであると思われます。 しかしながら/proc/keysの内容はこのプログラムの実行前と実行後で変化がなく、参照のリークを確かめることはできませんでした。 これは、DebianがこのLinuxカーネルの脆弱性に対してセキュリティアップデートを行っているため、パッチによって既に修正されているのだと考えました。 そこで、環境をVirtualBox上のDebian8.8に移し、 https://www.debian.org/releases/jessie/mips/ch08s06.html.ja https://www.hiroom2.com/2016/05/18/ubuntu-16-04%E3%81%A7%E3%82%AB%E3%83%BC%E3%83%8D%E3%83%AB%E3%82%92%E5%86%8D%E3%83%93%E3%83%AB%E3%83%89%E3%81%99%E3%82%8B/ http://d.hatena.ne.jp/adsaria/20081104/1225766991 というようなWebページを参考にして、Linuxカーネル3.18.25のビルドおよびインストールを行い、このカーネルを脆弱性のあるカーネルで上書きすることを試みました。

カーネルのビルド前に、/security/keys/process_keys.cのjoin_session_keyring関数を調べ、このカーネルが脆弱性を含んでいることを確認しました。

$cat security/keys/process_keys.c | grep "keyring == new->session_keyring" -4 -n
792-		}
793-	} else if (IS_ERR(keyring)) {
794-		ret = PTR_ERR(keyring);
795-		goto error2;
796:	} else if (keyring == new->session_keyring) {
797-		ret = 0;
798-		goto error2;
799-	}
800-

keyringがnew->session_keyringと等しかった際の処理にkey_put(keyring);の行が存在しないことから、このソースコードは脆弱性を含んでいることがわかります。

さらに、KEYCTL_SETPERMを第一引数としてkeyctlを呼び出したときに実行されるであろう部分にprintkを仕込んで、カーネルの上書きが成功していることを確かめようと考えました。

$ cat security/keys/keyctl.c | grep "Hello" -A 3 -B 6 -n
910-long keyctl_setperm_key(key_serial_t id, key_perm_t perm)
911-{
912-	struct key *key;
913-	key_ref_t key_ref;
914-	long ret;
915-
916:	printk("Hello, World!");
917-
918-	ret = -EINVAL;
919-	if (perm & ~(KEY_POS_ALL | KEY_USR_ALL | KEY_GRP_ALL | KEY_OTH_ALL))

この状態でカーネルのビルドを行い、インストールしてカーネルの再構築を行いました。

$ uname -r
3.18.25-leak-test

再起動後にlinuxバージョンの確認を行ったところ、バージョン3.18.25に変化していることからカーネルの再構築が完了したと考え、再度プログラムの実行を行いました。

$ cat /proc/keys
3840ddc9 I--Q---   100 perm 3f3f0000  1000  1000 keyring   leaked-keyring: empty

なお、dmesgでカーネルの出力を調べたところ、 Hello, World! の文字列が出力されているのが確認できました。

また、VirtualBox上でCD版KNOPPIXバージョン7.2.0を実行し、この環境で参照リークの再現が可能かどうかを調べました。

$ uname -nr
Microknoppix 3.9.6

Linuxバージョン3.9.6であり、このバージョンのリリースはCVE-2016-0728発見前の2013年であるためやはり脆弱性を含んでいることが予想され、結果としてkeyringの参照漏れが確認できました。

次に、この脆弱性を用いたroot権限昇格を行うエクスプロイトを"記述"します。

(おおよそ自明なものであるため回答可能文字数確保を目的として省略) // 原文ママ
(https://gist.github.com/PerceptionPointTeam/18b1e86d1c0f8531ff8f#gistcomment-1682326)
(gistで公開されているコードではなく、コメントで投稿されていたループ回数を2回減らしたほうのコードであることに注意)

これは、http://perception-point.io/2016/01/14/analysis-and-exploitation-of-a-linux-kernel-vulnerability-cve-2016-0728/ で紹介されていたものです。

「基礎からわかるTCP/IP ネットワークコンピューティング入門」によると、CPUはユーザーモード、カーネルモード(スーパーバイザモード)といった状態を持っており、ユーザーモードで動くアプリケーションがシステムコールを呼び出すと、動作モードがカーネルモードに変化します。 ユーザーモードでは特定のアドレス空間にしかアクセスできない一方、カーネルモードでは任意のメモリアドレスを参照することができ、任意の命令の実行が可能になります。 そのため、CPUがカーネルモードにあるときに任意のコードが実行された場合、容易に特権昇格を行うことができるようになります。 たとえば、commit_creds(prepare_kernel_cred(0));を実行すると、prepare_kernel_credはNULLが与えられた場合にデフォルト値としてuid=0,gid=0となるようなroot権限のcredentialsを返し(http://elixir.free-electrons.com/linux/v3.18.25/source/kernel/cred.c#L35 , http://elixir.free-electrons.com/linux/v3.18.25/source/kernel/cred.c#L600 )、commit_credsによってそのcredentialsを現在のタスクに適用する(http://elixir.free-electrons.com/linux/v3.18.25/source/kernel/cred.c#L401 )ため、以後そのアプリケーションはroot権限で実行されるようになります。

CVE-2016-0728を用いたroot権限昇格のエクスプロイトでも、このカーネルモードでの任意コード実行を目指します。

この脆弱性によって発生することは、キーリングの参照が残り続けること、そしてそれによって参照カウントのインクリメントが可能であることです。 key構造体(http://elixir.free-electrons.com/linux/v3.18.25/source/include/linux/key.h#L132 )は32bit整数値で参照カウントを持っているため、参照カウントのインクリメントを0x100000000回行うことでオーバーフローが発生し、参照カウントが0となるためにそのキーリングがもはや使われていないと判断され、ガベージコレクション機能によってキーが解放されます。 ただし、keyctl(KEYCTL_JOIN_SESSION_KEYRING,name)呼び出しによってjoin_session_keyring関数(http://elixir.free-electrons.com/linux/v3.18.25/source/security/keys/process_keys.c#L753 )が呼ばれて参照リークが発生する場合、内部的に新しいキーリングを作ろうと試みるため、prepare_creds();が呼ばれてキーリングの参照カウントが1加算され、そしてjoin_session_keyring関数から抜ける際にabort_creds関数によって再び参照カウントが1減少します。 しかしながら、abort_creds関数は内部的にcall_rcuを呼び出し(http://elixir.free-electrons.com/linux/v3.18.25/source/kernel/cred.c#L141 )、そのコールバック内でput_keyを遅延実行しようとします。 そのため、参照カウントのインクリメントに対してデクリメントが遅れ、カウントが現在のループ回数よりも多くなってしまうので、予期しないタイミングで参照カウントがオーバーフローを起こして0となってしまうおそれがあります。 そのため、残りのループ回数が半分になるたびにsleep(1)を呼び出して待機することで、意図しないオーバーフローを防ぐ必要があります(参照カウントのデクリメントが遅れても、呼び出した回数の2倍より大きくはならないためオーバーフローが起きることがない)。 このようにしてキーリングのメモリ空間を解放すれば、そのメモリ空間を上書きし、さらにキーリング構造体が持っている関数ポインタの位置に実行したい関数のアドレスを書き込み、システムコール内でその関数を呼び出させることで、CPUがカーネルモードの状態で任意のコードを実行可能になります。

そのため、先ほど参照カウンタが0になったことによって解放されたメモリアドレスを確保する必要があります。 エクスプロイトコードでは、fork()によってプロセスを分け、メッセージキューを「たくさん」用意して、そこにちょうどkey構造体と同じだけのサイズを持つ構造体を用いてメッセージ送信を「たくさん」行っています。 メッセージはlong型のヘッダと自由な長さのメッセージ内容からなる構造体である(http://www.geocities.co.jp/Athlete-Samos/7760/study/msgkyu1.html )ため、メッセージ内容の長さをsizeof(key)-sizeof(long)にすれば、ちょうど全体がkey型の構造体と同じサイズになるようにすることが可能です。 その上で、本来revoke関数へのポインタが配置されている位置にcommit_creds(prepare_kernel_cred(0));を実行する関数のアドレスを格納しておきます。 それにより、解放された領域にこのメッセージが割り当てられた場合、システムコール内でrevoke関数が呼ばれた際に特権昇格の命令が実行されます。

ただし、本来ユーザーモードではcommit_credsおよびprepare_kernel_credはメモリ保護されていて使用できないため、いくら実行時にカーネルモードで処理が行われるとしても、その命令を記述することができません。 そのため、予めこれらの命令が仮想メモリ空間上のどの位置に配置されるのかを調べておき、そのアドレスを直接参照することでcommit_credsとprepare_kernel_credを示すことができます。 仮想メモリ空間において、カーネル領域は同じLinuxカーネルならば同じ仮想アドレス上に展開される上、そのアドレスは/proc/kallsymsで確認可能なため、その中身を見ることで調べたり、攻撃の前に別のマシンで調べておくこともできます。

実際にこのエクスプロイトを用いて特権昇格を試みた結果を書きます。

$ gcc exploit.c -o exploit -lkeyutils -Wall
$ ./exploit attack
uid = 1000, euid = 1000
[+] increfs...
[+] finish increfs
[+] fork...
exploit...
強制終了

スタックトレースは、以下のようになりました。

[  +0.000003] Stack:
[  +0.000001]  ffffffff81091da6 0000000000400ae8 8948818f00000000 ffff880157
[  +0.000002]  0000000000000000 ffff880157c65280 ffff880157c652a1 ffffffff81
[  +0.000002]  00007fff92148680 ffff880157c65280 00000000fffffffd ffffffff81
[  +0.000001] Call Trace:
[  +0.000004]  [<ffffffff81091da6>] ? wait_task_inactive+0xa6/0x160
[  +0.000004]  [<ffffffff81244657>] ? key_revoke+0x37/0x80
[  +0.000002]  [<ffffffff81247380>] ? keyctl_revoke_key+0x30/0x70
[  +0.000003]  [<ffffffff81548e0d>] ? system_call_fastpath+0x16/0x1b
[  +0.000001] Code: c0 48 83 c4 30 5b 5d 41 5c 41 5d 41 5e c3 66 83 43 08 01
[  +0.000016] RIP  [<ffffffff81548345>] _raw_spin_unlock_irqrestore+0x5/0x20
[  +0.000002]  RSP <ffff8800db31bf08>
[  +0.000001] CR2: 00000000004009d0
[  +0.000002] ---[ end trace c403a12e9768e731 ]---

wait_task_inactiveで終了しているようでしたが、カーネルのkey_revokeのソースコードからwait_task_inactiveへの呼び出しは見つけられず、何らかのセキュリティによって強制的にプロセスを遮断されたのではないかと思いましたが、よくわかりませんでした。 また、/proc/kallsymsでkey_revokeやkeyctl_revoke_keyのアドレスを表示しましたが、このスタックトレースに表示されているアドレスとは異なっており、またその差もばらばらであったため一定のオフセット値によってずれているわけでもなさそうなので、ここで手詰まりとなりました。

同じプログラムを何回か試したところ、

$ ./exploit attack
uid = 1000, euid = 1000
[+] increfs...
[+] finish increfs
[+] fork...
exploit...
[+] keyctl_revoke: Input/output error
uid = 1000, euid = 1000
$ whoami
tatamo
$ exit
tatamo@debian:~$ ./exploit exp
uid = 1000, euid = 1000
[+] increfs...
[+] finish increfs
[+] fork...
exploit...
[+] keyctl_revoke: Required key not available
uid = 1000, euid = 1000
$ whoami
tatamo
$ exit

のように、異なるエラーメッセージが出てroot権限を得られないままプログラムが終了することがありました。 これは、オーバーフローを発生させた瞬間もやはりabort_credsの非同期性のために予期せぬオーバーフローが発生しうること、直後にfork()を行ったことによってさらに参照カウンタが動いていること、また解放されたメモリアドレスへの再割り当てを試みたが結果として何も割り当てられなかったこと、が原因なのではないかと推測します。

このような攻撃を防ぐための技術として、Supervisor Mode Execution Prevention(SMEP)とSupervisor Mode Access Prevention(SMAP)が挙げられます。 これはともに、カーネルモードのCPUがユーザー空間からの攻撃を防ぐために設計されたものです。 SMEPは、ユーザー空間上にあるコードをカーネルモードで動作しているCPUから実行することを禁止させます。 SMAPはIntel製のCPUでサポートされている機能であり、SMEPの実装でもあります。 これはカーネルモードで動作しているCPUのユーザー空間上のメモリに対しての読み書きを禁止します(https://en.wikipedia.org/wiki/Supervisor_Mode_Access_Prevention )。 これによって、カーネルが意図しない形でユーザー空間にアクセスした場合にはページフォールトを発生させ、今回の脆弱性を悪用したコードによるカーネルモードでの任意コード実行を防止することができます。 またubuntuでは、Kernel Address Display Restrictionと呼ばれる機能により、一般ユーザーが/proc/kallsymsを表示しようとした場合、すべてのアドレスが0で埋められて表示されるようになっています(http://inaz2.hatenablog.com/entry/2015/03/21/175433 )。 攻撃者が多くのLinuxディストリビューション上で動作するようなエクスプロイトコードを書きたいと思った場合、/proc/kakllsymsから動的にcommit_credsなどのメモリアドレスを取得しようとすることが考えられるため、そのような攻撃に対しての効果が期待されます。

以降、この問題を解こうとして感じたことを述べます。 まず、私はこの回答欄にエクスプロイトコードを"記述"しましたが、それは調べたら見つかっただけのものであって"作成"してはいません。 それどころか、このコードの一部は理解することすらできませんでした(なぜfork()をしているのか、メモリ確保のルールをどのように利用して解放されたアドレスに再び別のオブジェクトを割り当てるのか?)。 今の私にはこのようなエクスプロイトを作成することは逆立ちしても不可能であると感じたので、せめてできるだけ理解しようと試みました。 しかしそもそも、実際に脆弱性の再現を行ううえで、Linuxカーネルのソースコードを読むこと、そして再ビルドしてインストールすることはこれまで一度も行ったことがなかったため、なかなかスムーズに進まず、スタートラインにすら立てていない感覚に焦りを覚えていました。

実はカーネルを再構築して問題のコードを実行した後、/proc/keysをroot権限で表示するとユーザー側で実行していた参照リークを起こしているキーリングを見ることができないことを知らず、sudo catで表示していたために失敗したと思い込んでいました。 同様にdmesg -Hを実行するとlessされて表示されましたが、マウススクロールで表示が下に下がらなかったために一画面目の一番下の行が最新のカーネルログだと思い込んでしまい、実際にはカーネルに仕込んだ”Hello, World!”のメッセージが表示されていたにもかかわらず見落とし、しばらくカーネルの再構築に失敗したのではないかと考えていました。 これで悩んでかなりの時間を費やしたのですが、本当は想定通りに参照リークが発生していたこと、メッセージが表示されていたことに気付いたときは、なんだ成功していたのか、という気持ちと同時に、静かな興奮が沸き上がってくるのを感じました。 問題には直接関係なく、正しくカーネルの再構築ができているか確かめるのに役立つだろうと思って、ビルドの前にカーネルのソースコードを改変し、keyctl(KEYCTL_SETPERM, serial, KEY_POS_ALL | KEY_USR_ALL)システムコールが呼ばれたときにprintk(“Hello, World!”);が実行されるようにしていたことです。 実際にやってみれば(ビルドにかなりの時間がかかるものの)Linuxカーネルのインストールは思った以上に簡潔な手順で完了し、しかも自分が書き換えたコードがこうも容易にOSの中核部分で動いているのだ、これだけ自分がセキュリティを破るのに苦労しているカーネルというものはプログラミング可能なものなのだ、と実感したときのワクワク感はしばらく忘れられないと思います。


お目汚し失礼致しました。