作者:fatezero
0x00 前言
RubyEncoder 是一款對 Ruby 代碼進行混淆加密的軟件,因為最近我要破解某個使用 RubyEncoder 加密的 Ruby 程序, 所以工作就轉移到如何解密 RubyEncoder 加密的程序。
0x01 信息收集
要想了解 RubyEncoder,那肯定是要去官網下載一份試用版,但是無論你怎么填寫試用資料:

我隱約記得 Hacking Team RCS 也用了 RubyEncoder,所以比較幸運的, 我從 Hacking Team 郵件 中找到了一個可以登陸賬號密碼:
Username: alor@hackingteam.it
Password: Oyf4GSy0
下載到了 RubyEncoder-2.3,并偷偷的使用 Hacking Team 一個 License Key 成功激活了 RubyEncoder

先簡單試用一下 RubyEncoder

上圖是 RubyEncoder 對某個項目加密的主界面,在這個界面中可以看到,我們可以選擇支持的 Ruby 版本。 由于因為可以選多個版本號以及 1.8.x 也在支持的版本內,所以可以判定加密后的文件不會是 iseq 或者修改后的 iseq。

上圖是 RubyEncoder 支持的加密選項,可以進行 IP、Domain、MAC、聯網、時間、License 限制。其中除了 License 文件之外,其他都是紙老虎, 如果 License 文件沒有參與對文件的加密,那 License 限制也是紙老虎。不過根據官方文檔描述
The algorithm uses an idea of two keys. The first key (Project Id) is stored in the encrypted area of the protected script and is used to decrypt an external license file. The second key (Project Key) is stored in the license file and it is used to decrypt the bytecode from the protected script.
所以如果沒有 License 文件是很難將程序跑起來的,不過這篇文章的目的不是怎么樣解除這些限制,而是如何解密 RubyEncoder 加密后的 Ruby 代碼。
我們再來看一下 RubyEncoder 的目錄結構:
.
├── Loaders
│ ├── Linux
│ │ ├── loader.rb
│ │ ├── my.so
│ │ ├── rgloader.linux.so
│ │ ├── rgloader.linux.x86_64.so
│ │ ├── rgloader19.linux.so
│ │ ├── rgloader19.linux.x86_64.so
│ │ ├── rgloader192.linux.so
│ │ ├── rgloader192.linux.x86_64.so
│ │ ├── rgloader193.linux.so
│ │ ├── rgloader193.linux.x86_64.so
│ │ ├── rgloader20.linux.so
│ │ ├── rgloader20.linux.x86_64.so
│ │ ├── rgloader21.linux.so
│ │ ├── rgloader21.linux.x86_64.so
│ │ ├── rgloader22.linux.so
│ │ ├── rgloader22.linux.x86_64.so
│ │ ├── rgloader23.linux.so
│ │ └── rgloader23.linux.x86_64.so
│ ├── Mac\ OS\ X // 省略 ..
│ ├── MinGW // 省略 ...
│ └── Windows // 省略 ...
├── RubyEncoder
├── license.txt
├── licgen
├── rgencoder
├── rginfo
├── rubyencoder18.bundle
├── rubyencoder19.bundle
├── rubyencoder192.bundle
├── rubyencoder20.bundle
├── rubyencoder21.bundle
├── rubyencoder22.bundle
├── rubyencoder23.bundle
└── update
簡單看了一下 rubyencoder*.bundle 文件,應該是直接把整個 Ruby 給打包進來了,應該是加密的過程中需要 Ruby 的一些功能, 不過我并不是特別關注加密過程,所以直接看 Loaders 目錄下的文件,這個目錄下包含了所支持的平臺下、Ruby 版本的解密 so 文件。
當然除了需要下載 RubyEncoder 程序,還需要找一找有沒有前輩已經搞定這個程序的, google 一番之后找到 I found way to protect Source Code! :) 這個帖子。
這個帖子的思路是:
將 ruby_exec 修改成 ruby_exic 以便獲得 AST
使用修改后的 ParseTree 將 Ruby 內部的 AST 轉成 sexp
使用 ruby2ruby 將 sexp 轉成 Ruby 代碼
不過這個帖子當時使用的是 Ruby 1.8.7,也就是當時 Ruby 還是構建完 AST 之后就直接執行,1.9.x 后的 Ruby 需要編譯成 iseq。 另外由于 Ruby 1.8 和 Ruby 2.x 有很大的不同,上面的 ParseTree 在 Ruby 使用 iseq 之后就再也不能使用了。 所以上面的方法在 Ruby 2.x 中行不通了。
0x02 簡單逆向
我們使用 RubyEncoder 對以下代碼進行加密
puts "Hello World!"
得到下面加密后的代碼
# RubyEncoder v2.3.0
if not self.respond_to?(:RGLoader_load,:include_private) then _d = _d0 = File.expand_path(File.dirname(__FILE__)); while 1 do _f = _d + '/rgloader/loader.rb'; break if File.exist?(_f); _d1 = File.dirname(_d); if _d1 == _d then raise "Ruby script '"+__FILE__+"' is protected by RubyEncoder and requires a RubyEncoder loader to be installed. Please visit the http://www.rubyencoder.com/loaders/ RubyEncoder web site to download the required loader and unpack it into '"+_d0+"/rgloader/' directory in order to run this protected file."; exit; else _d = _d1; end; end; require _f; end; RGLoader_load('AAIAAAAEgAAAAIAAAAAA/0R/d65ujW/5OhgbeUf0jhTRfPXr0uXNuC7gK8ycmR473fPEIlsgFP1/KF+CYBVbQy4xoLUhBFtBlYwH2aDOtcTasNDJPMDtoEgRuRdFRDgJoX1oKhrm0ZKm9OdIM6MbXRc/fh4n984TVew76DqbxQTplVhMxzOCp/mKgLU+shxBFAAAAGAAAAA7Nu8kj4NtO8BQECP2bW1TonmX+NADX/HETWg1j5fvbB8gptZ38XCzJxOccT2CTUsTT8GFq67RttUD7IR/xN2FBCWKMZ1BlGYVlhSmSUc6hS5RfglTuyvdVdjnsgcnkTAVAAAAYAAAAIrxSQfPHlMc89mPBUXSQ6vxmM9yoDu7Rf+O87mTUW4L0VuAWkIhvFUBxXRVm6Q7kkWHg7D7cdIwwA62+ewy91l56aMIQujAKZrVn4T1zreKf1QdGvK+QGY4rIpGEmTBhBYAAABoAAAADdYzBFrSrrZ4o9uzaoq+Yxjk44lzEa+/oxXM7fmbm8gJ1W3MlUZyPqIjW01KUb6nZjWIAz629+KP5nL/GMP0BClkOjpXQ9b95R/qvlDzuP7UZHPeqaIJq2yMN7Mh9WROfAhLlhmK86AXAAAAcAAAAGgSDy/YvPJQsKnC+JvR+ITlVdWPGodUNT10I0CPLu9d81hMtEL9hU4t9yVfBcS2BWDqBg3ahhUTvqNYxwvX8NCHmZU4LQmdd3dJneWJzGy6VbAQeVDNeaJl8/SPdRn1VXaspqWGYFn1cXqp7rhHLUcAAAAA');
可以看到最關鍵的函數就是 RGLoader_load,所以直接將 rgloader22.linux.x86_64.so 丟進 IDA,找到 RGLoader_load 的實現:
int __cdecl rgloader_load(int a1, _DWORD *a2, int a3, int a4) {
// 省略 ...
v126 = v124;
v127 = _decode_node(v124);
mstream_close(v126);
ruby_xfree(v23);
if ( !v127 || decoder_error )
goto LABEL_243;
v128 = *(_DWORD *)(rb_thread_current() + 16);
v210 = *(_DWORD *)(v128 + 60);
if ( a1 == rgloader_module )
*(_DWORD *)(v128 + 60) = *(_DWORD *)(*(_DWORD *)(v128 + 24) + 56);
else
*(_DWORD *)(v128 + 60) = *(_DWORD *)(*(_DWORD *)(v128 + 24) + 16);
v129 = (char *)rg_current_realfilepath();
v130 = rb_sourcefile();
v131 = rb_str_new_cstr(v130);
v132 = rb_str_new_static("<encoded>", 9);
v133 = rb_iseq_new(v127, v132, v131, v129, 0, 0);
result = rb_iseq_eval(v133);
*(_DWORD *)(v128 + 60) = v210;
return result;
}
嗯,事實上,RubyEncoder 就算是到了 2.3 版本,還是和上面那個帖子所說的一樣:
It turns out, that RubyEncoder uses following scheme: modified Ruby-1.8.7 interpreter, that stores encoded AST nodes along with encoding/restriction options, while rgloader simply decodes it back to AST and executes.
只不過這里多了一步 v133 = rb_iseq_new(v127, v132, v131, v129, 0, 0); 將 AST 編譯成 iseq。
我們可以通過 hook rb_iseq_new 拿到 AST,hook `rb_iseq_eval`` 拿到 iseq。
下面我們修改 Ruby 代碼將 AST 以及 iseq dump 出來。
- one byte hack
cp rgloader22.linux.x86_64.so bak.so
sed 's/rb_iseq_eval/rb_iseq_evax/g' rgloader22.linux.x86_64.so > tmp.so
sed 's/rb_iseq_new/rb_iseq_nex/g' tmp.so > rgloader22.linux.x86_64.so
- 在 iseq.c 中實現 rb_iseq_nex
VALUE
rb_iseq_nex(NODE *node, VALUE name, VALUE path, VALUE absolute_path,
VALUE parent, enum iseq_type type)
{
rb_io_write(rb_stdout, rb_parser_dump_tree(node, 0));
printf("\n\n");
return rb_iseq_new(node, name, path, absolute_path, parent, type);
}
- 實現 vm.c 中實現 rb_iseq_evax
VALUE
rb_iseq_evax(VALUE iseqval)
{
rb_io_write(rb_stdout, rb_iseq_disasm(iseqval));
return 0;
}
結果:
###########################################################
## Do NOT use this node dump for any purpose other than ##
## debug and research. Compatibility is not guaranteed. ##
###########################################################
# @ NODE_SCOPE (line: 1)
# +- nd_tbl: (empty)
# +- nd_args:
# | (null node)
# +- nd_body:
# @ NODE_FCALL (line: 1)
# +- nd_mid: :puts
# +- nd_args:
# @ NODE_ARRAY (line: 1)
# +- nd_alen: 1
# +- nd_head:
# | @ NODE_STR (line: 1)
# | +- nd_lit: "Hello World!"
# +- nd_next:
# (null node)
== disasm: <RubyVM::InstructionSequence:<encoded>@./ruby-2.2.6/hello.rb>
0000 trace 1 ( 1)
0002 putself
0003 putstring "Hello World!"
0005 opt_send_without_block <callinfo!mid:puts, argc:1, FCALL|ARGS_SIMPLE>
0007 leave
上面就是 dump 出來的 AST 和 iseq,不過這些離我們的最終目標還有一點點距離。
0x03 生成代碼
由于之前的 parsetree 已經不能再使用了,google了一番之后,也沒有找到現成的, 之前的打算是寫一個類似 Python 的 uncompyle 之類的東西, 解析 iseq 結構、構建 AST、生成代碼, 不過后面發現自己實在沒那么多時間,于是就偷懶直接從 Ruby 的 AST 直接生成代碼。
對照著 Ruby 的 node.c、parse.y、compile.c 就可以寫出
試試看這個代碼反編譯的效果,測試文件 http.rb

感覺效果還是差強人意,代碼算是可以能看的,但是想要執行起來還要繼續僅需對代碼進行修改(因為node2ruby.c 還有挺多地方沒考慮到的)
總的來說,Ruby 寫的代碼還是比較友好的,像我這樣的新手都能很快上手,嗯,除了有些小錯誤外,順手提交了兩 PR
0x04 總結
寫 node2ruby.c 的時候就覺得如果不是特別熟悉 Ruby 的話,有些比較奇怪的語句還是想不到的。
對了,還記得我們上面所說的,如果沒有 License 文件,就很難將程序跑起來么? 嗯,我要解密的 Ruby 代碼就是必須要 License 文件的,而且我還沒有 License。
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/231/