YARV: 命令列のシリアライズによるRubyプログラムの難読化
はじめに
YARVコアの入ったrubyでは、YARVの命令列のシリアライズ機構を用いることでRubyプログラムを簡単に難読化できます。今回は、hello worldなRubyプログラムを難読化してみます。なお、今回の説明ではリビジョン11607を使い、YARVコアの入ったrubyがビルドした状態を前提にしています。また、YARV Maniacs 【第 8 回】 命令列のシリアライズを予め読んでおくと、良いと思います。
難読化ツールの利用
tool/compile.rbを使うと簡単にRubyプログラムの難読化ができます。ここでは、hello worldなRubyプログラムを難読化してみます。-oオプションで難読化処理されたRubyプログラムのファイル名を指定します。このオプションが無いと、デフォルトでは「a.rb」というファイルが生成されます。
% cat hello.rb
puts "hello, world"
% ./ruby -I./lib tool/compile.rb -o obfuscated.rb hello.rb
難読化されたhello worldなプログラムは以下の通りです。YARV命令列がシリアライズされ、それがBase64でエンコードされています。
% cat obfuscated.rb VM::InstructionSequence.load(Marshal.load(<<EOS____.unpack('m*')[0])).eval BAhbEiIkWUFSVkluc3RydWN0aW9uU2ltcGxlZGF0YUZvcm1hdGkGaQZpBjAi CzxtYWluPiINaGVsbG8ucmJbADoNdG9wbGV2ZWxbAFsKaQBpAFsAaQBpAFsA WwlbBjoLcHV0bmlsWwc6DnB1dHN0cmluZyIRaGVsbG8sIHdvcmxkWws6CXNl bmQ6CXB1dHNpBjBpDTBbBjoKbGVhdmU= EOS____
この難読化されたRubyプログラムを実行すると、全く同じ結果が得られます。
% ./ruby obfuscated.rb hello, world % diff =(./ruby hello.rb) =(./ruby obfuscated.rb)
またもやパッチ
実は、最新のソースコードでは、難読化したRubyプログラムは期待通りに動作しません。以下のパッチを適用する必要があります。
% svn diff Index: ruby.h =================================================================== --- ruby.h (revision 11607) +++ ruby.h (working copy) @@ -771,6 +771,8 @@ else if (!RTEST(obj)) { if (obj == Qnil) return T_NIL; if (obj == Qfalse) return T_FALSE; + } else { + if (SYMBOL_P(obj)) return T_SYMBOL; } return BUILTIN_TYPE(obj); }
このパッチを適用していないと、以下のようなエラーが出ます。
% ./ruby obfuscated.rb obfuscated.rb:1:in `load': can't convert Symbol to Symbol (Symbol#to_sym gives Symbol) (TypeError) from obfuscated.rb:1:in `<main>'
パッチ作成までの過程
まず、エラーメッセージを出力している場所をgrepで特定しました。
% grep -rn "can't convert" . | grep -v svn | grep gives ./object.c:1856: rb_raise(rb_eTypeError, "can't convert %s to %s (%s#%s gives %s)", ./object.c:1873: rb_raise(rb_eTypeError, "can't convert %s to %s (%s#%s gives %s)", ./object.c:1886: rb_raise(rb_eTypeError, "can't convert %s to Integer (%s#%s gives %s)",
object.cの1856行目のrb_raise関数の呼び出しが怪しそうだったので、以下のようにブレークポイントを配置しました。bp()はマクロで、debug.cで定義されているdebug_breakpoint関数に置換されます。bp()マクロは、「make gdb」と組み合わせて使います。これは結構便利です。
Index: object.c =================================================================== --- object.c (revision 11607) +++ object.c (working copy) @@ -1853,6 +1853,7 @@ v = convert_type(val, tname, method, Qtrue); if (TYPE(v) != type) { char *cname = rb_obj_classname(val); + bp(); rb_raise(rb_eTypeError, "can't convert %s to %s (%s#%s gives %s)", cname, tname, cname, method, rb_obj_classname(v)); }
makeでビルドします。make gdbを実行すると、GDBが起動して先ほどbp()と書いた所で実行が停止します。make gdbではtest.rbを対象にしてデバッグするため、cpコマンドでobfuscated.rbをtest.rbにコピーしておきました。
% make % cp obfuscated.rb test.rb % make gdb gdb -x run.gdb --quiet --args ./miniruby -I./lib ./test.rb Using host libthread_db library "/lib/libthread_db.so.1". Breakpoint 1 at 0x80cfb13: file debug.c, line 71. [Thread debugging using libthread_db enabled] [New Thread -1208252752 (LWP 2815)] [New Thread -1208255584 (LWP 2819)] [Switching to Thread -1208252752 (LWP 2815)] Breakpoint 1, debug_breakpoint () at debug.c:71 71 }
ちなみに、GDBの-xオプションで指定しているrun.gdbには以下のような記述が書かれています。GDBは、-xオプションで指定したスクリプトを実行してくれます。このスクリプトを見た時、こんなテクニックがあるのかと、ちょっと感動しました。
% cat run.gdb b debug_breakpoint run
GDBのセッションでbtコマンドを実行して、バックトレースを調査しました。
(gdb) bt #0 debug_breakpoint () at debug.c:71 #1 0x0807bc81 in rb_convert_type (val=3085928060, type=20, tname=0x811a823 "Symbol", method=0x8105156 "to_sym") at object.c:1856 #2 0x080d25bf in iseq_load (self=3085975580, data=3085928180, parent=0, opt=4) at iseq.c:333 #3 0x080d2956 in iseq_s_load (argc=1, argv=0xb7e2d01c, self=3085975580) at iseq.c:380 #4 0x080d3016 in call_cfunc (func=0x80d2900 <iseq_s_load>, recv=3085975580, len=Variable "len" is not available. ) at call_cfunc.ci:24 #5 0x080d6ade in th_eval (th=0x91a0e40, initial=0) at ./insns.def:1278 #6 0x080d8cbc in th_eval_body (th=0x91a0e40) at vm.c:1521 #7 0x080dadb8 in yarv_th_eval (th=0x91a0e40, iseqval=3085928580) at yarvcore.c:462 #8 0x080590fb in ruby_exec_internal () at eval.c:233 #9 0x08059136 in ruby_exec () at eval.c:246 #10 0x0805ca21 in ruby_run () at eval.c:266 #11 0x0805686f in main (argc=135429892, argv=0xb, envp=0x1) at main.c:46 (gdb)
フレーム#1を見てみると、TYPEマクロが正しい型を返していないことに気が付きました。printデバッグをしていると、object.cの1856行目の「TYPE(v)」は、obfuscated.rbで起動した時には、常に0x07つまりT_STRINGが返っていることがわかりました。
(gdb) f 1 #1 0x0807bc81 in rb_convert_type (val=3085928060, type=20, tname=0x811a823 "Symbol", method=0x8105156 "to_sym") at object.c:1856 1856 bp(); (gdb) list 1846 1847 VALUE 1848 rb_convert_type(VALUE val, int type, const char *tname, const char *method) 1849 { 1850 VALUE v; 1851 1852 if (TYPE(val) == type) return val; 1853 v = convert_type(val, tname, method, Qtrue); 1854 if (TYPE(v) != type) { 1855 char *cname = rb_obj_classname(val); 1856 bp(); 1857 rb_raise(rb_eTypeError, "can't convert %s to %s (%s#%s gives %s)", 1858 cname, tname, cname, method, rb_obj_classname(v)); 1859 } 1860 return v; 1861 } 1862 1863 VALUE 1864 rb_check_convert_type(VALUE val, int type, const char *tname, const char *method) 1865 { (gdb)
それで、TYPEマクロはrb_type関数に展開されるので、rb_type関数のコードを見ました。調べていると、「objの型が明らかにT_SYMBOLである時、IMMEDIATE_P(obj)は常に真である」という命題は成り立たないことがわかりました。つまり、IMMEDIATE_P(obj)が真でなくても、SYMBOL_P(obj)が真になるケースがあることに気が付きました。また、もともとこのif文は問題があって、else ifのelseが無いので、ロジックに抜けがある可能性が非常に高いと思いました。
static inline int rb_type(VALUE obj) { if (IMMEDIATE_P(obj)) { if (FIXNUM_P(obj)) return T_FIXNUM; if (obj == Qtrue) return T_TRUE; if (SYMBOL_P(obj)) return T_SYMBOL; if (obj == Qundef) return T_UNDEF; } else if (!RTEST(obj)) { if (obj == Qnil) return T_NIL; if (obj == Qfalse) return T_FALSE; } return BUILTIN_TYPE(obj); }
以上がパッチ作成までの流れです。
おわりに
今回は、Rubyプログラムを難読化する方法を紹介しました。最新のソースコードでは、難読化したRubyプログラムが期待通りに動作しなかったので、パッチを作成しました。また、パッチ作成時に、ささださんオリジナルのbp()マクロとmake gdbを組み合わせて、効率よくデバッグできることを確認しました。
追記(2007/02/02 23:05)
r11615から、ここで紹介したパッチを適用しなくても、Rubyプログラムを難読化できるようになりました。
matz 2007-02-02 22:19:44 +0900 (Fri, 02 Feb 2007) New Revision: 11615 Modified files: trunk/ChangeLog trunk/compile.c trunk/parse.y trunk/ruby.h trunk/string.c Log: * ruby.h (SYMBOL_P): make Symbol immediate again for performance. * string.c: redesign symbol methods. * parse.y (rb_id2str): store Strings for operator symbols. [ruby-dev:30235]