プログラムは ASCII で書くべきだよ

ふと、 main = 195; とかやって喜んでいた私はバカだったんじゃないかと思いました。 Binary 2.0 などとうかれてほとんどバイナリのコードをゴルフに submit してたのはどうなのか、と。プログラムは ASCII で書くべきだったんじゃないでしょうかと。それもメンテナンスしやすいように、 isprint が true を返すような文字で書くべきではないのか、と。

とりあえず Hello, world! 書きました。そのままコピペでたぶん最近の x86 & linux & glibc なら動きます。それ以外の環境では無理です。

char main[]="`j X$@P[PYPPPPX4.4 PZUX, P^XH,=)F(P_X3F()8)8@)8@@)8)8@PYX@@@@CQBaGHello, world!\n";

int 0x80 とか ret とか ASCII の範囲外だから苦労するね…これらの命令は自己書き換えで実行時に生成されています。気合いで。

バックスラッシュは \xc3 で ret とかされると興冷めなので、反則と考えているわけですが、最後の改行をめんどいので取っ払ってません。あとスタックの状態次第でたまに落ちるんで落ちたら何回か実行してみて下さい…

一応上のコードは適度にゴルフった結果です。その前に書いた、以下のコードは処理がそれなりにマジメなので落ちたりしないと思いますしバックスラッシュも入ってません。

char main[]=
    "QVWUZ%@@@@%    P^JJJJJJJJJJJJJJJJJJJJJJJJ3B PVXH, PTXHHHH!0Z_18R_)8)8)8)8"
    ")8)8)8)8WV_38WX_)8)8)8@)8@@@@)8)8)8T_VXOOOOOOOOOOOOOOOOOOOOOOOOWYVX!75Hel"
    "lVP^17^GGGGVX!75o, wVP^17^GGGGVX!75orldVP^17^GGGGVX!75aJ@@5@@@@VP^17^VXP["
    "PZBBBBBBBBBBBBBBC@@@@AHI@AHI@AHI@AHI@AHI@AHI@j__^Y`";

あ、アヒアヒとかは開いた空間を埋めるだけの命令なので、故意です。

んで ASCII 縛りの説明とか

ASCII 縛りというか C 文字列縛りというか。

いやこれ結構大変なんですよ。とりあえず使える命令がわからんと話にならないのでこういう表を作る。

http://shinh.skr.jp/binary/x86_ops.txt

これはバイナリツールと化している irb でサクサク作りました。以下とか参考に。

http://d.hatena.ne.jp/shinichiro_h/20061117#p3

あとこんなんでアセンブラかました結果がどういう機械語になるかを調べるものも作っておきました。 .irbrc に入ってます。

def asm(b)
  tmp = Tempfile.new('irb_asm', '/tmp')
  tmp.print(b)
  tmp.close
  system("nasm #{tmp.path} -o /tmp/irb_asm2")
  print `od -t x1z -A x /tmp/irb_asm2`
  nil
end

で、この表を見ていると色々気付きます。

まず絶対必須な int 0x80 (0xcd 0x80) も、 ret (0xc3) もありません。いきなり死ねる。そこで自己書き換えです…でも、どうやって?

まず call がありません。よって Binary Hacks #79 的な call pop での EIP 取得術はできません。

えーとどうするねん…と思ったのですが、これは以前考えたことがある問題でして、 return する場所は普通にスタックにあって、 return アドレスの少し後ろには call 命令、つまり main のアドレスがあるに違いないっ…と思って探したのですが、なんかありませんでした。

んで呼び出し元付近を適当に逆アセしてみると、結局、 call *8(%ebp) とかで main に飛んでくるみたいで、これは持ってる環境だとだいたいそんな感じでした。んじゃ main に限っては、 movl 8(%ebp), %esi で main のアドレスを拾って来れるわけです。

さて今度は、 main のアドレスから、 int 0x80 を置きたい位置にポインタを進めないといけない。しかし add は全て封印されています。繰り返し処理はたぶんできません(条件 jmp はあるんだけど、負値を指定しないと後ろに戻れない)。つーわけで適当にデカい負値を sub で作って、その値を sub することによって負数の引き算は足し算ですよーとかいう原理でポインタを進めます。

まぁそんな感じで自己書き換えはできる。あとは気合いとかで。

  • 実はレジスタ間の演算が一切できないのもつらいところ。必ずメモリvsレジスタで計算することになる。
  • さらに、 sub [EAX], ESI はいいけど、 sub [EAX], EAX はダメとか、レジスタに微妙な拘束条件がつきまくる。
  • でも移動は push/pop でできる。つか push pop はありがたい。
  • もちろん inc/dec も便利。
  • 即値 0 が使えたらラクなのになあという。最初に push 0x20; pop EAX; and AL, 0x40 とかで作って、適当に何度か push して使っています。
  • 小さい値使えないせいで、 [EBP+8] をするためにわざわざ 32 を引いてから [EBP+40] で取得的な苦労を…

というようなことして作られたソースコードは以下に置いときます。 \n を実行時に作ろうとしてうまくいかんかった痕跡が残っています。

http://shinh.skr.jp/binary/ascii_golf.s

無限るーぷ

スラド経由で

http://slashdot.jp/developers/comments.pl?sid=344503&cid=1078407

これ経由で

http://www.tietew.jp/cppll_novice/archive/1578

こちら。

http://eyeba11z.blogspot.com/2006_11_01_eyeba11z_archive.html

GCC なら普通にこんな感じで削れますね。あ、元のもそうですが x86 限定です。

http://shinh.skr.jp/binary/forever.c

なにかあれば下記メールアドレスへ。
shinichiro.hamaji _at_ gmail.com
shinichiro.h