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
// 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 = {};

View File

@ -48,14 +48,71 @@ 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+"_$",
: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
@ -69,12 +126,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 +175,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 +220,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 +247,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 +349,15 @@ protected
str = str[1,str.length - 2]
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
transformed = transform_string_split_concat(str, quote)
transform_string_split_concat(str, quote)
when 1
transformed = transform_string_fromCharCode(str)
#when 2
# # Currently no-op
# transformed = transform_string_unescape(str)
transform_string_fromCharCode(str)
end
#$stderr.puts "Obfuscating str: #{str.ljust 30} #{transformed}"
@ -353,12 +392,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 +448,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 +456,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 +513,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

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("")
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