アセンブリをコマンドライン上で直接コンパイル、逆コンパイルする
プログラムをある程度やっているとマシン語をそのままコードにうめこみたくなったり、突如としてあらわれたバイトコードを解読したくなります。コマンドラインから insn mov eax, 1 と書けば b801000000 を、 insn -d cccccccc と書けば nop nop nop nop を表示してくれるような気のきいたツールがあればいいのですが残念ながら無いようです(ないというか標準的なコマンドでは簡単に実現できない)。それを知人に愚痴ったところ、 echo "mov eax, 1" > a.s && nasm -f bin a.s -o a.o && od -tx1 a.o という強引な方法を教えてもらいました。強引すぎるので真っ先に脳内のブラックリストに追加された方法です。一番簡単で怠惰な方法を探す苦労と、強引だけど確実な方法を探す苦労のバランスがいまいち掴めない次第であります。それで、この方法では逆コンパイルはできない上にコマンドが面倒くさいので、もう少し使いやすくすると同時に逆コンパイルも可能なスクリプトを書いてみました。
insn:
#!/usr/bin/perl use strict; use warnings; use Getopt::Std; use File::Temp qw(tempfile); use Readonly; use Carp; Readonly my $ASM => 'nasm'; Readonly my $HEXDUMP => 'od -tx1'; Readonly my $OBJDUMP => 'objdump'; Readonly my %ARCH => ( 'i386' => { bits => 32 } ); Readonly my $DEFAULT_ARCH => 'i386'; my %opt = (); getopts('da:b:', \%opt); # get arch my $arch_name; my $arch; if (defined $opt{a}) { $arch_name = $opt{a}; croak 'Can not find the architecture' unless exists $ARCH{ $arch_name }; $arch = $ARCH{ $arch_name }; } unless (defined $arch) { $arch_name = $DEFAULT_ARCH; $arch = $ARCH{ $arch_name }; } # override arch spec if indicated my $bits = defined $opt{b} ? $opt{b} : $arch->{bits}; if (defined $opt{d}) { # deinsn my $s = join '', @ARGV; my @hex_insns = (); while ($s =~ /\G(..)/g) { push @hex_insns, hex $1; } my ($in, $in_filename) = tempfile(); print $in pack('C*', @hex_insns); print `$OBJDUMP -b binary -m $arch_name -D $in_filename | awk -F '\t' 'start == 1 { print \$3 } /^(:?00)+/ { start = 1 }'`; } else { # insn my ($in, $in_filename) = tempfile(); my $insns = join ' ', @ARGV; $insns =~ s/;/\n/g; print $in "bits $bits\n", $insns; (undef, my $out_filename) = tempfile(); my $dump = `$ASM -f bin $in_filename -o $out_filename 2> /dev/null && $HEXDUMP $out_filename 2> /dev/null`; croak "failed to parse or dump" unless $dump; print grep { defined } map { (/^[^\s]*\s?(.*)/m)[0] } split("\n", $dump); print "\n"; }
かなり付け焼き刃なのでアーキテクチャサポートも満足にできていません。とりあえず i386 限定になっています。使い方は上記したように、
% insn mov eax, 1\; mov ebx, 1 b8 01 00 00 00 bb 02 00 00 00 % insn -d b801000000bb02000000 mov $0x1, %eax mov $0x2, %ebx
逆コンパイル結果が gas 記法になってしまうのが残念なのですが、まあこれぐらいは我慢しましょう。
スクリプトのロジックはかなり複雑になっています。コンパイルするときは nasm にアセンブリを -f bin で渡します。これによってオブジェクトファイルを生成せずに生のマシン語を吐くように指定できます。あとはそれを od でダンプして見易いように 整形しています。逆コンパイルするときは objdump の -d を使うのですが、本来オブジェクトファイルに含まれているはずの情報が binary フォーマットによって欠落しているので、コマンドラインオプションで適切に指示してやる必要があります。まず -b で binary の bfdname を与え、次に -m でアーキテクチャを教えます。そして最後は -D でまるごと逆アセンブルするように指定します。よく使われる -d は .text を探して逆アセンブルするようで、 .text 云々を一切欠いている binary フォーマットでは無効のようです。
さてこれでわからないマシン語がでてきても大丈夫です。コマンドラインに移って直ちに
% insn -d cc int3
とすればいいのです。便利ですね。もちろん x86 のマシン語構成をまるごと覚えてしまうのが手っ取りばやいのですが、 x86 は複雑なことで有名なのでなかなかそうもいかないでしょう。それでも覚えたいという人は以下の資料を片手にがんばってみてはいかがでしょうか。
http://www.intel.co.jp/jp/developer/download/index.htm#ia32
それで、例えばCで実際に使うときには、
% insn 'mov eax,[esp+4];add eax,[esp+8];ret' | perl -e '$,=", ";print map{"0x$_"} split " ",<>;' 0x8b, 0x44, 0x24, 0x04, 0x03, 0x44, 0x24, 0x08, 0xc3
として、この出力結果をコピーして以下のようなプログラムを書きます。
insn1.c:
unsigned char _add_insns[] = { 0x8b, 0x44, 0x24, 0x04, 0x03, 0x44, 0x24, 0x08, 0xc3 }; int main() { int (*_add)(int, int) = ((int (*)(int, int))_add_insns; printf("%d\n", _add(1, 2); // 3 return 0; }
ところで、関係ないのですが、 Perl で文字列から二文字づつ読み取るという処理はどう書くのがベストなのでしょうか。 insn では
substr1.pl:
$s = 'abracadabra' x 100000; @o = (); while ($s =~ /\G(..)/\g) { push @o, $1; }
という方法を取っていますが、正規表現を使いまくるのはどうも気がひけるのです。素直にやるならば
substr2.pl:
$s = 'abracadabra' x 100000; $i = 0; $len = length $s; @o = (); while ($i < $len) { push @o, substr(\$s, $i, 2); $i += 2; }
なのでしょうが、これもあまり気にいりません。
他にもいろいろ考えてみました。
substr3.pl:
$s = 'abracadabra' x 100000; @o = grep { length } split(/(..)/, $s);
split の使いかたが根本的に間違っていますが、一応動きます。メモリ使いまくるのが難点。
substr4.pl:
$s = 'abracadabra' x 100000; @o = unpack('(a2)*', $s);
これもメモリ使いまくりそうだけど、 builtin 関数で処理してるのは大きいはず。
総合的にみてどれがベストなのか判断するためにベンチマークとってみます。
% type -a time time is a reserved word time is /usr/bin/time % /usr/bin/time --version GNU time 1.7 % /usr/bin/time perl substr1.pl 0.82user 0.02system 0:00.86elapsed 97%CPU (0avgtext+0avgdata 0maxresident)k 0inputs+0outputs (0major+10552minor)pagefaults 0swaps
あれ、メモリ使用量がでない。 xen だから?うーむ。直すの面倒なのでとりあえず実行速度だけベンチマークとります。
% for f in substr*.pl; echo -n $f ": "; do time perl $f; done substr1.pl : perl $f 0.80s user 0.05s system 96% cpu 0.880 total substr2.pl : perl $f 1.51s user 0.01s system 97% cpu 1.557 total substr3.pl : perl $f 1.36s user 0.12s system 97% cpu 1.523 total substr4.pl : perl $f 0.50s user 0.06s system 93% cpu 0.598 total
($f を展開してくれると助かるんですが)
速度的にはやはり unpack 版が一番速いようです。しかし意外にも正規表現版ががんばっています。メモリ使用量のことを考えますと、これはよくあるトレードオフの関係にあるようです。メモリを使いまくるって高速化するか、メモリを節約するかわりに速度を犠牲にするか。まあ builtin 関数で正規表現版のロジックに相当する関数があればいいのですがね。それにしてもこういうことを実験していると C の偉大さを認識せずにはいれませんね。なんといってもポインタの加算だけで事実上文字列シフトしていくわけですから。さて、上の実験ではやはり builtin 関数は速いけど正規表現も巷で言われているほど悪くない(適材適所なのですが)ということがわかりました。上の四つの方法より速いあるいはメモリ使用量が少ないという方法を知っていましたら是非教えてください。
- The URL to Trackback this entry is:
- http://dev.ariel-networks.com/Members/matsuyama/compile_and_decompile_assemblies_on_commandline_immediately/tbping
Re:アセンブリをコマンドライン上で直接コンパイル、逆コンパイルする
insn、使わせて頂きました。
パッチを逆アセンブル解析する場合に大変役立ちます。
ありがとうございます。
それでは、失礼します。