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.
- checksums.yaml +15 -0
- data/lib/jsobfu.rb +73 -0
- data/lib/jsobfu/ecma_tight.rb +316 -0
- data/lib/jsobfu/hoister.rb +84 -0
- data/lib/jsobfu/obfuscator.rb +144 -0
- data/lib/jsobfu/scope.rb +148 -0
- data/lib/jsobfu/utils.rb +366 -0
- data/samples/basic.rb +26 -0
- data/spec/integration_spec.rb +35 -0
- data/spec/jsobfu/hoister_spec.rb +68 -0
- data/spec/jsobfu/scope_spec.rb +201 -0
- data/spec/jsobfu/utils_spec.rb +156 -0
- data/spec/jsobfu_spec.rb +27 -0
- data/spec/spec_helper.rb +68 -0
- data/spec/support/matchers/be_in_charset.rb +5 -0
- data/spec/support/matchers/evaluate_to.rb +41 -0
- metadata +130 -0
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
|