Land #3224 - Fixes large-string expansion in JSObfu
commit
68a50e3663
|
@ -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 = {};
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
||||
|
|
Loading…
Reference in New Issue