node-marshal 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.rdoc +11 -2
- data/bin/noderbc +43 -5
- data/ext/node-marshal/node193.h +10 -1
- data/ext/node-marshal/node220.h +10 -1
- data/ext/node-marshal/node230.h +361 -0
- data/ext/node-marshal/nodedump.c +161 -28
- data/ext/node-marshal/nodedump.h +3 -12
- data/ext/node-marshal/nodeinfo.c +125 -2
- data/lib/node-marshal.rb +157 -2
- data/test/test_base.rb +39 -1
- data/test/test_lifegame.rb +2 -2
- data/test/test_obfuscator.rb +36 -0
- data/test/test_qcall.rb +46 -0
- metadata +6 -3
data/lib/node-marshal.rb
CHANGED
@@ -1,5 +1,26 @@
|
|
1
1
|
require_relative '../ext/node-marshal/nodemarshal.so'
|
2
|
-
|
2
|
+
begin
|
3
|
+
require 'zlib'
|
4
|
+
rescue LoadError
|
5
|
+
# If zlib library is absent in the system no support for
|
6
|
+
# compression will be provided in
|
7
|
+
end
|
8
|
+
|
9
|
+
# Implementation of Array::to_h method for Ruby 1.9 (and probably 2.0)
|
10
|
+
# Don't use for Ruby 2.2.x and Ruby 2.3.x
|
11
|
+
if !defined?([].to_h)
|
12
|
+
class Array
|
13
|
+
def to_h
|
14
|
+
h = {}
|
15
|
+
a = self
|
16
|
+
a.each do |x|
|
17
|
+
raise "wrong array length" if x.length != 2
|
18
|
+
h[x[0]] = x[1];
|
19
|
+
end
|
20
|
+
return h
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
3
24
|
|
4
25
|
class NodeMarshal
|
5
26
|
# call-seq:
|
@@ -12,7 +33,7 @@ class NodeMarshal
|
|
12
33
|
# with the command for nodemarshal.so inclusion (default is
|
13
34
|
# require_relative '../ext/node-marshal/nodemarshal.so')
|
14
35
|
def to_compiled_rb(outfile, *args)
|
15
|
-
compress =
|
36
|
+
compress = true
|
16
37
|
so_path = "require_relative '../ext/node-marshal/nodemarshal.so'"
|
17
38
|
if args.length > 0
|
18
39
|
opts = args[0]
|
@@ -25,6 +46,9 @@ class NodeMarshal
|
|
25
46
|
end
|
26
47
|
# Compression
|
27
48
|
if compress
|
49
|
+
if !defined?(Zlib)
|
50
|
+
raise "Compression is not supported: Zlib is absent"
|
51
|
+
end
|
28
52
|
zlib_include = "require 'zlib'"
|
29
53
|
data_txt = NodeMarshal.base85r_encode(Zlib::deflate(self.to_bin))
|
30
54
|
data_bin = "Zlib::inflate(NodeMarshal.base85r_decode(data_txt))"
|
@@ -61,4 +85,135 @@ EOS
|
|
61
85
|
node.to_compiled_rb(outfile, *args)
|
62
86
|
return true
|
63
87
|
end
|
88
|
+
|
89
|
+
# call-seq:
|
90
|
+
# obj.replace_symbols(syms_subs)
|
91
|
+
#
|
92
|
+
# Replaces some symbols inside parsed AST to user-defined aliases.
|
93
|
+
# It is designed to make code obfuscation easier. Be careful when using
|
94
|
+
# this ability: it is possible to break external libraries calls,
|
95
|
+
# operators overloading and some metaprogramming techniques.
|
96
|
+
# - +syms_subs+ -- Hash with the table of aliases. Keys are original names,
|
97
|
+
# values are aliases. Keys and values MUST BE strings (not symbols!).
|
98
|
+
def replace_symbols(syms_subs)
|
99
|
+
# Check input data
|
100
|
+
# a) type
|
101
|
+
if !(syms_subs.is_a?(Hash))
|
102
|
+
raise "symb_subs must be a hash"
|
103
|
+
end
|
104
|
+
# b) uniqueness of values inside the hash
|
105
|
+
values = syms_subs.values
|
106
|
+
if values.size != values.uniq.size
|
107
|
+
raise ArgumentError, "values (new names) must be unique"
|
108
|
+
end
|
109
|
+
# c) uniqueness of values after replacement
|
110
|
+
# TODO: MAKE IT!!!
|
111
|
+
# Use NodeMarshal C part to replace the symbols
|
112
|
+
self.to_hash # To initialize Hash with preparsed Ruby AST NODE
|
113
|
+
syms_subs.each do |key, value|
|
114
|
+
change_symbol(key, value)
|
115
|
+
end
|
116
|
+
self
|
117
|
+
end
|
118
|
+
|
119
|
+
# call-seq:
|
120
|
+
# obj.get_safe_symbols
|
121
|
+
#
|
122
|
+
# Returns an array that contains strings with the names of symbols that are safe
|
123
|
+
# to change. It excludes symbols that are present in the table of literals (and their derivatives
|
124
|
+
# such as @x and x=). Such operation is useful for attr_readed, attr_writer and another similar
|
125
|
+
# metaprogramming techniques handling
|
126
|
+
#
|
127
|
+
# - +our_symbols+ symbols created during node creation (must be found manually by the user
|
128
|
+
# by means of Symbol.all_symbols calling BEFORE and AFTER node creation.
|
129
|
+
def get_safe_symbols(our_symbols)
|
130
|
+
self.to_hash # To initialize Hash with preparsed Ruby AST NODE
|
131
|
+
symbolic_literals = self.literals.select {|x| x.is_a?(Symbol)}.map {|x| x.to_s}
|
132
|
+
fixed_symbols = [] + symbolic_literals
|
133
|
+
fixed_symbols += symbolic_literals.map {|x| "@#{x}"}
|
134
|
+
fixed_symbols += symbolic_literals.map {|x| "#{x}="}
|
135
|
+
our_symbols = our_symbols.dup
|
136
|
+
our_symbols -= fixed_symbols
|
137
|
+
end
|
138
|
+
|
139
|
+
# call-seq:
|
140
|
+
# obj.get_aliases_table(our_symbols)
|
141
|
+
#
|
142
|
+
# Returns a hash that has {"old_sym_name"=>"new_sym_name",...} format.
|
143
|
+
# "new_sym_name" are generated automatically.
|
144
|
+
#
|
145
|
+
# - +our_symbols+ -- An array that contains the list of symbols (AS STRINGS,
|
146
|
+
# NOT AS SYMBOLS) that can be renamed.
|
147
|
+
def get_aliases_table(our_symbols)
|
148
|
+
symbols_ary = get_safe_symbols(our_symbols)
|
149
|
+
pos = 0;
|
150
|
+
aliases_ary = symbols_ary.map do |sym|
|
151
|
+
pos += 1
|
152
|
+
if sym.length > 1 && sym[0..1] == '@@'
|
153
|
+
"@@q#{pos}"
|
154
|
+
elsif sym[0] == '@'
|
155
|
+
"@q#{pos}"
|
156
|
+
elsif sym[0] =~ /[A-Z]/
|
157
|
+
"Q#{pos}"
|
158
|
+
elsif sym[0] =~ /[a-z]/
|
159
|
+
"q#{pos}"
|
160
|
+
end
|
161
|
+
end
|
162
|
+
[symbols_ary, aliases_ary].transpose.to_h
|
163
|
+
end
|
164
|
+
|
165
|
+
# call-seq:
|
166
|
+
# obj.rename_ivars
|
167
|
+
def rename_ivars(*args)
|
168
|
+
if args.size == 0
|
169
|
+
excl_names = []
|
170
|
+
else
|
171
|
+
excl_names = args[0]
|
172
|
+
end
|
173
|
+
|
174
|
+
to_hash
|
175
|
+
syms = @nodehash[:symbols].select {|x| (x =~ /@[^@]/) == 0}
|
176
|
+
pos = 1;
|
177
|
+
syms_new = syms.map do |x|
|
178
|
+
if excl_names.find_index(x[1..-1]) != nil
|
179
|
+
str = x
|
180
|
+
else
|
181
|
+
str = "@ivar#{pos}"
|
182
|
+
end
|
183
|
+
pos = pos + 1;
|
184
|
+
str
|
185
|
+
end
|
186
|
+
syms_subs = [syms, syms_new].transpose.to_h
|
187
|
+
replace_symbols(syms_subs)
|
188
|
+
self
|
189
|
+
end
|
190
|
+
|
191
|
+
# call-seq:
|
192
|
+
# obj.rebuld
|
193
|
+
#
|
194
|
+
# Rebuilds the node by converting it to the binary dump and further restoring
|
195
|
+
# of it from this dump. It doesn't change the original node and returns rebuilt
|
196
|
+
# node.
|
197
|
+
def rebuild
|
198
|
+
NodeMarshal.new(:binmemory, to_bin)
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
# Designed for the logging of symbols table changes. When an example of
|
203
|
+
# SymbolsLogger is created the current global table of symbols is saved
|
204
|
+
# inside it. The created example can be used for finding new symbols in
|
205
|
+
# the global table. This is useful for code obfuscation.
|
206
|
+
class SymbolsLogger
|
207
|
+
def initialize
|
208
|
+
@symtbl_old = Symbol.all_symbols
|
209
|
+
end
|
210
|
+
|
211
|
+
def new_symbols
|
212
|
+
symtbl_new = Symbol.all_symbols
|
213
|
+
symtbl_new - @symtbl_old
|
214
|
+
end
|
215
|
+
|
216
|
+
def update
|
217
|
+
@symtbl_old = Symbol.all_symbols
|
218
|
+
end
|
64
219
|
end
|
data/test/test_base.rb
CHANGED
@@ -125,7 +125,6 @@ a, b = XYPoint.new(0.0, 0.0), XYPoint.new(3.0, 4.0)
|
|
125
125
|
c, d = PolarPoint.new(1.0, 0.0), PolarPoint.new(2.0, 90.0)
|
126
126
|
[a.distance(b), c.distance(d)]
|
127
127
|
EOS
|
128
|
-
|
129
128
|
assert_node_compiler(program)
|
130
129
|
end
|
131
130
|
|
@@ -148,6 +147,45 @@ EOS
|
|
148
147
|
end
|
149
148
|
end
|
150
149
|
|
150
|
+
# Test regular expressions (NODE_MATCH3 node issue)
|
151
|
+
def test_node_match3
|
152
|
+
program = <<-EOS
|
153
|
+
a = " d--abc"
|
154
|
+
a =~ Regexp.new("abc")
|
155
|
+
EOS
|
156
|
+
assert_node_compiler(program)
|
157
|
+
end
|
158
|
+
|
159
|
+
def test_node_block_pass
|
160
|
+
program = <<-EOS
|
161
|
+
[1, 2, 3, "A"].map(&:class)
|
162
|
+
EOS
|
163
|
+
assert_node_compiler(program)
|
164
|
+
end
|
165
|
+
|
166
|
+
# Check the reaction on the parsing errors during the node creation
|
167
|
+
# In the case of syntax error ArgumentError exception should be generated
|
168
|
+
def test_syntax_error
|
169
|
+
program = "a = 1; a++" # Contains syntax error
|
170
|
+
# a) from memory (string)
|
171
|
+
test_passed = false
|
172
|
+
begin
|
173
|
+
node = NodeMarshal.new(:srcmemory, program)
|
174
|
+
rescue ArgumentError
|
175
|
+
test_passed = true
|
176
|
+
end
|
177
|
+
assert_equal(test_passed, true);
|
178
|
+
# b) from file
|
179
|
+
File.open("_tmp.rb", "w") {|fp| fp << program }
|
180
|
+
test_passed = false
|
181
|
+
begin
|
182
|
+
node = NodeMarshal.new(:srcfile, "_tmp.rb")
|
183
|
+
rescue ArgumentError
|
184
|
+
test_passed = true
|
185
|
+
end
|
186
|
+
assert_equal(test_passed, true);
|
187
|
+
end
|
188
|
+
|
151
189
|
# Part of the tests: compare result of direct usage of eval
|
152
190
|
# for the source code and its
|
153
191
|
def assert_node_compiler(program)
|
data/test/test_lifegame.rb
CHANGED
@@ -58,11 +58,11 @@ EOS
|
|
58
58
|
File.open('lifegame_bin.rb', 'w') {|fp| fp << txt }
|
59
59
|
`ruby lifegame_bin.rb`
|
60
60
|
res_file = File.read('life.res')
|
61
|
-
`rm life.res`
|
61
|
+
`rm life.res`
|
62
62
|
# Run the life game without compilation to the file
|
63
63
|
load('lifegame.rb')
|
64
64
|
res_load = run_game
|
65
65
|
Object.send(:remove_const, :LifeGame)
|
66
|
-
assert_equal(res_file, res_load)
|
66
|
+
assert_equal(res_file, res_load)
|
67
67
|
end
|
68
68
|
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require_relative '../lib/node-marshal.rb'
|
2
|
+
require 'test/unit'
|
3
|
+
|
4
|
+
# Test node-marshal obfuscator abilities
|
5
|
+
class TestObfuscator < Test::Unit::TestCase
|
6
|
+
def test_sym_replace
|
7
|
+
# Create the node and find all new symbols
|
8
|
+
symlog = SymbolsLogger.new
|
9
|
+
node = NodeMarshal.new(:srcfile, 'lifegame.rb')
|
10
|
+
our_symbols = symlog.new_symbols.map(&:to_s)
|
11
|
+
node.to_hash
|
12
|
+
# Try to exclude symbols used in attr_reader and attr_writer constructions
|
13
|
+
our_symbols = node.get_safe_symbols(our_symbols)
|
14
|
+
# Prepare hash table for replacement
|
15
|
+
reptbl = node.get_aliases_table(our_symbols)
|
16
|
+
life_game_name = reptbl["LifeGame"]
|
17
|
+
grid_name = reptbl["Grid"]
|
18
|
+
make_step_name = reptbl["make_step!"].to_sym
|
19
|
+
cfg_glider_gun_name = reptbl["cfg_glider_gun!"].to_sym
|
20
|
+
# Replace symbols
|
21
|
+
puts "----- Symbols replacement table"
|
22
|
+
puts reptbl.to_s
|
23
|
+
puts "-------------------------------"
|
24
|
+
node.replace_symbols(reptbl)
|
25
|
+
# Rebuild node, save it to the file and reload it
|
26
|
+
node = node.rebuild
|
27
|
+
File.open('life.bin', 'wb') {|fp| fp << node.to_bin};
|
28
|
+
node.compile.eval
|
29
|
+
# Execute the node
|
30
|
+
grid_class = Object.const_get(life_game_name).const_get(grid_name)
|
31
|
+
g = grid_class.new(25, 80)
|
32
|
+
g.send(cfg_glider_gun_name)
|
33
|
+
75.times {g.send(make_step_name)}
|
34
|
+
puts g.to_ascii
|
35
|
+
end
|
36
|
+
end
|
data/test/test_qcall.rb
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
require_relative '../lib/node-marshal.rb'
|
3
|
+
require 'test/unit'
|
4
|
+
|
5
|
+
|
6
|
+
# This unit tests Ruby 2.3 &. safe navigation operator
|
7
|
+
class TestQCALL < Test::Unit::TestCase
|
8
|
+
def test_qcall
|
9
|
+
qcall_program = %q{
|
10
|
+
class Account
|
11
|
+
def initialize(owner_name, owner_address)
|
12
|
+
@owner_name = owner_name
|
13
|
+
@owner_info = (owner_address.nil?) ? nil : OwnerInfo.new(owner_address);
|
14
|
+
end
|
15
|
+
def owner_name
|
16
|
+
@owner_name
|
17
|
+
end
|
18
|
+
def owner_info
|
19
|
+
@owner_info
|
20
|
+
end
|
21
|
+
end
|
22
|
+
class OwnerInfo
|
23
|
+
def initialize(address)
|
24
|
+
@address = address
|
25
|
+
end
|
26
|
+
def address
|
27
|
+
@address
|
28
|
+
end
|
29
|
+
end
|
30
|
+
a = Account.new("Owner", "Moscow");
|
31
|
+
puts "'#{a&.owner_name}'"
|
32
|
+
puts "'#{a&.owner_info&.address}'"
|
33
|
+
b = Account.new("Owner", nil);
|
34
|
+
puts "'#{b&.owner_name}'"
|
35
|
+
puts "'#{b&.owner_info&.address}'"
|
36
|
+
[a&.owner_name, a&.owner_info&.address, b&.owner_name, b&.owner_info&.address]
|
37
|
+
}
|
38
|
+
|
39
|
+
node = NodeMarshal.new(:srcmemory, qcall_program)
|
40
|
+
bindump = node.to_bin
|
41
|
+
node = NodeMarshal.new(:binmemory, bindump)
|
42
|
+
res_node = node.compile.eval
|
43
|
+
res_text = eval(qcall_program)
|
44
|
+
assert_equal(res_text, res_node)
|
45
|
+
end
|
46
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: node-marshal
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Alexey Voskov
|
@@ -9,7 +9,7 @@ autorequire:
|
|
9
9
|
bindir:
|
10
10
|
- bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2015-
|
12
|
+
date: 2015-12-24 00:00:00.000000000 Z
|
13
13
|
dependencies: []
|
14
14
|
description: "This gem is designed for transformation of Ruby source code (eiher in
|
15
15
|
the form of files or strings) to the \nRuby nodes (syntax trees) used by Ruby MRI
|
@@ -34,6 +34,7 @@ files:
|
|
34
34
|
- ext/node-marshal/libobj/readme.txt
|
35
35
|
- ext/node-marshal/node193.h
|
36
36
|
- ext/node-marshal/node220.h
|
37
|
+
- ext/node-marshal/node230.h
|
37
38
|
- ext/node-marshal/nodedump.c
|
38
39
|
- ext/node-marshal/nodedump.h
|
39
40
|
- ext/node-marshal/nodeinfo.c
|
@@ -42,6 +43,8 @@ files:
|
|
42
43
|
- test/test_base.rb
|
43
44
|
- test/test_complex.rb
|
44
45
|
- test/test_lifegame.rb
|
46
|
+
- test/test_obfuscator.rb
|
47
|
+
- test/test_qcall.rb
|
45
48
|
- test/tinytet.rb
|
46
49
|
homepage:
|
47
50
|
licenses:
|
@@ -65,7 +68,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
65
68
|
version: '0'
|
66
69
|
requirements: []
|
67
70
|
rubyforge_project:
|
68
|
-
rubygems_version: 2.4.5
|
71
|
+
rubygems_version: 2.4.5.1
|
69
72
|
signing_key:
|
70
73
|
specification_version: 4
|
71
74
|
summary: Transforms Ruby sources to binary nodes (trees) that can be saved and loaded
|