Module: JSObfu::Utils

Defined in:
lib/jsobfu/utils.rb

Overview

Some quick utility functions to minimize dependencies

Constant Summary

MAX_STRING_CHUNK =

The maximum length of a string that can be passed through #transform_string without being chopped up into separate expressions and concatenated

10000
ALPHA_CHARSET =
([*'A'..'Z']+[*'a'..'z']).freeze
ALPHANUMERIC_CHARSET =
(ALPHA_CHARSET+[*'0'..'9']).freeze
JS_ESCAPE_MAP =

For escaping special chars in a Javascript quoted string

{ '\\' => '\\\\', "\r\n" => '\n', "\n" => '\n', "\r" => '\n', '"' => '\\"', "'" => "\\'" }

Class Method Summary (collapse)

Class Method Details

+ (String) escape_javascript(javascript)

Returns:

  • (String)

    javascript with special chars (newlines, quotes) escaped correctly



127
128
129
# File 'lib/jsobfu/utils.rb', line 127

def self.escape_javascript(javascript)
  javascript.gsub(/(\|<\/|\r\n|\342\200\250|\342\200\251|[\n\r"'])/u) {|match| JS_ESCAPE_MAP[match] }
end

+ (Integer) escape_length(str)

Stolen from obfuscatejs.rb Determines the length of an escape sequence

Parameters:

  • str (String)

    the String to check the length on

Returns:

  • (Integer)

    the length of the character at the head of the string



251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
# File 'lib/jsobfu/utils.rb', line 251

def self.escape_length(str)
  esc_len = nil
  if str[0,1] == "\\"
    case str[1,1]
    when "u"; esc_len = 6     # unicode \u1234
    when "x"; esc_len = 4     # hex, \x41
    when /[0-7]/              # octal, \123, \0
      str[1,3] =~ /([0-7]{1,3})/
      if $1.to_i(8) > 255
        str[1,3] =~ /([0-7]{1,2})/
      end
      esc_len = 1 + $1.length
    else; esc_len = 2         # \" \n, etc.
    end
  end
  esc_len
end

+ (String) js_eval(code, scope)

call to eval. A random eval method is chosen.

Parameters:

  • code (String)

    a quoted Javascript string

Returns:

  • (String)

    containing javascript code that wraps code in a



56
57
58
59
60
61
62
63
64
65
66
67
68
# File 'lib/jsobfu/utils.rb', line 56

def self.js_eval(code, scope)
  code = '"' + escape_javascript(code) + '"'
  ret_statement = random_string_encoding 'return '
  case rand(7)
    when 0; "window[#{transform_string('eval', scope, :quotes => false)}](#{code})"
    when 1; "[].constructor.constructor(\"#{ret_statement}\"+#{code})()"
    when 2; "(function(){}).constructor('', \"#{ret_statement}\"+#{code})()"
    when 3; "''.constructor.constructor('', \"#{ret_statement}\"+#{code})()"
    when 4; "Function(\"#{random_string_encoding 'eval'}\")()(#{code})"
    when 5; "Function(\"#{ret_statement}\"+#{code})()"
    when 6; "Function()(\"#{ret_statement}\"+#{code})()"
  end + ' '
end

+ (String) rand_base(num)

Convert a number to a random base (decimal, octal, or hexedecimal).

Given 10 as input, the possible return values are:

"10"
"0xa"
"012"

Parameters:

  • num (Integer)

    number to convert to random base

Returns:

  • (String)

    equivalent encoding in a different base



81
82
83
84
85
86
87
# File 'lib/jsobfu/utils.rb', line 81

def self.rand_base(num)
  case rand(3)
  when 0; num.to_s
  when 1; "0%o" % num
  when 2; "0x%x" % num
  end
end

+ (String) rand_text(charset, len)

Returns a random string of the desired length in the desired charset

Parameters:

  • charset (Array)

    the available chars

  • len (Integer)

    the desired length

Returns:

  • (String)

    random text



40
41
42
# File 'lib/jsobfu/utils.rb', line 40

def self.rand_text(charset, len)
  len.times.map { charset.sample }.join
end

+ (String) rand_text_alpha(len)

Returns a random alpha string of the desired length

Parameters:

  • len (Integer)

    the desired length

Returns:

  • (String)

    random a-zA-Z text



31
32
33
# File 'lib/jsobfu/utils.rb', line 31

def self.rand_text_alpha(len)
  rand_text(ALPHA_CHARSET, len)
end

+ (String) rand_text_alphanumeric(len)

Returns a random alphanumeric string of the desired length

Parameters:

  • len (Integer)

    the desired length

Returns:

  • (String)

    random a-zA-Z0-9 text



23
24
25
# File 'lib/jsobfu/utils.rb', line 23

def self.rand_text_alphanumeric(len)
  rand_text(ALPHANUMERIC_CHARSET, len)
end

+ (String) random_string_encoding(str)

Given a Javascript string str with NO escape characters, returns an

equivalent string with randomly escaped bytes

for every byte

Returns:

  • (String)

    Javascript string with a randomly-selected encoding



111
112
113
114
115
116
117
118
119
120
121
122
# File 'lib/jsobfu/utils.rb', line 111

def self.random_string_encoding(str)
  encoded = ''
  str.unpack("C*") { |c|
    encoded << case rand(3)
      when 0; "\\x%02x"%(c)
      when 1; "\\#{c.to_s(8)}"
      when 2; "\\u%04x"%(c)
      when 3; [c].pack("C")
    end
  }
  encoded
end

+ (String) random_var_encoding(var_name)

In Javascript, it is possible to refer to the same var in a couple different ways:

var AB = 1;
console.log(\u0041\u0042); // prints "1"

Returns:

  • (String)

    equivalent variable name



96
97
98
99
100
101
102
103
104
# File 'lib/jsobfu/utils.rb', line 96

def self.random_var_encoding(var_name)
  # TODO: add support for this to the rkelly parser, otherwise we can't encode twice
  # if var_name.length < 3 and rand(6) == 0
    # to_hex(var_name, "\\u00")
  # end

  # For now, do no encoding on var names (they are randomized anyways)
  var_name
end

+ (Array) safe_split(str, opts = {})

Split a javascript string, str, without breaking escape sequences.

The maximum length of each piece of the string is half the total length of the string, ensuring we (almost) always split into at least two pieces. This won't always be true when given a string like "AAx41", where escape sequences artificially increase the total length (escape sequences are considered a single character).

Returns an array of two-element arrays. The zeroeth element is a randomly generated variable name, the first is a piece of the string contained in quotes.

See #escape_length

Parameters:

  • str (String)

    the String to split

  • opts (Hash) (defaults to: {})

    the options hash

Options Hash (opts):

  • :quote (String)

    the quoting character (“|‘)

Returns:

  • (Array)

    2d array series of [[var_name, string], …]



218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
# File 'lib/jsobfu/utils.rb', line 218

def self.safe_split(str, opts={})
  quote = opts.fetch(:quote)

  parts = []
  max_len = str.length / 2
  while str.length > 0
    len = 0
    loop do
      e_len = escape_length(str[len..-1])
      e_len = 1 if e_len.nil?
      len += e_len
      # if we've reached the end of the string, bail
      break unless str[len]
      break if len > max_len
      # randomize the length of each part
      break if (rand(max_len) == 0)
    end

    part = str.slice!(0, len)

    parts.push("#{quote}#{part}#{quote}")
  end

  parts
end

+ (String) string_to_bytes(str)

Converts a string to a series of byte values

with random encodings (decimal/hex/octal)

Parameters:

  • str (String)

    the Javascript string to encode (no quotes)

Returns:

  • (String)

    containing a comma-separated list of byte values



311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
# File 'lib/jsobfu/utils.rb', line 311

def self.string_to_bytes(str)
  len = 0
  bytes = str.unpack("C*")
  encoded_bytes = []

  while str.length > 0
    if str[0,1] == "\\"
      str.slice!(0,1)
      # then this is an escape sequence and we need to deal with all
      # the special cases
      case str[0,1]
      # For chars that contain their non-escaped selves, step past
      # the backslash and let the rand_base() below decide how to
      # represent the character.
      when '"', "'", "\\", " "
        char = str.slice!(0,1).unpack("C").first
      # For symbolic escapes, use the known value
      when "n"; char = 0x0a; str.slice!(0,1)
      when "t"; char = 0x09; str.slice!(0,1)
      # Lastly, if it's a hex, unicode, or octal escape, pull out the
      # real value and use that
      when "x"
        # Strip the x
        str.slice!(0,1)
        char = str.slice!(0,2).to_i 16
      when "u"
        # This can potentially lose information in the case of
        # characters like \u0041, but since regular ascii is stored
        # as unicode internally, String.fromCharCode(0x41) will be
        # represented as 00 41 in memory anyway, so it shouldn't
        # matter.
        str.slice!(0,1)
        char = str.slice!(0,4).to_i 16
      when /[0-7]/
        # Octals are a bit harder since they are variable width and
        # don't necessarily mean what you might think. For example,
        # "\61" == "1" and "\610" == "10".  610 is a valid octal
        # number, but not a valid ascii character.  Javascript will
        # interpreter as much as it can as a char and use the rest
        # as a literal.  Boo.
        str =~ /([0-7]{1,3})/
        char = $1.to_i 8
        if char > 255
          str =~ /([0-7]{1,2})/
          char = $1.to_i 8
        end
        str.slice!(0, $1.length)
      end
    else
      char = str.slice!(0,1).unpack("C").first
    end
    encoded_bytes << rand_base(char) if char
  end

  encoded_bytes.join(',')
end

+ (String) to_hex(str, delimiter = "\\x")

Encodes the bytes in str as hex literals, each preceded by delimiter

Parameters:

  • str (String)

    the string to encode

  • delimiter (String) (defaults to: "\\x")

    prepended to every hex byte

Returns:

  • (String)

    hex encoded copy of str



49
50
51
# File 'lib/jsobfu/utils.rb', line 49

def self.to_hex(str, delimiter="\\x")
  str.bytes.to_a.map { |byte| delimiter+byte.to_s(16) }.join
end

+ (Object) transform_number(num)

Return a mathematical expression that will evaluate to the given number num.

num can be a float or an int, but should never be negative.



137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
# File 'lib/jsobfu/utils.rb', line 137

def self.transform_number(num)
  case num
  when Fixnum
    if num == 0
      r = rand(10) + 1
      transformed = "('#{JSObfu::Utils.rand_text_alpha(r)}'.length-#{r})"
    elsif num > 0 and num < 10
      # use a random string.length for small numbers
      transformed = "'#{JSObfu::Utils.rand_text_alpha(num)}'.length"
    else
      transformed = "("
      divisor = rand(num) + 1
      a = num / divisor.to_i
      b = num - (a * divisor)
      # recurse half the time for a
      a = (rand(2) == 0) ? transform_number(a) : rand_base(a)
      # recurse half the time for divisor
      divisor = (rand(2) == 0) ? transform_number(divisor) : rand_base(divisor)
      transformed << "#{a}*#{divisor}"
      transformed << "+#{b}"
      transformed << ")"
    end
  when Float
    transformed = "(#{num-num.floor}+#{rand_base(num.floor)})"
  end

  transformed
end

+ (Object) transform_string(str, scope, opts = {})

Convert a javascript string into something that will generate that string.

Randomly calls one of the transform_string_* methods

Parameters:

  • str (String)

    the string to transform

  • scope (Scope)

    the scope to use for variable allocation

  • opts (Hash) (defaults to: {})

    an optional options hash

Options Hash (opts):

  • :quotes (Boolean)

    str includes quotation marks (true)



175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
# File 'lib/jsobfu/utils.rb', line 175

def self.transform_string(str, scope, opts={})
  includes_quotes = opts.fetch(:quotes, true)
  str = str.dup
  quote = includes_quotes ? str[0,1] : '"'

  if includes_quotes
    str = str[1,str.length - 2]
    return quote*2 if str.length == 0
  end

  if str.length > MAX_STRING_CHUNK
    return safe_split(str, :quote => quote).map { |arg| transform_string(arg, scope) }.join('+')
  end

  case rand(2)
  when 0
    transform_string_split_concat(str, quote, scope)
  when 1
    transform_string_fromCharCode(str)
  end
end

+ (String) transform_string_fromCharCode(str)

Return a call to String.fromCharCode() with each char of the input as arguments

Example:

input : "A\n"
output: String.fromCharCode(0x41, 10)

Parameters:

  • str (String)

    the String to transform (with no quotes)

Returns:

  • (String)

    Javascript code that evaluates to #str



300
301
302
# File 'lib/jsobfu/utils.rb', line 300

def self.transform_string_fromCharCode(str)
  "String.fromCharCode(#{string_to_bytes(str)})"
end

+ (Object) transform_string_split_concat(str, quote, scope)

Split a javascript string, str, into multiple randomly-ordered parts and return an anonymous javascript function that joins them in the correct order. This method can be called safely on strings containing escape sequences. See #safe_split.



275
276
277
278
279
280
281
282
283
284
285
286
287
288
# File 'lib/jsobfu/utils.rb', line 275

def self.transform_string_split_concat(str, quote, scope)
  parts = safe_split(str, :quote => quote).map {|s| [scope.random_var_name, s] }
  func = "(function () { var "
  ret = "; return "
  parts.sort { |a,b| rand }.each do |part|
    func << "#{part[0]}=#{part[1]},"
  end
  func.chop!

  ret  << parts.map{|part| part[0]}.join("+")
  final = func + ret + " })()"

  final
end