From 14fed8c61032661f1f8bc7d83532bcee59d26388 Mon Sep 17 00:00:00 2001 From: Joe Vennix Date: Wed, 9 Apr 2014 15:45:48 -0500 Subject: [PATCH 1/5] Fixes large-string expansion in JSObfu. --- lib/rex/exploitation/jsobfu.rb | 135 +++++++++++------- .../lib/rex/exploitation/jsobfu_scope_spec.rb | 50 +++++++ spec/lib/rex/exploitation/jsobfu_spec.rb | 70 +++++---- 3 files changed, 181 insertions(+), 74 deletions(-) create mode 100644 spec/lib/rex/exploitation/jsobfu_scope_spec.rb diff --git a/lib/rex/exploitation/jsobfu.rb b/lib/rex/exploitation/jsobfu.rb index 4b84d16b53..22225558b7 100644 --- a/lib/rex/exploitation/jsobfu.rb +++ b/lib/rex/exploitation/jsobfu.rb @@ -48,14 +48,68 @@ module Exploitation # class JSObfu - # these keywords should never be used as a random var name - # 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 - ) + # A single Javascript scope, used as a key-value store + # to maintain uniqueness of members in generated closures. + # For speed this class is implemented as a subclass of Hash. + class Scope < Hash + + # these keywords should never be used as a random var name + # 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+"_$" + ) + end + + # @return [String] a unique random var name that is not a reserved keyword + def random_var_name + loop do + text = random_string + unless has_key?(text) or + RESERVED_KEYWORDS.include?(text) or + BUILTIN_VARS.include?(text) + + self[text] = nil + return text + end + 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 + def random_string + @rand_gen.generate + end + + end + + # + # The maximum length of a string that will 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 @@ -69,12 +123,8 @@ class JSObfu @code = code @funcs = {} @vars = {} + @scope = Scope.new @debug = false - @rand_gen = Rex::RandomIdentifierGenerator.new( - :max_length => 15, - :first_char_set => Rex::Text::Alpha+"_$", - :char_set => Rex::Text::AlphaNumeric+"_$" - ) end # @@ -122,23 +172,8 @@ class JSObfu obfuscate_r(@ast) 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 - # @return [String] a random string - def random_string - @rand_gen.generate - end - # # Recursive method to obfuscate the given +ast+. # @@ -182,12 +217,12 @@ protected # Variables when ::RKelly::Nodes::VarDeclNode if @vars[node.name].nil? - @vars[node.name] = random_var_name + @vars[node.name] = @scope.random_var_name end node.name = @vars[node.name] when ::RKelly::Nodes::ParameterNode if @vars[node.value].nil? - @vars[node.value] = random_var_name + @vars[node.value] = @scope.random_var_name end node.value = @vars[node.value] when ::RKelly::Nodes::ResolveNode @@ -209,7 +244,7 @@ protected # 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 + @funcs[node.value] = @scope.random_var_name if @vars[node.value].nil? @vars[node.value] = @funcs[node.value] end @@ -311,14 +346,16 @@ protected str = str[1,str.length - 2] return quote*2 if str.length == 0 + if str.length > MAX_STRING_CHUNK + return safe_split(str, quote).map { |args| transform_string(args[1]) }.join('+') + end + + transformed = nil 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}" @@ -353,12 +390,12 @@ protected break unless str[len] break if len > max_len # randomize the length of each part - break if (rand(4) == 0) + break if (rand(max_len) == 0) end part = str.slice!(0, len) - var = Rex::Text.rand_text_alpha(4) + var = @scope.random_var_name parts.push( [ var, "#{quote}#{part}#{quote}" ] ) end @@ -409,12 +446,6 @@ protected final end - - # TODO - #def transform_string_unescape(str) - # str - #end - # # Return a call to String.fromCharCode() with each char of the input as arguments # @@ -423,9 +454,17 @@ protected # output: String.fromCharCode(0x41, 10) # def transform_string_fromCharCode(str) - buf = "String.fromCharCode(" - bytes = str.unpack("C*") + "String.fromCharCode(#{string_to_bytes(str)})" + end + + # + # Return a comma-separated list of byte values with random encodings (decimal/hex/octal) + # + def string_to_bytes(str) len = 0 + bytes = str.unpack("C*") + encoded_bytes = [] + while str.length > 0 if str[0,1] == "\\" str.slice!(0,1) @@ -472,16 +511,12 @@ protected else char = str.slice!(0,1).unpack("C").first end - buf << "#{rand_base(char)}," + encoded_bytes << rand_base(char) end - # Strip off the last comma - buf = buf[0,buf.length-1] + ")" - transformed = buf - transformed + encoded_bytes.join(',') end - end end end diff --git a/spec/lib/rex/exploitation/jsobfu_scope_spec.rb b/spec/lib/rex/exploitation/jsobfu_scope_spec.rb new file mode 100644 index 0000000000..7674bc0514 --- /dev/null +++ b/spec/lib/rex/exploitation/jsobfu_scope_spec.rb @@ -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 diff --git a/spec/lib/rex/exploitation/jsobfu_spec.rb b/spec/lib/rex/exploitation/jsobfu_spec.rb index 25e05cc34e..2d4af27718 100644 --- a/spec/lib/rex/exploitation/jsobfu_spec.rb +++ b/spec/lib/rex/exploitation/jsobfu_spec.rb @@ -7,44 +7,66 @@ describe Rex::Exploitation::JSObfu do described_class.new("") end - describe '#random_var_name' do - subject(:random_var_name) { jsobfu.random_var_name } + # surround the string in quotes + def quote(str, q='"'); "#{q}#{str}#{q}" end - it { should be_a String } - it { should_not be_empty } + describe '#transform_string' do + 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 - 20.times { expect(jsobfu.random_var_name).to match(/\A[a-zA-Z0-9$_]+\Z/) } + it 'calls itself recursively' do + expect(jsobfu).to receive(:transform_string).at_least(2).times.and_call_original + jsobfu.send(:transform_string, js_string.dup) + end end - it 'does not start with a number' do - 20.times { expect(jsobfu.random_var_name).not_to match(/\A[0-9]/) } + context 'when given a string of length < MAX_STRING_CHUNK' do + 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 - context 'when a reserved word is generated' do - let(:reserved) { described_class::RESERVED_KEYWORDS.first } - let(:random) { 'abcdef' } - let(:generated) { [reserved, reserved, reserved, random] } + describe '#safe_split' do + let(:js_string) { Rex::Text.to_hex("ABCDEFG"*100, "\\x") } + let(:quote) { '"' } + let(:parts) { 50.times.map { jsobfu.send(:safe_split, js_string.dup, quote).map{ |a| a[1] } } } - before do - jsobfu.stub(:random_string) { generated.shift } + describe 'quoting' do + 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 - 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 - context 'when a non-unique random var is generated' do - let(:preexisting) { 'preexist' } - let(:random) { 'abcdef' } - let(:vars) { { 'jQuery' => preexisting } } - let(:generated) { [preexisting, preexisting, preexisting, random] } + describe 'splitting' do + context 'when given a hex-escaped series of bytes' do + let(:js_string) { Rex::Text.to_hex("ABCDEFG"*100, "\\x") } - before do - jsobfu.stub(:random_string) { generated.shift } - jsobfu.instance_variable_set("@vars", vars) + it 'never splits in the middle of a hex escape' do + expect(parts.flatten.all? { |part| part.start_with?('"\\') }).to be_true + 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 From 49139cc07fd55c302a49a0d1c202f294bda7eeb2 Mon Sep 17 00:00:00 2001 From: Joe Vennix Date: Wed, 9 Apr 2014 15:48:07 -0500 Subject: [PATCH 2/5] Use implicit return for assignment. --- lib/rex/exploitation/jsobfu.rb | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/rex/exploitation/jsobfu.rb b/lib/rex/exploitation/jsobfu.rb index 22225558b7..79086243aa 100644 --- a/lib/rex/exploitation/jsobfu.rb +++ b/lib/rex/exploitation/jsobfu.rb @@ -350,12 +350,11 @@ protected return safe_split(str, quote).map { |args| transform_string(args[1]) }.join('+') end - transformed = nil - case rand(2) + transformed = case rand(2) when 0 - transformed = transform_string_split_concat(str, quote) + transform_string_split_concat(str, quote) when 1 - transformed = transform_string_fromCharCode(str) + transform_string_fromCharCode(str) end #$stderr.puts "Obfuscating str: #{str.ljust 30} #{transformed}" From 52432ef482dbdb2e7fb1d52d9da7307b0b329d14 Mon Sep 17 00:00:00 2001 From: Joe Vennix Date: Wed, 9 Apr 2014 16:54:02 -0500 Subject: [PATCH 3/5] Use tiny var names by default. --- lib/rex/exploitation/jsobfu.rb | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/lib/rex/exploitation/jsobfu.rb b/lib/rex/exploitation/jsobfu.rb index 79086243aa..1e9743f6ad 100644 --- a/lib/rex/exploitation/jsobfu.rb +++ b/lib/rex/exploitation/jsobfu.rb @@ -74,14 +74,16 @@ class JSObfu @rand_gen = Rex::RandomIdentifierGenerator.new( :max_length => 15, :first_char_set => Rex::Text::Alpha+"_$", - :char_set => Rex::Text::AlphaNumeric+"_$" + :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 + text = @rand_gen.generate(len) unless has_key?(text) or RESERVED_KEYWORDS.include?(text) or BUILTIN_VARS.include?(text) @@ -89,6 +91,7 @@ class JSObfu self[text] = nil return text end + len += 1 end end @@ -97,15 +100,10 @@ class JSObfu super or (@parent and @parent.has_key?(key)) end - # @return [String] a random string - def random_string - @rand_gen.generate - end - end # - # The maximum length of a string that will be passed through + # The maximum length of a string that can be passed through # #transform_string without being chopped up into separate # expressions and concatenated # From b9284c56356cbdb62344fc1f75ce8da88b7d0925 Mon Sep 17 00:00:00 2001 From: Joe Vennix Date: Wed, 9 Apr 2014 16:56:10 -0500 Subject: [PATCH 4/5] Use actual vars so that jsobfu can randomize. --- data/js/detect/os.js | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/data/js/detect/os.js b/data/js/detect/os.js index 803d8d79ef..d37aaf2151 100644 --- a/data/js/detect/os.js +++ b/data/js/detect/os.js @@ -1,25 +1,25 @@ // Case matters, see lib/msf/core/constants.rb // All of these should match up with constants in ::Msf::HttpClients -clients_opera = "Opera"; -clients_ie = "MSIE"; -clients_ff = "Firefox"; -clients_chrome= "Chrome"; -clients_safari= "Safari"; +var clients_opera = "Opera"; +var clients_ie = "MSIE"; +var clients_ff = "Firefox"; +var clients_chrome= "Chrome"; +var clients_safari= "Safari"; // All of these should match up with constants in ::Msf::OperatingSystems -oses_linux = "Linux"; -oses_windows = "Microsoft Windows"; -oses_mac_osx = "Mac OS X"; -oses_freebsd = "FreeBSD"; -oses_netbsd = "NetBSD"; -oses_openbsd = "OpenBSD"; +var oses_linux = "Linux"; +var oses_windows = "Microsoft Windows"; +var oses_mac_osx = "Mac OS X"; +var oses_freebsd = "FreeBSD"; +var oses_netbsd = "NetBSD"; +var oses_openbsd = "OpenBSD"; // All of these should match up with the ARCH_* constants -arch_armle = "armle"; -arch_x86 = "x86"; -arch_x86_64 = "x86_64"; -arch_ppc = "ppc"; +var arch_armle = "armle"; +var arch_x86 = "x86"; +var arch_x86_64 = "x86_64"; +var arch_ppc = "ppc"; window.os_detect = {}; From bd8918e4e1d7b8cbdb3b923b7e513621e2931910 Mon Sep 17 00:00:00 2001 From: Joe Vennix Date: Wed, 9 Apr 2014 17:44:42 -0500 Subject: [PATCH 5/5] Re-add the #random_string(len) method to pass specs. --- lib/rex/exploitation/jsobfu.rb | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/rex/exploitation/jsobfu.rb b/lib/rex/exploitation/jsobfu.rb index 1e9743f6ad..2c8d00fcd2 100644 --- a/lib/rex/exploitation/jsobfu.rb +++ b/lib/rex/exploitation/jsobfu.rb @@ -83,7 +83,7 @@ class JSObfu def random_var_name len = 1 loop do - text = @rand_gen.generate(len) + text = random_string(len) unless has_key?(text) or RESERVED_KEYWORDS.include?(text) or BUILTIN_VARS.include?(text) @@ -100,6 +100,11 @@ class JSObfu 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 #