mrubyビルド設定項目解説

この投稿はmruby Advent Calendar 2013の24日目の記事です。
23日目はyamanekko さんのmrubyをRaspberry Pi(bare metal)で動かす: Windows編、25日目はyukihiro_matzさんの24時間一人mrubyハッカソンです。

はじめに

mrubyはカスタム性の高さも売りの一つで、/include/mrbconf.hを弄ることである程度動作を変えられるようになっています。
しかし、設定の多さに圧倒され、すべてデフォルトのまま使っているという人も多いのではないでしょうか?
そこで今回はわかる範囲で各種設定について説明してみようかと思います。

mrubyの設定(特にメモリーレイアウトに関する部分)は環境により動作が異なることが多々あります。
今回は筆者の開発環境であるVisualStudio2013にて試したものであり、gccやclangや特定環境専用のコンパイラなどでは結果が異なる可能性があることを念頭に置いてお読みください。

2013/11/25当時(git hash: f5bd87b9e6d0d8a84cf866b4847c1416e4f5c622 )の物です。
それ以降のmrubyを使用する場合は以下の解説の通りではない可能性があります。

2013/12/24 23:30追記
MRB_GC_TURN_OFF_GENERATIONALとMRB_GC_FIXED_ARENAの説明が間違っていたため、関連記述を変更しました。
また、間違いの指摘をしてくださったMatzさん、どうもありがとうございます。

MRB_USE_FLOAT

実数を扱う型をdoubleからfloatに変更します。
VS2013でデフォルト設定であればmrb_valueのサイズは16byteですが、これを定義すると8byteになります。
しかし当然ですがfloatになるので精度がかなり落ちます。
実数なんか使わないからメモリー消費量抑えたい場合などに使うといいでしょう。

MRB_INT16

整数を扱う型をint32_tからint16_tに変更します。
しかし、ポインターなどを扱う都合からmrb_valueのサイズが縮まったりはしません。
おそらくは16bit CPUなど2byte演算が常用される環境用なのだと思われます。

MRB_INT64

整数を扱う型をint32_tからint64_tに変更します。
MRB_INT16と同時には使えません。
デフォルトの設定(MRB_USE_FLOATやMRB_XX_BOXINGが無効の環境)では特にmrb_valueのサイズが増えたりはしないので、32bit範囲を超える演算を多用するなら有効にするのもよいでしょう。

MRB_NAN_BOXING

実数の特殊状態であるNaNの定義の一部を利用して値を投入することでメモリー消費量を落とす機能を有効にします。
MRB_USE_FLOAT、MRB_INT64とは同時には使えません。
一般的な環境であればこれが有効になるとmrb_valueのサイズが16byteから8byteになります。
ただし実数演算の実装依存なため、環境により動かない可能性があります。

MRB_ENDIAN_BIG

一部構造体のメンバーの配置順を変更します。
名前から誤解されがちですが、整数を格納するエンディアンがビッグエンディアンになったりはしません。
メンバーのアドレスが変わるぐらいしか変化がないので、通常の環境であればON/OFFどちらでも特に影響はない設定です。

MRB_WORD_BOXING

ポインターは下位2bitは使われないことと、整数の1bitを犠牲にすることでメモリー消費量を落とす機能を有効にします。
MRB_NAN_BOXINGと同時には使えません。
mrb_valueは4byteになりますが実数がポインターで保持されるため、実数を多用すると逆にメモリー消費量が増えてしまいます。
VS2013ではバグなのか私が知らない仕様なのか、MRB_INT16かMRB_INT64と共に定義すると一部ビットフィールドが無視されサイズが増えてしまいます。

MRB_FUNCALL_ARGC_MAX

mrb_funcall関数で渡せる引数の最大数を設定できます。
mrb_funcall関数はここで設定した分の長さを持つmrb_valueの配列をスタックに確保するので、スタックに余裕がない環境の場合は小さく設定するとよいでしょう。
(といっても配列一つなので大して変わりませんが)
ちなみに、ここで設定された以上の引数を要求するメソッドもmrb_funcall_argvを使えば呼ぶことができます。
なので、mrbgemなどどういう設定で使用されるかわからないものを実装する場合、mrb_funcall_argvを使ったほうがいいかもしれません。

MRB_HEAP_PAGE_SIZE

インスタンスなどに使用するヒープ領域の1確保単位を設定できます。
1確保単位と書きましたが、単位はbyteではなくインスタンスで、1024なら新たなヒープを確保せずに1024インスタンスを割り当てることができます。
またヒープを使用するのはあくまでインスタンスそのものだけで、arrayの本体である動的確保部分などは別途mrb_mallocで確保されます。

MRB_USE_IV_SEGLIST

インスタンス変数を保持するデータ構造をハッシュからリンクリスト形式に変更します。
インスタンス変数が少なければハッシュ計算が不要な分高速になるかもしれません。
ちなみにインスタンス変数一つにつき1要素ではなく、(執筆時点では)4つのインスタンス変数を保持できる配列が1要素のリストになります。

MRB_IVHASH_INIT_SIZE

インスタンス変数を保持するハッシュテーブルの初期サイズを設定します。
MRB_USE_IV_SEGLISTが定義されている場合は無視されます。

MRB_IREP_ARRAY_INIT_SIZE

何に使われているか不明です。
というより、grepしても出てこなかったので今はもう使われていないのではないかと思われます。
2013/12/24 23:30追記
hash: 9fab01caでmruby本体から削除されたようです。

MRB_GC_TURN_OFF_GENERATIONAL

世代別GCを無効にします(有効ではなく無効な点に注意)。
定義すると単なる3色インクリメンタルマーク&スイープになります。
逆にコメントアウトすると、追加で古いオブジェクトは暗黙でマークしてGCする処理が加わるようです。
GCの性能比較はよくわからないので割愛。

KHASH_DEFAULT_SIZE

Hashのデフォルトの初期サイズを設定します。
Hashクラスだけではなくmruby内部で使われるハッシュテーブル(名前空間の解決用など)にも適用されるようです。

POOL_ALIGNMENT

一括解放や確保できるメモリー空間として使用される独自のメモリープールにおいて、メモリー確保時に先頭アドレスをアラインするサイズを設定します。
メモリープールは主にパース結果など内部表現を保持するために使用されています。
アラインされないと速度が低下したり、そもそも読み書きに失敗するような環境において調整することを目的としているようです。

POOL_PAGE_SIZE

メモリープールの1確保単位を設定します。
メモリープールの容量が不足するたび、このサイズ分新たに確保して動作します。
当然ですが、大きくすれば処理速度が上がり、小さくすればメモリー効率が上がります。

MRB_STR_BUF_MIN_SIZE

文字列構築用バッファ確保(具体的にはmrb_str_buf_new関数)でメモリーを確保する際の最小サイズを設定します。
mrubyで扱う文字列全般というわけではなく、あくまでCレイヤーでなんらかの文字列を構築する際のバッファにだけ影響する様子。
メモリー再確保のコストを減らすためか、設定値以下のサイズを取得しようとすると自動的に設定値まで広げて確保されるようです。
mruby本体に限ればすべての使用箇所で適切にバッファサイズを指定しているので、いっそ小さな値にするとメモリー効率は上がるかもしれません。
(大して使用されていないので微々たるものでしょうが)

MRB_GC_ARENA_SIZE

Cレイヤーで処理する際の一時オブジェクト保存域であるarenaの初期サイズを指定します。
あくまで初期サイズであり、溢れるたびに1.5倍されていきます。
よほど特殊な例を除き、ちゃんとした実装であれば100を溢れることは滅多にありません。
自身のコード由来で大きくすることを検討している場合、下記のURLを参照のうえよく考えてから行うようにしてください。
arenaの詳細は http://www.rubyist.net/~matz/20130731.html を参照のこと。

MRB_GC_FIXED_ARENA

arenaのサイズを固定にします。
その場合のサイズはMRB_GC_ARENA_SIZEになります。
固定のメリットは若干の処理速度の向上と、arenaを大量に使うコードでクラッシュするため早期発見ができることです。
省メモリーが必須な環境なら、事前に固定に変更したうえで負荷テストなどを行うとarena溢れを防止できて効率的かもしれません。

DISABLE_STDIO

エラー時の出力などコンソールへの入出力を行わないようにします。
標準入出力先が存在しない環境など用と思われます。

ENABLE_DEBUG

デバッグ機能を有効にします。
有効だとmrb_stateにcode_fetch_hookというメンバーが増え、そこに関数ポインターを代入することで、1命令実行するごとにコールバックされるようになります。
当然ですがこれが有効のままだと処理速度は低下するので、有効のままリリースするのは推奨されません。

終わりに

いかがだったでしょうか。
書いてる側からすると、ヒープやプールなど独自に管理するメモリー領域が使われていたり、ON/OFFできる機能が意外と多かったりなど、知らなかったことが意外と多くて勉強になりました。
これを元にいろいろもっと気軽にmrbconfをカスタマイズするユーザーが増えてくれたら幸いです。

ではまた。

mrubyの小ネタ集

前回の関連で得た知見をそのまま忘れるに任せるのもちょっと惜しいと思ったので、小さく小ネタ集として残しておきます。

ローカル変数は最大約512個

mrubyのバイトコード上レジスタは最大でも512個しかありません。
そしてローカル変数1つにつき1つレジスタが割り振られるため、ローカル変数は最大で512個しか使えません。
さらに、メソッド呼び出しなど各種処理でもレジスタを使うので、前後のコード次第で事実上はもう少し少ない数になります。
足りなくなるということはないでしょうが、そういう仕組みなのだということは覚えておくと役に立つこともあるかもしれません。

リテラルの文字表現は最大1023文字

整数実数文字列といったリテラルを文字で表記するとき、最大文字数は1023文字となります。
文字列の場合は””を含まない本体の文字数、8進数は先頭の0もしくは0_を除いた文字数、16進数も同じく先頭の0xを除いた文字数が1023までなら使用できます。

少し前まではMRB_PARSER_BUF_SIZEという設定をいじれば変更できたのですが、私がしたバグ報告の結果、コンフィグ上から削除され変更は推奨されなくなりました。
一応/include/mruby/compile.hにまだ定義されているので、興味のある人は最大65535の範囲でいじってみましょう。

リテラルは使用した回数だけ定義される

短い文字列は毎回書いてしまうこともありますが、mrubyは11/30現在までのところ同一のリテラルをまとめる機能はありません。
実際同じ文字リテラルを複数使うコードをmrbcしたものをバイナリエディタで覗くと同じ文字列が複数個ならんでいるのが確認できます。

メモリー効率を考えるなら複数回同じリテラルは使わず、一度変数に代入し使いまわすようにするといいようです。

post引数という定義がある

post引数という定義の引数があります。
具体的には

def func(normal, *rest, post)

のpostという引数が相当します。

これは可変長引数の後の通常引数で、解釈は先頭から通常引数、末尾からpost引数と確定させていき、残りを可変長引数と解釈するようです。

mrubyの小ネタとして書きましたが、普通にrubyにもあります。

通常引数・オプション引数・post引数の数は31個まで

通常の引数の数は31個までしか受け取れません。
32個以上受け取るコードを書いた場合、特にシンタックスエラーにはなりませんがバイトコードは壊れます。
同様にオプション引数、post引数も31個しか定義できません。

合計が31個ではなく各引数が31個なので、組み合わせれば90個を超える引数でも可変長引数を使わずに受け取れます。

ローカル変数の名前はコンパイル時に破棄されている

2014/05/17追記:公式でローカル変数情報が実装されました、今ではKernel#local_variablesも動作するようになっています。

rubyであればKernel#local_variablesでローカル変数名の一覧が取得できますが、mrubyの場合コンパイル時にローカル変数名はすべて破棄されています。
ではどう管理されているかというと、変数一つにつきレジスタが一つ割り振られ、レジスタの1番地などという形で扱われます。
ちなみに、当然のことですがグローバル変数やインスタンス変数などはちゃんと変数名はシンボルとして処理されています。

終わりに

いかがだったでしょうか。
主にバイトコード内の定義上の限界から攻めていったので、最大数がどーたらというのが多かった気がします。
一応全部実際にコードを書いて試したつもりですが、何かミスなどあればコメントくだされば直しておきます。

ではまた。

mrubyにバグ報告をした@その2

mrubyを弄る過程でいろいろバグを発見して報告したので一応書くだけ書いておきます。

MRB_NAN_BOXING次第でコンパイル結果が変わる

MRB_NAN_BOXINGという設定のON/OFF次第でコンパイルしたバイトコードが変わる=>別の環境にもっていっても動かない、というバグ。

そもそもMRB_NAN_BOXINGとはは何かというと、doubleやfloatのNaN表現に余裕があることを利用し、そこに値を入れることでmrb_valueのサイズを減らそうというもの。

なぜそれでバグが起こるかというと、mrb_vtypeというmrubyの型情報をMRB_NAN_BOXINGで入れようとしたのだが、NaNの定義上の都合から0だけは入らず、すべて+1したものを入れるようにしていました。
それをそのままバイトコード側にも反映したため、MRB_NAN_BOXING次第で型のインデックスが1ずれ、動作しなくなっていたわけです。

バグ報告をしたところ、irep_pool_typeというバイトコード内に入れる専用の型情報が用意され、そちらから実行時に変換する形で修正されました。

実際に修正された時のdiff
https://github.com/mruby/mruby/commit/71354b91cb48ff3a5c1e3c09492d18a3c30efcb9

MRB_PARSER_BUF_SIZEが65536以上でバイトコードが壊れる

MRB_PARSER_BUF_SIZEが65536以上で65536文字以上のリテラルを使おうとするとバイトコードが壊れて実行できなくなるバグ。
あくまでバイトコードのロードに失敗するだけなので、ソースコードから直に実行する場合は問題ない。

まずMRB_PARSER_BUF_SIZEが何かから説明すると、パーサーで扱う最大のリテラル長を定義したもので、子の文字数以上のリテラルはパース時にエラーとされる。

で、なぜ65536文字以上だとだめなのかは簡単で、バイトコード上ではリテラルの長さに2byteしか割り当てていないため。

コンパイル時にエラーするべきでは?と報告したところ、そもそも65536以上にすることがないと判断されたのか、設定項目から削除され、1024固定となりました。

実際に修正された時のdiff
https://github.com/mruby/mruby/commit/ed0d9f0066eb541ae4ceddf47bdd85112feccddf

コンパイル時にレジスタ使い切るとクラッシュ

mrubyのVM上のレジスタは最大で512個なのだが、これを超えて使うようなソースコードを読み込ませるとクラッシュするというもの。
具体的にはローカル変数が512個あるコードなどが当たる。
(mrubyのVMではローカル変数一つにレジスタが一つ割り当てられている)

これは単にエラー処理の実装が間違っていて、解放済みメモリーを触ってしまっているだけ。
このエラー処理に分岐するのがレジスタ使い切りぐらいしか存在しないため、エンバグ後発見されなかっただけと思われる。

実際に修正された時のdiff
https://github.com/mruby/mruby/commit/56a93a3357ab9f50a4105980e0b4d39e7d400e3b

mrubyのバイトコードフォーマット解説

この投稿はmruby Advent Calendar 2013の3日目の記事です。
2日目はsuzukazeさんのmruby-redisでランキングを実装、4日目はdycoonさんのGUIアプリケーションなどが保持するmrubyのオブジェクトのGC対策です。

はじめに

mrubyのエンジンはソースコードを逐次解釈して実行しているわけではなく、専用のバイトコードに変換して保持し、それをVirtualMachineの上で解釈して実行しています。
であれば当然コンパイル済みバイナリファイルというのも存在するわけで、ならば当然どんな仕組みになっているか気になるわけです。

というわけで、大まかなファイルフォーマットについて調べたので今回説明しようと思います。

2013/11/25当時(git hash: f5bd87b9e6d0d8a84cf866b4847c1416e4f5c622 )の物です。
それ以降のmrubyを使用する場合は以下の解説の通りではない可能性があります。

全体の構造

ヘッダーとセッションの配列からなります。
ヘッダーは必ず一つ、セッションは複数個ですが必ず終端セクションで終わります。

ヘッダー

C風に書くとこんな感じ

ubig8_t signature[4];
ubig8_t version[4];
ubig16_t crc;
ubig32_t size;
ubig8_t compiler_name[4];
ubig8_t compiler_version[4];

※ubig%d_t: 符号なし %d bitビッグエンディアンの整数です、ただしubig8_t配列は文字列の場合もあります

signature: 0x52 0x49 0x54 0x45固定(ASCIIでRITE)
version: 0x30 0x30 0x30 0x32固定(ASCIIで0002)
crc:

uint16_t CalcCrc(const uint8_t * const ptr, const unsigned int size) {
  unsigned int a = 0;
  for (unsigned int i = 0; i < size; i++) {
    a |= ptr[i];
    for (unsigned int l = 0; l < 8; l++) {
      a = a << 1;
      if (a & 0x01000000) {
        a ^= 0x01102100;
      }
    }
  }
  return a >> 8;
}

※ptrはファイル全体のうちヘッダーのはじめからcrcまでを除いた部分、sizeはそのバイト数

こんな感じらしいのだが実際動かしてみるとずれるのでたぶんエンバグしている、が、めんどいので調べていない。
mrubyの/src/crc.cに実装があるので、知りたい人はそちらを見るとよい。

size: ファイル全体の長さ、ヘッダーも含む
compiler_name: コンパイルに使われたコンパイラーの識別子、特に使用されていない
compiler_version: コンパイルに使われたコンパイラーのバージョン、特に使用されていない

セクション

セクションは全部で3種類存在します。

  • 実行コード本体
  • 元となったソースコードの行番号情報
  • 終端セクション

詳しい説明は後で書くとして、共通のセクションヘッダーは以下の通りです。

ubig8_t signature[4];
ubig32_t size;

signature: セクションの識別子です、ここでセクションが何の情報を持つかが分岐します、0x49 0x52 0x45 0x50(ASCIIでIREP)か0x4C 0x49 0x4E 0x45(ASCIIでLINE)か0x45 0x4E 0x44 0x00(ASCIIでEND\0)のどれかになります。
size: セクションのサイズです、共通のセクションヘッダーのサイズも含みます。

linenoセクション

元となったソースコードのライン番号情報です、signatureは0x4C 0x49 0x4E 0x45(ASCIIでLINE)になります。
linenoの追加ヘッダー以下の通りです。

ubig16_t count;
ubig16_t start;

count: ライン情報の個数です。
start: よくわかりません。

ここにはlinenoの詳細を書くべきなのでしょうが、ぶっちゃけ興味なかったので調べておらずわからないのですっ飛ばします。

endセクション

終端セクションです、signatureは0x45 0x4E 0x44 0x00(ASCIIでEND\0)になります。
追加ヘッダーは特にありません。

このセクションが存在した時点でセクションの読み取りは終了となります。
一応ファイルサイズなどの情報もあるのでこの情報はなくても実行できますが、おそらくは利便性か速度上の都合なのでしょう。

irepセクション

実行コード本体です、signatureは0x49 0x52 0x45 0x50(ASCIIでIREP)になります。
irepの追加ヘッダー以下の通りです。

ubig8_t version[4];

version: irepの命令セットのバージョンです、現在は0x30 0x30 0x30 0x30(ASCIIで0000)固定です。

また、irepデータ本体を1つ以上持ちます。
いくつになるかは実際にirepデータを読んでみるまでわかりません。

irepデータ

関数1つやクラスの定義など1スコープの処理をまとめたものです。
定義は以下の通り

ubig32_t size;
ubig16_t localCount;
ubig16_t registerCount;
ubig16_t childCount;
ubig32_t code_count;
Code code[code_count];
ubig32_t pool_count;
Pool pool[pool_count];
ubig32_t symbol_count;
Symbol symbol[symbol_count];

size: irepデータのサイズです、sizeそのものも含みます。
localCount: ローカル変数の個数らしいですが何に使うのかわかりませんでした。
registerCount: このirepデータを実行するのに必要なレジスタの数です。
childCount: このirepに紐づく子のirepデータの数です。
code_count: irepバイトコードの個数です。
code: irepバイトコードです。
pool_count: リテラルプールの数です。
pool: リテラルプールです。
symbol_count: シンボルリストの数です。
symbol: シンボルリストです。

さらにchildCountの分だけ、irepデータが後に続きます。
それらのirepデータにも当然childCountがついているので、childCountが1の場合でも何十個とirepデータが続いている可能性があります。

リテラルプール

コード中で使用する文字列/整数/実数のリテラルを保持しています。
各リテラルはフォーマット上は文字列として保管され、パース時点でmrb_valueへと変換されます。
定義は以下の通り

ubig8_t type;
ubig16_t len;
ubig8_t body[len];

type: mrb_vtypeの値がそのまま入っており、0:文字列、1:整数、2実数となります。
len: bodyの長さです。
body: 文字列で表現されたリテラルで、終端記号(\0)は含まれていません。

シンボルリスト

コード中で使用するシンボル(メソッド名やクラス名など)を保持しています。
各シンボルはフォーマット上は文字列として保管され、パース時点でmrb_symへと変換されます。
定義は以下の通り

ubig16_t len;
ubig8_t body[len];
ubig8_t terminate;

len: bodyの長さです。
body: 文字列で表現されたシンボルです、リテラルと違いこちらは続くterminateが必ず0x00固定なのでC言語文字列として扱うことができます。
terminate: 必ず0x00固定です、bodyをC言語文字列として扱うためで、実際load.cを覗いてみるとchar *にキャストして使用されています。

irepバイトコード

VM上で実行できるバイトコードです。
すべての命令がオペコードと引数を合わせて4byteとなっています。
書くまでもないですが、定義は以下の通り

ubig32_t body;

body & 0x0000007F: オペコードです、2013/11/25時点では0~75まで定義されています。
body & 0xFFFFFF80: 引数ですが、オペコードにより引数の数とサイズが異なります。

各オペコードの説明まで始めるとそれだけで本一冊書けてしまうのと、ファイルフォーマットとは関係が薄いためここでは解説しません。

終わりに

いかがだったでしょうか。
実はオペコード関連も多少は読んでいて、ローカル変数はインデックス管理で名前が消滅しているとか面白い話もあったのですが、文中でも書いたように一冊本が書ける勢いなので省略しました。
mrubyを使う上ではあまり役に立たないかもしれませんが、個人的には面白かったです。

おまけとして、上記通りにパースして、ちょっとだけ命令の翻訳も行うようなアプリを書いてみたのでおいておきます。

parse_mruby2.zip

ではまた。

※2014/05/14付けでLVARセクションが追加されました、それらについての追加情報は以下を参照のこと。
rubyのバイトコードフォーマット解説その2

th135_ai v1.01リリース

http://wordpress.click3.org/garakuta/th135_ai.zip

バグ報告のあった「お互いが同時に射撃するなどするとTh135Data.player[0].objectにplayer2がownerのデータが含まれることがある」に対応した版になります。
原因は簡単で、キーが同一のobjectが複数個発生していて、その都合でobjectを取り違えていたというもの。
現状のままだとどうしようもなかったので、object_arrayメソッドを追加して、objectメソッドを非推奨としました。
一応playerのobjectに他方のobjectが入り込まないようにはしましたが、場合によっては同playerのobjectですら重複しそうなので非推奨です。

どうでもいい話ですが、バグ修正だけだしver1.00aにしようかとも思ったところを、紛いなりにもメソッド追加なので数字あげました。
個人的ルールとして、機能追加は数値up、バグ修正は末尾アルファベット追加すでにある場合はアルファベットをインクリメントとしています。

エクスプロイトを書きつつ学ぶWindowsセキュリティー機能 ~番外編SEHOP~

自分の環境だけ守れるセキュリティ機能もいいが、理想を言うならすべての環境で有効な防御こそを目指したい。

というわけで今回は前回から予定を変更して番外編、お題目は再びSEH overwrite攻撃について。
あ、今回からタイトルもつきました、番外編ですが。

第一回の記事ではSEH overwrite攻撃への対策は完璧だなどとのたまったわけですが、SoftwareDEPはWindowsXPなど一部環境ではコンパイルオプションで有効にできず、安全性が保たれるのはあくまでユーザーがDEPを有効にした環境下だけでした。
そこで今回は新たにセキュリティ機能を導入し、SEH overwrite攻撃をSoftwareDEP抜きで防げるところまで持って行くのを目標とします。
なぜ番外編なのかというと、今回は厳密にはWindowsのセキュリティー機能ではないからです、詳細は後述。

いつものようにサンプルコード集をDLしてください。
sehop.zip

おさらいと補足事項

SEH overwrite攻撃とは、スレッドスタック(以降スタック)上の例外ハンドラへのポインターを上書きし、任意のコードを実行させる攻撃。
全てのバイナリでSafeSEHが有効なら、登録済みの正規例外ハンドラかヒープ上しか例外ハンドラに指定できなくなる。
SoftwareDEPが有効ならヒープ上も例外ハンドラに指定できなくなる。

ここまでが第一回の記事で書いたことです。

ちなみにSEH overwriteは前回のBuffer Security Checkで検出することはできません。
チェックがreturn直前であるため、それ以前に例外を発生させて例外ハンドラへと処理が移ってしまえばチェックが実行されることはないからです。
逆にいうと、Buffer Security Checkがあるからこそ、それを回避して攻撃するためにSEH overwriteへと目が向いたともいえます。
(そういう意味では、記事を書く順番が逆ですね)

exploit1:SEHOP

さて、冒頭で「ユーザー設定によらない防御を行う」と書きましたが、今回最初に紹介するSEHOPはユーザーの設定を必要とする機能の一つです。
趣旨ずれてるじゃねえかと思われる人もいるかとは思いますが、ひとまず付き合ってください。
では行ってみましょう。

SEHOPとは

SEH overwrite攻撃には実は弱点があります、それは既存のSEHの情報を読むことができない、ということです。
それを利用したのがSEHOPで、SEHのリスト構造の終端に特定の例外ハンドラを設定しておき、例外発生時に例外ハンドラをたどっていってその例外ハンドラが見つからなかった場合、例外ハンドラを上書きされたと判断して強制終了する、というものです。
これを利用すれば、SafeSEHがかかっていないバイナリが紛れていようが、ヒープ領域に例外ハンドラを設定されようが関係ありません。

実際に試してみよう

今回は第一回の記事のexploit2と同じコードを使います。
・dllmain.h

#ifdef DLL_EXPORT_FLAG
#define EXPORT __declspec(dllexport)
#else
#define EXPORT __declspec(dllimport)
#endif /* TEST_EXPORTS */

EXPORT unsigned int xor(const unsigned int a, const unsigned int b);

・dllmain.cpp

#include <cstdio>

#include <Windows.h>

#define DLL_EXPORT_FLAG

#include "dllmain.h"

void call() {
  ::MessageBoxA(NULL, "message", "title", MB_OK);
  ::exit(0);
}

unsigned int xor(const unsigned int a, const unsigned int b) {
  return a ^ b;
}

BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved) {
  return TRUE;
}

・test.cpp

#include <cstdio>

#include "dllmain.h"

int main(const unsigned int argc, const char * const * argv) {
  FILE * const fp = ::fopen("file", "rb");
  unsigned int size;
  char s[256];
  ::fread(&size, 4, 1, fp);
  ::fread(s, 1, size, fp);
  const unsigned int key = 0xAA;
  for (unsigned int i = 0; i < size; i++) {
    ::printf("%02x ", xor(s[i], key));
  }
  ::fclose(fp);
  ::getchar();
  return 0;
}

バイナリでのみ提供されているライブラリを使用しているためsafesehがかけられないという前提です。

サンプルのexploit1を開き、その中のtest.exeを実行してみましょう。
以前と同じく、callの内容が実行されたかと思います。

では、SEHOPを有効にしてもう一度test.exeを実行してみましょう。
(SEHOPを有効にする方法はEMETがおすすめ)
すると今度はメッセージボックスが上がらず強制終了したかと思います。

exploit1:SEHOPまとめ

softwareDEPとSafeSEHがOFFでもSEHOPが有効ならSEH overwrite攻撃は防げる。

しかし弱点もあって、SEHOPはvista以降のOSでなければ使用できない上、コンパイルオプションでは極一部を除き有効にできず、サーバー向けを除きデフォルトOFFと
エンドユーザーに提供するのがなかなかに難しい状況になっています。

では、どうするか。
コンパイルオプションで有効にできないなら自作してしまえばいいのです。

SEHOPの自作

というわけで長い前置きも終わって番外編本編、今回はセキュリティー機能を自作する回になります。
いきなりですが、まずは実際に動作させてみましょう。

実際に試してみよう

今回はexploit1のコードでsehopを実装したdllをLoadLibraryするだけのコードを使います。
差分だけ貼ると

 #include <cstdio>
 
+#include <Windows.h>
+
 #include "dllmain.h"
 
 int main(const unsigned int argc, const char * const * argv) {
+  ::LoadLibraryW(L"sehop.dll");
   FILE * const fp = ::fopen("file", "rb");
   unsigned int size;
   char s[256];

となります。

ではさっそくSEHOPを無効にした環境でexploit1_sehopを開いてtest.exeを実行してみましょう。
ちゃんと強制終了したかと思います。
試しにsehop.dllをリネームしてみて、今度はメッセージボックスが出るのを確認してみるのもいいでしょう。

次からは、このsehop.dllが何をしているのかを説明します。

sehop.dllの仕組み

やっていることはwindowsのセキュリティー機能としてのSEHOPと同じで
識別用のSEHレコードを挿入し、例外時にフックして例外ハンドラを実行する前にSEHレコードの存在をチェック、見つからなければ強制終了するというものです。

例外のフック方法

まずは例外時のフック方法から解説します。
コードは以下のとおり。

FARPROC KiUserExceptionDispatcher;
FARPROC KiUserExceptionDispatcher2;

__declspec(naked) void d_KiUserExceptionDispatcher() {
  __asm {
    pushad
  }
  exception();
  __asm {
    popad
    CLD
    MOV ECX,DWORD PTR SS:[ESP+4]
    jmp KiUserExceptionDispatcher2
  }
}

__declspec(naked) void d_KiUserExceptionDispatcherXP() {
  __asm {
    pushad
  }
  exception();
  __asm {
    popad
    MOV ECX,DWORD PTR SS:[ESP+4]
    MOV EBX,DWORD PTR SS:[ESP]
    jmp KiUserExceptionDispatcher2
  }
}

bool HookException() {
  KiUserExceptionDispatcher = ::GetProcAddress(::GetModuleHandleW(L"ntdll.dll"), "KiUserExceptionDispatcher");
  if (KiUserExceptionDispatcher == NULL) {
    return false;
  }
  KiUserExceptionDispatcher2 = reinterpret_cast<FARPROC>(reinterpret_cast<unsigned int>(KiUserExceptionDispatcher) + 5);
  const unsigned char oldCode[] = {0xFC, 0x8B, 0x4C, 0x24, 0x04};
  unsigned char newCode[_countof(oldCode)] = {0xE9};
  *reinterpret_cast<unsigned int *>(&newCode[1]) = reinterpret_cast<unsigned int>(d_KiUserExceptionDispatcher) - reinterpret_cast<unsigned int>(KiUserExceptionDispatcher) - 5;
  if (!org::click3::DllHackLib::ChangeCode(reinterpret_cast<unsigned int>(KiUserExceptionDispatcher), oldCode, newCode, sizeof(unsigned char) * _countof(oldCode))) {
    const unsigned char oldCode[] = {0x8B, 0x4C, 0x24, 0x04, 0x8B, 0x1C, 0x24};
    unsigned char newCode[_countof(oldCode)] = {0xE9, 0x00, 0x00, 0x00, 0x00, 0x90, 0x90};
    *reinterpret_cast<unsigned int *>(&newCode[1]) = reinterpret_cast<unsigned int>(d_KiUserExceptionDispatcherXP) - reinterpret_cast<unsigned int>(KiUserExceptionDispatcher) - 5;
    if (!org::click3::DllHackLib::ChangeCode(reinterpret_cast<unsigned int>(KiUserExceptionDispatcher), oldCode, newCode, sizeof(unsigned char) * _countof(oldCode))) {
      return false;
    }
  }
  return true;
}

簡単に説明すると、windowsでは例外が発生した場合はntdll.dllのKiUserExceptionDispatcherに処理が移り、その中で必要ならSEHOPのチェックなど行った後にSEH連鎖をたどって例外ハンドラを実行してます。
ので、KiUserExceptionDispatcherを最初の部分を書き換え、自作関数に向け、自作関数からはインラインアセンブラを駆使して本来のKiUserExceptionDispatcherに処理を戻す、という手順になります。

KiUserExceptionDispatcherが2つあるのはXPとそれ以降でKiUserExceptionDispatcherの処理が若干異なるため。
一応xp sp3/vista sp1/windows8/windows8.1で試して動作しましたが、windows server系では動かないなどあるかもしれません、その場合はご了承ください。

とにかくこれで例外はフックできるようになりました、次はSEHレコードの追加です。

SEHレコードの追加

まずコードは以下のとおり

struct SehRecord {
  SehRecord *next;
  const void *ptr;
};

SehRecord *GetFirstSehRecord() {
  SehRecord *record;
  __asm {
    mov eax, FS:0
    mov record, eax
  }
  return record;
}

const SehRecord *AddLastSeh(const void * const ptr) {
  SehRecord *record = GetFirstSehRecord();
  SehRecord *prev = record;
  while (true) {
    if (reinterpret_cast<unsigned int>(record->next) == 0xFFFFFFFF) {
      break;
    }
    prev = record;
    record = record->next;
  }
  SehRecord * const current = record - 1;
  SehRecord * const prevRecord = current - 1;
  prev->next = prevRecord;
  prevRecord->ptr = record->ptr;
  prevRecord->next = current;
  current->ptr = ptr;
  current->next = record + 1;
  (record + 1)->ptr = record->ptr;
  (record + 1)->next = reinterpret_cast<SehRecord *>(0xFFFFFFFF);
  return current;
}

DWORD recordIndex;
DWORD ptrIndex;

void SetSehPtr(const void * const ptr) {
  TlsSetValue(ptrIndex, reinterpret_cast<void *>(reinterpret_cast<unsigned int>(ptr) ^ 0xAAAAAAAA));
}

const void *GetSehPtr() {
  return reinterpret_cast<const void *>(reinterpret_cast<unsigned int>(TlsGetValue(ptrIndex)) ^ 0xAAAAAAAA);
}

void SetSehRecord(const SehRecord * const record) {
  TlsSetValue(recordIndex, reinterpret_cast<void *>(reinterpret_cast<unsigned int>(record) ^ 0xAAAAAAAA));
}

const SehRecord *GetSehRecord() {
  return reinterpret_cast<const SehRecord *>(reinterpret_cast<unsigned int>(TlsGetValue(recordIndex)) ^ 0xAAAAAAAA);
}

void SetSehOp() {
  boost::random::mt19937 gen(timeGetTime());
  boost::random::uniform_int_distribution<unsigned int> dist(0, 0xFFFFFFFF);
  boost::random::variate_generator<boost::random::mt19937&, boost::random::uniform_int_distribution<unsigned int> > rand(gen, dist);
  SetSehPtr(reinterpret_cast<const void *>(rand()));
  SetSehRecord(AddLastSeh(GetSehPtr()));
}

大雑把な流れとしては、乱数を生成して例外ハンドラが[本来の最後の例外ハンドラ]->[乱数]->[本来の最後の例外ハンドラ]となるように2つSEHレコードを追加します。
本来の最後の例外ハンドラはすべての例外を処理してくれるため、それより後に処理が渡ることはありません、なので乱数でいいわけです。
最後のもう一度登録しなおしているのは、windowsそのもののSEHOPと競合した場合に不正環境だと誤認されないためです。

同様にwindowsのSEHOPはSEHレコードがすべてスタック上に配置されていないと強制終了するので、SEHレコードはスタック上の使ってなさそうな領域を勝手に書き換えて使っています。
本来いけないことなのですが、main以前に読み書きされる場所なので、VC++を使う場合は問題ありません。
他の環境の場合は、スタックのサイズを取得するなどしてスタック末尾を使うといいかもしれません。

SetSehOpをスレッドが開始されるたびに実行してやればいいわけです。
スレッド開始の検知方法はdllのエントリーポイントを使っています。
また、なぜSEHに格納した値やSEHレコードのアドレスなどをxorしているかというと、XPなどでdllが固定アドレスにロードされる場合、うまいことそれを利用してSEH連鎖が組まれるおそれがあるからです。
詳細は株式会社FFRIEMETによるSEHOP実装の改良の提案を参照してください。

次は例外時のSEHOPチェック部分です。

SEHOPチェック

いつものようにコードから

void exception() {
  for (const SehRecord *record = GetFirstSehRecord(); reinterpret_cast<unsigned int>(record) != 0xFFFFFFFF; record = record->next) {
    if (record == GetSehRecord() && record->ptr == GetSehPtr()) {
      return;
    }
  }
  ::abort();
}

この短さなら説明するまでもありませんが、単純にSEH連鎖をたどっていき、登録したSEHレコードに格納した時と同じ乱数があるかをチェックしているだけです。
SEH連鎖の末尾固定だとwinodwsのSEHOPと競合するため、SEH連鎖内のどこかにあればいいとしてあります。

終わりに

どうだったでしょうか?
言うまでもないですがこのsehop.dllは単にLoadLibraryするだけで基本どのアプリでも動きます。

今回はSEHOPの自作ができたので、それを紹介したいだけの自己満足記事としての色が強かったですが、次回があればちゃんと解説メインに戻るはず。

もし次回があればこんどこそASLRの解説をやりたいと思います。
ではまた!