Land #3224 - Fixes large-string expansion in JSObfu

bug/bundler_fix
sinn3r 2014-04-10 12:09:22 -05:00
commit 68a50e3663
No known key found for this signature in database
GPG Key ID: 2384DB4EF06F730B
4 changed files with 201 additions and 92 deletions

View File

@ -1,25 +1,25 @@
// Case matters, see lib/msf/core/constants.rb // Case matters, see lib/msf/core/constants.rb
// All of these should match up with constants in ::Msf::HttpClients // All of these should match up with constants in ::Msf::HttpClients
clients_opera = "Opera"; var clients_opera = "Opera";
clients_ie = "MSIE"; var clients_ie = "MSIE";
clients_ff = "Firefox"; var clients_ff = "Firefox";
clients_chrome= "Chrome"; var clients_chrome= "Chrome";
clients_safari= "Safari"; var clients_safari= "Safari";
// All of these should match up with constants in ::Msf::OperatingSystems // All of these should match up with constants in ::Msf::OperatingSystems
oses_linux = "Linux"; var oses_linux = "Linux";
oses_windows = "Microsoft Windows"; var oses_windows = "Microsoft Windows";
oses_mac_osx = "Mac OS X"; var oses_mac_osx = "Mac OS X";
oses_freebsd = "FreeBSD"; var oses_freebsd = "FreeBSD";
oses_netbsd = "NetBSD"; var oses_netbsd = "NetBSD";
oses_openbsd = "OpenBSD"; var oses_openbsd = "OpenBSD";
// All of these should match up with the ARCH_* constants // All of these should match up with the ARCH_* constants
arch_armle = "armle"; var arch_armle = "armle";
arch_x86 = "x86"; var arch_x86 = "x86";
arch_x86_64 = "x86_64"; var arch_x86_64 = "x86_64";
arch_ppc = "ppc"; var arch_ppc = "ppc";
window.os_detect = {}; window.os_detect = {};

View File

@ -48,14 +48,71 @@ module Exploitation
# #
class JSObfu class JSObfu
# these keywords should never be used as a random var name # A single Javascript scope, used as a key-value store
# source: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Reserved_Words # to maintain uniqueness of members in generated closures.
RESERVED_KEYWORDS = %w( # For speed this class is implemented as a subclass of Hash.
break case catch continue debugger default delete do else finally class Scope < Hash
for function if in instanceof new return switch this throw try
typeof var void while with class enum export extends import super # these keywords should never be used as a random var name
implements interface let package private protected public static yield # source: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Reserved_Words
) RESERVED_KEYWORDS = %w(
break case catch continue debugger default delete do else finally
for function if in instanceof new return switch this throw try
typeof var void while with class enum export extends import super
implements interface let package private protected public static yield
)
# these vars should not be shadowed as they may be used in other obfuscated code
BUILTIN_VARS = %w(
String window unescape
)
# @param [Rex::Exploitation::JSObfu::Scope] parent an optional parent scope,
# sometimes necessary to prevent needless var shadowing
def initialize(parent=nil)
@parent = parent
@rand_gen = Rex::RandomIdentifierGenerator.new(
:max_length => 15,
:first_char_set => Rex::Text::Alpha+"_$",
:char_set => Rex::Text::AlphaNumeric+"_$",
:min_length => 1
)
end
# @return [String] a unique random var name that is not a reserved keyword
def random_var_name
len = 1
loop do
text = random_string(len)
unless has_key?(text) or
RESERVED_KEYWORDS.include?(text) or
BUILTIN_VARS.include?(text)
self[text] = nil
return text
end
len += 1
end
end
# @return [Boolean] var is in scope
def has_key?(key)
super or (@parent and @parent.has_key?(key))
end
# @return [String] a random string that can be used as a var
def random_string(len)
@rand_gen.generate(len)
end
end
#
# The maximum length of a string that can be passed through
# #transform_string without being chopped up into separate
# expressions and concatenated
#
MAX_STRING_CHUNK = 10000
# #
# Abstract Syntax Tree generated by RKelly::Parser#parse # Abstract Syntax Tree generated by RKelly::Parser#parse
@ -69,12 +126,8 @@ class JSObfu
@code = code @code = code
@funcs = {} @funcs = {}
@vars = {} @vars = {}
@scope = Scope.new
@debug = false @debug = false
@rand_gen = Rex::RandomIdentifierGenerator.new(
:max_length => 15,
:first_char_set => Rex::Text::Alpha+"_$",
:char_set => Rex::Text::AlphaNumeric+"_$"
)
end end
# #
@ -122,23 +175,8 @@ class JSObfu
obfuscate_r(@ast) obfuscate_r(@ast)
end end
# @return [String] a unique random var name that is not a reserved keyword
def random_var_name
loop do
text = random_string
unless @vars.has_value?(text) or RESERVED_KEYWORDS.include?(text)
return text
end
end
end
protected protected
# @return [String] a random string
def random_string
@rand_gen.generate
end
# #
# Recursive method to obfuscate the given +ast+. # Recursive method to obfuscate the given +ast+.
# #
@ -182,12 +220,12 @@ protected
# Variables # Variables
when ::RKelly::Nodes::VarDeclNode when ::RKelly::Nodes::VarDeclNode
if @vars[node.name].nil? if @vars[node.name].nil?
@vars[node.name] = random_var_name @vars[node.name] = @scope.random_var_name
end end
node.name = @vars[node.name] node.name = @vars[node.name]
when ::RKelly::Nodes::ParameterNode when ::RKelly::Nodes::ParameterNode
if @vars[node.value].nil? if @vars[node.value].nil?
@vars[node.value] = random_var_name @vars[node.value] = @scope.random_var_name
end end
node.value = @vars[node.value] node.value = @vars[node.value]
when ::RKelly::Nodes::ResolveNode when ::RKelly::Nodes::ResolveNode
@ -209,7 +247,7 @@ protected
# Functions can also act as objects, so store them in the vars # Functions can also act as objects, so store them in the vars
# and the functions list so we can replace them in both places # and the functions list so we can replace them in both places
if @funcs[node.value].nil? and not @funcs.values.include?(node.value) if @funcs[node.value].nil? and not @funcs.values.include?(node.value)
@funcs[node.value] = random_var_name @funcs[node.value] = @scope.random_var_name
if @vars[node.value].nil? if @vars[node.value].nil?
@vars[node.value] = @funcs[node.value] @vars[node.value] = @funcs[node.value]
end end
@ -311,14 +349,15 @@ protected
str = str[1,str.length - 2] str = str[1,str.length - 2]
return quote*2 if str.length == 0 return quote*2 if str.length == 0
case rand(2) if str.length > MAX_STRING_CHUNK
return safe_split(str, quote).map { |args| transform_string(args[1]) }.join('+')
end
transformed = case rand(2)
when 0 when 0
transformed = transform_string_split_concat(str, quote) transform_string_split_concat(str, quote)
when 1 when 1
transformed = transform_string_fromCharCode(str) transform_string_fromCharCode(str)
#when 2
# # Currently no-op
# transformed = transform_string_unescape(str)
end end
#$stderr.puts "Obfuscating str: #{str.ljust 30} #{transformed}" #$stderr.puts "Obfuscating str: #{str.ljust 30} #{transformed}"
@ -353,12 +392,12 @@ protected
break unless str[len] break unless str[len]
break if len > max_len break if len > max_len
# randomize the length of each part # randomize the length of each part
break if (rand(4) == 0) break if (rand(max_len) == 0)
end end
part = str.slice!(0, len) part = str.slice!(0, len)
var = Rex::Text.rand_text_alpha(4) var = @scope.random_var_name
parts.push( [ var, "#{quote}#{part}#{quote}" ] ) parts.push( [ var, "#{quote}#{part}#{quote}" ] )
end end
@ -409,12 +448,6 @@ protected
final final
end end
# TODO
#def transform_string_unescape(str)
# str
#end
# #
# Return a call to String.fromCharCode() with each char of the input as arguments # Return a call to String.fromCharCode() with each char of the input as arguments
# #
@ -423,9 +456,17 @@ protected
# output: String.fromCharCode(0x41, 10) # output: String.fromCharCode(0x41, 10)
# #
def transform_string_fromCharCode(str) def transform_string_fromCharCode(str)
buf = "String.fromCharCode(" "String.fromCharCode(#{string_to_bytes(str)})"
bytes = str.unpack("C*") end
#
# Return a comma-separated list of byte values with random encodings (decimal/hex/octal)
#
def string_to_bytes(str)
len = 0 len = 0
bytes = str.unpack("C*")
encoded_bytes = []
while str.length > 0 while str.length > 0
if str[0,1] == "\\" if str[0,1] == "\\"
str.slice!(0,1) str.slice!(0,1)
@ -472,16 +513,12 @@ protected
else else
char = str.slice!(0,1).unpack("C").first char = str.slice!(0,1).unpack("C").first
end end
buf << "#{rand_base(char)}," encoded_bytes << rand_base(char)
end end
# Strip off the last comma
buf = buf[0,buf.length-1] + ")"
transformed = buf
transformed encoded_bytes.join(',')
end end
end end
end end
end end

View File

@ -0,0 +1,50 @@
require 'spec_helper'
require 'rex/exploitation/jsobfu'
describe Rex::Exploitation::JSObfu::Scope do
subject(:scope) do
described_class.new()
end
describe '#random_var_name' do
subject(:random_var_name) { scope.random_var_name }
it { should be_a String }
it { should_not be_empty }
it 'is composed of _, $, alphanumeric chars' do
20.times { expect(scope.random_var_name).to match(/\A[a-zA-Z0-9$_]+\Z/) }
end
it 'does not start with a number' do
20.times { expect(scope.random_var_name).not_to match(/\A[0-9]/) }
end
context 'when a reserved word is generated' do
let(:reserved) { described_class::RESERVED_KEYWORDS.first }
let(:random) { 'abcdef' }
let(:generated) { [reserved, reserved, reserved, random] }
before do
scope.stub(:random_string) { generated.shift }
end
it { should eq random }
end
context 'when a non-unique random var is generated' do
let(:preexisting) { 'preexist' }
let(:random) { 'abcdef' }
let(:generated) { [preexisting, preexisting, preexisting, random] }
before do
scope.stub(:random_string) { generated.shift }
scope[preexisting] = 1
end
it { should eq random }
end
end
end

View File

@ -7,44 +7,66 @@ describe Rex::Exploitation::JSObfu do
described_class.new("") described_class.new("")
end end
describe '#random_var_name' do # surround the string in quotes
subject(:random_var_name) { jsobfu.random_var_name } def quote(str, q='"'); "#{q}#{str}#{q}" end
it { should be_a String } describe '#transform_string' do
it { should_not be_empty } context 'when given a string of length > MAX_STRING_CHUNK' do
let(:js_string) { quote "ABC"*Rex::Exploitation::JSObfu::MAX_STRING_CHUNK }
it 'is composed of _, $, alphanumeric chars' do it 'calls itself recursively' do
20.times { expect(jsobfu.random_var_name).to match(/\A[a-zA-Z0-9$_]+\Z/) } expect(jsobfu).to receive(:transform_string).at_least(2).times.and_call_original
jsobfu.send(:transform_string, js_string.dup)
end
end end
it 'does not start with a number' do context 'when given a string of length < MAX_STRING_CHUNK' do
20.times { expect(jsobfu.random_var_name).not_to match(/\A[0-9]/) } let(:js_string) { quote "A"*(Rex::Exploitation::JSObfu::MAX_STRING_CHUNK/2).to_i }
it 'does not call itself recursively' do
expect(jsobfu).to receive(:transform_string).once.and_call_original
jsobfu.send(:transform_string, js_string.dup)
end
end end
end
context 'when a reserved word is generated' do describe '#safe_split' do
let(:reserved) { described_class::RESERVED_KEYWORDS.first } let(:js_string) { Rex::Text.to_hex("ABCDEFG"*100, "\\x") }
let(:random) { 'abcdef' } let(:quote) { '"' }
let(:generated) { [reserved, reserved, reserved, random] } let(:parts) { 50.times.map { jsobfu.send(:safe_split, js_string.dup, quote).map{ |a| a[1] } } }
before do describe 'quoting' do
jsobfu.stub(:random_string) { generated.shift } context 'when given a double-quote' do
let(:quote) { '"' }
it 'surrounds all the split strings with the same quote' do
expect(parts.flatten.all? { |part| part.start_with?(quote) }).to be_true
end
end end
it { should be random } context 'when given a single-quote' do
let(:quote) { "'" }
it 'surrounds all the split strings with the same quote' do
expect(parts.flatten.all? { |part| part.start_with?(quote) }).to be_true
end
end
end end
context 'when a non-unique random var is generated' do describe 'splitting' do
let(:preexisting) { 'preexist' } context 'when given a hex-escaped series of bytes' do
let(:random) { 'abcdef' } let(:js_string) { Rex::Text.to_hex("ABCDEFG"*100, "\\x") }
let(:vars) { { 'jQuery' => preexisting } }
let(:generated) { [preexisting, preexisting, preexisting, random] }
before do it 'never splits in the middle of a hex escape' do
jsobfu.stub(:random_string) { generated.shift } expect(parts.flatten.all? { |part| part.start_with?('"\\') }).to be_true
jsobfu.instance_variable_set("@vars", vars) end
end end
it { should be random } context 'when given a unicode-escaped series of bytes' do
let(:js_string) { Rex::Text.to_unescape("ABCDEFG"*100).gsub!('%', '\\') }
it 'never splits in the middle of a unicode escape' do
expect(parts.flatten.all? { |part| part.start_with?('"\\') }).to be_true
end
end
end end
end end