hh.gif
7割くらい書いたところで存在を忘れていました。
http://slashdot.jp/sp/binary2008/bin2008_shinh.shtml
何かに使えることがあるかもだから(無いと思うが) com2txt 書いとくかーと書いたのでした。オリジナルの com2txt は短すぎないか。 base64 よりはちょっとデコードしやすそうなフォーマットだとはいえ。ただうちでは動かんかったのだけど。
でまぁ com2txt だけじゃつまらないのでどうでもいいネタをしょぼしょぼしこんだのでした。
以下解答。
ruby hh.gif > hh_ruby.com
とかで出てきたファイルは
- ASCII のみで表現された Happy Hacking! と出力する COM ファイル
- オリジナルの GIF ファイルを出力する Ruby スクリプト
- オリジナルの GIF ファイルを出力する Perl スクリプト
であると同時に、
でもあります。
オリジナルのファイルは、
- Happy Hacking と書かれた GIF ファイル
- Happy Hacking! と出力する COM ファイル
- Ruby で記述された com2txt
- Perl で記述された com2txt
であると同時に、
という2種類のプログラムでもあります。
ついでにデータとして残りのキーワードを埋めてまして、
- *WILL* と、 UTF8 でおもむろに埋まっている (実はこれが一番難しいのではないか…)
- 10分以上 (655.35sec) 待つと "You" に変わるアニメ GIF (1秒で元の絵に戻るので gimp とかそのへんで見ると良い)
- "Happy Hacking" の方の RGB の下位 1bit に steganography で "Resistance" と書かれた画像が埋め込まれている (gimp の同じ色部分の範囲選択を使うとよくわかる)
キーワードで適当にぐぐると出てくるんですが、 Resistance is futile, You will be assimilated と、 Star Trek の敵役、集合知性チックな機械生命体、 Borg のセリフでした。まぁ適当。
Python と Haskell
まず Python と Haskell の方だけど、 Ruby/Perl/Python に C 混ぜるのは polyglot quine とかでやってたんだけど、 Haskell 混ぜるのはできる気がしてたけどやってなかったのでやってみたというかんじ。
とりあえずコード。
q= --0;""" 'i'{- "#""";print"ZnV0aWxl".decode("base64");"""=; 'ASCIIで書いたCOM+base64されたデータ'=~/^.{107}(\S+)/ms;q=$><<$+.unpack("m")#=;use MIME::Base64;print decode_base64$+ #-}:"s";main=putStrLn q--"""
最初の q= が色々な意味を持ってるのが一番大事なとこだと思う。
Perl だと q=...= で文字列リテラルになるので、次に = が出てくるところまではコメントみたいなもん。 perldoc -f q など参照。
Ruby, Python だとこれは q って変数に代入しているわけ。代入してるモノは --0 なので、これらの言語はデクリメントが無いため、単に 0 を代入している。
Haskell だとこれは代入なんだけど、代入してるモノは --0 は 0 じゃなくて、 -- のせいで行末までコメントアウトなので、次の行以降に書いてあるものを代入してることになる。
最後に x86 だと、 q= って jno なんですね。プログラム起動時にオーバーフローフラグは立ってないと仮定してる(今思えばこれはいいんだろうか…)として、 '=' == 61 バイトほど前方に jmp 。結果として4行目まで飛びます。
で、 q= --0 で Perl と Haskell と x86 が脱落してるところで、 """ とかして、 Python Ruby ともに脱落させてます。 Python は """ で複数行リテラルに突入、 Ruby は単に空文字列とそれ以降の文字列連結。
で次の行は Haskell 復活してるので、このへんに 'i'{- とかして、 'i' だけ見せて複数行コメントに。後半の部分とつなげると結局、
q='i':"s" main=putStrLn q
という感じ。 less で見れちゃうのもアレなので、 is は一応切っておくかなーという程度の配慮。
次の行で Python の処理を。
q= --0;""" 'i'{- "#""";print"ZnV0aWxl".decode("base64");"""=;
ダブルクォート一個で、 Ruby は文字列から脱出するけど Python は脱出しない。そこでコメント入れれば Ruby 脱落。でそのまま """ で文字列リテラルを脱出して、 print"ZnV0aWxl".decode("base64") と、これは単に futile を base64 encode しただけ。
あとは COM の ASCII 表現の部分は '' に囲まれていて、その後で Ruby / Perl 共通で文字列代入できるように、 'hogehoge-'=~/^.{107}(\S+)/ として、 base64 encode されたコードが $+ に入るようにしてある。んでいつも通り Ruby と Perl 切り離して $+ を base64 decode して出力すれば復元できる。
あとはまぁどの言語でもエラーにならんようにコメントとか適当に終了させてやって終わり。
jar
GIFAR というものがあるらしく、 jar は後ろから見てくから cat でつなげりゃいいらしいです。カンタンに作れて良いのです。
1111
該当 Perl コードは、
use MIME::Base64;$/=$\;open I,$_=shift||$0;@a=split"\n",encode_base64(('w'x$_)!~/^(ww+)\1+$/|$_-$+[1]-1010?<I>:(b.chr$+[ 1])!~/$/); print$`,@a,$';exit}
ってあたり。単なる base64 エンコード以外によくわからないことをやっていて、まず、
('w'x$_)!~/^(ww+)\1+$/
が個人的に有名な正規表現を使って素数判定をする物体。今回は | でつないでるので、素数じゃない時だけトリガーします。
$_-$+[1]-1010?<I>:(b.chr$+[ 1])!~/$/);
で素数じゃなくてかつ、 $_-$+[1]-1010 が 0 の場合だけ、です。さっきの正規表現で、 $+[1] には最大約数が入るようになっています。この条件を満たすのは、 1111, 1515,2020 の 3 つ。まぁ 11月11日ってことで 1111 だけがバイナリじゃない文字列を出力します。
b.chr$+[ 1])!~/$/
はちょっとわかりにくいですが、文字列 'b' と chr(101) を文字列結合して (101 は 1111 の最大約数)、 $` に代入する、というようなことをしています。ついでに encode_base64 が空文字列返すように、真じゃない条件式になっています。
というわけで print$`,@a,$' で be を出力して終わり。
余談ですが、 $+[1] の途中に不自然に存在する空白は、 GIF の comment extension を都合のいい位置で終わらせるために使っています。
steganography
こんなコードで抽出できます。
require 'sdl' img = SDL::Surface.load(ARGV[0] || 'main.gif') img.h.times{|y| img.w.times{|x| a = img.getRGB(img.getPixel(x, y)) b = [0, 0, 0] if a[0] == 1 || a[0] == 254 b = [255, 255, 255] end img.putPixel(x, y, img.mapRGB(*b)) } } img.saveBMP('decoded.bmp')
com2txt
基本的には COM ファイルを base64 エンコードしてデータとして、後は ASCII だけで書いた 16bit x86 の base64 デコーダがあればいいんだけど、まぁデコーダ作るのは簡単ではなかった。けどイマイチ何を頑張ったか覚えていない。自己書き換えはゴルフ的においしくないので、なるべく ASCII だけで表現しておいて、しょうがないところだけ自己書き換えを使うことにしている。具体的には後方 jmp とレジスタ指定 jmp だけは ASCII では無理だったので自己書き換えした。
よく覚えてないので箇条書きに…
- jmp は結構大変。なぜなら必ず前方でなくてはならず、飛ぶ量は 10 か 32 から 126 でないといけないから。(今回タブは無しってことにした)
- レジスタのクリアは最初は and AX, 0x4040; and AX, 0x2020; とかで。後はこれを push しておいて pop して使った。
- それ以外の mov は基本 xor で。
- 命令によって結構使えないレジスタが多い。メモリ指定するオペランドは BX, SI, DI を使う必要があって、メモリいじる時に読み込み元/書き込み先に使うレジスタは SI, DI か AH, BH, CH, DH 、レジスタの移動は基本 push/pop で、など細々とした制約が。
- 任意の数字を作る時は、用意しておいた 0 と、 sub を何回か使う。例えば 0x100 を作るなら 0-0x6060-0x6060-0x3e40 など。
- コードは、 <データを1バイトずつなめて 0-63 に変換するコード><データ><データをなめて 4B を 3B にしつつ自己書き換えしていくコード><先頭に jmp> って感じで配置した。自己書き換えをするロジックはデータより後ろにないとデコードしてる最中に自分を破壊してしまうことと、任意長がありえるデータなので終端検出する必要があり、終端検出するなら 0-63 への変換くらいは同時にやった方がラクだから。
- base64 の 4B => 3B 変換は割と大変だったと思う。シフトとかがあれば一発な 0b00AABBCC の AA を下位 2bit として取ってくる、とかが難しい。最終的に、
xor [SI+43], DI imul DI, [SI+44], byte 64 xor [SI+40], DI imul CX, [SI+41], byte 64
などとやっている。 0b00AABBCC を 64 倍して 0b0000AABB_CC00000000 にして、これをメモリに書いておいて (xor は出力先をゼロクリアしてあるので、 mov のかわり)、 1B ずらしてかけ算することによって、 0b0000AABB を 64 倍して 0b000000AA_BB000000 として、後は CH を使うことによってなんとかしたみたい。もちょい簡単にできそうなもんだけど。