jsobfu 0.1.0

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.
data/samples/basic.rb ADDED
@@ -0,0 +1,26 @@
1
+ # change this to if you copy it from the repo:
2
+ # require 'jsobfu'
3
+
4
+ require_relative '../lib/jsobfu'
5
+
6
+ source = %Q|
7
+ // some sample javascript code, to demonstrate usage:
8
+ this._send_websocket_request = function(address, callback) {
9
+ // create the websocket and remember when we started
10
+ try {
11
+ var socket = new WebSocket('ws://'+address);
12
+ } catch (sec_exception) {
13
+ if (callback) callback('error', sec_exception);
14
+ return;
15
+ }
16
+ var try_payload = function(){
17
+ TcpProbe.send("AAAAAAAAAAAAAAAAAAAAAAAAAA"+
18
+ "AAAAAAAAAAAAAAAAAAAAAAAAAA"+
19
+ "AAAAAAAAAAAAAAAAAAAAAAAAAA");
20
+ }
21
+ // wait a sec, then start the checks
22
+ setTimeout(this.check_socket, 200);
23
+ };
24
+ |
25
+
26
+ puts JSObfu.new(source).obfuscate
@@ -0,0 +1,35 @@
1
+ require 'spec_helper'
2
+ require 'execjs'
3
+
4
+ # add environment variable flag for long integration tests
5
+ unless ENV['INTEGRATION'] == 'false'
6
+
7
+ describe 'Integrations' do
8
+ Dir.glob(Pathname.new(__FILE__).dirname.join('integration/**.js').to_s).each do |path|
9
+ js = File.read(path)
10
+
11
+ if js =~ /\/\/@wip/
12
+ puts "Skipping @wip test #{File.basename path}\n"
13
+ next
14
+ end
15
+
16
+ num = 10
17
+
18
+ if js =~ /\/\/@times (\d+)/
19
+ num = $1.to_i
20
+ end
21
+
22
+ # ensure there is a global object to reference.
23
+ js = "window=this; #{js}"
24
+
25
+ num.times do
26
+ it "#{File.basename(path)} should evaluate to the same value before and after obfuscation" do
27
+ ob_js = JSObfu.new(js).obfuscate.to_s
28
+ expect(ob_js).to evaluate_to js
29
+ end
30
+ end
31
+
32
+ end
33
+ end
34
+
35
+ end
@@ -0,0 +1,68 @@
1
+ require 'spec_helper'
2
+
3
+ describe JSObfu::Hoister do
4
+ let(:code) { '' }
5
+ let(:ast) { RKelly::Parser.new.parse(code) }
6
+ subject(:hoister) { described_class.new }
7
+
8
+ describe "#scope" do
9
+
10
+ context 'when given Javascript code that declares var i' do
11
+ let(:code) { "var i = 3;" }
12
+
13
+ it 'has only the key "i" in its scope' do
14
+ hoister.accept(ast)
15
+ expect(hoister.scope.keys).to eq %w(i)
16
+ end
17
+ end
18
+
19
+ context 'when given Javascript code that declares var i, and an anonymous inner function that declares var j' do
20
+ let(:code) { "var i = 3; (function() { var j = 3; return j; })();" }
21
+
22
+ it 'has only the key "i" in its scope' do
23
+ hoister.accept(ast)
24
+ expect(hoister.scope.keys).to eq %w(i)
25
+ end
26
+ end
27
+
28
+ context 'when given Javascript code that declares var i, and an inner function named j' do
29
+ let(:code) { "var i = 3; function j() { return 0x55; }" }
30
+
31
+ it 'has the key "i" and "j" in its scope' do
32
+ hoister.accept(ast)
33
+ expect(hoister.scope.keys).to match_array %w(i j)
34
+ end
35
+
36
+ it 'has the key "j" in its #functions' do
37
+ hoister.accept(ast)
38
+ expect(hoister.functions).to match_array %w(j)
39
+ end
40
+ end
41
+
42
+ context 'when given Javascript code that refers to i, then later declares var i' do
43
+ let(:code) { "window.x = window.x || i; var i = 10;" }
44
+
45
+ it 'has the key "i" in its scope' do
46
+ hoister.accept(ast)
47
+ expect(hoister.scope.keys).to eq %w(i)
48
+ end
49
+ end
50
+ end
51
+
52
+ describe "#scope_declaration" do
53
+ context 'when scope has the keys "a", "b", and "c"' do
54
+ before { allow(hoister).to receive(:scope).and_return({a:1,b:2,c:3}) }
55
+
56
+ it 'returns the string "var a,b,c"' do
57
+ expect(hoister.scope_declaration(shuffle: false)).to eq "var a,b,c;"
58
+ end
59
+ end
60
+
61
+ context 'when the scope is empty' do
62
+ before { allow(hoister).to receive(:scope).and_return({}) }
63
+ it 'returns ""' do
64
+ expect(hoister.scope_declaration(shuffle: false)).to eq ""
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,201 @@
1
+ require 'spec_helper'
2
+
3
+ describe JSObfu::Scope do
4
+
5
+ subject(:scope) do
6
+ described_class.new
7
+ end
8
+
9
+ describe '#random_var_name' do
10
+ # the number of iterations while testing randomness
11
+ let(:n) { 20 }
12
+
13
+ subject(:random_var_name) { scope.random_var_name }
14
+
15
+ it { should be_a String }
16
+ it { should_not be_empty }
17
+
18
+ it 'is composed of _, $, alphanumeric chars' do
19
+ n.times { expect(scope.random_var_name).to match(/\A[a-zA-Z0-9$_]+\Z/) }
20
+ end
21
+
22
+ it 'does not start with a number' do
23
+ n.times { expect(scope.random_var_name).not_to match(/\A[0-9]/) }
24
+ end
25
+
26
+ context 'when a reserved word is generated' do
27
+ let(:reserved) { described_class::RESERVED_KEYWORDS.first }
28
+ let(:random) { 'abcdef' }
29
+ let(:generated) { [reserved, reserved, reserved, random] }
30
+
31
+ before do
32
+ allow(scope).to receive(:random_string) { generated.shift }
33
+ end
34
+
35
+ it { should eq random }
36
+ end
37
+
38
+ context 'when a non-unique random var is generated' do
39
+ let(:preexisting) { 'preexist' }
40
+ let(:random) { 'abcdef' }
41
+ let(:generated) { [preexisting, preexisting, preexisting, random] }
42
+
43
+ before do
44
+ allow(scope).to receive(:random_string) { generated.shift }
45
+ scope[preexisting] = 1
46
+ end
47
+
48
+ it { should eq random }
49
+ end
50
+ end
51
+
52
+ describe 'stack behavior' do
53
+ context 'add a var to the Scope, then call #pop!' do
54
+ let(:var_name) { 'a' }
55
+ it 'no longer contains that var' do
56
+ scope[var_name] = 1
57
+ scope.pop!
58
+ expect(scope).not_to have_key(var_name)
59
+ end
60
+ end
61
+
62
+ context 'add a var to the Scope, then call #push!' do
63
+ let(:var_name) { 'a' }
64
+ it 'still contains that var' do
65
+ scope[var_name] = 1
66
+ scope.push!
67
+ expect(scope).to have_key(var_name)
68
+ end
69
+ end
70
+
71
+ context 'add a var to the Scope, call #push!, then call #pop!' do
72
+ let(:var_name) { 'a' }
73
+ it 'still contains that var' do
74
+ scope[var_name] = 1
75
+ scope.push!
76
+ scope.pop!
77
+ expect(scope).to have_key(var_name)
78
+ end
79
+ end
80
+
81
+ context 'call #push!, add a var to the Scope, call #push!, then call #pop!' do
82
+ let(:var_name) { 'a' }
83
+ it 'still contains that var' do
84
+ scope.push!
85
+ scope[var_name] = 1
86
+ scope.push!
87
+ scope.pop!
88
+ expect(scope).to have_key(var_name)
89
+ end
90
+ end
91
+
92
+ context 'call #push!, add a var to the Scope, call #pop!, then call #push!' do
93
+ let(:var_name) { 'a' }
94
+ it 'no longer contains that var' do
95
+ scope.push!
96
+ scope[var_name] = 1
97
+ scope.pop!
98
+ scope.push!
99
+ expect(scope).not_to have_key(var_name)
100
+ end
101
+ end
102
+
103
+ context 'call #push!, add var1, call #push!, add var2, then call #pop!' do
104
+ let(:var1) { 'a' }
105
+ let(:var2) { 'b' }
106
+
107
+ before do
108
+ scope.push!
109
+ scope[var1] = 1
110
+ scope.push!
111
+ scope[var2] = 1
112
+ scope.pop!
113
+ end
114
+
115
+ it 'still contains var1' do
116
+ expect(scope).to have_key(var1)
117
+ end
118
+
119
+ it 'no longer contains var2' do
120
+ expect(scope).not_to have_key(var2)
121
+ end
122
+ end
123
+ end
124
+
125
+ describe '#rename_var' do
126
+ context 'when called more than once on the same var' do
127
+ let(:var) { 'a' }
128
+ let(:n) { 10 }
129
+ let(:first_rename) { scope.rename_var(var) }
130
+
131
+ it 'returns the same result' do
132
+ n.times { expect(scope.rename_var(var)).to eq first_rename }
133
+ end
134
+ end
135
+
136
+ context 'when called on different vars' do
137
+ let(:var1) { 'a' }
138
+ let(:var2) { 'b' }
139
+ let(:n) { 50 }
140
+
141
+ it 'returns a different result' do
142
+ n.times do
143
+ scope = described_class.new
144
+ expect(scope.rename_var(var1)).not_to eq scope.rename_var(var2)
145
+ end
146
+ end
147
+ end
148
+
149
+ context '#push!; #rename_var(a); #pop!; #rename_var(a)' do
150
+ let(:var) { 'a' }
151
+ let(:n) { 50 }
152
+
153
+ it 're-maps the vars to (usually) different random strings' do
154
+ scope.push!
155
+ first_var = scope.rename_var(var)
156
+ scope.pop!
157
+ n.times do
158
+ new_var = scope.rename_var(var)
159
+ if new_var == first_var # this is allowed to happen occasionally since shadowing is OK.
160
+ next
161
+ else
162
+ expect(new_var).not_to eq first_var
163
+ end
164
+ end
165
+ end
166
+ end
167
+
168
+ context '#push!; #push!; #rename_var(a); #pop!; #rename_var(a)' do
169
+ let(:var) { 'a' }
170
+ let(:n) { 50 }
171
+
172
+ it 're-maps the vars to (usually) different random strings' do
173
+ scope.push!
174
+ scope.push!
175
+ first_var = scope.rename_var(var)
176
+ scope.pop!
177
+ n.times do
178
+ new_var = scope.rename_var(var)
179
+ if new_var == first_var # this is allowed to happen occasionally since shadowing is OK.
180
+ next
181
+ else
182
+ expect(new_var).not_to eq first_var
183
+ end
184
+ end
185
+ end
186
+ end
187
+
188
+ context '#rename_var(a); push!; #push!; #rename_var(a);' do
189
+ let(:var) { 'a' }
190
+ let(:n) { 50 }
191
+
192
+ it 're-maps the vars to the same random string' do
193
+ first_var = scope.rename_var(var)
194
+ scope.push!
195
+ scope.push!
196
+ expect(scope.rename_var(var)).to eq first_var
197
+ end
198
+ end
199
+ end
200
+
201
+ end
@@ -0,0 +1,156 @@
1
+ require 'spec_helper'
2
+
3
+ describe JSObfu::Utils do
4
+ # the number of iterations while testing randomness
5
+ let(:n) { 50 }
6
+
7
+ describe '#rand_text_alphanumeric' do
8
+ let(:len) { 15 }
9
+
10
+ # generates a new random string on every call
11
+ def output; JSObfu::Utils.rand_text_alphanumeric(len); end
12
+
13
+ it 'returns strings of length 15' do
14
+ expect(n.times.map { output }.join.length).to be(n*len)
15
+ end
16
+
17
+ it 'returns strings in alpha charset' do
18
+ expect(n.times.map { output }.join).to be_in_charset(described_class::ALPHANUMERIC_CHARSET)
19
+ end
20
+ end
21
+
22
+ describe '#rand_text_alpha' do
23
+ let(:len) { 15 }
24
+
25
+ # generates a new random string on every call
26
+ def output; JSObfu::Utils.rand_text_alpha(len); end
27
+
28
+ it 'returns strings of length 15' do
29
+ expect(n.times.map { output }.join.length).to be(n*len)
30
+ end
31
+
32
+ it 'returns strings in alpha charset' do
33
+ expect(n.times.map { output }.join).to be_in_charset(described_class::ALPHA_CHARSET)
34
+ end
35
+ end
36
+
37
+ describe '#rand_text' do
38
+ let(:len) { 5 }
39
+ let(:charset) { described_class::ALPHA_CHARSET }
40
+
41
+ # generates a new random string on every call
42
+ def output; described_class.rand_text(charset, len); end
43
+
44
+ it 'returns strings of length 15' do
45
+ expect(n.times.map { output }.join.length).to be(n*len)
46
+ end
47
+
48
+ it 'returns strings in the specified charset' do
49
+ expect(n.times.map { output }.join).to be_in_charset(charset)
50
+ end
51
+ end
52
+
53
+ describe '#to_hex' do
54
+ let(:str) { '' }
55
+ let(:delimiter) { "\\x" }
56
+ subject(:hex_encoding) { described_class.to_hex(str, delimiter) }
57
+
58
+ context 'when given the string "ABC"' do
59
+ let(:str) { 'ABC' }
60
+ it { should eq "\\x41\\x42\\x43" }
61
+
62
+ context 'when the delimiter is "\\u00"' do
63
+ let(:delimiter) { "\\u00" }
64
+ it { should eq "\\u0041\\u0042\\u0043" }
65
+ end
66
+ end
67
+
68
+ context 'when given an empty string' do
69
+ let(:str) { '' }
70
+ it { should eq '' }
71
+ end
72
+ end
73
+
74
+ describe '#random_var_encoding' do
75
+ let(:var_name) { 'ABCD' }
76
+ let(:initial_value) { 123 }
77
+ let(:preamble) { "var #{var_name} = #{initial_value}"}
78
+
79
+ def encoded_var; described_class.random_var_encoding(var_name); end
80
+
81
+ context 'when called multiple times on the same var' do
82
+ it 'should evaluate to the same initial value' do
83
+ 10.times do
84
+ js = "(function(){ #{preamble}; return #{encoded_var}; })()"
85
+ expect(ExecJS.eval(js)).to eq initial_value
86
+ end
87
+ end
88
+ end
89
+ end
90
+
91
+ describe '#safe_split' do
92
+ let(:js_string) { "\\x66\\x67"*600 }
93
+ let(:quote) { '"' }
94
+ let(:opts) { { :quote => quote } }
95
+ let(:parts) { 50.times.map { described_class.safe_split(js_string.dup, opts) } }
96
+
97
+ describe 'quoting' do
98
+ context 'when given a double-quote' do
99
+ let(:quote) { '"' }
100
+ it 'surrounds all the split strings with the same quote' do
101
+ expect(parts.flatten.all? { |part| part.start_with?(quote) }).to be true
102
+ end
103
+ end
104
+
105
+ context 'when given a single-quote' do
106
+ let(:quote) { "'" }
107
+ it 'surrounds all the split strings with the same quote' do
108
+ expect(parts.flatten.all? { |part| part.start_with?(quote) }).to be true
109
+ end
110
+ end
111
+ end
112
+
113
+ describe 'splitting' do
114
+ context 'when given a hex-escaped series of bytes' do
115
+ let(:js_string) { "\\x66\\x67"*600 }
116
+
117
+ it 'never splits in the middle of a hex escape' do
118
+ expect(parts.flatten.all? { |part| part.start_with?('"\\') }).to be true
119
+ end
120
+ end
121
+
122
+ context 'when given a unicode-escaped series of bytes' do
123
+ let(:js_string) { "\\u0066\\u0067"*600 }
124
+
125
+ it 'never splits in the middle of a unicode escape' do
126
+ expect(parts.flatten.all? { |part| part.start_with?('"\\') }).to be true
127
+ end
128
+ end
129
+ end
130
+ end
131
+
132
+
133
+ # surround the string in quotes
134
+ def quote(str, q='"'); "#{q}#{str}#{q}" end
135
+
136
+ describe '#transform_string' do
137
+ context 'when given a string of length > MAX_STRING_CHUNK' do
138
+ let(:js_string) { quote "ABC"*described_class::MAX_STRING_CHUNK }
139
+
140
+ it 'calls itself recursively' do
141
+ expect(described_class).to receive(:transform_string).at_least(2).times.and_call_original
142
+ described_class.transform_string js_string, JSObfu::Scope.new
143
+ end
144
+ end
145
+
146
+ context 'when given a string of length < MAX_STRING_CHUNK' do
147
+ let(:js_string) { quote "A"*(described_class::MAX_STRING_CHUNK/2).to_i }
148
+
149
+ it 'does not call itself recursively' do
150
+ expect(described_class).to receive(:transform_string).once.and_call_original
151
+ described_class.transform_string js_string, JSObfu::Scope.new
152
+ end
153
+ end
154
+ end
155
+
156
+ end