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,6 +48,11 @@ module Exploitation
#
class JSObfu
# 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(
@ -57,6 +62,58 @@ class JSObfu
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,15 +513,11 @@ protected
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
encoded_bytes << rand_base(char)
end
encoded_bytes.join(',')
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