ハンド (逆) アセンブルのための x86 ニーモニックの覚え方

バイナリ列を見て x86 のコードかな〜とニヨニヨできる人に、x86 のコードであること、だけじゃなく実際のコード列も読めるようになってほしい!そんな願いから、今回は hex dump のバイト列を見つめてハンド逆アセンブルできるようになるための、効率良い覚え方を紹介します。
今回は、32-bit x86 について解説するよ。

まとめて覚えておきたい 8 つの命令、add, sub, adc, sbb, and, or, xor, cmp (00h〜3Dh)

これらの命令は近い所に配置されていて、しかも命令のルールがほとんど同じです。
つまり、ほとんど同じように覚えることができるのです。opcode map 上では次の領域が相当します。

0 1 2 3 4 5 6 7 8 9 A B C D E F
0 ←add ←or
1 ←adc ←sbb
2 ←and ←sub
3 ←xor ←cmp
4

並びの覚え方ですが… xor と cmp に関してはこのまま覚えましょう。その他については…

  • 00h〜 と 08h〜: ビットや値を (足す) 感じの操作 (add と or)
  • 10h〜 と 18h〜: キャリーを使った加減算 (adc と sbb)
  • 20h〜 と 28h〜: ビットや値を (引く) 感じの操作 (and と sub)
    • ここでは sub が後に来ていることに注意。

さて、ここでは add 命令 (00h〜05h) について詳しく見てみましょう。これが分かれば、他の 7 つの命令もおのずと理解できるはずです。

00h, 02h: レジスタやメモリに対する操作 (バイト)
命令フォーマット:
[00h|02h] [Mod R/M] ([SIB]) ([Address Offset])

00h と 02h の違いはデータがどちらに流れるかというものです。

00h: add reg/mem ← reg
02h: add reg/mem → reg

x86 の基本的な (Mod R/M を使う) 命令では、メモリオペランドは片方につけることしかできません。そのため、メモリをソースとして扱うかデスティネーションとして扱うかでオペコードが違うということになってきます。
ちなみに、レジスタ同士の場合 00h, 02h のどちらを使っても良いので、次の 2 つのバイト列は同じ操作を意味します。

00 c8: add al ← cl (add al, cl)
02 c1: add cl → al (add al, cl)

Mod R/M バイトは全部暗記しておくと読むのが楽になるけど、シェルコードの多くを読むだけであれば、Mod R/M バイトが C0h〜FFh ならレジスタ同士の操作 (SIB バイトやオフセットはつかない)、とだけ覚えておくと扱いやすいです。

01h, 03h: レジスタやメモリに対する操作 (ワード)

00h, 02h と基本は似ています。ただし、基本的にはワード (32-bit x86 の通常モードでは 32-bit) を扱います。ただし、プレフィックス 66h がオペコードの前についている場合、16-bit 操作になります。(16-bit モードの場合には、66h プレフィックスで 32-bit 操作になる)

命令フォーマット:
[01h|03h] [Mod R/M] ([SIB]) ([Address Offset])

もちろん、メモリ操作の方向も似ています。

01h: add reg/mem ← reg
03h: add reg/mem → reg

というわけで、レジスタ同士の演算では 01h, 03h で同じ操作の別バイト列が作れます。

01 c8: add eax ← ecx (add eax, ecx)
03 c1: add ecx → eax (add eax, ecx)
04h, 05h: eax レジスタと定数に対する操作
命令フォーマット:
[04h] [定数 (1-byte)]
[05h] [定数 (4-byte または 2-byte)]

これは、eax レジスタ (あるいは al レジスタ) と定数とで操作を行うための命令です。05h の方はプレフィックス 66h で 16-bit 操作になります。

04 0a:          add al,  10       (0x0a)
66 05 e8 03:    add ax,  1000     (0x03e8)
05 40 42 0f 00: add eax, 1000000  (0x000f4240)

もっと大きな命令グループ (レジスタ)

x86レジスタは 3 ビットの数値で表現されます。その数値が命令の 1 バイト目に含まれる命令グループを紹介しましょう。

AL eAX 0
CL eCX 1
DL eDX 2
BL eBX 3
AH eSP 4
CH eBP 5
DH eSI 6
BH eDI 7
0 1 2 3 4 5 6 7 8 9 A B C D E F
4
5
9
B
  • 40h-47h: INC reg
  • 48h-4Fh: DEC reg
    • INC と逆にレジスタから 1 を引く命令です。
  • 50h-57h: PUSH reg
    • ワードレジスタをスタックにプッシュする命令です。
  • 58h-5Fh: POP reg
    • PUSH と逆に、ワードレジスタをスタックからポップした値で置き換える命令です。
  • 90h-97h: XCHG eAX, reg
    • AX もしくは EAX レジスタと、指定のワードレジスタの内容を置換する命令です。90h (XCHG eAX, eAX) は NOP としても有名ですね。
  • B0h-B7h: MOV reg8, imm8
    • 指定のレジスタに定数を入れる際に用いられます。
  • B8h-BFh: MOV reg, imm
    • これも B0h-B7h と同じく頻繁に用いられます。ワード (16/32 ビット) の定数をレジスタに代入するというところだけが異なります。ちなみに 32-bit では関係ありませんが、64-bit ではほとんどの命令で 64-bit 即値を使うことができません。しかし、64-bit サイズの REX プレフィックスをつけたこの命令は 64-bit 即値をレジスタに即代入することができます。

条件分岐 (70h-7Fh)

0 1 2 3 4 5 6 7 8 9 A B C D E F
7

これには簡単な覚え方は…なさそうです。ただひとつだけ覚えておきたいのは、ある条件とその否定はかならず隣り合っていることです。つまり、short 条件分岐の 1 バイト目、最下位ビットを反転すると、条件が反対になるということです。

72 xx:  JE short XX
 ↑
 ↓
73 xx:  JNE short XX

…記事の書き方が雑になってないか?

気のせいではないです、今日中に公開することを急いだ結果がこれだよ! そのうちちゃんと書き足すです。

ハンド (逆) アセンブルを補助するための PDF リスト!

今回はこれを簡単に実現するために、手軽に印刷して参照できる PDF を作ってみました。
http://dl.dropbox.com/u/2476414/TechResources/x86_opcodemap_1_a4.pdf
http://dl.dropbox.com/u/2476414/TechResources/x86_opcodemap_1_b4.pdf
余白の大きい B4 版をオススメします。手元の紙に手軽に印刷できるよう一応 A4 版も作りましたが…ぶっちゃけ見づらいです。

続きは何書く?

Mod R/M バイトについてほとんど何も解説せずに記事を書き終えてしまった (しかもオペコード表だけだと分からない) ので、次回このシリーズを書くとしたら、 Mod R/M バイトと SIB バイトについて、あと Mod R/M を覚えないと書けない単項 (Unary) 命令についても解説してみようと思います。

グレイコード→次のグレイコード的なカウンタ

[twitter:@iorivur] さんがつぶやいた、グレイコードを直接出すカウンタは…できました。けど複雑な上に実質加算器なので、個人的にはこれはあまり好きになれません。

グレイコードとは

グレイコードとは、1 ステップ進めると 1 ビットだけ変化するという性質を持つ n ビットの数値符号です。ここでは、3 ビットのグレイコードを見てみましょう。

v b2 b1 b0
0 0 0 0
1 0 0 1
2 0 1 1
3 0 1 0
4 1 1 0
5 1 1 1
6 1 0 1
7 1 0 0

C 言語では、次の関数でグレイコードに変換できます。

int graycode(int v)
{
    return v ^ (v >> 1);
}

「上位のビットに」依存するグレイコード

上の関数は、通常の数値からグレイコードを生成します。では、グレイコードから次のグレイコードを生成できるのか? というのが [twitter:@iorivur] さんの疑問だったみたいです。でまぁ…卑怯ですが、できました。

  • 各ビットは、「自ビットより上位ビットすべての偶数パリティ」=P、「自ビット」=Bin、「キャリー」=Cin を受け取る
    • 偶数パリティは、データに含まれる 1 ビットの数が偶数なら 0、奇数なら 1 になるパリティ。ようは全ビットの XOR。
  • 各ビットはキャリーが 1 のときにしか更新されない
  • 最下位ビットのキャリーは、通常の加算器と異なり常に 1 (最下位ビットから更新が始まる)
  • 各ビットは出力の自ビット=Bout、キャリー=Cout を出力し、Cout はすぐ上のビットの Cin になる

このとき…

Cin P Bin Bout Cout
0 X X Bin 0
1 0 0 1 0
1 0 1 1 1
1 1 0 0 1
1 1 1 0 0

もっと省略すると、

Cin P Bin Bout Cout
0 X X Bin 0
1 X X ~P P^Bin

ただしこれは最上位ビットを除く話です。最上位ビットの場合には、0→1 のとき上記ルールが適用できますが、1→0 のときルールが適用できません。そのため、Cin=1, Bin=1 で、かつ下位ビットすべてが 0 ならば、Bout は 0 にする必要があります。
ビット毎に計算して正しいことを確かめてみてください。
各ビット毎のパリティをあらかじめ求めておく必要はあるものの、これで一応はグレイコード→次のグレイコードが一意に算出できます。だけどこれって加算器を変形しただけだから、実質バイナリ (数値) 変換してるのと変わらないのよね…。

そろそろあの長文に反論するか…。

ふむふむ。一回読んだだけでも、間違っているどころか詭弁に見える部分がある。
というわけで、私の立場から反論させていただきたい。

自己紹介

原著者の方が見ることも考慮して、自己紹介しておこう。私は 大居 司、情報セキュリティ業界の一年生で、今年のセプキャンには JNSA*1 協力メンバーの sutegoma2*2 枠として入場した。(見学させていただいた。)

文を読む

最初の印象は、

  • 間違っている部分がある
  • 詭弁と受け取られかねない部分がある
  • 批判せざるを得ない部分がある
  • これって本当に「批判」してる?

順不同にはなってしまうが、おおむね順番通りに指摘する。

情報を守る責任

まず喩え話の A さんが悪いのか B さんが悪いのか。明らかに B さんが悪いだろう。A さんは、自身のアカウント情報を適切に守らなかったという点で責任は生まれるかもしれないが、それは「悪さ」ではない。ここで明らかに間違っていると言えることのひとつが、喩え話の実例としてソニーの件を出したことだ。個人の例と明らかに異なるのは、ソニーが大量の個人情報を扱い、それを不適切な方法で保護してきたことだ。
この件で私が特に重要と思うのは、情報を保持するとき、常に保持と保護に関する責任が生じることだ。個人情報流出事件の報道を見てもらえればわかるはずだが、彼らはまず「流出させた」そのこと自身で批判されているのであり、なぜ流出したのかはその次にくる。
そして、さらに悪いのが Anonymous に関する扱い (GoogleNews で記事がほとんど見られなかった) だが、…貴方の目は節穴か?今からでも Google News で検索してくれ。いくらでも記事が見つかるだろう。間違っているのだと貴方が主張するのであれば、貴方はとんだ嘘つきだ。Anonymous などの記事が少ないとはいえないし、ソニーの批判が多いのは情報流出そのもの (+ この件固有の特殊事情)。私はそう考える。

貴方は間違っていると言い張れる。

私は高専を中退し、結局セプキャンの生徒としては参加できていない。ただし、経験上反例を挙げることはできる。この部分のことだ。

高校生のうちから勉強したからといって,実務で役立つことは皆無である.

役に立っている。はっきり言わせていただく。何度でも言わせていただく。役に立っている。あなたに批判されようと、嘘つきと言われようと、あなたは間違っていると声を大にして言い張れる。exploit の基礎知識、レインボーテーブルなどのオフライン攻撃、x86 CPU の保護機構、すべて高専の、しかも低学年のときに学んだことだ。
実のところこの反論を書こうとした最大の理由はここで、私も強く侮辱されたように感じたし、実際に今年のセプキャンで学び取っているすべての者を愚弄する行為に見えたからだ。はっきり言って、頭に来た。
それ以外の指摘としては、本当に (黒だろうが白だろうが) 情報セキュリティに興味のある人物はどれだけ止めようと学ぶということがある。(実際に私がそうだった。) それなら、正しい情報を正しく使えるように、指導していくというのも重要ではないのか? と。私は、こちらの方が良い効果をもたらすものと確信しているし、講師陣も倫理を教えることは非常に重視している。

追記

おそらく、「皆無」のような強すぎる否定を用いたことで、私が頭にきたんだと思う。例えば、「あまりない」とかだったら、この部分に関して反論することの正当性を半分失っていたことだろう。ちなみに、相手が「ない」と主張している場合に「ない」ことを証明することは比較的難しいが、「ある」ことの証明は簡単にできる。

見ていないこと / 見ていること

セプキャンのチューターに EULA 違反をするような人がいたという話に関しては、私は見ていないし、反証もできないので反論はしない。実のところ、それをやりかねないチューターは知っているので、まぁ…事実のように聞こえてしまう。ただ、筑波とか AC とかのくだり。あなたがセプキャンに参加した時期には正しいのかもしれないが、今年に私が見た限りにおいて、筑波がどうのこうの、というのは目にしなかった。 (私は当時のセプキャンを見ていないのでその点の反論はできないが、新たな主観的事実を提示することはできる。) そして、貴方が遭遇したであろう優生主義の人物に会ったとしたら、確かに、貴方が主張するように指摘してやれば良いのだ。ここで、新たな点が浮かび上がる。

それって批判してるの?

先ほど出てきた点、それはあなたが批判した様々なことが、セプキャンを避ける理由になるか、ということだ。
私が言いたい。関係ない理由を挙げた上で印象操作をしようとしていないか? と。もしそうなら、れっきとした詭弁だ。嘘をついているとか、間違いがあるとか以前の問題だと考えるし、そうでないなら反論してほしい。どちらにしろ…

見て判断してほしい

私の主張だけ見て偏った判断に流れるということも、これはマズいことだ。実のところ、彼の長文の中にも読むべき点はあるし、私自身も結構勢いで書いた部分があり、間違っているかもしれないからだ。URL を再掲しよう。

結論については保留する。怒りに任せて書いた部分もあることから、私自身も批判を免れない。
ただそれでも、私の立場から明らかに間違っている点についての指摘はできたと思うし、あり得る批判を少しでも並べることができたはずだ。

追記

結論だけ言わせていただく。私は、彼の主張が不公正で、判断を誤らせる要因になりかねないと考える。正直なところ、セプキャン希望者の判断の自由を逆に奪いかねないと思っている。私はこの (全体的に酷い) 文章を通じて、誤った部分を指摘し、真に (セプキャン希望者が) 自由に判断できるようにしておきたいと考えている。

*1:日本ネットワークセキュリティ協会

*2:DEFCON CTF への参戦を目的のひとつとするチーム。私は DEFCON CTF の予選を少しだけ手伝っただけだが。

線形合同法で色々やってみる。

任意の未来の状態を計算できる乱数について調べてたら、線形合同法ならパラメータを計算し直すだけで何とかならないかと考えた。あと 2ch のログでもこういうのを発見した。(出典: http://unkar.org/r/tech/1192628099 [擬似乱数2 - プログラム技術@2ch掲示板])

140 :デフォルトの名無しさん[sage]:2008/09/17(水) 19:03:50
XorShift ならちょっと考えればわかる。
線形合同法なら 2^{31}-1 だけ進めた x=Ax+C の A と C を求めれば可能。
メルセンヌツイスタやWELLなら
http://www.iro.umontreal.ca/~lecuyer/myftp/papers/jumpf2.pdf
の方法で周期よりも一つ少ないところへジャンプすれば不可能ではないはず。

というのも、訳あって SIMD 版と非 SIMD 版で全く同じ乱数を使用する必要があり、しかもある程度高速な方が好まれるという事情があるためだ。(あと本来の乱数性もそれほど必要ではない。) 線形合同法は、念の為に説明すると次の形の乱数生成法だ。(パラメータの制約などについては Wikipedia を参照してほしい。)
X_{n+1} = A \cdot X_n + B \pmod{M}

2 段重ね

では、これを二段重ねにすることを考えてみよう。剰余に関連する定理により、
X_{n+2} = A \cdot X_{n+1} + B \pmod{M}
X_{n+2} = A(A \cdot X_n + B) + B \pmod{M}
の両者は等価である。これをさらに展開すると面白いことになる。
X_{n+2} = P_2 \cdot X_n + Q_2 \pmod{M}
P_2 = A^2
Q_2 = AB + B
となる。つまり、線形合同法のパラメータを変更するだけで、2 段飛ばしの乱数を生成できるようになるのだ。

n 段重ね

さらに一般化しよう。k \geq 1 について、具体的な計算は省略するが、次のようになる。
X_{n+k} = P_k \cdot X_n + Q_k \pmod{M}
P_k = A^k \pmod{M}
Q_k = B \sum^{k-1}_{m=0} A^m \pmod{M}

2^n 段重ね, k+l 段重ね

このままだと Q_k を計算するコストが結構ある。これをカバーするには、二段重ねを応用して 2^n 段重ねをやって、これを最終的に合成すれば比較的高速に生成できる。2 つの n 段重ねの系列を合成 (k+l 段重ね) するためには、次の計算をすれば良い。
X_{n+k+l} = P_{k+l} \cdot X_n + Q_{k+l} \pmod{M}
P_{k+l} = P_k P_l \pmod{M}
Q_{k+l} = P_k Q_l + Q_k \pmod{M}
これを使ってできるのが、上にある 2ch ログで答えがあったような、「線形合同法による乱数列の巻き戻し」だ。周期が f の乱数列の場合、f-1 回後の乱数を生成するパラメータを生成すれば、乱数列の巻き戻しが可能になる。(一般的には周期が 2^{32}2^{64} となるから、その周期の一つ前を計算すれば良いわけだ。)

SIMD による乱数列の生成

これは簡単。例えば SSE2 などの 32x4 bit の場合、数式による表現では
S_0 = \{X_0, X_1, X_2, X_3\}
S_{n+1} = P_4 \cdot S_n + Q_4 \pmod{2^{32}}
とするだけ。

まとめ

SIMD でも変わらない乱数は意外と簡単に生成できた。あとは、他の乱数ももう少し詳しく調べることにする。

x86 で任意精度整数の演算 (一部) をやってみる

FAT12/FAT16 用のブートローダを書こうとしていたときに気づいた問題。FAT12/16 の最大クラスタ数はそれぞれ 2^12, 2^16 より少し少ない値だが、セクタに関してはそうではない。計算するときには 32 ビットの値でセクタ数を計算し、ディスクからのロード操作を行う必要がある。しかも私はブートローダを 8086 互換命令だけで構成したかったので、ヘタに 32 ビット演算 (や eax とかのレジスタ) を使うわけにもいかない。というわけで、16 ビットの命令だけで必要な演算が賄えるかどうかが鍵だった。必要なのは次の演算だ。

  • u32 + u32 -> u32
  • u32 - u32 -> u32
  • u16 * u16 -> u32
  • u32 / u16 -> u32
  • u32 % u16 -> u16

最初の 2 つ (加減算) は命令をよく知っていれば特に詰まるところなどない。乗算に関しては mul 命令をそのまま使うだけでこのようになる。ただし最後のふたつ、つまり、32 ビットの値を 16 ビットの値で割ったときの商と剰余に関しては、若干の工夫を必要とする…ように見えた。まぁ最初からやっていこう。

加算/減算

x86 には adc, sbb という命令がある。これは普通に加減算をするだけでなく、キャリー/ボローがある場合に余分に 1 を足す/引く命令だ。*1これを使うことで、多倍長の加減ができる。

; DX:AX <- DX:AX + DI:SI
add ax, si
adc dx, di
; DX:AX <- DX:AX - DI:SI
sub ax, si
sbb dx, di

乗算

mul 命令はまさにそのための命令だ。16 ビットの mul 命令は AX * (何か) の演算結果を DX:AX という 32 ビットの値として格納する。つまり…

; DX:AX <- AX * DX
mul dx

以上だ。

除算 (商, 剰余)

さて、8086 命令だけを使って u32 / u16 の命令を実装するには、どのように多倍長で計算するかということが重要になる。あれ、x86 の div 命令って u32 / u16 の演算をしなかったっけ? とお気づきの方はかなり頭が良い。しかし、ここには大きな落とし穴がある。念の為に div 命令の主な動作を下に示す。

; div X
AX = (DX:AX) / X ; Careful!
DX = (DX:AX) % X

ここで注意しておきたいのは、商は 16 ビットに収まらなければならないという点だ。例えば u32 / u16 の計算をして、答えが 0x10000 以上、つまり 16 ビットに収まらない場合については #DE (除算エラー) が発生してしまうのだ。これにより、単純な解は消える。何らかの多倍長演算をしなければならないことは一目瞭然だ。そこで、筆算が参考になるかな、と思い、適当な筆算をメモってみた。*2

; 691 / 7 = 98, 691 % 7 = 5

          0   9   8
   +---------------
 7 | (0)  6   9   1
          0
     ------
          6   9
          6   3
         ------
              6   1
              5   6
             ------
                  5

お分かりいただけるだろうか。これだけで答えは出たも当然だということを。2 桁の数を引っ張ってきて、それを 1 桁の数で割り、剰余を次に割る 2 桁の数のうちの上位にしたうえで被除数から新たに 1 桁持ってくる。この繰り返しだ。しかも、この原則は 2 進以上なら何でも構わない。10 進数でも、16 進数でも、あるいは 65536 進数や 4294967296 進数でも同じなのだ。*3ただし x86 の div 命令に当てはめると、最初に 2 桁を持ってきてしまった場合に (前述したような) 除算エラーが発生してしまう可能性がある。そのため、最初に持ってくる 2 桁はダミーの 0 を含むことになる。これで商は 16 ビットに収まることが確定する。
次以降は 2 桁 / 1 桁の、一見オーバーフローが発生しそうな演算だが、考えてみてほしい。1 桁毎の商が 10 以上になるのは、どういう場合だ? 上の 10 進数の例で考えるならば、部分的に扱う 2 桁の数が 70 以上になる場合だ。しかし…よく考えてみよう。部分的に扱う 2 桁の数のうち、上位部分は既に行われた演算の剰余だということを。そう、上位からやってくる数は、除数が 7 の場合は 0〜6 しかありえないのだ。下の桁は明らかに、0〜9 だ。つまり、最上位さえ適切に扱ってやれば、後で除算エラーが発生する可能性は消え失せるのだ。
さらに良いお知らせ。div 命令の仕様で、剰余は DX に格納される。しかし、DX というのは次の div 命令において上位 16 ビットとして扱われる値そのものである。つまり、次の桁を「持ってくる」には、AX 部分だけを交換すれば良いのだ。これは効率が良い上に、ブートローダのような小さなプログラムでの実装を容易にする。次のものは実際にブートローダで使うために試験的に実装したものだ。

; (CX:AX) = (AX:CX) / BX, DX = (AX:CX) % BX
xor dx, dx  ; 31 D2  ; 上位の除算でオーバーフローしないようにする
div bx      ; F7 F3  ; 上位の部分商と剰余を取る
xchg ax, cx ; 91     ; 上位の部分商と下位の桁を交換する
div bx      ; F7 F3  ; 下位の部分商と剰余 (これは全体の剰余に等しい) を取る

最適化のおかげで、わずか 7 バイトで u32 と u16 の商と剰余が取れている。商 (CX:AX) が元々の被除数 (AX:CX) からひっくり返った配置になるのが気に入らなければ、xchg ax, cx を先頭か末尾に付加して 8 バイト。これだけで全てが間に合う。意外と…小さく済むものだ。

*1:用語上はキャリー、ボローという異なる値だが、どちらも x86 命令では CF [キャリーフラグ] で表現され、2 の補数というアーキテクチャ上意味もほぼ同じだ。

*2:例は実際にメモったものとは異なります。

*3:ただしここでは符号無し演算のみとしている。

やっと「マトモ」になった mingw-w64

凄く場違いな話題だが、x64 Windows 向けのツールチェーンに加えられた飛躍的な進化について。x64 Windows 向けの例外処理もだいたい分かるぐらいに話す。 (カーネル/VM Advent Calendar : 1010+1001/01/11 分)

x86→x64

Windows はこのプラットフォームの変化によって、様々な部分が変化した。そして、色々な部分が進化した。例外ハンドリングもそのひとつだ。その代わり、コンパイラがマトモなモノでないとこの恩恵を得ることができない。x86 用の Windows がターゲットならばかなり腐ったコンパイラでも一丁前の実行ファイルを得られたが、x64 ではそうはいかない。具体的には次の部分がある。

  • x64 用標準 ABI の整備
    • レジスタ渡しになったのはかなりのプラスだが、関数のプロローグとエピローグの形は大きく制約されることになった。x86 だと少なくとも関数プロローグやエピローグの形はコンパイラ好みの任意のものが使用できる。
  • 例外ハンドリングの再編
    • テーブルベースの例外ハンドリングが標準になることで、セキュリティとパフォーマンスの向上。ただし弊害も。

mingw-w64 とは

32/64-bit 共用の Windows 向けツールチェーンであり、gccbinutils といったツールを使用して Windows 向けのネイティブアプリケーションを作成することができる。積極的な貢献によってユーザモードアプリケーションを作る分にはほとんど申し分ないほどに進化しており、カーネルモードのドライバも作れないことはない。
ただ、カーネルモードドライバ、特に 64-bit 向けのものを作るには、最大の障害が存在した。例外処理である。

SEH (Structured Exception Handling)

SEH (構造化例外処理) は、Windows に存在する低レベルの例外処理機構である。例えば、次のように使用する。

__try
{
    /** 例外発生の可能性があるハンドラ **/
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
    /** 例外発生時の処理 **/
}
__finally
{
    /** 何があろうと実行するルーチン **/
}

これによって、自分でカスタムのエラー処理を (C++ 例外などの高級なものから、ゼロ除算などの CPU 例外までキャッチするハンドラとして) 書くことができる。
しかしながら、これは x86 と x64 で大きくその機構が異なる。

x86 の場合

x86 用の SEH は基本的にスタックベースのものだ。スタックに例外ハンドラに関する情報を置き、以前に設定されていた例外ハンドラをリンクし、システムに登録する。こんな感じ。

function1→function2→function3→function4→function5 という呼び出し履歴がある場合:
+---------------------------+     +------------------------+     +------------------------+     +------------------------+     +------------------------+
|  fs:0 (例外登録レコード)  |<----|  function5 の例外処理  |---->|  function2 の例外処理  |---->|  function1 の例外処理  |---->|  デフォルトの例外処理  |
+---------------------------+     +------------------------+     +------------------------+     +------------------------+     +------------------------+

例外登録レコードはいわゆる __try ブロックに入ったところでリンクとシステムへの登録が行われ、次の命令の例外からはそちらを参照することになる。(これには若干のオーバーヘッドがある。) もし例外が発生したとき、function5 の例外フィルタはそれをハンドルするかもしれない。あるいは、実行を継続させるかもしれない。あるいは自分で処理ができなかったなどの理由で、後ろにある例外ハンドラにたらい回しすることもできる。ここでは function1,2,5 に __try/__except ブロックがあるので、function5 の例外ハンドラが処理できなかった場合には function2 の例外ハンドラにたらい回しされるわけだ。もちろん関数を抜けるときには自分の持ってる例外処理エントリを外した上で改めてシステムに登録し直す。
この機構は単純な反面リスクも抱えており、スタックオーバーフローで狙われる危険も大きかった (例外ハンドラに関する情報をスタックに置くので、そこを上書きされるリスクがあったのだ。) これを回避するために様々な保護機構が後付けされていった。(SafeSEH, SEHOP など。加えて ASLR はアドレスをランダムに配置することで SEH 保護機構の効果をかなり高める。)

x64 の場合

しかし、x64 Windows では最初からセキュアな方法論が採用されている。ちゃんとしたコンパイラで吐き出した x64 向け PE ファイルには、各関数に関する unwind 情報が保存されている。

- function 1 : rbp, rsi, rdi を保存
- function 2 : rbp を保存; 24 バイトのバッファを確保
- function 3 : rbp, xmm6, xmm7 を特定箇所に保存; 136 バイトのバッファを確保
- function 4 : rbp を保存....
- function 5 : ....

のように。これを何に使うか? 完全なコールスタックを復元するためである。この方法は DWARF2 Exception Handling の方法論に近い。
この場合、__try/__except 句のある関数で特別な処理 (システムへの登録と登録解除など) は行われない。これによりパフォーマンスが向上する。そして例外発生時には、そのときのスタックから元のコールスタックを復元した上で、最も近い例外ハンドラを探して呼び出す。(微妙にまちがってるけどだいたいはこんな感じ。)

  1. 例外発生アドレス (RIP) を得る
  2. RIP に対応する関数を取得する。
    • これが取得できない場合もある。例えば、関数がスタックを一切使用せず、例外ハンドラもなく、そして一切の関数を呼び出さない場合 (leaf function と呼ばれる) が該当する。
    • この場合、戻りアドレス (RIP) は現在のスタックアドレスに存在すると仮定し、2. に戻る。
  3. 関数が例外ハンドラを持っていて、なおかつ RIP が PE ファイルで指定される特定の範囲内であれば、対応する例外ハンドラを呼び出す。
    • 例外ハンドラが処理できなかった場合は次以降。
  4. 関数の unwind 情報を得る。
  5. unwind 情報に格納されているプロローグの位置を得て、関数のプロローグかどうかを判定する。
    • 下の例で、insn ends at pc+0x というのがまさにその情報。
    • プロローグなら、そのための unwind 情報 (サブセット) を算出する。
  6. あるいは、RIP に続くコード列を得て、静的解析で関数のエピローグかどうかを判定する。
    • 関数エピローグは保存した情報を復元している最中なので、これを判定することは必須。
    • これを正しく行うために、ABI で関数のエピローグにおけるコード列が大きく制約されているのだ。
    • エピローグなら、そのための unwind 情報 (サブセット) を算出する。
  7. 対応する unwind 情報を元に、レジスタを (仮想的に) 復元する。
  8. 関数の戻りアドレス (RIP) を取得し、2. に戻る。

ここからは x86 の SEH と同様だ。この方法論が優れているのは、例外ハンドリングに関する全ての情報は PE ファイルの読み取り専用領域に格納されているため、例外ハンドリングをクラッキングのために (ほとんどの場合) 使用できなくなるという点だ。
しかし悪く言えば、x64 の SEH はテーブルに頼り切っている。これは、当該プログラム中で SEH を使わない場合でも望んでいない状況/クラッシュを引き起こす可能性がある。例えば、unwind テーブルつきのバイナリから unwind テーブルなしのバイナリが呼ばれた場合、SEH は状況によっては正常に動作しない。

sample.exe → sample.dll と呼び出しの最中:
-----------------------------------------------------------------
sample.exe (MSVC でビルド、実行 [例外ハンドリングをしている])
  → sample.dll (mingw-w64 でビルド、実行 [SEH, unwind ともになし])
----------------------------------------------------------------- ↑before↓after
sample.exe (例外ハンドリングをしていたのに正常に呼び出されず、クラッシュ)
  → sample.dll (例外発生↑)

また x86 であれば強制的に例外ハンドラを登録してやればユーザの指定する動的なハンドラを実行することができたが、x64 ではこの方法は基本的に「不可」である。*1代わりに VEH (Vectored Exception Handling) と呼ばれる機構があるが、カーネルモードのドライバはこの VEH を利用することができない。
(今までの) mingw-w64 はまさにこの条件に当てはまってしまう。unwind 情報を吐き出すことができず、安全なカーネルモードドライバを書くには少々危ない感じだった。SEH のキーワードである __try/__except も mingw-w64 の gcc は解釈せず、x64 カーネルモードドライバで例外を使うにはハックが必要なものだと思っていた。厄介である。

mingw-w64 の進歩

しかし、最近のバージョンの mingw-w64 では、ちゃんと関数の unwind 情報を吐き出すようになっている。

$ x86_64-w64-mingw32-objdump -p a.exe | less
--------------------------------------------------------------
// なにもしない <main> 関数の unwind 情報
 000000000000606c:
        Flags: UNW_FLAG_NHANDLER.
        Entry has 3 codes.      Prologue size: 8, Frame offset = 0x2.
        Frame register is rbp.
         At pc 0x0000000000401550 there are the following saves (in logical order).
          insn ends at pc+0x01: push rbp.
          insn ends at pc+0x08: save stack region of size 0x0000000000000020.
          insn ends at pc+0x08: FPReg = (FrameReg) + 0x0000000000000000.

これは、binutilsgcc (4.6 系のみ) の双方が unwind 情報をサポートした効果である。では、return 0 のみを書いたシンプルな main 関数のアセンブリ出力を見てみよう。

        .globl  main
        .def    main;   .scl    2;      .type   32;     .endef
        .seh_proc       main
main:
        pushq   %rbp
        .seh_pushreg    %rbp
        movq    %rsp, %rbp
        subq    $32, %rsp
        .seh_stackalloc 32
        .seh_setframe   %rbp, 32
        .seh_endprologue
        call    __main
        movl    $0, %eax
        addq    $32, %rsp
        popq    %rbp
        ret
        .seh_endproc

すぐに、.seh_ で始まるキーワードが多数使用されていることに気づく。そう。SEH 関連の情報を吐き出す疑似命令を as がサポートし、gcc はそれに合わせた出力を行い、ld は複数オブジェクトの unwind 情報を統合することで、このような unwind 情報を吐き出すことに成功しているのだ。
これなら、このバイナリから例外を問題なく流すことができる。(mingw-w64 で作成したバイナリによって例外ハンドリングが滞ることがない。)
しかし、これは SEH を「使える」ことを意味しない。gcc は本来 SEH のための __try/__except キーワードをサポートしておらず、最近のバージョンの mingw-w64 でもエラーとなる。となるとカーネルモードでは使いようがない…そう思った。しかし mingw-w64 が吐き出したバイナリの情報を見ていると、興味深いことに気づいた。

$ x86_64-w64-mingw32-objdump -p a.exe | less
--------------------------------------------------------------
// !?
 000000000000602c:
        Flags: UNW_FLAG_EHANDLER.
        Entry has 1 codes.      Prologue size: 4, Frame offset = 0x0.
        Frame register is none.
         At pc 0x0000000000401510 there are the following saves (in logical order).
          insn ends at pc+0x04: save stack region of size 0x0000000000000028.
        User data:
          000: d0 28 00 00 01 00 00 00 1e 15 00 00 28 15 00 00
          010: d0 19 00 00 28 15 00 00
 000000000000604c:
        Flags: UNW_FLAG_EHANDLER.
        Entry has 1 codes.      Prologue size: 4, Frame offset = 0x0.
        Frame register is none.
         At pc 0x0000000000401530 there are the following saves (in logical order).
          insn ends at pc+0x04: save stack region of size 0x0000000000000028.
        User data:
          000: d0 28 00 00 01 00 00 00 3e 15 00 00 48 15 00 00
          010: d0 19 00 00 48 15 00 00

なんと! mingw-w64 の objdump では User data とのみ記されているが、これは紛れもなく例外ハンドラの情報だ。*2 gcc は SEH キーワードが使えないのになぜできるのか? 逆アセンブリと照合すると、この 2 つの関数は WinMainCRTStartup と mainCRTStartup だ。これは mingw-w64 の CRT 内に目標がありそうな雰囲気。というわけで、mingw-w64-crt/crt 中の crtexe.c を参照する。

/**
    mingw-w64 ソースコードより引用。
    当該部分はパブリックドメインである。
    http://mingw-w64.svn.sourceforge.net/viewvc/mingw-w64/trunk/mingw-w64-crt/crt/crtexe.c
    http://mingw-w64.svn.sourceforge.net/viewvc/mingw-w64/trunk/mingw-w64-crt/crt/crtexe.c?view=log
**/
int WinMainCRTStartup (void)
{
  int ret = 255;
#ifdef __SEH__
  asm ("\t.l_startw:\n"
    "\t.seh_handler __C_specific_handler, @except\n"
    "\t.seh_handlerdata\n"
    "\t.long 1\n"
    "\t.rva .l_startw, .l_endw, _gnu_exception_handler ,.l_endw\n"
    "\t.text"
    );  
#endif
  /* (中略) */
#ifdef __SEH__
  asm ("\t.l_endw: nop\n");
#endif
  return ret;
}

読めれば簡単だ。先ほどの例と同じく .seh_* というキーワードを、今度はインラインアセンブリとして記述している。(#ifdef が入っているのは、unwind 情報出力をサポートしない古い mingw-w64 に加え、テーブルベースの SEH ではない 32-bit 版をサポートするため。) しかも、ここで SEH の例外ハンドラを設定しており、例外がきちんと CRT に渡されるようになっている。*3最後の nop は、ラベルを正常に働かせるためのダミーだろう。となれば、この手法を使えばどのようなアプリケーションであってもそのための正常な SEH テーブルのデータを構築できる。というわけで、やってみよう…アレ、動かない!?

mingw-w64 のバグ

32-bit でクラッシュするのは予期してたモノ*4だけど、64-bit でもクラッシュするのは予想外だぞ。というわけで、WinDbg を駆使してなんとかバグ発見。(例外ハンドラではソフトウェアブレークポイントが無効になることを知らず、えらく時間を食った。) 昨日 mingw-w64 プロジェクトにパッチを投げて、即日で修正された

    "\t.rva .l_startw, .l_endw, _gnu_exception_handler ,.l_endw\n"

これは 4 つのポインタをデータとして出力しているが、このフォーマットは次のようになっている。*5

  • BeginAddress
    • __try ブロックの開始アドレス
  • EndAddress
    • __try ブロックの終了アドレス
  • HandlerAddress
    • 例外フィルタ (関数への RVA)
  • JumpTarget
    • __except ブロックのアドレス (mingw-w64 においては未使用)

そして、BeginAddress と EndAddress の間で例外が発生したならば例外フィルタ (そして必要ならハンドラ) を呼び出す…のだが、この判定は、次の C コードにほぼ等しい。

if (target >= BeginAddress && target < EndAddress)
    /* catch exception */

しかし、.l_endw 直後の nop 命令…の前の命令は call 命令で、実際にはこの戻りアドレスである nop 命令のアドレスが判定に用いられる。しかし nop 命令は .l_endw の後にあるため、フレームベースの例外ハンドリングが正しく働いていなかった*6。(CRT が例外を別の手段でキャッチしているため、今まで顕在化しなかっただけ。) というわけで、.l_endw と .l_endw+1 に置換するか、.l_endw ラベルの前に nop 命令を挿入するかのどちらかで直る。(もう一方も同様に修正する。) 修正版はこんな感じ。(前者の修正を適用したものだが、本家の mingw-w64 には後者のパッチが採用された。)

/**
    mingw-w64 ソースコードを修正。
    当該部分はパブリックドメインである。
    http://mingw-w64.svn.sourceforge.net/viewvc/mingw-w64/trunk/mingw-w64-crt/crt/crtexe.c
    http://mingw-w64.svn.sourceforge.net/viewvc/mingw-w64/trunk/mingw-w64-crt/crt/crtexe.c?view=log
**/
int WinMainCRTStartup (void)
{
  int ret = 255;
#ifdef __SEH__
  asm ("\t.l_startw:\n"
    "\t.seh_handler __C_specific_handler, @except\n"
    "\t.seh_handlerdata\n"
    "\t.long 1\n"
    "\t.rva .l_startw, .l_endw+1, _gnu_exception_handler ,.l_endw+1\n"
    "\t.text"
    );  
#endif
  /* (中略) */
#ifdef __SEH__
  asm ("\t.l_endw: nop\n");
#endif
  return ret;
}

ちなみにこれは mingw-w64 標準の例外ハンドリングをうまく働かせるための修正だが… mingw-w64 標準の例外フィルタ (_gnu_exception_handler) は、例外を signal に変換する機能を持つ。signal の使い方を知っているなら、Linux と同じように例外をハンドルすることが可能だ。(残念ながら sigaction*7 ではないが。)

となればやること。

さて、では別のこともやってみよう。本来は x64 カーネルモードで例外を発生させるサンプルを作る予定だったが、安全性を検証する時間がなかったので代わりに VMware を検出するサンプルを作ってみた。この状況なら signal も使えなくもないが、ネタ的にはあまり面白くないので、今は眠っていてもらう。

#include <stdio.h>
#include <stdlib.h>
#include <windows.h>

long CALLBACK
detect_vmware_except(
	PEXCEPTION_POINTERS data
)
{
	data->ContextRecord->Rbx = 0;
	data->ContextRecord->Rip += 1;
	return EXCEPTION_CONTINUE_EXECUTION;
}

__attribute__((noinline))
int detect_vmware(void)
{
	DWORD result;
	__asm__ __volatile__
	(
		".seh_handler __C_specific_handler, @except\n\t"
		".seh_handlerdata\n\t"
		".long 1\n\t"
		".rva .l_vmdetect, .l_vmdetect+1, detect_vmware_except\n\t"
		".long 0\n\t"
		".text\n\t"
		"xorl %%ebx, %%ebx\n\t"
		"movl $0x564d5868, %%eax\n\t"
		"movl $10, %%ecx\n\t"
		"movw $0x5658, %%dx\n"
		".l_vmdetect:\n\t"
		".byte 0xed" /* in (%dx),%eax */
		: "=b"(result)
		: : "%eax", "%ecx", "%edx", "cc", "memory"
	);
	return result == 0x564d5868;
}

int main(int argc, char** argv)
{
	if (detect_vmware())
	{
		printf("VMware is detected!\n");
	}
	else
	{
		printf("VMware is NOT detected.\n");
	}
	return 0;
}

VMware の検出には、バックドアポートを使用している。しかし、そこで使用している in 命令は VMware の無い環境下では例外を発生させてしまう。そこで、この部分で発生した例外をキャッチし、適切に握り潰しているのが上記のサンプルだ。64-bit、gcc 4.6 ベースの mingw-w64 では適切に動作する。ここではさらに洗練された握り潰し方を採用している。JumpTarget はダミーの .long で適当に埋めて、その上で in 命令の 1 バイトだけが __try ブロックに相当するような実装になっている。(end に相当するラベルがない。) ここで例外が発生した場合、in 命令だけをスキップさせ、VMware が検出されなかったことを示すダミーの数字を入れることで、ちゃんと「非 VMware」の検出にも成功しているのだ。
ちなみに、__attribute__((noinline)) と書いているのは、不必要な (-O3 などのオプションによる) インライン化を防止するためだ。MS 製コンパイラなら SEH のエントリを適切に生成してくれるが、mingw-w64 ではまだそうはいかない。万が一インライン化されると不必要な衝突を引き起こす可能性がある (今回のコードの場合は多分問題ないけど。) ため、この属性を付けてインライン化を防止している。

まとめ

というわけで、どうだろう? 御世辞にも洗練されたとは言えない実装だが、x64、非 MS コンパイラでも一応 SEH がマトモに使えるようになってきた。あなたなら、これをどう使う?

*1:全くできないわけではないのだが、マトモなプログラムで gs:0 を弄るヤツは見たことがない。

*2:これは objdump が例外ハンドラのための情報を解釈できないため。加えて、関数テーブルのフラグである UNW_FLAG_EHANDLER も、この関数が例外ハンドラを持っていることを示している。

*3:ソースコード中の __C_specific_handler (msvcrt, ntdll, ntoskrnl など、システム中の複数の DLL で定義されている) は、システムが保持するデフォルトの例外ハンドラで、このルーチンにより同じくソースコード中に記述されている _gnu_exception_handler が呼び出されることになる。この関数は基本的に POSIX 風の signal を呼び出し、それによって適切に処理されなければアプリケーションを強制終了させる。

*4:32-bit 版 mingw-w64-crt は、SetUnhandledExceptionFilter のみを使って signal を実装しているので、これを置き換えればクラッシュすることは予想できた。

*5:ちなみに当該部分の直前、.long 1 というのは、この 4 つ組の RVA で構成される例外ハンドラ情報の数である。

*6:いわゆる off-by-one エラーの変種。

*7:いわゆる POSIX シグナルのこと。

私の目から見た PacSec 2010 (3)

やっと PacSec Day 2。かなり飛ばし飛ばしですが、最終回です。

PacSec Day 2 : 時間の見積もり (2つの意味で)

とりあえず、そのまま喋ればオーバーになる程度*1の文章量。少々の要約はやむを得ない。大丈夫だと言い聞かせつつ、意図的に少し遅めに出発。今まで少し早く出ようという風に考えて予定を立てると、すごく早く着いてしまうという謎があることと、今年はホテルに一番近い地下鉄駅入り口を覚えていることなどなどから、ある程度遅く行ったとしても「遅刻」にはならないだろうと考える。実のところ結構ギリギリの時間だったが、それでも開始時間直前に到着。

PacSec Day 2 : 同時通訳との調整

最初の発表を聴き終わった直後*2に、個人的に呼ばれる。と思ったら、同時通訳の調整とのこと。今回日本語版が 84 枚、英語版は 85 枚*3という枚数に若干ツッコミを受けたものの、同時通訳の調整は比較的スムーズにいった。昨年はプレゼンを調整するために若干スライドの内容を (同時通訳の人があらかじめ読んでいるものから) 結構な量変えてしまったのでお叱りを受けたのだが、今回は誤字脱字の修正*4と 1 枚のスライドの「文章」を「グラフ」に変えたことだけ。具体的な訳語と「どの部分を重点的にしゃべるか」ということに関して集中的に調整した。

PacSec Day 2 : 発表 (午前)

Dong-Joo Ha & Ki-Chan Ahn - "ゲームコンソールへ転移するマルウェア : エンベッドされたデバイスマルウェアのためのアンチウィルスからの隠れ家" (Dong-Joo Ha: [twitter:@ChakYi] ; Ki-Chan Ahn: [twitter:@externalist])

AVTokyo 2010 で発表された方 (ChakYi&Externalist) だ。今回は Externalist こと Ki-Chan Ahn 氏といっしょに。交代交代で内容について喋るというスタンスのようだ。内容はほぼ同一であるものの、時間枠がそちらよりあるためかデモなどが幾つか加わっている。重要なことは前回までに説明を終えてしまっているので今回は追加の技術的解説を。Nintendo DS で自作ソフトウェア、そしてハッキング用 Metasploit を動かす方法については多分多くの方がご存知だろう。というわけで Wii にウイルスを仕込むまでの技術的過程。
Wii の実行ファイルは dol ファイルと呼ばれる形式だ。かなりシンプルなファイルフォーマットで、セクションはある程度固定。7 個のコードセグメントと 11 個のデータセグメント、そして BSS*5 が使えるとのこと。ウイルスは Wii 用の SDK、正規のものじゃなく自作ソフトウェアを作るのに使うヤツと同じものを使う。オフセットは調整するか、あるいはツールチェーンで実行ファイルを作成するときに予め PIC を生成する。ウイルスを実行させるのは簡単。

  • 使われていないオフセットにウイルスを仕込む。
  • エントリポイントをウイルス内にする。
  • ウイルス内で、オリジナルのエントリポイントを実行するように最終的な仕込みをする。

これだけ。海賊版ユーザに気づかれることなく「元のゲーム」を実行させ、その裏では破壊活動や他プラットフォームへの感染*6、ルータの設定変更、盗聴などやりたい放題だ。このケースの場合、穴はユーザが意図的に空けていることに注意しよう。まぁアレですね。

村上純一 - "Exploring the x64" ([twitter:@junichi_m])

さてフォティーンフォティ*7で有名な村上さんの発表。Exploring the x64 というタイトルからして、x64 CPU 全般について話すのかと (勝手に) 勘違いしていた。実際には、x64 版 Windows において x86 版とどれだけ変わったかに関する内容。例えばシステムコールの呼び方 (デモで、32 ビットのアプリケーションから 64 ビットのシステムコールを呼び出すというものが見れた。) や他プロセスに対する侵入 (32→64 のリモートスレッド生成だけは [通常の方法では] エラーになるなど。)、などなど。内容に関しては、64-bit プロセスの解析をやりやすくするためには役に立ちそうな感じがした。正直 x64 Windows については重点的に勉強、あるいは資料収集をしていたので、内容が (私にとっては) 既報が多かった感じがする。この辺に関して私が何か言うのは客観性に欠けるので、他の方がどのように思ったかも個人的に聞きたいところだ。

PacSec Day 2 : 発表 (自分)

「!?」
発表開始直後、トンデモナイことに気づいた。「Android タブレットがうまく動かない!?」
なぜかタッチ機能がマトモに使えなくなってしまったのだ。タッチパネルは精密というか、場合によって正常に動作してくれないときがある。多分だが、そのときの緊張による汗で指が湿っていたことが、タッチ機能が利用不能になった理由だと思う。とにかく、これによって原稿ナシで発表に臨む必要に迫られた。だが、不思議とうまくいった。体調がよかったから? 心理的にもストレスが少なかったから? 単なる偶然? …それは分からない。一応スピーチに詰まることはある*8が、去年や今年の x86 勉強会に比べれば飛躍的な進歩と言えるほどのスピーチになった。ちょっと原稿中にウィットとかジョークとかも仕込んでたのを言う余裕はなかったけどね。
発表途中でスライドの微妙な間違いや翻訳のし忘れを見つけたのは少し恥ずかしかった。ただ…マトモだった。去年は PowerPoint で作ってフォントを埋め込んだにも関わらず盛大にズレてしまったが、今回は PDF にエクスポートしておいたのでその問題はなかった。発表当日にプレゼン可能なシステムの調整で手間取ったぐらいか。
時間は元々使い切るつもりで動いた。その甲斐あって、発表はちょうど 60 分 (発表開始直後のトラブルを除けば 58 分くらい) で終了。質疑応答はそれほど無いと予想してたので、まさにピッタリと言っていい。まぁそのとき質問がまったく無かったのはちょっと寂しかったけど。まぁ無事終了。

PacSec Day 2 : 発表 (午後)

Jussipekka Leiwo - "コモンクリテリアとFIPS 140-2認定されたUSBトークンにおける認証脆弱性の分析"

コモンクライテリアは、ある一定の視点からセキュリティ設計を認証する制度だ。FIPS 140-2 が、暗号化や署名などに用いる暗号モジュールの要件を定めた規格。これらの規格に合致した USB トークン (ユーザのクライアント認証とか電子署名とか…に使うと記憶。) であっても、それが即座に安全であることを意味しない。この発表では SanDisk と Kingson の USB トークンから見つかった脆弱性より、認証制度の脆弱性を分析する…とのこと。
まず脆弱性について。これは USB トークンとホスト PC のソフトウェア間における認証プロトコルの設計が悪く、単純なリプレイ攻撃の餌食になる可能性があるとか。そしてその脆弱性を持ちつつ複数の認証を通過してしまったことについて考察が行われた。特に興味を持ったのはバージョニングの問題。検証をうけたバージョンと出荷するバージョンが食い違うことなどの問題だね。そしてこれらを防止するためのアドバイスが行われた。

Isaac Dawson - "ウェブブラウザー・テスティングシステム"

この方は (少なくとも) 発表当時日本に在住しておられる方だ。日本語でブログも書いておられる
この方の話題はいわゆるファジングの一種…と分類していいのかな? 連続したリクエストによって脆弱性を探し当てるアプローチだ。ターゲット生成からその自動化まで、統一化されたフレームワークを作り、素早く脆弱性を発見しようというものに見える。各種 OS のために自動化モジュールを作っておいて、統一化された手段で検証を行うことができる。これ以上の部分がイマイチよく分からなかったのだが、「ツールの作り込み」という点ではすごく好感が持てた。
最近会った人によく話すのだが、私は今のセキュリティのためのツールは十分に「統合」ができていないと考える。そして、少なくとも情報を保存し、連携するための強力なツールが必要だと主張している。彼のツールはまさにその点で驚きがあった。

PacSec Day 2 : コーヒーブレーク + 閉会直後

何人かの人と喋っていて、一部の人は私の VM について、「マルウェアを効率よく解析するのに最適」だと考えていることに気づいた。例えば、今のライブドアの親会社。そこで働く方の一人が、自社内のセキュリティインシデントの解析 (ここにはマルウェア解析も含まれる!) のために私が開発中のツールが使えるのではないかということを仰った。私は、自身の VM における現状の設計ではマルウェアを解析するには危険すぎるとしながらも、その可能性については認めた。マルウェアを「解析」するためには何が必要なんだろう。色々考えたり。あとは楽天の方とも喋って色々な可能性について考えてみたり。
正直なところ、こういう休憩時間に声を掛けられることが多かった。反響自体はそこそこあったようだ。前回よりもかなり。
帰る直前、あるアメリカ人に会った。質問は…少し奇妙だった。君が (侵害していることを) 知っている米国特許はどこの企業が持ってるんだ? と。私は主要な特許を保有する 2 社を挙げた。彼が何かをしたがっていたのかもしれないが、正直なところ、わからない。

PacSec Day 2 Party

まさに、「濃い」の一言に尽きる。オフレコなことも含めて、様々なことについて話し合った。…本当に色々と。
曖昧な言い方をしているのは、…ぶっちゃけ飲み会の記憶が混乱しているからだ。酒には思いっきり弱く「なってる」*9ので。断片的な情報を思い出してみると… DEFCON のパティ * 20枚バーガー、私がまだ就職できていないこと、VM、画像処理、スマートフォン、各国の IT 事情、…などなど。そしてこの場で、あのリバースエンジニアリングコンテスト、Hackers' Dream について教えてもらった。海外におけるリバースエンジニアリングのコンテストはどんなものなのかと興味を持ったし、時間があったら参加してみるよと曖昧な返事をしたことを覚えている。

私の目から見た PacSecPacSec を通して見た自分

ゆっくり変化している、といったのが正直なところ。ある面では、年々悪くなっている。他の面では、良くなっている部分もある。セキュリティの脅威もトレンドを変えていき、そこに研究対象も生まれる。いつまでも 200x 年ではいられない。そして、その中で PacSec が高いレベルを保つ反面、その変化に苦しむ一面も見受けられた。そして自分の成長は…やはり実感した。それでも、まだまだ及ばないというのが正直なところ。これを書く時点で既に 2011 年だが、今年何を頑張るべきかという明確な目標の一片が見えてきたと言っていい。自分にとって PacSec というカンファレンスは自分を整理しつつ、より高いレベルに運んでくれた恩人のようなものだ。今年こそは何らかの形で恩を返せれば、と思う。

*1:ここには私が噛むであろうことを考慮した上で。あとは質疑応答の時間はある程度使ってしまっても良いと考えている。

*2:多分そうだと思う。村上さんが前で話しているのを立って聞いていたのを覚えているからだ。

*3:スライド枚数が違うのは、英語スライドには一部文章を詰め込む必要がある部分が存在し、そこについて一枚にまとめることができなかったため。念の為に言っておくが、同時通訳付き、質疑応答時間付きで 60 分である。多分他の方より圧倒的に多い枚数。

*4:後で調べるとそれだけでは済まなかったのだが。

*5:こうやって書くと 19 個ものセグメントが使える、リッチなフォーマットと思われるかもしれないが、ELF に比べるとかなり単純、固定機能のオブジェクト形式である。

*6:脆弱な Windows PC に対する攻撃も含む。

*7:Google 日本語入力で最近候補に出るようになった!おめでとう!

*8:あとで英語圏の参加者の 1 人から、同時通訳で "it... it..." が続いてわかりにくかったこともあったよと指摘を頂いた。申し訳ない。

*9:私は元々下戸だが、それ以上にコンサータの副作用が効いている。医師にもあまり飲み過ぎないよう指導されている。