YARV: 命令列のシリアライズによるRubyプログラムの難読化

はじめに

YARVコアの入ったrubyでは、YARVの命令列のシリアライズ機構を用いることでRubyプログラムを簡単に難読化できます。今回は、hello worldRubyプログラムを難読化してみます。なお、今回の説明ではリビジョン11607を使い、YARVコアの入ったrubyがビルドした状態を前提にしています。また、YARV Maniacs 【第 8 回】 命令列のシリアライズを予め読んでおくと、良いと思います。

難読化ツールの利用

tool/compile.rbを使うと簡単にRubyプログラムの難読化ができます。ここでは、hello worldRubyプログラムを難読化してみます。-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]