module JSObfu::Utils

Some quick utility functions to minimize dependencies

Constants

ALPHANUMERIC_CHARSET
ALPHA_CHARSET
JS_ESCAPE_MAP

For escaping special chars in a Javascript quoted string

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

Public Class Methods

escape_javascript(javascript) click to toggle source

Taken from Rails ActionView: api.rubyonrails.org/classes/ActionView/Helpers/JavaScriptHelper.html

@return [String] javascript with special chars (newlines, quotes) escaped correctly

# 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
escape_length(str) click to toggle source

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

@param str [String] the String to check the length on @return [Integer] the length of the character at the head of the string

# 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
js_eval(code, scope) click to toggle source

@param [String] code a quoted Javascript string @return [String] containing javascript code that wraps code in a call to eval. A random eval method is chosen.

# 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
rand_base(num) click to toggle source

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

Given 10 as input, the possible return values are:

"10"
"0xa"
"012"

@param num [Integer] number to convert to random base @return [String] equivalent encoding in a different base

# 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
rand_text(charset, len) click to toggle source

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

@param [Array] charset the available chars @param [Integer] len the desired length @return [String] random text

# File lib/jsobfu/utils.rb, line 40
def self.rand_text(charset, len)
  len.times.map { charset.sample }.join
end
rand_text_alpha(len) click to toggle source

Returns a random alpha string of the desired length

@param [Integer] len the desired length @return [String] random a-zA-Z text

# File lib/jsobfu/utils.rb, line 31
def self.rand_text_alpha(len)
  rand_text(ALPHA_CHARSET, len)
end
rand_text_alphanumeric(len) click to toggle source

Returns a random alphanumeric string of the desired length

@param [Integer] len the desired length @return [String] random a-zA-Z0-9 text

# File lib/jsobfu/utils.rb, line 23
def self.rand_text_alphanumeric(len)
  rand_text(ALPHANUMERIC_CHARSET, len)
end
random_string_encoding(str) click to toggle source

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

equivalent string with randomly escaped bytes

@return [String] Javascript string with a randomly-selected encoding for every byte

# 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
random_var_encoding(var_name) click to toggle source

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"

@return [String] equivalent variable name

# 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
safe_split(str, opts={}) click to toggle source

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 +quote+s.

See escape_length

@param str [String] the String to split @param opts [Hash] the options hash @option opts [String] :quote the quoting character (“|')

@return [Array] 2d array series of [[var_name, string], …]

# 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_to_bytes(str) click to toggle source

Converts a string to a series of byte values

@param str [String] the Javascript string to encode (no quotes) @return [String] containing a comma-separated list of byte values with random encodings (decimal/hex/octal)

# 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("U").first
    end
    encoded_bytes << rand_base(char) if char
  end

  encoded_bytes.join(',')
end
to_hex(str, delimiter="\\x") click to toggle source

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

@param [String] str the string to encode @param [String] delimiter prepended to every hex byte @return [String] hex encoded copy of str

# 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
transform_number(num) click to toggle source

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.

# File lib/jsobfu/utils.rb, line 137
def self.transform_number(num)
  case num
  when Integer
    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
transform_string(str, scope, opts={}) click to toggle source

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

Randomly calls one of the transform_string_* methods

@param str [String] the string to transform @param scope [Scope] the scope to use for variable allocation @param opts [Hash] an optional options hash @option opts :quotes [Boolean] str includes quotation marks (true)

# 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
transform_string_fromCharCode(str) click to toggle source

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

Example:

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

@param str [String] the String to transform (with no quotes) @return [String] Javascript code that evaluates to str

# File lib/jsobfu/utils.rb, line 300
def self.transform_string_fromCharCode(str)
  "String.fromCharCode(#{string_to_bytes(str)})"
end
transform_string_split_concat(str, quote, scope) click to toggle source

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.

# 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