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
@@ -0,0 +1,144 @@
|
|
1
|
+
require_relative 'ecma_tight'
|
2
|
+
|
3
|
+
class JSObfu::Obfuscator < JSObfu::ECMANoWhitespaceVisitor
|
4
|
+
|
5
|
+
# @return [JSObfu::Scope] the scope maintained while walking the ast
|
6
|
+
attr_reader :scope
|
7
|
+
|
8
|
+
# Note: At a high level #renames is not that useful, because var shadowing can
|
9
|
+
# cause multiple variables in different contexts to be mapped separately.
|
10
|
+
# - joev
|
11
|
+
|
12
|
+
# @return [Hash] of original var/fn names to our new random neames
|
13
|
+
attr_reader :renames
|
14
|
+
|
15
|
+
# @param opts [Hash] the options hash
|
16
|
+
# @option opts [JSObfu::Scope] :scope the optional scope to save vars to
|
17
|
+
def initialize(opts={})
|
18
|
+
@scope = opts.fetch(:scope, JSObfu::Scope.new)
|
19
|
+
@renames = {}
|
20
|
+
super()
|
21
|
+
end
|
22
|
+
|
23
|
+
# Maintains a stack of closures that we have visited. This method is called
|
24
|
+
# everytime we visit a nested function.
|
25
|
+
#
|
26
|
+
# Javascript is functionally-scoped, so a function(){} creates its own
|
27
|
+
# unique closure. When resolving variables, Javascript looks "up" the
|
28
|
+
# closure stack, ending up as a property lookup in the global scope
|
29
|
+
# (available as `window` in all browsers)
|
30
|
+
#
|
31
|
+
# This is changed in newer ES versions, where a `let` keyword has been
|
32
|
+
# introduced, which has regular C-style block scoping. We'll ignore this
|
33
|
+
# feature since it is not yet widely used.
|
34
|
+
def visit_SourceElementsNode(o)
|
35
|
+
scope.push!
|
36
|
+
|
37
|
+
hoister = JSObfu::Hoister.new(parent_scope: scope)
|
38
|
+
o.value.each { |x| hoister.accept(x) }
|
39
|
+
|
40
|
+
hoister.scope.keys.each do |key|
|
41
|
+
rename_var(key)
|
42
|
+
end
|
43
|
+
|
44
|
+
ret = super
|
45
|
+
ret = hoister.scope_declaration + ret
|
46
|
+
|
47
|
+
scope.pop!
|
48
|
+
|
49
|
+
ret
|
50
|
+
end
|
51
|
+
|
52
|
+
def visit_FunctionDeclNode(o)
|
53
|
+
o.value = if o.value and o.value.length > 0
|
54
|
+
JSObfu::Utils::random_var_encoding(scope.rename_var(o.value))
|
55
|
+
else
|
56
|
+
if rand(3) != 0
|
57
|
+
JSObfu::Utils::random_var_encoding(scope.random_var_name)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
super
|
62
|
+
end
|
63
|
+
|
64
|
+
def visit_FunctionExprNode(o)
|
65
|
+
if o.value != 'function'
|
66
|
+
o.value = JSObfu::Utils::random_var_encoding(rename_var(o.value))
|
67
|
+
end
|
68
|
+
|
69
|
+
super
|
70
|
+
end
|
71
|
+
|
72
|
+
# Called whenever a variable is declared.
|
73
|
+
def visit_VarDeclNode(o)
|
74
|
+
o.name = JSObfu::Utils::random_var_encoding(rename_var(o.name))
|
75
|
+
|
76
|
+
super
|
77
|
+
end
|
78
|
+
|
79
|
+
# Called whenever a variable is referred to (not declared).
|
80
|
+
#
|
81
|
+
# If the variable was never added to scope, it is assumed to be a global
|
82
|
+
# object (like "document"), and hence will not be obfuscated.
|
83
|
+
#
|
84
|
+
def visit_ResolveNode(o)
|
85
|
+
new_val = rename_var(o.value, :generate => false)
|
86
|
+
|
87
|
+
if new_val
|
88
|
+
o.value = JSObfu::Utils::random_var_encoding(new_val)
|
89
|
+
super
|
90
|
+
else
|
91
|
+
# A global is used, at least obfuscate the lookup
|
92
|
+
"window[#{JSObfu::Utils::transform_string(o.value, scope, :quotes => false)}]"
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# Called on a dot lookup, like X.Y
|
97
|
+
def visit_DotAccessorNode(o)
|
98
|
+
obf_str = JSObfu::Utils::transform_string(o.accessor, scope, :quotes => false)
|
99
|
+
"#{o.value.accept(self)}[(#{obf_str})]"
|
100
|
+
end
|
101
|
+
|
102
|
+
# Called when a parameter is declared. "Shadowed" parameters in the original
|
103
|
+
# source are preserved - the randomized name is "shadowed" from the outer scope.
|
104
|
+
def visit_ParameterNode(o)
|
105
|
+
o.value = JSObfu::Utils::random_var_encoding(rename_var(o.value))
|
106
|
+
|
107
|
+
super
|
108
|
+
end
|
109
|
+
|
110
|
+
# A property node in an object "{}"
|
111
|
+
def visit_PropertyNode(o)
|
112
|
+
# if it is a non-alphanumeric property, obfuscate the string's bytes
|
113
|
+
if o.name =~ /^[a-zA-Z_][a-zA-Z0-9_]*$/
|
114
|
+
o.instance_variable_set :@name, '"'+JSObfu::Utils::random_string_encoding(o.name)+'"'
|
115
|
+
end
|
116
|
+
|
117
|
+
super
|
118
|
+
end
|
119
|
+
|
120
|
+
def visit_NumberNode(o)
|
121
|
+
o.value = JSObfu::Utils::transform_number(o.value)
|
122
|
+
super
|
123
|
+
end
|
124
|
+
|
125
|
+
def visit_StringNode(o)
|
126
|
+
o.value = JSObfu::Utils::transform_string(o.value, scope)
|
127
|
+
super
|
128
|
+
end
|
129
|
+
|
130
|
+
def visit_TryNode(o)
|
131
|
+
if o.catch_block
|
132
|
+
o.instance_variable_set :@catch_var, rename_var(o.catch_var)
|
133
|
+
end
|
134
|
+
super
|
135
|
+
end
|
136
|
+
|
137
|
+
protected
|
138
|
+
|
139
|
+
# Assigns the var +var_name+ a new obfuscated name
|
140
|
+
def rename_var(var_name, opts={})
|
141
|
+
@renames[var_name] = scope.rename_var(var_name, opts)
|
142
|
+
end
|
143
|
+
|
144
|
+
end
|
data/lib/jsobfu/scope.rb
ADDED
@@ -0,0 +1,148 @@
|
|
1
|
+
require_relative 'utils'
|
2
|
+
require 'set'
|
3
|
+
|
4
|
+
# A single Javascript scope, used as a key-value store
|
5
|
+
# to maintain uniqueness of members in generated closures.
|
6
|
+
# For speed this class is implemented as a subclass of Hash.
|
7
|
+
class JSObfu::Scope < Hash
|
8
|
+
|
9
|
+
# these keywords should never be used as a random var name
|
10
|
+
# source: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Reserved_Words
|
11
|
+
RESERVED_KEYWORDS = %w(
|
12
|
+
break case catch continue debugger default delete do else finally
|
13
|
+
for function if in instanceof new return switch this throw try
|
14
|
+
typeof var void while with class enum export extends import super
|
15
|
+
implements interface let package private protected public static yield
|
16
|
+
const let
|
17
|
+
)
|
18
|
+
|
19
|
+
# these vars should not be shadowed as they in the exploit code,
|
20
|
+
# and generating them would cause problems.
|
21
|
+
BUILTIN_VARS = %w(
|
22
|
+
String window unescape location chrome document navigator location
|
23
|
+
frames ActiveXObject XMLHttpRequest Function eval Object Math CSS
|
24
|
+
parent opener event frameElement Error TypeError setTimeout setInterval
|
25
|
+
top arguments Array Date
|
26
|
+
)
|
27
|
+
|
28
|
+
# @return [JSObfu::Scope] parent that spawned this scope
|
29
|
+
attr_accessor :parent
|
30
|
+
|
31
|
+
# @return [Hash] mapping old var names to random ones
|
32
|
+
attr_accessor :renames
|
33
|
+
|
34
|
+
# @param [Hash] opts the options hash
|
35
|
+
# @option opts [Rex::Exploitation::JSObfu::Scope] :parent an optional parent scope,
|
36
|
+
# sometimes necessary to prevent needless var shadowing
|
37
|
+
# @option opts [Integer] :min_len minimum length of the var names
|
38
|
+
def initialize(opts={})
|
39
|
+
@parent = opts[:parent]
|
40
|
+
@first_char_set = opts[:first_char_set] || [*'A'..'Z']+[*'a'..'z']+['_', '$']
|
41
|
+
@char_set = opts[:first_char_set] || @first_char_set + [*'0'..'9']
|
42
|
+
@min_len = opts[:min_len] || 1
|
43
|
+
@renames = {}
|
44
|
+
end
|
45
|
+
|
46
|
+
# Generates a unique, "safe" random variable
|
47
|
+
# @return [String] a unique random var name that is not a reserved keyword
|
48
|
+
def random_var_name
|
49
|
+
len = @min_len
|
50
|
+
loop do
|
51
|
+
text = random_string(len)
|
52
|
+
unless has_key?(text) or
|
53
|
+
RESERVED_KEYWORDS.include?(text) or
|
54
|
+
BUILTIN_VARS.include?(text)
|
55
|
+
|
56
|
+
self[text] = nil
|
57
|
+
|
58
|
+
return text
|
59
|
+
end
|
60
|
+
len += 1
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# Re-maps your +var_name+ to a unique, random
|
65
|
+
# names in the current scope
|
66
|
+
#
|
67
|
+
# @param var_name [String] the name you want to replace. This
|
68
|
+
# name will be remembered in the #renames hash
|
69
|
+
# @param opts [Hash] the options hash
|
70
|
+
# @option opts [Boolean] :generate if the variable was not
|
71
|
+
# explicitly renamed before, in this scope or any parent
|
72
|
+
# scope, generate a new random name
|
73
|
+
#
|
74
|
+
# @return [String] the randomly generated replacement name
|
75
|
+
# @return nil if generate=false and +var_name+ was not already replaced
|
76
|
+
def rename_var(var_name, opts={})
|
77
|
+
return var_name if BUILTIN_VARS.include?(var_name)
|
78
|
+
|
79
|
+
generate = opts.fetch(:generate, true)
|
80
|
+
unresolved = opts.fetch(:unresolved, [])
|
81
|
+
renamed = @renames[var_name]
|
82
|
+
|
83
|
+
if renamed.nil? and parent
|
84
|
+
renamed = parent.rename_var(var_name, :generate => false)
|
85
|
+
end
|
86
|
+
|
87
|
+
if renamed.nil? and generate
|
88
|
+
@renames[var_name] = random_var_name
|
89
|
+
renamed = @renames[var_name]
|
90
|
+
end
|
91
|
+
|
92
|
+
#puts "Mapped #{var_name} => #{renamed}" if renamed
|
93
|
+
|
94
|
+
renamed
|
95
|
+
end
|
96
|
+
|
97
|
+
# @return [Boolean] scope has members
|
98
|
+
def empty?
|
99
|
+
self.keys.empty? and (parent.nil? or parent.empty?)
|
100
|
+
end
|
101
|
+
|
102
|
+
# @return [Boolean] scope has no parent
|
103
|
+
def top?
|
104
|
+
parent.nil?
|
105
|
+
end
|
106
|
+
|
107
|
+
def top
|
108
|
+
p = self
|
109
|
+
p = p.parent until p.parent.nil?
|
110
|
+
p
|
111
|
+
end
|
112
|
+
|
113
|
+
# Check if we've used this var before. This will also check any
|
114
|
+
# attached parent scopes (and their parents, recursively)
|
115
|
+
#
|
116
|
+
# @return [Boolean] whether var is in scope
|
117
|
+
def has_key?(key)
|
118
|
+
super or (parent and parent.has_key?(key))
|
119
|
+
end
|
120
|
+
|
121
|
+
# replaces this Scope in the "parent" chain with a copy,
|
122
|
+
# empties current scope, and returns. Essentially an in-place
|
123
|
+
# push operation
|
124
|
+
def push!
|
125
|
+
replacement = dup
|
126
|
+
replacement.parent = @parent
|
127
|
+
replacement.renames = @renames
|
128
|
+
@renames = {}
|
129
|
+
@parent = replacement
|
130
|
+
clear
|
131
|
+
end
|
132
|
+
|
133
|
+
# "Consumes" the parent and replaces self with it
|
134
|
+
def pop!
|
135
|
+
clear
|
136
|
+
if @parent
|
137
|
+
merge! @parent
|
138
|
+
@renames = @parent.renames
|
139
|
+
@parent = @parent.parent
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
# @return [String] a random string that can be used as a var
|
144
|
+
def random_string(len)
|
145
|
+
@first_char_set.sample + (len-1).times.map { @char_set.sample }.join
|
146
|
+
end
|
147
|
+
|
148
|
+
end
|
data/lib/jsobfu/utils.rb
ADDED
@@ -0,0 +1,366 @@
|
|
1
|
+
#
|
2
|
+
# Some quick utility functions to minimize dependencies
|
3
|
+
#
|
4
|
+
module JSObfu::Utils
|
5
|
+
|
6
|
+
#
|
7
|
+
# The maximum length of a string that can be passed through
|
8
|
+
# #transform_string without being chopped up into separate
|
9
|
+
# expressions and concatenated
|
10
|
+
#
|
11
|
+
MAX_STRING_CHUNK = 10000
|
12
|
+
|
13
|
+
ALPHA_CHARSET = ([*'A'..'Z']+[*'a'..'z']).freeze
|
14
|
+
ALPHANUMERIC_CHARSET = (ALPHA_CHARSET+[*'0'..'9']).freeze
|
15
|
+
|
16
|
+
# For escaping special chars in a Javascript quoted string
|
17
|
+
JS_ESCAPE_MAP = { '\\' => '\\\\', "\r\n" => '\n', "\n" => '\n', "\r" => '\n', '"' => '\\"', "'" => "\\'" }
|
18
|
+
|
19
|
+
# Returns a random alphanumeric string of the desired length
|
20
|
+
#
|
21
|
+
# @param [Integer] len the desired length
|
22
|
+
# @return [String] random a-zA-Z0-9 text
|
23
|
+
def self.rand_text_alphanumeric(len)
|
24
|
+
rand_text(ALPHANUMERIC_CHARSET, len)
|
25
|
+
end
|
26
|
+
|
27
|
+
# Returns a random alpha string of the desired length
|
28
|
+
#
|
29
|
+
# @param [Integer] len the desired length
|
30
|
+
# @return [String] random a-zA-Z text
|
31
|
+
def self.rand_text_alpha(len)
|
32
|
+
rand_text(ALPHA_CHARSET, len)
|
33
|
+
end
|
34
|
+
|
35
|
+
# Returns a random string of the desired length in the desired charset
|
36
|
+
#
|
37
|
+
# @param [Array] charset the available chars
|
38
|
+
# @param [Integer] len the desired length
|
39
|
+
# @return [String] random text
|
40
|
+
def self.rand_text(charset, len)
|
41
|
+
len.times.map { charset.sample }.join
|
42
|
+
end
|
43
|
+
|
44
|
+
# Encodes the bytes in +str+ as hex literals, each preceded by +delimiter+
|
45
|
+
#
|
46
|
+
# @param [String] str the string to encode
|
47
|
+
# @param [String] delimiter prepended to every hex byte
|
48
|
+
# @return [String] hex encoded copy of str
|
49
|
+
def self.to_hex(str, delimiter="\\x")
|
50
|
+
str.bytes.to_a.map { |byte| delimiter+byte.to_s(16) }.join
|
51
|
+
end
|
52
|
+
|
53
|
+
# @param [String] code a quoted Javascript string
|
54
|
+
# @return [String] containing javascript code that wraps +code+ in a
|
55
|
+
# call to +eval+. A random eval method is chosen.
|
56
|
+
def self.js_eval(code, scope)
|
57
|
+
code = '"' + escape_javascript(code) + '"'
|
58
|
+
ret_statement = random_string_encoding 'return '
|
59
|
+
case rand(7)
|
60
|
+
when 0; "window[#{transform_string('eval', scope, :quotes => false)}](#{code})"
|
61
|
+
when 1; "[].constructor.constructor(\"#{ret_statement}\"+#{code})()"
|
62
|
+
when 2; "(function(){}).constructor('', \"#{ret_statement}\"+#{code})()"
|
63
|
+
when 3; "''.constructor.constructor('', \"#{ret_statement}\"+#{code})()"
|
64
|
+
when 4; "Function(\"#{random_string_encoding 'eval'}\")()(#{code})"
|
65
|
+
when 5; "Function(\"#{ret_statement}\"+#{code})()"
|
66
|
+
when 6; "Function()(\"#{ret_statement}\"+#{code})()"
|
67
|
+
end + ' '
|
68
|
+
end
|
69
|
+
|
70
|
+
#
|
71
|
+
# Convert a number to a random base (decimal, octal, or hexedecimal).
|
72
|
+
#
|
73
|
+
# Given 10 as input, the possible return values are:
|
74
|
+
# "10"
|
75
|
+
# "0xa"
|
76
|
+
# "012"
|
77
|
+
#
|
78
|
+
# @param num [Integer] number to convert to random base
|
79
|
+
# @return [String] equivalent encoding in a different base
|
80
|
+
#
|
81
|
+
def self.rand_base(num)
|
82
|
+
case rand(3)
|
83
|
+
when 0; num.to_s
|
84
|
+
when 1; "0%o" % num
|
85
|
+
when 2; "0x%x" % num
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
# In Javascript, it is possible to refer to the same var in a couple
|
90
|
+
# different ways:
|
91
|
+
#
|
92
|
+
# var AB = 1;
|
93
|
+
# console.log(\u0041\u0042); // prints "1"
|
94
|
+
#
|
95
|
+
# @return [String] equivalent variable name
|
96
|
+
def self.random_var_encoding(var_name)
|
97
|
+
if var_name.length < 3 and rand(6) == 0
|
98
|
+
to_hex(var_name, "\\u00")
|
99
|
+
else
|
100
|
+
var_name
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
# Given a Javascript string +str+ with NO escape characters, returns an
|
105
|
+
# equivalent string with randomly escaped bytes
|
106
|
+
#
|
107
|
+
# @return [String] Javascript string with a randomly-selected encoding
|
108
|
+
# for every byte
|
109
|
+
def self.random_string_encoding(str)
|
110
|
+
encoded = ''
|
111
|
+
str.unpack("C*") { |c|
|
112
|
+
encoded << case rand(3)
|
113
|
+
when 0; "\\x%02x"%(c)
|
114
|
+
when 1; "\\#{c.to_s(8)}"
|
115
|
+
when 2; "\\u%04x"%(c)
|
116
|
+
when 3; [c].pack("C")
|
117
|
+
end
|
118
|
+
}
|
119
|
+
encoded
|
120
|
+
end
|
121
|
+
|
122
|
+
# Taken from Rails ActionView: http://api.rubyonrails.org/classes/ActionView/Helpers/JavaScriptHelper.html
|
123
|
+
#
|
124
|
+
# @return [String] +javascript+ with special chars (newlines, quotes) escaped correctly
|
125
|
+
def self.escape_javascript(javascript)
|
126
|
+
javascript.gsub(/(\|<\/|\r\n|\342\200\250|\342\200\251|[\n\r"'])/u) {|match| JS_ESCAPE_MAP[match] }
|
127
|
+
end
|
128
|
+
|
129
|
+
#
|
130
|
+
# Return a mathematical expression that will evaluate to the given number
|
131
|
+
# +num+.
|
132
|
+
#
|
133
|
+
# +num+ can be a float or an int, but should never be negative.
|
134
|
+
#
|
135
|
+
def self.transform_number(num)
|
136
|
+
case num
|
137
|
+
when Fixnum
|
138
|
+
if num == 0
|
139
|
+
r = rand(10) + 1
|
140
|
+
transformed = "('#{JSObfu::Utils.rand_text_alpha(r)}'.length-#{r})"
|
141
|
+
elsif num > 0 and num < 10
|
142
|
+
# use a random string.length for small numbers
|
143
|
+
transformed = "'#{JSObfu::Utils.rand_text_alpha(num)}'.length"
|
144
|
+
else
|
145
|
+
transformed = "("
|
146
|
+
divisor = rand(num) + 1
|
147
|
+
a = num / divisor.to_i
|
148
|
+
b = num - (a * divisor)
|
149
|
+
# recurse half the time for a
|
150
|
+
a = (rand(2) == 0) ? transform_number(a) : rand_base(a)
|
151
|
+
# recurse half the time for divisor
|
152
|
+
divisor = (rand(2) == 0) ? transform_number(divisor) : rand_base(divisor)
|
153
|
+
transformed << "#{a}*#{divisor}"
|
154
|
+
transformed << "+#{b}"
|
155
|
+
transformed << ")"
|
156
|
+
end
|
157
|
+
when Float
|
158
|
+
transformed = "(#{num-num.floor}+#{rand_base(num.floor)})"
|
159
|
+
end
|
160
|
+
|
161
|
+
transformed
|
162
|
+
end
|
163
|
+
|
164
|
+
#
|
165
|
+
# Convert a javascript string into something that will generate that string.
|
166
|
+
#
|
167
|
+
# Randomly calls one of the +transform_string_*+ methods
|
168
|
+
#
|
169
|
+
# @param str [String] the string to transform
|
170
|
+
# @param scope [Scope] the scope to use for variable allocation
|
171
|
+
# @param opts [Hash] an optional options hash
|
172
|
+
# @option opts :quotes [Boolean] str includes quotation marks (true)
|
173
|
+
def self.transform_string(str, scope, opts={})
|
174
|
+
includes_quotes = opts.fetch(:quotes, true)
|
175
|
+
str = str.dup
|
176
|
+
quote = includes_quotes ? str[0,1] : '"'
|
177
|
+
|
178
|
+
if includes_quotes
|
179
|
+
str = str[1,str.length - 2]
|
180
|
+
return quote*2 if str.length == 0
|
181
|
+
end
|
182
|
+
|
183
|
+
if str.length > MAX_STRING_CHUNK
|
184
|
+
return safe_split(str, :quote => quote).map { |arg| transform_string(arg, scope) }.join('+')
|
185
|
+
end
|
186
|
+
|
187
|
+
case rand(2)
|
188
|
+
when 0
|
189
|
+
transform_string_split_concat(str, quote, scope)
|
190
|
+
when 1
|
191
|
+
transform_string_fromCharCode(str)
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
#
|
196
|
+
# Split a javascript string, +str+, without breaking escape sequences.
|
197
|
+
#
|
198
|
+
# The maximum length of each piece of the string is half the total length
|
199
|
+
# of the string, ensuring we (almost) always split into at least two
|
200
|
+
# pieces. This won't always be true when given a string like "AA\x41",
|
201
|
+
# where escape sequences artificially increase the total length (escape
|
202
|
+
# sequences are considered a single character).
|
203
|
+
#
|
204
|
+
# Returns an array of two-element arrays. The zeroeth element is a
|
205
|
+
# randomly generated variable name, the first is a piece of the string
|
206
|
+
# contained in +quote+s.
|
207
|
+
#
|
208
|
+
# See #escape_length
|
209
|
+
#
|
210
|
+
# @param str [String] the String to split
|
211
|
+
# @param opts [Hash] the options hash
|
212
|
+
# @option opts [String] :quote the quoting character ("|')
|
213
|
+
#
|
214
|
+
# @return [Array] 2d array series of [[var_name, string], ...]
|
215
|
+
#
|
216
|
+
def self.safe_split(str, opts={})
|
217
|
+
quote = opts.fetch(:quote)
|
218
|
+
|
219
|
+
parts = []
|
220
|
+
max_len = str.length / 2
|
221
|
+
while str.length > 0
|
222
|
+
len = 0
|
223
|
+
loop do
|
224
|
+
e_len = escape_length(str[len..-1])
|
225
|
+
e_len = 1 if e_len.nil?
|
226
|
+
len += e_len
|
227
|
+
# if we've reached the end of the string, bail
|
228
|
+
break unless str[len]
|
229
|
+
break if len > max_len
|
230
|
+
# randomize the length of each part
|
231
|
+
break if (rand(max_len) == 0)
|
232
|
+
end
|
233
|
+
|
234
|
+
part = str.slice!(0, len)
|
235
|
+
|
236
|
+
parts.push("#{quote}#{part}#{quote}")
|
237
|
+
end
|
238
|
+
|
239
|
+
parts
|
240
|
+
end
|
241
|
+
|
242
|
+
#
|
243
|
+
# Stolen from obfuscatejs.rb
|
244
|
+
# Determines the length of an escape sequence
|
245
|
+
#
|
246
|
+
# @param str [String] the String to check the length on
|
247
|
+
# @return [Integer] the length of the character at the head of the string
|
248
|
+
#
|
249
|
+
def self.escape_length(str)
|
250
|
+
esc_len = nil
|
251
|
+
if str[0,1] == "\\"
|
252
|
+
case str[1,1]
|
253
|
+
when "u"; esc_len = 6 # unicode \u1234
|
254
|
+
when "x"; esc_len = 4 # hex, \x41
|
255
|
+
when /[0-7]/ # octal, \123, \0
|
256
|
+
str[1,3] =~ /([0-7]{1,3})/
|
257
|
+
if $1.to_i(8) > 255
|
258
|
+
str[1,3] =~ /([0-7]{1,2})/
|
259
|
+
end
|
260
|
+
esc_len = 1 + $1.length
|
261
|
+
else; esc_len = 2 # \" \n, etc.
|
262
|
+
end
|
263
|
+
end
|
264
|
+
esc_len
|
265
|
+
end
|
266
|
+
|
267
|
+
#
|
268
|
+
# Split a javascript string, +str+, into multiple randomly-ordered parts
|
269
|
+
# and return an anonymous javascript function that joins them in the
|
270
|
+
# correct order. This method can be called safely on strings containing
|
271
|
+
# escape sequences. See #safe_split.
|
272
|
+
#
|
273
|
+
def self.transform_string_split_concat(str, quote, scope)
|
274
|
+
parts = safe_split(str, :quote => quote).map {|s| [scope.random_var_name, s] }
|
275
|
+
func = "(function () { var "
|
276
|
+
ret = "; return "
|
277
|
+
parts.sort { |a,b| rand }.each do |part|
|
278
|
+
func << "#{part[0]}=#{part[1]},"
|
279
|
+
end
|
280
|
+
func.chop!
|
281
|
+
|
282
|
+
ret << parts.map{|part| part[0]}.join("+")
|
283
|
+
final = func + ret + " })()"
|
284
|
+
|
285
|
+
final
|
286
|
+
end
|
287
|
+
|
288
|
+
#
|
289
|
+
# Return a call to String.fromCharCode() with each char of the input as arguments
|
290
|
+
#
|
291
|
+
# Example:
|
292
|
+
# input : "A\n"
|
293
|
+
# output: String.fromCharCode(0x41, 10)
|
294
|
+
#
|
295
|
+
# @param str [String] the String to transform (with no quotes)
|
296
|
+
# @return [String] Javascript code that evaluates to #str
|
297
|
+
#
|
298
|
+
def self.transform_string_fromCharCode(str)
|
299
|
+
"String.fromCharCode(#{string_to_bytes(str)})"
|
300
|
+
end
|
301
|
+
|
302
|
+
#
|
303
|
+
# Converts a string to a series of byte values
|
304
|
+
#
|
305
|
+
# @param str [String] the Javascript string to encode (no quotes)
|
306
|
+
# @return [String] containing a comma-separated list of byte values
|
307
|
+
# with random encodings (decimal/hex/octal)
|
308
|
+
#
|
309
|
+
def self.string_to_bytes(str)
|
310
|
+
len = 0
|
311
|
+
bytes = str.unpack("C*")
|
312
|
+
encoded_bytes = []
|
313
|
+
|
314
|
+
while str.length > 0
|
315
|
+
if str[0,1] == "\\"
|
316
|
+
str.slice!(0,1)
|
317
|
+
# then this is an escape sequence and we need to deal with all
|
318
|
+
# the special cases
|
319
|
+
case str[0,1]
|
320
|
+
# For chars that contain their non-escaped selves, step past
|
321
|
+
# the backslash and let the rand_base() below decide how to
|
322
|
+
# represent the character.
|
323
|
+
when '"', "'", "\\", " "
|
324
|
+
char = str.slice!(0,1).unpack("C").first
|
325
|
+
# For symbolic escapes, use the known value
|
326
|
+
when "n"; char = 0x0a; str.slice!(0,1)
|
327
|
+
when "t"; char = 0x09; str.slice!(0,1)
|
328
|
+
# Lastly, if it's a hex, unicode, or octal escape, pull out the
|
329
|
+
# real value and use that
|
330
|
+
when "x"
|
331
|
+
# Strip the x
|
332
|
+
str.slice!(0,1)
|
333
|
+
char = str.slice!(0,2).to_i 16
|
334
|
+
when "u"
|
335
|
+
# This can potentially lose information in the case of
|
336
|
+
# characters like \u0041, but since regular ascii is stored
|
337
|
+
# as unicode internally, String.fromCharCode(0x41) will be
|
338
|
+
# represented as 00 41 in memory anyway, so it shouldn't
|
339
|
+
# matter.
|
340
|
+
str.slice!(0,1)
|
341
|
+
char = str.slice!(0,4).to_i 16
|
342
|
+
when /[0-7]/
|
343
|
+
# Octals are a bit harder since they are variable width and
|
344
|
+
# don't necessarily mean what you might think. For example,
|
345
|
+
# "\61" == "1" and "\610" == "10". 610 is a valid octal
|
346
|
+
# number, but not a valid ascii character. Javascript will
|
347
|
+
# interpreter as much as it can as a char and use the rest
|
348
|
+
# as a literal. Boo.
|
349
|
+
str =~ /([0-7]{1,3})/
|
350
|
+
char = $1.to_i 8
|
351
|
+
if char > 255
|
352
|
+
str =~ /([0-7]{1,2})/
|
353
|
+
char = $1.to_i 8
|
354
|
+
end
|
355
|
+
str.slice!(0, $1.length)
|
356
|
+
end
|
357
|
+
else
|
358
|
+
char = str.slice!(0,1).unpack("C").first
|
359
|
+
end
|
360
|
+
encoded_bytes << rand_base(char) if char
|
361
|
+
end
|
362
|
+
|
363
|
+
encoded_bytes.join(',')
|
364
|
+
end
|
365
|
+
|
366
|
+
end
|