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
Abstract Syntax Tree generated by RKelly::Parser#parse
Public Class Methods
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
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
Parse and obfuscate
# File lib/rex/exploitation/jsobfu.rb, line 119 def obfuscate parse obfuscate_r(@ast) end
@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
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
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
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
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
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
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
@return [String] a random string
# File lib/rex/exploitation/jsobfu.rb, line 137 def random_string @rand_gen.generate end
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
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
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
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
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