アセンブラ短歌で遊ぼう!

こんにちは。弥生の狩野です。Misoca+弥生+ALTOA Advent Calendar 2018 - Qiita の15日目の記事です。

みなさん、アセンブラ短歌詠んでますか?

アセンブラ短歌とは

「アセンブラ短歌」は五・七・五・七・七の三十一バイト(みそひとバイト)から成る 機械語コードでプログラムを書いてみるという近未来の文化的趣味であり,近年, 国内のハッカー間で密かなブームが起きています.(アセンブラ短歌より)

風流ですね。

まだご存知ない方は、ぜひ、アセンブラ短歌 - 坂井弘亮アセンブラ短歌をご覧ください! 楽しい世界が広がっています。

詠んでみる

アーキテクチャは、x86/Linux/32bitです。

53 0f 31 6a 04
5a 83 e0 01 74 10 90
68 a1 f9 d7 c4
89 e1 89 c3 c1 e0 02
cd 80 58 31 c0 5b c3

実行する

標準出力に☆彡が流れたり流れなかったりします。流れたら幸運ですね。

f:id:ym_AdventC:20181213201240p:plain
アセンブラ短歌を実行したコンソールの様子

解説

上にあげた機械語コードを逆アセンブルしたものが以下のリストです。 このアセンブリをもとに、簡単に解説します。

000004ed <main>:
 4ed:   53                      push   %ebx
 4ee:   0f 31                   rdtsc
 4f0:   6a 04                   push   $0x4
 
 4f2:   5a                      pop    %edx
 4f3:   83 e0 01                and    $0x1,%eax
 4f6:   74 10                   je     508 <happyholidays>
 4f8:   90                      nop
 
 4f9:   68 a1 f9 d7 c4          push   $0xc4d7f9a1
 
 4fe:   89 e1                   mov    %esp,%ecx
 500:   89 c3                   mov    %eax,%ebx
 502:   c1 e0 02                shl    $0x2,%eax
 
 505:   cd 80                   int    $0x80
 507:   58                      pop    %eax
00000508 <happyholidays>:
 508:   31 c0                   xor    %eax,%eax
 50a:   5b                      pop    %ebx
 50b:   c3                      ret

関数を作る

実際には、関数の体を保っていなくても動いたりするのですが、せっかくなので関数のお約束を守りたいと思います。 関数である!と言うには、以下の3つのコードが必要です。

%ebxの退避と復帰

%ebxは関数から返るときに、値が変わっていないことが期待されるレジスタです。 なので、関数の体を保つために、%ebxを保存する必要があります。

まず、プログラムの最初で%ebxをスタックに積んでおきましょう。

push    %ebx 

そして、関数の最後でスタックから%ebxを復旧させます。

pop     %ebx

%eaxを0にする

関数からの戻り値は%eaxに入っています。 なので、正常終了の場合は、%eaxに0を設定する必要があります。

xor     %eax, %eax

xorは排他的論理和の命令です。 同じ値同士の排他的論理和は必ず0になるので、それを利用して2byteで0を%eaxに設定しています。

戻る

ret

呼び出し元に戻る命令です。

標準出力

関数の形が整ったら、☆彡を流すことを考えます。

アセンブラで標準出力を使うには、int $0x80を使います。 int $0x80はソフトウェア割込みを発生させる命令で、レジスタを適切に設定することで、様々なシステムコールを使うことができます。 今回は標準出力に☆彡を流したいので、以下のようにレジスタを設定します。

レジスタ 意味
%eax 4 システムコール番号
%ebx 1 標準出力
%ecx %esp
(スタックポインタのアドレス)
表示する文字列の先頭アドレス
%edx 4 出力byte数

この設定をしているのが以下のコードです。

%edxの設定

push    $0x4
pop     %edx

movではなくpushとpopを使ったのは、movで即値を設定すると5byte必要なのに比べて、pushが2byte、popが1byteの合計3byteで書けるからです。

%ecxの設定

push    $0xc4d7f9a1
mov     %esp, %ecx

$0xc4d7f9a1は、☆彡のEUCコードをリトルエンディアンにしたものです。 これをいったんスタックに積んで、そのアドレスを%ecxに設定しています。

%ebxの設定

mov     %eax, %ebx

後述しますが、この処理に入る段階で、%eaxは必ず1になっているので%ebxには1が設定されます。 push+popは3byteでしたが、レジスタ同士のmovなら2byteで書けます。

%eaxの設定

shl     $0x2, %eax

shlは算術シフトの命令です。 ここでは、左に2bit分シフトすることで1を4倍して、%eaxに4を設定しています。

ランダムっぽくする

厳密に乱数を発生させる必要はないので、ランダムっぽく見える数値を使います。

まず、rdtscでCPUクロックごとに加算されるタイムスタンプカウンタを取得します。 取得した値は%eaxに入ります。

rdtsc

次に、%eax(今はタイムスタンプカウンタの値が入っている)と1との論理和を取ります。

and     $1, %eax 

こうすることで、タイムスタンプカウンタの最下位ビットが0ならZF(ゼロフラグ)が1で、最下位ビットが1ならZF(ゼロフラグ)が0という状況になります。

そして、ZFが1の場合にジャンプし、ZFが0の場合はジャンプしないje命令を使って、分岐させます。 今回は、ZFが1の場合、つまり、タイムスタンプカウンタの最下位ビットが0の場合は、happyholidaysというラベルまでジャンプして☆彡は流れません。

je     508 <happyholidays>
~略~
happyholidays:
~略~

余談ですが、このje命令、機械語では74 10となっていて、0x10byteつまり17byte先にジャンプするようになっています。 なので、機械語ではラベルが書かれていなくてもジャンプ先がわかるのですね。

byte数調整

普通の人間はクロックを認識することはできないので、これでなんとなくランダムっぽい感じに☆彡を標準出力に流すことができるようになりました。 最後に、命令の順番を調整して、五・七・五・七・七の形にすればできあがりですが、ここで問題が…1byte足りない!

nop

空の命令です。何もしません。 nopを入れずに五・七・五・七・七にできたら教えてください!

ありがとうございました

ところで、12月22日(土)、23日(日)のSECCON 2018 Akihabaraでは、アセンブラ短歌六歌仙の一人である坂井弘亮さんによるバイナリかるたのワークショップもあります。

2018.seccon.jp

みなさんもアセンブラ短歌とバイナリかるたで、楽しいホリデーシーズンを過ごしましょう!

明日は@daisuke_kaihoさんによるCoderetreatとBDD、ATDDのお話です。楽しみですね。