class Rex::Exploitation::JSObfu

Obfuscate JavaScript by randomizing as much as possible and removing easily-signaturable string constants.

Example:

js = ::Rex::Exploitation::JSObfu.new %Q|
  var a = "0\\612\\063\\x34\\x35\\x36\\x37\\x38\\u0039";
  var b = { foo : "foo", bar : "bar" }
  alert(a);
  alert(b.foo);
|
js.obfuscate
puts js

Example Output:

var VwxvESbCgv = String.fromCharCode(0x30,0x31,062,063,064,53,0x36,067,070,0x39);
var ToWZPn = {
  "\146\157\x6f": (function () { var yDyv="o",YnCL="o",Qcsa="f"; return Qcsa+YnCL+yDyv })(),
  "\142ar": String.fromCharCode(0142,97,0162)
};
alert(VwxvESbCgv);
alert(ToWZPn.foo);

NOTE: Variables MUST be declared with a ‘var’ statement BEFORE first use (or not at all) for this to generate correct code! If variables are not declared they will not be randomized but the generated code will be correct.

Bad Example Javascript:

a = "asdf"; // this variable hasn't been declared and will not be randomized
var a;
alert(a); // real js engines will alert "asdf" here

Bad Example Obfuscated:

a = (function () { var hpHu="f",oyTm="asd"; return oyTm+hpHu })();
var zSrnHpEfJZtg;
alert(zSrnHpEfJZtg);

Notice that the first usage of a (before it was declared) is not randomized. Thus, the obfuscated version will alert ‘undefined’ instead of “asdf”.

Constants

RESERVED_KEYWORDS

these keywords should never be used as a random var name source: developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Reserved_Words

Attributes

ast[R]

Abstract Syntax Tree generated by RKelly::Parser#parse

Public Class Methods

new(code) click to toggle source

Saves code for later obfuscation with obfuscate

# File lib/rex/exploitation/jsobfu.rb, line 67
def initialize(code)
  @code = code
  @funcs = {}
  @vars  = {}
  @debug = false
  @rand_gen = Rex::RandomIdentifierGenerator.new(
    :max_length => 15,
    :first_char_set => Rex::Text::Alpha+"_$",
    :char_set => Rex::Text::AlphaNumeric+"_$"
  )
end

Public Instance Methods

<<(str) click to toggle source

Add str to the un-obfuscated code.

Calling this method after obfuscate is undefined

# File lib/rex/exploitation/jsobfu.rb, line 84
def <<(str)
  @code << str
end
obfuscate() click to toggle source

Parse and obfuscate

# File lib/rex/exploitation/jsobfu.rb, line 119
def obfuscate
  parse
  obfuscate_r(@ast)
end
random_var_name() click to toggle source

@return [String] a unique random var name that is not a reserved keyword

# File lib/rex/exploitation/jsobfu.rb, line 125
def random_var_name
  loop do
    text = random_string
    unless @vars.has_value?(text) or RESERVED_KEYWORDS.include?(text)
      return text
    end
  end
end
sym(lookup) click to toggle source

Return the obfuscated name of a symbol

You MUST call obfuscate before this method!

# File lib/rex/exploitation/jsobfu.rb, line 105
def sym(lookup)
  if @vars[lookup]
    ret = @vars[lookup]
  elsif @funcs[lookup]
    ret = @funcs[lookup]
  else
    ret = lookup
  end
  ret
end
to_s() click to toggle source

Return the (possibly obfuscated) code as a string.

If obfuscate has not been called before this, returns the parsed, unobfuscated code. This can be useful for example to remove comments and standardize spacing.

# File lib/rex/exploitation/jsobfu.rb, line 95
def to_s
  parse if not @ast
  @ast.to_ecma
end

Protected Instance Methods

escape_length(str) click to toggle source

Stolen from obfuscatejs.rb

Determines the length of an escape sequence

# File lib/rex/exploitation/jsobfu.rb, line 372
def 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
obfuscate_r(ast) click to toggle source

Recursive method to obfuscate the given ast.

ast should be the result of RKelly::Parser#parse

# File lib/rex/exploitation/jsobfu.rb, line 146
def obfuscate_r(ast)
  ast.each do |node|
    #if node.respond_to? :value and node.value.kind_of? String and node.value =~ /bodyOnLoad/i
    # $stdout.puts("bodyOnLoad: #{node.class}: #{node.value}")
    #end

    case node
    when nil
      nil

    when ::RKelly::Nodes::SourceElementsNode
      # Recurse
      obfuscate_r(node.value)

    #when ::RKelly::Nodes::ObjectLiteralNode
      # TODO
      #$stdout.puts(node.methods - Object.new.methods)
      #$stdout.puts(node.value.inspect)

    when ::RKelly::Nodes::PropertyNode
      # Property names must be bare words or string literals NOT
      # expressions!  Can't use transform_string() here
      if node.name =~ /^[a-zA-Z_][a-zA-Z0-9_]*$/
        n = '"'
        node.name.unpack("C*") { |c|
          case rand(3)
          when 0; n << "\\x%02x"%(c)
          when 1; n << "\\#{c.to_s 8}"
          when 2; n << [c].pack("C")
          end
        }
        n << '"'
        node.instance_variable_set(:@name, n)
      end

    # Variables
    when ::RKelly::Nodes::VarDeclNode
      if @vars[node.name].nil?
        @vars[node.name] = random_var_name
      end
      node.name = @vars[node.name]
    when ::RKelly::Nodes::ParameterNode
      if @vars[node.value].nil?
        @vars[node.value] = random_var_name
      end
      node.value = @vars[node.value]
    when ::RKelly::Nodes::ResolveNode
      #$stdout.puts("Resolve bodyOnload: #{@vars[node.value]}") if "bodyOnLoad" == node.value
      node.value = @vars[node.value] if @vars[node.value]
    when ::RKelly::Nodes::DotAccessorNode
      case node.value
      when ::RKelly::Nodes::ResolveNode
        if @vars[node.value.value]
          node.value.value = @vars[node.value.value]
        end
      #else
      #       $stderr.puts("Non-resolve node as target of dotaccessor: #{node.value.class}")
      end

    # Functions
    when ::RKelly::Nodes::FunctionDeclNode
      #$stdout.puts("FunctionDecl: #{node.value}")
      # Functions can also act as objects, so store them in the vars
      # and the functions list so we can replace them in both places
      if @funcs[node.value].nil? and not @funcs.values.include?(node.value)
        @funcs[node.value] = random_var_name
        if @vars[node.value].nil?
          @vars[node.value] = @funcs[node.value]
        end
        node.value = @funcs[node.value]
      end
    when ::RKelly::Nodes::FunctionCallNode
      # The value of a FunctionCallNode is some sort of accessor node or a ResolveNode
      # so this is basically useless
      #$stdout.puts("Function call: #{node.name} => #{@funcs[node.name]}")
      #node.value = @funcs[node.value] if @funcs[node.value]

    # Transformers
    when ::RKelly::Nodes::NumberNode
      node.value = transform_number(node.value)
    when ::RKelly::Nodes::StringNode
      node.value = transform_string(node.value)
    else
      #$stderr.puts "#{node.class}: #{node.value}"
      #$stderr.puts "#{node.class}"
    end

    #unless node.kind_of? ::RKelly::Nodes::SourceElementsNode
    # $stderr.puts "#{node.class}: #{node.value}"
    #end
  end

  nil
end
parse() click to toggle source

Generate an Abstract Syntax Tree (ast) for later obfuscation

# File lib/rex/exploitation/jsobfu.rb, line 244
def parse
  parser = RKelly::Parser.new
  @ast = parser.parse(@code)
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"
# File lib/rex/exploitation/jsobfu.rb, line 257
def rand_base(num)
  case rand(3)
  when 0; num.to_s
  when 1; "0%o" % num
  when 2; "0x%x" % num
  end
end
random_string() click to toggle source

@return [String] a random string

# File lib/rex/exploitation/jsobfu.rb, line 137
def random_string
  @rand_gen.generate
end
safe_split(str, quote) 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

# File lib/rex/exploitation/jsobfu.rb, line 342
def safe_split(str, 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(4) == 0)
    end

    part = str.slice!(0, len)

    var = Rex::Text.rand_text_alpha(4)
    parts.push( [ var, "#{quote}#{part}#{quote}" ] )
  end

  parts
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/rex/exploitation/jsobfu.rb, line 271
def transform_number(num)
  case num
  when Fixnum
    if num == 0
      r = rand(10) + 1
      transformed = "('#{Rex::Text.rand_text_alpha(r)}'.length - #{r})"
    elsif num > 0 and num < 10
      # use a random string.length for small numbers
      transformed = "'#{Rex::Text.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

  #puts("#{num} == #{transformed}")

  transformed
end
transform_string(str) click to toggle source

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

Randomly calls one of the transform_string_* methods

# File lib/rex/exploitation/jsobfu.rb, line 307
def transform_string(str)
  quote = str[0,1]
  # pull off the quotes
  str = str[1,str.length - 2]
  return quote*2 if str.length == 0

  case rand(2)
  when 0
    transformed = transform_string_split_concat(str, quote)
  when 1
    transformed = transform_string_fromCharCode(str)
  #when 2
  #   # Currently no-op
  #   transformed = transform_string_unescape(str)
  end

  #$stderr.puts "Obfuscating str: #{str.ljust 30} #{transformed}"
  transformed
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)
# File lib/rex/exploitation/jsobfu.rb, line 424
def transform_string_fromCharCode(str)
  buf = "String.fromCharCode("
  bytes = str.unpack("C*")
  len = 0
  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
    buf << "#{rand_base(char)},"
  end
  # Strip off the last comma
  buf = buf[0,buf.length-1] + ")"
  transformed = buf

  transformed
end
transform_string_split_concat(str, quote) 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/rex/exploitation/jsobfu.rb, line 396
def transform_string_split_concat(str, quote)
  parts = safe_split(str, quote)
  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