class Rex::Exploitation::ObfuscateJS
Obfuscates javascript in various ways
Attributes
Public Class Methods
Initialize an instance of the obfuscator
# File lib/rex/exploitation/obfuscatejs.rb, line 69 def initialize(js = "", opts = {}) @js = js @dynsym = {} @opts = { 'Symbols' => { 'Variables'=>[], 'Methods'=>[], 'Namespaces'=>[], 'Classes'=>[] }, 'Strings'=>false } @done = false update_opts(opts) if (opts.length > 0) end
Obfuscates a javascript string.
Options are 'Symbols', described below, and 'Strings', a boolean which specifies whether strings within the javascript should be mucked with (defaults to false).
The 'Symbols' argument should have the following format:
{ 'Variables' => [ 'var1', ... ], 'Methods' => [ 'method1', ... ], 'Namespaces' => [ 'n', ... ], 'Classes' => [ { 'Namespace' => 'n', 'Class' => 'y'}, ... ] }
Make sure you order your methods, classes, and namespaces by most specific to least specific to prevent partial substitution. For instance, if you have two methods (joe and joeBob), you should place joeBob before joe because it is more specific and will be globally replaced before joe is replaced.
A simple example follows:
<code> js = ObfuscateJS.new
<<ENDJS
function say_hi() { var foo = "Hello, world"; document.writeln(foo); }
ENDJS js.obfuscate(
'Symbols' => { 'Variables' => [ 'foo' ], 'Methods' => [ 'say_hi' ] } 'Strings' => true
) </code>
which should generate something like the following:
function oJaDYRzFOyJVQCOHk() { var cLprVG = "\x48\x65\x6c\x6c\x6f\x2c\x20\x77\x6f\x72\x6c\x64"; document.writeln(cLprVG); }
String obfuscation tries to deal with escaped quotes within strings but won't catch things like
"\\"
so be careful.
# File lib/rex/exploitation/obfuscatejs.rb, line 62 def self.obfuscate(js, opts = {}) ObfuscateJS.new(js).obfuscate(opts) end
Public Instance Methods
# File lib/rex/exploitation/obfuscatejs.rb, line 165 def +(str) @js + str end
# File lib/rex/exploitation/obfuscatejs.rb, line 162 def <<(str) @js << str end
Obfuscates the javascript string passed to the constructor
# File lib/rex/exploitation/obfuscatejs.rb, line 120 def obfuscate(opts = {}) #return @js if (@done) @done = true update_opts(opts) if (@opts['Strings']) obfuscate_strings() # Full space randomization does not work for javascript -- despite # claims that space is irrelavent, newlines break things. Instead, # use only space (0x20) and tab (0x09). #@js.gsub!(/[\x09\x20]+/) { |s| # len = rand(50)+2 # set = "\x09\x20" # buf = '' # while (buf.length < len) # buf << set[rand(set.length)].chr # end # # buf #} end # Remove our comments remove_comments # Globally replace symbols replace_symbols(@opts['Symbols']) if @opts['Symbols'] return @js end
Returns the dynamic symbol associated with the supplied symbol name
If obfuscation has not yet been performed (i.e. obfuscate() has not been called), then this method simply returns its argument
# File lib/rex/exploitation/obfuscatejs.rb, line 113 def sym(name) @dynsym[name] || name end
Returns the replaced javascript string
# File lib/rex/exploitation/obfuscatejs.rb, line 157 def to_s @js end
# File lib/rex/exploitation/obfuscatejs.rb, line 85 def update_opts(opts) if (opts.nil? or opts.length < 1) return end if (@opts['Symbols'] && opts['Symbols']) ['Variables', 'Methods', 'Namespaces', 'Classes'].each { |k| if (@opts['Symbols'][k] && opts['Symbols'][k]) opts['Symbols'][k].each { |s| if (not @opts['Symbols'][k].include? s) @opts['Symbols'][k].push(s) end } elsif (opts['Symbols'][k]) @opts['Symbols'][k] = opts['Symbols'][k] end } elsif opts['Symbols'] @opts['Symbols'] = opts['Symbols'] end @opts['Strings'] ||= opts['Strings'] end
Protected Instance Methods
Change each string into some javascript that will generate that string
There are a couple of caveats to using string obfuscation:
* it tries to deal with escaped quotes within strings but won't catch things like: "\\" * depending on the random choices, this can easily balloon a short string up to hundreds of kilobytes if called multiple times.
so be careful.
# File lib/rex/exploitation/obfuscatejs.rb, line 222 def obfuscate_strings() @js.gsub!(/".*?[^\\]"|'.*?[^\\]'/) { |str| buf = '' quote = str[0,1] # Pull the quotes off either end str = str[1, str.length-2] case (rand(2)) # Disable hex encoding for now. It's just too big a hassle. #when 0 # # This is where we can run into trouble with generating # # incorrect code. If we hex encode a string twice, the second # # encoding will generate the first instead of the original # # string. # if str =~ /\\x/ # # Always have to remove spaces from strings so the space # # randomization doesn't mess with them. # buf = quote + str.gsub(/ /, '\x20') + quote # else # buf = '"' + Rex::Text.to_hex(str) + '"' # end when 0 # # Escape sequences when naively encoded for unescape become a # literal backslash instead of the intended meaning. To avoid # that problem, we scan the string for escapes and leave them # unmolested. # buf << 'unescape("' bytes = str.unpack("C*") c = 0 while bytes[c] if bytes[c].chr == "\\" # XXX This is pretty slow. esc_len = parse_escape(bytes, c) buf << bytes[c, esc_len].map{|a| a.chr}.join c += esc_len next end buf << "%%%0.2x"%(bytes[c]) # Break the string into smaller strings if bytes[c+1] and rand(10) == 0 buf << '" + "' end c += 1 end buf << '")' when 1 buf = "String.fromCharCode( " bytes = str.unpack("C*") c = 0 while bytes[c] if bytes[c].chr == "\\" case bytes[c+1].chr # For chars that contain their non-escaped selves, step # past the backslash and let the rand() below decide # how to represent the character. when '"'; c += 1 when "'"; c += 1 when "\\"; c += 1 # For others, just take the hex representation out of # laziness. when "n"; buf << "0x0a"; c += 2; next when "t"; buf << "0x09"; c += 2; next # Lastly, if it's a hex, unicode, or octal escape, # leave it, and anything after it, alone. At some # point we may want to parse up to the end of the # escapes and encode subsequent non-escape characters. # Since this is the lazy way to do it, spaces after an # escape sequence will get away unmodified. To prevent # the space randomizer from hosing the string, convert # spaces specifically. else buf = buf[0,buf.length-1] + " )" buf << ' + ("' + bytes[c, bytes.length].map{|a| a==0x20 ? '\x20' : a.chr}.join + '" ' break end end case (rand(3)) when 0 buf << " %i,"%(bytes[c]) when 1 buf << " 0%o,"%(bytes[c]) when 2 buf << " 0x%0.2x,"%(bytes[c]) end c += 1 end # Strip off the last comma buf = buf[0,buf.length-1] + " )" end buf } @js end
# File lib/rex/exploitation/obfuscatejs.rb, line 317 def parse_escape(bytes, offset) esc_len = 0 if bytes[offset].chr == "\\" case bytes[offset+1].chr when "u"; esc_len = 6 # unicode \u1234 when "x"; esc_len = 4 # hex, \x41 when /[0-9]/ # octal, \123, \0 oct = bytes[offset+1, 4].map{|a|a.chr}.join oct =~ /([0-9]+)/ esc_len = 1 + $1.length else; esc_len = 2 # \" \n, etc. end end esc_len end
Get rid of both single-line C++ style comments and multiline C style comments.
Note: embedded comments (e.g.: “//*/*/”) will break this, but they also break real javascript engines so I don't care.
# File lib/rex/exploitation/obfuscatejs.rb, line 178 def remove_comments @js.gsub!(%r{\s+//.*$}, '') @js.gsub!(%r{/\*.*?\*/}m, '') end
Replace method, class, and namespace symbols found in the javascript string
# File lib/rex/exploitation/obfuscatejs.rb, line 185 def replace_symbols(symbols) taken = { } # Generate random symbol names [ 'Variables', 'Methods', 'Classes', 'Namespaces' ].each { |symtype| next if symbols[symtype].nil? symbols[symtype].each { |sym| dyn = Rex::Text.rand_text_alpha(rand(32)+1) until dyn and not taken.key?(dyn) taken[dyn] = true if symtype == 'Classes' full_sym = sym['Namespace'] + "." + sym['Class'] @dynsym[full_sym] = dyn @js.gsub!(/#{full_sym}/) { |m| sym['Namespace'] + "." + dyn } else @dynsym[sym] = dyn @js.gsub!(/#{sym}/, dyn) end } } end